@productfactory/agent-supervisor
v0.3.0
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:
sessionIdmodelIdmcpServersrepoDir
Environment
Optional runtime environment variables:
CODING_AGENT_ACP_COMMANDCLAUDE_ACP_COMMAND
By default the supervisor launches:
npx -y @agentclientprotocol/claude-agent-acpCallback 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.
