Files
PaperClipAI/packages/paperclip-runner/test/acpx-sidecar-contract.test.mjs
Dotta 1ee738cf48 feat(runner): project durable ACPX events (#12424)
## 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
2026-08-31 03:33:38 -05:00

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,
};
}