Skip to content
Merged
5 changes: 4 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
# Location of package manifests: the project's own at the root, and the
# hash-locked tool sets the workflows install (ci.txt, publish.txt).
# Dependabot does not look that deep from the root, so the directory is named.
directories: ["/", "/.github/requirements"]
# Updates belong on dev: main is the released branch and merging into it
# publishes, so a PR opened against main just sits there.
target-branch: "dev"
Expand Down
9 changes: 9 additions & 0 deletions .github/requirements/publish.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# What the publish job installs; publish.txt is generated from this file (the command is in publish.yml).
# The job holds the PyPI token: build makes the distributions and twine uploads them. Its version bump
# uses only the standard library.
build
twine
# The build backend. The job runs `python -m build --no-isolation`, so the backend is this locked one
# and not whatever is newest on PyPI when the job runs. It must satisfy `build-system.requires` in
# pyproject.toml.
setuptools
472 changes: 472 additions & 0 deletions .github/requirements/publish.txt

Large diffs are not rendered by default.

15 changes: 11 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ jobs:
with:
python-version: "3.12"

- name: Install build tools
# This job holds the PyPI token, so its tools and its build backend are wheels only at locked hashes
# and nothing else is installed. Regenerate publish.txt with
# uv pip compile .github/requirements/publish.in --generate-hashes --python-version 3.12 \
# --python-platform x86_64-manylinux_2_28 --only-binary :all: --exclude-newer <7 days ago> \
# -o .github/requirements/publish.txt
# --python-version is the one set up above. --exclude-newer keeps the 7-day wait Dependabot uses:
# nothing uploaded in the last week.
- name: Install build tools (hash-locked, from .github/requirements/publish.in)
run: |
python -m pip install --upgrade pip
pip install build twine
python -m pip install --require-hashes --only-binary :all: -r .github/requirements/publish.txt

- name: Bump patch version in pyproject.toml
id: bump
Expand Down Expand Up @@ -60,8 +66,9 @@ jobs:
print(f"Bumped version -> {new_version}")
EOF

# --no-isolation: the backend is the setuptools locked in publish.txt, not a fresh download.
- name: Build distribution
run: python -m build
run: python -m build --no-isolation

- name: Publish to PyPI
env:
Expand Down
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# The source distribution carries no tests. setuptools adds test*/test*.py to an sdist by default;
# this line takes them out again (test/test_sdist_manifest.py).
prune test
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,31 @@ executor.execute_action({"my_tool": [["MY_add", [1, 2]], ["MY_add", {"a": 3, "b"
| `file_listing` | `get_dir_files_as_list`: action files under a directory | — |
| `socket_server`, `socket_auth` | `start_action_socket_server` and the request handlers (plain, secret header, JSON-envelope token) | executor, payload check, errors answered, framing, TLS, reply templates, secret |
| `builtins_policy` | `SAFE_BUILTINS`: the builtins an action list may call | — |
| `request_record`, `request_context` | Versioned request-result validation, JSON serialization/schema and isolated `RunContext` storage | source, phase, engine, worker and run identities |

`__all__` in `je_action_core/__init__.py` is the supported import surface.

## Request results

HTTP `request_method`: uppercase ASCII token; schema and Python validation enforce the same rule.

Run identities are validated when the context is constructed. Retries compare JSON content with object keys sorted, distinguishing booleans from numbers. Assertion messages must be strings when present, and failed assertions require a message. Integer values beyond the portable JSON conversion limit and unrepresentable measurement values raise field-located contract errors.

Request results are separate from executor action records. `RequestRecord` v1 gives functional,
load and synthetic runs the same method/URL, numeric status, nullable measurements, structured error,
assertions and run/worker/step identities. `validate_request_record` returns a detached JSON-safe result;
`serialize_request_record` exports it and `request_record_schema` describes the contract.
Bytes become base64 objects, datetimes become epoch seconds and timedeltas become milliseconds.
Unknown versions, invalid fields, non-finite measurements and opaque objects raise `RequestRecordError`.

`RunContext(source="loaddensity", phase="load", engine="asyncio")` owns one run's records.
Use `capture(fields)` for a new result, `append(record)` for a validated retry, `snapshot()` for a detached
copy and `to_json()` for an array export. Identical record IDs deduplicate; conflicting retries and foreign
run identities raise an error. `use_run_context(context)` activates it for a thread/task scope and restores
the previous context on exit; `get_run_context()` returns the active context or `None`.
Storage retains the complete run; long-lived monitors should create a new context per iteration and persist results.
The contract records the runner's judgement and does not decide whether a status code is successful.

## Package gate

`PackageManager.add_package_to_executor` imports a package and registers its members as commands. An action list
Expand Down Expand Up @@ -121,6 +143,10 @@ the projects this package is for. `architecture.md` §6 lists which of them have
settings each one uses. All four run their TCP servers on `socket_server`: LoadDensity with the JSON-envelope
token, framing and TLS, FileAutomation with the `AUTH` header and its ACL.

[WebRunner](https://github.com/Integration-Automation/WebRunner) (`WR_`) runs its action executor on it as well. Its
own action format (`[name, [args], {kwargs}]`) is a parser, and its retries and action span wrap `attempt`.
Failure screenshots go into `failure_record`, and repeated actions are numbered (`DuplicateKeys.NUMBER`).

n and an ACL.

## Development
Expand Down
26 changes: 26 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,31 @@ executor.execute_action({"my_tool": [["MY_add", [1, 2]], ["MY_add", {"a": 3, "b"
| `file_listing` | `get_dir_files_as_list`:找出目录下的 action 文件 | — |
| `socket_server`、`socket_auth` | `start_action_socket_server` 与请求处理器(无认证、密钥标头、JSON 信封令牌) | 执行器、内容检查、要回复的错误、分帧、TLS、回复模板、密钥 |
| `builtins_policy` | `SAFE_BUILTINS`:action 列表可以调用的内置函数 | — |
| `request_record`、`request_context` | 版本化 request 结果验证、JSON 序列化/schema 与隔离的 `RunContext` 存储 | 来源、阶段、引擎、worker 与 run 标识 |

`je_action_core/__init__.py` 里的 `__all__` 是正式支持的导入接口。

## Request 结果

HTTP `request_method`: uppercase ASCII token; schema and Python validation enforce the same rule.

Run identity 在 context 创建时即验证。重送以排序对象键后的 JSON 内容比较,区分布尔值与数字。
断言信息存在时必须是字符串,失败断言必须带信息。超过可移植 JSON 转换限制的整数及无法表示的测量值
会产生包含字段位置的契约错误。

Request 结果与 executor 的 action 记录分开。`RequestRecord` v1 让功能、负载与合成监控使用
相同的方法/URL、数字状态码、可为 null 的测量值、结构化错误、assertion 与 run/worker/step 标识。
`validate_request_record` 返回隔离且可序列化的结果;`serialize_request_record` 输出 JSON,
`request_record_schema` 描述契约。bytes 转为 base64 对象、datetime 转为 epoch 秒、timedelta 转为毫秒。
未知版本、非法字段、非有限测量值与不支持的对象会引发 `RequestRecordError`。

`RunContext(source="loaddensity", phase="load", engine="asyncio")` 保存一次运行的记录。
`capture(fields)` 添加结果,`append(record)` 接收已验证的重发,`snapshot()` 获取隔离副本,
`to_json()` 输出数组。同 ID 的相同结果不重复计数;冲突结果与其他 run 的标识会引发错误。
`use_run_context(context)` 在线程/task 范围启用它,退出时恢复原 context;
`get_run_context()` 返回当前 context 或 `None`。存储保留整次运行,长期监控应每轮创建新 context 并持久化结果。
契约保存 runner 的判断,不自行决定状态码是否成功。

## 包闸门

`PackageManager.add_package_to_executor` 会导入一个包,并把它的成员注册成命令。所以只要 action 列表写得出
Expand Down Expand Up @@ -113,6 +135,10 @@ package_manager.set_allow_arbitrary_packages(False) # 其他包在导入前就
FileAutomation(`FA_`)而做的。`architecture.md` §6 列出其中哪些已经改用它,以及各自用了哪些部件与配置。
四个项目的 TCP 服务器都运行在 `socket_server` 上:LoadDensity 用 JSON 信封令牌、分帧与 TLS,FileAutomation 用 `AUTH` 标头与它的访问控制列表。

[WebRunner](https://github.com/Integration-Automation/WebRunner)(`WR_`)的 action 执行器也运行在它上面。它自己的 action 格式
(`[name, [args], {kwargs}]`)是一个解析器,重试与 action span 包在 `attempt` 外面。失败截图放进 `failure_record`,
重复的 action 会编号(`DuplicateKeys.NUMBER`)。

## 开发

```bash
Expand Down
26 changes: 26 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,31 @@ executor.execute_action({"my_tool": [["MY_add", [1, 2]], ["MY_add", {"a": 3, "b"
| `file_listing` | `get_dir_files_as_list`:找出目錄底下的 action 檔 | — |
| `socket_server`、`socket_auth` | `start_action_socket_server` 與請求處理器(無驗證、祕密標頭、JSON 信封權杖) | 執行器、內容檢查、要回覆的錯誤、分框、TLS、回覆範本、祕密 |
| `builtins_policy` | `SAFE_BUILTINS`:action 清單可以呼叫的內建函式 | — |
| `request_record`、`request_context` | 版本化 request 結果驗證、JSON 序列化/schema 與隔離的 `RunContext` 儲存 | 來源、階段、引擎、worker 與 run 識別 |

`je_action_core/__init__.py` 裡的 `__all__` 是正式支援的匯入介面。

## Request 結果

HTTP `request_method`: uppercase ASCII token; schema and Python validation enforce the same rule.

Run identity 在 context 建立時即驗證。重送以排序物件鍵後的 JSON 內容比較,區分布林值與數字。
斷言訊息存在時必須是字串,失敗斷言必須帶訊息。超過可攜式 JSON 轉換限制的整數及無法表示的測量值
會產生包含欄位位置的契約錯誤。

Request 結果與 executor 的 action 紀錄分開。`RequestRecord` v1 讓功能、負載與合成監控使用
相同的方法/URL、數字狀態碼、可為 null 的量測值、結構化錯誤、assertion 與 run/worker/step 識別。
`validate_request_record` 回傳隔離且可序列化的結果;`serialize_request_record` 輸出 JSON,
`request_record_schema` 描述契約。bytes 轉成 base64 物件、datetime 轉成 epoch 秒、timedelta 轉成毫秒。
未知版本、非法欄位、非有限量測值與不支援的物件會引發 `RequestRecordError`。

`RunContext(source="loaddensity", phase="load", engine="asyncio")` 保存一次執行的紀錄。
`capture(fields)` 新增結果,`append(record)` 接收已驗證的重送,`snapshot()` 取得隔離副本,
`to_json()` 輸出陣列。同 ID 的相同結果不重複計數;衝突結果與其他 run 的識別會引發錯誤。
`use_run_context(context)` 在執行緒/task 範圍啟用它,離開時還原原 context;
`get_run_context()` 回傳目前 context 或 `None`。儲存保留整次執行,長期監控應每輪建立新 context 並持久化結果。
契約保存 runner 的判定,不自行決定狀態碼是否成功。

## 套件閘門

`PackageManager.add_package_to_executor` 會匯入一個套件,並把它的成員註冊成命令。所以只要 action 清單寫得出
Expand Down Expand Up @@ -113,6 +135,10 @@ package_manager.set_allow_arbitrary_packages(False) # 其他套件在匯入前
FileAutomation(`FA_`)而做的。`architecture.md` §6 列出其中哪些已經改用它,以及各自用了哪些元件與設定。
四個專案的 TCP 伺服器都跑在 `socket_server` 上:LoadDensity 用 JSON 信封權杖、分框與 TLS,FileAutomation 用 `AUTH` 標頭與它的存取控制清單。

[WebRunner](https://github.com/Integration-Automation/WebRunner)(`WR_`)的 action 執行器也跑在它上面。它自己的 action 格式
(`[name, [args], {kwargs}]`)是一個解析器,重試與 action span 包在 `attempt` 外面。失敗截圖放進 `failure_record`,
重複的 action 會編號(`DuplicateKeys.NUMBER`)。

## 開發

```bash
Expand Down
23 changes: 18 additions & 5 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ settings: command prefix, document key, exceptions, messages and reporting. The
| `je_action_core/socket_server.py` | `ActionTCPServer` (`close_flag`, `close_event`, `request_stop`), the template `ActionRequestHandler`, `SocketServerSettings` (framing, TLS, `ReplyMessages`, secret), `server_tls_context`, `start_action_socket_server` |
| `je_action_core/socket_auth.py` | `SecretHeaderRequestHandler` (`<prefix><secret>` first line) and `EnvelopeTokenRequestHandler` (JSON envelope token) |
| `je_action_core/builtins_policy.py` | `SAFE_BUILTINS`, `safe_builtin_commands()` |
| `test/` | One test module per piece, plus the workflow pinning checks |
| `test/` | One test module per piece, plus the workflow checks (pinned actions, Dependabot, the publish job's hash-locked tools and build backend) and the sdist manifest check (`MANIFEST.in` keeps `test/` out of the sdist) |
| `.github/requirements/` | The hash-locked tool sets the workflows install, each generated from the `.in` beside it: `ci.txt` (the test job) and `publish.txt` (`build`, `twine` and the build backend `setuptools`, the only install of the job that holds the PyPI token; it runs `python -m build --no-isolation`, so the build uses that locked backend instead of downloading the newest) |

Nothing in the package imports a project; the projects import it.

Expand Down Expand Up @@ -85,24 +86,36 @@ Nothing in the package imports a project; the projects import it.

## 6. Cross-project boundaries

RunContext rejects invalid identities during construction and compares sorted JSON for retries; booleans and numbers are distinct. Assertion schema constraints and uppercase ASCII HTTP method tokens match the Python validator. Measurements must be representable as finite numbers; integers must fit the portable JSON conversion limit.

`request_record.py` owns the separate RequestRecord v1 contract (TypedDict, validator, JSON serializer
and JSON Schema); `request_context.py` owns explicit `RunContext` storage and ContextVar scopes.
They use only the standard library and do not alter executor action records. Framework adapters remain
in the consumers. Contexts retain complete runs; consumers must rotate/persist long-lived monitoring runs.
New consumers are developed against isolated checkouts until the core release exposes these APIs;
their existing package imports must remain usable with the previously published dependency floor.

**Used by.** Each project that moves adds a row here; the same round updates that project's own
`architecture.md` §6. The projects install it from PyPI (`je_action_core>=0.0.1`); a change they need is released
first and their minimum version raised in the same round.
`architecture.md` §6. The projects install it from PyPI (`je_action_core>=0.0.1` to `>=0.0.3`, each the release that
has what it uses); a change they need is released first and their minimum version raised in the same round.

| Project | Pieces | Settings |
|---|---|---|
| APITestka (`AT_`, `api_testka`) | executor, registry, package manager, callback executor, JSON files, file listing, socket server (9939) | `LegacyActionParser`, plain record keys, `LoggingReporter`, `strip_runner_metadata` as `prepare`; functions-only registry; gate on, prefixed members, every load error logged; callback returns `None` on failure; socket reads the prefix and answers every error |
| MailThunder (`MT_`, `mail_thunder`, legacy `auto_control`) | executor, registry, package manager, JSON files, file listing, socket server (9942) | `LegacyActionParser`, `EmptyListPolicy.RETURN_EMPTY`, plain record keys, `LoggingReporter` with its empty message, `SAFE_BUILTINS`; functions-only registry; gate on, identifier-path names, import and attribute errors logged; `from_document` in its payload check; socket rejects oversized payloads and answers `ValueError`, `OSError`, `TypeError` |
| LoadDensity (`LD_`, `load_density`) | executor, registry, package manager, callback executor, JSON files, socket server (9940) | `LegacyActionParser`, plain record keys, `PrintReporter`, `SAFE_BUILTINS`; functions-only registry; gate on, bare member names, functions only, ASCII names, errors printed; callback raises after printing; JSON wraps every error; `EnvelopeTokenRequestHandler`, raw or `LENGTH_PREFIX` framing, optional TLS, `Error: <text>` replies, size-only log line, run under gevent. Its file listing stays in LoadDensity |
| WebRunner (`WR_`, `webdriver_wrapper`) | executor, registry | its own `[name, [args], {kwargs}]` parser and list rules (`action_list_of`: an empty list runs nothing, its error logged); functions-only registry; `PrintReporter` with `on_start` / `on_failure` logged; `failure_record` = the error text plus failure screenshot and trace; `DuplicateKeys.NUMBER`; refused commands and the script gate in `_execute_event`, retries and the action span in `attempt`; `collect_action_results` for MCP and the async executor. Its package manager, callback executor, JSON files and socket server stay in WebRunner (`progress.md` #4) |
| FileAutomation (`FA_`, `auto_control`) | registry, executor pipeline, package loader, callback executor, JSON files, TCP server (9943) | any-callable registry (`"<name> is not callable"`); `StrictActionParser`, `indexed_record_key`, its three list messages; tracing through `invoke`; `check_and_add` for the member count, gate off, `ImportError` logged; strict callback, errors raised; JSON wraps `JSONDecodeError` / `OSError` on read and `OSError` / `TypeError` on write; `SecretHeaderRequestHandler` (`AUTH <secret>`), the ACL as `validate`, `<key> -> <value>` records and prefixed error replies. Dry run, validate, substitute, parallel runs, metrics and its HTTP server stay in FileAutomation |

**What the projects rely on here.** They rely on these, and none may change without changing the projects in
the same round:

- the names in `__all__`, and the settings fields they pass;
- `event_dict` staying the live mapping of a registry, executor and callback executor;
- the record formats `"execute: <action>"` and `"execute[<index>]: <action>"`, and `repr(error)` as a failed
action's record;
- the record formats `"execute: <action>"` and `"execute[<index>]: <action>"`, `repr(error)` as a failed
action's default record, and `" #2"`, `" #3"` … for numbered repeats;
- `attempt` and `_execute_event` as the override points WebRunner uses, and the order `on_start`, then
`rules.extract`, then per action `attempt` with `on_failure` before `failure_record`;
- the error texts of `LegacyActionParser` (`f"{message} {action}"`) and of `StrictActionParser` (FileAutomation's
messages);
- the socket protocol: `Return_Data_Over_JE`, `quit_server`, one line per value.
Expand Down
Loading
Loading