Skip to content

Execution Policy v1 and governed local execution

apizr execute is an experimental trusted-code execution command. It starts one fresh Python process per invocation. This is not a filesystem or network sandbox. It runs with the caller's privileges and requires trusted source. inspect, generate rest and generate mcp remain static and non-executing. REST/MCP v1 retain their existing in-process runtimes by default; optional governed generation delegates calls to this same worker implementation.

Boundary and contracts

apizr.execution consumes a validated inspection, selected capability identity, original source bytes, executable bytes (for notebooks), and an execution policy. It uses the shared apizr.interfaces.InvocationContract, planner, argument validation, exact-byte source loader and callable verification. It does not infer eligibility, annotations or effects again. Readiness remains the authority for can_generate_interface; execution policy expresses separate runtime requirements.

Modules separate typed policy/backend capabilities (policy.py), bound plan and result (model.py), validation (planner.py), canonical serialization, framed JSON (protocol.py), parent supervision and worker execution. execute_cli.py connects the explicit command to these APIs. No backend registry or plugin system is introduced into v1; the separate OCI backend declares its own version and actual controls without changing interface semantics.

Contract Version
Policy apizr.execution/v1
Backend capabilities apizr.backend/v1
Local backend apizr.local-process/v1
Runtime plan apizr.runtime/v1
Public result apizr.execution-result/v1

Enforced and unsupported controls

local_capabilities() provides a typed machine-readable declaration. This backend is available on POSIX hosts (tested on Linux and macOS), where pipe selectors and process groups are available. Other hosts are refused before worker launch. This does not change Apizr's advertised Python 3.11–3.14 range or claim that other Apizr commands require this backend.

Control Local-process v1 behavior
Wall time Mandatory monotonic deadline; kill worker/process group with SIGKILL, reap direct child and close pipes
Input bytes Bound canonical invocation JSON before spawning, validate again in worker; CLI also bounds the argument file
Output bytes Bound complete response JSON in worker and parent, including framing length checks before accepting payload
Environment Empty inherited map by default; explicitly allow named variables, or explicitly opt into full inheritance
Working directory Fresh temporary directory for each call, removed after completion
Network denial Unsupported; requested denial refuses planning
Filesystem sandbox Unsupported; requested sandbox refuses planning
Subprocess denial Unsupported; requested denial refuses planning

The policy defaults are 5,000 ms, 1 MiB input and 1 MiB output. Size limits count UTF-8 canonical JSON including its final newline; output includes the result envelope, but excludes the eight-byte frame header. Output has a minimum of 128 bytes so controlled error envelopes fit. Maximum configurable input/output is 16 MiB; maximum wall time is one hour. The private request frame has a separate 64 MiB ceiling because it also carries the bound planning evidence.

A timeout is process termination, not coroutine cancellation. Timing includes worker startup and protocol exchange; operating-system process creation and scheduling are not real-time guarantees. The parent kills ordinary descendants remaining in the worker's process group and waits for the direct child. A subprocess deliberately creating a separate session can escape that group: subprocess containment is not a supported control. There is no memory limit, CPU allocation limit, PID limit, filesystem restriction, syscall filtering, privilege drop or mount restriction. A large user allocation can happen before JSON encoding; byte limits govern the protocol, not the worker's address space. Host file/network access and subprocess creation are intentionally tested as possible. Stronger controls are tracked in issue #45.

No Python monkeypatch is presented as an isolation mechanism. Unsupported policy fields are rejected, and supported vocabulary requesting unavailable enforcement fails closed. The model has no best_effort escape hatch.

Effects and deterministic planning

effects.require_known is a sorted set of IR effect names. If any required effect is UNKNOWN, planning refuses with unknown_required_effect. This is a knowledge requirement, not a claim that a known effect is safe or absent. Current readiness and IR effects are unchanged. The strict example intentionally refuses currently unknown network/filesystem-write effects.

policy_bytes and plan_bytes use canonical sorted UTF-8 JSON with one trailing newline. policy_digest and plan_digest compute external SHA-256 digests. The plan binds source and executable digests, logical module, capability identity, IR/readiness digests, shared interface and its digest, policy and its digest, and backend version. It retains the validated inspection evidence so the worker can revalidate the complete binding without source analysis. Independently supplied mismatched objects are rejected. Golden policy/plan files and property tests check byte stability on the supported Python matrix.

No policy/plan contains PID, timestamp, host name, temporary path, random ID or environment values. Environment allowlists contain names only, sorted and deduplicated. Runtime results are separate from deterministic planning artifacts. Execution plans are attestable artifacts, not attestations. There is no receipt, signature, Continuum Attest dependency or provenance verification.

Parent, worker and protocol

The parent validates JSON input using the shared contract before starting user execution. It writes source bytes into a temporary bundle and starts the installed interpreter using an argument array: python -I -m apizr.execution.worker. There is no shell, pickle or marshal. Isolated mode excludes the current directory, user site packages and Python startup environment overrides; the installed Apizr environment remains trusted. The supervisor never imports the target module.

Stdin carries one eight-byte unsigned big-endian payload length followed by one JSON request. Stdout carries exactly one similarly framed response. Nonblocking pipe supervision bounds reads, rejects excess/truncated/malformed data, detects abnormal exit and applies the deadline even when a worker closes pipes but keeps running. The worker reserves a non-inheritable output descriptor and redirects ordinary stdout to stderr. The parent discards stderr, so user prints, tracebacks and exception text are not forwarded as public diagnostics. This protocol is not an adversarial security boundary against trusted code deliberately inspecting or rewriting its own process descriptors.

Before importing, the worker validates the request/plan digest chain and exact executable bytes. The shared loader compiles the verified bytes rather than a cached pyc or a second read. Notebook execution uses the statically transformed Python digest. After import, shared binding verification checks symbol/function, sync/async form, absence of generator/variadics, parameter names/kinds and default presence, without evaluating annotations. Source import itself is the trusted-code boundary: module initialization, decorators and default expressions execute there.

The worker reconstructs positional-only and keyword-only arguments with the existing shared semantics. Omitted defaults remain omitted; explicit null must satisfy the annotation. Sync calls run directly; async calls run using the worker's own event loop. No thread is used to interrupt user code. Each call starts afresh, so module globals, environment changes and mutable defaults do not persist. This intentionally differs from current REST/MCP in-process state lifetime; optional governed transport integration preserves this difference explicitly.

Results and failures

Successful values must already be finite JSON data: null, bool, int, finite float, str, lists and dictionaries with string keys. Sets, tuples, bytes, arbitrary objects, non-string keys and non-finite floats fail conversion, matching the conservative MCP result semantics. Return annotations do not enforce outputs.

Stable result statuses are success, invalid_input, timeout, worker_failed, binding_failed, policy_refused, result_invalid, execution_failed, output_limit and source_mismatch. Failure values are null. No exception repr, traceback, stderr, PID or private runtime path is included. User-selected successful return values are application data: trusted code can intentionally return anything it can access, including explicitly allowed environment data. Sanitized diagnostics are not data-loss prevention.

Real process tests cover sync/async, non-cooperative loops/sleep, crashes, noisy stdio, framing failures, source tampering, binding conflicts, permitted host access, environment filtering and state reset. Tests have hard outer timeouts. Hypothesis checks canonical policy/plan determinism, argument transport, finite JSON, source mutation, output boundaries and allowlists. Strict Pyright and separate branch-aware 90% floors cover the package and worker/supervisor/protocol together.