From f6a211479fafc05a097adfb4fd3c434f64172463 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:09:29 -0500 Subject: [PATCH] fix: share current CLI runtimes across sandbox adapters (#12994) ## Thinking Path - Paperclip Runner needs its runtime preinstalled for fast sandbox startup. - Native and local adapters should launch one current CLI installation per provider. - An older global copy can shadow that installation, and exact native compatibility pins must match it. - Update the qualified releases and binary digests, expose shared CLI entrypoints from the provider pack, and prefer the image-owned bin directory. - Keep dependency installation in the image build; task startup only discovers, links, and verifies artifacts. ## Linked Issues or Issue Description **What happened?** Remote native startup rejected a stale global Codex, while CLI-only images lacked runnerd entirely. **Expected behavior** An image-baked runtime starts without uploading binaries or installing packages. All adapters share the same current provider CLI. **Steps to reproduce** Start a native remote task with the old global Codex and the updated runtime available only under `/opt/paperclip-runner/bin`. **Paperclip version or commit** Discovery behavior at `54a99d884`. **Deployment mode** Docker with a remote sandbox. ## What Changed - Prefer `/opt/paperclip-runner/bin`, then the user's local bin directory, then PATH. Existing metadata and version validation remains in force. - Qualify Codex 0.153.4, OpenCode 1.18.29, and Claude SDK 0.3.263 / CLI 2.1.263. Update binary digests, TypeScript/Rust checks, registry defaults, and the displayed OpenCode version together. - Share Codex and Claude's native executable with the ACP bridges through exact dependency overrides. Preserve the separately qualified ACP bridge implementations and their security patches. - Expose shared provider-pack CLI launchers; fail the pack build if Codex ACP resolves a separate Codex installation. Update the eval image's other agent CLIs to current stable releases and remove duplicate global provider installs. - Document the single-current-CLI policy in source comments and development guidance. Latest stable releases are resolved at review/build preparation and pinned; task startup never auto-updates. ## Verification - Native-session and adapter-registry suites: 158 tests passed. - Provider suites: 88 tests passed, 7 Linux-only checks skipped on macOS. One existing macOS temporary-path alias assertion passed when rerun with canonical `TMPDIR=/private/tmp`. - Package-contract and OpenCode materialization tests: 11 passed. - Full typecheck, build, and token gates passed. Rust native-provider/recovery tests: 19 passed. - Broad local suite: 5,974 passed, 23 failed, 41 skipped. Failures are in unchanged macOS workspace/path/port and connection suites; focused runtime tests pass. All latest-head Linux PR checks passed, including the full test shards, typecheck, build, runner verification, browser suites, and canary dry run. - The standalone fleet image built with one current provider CLI each and passed native Codex/Claude binary-integrity checks. A disposable Daytona sandbox reported ready in 798 ms; its baked runner completed an API-key `gpt-5.6-luna` turn in 2,430 ms and returned the expected marker with a usage receipt. No runtime artifacts were uploaded or installed. - The normal shared `codex exec` entrypoint also completed an API-key `gpt-5.6-luna` turn in 2,321 ms. - Both image builds verify the complete generated lockfile against a reviewed SHA-256 before package installation or lifecycle execution. Root lockfile changes remain CI-owned. Merge and rollout remain on hold for operator review. ## Risks - Updating provider CLIs changes their behavior for all adapters; version probes and live native smoke testing are required before image promotion. - The image-owned directory takes precedence. Its entries must launch the same shared CLI as the global PATH, not a private older/newer copy. - Application qualification pins and the deployed image must move together. No startup fallback installation is added. - No schema or authentication-policy changes. ## Model Used OpenAI GPT-6 (Codex). The session does not expose a more specific model ID or context-window size. Used reasoning, repository inspection, code execution, and browser verification. ## 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 - [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 --- doc/DEVELOPING.md | 16 ++++ docker/daytona-runner/Dockerfile | 69 ++++++++------- docker/daytona-runner/README.md | 12 ++- docs/deploy/environment-variables.md | 2 +- package.json | 2 + packages/paperclip-runner/package.json | 3 +- .../runner-core/src/acpx_provider_backend.rs | 8 +- .../crates/runner-core/src/codex_provider.rs | 2 +- .../runner-core/src/provider_backend.rs | 4 +- .../tests/native_provider_backend.rs | 4 +- .../scripts/build-provider-pack.mjs | 34 +++++++- .../scripts/materialize-opencode-binary.mjs | 2 +- .../materialize-opencode-binary.test.mjs | 12 +-- .../runner-protocol-eval-campaign.test.mjs | 2 +- .../src/backends/codex-native-backend.ts | 2 +- .../backends/native-backend-factory.test.ts | 6 +- .../paperclip-runner/src/cli/eval-session.ts | 6 +- .../src/cli/opencode-app-server-proxy.ts | 2 +- .../acpx/installation-integrity.test.ts | 6 +- .../drivers/acpx/installation-integrity.ts | 20 ++--- .../drivers/acpx/qualified-profiles.test.ts | 4 +- .../src/drivers/acpx/qualified-profiles.ts | 4 +- .../opencode/opencode-server-driver.test.ts | 4 +- .../opencode/opencode-server-driver.ts | 2 +- .../src/live/live-session.test.ts | 2 +- .../paperclip-runner/src/live/live-session.ts | 2 +- .../src/live/runnerd-codex-transport.ts | 2 +- .../test/acpx-codex-package-contract.test.mjs | 7 +- .../test/fixtures/fake-opencode-server.mjs | 2 +- ...entprotocol__claude-agent-acp@0.70.0.patch | 10 +++ ...agentclientprotocol__codex-acp@1.6.2.patch | 2 +- pnpm-workspace.yaml | 9 ++ server/src/__tests__/adapter-registry.test.ts | 4 +- server/src/adapters/registry.ts | 2 +- .../native-session-executor.test.ts | 84 ++++++++++++++++++- .../native-runtime/native-session-executor.ts | 13 +-- .../native-runtime/provider-profile.ts | 2 +- .../codex-local/config-fields.test.tsx | 4 +- ui/src/adapters/codex-local/config-fields.tsx | 2 +- 39 files changed, 272 insertions(+), 103 deletions(-) diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index cab7762fce..2ad1963b6f 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -807,6 +807,22 @@ agent workspace. The host `HOME` itself, a directory that contains it, a filesystem root, a `CODEX_HOME` overlap, or a canonical path outside the assigned workspace is rejected before provider startup. +### Preinstalled remote runner runtime + +For fast sandbox startup, bake `paperclip-runnerd` and the latest stable agent +CLIs into the sandbox image. Keep one version of each CLI shared by native and +local adapters; never retain an older global CLI beside a newer private copy. +Pin the resolved releases at image build time for reproducibility and refresh +the runner's qualification versions and binary digests together with those pins. +The ACP bridges remain separately qualified protocol dependencies. + +Native discovery checks `/opt/paperclip-runner/bin`, then `$HOME/.local/bin`, +then PATH. Any preferred-directory entry must launch the same shared CLI that +normal adapters use. Discovery picks the first executable; it does not compare +versions across directories. With these artifacts preinstalled, startup links +and verifies them without uploading a binary or installing packages. Deploy +the updated sandbox image with the matching runner qualification changes. + ### Native runner restart recovery Paperclip Runner keeps its heartbeat run, native session, logical runner, and diff --git a/docker/daytona-runner/Dockerfile b/docker/daytona-runner/Dockerfile index 87d819f49c..67e88e6fa0 100644 --- a/docker/daytona-runner/Dockerfile +++ b/docker/daytona-runner/Dockerfile @@ -18,27 +18,27 @@ WORKDIR /workspace COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc tsconfig.base.json ./ COPY patches ./patches COPY scripts/link-plugin-dev-sdk.mjs ./scripts/link-plugin-dev-sdk.mjs -COPY packages/paperclip-eval-kernel/package.json ./packages/paperclip-eval-kernel/package.json -COPY packages/paperclip-runner/package.json ./packages/paperclip-runner/package.json -RUN pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...' -COPY packages/paperclip-eval-kernel/tsconfig.json ./packages/paperclip-eval-kernel/tsconfig.json -COPY packages/paperclip-eval-kernel/src ./packages/paperclip-eval-kernel/src -COPY packages/paperclip-runner/styles.css packages/paperclip-runner/tsconfig.json packages/paperclip-runner/tsconfig.surfaces.json ./packages/paperclip-runner/ -COPY packages/paperclip-runner/protocol ./packages/paperclip-runner/protocol -COPY packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_sidecar_contract.rs ./packages/paperclip-runner/runner/crates/runner-core/src/generated_acpx_sidecar_contract.rs -COPY \ - packages/paperclip-runner/scripts/acpx-sidecar-contract.mjs \ - packages/paperclip-runner/scripts/build-provider-pack.mjs \ - packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs \ - packages/paperclip-runner/scripts/generate-acpx-sidecar-contract.mjs \ - packages/paperclip-runner/scripts/generate-protocol-schema-module.mjs \ - ./packages/paperclip-runner/scripts/ -COPY packages/paperclip-runner/src ./packages/paperclip-runner/src +# CI owns pnpm-lock.yaml and regenerates it for manifest-only PRs. Include the +# complete workspace manifest graph so all source-owned patches apply, then +# resolve before the frozen install. This happens only during image creation. +COPY packages ./packages +COPY server/package.json ./server/package.json +COPY ui/package.json ./ui/package.json +COPY cli/package.json ./cli/package.json +# The complete resolved lock (including transitive integrity hashes) is reviewed. +# Reject registry-time drift BEFORE installing packages or running lifecycle code. +# Refresh this digest together with source/provider dependency changes. +ARG PAPERCLIP_RUNNER_LOCK_SHA256=47a7c09302d47843054d0301f8f52f3da935b9c6ac771bace0409da752b6af7f +RUN pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile \ + && printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \ + && sha256sum -c /tmp/provider-lock.sha256 \ + && pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...' ARG PAPERCLIP_RUNNER_SOURCE_REVISION RUN test -n "${PAPERCLIP_RUNNER_SOURCE_REVISION}" RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \ && PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_RUNNER_SOURCE_REVISION}" \ - node packages/paperclip-runner/scripts/build-provider-pack.mjs /provider-pack + node packages/paperclip-runner/scripts/build-provider-pack.mjs /provider-pack \ + && chmod -R a+rX /provider-pack # Fleet sandbox base image. Keep this section aligned with # paperclipai/paperclip-cloud/fleet-sandbox-image/Dockerfile. The only Paperclip @@ -50,19 +50,25 @@ USER root ENV PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=/opt/paperclip-runner/provider-pack ENV PATH=${PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT}/node_modules/.bin:/usr/local/share/nvm/current/bin:/usr/local/python/current/bin:/usr/local/py-utils/bin:${PATH} -RUN npm install -g \ - @anthropic-ai/claude-code@2.1.19 \ - @openai/codex@0.148.0 \ - @xai-official/grok@1.0.3 \ - @google/gemini-cli@0.56.0 \ - @moonshot-ai/kimi-code@0.38.0 \ - opencode-ai@1.18.17 \ +# Share the pack's latest stable Codex, Claude and OpenCode with every adapter. +# Never add a second CLI version to work around a runner compatibility pin. +RUN npm uninstall -g @anthropic-ai/claude-code @openai/codex opencode-ai \ + && npm install -g \ + @xai-official/grok@1.0.13 \ + @google/gemini-cli@0.58.0 \ + @moonshot-ai/kimi-code@0.41.0 \ && npm cache clean --force -RUN pip install --no-cache-dir --break-system-packages hermes-agent==0.15.2 +# Hermes requires Python >=3.11,<3.14; the image's default Python is newer. +# Use the distro interpreter in a dedicated environment without changing PATH +# or keeping an older Hermes CLI. Everything is installed before sandbox boot. +# Debian 13 in the sandbox base already includes Python 3.13 and python3-venv. +RUN /usr/bin/python3 -m venv /opt/hermes \ + && /opt/hermes/bin/pip install --no-cache-dir hermes-agent==0.19.0 \ + && ln -sf /opt/hermes/bin/hermes /usr/local/bin/hermes -ARG CURSOR_VERSION=2026.08.11-e8db854 -ARG CURSOR_SHA256_AMD64=bfff4bf6f4e9dd30c1d0ef0a70b6077b074015dd2948e4c50685d53afdcfce5a +ARG CURSOR_VERSION=2026.09.02-c22c1a3 +ARG CURSOR_SHA256_AMD64=b73b59854762535c0fc20d7ccc51c3b5a356a851491088d60a362be48750f53c RUN set -eu; \ arch="$(dpkg --print-architecture)"; \ [ "$arch" = "amd64" ] || { echo "FATAL: cursor pin only covers amd64, not $arch" >&2; exit 1; }; \ @@ -76,8 +82,8 @@ RUN set -eu; \ ln -sf /usr/local/bin/cursor-agent /usr/local/bin/agent; \ chmod -R a+rX /opt/cursor -ARG GH_VERSION=2.98.0 -ARG GH_SHA256_AMD64=3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de +ARG GH_VERSION=2.100.0 +ARG GH_SHA256_AMD64=e4d4bb4498e8d007abe545b6568926793ace1b6447da598294a610018cb164be RUN set -eu; \ arch="$(dpkg --print-architecture)"; \ [ "$arch" = "amd64" ] || { echo "FATAL: gh pin only covers amd64, not $arch" >&2; exit 1; }; \ @@ -90,6 +96,10 @@ RUN set -eu; \ COPY --from=runnerd-build /workspace/packages/paperclip-runner/runner/target/release/paperclip-runnerd /usr/local/bin/paperclip-runnerd COPY --from=provider-pack-build /provider-pack /opt/paperclip-runner/provider-pack +RUN set -eu; for cli in codex claude opencode; do \ + printf '#!/bin/sh\nexec /opt/paperclip-runner/provider-pack/node_modules/.bin/%s "$@"\n' "$cli" > "/usr/local/bin/$cli"; \ + chmod 755 "/usr/local/bin/$cli"; \ + done # Keep revision-dependent metadata below the stable agent CLI installation # layers. A source-only image miss can then reuse those expensive layers from @@ -100,7 +110,6 @@ RUN test -n "${PAPERCLIP_RUNNER_CONTENT_ID}" \ && test -n "${PAPERCLIP_RUNNER_SOURCE_REVISION}" RUN set -eu; \ - chmod -R a+rX /opt/paperclip-runner/provider-pack; \ printf '%s\n' 'export PATH=/opt/paperclip-runner/provider-pack/node_modules/.bin:$PATH' \ > /etc/profile.d/01-paperclip-runner-provider-pack.sh; \ chmod 0644 /etc/profile.d/01-paperclip-runner-provider-pack.sh; \ diff --git a/docker/daytona-runner/README.md b/docker/daytona-runner/README.md index 766f5e8b3c..229529b962 100644 --- a/docker/daytona-runner/README.md +++ b/docker/daytona-runner/README.md @@ -2,7 +2,7 @@ This image is the Paperclip Cloud fleet sandbox image plus a source-built `paperclip-runnerd` and immutable provider pack. The pack contains Node 24.11, -OpenCode 1.18.17, the compiled OpenCode proxy, ACPX 0.13.1 sidecar, qualified ACP +OpenCode 1.18.29, the compiled OpenCode proxy, ACPX 0.13.1 sidecar, qualified ACP agents, and the production lockfile. Its manifest digests each executable bridge and binds the pack to the runner source revision, avoiding artifact upload and npm installation on every fresh lease. @@ -67,3 +67,13 @@ full Git SHA as `PAPERCLIP_RUNNER_SOURCE_REVISION`. Do not bake provider credentials, Paperclip bootstrap tickets, or Daytona preview tokens into this image. They remain per-run secret material. + +Provider CLI updates are manifest-only changes: repository CI owns the root +lockfile. The image build resolves the complete workspace manifest graph before +its frozen install, matching CI when a source commit precedes the lockfile bot. +The complete resolved lockfile must match `PAPERCLIP_RUNNER_LOCK_SHA256` before +package installation or lifecycle execution. Review and refresh that digest +with source dependency changes; registry-time resolution drift fails closed. +Keep one latest stable CLI installation per provider; refresh exact runtime +versions and qualification digests together, never install a private older copy +or download dependencies when a task starts. diff --git a/docs/deploy/environment-variables.md b/docs/deploy/environment-variables.md index 678552b25b..2f6900a244 100644 --- a/docs/deploy/environment-variables.md +++ b/docs/deploy/environment-variables.md @@ -23,7 +23,7 @@ All environment variables that Paperclip uses for server configuration. | `PAPERCLIP_RUNNER_CA_BUNDLE_PATH` | (unset) | Optional PEM CA bundle for direct runner WSS. Platform roots remain enabled. There is no insecure TLS bypass. | | `PAPERCLIP_RUNNER_REMOTE_BINARY_PATH` | (host build) | Host-local path to a `paperclip-runnerd` artifact built for the remote target OS and architecture. Required when Paperclip and the remote sandbox do not share a compatible platform; build metadata and the required transport mode are verified before launch. | | `PAPERCLIP_RUNNER_REMOTE_CODEX_PATH` | (unset) | Optional host-local path to a Codex executable built for the remote target OS and architecture. For remote Codex-backed runners, Paperclip stages and verifies this executable beside `paperclip-runnerd`. | -| `PAPERCLIP_RUNNER_REMOTE_CODEX_NPM_SPEC` | (unset) | Optional pinned npm package spec (for example, `@openai/codex@0.148.0`) installed inside each fresh remote lease when its Codex harness is not baked into the sandbox image. Mutually exclusive with `PAPERCLIP_RUNNER_REMOTE_CODEX_PATH`; Paperclip verifies the installed executable before starting `runnerd`. | +| `PAPERCLIP_RUNNER_REMOTE_CODEX_NPM_SPEC` | (unset) | Optional pinned npm package spec (for example, `@openai/codex@0.153.4`) installed inside each fresh remote lease when its Codex harness is not baked into the sandbox image. Mutually exclusive with `PAPERCLIP_RUNNER_REMOTE_CODEX_PATH`; Paperclip verifies the installed executable before starting `runnerd`. | | `PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH` | (unset) | Host-local path to the immutable provider pack built by `pnpm --filter @paperclipai/paperclip-runner build:provider-pack`. The pack includes its target-built Node 24.11 runtime, locked production dependencies, OpenCode proxy/executable, and ACPX sidecar. Remote OpenCode and ACPX fail closed without it. A preinstalled pack is accepted only when its complete digested manifest matches this build-owned pack; otherwise Paperclip stages this pack into the sandbox. | | `PAPERCLIP_HIDDEN_SETTINGS` | (unset) | Comma-separated settings surfaces to hide from the UI and floor at the API, for operators hosting Paperclip for others (managed cloud, internal shared server). See [Hiding settings surfaces](#hiding-settings-surfaces). | | `PAPERCLIP_SETTING_DEFAULTS` | (unset) | JSON object replacing the schema default of selected instance settings, for hosting operators. See [Operator setting defaults](#operator-setting-defaults). | diff --git a/package.json b/package.json index 3a27287be9..309517bd71 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,8 @@ "@agentclientprotocol/codex-acp@1.6.2": "patches/@agentclientprotocol__codex-acp@1.6.2.patch" }, "overrides": { + "@agentclientprotocol/codex-acp@1.6.2>@openai/codex": "0.153.4", + "@agentclientprotocol/claude-agent-acp@0.70.0>@anthropic-ai/claude-agent-sdk": "0.3.263", "rollup": ">=4.59.0", "react": "^19.2.8", "react-dom": "^19.2.8" diff --git a/packages/paperclip-runner/package.json b/packages/paperclip-runner/package.json index 030fc51505..16941733a5 100644 --- a/packages/paperclip-runner/package.json +++ b/packages/paperclip-runner/package.json @@ -158,10 +158,11 @@ "dependencies": { "@agentclientprotocol/claude-agent-acp": "0.70.0", "@agentclientprotocol/codex-acp": "1.6.2", + "@openai/codex": "0.153.4", "acpx": "0.13.1", "ajv": "^8.20.0", "json-schema-to-ts": "^3.1.1", - "opencode-ai": "1.18.17", + "opencode-ai": "1.18.29", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1" }, diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs index 92e7323b49..ca259d30d2 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs @@ -146,7 +146,7 @@ impl AcpxProviderDescriptor { "@agentclientprotocol/claude-agent-acp", "0.70.0", Some("@anthropic-ai/claude-agent-sdk"), - Some("0.3.232"), + Some("0.3.263"), "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a", ), "codex" => ( @@ -154,7 +154,7 @@ impl AcpxProviderDescriptor { "@agentclientprotocol/codex-acp", "1.6.2", Some("@openai/codex"), - Some("0.148.0"), + Some("0.153.4"), "sha256:7a923b3829884d3cabcc9659d22cace3f86813e7bfffc90974b10140a45bc400", ), "pi" => return Err(DurableRunnerError::invalid( @@ -1601,7 +1601,7 @@ mod tests { "@agentclientprotocol/claude-agent-acp", "0.70.0", json!("@anthropic-ai/claude-agent-sdk"), - json!("0.3.232"), + json!("0.3.263"), "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a", ) } else { @@ -1610,7 +1610,7 @@ mod tests { "@agentclientprotocol/codex-acp", "1.6.2", json!("@openai/codex"), - json!("0.148.0"), + json!("0.153.4"), "sha256:7a923b3829884d3cabcc9659d22cace3f86813e7bfffc90974b10140a45bc400", ) }; diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs index b4ec2944f0..ee04c64c96 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs @@ -24,7 +24,7 @@ use crate::qualified_launch::verify_launch_artifact; use crate::question_response::validate_question_response; pub const CODEX_APP_SERVER_MAX_FRAME_BYTES: usize = 4 * 1024 * 1024; -const QUALIFIED_OPENCODE_VERSION: &str = "1.18.17"; +const QUALIFIED_OPENCODE_VERSION: &str = "1.18.29"; const DEFAULT_PROVIDER_TRACE_MAX_BYTES: usize = 64 * 1024 * 1024; const MAX_BUFFERED_MESSAGES: usize = 1_024; const MAX_BUFFERED_MESSAGE_BYTES: usize = 16 * 1024 * 1024; diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs index 9b2aab8c63..3d6be65aef 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/provider_backend.rs @@ -3267,7 +3267,7 @@ mod tests { CodexProviderConfig { provider: "opencode".to_owned(), driver: "opencode_server".to_owned(), - provider_version: "1.18.17".to_owned(), + provider_version: "1.18.29".to_owned(), command: PathBuf::from("node"), args: Vec::new(), cwd: std::env::current_dir() @@ -3495,7 +3495,7 @@ mod tests { CodexProviderConfig { provider: "opencode".to_owned(), driver: "opencode_server".to_owned(), - provider_version: "1.18.17".to_owned(), + provider_version: "1.18.29".to_owned(), command: PathBuf::from("node"), args: Vec::new(), cwd: std::env::current_dir() diff --git a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs index ba36834ab7..3c2f37c674 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/tests/native_provider_backend.rs @@ -144,7 +144,7 @@ fn prepare_payload(directory: &Path, agent: &str) -> Value { fn prepare_payload_with_mode(directory: &Path, agent: &str, mode: &str) -> Value { let operations = Vec::new(); let (runtime_package, runtime_version) = if agent == "codex" { - (json!("@openai/codex"), json!("0.148.0")) + (json!("@openai/codex"), json!("0.153.4")) } else { (Value::Null, Value::Null) }; @@ -240,7 +240,7 @@ fn opencode_prepare_payload(directory: &Path) -> Value { "kind": "opencode", "provider": "opencode", "driver": "opencode_server", - "providerVersion": "1.18.17", + "providerVersion": "1.18.29", "command": directory.join("qualified-opencode-proxy-command"), "args": [directory.join("qualified-opencode-proxy-script")], "cwd": directory, diff --git a/packages/paperclip-runner/scripts/build-provider-pack.mjs b/packages/paperclip-runner/scripts/build-provider-pack.mjs index b616674819..c9648a1ae5 100644 --- a/packages/paperclip-runner/scripts/build-provider-pack.mjs +++ b/packages/paperclip-runner/scripts/build-provider-pack.mjs @@ -9,12 +9,14 @@ import { readdirSync, readFileSync, readlinkSync, + realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; +import { dirname, join, relative, resolve } from "node:path"; +import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -127,6 +129,15 @@ try { throw new Error(`pnpm deploy failed with exit code ${deployed.status}`); } + // Fail the image build if a bridge silently brings back an older/private + // provider CLI. A direct dependency alone does not deduplicate pnpm's graph. + const packRequire = createRequire(join(temporaryRoot, "package.json")); + const codexAcpRequire = createRequire(packRequire.resolve("@agentclientprotocol/codex-acp/package.json")); + if (realpathSync(codexAcpRequire.resolve("@openai/codex/package.json")) !== + realpathSync(packRequire.resolve("@openai/codex/package.json"))) { + throw new Error("Codex ACP must share the image's Codex installation"); + } + // Reuse the already-qualified build interpreter instead of introducing a // package-manager lifecycle hook or a second binary supply chain. The pack // manifest binds the copied bytes, platform, architecture, and minimum @@ -158,6 +169,23 @@ try { // every build and leaks a nonexistent host path after relocation. Replace // every provider-facing shim with a pack-relative launcher that always uses // the pinned Node executable owned by this pack. + // The image exposes these same installations to every adapter. Never add a + // separate global/runner-only CLI version; refresh these packages and their + // qualification digests together to the latest stable releases. + writePortableNodeShim("codex", "@openai/codex/bin/codex.js"); + const claudeAcpRequire = createRequire( + packRequire.resolve("@agentclientprotocol/claude-agent-acp/package.json"), + ); + // Use the ACP bridge's SDK dependency directly, avoiding a second peer- + // resolved SDK installation just to expose its CLI on the global PATH. + const sdkRequire = createRequire(claudeAcpRequire.resolve("@anthropic-ai/claude-agent-sdk")); + const claudeExecutable = sdkRequire.resolve( + `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude`, + ); + writePortableExecutableShim( + "claude", + relative(realpathSync(join(temporaryRoot, "node_modules")), realpathSync(claudeExecutable)), + ); writePortableExecutableShim("node", "node/bin/node"); writePortableExecutableShim("opencode", "opencode-ai/bin/opencode.exe"); writePortableNodeShim("acpx", "acpx/dist/cli.js"); @@ -262,8 +290,8 @@ try { const payload = { pins: { nodeMinimum: minimumNodeVersion.join("."), - codex: "0.148.0", - opencode: "1.18.17", + codex: "0.153.4", + opencode: "1.18.29", acpx: "0.13.1", claudeAcp: "0.70.0", codexAcp: "1.6.2", diff --git a/packages/paperclip-runner/scripts/materialize-opencode-binary.mjs b/packages/paperclip-runner/scripts/materialize-opencode-binary.mjs index 37cb04a85a..f4be2ea8ab 100644 --- a/packages/paperclip-runner/scripts/materialize-opencode-binary.mjs +++ b/packages/paperclip-runner/scripts/materialize-opencode-binary.mjs @@ -13,7 +13,7 @@ import { import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; -const OPENCODE_VERSION = "1.18.17"; +const OPENCODE_VERSION = "1.18.29"; const BASELINE_PACKAGE = "opencode-linux-x64-baseline"; function readPackage(path) { diff --git a/packages/paperclip-runner/scripts/materialize-opencode-binary.test.mjs b/packages/paperclip-runner/scripts/materialize-opencode-binary.test.mjs index b6f7bbfe7d..4bae1e206e 100644 --- a/packages/paperclip-runner/scripts/materialize-opencode-binary.test.mjs +++ b/packages/paperclip-runner/scripts/materialize-opencode-binary.test.mjs @@ -35,14 +35,14 @@ async function fixture(options = {}) { join(packageRoot, "package.json"), JSON.stringify({ name: "opencode-ai", - version: options.packageVersion ?? "1.18.17", + version: options.packageVersion ?? "1.18.29", }), ), writeFile( join(baselineRoot, "package.json"), JSON.stringify({ name: "opencode-linux-x64-baseline", - version: options.baselineVersion ?? "1.18.17", + version: options.baselineVersion ?? "1.18.29", }), ), writeFile(join(packageRoot, "bin", "opencode.exe"), "sentinel\n"), @@ -50,11 +50,11 @@ async function fixture(options = {}) { const source = join(baselineRoot, "bin", "opencode"); if (options.symlinkSource) { const realSource = join(root, "real-opencode"); - await writeFile(realSource, "#!/bin/sh\necho 1.18.17\n"); + await writeFile(realSource, "#!/bin/sh\necho 1.18.29\n"); await chmod(realSource, 0o755); await symlink(realSource, source); } else { - await writeFile(source, "#!/bin/sh\necho 1.18.17\n"); + await writeFile(source, "#!/bin/sh\necho 1.18.29\n"); await chmod(source, 0o755); } return packageRoot; @@ -67,7 +67,7 @@ test("materializes the pinned baseline executable with a verified version", asyn platform: "linux", architecture: "x64", }); - assert.equal(result.version, "1.18.17"); + assert.equal(result.version, "1.18.29"); assert.match(result.sourceDigest, /^[0-9a-f]{64}$/); }); @@ -80,7 +80,7 @@ test("refuses version, file-type, and platform drift", async () => { platform: "linux", architecture: "x64", }), - /Expected opencode-linux-x64-baseline@1\.18\.17/, + /Expected opencode-linux-x64-baseline@1\.18\.29/, ); const symlinkSource = await fixture({ symlinkSource: true }); diff --git a/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.test.mjs b/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.test.mjs index 26ca75ea07..ef1874faea 100644 --- a/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.test.mjs +++ b/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.test.mjs @@ -34,7 +34,7 @@ async function fixture() { provider: "opencode", driver: "opencode_server", model: "openrouter/example/model", - opencodeVersion: "1.18.17", + opencodeVersion: "1.18.29", }; const evalCase = { schema: "paperclip-runner/eval-case/v1", diff --git a/packages/paperclip-runner/src/backends/codex-native-backend.ts b/packages/paperclip-runner/src/backends/codex-native-backend.ts index bde981c261..fa06ca094e 100644 --- a/packages/paperclip-runner/src/backends/codex-native-backend.ts +++ b/packages/paperclip-runner/src/backends/codex-native-backend.ts @@ -66,7 +66,7 @@ function transportDriverIdentity(input: NativeExecutionInput): { return { kind: "opencode_server", displayName: "OpenCode server", - version: "1.18.17", + version: "1.18.29", }; case "claude_managed": return { diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 545777babc..3f9ffb18b6 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -101,7 +101,7 @@ function acpxExecution( ? "@openai/codex" : "@anthropic-ai/claude-agent-sdk", agentRuntimeVersion: - agent === "pi" ? "0.84.2" : agent === "codex" ? "0.148.0" : "0.3.232", + agent === "pi" ? "0.84.2" : agent === "codex" ? "0.153.4" : "0.3.263", commandDigest: agent === "codex" ? "sha256:7a923b3829884d3cabcc9659d22cace3f86813e7bfffc90974b10140a45bc400" @@ -266,7 +266,7 @@ describe("native backend factory", () => { await expect(backend.descriptor()).resolves.toMatchObject({ kind: "runner", name: "opencode_server", - version: "1.18.17", + version: "1.18.29", capabilities: { steering: false, resume: true, @@ -418,7 +418,7 @@ describe("native backend factory", () => { await expect(backend.descriptor()).resolves.toMatchObject({ kind: "runner", name: "opencode_server", - version: "1.18.17", + version: "1.18.29", capabilities: { resume: true, interruption: true, diff --git a/packages/paperclip-runner/src/cli/eval-session.ts b/packages/paperclip-runner/src/cli/eval-session.ts index 7aadfd3fd2..c88c047920 100644 --- a/packages/paperclip-runner/src/cli/eval-session.ts +++ b/packages/paperclip-runner/src/cli/eval-session.ts @@ -159,9 +159,9 @@ export function evalSessionProviderVersion( request: EvalSessionRequest, ): string | null { if (request.provider === "opencode") { - const version = request.opencodeVersion ?? "1.18.17"; - if (version !== "1.18.17") { - throw new Error(`OpenCode evals require exact version 1.18.17; received ${version}`); + const version = request.opencodeVersion ?? "1.18.29"; + if (version !== "1.18.29") { + throw new Error(`OpenCode evals require exact version 1.18.29; received ${version}`); } return version; } diff --git a/packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts b/packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts index 45721a1191..1b1ba26ccd 100644 --- a/packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts +++ b/packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts @@ -331,7 +331,7 @@ async function handle(message: RpcMessage): Promise { case "initialize": result = { user: { sessionId: "opencode" }, - serverInfo: { name: "opencode", version: "1.18.17" }, + serverInfo: { name: "opencode", version: "1.18.29" }, }; break; case "thread/start": diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts index cecf96be27..cb605c8949 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -429,7 +429,7 @@ describe("ACPX installation integrity", () => { }, { name: "@anthropic-ai/claude-agent-sdk", - version: "0.3.232", + version: "0.3.263", directory: join(dependencyRoot, "claude-agent-sdk"), }, { @@ -457,7 +457,7 @@ describe("ACPX installation integrity", () => { bin: "bin/server.js", dependencies: { "@agentclientprotocol/sdk": "1.3.0", - "@anthropic-ai/claude-agent-sdk": "0.3.232", + "@anthropic-ai/claude-agent-sdk": "0.3.263", zod: "^3.25.0 || ^4.0.0", }, }), @@ -528,7 +528,7 @@ describe("ACPX installation integrity", () => { bin: "bin/server.js", dependencies: { "@agentclientprotocol/sdk": "1.3.0", - "@anthropic-ai/claude-agent-sdk": "0.3.232", + "@anthropic-ai/claude-agent-sdk": "0.3.263", zod: "^3.25.0 || ^4.0.0", }, }), diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index becb0b496c..ef2f72a926 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -45,25 +45,25 @@ const VERIFIED_PROVIDER_RUNTIME_TARGET_ENV = const QUALIFIED_CLAUDE_LINUX_X64_RUNTIME = Object.freeze({ runtimePackageName: "@anthropic-ai/claude-agent-sdk", - runtimePackageVersion: "0.3.232", + runtimePackageVersion: "0.3.263", packageName: "@anthropic-ai/claude-agent-sdk-linux-x64", - packageVersion: "0.3.232", - dependencyDeclaration: "0.3.232", + packageVersion: "0.3.263", + dependencyDeclaration: "0.3.263", relativeExecutable: "claude", executableDigest: - "sha256:61d23f8749136907d586d5b11831ea8a5234d4c1dea40a5e55c33b52e204c6d1", + "sha256:26d020351e8112f4006790f3cfce43b4c9df0c1bb1d0e542364d64151b81d5ba", environmentVariable: "CLAUDE_CODE_EXECUTABLE", }); const QUALIFIED_CODEX_LINUX_X64_RUNTIME = Object.freeze({ runtimePackageName: "@openai/codex", - runtimePackageVersion: "0.148.0", + runtimePackageVersion: "0.153.4", packageName: "@openai/codex-linux-x64", - packageVersion: "0.148.0-linux-x64", - dependencyDeclaration: "npm:@openai/codex@0.148.0-linux-x64", + packageVersion: "0.153.4-linux-x64", + dependencyDeclaration: "npm:@openai/codex@0.153.4-linux-x64", relativeExecutable: "vendor/x86_64-unknown-linux-musl/bin/codex", executableDigest: - "sha256:ac2cfed85fb647d61e0150b8548102b330e4799d9d81ad5d354de701edf6b074", + "sha256:56ef98ab4032d317ab26e9b5e5a175650717351edb16ed9cde0cb6d1734d62da", environmentVariable: "CODEX_PATH", }); @@ -79,8 +79,8 @@ const QUALIFIED_CLAUDE_PROVIDER_DEPENDENCIES = Object.freeze([ }), Object.freeze({ packageName: "@anthropic-ai/claude-agent-sdk", - packageVersion: "0.3.232", - dependencyDeclaration: "0.3.232", + packageVersion: "0.3.263", + dependencyDeclaration: "0.3.263", }), Object.freeze({ packageName: "zod", diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts index 3b8fa9135c..09a79ac021 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -27,14 +27,14 @@ describe("qualified ACPX profiles", () => { it("binds Codex ACP to the CLI runtime it launches", () => { expect(QUALIFIED_ACPX_PROFILES.codex).toMatchObject({ agentRuntimePackage: "@openai/codex", - agentRuntimeVersion: "0.148.0", + agentRuntimeVersion: "0.153.4", }); }); it("binds Claude ACP to the SDK and native CLI runtime it launches", () => { expect(QUALIFIED_ACPX_PROFILES.claude).toMatchObject({ agentRuntimePackage: "@anthropic-ai/claude-agent-sdk", - agentRuntimeVersion: "0.3.232", + agentRuntimeVersion: "0.3.263", }); }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index b953e19f5f..d851031398 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -64,7 +64,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< agentServerPackage: "@agentclientprotocol/claude-agent-acp", agentServerVersion: "0.70.0", agentRuntimePackage: "@anthropic-ai/claude-agent-sdk", - agentRuntimeVersion: "0.3.232", + agentRuntimeVersion: "0.3.263", commandDigest: "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a", qualificationModel: "claude-sonnet-5", @@ -80,7 +80,7 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< agentServerPackage: "@agentclientprotocol/codex-acp", agentServerVersion: "1.6.2", agentRuntimePackage: "@openai/codex", - agentRuntimeVersion: "0.148.0", + agentRuntimeVersion: "0.153.4", commandDigest: "sha256:7a923b3829884d3cabcc9659d22cace3f86813e7bfffc90974b10140a45bc400", qualificationModel: "gpt-5.6-sol", diff --git a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts index 2c4aca9107..b3452ab6c8 100644 --- a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts +++ b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.test.ts @@ -308,7 +308,7 @@ describe("OpenCodeServerDriver", () => { headers: { "Content-Type": "application/json" }, }); if (url.pathname === "/global/health") - return json({ healthy: true, version: "1.18.17" }); + return json({ healthy: true, version: "1.18.29" }); if (url.pathname === "/event") { return new Response( new ReadableStream({ @@ -680,7 +680,7 @@ describe("OpenCodeServerDriver", () => { output: 2, costUsd: 0.001, provider: "openrouter", - driverVersion: "1.18.17", + driverVersion: "1.18.29", }); await session.interrupt?.({ turnId: turn.turnId }); const snapshot = await session.snapshot(); diff --git a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts index a764d7b605..6e309cc09c 100644 --- a/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts +++ b/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts @@ -73,7 +73,7 @@ import { nativeMcpLaunchBinding } from "../native-mcp.js"; import { materializeNativeRuntimeSkills } from "../runtime-context-materializer.js"; export const OPENCODE_SERVER_DRIVER_KIND = "opencode_server" as const; -export const QUALIFIED_OPENCODE_VERSION = "1.18.17" as const; +export const QUALIFIED_OPENCODE_VERSION = "1.18.29" as const; export const QUALIFIED_OPENCODE_MODEL = "openrouter/deepseek/deepseek-v4-flash-0731" as const; diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index da740a6d1a..5018df90b5 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -656,7 +656,7 @@ describe("Capability live runnerd and Codex session", () => { expect(session.snapshot().config).toMatchObject({ provider: "opencode", driver: "opencode_server", - providerVersion: "1.18.17", + providerVersion: "1.18.29", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731", }); await service.shutdown(session.id); diff --git a/packages/paperclip-runner/src/live/live-session.ts b/packages/paperclip-runner/src/live/live-session.ts index 88ded8caf3..8cc9c15c2a 100644 --- a/packages/paperclip-runner/src/live/live-session.ts +++ b/packages/paperclip-runner/src/live/live-session.ts @@ -924,7 +924,7 @@ export class CapabilityLiveSessionService { ? "aws_agentcore_harness_api" : input.provider === "acpx" ? "acpx_runtime" : "codex_app_server", providerVersion: input.provider === "opencode" - ? "1.18.17" + ? "1.18.29" : input.provider === "claude_managed" ? input.managedProfile!.agentVersion : input.provider === "aws_agentcore" diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index 420d304d00..92ccd17a15 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -3160,7 +3160,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport { ? "opencode_server" : "codex_app_server", providerVersion: - provider === "opencode" ? "1.18.17" : "codex-app-server-v1", + provider === "opencode" ? "1.18.29" : "codex-app-server-v1", command: provider === "opencode" ? providerNodeCommand diff --git a/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs b/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs index abc5978a35..9ea9df7dfb 100644 --- a/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs +++ b/packages/paperclip-runner/test/acpx-codex-package-contract.test.mjs @@ -54,7 +54,10 @@ const nativeSessionExecutor = await readFile( ); test("the runner pins every qualified ACPX production dependency", () => { - assert.equal(runnerPackage.dependencies["@openai/codex"], undefined); + assert.equal(runnerPackage.dependencies["@openai/codex"], "0.153.4"); + assert.equal(runnerPackage.dependencies["@anthropic-ai/claude-agent-sdk"], undefined); + assert.equal(rootPackage.pnpm.overrides["@agentclientprotocol/codex-acp@1.6.2>@openai/codex"], runnerPackage.dependencies["@openai/codex"]); + assert.equal(rootPackage.pnpm.overrides["@agentclientprotocol/claude-agent-acp@0.70.0>@anthropic-ai/claude-agent-sdk"], "0.3.263"); assert.equal(runnerPackage.optionalDependencies, undefined); assert.equal(runnerPackage.dependencies.node, undefined); assert.equal(runnerPackage.dependencies.acpx, "0.13.1"); @@ -131,7 +134,7 @@ test("old and new pnpm configuration both apply the exact runtime patches", () = providerPackBuilder, /copyFileSync\(process\.execPath, stableNodeCommand\)/, ); - assert.match(codexPatch, /\+ "@openai\/codex": "0\.148\.0"/); + assert.match(codexPatch, /\+ "@openai\/codex": "0\.153\.4"/); }); test("the ACPX patch preserves launch-only state and verified spawning", () => { diff --git a/packages/paperclip-runner/test/fixtures/fake-opencode-server.mjs b/packages/paperclip-runner/test/fixtures/fake-opencode-server.mjs index ee93ebdd66..787ff49765 100644 --- a/packages/paperclip-runner/test/fixtures/fake-opencode-server.mjs +++ b/packages/paperclip-runner/test/fixtures/fake-opencode-server.mjs @@ -214,7 +214,7 @@ 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.17" }); + return json(response, 200, { healthy: true, version: "1.18.29" }); if (request.url === "/event") { eventConnections += 1; response.writeHead(200, { diff --git a/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch b/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch index dcdcd3bcdd..f1d54bb069 100644 --- a/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch +++ b/patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch @@ -49,3 +49,13 @@ diff --git a/dist/acp-agent.js b/dist/acp-agent.js ...mcpServers, ...(fileChangeAuditSupport ? { [FILE_CHANGE_AUDIT_SERVER_NAME]: fileChangeAuditSupport.mcpServer } +diff --git a/package.json b/package.json +--- a/package.json ++++ b/package.json +@@ -65,5 +65,5 @@ + "dependencies": { + "@agentclientprotocol/sdk": "1.3.0", +- "@anthropic-ai/claude-agent-sdk": "0.3.232", ++ "@anthropic-ai/claude-agent-sdk": "0.3.263", + "zod": "^3.25.0 || ^4.0.0" + }, diff --git a/patches/@agentclientprotocol__codex-acp@1.6.2.patch b/patches/@agentclientprotocol__codex-acp@1.6.2.patch index efe631113b..445f6d423e 100644 --- a/patches/@agentclientprotocol__codex-acp@1.6.2.patch +++ b/patches/@agentclientprotocol__codex-acp@1.6.2.patch @@ -6,7 +6,7 @@ diff --git a/package.json b/package.json "dependencies": { "@agentclientprotocol/sdk": "^1.3.0", - "@openai/codex": "^0.148.0", -+ "@openai/codex": "0.148.0", ++ "@openai/codex": "0.153.4", "diff": "^9.0.0", "open": "^11.0.0", "vscode-jsonrpc": "^9.0.1", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9723d1c65f..929bd5f8b4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,3 +22,12 @@ patchedDependencies: "@agentclientprotocol/claude-agent-acp@0.70.0": patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch "@agentclientprotocol/claude-agent-acp@0.73.0": patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch "@agentclientprotocol/codex-acp@1.6.2": patches/@agentclientprotocol__codex-acp@1.6.2.patch + +# Agent CLIs share the current runtime used by the native provider pack. +# pnpm patches change package files, but overrides control dependency resolution. +overrides: + "@agentclientprotocol/codex-acp@1.6.2>@openai/codex": "0.153.4" + "@agentclientprotocol/claude-agent-acp@0.70.0>@anthropic-ai/claude-agent-sdk": "0.3.263" + rollup: ">=4.59.0" + react: "^19.2.8" + react-dom: "^19.2.8" diff --git a/server/src/__tests__/adapter-registry.test.ts b/server/src/__tests__/adapter-registry.test.ts index c9e61f2e1f..e47f818f3d 100644 --- a/server/src/__tests__/adapter-registry.test.ts +++ b/server/src/__tests__/adapter-registry.test.ts @@ -312,8 +312,8 @@ describe("server adapter registry", () => { const expectedCodexInstall = `if ! command -v 'codex' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@openai/codex")}; fi`; const expectedGeminiInstall = `if ! command -v 'gemini' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@google/gemini-cli")}; fi`; const expectedOpenCodeInstall = `if ! command -v 'opencode' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("opencode-ai")}; fi`; - const expectedRunnerCodexInstall = `if ! command -v 'codex' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@openai/codex@0.148.0")}; fi`; - const expectedRunnerOpenCodeInstall = `if ! command -v 'opencode' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("opencode-ai@1.18.17")}; fi`; + const expectedRunnerCodexInstall = `if ! command -v 'codex' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@openai/codex@0.153.4")}; fi`; + const expectedRunnerOpenCodeInstall = `if ! command -v 'opencode' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("opencode-ai@1.18.29")}; fi`; expect(findActiveServerAdapter("claude_local")?.getRuntimeCommandSpec?.({})).toEqual({ command: "claude", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index f303cce14d..86791b6933 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -486,7 +486,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = { "opencode", `opencode-ai@${QUALIFIED_OPENCODE_RUNNER_VERSION}`, ) - : buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex@0.148.0"), + : buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex@0.153.4"), agentConfigurationDoc: "# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex, OpenCode, Claude Managed, AWS AgentCore, or a qualified Claude/Codex ACP agent through the Rust Paperclip runner and authenticated PRP transport. Pi is not available through the qualified ACPX profile. Managed providers use company-scoped qualified profiles, explicit retention acknowledgement, and spend limits.\n", getConfigSchema: () => ({ diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 686001f88f..df9910fc49 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -624,8 +624,8 @@ describe("remote provider pack manifest", () => { const payload = { pins: { nodeMinimum: "24.11.0", - codex: "0.148.0", - opencode: "1.18.17", + codex: "0.153.4", + opencode: "1.18.29", acpx: "0.13.1", claudeAcp: "0.70.0", codexAcp: "1.6.2", @@ -682,7 +682,7 @@ describe("remote provider pack manifest", () => { ); await writeManifest(); expect(readRemoteProviderPackManifest(root).payload.pins.opencode).toBe( - "1.18.17", + "1.18.29", ); for (const [artifactName, substituteName] of [ ["nodeCommand", "productionLock"], @@ -6011,6 +6011,84 @@ describe("runnerd provider runtime wiring", () => { ); }); + it("uses the image's shared Codex without uploading or installing artifacts", async () => { + const syncIn = vi.fn(async () => undefined); + const remoteExecute = vi.fn( + async (command: { command: string; args?: string[] }) => { + let stdout = ""; + const script = command.args?.[1] ?? ""; + if (command.args?.[0] === "--build-metadata") { + stdout = JSON.stringify({ + schema: "paperclip-runner/runnerd-build-metadata/v1", + binaryName: "paperclip-runnerd", + packageName: "@paperclipai/paperclip-runner", + binaryContractVersion: 2, + prpTransportModes: ["listen_ws"], + }); + } else if (command.args?.[0] === "--version") { + if ( + command.command.endsWith( + "/.paperclip-runtime/paperclip-runner/bin/codex", + ) + ) { + throw new Error("reached-preinstalled-codex-verification"); + } + stdout = "codex-cli 0.153.4"; + } else if (script.includes("command -v paperclip-runnerd")) { + stdout = "/usr/local/bin/paperclip-runnerd\n"; + } else if (script.includes("command -v codex")) { + stdout = script.includes("/opt/paperclip-runner/bin/codex") + ? "/opt/paperclip-runner/bin/codex\n" + : "/usr/local/bin/codex\n"; + } else if (!script.includes("ln -sfn")) { + throw new Error(`unexpected command: ${command.command}`); + } + return { + exitCode: 0, + signal: null, + timedOut: false, + stderr: "", + stdout, + }; + }, + ); + await createRunnerdBackend({ + db: leaseDb(execution), + execution, + runnerInstanceId: "runner-image-runtime", + runnerIngressAuthorized: true, + runnerExecutionTarget: { + kind: "remote", + transport: "sandbox", + remoteCwd: "/workspace", + environmentId: "environment", + leaseId: "lease", + providerKey: "daytona", + effectiveCapabilities: { runnerWebSocketIngress: true }, + runner: { execute: remoteExecute, syncIn }, + } as never, + }); + state.createTransport.mockClear(); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + const transport = state.createTransport.mock + .calls[0]![0] as RunnerTransportOptions & { + controlPlaneRegistration: (authority: unknown) => Promise; + }; + await expect(transport.controlPlaneRegistration({})).rejects.toThrow( + "reached-preinstalled-codex-verification", + ); + expect(syncIn).not.toHaveBeenCalled(); + expect(remoteExecute).toHaveBeenCalledWith( + expect.objectContaining({ + command: "/opt/paperclip-runner/bin/codex", + args: ["--version"], + }), + ); + expect( + remoteExecute.mock.calls.some(([call]) => call.command === "npm"), + ).toBe(false); + }); + it("binds a remote launch to the configured controller-owned runner artifact", async () => { const remoteCwd = "/home/daytona/paperclip-workspace"; const controllerArtifact = "/controller/artifacts/paperclip-runnerd"; diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 80d850afbe..1639ded4eb 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -5036,8 +5036,8 @@ const RUNNERD_BINARY_CONTRACT_VERSION = 2; const REMOTE_PROVIDER_PACK_SCHEMA = "paperclip-runner/remote-provider-pack/v1"; const REMOTE_PROVIDER_PACK_PINS = { nodeMinimum: "24.11.0", - codex: "0.148.0", - opencode: "1.18.17", + codex: "0.153.4", + opencode: "1.18.29", acpx: "0.13.1", claudeAcp: "0.70.0", codexAcp: "1.6.2", @@ -6463,6 +6463,9 @@ async function createRunnerdBackendWithinSessionClaim( return parseRemoteExecutableCandidate(result.stdout); }; + // Image policy: keep one latest stable CLI installation shared by native and + // local adapters. Preferred bin entries must point to that same installation; + // never bake an older global CLI alongside a private runner-only version. const discoverPreinstalledExecutable = async ( name: "paperclip-runnerd" | "codex", ) => { @@ -6471,9 +6474,9 @@ async function createRunnerdBackendWithinSessionClaim( command: "sh", args: [ "-c", - `candidate="$HOME/.local/bin/${name}"; ` + - `if [ -x "$candidate" ]; then printf '%s\\n' "$candidate"; ` + - `else command -v ${name} 2>/dev/null || true; fi`, + `for candidate in /opt/paperclip-runner/bin/${name} "$HOME/.local/bin/${name}"; do ` + + `if [ -x "$candidate" ]; then printf '%s\\n' "$candidate"; exit 0; fi; done; ` + + `command -v ${name} 2>/dev/null || true`, ], cwd: remoteTarget.remoteCwd, bypassSession: true, diff --git a/server/src/services/native-runtime/provider-profile.ts b/server/src/services/native-runtime/provider-profile.ts index aa09ba455d..197f82f1f8 100644 --- a/server/src/services/native-runtime/provider-profile.ts +++ b/server/src/services/native-runtime/provider-profile.ts @@ -9,7 +9,7 @@ import { CLAUDE_MANAGED_QUALIFIED_MODEL, } from "../provider-profile-qualification.js"; -export const QUALIFIED_OPENCODE_RUNNER_VERSION = "1.18.17" as const; +export const QUALIFIED_OPENCODE_RUNNER_VERSION = "1.18.29" as const; export const DEFAULT_OPENCODE_RUNNER_MODEL = "openrouter/deepseek/deepseek-v4-flash-0731" as const; export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const; diff --git a/ui/src/adapters/codex-local/config-fields.test.tsx b/ui/src/adapters/codex-local/config-fields.test.tsx index 72d32af7cc..62bc325b20 100644 --- a/ui/src/adapters/codex-local/config-fields.test.tsx +++ b/ui/src/adapters/codex-local/config-fields.test.tsx @@ -29,7 +29,7 @@ describe("Paperclip Runner Codex configuration", () => { const html = renderRunner({ provider: "codex" }); expect(html).toContain(''); - expect(html).toContain("OpenCode 1.18.17"); + expect(html).toContain("OpenCode 1.18.29"); expect(html).toContain("ACPX"); expect(html).toContain("Automatic (isolated)"); expect(html).not.toContain("Ask when requested"); @@ -46,7 +46,7 @@ describe("Paperclip Runner Codex configuration", () => { }); expect(html).toContain( - '', + '', ); expect(html).toContain( '', diff --git a/ui/src/adapters/codex-local/config-fields.tsx b/ui/src/adapters/codex-local/config-fields.tsx index 5ad85dc149..706e0df118 100644 --- a/ui/src/adapters/codex-local/config-fields.tsx +++ b/ui/src/adapters/codex-local/config-fields.tsx @@ -236,7 +236,7 @@ export function CodexLocalConfigFields({ }} > - +