paperclip/packages/paperclip-runner/docs/local-runner.md

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.