Skip to main content

Agent Run Supervisor

English  ·  简体中文

CI codecov Python License: MIT

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


What it is

Anything that drives an external coding AGENT rebuilds the same plumbing: launch the agent process, babysit it, decide what it may touch, read a stream of protocol events, work out how the run really ended, and scrub secrets before anything hits disk. Written ad-hoc, every caller grows its own copy.

Agent Run Supervisor (ARS) is that layer, factored out and kept local. Your application submits a run — which registered agent, which model, which workspace, which prompt — and ARS launches exactly one supervised process from the command an operator registered, mediates every permission request default-deny, normalizes agent output into ordered events, classifies a supervisor-owned status, and writes redacted artifacts. You get auditable evidence instead of process-lifecycle code: what did the agent try to do, what was it allowed to do, how did it end?

ARS owns ARS deliberately does not own
the process it starts: PID/PGID, timeouts, signals, reap the software it starts — you install and upgrade agents yourself
the ACP conversation: capabilities, exact model/effort, continuity the agent's own conversation and context state
permission mediation against a caller-frozen grant the business verdict, which stays with your application
redacted per-run evidence under one supervisor root agent $HOME, auth stores, plugins, caches, config
caller authentication over a local socket credentials — ARS resolves, mints, refreshes, and stores none

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

trusted caller  →  arsd (local UDS)  →  ars-core / Native ACP  →  external AGENT process

Every hop is on one machine, under one unprivileged user:

  1. arsd starts: parse the agent registry exactly once → reconcile durable run/session facts → then bind the socket. Any registry defect refuses to listen before anything is written.
  2. Your application connects to a 0600 socket inside a 0700 directory — no TCP, no root, no public ingress. arsd authenticates peer credentials, maps them to a principal, and keys admission on your own request_id, which doubles as the idempotency key. Runs and sessions are owner-scoped.
  3. ars-core runs the work: one in-process RunTask owning one supervised process and one Native ACP connection, driven by an immutable spec sealed before spawn. The agent is the command you registered, launched exactly as declared — no command, argv, or environment ever comes from the wire, and the registry is never re-read while serving.

Back over the same socket come normalized seq-ordered events, a supervisor-owned status, and redacted local artifacts.

Two protocols, two version lines — neither implies the other. Downstream, ARS speaks ACP Protocol v1 over stdio JSON-RPC to the agent. Upstream, your application speaks the ARS-owned arsd API v3: every request envelope your client sends carries api_version, and anything other than 3 is refused rather than guessed — for every operation, including server_info. Result and error frames carry the correlating request_id, not a version. There is no drain window, dual protocol, or alias: v3 is a clean cutover.

Runs terminate; Sessions do not close. A submit without session_id atomically creates one durable Session and runs its first Run; a submit carrying session_id reuses that existing Session and can never fall back to creating one. A Session has no open/active/closed state, no one-shot variant, and no close operation — a Run reaching a terminal never ends it. What can stop reuse is narrower and honest: a live lease means one Run at a time, and quarantine is durable evidence that continuity was proven unsafe.

Replayed history is not this Run's evidence. An agent that replays a whole conversation while serving session/load has every replayed frame validated against the expected Session identity first, and then separated from the current Run: replay contributes no per-event execution evidence, no permission mediation accounting, no tool-call closure, and no final_message, so a tool call that completed in an earlier Run is never charged to this Run's frozen grant. A Run that observed replay keeps exactly one bounded session_replay_summary event, carrying aggregate counts and no replayed content.

Current-Run evidence is admitted through a bounded serial ledger. An accepted event takes its real seq and its final NDJSON line at acceptance, so the count and the exact bytes charged are the ones actually written, and both stay charged until the durable append returns. Event-count and byte ceilings are enforced independently — 1024 → 8192 events alongside 8 → 64 MiB — and expand only while the persisting sink keeps making durable progress. A temporarily full ledger gives each accepted event one absolute FIFO deadline, so a stalled or failed evidence sink is never hidden by a bigger buffer: it fails closed, and the Run reports a bounded EVIDENCE_PIPELINE failure.

Requirements

Need Requirement
Runtime Python ≥ 3.11, with zero third-party runtime dependencies.
Driving a real agent The native extra, pinning the official ACP client agent-client-protocol==0.12.1. A base install imports fine and fails only when the SDK is used. ARS is stdio ACP only and never installs the SDK's http extra.
Running arsd Linux with a POSIX user session for the AF_UNIX socket, plus a supervisor root, an agents file, and at least one caller mapping you supply. Crash containment additionally expects a user-level service manager cgroup and a CPython build with pidfd support.
Running an agent The agent installed by you, plus one registry entry naming its command.

Install and quick start

pip install 'agent-run-supervisor[native]'      # or: uv pip install 'agent-run-supervisor[native]'

Since the runtime is standard library only, a source checkout runs without installing anything:

git clone https://github.com/jovijovi/agent-run-supervisor.git
cd agent-run-supervisor
PYTHONPATH=src python3 -m agent_run_supervisor --help
# write a small agents file first (next section), then check it — read-only
PYTHONPATH=src python3 -m agent_run_supervisor agents validate --agents-file <your-agents.toml>

For the test suite and dev tooling: pip install -e '.[dev,native]', or make sync with uv. Nothing here launches an agent implicitly: agents validate, run inspect, and --print-service-unit are read-only. agents doctor is the one diagnostic that does start an external child, which writes its own agent-owned state; it is reaped on every path, and a group surviving SIGTERM and SIGKILL is reported as a failed probe rather than left running.

The agent registry

--agents-file points at one operator-owned TOML file, read exactly once at daemon startup into an immutable in-memory snapshot. Replace it atomically; a replacement takes effect at the next daemon start.

schema_version = 1

[agents.native-agent]                              # table key = the agent_id a caller names
profile         = "standard-native-acp-v1"         # how to speak ACP; not an agent list
command         = "some-agent"                     # bare name → PATH, or an absolute path
args            = ["acp"]
mediation       = "ask-privileged-tool-families-v1"  # selects a source-owned binding
env_passthrough = ["SSH_AUTH_SOCK", "SOME_PROVIDER_TOKEN"]
env_overlay     = { SOME_AGENT_HOME = "/home/<service-user>/.some-agent" }
forbidden_capabilities = ["terminal"]

Every value above is a placeholder. The closed field set is profile, command, args, mediation, env_passthrough, env_overlay, model_selector, effort_selector, forbidden_capabilities, and session_epoch — nothing else, and an unknown key at any level is refused. For an agent whose CLI is not natively ACP, point command at the ACP adapter you installed. The adapter executable remains an operator-owned deployment fact. Select standard-native-acp-v1 only when the ACP behavior it exposes conforms to that contract; an evidenced ACP-semantic deviation selects the corresponding source-owned compatibility profile. Four compatibility profiles differ from the standard contract: claude-agent-acp-compat-v1 carries its ACP-level compatibility differences. codex-agent-acp-compat-v1 keeps separate model and effort selectors while deriving Codex's ACP mode from each Run's frozen grant: any subset of {read, search}, including empty, requires read-only; every other valid grant requires agent, and the policy never selects agent-full-access. ARS sets the mode and reads it back exactly before the model, then configures model and effort and re-proves it once at the post-effort readback before any prompt. The mode is recomputed on every Run, including session/load. cursor-native-acp-v1 deviates twice. It uses model-only configuration fidelity — its model selector is the whole configuration, with no separate effort selector — and, at revision 3, it drives Cursor's own ACP mode from the Run's frozen grant: a Run whose grant capabilities are exactly a subset of {read, search} must run in ask mode, every other valid grant runs in agent mode, and the mode is set and exact-read-back before the model and re-proven after it, failing before any prompt otherwise. That mode selection is a cooperative mitigation of an agent that can complete an edit in agent mode without asking — it is not an OS sandbox and not a strong permission guarantee, and ACP permission mediation plus the post-completion violation detector stay the enforcement line. Like every other profile, it adds no startup permission policy and never repoints your agent's own configuration root, so agent-owned Session state stays where the agent put it and resumes through a real session/load. reasonix-agent-acp-compat-v1 keeps separate model and effort selectors and sets tool_approval=ask, with exact readback, before model and effort on every new or loaded Session. It never automatically selects Reasonix auto, yolo, or work_mode. standard-native-acp-v1 behaves ordinarily.

A concrete operator example for the installed OMP and Reasonix commands is examples/agents.omp-reasonix.toml. OMP uses the standard profile with its thinking effort selector; Reasonix uses the compatibility profile above. ARS does not install either command or write an operator's live registry.

  • Your command is launched exactly as declared. argv[0] is the declared string byte-for-byte, and a bare name is found by ordinary PATH lookup over the child's projected PATH, so shims, symlink farms, and agent self-update keep working. There is no pre-flight resolution check; a failed exec reports COMMAND_NOT_FOUND, COMMAND_NOT_EXECUTABLE, or SPAWN_FAILED — configuration errors, not security refusals.
  • PATH is the usual cause of "works in my shell, fails under ARS." A user daemon inherits a minimal environment, so declare what your agent needs. SSH_AUTH_SOCK is opt-in on purpose: forwarding it hands the agent live use of your SSH keys.
  • Read-once has a price and a payoff. A registry edit costs one daemon restart; an agent upgrade behind an unchanged registered command costs nothing at all, and existing sessions still resume through a real session/load.

ARS checks its own configuration file and nothing beyond it: the resolved agents file must be a regular file that is not group- or world-writable, and there is no ownership, mode, ancestor, symlink, or digest check on command or on anything the agent later loads. Full contract — grammar, bounds, refusal codes, environment layers, session_epoch, honest limits: docs/design/agent-registry.md.

Run arsd

Operator commands are on the agent-run-supervisor script; the daemon is a module entry point. In order:

# 1. check the file offline — no side effects
agent-run-supervisor agents validate --agents-file <agents-file>
# 2. per-agent diagnostics; without --no-probe this starts the registered command
agent-run-supervisor agents doctor --agents-file <agents-file> --agent <agent-id>
# 3. render a user-scope systemd unit to stdout — pure text, installs nothing
python3 -m agent_run_supervisor.arsd --agents-file /absolute/path/to/agents.toml --print-service-unit
# 4. start the daemon
python3 -m agent_run_supervisor.arsd \
  --supervisor-root <supervisor-root> \
  --agents-file /absolute/path/to/agents.toml \
  --caller-mapping <UID>:<principal_id>:<owner>:<namespace>

Between steps 2 and 3, run the mandatory denied-action mediation canary for each agent: mediation is cooperative-agent policy, not an OS sandbox, and zero permission events prove nothing about denial.

--agents-file must be an absolute path in both --print-service-unit and daemon mode — the same check runs either way, so a rendered unit can never carry a path the daemon would reject. Daemon mode additionally requires --supervisor-root and at least one --caller-mappingzero mappings refuse to listen — and refuses to run as root. --socket defaults to $XDG_RUNTIME_DIR/agent-run-supervisor/arsd.sock, falling back to <supervisor-root>/arsd/arsd.sock. Caller mappings, socket paths, and the registry path are deployment values: keep them in a mode-0600 unit file, never in a repository. On restart the daemon reconciles durable facts only and never re-prompts — and is stricter than a tolerant reader: a corrupt terminal record, unattributable uncertainty, or a launch record without its spec each refuse to listen, and a run that may have been dispatched without a trustworthy terminal result ends unknown / quarantined / retryable=false.

Use from Python

ArsdClient is the supported caller boundary: explicitly connected, context-managed, never silently reconnecting, never replaying a request.

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

    # Placeholders. Replace owner/namespace/agent, model/effort, the grant and
    # policy hashes, and the input refs with values from your own admission and
    # grant pipeline; a configured daemon will refuse these as-is.
    ack = client.submit(                      # caller-owned request_id = idempotency key
        request_id="my-caller-request-id",
        payload={
            "request": {
                "owner": "my-team",
                "namespace": "my-team/docs",
                "agent_id": "native-agent",           # an agent_id in your registry
                # "session_id": "<a Session id>",     # OMIT it to create a new
                                                      # durable Session; name one
                                                      # to reuse it, existing-only.
                                                      # A present null is refused:
                                                      # absent and null are not
                                                      # the same statement.
                "expected_binding_hash": None,
                "input_refs": [
                    {"ref": "prompt:inline", "content_hash": "sha256:" + "a" * 64},
                ],
                "requested_model": "<model-the-agent-advertises>",
                "requested_effort": "<effort-the-agent-advertises>",
                "grant_ref": "grant:my-caller-grant-1",
                "grant_hash": "sha256:" + "b" * 64,
                "grant_role_hash": "sha256:" + "c" * 64,
                "grant_capabilities": ["read"],
                "mcp_snapshot_hashes": [],
                "credential_refs": [],
                "limits": {},                         # {} takes the sealed defaults
                "evidence_policy_hash": "sha256:" + "d" * 64,
                "recovery_policy_hash": "sha256:" + "e" * 64,
            },
            "prompt_text": "Summarize the diff in plain language.",
            "workspace_root": "/path/to/bound/workspace",
        },
    )
    run_id = ack["run_id"]                    # ack is {"run_id": ..., "accepted_at": ...}

    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

    with client.run_events(run_id, follow=True) as stream:   # live event frames
        for frame in stream:
            ...

The terminal result carries the session_id this Run used — the one to pass back on the next submit. session_list() and session_status(id) round out the surface, both owner-scoped, and both project identity, last-use observations, and optional quarantine evidence rather than any lifecycle state. agent_list() is the one read-only roster query: it returns the complete {"agent_ids": [...]} object — the unique, stable-sorted canonical ids in the immutable registry snapshot this daemon loaded at startup, and nothing else about an entry. A request never re-reads the agents file, so an edit takes effect at the next daemon start; and registration is not health, authorization, or execution eligibility, because submit remains the admission boundary. The request key set above is closed and complete: unknown keys are refused, and there is no shell text, argv, environment value, executable path, or credential material on it — those fields do not exist on the wire, and credential_refs are references ARS never resolves to values. Errors are typed and fail closed: exceptions carry a stable code such as PEER_UID_DENIED, OWNER_MISMATCH, IDEMPOTENCY_CONFLICT, or CAPACITY_EXHAUSTED, and server-side text is never echoed back into one.

Guarantees and boundaries

ARS guarantees ARS does not claim
default-deny mediation against the caller's frozen grant, with redacted evidence for every decision; the mediation environment binding is source-owned in key and value, applied last, and never authored or disabled by an entry a sandbox. This is cooperative-agent policy, not OS isolation: the agent runs as the daemon's UID with that UID's full authority
no projected environment value serialized out of the resolved carrier into structured launch/Spec/environment material, any hash input, or a configuration-inspection response — not a digest, fingerprint, or length of one; the carrier goes to exec and nowhere else that an environment value never appears in Run evidence. ARS does not scan free-form agent text against the values it projected, so an agent that echoes one back may have it retained in bounded Run/Session evidence unless static credential-shape or sensitive-key redaction catches it
deterministic redacted artifacts: 0700 directories, 0600 files, atomic final writes, and exactly two writable surfaces — the supervisor root and the socket path integrity or supply-chain verification. ARS does not check that the executable it launched is the one you intended or came from a trusted publisher
termination of its direct child and every descendant still in the process group ARS created a complete kill switch. A descendant that leaves the group is outside it — and when work continues elsewhere, the run fails loudly as unknown / quarantined
fail-closed uncertainty: no auto-retry, replay, or resume of a prompt that may already have been dispatched, and no unquarantine tool crash containment by itself. Production expects a user-level service manager cgroup (Restart=on-failure, KillMode=control-group)
technical supervision facts only — business_verdict is always null and belongs to the caller credential management, ingress, gateway, or chat integration. Agents authenticate through their own stores under their own HOME

Real isolation belongs at the OS layer — dedicated UID, user namespaces, seccomp/Landlock, bwrap/container/VM boundaries, cgroup limits — and composes here: register the wrapper as the command.

Documentation

Read For
docs/design/agent-registry.md the operator contract: grammar, bounds, refusal codes, environment layers, restart semantics
docs/design/architecture.md system shape, the four authority layers, reconciliation, storage
docs/design/result-event-schema.md the caller-stable JSON shapes ARS emits
docs/roadmap/current-status.md where the project actually is, and what is not approved

Development

make sync      # uv sync --locked --extra dev --extra release --extra native
make verify    # the single local gate — identical to CI
make build     # sdist/wheel + twine check
make help      # list all targets

Without Make: uv sync --locked --extra dev --extra release --extra native then ./scripts/verify_local.sh; without uv: pip install -e '.[dev,release,native]' then python3 -m pytest -q. make verify runs tests, read-only CLI smoke, docs index checks, a static safety scan, and package checks — see docs/roadmap/verification.md. The suite drives the Native ACP core and arsd against a hermetic fake agent over temporary sockets; suites needing a real agent runtime are opt-in and never run in CI.

Contributing

Issues and pull requests are welcome. Documentation precedes code here, so read the authority chain first: GOAL.mddocs/product/prd.mddocs/design/docs/roadmap/, where non-approvals.md records what is explicitly out of scope; anything under docs/archive/ is cold history, never current authority.

Branch from main with a short-lived feat/ · fix/ · docs/ · cicd/ branch, write the test first, keep the runtime standard-library only, make make verify green before opening a PR, use Conventional Commits, and never commit secrets — no keys, tokens, real UID mappings, socket paths, or other deployment values; use [REDACTED] in docs and examples. Full process: docs/AI_FLOW.md.

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.7.8.tar.gz (288.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.7.8-py3-none-any.whl (242.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for agent_run_supervisor-0.7.8.tar.gz
Algorithm Hash digest
SHA256 6099c28cd7edd30d24da4113dd179d5bd9e1f9725eccaf6ef808a752f0c136d9
MD5 2e0fc6a1541398dce381c16c89291b55
BLAKE2b-256 fc76f4500f8e92f297fdb7f93a8a1838a3041cd6e245c63dcc6b734119964036

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_run_supervisor-0.7.8.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.7.8-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_run_supervisor-0.7.8-py3-none-any.whl
Algorithm Hash digest
SHA256 5b02460acfb78f0ac359ed90f20d827946a7c5ae475010e6e1ffce239e1caf2e
MD5 7fbd31a3e7ab7415387e4f2b28e66302
BLAKE2b-256 42aca1da12d7a0a7770ef2f0601cedf15bb05f41b02ec98370f5fd88fc8ffcfa

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_run_supervisor-0.7.8-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

This release

0.7.8 This release

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

0.5.1

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