Skip to main content

Agent Run Supervisor

English  ·  简体中文

CI codecov Python 3.11+ License: MIT

A small, local-first supervisor for external coding AGENTs.
One local daemon, one process per run, and redacted, auditable local evidence.

What it is

Anything that drives an external coding AGENT ends up rebuilding the same plumbing: launching and babysitting the agent process, deciding what the agent is allowed to touch, reading a stream of protocol events, classifying how the run ended, and scrubbing secrets out of everything before it touches disk. Written ad-hoc, every caller grows its own subtly unsafe copy.

Agent Run Supervisor (ARS) factors that out into one independent local layer. Your application submits a run — which agent profile, which model, which workspace, which prompt — and ARS does the rest: it admits the request, launches exactly one supervised agent process, mediates every permission request under a default-deny policy, normalizes what the agent emits into ordered events, classifies a supervisor-owned status, and writes redacted artifacts with restrictive permissions.

What you get back is auditable evidence, not a tangle of process-lifecycle code.

Use it when you want to run a coding agent programmatically and still be able to answer what did it try to do, what was it allowed to do, and how did it actually end?

How it works

A trusted local caller submits over the arsd Unix-domain socket; arsd authenticates the peer and admits the request; ars-core runs one RunTask over Native ACP against a registered external AGENT; normalized events, status, and redacted local artifacts come back

The primary path is entirely local:

  1. Your application connects to arsd, the small unprivileged supervisor daemon.
  2. arsd listens on a Unix-domain socket — a 0600 socket inside a 0700 directory. No TCP, no root, no public ingress.
  3. The peer is authenticated and the request is admitted. arsd reads the peer's credentials from the socket and maps them to a principal, then admits the request against your caller-owned request_id, which doubles as the idempotency key. Runs and sessions are owner-scoped: only the caller that owns one can query, stream, cancel, or close it.
  4. ars-core runs the work. One in-process RunTask owns one supervised agent process and one Native ACP connection, driven by an immutable run spec frozen at admission.
  5. The agent is a registered external process launched from a closed profile — no arbitrary command, argv, or environment passthrough from the wire.

Coming back, you get normalized, seq-ordered events and a supervisor-owned status over the same socket, plus redacted local artifacts on disk. ARS reports technical supervision facts only; your application owns the business verdict.

Two protocols, two different 1s

ARS sits between two independently versioned protocols. Both currently say 1, and they are not the same 1:

  • ACP Protocol v1 — the downstream Agent Client Protocol, spoken over stdio JSON-RPC between ARS and the external AGENT process. Every registered profile freezes ACP protocol version 1 in its contract, and a live agent that reports anything else fails the run at initialize, before any prompt is dispatched.
  • arsd API v1 — the upstream ARS-owned wire between your application and arsd over the Unix socket. Every frame carries api_version (currently 1); an unknown version is rejected rather than guessed.

They move independently: an ACP protocol change is an agent-compatibility fact, an arsd API change is a caller-compatibility fact, and neither implies the other.

Design detail lives in docs/design/architecture.md.

Install

Install from this repository — that is the supported way to get everything described below.

git clone https://github.com/jovijovi/agent-run-supervisor.git
cd agent-run-supervisor

The runtime is Python standard library only, so a checkout is immediately usable without installing anything:

PYTHONPATH=src python3 -m agent_run_supervisor doctor

For an editable local install into the active environment:

pip install -e .

# with the optional extras used by the test suite and the Native ACP suites
pip install -e '.[dev,native]'

Nothing in ARS launches an agent implicitly. doctor, replay, --print-service-unit, and runtime-binding inspect-run are read-only and start no agent process.

Run arsd locally

arsd is a module entry point, not a console script:

# Options and boundaries (read-only)
PYTHONPATH=src python3 -m agent_run_supervisor.arsd --help

# Render a user-scope systemd unit to stdout and exit.
# Pure text: no privilege check, no reconciliation, no socket bind — nothing is
# installed, enabled, or started. --binding-root is required here too, so a
# rendered unit can never silently omit it; the path is argv data, not accessed.
PYTHONPATH=src python3 -m agent_run_supervisor.arsd \
  --binding-root <binding-root> \
  --print-service-unit

# Start the daemon
PYTHONPATH=src python3 -m agent_run_supervisor.arsd \
  --supervisor-root <supervisor-root> \
  --binding-root <binding-root> \
  --caller-mapping <UID>:<principal_id>:<owner>:<namespace>

Daemon mode requires --supervisor-root, --binding-root, and at least one --caller-mappingzero mappings refuse to listen, and the daemon refuses to start as root. --socket defaults to $XDG_RUNTIME_DIR/agent-run-supervisor/arsd.sock, falling back to <supervisor-root>/arsd/arsd.sock. --max-concurrent-runs, --max-connections, and --log-level bound the rest.

Caller mappings, socket paths, and the Binding root are deployment values. Keep them in a mode-0600 unit file, never in a repository.

If the daemon is restarted, it reconciles durable facts only: a run that may have been dispatched without a trustworthy terminal result ends unknown / quarantined / retryable=false and is never re-prompted.

The Runtime Binding

--binding-root points at the operator-owned Runtime Binding, the deployment half of a run. The source contract owns the launch and compatibility semantics — launch shape, ACP protocol and capabilities, selectors, permission and session semantics — and, for a wrapped-ACP profile, also the identity of the ARS-controlled interpreter and adapter entry, which are ARS artifacts rather than operator deployment facts. The Binding owns the operator's deployment facts: which downstream or direct AGENT CLI artifact is installed, at which immutable path, at which version and digest, plus any config-root value the profile declared. It never declares a command, argv, env key, adapter, capability, or selector. A caller chooses neither side.

ARS opens the Binding root read-only, exactly once per run, and never creates, writes, or promotes it. Every registered profile refuses admission fail-closed until an operator has prepared an immutable artifact root the daemon's own UID cannot rewrite and promoted a generation for that profile — so a freshly started daemon with no promoted Binding runs nothing.

The operator surface is a separate CLI:

agent-run-supervisor runtime-binding validate    --binding-root <root> --profile <id> --generation <gen>
agent-run-supervisor runtime-binding promote     --binding-root <root> --profile <id> --generation <gen>
agent-run-supervisor runtime-binding rollback    --binding-root <root> --profile <id> --generation <gen>
agent-run-supervisor runtime-binding inspect-run --run-dir <native-run-dir>

From a checkout without installing, replace agent-run-supervisor with PYTHONPATH=src python3 -m agent_run_supervisor.

There is no --force: a generation that does not validate is never promoted. Nothing here installs an artifact, edits a unit file, escalates privilege, or restarts the daemon. Promotion needs no restart and takes effect on the next run, because admission re-reads the active pointer per run and never re-points a run that is already sealed.

Call it from Python

ArsdClient is the supported caller boundary: explicitly connected, context-managed, never silently reconnecting and never replaying a request. Every frame carries api_version (currently 1); an unknown version is rejected rather than guessed.

from agent_run_supervisor.arsd.client import ArsdClient

socket_path = "<XDG_RUNTIME_DIR>/agent-run-supervisor/arsd.sock"

with ArsdClient(socket_path) as client:
    client.server_info()                      # protocol/version handshake facts

    ack = client.submit(                      # caller-owned request_id = idempotency key
        request_id="my-caller-request-id",
        payload={
            "request": {...},                 # versioned AgentRunRequest (see below)
            "prompt_text": "Summarize the diff in plain language.",
            "workspace_root": "/path/to/bound/workspace",
        },
    )
    run_id = ack["run_id"]

    client.run_status(run_id)                          # accepted → progress → one terminal result
    client.run_events(run_id, from_seq=0, limit=100)   # bounded, seq-ordered page
    client.run_cancel(run_id)                          # cooperative; never rewrites a terminal fact

    client.session_list()                     # owner-scoped session inventory
    client.session_status("my-session-id")
    client.session_close("my-session-id")

# Live tailing: follow=True returns a context-managed subscription of event frames
with ArsdClient(socket_path) as client:
    with client.run_events(run_id, from_seq=0, follow=True) as stream:
        for frame in stream:
            ...

The request object is a versioned AgentRunRequest: owner / namespace, profile_id, the session-reuse choice, requested_model / requested_effort, input references, the frozen execution_grant reference and hashes, credential references, and limits.

It never carries shell text, argv, environment values, executable paths, or credential material — those fields do not exist on the wire.

Errors are typed and fail closed. Client exceptions carry a stable code (for example PEER_UID_DENIED, OWNER_MISMATCH, IDEMPOTENCY_CONFLICT, CAPACITY_EXHAUSTED); server-side message text is never echoed back into an exception.

Agent profiles

A profile is a closed, versioned, code-registered launch definition, and every registered profile speaks ACP Protocol v1 — distinct from the arsd API v1 your application speaks, as described above. Model and effort must read back exactly from the live agent: a missing capability, an unadvertised value, or an inexact readback fails the run before any prompt is dispatched.

profile_id Agent Launch requested_model requested_effort
opencode-native-acp OpenCode direct ACP kimi-for-coding/k3 (default) low / high / max (default max)
codex-acp-1.1.7 Codex, via its official ACP adapter wrapped ACP gpt-5.6-sol max
claude-agent-acp-0.63.0 Claude, via its official ACP adapter wrapped ACP claude-fable-5[1m], opus[1m] (default) max

Submit every literal above verbatim — but they come from two different places. profile_id is ARS registry input: it names a contract in the code-registered registry and is matched exactly at admission. The model and effort literals are live ACP values: the agent advertises them over the wire and must read them back exactly. Neither kind is interchangeable with the selector names a vendor's own CLI accepts — that is a third namespace.

A profile_id is not a CLI version. It is an ARS-owned identifier for a closed launch and compatibility contract: the launch shape, the ACP protocol and required/forbidden capabilities, the selector IDs, the exact model/effort domains proven by discovery, and the permission, config, and session semantics. Which downstream CLI build is actually deployed — path, version, digest — is a Runtime Binding fact owned by the operator, never a source constant, which is why a profile ID that still carries an adapter version pins the adapter contract, not the agent CLI you installed. And this is why speaking generic ACP does not remove the need for profiles: ACP standardizes the wire, not the launch, the selector names, the permission semantics, or the literals a given agent will actually accept and read back.

The Claude contract moved to claude-agent-acp-0.63.0. The registered Claude source contract is claude-agent-acp-0.63.0 (revision 4), frozen against zero-prompt ACP discovery of the 0.63.0 adapter. There is no 0.61.0 compatibility alias: the retired ID is now an unknown profile and admission refuses it. Registration is a source fact only — the new revision still needs its own operator acceptance and a Binding generation promoted at the new adapter_contract_hash before any Run can use it, and a generation accepted under the old contract fails closed by design.

Each profile launches an agent runtime that you install and pin, but the split differs by launch kind. A wrapped-ACP profile source-freezes the interpreter and adapter entry by absolute path and hash, while the Binding freezes the downstream CLI by immutable path, version, and digest closure. The direct-ACP profile has no separate adapter — the deployed executable is both the agent CLI and the ACP implementation — so its executable and interpreter closure is bound entirely through the Runtime Binding. Either way, identity is attested before spawn. A source checkout does not, by itself, make an agent launchable: you still install the agent locally and promote a Binding generation for its profile.

Guarantees and boundaries

What ARS guarantees

  • Supervisor, not business judge. Protocol or process completion is never a business verdict. business_verdict is always null and belongs to the caller.
  • Default-deny, caller-frozen permissions. The caller freezes the execution grant; ARS enforces it and never widens or refreshes it. Registered workspace-internal reads may be allowed; write, terminal, execute, and unknown operations are denied. Every decision produces redacted mediation evidence.
  • Auditable by default. Runs produce deterministic, redacted artifacts with restrictive permissions: 0700 directories, 0600 files, atomic final writes.
  • Fail closed on uncertainty. Invalid input, protocol drift, denied permissions, timeouts, and untrustworthy recovery all resolve to deterministic non-success states rather than a guess.
  • Local and unprivileged. A 0600 socket in a 0700 directory, peer-credential authentication against an explicit caller policy, and no root.

What ARS is not

  • Not a sandbox. This is cooperative-agent policy mediation, not OS-level isolation, not hostile-process containment, and not multi-tenancy.
  • Not a crash-containment mechanism by itself. Production expects a user-level service manager cgroup (Restart=on-failure, KillMode=control-group) so that killing the daemon kills every agent descendant.
  • Not an ingress, a gateway, or a chat integration. No public ingress, no message delivery, no agent-to-agent routing. Those belong to the caller and its platform.

Requirements

Need Requirement
Runtime Python ≥ 3.11, standard library only — zero third-party runtime dependencies.
Running arsd Linux with a POSIX user session for the AF_UNIX socket, plus a supervisor root, a Runtime Binding root, and at least one caller mapping you supply. Crash containment additionally needs a user-level service manager cgroup and a CPython build with pidfd support.
Running an agent Each profile launches an agent runtime you install and pin locally, plus a promoted Binding generation for that profile. A checkout alone does not provide OpenCode, Codex, or Claude.
Tests (optional) The dev extra for the suite; the native extra adds the ACP client library (agent-client-protocol, pinned to 0.11.1) used by the Native ACP and arsd suites.

Development

The primary path uses uv; the root Makefile wraps the common commands.

make sync      # uv sync --locked --extra dev --extra release --extra native
make verify    # full local gate (same as CI)
make build     # sdist/wheel + twine check
make clean     # remove build artifacts, caches, local scratch data
make help      # list all targets

Equivalent without Make:

uv sync --locked --extra dev --extra release --extra native
./scripts/verify_local.sh

make verify / ./scripts/verify_local.sh is the single local gate: tests, read-only CLI smoke, docs index checks, a static safety scan, and package checks. It is what CI runs, and it is documented in docs/roadmap/verification.md.

The suite covers the Native ACP core and the arsd daemon — protocol framing, peer authentication and ownership, admission and idempotency, reconciliation, the client round-trip — against a hermetic fake agent and temporary sockets. Suites that need a real agent runtime are opt-in, skipped by default, and never run in CI.

pip fallback (without uv):

pip install -e '.[dev,release,native]'
python3 -m pytest -q

License

© the agent-run-supervisor authors. Released under the MIT license (see LICENSE).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_run_supervisor-0.5.1.tar.gz (341.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_run_supervisor-0.5.1-py3-none-any.whl (279.5 kB view details)

Uploaded Python 3

File details

Details for the file agent_run_supervisor-0.5.1.tar.gz.

File metadata

  • Download URL: agent_run_supervisor-0.5.1.tar.gz
  • Upload date:
  • Size: 341.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for agent_run_supervisor-0.5.1.tar.gz
Algorithm Hash digest
SHA256 e64905e06f6191d9841bdff7cfe8ecf264d88c6d220e618e31f2fa25aa51deb7
MD5 aad96c553249f225a92e4d5f1d782cd1
BLAKE2b-256 a5cd32439f199ebc07a54ee5e13cbf1bc03f238518336b7fc52bfc6f66d10092

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_run_supervisor-0.5.1.tar.gz:

Publisher: release.yml on jovijovi/agent-run-supervisor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_run_supervisor-0.5.1-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_run_supervisor-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b7db2c442ed25d8bac1bdb8fec7dc0d814ab9bade59f63d539f713c3fa71306b
MD5 df674d154d411ac7767f22c4f549df11
BLAKE2b-256 c91be00e5aed9758f9aec5300281a777c1ed61025119dffa59901f83531eef68

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_run_supervisor-0.5.1-py3-none-any.whl:

Publisher: release.yml on jovijovi/agent-run-supervisor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.8

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.3

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.3

2 files

0.5.2

2 files

This release

0.5.1 This release

2 files

0.5.0

2 files

0.2.0

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page