diff --git a/Dockerfile b/Dockerfile index c41520df32..0e984a1640 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY packages/adapter-utils/package.json packages/adapter-utils/ COPY packages/google-sheets-mcp-server/package.json packages/google-sheets-mcp-server/ COPY packages/kv-demo-mcp-server/package.json packages/kv-demo-mcp-server/ COPY packages/mcp-server/package.json packages/mcp-server/ +COPY packages/paperclip-runner/package.json packages/paperclip-runner/ COPY packages/skills-catalog/package.json packages/skills-catalog/ COPY packages/tailscale-https-broker/package.json packages/tailscale-https-broker/ COPY packages/teams-catalog/package.json packages/teams-catalog/ diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md new file mode 100644 index 0000000000..efb874ce07 --- /dev/null +++ b/packages/paperclip-runner/README.md @@ -0,0 +1,27 @@ +# Paperclip Runner + +This private workspace package contains the staged Paperclip Runner work. + +This change adds only the language-neutral PRP v1 contract. It does not add a +runtime, a server adapter, or a public package export. The schemas and fixtures +are safe to review and merge before production behavior exists. + +The first provider scope is Codex. The protocol contains provider-neutral event +and semantic receipt shapes, but their presence does not authorize a tool or +enable a provider. + +Run the protocol gate with: + +```sh +pnpm --filter @paperclipai/paperclip-runner check:protocol +``` + +Use `generate:protocol-manifest` after a schema or fixture change. Commit the +updated manifest with its source change. Do not edit the manifest by hand. +The gate compiles every schema with AJV 2020-12. It validates accepted replay, +question, and cross-language conformance fixtures against the declared schemas. +It also proves that the unsupported required-version fixture is rejected. + +The next pull request will add TypeScript validation and replay behavior. The +public root and `./testing` exports will arrive with the package boundary pull +request. diff --git a/packages/paperclip-runner/package.json b/packages/paperclip-runner/package.json new file mode 100644 index 0000000000..c289744ee4 --- /dev/null +++ b/packages/paperclip-runner/package.json @@ -0,0 +1,21 @@ +{ + "name": "@paperclipai/paperclip-runner", + "version": "0.0.0", + "private": true, + "license": "MIT", + "engines": { + "node": ">=24.11.0" + }, + "type": "module", + "scripts": { + "build": "pnpm run check:protocol-manifest", + "typecheck": "node --check scripts/protocol-contract.mjs && node --check scripts/generate-protocol-manifest.mjs", + "test": "node --test test/protocol-contract.test.mjs", + "generate:protocol-manifest": "node scripts/generate-protocol-manifest.mjs", + "check:protocol-manifest": "node scripts/generate-protocol-manifest.mjs --check", + "check:protocol": "pnpm run typecheck && pnpm run check:protocol-manifest && pnpm test" + }, + "devDependencies": { + "ajv": "^8.20.0" + } +} diff --git a/packages/paperclip-runner/protocol/README.md b/packages/paperclip-runner/protocol/README.md new file mode 100644 index 0000000000..405c2bbd34 --- /dev/null +++ b/packages/paperclip-runner/protocol/README.md @@ -0,0 +1,32 @@ +# PRP v1 Contract + +The JSON Schema files in `schemas/` are the language-neutral source of truth for +Paperclip Runner Protocol version 1. The fixtures in `fixtures/` define accepted +and rejected compatibility cases. + +## Compatibility + +- `protocolVersion`, `fixtureVersion`, and `event.schemaVersion` are required. +- A consumer fails closed when a required version or schema discriminator is + not supported. +- A v1 envelope can contain an unknown optional property when its schema marks + that object as extensible. +- A consumer ignores an unknown optional property until a later contract gives + it meaning. +- A required field, enum value, or typed structured-input field is not optional. +- Question and answer identifiers are stable across the provider boundary. + +The `unknown-optional-fields.json` fixture must be accepted. The +`unsupported-required-version.json` fixture must be rejected. + +## Scope + +The first provider descriptor and adapter fixture cover Codex only. The schemas +for provider-neutral events and semantic receipts do not enable those actions. +Discovery and authorization are separate contracts. + +The conformance manifest records every source file and its SHA-256 digest. Run +`pnpm generate:protocol-manifest` from this package after a source change. CI +runs the same generator with `--check` to reject drift. This check also compiles +the JSON Schemas and validates every replay, question, and cross-language +conformance fixture against its declared schema. diff --git a/packages/paperclip-runner/protocol/fixtures/conformance-expected-output.json b/packages/paperclip-runner/protocol/fixtures/conformance-expected-output.json new file mode 100644 index 0000000000..6dc9bc9aff --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/conformance-expected-output.json @@ -0,0 +1 @@ +{"schemaVersion":"paperclip.runner.conformance.output.v1","runIdentity":{"runId":"run_conformance_0001","sessionId":"session_conformance_0001"},"result":{"status":"succeeded","summary":"Standalone Conformance fixture accepted."}} diff --git a/packages/paperclip-runner/protocol/fixtures/conformance-minimal-run.json b/packages/paperclip-runner/protocol/fixtures/conformance-minimal-run.json new file mode 100644 index 0000000000..ad6a9fc579 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/conformance-minimal-run.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": "paperclip.runner.conformance.fixture.v1", + "run": { + "runId": "run_conformance_0001", + "sessionId": "session_conformance_0001", + "companyId": "company_conformance", + "issueId": "issue_conformance", + "agentId": "agent_conformance" + }, + "events": [ + { + "eventId": "event_conformance_0001", + "runId": "run_conformance_0001", + "sequence": 1, + "type": "run.started" + }, + { + "eventId": "event_conformance_0002", + "runId": "run_conformance_0001", + "sequence": 2, + "type": "run.completed" + } + ], + "result": { + "runId": "run_conformance_0001", + "status": "succeeded", + "summary": "Standalone Conformance fixture accepted." + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/questions/codex.json b/packages/paperclip-runner/protocol/fixtures/questions/codex.json new file mode 100644 index 0000000000..d1ec06850f --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/questions/codex.json @@ -0,0 +1,40 @@ +{ + "schema": "paperclip.question_adapter_fixture.v1", + "adapter": "codex", + "nativeRequest": { + "method": "item/tool/requestUserInput", + "params": { + "title": "Deployment input", + "questions": [{ + "id": "environment", + "header": "Environment", + "question": "Where should we deploy?", + "options": [ + { "label": "Staging", "description": "Deploy to staging first." }, + { "label": "Production", "description": "Deploy directly to production." } + ] + }] + } + }, + "canonicalQuestionSet": { + "schema": "paperclip.question_set.v1", + "title": "Deployment input", + "submitLabel": "Submit answers", + "questions": [{ + "id": "environment", + "header": "Environment", + "prompt": "Where should we deploy?", + "required": true, + "answerMode": "single_select", + "options": [ + { "id": "option-1", "label": "Staging", "description": "Deploy to staging first." }, + { "id": "option-2", "label": "Production", "description": "Deploy directly to production." } + ] + }] + }, + "canonicalResponse": { + "schema": "paperclip.question_response.v1", + "answers": { "environment": { "selectedOptionIds": ["option-1"] } } + }, + "nativeResponse": { "answers": { "environment": { "answers": ["Staging"] } } } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/duplicate-event.json b/packages/paperclip-runner/protocol/fixtures/replay/duplicate-event.json new file mode 100644 index 0000000000..4555eb448a --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/duplicate-event.json @@ -0,0 +1,30 @@ +{ + "schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1, + "name": "Duplicate event", "description": "At-least-once delivery repeats one source event without repeating its effect.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_replay", "issueId": "issue_replay_duplicate", + "runId": "run_replay_duplicate", "environmentLeaseId": "lease_replay_duplicate", "runnerInstanceId": "runner_replay", + "normalizedSessionId": "session_replay_duplicate", "driverSessionId": "driver_replay_duplicate" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.0.0" }, + "steer": false, "interrupt": true, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true + }, + "commands": [ + { "schema": "paperclip.prp.command.v1", "commandId": "command_duplicate_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:30:00.000Z", "payload": {} } + ], + "events": [ + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_01", "sourceSeq": 1, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.010Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_02", "sourceSeq": 2, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "turnId": "turn_replay_duplicate", "eventType": "turn.started", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.020Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_03", "sourceSeq": 3, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "turnId": "turn_replay_duplicate", "itemId": "item_replay_duplicate", "eventType": "item.completed", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.030Z", "payload": { "kind": "assistant_message", "text": "Exactly one projection." } }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_03", "sourceSeq": 3, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "turnId": "turn_replay_duplicate", "itemId": "item_replay_duplicate", "eventType": "item.completed", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:30:00.030Z", "payload": { "kind": "assistant_message", "text": "Exactly one projection." } }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_04", "sourceSeq": 4, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.040Z", + "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Duplicate delivery was idempotent.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } + }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_05", "sourceSeq": 5, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "turnId": "turn_replay_duplicate", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.050Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_duplicate_06", "sourceSeq": 6, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_duplicate", "normalizedSessionId": "session_replay_duplicate", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:30:00.060Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done" } } + ], + "requests": [], + "result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Duplicate delivery was idempotent.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/failed-run.json b/packages/paperclip-runner/protocol/fixtures/replay/failed-run.json new file mode 100644 index 0000000000..8327cd5f5d --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/failed-run.json @@ -0,0 +1,106 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Failed run", + "description": "The driver reports an item and turn failure before the run terminalizes.", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_replay", + "issueId": "issue_replay_failed", + "runId": "run_replay_failed", + "environmentLeaseId": "lease_replay_failed", + "runnerInstanceId": "runner_replay", + "normalizedSessionId": "session_replay_failed", + "driverSessionId": "driver_replay_failed" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { "kind": "fake", "version": "1.0.0" }, + "steer": true, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true + }, + "commands": [ + { + "schema": "paperclip.prp.command.v1", + "commandId": "command_failed_prepare", + "controllerSeq": 1, + "type": "run.prepare", + "issuedAt": "2026-08-07T12:10:00.000Z", + "payload": {} + }, + { + "schema": "paperclip.prp.command.v1", + "commandId": "command_failed_turn", + "controllerSeq": 2, + "type": "turn.start", + "issuedAt": "2026-08-07T12:10:00.010Z", + "payload": { "text": "Run failing scenario" } + } + ], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_01", "sourceSeq": 1, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "eventType": "session.started", "schemaVersion": 1, + "priority": 0, "emittedAt": "2026-08-07T12:10:00.020Z", "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_02", "sourceSeq": 2, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "turnId": "turn_replay_failed", + "eventType": "turn.started", "schemaVersion": 1, "priority": 1, + "emittedAt": "2026-08-07T12:10:00.030Z", "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_03", "sourceSeq": 3, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "turnId": "turn_replay_failed", "itemId": "item_failed_command", + "eventType": "item.failed", "schemaVersion": 1, "priority": 0, + "emittedAt": "2026-08-07T12:10:00.040Z", "payload": { "kind": "command", "message": "Command exited with code 1" } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_04", "sourceSeq": 4, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "turnId": "turn_replay_failed", + "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, + "emittedAt": "2026-08-07T12:10:00.050Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", + "summary": "The scripted command failed and needs a retry.", + "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Retry the failed command", "blocksCompletion": true }] }, + "evidence": [], "verification": [{ "commandOrCheck": "fake command", "status": "failed" }], + "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "retry", "summary": "Retry after correcting the command", "idempotencyKey": "retry_replay_failed" } + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_05", "sourceSeq": 5, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "turnId": "turn_replay_failed", + "eventType": "turn.failed", "schemaVersion": 1, "priority": 0, + "emittedAt": "2026-08-07T12:10:00.060Z", "payload": { "message": "Fake driver failure" } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_failed_06", "sourceSeq": 6, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_failed", + "normalizedSessionId": "session_replay_failed", "eventType": "run.terminal", "schemaVersion": 1, + "priority": 0, "emittedAt": "2026-08-07T12:10:00.070Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "failed", "runTerminalState": "failed", "reportedWorkDisposition": "yielded" } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", + "summary": "The scripted command failed and needs a retry.", + "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Retry the failed command", "blocksCompletion": true }] }, + "evidence": [], "verification": [{ "commandOrCheck": "fake command", "status": "failed" }], + "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "retry", "summary": "Retry after correcting the command", "idempotencyKey": "retry_replay_failed" } + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/happy-path.json b/packages/paperclip-runner/protocol/fixtures/replay/happy-path.json new file mode 100644 index 0000000000..c5f656d22e --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/happy-path.json @@ -0,0 +1,230 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Happy path", + "description": "A typed assistant message completes and the run proposes a successful result.", + "identity": { + "schema": "paperclip.prp.identity.v1", + "companyId": "company_replay", + "issueId": "issue_replay_happy", + "runId": "run_replay_happy", + "environmentLeaseId": "lease_replay_happy", + "runnerInstanceId": "runner_replay", + "normalizedSessionId": "session_replay_happy", + "driverSessionId": "driver_replay_happy", + "providerSessionId": "provider_replay_happy" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", + "sessionReusePolicy": "new_per_run", + "driver": { "kind": "fake", "version": "1.0.0" }, + "steer": true, + "interrupt": true, + "resume": true, + "runtimeRequests": true, + "structuredResult": true, + "typedEvents": true, + "unsupported": [] + }, + "commands": [ + { + "schema": "paperclip.prp.command.v1", + "commandId": "command_happy_prepare", + "controllerSeq": 1, + "type": "run.prepare", + "issuedAt": "2026-08-07T12:00:00.000Z", + "payload": { "workspace": "fixture" } + }, + { + "schema": "paperclip.prp.command.v1", + "commandId": "command_happy_session", + "controllerSeq": 2, + "type": "session.open", + "issuedAt": "2026-08-07T12:00:00.010Z", + "payload": { "reuse": "new_per_run" } + }, + { + "schema": "paperclip.prp.command.v1", + "commandId": "command_happy_turn", + "controllerSeq": 3, + "type": "turn.start", + "issuedAt": "2026-08-07T12:00:00.020Z", + "payload": { "text": "Inspect the fixture" } + } + ], + "events": [ + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_01", + "sourceSeq": 1, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "eventType": "runtime.phase.changed", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-07T12:00:00.030Z", + "payload": { "phase": "workspace_preparing" } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_02", + "sourceSeq": 2, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "eventType": "session.started", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-07T12:00:00.040Z", + "payload": { "driverSessionId": "driver_replay_happy" } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_03", + "sourceSeq": 3, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "eventType": "turn.started", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-07T12:00:00.050Z", + "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_04", + "sourceSeq": 4, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "itemId": "item_replay_message", + "eventType": "item.started", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-07T12:00:00.060Z", + "payload": { "kind": "assistant_message", "text": "" } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_05", + "sourceSeq": 5, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "itemId": "item_replay_message", + "eventType": "item.delta", + "schemaVersion": 1, + "priority": 2, + "emittedAt": "2026-08-07T12:00:00.070Z", + "payload": { "text": "Validated fixture. " } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_06", + "sourceSeq": 6, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "itemId": "item_replay_message", + "eventType": "item.completed", + "schemaVersion": 1, + "priority": 1, + "emittedAt": "2026-08-07T12:00:00.080Z", + "payload": { "kind": "assistant_message", "text": "Validated fixture. Replay complete." } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_07", + "sourceSeq": 7, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "eventType": "run.result.proposed", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-07T12:00:00.090Z", + "payload": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The scripted run completed successfully.", + "completionClaim": { + "contractRevision": "replay-contract-v1", + "objectiveSatisfied": true, + "criteria": [], + "remainingWork": [] + }, + "evidence": [], + "verification": [{ "commandOrCheck": "fixture replay", "status": "passed" }], + "attentionRequests": [], + "artifacts": [] + } + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_08", + "sourceSeq": 8, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "turnId": "turn_replay_happy", + "eventType": "turn.completed", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-07T12:00:00.100Z", + "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", + "sourceEventId": "event_happy_09", + "sourceSeq": 9, + "sourceInstanceId": "runner_replay", + "sourceKind": "runner", + "runId": "run_replay_happy", + "normalizedSessionId": "session_replay_happy", + "eventType": "run.terminal", + "schemaVersion": 1, + "priority": 0, + "emittedAt": "2026-08-07T12:00:00.110Z", + "payload": { + "schema": "paperclip.prp.terminal.v1", + "turnTerminalState": "completed", + "runTerminalState": "succeeded", + "reportedWorkDisposition": "done", + "workAssessmentId": "assessment_replay_happy", + "statusDecisionId": "decision_replay_happy" + } + } + ], + "requests": [], + "result": { + "schema": "paperclip.run_result.v1", + "reportedWorkDisposition": "done", + "summary": "The scripted run completed successfully.", + "completionClaim": { + "contractRevision": "replay-contract-v1", + "objectiveSatisfied": true, + "criteria": [], + "remainingWork": [] + }, + "evidence": [], + "verification": [{ "commandOrCheck": "fixture replay", "status": "passed" }], + "attentionRequests": [], + "artifacts": [] + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/interrupted-run.json b/packages/paperclip-runner/protocol/fixtures/replay/interrupted-run.json new file mode 100644 index 0000000000..26bf4ed3aa --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/interrupted-run.json @@ -0,0 +1,90 @@ +{ + "schema": "paperclip.prp.fixture.v1", + "fixtureVersion": 1, + "protocolVersion": 1, + "name": "Interrupted run", + "description": "A pending runtime permission request is followed by an explicit turn interruption.", + "identity": { + "schema": "paperclip.prp.identity.v1", "companyId": "company_replay", "issueId": "issue_replay_interrupted", + "runId": "run_replay_interrupted", "environmentLeaseId": "lease_replay_interrupted", + "runnerInstanceId": "runner_replay", "normalizedSessionId": "session_replay_interrupted", + "driverSessionId": "driver_replay_interrupted" + }, + "capabilities": { + "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "reuse_per_issue", + "driver": { "kind": "fake", "version": "1.0.0" }, "steer": true, "interrupt": true, + "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true + }, + "commands": [ + { + "schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_prepare", "controllerSeq": 1, + "type": "run.prepare", "issuedAt": "2026-08-07T12:20:00.000Z", "payload": {} + }, + { + "schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_turn", "controllerSeq": 2, + "type": "turn.start", "issuedAt": "2026-08-07T12:20:00.010Z", "payload": { "text": "Wait for interruption" } + }, + { + "schema": "paperclip.prp.command.v1", "commandId": "command_interrupted_interrupt", "controllerSeq": 3, + "type": "turn.interrupt", "issuedAt": "2026-08-07T12:20:00.050Z", "payload": { "reason": "operator" } + } + ], + "events": [ + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_01", "sourceSeq": 1, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "eventType": "session.started", "schemaVersion": 1, + "priority": 0, "emittedAt": "2026-08-07T12:20:00.020Z", "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_02", "sourceSeq": 2, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "turnId": "turn_replay_interrupted", + "eventType": "turn.started", "schemaVersion": 1, "priority": 1, + "emittedAt": "2026-08-07T12:20:00.030Z", "payload": {} + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_03", "sourceSeq": 3, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "turnId": "turn_replay_interrupted", + "eventType": "runtime_request.created", "schemaVersion": 1, "priority": 0, + "emittedAt": "2026-08-07T12:20:00.040Z", + "payload": { "request": { "schema": "paperclip.runtime_request.v1", "requestKind": "runtime", "requestId": "request_interrupted_permission", "type": "permission", "status": "pending", "prompt": "Allow the fake command?" } } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_04", "sourceSeq": 4, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "turnId": "turn_replay_interrupted", + "eventType": "turn.interrupted", "schemaVersion": 1, "priority": 0, + "emittedAt": "2026-08-07T12:20:00.060Z", "payload": { "reason": "operator" } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_05", "sourceSeq": 5, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "eventType": "run.result.proposed", "schemaVersion": 1, + "priority": 0, "emittedAt": "2026-08-07T12:20:00.070Z", + "payload": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The turn was interrupted.", + "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Resume the interrupted turn", "blocksCompletion": true }] }, + "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "same_agent", "summary": "Resume in the same normalized session", "idempotencyKey": "resume_replay_interrupted" } + } + }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_interrupted_06", "sourceSeq": 6, + "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_interrupted", + "normalizedSessionId": "session_replay_interrupted", "eventType": "run.terminal", "schemaVersion": 1, + "priority": 0, "emittedAt": "2026-08-07T12:20:00.080Z", + "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "interrupted", "runTerminalState": "cancelled", "reportedWorkDisposition": "yielded" } + } + ], + "requests": [ + { "schema": "paperclip.runtime_request.v1", "requestKind": "runtime", "requestId": "request_interrupted_permission", "type": "permission", "status": "pending", "prompt": "Allow the fake command?" } + ], + "result": { + "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "yielded", "summary": "The turn was interrupted.", + "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Resume the interrupted turn", "blocksCompletion": true }] }, + "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], + "continuation": { "kind": "same_agent", "summary": "Resume in the same normalized session", "idempotencyKey": "resume_replay_interrupted" } + } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/source-gap.json b/packages/paperclip-runner/protocol/fixtures/replay/source-gap.json new file mode 100644 index 0000000000..130e88e22d --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/source-gap.json @@ -0,0 +1,19 @@ +{ + "schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1, + "name": "Source sequence gap", "description": "The runner stream skips source sequence 3 and replay records the missing cursor.", + "identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_replay", "issueId": "issue_replay_gap", "runId": "run_replay_gap", "environmentLeaseId": "lease_replay_gap", "runnerInstanceId": "runner_replay", "normalizedSessionId": "session_replay_gap", "driverSessionId": "driver_replay_gap" }, + "capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.0.0" }, "steer": false, "interrupt": false, "resume": true, "runtimeRequests": false, "structuredResult": true, "typedEvents": true }, + "commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_gap_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:40:00.000Z", "payload": {} }], + "events": [ + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_01", "sourceSeq": 1, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_gap", "normalizedSessionId": "session_replay_gap", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.010Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_02", "sourceSeq": 2, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_gap", "normalizedSessionId": "session_replay_gap", "turnId": "turn_replay_gap", "eventType": "turn.started", "schemaVersion": 1, "priority": 1, "emittedAt": "2026-08-07T12:40:00.020Z", "payload": {} }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_04", "sourceSeq": 4, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_gap", "normalizedSessionId": "session_replay_gap", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.040Z", + "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", "summary": "The source gap requires reconciliation.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Replay missing source event", "blocksCompletion": true }] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } + }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_05", "sourceSeq": 5, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_gap", "normalizedSessionId": "session_replay_gap", "turnId": "turn_replay_gap", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.050Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_gap_06", "sourceSeq": 6, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_gap", "normalizedSessionId": "session_replay_gap", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:40:00.060Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "needs_review" } } + ], + "requests": [], + "result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "needs_review", "summary": "The source gap requires reconciliation.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": false, "criteria": [], "remainingWork": [{ "description": "Replay missing source event", "blocksCompletion": true }] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/unknown-optional-fields.json b/packages/paperclip-runner/protocol/fixtures/replay/unknown-optional-fields.json new file mode 100644 index 0000000000..0a51d2ab44 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/unknown-optional-fields.json @@ -0,0 +1,19 @@ +{ + "schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 1, + "name": "Unknown optional fields", "description": "A future producer adds optional fields that a PRP v1 consumer preserves and ignores safely.", + "futureFixtureHint": { "producerVersion": "1.1-preview" }, + "identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_replay", "issueId": "issue_replay_future", "runId": "run_replay_future", "environmentLeaseId": "lease_replay_future", "runnerInstanceId": "runner_replay", "normalizedSessionId": "session_replay_future", "driverSessionId": "driver_replay_future", "futureIdentityLabel": "preserved" }, + "capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "fake", "version": "1.1.0", "futureDriverField": true }, "steer": true, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true, "futureCapability": { "mode": "optional" } }, + "commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_future_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T12:50:00.000Z", "payload": {}, "futureDeadlineMode": "soft" }], + "events": [ + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_01", "sourceSeq": 1, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_future", "normalizedSessionId": "session_replay_future", "eventType": "session.started", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.010Z", "payload": { "futureSessionDetail": "ignored" }, "futureEnvelopeField": 42 }, + { + "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_02", "sourceSeq": 2, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_future", "normalizedSessionId": "session_replay_future", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.020Z", + "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional fields did not change the v1 projection.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], "futureResultAnnotation": { "safe": true } } + }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_03", "sourceSeq": 3, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_future", "normalizedSessionId": "session_replay_future", "turnId": "turn_replay_future", "eventType": "turn.completed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.030Z", "payload": {} }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_future_04", "sourceSeq": 4, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_future", "normalizedSessionId": "session_replay_future", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T12:50:00.040Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done", "futureTerminalReason": "optional" } } + ], + "requests": [], + "result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "Unknown optional fields did not change the v1 projection.", "completionClaim": { "contractRevision": "replay-contract-v1", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [], "futureResultAnnotation": { "safe": true } } +} diff --git a/packages/paperclip-runner/protocol/fixtures/replay/unsupported-required-version.json b/packages/paperclip-runner/protocol/fixtures/replay/unsupported-required-version.json new file mode 100644 index 0000000000..f8c63b4e46 --- /dev/null +++ b/packages/paperclip-runner/protocol/fixtures/replay/unsupported-required-version.json @@ -0,0 +1,13 @@ +{ + "schema": "paperclip.prp.fixture.v1", "fixtureVersion": 1, "protocolVersion": 2, + "name": "Unsupported required version", "description": "A fixture that requires PRP v2 must fail closed in a PRP v1 consumer.", + "identity": { "schema": "paperclip.prp.identity.v1", "companyId": "company_replay", "issueId": "issue_replay_v2", "runId": "run_replay_v2", "environmentLeaseId": "lease_replay_v2", "runnerInstanceId": "runner_replay", "normalizedSessionId": "session_replay_v2", "driverSessionId": "driver_replay_v2" }, + "capabilities": { "schema": "paperclip.prp.capabilities.v1", "sessionReusePolicy": "new_per_run", "driver": { "kind": "future", "version": "2.0.0" }, "steer": true, "interrupt": true, "resume": true, "runtimeRequests": true, "structuredResult": true, "typedEvents": true }, + "commands": [{ "schema": "paperclip.prp.command.v1", "commandId": "command_v2_prepare", "controllerSeq": 1, "type": "run.prepare", "issuedAt": "2026-08-07T13:00:00.000Z", "payload": {} }], + "events": [ + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_v2_01", "sourceSeq": 1, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_v2", "normalizedSessionId": "session_replay_v2", "eventType": "run.result.proposed", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T13:00:00.010Z", "payload": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result requires PRP v2.", "completionClaim": { "contractRevision": "replay-contract-v2", "objectiveSatisfied": true, "criteria": [], "remainingWork": [] }, "evidence": [], "verification": [], "attentionRequests": [], "artifacts": [] } }, + { "schema": "paperclip.prp.event.v1", "sourceEventId": "event_v2_02", "sourceSeq": 2, "sourceInstanceId": "runner_replay", "sourceKind": "runner", "runId": "run_replay_v2", "normalizedSessionId": "session_replay_v2", "eventType": "run.terminal", "schemaVersion": 1, "priority": 0, "emittedAt": "2026-08-07T13:00:00.020Z", "payload": { "schema": "paperclip.prp.terminal.v1", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "done" } } + ], + "requests": [], + "result": { "schema": "paperclip.run_result.v1", "reportedWorkDisposition": "done", "summary": "This result requires PRP v2.", "completionClaim": { "contractRevision": "replay-contract-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 new file mode 100644 index 0000000000..7aded07243 --- /dev/null +++ b/packages/paperclip-runner/protocol/manifest.json @@ -0,0 +1,173 @@ +{ + "schema": "paperclip.prp.contract_manifest.v1", + "protocolVersion": 1, + "fixtureVersion": 1, + "generatedFrom": [ + "protocol/schemas", + "protocol/fixtures" + ], + "schemas": [ + { + "path": "schemas/capabilities.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json", + "sha256": "e62a80328e5edde136b35463033280922ac2220575a83046b07fe034d233db88" + }, + { + "path": "schemas/command.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/command.schema.json", + "sha256": "fc813e6b4d5b7e6890b496bd69848ada25dba58adbf7cf04163ca9e9bf2bcb79" + }, + { + "path": "schemas/conformance-fixture.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/conformance-fixture.schema.json", + "sha256": "0febc66d128da1f251f9e932d78353a67f47ee60b7427e0a347c287bb6b7ea79" + }, + { + "path": "schemas/conformance-output.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/conformance-output.schema.json", + "sha256": "96f78846dffc4a8300184751b9943214506f32ee1f393c6c0c453b666cf12d59" + }, + { + "path": "schemas/event.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/event.schema.json", + "sha256": "b930af4a187c26312b4fff66813498b95332cb4e972f8b9282cdceafdf0de32c" + }, + { + "path": "schemas/fixture.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/fixture.schema.json", + "sha256": "8ac87c2d8c639981277b5f72b9757a95badf85ff482d11596e4c63e01c616cba" + }, + { + "path": "schemas/identity.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/identity.schema.json", + "sha256": "8a8a1943f14c739284daa241c5189f3b8026c050472c4f9909897456da506f48" + }, + { + "path": "schemas/provider-descriptor.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json", + "sha256": "26e8af9314f1ca49578bfbacdcc111332225b422ea4ff68918a25ddac59129a6" + }, + { + "path": "schemas/provider-event.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json", + "sha256": "84521ea87ae782a7c07d1ef68b70f87fc4c436e79951a8076b7ba55e923fa88b" + }, + { + "path": "schemas/question-adapter-fixture.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/question-adapter-fixture.schema.json", + "sha256": "002062abb4df9a98c92003eb4af43afbe8bdedd15d08b8697ad221fcc28ad9c7" + }, + { + "path": "schemas/question-response.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/question-response.schema.json", + "sha256": "dec308b0daa4d882deb3878a1ad6ae5360f3342060f63ca636d65d1d9af1e055" + }, + { + "path": "schemas/question-set.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json", + "sha256": "3bda40311ae3153bbf9d4a8c59f716680e17d711a4db3570193963918359751c" + }, + { + "path": "schemas/request.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/request.schema.json", + "sha256": "bd41f01c00eddc1b635d3131a7183ddb616ff8fe68d5e348ddd9593a674c8d8e" + }, + { + "path": "schemas/result.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/result.schema.json", + "sha256": "90f02c0a96e8ab9f75970fd788b61c18ccbd9abbd005bedfba280701729adce0" + }, + { + "path": "schemas/semantic-tool.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/semantic-tool.schema.json", + "sha256": "f53f6832c0d65caaccf540662fdb08cf62d9bab45a18def7b559c4068ef54efc" + }, + { + "path": "schemas/stop-reason.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/stop-reason.schema.json", + "sha256": "ed0c0807904ff1198273458057e1a565e828dba499372c0c80e2c69a1d600a13" + }, + { + "path": "schemas/terminal.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json", + "sha256": "e9093399ac6422e77b060269910ffaba9a3d574ed578832821e310a0582edffd" + }, + { + "path": "schemas/usage.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/usage.schema.json", + "sha256": "28fdbb3202095144649dddc438e6270c66d599c6f7e86838687f58d644726456" + }, + { + "path": "schemas/workspace-diff.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/workspace-diff.schema.json", + "sha256": "f71b21f41cfc4c40aeaa503d1981d29355b4dd8b15036fcf3d8df5c35f8b8f6f" + }, + { + "path": "schemas/workspace-file-reference.schema.json", + "id": "https://paperclip.dev/schemas/prp/v1/workspace-file-reference.schema.json", + "sha256": "0ab57e76c9710f520ed9d15cc5d0f25c68176beff3aff556ad62dba9b922eb23" + } + ], + "fixtures": [ + { + "path": "fixtures/conformance-expected-output.json", + "sha256": "5a643639c9df0a2925ba95c5c5dfa2217605018bfa0dd3954ab4aca5fcfdc2c6", + "expectation": "accept", + "compatibilityCase": "cross-language-output" + }, + { + "path": "fixtures/conformance-minimal-run.json", + "sha256": "1e4304bcb51c3d7aa0575cf47b1ff3901700a254a60cf7432003ce9af0175e3a", + "expectation": "accept", + "compatibilityCase": "cross-language-input" + }, + { + "path": "fixtures/questions/codex.json", + "sha256": "ad673b6f9d70b74b53656e622d15fad1a66430f39d62b9ef71479d194a198bd6", + "expectation": "accept", + "compatibilityCase": "codex-structured-input" + }, + { + "path": "fixtures/replay/duplicate-event.json", + "sha256": "779c146966d2825ffcde88bfff8b1fb238c162d40c578980ad3bd1a7def405d7", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/failed-run.json", + "sha256": "59281021b6ff11e4765213043580fc91eca8a02bedb3fe0c012bef9ad807da12", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/happy-path.json", + "sha256": "dfe0aeb4e7a217f8d375afe6e6d1ab0151bb8e81921b07ae0aaa6c4d0b8f8a1d", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/interrupted-run.json", + "sha256": "2056789605ffde847b3272e751226ce4be61527f6aaa4fe3f5557a621af0ad5c", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/source-gap.json", + "sha256": "d8a3eb2861e8198226360e666e5296ff5d7215f81d255a273b21d2bfe7dcc2ef", + "expectation": "accept", + "compatibilityCase": "canonical" + }, + { + "path": "fixtures/replay/unknown-optional-fields.json", + "sha256": "4dbc4b70c299fe21791ca66949f3cb189c8f76514b62c52f5f40a489a84515c4", + "expectation": "accept", + "compatibilityCase": "additive-optional-fields" + }, + { + "path": "fixtures/replay/unsupported-required-version.json", + "sha256": "60ac964d2cf49dc80ada0cb9c946b25952f3e2be10719801d3f2f8bd98d27699", + "expectation": "reject", + "compatibilityCase": "unknown-required-version" + } + ] +} diff --git a/packages/paperclip-runner/protocol/schemas/capabilities.schema.json b/packages/paperclip-runner/protocol/schemas/capabilities.schema.json new file mode 100644 index 0000000000..fdaf331bb5 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/capabilities.schema.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json", + "title": "PRP negotiated capabilities", + "type": "object", + "required": [ + "schema", + "sessionReusePolicy", + "driver", + "steer", + "interrupt", + "resume", + "runtimeRequests", + "structuredResult", + "typedEvents" + ], + "properties": { + "schema": { "const": "paperclip.prp.capabilities.v1" }, + "sessionReusePolicy": { + "enum": ["new_per_run", "reuse_per_issue", "reuse_per_workspace"] + }, + "driver": { + "type": "object", + "required": ["kind", "version"], + "properties": { + "kind": { "type": "string", "minLength": 1, "maxLength": 80 }, + "version": { "type": "string", "minLength": 1, "maxLength": 80 } + }, + "additionalProperties": true + }, + "steer": { "type": "boolean" }, + "interrupt": { "type": "boolean" }, + "resume": { "type": "boolean" }, + "runtimeRequests": { "type": "boolean" }, + "structuredResult": { "type": "boolean" }, + "typedEvents": { "type": "boolean" }, + "typedEventFamilies": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "required": ["family", "version", "availability", "detailLevel"], + "properties": { + "family": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[a-z][a-z0-9_.-]*$" }, + "version": { "const": 1 }, + "availability": { "enum": ["available", "unsupported", "policy_disabled"] }, + "detailLevel": { "enum": ["summary", "structured"] } + }, + "additionalProperties": false + } + }, + "semanticTools": { + "type": "object", + "required": ["schema", "schemaVersion", "operations"], + "properties": { + "schema": { "const": "paperclip.prp.semantic_tools.v1" }, + "schemaVersion": { "const": 1 }, + "operations": { + "type": "array", + "items": { + "type": "object", + "required": ["operationId", "version", "availability", "redactionDisposition"], + "properties": { + "operationId": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "version": { "const": 1 }, + "availability": { "enum": ["available", "denied", "unsupported"] }, + "redactionDisposition": { "const": "digest_only" }, + "requiredClaims": { + "type": "array", + "items": { "type": "string", "minLength": 1, "maxLength": 160 }, + "uniqueItems": true + }, + "reasonCode": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-z][a-z0-9_.:-]*$" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "unsupported": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/command.schema.json b/packages/paperclip-runner/protocol/schemas/command.schema.json new file mode 100644 index 0000000000..b6ae97cca7 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/command.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/command.schema.json", + "title": "PRP runner command", + "type": "object", + "required": ["schema", "commandId", "controllerSeq", "type", "issuedAt", "payload"], + "properties": { + "schema": { "const": "paperclip.prp.command.v1" }, + "commandId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "controllerSeq": { "type": "integer", "minimum": 1 }, + "type": { + "enum": [ + "run.prepare", + "run.attach", + "session.open", + "turn.start", + "turn.steer", + "turn.interrupt", + "turn.stop", + "request.resolve", + "interaction.receipt", + "semantic_tool.result", + "session.snapshot", + "session.close", + "session.budget.increase", + "session.destroy", + "run.cancel", + "runner.drain", + "runner.suspend", + "runner.shutdown" + ] + }, + "issuedAt": { "type": "string", "format": "date-time" }, + "deadlineAt": { "type": "string", "format": "date-time" }, + "precondition": { + "type": "object", + "properties": { + "runnerState": { "type": "array", "items": { "type": "string" } }, + "runState": { "type": "array", "items": { "type": "string" } }, + "sessionState": { "type": "array", "items": { "type": "string" } }, + "activeTurnId": { "type": ["string", "null"] } + }, + "additionalProperties": true + }, + "payload": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/conformance-fixture.schema.json b/packages/paperclip-runner/protocol/schemas/conformance-fixture.schema.json new file mode 100644 index 0000000000..cae7cd0b4b --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/conformance-fixture.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/conformance-fixture.schema.json", + "title": "Paperclip Runner cross-language conformance fixture", + "type": "object", + "required": ["schemaVersion", "run", "events", "result"], + "properties": { + "schemaVersion": { "const": "paperclip.runner.conformance.fixture.v1" }, + "run": { + "type": "object", + "required": ["runId", "sessionId", "companyId", "issueId", "agentId"], + "properties": { + "runId": { "$ref": "#/$defs/id" }, + "sessionId": { "$ref": "#/$defs/id" }, + "companyId": { "$ref": "#/$defs/id" }, + "issueId": { "$ref": "#/$defs/id" }, + "agentId": { "$ref": "#/$defs/id" } + }, + "additionalProperties": true + }, + "events": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["eventId", "runId", "sequence", "type"], + "properties": { + "eventId": { "$ref": "#/$defs/id" }, + "runId": { "$ref": "#/$defs/id" }, + "sequence": { "type": "integer", "minimum": 1 }, + "type": { "enum": ["run.started", "run.completed"] } + }, + "additionalProperties": true + } + }, + "result": { + "type": "object", + "required": ["runId", "status", "summary"], + "properties": { + "runId": { "$ref": "#/$defs/id" }, + "status": { "enum": ["succeeded", "failed", "interrupted"] }, + "summary": { "type": "string", "minLength": 1, "maxLength": 4000 } + }, + "additionalProperties": true + } + }, + "$defs": { + "id": { "type": "string", "minLength": 1, "maxLength": 160 } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/conformance-output.schema.json b/packages/paperclip-runner/protocol/schemas/conformance-output.schema.json new file mode 100644 index 0000000000..e348c75e4a --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/conformance-output.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/conformance-output.schema.json", + "title": "Paperclip Runner cross-language conformance output", + "type": "object", + "required": ["schemaVersion", "runIdentity", "result"], + "properties": { + "schemaVersion": { "const": "paperclip.runner.conformance.output.v1" }, + "runIdentity": { + "type": "object", + "required": ["runId", "sessionId"], + "properties": { + "runId": { "$ref": "#/$defs/id" }, + "sessionId": { "$ref": "#/$defs/id" } + }, + "additionalProperties": true + }, + "result": { + "type": "object", + "required": ["status", "summary"], + "properties": { + "status": { "enum": ["succeeded", "failed", "interrupted"] }, + "summary": { "type": "string", "minLength": 1, "maxLength": 4000 } + }, + "additionalProperties": true + } + }, + "$defs": { + "id": { "type": "string", "minLength": 1, "maxLength": 160 } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/event.schema.json b/packages/paperclip-runner/protocol/schemas/event.schema.json new file mode 100644 index 0000000000..332e70b396 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/event.schema.json @@ -0,0 +1,173 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/event.schema.json", + "title": "PRP native event", + "type": "object", + "required": [ + "schema", + "sourceEventId", + "sourceSeq", + "sourceInstanceId", + "sourceKind", + "runId", + "eventType", + "schemaVersion", + "priority", + "emittedAt", + "payload" + ], + "properties": { + "schema": { "const": "paperclip.prp.event.v1" }, + "sourceEventId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "sourceSeq": { "type": "integer", "minimum": 1 }, + "sourceInstanceId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "sourceKind": { "enum": ["runner", "control_plane"] }, + "runId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "normalizedSessionId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "turnId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "itemId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "eventType": { + "enum": [ + "runner.connected", "runner.reconnected", "runner.reconciled", "runner.disconnected", "runner.draining", "runner.suspending", "runner.suspended", "runner.stopped", "runner.diagnostic", + "runtime.phase.changed", "sandbox.metric", "workspace.ready", "workspace.change.updated", "workspace.diff.recorded", "workspace.file.referenced", + "harness.starting", "harness.ready", "harness.exited", "harness.diagnostic", + "plan.updated", + "tool.execution.started", "tool.execution.progressed", "tool.execution.completed", + "research.started", "research.progressed", "research.completed", + "delegation.started", "delegation.updated", "delegation.completed", + "model.route.changed", "model.verification.updated", "context.compacted", + "artifact.viewed", "artifact.generated", "review.mode.changed", + "hook.started", "hook.completed", "memory.citation.referenced", + "safety.review.started", "safety.review.completed", "terminal.input.sent", + "wait.started", "wait.completed", "provider.notice.recorded", + "session.starting", "session.started", "session.resuming", "session.resumed", "session.reconciled", "session.updated", "session.closed", "session.failed", + "turn.submitted", "turn.accepted", "turn.started", "turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled", + "item.started", "item.delta", "item.completed", "item.failed", "usage.reported", + "semantic_tool.input", "semantic_tool.result", + "mcp_app.discovered", "mcp_app.resource.resolved", "mcp_app.initializing", "mcp_app.ready", "mcp_app.tool_input", "mcp_app.tool_result", "mcp_app.action.requested", "mcp_app.action.resolved", "mcp_app.host_context.changed", "mcp_app.failed", "mcp_app.teardown", + "runtime_request.created", "runtime_request.resolved", "runtime_request.expired", "runtime_request.cancelled", + "interaction.request.proposed", "interaction.request.materialized", "interaction.request.rejected", "interaction.response.progressed", "interaction.response.resolved", "interaction.response.delivered", + "run.attached", "run.detached", "run.result.proposed", "run.result.accepted", "run.result.rejected", + "attention.request.proposed", "attention.request.routed", "attention.request.resolved", "attention.request.expired", "attention.request.superseded", + "work.assessment.recorded", "issue.status.decision.recorded", "issue.status.decision.applied", "issue.status.decision.rejected", "issue.status.decision.superseded", + "run.terminal" + ] + }, + "schemaVersion": { "const": 1 }, + "priority": { "enum": [0, 1, 2] }, + "emittedAt": { "type": "string", "format": "date-time" }, + "observedAt": { "type": "string", "format": "date-time" }, + "payload": { + "type": "object", + "properties": { + "channel": { "enum": ["progress", "final", "summary", "detail", "unknown"] }, + "providerPhase": { "type": "string" }, + "providerMethod": { "type": "string" } + }, + "additionalProperties": true + }, + "debug": { "type": "object", "additionalProperties": true } + }, + "allOf": [ + { "if": { "properties": { "eventType": { "const": "plan.updated" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/plan" } } } }, + { "if": { "properties": { "eventType": { "enum": ["tool.execution.started", "tool.execution.progressed", "tool.execution.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/toolExecution" } } } }, + { "if": { "properties": { "eventType": { "enum": ["research.started", "research.progressed", "research.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/research" } } } }, + { "if": { "properties": { "eventType": { "enum": ["delegation.started", "delegation.updated", "delegation.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/delegation" } } } }, + { "if": { "properties": { "eventType": { "const": "model.route.changed" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/modelRoute" } } } }, + { "if": { "properties": { "eventType": { "const": "model.verification.updated" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/modelVerification" } } } }, + { "if": { "properties": { "eventType": { "const": "context.compacted" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/contextCompacted" } } } }, + { "if": { "properties": { "eventType": { "const": "artifact.viewed" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/artifactViewed" } } } }, + { "if": { "properties": { "eventType": { "const": "artifact.generated" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/artifactGenerated" } } } }, + { "if": { "properties": { "eventType": { "const": "review.mode.changed" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/reviewMode" } } } }, + { "if": { "properties": { "eventType": { "enum": ["hook.started", "hook.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/hook" } } } }, + { "if": { "properties": { "eventType": { "const": "memory.citation.referenced" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/memoryCitation" } } } }, + { "if": { "properties": { "eventType": { "enum": ["safety.review.started", "safety.review.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/safetyReview" } } } }, + { "if": { "properties": { "eventType": { "const": "terminal.input.sent" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/terminalInput" } } } }, + { "if": { "properties": { "eventType": { "enum": ["wait.started", "wait.completed"] } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/wait" } } } }, + { "if": { "properties": { "eventType": { "const": "provider.notice.recorded" } } }, "then": { "properties": { "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json#/$defs/providerNotice" } } } }, + { + "if": { "properties": { "eventType": { "const": "workspace.file.referenced" } } }, + "then": { + "properties": { + "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/workspace-file-reference.schema.json" } + } + } + }, + { + "if": { "properties": { "eventType": { "enum": ["workspace.change.updated", "workspace.diff.recorded"] } } }, + "then": { + "properties": { + "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/workspace-diff.schema.json" } + } + } + }, + { + "if": { "properties": { "eventType": { "const": "workspace.diff.recorded" } } }, + "then": { + "properties": { + "payload": { + "allOf": [ + { "$ref": "https://paperclip.dev/schemas/prp/v1/workspace-diff.schema.json" }, + { "type": "object", "properties": { "complete": { "const": true } } } + ] + } + } + } + }, + { + "if": { "properties": { "eventType": { "enum": ["semantic_tool.input", "mcp_app.tool_input"] } } }, + "then": { + "properties": { + "payload": { + "type": "object", + "properties": { + "semantic_tool": { + "allOf": [ + { "$ref": "https://paperclip.dev/schemas/prp/v1/semantic-tool.schema.json" }, + { "type": "object", "properties": { "phase": { "const": "input" } } } + ] + } + }, + "additionalProperties": true + } + } + } + }, + { + "if": { "properties": { "eventType": { "enum": ["semantic_tool.result", "mcp_app.tool_result"] } } }, + "then": { + "properties": { + "payload": { + "type": "object", + "properties": { + "semantic_tool": { + "allOf": [ + { "$ref": "https://paperclip.dev/schemas/prp/v1/semantic-tool.schema.json" }, + { "type": "object", "properties": { "phase": { "const": "result" } } } + ] + } + }, + "additionalProperties": true + } + } + } + }, + { + "if": { "properties": { "eventType": { "const": "run.terminal" } } }, + "then": { + "properties": { + "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json" } + } + } + }, + { + "if": { "properties": { "eventType": { "const": "run.result.proposed" } } }, + "then": { + "properties": { + "payload": { "$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json" } + } + } + } + ], + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/fixture.schema.json b/packages/paperclip-runner/protocol/schemas/fixture.schema.json new file mode 100644 index 0000000000..18d33ade54 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/fixture.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/fixture.schema.json", + "title": "PRP scripted replay fixture", + "type": "object", + "required": [ + "schema", + "fixtureVersion", + "protocolVersion", + "name", + "description", + "identity", + "capabilities", + "commands", + "events", + "result" + ], + "properties": { + "schema": { "const": "paperclip.prp.fixture.v1" }, + "fixtureVersion": { "const": 1 }, + "protocolVersion": { "const": 1 }, + "name": { "type": "string", "minLength": 1, "maxLength": 120 }, + "description": { "type": "string", "minLength": 1, "maxLength": 1000 }, + "identity": { "$ref": "https://paperclip.dev/schemas/prp/v1/identity.schema.json" }, + "capabilities": { "$ref": "https://paperclip.dev/schemas/prp/v1/capabilities.schema.json" }, + "commands": { + "type": "array", + "items": { "$ref": "https://paperclip.dev/schemas/prp/v1/command.schema.json" } + }, + "events": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://paperclip.dev/schemas/prp/v1/event.schema.json" } + }, + "requests": { + "type": "array", + "items": { "$ref": "https://paperclip.dev/schemas/prp/v1/request.schema.json" } + }, + "result": { "$ref": "https://paperclip.dev/schemas/prp/v1/result.schema.json" } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/identity.schema.json b/packages/paperclip-runner/protocol/schemas/identity.schema.json new file mode 100644 index 0000000000..8279ef857c --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/identity.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/identity.schema.json", + "title": "PRP identity graph", + "type": "object", + "required": [ + "schema", + "companyId", + "issueId", + "runId", + "environmentLeaseId", + "runnerInstanceId", + "normalizedSessionId", + "driverSessionId" + ], + "properties": { + "schema": { "const": "paperclip.prp.identity.v1" }, + "companyId": { "$ref": "#/$defs/stableId" }, + "issueId": { "$ref": "#/$defs/stableId" }, + "runId": { "$ref": "#/$defs/stableId" }, + "environmentLeaseId": { "$ref": "#/$defs/stableId" }, + "runnerInstanceId": { "$ref": "#/$defs/stableId" }, + "normalizedSessionId": { "$ref": "#/$defs/stableId" }, + "driverSessionId": { "$ref": "#/$defs/stableId" }, + "providerSessionId": { "$ref": "#/$defs/stableId" } + }, + "additionalProperties": true, + "$defs": { + "stableId": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } +} diff --git a/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json new file mode 100644 index 0000000000..3bfbb66a06 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json", + "title": "PRP provider runtime descriptor", + "type": "object", + "required": ["provider", "driver", "model", "executionKind", "providerVersion"], + "properties": { + "provider": { "const": "codex" }, + "driver": { "const": "codex_app_server" }, + "model": { "type": ["string", "null"], "maxLength": 240 }, + "executionKind": { "const": "local_process" }, + "providerVersion": { "type": "string", "minLength": 1, "maxLength": 120 }, + "providerSessionId": { "type": ["string", "null"], "maxLength": 240 }, + "processId": { "type": ["integer", "null"], "minimum": 1 } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/provider-event.schema.json b/packages/paperclip-runner/protocol/schemas/provider-event.schema.json new file mode 100644 index 0000000000..8af5200ddf --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/provider-event.schema.json @@ -0,0 +1,167 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json", + "title": "Provider-neutral runner activity payload", + "oneOf": [ + { "$ref": "#/$defs/plan" }, + { "$ref": "#/$defs/toolExecution" }, + { "$ref": "#/$defs/research" }, + { "$ref": "#/$defs/delegation" }, + { "$ref": "#/$defs/modelRoute" }, + { "$ref": "#/$defs/modelVerification" }, + { "$ref": "#/$defs/contextCompacted" }, + { "$ref": "#/$defs/artifactViewed" }, + { "$ref": "#/$defs/artifactGenerated" }, + { "$ref": "#/$defs/reviewMode" }, + { "$ref": "#/$defs/hook" }, + { "$ref": "#/$defs/memoryCitation" }, + { "$ref": "#/$defs/safetyReview" }, + { "$ref": "#/$defs/terminalInput" }, + { "$ref": "#/$defs/wait" }, + { "$ref": "#/$defs/providerNotice" } + ], + "$defs": { + "id": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" }, + "shortText": { "type": "string", "maxLength": 4000 }, + "nullableShortText": { "type": ["string", "null"], "maxLength": 4000 }, + "safePath": { "type": ["string", "null"], "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).*$" }, + "safeUrl": { "type": ["string", "null"], "maxLength": 8192, "pattern": "^https?://" }, + "plan": { + "type": "object", + "required": ["schema", "planId", "revision", "steps", "complete", "syncStatus"], + "properties": { + "schema": { "const": "paperclip.plan.updated.v1" }, + "planId": { "$ref": "#/$defs/id" }, + "revision": { "type": "integer", "minimum": 1 }, + "explanation": { "$ref": "#/$defs/nullableShortText" }, + "steps": { "type": "array", "maxItems": 256, "items": { "type": "object", "required": ["stepId", "body", "status"], "properties": { "stepId": { "$ref": "#/$defs/id" }, "body": { "$ref": "#/$defs/shortText" }, "status": { "enum": ["pending", "in_progress", "completed", "blocked"] } }, "additionalProperties": false } }, + "complete": { "type": "boolean" }, + "syncStatus": { "enum": ["streaming", "pending", "synchronized", "conflict", "not_applicable"] }, + "documentRevision": { "type": ["integer", "null"], "minimum": 1 } + }, + "additionalProperties": false + }, + "toolExecution": { + "type": "object", + "required": ["schema", "executionId", "transport", "operation", "status", "output", "outputBytes", "outputTruncated", "outputDigest"], + "properties": { + "schema": { "const": "paperclip.tool.execution.v1" }, + "executionId": { "$ref": "#/$defs/id" }, + "transport": { "enum": ["process", "mcp", "dynamic", "builtin"] }, + "operation": { "enum": ["read", "search", "list", "execute", "edit", "unknown"] }, + "name": { "$ref": "#/$defs/nullableShortText" }, + "target": { "$ref": "#/$defs/safePath" }, + "namespace": { "type": ["string", "null"], "maxLength": 240 }, + "readOnly": { "type": ["boolean", "null"] }, + "status": { "enum": ["running", "completed", "failed", "cancelled", "interrupted"] }, + "durationMs": { "type": ["integer", "null"], "minimum": 0 }, + "exitCode": { "type": ["integer", "null"] }, + "progress": { "$ref": "#/$defs/nullableShortText" }, + "output": { "type": ["string", "null"], "maxLength": 65536 }, + "outputBytes": { "type": "integer", "minimum": 0 }, + "outputTruncated": { "type": "boolean" }, + "outputDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-f0-9]{64}$" } + }, + "additionalProperties": false + }, + "research": { + "type": "object", + "required": ["schema", "researchId", "action", "status", "sources"], + "properties": { + "schema": { "const": "paperclip.research.v1" }, + "researchId": { "$ref": "#/$defs/id" }, + "action": { "enum": ["search", "open_page", "find_in_page", "other"] }, + "status": { "enum": ["running", "completed", "failed", "cancelled"] }, + "query": { "$ref": "#/$defs/nullableShortText" }, + "url": { "$ref": "#/$defs/safeUrl" }, + "pattern": { "$ref": "#/$defs/nullableShortText" }, + "sources": { "type": "array", "maxItems": 64, "items": { "type": "object", "required": ["sourceId", "title", "url", "snippet"], "properties": { "sourceId": { "$ref": "#/$defs/id" }, "title": { "$ref": "#/$defs/shortText" }, "url": { "$ref": "#/$defs/safeUrl" }, "snippet": { "$ref": "#/$defs/nullableShortText" } }, "additionalProperties": false } } + }, + "additionalProperties": false + }, + "delegation": { + "type": "object", + "required": ["schema", "delegationId", "action", "status", "children"], + "properties": { + "schema": { "const": "paperclip.delegation.v1" }, + "delegationId": { "$ref": "#/$defs/id" }, + "action": { "enum": ["spawn", "message", "resume", "wait", "close"] }, + "status": { "enum": ["running", "waiting", "completed", "failed", "interrupted", "closed"] }, + "children": { "type": "array", "maxItems": 64, "items": { "type": "object", "required": ["childId", "role", "model", "status", "summary", "activitySummary"], "properties": { "childId": { "$ref": "#/$defs/id" }, "role": { "type": ["string", "null"], "maxLength": 160 }, "model": { "type": ["string", "null"], "maxLength": 240 }, "status": { "enum": ["running", "waiting", "completed", "failed", "interrupted", "closed"] }, "summary": { "$ref": "#/$defs/nullableShortText" }, "activitySummary": { "$ref": "#/$defs/nullableShortText" } }, "additionalProperties": false } } + }, + "additionalProperties": false + }, + "modelRoute": { + "type": "object", + "required": ["schema", "routeId", "provider", "requestedModel", "fromModel", "effectiveModel", "reason"], + "properties": { "schema": { "const": "paperclip.model.route_changed.v1" }, "routeId": { "$ref": "#/$defs/id" }, "provider": { "type": "string", "minLength": 1, "maxLength": 160 }, "requestedModel": { "type": "string", "minLength": 1, "maxLength": 240 }, "fromModel": { "type": ["string", "null"], "maxLength": 240 }, "effectiveModel": { "type": "string", "minLength": 1, "maxLength": 240 }, "reason": { "$ref": "#/$defs/shortText" } }, + "additionalProperties": false + }, + "modelVerification": { + "type": "object", + "required": ["schema", "verificationId", "status", "classes", "buffering", "summary"], + "properties": { "schema": { "const": "paperclip.model.verification.v1" }, "verificationId": { "$ref": "#/$defs/id" }, "status": { "enum": ["running", "completed", "failed"] }, "classes": { "type": "array", "maxItems": 32, "items": { "type": "string", "maxLength": 160 } }, "buffering": { "type": "boolean" }, "summary": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "contextCompacted": { + "type": "object", + "required": ["schema", "compactionId", "reason", "preTokens", "postTokens", "sameSession"], + "properties": { "schema": { "const": "paperclip.context.compacted.v1" }, "compactionId": { "$ref": "#/$defs/id" }, "reason": { "enum": ["context_window", "manual", "provider", "unknown"] }, "preTokens": { "type": ["integer", "null"], "minimum": 0 }, "postTokens": { "type": ["integer", "null"], "minimum": 0 }, "sameSession": { "type": "boolean" } }, + "additionalProperties": false + }, + "artifactViewed": { + "type": "object", + "required": ["schema", "artifactId", "reference", "mediaType", "title"], + "properties": { "schema": { "const": "paperclip.artifact.viewed.v1" }, "artifactId": { "$ref": "#/$defs/id" }, "reference": { "$ref": "#/$defs/safePath" }, "mediaType": { "type": ["string", "null"], "maxLength": 160 }, "title": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "artifactGenerated": { + "type": "object", + "required": ["schema", "artifactId", "status", "reference", "mediaType", "registered", "failure"], + "properties": { "schema": { "const": "paperclip.artifact.generated.v1" }, "artifactId": { "$ref": "#/$defs/id" }, "status": { "enum": ["running", "completed", "failed"] }, "reference": { "$ref": "#/$defs/safePath" }, "mediaType": { "type": ["string", "null"], "maxLength": 160 }, "registered": { "type": "boolean" }, "transparentBackground": { "type": ["boolean", "null"] }, "failure": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "reviewMode": { + "type": "object", + "required": ["schema", "reviewId", "state", "scope"], + "properties": { "schema": { "const": "paperclip.review.mode_changed.v1" }, "reviewId": { "$ref": "#/$defs/id" }, "state": { "enum": ["entered", "exited"] }, "scope": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "hook": { + "type": "object", + "required": ["schema", "hookId", "event", "scope", "status", "blocking", "durationMs", "summary"], + "properties": { "schema": { "const": "paperclip.hook.v1" }, "hookId": { "$ref": "#/$defs/id" }, "event": { "type": "string", "minLength": 1, "maxLength": 160 }, "scope": { "type": "string", "minLength": 1, "maxLength": 160 }, "status": { "enum": ["running", "completed", "failed", "cancelled"] }, "blocking": { "type": "boolean" }, "durationMs": { "type": ["integer", "null"], "minimum": 0 }, "summary": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "memoryCitation": { + "type": "object", + "required": ["schema", "citationId", "messageItemId", "label", "available", "reference"], + "properties": { "schema": { "const": "paperclip.memory.citation.v1" }, "citationId": { "$ref": "#/$defs/id" }, "messageItemId": { "$ref": "#/$defs/id" }, "label": { "$ref": "#/$defs/shortText" }, "available": { "type": "boolean" }, "reference": { "type": ["string", "null"], "maxLength": 4096 } }, + "additionalProperties": false + }, + "safetyReview": { + "type": "object", + "required": ["schema", "reviewId", "targetExecutionId", "status", "decision", "summary"], + "properties": { "schema": { "const": "paperclip.safety.review.v1" }, "reviewId": { "$ref": "#/$defs/id" }, "targetExecutionId": { "anyOf": [{ "$ref": "#/$defs/id" }, { "type": "null" }] }, "status": { "enum": ["running", "completed", "failed"] }, "decision": { "enum": ["pending", "allowed", "denied", "user_required", "unknown"] }, "summary": { "$ref": "#/$defs/nullableShortText" } }, + "additionalProperties": false + }, + "terminalInput": { + "type": "object", + "required": ["schema", "executionId", "origin", "inputClass", "byteCount"], + "properties": { "schema": { "const": "paperclip.terminal.input_sent.v1" }, "executionId": { "$ref": "#/$defs/id" }, "origin": { "enum": ["agent", "user", "system"] }, "inputClass": { "enum": ["text", "control", "eof"] }, "byteCount": { "type": "integer", "minimum": 0 } }, + "additionalProperties": false + }, + "wait": { + "type": "object", + "required": ["schema", "waitId", "reason", "status", "plannedDurationMs", "elapsedDurationMs"], + "properties": { "schema": { "const": "paperclip.wait.v1" }, "waitId": { "$ref": "#/$defs/id" }, "reason": { "enum": ["timer", "provider", "rate_limit", "unknown"] }, "status": { "enum": ["running", "completed", "interrupted", "cancelled"] }, "plannedDurationMs": { "type": ["integer", "null"], "minimum": 0 }, "elapsedDurationMs": { "type": ["integer", "null"], "minimum": 0 } }, + "additionalProperties": false + }, + "providerNotice": { + "type": "object", + "required": ["schema", "noticeId", "severity", "category", "scope", "recoverable", "userActionable", "summary"], + "properties": { "schema": { "const": "paperclip.provider.notice.v1" }, "noticeId": { "$ref": "#/$defs/id" }, "severity": { "enum": ["info", "warning", "error"] }, "category": { "type": "string", "minLength": 1, "maxLength": 160 }, "scope": { "enum": ["turn", "session", "environment", "account"] }, "recoverable": { "type": "boolean" }, "userActionable": { "type": "boolean" }, "summary": { "$ref": "#/$defs/shortText" } }, + "additionalProperties": false + } + } +} diff --git a/packages/paperclip-runner/protocol/schemas/question-adapter-fixture.schema.json b/packages/paperclip-runner/protocol/schemas/question-adapter-fixture.schema.json new file mode 100644 index 0000000000..04715dd336 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/question-adapter-fixture.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/question-adapter-fixture.schema.json", + "title": "Harness question adapter conformance fixture", + "type": "object", + "required": ["schema", "adapter", "nativeRequest", "canonicalQuestionSet", "canonicalResponse", "nativeResponse"], + "properties": { + "schema": { "const": "paperclip.question_adapter_fixture.v1" }, + "adapter": { "type": "string", "minLength": 1, "maxLength": 160 }, + "nativeRequest": {}, + "canonicalQuestionSet": { "$ref": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json" }, + "canonicalResponse": { "$ref": "https://paperclip.dev/schemas/prp/v1/question-response.schema.json" }, + "nativeResponse": {} + }, + "additionalProperties": false +} diff --git a/packages/paperclip-runner/protocol/schemas/question-response.schema.json b/packages/paperclip-runner/protocol/schemas/question-response.schema.json new file mode 100644 index 0000000000..f089054853 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/question-response.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/question-response.schema.json", + "title": "Paperclip question response", + "type": "object", + "required": ["schema", "answers"], + "properties": { + "schema": { "const": "paperclip.question_response.v1" }, + "answers": { + "type": "object", + "propertyNames": { "minLength": 1, "maxLength": 160 }, + "additionalProperties": { + "type": "object", + "properties": { + "selectedOptionIds": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "minLength": 1, "maxLength": 160 } + }, + "text": { "type": "string", "maxLength": 100000 }, + "customText": { "type": "string", "maxLength": 100000 } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/packages/paperclip-runner/protocol/schemas/question-set.schema.json b/packages/paperclip-runner/protocol/schemas/question-set.schema.json new file mode 100644 index 0000000000..995bfa8f72 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/question-set.schema.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json", + "title": "Paperclip question set", + "type": "object", + "required": ["schema", "questions"], + "properties": { + "schema": { "const": "paperclip.question_set.v1" }, + "title": { "type": "string", "maxLength": 1000 }, + "description": { "type": "string", "maxLength": 4000 }, + "submitLabel": { "type": "string", "maxLength": 200 }, + "questions": { + "type": "array", + "minItems": 1, + "maxItems": 64, + "items": { "$ref": "#/$defs/question" } + } + }, + "$defs": { + "option": { + "type": "object", + "required": ["id", "label"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 160 }, + "label": { "type": "string", "minLength": 1, "maxLength": 1000 }, + "description": { "type": "string", "maxLength": 4000 } + }, + "additionalProperties": false + }, + "customAnswer": { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "const": true }, + "label": { "type": "string", "maxLength": 1000 }, + "placeholder": { "type": "string", "maxLength": 1000 } + }, + "additionalProperties": false + }, + "textValidation": { + "type": "object", + "properties": { + "minLength": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "maxLength": { "type": "integer", "minimum": 0, "maximum": 100000 }, + "pattern": { "type": "string", "maxLength": 1000 }, + "inputType": { "enum": ["text", "number", "integer"] }, + "minimum": { "type": "number" }, + "maximum": { "type": "number" } + }, + "additionalProperties": false + }, + "question": { + "type": "object", + "required": ["id", "prompt", "required", "answerMode"], + "properties": { + "id": { "type": "string", "minLength": 1, "maxLength": 160 }, + "header": { "type": "string", "maxLength": 1000 }, + "prompt": { "type": "string", "minLength": 1, "maxLength": 4000 }, + "helpText": { "type": "string", "maxLength": 4000 }, + "required": { "type": "boolean" }, + "answerMode": { "enum": ["single_select", "multi_select", "text"] }, + "options": { + "type": "array", + "maxItems": 128, + "items": { "$ref": "#/$defs/option" } + }, + "customAnswer": { "$ref": "#/$defs/customAnswer" }, + "textValidation": { "$ref": "#/$defs/textValidation" } + }, + "allOf": [ + { + "if": { "properties": { "answerMode": { "const": "text" } }, "required": ["answerMode"] }, + "then": { + "properties": { "options": { "type": "array", "maxItems": 0 } }, + "not": { "required": ["customAnswer"] } + } + }, + { + "if": { "properties": { "answerMode": { "enum": ["single_select", "multi_select"] } }, "required": ["answerMode"] }, + "then": { + "required": ["options"], + "properties": { "options": { "type": "array", "minItems": 1 } } + } + } + ], + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/packages/paperclip-runner/protocol/schemas/request.schema.json b/packages/paperclip-runner/protocol/schemas/request.schema.json new file mode 100644 index 0000000000..78441bc7bf --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/request.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/request.schema.json", + "title": "PRP request", + "oneOf": [ + { + "type": "object", + "required": ["schema", "requestKind", "requestId", "type", "status", "prompt", "input"], + "properties": { + "schema": { "const": "paperclip.runtime_request.v2" }, + "requestKind": { "const": "runtime" }, + "requestId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "type": { "const": "input" }, + "status": { "enum": ["pending", "resolved", "expired", "cancelled"] }, + "prompt": { "type": "string", "minLength": 1, "maxLength": 4000 }, + "input": { "$ref": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json" }, + "origin": { + "type": "object", + "required": ["adapter"], + "properties": { + "adapter": { "type": "string", "minLength": 1, "maxLength": 160 }, + "provider": { "type": "string", "minLength": 1, "maxLength": 160 }, + "method": { "type": "string", "minLength": 1, "maxLength": 500 } + }, + "additionalProperties": false + }, + "turnId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "itemId": { "type": "string", "minLength": 1, "maxLength": 160 } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["schema", "requestKind", "requestId", "type", "status", "prompt"], + "properties": { + "schema": { "const": "paperclip.runtime_request.v1" }, + "requestKind": { "const": "runtime" }, + "requestId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "type": { "enum": ["permission", "input"] }, + "status": { "enum": ["pending", "resolved", "expired", "cancelled"] }, + "prompt": { "type": "string", "minLength": 1, "maxLength": 4000 }, + "choices": { + "type": "array", + "items": { + "type": "object", + "required": ["key", "label"], + "properties": { + "key": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + { + "type": "object", + "required": ["schema", "requestKind", "requestId", "kind", "blocking", "payload"], + "properties": { + "schema": { "const": "paperclip.interaction_request.v1" }, + "requestKind": { "const": "issue_thread" }, + "requestId": { "type": "string", "minLength": 1, "maxLength": 160 }, + "kind": { + "enum": [ + "request_confirmation", + "request_checkbox_confirmation", + "request_item_verdicts", + "ask_user_questions", + "suggest_tasks" + ] + }, + "blocking": { "type": "boolean" }, + "payload": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": true + } + ] +} diff --git a/packages/paperclip-runner/protocol/schemas/result.schema.json b/packages/paperclip-runner/protocol/schemas/result.schema.json new file mode 100644 index 0000000000..62983b280e --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/result.schema.json @@ -0,0 +1,151 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/result.schema.json", + "title": "PRP structured run result", + "type": "object", + "required": [ + "schema", + "reportedWorkDisposition", + "summary", + "completionClaim", + "evidence", + "verification", + "attentionRequests", + "artifacts" + ], + "properties": { + "schema": { "const": "paperclip.run_result.v1" }, + "reportedWorkDisposition": { + "enum": ["done", "blocked", "needs_review", "yielded"] + }, + "summary": { "type": "string", "minLength": 1, "maxLength": 12000 }, + "completionClaim": { + "type": "object", + "required": ["contractRevision", "objectiveSatisfied", "criteria", "remainingWork"], + "properties": { + "contractRevision": { "type": "string", "minLength": 1 }, + "objectiveSatisfied": { "type": "boolean" }, + "criteria": { + "type": "array", + "items": { + "type": "object", + "required": ["criterionId", "status", "evidenceRefs"], + "properties": { + "criterionId": { "type": "string", "minLength": 1 }, + "status": { "enum": ["satisfied", "not_satisfied", "unknown"] }, + "evidenceRefs": { "type": "array", "items": { "type": "string" } }, + "explanation": { "type": "string" } + }, + "additionalProperties": true + } + }, + "remainingWork": { + "type": "array", + "items": { + "type": "object", + "required": ["description", "blocksCompletion"], + "properties": { + "description": { "type": "string", "minLength": 1 }, + "blocksCompletion": { "type": "boolean" } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "evidence": { "type": "array", "items": { "type": "object" } }, + "verification": { + "type": "array", + "description": "Checks used to verify the work. Use failed only when the check actually ran and found a defect in the work. If a check could not run or complete because the environment, tool, dependency, permission, credential, policy, external service, or budget was unavailable, use not_run with reasonCode even when the attempted command exited non-zero.", + "items": { + "type": "object", + "required": ["commandOrCheck", "status"], + "properties": { + "commandOrCheck": { "type": "string", "minLength": 1 }, + "status": { + "enum": ["passed", "failed", "not_run"], + "description": "passed means the check ran and succeeded; failed means it ran to a meaningful verdict and found the work incorrect; not_run means no meaningful verdict was possible because the check was not attempted or could not complete." + }, + "reasonCode": { + "description": "Required for not_run; identify why the check had no meaningful verdict.", + "enum": [ + "environment_unavailable", + "tool_unavailable", + "dependency_missing", + "permission_denied", + "credential_missing", + "policy_restricted", + "external_service_unavailable", + "budget_exhausted", + "other" + ] + }, + "detail": { "type": "string" }, + "artifactRef": { "type": "string" } + }, + "additionalProperties": true + } + }, + "blocker": { + "type": "object", + "required": ["reasonCode", "owner", "unblockAction", "scope"], + "properties": { + "reasonCode": { "type": "string", "minLength": 1 }, + "owner": { "type": "object", "additionalProperties": true }, + "unblockAction": { "type": "string", "minLength": 1 }, + "scope": { "enum": ["current_track", "task_wide"] } + }, + "additionalProperties": true + }, + "attentionRequests": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, + "ownerClass": { "enum": ["human", "agent", "external_system"] }, + "targetAgentId": { "type": "string", "minLength": 1 } + }, + "additionalProperties": true + } + }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "ref"], + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "ref": { "type": "string", "minLength": 1 }, + "title": { "type": "string" } + }, + "additionalProperties": true + } + }, + "continuation": { + "type": "object", + "required": ["kind", "summary", "idempotencyKey"], + "properties": { + "kind": { + "enum": ["same_agent", "retry", "delegated_issue", "response_wake", "monitor"] + }, + "summary": { "type": "string", "minLength": 1 }, + "idempotencyKey": { "type": "string", "minLength": 1 } + }, + "additionalProperties": true + } + }, + "allOf": [ + { + "if": { "properties": { "reportedWorkDisposition": { "const": "blocked" } } }, + "then": { "required": ["blocker"] } + }, + { + "if": { "properties": { "reportedWorkDisposition": { "const": "yielded" } } }, + "then": { "required": ["continuation"] } + } + ], + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json b/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json new file mode 100644 index 0000000000..657a7e4e9f --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json @@ -0,0 +1,164 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/semantic-tool.schema.json", + "title": "PRP provider-neutral semantic tool envelope", + "type": "object", + "required": [ + "schema", + "schemaVersion", + "phase", + "operationId", + "callId", + "correlation", + "idempotencyKey", + "content" + ], + "properties": { + "schema": { "const": "paperclip.prp.semantic_tool.v1" }, + "schemaVersion": { "const": 1 }, + "phase": { "enum": ["input", "result"] }, + "operationId": { "$ref": "#/$defs/stableId" }, + "callId": { "$ref": "#/$defs/stableId" }, + "correlation": { + "type": "object", + "required": ["runId", "normalizedSessionId", "turnId", "itemId"], + "properties": { + "runId": { "$ref": "#/$defs/stableId" }, + "normalizedSessionId": { "$ref": "#/$defs/stableId" }, + "turnId": { "$ref": "#/$defs/stableId" }, + "itemId": { "$ref": "#/$defs/stableId" }, + "requestId": { "$ref": "#/$defs/stableId" } + }, + "additionalProperties": true + }, + "idempotencyKey": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 240 + }, + "content": { "$ref": "#/$defs/safeContent" }, + "outcome": { + "enum": ["succeeded", "denied", "conflict", "duplicate", "unavailable", "failed"] + }, + "code": { "$ref": "#/$defs/stableCode" }, + "retryable": { "type": "boolean" }, + "authorizationBoundary": { + "enum": ["company", "actor", "active_task", "grant", "governed_action", "lock", "revision"] + }, + "operationReceiptId": { "$ref": "#/$defs/stableId" }, + "auditReceiptId": { "$ref": "#/$defs/stableId" }, + "currentRevision": { + "oneOf": [ + { "type": "integer", "minimum": 0 }, + { "$ref": "#/$defs/stableId" } + ] + }, + "duplicateOfReceiptId": { "$ref": "#/$defs/stableId" }, + "artifactRefs": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/$defs/safeReference" }, + { + "type": "object", + "properties": { "kind": { "enum": ["artifact", "work_product"] } } + } + ] + }, + "uniqueItems": true + }, + "targets": { + "type": "array", + "items": { "$ref": "#/$defs/immutableTarget" }, + "uniqueItems": true + }, + "causalRefs": { + "type": "array", + "items": { "$ref": "#/$defs/safeReference" }, + "uniqueItems": true + } + }, + "allOf": [ + { + "if": { "properties": { "phase": { "const": "result" } } }, + "then": { + "required": [ + "outcome", + "code", + "retryable", + "authorizationBoundary", + "operationReceiptId" + ] + } + } + ], + "additionalProperties": true, + "$defs": { + "stableId": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "stableCode": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-z][a-z0-9_.:-]*$" + }, + "safeContent": { + "type": "object", + "required": ["digest", "redactionDisposition", "references"], + "properties": { + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "redactionDisposition": { + "enum": ["digest_only", "allowlisted_references", "redacted"] + }, + "references": { + "type": "array", + "items": { "$ref": "#/$defs/safeReference" }, + "uniqueItems": true + } + }, + "additionalProperties": true + }, + "safeReference": { + "type": "object", + "required": ["kind", "id"], + "properties": { + "kind": { + "enum": [ + "task", + "document_revision", + "interaction", + "approval", + "decision", + "artifact", + "work_product", + "wake", + "monitor", + "audit", + "operation" + ] + }, + "id": { "$ref": "#/$defs/stableId" } + }, + "additionalProperties": true + }, + "immutableTarget": { + "type": "object", + "required": ["kind", "id", "immutable"], + "properties": { + "kind": { "enum": ["document_revision", "interaction", "approval", "decision"] }, + "id": { "$ref": "#/$defs/stableId" }, + "immutable": { "const": true }, + "revisionId": { "$ref": "#/$defs/stableId" }, + "decisionId": { "$ref": "#/$defs/stableId" } + }, + "additionalProperties": true + } + } +} diff --git a/packages/paperclip-runner/protocol/schemas/stop-reason.schema.json b/packages/paperclip-runner/protocol/schemas/stop-reason.schema.json new file mode 100644 index 0000000000..183615f559 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/stop-reason.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/stop-reason.schema.json", + "title": "PRP terminal stop reason receipt", + "type": "object", + "required": [ + "schema", + "schemaVersion", + "receiptId", + "kind", + "code", + "retryable", + "decisionId", + "limitClass", + "aggregate" + ], + "properties": { + "schema": { "const": "paperclip.prp.stop_reason.v1" }, + "schemaVersion": { "const": 1 }, + "receiptId": { "$ref": "#/$defs/stableId" }, + "kind": { "enum": ["budget", "cost"] }, + "code": { + "type": "string", + "minLength": 1, + "maxLength": 160, + "pattern": "^[a-z][a-z0-9_.:-]*$" + }, + "retryable": { "type": "boolean" }, + "decisionId": { "$ref": "#/$defs/stableId" }, + "limitClass": { + "enum": [ + "company_monthly", + "actor_monthly", + "project_monthly", + "run_cost", + "provider_quota" + ] + }, + "aggregate": { + "type": "object", + "required": ["unit", "observed", "limit", "window"], + "properties": { + "unit": { "enum": ["cents", "usd_micros", "tokens"] }, + "observed": { "type": "integer", "minimum": 0 }, + "limit": { "type": "integer", "minimum": 0 }, + "window": { "enum": ["run", "monthly_utc", "provider_window"] } + }, + "additionalProperties": true + }, + "relatedReceiptIds": { + "type": "array", + "items": { "$ref": "#/$defs/stableId" }, + "uniqueItems": true + } + }, + "additionalProperties": true, + "$defs": { + "stableId": { + "type": "string", + "minLength": 1, + "maxLength": 240, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + } + } +} diff --git a/packages/paperclip-runner/protocol/schemas/terminal.schema.json b/packages/paperclip-runner/protocol/schemas/terminal.schema.json new file mode 100644 index 0000000000..3538d20120 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/terminal.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/terminal.schema.json", + "title": "PRP run terminal state", + "type": "object", + "required": [ + "schema", + "turnTerminalState", + "runTerminalState", + "reportedWorkDisposition" + ], + "properties": { + "schema": { "const": "paperclip.prp.terminal.v1" }, + "turnTerminalState": { + "enum": ["completed", "failed", "interrupted", "cancelled"] + }, + "runTerminalState": { "enum": ["succeeded", "failed", "cancelled"] }, + "reportedWorkDisposition": { + "enum": ["done", "blocked", "needs_review", "yielded"] + }, + "workAssessmentId": { "type": "string", "minLength": 1 }, + "statusDecisionId": { "type": "string", "minLength": 1 }, + "stopReason": { "$ref": "https://paperclip.dev/schemas/prp/v1/stop-reason.schema.json" } + }, + "additionalProperties": true +} diff --git a/packages/paperclip-runner/protocol/schemas/usage.schema.json b/packages/paperclip-runner/protocol/schemas/usage.schema.json new file mode 100644 index 0000000000..68c912f2dc --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/usage.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/usage.schema.json", + "title": "PRP provider usage receipt", + "type": "object", + "required": ["provider", "model", "cumulative", "runDelta"], + "properties": { + "provider": { "type": "string", "minLength": 1, "maxLength": 80 }, + "model": { "type": ["string", "null"], "maxLength": 240 }, + "providerSessionId": { "type": ["string", "null"], "maxLength": 240 }, + "providerRequestId": { "type": ["string", "null"], "maxLength": 240 }, + "cumulative": { "$ref": "#/$defs/measurement" }, + "runDelta": { "$ref": "#/$defs/measurement" } + }, + "additionalProperties": false, + "$defs": { + "measurement": { + "type": "object", + "required": ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "activeSeconds", "requests", "providerCostUsd"], + "properties": { + "inputTokens": { "type": "integer", "minimum": 0 }, + "outputTokens": { "type": "integer", "minimum": 0 }, + "cacheReadTokens": { "type": "integer", "minimum": 0 }, + "cacheWriteTokens": { "type": "integer", "minimum": 0 }, + "activeSeconds": { "type": "number", "minimum": 0 }, + "requests": { "type": "integer", "minimum": 0 }, + "providerCostUsd": { "type": "number", "minimum": 0 } + }, + "additionalProperties": false + } + } +} diff --git a/packages/paperclip-runner/protocol/schemas/workspace-diff.schema.json b/packages/paperclip-runner/protocol/schemas/workspace-diff.schema.json new file mode 100644 index 0000000000..ebf317dbae --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/workspace-diff.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/workspace-diff.schema.json", + "title": "Paperclip workspace diff", + "type": "object", + "required": ["schema", "changeSetId", "revision", "source", "complete", "files", "totals", "patchArtifactRef"], + "properties": { + "schema": { "const": "paperclip.workspace.diff.v1" }, + "changeSetId": { "type": "string", "minLength": 1, "maxLength": 200 }, + "revision": { "type": "integer", "minimum": 1 }, + "source": { "enum": ["harness_reported", "runner_verified"] }, + "complete": { "type": "boolean" }, + "files": { + "type": "array", + "maxItems": 2000, + "items": { + "type": "object", + "required": ["path", "operation", "previousPath", "additions", "deletions", "binary", "diff"], + "properties": { + "path": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" }, + "operation": { "enum": ["create", "modify", "delete", "rename", "mode_change"] }, + "previousPath": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" } + ] + }, + "additions": { "type": ["integer", "null"], "minimum": 0 }, + "deletions": { "type": ["integer", "null"], "minimum": 0 }, + "binary": { "type": "boolean" }, + "diff": { "type": ["string", "null"], "maxLength": 262144 } + }, + "additionalProperties": false + } + }, + "totals": { + "type": "object", + "required": ["files", "additions", "deletions"], + "properties": { + "files": { "type": "integer", "minimum": 0 }, + "additions": { "type": ["integer", "null"], "minimum": 0 }, + "deletions": { "type": ["integer", "null"], "minimum": 0 } + }, + "additionalProperties": false + }, + "patchArtifactRef": { "type": ["string", "null"], "maxLength": 2048 } + }, + "additionalProperties": false +} diff --git a/packages/paperclip-runner/protocol/schemas/workspace-file-reference.schema.json b/packages/paperclip-runner/protocol/schemas/workspace-file-reference.schema.json new file mode 100644 index 0000000000..e0dc8555d2 --- /dev/null +++ b/packages/paperclip-runner/protocol/schemas/workspace-file-reference.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://paperclip.dev/schemas/prp/v1/workspace-file-reference.schema.json", + "title": "Paperclip workspace file reference", + "type": "object", + "required": ["schema", "referenceId", "source", "path", "displayName", "mediaType", "presentation", "line", "preview", "previewTruncated", "contentDigest"], + "properties": { + "schema": { "const": "paperclip.workspace.file_reference.v1" }, + "referenceId": { "type": "string", "minLength": 1, "maxLength": 240 }, + "source": { "enum": ["harness_reported", "runner_verified"] }, + "path": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" }, + "displayName": { "type": "string", "minLength": 1, "maxLength": 255 }, + "mediaType": { "type": ["string", "null"], "maxLength": 160 }, + "presentation": { "enum": ["document", "code", "image", "generic"] }, + "line": { "type": ["integer", "null"], "minimum": 1 }, + "preview": { "type": ["string", "null"], "maxLength": 262144 }, + "previewTruncated": { "type": "boolean" }, + "contentDigest": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" } + ] + } + }, + "additionalProperties": false +} diff --git a/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs b/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs new file mode 100644 index 0000000000..3bc3d59462 --- /dev/null +++ b/packages/paperclip-runner/scripts/generate-protocol-manifest.mjs @@ -0,0 +1,114 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + SUPPORTED_FIXTURE_VERSION, + SUPPORTED_PROTOCOL_VERSION, + assertCodexQuestionFixture, + assertConformanceFixturePair, + assertReplayFixtureCompatibility, + assertSchemaInstance, + compileProtocolValidators, + listJsonFiles, + loadSchemaCatalog, + portableRelative, + readJson, + sha256, +} from "./protocol-contract.mjs"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +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"; + +export async function buildProtocolManifest() { + const schemas = await loadSchemaCatalog(schemaDirectory); + const validators = compileProtocolValidators(schemas); + const fixtureFiles = await listJsonFiles(fixtureDirectory); + const fixtures = []; + const fixtureValues = new Map(); + + for (const path of fixtureFiles) { + const relativePath = portableRelative(protocolRoot, path); + const { source, value } = await readJson(path); + fixtureValues.set(relativePath, value); + let expectation = "accept"; + let compatibilityCase = "canonical"; + + if (relativePath.startsWith("fixtures/replay/")) { + if (relativePath === expectedRejectedFixture) { + expectation = "reject"; + compatibilityCase = "unknown-required-version"; + try { + assertReplayFixtureCompatibility(value); + throw new Error(`${relativePath} did not fail closed`); + } catch (error) { + if (!String(error.message).startsWith("unsupported_required_version:")) throw error; + } + assertSchemaInstance(validators.fixture, value, relativePath, false); + } else { + assertReplayFixtureCompatibility(value); + assertSchemaInstance(validators.fixture, value, relativePath); + if (relativePath.endsWith("unknown-optional-fields.json")) { + compatibilityCase = "additive-optional-fields"; + } + } + } else if (relativePath === "fixtures/questions/codex.json") { + assertCodexQuestionFixture(value); + assertSchemaInstance(validators.questionAdapterFixture, value, relativePath); + compatibilityCase = "codex-structured-input"; + } else if (relativePath === "fixtures/conformance-minimal-run.json") { + assertSchemaInstance(validators.conformanceFixture, value, relativePath); + compatibilityCase = "cross-language-input"; + } else if (relativePath === "fixtures/conformance-expected-output.json") { + assertSchemaInstance(validators.conformanceOutput, value, relativePath); + compatibilityCase = "cross-language-output"; + } + + fixtures.push({ + path: relativePath, + sha256: sha256(source), + expectation, + compatibilityCase, + }); + } + + assertConformanceFixturePair( + fixtureValues.get("fixtures/conformance-minimal-run.json"), + fixtureValues.get("fixtures/conformance-expected-output.json"), + ); + + return { + schema: "paperclip.prp.contract_manifest.v1", + protocolVersion: SUPPORTED_PROTOCOL_VERSION, + fixtureVersion: SUPPORTED_FIXTURE_VERSION, + generatedFrom: ["protocol/schemas", "protocol/fixtures"], + schemas: schemas.map((record) => ({ + path: portableRelative(protocolRoot, record.path), + id: record.value.$id, + sha256: sha256(record.source), + })), + fixtures, + }; +} + +async function main() { + const encoded = `${JSON.stringify(await buildProtocolManifest(), null, 2)}\n`; + if (process.argv.includes("--check")) { + const current = await readFile(outputPath, "utf8").catch(() => ""); + if (current !== encoded) { + process.stderr.write("The generated PRP contract manifest is stale. Run pnpm generate:protocol-manifest.\n"); + process.exitCode = 1; + } else { + process.stdout.write("The generated PRP contract manifest matches its sources.\n"); + } + } else { + await writeFile(outputPath, encoded); + process.stdout.write(`Wrote ${outputPath}\n`); + } +} + +if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) await main(); diff --git a/packages/paperclip-runner/scripts/protocol-contract.mjs b/packages/paperclip-runner/scripts/protocol-contract.mjs new file mode 100644 index 0000000000..35743d4c7d --- /dev/null +++ b/packages/paperclip-runner/scripts/protocol-contract.mjs @@ -0,0 +1,243 @@ +import { createHash } from "node:crypto"; +import { readdir, readFile } from "node:fs/promises"; +import { relative, resolve, sep } from "node:path"; + +import Ajv2020 from "ajv/dist/2020.js"; + +export const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"; +export const PRP_SCHEMA_ID_PREFIX = "https://paperclip.dev/schemas/prp/v1/"; +export const SUPPORTED_FIXTURE_VERSION = 1; +export const SUPPORTED_PROTOCOL_VERSION = 1; +export const SUPPORTED_EVENT_SCHEMA_VERSION = 1; + +function contractError(code, detail) { + return new Error(`${code}: ${detail}`); +} + +export async function readJson(path) { + const source = await readFile(path, "utf8"); + try { + return { source, value: JSON.parse(source) }; + } catch (error) { + throw contractError("invalid_json", `${path}: ${error.message}`); + } +} + +export async function listJsonFiles(root) { + const entries = await readdir(root, { recursive: true, withFileTypes: true }); + return entries + .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) + .map((entry) => resolve(entry.parentPath, entry.name)) + .sort(); +} + +export function portableRelative(root, path) { + return relative(root, path).split(sep).join("/"); +} + +export function sha256(source) { + return createHash("sha256").update(source).digest("hex"); +} + +function collectReferences(value, output = []) { + if (Array.isArray(value)) { + for (const item of value) collectReferences(item, output); + return output; + } + if (value === null || typeof value !== "object") return output; + for (const [key, child] of Object.entries(value)) { + if (key === "$ref" && typeof child === "string") output.push(child); + else collectReferences(child, output); + } + return output; +} + +function resolveJsonPointer(value, fragment) { + if (fragment === "") return value; + if (!fragment.startsWith("/")) return undefined; + return fragment + .slice(1) + .split("/") + .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")) + .reduce((current, part) => { + if (current === null || typeof current !== "object") return undefined; + return Object.hasOwn(current, part) ? current[part] : undefined; + }, value); +} + +export async function loadSchemaCatalog(schemaDirectory) { + const files = await listJsonFiles(schemaDirectory); + const records = await Promise.all(files.map(async (path) => ({ path, ...(await readJson(path)) }))); + const ids = new Map(); + + for (const record of records) { + const schema = record.value; + if (schema.$schema !== JSON_SCHEMA_DIALECT) { + throw contractError("unsupported_schema_dialect", portableRelative(schemaDirectory, record.path)); + } + if (typeof schema.$id !== "string" || !schema.$id.startsWith(PRP_SCHEMA_ID_PREFIX)) { + throw contractError("invalid_schema_id", portableRelative(schemaDirectory, record.path)); + } + if (ids.has(schema.$id)) throw contractError("duplicate_schema_id", schema.$id); + if (typeof schema.title !== "string" || schema.title.length === 0) { + throw contractError("missing_schema_title", schema.$id); + } + ids.set(schema.$id, record); + } + + for (const record of records) { + for (const reference of collectReferences(record.value)) { + const [targetId, fragment = ""] = reference.split("#", 2); + const target = targetId === "" ? record : ids.get(targetId); + if (target === undefined) { + throw contractError("unresolved_schema_reference", `${record.value.$id} -> ${reference}`); + } + if (resolveJsonPointer(target.value, fragment) === undefined) { + throw contractError("unresolved_schema_fragment", `${record.value.$id} -> ${reference}`); + } + } + } + + return records; +} + +export function compileProtocolValidators(schemaRecords) { + const ajv = new Ajv2020({ + allErrors: true, + strict: true, + strictRequired: false, + formats: { + "date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/, + }, + }); + for (const record of schemaRecords) ajv.addSchema(record.value); + + const get = (name) => { + const id = `${PRP_SCHEMA_ID_PREFIX}${name}.schema.json`; + const validator = ajv.getSchema(id); + if (validator === undefined) throw contractError("missing_schema_validator", id); + return validator; + }; + return { + conformanceFixture: get("conformance-fixture"), + conformanceOutput: get("conformance-output"), + fixture: get("fixture"), + questionAdapterFixture: get("question-adapter-fixture"), + }; +} + +export function assertSchemaInstance(validator, value, location, expectedValid = true) { + const valid = validator(value); + if (valid !== expectedValid) { + const detail = (validator.errors ?? []) + .slice(0, 8) + .map((error) => `${error.instancePath || "/"} ${error.message}`) + .join("; "); + const expectation = expectedValid ? "accepted" : "rejected"; + throw contractError("schema_validation_failed", `${location} must be ${expectation}: ${detail || "no AJV error"}`); + } + return value; +} + +function requireSchema(value, expected, location) { + if (value?.schema !== expected) { + throw contractError("unsupported_required_schema", `${location} requires ${String(value?.schema)}`); + } +} + +function requireVersion(value, expected, name) { + if (value !== expected) { + throw contractError("unsupported_required_version", `${name}=${String(value)}; supported=${expected}`); + } +} + +export function assertReplayFixtureCompatibility(fixture) { + requireSchema(fixture, "paperclip.prp.fixture.v1", "fixture"); + requireVersion(fixture.fixtureVersion, SUPPORTED_FIXTURE_VERSION, "fixtureVersion"); + requireVersion(fixture.protocolVersion, SUPPORTED_PROTOCOL_VERSION, "protocolVersion"); + requireSchema(fixture.identity, "paperclip.prp.identity.v1", "identity"); + requireSchema(fixture.capabilities, "paperclip.prp.capabilities.v1", "capabilities"); + + if (!Array.isArray(fixture.commands)) throw contractError("invalid_fixture", "commands must be an array"); + for (const [index, command] of fixture.commands.entries()) { + requireSchema(command, "paperclip.prp.command.v1", `commands[${index}]`); + } + + if (!Array.isArray(fixture.events) || fixture.events.length === 0) { + throw contractError("invalid_fixture", "events must be a non-empty array"); + } + 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`); + } + + requireSchema(fixture.result, "paperclip.run_result.v1", "result"); + return fixture; +} + +export function assertCodexQuestionFixture(fixture) { + requireSchema(fixture, "paperclip.question_adapter_fixture.v1", "question fixture"); + if (fixture.adapter !== "codex") throw contractError("unsupported_provider", String(fixture.adapter)); + requireSchema(fixture.canonicalQuestionSet, "paperclip.question_set.v1", "canonicalQuestionSet"); + requireSchema(fixture.canonicalResponse, "paperclip.question_response.v1", "canonicalResponse"); + if (fixture.nativeRequest?.method !== "item/tool/requestUserInput") { + throw contractError("invalid_codex_question_fixture", "native request method"); + } + + const questions = fixture.canonicalQuestionSet.questions; + if (!Array.isArray(questions) || questions.length === 0) { + throw contractError("invalid_codex_question_fixture", "questions must be non-empty"); + } + const questionIds = new Set(); + const optionIdsByQuestion = new Map(); + for (const question of questions) { + if (typeof question.id !== "string" || question.id.length === 0 || questionIds.has(question.id)) { + throw contractError("invalid_codex_question_fixture", "question IDs must be unique"); + } + questionIds.add(question.id); + const optionIds = new Set(); + for (const option of question.options ?? []) { + if (typeof option.id !== "string" || option.id.length === 0 || optionIds.has(option.id)) { + throw contractError("invalid_codex_question_fixture", `option IDs for ${question.id} must be unique`); + } + optionIds.add(option.id); + } + optionIdsByQuestion.set(question.id, optionIds); + } + for (const [answerId, answer] of Object.entries(fixture.canonicalResponse.answers ?? {})) { + if (!questionIds.has(answerId)) { + throw contractError("invalid_codex_question_fixture", `answer has unknown question ID ${answerId}`); + } + for (const optionId of answer.selectedOptionIds ?? []) { + if (!optionIdsByQuestion.get(answerId)?.has(optionId)) { + throw contractError("invalid_codex_question_fixture", `answer has unknown option ID ${optionId}`); + } + } + } + return fixture; +} + +export function assertConformanceFixturePair(fixture, output) { + if (fixture?.schemaVersion !== "paperclip.runner.conformance.fixture.v1") { + throw contractError("unsupported_required_schema", `conformance fixture requires ${String(fixture?.schemaVersion)}`); + } + if (output?.schemaVersion !== "paperclip.runner.conformance.output.v1") { + throw contractError("unsupported_required_schema", `conformance output requires ${String(output?.schemaVersion)}`); + } + if (fixture.run?.runId !== output.runIdentity?.runId || fixture.run?.sessionId !== output.runIdentity?.sessionId) { + throw contractError("conformance_identity_mismatch", "run or session identity differs"); + } + for (const [index, event] of (fixture.events ?? []).entries()) { + if (event.runId !== fixture.run.runId || event.sequence !== index + 1) { + throw contractError("invalid_conformance_event", `events[${index}] does not match the run sequence`); + } + } + if ( + fixture.result?.status !== output.result?.status + || fixture.result?.summary !== output.result?.summary + || fixture.result?.runId !== fixture.run.runId + ) { + throw contractError("conformance_result_mismatch", "expected output does not match the fixture result"); + } + return { fixture, output }; +} diff --git a/packages/paperclip-runner/test/protocol-contract.test.mjs b/packages/paperclip-runner/test/protocol-contract.test.mjs new file mode 100644 index 0000000000..523b79f5ac --- /dev/null +++ b/packages/paperclip-runner/test/protocol-contract.test.mjs @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { buildProtocolManifest } from "../scripts/generate-protocol-manifest.mjs"; +import { + assertCodexQuestionFixture, + assertConformanceFixturePair, + assertReplayFixtureCompatibility, + assertSchemaInstance, + compileProtocolValidators, + loadSchemaCatalog, + readJson, +} from "../scripts/protocol-contract.mjs"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const protocolRoot = resolve(packageRoot, "protocol"); + +async function fixture(relativePath) { + return (await readJson(resolve(protocolRoot, "fixtures", relativePath))).value; +} + +test("all schema IDs are unique and all external references resolve", async () => { + const schemas = await loadSchemaCatalog(resolve(protocolRoot, "schemas")); + assert.equal(schemas.length, 20); + assert.doesNotThrow(() => compileProtocolValidators(schemas)); +}); + +test("the generated manifest matches all checked-in schemas and fixtures", async () => { + const expected = `${JSON.stringify(await buildProtocolManifest(), null, 2)}\n`; + const actual = await readFile(resolve(protocolRoot, "manifest.json"), "utf8"); + assert.equal(actual, expected); +}); + +test("canonical replay fixtures use supported required versions", async () => { + for (const name of [ + "duplicate-event.json", + "failed-run.json", + "happy-path.json", + "interrupted-run.json", + "source-gap.json", + ]) { + assert.equal(assertReplayFixtureCompatibility(await fixture(`replay/${name}`)).protocolVersion, 1); + } +}); + +test("unknown additive fields remain compatible with PRP v1", async () => { + const value = await fixture("replay/unknown-optional-fields.json"); + assert.equal(value.futureFixtureHint.producerVersion, "1.1-preview"); + assert.doesNotThrow(() => assertReplayFixtureCompatibility(value)); +}); + +test("unknown required versions and schemas fail closed", async () => { + const unsupported = await fixture("replay/unsupported-required-version.json"); + assert.throws( + () => assertReplayFixtureCompatibility(unsupported), + /unsupported_required_version: protocolVersion=2; supported=1/, + ); + + const eventVersion = structuredClone(await fixture("replay/happy-path.json")); + eventVersion.events[0].schemaVersion = 2; + assert.throws(() => assertReplayFixtureCompatibility(eventVersion), /unsupported_required_version/); + + const commandSchema = structuredClone(await fixture("replay/happy-path.json")); + commandSchema.commands[0].schema = "paperclip.prp.command.v2"; + assert.throws(() => assertReplayFixtureCompatibility(commandSchema), /unsupported_required_schema/); +}); + +test("accepted fixtures satisfy the complete JSON Schemas", async () => { + const schemas = await loadSchemaCatalog(resolve(protocolRoot, "schemas")); + const validators = compileProtocolValidators(schemas); + const happyPath = await fixture("replay/happy-path.json"); + assert.doesNotThrow(() => assertSchemaInstance(validators.fixture, happyPath, "happy-path")); + + const missingRequiredField = structuredClone(happyPath); + delete missingRequiredField.commands[0].commandId; + assert.throws( + () => assertSchemaInstance(validators.fixture, missingRequiredField, "missing-command-id"), + /schema_validation_failed: missing-command-id must be accepted: \/commands\/0 must have required property 'commandId'/, + ); + + const unsupported = await fixture("replay/unsupported-required-version.json"); + assert.doesNotThrow(() => assertSchemaInstance(validators.fixture, unsupported, "required-v2", false)); +}); + +test("the Codex question fixture uses stable provider-neutral IDs", async () => { + const value = await fixture("questions/codex.json"); + assert.doesNotThrow(() => assertCodexQuestionFixture(value)); + assert.deepEqual(Object.keys(value.canonicalResponse.answers), ["environment"]); +}); + +test("the cross-language conformance input and output have one stable identity", async () => { + const input = await fixture("conformance-minimal-run.json"); + const output = await fixture("conformance-expected-output.json"); + const schemas = await loadSchemaCatalog(resolve(protocolRoot, "schemas")); + const validators = compileProtocolValidators(schemas); + assert.doesNotThrow(() => assertSchemaInstance(validators.conformanceFixture, input, "conformance-input")); + assert.doesNotThrow(() => assertSchemaInstance(validators.conformanceOutput, output, "conformance-output")); + assert.doesNotThrow(() => assertConformanceFixturePair(input, output)); + assert.equal(input.result.summary, output.result.summary); + + const missingSessionId = structuredClone(output); + delete missingSessionId.runIdentity.sessionId; + assert.throws( + () => assertSchemaInstance(validators.conformanceOutput, missingSessionId, "missing-session-id"), + /schema_validation_failed: missing-session-id must be accepted: \/runIdentity must have required property 'sessionId'/, + ); +});