mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Rust runner now owns validated, scoped ACPX reducer events and safe session suspension > - Durable PRP transport must receive provider-neutral events rather than sidecar-native envelopes > - Semantic calls and questions must retain the exact run, session, turn, item, and provider-request authority used by the durable command stream > - Terminal, result, assistant, process, and diagnostic events also need one reviewed projection boundary > - Permission requests remain impossible under the pinned Codex policy and must fail closed if they reach projection > - This pull request adds only that package-local projection without selecting ACPX in runnerd ## Linked Issues or Issue Description Refs #12422 ## What Changed - Add a validated durable ACPX event projection context bound to one run, normalized session, turn, and item. - Pass already normalized activity events through without reintroducing provider-native envelopes. - Project authorized tool calls into canonical semantic input receipts with exact correlation and content digests. - Project structured questions into provider-neutral `paperclip.runtime_request.v2` events. - Preserve both the public projected request identity and the original provider request identity so responses resolve the exact sidecar request. - Project dynamic semantic operation results as `semantic_tool.result`; only reserved finish/block operations may propose the run result. - Project semantic completion results into `run.result.proposed`. - Project terminal-flushed assistant messages on the final channel and turn terminal states into existing provider-neutral event families. - Project sanitized process metadata and diagnostics into bounded harness diagnostics. - Validate runtime-request origins against their strict durable shape and fall back from empty optional titles to a valid question prompt. - Reject invalid identities, projected-identity collisions, unstable semantic receipt identities, permission requests, and cross-turn projection fail closed. - Add integration coverage across reducer event families, correlation, identity validation, projected question resolution, and pinned-policy denial. - Document the durable projection boundary. - Do not change dependencies, lockfiles, workflows, runnerd selection, server behavior, UI, or migrations. ## Verification - Replay base: `80639f4f69c8938eb74bdc0833df93e0ed91dab3` (`master` after #12422 merged). - Exact replay head: `3cb29581d2bcbc4b47f8069baffd721c6ce4e444`. - Stable patch ID: `92910b56575e67ae83960177d467a565019ba282`. - The exact delta is 22 files, 1,206 additions, and 59 deletions, all in `packages/paperclip-runner`; it contains no lockfile, workflow, server, UI, dependency, or migration change. - `git diff --check` and the Cargo formatting check pass on the replayed delta. - Exact-head GitHub Actions run `33372209037` (attempt 2): **PASSED** with 23/23 jobs passed. - Greptile reviewed exact head `3cb29581d2bcbc4b47f8069baffd721c6ce4e444`: **5/5**, with zero unresolved review threads. - Superagent, contributor trust, Socket, and Snyk security checks: **PASSED**. - No local test result is claimed. GitHub Actions is the authoritative verification environment for this replayed revision. ## Risks - This function accepts reducer output, not raw sidecar frames. Callers must preserve the existing scope-first decode and reduction order. - Semantic input includes the already sanitized provider input while its content receipt uses the same canonical digest. - Structured input preserves the validated provider-neutral question set and sanitized origin. - Noncanonical provider request identities are deterministically projected for PRP while the original identity remains authoritative for the sidecar resolution command. - Existing PRP v1 identifiers remain schema-compatible; the only public ID-schema change widens turn/item limits from 160 to 240 characters. The internal ACPX sidecar wire schema now mirrors the stable IDs its Rust transport already enforced. - The projector verifies event-carried terminal and assistant turn identifiers against the durable context. - No production path invokes this projector in this pull request. Durable command execution remains the next slice. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex with GPT-5.6, agentic reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used with version and capability details - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have linked the preceding public PR or described the issue in-PR - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [ ] 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
109 lines
3.1 KiB
JavaScript
109 lines
3.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
|
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
|
|
import { readAcpxSidecarProtocolVersion } from "../scripts/acpx-sidecar-contract.mjs";
|
|
|
|
const schema = JSON.parse(
|
|
await readFile(
|
|
new URL(
|
|
"../protocol/provider-schemas/acpx-sidecar.schema.json",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
),
|
|
);
|
|
const validate = new Ajv2020({ allErrors: true, strict: true }).compile(schema);
|
|
const protocolVersion = readAcpxSidecarProtocolVersion(schema);
|
|
|
|
const messages = [
|
|
{
|
|
protocolVersion,
|
|
id: 1,
|
|
command: "initialize",
|
|
params: {},
|
|
},
|
|
{
|
|
protocolVersion,
|
|
id: 1,
|
|
ok: true,
|
|
result: {},
|
|
},
|
|
{
|
|
protocolVersion,
|
|
sequence: 1,
|
|
eventType: "runtime.event",
|
|
runId: "run-1",
|
|
turnId: "turn-1",
|
|
payload: {},
|
|
},
|
|
];
|
|
|
|
test("the ACPX sidecar schema accepts each versioned message family", () => {
|
|
for (const message of messages) {
|
|
assert.equal(validate(message), true, JSON.stringify(validate.errors));
|
|
}
|
|
});
|
|
|
|
test("the ACPX sidecar schema shares the durable stable-identity boundary", () => {
|
|
const longestTurnId = "t".repeat(240);
|
|
assert.equal(validate({ ...messages[2], turnId: longestTurnId }), true);
|
|
assert.equal(validate({ ...messages[2], turnId: "t".repeat(241) }), false);
|
|
for (const turnId of ["turn 1", "réturn-1", "turn/1", "_turn-1"]) {
|
|
assert.equal(validate({ ...messages[2], turnId }), false, turnId);
|
|
}
|
|
assert.equal(validate({ ...messages[2], runId: "r".repeat(161) }), false);
|
|
for (const runId of ["run 1", "rún-1", "run/1", "_run-1"]) {
|
|
assert.equal(validate({ ...messages[2], runId }), false, runId);
|
|
}
|
|
});
|
|
|
|
test("the ACPX sidecar schema fails closed on drift", () => {
|
|
for (const message of [
|
|
{ ...messages[0], protocolVersion: protocolVersion + 1 },
|
|
{ ...messages[0], command: "session.destroy" },
|
|
{ protocolVersion, id: 1, ok: true, result: {}, error: error() },
|
|
{ protocolVersion, id: 1, ok: true },
|
|
{ protocolVersion, id: 1, ok: false },
|
|
{ protocolVersion, id: 1, ok: false, result: {}, error: error() },
|
|
{ ...messages[2], unexpected: true },
|
|
]) {
|
|
assert.equal(validate(message), false);
|
|
}
|
|
});
|
|
|
|
test("every ACPX sidecar message family uses the shared version", () => {
|
|
for (const family of ["request", "response", "event"]) {
|
|
assert.deepEqual(schema.$defs[family].properties.protocolVersion, {
|
|
$ref: "#/$defs/protocolVersion",
|
|
});
|
|
}
|
|
});
|
|
|
|
test("the ACPX sidecar schema id carries the declared protocol version", () => {
|
|
assert.equal(
|
|
schema.$id,
|
|
`https://paperclip.dev/schemas/acpx-sidecar/v${protocolVersion}/message.schema.json`,
|
|
);
|
|
});
|
|
|
|
test("a coordinated family-version upgrade cannot outpace the schema id", () => {
|
|
const driftedSchema = structuredClone(schema);
|
|
driftedSchema.$defs.protocolVersion.const = protocolVersion + 1;
|
|
|
|
assert.throws(
|
|
() => readAcpxSidecarProtocolVersion(driftedSchema),
|
|
/must match its authoritative schema \$id/,
|
|
);
|
|
});
|
|
|
|
function error() {
|
|
return {
|
|
code: "runtime_failed",
|
|
message: "The runtime failed.",
|
|
retryable: false,
|
|
};
|
|
}
|