> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockx.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Executor

> How the resident Python process loads and runs user functions, suspends IO with greenlet, round-trips with the Worker over UDS, and which SDK entry points user functions can use and where they are intercepted.

Python Executor is a resident Python process (`python -m blockx_executor`) in the Function Executor Pool local to the Worker. It receives `ExecuteCall` from the Worker, loads and runs the user function, translates the BlockDB / RPC / subfunction calls made by the user function into `CallWaiting` messages sent back to the Worker, and restores the original execution stack when `ResumeCall` arrives. See [Architecture overview](/en/architecture/overview) for where it sits in the system, and [Call execution subsystem](/en/components/call-execution) for its Worker-side counterpart (dispatcher, executor adapter, subfunctions).

## Responsibilities and boundaries

Responsible for:

* Receiving `ExecuteCall` / `ResumeCall` / `CancelCall` / `HeartbeatAck` and emitting `CallWaiting` / `CallCompleted` / `CallFailed` / `Heartbeat` over a single long-lived UDS connection.
* Compiling and caching function modules by source digest, isolating module namespaces per task, and fetching source from the Worker by digest when it is missing.
* Using `greenlet` to suspend synchronous-style user code at SDK call points and resume it after `ResumeCall`.
* Running cancellation and deadline / budget checks at safe points, and preempting pure-CPU loops with a `SIGALRM` timer.
* Reporting process metrics (RSS, CPU utilization, cumulative CPU seconds, call counts per state) every heartbeat interval.

Not responsible for:

* Deciding the call dispatch order or which Executor to pick (that is the Worker dispatcher's job).
* Accessing BlockDB / node RPC / Function Code View directly. All SDK IO is intercepted and goes out through the Worker.
* Persisting or recovering any call across processes. After a process restart, all context is rebuilt.

Core invariants:

* User code runs only in greenlets on the main thread, one at a time; the UDS reader / writer / heartbeat threads are separate and never switch greenlets.
* An `ExecContext` has at most one pending `requestId` at any time; a `ResumeCall` must match `callId`, the `resumeKind` corresponding to the state, and `requestId` all at once, otherwise it is dropped as stale.
* A second `ExecuteCall` for the same `callId` is treated as a new attempt: the old greenlet is marked stale and terminated without emitting a terminal state; the new attempt gets a new greenlet.
* Every discarded context emits exactly one terminal message (`CallCompleted` or `CallFailed`), except on attempt replacement.
* Compiled artifacts are shared across tasks by digest; exec'd module namespaces are isolated per `task_id` and released when the task's last call finishes.

## Code location

| Path                                                                       | Purpose                                                                                                                                                                                                                                                                             |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `python/pyproject.toml`                                                    | The `uv`-managed project `blockx-python` (Python ≥ 3.12). External SDKs are pulled in by git tag: `blockx-py@v0.1.64`, `blockdb-py@v0.1.26`, `leafage-py@v0.2.5`, `chaintable-py@v0.1.4`; runtime dependencies are `greenlet`, `msgspec`, `orjson`, `opentelemetry-*`, `pyRFC3339`. |
| `python/blockx_executor/`                                                  | The Executor process itself (entry point `__main__.py`). See the collapsible table below for the file list.                                                                                                                                                                         |
| `python/blockx_sdk/`                                                       | A very thin provider seam layer (about 150 lines): `db` / `rpc` / `call_subfunc` / `sleep` and `get_provider()`.                                                                                                                                                                    |
| `python/blockx_audit/`                                                     | Static auditor for function code (`python -m blockx_audit` starts a stdin/stdout frame-protocol daemon), invoked by the Worker before loading; see [Function Code View](/en/components/function-code) for details.                                                                  |
| `python/tests/`                                                            | `unittest` unit tests, split into files by implementation boundary.                                                                                                                                                                                                                 |
| `internal/worker/adapters/executor/spawner.go`                             | Where the Go side spawns the process and decides the command-line arguments passed to `blockx_executor`.                                                                                                                                                                            |
| `api/uds/types.go`                                                         | Go definitions of the UDS message types and payload structures (the Python-side field-name constants live in `blockx_executor/wire_keys.py`).                                                                                                                                       |
| `cmd/worker/worker_process_*_test.go`, `cmd/syncinvoker/*_test.go`, `e2e/` | E2E tests that spawn a real Python process.                                                                                                                                                                                                                                         |
| `docs/specs/python-function-executor.md` and others                        | Design specs; see the end of this page.                                                                                                                                                                                                                                             |

<AccordionGroup>
  <Accordion title="blockx_executor module file list">
    | File                                   | One-liner                                                                                                                                                                                            |
    | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `__main__.py`                          | Parses the command line, installs logging / tracing / stdout capture, creates the `UDSSession`, runs the main loop `run_until_idle()` + `wait_for_message()`; turns `SIGTERM` into a clean exit.     |
    | `runtime.py`                           | `ExecutorRuntime`: context table, runnable / sleep queues, inbound message dispatch, greenlet scheduling, cancellation and deadlines, heartbeat thread, attempt replacement.                         |
    | `context.py`                           | `ExecState`, `ExecContext` (including the budget accounting fields), `ResumePayload`, `DebugCapture`, plus greenlet-local access to the current context.                                             |
    | `sdk_bridge.py`                        | `SdkBridge`: `do_read_req` / `do_write_req` / `do_capability_req` / `do_subcall` / `do_function_code_req` / `do_sleep`, centered on `_perform_wait`; error mapping and the `RemoteCallError` family. |
    | `sdk_bridge_provider.py`               | `SdkBridgeProvider`: implements `blockx_sdk.Provider` and forwards provider calls to `SdkBridge`.                                                                                                    |
    | `blockdb_bridge.py`                    | `BridgeChannel`: a pseudo gRPC channel that turns `blockdb-py` / `blockx-py` unary calls into `read_req` / `write_req`, with protobuf carried in a binary sidecar.                                   |
    | `noderpc_bridge.py`                    | `NodeRPCBridgeProvider`: replaces `leafage._endpoints.W3_DICT` and turns EVM JSON-RPC requests into `read_req`.                                                                                      |
    | `module_registry.py`                   | `ModuleRegistry`: per-digest compile LRU, per-task exec namespaces, audit gate validation, entry-name resolution.                                                                                    |
    | `restricted_runtime.py`                | Restricted namespace for audited code: `SAFE_BUILTINS`, facade importer, `blockx.function.call` rebound to the subfunction bridge.                                                                   |
    | `uds_session.py`                       | `UDSSession`: one UDS connection + reader / writer threads + a `threading.Event` that wakes the main loop.                                                                                           |
    | `uds_codec.py`                         | Frame encoding / decoding: `uint32` length prefix + JSON, or `BXB1`-tagged JSON + binary sidecar; `orjson` fast path with `msgspec` / stdlib fallback.                                               |
    | `messages.py`                          | `MessageEnvelope` and the `call_waiting` / `call_completed` / `call_failed` / `heartbeat` constructors.                                                                                              |
    | `wire_keys.py`                         | Protocol field-name constants (`F_*`, `FP_*`, `MT_*`).                                                                                                                                               |
    | `process_metrics.py`                   | `ProcessMetricsSampler`: RSS sampling from `/proc/self/statm` and CPU sampling from `time.process_time()`, startup probing, degradation.                                                             |
    | `sleep_patch.py`                       | Process-level `time.sleep` wrapper: becomes a cooperative sleep when an `ExecContext` is active, otherwise blocks as usual.                                                                          |
    | `debug_capture.py`                     | Tees `sys.stdout` / `sys.stderr` into the current call's `DebugCapture` (debug calls only).                                                                                                          |
    | `logging_setup.py`, `tracing_setup.py` | Structured JSON logging (call fields attached automatically) and OTel spans / trace propagation across UDS.                                                                                          |
    | `metrics.py`                           | Truncation helpers for error stacks and input previews.                                                                                                                                              |
    | `third_party_caches.py`                | The heartbeat thread also trims `eth_abi`'s unbounded `lru_cache`.                                                                                                                                   |
  </Accordion>
</AccordionGroup>

## Core types and interfaces

| Type / function                                   | File                                     | One-liner                                                                                                                                                                                       |
| ------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ExecutorRuntime`                                 | `blockx_executor/runtime.py`             | Process controller. `submit_call` / `resume_call` / `cancel_call` are the three inbound semantic entry points, `run_until_idle` is the scheduling loop, `build_heartbeat` assembles heartbeats. |
| `ExecContext`, `ExecState`                        | `blockx_executor/context.py`             | Local record and state enum for a single call attempt.                                                                                                                                          |
| `SdkBridge._perform_wait`                         | `blockx_executor/sdk_bridge.py`          | The single implementation of "emit `CallWaiting` → yield the greenlet → validate, then return the result / raise".                                                                              |
| `SdkBridgeProvider`                               | `blockx_executor/sdk_bridge_provider.py` | The provider installed through `blockx_sdk.runtime.set_provider()`.                                                                                                                             |
| `blockx_sdk.Provider`                             | `blockx_sdk/provider.py`                 | The SDK seam protocol: `read_req` / `write_req` / `capability_req` / `subcall` / `sleep`.                                                                                                       |
| `BridgeChannel`, `make_blockdb_channel_factory`   | `blockx_executor/blockdb_bridge.py`      | The pseudo channel injected into `blockdb._grpc_client.connection.set_channel_factory` and `blockx._grpc_client.connection.set_channel_factory`.                                                |
| `install_noderpc_bridge`                          | `blockx_executor/noderpc_bridge.py`      | Replaces leafage's per-chain Web3 provider.                                                                                                                                                     |
| `ModuleRegistry.load_callable`                    | `blockx_executor/module_registry.py`     | Returns the entry callable for this task; fetches source through `source_loader` when it is missing.                                                                                            |
| `UDSSession`                                      | `blockx_executor/uds_session.py`         | `send_message` / `drain_incoming` / `wait_for_message` / `is_connected`.                                                                                                                        |
| `MessageEnvelope`                                 | `blockx_executor/messages.py`            | Unified envelope for inbound and outbound messages; `binary_payload` carries the sidecar.                                                                                                       |
| `ProcessMetricsSampler`                           | `blockx_executor/process_metrics.py`     | `probe_capabilities` / `sample_memory_bytes` / `sample_cpu_metrics`.                                                                                                                            |
| `RemoteCallError` and subclasses                  | `blockx_executor/sdk_bridge.py`          | Synchronous exceptions mapped from `ResumeCall.error`: `SubcallFailedError`, `SubcallDepthExceededError`, `SubcallCycleDetectedError`, `FunctionCodeFetchError`.                                |
| `CallCancelledError`, `CallDeadlineExceededError` | `blockx_executor/sdk_bridge.py`          | Framework-level cancellation / timeout signals; the former inherits from `BaseException`, so a user `except Exception` cannot catch it.                                                         |

The `Provider` protocol verbatim:

```python theme={null}
class Provider(Protocol):
    def read_req(self, backend: str, operation: str, params: Mapping[str, Any], *, cache_key: Optional[str] = None) -> Any: ...
    def write_req(self, backend: str, operation: str, params: Mapping[str, Any]) -> Any: ...
    def capability_req(self, backend: str, operation: str, params: Mapping[str, Any], *, cache_key: str = "") -> Any: ...
    def subcall(self, function_id: str, args: Any) -> Any: ...
    def sleep(self, seconds: float) -> None: ...
```

## User function programming model

A function module is a piece of Python source whose entry point is a top-level `def`. The entry-name resolution rules are in `resolve_entry_name` in `blockx_audit/tables.py`: prefer the top-level def whose name matches the last segment of `functionId`, then `def _` (or a `_ = some_def` alias), and finally fall back to the first public def. In practice, just write `def _(...)`.

When `ExecuteCall.payload.args` is a JSON array it is unpacked positionally; otherwise it is passed as a single argument (`_run_callable` in `runtime.py`: `target(*input_payload) if isinstance(input_payload, list) else target(input_payload)`). The return value is row data directly: a `dict` is one row, a `list[dict]` is multiple rows, and `None` means no output (see [Plugin system](/en/components/plugins) for details). Return values are serialized by `uds_codec.dumps_wire`: Python `int` values beyond 64 bits (e.g. uint256) can be returned directly, `datetime` / `date` become RFC 3339 strings, dataclasses are converted through `asdict` as a fallback (with a one-time warning), and other types such as `Decimal` are not serializable and must be converted to `str` first (which is why the example below uses `str(total)`).

Below is a real case from `cmd/worker/worker_process_subcall_test.go`. It has no `import` at all, because the unrestricted namespace is preloaded with `tables.PRELUDE_IMPORTS` (`json`, `Decimal`, `datetime`, `date`, `blockdb.Table` / `BlockTable` / `TimeTable` / `Block`, `blockx.function`, `leafage.ChainState`, and so on):

```python theme={null}
def _(p):
    child = function.call("adder", {"left": p["left"], "right": p["right"]})
    total = Decimal(str(child["sum"])) + Decimal("0.50")
    return {
        "child": child,
        "encoded": json.dumps({"sum": child["sum"]}, sort_keys=True, separators=(",", ":")),
        "total": str(total),
        "day": date(2026, 6, 10).isoformat(),
        "stamp": datetime(2026, 6, 10, 9, 30).isoformat(),
    }
```

The node RPC read pattern from `e2e/perf/s4_noderpc_batch_test.go`:

```python theme={null}
from leafage import ChainState

def _(p):
    cs = ChainState(p["chain"], p["block_id"])
    code = cs.get_address_code(p["address"])
    bal = cs.get_address_balance(p["address"])
    return len(code) + bal
```

How each kind of SDK call looks in user code and where it is intercepted:

| User code                                                                            | External package | Executor interception point                                                                                                                                                                                        | Provider method used                                 |
| ------------------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| `Table("t").get_row(...)`, `BlockTable("s").get_state(...)`, `upsert_rows(...)`      | `blockdb-py`     | `BridgeChannel` (gRPC channel layer; `WriteService/` goes through the write path)                                                                                                                                  | `read_req` / `write_req`, backend `blockdb_executor` |
| `ChainState(...).get_address_balance(...)` and similar                               | `leafage-py`     | `NodeRPCBridgeProvider.make_request`                                                                                                                                                                               | `read_req`, backend `rpc`                            |
| `function.call("child", *args)`                                                      | `blockx-py`      | `blockx.function.call` switches to `blockx_sdk.runtime.get_provider().subcall(...)` when it sees `BLOCKX_EXECUTOR_RUNTIME=1`; in restricted mode `restricted_runtime._subcall_call` binds directly to the provider | `subcall`                                            |
| `blockx_sdk.call_subfunc(...)`, `blockx_sdk.db.get(...)`, `blockx_sdk.rpc.call(...)` | `blockx_sdk`     | Calls the provider directly                                                                                                                                                                                        | `subcall` / `read_req`                               |
| `time.sleep(x)`, `blockx_sdk.sleep(x)`                                               | stdlib           | `sleep_patch.install_global_sleep_patch`                                                                                                                                                                           | `sleep` (local timer, no `CallWaiting` emitted)      |

The relationship between `blockx_sdk` and `blockx-py`: `blockx-py` / `blockdb-py` / `leafage-py` are the SDKs users actually face, and they can also connect to the services directly outside the Executor; `blockx_sdk` is not a replacement for them but the seam inside the Executor process: `blockx_sdk.runtime` keeps the current `Provider` in `contextvars`, and `blockx-py`'s `function.call` lazily imports it when it detects the Executor environment. The early `blockx_sdk.db` / `rpc` / `call_subfunc` entry points now mainly appear in unit tests, Go-side E2E fixtures, and the load functions generated by `cmd/bundle-loadgen`.

<Note>
  When auditing is enabled (`ExecuteCall.payload.audit` is present), the module is exec'd inside `restricted_runtime.restricted_namespace`: `__builtins__` contains only `SAFE_BUILTINS`, `import` only yields facades of whitelisted modules, and `blockx` exposes only `function` and the capability classes. See `docs/specs/function-code-python-whitelist.md` in the blockx repo for which syntax and APIs are whitelisted.
</Note>

## Data flow / execution flow

```mermaid theme={null}
sequenceDiagram
    participant W as Worker
    participant S as UDSSession threads
    participant R as ExecutorRuntime main thread
    participant G as call greenlet
    participant B as SdkBridge

    W->>S: ExecuteCall(callId, functionCodeDigest, args)
    S->>R: handle_inbound_message after drain_incoming
    R->>R: submit_call admission, create ExecContext and greenlet, enqueue
    R->>G: _step_context first greenlet.switch()
    G->>G: ModuleRegistry.load_callable (do_function_code_req fetches source on miss)
    G->>G: set_provider(SdkBridgeProvider), invoke user entry point
    G->>B: Table.get_row → BridgeChannel → provider.read_req
    B->>B: _perform_wait generates requestId, state becomes WAITING_IO
    B->>S: emit CallWaiting(waitKind=io, cacheKey, request, sidecar)
    S->>W: frame written out serially
    B->>R: greenlet.parent.switch() yields
    W->>S: ResumeCall(callId, requestId, resumeKind=io, result or error, budgetUsedMs)
    S->>R: resume_call validates attempt / state / requestId
    R->>G: next _step_context resumes with greenlet.switch(payload)
    B->>B: back to RUNNING, charge_wait, check cancellation / deadline
    B-->>G: return result or raise RemoteCallError
    G->>G: user function returns
    G->>S: emit CallCompleted(output, effectiveExecutionDurationUs, queueWaitUs)
    S->>W: frame written out
```

The main loop lives in `__main__.py`: `while session.is_connected(): runtime.run_until_idle(); session.wait_for_message(timeout=runtime.next_scheduler_delay(None))`. Each round of `run_until_idle` first wakes sleepers that are due, refreshes the connection state, and processes inbound messages, then takes one `call_id` from `_sleep_ready` (preferred, yielding after 64 consecutive picks) or `_runnable` and hands it to `_step_context`. `_step_context` has only three branches: if `cancel_reason` is set, `_terminate_context`; if `resume_payload` is set, `switch(payload)` back to the wait point; otherwise the first `switch()` enters `_run_callable`.

At startup, `ExecutorRuntime.__init__` performs the one-time hook installation: `install_global_sleep_patch()`, `_install_blockdb_bridge` (`blockdb`'s `mode.set_mock(False)` + `set_channel_factory`), `_install_noderpc_bridge`, `_install_localtest_bridge` (`blockx-py`'s channel factory, used by the Python clients of capability backends), and sets `BLOCKX_EXECUTOR_RUNTIME=1`. `SdkBridgeProvider` is not installed globally once; instead `set_provider` runs on every entry into `_run_callable` and `reset_provider` on exit.

<Info>
  The SDK hooks combine two techniques: `blockx_sdk` uses provider injection; `blockdb-py` and `blockx-py` are replaced at the gRPC channel layer through the `set_channel_factory` seam they expose; `leafage-py` is patched by replacing `W3_DICT` entries. There are no hooks at the socket layer.
</Info>

BlockDB and NodeRPC request bodies do not go through JSON: `MessageEnvelope.binary_payload` carries the raw protobuf / JSON-RPC bytes, and `uds_codec.encode_frame` packs them into a `BXB1`-tagged frame; the Worker's `ResumeCall` reply also carries the response bytes in a sidecar, `runtime._handle_inbound_message_typed` puts them into `result["Data"]`, and the bridge hands them to the gRPC stub for deserialization. blockdb-py additionally caches mode / channel / stub on its side. There is no Python-local L1 read cache and no protocol-level batching inside the Executor process.

## State and lifecycle

The actual values of `ExecState` (`context.py`):

```mermaid theme={null}
stateDiagram-v2
    [*] --> RUNNABLE : ExecuteCall / submit_call
    RUNNABLE --> RUNNING : scheduler switches into the greenlet
    RUNNING --> WAITING_IO : read_req / write_req / capability_req
    RUNNING --> WAITING_SUBCALL : subcall
    RUNNING --> WAITING_FUNCTION_CODE : digest miss, fetch source
    RUNNING --> WAITING_SLEEP : sleep via local timer
    WAITING_IO --> RUNNING : rescheduled after ResumeCall matches
    WAITING_SUBCALL --> RUNNING : rescheduled after ResumeCall matches
    WAITING_FUNCTION_CODE --> RUNNING : ResumeCall or local leader wake-up
    WAITING_SLEEP --> RUNNING : timer expires, enters sleep_ready
    RUNNING --> DONE : return / exception / cancellation / deadline
    WAITING_IO --> DONE : CancelCall / attempt replacement / connection lost
    WAITING_SUBCALL --> DONE : same as above
    WAITING_FUNCTION_CODE --> DONE : same as above
    WAITING_SLEEP --> DONE : same as above
    RUNNABLE --> DONE : cancelled before running
    DONE --> [*]
```

A few additional notes:

* On resume, the state does not first go back to `RUNNABLE`: `resume_call` only writes `resume_payload` and enqueues with `queued=True`, and `_perform_wait` switches directly to `RUNNING` after `switch()` returns. Whether a waiting context is already queued is expressed by the `queued` flag, not by the state.
* A `DONE` context is removed from `_contexts` immediately, so `inflight_count()` is just `len(_contexts)`.
* `WAITING_FUNCTION_CODE` is used for fetching source by digest. Concurrent misses on the same digest are coalesced locally (singleflight) in `SdkBridge._function_code_fetches`; only the leader emits `CallWaiting(waitKind=function_code)`.

**Attempts and requestId.** `_replace_existing_attempt` runs inside `submit_call`: the old context is marked `stale`, removed from the queues, and terminated with `_terminate_context(..., suppress_terminal_emit=True)`; `attempt_seq` comes from `ExecuteCall.payload.attemptSeq` (default 1), not from a local counter. `requestId` has the form `req-<processInstanceId>-<seq>` (`next_request_id`); the process-instance-id prefix ensures a restarted process never collides with the old process's pending requests, and `seq` increases monotonically within the process. Stale resumes / cancels increment `_stale_resume_dropped` / `_stale_cancel_dropped` respectively.

**Cancellation.** For `WAITING_*` states, `cancel_call` runs `_terminate_context` immediately (`throw(CallCancelledError)` into the greenlet, then `GreenletExit`); for `RUNNING` / `RUNNABLE` it only sets `cancel_reason` and enqueues, and `check_cancelled` raises at the next safe point. `CancelCall.payload.reason` defaults to `worker_shutdown`; the local timeout latch uses `call_timeout`, and the terminal state is classified as `deadline_exceeded` rather than `cancelled`.

**Deadline and budget.** `ExecContext.deadline_expired_reason()` is the single decision point: `budget_ms` (`callBudgetMs`, which counts only CPU + IO backend + subfunction + sleep time, not queueing) takes precedence over the absolute `call_deadline_ms`. The safe points are: before entering the user function, before sending and after resuming in every `_perform_wait`, after waking from sleep, and before returning. In addition, `_switch_greenlet_with_deadline` calls `signal.setitimer(ITIMER_REAL, remaining, 0.1)` before every switch into the greenlet, and `_on_deadline_signal` raises `_DeadlineSignalExpired` only when that call is `RUNNING` and has actually expired. This `SIGALRM` path can preempt pure-CPU loops; loops that swallow `BaseException` or long C calls still rely on the Worker's wedge handling.

**Heartbeat and connection.** `_heartbeat_loop` is a separate thread that sends `Heartbeat` every `heartbeat_interval_s`; the payload contains `runnableCount`, `inflightCount`, `availableContexts` (`max(0, max_inflight - inflight)`; `1<<30` when unlimited), `memoryBytes`, `executorCpuUtilization`, `executorCpuSecondsTotal`, `executorProcessInstanceId`, `lastSchedulerActiveAtMs`, `runningCallId`, `callStateCounts`, `sentAtMs`. `ProcessMetricsSampler` performs a capability probe with retries when `ExecutorRuntime` is constructed, and process startup fails if the probe fails; sampling failures at runtime keep the previous values. After the UDS disconnects, `_refresh_transport_state` marks every active context `transport_lost` and enqueues it to converge, and `submit_call` rejects new calls.

**Admission.** A top-level call raises `ExecutorCapacityExhausted` when `inflight_count() >= executor_max_inflight` (replied as `CallFailed(errorKind=executor_capacity_exhausted, retryable=true)`); subfunctions skip this gate and only require the parent context to be resident in this process, otherwise `subcall_parent_not_resident` (not retryable). `executor_max_runnable` is only received and stored; it is not enforced locally.

## Configuration

Command-line arguments (`blockx_executor/__main__.py`, passed in by `executorRuntimeArgs` in `internal/worker/adapters/executor/spawner.go`):

| Argument                  | Default  | Description                                                                                      |
| ------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `--executor-id`           | required | Logical executor id, reusable across restarts.                                                   |
| `--socket-path`           | required | The UDS path the Worker listens on; in sandbox mode this is the mount path inside the container. |
| `--heartbeat-interval`    | `1.0`    | Heartbeat interval in seconds.                                                                   |
| `--executor-max-inflight` | `128`    | Upper bound on resident contexts; `0` means unlimited.                                           |
| `--executor-max-runnable` | `128`    | Stored only; the dispatch threshold on the Worker side.                                          |

Environment variables:

| Variable                                                                                                    | Default                   | Description                                                                       |
| ----------------------------------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------- |
| `BLOCKX_LOG_LEVEL` / `BLOCKX_LOG_FORMAT`                                                                    | `info` / JSON             | Structured log level and format (`text` switches to plain text).                  |
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_SAMPLER_ARG`, `BLOCKX_OTEL_SPAN_FILE`, `BLOCKX_OTEL_UDS_EVENTS` | No tracing when unset     | OTLP export, sampling ratio, file export, fine-grained UDS send / receive events. |
| `BLOCKX_EXECUTOR_CODE_CACHE_MAX_ENTRIES`                                                                    | `512`                     | LRU capacity of the `ModuleRegistry` compile cache; `0` means unlimited.          |
| `BLOCKX_EXECUTOR_ABI_CACHE_TRIM_THRESHOLD`                                                                  | `4096`                    | Trim threshold for the `eth_abi` cache.                                           |
| `BLOCKX_EXECUTOR_RUNTIME`                                                                                   | Set to `1` by the runtime | `blockx-py` uses it to detect that it is running inside the Executor.             |

In sandbox mode (`EXECUTOR_SPAWN_MODE=sandbox`), the Go side only passes through the variables in the `childEnvKeys` whitelist in `internal/worker/adapters/executor/pool.go` (everything in the table above plus `LEAFAGE_ENDPOINT` and `PROXY_SOCKET_PATH`); bare-process mode still inherits the entire parent environment. The container has an empty netns, so a direct OTLP connection is unavailable. The Python process itself is unaware of the spawn mode; see `docs/specs/executor-sandbox-isolation.md` in the blockx repo.

## Extension points

* **Connect a new external SDK to the Worker IO channel**: write a bridge module (see `blockdb_bridge.py` / `noderpc_bridge.py` for reference) that replaces the SDK's transport seam with calls to `provider.read_req` / `write_req`, and install it in `ExecutorRuntime.__init__`; the backend name must match the backend registered in the Worker-side IO subsystem (see [IO access subsystem](/en/components/io-subsystem)). If the SDK is gRPC unary, reuse `BridgeChannel(provider_getter, backend=..., streaming_methods=frozenset())` directly.
* **Add a new SDK entry point for user code**: only add a thin wrapper under `blockx_sdk` that calls `get_provider()`; do not hold connections in the SDK. For restricted mode, also update the whitelist in `blockx_audit/tables.py` and the `restricted_runtime` facade.
* **Add a new `ResumeCall` error type**: add a `RemoteCallError` subclass in `sdk_bridge.py` and extend `_map_resume_error`; use `register_io_error_kinds` for backend-specific mappings.
* **Add a heartbeat field or message type**: add a constructor in `messages.py`, a constant in `wire_keys.py`, a branch in `runtime._handle_inbound_message_typed`, and update `api/uds/types.go` in step.
* **Change safe points or accounting**: all expiry decisions go through `ExecContext.deadline_expired_reason` and `raise_if_deadline_expired`; do not write separate checks elsewhere.

## Testing

```bash theme={null}
uv sync --project python                                    # first time or after uv.lock changes; the Go E2E tests use this .venv too
PYTHONPATH=python uv run --project python python -m unittest discover -s python/tests
PYTHONPATH=python uv run --project python python -m unittest python.tests.test_executor_protocol_contract   # single module
```

`python/tests/` is split into files by implementation boundary (`test_<boundary>.py`), and each file is split into `TestCase`s by logical block (`<Boundary><Theme>Test`); the convention is in `docs/specs/python-test-organization.md` in the blockx repo. The main boundaries:

* Runtime: `test_executor_runtime_scheduler.py`, `test_executor_runtime_termination.py`, `test_executor_runtime_attempt.py`, `test_executor_budget.py`, `test_executor_subcall.py`, `test_executor_oom_hardening.py`, `test_terminal_bounds.py`.
* Protocol: `test_executor_protocol_contract.py` (`ExecuteCall` validation, inbound control messages, heartbeat fields), `test_uds_codec.py`, `test_uds_session.py`, `test_envelope_*.py`.
* SDK hooks: `test_executor_sdk_hook.py`, `test_sdk_bridge_provider.py`, `test_sdk_bridge_capability.py`, `test_perform_wait_emit_failure.py`, `test_blockdb_bridge.py`, `test_noderpc_bridge.py`, `test_sleep_patch.py`.
* Loading and isolation: `test_module_registry.py`, `test_restricted_runtime.py`, `test_executor_audit_gate.py`.
* Context and metrics: `test_exec_context_*.py`, `test_process_metrics_sampler.py`, `test_logging_setup.py`, `test_tracing_setup.py`, `test_debug_capture.py`.
* Auditor: `test_audit_*.py`.

Unit tests do not need a real UDS: when `ExecutorRuntime("exec-1")` is constructed without a `session`, messages go to `drain_outbox()`; feed inbound messages with `handle_inbound_message(dict)` and advance with `run_until_idle()`.

E2E tests live on the Go side: `cmd/worker/worker_process_*_test.go` and `cmd/syncinvoker/*_test.go` spawn a real Worker + `python/.venv/bin/python -m blockx_executor`; `e2e/system` brings up the whole system; `e2e/perf` runs under `systemd-run` cgroup limits through `test.sh` at the repo root. See [Testing](/en/development/testing) for the commands.

## Related docs

blockx repo specs:

* `docs/specs/python-function-executor.md` — the in-process design of the Executor: scheduling, states, attempt / requestId, cancellation and deadlines, resource bounds (the key one).
* `docs/specs/worker-executor-connection-and-python-sdk-hook.md` §6–§8 — the SDK hook approach, the `SdkBridge` synchronous contract, the suspend / resume sequence.
* `docs/specs/python-executor-process-metrics.md` — sampling and degradation of process metrics in heartbeats.
* `docs/specs/2026-07-23-execute-call-function-code-reference.md` — the protocol for fetching source by digest.
* `docs/specs/executor-sandbox-isolation.md` — the containerd sandbox spawn mode.
* `docs/specs/2026-07-30-python-executor-cpu-optimization.md` — the CPU optimization plan.
* `docs/specs/python-test-organization.md` — the unit test organization standard.
* `docs/specs/function-code-python-whitelist.md`, `docs/specs/function-code-audit-design.md` — user code whitelist and auditing.

On this site:

* [Call execution subsystem](/en/components/call-execution) — Worker-side dispatcher, executor adapter, subfunctions.
* [Protocols and interfaces](/en/architecture/protocols) — UDS frame format and message fields.
* [Function Code View](/en/components/function-code) — where source comes from and `blockx_audit`.
* [IO access subsystem](/en/components/io-subsystem) — where `CallWaiting(waitKind=io)` goes on the Worker side.
* [Observability](/en/components/observability) — log fields, metrics, tracing.
