mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +01:00
Co-Authored-By: Paperclip <noreply@paperclip.ing> * commit 'adb69e8f09852c772d9901ae270e5b2d66e39c77': (84 commits) feat: default new Codex agents to Paperclip Runner on Linux and macOS (#15683) docs(skills): add slow-query diagnosis and fix skill (#16303) feat(sentry): add opt-in run failure reporting with saved run references (#16300) refactor(server): isolate provider message admission and runtime policies (#16289) refactor(server): isolate provider webhook lifecycle decisions (#16283) fix(workspaces): keep cleanup Git scans fresh and dedupe page reads (#16302) fix(sentry): keep provider-rejected Grok wakes out of run failure reports (#16297) fix(runner): report Runner disconnect age when a Dot bridge exits (#16296) refactor(server): isolate chat conversation and control policies (#16276) perf(recovery): use indexed execution identity lookups (#16291) perf(runs): bound history and reduce repeated context reads (#16286) fix(native-runtime): report which close barrier failed (#16298) feat(skills): select and validate tracked GitHub branches (#16288) fix(observability): sample native runs independently of triggers (#16285) fix(github): retry and recover review publication (#16284) perf(workspaces): wake terminal cleanup from committed changes (#16282) fix(mcp): deduplicate discovered tool descriptors (#16281) perf: remove history scans from task and chat queries (#16278) docs(skills): add maintenance bot setup workflow (#16279) fix(observability): time execution configuration steps (#16236) ... # Conflicts: # .dockerignore # doc/plans/2026-10-06-default-agent-runner.md # maps/agent-access-map/agents.md # maps/agent-access-map/onboarding.md # maps/agent-access-map/runs-adapters.md # maps/feature-map/onboarding.md # maps/feature-map/runs-adapters.md # packages/adapter-utils/src/types.ts # packages/adapters/codex-local/src/index.ts # packages/paperclip-runner/acpx-profiles.json # packages/paperclip-runner/protocol/manifest.json # packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_profiles.rs # packages/paperclip-runner/scripts/generate-acpx-profiles.mjs # packages/paperclip-runner/src/contracts/native-execution.test.ts # packages/paperclip-runner/src/contracts/native-execution.ts # packages/paperclip-runner/src/drivers/acpx/copilot-installation.test.ts # packages/paperclip-runner/src/drivers/acpx/generated-profiles.ts # packages/paperclip-runner/test/fixtures/copilot-profile-v18-identity.json # packages/shared/src/agent-runner.ts # packages/shared/src/index.ts # server/src/__tests__/adapter-auth-signal-routes.test.ts # server/src/__tests__/agent-adapter-validation-routes.test.ts # server/src/__tests__/agent-skills-routes.test.ts # server/src/__tests__/agents-pending-approval-config.test.ts # server/src/__tests__/built-in-agents.test.ts # server/src/__tests__/company-portability.test.ts # server/src/__tests__/native-status-arbiter-corpus.test.ts # server/src/__tests__/onboarding-seed-route.test.ts # server/src/__tests__/plugin-managed-agents.test.ts # server/src/dev-native-runner-status.ts # server/src/lib/agent-records.ts # server/src/lib/agent-runner-platform.ts # server/src/routes/adapters.ts # server/src/routes/agents.ts # server/src/services/agent-hiring.ts # server/src/services/agent-runner-selection.test.ts # server/src/services/agent-runner-selection.ts # server/src/services/built-in-agents.ts # server/src/services/company-portability.ts # server/src/services/native-runtime/runtime-mode.test.ts # server/src/services/native-runtime/runtime-mode.ts # server/src/services/native-runtime/setup-readiness.test.ts # server/src/services/native-runtime/setup-readiness.ts # server/src/services/plugin-managed-agents.ts # tests/e2e/chat-adapters-ui-messaging.spec.ts # tests/runner-e2e/catalog.ts # tests/runner-e2e/first-task-fixtures.ts # tests/runner-e2e/first-task-flow.ts # tests/runner-e2e/first-task.test.ts # tests/runner-e2e/runner.spec.ts # tests/storybook-visual/storybook-visual.spec.ts # ui/src/adapters/adapter-display-registry.test.ts # ui/src/adapters/adapter-display-registry.ts # ui/src/adapters/codex-local/config-fields.test.tsx # ui/src/adapters/codex-local/config-fields.tsx # ui/src/adapters/types.ts # ui/src/components/AgentConfigForm.render.test.tsx # ui/src/components/CodexRunnerSelect.tsx # ui/src/components/ConfigureBuiltInAgentModal.tsx # ui/src/components/OnboardingWizard.tsx # ui/src/components/ui/select.tsx # ui/src/lib/test-agent-setup.ts # ui/src/pages/AgentDetail.tsx # ui/src/pages/CompanyImport.tsx
319 lines
18 KiB
Markdown
319 lines
18 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.
|
||
|
||
Text-mode questions may include `initialText` (at most 100,000 Unicode code points,
|
||
within the existing 196 KiB question-set bound). It supplies an editable starting
|
||
draft, including empty text and whitespace. ACP plain-string `default` values map
|
||
without trimming; numeric defaults become editable numeric text. Select questions
|
||
do not accept this field. The UI preserves saved drafts and existing responses
|
||
ahead of provider defaults. Initial text never submits or resolves a request:
|
||
the operator must explicitly submit, and all existing response validation still
|
||
applies. Canonical text answers retain submitted whitespace and literal escape
|
||
sequences; legacy and select custom answers retain their existing normalization.
|
||
JSON Schema, TypeScript and Rust count draft Unicode code points alike. Existing
|
||
submitted-answer limits still count UTF-16 code units; a draft can require editing
|
||
before it satisfies those limits or field-specific constraints. Required blank
|
||
answers remain invalid. Redaction, company scope and durable
|
||
replay rules remain in force.
|
||
|
||
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.
|
||
|
||
`paperclip.native-execution-input.v5` remains the ordinary input format.
|
||
Claude inputs with explicitly configured effort use v7 with the optional
|
||
`provider.claudeEffort` field. V7 retains the compact v3 model envelope; effort
|
||
is host configuration, never model-visible task text. V6 remains the separate
|
||
OpenAI Dot format. Readers retain persisted v1-v6 contracts. Claude effort is
|
||
acknowledged on each ACP connection before prompting, bound into the session
|
||
identity and recovery checkpoint, and cross-checked against the public provider
|
||
descriptor. Missing effort retains the provider default; values are never clamped.
|
||
|
||
The v5 input format It retains
|
||
the v4 permission pinning and adds optional completion-source references;
|
||
`paperclip.native-model-envelope.v3` is the corresponding explicit model
|
||
projection. Readers continue to accept persisted v1-v4 inputs, and v5 readers
|
||
must preserve the historical behavior of inputs that do not carry the new
|
||
optional fields. Missing Codex and OpenCode policy fields retain their
|
||
historical effective behavior. Legacy ACPX `permissionPolicy: "interactive"`
|
||
is interpreted as `approve-reads`, while new v4 and v5 ACPX executions default
|
||
to `approve-all` at the server boundary.
|
||
|
||
When a healthy provider session is resumed, its persisted v4 or v5 input format
|
||
is retained even if the newly built input uses the other format. This avoids
|
||
rotating an active session for a presentation-only schema change. A safe
|
||
rollback from v5 to v4 removes only the optional completion-source references;
|
||
it retains the task, contract, provider, workspace, and permission fields.
|
||
Format changes still go through the normal provider-session identity checks,
|
||
and an active turn is never mutated in place.
|
||
|
||
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 binds to `0.0.0.0` and a
|
||
single run-bound path. The optional `--listen-port` selects a port in
|
||
`1..=65535` and defaults to `43127`. Warm attachments retain the existing
|
||
listening port. 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.
|
||
|
||
|
||
## Claude effort after the Codex default merge
|
||
|
||
Native input v7 adds configured Claude effort while retaining v1–v6 readers,
|
||
including v6 Dot and Grok Bot bindings. The shared executor retains the merged
|
||
Codex close/cleanup implementation. Copilot's shared source attestation advances
|
||
to pending profile 19 rather than overwriting the merged profile-18 fixture;
|
||
its provider executable and permission closure are unchanged. Historical private
|
||
Claude profile-18 evidence remains source-bound under its separate fixture name.
|
||
Pi profile 25 retains its existing identity. These source identities require
|
||
fresh packaged compatibility checks; earlier provider runs are not relabeled.
|