mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Runner exposes assigned tools to Claude through an authenticated MCP bridge. > - Claude can defer these tools and search for them before a simple task can finish. > - Title and terminal tools are common requirements for a task turn. > - This pull request marks those three tools for immediate loading by Claude. > - The remaining tools keep the SDK deferral policy and the existing access checks. ## Linked Issues or Issue Description Refs #15993, #15994, #15997. **What existing behavior does this improve?** Claude Runner tool discovery during task execution. **Current behavior** Claude can use ToolSearch to discover the mandatory title and terminal tools before it answers a simple task. **Proposed behavior** Expose `set_task_title`, `paperclip_finish`, and `paperclip_block` in the first Claude request through per-tool `anthropic/alwaysLoad` metadata. Keep other tools deferred under the SDK policy. **Reason and benefit** Remove an avoidable tool discovery step without changing the model, effort, or authorization. Repeated product measurements will check the latency benefit. ## What Changed - Add closed metadata for the three essential tools only in Claude runtime hosts. - Keep incoming metadata discarded and private tools hidden. Keep other providers unchanged. - Test real bridge catalogs and provider routing. Check the pinned Claude SDK request against a loopback fixture with no paid calls. - Advance the pending Copilot source binding because it attests the shared bridge file. Preserve historical profile readers and qualification limits. ## Verification - Focused bridge, admission, profile, and effort controls: 185 passed, one skipped. Three provider-routing cases pass. - Runner TypeScript compilation passes. - The production SDK fixture loads exactly three essential tools with the change and zero in the controlled baseline. The document tool stays deferred. - Six real API-key tasks used Opus 5.5 / High in alternating order A B B A A B. Median first assistant receipt was 22.021 s baseline vs 17.060 s candidate (22.5% lower). Median total was 24.119 s vs 19.150 s (20.6% lower). ToolSearch was 3/3 baseline vs 0/3 candidate. All six tasks saved a title and accepted finish. A separate candidate follow-up resumed the same provider session and finished without ToolSearch. - Median startup was 4.786 s baseline vs 5.381 s candidate. This change does not improve startup. Three samples per version give directional evidence only. - Estimated cost for the six tasks was $0.850900800. Candidate total was $0.450341800 vs baseline $0.400559000; the first candidate wrote a new cache prefix. These rate-card estimates are not invoices. - The benchmark used source `5aed3c6a083cf8e6246815455abde3b29ecbbbac`. Rebuilding both bundles at master-reconciled head `d2da07259048c0a2bebd210c35cc0ebb3ff42c67` produced identical SHA-256 values. Baseline bundle SHA-256: `10a92c373bba4fb630bf4fefeab953d850a77cf8258bc2e94711410a73a78ecd`. Candidate: `40348fc67a3b1ae5cc1f27f933f369981d31bcdf5d1bdc7b1797968ddfe53938`. Baseline differs only by compiling the Claude preload selector as false. Fresh task sessions avoid checkpoint changes. - Full local runtime-host runs exposed credential lease collisions in existing cases. A later complete host run passed 67 cases and timed out in an existing admission-abort case; that case passes in isolation. All failures are retained. Focused controls pass. Complete Runner suites, repository typecheck, tests, browser checks, and build pass in cloud CI on this head. ## Risks - These three tool schemas add input to Claude requests. Measurements must include token usage and cost. - Copilot source identity changes because it includes the bridge source hash. It remains pending qualification. Its catalog and executable closure do not change. - The test fixture proves request shape. It does not prove real-provider latency or platform package qualification. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, tool use, and code execution. The exact serving model ID and context window are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (focused controls and isolated cancellation check; broad local failures are retained above) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
334 lines
19 KiB
Markdown
334 lines
19 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 v9 with the optional
|
||
`provider.claudeEffort` field. V9 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-v8 contracts, including v7 home modes and v8 Muse. 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 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 v9 adds configured Claude effort while retaining v1–v8 readers.
|
||
It carries the explicit home mode from v7; v8 Muse remains separate. The shared
|
||
executor retains master's provider and cleanup behavior. Copilot's shared source
|
||
attestation advances from pending profile 21 to 22 without replacing historical
|
||
fixtures. Pi advances from master's profile 24 to 27 to bind the combined source,
|
||
including master's OpenRouter attribution. The prior branch profile 24 is
|
||
preserved separately from master's published profile 24.
|
||
Provider executable bytes remain unchanged. These new source identities require
|
||
fresh compatibility checks; earlier provider runs retain their original identities.
|
||
|
||
## Claude essential task tools
|
||
|
||
Claude's runner-owned MCP bridge marks `set_task_title`, `paperclip_finish` and
|
||
`paperclip_block` with `_meta["anthropic/alwaysLoad"]: true`. The pinned SDK can
|
||
include these definitions in its first request without a ToolSearch round trip.
|
||
Other public tools keep their existing deferral policy. Private operations stay
|
||
undiscoverable, and incoming tool definitions cannot set this metadata themselves.
|
||
The host enables this only for Claude; Codex and Copilot catalogs are unchanged.
|
||
No server-wide tool preloading, model change or permission change is introduced.
|
||
|
||
The shared bridge's source binding advances pending Copilot profile 22 to 23.
|
||
Profile 22 remains decodable and its fixture is immutable. Copilot remains pending;
|
||
the revision does not qualify a new provider or change its executable closure.
|