Files
PaperClipAI/packages/paperclip-runner/test/fixtures/fake-opencode-server.mjs
T
Devin FoleyandPaperclip be6f49a425 feat(runner): refresh shared coding harness runtimes (#13838)
## Thinking Path

> - Paperclip runs agents through local adapters and the native runner.
> - Both paths must use the same installed provider CLI.
> - New models require current harness releases.
> - The runner still pins Codex 0.153.4, Claude SDK 0.3.263, and
OpenCode 1.18.29.
> - Changing the image alone would fail the runner's exact version and
executable checks.
> - This pull request updates those dependencies, integrity checks,
controller checks, and image pins together.
> - Shared installations can then run the current models without a
task-time download.

## Linked Issues or Issue Description

Refs #13829, which updates model choices and reasoning controls.
Searches found no open PR that updates these runtime pins.

**Current behavior**

The shared provider pack ships old CLIs. Claude Code 2.1.263 cannot run
Opus 5.5, which requires 2.1.280. Remote controllers reject provider
packs whose versions differ from their declared pins.

**Proposed behavior**

Use Codex 0.156.0, Claude Agent SDK 0.3.280 / Claude Code 2.1.280, and
OpenCode 1.18.32 throughout the runner. Keep the reviewed ACP bridge
patches and one shared CLI installation per provider.

**Reason and benefit**

Current harnesses support the new model IDs while preserving executable
verification and remote provider-pack compatibility checks.

## What Changed

- Update dependency overrides, the Codex ACP package patch, runtime
profiles, and remote controller pins.
- Verify the new Claude Linux x64 and macOS arm64/x64 executables and
Codex Linux x64 executable against integrity-verified npm archives.
- Refresh OpenCode version checks, fixtures, and the runner
configuration label.
- Refresh the eval image's Grok, Gemini, Kimi, Cursor, and GitHub CLI
pins and archive hashes. Hermes remains current at 0.19.0.
- Refresh the build-time lock digest from clean pnpm 9.15.4 resolution.
Leave lockfile commits to repository automation.
- Document model compatibility and the separation between CLI runtimes
and patched ACP bridges.

## Verification

- `pnpm -r typecheck` and `pnpm build` passed.
- Rust workspace release tests passed.
- Package/patch and OpenCode binary-materialization contract tests: 11
passed.
- Real Codex 0.156.0 startup-ownership and paginated session-resume
probes passed with isolated synthetic homes and no model turn.
- Codex app-server `thread/start` preserved `gpt-6-sol` and
`gpt-6-luna`; no `turn/start` was sent. An unauthenticated built-in
catalog does not include those account-served entries.
- Installed Claude integrity probes passed for `claude-opus-5-5` and
`claude-fable-5-1`.
- `pnpm --filter @paperclipai/paperclip-runner
test:opencode:qualification` passed with the actual OpenCode 1.18.32
executable under Node 24 and Node 25. The loopback provider exercise
covers health/version, session creation/read/delete, SSE, and a
completed async prompt.
- `pnpm check:token-gates` passed.
- The targeted runner suite passed 130 tests. Three macOS failures in
snapshot module lookup and OpenCode final-message selection also
reproduce on the unchanged base; Linux CI will provide the platform
check.
- [Final Linux
CI](https://github.com/paperclipai/paperclip/actions/runs/35798076399):
all gates passed. Four jobs needed one retry after their CI workers
received shutdown signals. The PR has 55 successful checks, two skipped
checks, Greptile 5/5, and no unresolved review threads.
- Changed runner configuration UI tests: 5 passed.
- Full macOS `pnpm test:run` reached 13,094 passing server tests, 84
skipped, and 18 failures before the wrapper stopped. Failures involved
skill-cache publication permissions, missing bundled connector skills in
the worktree, and a conversation-reset timing case. The 10 cache
permission failures reproduce on the unchanged base; both
conversation-reset cases passed on a targeted retry. The wrapper did not
reach its later workspace/serialized groups locally; Linux CI covers
those groups.
- The local Docker daemon did not respond, so no local Docker build was
run. No billable model requests were made.

## Risks

- Deploy the matching controller and provider pack together. Older
controllers enforce their previous exact pins.
- Current upstream CLIs can change behavior. Existing protocol tests and
isolated real Codex probes cover the integration boundaries;
authenticated model inference is not part of these checks.
- ACP bridge package versions and executable digests stay unchanged
because their executable bytes are unchanged. Only the underlying
CLI/SDK dependencies move.
- No schema migration. Revert the runtime and image pins together to
roll back.

## Model Used

OpenAI GPT-6 via Codex, with repository tools, code execution, and web
research. The exact serving model ID and context window were not exposed
by this session.

## 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
- [x] I have run tests locally and they pass for the changed surfaces
and real-executable probes; full macOS-suite limitations are listed
above
- [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 <noreply@paperclip.ing>
2026-09-22 17:02:29 -07:00

760 lines
24 KiB
JavaScript

#!/usr/bin/env node
import { createServer } from "node:http";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const args = process.argv.slice(2);
// Exercise diagnostic redaction without copying a real environment credential
// into stderr or the provider trace.
process.stderr.write("authorization=synthetic-redaction-sentinel\n");
const port = Number(args[args.indexOf("--port") + 1]);
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
const password = process.env.OPENCODE_SERVER_PASSWORD ?? "";
const expectedAuth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
const session = { id: "ses_fake_1", title: "Paperclip fake" };
const clients = new Set();
let eventConnections = 0;
const runtimeConfig = JSON.parse(
await readFile(
join(process.env.XDG_CONFIG_HOME, "opencode", "opencode.json"),
"utf8",
),
);
const mcp = runtimeConfig.mcp?.paperclip;
let mcpRequestId = 1;
const mcpEvidence = { tools: [], calls: [] };
function nativeQuestion() {
return {
id: "question-native-1",
sessionID: session.id,
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.",
},
],
custom: true,
},
{
id: "regions",
header: "Regions",
question: "Which regions?",
options: [{ label: "US" }, { label: "EU" }],
multiple: true,
},
],
};
}
function nativePermission(style) {
return style === "legacy"
? {
id: "permission-native-1",
type: "tool",
pattern: "echo OK",
sessionID: session.id,
messageID: "message-permission",
callID: "call-permission",
title: "Run validation command",
metadata: {},
time: { created: Date.now() },
}
: {
id: "permission-native-1",
sessionID: session.id,
permission: "bash",
patterns: ["echo OK"],
always: ["echo *"],
metadata: {},
tool: { messageID: "message-permission", callID: "call-permission" },
};
}
let pendingQuestion = await readFile(
join(process.env.XDG_DATA_HOME, "fake-pending-question.json"),
"utf8",
)
.then((value) => JSON.parse(value))
.catch(() => null);
let pendingPermission = await readFile(
join(process.env.XDG_DATA_HOME, "fake-pending-permission.json"),
"utf8",
)
.then((value) => JSON.parse(value))
.catch(() => null);
let permissionStyle = "v2";
await mkdir(process.env.XDG_DATA_HOME, { recursive: true });
await writeFile(
join(process.env.XDG_DATA_HOME, "fake-environment.json"),
JSON.stringify({
keys: Object.keys(process.env).sort(),
home: process.env.HOME,
configHome: process.env.XDG_CONFIG_HOME,
projectConfigDisabled: process.env.OPENCODE_DISABLE_PROJECT_CONFIG,
}),
);
function json(response, status, value) {
response.statusCode = status;
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(value));
}
function emit(value) {
const frame = `data: ${JSON.stringify(value)}\n\n`;
for (const response of clients) response.write(frame);
}
async function mcpRequest(method, params) {
const response = await fetch(mcp.url, {
method: "POST",
headers: {
...mcp.headers,
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: mcpRequestId++,
method,
...(params ? { params } : {}),
}),
});
return response.json();
}
async function callFirstPaperclipTool() {
if (!mcp?.url) return;
await mcpRequest("initialize", {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "fake-opencode", version: "1" },
});
const listed = await mcpRequest("tools/list");
mcpEvidence.tools = (listed.result?.tools ?? []).map((entry) => entry.name);
const tool = listed.result?.tools?.find(
(entry) =>
!["paperclip_finish", "paperclip_block"].includes(entry.name) &&
(entry.inputSchema?.required?.length ?? 0) === 0,
);
if (tool)
mcpEvidence.calls.push(
await mcpRequest("tools/call", {
name: `paperclip_${tool.name}`,
arguments: {},
}),
);
await writeFile(
join(process.env.XDG_DATA_HOME, "fake-mcp-evidence.json"),
JSON.stringify(mcpEvidence),
);
}
async function callTerminalTool(promptBody) {
const prompt = JSON.parse(promptBody.parts?.[0]?.text ?? "{}");
const blocked = String(prompt.message ?? "").includes("block-result");
const result = {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: blocked ? "blocked" : "done",
summary: blocked
? "Fake provider is blocked."
: "Fake provider completed the task.",
completionClaim: {
contractRevision:
prompt.task?.completionContract?.revision ?? "codex-demo-v1",
objectiveSatisfied: !blocked,
criteria: (prompt.task?.completionContract?.criteria ?? []).map(
(criterion) => ({
criterionId: criterion.id,
status: blocked ? "unknown" : "satisfied",
evidenceRefs: [],
}),
),
remainingWork: blocked
? [
{
description: "Waiting on a test dependency.",
blocksCompletion: true,
},
]
: [],
},
evidence: [],
verification: [],
attentionRequests: blocked
? [{ kind: "blocker", summary: "Waiting on a test dependency." }]
: [],
artifacts: [],
...(blocked
? {
blocker: {
reasonCode: "test_dependency",
owner: { kind: "external", name: "fixture" },
unblockAction: "Release the fixture dependency.",
scope: "current_track",
},
}
: {}),
};
if (String(prompt.message ?? "").includes("repair-criteria")) {
const bad = structuredClone(result);
bad.completionClaim.criteria = String(prompt.message).includes("repair-criteria-missing") ? []
: String(prompt.message).includes("repair-criteria-duplicate") ? [...result.completionClaim.criteria, ...result.completionClaim.criteria]
: [{ criterionId: "invented", status: "satisfied", evidenceRefs: [] }];
const rejected = await mcpRequest("tools/call", { name: "paperclip_finish", arguments: bad });
await writeFile(join(process.env.XDG_DATA_HOME, "fake-criteria-repair.json"), JSON.stringify(rejected));
}
return mcpRequest("tools/call", {
name: blocked ? "paperclip_block" : "paperclip_finish",
arguments: result,
});
}
const server = createServer(async (request, response) => {
if (request.headers.authorization !== expectedAuth)
return json(response, 401, { error: "unauthorized" });
if (request.url === "/global/health")
return json(response, 200, { healthy: true, version: "1.18.32" });
if (request.url === "/event") {
eventConnections += 1;
response.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
});
response.write(
`data: ${JSON.stringify({ type: "server.connected", properties: {} })}\n\n`,
);
if (eventConnections === 1) {
response.end();
return;
}
clients.add(response);
request.on("close", () => clients.delete(response));
return;
}
if (request.method === "POST" && request.url === "/session")
return json(response, 200, session);
if (request.method === "GET" && request.url === `/session/${session.id}`)
return json(response, 200, session);
if (
request.method === "GET" &&
request.url === `/session/${session.id}/message`
)
return json(response, 200, []);
if (request.method === "GET" && request.url === "/session/status")
return json(response, 200, { [session.id]: { type: "idle" } });
if (request.method === "GET" && request.url?.startsWith("/question?"))
return json(response, 200, pendingQuestion ? [pendingQuestion] : []);
if (request.method === "GET" && request.url?.startsWith("/permission?"))
return json(response, 200, pendingPermission ? [pendingPermission] : []);
if (
request.method === "POST" &&
request.url?.startsWith("/question/question-native-1/reply?")
) {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", async () => {
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
await writeFile(
join(process.env.XDG_DATA_HOME, "fake-question-reply.json"),
JSON.stringify({ url: request.url, body }),
);
const repliedQuestion = pendingQuestion;
pendingQuestion = null;
emit({
type: "question.replied",
id: "event-question-replied",
properties: repliedQuestion,
});
setTimeout(() => {
json(response, 200, true);
emit({
type: "session.idle",
id: "event-question-idle",
properties: { sessionID: session.id },
});
}, 10);
});
return;
}
if (
request.method === "POST" &&
request.url?.startsWith("/question/question-native-1/reject?")
) {
const rejectedQuestion = pendingQuestion;
pendingQuestion = null;
emit({
type: "question.rejected",
id: "event-question-rejected",
properties: rejectedQuestion,
});
setTimeout(() => {
json(response, 200, true);
emit({
type: "session.idle",
id: "event-question-rejected-idle",
properties: { sessionID: session.id },
});
}, 10);
return;
}
if (
request.method === "POST" &&
request.url?.startsWith("/permission/permission-native-1/reply?")
) {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", async () => {
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
await writeFile(
join(process.env.XDG_DATA_HOME, "fake-permission-reply.json"),
JSON.stringify({ url: request.url, body }),
);
pendingPermission = null;
emit({
type:
permissionStyle === "legacy"
? "permission.replied"
: "permission.v2.replied",
id: "event-permission-replied",
properties:
permissionStyle === "legacy"
? {
sessionID: session.id,
permissionID: "permission-native-1",
response: body.reply,
}
: {
sessionID: session.id,
requestID: "permission-native-1",
reply: body.reply,
},
});
setTimeout(() => {
json(response, 200, true);
emit({
type: "session.idle",
id: "event-permission-idle",
properties: { sessionID: session.id },
});
}, 10);
});
return;
}
if (
request.method === "POST" &&
request.url === `/session/${session.id}/abort`
)
return json(response, 200, true);
if (
request.method === "POST" &&
request.url === `/session/${session.id}/prompt_async`
) {
const chunks = [];
request.on("data", (chunk) => chunks.push(chunk));
request.on("end", () => {
const promptPayload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
if (
promptPayload.providerID !== "openrouter" ||
promptPayload.modelID !== "deepseek/deepseek-v4-flash-0731" ||
"model" in promptPayload
) {
return json(response, 400, {
error: "OpenCode 1.18 prompt model fields must be top-level",
});
}
json(response, 204, null);
setTimeout(async () => {
await callFirstPaperclipTool();
const parsedPrompt = JSON.parse(promptPayload.parts?.[0]?.text ?? "{}");
if (String(parsedPrompt.message ?? "").includes("native-question")) {
pendingQuestion = nativeQuestion();
emit({
type: "question.asked",
id: "event-question-native-1",
properties: pendingQuestion,
});
emit({
type: "question.asked",
id: "event-question-native-1",
properties: pendingQuestion,
});
return;
}
if (String(parsedPrompt.message ?? "").includes("native-permission")) {
permissionStyle = String(parsedPrompt.message ?? "").includes(
"legacy",
)
? "legacy"
: "v2";
pendingPermission = nativePermission(permissionStyle);
emit({
type:
permissionStyle === "legacy"
? "permission.updated"
: "permission.v2.asked",
id: `event-permission-${permissionStyle}`,
properties: pendingPermission,
});
return;
}
if (String(parsedPrompt.message ?? "").includes("session-aborted")) {
emit({
type: "session.error",
id: "event-session-aborted",
properties: {
sessionID: session.id,
error: {
name: "MessageAbortedError",
data: { message: "Aborted" },
},
},
});
return;
}
const textBeforeFinish = String(parsedPrompt.message ?? "").includes(
"text-before-finish",
);
const correlatedFinal = String(parsedPrompt.message ?? "").includes(
"correlated-final-message",
);
const finalAfterToolCommentary = String(
parsedPrompt.message ?? "",
).includes("final-after-tool-commentary");
const commentaryOnlyBeforeWork = String(
parsedPrompt.message ?? "",
).includes("commentary-only-before-work");
emit({
type: "message.updated",
id: "event-user",
properties: {
sessionID: session.id,
info: { id: "message-user", sessionID: session.id, role: "user" },
},
});
emit({
type: "message.part.updated",
id: "event-user-part",
properties: {
sessionID: session.id,
part: {
id: "part-user",
messageID: "message-user",
type: "text",
text: "submitted prompt must not become assistant text",
},
},
});
emit({
type: "message.updated",
id: "event-assistant",
properties: {
sessionID: session.id,
info: {
id: "message-assistant",
sessionID: session.id,
role: "assistant",
},
},
});
emit({
type: "message.part.updated",
id: "event-patch",
properties: {
sessionID: session.id,
part: {
id: "part-patch",
messageID: "message-assistant",
type: "patch",
files: ["src/index.ts"],
},
},
});
if (commentaryOnlyBeforeWork) {
emit({
type: "message.part.updated",
id: "event-opening-commentary",
properties: {
sessionID: session.id,
part: {
id: "part-opening-commentary",
messageID: "message-assistant",
type: "text",
text: "I will inspect the workspace first.",
time: { start: 1, end: 2 },
},
},
});
emit({
type: "message.part.updated",
id: "event-work-tool",
properties: {
sessionID: session.id,
part: {
id: "part-work-tool",
messageID: "message-assistant",
type: "tool",
tool: "bash",
state: {
status: "completed",
input: { command: "pwd" },
output: "/workspace",
},
},
},
});
await callTerminalTool(promptPayload);
} else if (finalAfterToolCommentary) {
emit({
type: "message.part.updated",
id: "event-tool-commentary",
properties: {
sessionID: session.id,
part: {
id: "part-tool-commentary",
messageID: "message-assistant",
type: "text",
text: "This deliberately long pre-tool commentary discusses possible schema corrections, retries, compatibility details, and several implementation alternatives, but it is not the provider's final response.",
time: { start: 1, end: 2 },
},
},
});
await callTerminalTool(promptPayload);
emit({
type: "message.part.updated",
id: "event-tool-part",
properties: {
sessionID: session.id,
part: {
id: "part-tool",
messageID: "message-assistant",
type: "tool",
tool: "paperclip_paperclip_finish",
state: { status: "completed", output: "accepted" },
},
},
});
emit({
type: "message.updated",
id: "event-post-tool-final-message",
properties: {
sessionID: session.id,
info: {
id: "message-post-tool-final",
sessionID: session.id,
role: "assistant",
},
},
});
emit({
type: "message.part.updated",
id: "event-post-tool-final",
properties: {
sessionID: session.id,
part: {
id: "part-post-tool-final",
messageID: "message-post-tool-final",
type: "text",
text: "This is the complete substantive answer emitted after the accepted completion tool call.",
time: { start: 3, end: 4 },
},
},
});
} else if (correlatedFinal) {
emit({
type: "message.part.updated",
id: "event-commentary",
properties: {
sessionID: session.id,
part: {
id: "part-commentary",
messageID: "message-assistant",
type: "text",
text: "I will write the result now.",
time: { start: 1, end: 2 },
},
},
});
await callTerminalTool(promptPayload);
emit({
type: "message.updated",
id: "event-final-message",
properties: {
sessionID: session.id,
info: {
id: "message-final",
sessionID: session.id,
role: "assistant",
},
},
});
emit({
type: "message.part.updated",
id: "event-finish-part",
properties: {
sessionID: session.id,
part: {
id: "part-finish",
messageID: "message-final",
type: "tool",
tool: "paperclip_paperclip_finish",
state: { status: "completed", output: "accepted" },
},
},
});
emit({
type: "message.part.updated",
id: "event-correlated-answer",
properties: {
sessionID: session.id,
part: {
id: "part-correlated-answer",
messageID: "message-final",
type: "text",
text: "Correlated substantive final answer.",
time: { start: 3, end: 4 },
},
},
});
emit({
type: "message.part.updated",
id: "event-duplicate-finish",
properties: {
sessionID: session.id,
part: {
id: "part-duplicate-finish",
messageID: "message-final",
type: "tool",
tool: "unknown",
callID: "functions.paperclip_paperclip_finish:24",
state: {
status: "error",
error: "Tool execution aborted",
metadata: { interrupted: true },
},
},
},
});
emit({
type: "message.updated",
id: "event-trailing-message",
properties: {
sessionID: session.id,
info: {
id: "message-trailing",
sessionID: session.id,
role: "assistant",
},
},
});
emit({
type: "message.part.updated",
id: "event-trailing-ack",
properties: {
sessionID: session.id,
part: {
id: "part-trailing-ack",
messageID: "message-trailing",
type: "text",
text: "Done.",
time: { start: 5, end: 6 },
},
},
});
} else if (textBeforeFinish) {
emit({
type: "message.part.updated",
id: "event-answer",
properties: {
sessionID: session.id,
part: {
id: "part-answer",
messageID: "message-assistant",
type: "text",
text: "Substantive answer before finish.",
time: { start: 1, end: 2 },
},
},
});
await callTerminalTool(promptPayload);
emit({
type: "message.part.updated",
id: "event-ack",
properties: {
sessionID: session.id,
part: {
id: "part-ack",
messageID: "message-assistant",
type: "text",
text: "Done.",
time: { start: 3, end: 4 },
},
},
});
} else {
await callTerminalTool(promptPayload);
emit({
type: "message.part.updated",
id: "event-1",
properties: {
sessionID: session.id,
part: {
id: "part-1",
messageID: "message-assistant",
type: "text",
text: "done [guide](guide.md)",
time: { start: 1, end: 2 },
},
},
});
emit({
type: "message.part.updated",
id: "event-1",
properties: {
sessionID: session.id,
part: {
id: "part-1",
messageID: "message-assistant",
type: "text",
text: "done",
},
},
});
}
emit({
type: "message.updated",
id: "event-2",
properties: {
info: {
id: "message-assistant",
sessionID: session.id,
role: "assistant",
tokens: { input: 3, output: 2 },
cost: 0.001,
},
},
});
emit({
type: "session.idle",
id: "event-3",
properties: { sessionID: session.id },
});
}, 100);
});
return;
}
json(response, 404, { error: "not found" });
});
server.listen(port, "127.0.0.1");
for (const signal of ["SIGTERM", "SIGINT"])
process.on(signal, () => server.close(() => process.exit(0)));