From e18632ebcb876841845cb6bda9bc90a56ed872a8 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:48:06 -0500 Subject: [PATCH] Add durable semantic tool receipts (#12353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Semantic tools cross a trust boundary between a provider and the control plane > - Durable runs need exact input, result, denial, duplicate, and reconciliation receipts > - Replay must reject unsupported required versions and mismatched receipt pairs > - This pull request adds the receipt builders and deterministic replay fixtures > - It keeps newer sequence and gap safety limits from the current stack > - The benefit is auditable semantic activity before more providers use it ## Linked Issues or Issue Description **Subsystem affected** packages/paperclip-runner **Problem or motivation** Semantic tool calls have basic authorization records, but durable replay does not yet cover reconciled calls, denial redaction, duplicate receipts, governance targets, or artifact references. **Proposed solution** Add bounded semantic receipt builders, a reconciled phase, strict pair binding, fail-closed version checks, and generated replay oracles for the important lifecycle cases. **Alternatives considered** The runner could store provider-native tool payloads. That would weaken protocol portability and make redaction and retry behavior provider-specific. **Roadmap alignment** This supports the existing experimental Paperclip Runner rollout. It does not enable a production adapter. ## What Changed - Add semantic input and result receipt builders. - Add optional reconciliation receipts for pending calls. - Reject unsupported semantic receipt versions. - Validate receipt correlation, operation, idempotency, and digest bindings. - Add deterministic replay fixtures and generated golden outputs. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` - `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript` - `pnpm -r typecheck` - `pnpm build` - Replay golden and protocol manifest checks pass. - The branch changes 27 files relative to its declared base. ## Risks The main risk is accepting a receipt that belongs to another call or replaying a duplicate as a new mutation. Binding checks compare correlation, operation, idempotency, and content digest fields. Fixtures cover denials, duplicates, governance chains, optional fields, artifacts, and unsupported versions. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, `gpt-5`, with agentic reasoning, tool use, and code execution. ## 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 linked an existing public issue or described the issue in-PR - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [x] I have run tests locally and they pass - [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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- ...tic-tool-artifact-happy-path.snapshot.json | 136 ++++++++++ ...ntic-tool-artifact-happy-path.summary.json | 9 + ...ool-conflict-duplicate-retry.snapshot.json | 172 ++++++++++++ ...tool-conflict-duplicate-retry.summary.json | 9 + ...mantic-tool-denial-redaction.snapshot.json | 137 ++++++++++ ...emantic-tool-denial-redaction.summary.json | 9 + ...tool-governance-wake-monitor.snapshot.json | 139 ++++++++++ ...-tool-governance-wake-monitor.summary.json | 9 + ...ol-unknown-optional-envelope.snapshot.json | 133 +++++++++ ...ool-unknown-optional-envelope.summary.json | 9 + .../semantic-tool-artifact-happy-path.json | 186 +++++++++++++ ...emantic-tool-conflict-duplicate-retry.json | 120 +++++++++ .../semantic-tool-denial-redaction.json | 102 +++++++ ...semantic-tool-governance-wake-monitor.json | 90 +++++++ ...mantic-tool-unknown-optional-envelope.json | 72 +++++ ...tic-tool-unsupported-required-version.json | 55 ++++ .../paperclip-runner/protocol/manifest.json | 98 ++++++- .../schemas/semantic-tool.schema.json | 7 +- .../scripts/generate-protocol-manifest.mjs | 8 +- .../scripts/generate-replay-goldens.mjs | 5 + .../scripts/protocol-contract.mjs | 4 + packages/paperclip-runner/src/index.ts | 1 + .../src/protocol/generated/schema-bundle.ts | 13 +- .../src/protocol/replay-contract.test.ts | 193 +++++++++++++ .../src/protocol/replay-contract.ts | 83 ++++-- .../protocol/semantic-tool-receipts.test.ts | 149 +++++++++++ .../src/protocol/semantic-tool-receipts.ts | 253 ++++++++++++++++++ 27 files changed, 2176 insertions(+), 25 deletions(-) create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.summary.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.summary.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.summary.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.summary.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.snapshot.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.summary.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-artifact-happy-path.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-conflict-duplicate-retry.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-denial-redaction.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-governance-wake-monitor.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unknown-optional-envelope.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unsupported-required-version.json create mode 100644 packages/paperclip-runner/src/protocol/semantic-tool-receipts.test.ts create mode 100644 packages/paperclip-runner/src/protocol/semantic-tool-receipts.ts diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.snapshot.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.snapshot.json new file mode 100644 index 0000000000..9ea8e8baa8 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.snapshot.json @@ -0,0 +1,136 @@ +{ + "schema": "paperclip.prp.session-snapshot.v1", + "fixtureName": "Semantic tool artifact happy path", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_happy", + "issueId": "issue_semantic_happy", + "runId": "run_semantic_happy", + "environmentLeaseId": "lease_semantic_happy", + "runnerInstanceId": "runner_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "driverSessionId": "driver_semantic_happy" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { + "kind": "provider-neutral", + "version": "1.0.0" + }, + "steer": true, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [ + { + "operationId": "register_deliverable", + "version": 1, + "availability": "available", + "redactionDisposition": "digest_only", + "requiredClaims": [] + } + ] + }, + "unsupported": [] + }, + "runPhase": "terminal", + "sessionState": "not_started", + "turnState": "not_started", + "activeTurnId": null, + "items": [], + "requests": [], + "proposedResult": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The artifact was registered through the semantic tool receipt.", + "completionClaim": { + "contractRevision": "semantic-receipts-v1", + "objectiveSatisfied": true, + "criteria": [], + "remainingWork": [] + }, + "evidence": [ + { + "receiptId": "receipt_semantic_happy" + } + ], + "verification": [ + { + "commandOrCheck": "semantic artifact receipt", + "status": "passed", + "artifactRef": "artifact_semantic_happy" + } + ], + "attentionRequests": [], + "artifacts": [ + { + "kind": "artifact", + "ref": "artifact_semantic_happy", + "title": "Semantic receipt report" + } + ] + }, + "terminal": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "done", + "workAssessmentId": "assessment_semantic_happy", + "statusDecisionId": "decision_semantic_happy" + }, + "timeline": [ + { + "position": 1, + "sourceEventId": "semantic_happy_01", + "sourceSeq": 1, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:00:00.000Z", + "itemId": "item_semantic_happy", + "summary": "mcp_app tool_input" + }, + { + "position": 2, + "sourceEventId": "semantic_happy_02", + "sourceSeq": 2, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:00:00.010Z", + "itemId": "item_semantic_happy", + "summary": "mcp_app tool_result" + }, + { + "position": 3, + "sourceEventId": "semantic_happy_03", + "sourceSeq": 3, + "eventType": "run.result.proposed", + "emittedAt": "2026-08-11T10:00:00.020Z", + "summary": "The artifact was registered through the semantic tool receipt." + }, + { + "position": 4, + "sourceEventId": "semantic_happy_04", + "sourceSeq": 4, + "eventType": "run.terminal", + "emittedAt": "2026-08-11T10:00:00.030Z", + "summary": "Run succeeded" + } + ], + "sourceCursors": { + "runner:runner_semantic_happy": 4 + }, + "processedEventIds": [ + "semantic_happy_01", + "semantic_happy_02", + "semantic_happy_03", + "semantic_happy_04" + ], + "duplicateEventIds": [], + "outOfOrderEventIds": [], + "gaps": [], + "integrity": "complete" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.summary.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.summary.json new file mode 100644 index 0000000000..9cddc8ebf6 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-artifact-happy-path.summary.json @@ -0,0 +1,9 @@ +{ + "runId": "run_semantic_happy", + "integrity": "complete", + "timelineCount": 4, + "duplicateEventIds": [], + "gaps": [], + "turnTerminalState": "completed", + "runTerminalState": "succeeded" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.snapshot.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.snapshot.json new file mode 100644 index 0000000000..0b6121a2e0 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.snapshot.json @@ -0,0 +1,172 @@ +{ + "schema": "paperclip.prp.session-snapshot.v1", + "fixtureName": "Semantic tool conflict and duplicate retry", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_retry", + "issueId": "issue_semantic_retry", + "runId": "run_semantic_retry", + "environmentLeaseId": "lease_semantic_retry", + "runnerInstanceId": "runner_semantic_retry", + "normalizedSessionId": "session_semantic_retry", + "driverSessionId": "driver_semantic_retry" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { + "kind": "provider-neutral", + "version": "1.0.0" + }, + "steer": false, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [ + { + "operationId": "write_document", + "version": 1, + "availability": "available", + "redactionDisposition": "digest_only", + "requiredClaims": [] + } + ] + }, + "unsupported": [] + }, + "runPhase": "terminal", + "sessionState": "not_started", + "turnState": "not_started", + "activeTurnId": null, + "items": [], + "requests": [], + "proposedResult": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The revision-safe retry committed once.", + "completionClaim": { + "contractRevision": "semantic-receipts-v1", + "objectiveSatisfied": true, + "criteria": [], + "remainingWork": [] + }, + "evidence": [ + { + "receiptId": "receipt_semantic_conflict" + }, + { + "receiptId": "receipt_semantic_document_commit" + } + ], + "verification": [ + { + "commandOrCheck": "conflict then exact duplicate retry", + "status": "passed" + } + ], + "attentionRequests": [], + "artifacts": [] + }, + "terminal": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "done", + "workAssessmentId": "assessment_semantic_retry", + "statusDecisionId": "decision_semantic_retry" + }, + "timeline": [ + { + "position": 1, + "sourceEventId": "semantic_retry_01", + "sourceSeq": 1, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:20:00.000Z", + "itemId": "item_semantic_conflict", + "summary": "mcp_app tool_input" + }, + { + "position": 2, + "sourceEventId": "semantic_retry_02", + "sourceSeq": 2, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:20:00.010Z", + "itemId": "item_semantic_conflict", + "summary": "mcp_app tool_result" + }, + { + "position": 3, + "sourceEventId": "semantic_retry_03", + "sourceSeq": 3, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:20:00.020Z", + "itemId": "item_semantic_fresh", + "summary": "mcp_app tool_input" + }, + { + "position": 4, + "sourceEventId": "semantic_retry_04", + "sourceSeq": 4, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:20:00.030Z", + "itemId": "item_semantic_fresh", + "summary": "mcp_app tool_result" + }, + { + "position": 5, + "sourceEventId": "semantic_retry_05", + "sourceSeq": 5, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:20:00.040Z", + "itemId": "item_semantic_duplicate", + "summary": "mcp_app tool_input" + }, + { + "position": 6, + "sourceEventId": "semantic_retry_06", + "sourceSeq": 6, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:20:00.050Z", + "itemId": "item_semantic_duplicate", + "summary": "mcp_app tool_result" + }, + { + "position": 7, + "sourceEventId": "semantic_retry_07", + "sourceSeq": 7, + "eventType": "run.result.proposed", + "emittedAt": "2026-08-11T10:20:00.060Z", + "summary": "The revision-safe retry committed once." + }, + { + "position": 8, + "sourceEventId": "semantic_retry_08", + "sourceSeq": 8, + "eventType": "run.terminal", + "emittedAt": "2026-08-11T10:20:00.070Z", + "summary": "Run succeeded" + } + ], + "sourceCursors": { + "runner:runner_semantic_retry": 8 + }, + "processedEventIds": [ + "semantic_retry_01", + "semantic_retry_02", + "semantic_retry_03", + "semantic_retry_04", + "semantic_retry_05", + "semantic_retry_06", + "semantic_retry_07", + "semantic_retry_08" + ], + "duplicateEventIds": [], + "outOfOrderEventIds": [], + "gaps": [], + "integrity": "complete" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.summary.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.summary.json new file mode 100644 index 0000000000..ddbb0c594d --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.summary.json @@ -0,0 +1,9 @@ +{ + "runId": "run_semantic_retry", + "integrity": "complete", + "timelineCount": 8, + "duplicateEventIds": [], + "gaps": [], + "turnTerminalState": "completed", + "runTerminalState": "succeeded" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.snapshot.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.snapshot.json new file mode 100644 index 0000000000..be9ad87df4 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.snapshot.json @@ -0,0 +1,137 @@ +{ + "schema": "paperclip.prp.session-snapshot.v1", + "fixtureName": "Semantic tool denial and redaction", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_denied", + "issueId": "issue_semantic_denied", + "runId": "run_semantic_denied", + "environmentLeaseId": "lease_semantic_denied", + "runnerInstanceId": "runner_semantic_denied", + "normalizedSessionId": "session_semantic_denied", + "driverSessionId": "driver_semantic_denied" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { + "kind": "provider-neutral", + "version": "1.0.0" + }, + "steer": false, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [ + { + "operationId": "decide_approval", + "version": 1, + "availability": "denied", + "redactionDisposition": "digest_only", + "requiredClaims": [ + "governance:approvals:decide" + ], + "reasonCode": "required_claim_missing" + } + ] + }, + "unsupported": [] + }, + "runPhase": "terminal", + "sessionState": "not_started", + "turnState": "not_started", + "activeTurnId": null, + "items": [], + "requests": [], + "proposedResult": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "needs_review", + "summary": "The governed action was denied without fallback.", + "completionClaim": { + "contractRevision": "semantic-receipts-v1", + "objectiveSatisfied": false, + "criteria": [], + "remainingWork": [ + { + "description": "An authorized approver must decide.", + "blocksCompletion": true + } + ] + }, + "evidence": [ + { + "receiptId": "receipt_semantic_denied" + } + ], + "verification": [ + { + "commandOrCheck": "no fallback semantic call", + "status": "passed" + } + ], + "attentionRequests": [], + "artifacts": [] + }, + "terminal": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "needs_review", + "workAssessmentId": "assessment_semantic_denied", + "statusDecisionId": "decision_semantic_denied" + }, + "timeline": [ + { + "position": 1, + "sourceEventId": "semantic_denied_01", + "sourceSeq": 1, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:10:00.000Z", + "itemId": "item_semantic_denied", + "summary": "mcp_app tool_input" + }, + { + "position": 2, + "sourceEventId": "semantic_denied_02", + "sourceSeq": 2, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:10:00.010Z", + "itemId": "item_semantic_denied", + "summary": "mcp_app tool_result" + }, + { + "position": 3, + "sourceEventId": "semantic_denied_03", + "sourceSeq": 3, + "eventType": "run.result.proposed", + "emittedAt": "2026-08-11T10:10:00.020Z", + "summary": "The governed action was denied without fallback." + }, + { + "position": 4, + "sourceEventId": "semantic_denied_04", + "sourceSeq": 4, + "eventType": "run.terminal", + "emittedAt": "2026-08-11T10:10:00.030Z", + "summary": "Run succeeded" + } + ], + "sourceCursors": { + "runner:runner_semantic_denied": 4 + }, + "processedEventIds": [ + "semantic_denied_01", + "semantic_denied_02", + "semantic_denied_03", + "semantic_denied_04" + ], + "duplicateEventIds": [], + "outOfOrderEventIds": [], + "gaps": [], + "integrity": "complete" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.summary.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.summary.json new file mode 100644 index 0000000000..79027b0271 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-denial-redaction.summary.json @@ -0,0 +1,9 @@ +{ + "runId": "run_semantic_denied", + "integrity": "complete", + "timelineCount": 4, + "duplicateEventIds": [], + "gaps": [], + "turnTerminalState": "completed", + "runTerminalState": "succeeded" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.snapshot.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.snapshot.json new file mode 100644 index 0000000000..e5d650b90a --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.snapshot.json @@ -0,0 +1,139 @@ +{ + "schema": "paperclip.prp.session-snapshot.v1", + "fixtureName": "Semantic governance target and continuation chain", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_governance", + "issueId": "issue_semantic_governance", + "runId": "run_semantic_governance", + "environmentLeaseId": "lease_semantic_governance", + "runnerInstanceId": "runner_semantic_governance", + "normalizedSessionId": "session_semantic_governance", + "driverSessionId": "driver_semantic_governance" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "reuse_per_issue", + "driver": { + "kind": "provider-neutral", + "version": "1.0.0" + }, + "steer": true, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [ + { + "operationId": "request_human_input", + "version": 1, + "availability": "available", + "redactionDisposition": "digest_only", + "requiredClaims": [] + } + ] + }, + "unsupported": [] + }, + "runPhase": "terminal", + "sessionState": "not_started", + "turnState": "not_started", + "activeTurnId": null, + "items": [], + "requests": [], + "proposedResult": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "yielded", + "summary": "The immutable decision target is waiting on a bounded monitor continuation.", + "completionClaim": { + "contractRevision": "semantic-receipts-v1", + "objectiveSatisfied": false, + "criteria": [], + "remainingWork": [ + { + "description": "Wait for the interaction response.", + "blocksCompletion": true + } + ] + }, + "evidence": [ + { + "receiptId": "receipt_semantic_governance" + } + ], + "verification": [ + { + "commandOrCheck": "immutable target and causal chain", + "status": "passed" + } + ], + "attentionRequests": [], + "artifacts": [], + "continuation": { + "kind": "monitor", + "summary": "Recheck after the typed response wake.", + "idempotencyKey": "monitor_semantic_governance" + } + }, + "terminal": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "yielded", + "workAssessmentId": "assessment_semantic_governance", + "statusDecisionId": "decision_semantic_governance" + }, + "timeline": [ + { + "position": 1, + "sourceEventId": "semantic_governance_01", + "sourceSeq": 1, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:30:00.000Z", + "itemId": "item_semantic_governance", + "summary": "mcp_app tool_input" + }, + { + "position": 2, + "sourceEventId": "semantic_governance_02", + "sourceSeq": 2, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:30:00.010Z", + "itemId": "item_semantic_governance", + "summary": "mcp_app tool_result" + }, + { + "position": 3, + "sourceEventId": "semantic_governance_03", + "sourceSeq": 3, + "eventType": "run.result.proposed", + "emittedAt": "2026-08-11T10:30:00.020Z", + "summary": "The immutable decision target is waiting on a bounded monitor continuation." + }, + { + "position": 4, + "sourceEventId": "semantic_governance_04", + "sourceSeq": 4, + "eventType": "run.terminal", + "emittedAt": "2026-08-11T10:30:00.030Z", + "summary": "Run succeeded" + } + ], + "sourceCursors": { + "runner:runner_semantic_governance": 4 + }, + "processedEventIds": [ + "semantic_governance_01", + "semantic_governance_02", + "semantic_governance_03", + "semantic_governance_04" + ], + "duplicateEventIds": [], + "outOfOrderEventIds": [], + "gaps": [], + "integrity": "complete" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.summary.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.summary.json new file mode 100644 index 0000000000..4e380b2797 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-governance-wake-monitor.summary.json @@ -0,0 +1,9 @@ +{ + "runId": "run_semantic_governance", + "integrity": "complete", + "timelineCount": 4, + "duplicateEventIds": [], + "gaps": [], + "turnTerminalState": "completed", + "runTerminalState": "succeeded" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.snapshot.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.snapshot.json new file mode 100644 index 0000000000..df5576fb66 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.snapshot.json @@ -0,0 +1,133 @@ +{ + "schema": "paperclip.prp.session-snapshot.v1", + "fixtureName": "Semantic tool unknown optional envelope fields", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_future", + "issueId": "issue_semantic_future", + "runId": "run_semantic_future", + "environmentLeaseId": "lease_semantic_future", + "runnerInstanceId": "runner_semantic_future", + "normalizedSessionId": "session_semantic_future", + "driverSessionId": "driver_semantic_future" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { + "kind": "provider-neutral", + "version": "1.0.0" + }, + "steer": false, + "interrupt": true, + "resume": true, + "runtimeRequests": false, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [ + { + "operationId": "get_task_context", + "version": 1, + "availability": "available", + "redactionDisposition": "digest_only", + "requiredClaims": [], + "futureAvailabilityHint": "optional" + } + ], + "futureNegotiationHint": { + "safe": true + } + }, + "unsupported": [] + }, + "runPhase": "terminal", + "sessionState": "not_started", + "turnState": "not_started", + "activeTurnId": null, + "items": [], + "requests": [], + "proposedResult": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "Unknown optional receipt fields were ignored by the v1 projection.", + "completionClaim": { + "contractRevision": "semantic-receipts-v1", + "objectiveSatisfied": true, + "criteria": [], + "remainingWork": [] + }, + "evidence": [ + { + "receiptId": "receipt_semantic_future" + } + ], + "verification": [ + { + "commandOrCheck": "optional field projection stability", + "status": "passed" + } + ], + "attentionRequests": [], + "artifacts": [] + }, + "terminal": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "done", + "workAssessmentId": "assessment_semantic_future", + "statusDecisionId": "decision_semantic_future" + }, + "timeline": [ + { + "position": 1, + "sourceEventId": "semantic_future_01", + "sourceSeq": 1, + "eventType": "mcp_app.tool_input", + "emittedAt": "2026-08-11T10:50:00.000Z", + "itemId": "item_semantic_future", + "summary": "mcp_app tool_input" + }, + { + "position": 2, + "sourceEventId": "semantic_future_02", + "sourceSeq": 2, + "eventType": "mcp_app.tool_result", + "emittedAt": "2026-08-11T10:50:00.010Z", + "itemId": "item_semantic_future", + "summary": "mcp_app tool_result" + }, + { + "position": 3, + "sourceEventId": "semantic_future_03", + "sourceSeq": 3, + "eventType": "run.result.proposed", + "emittedAt": "2026-08-11T10:50:00.020Z", + "summary": "Unknown optional receipt fields were ignored by the v1 projection." + }, + { + "position": 4, + "sourceEventId": "semantic_future_04", + "sourceSeq": 4, + "eventType": "run.terminal", + "emittedAt": "2026-08-11T10:50:00.030Z", + "summary": "Run succeeded" + } + ], + "sourceCursors": { + "runner:runner_semantic_future": 4 + }, + "processedEventIds": [ + "semantic_future_01", + "semantic_future_02", + "semantic_future_03", + "semantic_future_04" + ], + "duplicateEventIds": [], + "outOfOrderEventIds": [], + "gaps": [], + "integrity": "complete" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.summary.json b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.summary.json new file mode 100644 index 0000000000..97efecb1d3 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/golden/semantic-tool-unknown-optional-envelope.summary.json @@ -0,0 +1,9 @@ +{ + "runId": "run_semantic_future", + "integrity": "complete", + "timelineCount": 4, + "duplicateEventIds": [], + "gaps": [], + "turnTerminalState": "completed", + "runTerminalState": "succeeded" +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-artifact-happy-path.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-artifact-happy-path.json new file mode 100644 index 0000000000..d84d3202af --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-artifact-happy-path.json @@ -0,0 +1,186 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Semantic tool artifact happy path", + "description": "A provider-neutral register_deliverable call emits safe input and result receipts linked to the terminal artifact result.", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_happy", + "issueId": "issue_semantic_happy", + "runId": "run_semantic_happy", + "environmentLeaseId": "lease_semantic_happy", + "runnerInstanceId": "runner_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "driverSessionId": "driver_semantic_happy" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": true, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [{ + "operationId": "register_deliverable", + "version": 1, + "availability": "available", + "redactionDisposition": "digest_only", + "requiredClaims": [] + }] + }, + "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "semantic_happy_01", + "sourceSeq": 1, + "sourceInstanceId": "runner_semantic_happy", + "sourceKind": "runner", + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "itemId": "item_semantic_happy", + "eventType": "mcp_app.tool_input", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-11T10:00:00.000Z", + "payload": { + "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", + "schemaVersion": 1, + "phase": "input", + "operationId": "register_deliverable", + "callId": "call_semantic_happy", + "correlation": { + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "itemId": "item_semantic_happy" + }, + "idempotencyKey": "artifact_once", + "content": { + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "redactionDisposition": "digest_only", + "references": [{ "kind": "task", "id": "issue_semantic_happy" }] + } + } + } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "semantic_happy_02", + "sourceSeq": 2, + "sourceInstanceId": "runner_semantic_happy", + "sourceKind": "runner", + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "itemId": "item_semantic_happy", + "eventType": "mcp_app.tool_result", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-11T10:00:00.010Z", + "payload": { + "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", + "schemaVersion": 1, + "phase": "result", + "operationId": "register_deliverable", + "callId": "call_semantic_happy", + "correlation": { + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "itemId": "item_semantic_happy" + }, + "idempotencyKey": "artifact_once", + "content": { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "redactionDisposition": "allowlisted_references", + "references": [ + { "kind": "artifact", "id": "artifact_semantic_happy" }, + { "kind": "work_product", "id": "work_product_semantic_happy" } + ] + }, + "outcome": "succeeded", + "code": "ok", + "retryable": false, + "authorizationBoundary": "active_task", + "operationReceiptId": "receipt_semantic_happy", + "auditReceiptId": "audit_semantic_happy", + "currentRevision": 2, + "artifactRefs": [ + { "kind": "artifact", "id": "artifact_semantic_happy" }, + { "kind": "work_product", "id": "work_product_semantic_happy" } + ] + } + } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "semantic_happy_03", + "sourceSeq": 3, + "sourceInstanceId": "runner_semantic_happy", + "sourceKind": "runner", + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "eventType": "run.result.proposed", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-11T10:00:00.020Z", + "payload": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The artifact was registered through the semantic tool receipt.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_happy" }], + "verification": [{ "commandOrCheck": "semantic artifact receipt", "status": "passed", "artifactRef": "artifact_semantic_happy" }], + "attentionRequests": [], + "artifacts": [{ "kind": "artifact", "ref": "artifact_semantic_happy", "title": "Semantic receipt report" }] + } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "semantic_happy_04", + "sourceSeq": 4, + "sourceInstanceId": "runner_semantic_happy", + "sourceKind": "runner", + "runId": "run_semantic_happy", + "normalizedSessionId": "session_semantic_happy", + "turnId": "turn_semantic_happy", + "eventType": "run.terminal", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-11T10:00:00.030Z", + "payload": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "done", + "workAssessmentId": "assessment_semantic_happy", + "statusDecisionId": "decision_semantic_happy" + } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The artifact was registered through the semantic tool receipt.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_happy" }], + "verification": [{ "commandOrCheck": "semantic artifact receipt", "status": "passed", "artifactRef": "artifact_semantic_happy" }], + "attentionRequests": [], + "artifacts": [{ "kind": "artifact", "ref": "artifact_semantic_happy", "title": "Semantic receipt report" }] + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-conflict-duplicate-retry.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-conflict-duplicate-retry.json new file mode 100644 index 0000000000..ef3b6a91c1 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-conflict-duplicate-retry.json @@ -0,0 +1,120 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Semantic tool conflict and duplicate retry", + "description": "A stale document write conflicts, a fresh retry succeeds, and an exact idempotent retry returns the original operation receipt.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_semantic_retry", "issueId": "issue_semantic_retry", + "runId": "run_semantic_retry", "environmentLeaseId": "lease_semantic_retry", "runnerInstanceId": "runner_semantic_retry", + "normalizedSessionId": "session_semantic_retry", "driverSessionId": "driver_semantic_retry" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", + "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": false, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true, + "semanticTools": { "schema": "paperclip.prp.semantic_tools.v1", "schemaVersion": 1, "operations": [{ + "operationId": "write_document", "version": 1, "availability": "available", "redactionDisposition": "digest_only", "requiredClaims": [] + }] }, + "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_01", "sourceSeq": 1, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_conflict", "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.000Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", "operationId": "write_document", "callId": "call_semantic_conflict", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_conflict" }, + "idempotencyKey": "write_plan_stale", + "content": { "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", "redactionDisposition": "digest_only", "references": [{ "kind": "document_revision", "id": "revision_plan_stale" }] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_02", "sourceSeq": 2, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_conflict", "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.010Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "write_document", "callId": "call_semantic_conflict", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_conflict" }, + "idempotencyKey": "write_plan_stale", + "content": { "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", "redactionDisposition": "allowlisted_references", "references": [{ "kind": "document_revision", "id": "revision_plan_2" }] }, + "outcome": "conflict", "code": "document_revision_conflict", "retryable": true, "authorizationBoundary": "revision", + "operationReceiptId": "receipt_semantic_conflict", "auditReceiptId": "audit_semantic_conflict", "currentRevision": "revision_plan_2" + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_03", "sourceSeq": 3, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_fresh", "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.020Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", "operationId": "write_document", "callId": "call_semantic_fresh", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_fresh" }, + "idempotencyKey": "write_plan_retry", + "content": { "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", "redactionDisposition": "digest_only", "references": [{ "kind": "document_revision", "id": "revision_plan_2" }] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_04", "sourceSeq": 4, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_fresh", "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.030Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "write_document", "callId": "call_semantic_fresh", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_fresh" }, + "idempotencyKey": "write_plan_retry", + "content": { "digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", "redactionDisposition": "allowlisted_references", "references": [{ "kind": "document_revision", "id": "revision_plan_3" }] }, + "outcome": "succeeded", "code": "ok", "retryable": false, "authorizationBoundary": "revision", + "operationReceiptId": "receipt_semantic_document_commit", "auditReceiptId": "audit_semantic_document_commit", "currentRevision": "revision_plan_3" + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_05", "sourceSeq": 5, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_duplicate", "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.040Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", "operationId": "write_document", "callId": "call_semantic_duplicate", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_duplicate" }, + "idempotencyKey": "write_plan_retry", + "content": { "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", "redactionDisposition": "digest_only", "references": [{ "kind": "document_revision", "id": "revision_plan_2" }] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_06", "sourceSeq": 6, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "itemId": "item_semantic_duplicate", "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:20:00.050Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "write_document", "callId": "call_semantic_duplicate", + "correlation": { "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", "itemId": "item_semantic_duplicate" }, + "idempotencyKey": "write_plan_retry", + "content": { "digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", "redactionDisposition": "allowlisted_references", "references": [{ "kind": "document_revision", "id": "revision_plan_3" }] }, + "outcome": "duplicate", "code": "duplicate", "retryable": false, "authorizationBoundary": "revision", + "operationReceiptId": "receipt_semantic_document_commit", "duplicateOfReceiptId": "receipt_semantic_document_commit", "currentRevision": "revision_plan_3" + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_07", "sourceSeq": 7, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:20:00.060Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "The revision-safe retry committed once.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_conflict" }, { "receiptId": "receipt_semantic_document_commit" }], + "verification": [{ "commandOrCheck": "conflict then exact duplicate retry", "status": "passed" }], "attentionRequests": [], "artifacts": [] + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_retry_08", "sourceSeq": 8, "sourceInstanceId": "runner_semantic_retry", + "sourceKind": "runner", "runId": "run_semantic_retry", "normalizedSessionId": "session_semantic_retry", "turnId": "turn_semantic_retry", + "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:20:00.070Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done", "workAssessmentId": "assessment_semantic_retry", "statusDecisionId": "decision_semantic_retry" } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "The revision-safe retry committed once.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_conflict" }, { "receiptId": "receipt_semantic_document_commit" }], + "verification": [{ "commandOrCheck": "conflict then exact duplicate retry", "status": "passed" }], "attentionRequests": [], "artifacts": [] + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-denial-redaction.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-denial-redaction.json new file mode 100644 index 0000000000..eab3a516ed --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-denial-redaction.json @@ -0,0 +1,102 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Semantic tool denial and redaction", + "description": "A governed action is denied with digest-only content, a typed authorization boundary, and no fallback semantic call.", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_semantic_denied", + "issueId": "issue_semantic_denied", + "runId": "run_semantic_denied", + "environmentLeaseId": "lease_semantic_denied", + "runnerInstanceId": "runner_semantic_denied", + "normalizedSessionId": "session_semantic_denied", + "driverSessionId": "driver_semantic_denied" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": false, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "semanticTools": { + "schema": "paperclip.prp.semantic_tools.v1", + "schemaVersion": 1, + "operations": [{ + "operationId": "decide_approval", + "version": 1, + "availability": "denied", + "redactionDisposition": "digest_only", + "requiredClaims": ["governance:approvals:decide"], + "reasonCode": "required_claim_missing" + }] + }, + "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_denied_01", "sourceSeq": 1, + "sourceInstanceId": "runner_semantic_denied", "sourceKind": "runner", "runId": "run_semantic_denied", + "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", "itemId": "item_semantic_denied", + "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:10:00.000Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", + "operationId": "decide_approval", "callId": "call_semantic_denied", + "correlation": { "runId": "run_semantic_denied", "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", "itemId": "item_semantic_denied" }, + "idempotencyKey": "denied_once", + "content": { "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "redactionDisposition": "redacted", "references": [] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_denied_02", "sourceSeq": 2, + "sourceInstanceId": "runner_semantic_denied", "sourceKind": "runner", "runId": "run_semantic_denied", + "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", "itemId": "item_semantic_denied", + "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:10:00.010Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", + "operationId": "decide_approval", "callId": "call_semantic_denied", + "correlation": { "runId": "run_semantic_denied", "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", "itemId": "item_semantic_denied" }, + "idempotencyKey": "denied_once", + "content": { "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", "redactionDisposition": "redacted", "references": [] }, + "outcome": "denied", "code": "required_claim_missing", "retryable": false, + "authorizationBoundary": "grant", "operationReceiptId": "receipt_semantic_denied", "auditReceiptId": "audit_semantic_denied", "currentRevision": 4 + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_denied_03", "sourceSeq": 3, + "sourceInstanceId": "runner_semantic_denied", "sourceKind": "runner", "runId": "run_semantic_denied", + "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:10:00.020Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", + "summary": "The governed action was denied without fallback.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "An authorized approver must decide.", "blocksCompletion": true }] }, + "evidence": [{ "receiptId": "receipt_semantic_denied" }], + "verification": [{ "commandOrCheck": "no fallback semantic call", "status": "passed" }], + "attentionRequests": [], "artifacts": [] + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_denied_04", "sourceSeq": 4, + "sourceInstanceId": "runner_semantic_denied", "sourceKind": "runner", "runId": "run_semantic_denied", + "normalizedSessionId": "session_semantic_denied", "turnId": "turn_semantic_denied", + "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:10:00.030Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "needs_review", "workAssessmentId": "assessment_semantic_denied", "statusDecisionId": "decision_semantic_denied" } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", + "summary": "The governed action was denied without fallback.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "An authorized approver must decide.", "blocksCompletion": true }] }, + "evidence": [{ "receiptId": "receipt_semantic_denied" }], + "verification": [{ "commandOrCheck": "no fallback semantic call", "status": "passed" }], + "attentionRequests": [], "artifacts": [] + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-governance-wake-monitor.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-governance-wake-monitor.json new file mode 100644 index 0000000000..98735d3a58 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-governance-wake-monitor.json @@ -0,0 +1,90 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Semantic governance target and continuation chain", + "description": "A semantic result binds immutable interaction and approval targets while carrying only safe wake and monitor causal references.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_semantic_governance", "issueId": "issue_semantic_governance", + "runId": "run_semantic_governance", "environmentLeaseId": "lease_semantic_governance", "runnerInstanceId": "runner_semantic_governance", + "normalizedSessionId": "session_semantic_governance", "driverSessionId": "driver_semantic_governance" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "reuse_per_issue", + "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": true, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true, + "semanticTools": { "schema": "paperclip.prp.semantic_tools.v1", "schemaVersion": 1, "operations": [{ + "operationId": "request_human_input", "version": 1, "availability": "available", "redactionDisposition": "digest_only", "requiredClaims": [] + }] }, + "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_governance_01", "sourceSeq": 1, "sourceInstanceId": "runner_semantic_governance", + "sourceKind": "runner", "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", + "turnId": "turn_semantic_governance", "itemId": "item_semantic_governance", "eventType": "mcp_app.tool_input", + "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:30:00.000Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", "operationId": "request_human_input", "callId": "call_semantic_governance", + "correlation": { "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", "turnId": "turn_semantic_governance", "itemId": "item_semantic_governance", "requestId": "request_semantic_governance" }, + "idempotencyKey": "interaction_once", + "content": { "digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555", "redactionDisposition": "digest_only", "references": [{ "kind": "document_revision", "id": "revision_plan_7" }] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_governance_02", "sourceSeq": 2, "sourceInstanceId": "runner_semantic_governance", + "sourceKind": "runner", "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", + "turnId": "turn_semantic_governance", "itemId": "item_semantic_governance", "eventType": "mcp_app.tool_result", + "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:30:00.010Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "request_human_input", "callId": "call_semantic_governance", + "correlation": { "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", "turnId": "turn_semantic_governance", "itemId": "item_semantic_governance", "requestId": "request_semantic_governance" }, + "idempotencyKey": "interaction_once", + "content": { "digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", "redactionDisposition": "allowlisted_references", "references": [{ "kind": "interaction", "id": "interaction_semantic_governance" }, { "kind": "wake", "id": "wake_semantic_governance" }, { "kind": "monitor", "id": "monitor_semantic_governance" }] }, + "outcome": "succeeded", "code": "ok", "retryable": false, "authorizationBoundary": "governed_action", + "operationReceiptId": "receipt_semantic_governance", "auditReceiptId": "audit_semantic_governance", "currentRevision": 8, + "targets": [ + { "kind": "interaction", "id": "interaction_semantic_governance", "immutable": true, "revisionId": "revision_plan_7" }, + { "kind": "approval", "id": "approval_semantic_governance", "immutable": true, "decisionId": "decision_approval_semantic_governance" } + ], + "causalRefs": [ + { "kind": "document_revision", "id": "revision_plan_7" }, + { "kind": "interaction", "id": "interaction_semantic_governance" }, + { "kind": "approval", "id": "approval_semantic_governance" }, + { "kind": "decision", "id": "decision_approval_semantic_governance" }, + { "kind": "wake", "id": "wake_semantic_governance" }, + { "kind": "monitor", "id": "monitor_semantic_governance" } + ] + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_governance_03", "sourceSeq": 3, "sourceInstanceId": "runner_semantic_governance", + "sourceKind": "runner", "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", "turnId": "turn_semantic_governance", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:30:00.020Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The immutable decision target is waiting on a bounded monitor continuation.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Wait for the interaction response.", "blocksCompletion": true }] }, + "evidence": [{ "receiptId": "receipt_semantic_governance" }], + "verification": [{ "commandOrCheck": "immutable target and causal chain", "status": "passed" }], + "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "monitor", "summary": "Recheck after the typed response wake.", "idempotencyKey": "monitor_semantic_governance" } + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_governance_04", "sourceSeq": 4, "sourceInstanceId": "runner_semantic_governance", + "sourceKind": "runner", "runId": "run_semantic_governance", "normalizedSessionId": "session_semantic_governance", "turnId": "turn_semantic_governance", + "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:30:00.030Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "yielded", "workAssessmentId": "assessment_semantic_governance", "statusDecisionId": "decision_semantic_governance" } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The immutable decision target is waiting on a bounded monitor continuation.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Wait for the interaction response.", "blocksCompletion": true }] }, + "evidence": [{ "receiptId": "receipt_semantic_governance" }], + "verification": [{ "commandOrCheck": "immutable target and causal chain", "status": "passed" }], + "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "monitor", "summary": "Recheck after the typed response wake.", "idempotencyKey": "monitor_semantic_governance" } + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unknown-optional-envelope.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unknown-optional-envelope.json new file mode 100644 index 0000000000..e763dd43ef --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unknown-optional-envelope.json @@ -0,0 +1,72 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Semantic tool unknown optional envelope fields", + "description": "Unknown additive fields inside typed semantic envelopes validate without changing the existing v1 reducer projection.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_semantic_future", "issueId": "issue_semantic_future", "runId": "run_semantic_future", + "environmentLeaseId": "lease_semantic_future", "runnerInstanceId": "runner_semantic_future", "normalizedSessionId": "session_semantic_future", "driverSessionId": "driver_semantic_future" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": false, "interrupt": true, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true, + "semanticTools": { "schema": "paperclip.prp.semantic_tools.v1", "schemaVersion": 1, "operations": [{ + "operationId": "get_task_context", "version": 1, "availability": "available", "redactionDisposition": "digest_only", "requiredClaims": [], + "futureAvailabilityHint": "optional" + }], "futureNegotiationHint": { "safe": true } }, + "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_future_01", "sourceSeq": 1, "sourceInstanceId": "runner_semantic_future", + "sourceKind": "runner", "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", "itemId": "item_semantic_future", + "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:50:00.000Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "input", "operationId": "get_task_context", "callId": "call_semantic_future", + "correlation": { "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", "itemId": "item_semantic_future", "futureCorrelationHint": 1 }, + "idempotencyKey": null, + "content": { "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777", "redactionDisposition": "digest_only", "references": [], "futureContentHint": "optional" }, + "futureEnvelopeHint": { "version": "1.1-preview" } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_future_02", "sourceSeq": 2, "sourceInstanceId": "runner_semantic_future", + "sourceKind": "runner", "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", "itemId": "item_semantic_future", + "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T10:50:00.010Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "get_task_context", "callId": "call_semantic_future", + "correlation": { "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", "itemId": "item_semantic_future", "futureCorrelationHint": 1 }, + "idempotencyKey": null, + "content": { "digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", "redactionDisposition": "digest_only", "references": [{ "kind": "task", "id": "issue_semantic_future" }], "futureContentHint": "optional" }, + "outcome": "succeeded", "code": "ok", "retryable": false, "authorizationBoundary": "active_task", "operationReceiptId": "receipt_semantic_future", "currentRevision": 1, + "futureEnvelopeHint": { "version": "1.1-preview" } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_future_03", "sourceSeq": 3, "sourceInstanceId": "runner_semantic_future", + "sourceKind": "runner", "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:50:00.020Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional receipt fields were ignored by the v1 projection.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_future" }], "verification": [{ "commandOrCheck": "optional field projection stability", "status": "passed" }], + "attentionRequests": [], "artifacts": [] + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_future_04", "sourceSeq": 4, "sourceInstanceId": "runner_semantic_future", + "sourceKind": "runner", "runId": "run_semantic_future", "normalizedSessionId": "session_semantic_future", "turnId": "turn_semantic_future", + "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T10:50:00.030Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done", "workAssessmentId": "assessment_semantic_future", "statusDecisionId": "decision_semantic_future" } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional receipt fields were ignored by the v1 projection.", + "completionClaim": { "contractRevision": "semantic-receipts-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, + "evidence": [{ "receiptId": "receipt_semantic_future" }], "verification": [{ "commandOrCheck": "optional field projection stability", "status": "passed" }], + "attentionRequests": [], "artifacts": [] + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unsupported-required-version.json b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unsupported-required-version.json new file mode 100644 index 0000000000..6230b4e443 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/semantic-tool-unsupported-required-version.json @@ -0,0 +1,55 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Unsupported semantic tool envelope version", + "description": "A required semantic_tool v2 envelope must fail closed in a PRP v1 consumer.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_semantic_v2", "issueId": "issue_semantic_v2", "runId": "run_semantic_v2", + "environmentLeaseId": "lease_semantic_v2", "runnerInstanceId": "runner_semantic_v2", "normalizedSessionId": "session_semantic_v2", "driverSessionId": "driver_semantic_v2" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "provider-neutral", "version": "1.0.0" }, + "steer": false, "interrupt": true, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true, "unsupported": [] + }, + "commands": [], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_v2_01", "sourceSeq": 1, "sourceInstanceId": "runner_semantic_v2", + "sourceKind": "runner", "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", "itemId": "item_semantic_v2", + "eventType": "mcp_app.tool_input", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T11:00:00.000Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v2", "schemaVersion": 2, "phase": "input", "operationId": "get_task_context", "callId": "call_semantic_v2", + "correlation": { "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", "itemId": "item_semantic_v2" }, + "idempotencyKey": null, + "content": { "digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999", "redactionDisposition": "digest_only", "references": [] } + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_v2_02", "sourceSeq": 2, "sourceInstanceId": "runner_semantic_v2", + "sourceKind": "runner", "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", "itemId": "item_semantic_v2", + "eventType": "mcp_app.tool_result", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-11T11:00:00.010Z", + "payload": { "semantic_tool": { + "schema": "paperclip.prp.semantic_tool.v1", "schemaVersion": 1, "phase": "result", "operationId": "get_task_context", "callId": "call_semantic_v2", + "correlation": { "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", "itemId": "item_semantic_v2" }, + "idempotencyKey": null, + "content": { "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "redactionDisposition": "digest_only", "references": [] }, + "outcome": "succeeded", "code": "ok", "retryable": false, "authorizationBoundary": "active_task", "operationReceiptId": "receipt_semantic_v2" + } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_v2_03", "sourceSeq": 3, "sourceInstanceId": "runner_semantic_v2", + "sourceKind": "runner", "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T11:00:00.020Z", + "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result must not replay.", "completionClaim": { "contractRevision": "semantic-receipts-v2", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "semantic_v2_04", "sourceSeq": 4, "sourceInstanceId": "runner_semantic_v2", + "sourceKind": "runner", "runId": "run_semantic_v2", "normalizedSessionId": "session_semantic_v2", "turnId": "turn_semantic_v2", + "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-11T11:00:00.030Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done" } + } + ], + "requests": [], + "result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result must not replay.", "completionClaim": { "contractRevision": "semantic-receipts-v2", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } +} diff --git a/packages/paperclip-runner/protocol/manifest.json b/packages/paperclip-runner/protocol/manifest.json index 9400ab615a..cba9dcbeee 100644 --- a/packages/paperclip-runner/protocol/manifest.json +++ b/packages/paperclip-runner/protocol/manifest.json @@ -80,7 +80,7 @@ { "path": "schemas/semantic-tool.schema.json", "id": "https://paperclip.dev/schemas/prp/v1/semantic-tool.schema.json", - "sha256": "f53f6832c0d65caaccf540662fdb08cf62d9bab45a18def7b559c4068ef54efc" + "sha256": "72dba6b17be1358160633e2ac430002b8be1ab311a236289a942cb05b21385da" }, { "path": "schemas/stop-reason.schema.json", @@ -235,6 +235,66 @@ "expectation": "accept", "compatibilityCase": "deterministic-replay-oracle" }, + { + "path": "fixtures/replay/golden/semantic-tool-artifact-happy-path.snapshot.json", + "sha256": "78d61ea44793714dc652b86e0db8b2fdfd8e7f8617f03de6bd88904f51b20491", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-artifact-happy-path.summary.json", + "sha256": "8b3e72e071f7458e67180fdd9933bb89cb6458f2507f2706000c1f172d1e529f", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.snapshot.json", + "sha256": "c236827d1cadd5841a058c3f5ec0d6a542a4676b2ce65dee65e8435216266545", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-conflict-duplicate-retry.summary.json", + "sha256": "f0b277d37c422b90d2012ad61ea5fbe214ebf1607f1a6c2419c518c4f9a52b7e", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-denial-redaction.snapshot.json", + "sha256": "368678297d7763ea5d5d453afa9369e22484ecfc70de7f5769f4cbb05c372565", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-denial-redaction.summary.json", + "sha256": "b0379c2ea8d30e9c2866d3737d9fdc73711de24a955364bd78b75769d925764f", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-governance-wake-monitor.snapshot.json", + "sha256": "4c5f747c497d1a4a1ba1af4e7efaf3951dafc8e095f8f9816fa868dac03803f6", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-governance-wake-monitor.summary.json", + "sha256": "a20a038c04e3f023f7eb478b67144fca48dd288cf484e74d66810d92debc62e5", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-unknown-optional-envelope.snapshot.json", + "sha256": "27fbc947268d2b2a5a60127cd6b575a820b235bc7bce5ec7ade6a6196d4652b6", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, + { + "path": "fixtures/replay/golden/semantic-tool-unknown-optional-envelope.summary.json", + "sha256": "51fb686791aa158f9a23a0c2b90ca24d43bcc953add9e3277ab3f01b958fc26a", + "expectation": "accept", + "compatibilityCase": "deterministic-replay-oracle" + }, { "path": "fixtures/replay/golden/source-gap.snapshot.json", "sha256": "7db7f25e423a721e60adfcdfee02db6d5e6fcd4a644ee180f5e29810ebbc0d78", @@ -271,6 +331,42 @@ "expectation": "accept", "compatibilityCase": "canonical" }, + { + "path": "fixtures/replay/semantic-tool-artifact-happy-path.json", + "sha256": "17e4e7ef03563e26111d028b056985c80a760bf6d45726a4e30bc09c1a43ae38", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/semantic-tool-conflict-duplicate-retry.json", + "sha256": "8298d73d1be7e54522e1947a6b029f6e58a5a41d4cc190a335ac50d2199d1a2e", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/semantic-tool-denial-redaction.json", + "sha256": "4409606c6ae1becd2708f4ff7c5ca82921901cbd0d5dfada171238b032e5343c", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/semantic-tool-governance-wake-monitor.json", + "sha256": "56f14ca3284e01894ee8ee70b26d4cdc1181d4e09362e3aa89170fb81f4537ff", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/semantic-tool-unknown-optional-envelope.json", + "sha256": "53b90800c7d49877892b4e7b0a3bde21bdda6faa55ac89a008a2ff5420e79d85", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/semantic-tool-unsupported-required-version.json", + "sha256": "ae1a1a873133f11e18f94b0005318000963e1c747dc4e85ae2dfdc08570aadfa", + "expectation": "reject", + "compatibilityCase": "unknown-required-version" + }, { "path": "fixtures/replay/source-gap.json", "sha256": "d8a3eb2861e8198226360e666e5296ff5d7215f81d255a273b21d2bfe7dcc2ef", diff --git a/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json b/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json index 657a7e4e9f..93ff4dde4a 100644 --- a/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json +++ b/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json @@ -16,7 +16,7 @@ "properties": { "schema": { "const": "paperclip.prp.semantic_tool.v1" }, "schemaVersion": { "const": 1 }, - "phase": { "enum": ["input", "result"] }, + "phase": { "enum": ["input", "result", "reconciled"] }, "operationId": { "$ref": "#/$defs/stableId" }, "callId": { "$ref": "#/$defs/stableId" }, "correlation": { @@ -80,7 +80,10 @@ }, "allOf": [ { - "if": { "properties": { "phase": { "const": "result" } } }, + "if": { + "properties": { "phase": { "enum": ["result", "reconciled"] } }, + "required": ["phase"] + }, "then": { "required": [ "outcome", diff --git a/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs b/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs index 6535eb07fc..eda018031a 100644 --- a/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs +++ b/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs @@ -22,8 +22,10 @@ const protocolRoot = resolve(packageRoot, "protocol"); const schemaDirectory = resolve(protocolRoot, "schemas"); const fixtureDirectory = resolve(protocolRoot, "fixtures"); const outputPath = resolve(protocolRoot, "manifest.json"); -const expectedRejectedFixture = - "fixtures/replay/unsupported-required-version.json"; +const expectedRejectedFixtures = new Set([ + "fixtures/replay/unsupported-required-version.json", + "fixtures/replay/semantic-tool-unsupported-required-version.json", +]); export async function buildProtocolManifest() { const schemas = await loadSchemaCatalog(schemaDirectory); @@ -42,7 +44,7 @@ export async function buildProtocolManifest() { if (relativePath.startsWith("fixtures/replay/golden/")) { compatibilityCase = "deterministic-replay-oracle"; } else if (relativePath.startsWith("fixtures/replay/")) { - if (relativePath === expectedRejectedFixture) { + if (expectedRejectedFixtures.has(relativePath)) { expectation = "reject"; compatibilityCase = "unknown-required-version"; try { diff --git a/packages/paperclip-runner/scripts/generate-replay-goldens.mjs b/packages/paperclip-runner/scripts/generate-replay-goldens.mjs index e2699d0825..5784fe07fb 100644 --- a/packages/paperclip-runner/scripts/generate-replay-goldens.mjs +++ b/packages/paperclip-runner/scripts/generate-replay-goldens.mjs @@ -18,6 +18,11 @@ const fixtureNames = [ "duplicate-event", "source-gap", "unknown-optional-fields", + "semantic-tool-artifact-happy-path", + "semantic-tool-denial-redaction", + "semantic-tool-conflict-duplicate-retry", + "semantic-tool-governance-wake-monitor", + "semantic-tool-unknown-optional-envelope", ]; const check = process.argv.includes("--check"); const stale = []; diff --git a/packages/paperclip-runner/scripts/protocol-contract.mjs b/packages/paperclip-runner/scripts/protocol-contract.mjs index 9ee5e8bde8..a800538915 100644 --- a/packages/paperclip-runner/scripts/protocol-contract.mjs +++ b/packages/paperclip-runner/scripts/protocol-contract.mjs @@ -170,6 +170,10 @@ export function assertReplayFixtureCompatibility(fixture) { for (const [index, event] of fixture.events.entries()) { requireSchema(event, "paperclip.prp.event.v1", `events[${index}]`); requireVersion(event.schemaVersion, SUPPORTED_EVENT_SCHEMA_VERSION, `events[${index}].schemaVersion`); + const semanticToolVersion = event.payload?.semantic_tool?.schemaVersion; + if (semanticToolVersion !== undefined) { + requireVersion(semanticToolVersion, 1, `events[${index}].payload.semantic_tool.schemaVersion`); + } } requireSchema(fixture.result, "paperclip.run_result.v1", "result"); diff --git a/packages/paperclip-runner/src/index.ts b/packages/paperclip-runner/src/index.ts index 3c56edc441..a25d3f32ac 100644 --- a/packages/paperclip-runner/src/index.ts +++ b/packages/paperclip-runner/src/index.ts @@ -19,6 +19,7 @@ export { export type { DurableRecoveryIdentity } from "./control-plane/prp-transport-types.js"; export * from "./protocol/replay-contract.js"; export * from "./protocol/result-normalization.js"; +export * from "./protocol/semantic-tool-receipts.js"; export * from "./provider-events.js"; export * from "./reducer/session-reducer.js"; export * from "./semantic-tools/index.js"; diff --git a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts index d6b096a2db..62948faf9e 100644 --- a/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts +++ b/packages/paperclip-runner/src/protocol/generated/schema-bundle.ts @@ -1866,7 +1866,8 @@ export const semanticToolSchema = { "phase": { "enum": [ "input", - "result" + "result", + "reconciled" ] }, "operationId": { @@ -2002,9 +2003,15 @@ export const semanticToolSchema = { "if": { "properties": { "phase": { - "const": "result" + "enum": [ + "result", + "reconciled" + ] } - } + }, + "required": [ + "phase" + ] }, "then": { "required": [ diff --git a/packages/paperclip-runner/src/protocol/replay-contract.test.ts b/packages/paperclip-runner/src/protocol/replay-contract.test.ts index 5cb2f31f41..3708f5644f 100644 --- a/packages/paperclip-runner/src/protocol/replay-contract.test.ts +++ b/packages/paperclip-runner/src/protocol/replay-contract.test.ts @@ -19,6 +19,11 @@ const validFixtures = [ "duplicate-event.json", "source-gap.json", "unknown-optional-fields.json", + "semantic-tool-artifact-happy-path.json", + "semantic-tool-denial-redaction.json", + "semantic-tool-conflict-duplicate-retry.json", + "semantic-tool-governance-wake-monitor.json", + "semantic-tool-unknown-optional-envelope.json", ]; async function readFixture( @@ -29,6 +34,31 @@ async function readFixture( ) as Record; } +function reconciledEvent( + events: Array>, +): Record { + const reconciled = structuredClone(events[0]!); + reconciled.sourceEventId = "semantic_happy_reconciled"; + reconciled.sourceSeq = 2; + reconciled.eventType = "semantic_tool.reconciled"; + const payload = reconciled.payload as Record; + const semanticTool = payload.semantic_tool as Record; + const resultPayload = events[1]!.payload as Record; + const result = resultPayload.semantic_tool as Record; + semanticTool.phase = "reconciled"; + for (const field of [ + "content", + "outcome", + "code", + "retryable", + "authorizationBoundary", + "operationReceiptId", + ]) { + semanticTool[field] = structuredClone(result[field]); + } + return reconciled; +} + describe("PRP v1 JSON Schema contract", () => { for (const fixtureName of validFixtures) { it(`validates ${fixtureName}`, async () => { @@ -73,6 +103,169 @@ describe("PRP v1 JSON Schema contract", () => { }); }); + it("fails closed on an unsupported required semantic-tool version", async () => { + const result = parsePrpFixtureText( + await readFile( + new URL( + "semantic-tool-unsupported-required-version.json", + fixtureDirectory, + ), + "utf8", + ), + ); + expect(result).toMatchObject({ + ok: false, + issues: [ + { + code: "unsupported_required_version", + path: "/events/0/payload/semantic_tool/schemaVersion", + }, + ], + }); + }); + + it("accepts a pending semantic call closed by reconciliation alone", async () => { + const fixture = await readFixture("semantic-tool-artifact-happy-path.json"); + const events = fixture.events as Array>; + const reconciled = reconciledEvent(events); + events[1] = reconciled; + + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: true, + }); + + const semanticTool = ( + reconciled.payload as Record + ).semantic_tool as Record; + delete semanticTool.outcome; + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: false, + issues: expect.arrayContaining([ + expect.objectContaining({ code: "schema_validation" }), + ]), + }); + }); + + it("binds reconciliation identity while preserving recovered result content", async () => { + const fixture = await readFixture("semantic-tool-artifact-happy-path.json"); + const events = fixture.events as Array>; + const reconciled = reconciledEvent(events); + const payload = reconciled.payload as Record; + const semanticTool = payload.semantic_tool as Record; + events[1] = reconciled; + + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: true, + }); + expect( + (semanticTool.content as Record).digest, + ).toBe("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + semanticTool.operationId = "different_operation"; + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: false, + issues: [ + expect.objectContaining({ + path: "/events/1/payload/semantic_tool/operationId", + }), + ], + }); + + semanticTool.operationId = ( + (events[0]!.payload as Record).semantic_tool as Record< + string, + unknown + > + ).operationId; + reconciled.turnId = "different-turn"; + (semanticTool.correlation as Record).turnId = + "different-turn"; + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: false, + issues: [ + expect.objectContaining({ + path: "/events/1/payload/semantic_tool/correlation/turnId", + }), + ], + }); + }); + + it( + "allows reconciliation to omit optional correlation metadata", + async () => { + const fixture = await readFixture( + "semantic-tool-artifact-happy-path.json", + ); + const events = fixture.events as Array>; + const input = (events[0]!.payload as Record) + .semantic_tool as Record; + const inputCorrelation = input.correlation as Record; + inputCorrelation.requestId = "request_semantic_happy"; + inputCorrelation.futureTraceId = "trace_semantic_happy"; + + const reconciled = reconciledEvent(events); + const reconciledEnvelope = ( + reconciled.payload as Record + ).semantic_tool as Record; + const reconciledCorrelation = reconciledEnvelope.correlation as Record< + string, + unknown + >; + delete reconciledCorrelation.requestId; + delete reconciledCorrelation.futureTraceId; + events[1] = reconciled; + + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: true, + }); + }, + ); + + it( + "preserves replacement-runner provenance during reconciliation", + async () => { + const fixture = await readFixture( + "semantic-tool-artifact-happy-path.json", + ); + const events = fixture.events as Array>; + const reconciled = reconciledEvent(events); + const inputSourceInstanceId = events[0]!.sourceInstanceId; + reconciled.sourceInstanceId = "runner_semantic_other"; + events[1] = reconciled; + + const result = parsePrpFixtureText(JSON.stringify(fixture)); + expect(result).toMatchObject({ ok: true }); + if (result.ok) { + expect(result.fixture.events[0]?.sourceInstanceId).toBe( + inputSourceInstanceId, + ); + expect(result.fixture.events[1]?.sourceInstanceId).toBe( + "runner_semantic_other", + ); + } + }, + ); + + it("rejects result and reconciliation as two terminal phases for one call", async () => { + const fixture = await readFixture("semantic-tool-artifact-happy-path.json"); + const events = fixture.events as Array>; + const reconciled = reconciledEvent(events); + for (const event of events.slice(1)) { + event.sourceSeq = Number(event.sourceSeq) + 1; + } + events.splice(1, 0, reconciled); + + expect(parsePrpFixtureText(JSON.stringify(fixture))).toMatchObject({ + ok: false, + issues: [ + expect.objectContaining({ + path: "/events/0/payload/semantic_tool/callId", + message: expect.stringContaining("exactly one result or reconciled"), + }), + ], + }); + }); + it("fails closed on unsupported nested required schema versions", async () => { const fixture = await readFixture(); const events = fixture.events as Array>; diff --git a/packages/paperclip-runner/src/protocol/replay-contract.ts b/packages/paperclip-runner/src/protocol/replay-contract.ts index 2b3764ff75..bd83f73416 100644 --- a/packages/paperclip-runner/src/protocol/replay-contract.ts +++ b/packages/paperclip-runner/src/protocol/replay-contract.ts @@ -207,14 +207,20 @@ function ajvIssue(error: ErrorObject): ProtocolValidationIssue { }; } +interface SemanticCallBinding { + envelope: PrpSemanticToolEnvelope; + index: number; +} + function bindingIssues(fixture: PrpFixture): ProtocolValidationIssue[] { const issues: ProtocolValidationIssue[] = []; const uniqueEvents = new Map(); const semanticCalls = new Map< string, { - input?: { envelope: PrpSemanticToolEnvelope; index: number }; - result?: { envelope: PrpSemanticToolEnvelope; index: number }; + input?: SemanticCallBinding; + result?: SemanticCallBinding; + reconciled?: SemanticCallBinding; } >(); fixture.events.forEach((event, index) => { @@ -283,32 +289,77 @@ function bindingIssues(fixture: PrpFixture): ProtocolValidationIssue[] { message: `semantic_tool call ${semanticTool.callId} must contain exactly one ${phase} envelope`, }); } else { - call[phase] = { envelope: semanticTool, index }; + call[phase] = { + envelope: semanticTool, + index, + }; semanticCalls.set(semanticTool.callId, call); } } }); for (const [callId, call] of semanticCalls) { - if (call.input === undefined || call.result === undefined) { - const present = call.input ?? call.result; + const terminalPhaseCount = + Number(call.result !== undefined) + Number(call.reconciled !== undefined); + if (call.input === undefined || terminalPhaseCount !== 1) { + const present = call.input ?? call.result ?? call.reconciled; issues.push({ code: "binding_mismatch", path: `/events/${present?.index ?? 0}/payload/semantic_tool/callId`, - message: `semantic_tool call ${callId} must contain one input and one result envelope`, + message: `semantic_tool call ${callId} must contain one input and exactly one result or reconciled envelope`, }); continue; } - for (const field of ["operationId", "idempotencyKey"] as const) { - if ( - canonicalJson(call.input.envelope[field]) !== - canonicalJson(call.result.envelope[field]) - ) { - issues.push({ - code: "binding_mismatch", - path: `/events/${call.result.index}/payload/semantic_tool/${field}`, - message: `semantic_tool result ${field} must match its input envelope`, - }); + if (call.result !== undefined) { + for (const field of [ + "operationId", + "idempotencyKey", + "correlation", + ] as const) { + if ( + canonicalJson(call.input.envelope[field]) !== + canonicalJson(call.result.envelope[field]) + ) { + issues.push({ + code: "binding_mismatch", + path: `/events/${call.result.index}/payload/semantic_tool/${field}`, + message: `semantic_tool result ${field} must match its input envelope`, + }); + } + } + } + if (call.reconciled !== undefined) { + // A replacement runner may reconcile a call after recovering the run. + // The authenticated ingestion boundary owns runner authorization, while + // replay keeps each event's sourceInstanceId as immutable provenance. + for (const field of ["operationId", "idempotencyKey"] as const) { + if ( + canonicalJson(call.input.envelope[field]) !== + canonicalJson(call.reconciled.envelope[field]) + ) { + issues.push({ + code: "binding_mismatch", + path: `/events/${call.reconciled.index}/payload/semantic_tool/${field}`, + message: `semantic_tool reconciled ${field} must match its input envelope`, + }); + } + } + for (const field of [ + "runId", + "normalizedSessionId", + "turnId", + "itemId", + ] as const) { + if ( + call.input.envelope.correlation[field] !== + call.reconciled.envelope.correlation[field] + ) { + issues.push({ + code: "binding_mismatch", + path: `/events/${call.reconciled.index}/payload/semantic_tool/correlation/${field}`, + message: `semantic_tool reconciled correlation ${field} must match its input envelope`, + }); + } } } } diff --git a/packages/paperclip-runner/src/protocol/semantic-tool-receipts.test.ts b/packages/paperclip-runner/src/protocol/semantic-tool-receipts.test.ts new file mode 100644 index 0000000000..ad63145d83 --- /dev/null +++ b/packages/paperclip-runner/src/protocol/semantic-tool-receipts.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from "vitest"; + +import { validatePrpEvent, type PrpEvent } from "./replay-contract.js"; +import { + createPrpBudgetStopReason, + createPrpSemanticToolInputEnvelope, + createPrpSemanticToolReconciledEnvelope, + createPrpSemanticToolResultEnvelope, + semanticAuthorizationBoundaryForCode, +} from "./semantic-tool-receipts.js"; + +const correlation = { + runId: "run_receipt_test", + normalizedSessionId: "session_receipt_test", + turnId: "turn_receipt_test", + itemId: "item_receipt_test", +}; + +describe("PRP provider-neutral semantic receipts", () => { + it("digests raw content and retains only allowlisted references", () => { + const envelope = createPrpSemanticToolInputEnvelope({ + operationId: "get_task_context", + callId: "call_receipt_test", + correlation, + content: { authorization: "Bearer secret-token", apiKey: "do-not-emit" }, + references: [{ kind: "task", id: "issue_receipt_test" }], + }); + + expect(envelope.content).toMatchObject({ + digest: expect.stringMatching(/^sha256:[a-f0-9]{64}$/), + redactionDisposition: "digest_only", + references: [{ kind: "task", id: "issue_receipt_test" }], + }); + expect(JSON.stringify(envelope)).not.toContain("secret-token"); + expect(JSON.stringify(envelope)).not.toContain("do-not-emit"); + }); + + it("gives exact retries the same operation receipt without provider identifiers", () => { + const base = { + operationId: "write_document", + correlation, + idempotencyKey: "write_once", + content: { revisionId: "revision_1" }, + outcome: "succeeded" as const, + code: "ok", + retryable: false, + authorizationBoundary: "revision" as const, + }; + const first = createPrpSemanticToolResultEnvelope({ ...base, callId: "call_first" }); + const retry = createPrpSemanticToolResultEnvelope({ ...base, callId: "call_retry" }); + + expect(retry.operationReceiptId).toBe(first.operationReceiptId); + expect(retry.operationReceiptId).toMatch(/^semantic_receipt:[a-f0-9]{32}$/); + expect(retry).not.toHaveProperty("provider"); + }); + + it("validates semantic input/result events and a terminal stop receipt", () => { + const input = createPrpSemanticToolInputEnvelope({ + operationId: "get_task_context", + callId: "call_receipt_test", + correlation, + content: {}, + }); + const result = createPrpSemanticToolResultEnvelope({ + operationId: "get_task_context", + callId: "call_receipt_test", + correlation, + content: {}, + outcome: "succeeded", + code: "ok", + retryable: false, + authorizationBoundary: "active_task", + }); + + expect(validatePrpEvent(event("mcp_app.tool_input", input)).ok).toBe(true); + expect(validatePrpEvent(event("mcp_app.tool_result", result)).ok).toBe(true); + expect( + createPrpBudgetStopReason({ + receiptId: "stop_receipt_test", + kind: "cost", + code: "run_cost_limit", + retryable: true, + decisionId: "decision_receipt_test", + limitClass: "run_cost", + aggregate: { unit: "usd_micros", observed: 100, limit: 100, window: "run" }, + }), + ).toMatchObject({ + schema: "paperclip.prp.stop_reason.v1", + kind: "cost", + decisionId: "decision_receipt_test", + }); + }); + + it("requires a complete terminal receipt when a pending tool call is reconciled", () => { + const semanticTool = createPrpSemanticToolReconciledEnvelope({ + operationId: "get_task_context", + callId: "call_reconciled_test", + correlation, + content: {}, + outcome: "succeeded", + code: "recovered_receipt", + retryable: false, + authorizationBoundary: "active_task", + }); + const reconciled = event("semantic_tool.reconciled", semanticTool); + + expect(validatePrpEvent(reconciled).ok).toBe(true); + expect( + validatePrpEvent({ + ...reconciled, + payload: { + semantic_tool: { + ...semanticTool, + outcome: undefined, + }, + }, + }).ok, + ).toBe(false); + }); + + it("maps denial and conflict codes onto protocol authorization boundaries", () => { + expect(semanticAuthorizationBoundaryForCode("company_scope_denied")).toBe("company"); + expect(semanticAuthorizationBoundaryForCode("actor_role_denied")).toBe("actor"); + expect(semanticAuthorizationBoundaryForCode("document_revision_conflict")).toBe("revision"); + expect(semanticAuthorizationBoundaryForCode("approval_required")).toBe("governed_action"); + }); +}); + +function event( + eventType: "mcp_app.tool_input" | "mcp_app.tool_result" | "semantic_tool.reconciled", + semanticTool: Record, +): PrpEvent { + return { + schema: "paperclip.prp.event.v1", + sourceEventId: `${eventType}:receipt_test`, + sourceSeq: eventType === "mcp_app.tool_input" ? 1 : eventType === "mcp_app.tool_result" ? 2 : 3, + sourceInstanceId: "runner_receipt_test", + sourceKind: "runner", + runId: correlation.runId, + normalizedSessionId: correlation.normalizedSessionId, + turnId: correlation.turnId, + itemId: correlation.itemId, + eventType, + schemaVersion: 1, + priority: 1, + emittedAt: "2026-08-11T12:00:00.000Z", + payload: { semantic_tool: semanticTool }, + } as PrpEvent; +} diff --git a/packages/paperclip-runner/src/protocol/semantic-tool-receipts.ts b/packages/paperclip-runner/src/protocol/semantic-tool-receipts.ts new file mode 100644 index 0000000000..52af236914 --- /dev/null +++ b/packages/paperclip-runner/src/protocol/semantic-tool-receipts.ts @@ -0,0 +1,253 @@ +import { createHash } from "node:crypto"; + +import type { + PrpEvent, + PrpSemanticToolEnvelope, + PrpStopReason, +} from "./replay-contract.js"; + +export type PrpSemanticAuthorizationBoundary = + | "company" + | "actor" + | "active_task" + | "grant" + | "governed_action" + | "lock" + | "revision"; + +export type PrpSemanticToolOutcome = + | "succeeded" + | "denied" + | "conflict" + | "duplicate" + | "unavailable" + | "failed"; + +export interface PrpSemanticCorrelation { + runId: string; + normalizedSessionId: string; + turnId: string; + itemId: string; + requestId?: string; +} + +export interface PrpSemanticSafeReference { + kind: + | "task" + | "document_revision" + | "interaction" + | "approval" + | "decision" + | "artifact" + | "work_product" + | "wake" + | "monitor" + | "audit" + | "operation"; + id: string; +} + +export interface PrpSemanticImmutableTarget { + kind: "document_revision" | "interaction" | "approval" | "decision"; + id: string; + immutable: true; + revisionId?: string; + decisionId?: string; +} + +interface SemanticEnvelopeBaseInput { + operationId: string; + callId: string; + correlation: PrpSemanticCorrelation; + idempotencyKey?: string | null; + content: unknown; + references?: readonly PrpSemanticSafeReference[]; + redactionDisposition?: "digest_only" | "allowlisted_references" | "redacted"; +} + +export interface CreatePrpSemanticToolResultInput extends SemanticEnvelopeBaseInput { + outcome: PrpSemanticToolOutcome; + code: string; + retryable: boolean; + authorizationBoundary: PrpSemanticAuthorizationBoundary; + operationReceiptId?: string; + auditReceiptId?: string; + currentRevision?: string | number; + duplicateOfReceiptId?: string; + artifactRefs?: readonly PrpSemanticSafeReference[]; + targets?: readonly PrpSemanticImmutableTarget[]; + causalRefs?: readonly PrpSemanticSafeReference[]; +} + +/** + * Creates a safe input envelope: raw content is reduced to a canonical digest, + * while callers may add only typed, non-secret references. + */ +export function createPrpSemanticToolInputEnvelope( + input: SemanticEnvelopeBaseInput, +): PrpSemanticToolEnvelope { + return { + schema: "paperclip.prp.semantic_tool.v1", + schemaVersion: 1, + phase: "input", + operationId: input.operationId, + callId: input.callId, + correlation: { ...input.correlation }, + idempotencyKey: input.idempotencyKey ?? null, + content: safeContent(input), + } as PrpSemanticToolEnvelope; +} + +/** Creates the provider-neutral semantic result receipt carried on PRP. */ +export function createPrpSemanticToolResultEnvelope( + input: CreatePrpSemanticToolResultInput, +): PrpSemanticToolEnvelope { + return createPrpSemanticToolTerminalEnvelope(input, "result"); +} + +/** Creates a complete terminal receipt for an outcome recovered after restart. */ +export function createPrpSemanticToolReconciledEnvelope( + input: CreatePrpSemanticToolResultInput, +): PrpSemanticToolEnvelope { + return createPrpSemanticToolTerminalEnvelope(input, "reconciled"); +} + +function createPrpSemanticToolTerminalEnvelope( + input: CreatePrpSemanticToolResultInput, + phase: "result" | "reconciled", +): PrpSemanticToolEnvelope { + const operationReceiptId = input.operationReceiptId ?? derivedReceiptId(input); + return { + schema: "paperclip.prp.semantic_tool.v1", + schemaVersion: 1, + phase, + operationId: input.operationId, + callId: input.callId, + correlation: { ...input.correlation }, + idempotencyKey: input.idempotencyKey ?? null, + content: safeContent(input), + outcome: input.outcome, + code: input.code, + retryable: input.retryable, + authorizationBoundary: input.authorizationBoundary, + operationReceiptId, + ...(input.auditReceiptId === undefined ? {} : { auditReceiptId: input.auditReceiptId }), + ...(input.currentRevision === undefined ? {} : { currentRevision: input.currentRevision }), + ...(input.duplicateOfReceiptId === undefined + ? {} + : { duplicateOfReceiptId: input.duplicateOfReceiptId }), + ...(input.artifactRefs === undefined ? {} : { artifactRefs: [...input.artifactRefs] }), + ...(input.targets === undefined ? {} : { targets: [...input.targets] }), + ...(input.causalRefs === undefined ? {} : { causalRefs: [...input.causalRefs] }), + } as PrpSemanticToolEnvelope; +} + +export function createPrpBudgetStopReason(input: { + receiptId: string; + kind: "budget" | "cost"; + code: string; + retryable: boolean; + decisionId: string; + limitClass: "company_monthly" | "actor_monthly" | "project_monthly" | "run_cost" | "provider_quota"; + aggregate: { + unit: "cents" | "usd_micros" | "tokens"; + observed: number; + limit: number; + window: "run" | "monthly_utc" | "provider_window"; + }; + relatedReceiptIds?: readonly string[]; +}): PrpStopReason { + return { + schema: "paperclip.prp.stop_reason.v1", + schemaVersion: 1, + receiptId: input.receiptId, + kind: input.kind, + code: input.code, + retryable: input.retryable, + decisionId: input.decisionId, + limitClass: input.limitClass, + aggregate: { ...input.aggregate }, + ...(input.relatedReceiptIds === undefined + ? {} + : { relatedReceiptIds: [...input.relatedReceiptIds] }), + } as PrpStopReason; +} + +/** Returns only typed result receipts; optional provider payload stays ignored. */ +export function prpSemanticToolResultReceipts( + events: readonly PrpEvent[], +): PrpSemanticToolEnvelope[] { + return events.flatMap((event) => { + if (event.eventType !== "mcp_app.tool_result") return []; + const payload = asRecord(event.payload); + const envelope = asRecord(payload?.semantic_tool); + if ( + envelope?.schema !== "paperclip.prp.semantic_tool.v1" + || envelope.schemaVersion !== 1 + || envelope.phase !== "result" + ) return []; + return [structuredClone(envelope) as PrpSemanticToolEnvelope]; + }); +} + +export function semanticAuthorizationBoundaryForCode( + code: string, +): PrpSemanticAuthorizationBoundary { + if (code.includes("company")) return "company"; + if (code.includes("actor") || code.includes("role")) return "actor"; + if (code.includes("claim") || code.includes("exposed")) return "grant"; + if (code.includes("revision") || code.includes("idempotency_conflict")) return "revision"; + if (code.includes("lock") || code.includes("ownership")) return "lock"; + if (code.includes("approval") || code.includes("interaction") || code.includes("governed")) { + return "governed_action"; + } + return "active_task"; +} + +export function semanticOutcomeForCode(input: { + ok: boolean; + code: string; + disposition?: unknown; +}): PrpSemanticToolOutcome { + if (input.ok) return input.disposition === "duplicate" ? "duplicate" : "succeeded"; + if (input.code.includes("conflict") || input.code.includes("revision")) return "conflict"; + if (input.code.includes("unavailable") || input.code.includes("not_exposed")) return "unavailable"; + return "denied"; +} + +export function digestPrpSemanticContent(value: unknown): string { + return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`; +} + +function safeContent(input: SemanticEnvelopeBaseInput) { + return { + digest: digestPrpSemanticContent(input.content), + redactionDisposition: input.redactionDisposition ?? "digest_only", + references: [...(input.references ?? [])], + }; +} + +function derivedReceiptId(input: CreatePrpSemanticToolResultInput): string { + const identity = input.idempotencyKey === undefined || input.idempotencyKey === null + ? `${input.correlation.runId}:${input.operationId}:${input.callId}` + : `${input.correlation.runId}:${input.operationId}:${input.idempotencyKey}`; + return `semantic_receipt:${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`; +} + +function asRecord(value: unknown): Record | null { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? value as Record + : null; +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + const record = asRecord(value); + if (record !== null) { + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) + .join(",")}}`; + } + return JSON.stringify(value) ?? "null"; +}