From fdbc69172d6e1c8aa77f0575aad9731fdbb4f015 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:59:03 -0500 Subject: [PATCH] feat(runner): add PRP v1 schemas and fixtures (#12087) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Runner needs a language-neutral contract between the server and the runner process. > - A shared contract must exist before TypeScript, Rust, transport, or provider implementations can depend on it. > - Required protocol versions must fail closed, while safe optional fields must remain compatible. > - The contract also needs deterministic fixtures and a drift gate for later cross-language work. > - This pull request adds that contract without adding runtime behavior. > - The benefit is a small, reviewable source of truth for the next implementation pull requests. ## Linked Issues or Issue Description **Subsystem affected** Cross-cutting. This pull request adds a private package contract for later server, TypeScript, and Rust work. **Problem or motivation** Paperclip Runner does not have a small language-neutral protocol boundary on `master`. A runtime implementation without this boundary can drift between languages, accept unsupported required versions, or silently change canonical fixtures. **Proposed solution** Add PRP v1 JSON Schemas, accepted and rejected fixtures, a Codex structured-question fixture, and a generated SHA-256 manifest. Run compatibility and manifest checks during the package build. Keep the package private and export nothing in this pull request. **Alternatives considered** The combined runner branch contains schemas together with providers, SDKs, labs, and server behavior. That change is too large for normal review. Generating TypeScript validators in this pull request would also cross into the next review unit. **Roadmap alignment** This contract supports the governed tool and control-plane direction in `ROADMAP.md`. It does not enable a new production adapter or endpoint. **Additional context** Refs #12084 and #11962. This pull request was reviewed as a stack on #12084, then rebased and retargeted to `master` after #12084 merged. The current delta is 38 files. ## What Changed - Added 20 PRP v1 JSON Schemas with stable identifiers and resolved references, including explicit cross-language conformance input and output schemas. - Added canonical replay, cross-language, and Codex question fixtures. - Added accepted cases for additive optional fields and a rejected case for an unsupported required protocol version. - Added a deterministic manifest with SHA-256 digests for every schema and fixture. - Added package-local schema-instance, schema-reference, compatibility, question-ID, conformance-pair, and drift checks. - Added a private workspace package with no public exports and no production runtime behavior. - Added the package manifest to the Docker dependency-stage inventory required for every workspace package. This does not copy or build runner runtime code into the production image. - Kept the provider descriptor and question fixture Codex-only. No deferred provider package or dependency is present. ## Verification - `pnpm install --frozen-lockfile` passed with Node 24.19.0 and pnpm 9.15.4. No lockfile change is committed. - `pnpm --filter @paperclipai/paperclip-runner check:protocol` passed with 8 tests. - The committed AJV 2020-12 gate accepted every canonical v1 replay, question, and cross-language conformance fixture. It rejected the required v2 fixture, a replay fixture with a missing required command ID, and conformance output with a missing session ID. - `pnpm -r typecheck` passed. - `pnpm build` passed and ran the protocol manifest drift check. - `pnpm check:token-gates` passed. - `node ./scripts/check-docker-deps-stage.mjs` passed. - `git diff --check` passed. - The delta against its declared base is 38 files. - `pnpm test:run` completed with 4,687 passing tests, 19 skipped tests, and 29 failures across 9 unchanged server files. The failures reproduce macOS path aliases, local listener probes, workspace-runtime assumptions, and one connection-retry timeout. No changed-file test failed. Linux CI must pass before this pull request is ready. - `pnpm check:tokens` reports existing personal-name references outside this pull request. A scoped scan of `packages/paperclip-runner` found no secret-like values, internal references, or deferred-provider names. - PR #12084 was squash-merged, and this branch was rebased onto that merge and retargeted to `master`. The first master-base policy run correctly caught the missing Docker dependency-stage manifest copy; commit `4fa1ea7c` fixes that gate, and the complete Linux matrix is green. - Serialized server shard 1 initially hit an unchanged heartbeat test-harness timeout and a later assertion in the same file. Its isolated rerun passed in 3m57s. All other shards passed on their first attempt. - Greptile reviewed the final commit at 5/5 with no blocking failure. Both earlier actionable validation threads are resolved, and no review thread remains open. ## Risks Low production risk. The package is private and has no exports, server adapter, endpoint, or process. AJV is a package-only development dependency that the server workspace already uses. The main risk is contract churn before the TypeScript and Rust consumers land. The generated manifest and compatibility fixtures make that churn explicit. I checked `ROADMAP.md`. This change defines a contract for planned control-plane work and does not add overlapping product behavior. ## Model Used OpenAI Codex with GPT-5 was used. The exact serving model ID and context size were not exposed. The model used high reasoning, repository tools, GitHub tools, and local code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- Dockerfile | 1 + packages/paperclip-runner/README.md | 27 ++ packages/paperclip-runner/package.json | 21 ++ packages/paperclip-runner/protocol/README.md | 32 +++ .../fixtures/conformance-expected-output.json | 1 + .../fixtures/conformance-minimal-run.json | 29 +++ .../protocol/fixtures/questions/codex.json | 40 +++ .../fixtures/replay/duplicate-event.json | 30 +++ .../protocol/fixtures/replay/failed-run.json | 106 ++++++++ .../protocol/fixtures/replay/happy-path.json | 230 +++++++++++++++++ .../fixtures/replay/interrupted-run.json | 90 +++++++ .../protocol/fixtures/replay/source-gap.json | 19 ++ .../replay/unknown-optional-fields.json | 19 ++ .../replay/unsupported-required-version.json | 13 + .../paperclip-runner/protocol/manifest.json | 173 +++++++++++++ .../protocol/schemas/capabilities.schema.json | 98 +++++++ .../protocol/schemas/command.schema.json | 48 ++++ .../schemas/conformance-fixture.schema.json | 51 ++++ .../schemas/conformance-output.schema.json | 32 +++ .../protocol/schemas/event.schema.json | 173 +++++++++++++ .../protocol/schemas/fixture.schema.json | 42 +++ .../protocol/schemas/identity.schema.json | 36 +++ .../schemas/provider-descriptor.schema.json | 17 ++ .../schemas/provider-event.schema.json | 167 ++++++++++++ .../question-adapter-fixture.schema.json | 16 ++ .../schemas/question-response.schema.json | 28 ++ .../protocol/schemas/question-set.schema.json | 90 +++++++ .../protocol/schemas/request.schema.json | 79 ++++++ .../protocol/schemas/result.schema.json | 151 +++++++++++ .../schemas/semantic-tool.schema.json | 164 ++++++++++++ .../protocol/schemas/stop-reason.schema.json | 65 +++++ .../protocol/schemas/terminal.schema.json | 26 ++ .../protocol/schemas/usage.schema.json | 32 +++ .../schemas/workspace-diff.schema.json | 49 ++++ .../workspace-file-reference.schema.json | 26 ++ .../scripts/generate-protocol-manifest.mjs | 114 ++++++++ .../scripts/protocol-contract.mjs | 243 ++++++++++++++++++ .../test/protocol-contract.test.mjs | 110 ++++++++ 38 files changed, 2688 insertions(+) create mode 100644 packages/paperclip-runner/README.md create mode 100644 packages/paperclip-runner/package.json create mode 100644 packages/paperclip-runner/protocol/README.md create mode 100644 packages/paperclip-runner/protocol/fixtures/conformance-expected-output.json create mode 100644 packages/paperclip-runner/protocol/fixtures/conformance-minimal-run.json create mode 100644 packages/paperclip-runner/protocol/fixtures/questions/codex.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/duplicate-event.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/failed-run.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/happy-path.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/interrupted-run.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/source-gap.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/unknown-optional-fields.json create mode 100644 packages/paperclip-runner/protocol/fixtures/replay/unsupported-required-version.json create mode 100644 packages/paperclip-runner/protocol/manifest.json create mode 100644 packages/paperclip-runner/protocol/schemas/capabilities.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/command.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/conformance-fixture.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/conformance-output.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/event.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/fixture.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/identity.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/provider-descriptor.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/provider-event.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/question-adapter-fixture.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/question-response.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/question-set.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/request.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/result.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/semantic-tool.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/stop-reason.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/terminal.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/usage.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/workspace-diff.schema.json create mode 100644 packages/paperclip-runner/protocol/schemas/workspace-file-reference.schema.json create mode 100644 packages/paperclip-runner/scripts/generate-protocol-manifest.mjs create mode 100644 packages/paperclip-runner/scripts/protocol-contract.mjs create mode 100644 packages/paperclip-runner/test/protocol-contract.test.mjs 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'/, + ); +});