From 7a3abb88a00722c7f82d7a41476170639545fa4d Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:43:34 -0500 Subject: [PATCH] feat(runner): authorize server Codex tools (#12385) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The hidden native coordinator already computes a run-scoped semantic tool projection. > - The durable Codex backend now accepts and enforces that projection. > - The server did not include the projection in its `run.prepare` command. > - Codex therefore received no production semantic tools even when the server authorized them. > - This pull request adds the deterministic wire projection and sends it to runnerd. > - The benefit is one fail-closed authorization catalog from the server through Codex. ## Linked Issues or Issue Description Refs #12384 **What existing behavior does this improve?** This improves the existing flagged Paperclip Runner Codex path. **Current behavior** The server creates a run-scoped list of authorized read tools. It does not pass that list to runnerd, so the production Codex session starts with no tools. **Proposed behavior** The server maps the authorized definitions to the versioned runner contract. It computes a cross-language catalog digest. It includes that immutable contract in `run.prepare`. **Reason and benefit** Runnerd and the server now enforce the same catalog identity. Unknown, duplicate, changed, or malformed tool contracts fail before Codex can use them. **Breaking changes** None. Direct adapters are unchanged. A native run with an empty server projection still starts with no dynamic tools. ## What Changed - Add a deterministic semantic-definition to runner-authorization projection. - Match the Rust canonical digest with a shared test vector. - Include the server coordinator projection in the native Codex `run.prepare` command. - Extend the native Codex vertical slice to require and execute a semantic tool. - Verify the production prepare payload in a host-independent server test. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` (354 tests pass) - `pnpm --filter @paperclipai/server exec vitest run src/services/native-runtime/native-codex-runner.test.ts` - `pnpm --filter @paperclipai/server typecheck` - `pnpm -r typecheck` - `pnpm build` - The embedded-Postgres vertical slice is present for CI. This local host reports that embedded Postgres is unavailable, so Vitest skips that host-dependent test locally. - Confirmed that the PR changes 7 files against `runner-codex-durable-tools`. - Confirmed that `pnpm-lock.yaml` is unchanged. ## Risks The main risk is a catalog digest mismatch between TypeScript and Rust. Both implementations use canonical JSON. They share the same fixed digest vector. Runnerd also recomputes the digest and rejects a mismatch. The rollout flag and the existing native runtime selection rules remain unchanged. ## Model Used OpenAI Codex with GPT-5 and repository tool use. ## 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 - [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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- .../src/bin/fake-codex-app-server.rs | 139 +++++++++++++++++- .../crates/runner-core/src/provider_bridge.rs | 23 ++- .../src/semantic-tools/index.ts | 1 + .../runner-authorized-tools.test.ts | 86 +++++++++++ .../semantic-tools/runner-authorized-tools.ts | 90 ++++++++++++ .../native-codex-runner.integration.test.ts | 11 ++ .../native-codex-runner.test.ts | 50 ++++++- .../native-runtime/native-codex-runner.ts | 71 ++++++--- server/src/vendor/paperclip-runner/index.ts | 3 + 9 files changed, 449 insertions(+), 25 deletions(-) create mode 100644 packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.test.ts create mode 100644 packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.ts diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs index 81763e1043..3e92e418af 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs @@ -56,12 +56,144 @@ fn has_task_context_tool(message: &Value) -> bool { .is_some_and(|tools| { tools.iter().any(|tool| { tool.get("name").and_then(Value::as_str) == Some("get_task_context") - && tool.get("description").and_then(Value::as_str) == Some("Read task context.") + && tool + .get("description") + .and_then(Value::as_str) + .is_some_and(|description| !description.trim().is_empty()) && tool.pointer("/inputSchema/type").and_then(Value::as_str) == Some("object") }) }) } +fn matches_task_context_result(result: &Value, expected_canonical: Option<&Value>) -> bool { + let Some(expected) = expected_canonical else { + return result == &json!({"ok": true, "task": {"id": "task-1"}}); + }; + if result.get("ok") != Some(&json!(true)) + || result.get("operationId").and_then(Value::as_str) != Some("get_task_context") + || result.get("callId").and_then(Value::as_str) != Some("semantic-call-1") + { + return false; + } + [ + ("/value/company/id", "/companyId"), + ("/value/actor/id", "/actorId"), + ("/value/activeTask/id", "/taskId"), + ("/value/run/id", "/runId"), + ] + .into_iter() + .all(|(actual_pointer, expected_pointer)| { + let actual = result.pointer(actual_pointer).and_then(Value::as_str); + let expected = expected.pointer(expected_pointer).and_then(Value::as_str); + actual.is_some_and(|value| !value.is_empty()) && actual == expected + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn thread_start(tool: Value) -> Value { + json!({ + "method": "thread/start", + "params": {"dynamicTools": [tool]}, + }) + } + + #[test] + fn task_context_tool_accepts_any_non_empty_description() { + let message = thread_start(json!({ + "name": "get_task_context", + "description": "Read the active task, actor, wake context, ancestors, and budget summary.", + "inputSchema": {"type": "object"}, + })); + + assert!(has_task_context_tool(&message)); + } + + #[test] + fn task_context_tool_rejects_blank_descriptions_and_wrong_schemas() { + for tool in [ + json!({ + "name": "get_task_context", + "description": " ", + "inputSchema": {"type": "object"}, + }), + json!({ + "name": "get_task_context", + "description": "Read task context.", + "inputSchema": {"type": "string"}, + }), + json!({ + "name": "get_task_history", + "description": "Read task context.", + "inputSchema": {"type": "object"}, + }), + ] { + assert!(!has_task_context_tool(&thread_start(tool))); + } + } + + #[test] + fn task_context_result_preserves_exact_legacy_fixture_by_default() { + assert!(matches_task_context_result( + &json!({"ok": true, "task": {"id": "task-1"}}), + None, + )); + assert!(!matches_task_context_result( + &json!({ + "ok": true, + "operationId": "get_task_context", + "callId": "semantic-call-1", + "value": { + "company": {"id": "company-1"}, + "actor": {"id": "actor-1"}, + "activeTask": {"id": "task-1"}, + "run": {"id": "run-1"}, + }, + }), + None, + )); + } + + #[test] + fn task_context_result_accepts_only_the_expected_canonical_binding() { + let expected = json!({ + "companyId": "company-1", + "actorId": "actor-1", + "taskId": "task-1", + "runId": "run-1", + }); + let canonical = json!({ + "ok": true, + "operationId": "get_task_context", + "callId": "semantic-call-1", + "value": { + "company": {"id": "company-1"}, + "actor": {"id": "actor-1"}, + "activeTask": {"id": "task-1"}, + "run": {"id": "run-1"}, + }, + }); + + assert!(matches_task_context_result(&canonical, Some(&expected))); + assert!(!matches_task_context_result( + &json!({ + "ok": true, + "operationId": "get_task_context", + "callId": "semantic-call-1", + "value": { + "company": {"id": "company-1"}, + "actor": {"id": "actor-1"}, + "activeTask": {"id": "wrong-task"}, + "run": {"id": "run-1"}, + }, + }), + Some(&expected), + )); + } +} + fn finish_turn(state_path: &Path, state: &mut FakeState, status: &str) -> io::Result<()> { let turn_id = state .active_turn_id @@ -210,6 +342,9 @@ fn run() -> Result<(), Box> { .iter() .any(|value| value == "--finish-turn-with-pending-tool"); let require_dynamic_tool = args.iter().any(|value| value == "--require-dynamic-tool"); + let expected_canonical_task_context = argument(&args, "--expected-canonical-task-context") + .map(|value| serde_json::from_str::(&value)) + .transpose()?; let hold_turn = args.iter().any(|value| value == "--hold-turn"); let exit_after_turn_start = args.iter().any(|value| value == "--exit-after-turn-start"); let exit_after_turn_completion = args @@ -359,7 +494,7 @@ fn run() -> Result<(), Box> { .and_then(Value::as_str) .ok_or("semantic tool response omitted content text")?; let result: Value = serde_json::from_str(text)?; - if result != json!({"ok": true, "task": {"id": "task-1"}}) { + if !matches_task_context_result(&result, expected_canonical_task_context.as_ref()) { return Err("semantic tool response changed the operation result".into()); } log_call(call_log.as_deref(), &format!("tool-response:{text}"))?; diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/provider_bridge.rs b/packages/paperclip-runner/runner/crates/runner-core/src/provider_bridge.rs index 62213d3378..a01d35172b 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/provider_bridge.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/provider_bridge.rs @@ -1297,7 +1297,12 @@ fn canonical_json_number(value: &serde_json::Number) -> String { return "0".to_owned(); } - let body = if (1e-6..1e21).contains(&float.abs()) { + let magnitude = float.abs(); + // The lower ECMAScript threshold is inclusive: JSON.stringify(1e-6) + // produces decimal notation, while values below it use an exponent. + // Spell both comparisons out so that the wire-digest boundary is explicit. + let uses_decimal_notation = magnitude >= 1e-6 && magnitude < 1e21; + let body = if uses_decimal_notation { if decimal_position <= 0 { format!("0.{}{}", "0".repeat((-decimal_position) as usize), digits) } else if decimal_position >= digits.len() as i32 { @@ -1415,6 +1420,22 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn canonical_number_uses_decimal_notation_at_javascript_lower_boundary() { + for encoded in ["1e-6", "0.000001"] { + let value: Value = serde_json::from_str(encoded).unwrap(); + assert_eq!(canonical_json(&value), "0.000001"); + } + + for encoded in ["-1e-6", "-0.000001"] { + let value: Value = serde_json::from_str(encoded).unwrap(); + assert_eq!(canonical_json(&value), "-0.000001"); + } + + let below_boundary: Value = serde_json::from_str("9.99999e-7").unwrap(); + assert_eq!(canonical_json(&below_boundary), "9.99999e-7"); + } + #[test] fn worst_case_result_identity_overhead_fits_the_admission_reserve() { let call_id = "x".repeat(160); diff --git a/packages/paperclip-runner/src/semantic-tools/index.ts b/packages/paperclip-runner/src/semantic-tools/index.ts index a0d2159788..749a6d1e61 100644 --- a/packages/paperclip-runner/src/semantic-tools/index.ts +++ b/packages/paperclip-runner/src/semantic-tools/index.ts @@ -3,4 +3,5 @@ export * from "./discovery.js"; export * from "./dispatcher.js"; export * from "./receipts.js"; export * from "./redaction.js"; +export * from "./runner-authorized-tools.js"; export * from "./types.js"; diff --git a/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.test.ts b/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.test.ts new file mode 100644 index 0000000000..8f402b6d1a --- /dev/null +++ b/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import type { PaperclipSemanticToolDefinition } from "./types.js"; +import { createPaperclipRunnerAuthorizedToolSet } from "./runner-authorized-tools.js"; + +function definition( + name: "get_task_context" | "get_task_history", +): PaperclipSemanticToolDefinition { + return { + name, + description: + name === "get_task_context" + ? "Read the active task context." + : "Read bounded comments on the active task.", + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + annotations: { + semanticContract: "paperclip.semantic-action.v1", + version: 1, + placement: "always", + effect: "read", + requiredClaims: [], + }, + }; +} + +describe("runner authorized tool projection", () => { + it("matches the Rust catalog digest vector", () => { + const set = createPaperclipRunnerAuthorizedToolSet([ + definition("get_task_context"), + ]); + + expect(set).toMatchObject({ + schema: "paperclip.runner.authorized-tools.v1", + schemaVersion: 1, + catalogDigest: + "sha256:4e0332535c9e2ff1f5e43089517ee1b46654bfc9cb2ed51efbea4be50db21009", + operations: [ + { + operationId: "get_task_context", + version: 1, + responseSchema: { type: "object" }, + }, + ], + }); + }); + + it("matches Rust number canonicalization at JavaScript's decimal boundary", () => { + const base = definition("get_task_context"); + const set = createPaperclipRunnerAuthorizedToolSet([ + { + ...base, + inputSchema: { + type: "object", + properties: { + limit: { type: "number", default: 1.0 }, + epsilon: { type: "number", default: 1e-6 }, + }, + }, + }, + ]); + + expect(set.catalogDigest).toBe( + "sha256:1c93693d9b5b48b46c83cd1c11d1ea329774f1b9b0ae741197cb2b8e992c4b8d", + ); + }); + + it("uses operation identity order and rejects duplicates", () => { + const reverse = createPaperclipRunnerAuthorizedToolSet([ + definition("get_task_history"), + definition("get_task_context"), + ]); + const ordered = createPaperclipRunnerAuthorizedToolSet([ + definition("get_task_context"), + definition("get_task_history"), + ]); + + expect(reverse).toEqual(ordered); + expect(() => + createPaperclipRunnerAuthorizedToolSet([ + definition("get_task_context"), + definition("get_task_context"), + ]), + ).toThrow("paperclip_runner_authorized_tools_invalid"); + }); +}); diff --git a/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.ts b/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.ts new file mode 100644 index 0000000000..d8a6fbec41 --- /dev/null +++ b/packages/paperclip-runner/src/semantic-tools/runner-authorized-tools.ts @@ -0,0 +1,90 @@ +import { createHash } from "node:crypto"; + +import type { PaperclipJsonSchema } from "../catalog/semantic-action-types.js"; +import type { PaperclipSemanticToolDefinition } from "./types.js"; + +export const PAPERCLIP_RUNNER_AUTHORIZED_TOOLS_SCHEMA = + "paperclip.runner.authorized-tools.v1" as const; + +export interface PaperclipRunnerAuthorizedTool { + readonly operationId: string; + readonly version: 1; + readonly description: string; + readonly inputSchema: PaperclipJsonSchema; + readonly responseSchema: PaperclipJsonSchema; +} + +export interface PaperclipRunnerAuthorizedToolSet { + readonly schema: typeof PAPERCLIP_RUNNER_AUTHORIZED_TOOLS_SCHEMA; + readonly schemaVersion: 1; + readonly catalogDigest: string; + readonly operations: readonly PaperclipRunnerAuthorizedTool[]; +} + +export function createPaperclipRunnerAuthorizedToolSet( + definitions: readonly PaperclipSemanticToolDefinition[], +): PaperclipRunnerAuthorizedToolSet { + const names = new Set(); + const operations = definitions + .map((definition): PaperclipRunnerAuthorizedTool => { + if (definition.annotations.version !== 1 || names.has(definition.name)) { + throw new Error("paperclip_runner_authorized_tools_invalid"); + } + names.add(definition.name); + return { + operationId: definition.name, + version: 1, + description: definition.description, + inputSchema: definition.inputSchema, + responseSchema: definition.outputSchema, + }; + }) + .sort((left, right) => + left.operationId < right.operationId + ? -1 + : left.operationId > right.operationId + ? 1 + : 0, + ); + return deepFreeze({ + schema: PAPERCLIP_RUNNER_AUTHORIZED_TOOLS_SCHEMA, + schemaVersion: 1, + catalogDigest: digestOperations(operations), + operations, + }); +} + +function deepFreeze(value: T): T { + if (typeof value !== "object" || value === null || Object.isFrozen(value)) { + return value; + } + Object.freeze(value); + for (const child of Object.values(value)) deepFreeze(child); + return value; +} + +function digestOperations( + operations: readonly PaperclipRunnerAuthorizedTool[], +): string { + return `sha256:${createHash("sha256") + .update(canonicalJson(operations)) + .digest("hex")}`; +} + +function canonicalJson(value: unknown): string { + if (value === null || typeof value !== "object") { + const encoded = JSON.stringify(value); + if (encoded === undefined) { + throw new Error("paperclip_runner_authorized_tools_invalid"); + } + return encoded; + } + if (Array.isArray(value)) { + return `[${value.map(canonicalJson).join(",")}]`; + } + const object = value as Record; + return `{${Object.keys(object) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`) + .join(",")}}`; +} diff --git a/server/src/services/native-runtime/native-codex-runner.integration.test.ts b/server/src/services/native-runtime/native-codex-runner.integration.test.ts index feafba78c4..eb62b42473 100644 --- a/server/src/services/native-runtime/native-codex-runner.integration.test.ts +++ b/server/src/services/native-runtime/native-codex-runner.integration.test.ts @@ -201,6 +201,15 @@ describeEmbeddedPostgres("native Codex server vertical slice", () => { resolve(runtimeRoot, "fake-codex-state.json"), "--call-log", resolve(runtimeRoot, "fake-codex-calls.log"), + "--require-dynamic-tool", + "--emit-tool-call", + "--expected-canonical-task-context", + JSON.stringify({ + companyId, + actorId: agentId, + taskId: issueId, + runId, + }), ], providerVersion: "fake-codex-v1", }, @@ -256,6 +265,8 @@ describeEmbeddedPostgres("native Codex server vertical slice", () => { .from(heartbeatRunEvents) .where(eq(heartbeatRunEvents.runId, runId)); expect(eventTypes.map((event) => event.eventType)).toEqual(expect.arrayContaining([ + "semantic_tool.input", + "semantic_tool.result", "turn.completed", "run.result.proposed", "run.terminal", diff --git a/server/src/services/native-runtime/native-codex-runner.test.ts b/server/src/services/native-runtime/native-codex-runner.test.ts index 8e7034f0f1..1eb6aec0e0 100644 --- a/server/src/services/native-runtime/native-codex-runner.test.ts +++ b/server/src/services/native-runtime/native-codex-runner.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; -import { buildNativeRunnerArguments } from "./native-codex-runner.js"; +import type { PaperclipSemanticToolDefinition } from "../../vendor/paperclip-runner/index.js"; +import { + buildNativeRunnerArguments, + buildNativeRunnerPreparePayload, +} from "./native-codex-runner.js"; describe("buildNativeRunnerArguments", () => { it("binds every durable identity without exposing the bootstrap ticket", () => { @@ -21,3 +25,47 @@ describe("buildNativeRunnerArguments", () => { expect(args.join(" ")).not.toContain("bootstrap"); }); }); + +const tool: PaperclipSemanticToolDefinition = { + name: "get_task_context", + description: "Read the active task context.", + inputSchema: { type: "object" }, + outputSchema: { type: "object" }, + annotations: { + semanticContract: "paperclip.semantic-action.v1", + version: 1, + placement: "always", + effect: "read", + requiredClaims: [], + }, +}; + +describe("buildNativeRunnerPreparePayload", () => { + it("binds the coordinator tool projection to run.prepare", () => { + expect(buildNativeRunnerPreparePayload({ + cwd: "/workspace", + model: "test-model", + resumeProviderSessionId: "thread-1", + completionContract: { revision: "1", criterionIds: ["objective"] }, + semanticTools: [tool], + providerLaunch: { + command: "/bin/fake-codex", + args: ["app-server"], + providerVersion: "fake-1", + }, + })).toMatchObject({ + provider: { + provider: "codex", + driver: "codex_app_server", + providerSessionId: "thread-1", + }, + authorizedTools: { + schema: "paperclip.runner.authorized-tools.v1", + schemaVersion: 1, + catalogDigest: + "sha256:4e0332535c9e2ff1f5e43089517ee1b46654bfc9cb2ed51efbea4be50db21009", + operations: [{ operationId: "get_task_context", version: 1 }], + }, + }); + }); +}); diff --git a/server/src/services/native-runtime/native-codex-runner.ts b/server/src/services/native-runtime/native-codex-runner.ts index b3dd5cac1d..b48a35986b 100644 --- a/server/src/services/native-runtime/native-codex-runner.ts +++ b/server/src/services/native-runtime/native-codex-runner.ts @@ -8,11 +8,53 @@ import type { AdapterExecutionResult } from "@paperclipai/adapter-utils"; import type { Db } from "@paperclipai/db"; import { resolvePaperclipInstanceRoot } from "../../home-paths.js"; +import { + createPaperclipRunnerAuthorizedToolSet, + type PaperclipSemanticToolDefinition, +} from "../../vendor/paperclip-runner/index.js"; import { runnerPrpCoordinator } from "./runner-prp-coordinator.js"; const moduleDirectory = dirname(fileURLToPath(import.meta.url)); const RUNNER_VERSION = "paperclip-runner-v1"; +interface NativeRunnerProviderLaunch { + readonly command: string; + readonly args: string[]; + readonly providerVersion?: string; +} + +interface NativeRunnerPrepareInput { + readonly cwd: string; + readonly model: string | null; + readonly resumeProviderSessionId: string | null; + readonly completionContract: { revision: string; criterionIds: string[] }; + readonly semanticTools: readonly PaperclipSemanticToolDefinition[]; + readonly providerLaunch?: NativeRunnerProviderLaunch; +} + +export function buildNativeRunnerPreparePayload( + input: NativeRunnerPrepareInput, +): Record { + return { + provider: { + provider: "codex", + driver: "codex_app_server", + providerVersion: input.providerLaunch?.providerVersion ?? "codex-app-server-v1", + command: input.providerLaunch?.command ?? "codex", + args: input.providerLaunch?.args ?? ["app-server"], + cwd: input.cwd, + ...(input.model ? { model: input.model } : {}), + ...(input.resumeProviderSessionId + ? { providerSessionId: input.resumeProviderSessionId } + : {}), + instructions: "", + approvalPolicy: "never", + }, + completionContract: input.completionContract, + authorizedTools: createPaperclipRunnerAuthorizedToolSet(input.semanticTools), + }; +} + function executableName(): string { return process.platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd"; } @@ -152,11 +194,7 @@ export async function executeNativeCodexRunner(input: { /** Internal test seam; production always uses the instance runtime root. */ runtimeRoot?: string; /** Internal conformance seam; production always launches `codex app-server`. */ - providerLaunch?: { - command: string; - args: string[]; - providerVersion?: string; - }; + providerLaunch?: NativeRunnerProviderLaunch; onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; onSpawn: (meta: { pid: number; @@ -191,23 +229,14 @@ export async function executeNativeCodexRunner(input: { runnerDigest, }); - prepared.queueCommand("run.prepare", { - provider: { - provider: "codex", - driver: "codex_app_server", - providerVersion: input.providerLaunch?.providerVersion ?? "codex-app-server-v1", - command: input.providerLaunch?.command ?? "codex", - args: input.providerLaunch?.args ?? ["app-server"], - cwd: input.cwd, - ...(input.model ? { model: input.model } : {}), - ...(input.resumeProviderSessionId - ? { providerSessionId: input.resumeProviderSessionId } - : {}), - instructions: "", - approvalPolicy: "never", - }, + prepared.queueCommand("run.prepare", buildNativeRunnerPreparePayload({ + cwd: input.cwd, + model: input.model, + resumeProviderSessionId: input.resumeProviderSessionId, completionContract: input.completionContract, - }, `prepare_${input.runId}`); + semanticTools: prepared.semanticTools, + ...(input.providerLaunch ? { providerLaunch: input.providerLaunch } : {}), + }), `prepare_${input.runId}`); prepared.queueCommand("session.open", {}, `open_${input.runId}`); prepared.queueCommand("turn.start", { text: input.prompt }, `turn_${input.runId}`); diff --git a/server/src/vendor/paperclip-runner/index.ts b/server/src/vendor/paperclip-runner/index.ts index 3fb08b0758..26e8b49165 100644 --- a/server/src/vendor/paperclip-runner/index.ts +++ b/server/src/vendor/paperclip-runner/index.ts @@ -19,6 +19,7 @@ export type { PaperclipSemanticToolCall, PaperclipSemanticToolDefinition, PaperclipSemanticToolResult, + PaperclipRunnerAuthorizedToolSet, PaperclipQuestionSet, PaperclipRuntimeInputRequest, PrpEvent, @@ -38,6 +39,8 @@ const runner = await import(sourceUrl.href) as RunnerModule; export const DurablePrpControlPlane = runner.DurablePrpControlPlane; export const PaperclipSemanticDispatcher = runner.PaperclipSemanticDispatcher; +export const createPaperclipRunnerAuthorizedToolSet = + runner.createPaperclipRunnerAuthorizedToolSet; export const parsePaperclipQuestionSet = runner.parsePaperclipQuestionSet; export const parsePaperclipQuestionResponse = runner.parsePaperclipQuestionResponse; export const validatePrpEvent = runner.validatePrpEvent;