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. At revision 4 it negotiates Cursor's parameterized model picker (clientCapabilities._meta.parameterizedModelPicker = true) and uses parameterized configuration fidelity: the requested model string base[id=value,...] names the base model and every parameter the agent advertises for it (for example context, reasoning effort, and fast mode), each is set on its own advertised selector, and the whole configuration is read back exactly before any prompt; the requested effort is N/A. Since revision 3 it also 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-mapping — zero 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.md → docs/product/prd.md → docs/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.

Metadata

Release files for agent-run-supervisor 0.7.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agent-run-supervisor 0.7.9
File Size Uploaded
agent_run_supervisor-0.7.9.tar.gz 293.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for agent-run-supervisor 0.7.9
File Interpreter ABI Platform
agent_run_supervisor-0.7.9-py3-none-any.whl Python 3 none any Details

Total release size: 539.8 kB

Release files / agent_run_supervisor-0.7.9.tar.gz

Download URL agent_run_supervisor-0.7.9.tar.gz
Size 293.2 kB
Tags Source
SHA-256 checksum
How to use checksums
bf34a65ef97211b6539053b290158a80482fc6933dd74cdf980ac555eec055b0
BLAKE2b-256 checksum
How to use checksums
b342f4e39ab543f8b742a9c57a8e55ff527d8226b0b69e394c23dcbf9c6502cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / agent_run_supervisor-0.7.9-py3-none-any.whl

Download URL agent_run_supervisor-0.7.9-py3-none-any.whl
Size 246.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3179b07b69de1965a4e8d495c837423cbb3a4b9ed739aa1ad1b31b6de36be49
BLAKE2b-256 checksum
How to use checksums
d5c32e5d55f15165e71c0d724482eb1c7484eb18522570bd67e1465a21feacf0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.9 This release

2 release files

0.7.8

2 release files

0.7.7

2 release files

0.7.6

2 release files

0.7.5

2 release files

0.7.4

2 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.2.0

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release 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