269 lines
14 KiB
Markdown
269 lines
14 KiB
Markdown
# PRP Compatibility and Versioning Policy
|
|
|
|
## Authority
|
|
|
|
The JSON Schema files in [`protocol/schemas/`](../protocol/schemas/) are the
|
|
language-neutral source of truth for the Replay and Local runner executable contract. The
|
|
generated TypeScript schema module is checked against those files before every
|
|
TypeScript typecheck. Rust consumes the same fixtures and must produce the same
|
|
golden parity summaries.
|
|
|
|
The reviewable architecture and trust boundaries are defined in
|
|
[`architecture.md`](./architecture.md); durable transport and recovery behavior
|
|
is defined in [`durable-recovery.md`](./durable-recovery.md). Local runner reuses
|
|
the protocol contract for local live events. It adds package-local stdio and
|
|
stream envelopes, but it does not add durable transport, persistence, or
|
|
production control-plane behavior.
|
|
|
|
## Version fields
|
|
|
|
| Field | Replay support | Compatibility rule |
|
|
|---|---:|---|
|
|
| `protocolVersion` | `1` | Required. Negotiate the highest overlapping version; no overlap fails closed. |
|
|
| `fixtureVersion` | `1` | Required by the conformance corpus. Unknown values fail closed. |
|
|
| `event.schemaVersion` | `1` | Required on every event. Unknown values fail closed before reduction. |
|
|
| `capabilities.semanticTools.schemaVersion` | `1` | Optional advertisement. When present, an unknown required version fails closed. |
|
|
| `payload.semantic_tool.schemaVersion` | `1` | Optional on paired semantic tool input/result events. When present, an unknown required version fails closed. |
|
|
| `terminal.stopReason.schemaVersion` | `1` | Optional budget/cost receipt. When present, an unknown required version fails closed. |
|
|
| Typed `schema` discriminators | `*.v1` | Required. Unknown required schema identities fail JSON Schema validation. |
|
|
|
|
Wire protocol versions and fixture-corpus versions are independent. A fixture
|
|
format can evolve without changing PRP, and a future PRP version can be
|
|
represented only after the consumer advertises support for it.
|
|
|
|
## Forward compatibility
|
|
|
|
- Unknown object properties are accepted and preserved by validation. Reducers
|
|
ignore fields they do not understand until a later schema version gives those
|
|
fields defined behavior.
|
|
- Unknown required versions, schema discriminators, enum values, and required
|
|
fields fail closed. A consumer must never guess at their semantics.
|
|
- Scripted fixtures bind every event to the fixture run/session, require
|
|
contiguous controller command order, exactly one unique proposed result, and
|
|
exactly one unique terminal event.
|
|
- The top-level fixture result must equal the `run.result.proposed` payload after
|
|
canonical key ordering. Repeated `sourceEventId` deliveries must be
|
|
byte-equivalent after the same normalization.
|
|
|
|
The forward-compatibility fixture proves that optional fields survive validation
|
|
without changing the v1 snapshot. The unsupported-version fixture proves that a
|
|
required v2 protocol cannot be replayed by this consumer.
|
|
|
|
## Within-turn checklist snapshots
|
|
|
|
`plan.updated` / `paperclip.plan.updated.v1` is a complete, ordered snapshot of
|
|
the provider's checklist for one active turn. It is not a Paperclip Plan
|
|
document and must never be inferred from assistant prose, Codex proposed-plan
|
|
items, or generic TodoWrite output. Every replacement uses the provider turn ID
|
|
as `planId`; PRP `sourceSeq`, not an optional provider revision, determines
|
|
snapshot order. An empty step array clears the checklist, and `complete` is true
|
|
only when a non-empty snapshot contains only `completed` steps. The legacy
|
|
document-coupling fields are always `syncStatus: "not_applicable"` and
|
|
`documentRevision: null`.
|
|
|
|
| Qualified adapter profile | Checklist support |
|
|
|---|---|
|
|
| Direct Codex App Server | `turn/plan/updated` |
|
|
| ACPX Codex | Structured ACP `plan` entries |
|
|
| ACPX Claude | Structured ACP `plan` entries |
|
|
| ACPX Pi | Unavailable; no production-qualified profile is exposed |
|
|
| OpenCode | Unsupported until it exposes a structured plan event |
|
|
|
|
Codex `turn/diff/updated` is normalized separately as the latest same-turn
|
|
`workspace.change.updated` snapshot. Together these two independent event
|
|
families can drive a turn-status UI without changing the public PRP family or
|
|
adding a control-plane endpoint.
|
|
|
|
## Provider-neutral semantic receipts
|
|
|
|
- `capabilities.semanticTools` advertises stable operation IDs, availability,
|
|
required claims, and redaction disposition without naming a provider API.
|
|
- `mcp_app.tool_input` and `mcp_app.tool_result` may carry paired
|
|
`semantic_tool` envelopes. Correlation IDs must match the containing event;
|
|
operation ID and idempotency key must match across the pair.
|
|
- Content is represented by a canonical SHA-256 digest plus allowlisted typed
|
|
references. Raw credentials, provider payloads, and hidden identifiers do not
|
|
belong on the wire.
|
|
- Result receipts distinguish success, denial, conflict, exact duplicate,
|
|
unavailable, and failure. They can name the authorization boundary, safe
|
|
revision, artifact/work-product refs, immutable governed targets, and bounded
|
|
wake/monitor causality.
|
|
- `terminal.stopReason` records budget/cost kind, stable code, retryability,
|
|
limit class, safe aggregate, and decision receipt.
|
|
|
|
These fields are trace evidence only. The v1 reducer ignores `semantic_tool`
|
|
payloads, so adding or extending the optional envelope has no projection
|
|
effect. Eval `trace_completeness` treats PRP wire receipts as authoritative when
|
|
present and retains the pre-existing scalar fallback for live evidence that has
|
|
not yet emitted them.
|
|
|
|
## Provider-neutral structured input
|
|
|
|
Harness-initiated forms cross PRP as `paperclip.runtime_request.v2` with
|
|
`requestKind: "runtime"`, `type: "input"`, and an embedded
|
|
`paperclip.question_set.v1`. A submission is always
|
|
`{ "action": "submit", "response": paperclip.question_response.v1 }`.
|
|
Codex answer objects, OpenCode answer arrays, and ACP typed content exist only
|
|
inside their adapters; `origin` may retain the provider method and adapter name
|
|
for diagnostics but never provider response data.
|
|
|
|
Question and option order is significant, while answers are keyed by stable
|
|
question IDs and selections reference stable option IDs. The canonical modes
|
|
are `text`, `single_select`, and `multi_select`. Text validation is repeated at
|
|
the untrusted server edge and again against the persisted question set before a
|
|
provider receives the translated response.
|
|
|
|
Every harness adapter must add a
|
|
`paperclip.question_adapter_fixture.v1` fixture proving its native request
|
|
normalizes to the canonical shape and its canonical response can be translated
|
|
back. The shared fixture format deliberately contains both native and canonical
|
|
objects so adding a provider does not change PRP or the UI contract.
|
|
|
|
V1 runtime requests and their legacy resolutions remain accepted during the
|
|
migration. A request that contains no structured form stays on the legacy path;
|
|
once a provider supplies a form, malformed or unsupported fields fail closed
|
|
instead of silently degrading. ACPX sidecars advertise only form elicitation
|
|
and use sidecar protocol v2 `runtime.input_requested` / `input.resolve` frames.
|
|
|
|
The live lifecycle pauses and resumes the same provider turn. If the provider
|
|
process is lost first, Paperclip emits one non-replayable
|
|
`runtime_request.expired` fact and materializes an idempotent durable
|
|
`ask_user_questions` interaction using the identical question set. Explicit
|
|
cancellation and already-resolved requests never create that fallback.
|
|
|
|
## Native execution permission compatibility
|
|
|
|
`paperclip.native-execution-input.v4` pins the effective harness permission
|
|
policy in the closed provider configuration: `approvalPolicy` for Codex and
|
|
`permissionMode` for OpenCode and ACPX. The pinned value participates in
|
|
provider-session identity, so an incompatible idle or recovered session is
|
|
replaced on the next execution. An active turn is never mutated in place.
|
|
|
|
Persisted v1-v3 inputs remain replayable. Missing Codex and OpenCode policy
|
|
fields retain their historical effective behavior. Legacy ACPX
|
|
`permissionPolicy: "interactive"` is interpreted as `approve-reads`, while a
|
|
new v4 ACPX execution defaults to `approve-all` at the server boundary.
|
|
|
|
See [Adding a harness](adding-a-harness.md) for the permission catalog,
|
|
isolation rules, and provider conformance requirements.
|
|
|
|
Seven conformance fixtures cover artifact success, redacted denial without
|
|
fallback, stale conflict plus duplicate retry, governed target and continuation
|
|
causality, budget/cost stop, unknown optional fields, and rejection of an
|
|
unknown required version. The six accepted fixtures have shared TypeScript and
|
|
Rust golden parity summaries.
|
|
|
|
## Replay semantics
|
|
|
|
- Events are applied in fixture order and ordered independently by
|
|
`(sourceKind, sourceInstanceId, sourceSeq)`.
|
|
- A repeated source event ID has no second projection effect.
|
|
- A forward source-sequence gap is recorded explicitly; the reducer never
|
|
invents a missing event.
|
|
- An event at or behind the committed source cursor is ignored and recorded as
|
|
out of order.
|
|
- Replaying an already-applied batch leaves the snapshot unchanged.
|
|
|
|
The CLI and browser import the same `replayReplayFixtureText` function, so
|
|
validation, compatibility errors, and final snapshots cannot drift between the
|
|
two surfaces.
|
|
|
|
## Local envelope rules
|
|
|
|
- Mock-core commands use `paperclip.prp.command.v1` over stdin JSONL.
|
|
- Runner output uses `paperclip.runner.stream.v1` over stdout JSONL.
|
|
- Fake-harness commands use `paperclip.fake_harness.command.v1`.
|
|
- Fake-harness output uses `paperclip.fake_harness.message.v1`.
|
|
- An equivalent repeated `commandId` returns a duplicate receipt and has no
|
|
second driver effect. Reuse with different data is rejected.
|
|
- A new command must use the next contiguous `controllerSeq`.
|
|
- Harness logs are bounded diagnostic data. They are not canonical PRP events.
|
|
- `run.result.proposed`, `harness.exited`, and `run.terminal` are separate facts
|
|
and appear in that order when a semantic result exists.
|
|
- The live browser rejects an event with an invalid schema, run ID, or session
|
|
ID before it reaches the reducer.
|
|
|
|
These envelopes are local Local runner implementation contracts.
|
|
|
|
## Durable wire rules
|
|
|
|
- The runner opens loopback `ws://` or hostname-verified `wss://`, or accepts a
|
|
preview-proxy connection on its fixed listener, and completes the PRP v1
|
|
authenticated handshake before any command result or event.
|
|
- A one-use bootstrap bearer capability returns a short-lived connection lease
|
|
in `welcome`. Later connections use that lease. Neither raw capability is
|
|
durable state.
|
|
- `welcome.payload.connectionLeaseRenewalVersion: 1` opts into authenticated
|
|
`lease_renew` / `lease_renewed` control frames. Renewal extends the persisted
|
|
expiry on the same live authority without restarting provider work. Identity,
|
|
protocol, and revocation epoch remain fixed; expired or revoked leases cannot
|
|
renew. See [durable recovery](durable-recovery.md#execution-duration-and-operation-deadlines)
|
|
for retry and warm-handoff rules. Peers lacking this capability retain their
|
|
original lease expiry.
|
|
- `hello.resume` reports the last processed controller sequence, next source
|
|
sequence, cumulative ACK cursor, and current unacknowledged range.
|
|
- `welcome` selects the one overlapping protocol version, returns the core's
|
|
cumulative ACK cursor, and carries at most one durable pending command.
|
|
- An event is durable before send. Event IDs and source sequences stay stable
|
|
across replay and process restart.
|
|
- An ACK is cumulative. The runner rejects a cursor behind its durable ACK or
|
|
beyond its produced source cursor.
|
|
- An equal repeated command ID and canonical digest returns its stored result.
|
|
Reuse with different bytes fails closed and cannot repeat an effect.
|
|
- Frames are bounded at 1 MiB and upgrade headers at 16 KiB. Unknown or invalid
|
|
required protocol data fails closed; malformed JSON is a bounded diagnostic.
|
|
|
|
Runnerd build-metadata contract v2 advertises the exact transport inventory:
|
|
`dial_ws_loopback`, `dial_wss`, and `listen_ws`. Plaintext dial destinations
|
|
must resolve entirely to loopback. Public dial targets require TLS trust and
|
|
hostname validation; a private CA bundle augments the platform roots and must
|
|
be a bounded, private, regular file. Listener mode is fixed to port 43127 and a
|
|
single run-bound path. All modes retain the same message/frame bounds and PRP
|
|
authentication.
|
|
|
|
These are package-local Durable recovery and transport rules. Control-plane
|
|
admission and deployment policy remain separately reviewed work.
|
|
|
|
## Change policy
|
|
|
|
1. Change JSON Schema first.
|
|
2. Regenerate the TypeScript schema module.
|
|
3. Add or revise a shared fixture and its golden snapshot/summary.
|
|
4. Prove TypeScript and Rust parity.
|
|
5. Update this policy and the normative spike specification when behavior
|
|
changes.
|
|
|
|
Breaking changes require a new required version. Additive optional fields may
|
|
remain in v1 only when old consumers can safely ignore them.
|
|
|
|
## Package-level compatibility
|
|
|
|
PRP is one independently versioned component of the runner bundle. Catalog,
|
|
runner-client, control-plane-adapter, testkit, and eval-corpus compatibility is
|
|
declared by `PAPERCLIP_RUNNER_COMPATIBILITY` and checked before execution by
|
|
`assertPaperclipRunnerCompatibility`. A mismatch fails with
|
|
`paperclip_runner_incompatible` and stable per-issue codes; a provider-specific
|
|
tool error is not a compatibility negotiation mechanism.
|
|
|
|
See [ADR 0001](adr/0001-runner-testing-eval-package-boundaries.md) for the
|
|
component rules and clean-consumer packaging gate.
|
|
|
|
## Evals integration negotiation
|
|
|
|
The packed `./evals` entry point adds a stricter execution preflight for the
|
|
App/Evals join. `assertPaperclipRunnerEvalCompatibility` requires simultaneous
|
|
agreement on package semver, runnerd build metadata, a common PRP version,
|
|
semantic catalog version and SHA-256 digest, harness-driver contract and
|
|
required capabilities, and the native-execution version. It reports
|
|
`paperclip_runner_eval_incompatible` with expected/received values for every
|
|
mismatch and must run before launching a provider.
|
|
|
|
runnerd itself reports `paperclip-runner/runnerd-build-metadata/v1` from
|
|
`--build-metadata`. The consumer passes its path and expected content digest to
|
|
`resolvePaperclipRunnerdArtifact`; implicit PATH or source-tree discovery is
|
|
not part of the contract. Native attempt output is
|
|
`paperclip-runner/native-execution/v1`, whose parser accepts unknown additive
|
|
fields but rejects unknown required versions and inconsistent terminal,
|
|
semantic-denial, usage, or transcript facts. Full fields and the deterministic
|
|
gate are exercised by the package-local deterministic conformance suite.
|