@productfactory/agent-supervisor
v0.5.5
Published
A runtime supervisor for ACP-based coding-agent prompts running inside a sandboxed environment.
Readme
@productfactory/agent-supervisor
A runtime supervisor for ACP-based coding-agent prompts running inside a sandboxed environment.
The supervisor launches a single ACP prompt, auto-approves tool permission prompts, streams raw session notifications back to a caller-supplied HTTP callback, emits heartbeats, and exits when the prompt finishes.
Run
The supervisor reads its config from FACTORY_SUPERVISOR_CONFIG_PATH:
FACTORY_SUPERVISOR_CONFIG_PATH=/path/to/config.json factory-agent-supervisorConfig
Required fields in the JSON config:
runIdtaskIdsandboxIdcallbackUrltokenpromptIdprompt
Optional fields:
agentTarget("claude"or"codex", default"claude")sessionIdmodelIdmcpServersrepoDir
Environment
Optional runtime environment variables:
CODING_AGENT_ACP_COMMANDCLAUDE_ACP_COMMANDCODEX_ACP_COMMAND
By default the supervisor launches the bundled @agentclientprotocol/claude-agent-acp
dependency through the current Node executable. The resolved command is equivalent
to:
node ./node_modules/@agentclientprotocol/claude-agent-acp/dist/index.jsThe ACP agent package is a pinned dependency intentionally. This keeps prompt
runtime off the npm registry and makes ACP behavior part of the supervisor
release. To upgrade it, check the @agentclientprotocol/claude-agent-acp
changelog, update the dependency version, update @agentclientprotocol/sdk if
the agent depends on a newer SDK, and run pnpm typecheck, pnpm test, and
pnpm build.
Claude remains the default unless agentTarget is explicitly set. Future Codex
support is dormant behind agentTarget: "codex", which launches the bundled
@zed-industries/codex-acp adapter instead. The resolved command is equivalent
to:
node ./node_modules/@zed-industries/codex-acp/bin/codex-acp.jsCODING_AGENT_ACP_COMMAND overrides every target. CLAUDE_ACP_COMMAND and
CODEX_ACP_COMMAND override only their matching target.
Other ACP adapters are not bundled yet. If an adapter needs a different runtime
or process lifecycle, run it through CODING_AGENT_ACP_COMMAND for experiments
until sandbox compatibility and cleanup behavior are proven.
Callback contract (0.3.0)
Every POST to callbackUrl is HMAC-SHA256 signed over
`${timestamp}.${nonce}.${body}` with token as the key. Three
supervisor headers carry the proof alongside a Bearer for identification:
authorization: Bearer <token>x-supervisor-timestamp: <unix ms>— callers should enforce a ±5min window.x-supervisor-nonce: <uuid>— unique per request; callers should reject reuse.x-supervisor-signature: sha256=<hex>— the HMAC over the raw body.
For every ACP sessionUpdate callback, the supervisor pushes the raw
SessionNotification onto a batch buffer. The buffer flushes on:
- 750ms elapsed since the first notification in the batch (fixed window), or
- 50 notifications accumulated, or
- ~1MB of estimated serialized size, or
- any terminal report (
prompt_complete,prompt_failed,destroy()).
Each flush is one report with payload shape:
{
"kind": "session_update",
"runId": "...", "taskId": "...", "sandboxId": "...",
"promptId": "...", "sequence": 42, "sessionId": "...",
"notifications": [/* raw SessionNotification[], in arrival order */],
"snapshot": {/* cumulative state -- a recovery backstop, not the source of truth */}
}Callers should render UI off notifications[] and treat snapshot as the
reconciliation layer for dropped or out-of-order callbacks.
Parity invariant
For any given prompt, the notifications emitted by the supervisor —
their order, sessionUpdate kind, and update payload — are identical
to those a caller would see from an in-process ACP loop.
snapshot is the cumulative view of the session: at-notification in
in-process, end-of-batch in supervisor mode. Callers that render off
notifications[] get exact parity; callers that render off snapshot
see monotonically-advancing state with per-batch granularity (typically
sub-second).
Across report boundaries, retry-induced reorder can drop a late report;
the next report's snapshot reconciles state.
Heartbeats and sequence
Heartbeats fire outside the sequenced chain and always carry
sequence: 0. Callers must exempt kind === "heartbeat" from any
monotonic-sequence check and must not advance lastSequence from a
heartbeat. This prevents a wire-reorder race where a heartbeat lands
ahead of an in-flight chained report and steals its sequence slot.
Deferred: permission requests
ACP's requestPermission is a request/response handler, not a
SessionNotification, so it does not flow through notifications[]. If the
ACP agent is launched with --dangerously-skip-permissions (the current
default for this supervisor's callers), the handler never fires. A
bidirectional permission channel is a candidate for a future version if that
flag is disabled.
Testing
pnpm --filter @productfactory/agent-supervisor testThe suite covers signing, batching/flushing, terminal flush resilience,
401-as-terminal, retries, and adversarial edge cases. Every change to
reporter.ts must keep the suite green.
