The short architecture overview. README.md is the user guide; finished work is in docs/updates/.
je_action_core is the keyword-driven action executor that APITestka, LoadDensity, MailThunder and FileAutomation
share (workspace item L-6). It holds one implementation of what those four used to copy: the executor, the
command registry, the package manager with its package gate, the callback executor, action-file reading and
writing, action-file discovery and the plain TCP action server. A project supplies everything that differs as
settings: command prefix, document key, exceptions, messages and reporting. The package has no dependencies.
| Path | What it holds |
|---|---|
je_action_core/exceptions.py |
Default exceptions (ActionCoreException and one subclass per piece), used when a project passes none |
je_action_core/registry.py |
CommandRegistry (name -> callable, event_dict is the live dict), CommandPolicy for caller-added commands |
je_action_core/action_list.py |
ActionListRules (document key, legacy keys, empty-list policy, messages); LegacyActionParser and StrictActionParser bind one action to its command |
je_action_core/executor.py |
ActionExecutor and ExecutorSettings; record-key functions and unique_record_key, DuplicateKeys, repr_failure, the ActionListSource protocol |
je_action_core/reporting.py |
ExecutionReporter hooks; LoggingReporter and PrintReporter |
je_action_core/package_manager.py |
PackageManager (members as commands) behind the package gate; PackageManagerSettings |
je_action_core/callback.py |
CallbackFunctionExecutor and CallbackSettings (legacy or strict checks, raise or return None) |
je_action_core/json_io.py |
ActionJsonFile (UTF-8, locked, project exception and message templates) and default read_action_json / write_action_json |
je_action_core/file_listing.py |
get_dir_files_as_list |
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 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.
je_action_core.__all__is the supported import surface. Every settings class is a frozen dataclass, so a project builds its settings once at import time.- A project's executor subclasses
ActionExecutoror instantiates it with itsExecutorSettingsand aCommandRegistry, then fillsevent_dictwith its own commands. Its existing module-level functions (execute_action,execute_files,add_command_to_executor) delegate to that instance.
-
ActionExecutor.execute_action(action_list):- The reporter's
on_startsees the list as given. Thenrules.extract(ActionListRules, or anything withextract) returns the list, raises, or returnsNone(on_empty; run nothing, return{}). - Each action passes
attempt, which calls_execute_event: reporteron_event, thenprepare, thenparser.bind, theninvoke. - Its record is the result, or
failure_record(action, error)(repr(error)by default);on_successoron_failureis called. - The record is stored under
record_key(index, action), numbered#2,#3… whenduplicate_keysisNUMBERand the key is taken.on_recordssees the whole record.
collect_action_resultsruns the same steps withouton_recordsand also returns the keys that failed.run_oneis steps 2 and 3 for one action. - The reporter's
-
PackageManager.add_package_to_executor(package):_check_allowedapplies the gate (refuse, allow, or warn).check_packagevalidates the name, thenfind_spec,import_module, and caches the module.get_memberruns once per predicate and writesevent_dict[name] = member, naming each member<package>_<member>or bare.
-
CallbackFunctionExecutor.callback_function: looks the trigger up, then (strict style only) checks the method, calls the trigger withkwargs, then calls the callback with no arguments,**paramor*param. Errors are logged and then raised or turned intoNone. -
Socket server (
ActionRequestHandler.handle, a template):securewraps the connection in TLS when configured.read_requestreads one 8 KiBrecvor one length-prefixed frame.decodeturns the bytes into text.processlogs the request, then either handlesquit_server(on_quit) or runsrun_text:json.loads, thenrun_document(validate,execute, one line per record, the end marker).- A failure is answered by
reply_failure(stage)with that stage's template.
The dialects override
process: the secret header is checked first, or the JSON envelope is unwrapped.
- Reporting: subclass
ExecutionReporter, or pass log hooks (log_info,log_error) to the settings of the package manager, callback executor, JSON file and socket server. - Wrapping every action (retries, a span around the whole action): override
ActionExecutor.attempt. Wrapping every command call (tracing, metrics): overrideActionExecutor.invoke. - Action rules:
ExecutorSettings.parsertakes any object withbind(action, resolve);preparerewrites an action before it is bound. - Socket dialect: subclass
ActionRequestHandlerand override the step that differs (read_request,decode,process,on_quit,write); pass it ashandler_classtostart_action_socket_server. - New piece: add a module, export it in
__init__.pyand__all__, addtest/test_<piece>.py, and add a row to §2 and to the README's table (three languages).
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 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_dictstaying the live mapping of a registry, executor and callback executor;- the record formats
"execute: <action>"and"execute[<index>]: <action>",repr(error)as a failed action's default record, and" #2"," #3"… for numbered repeats; attemptand_execute_eventas the override points WebRunner uses, and the orderon_start, thenrules.extract, then per actionattemptwithon_failurebeforefailure_record;- the error texts of
LegacyActionParser(f"{message} {action}") and ofStrictActionParser(FileAutomation's messages); - the socket protocol:
Return_Data_Over_JE,quit_server, one line per value.
What this package relies on. Nothing outside the standard library.
- No runtime dependencies. Python 3.10 and later. Every settings object is a frozen dataclass; mutable state lives on instances (registries, caches, the gate).
- Behaviour that differs between projects is a setting, never a project-name check. A refactor in a project that moves onto this package must not change that project's behaviour; where a project behaved differently for no reason, that change is a separate commit in the project.
- The package gate exists so an action list can never load
osorsubprocesson its own: the gate switches are never action commands. - Functions ≤ 50 lines, files ≤ 500 lines, lines ≤ 120 characters; every public function and class has type hints
and a docstring; every change ships with tests (
CLAUDE.md).
- A module is added, removed or renamed (§2), or the import surface changes (§3).
- A flow in §4 changes.
- A project moves onto the package, changes the pieces or settings it uses, or a contract in §6 changes.