English · 简体中文
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
trusted caller → arsd (local UDS) → ars-core / Native ACP → external AGENT process
Every hop is on one machine, under one unprivileged user:
arsdstarts: 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.- Your application connects to a
0600socket inside a0700directory — no TCP, no root, no public ingress.arsdauthenticates peer credentials, maps them to a principal, and keys admission on your ownrequest_id, which doubles as the idempotency key. Runs and sessions are owner-scoped. ars-coreruns the work: one in-processRunTaskowning 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 projectedPATH, so shims, symlink farms, and agent self-update keep working. There is no pre-flight resolution check; a failed exec reportsCOMMAND_NOT_FOUND,COMMAND_NOT_EXECUTABLE, orSPAWN_FAILED— configuration errors, not security refusals. PATHis 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_SOCKis 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6099c28cd7edd30d24da4113dd179d5bd9e1f9725eccaf6ef808a752f0c136d9
|
|
| MD5 |
2e0fc6a1541398dce381c16c89291b55
|
|
| BLAKE2b-256 |
fc76f4500f8e92f297fdb7f93a8a1838a3041cd6e245c63dcc6b734119964036
|
Provenance
The following attestation bundles were made for agent_run_supervisor-0.7.8.tar.gz:
Publisher:
release.yml on jovijovi/agent-run-supervisor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_run_supervisor-0.7.8.tar.gz -
Subject digest:
6099c28cd7edd30d24da4113dd179d5bd9e1f9725eccaf6ef808a752f0c136d9 - Sigstore transparency entry: 2559665889
- Sigstore integration time:
-
Permalink:
jovijovi/agent-run-supervisor@aacce7b81d3b05a46de1d1944870b6df1aa41892 -
Branch / Tag:
refs/tags/v0.7.8 - Owner: https://github.com/jovijovi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aacce7b81d3b05a46de1d1944870b6df1aa41892 -
Trigger Event:
push
-
Statement type:
File details
Details for the file agent_run_supervisor-0.7.8-py3-none-any.whl.
File metadata
- Download URL: agent_run_supervisor-0.7.8-py3-none-any.whl
- Upload date:
- Size: 242.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b02460acfb78f0ac359ed90f20d827946a7c5ae475010e6e1ffce239e1caf2e
|
|
| MD5 |
7fbd31a3e7ab7415387e4f2b28e66302
|
|
| BLAKE2b-256 |
42aca1da12d7a0a7770ef2f0601cedf15bb05f41b02ec98370f5fd88fc8ffcfa
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_run_supervisor-0.7.8-py3-none-any.whl -
Subject digest:
5b02460acfb78f0ac359ed90f20d827946a7c5ae475010e6e1ffce239e1caf2e - Sigstore transparency entry: 2559665995
- Sigstore integration time:
-
Permalink:
jovijovi/agent-run-supervisor@aacce7b81d3b05a46de1d1944870b6df1aa41892 -
Branch / Tag:
refs/tags/v0.7.8 - Owner: https://github.com/jovijovi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aacce7b81d3b05a46de1d1944870b6df1aa41892 -
Trigger Event:
push
-
Statement type: