146 lines
6.7 KiB
Markdown
146 lines
6.7 KiB
Markdown
# Local runner Local Protocol and Supervision Reference
|
|
|
|
## Scope
|
|
|
|
Local runner proves one native local session. It uses no Paperclip service, database,
|
|
network provider, or model. The TypeScript mock core, Rust runner, and Rust fake
|
|
harness are all inside `packages/paperclip-runner/`.
|
|
|
|
## Processes and transports
|
|
|
|
| Boundary | Transport | Input | Output |
|
|
|---|---|---|---|
|
|
| Mock core → runner | stdin JSONL | `paperclip.prp.command.v1` | — |
|
|
| Runner → mock core | stdout JSONL | — | `paperclip.runner.stream.v1` |
|
|
| Runner → fake harness | stdin JSONL | `paperclip.fake_harness.command.v1` | — |
|
|
| Fake harness → runner | stdout JSONL | — | `paperclip.fake_harness.message.v1` |
|
|
| Mock core → browser | local HTTP + NDJSON | start/actions | validated events and final trace |
|
|
|
|
Stderr is diagnostic input. It never becomes a command or canonical event.
|
|
|
|
## Deterministic lifecycle
|
|
|
|
1. `run.prepare` creates the harness process group.
|
|
2. The harness sends `ready`.
|
|
3. `session.open` starts one fake driver session.
|
|
4. `turn.start` starts one scripted turn.
|
|
5. The harness emits typed events, requests, logs, a semantic result, and a
|
|
terminal proposal.
|
|
6. The runner waits for the harness process and records `harness.exited`.
|
|
7. The runner emits one `run.terminal` event and exits zero.
|
|
|
|
The runner gives each canonical event a deterministic source ID, source
|
|
sequence, and timestamp. Script delay can be overridden for fast tests.
|
|
|
|
## Command rules
|
|
|
|
- `controllerSeq` starts at 1 and is contiguous.
|
|
- The runner stores the canonical JSON for each accepted `commandId`.
|
|
- The same ID and same content returns `duplicate` without repeating an effect.
|
|
- The same ID with different content is rejected.
|
|
- A sequence gap is rejected.
|
|
- Local runner supports prepare, open, turn start, request resolution, interruption,
|
|
stop, close, and cancel command shapes. A script consumes only the commands it
|
|
expects.
|
|
|
|
## Fake-driver scripts
|
|
|
|
Scripts live under `protocol/fixtures/local-runner/scripts/`.
|
|
|
|
| Script | Proof |
|
|
|---|---|
|
|
| `happy-path` | Lifecycle, tool/command item, file item, logs, result, and success terminal |
|
|
| `permission-input` | Permission request and free-text input round-trip |
|
|
| `interrupted` | Operator interruption, yielded result, exit 130, and cancelled terminal |
|
|
| `error` | Non-zero process exit stays separate from the yielded semantic result |
|
|
| `duplicate-terminal` | A second terminal proposal is ignored and diagnosed |
|
|
| `linger` | Supervisor terminates the harness and its worker process group |
|
|
|
|
## Cleanup and credential boundary
|
|
|
|
The runner starts the harness in a new process group on Unix. Normal exit waits
|
|
for the process. Controller EOF or explicit cleanup sends `TERM`, waits for the
|
|
configured grace time, and then sends `KILL` to the full group. Drop cleanup is
|
|
a final guard.
|
|
|
|
The mock core passes only a small process environment. The runner also clears
|
|
the inherited environment before it starts the fake harness. Paperclip keys,
|
|
model keys, and AgentMail keys are not forwarded.
|
|
|
|
## Bounded diagnostics
|
|
|
|
The supervisor frames harness stdout and stderr with a 64 KiB per-line default
|
|
before it creates a `String`; a newline-free or otherwise oversized stdout
|
|
frame is a protocol violation and fails the run. Oversized stderr frames are
|
|
drained with bounded memory and represented by a short diagnostic marker. The
|
|
separate log-tail buffer then keeps only the configured number of lines and
|
|
bytes. It reports retained lines, retained bytes, and the dropped-line count
|
|
with `harness.exited`.
|
|
|
|
## Result and terminal authority
|
|
|
|
These facts are independent:
|
|
|
|
- `run.result.proposed` is the fake driver's structured work claim;
|
|
- `harness.exited` is the operating-system process fact;
|
|
- `run.terminal` is the one canonical terminal event for the local trace.
|
|
|
|
A scripted error can report useful yielded work and exit 7. An interruption can
|
|
report yielded work and exit 130. Neither process fact silently replaces the
|
|
semantic result.
|
|
|
|
The fake harness terminal object is only a proposal. The runner preserves it as
|
|
`harnessTerminalProposal`, validates its enum fields, and derives the canonical
|
|
turn/run terminal states from runner-owned facts: the process exit, whether a
|
|
semantic result was observed, an accepted interrupt/cancel command, controller
|
|
closure, and protocol violations. `reconciliation` records the decision and
|
|
whether the proposal contradicted those facts. A success proposal followed by a
|
|
non-zero exit therefore becomes a failed canonical terminal; the reverse
|
|
contradiction becomes succeeded only when both the process and semantic result
|
|
succeeded.
|
|
|
|
## Browser live mode
|
|
|
|
The Vite middleware starts the same TypeScript mock-core controller used by
|
|
tests. It streams history plus new events as NDJSON. The browser validates each
|
|
event with the replay schema, checks run/session binding, and applies the
|
|
session reducer. At completion it replays the full event list and compares both final
|
|
snapshots.
|
|
|
|
The browser reuses package-local shadcn-style Button, Badge, Card, and Textarea
|
|
components. All visual values remain in `styles.css`.
|
|
|
|
### Local transport trust model
|
|
|
|
The browser server is a developer-only loopback transport, not an authenticated
|
|
Paperclip API. Vite dev and preview default to `127.0.0.1`. Every
|
|
`/api/localRunner/*` request is checked before route lookup or process startup:
|
|
|
|
- both ends of the accepted socket and the HTTP `Host` must be loopback;
|
|
- an `Origin`, when a browser supplies one, must exactly match the request
|
|
origin, and cross-site Fetch Metadata values are rejected;
|
|
- JSON actions require `application/json` and bodies are capped at 16 KiB,
|
|
including streamed/chunked requests;
|
|
- the server permits at most 4 active runs, retains at most 16 total runs (old
|
|
completed entries are evicted), and permits at most 8 live stream clients per
|
|
run.
|
|
|
|
These are secure defaults plus defense in depth: even if Vite is accidentally
|
|
started with a broad `--host`, non-loopback Local runner API traffic is rejected.
|
|
There is intentionally no user identity, session cookie, or reusable bearer
|
|
credential in this spike.
|
|
|
|
Residual risk: another process running as the same local user is inside this
|
|
trust boundary and can make loopback requests. The scripted Local runner event set is
|
|
finite, but the retained event history inside one active run is not yet a
|
|
general-purpose per-session byte budget. A later real harness/network phase must
|
|
add authenticated capabilities, per-session event/byte budgets, durable
|
|
backpressure, and production authorization instead of widening this local
|
|
transport.
|
|
|
|
## Deferred work
|
|
|
|
Local runner has no durable outbox, ACK, reconnect, runner restart recovery, real
|
|
harness, production Paperclip bridge, or browser-to-runner connection. Those
|
|
items require later authorized phases.
|