mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Merge reviewed Claude behavior into platform qualification
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/claude-runner-default: docs: link the approved protocol review boundary fix(runner): retain versioned profiles in Docker context docs: record Claude review fixes and protocol prerequisite fix(claude): preserve permissions and connection claims at creation feat(runner): admit Claude effort before native prompts and resume fix: preserve TOML table semantics during Codex startup trust
This commit is contained in:
commit
18626f677f
41 files changed
+417
-109
No files matched your search
@@ -44,6 +44,7 @@ packages/paperclip-runner/test/**
|
||||
packages/paperclip-runner/test/fixtures/**
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v16-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v17-identity.json
|
||||
!packages/paperclip-runner/test/fixtures/copilot-profile-v18-identity.json
|
||||
!packages/paperclip-runner/test-fixtures
|
||||
packages/paperclip-runner/test-fixtures/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp
|
||||
@@ -53,6 +54,7 @@ packages/paperclip-runner/test-fixtures/pi-acp/**
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v20-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v21-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v22-identity.json
|
||||
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v23-identity.json
|
||||
!packages/paperclip-runner/docs/capability-contract.md
|
||||
# check:runner-workflow-traceability access()es every regression test the
|
||||
# stress-traceability spec names — those are src/**/*.test.ts files, so
|
||||
|
||||
@@ -129,6 +129,52 @@ and require fresh Linux CI. Keep ARM startup sign-off unresolved until proven
|
||||
on a compatible target; package installation and direct execution alone do not
|
||||
qualify the native runner. All Docker/native builds remain remote.
|
||||
|
||||
Checkpoint `598e190cb308f4d7f8f43c188c2e7d88c3fc90d1` passes
|
||||
[ordinary CI](https://github.com/paperclipai/paperclip/actions/runs/38279960223):
|
||||
both required gates, 54 successful checks/statuses, and four conditional skips.
|
||||
Fresh review is 5/5; all 15 review threads are resolved. Human code-owner approval
|
||||
is still absent. The test-drive serves `2026.1005.0+350.git.598e190cb3`; all four
|
||||
execution configurations and both historical native runs survive restart.
|
||||
The verified Apple Silicon daemon remains unchanged, `needsBuild: false`, with
|
||||
no binary override, native compilation, Docker build, or provider turn locally.
|
||||
Receipts: `/private/tmp/ci-final-598e190-ci-review-receipt.json` and
|
||||
`/private/tmp/pr2-final-preservation-after.json`.
|
||||
|
||||
The [direct/native ARM diagnostic](https://github.com/paperclipai/paperclip/actions/runs/38280515437)
|
||||
records GNU libc 2.39, AppArmor enabled with restricted unprivileged user
|
||||
namespaces, an unconfined current profile, zero effective capabilities, and no
|
||||
seccomp filter. Native startup still fails at bubblewrap loopback `RTM_NEWADDR`.
|
||||
The direct control stopped before Codex spawn at a TOML preservation guard,
|
||||
so it does not establish the sandbox failure's cause. Receipt:
|
||||
`/private/tmp/ci-final-arm-direct-diagnostic-598-receipt.json`.
|
||||
|
||||
Pure, integrity-bound public `smol-toml` 1.9.1 reproduction identifies that guard
|
||||
failure: its null-prototype tables differ from the writer's spread-created
|
||||
ordinary objects. Preserve the parser's tables while changing only the intended
|
||||
trust field; keep strict comparison, original comments/bytes, private writes,
|
||||
and malformed-input rejection. No parser pin, semantic bypass, or permission
|
||||
change. The existing suite passes 22 controls across both table representations;
|
||||
real 1.9.1 treatment repairs five valid configurations and keeps two invalid
|
||||
configurations unchanged. Runner/server typechecks pass. Receipt:
|
||||
`/private/tmp/codex-default-startup-trust-fix-receipt.json`.
|
||||
|
||||
The [four-target performance qualifier](https://github.com/paperclipai/paperclip/actions/runs/38281631416)
|
||||
uses literal `598e190` product/workflow source. Observed Linux x64/ARM64 and Apple
|
||||
Silicon builds pass with actual concurrent JavaScript/native preparation.
|
||||
Cache restore is exercised; observed results are cold misses, not measured
|
||||
speedups. The cache writer is reachable through the existing verified master
|
||||
canary push, with exact source and repository guards; no write-gate expansion
|
||||
is needed. Linux daemon bytes match the measured GNU 2.39 baseline. Remaining
|
||||
producer/consumer results must be recorded before qualification claims.
|
||||
|
||||
Next action: freeze the narrow trust correction, repeat the bounded direct
|
||||
control with explicit corrected-tooling provenance, and retain native startup
|
||||
as mandatory. A private namespace negative control and bounded read-only
|
||||
AppArmor journal may help distinguish host denial. Do not modify host policy,
|
||||
relax Codex's sandbox, or substitute direct execution for native qualification.
|
||||
Require fresh CI/review after the correction. ARM startup and managed staging
|
||||
remain unresolved; no merge or production recommendation is made.
|
||||
|
||||
## Current addition: Linux ARM64
|
||||
|
||||
The user now requires ARM Linux support as well. Treat ARM Linux as ARM64
|
||||
|
||||
@@ -6,10 +6,13 @@ Updated 2026-10-12.
|
||||
|
||||
Start from `058af8f1b69c5fa3d9bc974b9eab73c141b52823`; branch `codex/claude-runner-default`, checkout `/Users/dotta/.codex/worktrees/670b/paperclip`. New Claude agents eventually default to native ACPX Claude on Apple Silicon, Intel Mac, Linux x64 and Linux ARM64. Codex selection remains owned by the dependency branch. On 2026-10-12 the user explicitly approved preserving its latest Linux ARM64 default from `dcdf09a0f33536fa2b6daaf6e731d921d5711282` during reconciliation. Ordinary Claude defaults become Opus 5.5; explicit models and existing runner selections remain intact.
|
||||
|
||||
Local Apple Silicon API-key task and follow-up are the first gate. Keep existing connection/login tools. Complete core settings and local subscription next, then package/qualify other targets. Two reviewable PRs; no merge or deployment. Linux ARM64 covers installed hosts and SSH, not ARM cloud images. Separate $50 Claude ceiling including setup probes, unknown-charge holds and task turns. No automatic paid retry loops. Docker and expensive Rust builds run in cloud infrastructure; never build Docker images on this Mac.
|
||||
Local Apple Silicon API-key task and follow-up are the first gate. Keep existing connection/login tools. Complete core settings and local subscription next, then package/qualify other targets. Two behavior/platform delivery boundaries plus the user-approved protocol prerequisite PR; no merge or deployment. Linux ARM64 covers installed hosts and SSH, not ARM cloud images. Separate $50 Claude ceiling including setup probes, unknown-charge holds and task turns. No automatic paid retry loops. Docker and expensive Rust builds run in cloud infrastructure; never build Docker images on this Mac.
|
||||
|
||||
## Current state
|
||||
|
||||
- Review boundaries: the user approved a separate shared-protocol prerequisite to keep each PR below the repository's 100-file limit. The behavior PR is [#15993](https://github.com/paperclipai/paperclip/pull/15993); the platform branch is `codex/claude-runner-platforms` and remains gated. Protocol prerequisite [#15994](https://github.com/paperclipai/paperclip/pull/15994), initial candidate `2e8d5e906e`, has 44 source files and preserves latest Codex startup changes from `a200495cc7`. Behavior candidate `12f30702d7` includes that prerequisite.
|
||||
- Review corrections preserve legacy ACP permission aliases, retained native import settings and fixed Claude OAuth claims through native creation. Unknown inherited-property model names stay unpriced. Legacy onboarding cases now explicitly select Legacy; ordinary native Claude onboarding skips unqualified controller targets before provisioning. Focused import52, selection/pricing117, OAuth54, CLI fixture104, shared Pi92, Copilot28 and Pi evidence72 controls passed; final CI/review remains required.
|
||||
- Shared source attestations are versioned rather than rewriting historical identities: Pi v23 and pending Copilot v18 bind the changed Claude effort sources. Prior declarations remain. Executable closures and platform qualification are unchanged; prior mismatched profiles require session reopening. These new generated bindings require a current-source daemon/package build.
|
||||
- Local ordinary API-key onboarding, useful task, revision, cold resume with high effort, independent artifact downloads and persistence passed. The active preview is `http://127.0.0.1:3105/CLAA/issues/CLAA-1`.
|
||||
- The user redirected the remaining live checks to API keys. Subscription login remains supported, but no further attended subscription qualification is required; subscription follow-up is not claimed.
|
||||
- Shared runner controls passed light/dark, 390-pixel mobile and keyboard opening/closing checks. The walkthrough found native Command/Extra args controls; they are now hidden for native execution. The saved Claude preservation test covers this path. Agent form 111 tests, UI TypeScript and token gates passed.
|
||||
@@ -86,3 +89,8 @@ Independent downloads matched both byte oracles, and the original remained uncha
|
||||
Executable identities: daemon source `5f0b898e9ecef77afefe309a230e04d7fef66cde`, sha256 `228c7814c4786614c8358b205da01cf43df822ca5e6f6b74d5a1e8304d03a6de`; sidecar sha256 `daabddfc1373e6b6d2a754cb60bc4c33d7a8b6d6d5c3564ce511ebfaeb8a463d`; Claude SDK 0.3.286 executable sha256 `75e3016e9d2570767b08e43a7467d4817a4f149232c169ca295f2c95fef21433`. Behavior source is `9788c05d0a9825d825c0461eab3191da6a6dcad7`. Reconciled source `966b3d5bc294442f71c1def9406ed2027f41d191` merges Codex dependency `123a18837478d8cd0e2aff23f7e7aa86c9107de3`, retaining its GNU libc 2.39 gate and Linux ARM64 selection. Claude admission/sandbox/input and daemon Rust sources did not change during that reconciliation; the rebuilt sidecar digest is identical. Updated runner TypeScript build, server TypeScript, 95 selection controls and 15 installed-startup controls passed after reconciliation.
|
||||
|
||||
[Corrected cloud source checks](https://github.com/paperclipai/paperclip/actions/runs/38279356267) and [corrected four-target packaging](https://github.com/paperclipai/paperclip/actions/runs/38279359427) run on frozen behavior source 9788c05. Carry forward only unchanged evidence; final source and package consumers still require passing checks. Active company caps after archiving the unused subscription-only fixture total $42: original $17, failed candidate $9, API acceptance $16. Agent limits share these allocations, and unknown-charge holds remain in place. Known API estimates now total $0.9975406; this excludes the original unpriced receipt and is not a provider invoice.
|
||||
|
||||
|
||||
## Approved prerequisite split and current CI
|
||||
|
||||
The behavior PR now targets the protocol branch and has 64 changed files. Protocol #15994 targets the latest Codex dependency and adds the shared v7 effort/resume contract with historical readers. Cloud Docker-context verification found that `.dockerignore` omitted the new Pi v23 and Copilot v18 identity inputs; explicit exceptions retain them without broadening the fixture context. The old 9788c05 package campaign passed all four daemon producers and Apple Silicon clean install, but the Linux ARM64 consumer failed durable-suspend settlement. Retain that failure; current-source package qualification and ARM diagnosis remain required. No local Docker or Rust compilation occurred.
|
||||
@@ -278,7 +278,7 @@ describe("claude_local ACP lane", () => {
|
||||
onMeta: async (payload) => { meta.push(payload); },
|
||||
}));
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(meta[0]?.env?.ANTHROPIC_MODEL).toBe("claude-opus-5");
|
||||
expect(meta[0]?.env?.ANTHROPIC_MODEL).toBe("claude-opus-5-5");
|
||||
});
|
||||
|
||||
it("keeps ACP model precedence consistent with CLI and provider overrides", () => {
|
||||
|
||||
@@ -78,7 +78,7 @@ function buildContext(config: Record<string, unknown> = {}) {
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config,
|
||||
config: { model: "claude-opus-5", ...config },
|
||||
context: {},
|
||||
onLog: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
@@ -189,11 +189,11 @@ describe("claude remote execution", () => {
|
||||
remoteDir: `${managedRemoteWorkspace}/.paperclip-runtime/claude/mcp-config`,
|
||||
followSymlinks: true,
|
||||
}));
|
||||
expect(runChildProcess).toHaveBeenCalledTimes(1);
|
||||
const call = runChildProcess.mock.calls[0] as unknown as
|
||||
expect(runChildProcess.mock.calls.filter(call => !call[2].includes("--version"))).toHaveLength(1);
|
||||
const call = runChildProcess.mock.calls.find(call => !call[2].includes("--version")) as unknown as
|
||||
| [string, string, string[], { env: Record<string, string>; remoteExecution?: { remoteCwd: string } | null }]
|
||||
| undefined;
|
||||
expect(call?.[2]).toEqual(expect.arrayContaining(["--model", "claude-opus-5"]));
|
||||
expect(call?.[2]).toEqual(expect.arrayContaining(["--model", "claude-opus-5-5"]));
|
||||
expect(call?.[2]).toContain("--dangerously-skip-permissions");
|
||||
expect(call?.[2]).not.toContain("--allowedTools");
|
||||
expect(call?.[2]).toContain("--append-system-prompt-file");
|
||||
@@ -255,7 +255,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: "12345678-1234-4abc-9def-123456789012",
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
},
|
||||
@@ -280,8 +280,8 @@ describe("claude remote execution", () => {
|
||||
onLog: async () => {},
|
||||
});
|
||||
|
||||
expect(runChildProcess).toHaveBeenCalledTimes(1);
|
||||
const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[]] | undefined;
|
||||
expect(runChildProcess.mock.calls.filter(call => !call[2].includes("--version"))).toHaveLength(1);
|
||||
const call = runChildProcess.mock.calls.find(call => !call[2].includes("--version")) as unknown as [string, string, string[]] | undefined;
|
||||
expect(call?.[2]).not.toContain("--resume");
|
||||
});
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config: { engine: "cli", command: "claude", cwd: rootDir, paperclipRuntimeSkills: [] },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude", cwd: rootDir, paperclipRuntimeSkills: [] },
|
||||
context: {},
|
||||
onLog,
|
||||
});
|
||||
@@ -345,7 +345,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: "12345678-1234-4abc-9def-123456789012",
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
},
|
||||
@@ -370,8 +370,8 @@ describe("claude remote execution", () => {
|
||||
onLog: async () => {},
|
||||
});
|
||||
|
||||
expect(runChildProcess).toHaveBeenCalledTimes(1);
|
||||
const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[]] | undefined;
|
||||
expect(runChildProcess.mock.calls.filter(call => !call[2].includes("--version"))).toHaveLength(1);
|
||||
const call = runChildProcess.mock.calls.find(call => !call[2].includes("--version")) as unknown as [string, string, string[]] | undefined;
|
||||
expect(call?.[2]).toContain("--resume");
|
||||
expect(call?.[2]).toContain("12345678-1234-4abc-9def-123456789012");
|
||||
});
|
||||
@@ -413,7 +413,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
},
|
||||
@@ -463,7 +463,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
...config,
|
||||
@@ -619,7 +619,7 @@ describe("claude remote execution", () => {
|
||||
sessionDisplayId: "12345678-1234-4abc-9def-123456789012",
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
env: { ANTHROPIC_API_KEY: "fixture-anthropic-key" },
|
||||
@@ -633,7 +633,7 @@ describe("claude remote execution", () => {
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(runChildProcess).toHaveBeenCalledTimes(2);
|
||||
const first = (runChildProcess.mock.calls[0] as unknown as [string, string, string[], { stdin?: string }])[3]?.stdin ?? "";
|
||||
const first = (runChildProcess.mock.calls.find(call => !call[2].includes("--version")) as unknown as [string, string, string[], { stdin?: string }])[3]?.stdin ?? "";
|
||||
const retry = (runChildProcess.mock.calls[1] as unknown as [string, string, string[], { stdin?: string }])[3]?.stdin ?? "";
|
||||
expect(first).toContain("## Compact assignment");
|
||||
expect(first).not.toContain("Explain the next step before starting work.");
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -170,7 +170,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -202,7 +202,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -239,7 +239,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -269,7 +269,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -296,7 +296,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -323,7 +323,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -361,7 +361,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -401,7 +401,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
}),
|
||||
@@ -428,7 +428,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -451,7 +451,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -471,7 +471,7 @@ describe("claude sandbox hello probe diagnostics", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -494,7 +494,7 @@ describe("claude auth mode hints", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
env: { CLAUDE_CODE_OAUTH_TOKEN: "oauth-test-token" },
|
||||
@@ -515,7 +515,7 @@ describe("claude auth mode hints", () => {
|
||||
it("reports an intentionally selected managed API account without a subscription warning", async () => {
|
||||
probeResult.value = { exitCode: 0, stdout: successStdout, stderr: "" };
|
||||
const result = await testEnvironment({ companyId: "company-1", adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude", managedAiConnection: { provider: "anthropic", method: "api_key" }, env: { ANTHROPIC_API_KEY: "api-test-key" } },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude", managedAiConnection: { provider: "anthropic", method: "api_key" }, env: { ANTHROPIC_API_KEY: "api-test-key" } },
|
||||
executionTarget: sandboxTarget, environmentName: "Daytona",
|
||||
});
|
||||
expect(result.checks.find(check => check.code === "claude_anthropic_api_key_overrides_subscription")).toMatchObject({ level: "info", message: "Using the selected Claude API connection." });
|
||||
@@ -528,7 +528,7 @@ describe("claude auth mode hints", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "claude",
|
||||
env: {
|
||||
@@ -606,7 +606,7 @@ describe("claude CLI local hello probe hardening", () => {
|
||||
await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: {
|
||||
config: { model: "claude-opus-5",
|
||||
engine: "cli",
|
||||
command: "/tmp/evil/claude",
|
||||
env: {
|
||||
@@ -683,7 +683,7 @@ describe("claude CLI local hello probe hardening", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: null,
|
||||
environmentName: null,
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("claude sandbox auth-missing check", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sandboxTarget,
|
||||
environmentName: "Daytona",
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe("claude sandbox auth-missing check", () => {
|
||||
const result = await testEnvironment({
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_local",
|
||||
config: { engine: "cli", command: "claude" },
|
||||
config: { model: "claude-opus-5", engine: "cli", command: "claude" },
|
||||
executionTarget: sshTarget,
|
||||
environmentName: "Remote host",
|
||||
});
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
"requiresProviderPolicy": false
|
||||
},
|
||||
"pi": {
|
||||
"agentProfileVersion": 22,
|
||||
"agentProfileVersion": 23,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"cursor": {
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 17,
|
||||
"agentProfileVersion": 18,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"requiresProviderPolicy": true,
|
||||
"qualificationStatus": "pending"
|
||||
},
|
||||
|
||||
@@ -3200,7 +3200,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
|
||||
let published: Value = serde_json::from_str(include_str!(
|
||||
"../../../../test-fixtures/pi-acp/profile-v22-identity.json"
|
||||
"../../../../test-fixtures/pi-acp/profile-v23-identity.json"
|
||||
))
|
||||
.unwrap();
|
||||
let mut value = descriptor("codex");
|
||||
@@ -3224,6 +3224,7 @@ mod tests {
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v19-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v20-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v21-identity.json"),
|
||||
include_str!("../../../../test-fixtures/pi-acp/profile-v22-identity.json"),
|
||||
] {
|
||||
let prior: Value = serde_json::from_str(prior).unwrap();
|
||||
value["commandDigest"] = prior["commandDigest"].clone();
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
|
||||
agent_runtime_version: Some("1.0.0"),
|
||||
command_digest:
|
||||
"sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"cursor" => AcpxReleaseProfile {
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
|
||||
const CODEX_ACPX_DIGEST: &str =
|
||||
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3";
|
||||
const PI_ACPX_DIGEST: &str =
|
||||
"sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9";
|
||||
"sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6";
|
||||
|
||||
fn temporary_directory(label: &str) -> PathBuf {
|
||||
let nonce = SystemTime::now()
|
||||
|
||||
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
|
||||
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
|
||||
assert.equal(contract.acpxPatchSha256, createHash("sha256")
|
||||
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v22-identity.json"], ["copilot", "test/fixtures/copilot-profile-v17-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v23-identity.json"], ["copilot", "test/fixtures/copilot-profile-v18-identity.json"]]) {
|
||||
const identity = await readJson(path);
|
||||
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
|
||||
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
|
||||
|
||||
@@ -87,8 +87,16 @@ describe("Copilot build-owned installation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v17 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
it("preserves v17 executable and policy identity while binding the shared Claude effort sources", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v18 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
|
||||
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
|
||||
@@ -119,7 +127,7 @@ describe("Copilot build-owned installation", () => {
|
||||
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
|
||||
});
|
||||
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
identity.declaration[field] = "0".repeat(64);
|
||||
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
|
||||
|
||||
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 22,
|
||||
"agentProfileVersion": 23,
|
||||
"agentServerPackage": "pi-acp",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"commandDigest": "sha256:e92078bee3c23bec4100aa589013a44613d054cd686826534025d8019e9f39a9",
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"cursor": {
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 17,
|
||||
"agentProfileVersion": 18,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:481d0852ae8272a90f9912723d540518a874a0da65a9070e33b52ea1a14cbd7f",
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"qualificationStatus": "pending",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
|
||||
@@ -88,11 +88,19 @@ describe("Pi installation factory", () => {
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves profile 22 runtime bytes and revises only shared Claude effort source attestations", async () => {
|
||||
const read = async (version: number) => JSON.parse(await readFile(new URL(`../../../test-fixtures/pi-acp/profile-v${version}-identity.json`, import.meta.url), "utf8"));
|
||||
const prior = await read(22), current = await read(23);
|
||||
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
|
||||
.toEqual(["agentProfileVersion", "recoveryIdentitySourceSha256", "runtimeSandboxSourceSha256"]);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
|
||||
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
|
||||
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v22-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v23-identity.json", import.meta.url), "utf8"));
|
||||
const declaration = identity.declaration;
|
||||
expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`);
|
||||
@@ -123,21 +131,21 @@ describe("Pi installation factory", () => {
|
||||
|
||||
it("rejects legacy profiles and tampered executable identities", () => {
|
||||
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 22");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 22");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 23");
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 23");
|
||||
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 23");
|
||||
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
|
||||
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
|
||||
}
|
||||
|
||||
@@ -5,16 +5,66 @@ import {
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
|
||||
import { parse } from "smol-toml";
|
||||
import { trustCodexStartupRoot } from "./codex-startup-trust.js";
|
||||
|
||||
describe("isolated Codex startup trust", () => {
|
||||
const parserStyle = vi.hoisted(() => ({ nullPrototypeTables: false }));
|
||||
vi.mock("smol-toml", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("smol-toml")>();
|
||||
const setTablePrototypes = (value: unknown): void => {
|
||||
if (value === null || typeof value !== "object" || value instanceof Date) return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(setTablePrototypes);
|
||||
return;
|
||||
}
|
||||
Object.values(value).forEach(setTablePrototypes);
|
||||
Object.setPrototypeOf(value, parserStyle.nullPrototypeTables ? null : Object.prototype);
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
parse: (...args: Parameters<typeof actual.parse>) => {
|
||||
const result = actual.parse(...args);
|
||||
// Exercise both table representations independently of the installed
|
||||
// parser version while retaining the real TOML values.
|
||||
setTablePrototypes(result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ name: "ordinary tables", nullPrototypeTables: false },
|
||||
{ name: "null-prototype tables", nullPrototypeTables: true },
|
||||
])("isolated Codex startup trust with $name", ({ nullPrototypeTables }) => {
|
||||
beforeEach(() => { parserStyle.nullPrototypeTables = nullPrototypeTables; });
|
||||
afterEach(() => { parserStyle.nullPrototypeTables = false; });
|
||||
|
||||
it.each(["missing", "empty"])("initializes %s config in an isolated home", (kind) => {
|
||||
const temp = realpathSync(mkdtempSync(join(tmpdir(), "codex-trust-empty-")));
|
||||
try {
|
||||
const cwd = join(temp, "project");
|
||||
const home = join(temp, "home");
|
||||
mkdirSync(cwd);
|
||||
if (kind === "empty") {
|
||||
mkdirSync(home);
|
||||
writeFileSync(join(home, "config.toml"), "");
|
||||
}
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
const path = join(home, "config.toml");
|
||||
const updated = readFileSync(path, "utf8");
|
||||
expect(parse(updated)).toEqual({ projects: { [cwd]: { trust_level: "trusted" } } });
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
expect(readFileSync(path, "utf8")).toBe(updated);
|
||||
} finally { rmSync(temp, { recursive: true, force: true }); }
|
||||
});
|
||||
it.each(["missing", "table", "empty-table", "inline", "empty-inline", "inline-projects"])(
|
||||
"preserves managed comments and unrelated content with %s project configuration", (kind) => {
|
||||
const temp = realpathSync(mkdtempSync(join(tmpdir(), "codex-trust-comments-")));
|
||||
@@ -60,15 +110,17 @@ describe("isolated Codex startup trust", () => {
|
||||
mkdirSync(home);
|
||||
writeFileSync(
|
||||
join(home, "config.toml"),
|
||||
'model = "test"\n[mcp_servers.fixture]\nurl = "http://localhost/example"\n',
|
||||
'model = "test"\nstarted = 2026-10-12T12:00:00Z\nlimits = [inf, nan, 9007199254740991]\n[mcp_servers.fixture]\nurl = "http://localhost/example"\n',
|
||||
);
|
||||
const original = readFileSync(join(home, "config.toml"), "utf8");
|
||||
const unrelated = parse(original);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
expect(parse(readFileSync(join(home, "config.toml"), "utf8"))).toEqual({
|
||||
model: "test",
|
||||
mcp_servers: { fixture: { url: "http://localhost/example" } },
|
||||
...unrelated,
|
||||
projects: { [cwd]: { trust_level: "trusted" } },
|
||||
});
|
||||
expect(readFileSync(join(home, "config.toml"), "utf8")).toContain(original);
|
||||
expect(readFileSync(join(home, "config.toml"), "utf8")).not.toContain(
|
||||
`[projects."${temp}"]`,
|
||||
);
|
||||
|
||||
@@ -92,24 +92,25 @@ export function trustCodexStartupRoot(codexHome: string, cwd: string): void {
|
||||
const path = join(codexHome, "config.toml");
|
||||
const source = existsSync(path) ? readFileSync(path, "utf8") : "";
|
||||
const config = parse(source);
|
||||
const projects = config.projects ?? {};
|
||||
// Retain the parser's table prototypes so strict candidate comparison also
|
||||
// works with parsers that return null-prototype TOML tables.
|
||||
const projects = config.projects ?? parse("");
|
||||
if (
|
||||
typeof projects !== "object" ||
|
||||
Array.isArray(projects) ||
|
||||
projects instanceof Date
|
||||
)
|
||||
throw new Error("codex_startup_trust_invalid_projects");
|
||||
const project = projects[root] ?? {};
|
||||
const project = projects[root] ?? parse("");
|
||||
if (
|
||||
typeof project !== "object" ||
|
||||
Array.isArray(project) ||
|
||||
project instanceof Date
|
||||
)
|
||||
throw new Error("codex_startup_trust_invalid_project");
|
||||
config.projects = {
|
||||
...projects,
|
||||
[root]: { ...project, trust_level: "trusted" },
|
||||
};
|
||||
project.trust_level = "trusted";
|
||||
projects[root] = project;
|
||||
config.projects = projects;
|
||||
const updated = editTrust(source, root, config);
|
||||
if (updated === source) return;
|
||||
const temporary = resolve(codexHome, `config.toml.${randomUUID()}.tmp`);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"commandDigest": "sha256:eba77e17e5e874d0dc80585abbf0266f3005c6d6fb746dda29cafad95657f6c6",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "pi",
|
||||
"agentProfileVersion": 23,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "0.0.33",
|
||||
"agentRuntimeVersion": "1.0.0",
|
||||
"nodeVersion": "24.21.0",
|
||||
"closure": {
|
||||
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
|
||||
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
|
||||
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
|
||||
},
|
||||
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
|
||||
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
|
||||
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
|
||||
"pathPolicy": "sdk-normalization-v1",
|
||||
"mcpToolNames": "provider-safe-exact-source-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
|
||||
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
|
||||
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
|
||||
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"nativeInputDisposition": "queued-only-v1",
|
||||
"cacheWarming": "disabled-owned-decision-v1",
|
||||
"thinkingLevel": {
|
||||
"supported": [
|
||||
"off",
|
||||
"low",
|
||||
"high",
|
||||
"max"
|
||||
],
|
||||
"newConfigurationDefault": "low",
|
||||
"admission": "explicit-native-effective-before-prompt-v1",
|
||||
"recovery": "mode-bound-v1"
|
||||
},
|
||||
"modelSelection": "caller-selected-native-verified-v1",
|
||||
"providerConfigurationSourceSha256": "d5b4c1a426afc85f040c9b55358d1d645299586dc8cec72d1be2509187ca3865",
|
||||
"credentialEnvironmentSourceSha256": "f8dc40ea1aa894bd44754a35a8f65fbcaa2f4c7ffc88cc5fdaa965e0d378d9bc",
|
||||
"runtimeSandboxSourceSha256": "8767456c125b53b1e85fbfce84245c9b213f9ef27a58970cf73dbc2ff82005d8",
|
||||
"recoveryIdentitySourceSha256": "8479e04da1ae2f7742e34f045b0270b99e9c3ae39a5b8fb9f164e5776d83224c",
|
||||
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
|
||||
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fb4a232460dc9e6c7e7221cb4ddd66d7cb82fa0b35e1b3eb04f2d1ee5ee18c86",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -499,13 +499,13 @@ describeEmbeddedPostgres("hired agent provider credential inheritance", () => {
|
||||
const parent = await seedParentAgent(companyId, "paperclip_runner", {});
|
||||
await db.update(agents).set({ adapterConfig: {
|
||||
provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5", acpxPermissionMode: "approve-all",
|
||||
env: { ANTHROPIC_API_KEY: secretRef("parent-secret") }, cwd: "/private/parent",
|
||||
env: { PRIVATE_PARENT_TOKEN: secretRef("parent-secret") }, cwd: "/private/parent",
|
||||
invocationLimits: { maxIterations: 3, credentials: "never-copy" },
|
||||
} }).where(eq(agents.id, parent.id));
|
||||
const res = await hire(agentActor(companyId, parent.id), companyId, {
|
||||
name: "Approval Snapshot Child", role: "engineer", adapterType: "paperclip_runner", inheritRuntimeFrom: "caller",
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(res.body.agent.status).toBe("pending_approval");
|
||||
expect(res.body.approval.payload.inheritRuntimeFrom).toBeUndefined();
|
||||
expect(res.body.approval.payload.adapterConfig).toMatchObject({ provider: "acpx", acpxAgent: "claude", acpxPermissionMode: "approve-all" });
|
||||
|
||||
@@ -1853,8 +1853,8 @@ describe("agent permission routes", () => {
|
||||
}),
|
||||
{ runnerResolved: true, aiConnectionInstall: undefined, createdByUserId: "board-user", responsibleUserId: "board-user", claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } },
|
||||
);
|
||||
if (adapterCase.adapterType === "codex_local") {
|
||||
expect(mockAgentService.create.mock.calls[0][1].adapterConfig).toMatchObject({ provider: "codex" });
|
||||
if (["codex_local", "claude_local"].includes(adapterCase.adapterType)) {
|
||||
if (adapterCase.adapterType === "codex_local") expect(mockAgentService.create.mock.calls[0][1].adapterConfig).toMatchObject({ provider: "codex" });
|
||||
const { AGENT_RUNNER_SSH_PLATFORM_COMMAND } = await import("../services/agent-runner-selection.js");
|
||||
expect(mockRunSshCommand).toHaveBeenCalledWith(expect.objectContaining({ host: "qa.example.invalid" }), AGENT_RUNNER_SSH_PLATFORM_COMMAND, { timeoutMs: 10_000 });
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,16 @@ const withEnv = (env: Record<string, unknown>) => ({ env });
|
||||
// --- The pure server-enforced binding invariant (no database) ----------------
|
||||
|
||||
describe("assertClaudeOAuthBindingInvariant", () => {
|
||||
it.each([undefined, "claude"])("enforces the fixed binding for native Claude (agent: %s)", acpxAgent => {
|
||||
const nextConfig = { provider: "acpx", ...(acpxAgent && { acpxAgent }), ...withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }) };
|
||||
expect(assertClaudeOAuthBindingInvariant({ adapterType: "paperclip_runner", nextConfig }))
|
||||
.toEqual({ introducesBinding: true, keepsBinding: false });
|
||||
expect(() => assertClaudeOAuthBindingInvariant({ adapterType: "paperclip_runner", nextConfig: {
|
||||
...nextConfig, env: { ...nextConfig.env, ANTHROPIC_API_KEY: "fixture-api-key" },
|
||||
} })).toThrow(CLAUDE_OAUTH_CREDENTIAL_CONFLICT);
|
||||
expect(assertClaudeOAuthBindingInvariant({ adapterType: "paperclip_runner", nextConfig: { ...nextConfig, acpxAgent: "codex" } }))
|
||||
.toEqual({ introducesBinding: false, keepsBinding: false });
|
||||
});
|
||||
it("reports that a create introduces the fixed binding", () => {
|
||||
const decision = assertClaudeOAuthBindingInvariant({
|
||||
adapterType: "claude_local",
|
||||
@@ -312,6 +322,7 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
role: "engineer",
|
||||
status: "idle" as const,
|
||||
adapterType: "claude_local",
|
||||
runner: "legacy" as const,
|
||||
defaultEnvironmentId: scope.environmentId,
|
||||
adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_BINDING }, ...extraEnv } },
|
||||
runtimeConfig: {},
|
||||
@@ -359,11 +370,13 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
it("inserts the fixed binding and consumes a valid claim exactly once", async () => {
|
||||
it.each([false, true])("inserts the fixed binding and consumes a valid claim exactly once (native: %s)", async native => {
|
||||
const scope = await seedScope();
|
||||
const sessionId = await seedStoredClaim(scope, Date.now() + 60_000);
|
||||
const legacyInput = createInput(scope);
|
||||
const input = native ? { ...legacyInput, runner: undefined, adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", ...legacyInput.adapterConfig } } : legacyInput;
|
||||
|
||||
const created = await createAgentLifecycle(db).requestHire(scope.companyId, createInput(scope), {
|
||||
const created = await createAgentLifecycle(db).requestHire(scope.companyId, input, {
|
||||
claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId },
|
||||
});
|
||||
|
||||
@@ -377,7 +390,7 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
|
||||
// A replay of the same claim inserts no second agent.
|
||||
await expect(
|
||||
createAgentLifecycle(db).requestHire(scope.companyId, createInput(scope), {
|
||||
createAgentLifecycle(db).requestHire(scope.companyId, input, {
|
||||
claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId },
|
||||
}),
|
||||
).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED });
|
||||
@@ -616,6 +629,7 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
role: "engineer",
|
||||
status: "idle",
|
||||
adapterType: "claude_local",
|
||||
runner: "legacy",
|
||||
defaultEnvironmentId: scope.environmentId,
|
||||
adapterConfig: { env: {} },
|
||||
runtimeConfig: {},
|
||||
@@ -680,6 +694,7 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
role: "engineer",
|
||||
status: "idle",
|
||||
adapterType: "claude_local",
|
||||
runner: "legacy",
|
||||
defaultEnvironmentId: scope.environmentId,
|
||||
adapterConfig: { env: {} },
|
||||
runtimeConfig: {},
|
||||
@@ -755,6 +770,7 @@ describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => {
|
||||
role: "engineer",
|
||||
status: "idle",
|
||||
adapterType: "claude_local",
|
||||
runner: "legacy",
|
||||
defaultEnvironmentId: scope.environmentId,
|
||||
adapterConfig: { env: {} },
|
||||
runtimeConfig: {},
|
||||
|
||||
@@ -48,6 +48,7 @@ afterEach(() => {
|
||||
|
||||
async function writeHelpWithoutEffortClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const argv = process.argv.slice(2);
|
||||
if (argv.includes("--help")) {
|
||||
process.stdout.write("Usage: claude [options]\\n --print\\n --model <id>\\n");
|
||||
@@ -306,6 +307,7 @@ describe("claude_local environment diagnostics", () => {
|
||||
adapterType: "claude_local",
|
||||
config: {
|
||||
engine: "cli",
|
||||
model: "claude-opus-5",
|
||||
command: "claude",
|
||||
},
|
||||
executionTarget: {
|
||||
@@ -379,6 +381,7 @@ describe("claude_local environment diagnostics", () => {
|
||||
await fs.writeFile(path.join(sourceConfigDir, "credentials.json"), JSON.stringify({ token: "local" }), "utf8");
|
||||
await fs.writeFile(path.join(remoteHome, ".claude", ".credentials.json"), JSON.stringify({ token: "remote" }), "utf8");
|
||||
await fs.writeFile(commandPath, `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || "";
|
||||
|
||||
@@ -20,6 +20,7 @@ async function writeFailingClaudeCommand(
|
||||
const payload = JSON.stringify(options.resultEvent);
|
||||
const exit = options.exitCode ?? 1;
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
console.log(${JSON.stringify(payload)});
|
||||
process.exit(${exit});
|
||||
`;
|
||||
@@ -33,6 +34,7 @@ async function writeTextFailingClaudeCommand(
|
||||
): Promise<void> {
|
||||
const exit = options.exitCode ?? 1;
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
if (${JSON.stringify(options.stdout ?? "")}) {
|
||||
process.stdout.write(${JSON.stringify(options.stdout ?? "")});
|
||||
}
|
||||
@@ -47,6 +49,7 @@ process.exit(${exit});
|
||||
|
||||
async function writeFakeClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -88,6 +91,7 @@ console.log(JSON.stringify({ type: "result", session_id: "11111111-1111-4111-811
|
||||
|
||||
async function writeHelpWithoutEffortClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -126,6 +130,7 @@ console.log(JSON.stringify({ type: "result", session_id: "33333333-3333-4333-833
|
||||
|
||||
async function writeHelpWithEffortClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -185,6 +190,7 @@ afterEach(() => {
|
||||
|
||||
async function writePoisonedMessageIdClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
|
||||
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
||||
@@ -221,6 +227,7 @@ console.log(JSON.stringify({ type: "result", session_id: "bbbbbbbb-bbbb-4bbb-8bb
|
||||
|
||||
async function writeAlwaysPoisonedMessageIdClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
|
||||
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
||||
@@ -251,6 +258,7 @@ process.exit(1);
|
||||
|
||||
async function writeRetryThenSucceedClaudeCommand(commandPath: string): Promise<void> {
|
||||
const script = `#!/usr/bin/env node
|
||||
if (process.argv.includes("--version")) { console.log("2.1.286 (Claude Code)"); process.exit(0); }
|
||||
const fs = require("node:fs");
|
||||
|
||||
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
||||
@@ -410,9 +418,9 @@ describe("claude execute", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, "claude-opus-5"],
|
||||
["", "claude-opus-5"],
|
||||
[" ", "claude-opus-5"],
|
||||
[undefined, "claude-opus-5-5"],
|
||||
["", "claude-opus-5-5"],
|
||||
[" ", "claude-opus-5-5"],
|
||||
["claude-sonnet-4-5", "claude-sonnet-4-5"],
|
||||
])("passes the resolved model to the CLI for %j", async (model, expected) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-default-"));
|
||||
|
||||
@@ -6156,7 +6156,7 @@ describe("company portability", () => {
|
||||
await expect(portability.importBundle(input, "user-1")).rejects.toMatchObject({ status: 422, details: { code: "paperclip_runner_dot_disabled" } });
|
||||
expect(agentSvc.create).not.toHaveBeenCalled();
|
||||
instanceSettingsSvc.getExperimental.mockResolvedValue({ enableNativeRunner: false, enableOpenAiDot: true });
|
||||
await expect(portability.importBundle({ ...input, adapterOverrides: { claudecoder: { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-4-6" } } } }, "user-1"))
|
||||
await expect(portability.importBundle({ ...input, adapterOverrides: { claudecoder: { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "grok", model: "grok-build-1.0.13" } } } }, "user-1"))
|
||||
.rejects.toMatchObject({ status: 422, details: { code: "paperclip_runner_rollout_disabled" } });
|
||||
expect(agentSvc.create).not.toHaveBeenCalled();
|
||||
await portability.importBundle(input, "user-1");
|
||||
@@ -6190,7 +6190,7 @@ describe("company portability", () => {
|
||||
adapterOverrides: {
|
||||
claudecoder: {
|
||||
adapterType: "paperclip_runner",
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-4-6" },
|
||||
adapterConfig: { provider: "opencode", model: "openrouter/deepseek/deepseek-v4-flash-0731" },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6222,7 +6222,7 @@ describe("company portability", () => {
|
||||
await portability.importBundle(request, "user-1");
|
||||
expect(agentSvc.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
|
||||
adapterType: "paperclip_runner",
|
||||
adapterConfig: expect.objectContaining({ provider: "acpx", acpxAgent: "claude" }),
|
||||
adapterConfig: expect.objectContaining({ provider: "opencode" }),
|
||||
}), { createdByUserId: "user-1" });
|
||||
|
||||
await portability.importBundle({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { agents, type Db } from "@paperclipai/db";
|
||||
import { agentHarnessType } from "@paperclipai/shared";
|
||||
import { collectSecretRefs, collectUserSecretRefs, syncAgentAdapterEnvBindings } from "./agent-secret-bindings.js";
|
||||
import { logActivity } from "./activity-log.js";
|
||||
import { claudeOAuthBindingsMatchExactly, claudeOAuthClaimRejectedError,
|
||||
@@ -166,7 +167,7 @@ export function agentCredentialService(db: Db) {
|
||||
if (
|
||||
!parent ||
|
||||
parent.companyId !== input.companyId ||
|
||||
parent.adapterType !== CLAUDE_LOCAL_ADAPTER_TYPE ||
|
||||
agentHarnessType(parent.adapterType, parent.adapterConfig as Record<string, unknown>) !== CLAUDE_LOCAL_ADAPTER_TYPE ||
|
||||
!claudeOAuthBindingsMatchExactly(parentBinding, childBinding)
|
||||
) {
|
||||
throw claudeOAuthClaimRejectedError();
|
||||
|
||||
@@ -219,6 +219,15 @@ describe("Claude runner defaults", () => {
|
||||
expect(resolveNewAgentRunner(legacy)).toEqual({ adapterType: legacy.adapterType, adapterConfig: legacy.adapterConfig });
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { model: "custom" } }).adapterConfig.model).toBe("custom");
|
||||
});
|
||||
it.each(["permissionMode", "acpPermissionMode"])("translates the legacy Claude %s without granting broader permissions", key => {
|
||||
for (const mode of ["deny-all", "approve-reads", "approve-all"]) {
|
||||
const native = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { engine: "acp", [key]: mode } });
|
||||
expect(native.adapterConfig.acpxPermissionMode).toBe(mode);
|
||||
expect(native.adapterConfig[key]).toBeUndefined();
|
||||
}
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "deny-all", acpxPermissionMode: "approve-all" } })).toThrow("cannot honor");
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "unrecognized" } })).toThrow("Legacy runner");
|
||||
});
|
||||
it("retains saved native Claude and allows an explicit compatible Legacy switch", () => {
|
||||
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "custom", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" } };
|
||||
expect(resolveNewAgentRunner(saved)).toEqual(saved);
|
||||
|
||||
@@ -41,16 +41,27 @@ function incompatible(fields: string[]): never {
|
||||
}
|
||||
|
||||
function resolveClaudeRunnerConfig(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const unsupported = claudeRunnerUnsupportedSettings(config);
|
||||
const translated = { ...config };
|
||||
const legacyPermission = [config.permissionMode, config.acpPermissionMode].find(value => typeof value === "string" && value.trim()) as string | undefined;
|
||||
if (legacyPermission !== undefined) {
|
||||
const mode = legacyPermission.trim();
|
||||
if (!["approve-all", "approve-reads", "deny-all"].includes(mode)
|
||||
|| (config.acpxPermissionMode !== undefined && config.acpxPermissionMode !== mode)) incompatible(["permissionMode", "acpPermissionMode"]);
|
||||
translated.acpxPermissionMode = mode;
|
||||
}
|
||||
for (const key of ["permissionMode", "acpPermissionMode"]) {
|
||||
if (config[key] !== undefined && typeof config[key] !== "string") incompatible([key]);
|
||||
delete translated[key];
|
||||
}
|
||||
const unsupported = claudeRunnerUnsupportedSettings(translated);
|
||||
const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort;
|
||||
if (config.model !== undefined && typeof config.model !== "string") unsupported.push("model");
|
||||
if (unsupported.length) incompatible(unsupported);
|
||||
const translated = { ...config };
|
||||
for (const key of ["engine", "command", "mode", "nonInteractivePermissions", "dangerouslySkipPermissions", "chrome", "maxTurnsPerRun", "effort", "reasoningEffort"]) delete translated[key];
|
||||
for (const key of ["engine", "command", "mode", "acpMode", "nonInteractivePermissions", "acpNonInteractivePermissions", "dangerouslySkipPermissions", "chrome", "maxTurnsPerRun", "effort", "reasoningEffort"]) delete translated[key];
|
||||
const env = config.env && typeof config.env === "object" ? config.env as Record<string, unknown> : {};
|
||||
const model = resolveClaudeModel(config.model, env);
|
||||
if (!model) incompatible(["model"]);
|
||||
return { ...translated, ...(effort === undefined || effort === "" ? {} : { modelReasoningEffort: effort }), provider: "acpx", acpxAgent: "claude", model, acpxPermissionMode: config.acpxPermissionMode ?? "approve-all", lifecycleMode: config.lifecycleMode ?? "per_turn" };
|
||||
return { ...translated, ...(effort === undefined || effort === "" ? {} : { modelReasoningEffort: effort }), provider: "acpx", acpxAgent: "claude", model, acpxPermissionMode: translated.acpxPermissionMode ?? "approve-all", lifecycleMode: config.lifecycleMode ?? "per_turn" };
|
||||
}
|
||||
|
||||
/** Idempotent translation. Missing dependencies or credentials never select another runner. */
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("complete Anthropic receipt pricing", () => {
|
||||
});
|
||||
it.each([
|
||||
{ complete: false }, { billingType: "unknown" }, { billingType: "subscription_included" },
|
||||
{ provider: "unknown" }, { biller: "aws_bedrock" }, { model: "claude-sonnet-latest" },
|
||||
{ provider: "unknown" }, { biller: "aws_bedrock" }, { model: "claude-sonnet-latest" }, { model: "constructor" }, { model: "toString" }, { model: "__proto__" },
|
||||
{ usageBasis: "session_cumulative" }, { costUsd: 0.12 }, { costUsdExact: "0.12" }, { cacheAdjustedCostUsd: 0.12 },
|
||||
{ pricingContext: { serviceTier: "priority" } }, { usageByModel: [{ model: "other", usage: { inputTokens: 1, outputTokens: 1 }, costUsd: 1 }] },
|
||||
{ usage: undefined }, { usage: { inputTokens: 1, outputTokens: 1 } },
|
||||
|
||||
@@ -13,7 +13,7 @@ const RATE_CARDS: Record<string, { rates: readonly string[]; version: string }>
|
||||
* claim. ACP aggregates cache writes without TTL; conservatively price them at
|
||||
* the documented one-hour rate rather than silently assume five-minute writes. */
|
||||
export function priceAnthropicReceipt(receipt: AdapterUsageCheckpoint): AdapterUsageCheckpoint {
|
||||
const card = receipt.model ? RATE_CARDS[receipt.model] : undefined;
|
||||
const card = receipt.model && Object.hasOwn(RATE_CARDS, receipt.model) ? RATE_CARDS[receipt.model] : undefined;
|
||||
if (!receipt.complete || !receipt.usage || receipt.usageBasis !== "per_run"
|
||||
|| receipt.provider !== "anthropic" || receipt.biller !== "anthropic"
|
||||
|| !["api", "metered_api"].includes(receipt.billingType ?? "")
|
||||
|
||||
@@ -355,12 +355,14 @@ export function assertAgentCoreProfileRecoveryBinding(input: {
|
||||
}
|
||||
|
||||
export function claudeRunnerUnsupportedSettings(config: Record<string, unknown>): string[] {
|
||||
const unsupported = ["chrome", "agentCommand", "stateDir", "warmHandleIdleMs", "extraArgs", "args", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "maxTurnsPerRun"]
|
||||
const unsupported = ["chrome", "agentCommand", "acpAgentCommand", "stateDir", "acpStateDir", "warmHandleIdleMs", "acpWarmHandleIdleMs", "extraArgs", "args", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "maxTurnsPerRun", "permissionMode", "acpPermissionMode"]
|
||||
.filter(key => config[key] !== undefined && config[key] !== false && config[key] !== "" && config[key] !== 0 && !(Array.isArray(config[key]) && config[key].length === 0));
|
||||
if (config.command && config.command !== "claude") unsupported.push("command");
|
||||
if (config.mode && config.mode !== "persistent") unsupported.push("mode");
|
||||
if (config.acpMode && config.acpMode !== "persistent") unsupported.push("acpMode");
|
||||
if (config.engine && !["auto", "cli", "acp"].includes(String(config.engine))) unsupported.push("engine");
|
||||
if (config.nonInteractivePermissions && config.nonInteractivePermissions !== "deny") unsupported.push("nonInteractivePermissions");
|
||||
if (config.acpNonInteractivePermissions && config.acpNonInteractivePermissions !== "deny") unsupported.push("acpNonInteractivePermissions");
|
||||
if (config.dangerouslySkipPermissions === false) unsupported.push("dangerouslySkipPermissions");
|
||||
return unsupported;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
SecretVersionSelector,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
agentHarnessType,
|
||||
CLASS3_STATIC_LEASE_ALLOWLIST,
|
||||
createSecretProviderConfigSchema,
|
||||
deriveProjectUrlKey,
|
||||
@@ -279,7 +280,7 @@ export interface ClaudeOAuthBindingInvariantDecision {
|
||||
export function assertClaudeOAuthBindingInvariant(
|
||||
input: ClaudeOAuthBindingInvariantInput,
|
||||
): ClaudeOAuthBindingInvariantDecision {
|
||||
const isClaudeLocal = input.adapterType === CLAUDE_LOCAL_ADAPTER_TYPE;
|
||||
const isClaudeLocal = agentHarnessType(input.adapterType ?? "", asRecord(input.nextConfig) ?? {}) === CLAUDE_LOCAL_ADAPTER_TYPE;
|
||||
const nextIsFixed = hasFixedClaudeOAuthBinding(input.nextConfig);
|
||||
const priorIsFixed = hasFixedClaudeOAuthBinding(input.priorConfig);
|
||||
|
||||
|
||||
@@ -1430,7 +1430,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
|
||||
groups: ["onboarding"],
|
||||
profiles: runnerProfiles.filter(profile => ["legacy-codex", "legacy-claude", "runner-codex", "runner-acpx-claude"].includes(profile.id)),
|
||||
environments: [localEnvironment], tasks: firstTaskTasks, expectedMatrixSize: 52,
|
||||
definitionMetadata: { version: 6, runGrading: "evidenced-nonexecution-and-refusal", credentialPersistenceCheck: false, questionChoiceMinimum: 2, nativeSetup: "qualified-platform-wizard-saved-default", productionInstructions: true, qualityGrading: "informational" },
|
||||
definitionMetadata: { version: 7, runGrading: "evidenced-nonexecution-and-refusal", credentialPersistenceCheck: false, questionChoiceMinimum: 2, nativeSetup: "qualified-platform-wizard-saved-default", claudeDefaultTarget: "darwin-arm64", legacySetup: "explicit-advanced-runner-choice", productionInstructions: true, qualityGrading: "informational" },
|
||||
},
|
||||
{
|
||||
id: "agent-chat", label: "Persistent Agent Chat",
|
||||
|
||||
@@ -204,7 +204,7 @@ export async function setupFirstTaskFixtures(input: {
|
||||
exact: true,
|
||||
});
|
||||
const savedKey = page.getByRole("combobox", { name: "Saved API key" });
|
||||
if (execution.profile.id === "legacy-codex") {
|
||||
if (["legacy-codex", "legacy-claude"].includes(execution.profile.id)) {
|
||||
await page.getByText("Advanced", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Runner", exact: true }).click();
|
||||
await page.getByRole("option", { name: "Legacy runner", exact: true }).click();
|
||||
|
||||
@@ -189,8 +189,8 @@ describe("Pi controls catalog admission", () => {
|
||||
// Extended v3 states the strict file oracle's task-wide Bash limit.
|
||||
const pi = runnerMatrix.find(c => c.profile.qualificationCandidate === "pi")!.profile;
|
||||
expect(pi.modelQualification?.qualificationId).toBe("pi:0.0.33:1.0.0:openrouter");
|
||||
expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 22, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" });
|
||||
expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 22,
|
||||
expect(runnerSuites.find(s => s.id === "pi-native")!.definitionMetadata).toMatchObject({ version: 23, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", agentMemoryParent: "public-managed-file-seed-before-admission", incompleteTerminalCleanup: "retirement-retained-with-failed-watch", profileVersion: 23, agentMemoryContent: "utf8-nonce-plus-final-lf", agentMemoryPrompt: "single-json-write-and-content-bound-native-read-both-runs", agentMemoryReadAuthority: "local-withheld-or-exact-remote-agent-run-file", taskPromptTransport: "fenced-markdown-paste-and-multiline-literal-escapes", nativeFinish: "current-contract-objective-evidence-refs", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7" });
|
||||
expect(runnerSuites.find(s => s.id === "pi-controls")!.definitionMetadata).toMatchObject({ version: 10, remoteProcExit: "separately-confirmed-absence-after-read-failure", taskCreation: "explicit-title-and-creation-response-id", profileVersion: 23,
|
||||
remoteProcessIdentity: "observer-pid-startTicks-bootId",
|
||||
controlPlaneSettlement: "required-scoped-result-and-terminal-after-runner" });
|
||||
expect(runnerSuites.find(s => s.id === "extended-harnesses")!.definitionMetadata).toMatchObject({ version: 5, piFileArtifactTitle: "exact-filename", piFileCommandTransport: "fenced-bash-markdown-paste" });
|
||||
@@ -200,7 +200,7 @@ describe("Pi controls catalog admission", () => {
|
||||
}
|
||||
const hashes = Object.fromEntries(runnerSuites.filter(s => ["pi-native", "native-active-stop", "extended-harnesses", "rich-acp-warm-continuity"].includes(s.id)).map(s => [s.id, suiteDefinitionHash(s)]));
|
||||
expect(hashes).toEqual({
|
||||
"pi-native": "f88d1648fe02ac07b9f43cd5d747131d98844add29fda73746bb1b2bceec0df9",
|
||||
"pi-native": "3c77ced7edfb99aa12052f451398da7580466a54abadcaf287d31ee12c16382d",
|
||||
"native-active-stop": "2d4fdeeb75bd531b309bd1b35cfa5680ceefdeee6b7155b068dd011ce9901980",
|
||||
"rich-acp-warm-continuity": "331b88d9538a132670789fb7864df7df5f4bf294ae19b62f53d8a02f901774f0",
|
||||
"extended-harnesses": "5de4fac78d4d581bc0954f07b5cf2df2862d37f8b0205325eede6d9ef6d9f5e6",
|
||||
|
||||
@@ -561,6 +561,9 @@ for (const execution of executions) {
|
||||
page,
|
||||
request,
|
||||
}, testInfo) => {
|
||||
test.skip(execution.suite.id === "first-task" && execution.profile.id === "runner-acpx-claude"
|
||||
&& (process.platform !== "darwin" || process.arch !== "arm64"),
|
||||
"Ordinary native Claude onboarding is qualified on Apple Silicon only in this delivery.");
|
||||
test.setTimeout(
|
||||
execution.task.flow === "warm_three_turn"
|
||||
? deadlineMs
|
||||
|
||||
@@ -1901,7 +1901,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
defaultRunner={runnerAvailability?.defaultRunner}
|
||||
supportedRunners={runnerAvailability?.supportedRunners}
|
||||
onChange={next => isCreate ? set!({ runner: next }) : setOverlay(prev => ({ ...prev, runner: next }))} />}
|
||||
{isLocal && fieldsAdapterType !== "paperclip_runner" && (<> {/*
|
||||
{isLocal && (!isRunnerHarness || fieldsAdapterType !== "paperclip_runner") && (<> {/*
|
||||
The command names a binary on the execution host, so the
|
||||
managed-sandbox-only policy hides it: the platform-managed image
|
||||
owns the binary. Hiding is presentation only. A stored
|
||||
|
||||
@@ -107,7 +107,7 @@ it("offers one Codex harness without a runner/provider tile", async () => {
|
||||
expect(document.querySelectorAll('input[value="codex_local"]')).toHaveLength(1);
|
||||
expect(document.querySelector("select")).toBeNull();
|
||||
});
|
||||
it.each(["claude", "grok", "opencode"])("preserves explicit experimental %s creation in Advanced", async provider => {
|
||||
it.each(["grok", "opencode"])("preserves explicit experimental %s creation in Advanced", async provider => {
|
||||
await name();
|
||||
await act(async () => (document.querySelector("summary") as HTMLElement).click());
|
||||
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Experimental harness"]')!.click());
|
||||
|
||||
@@ -1269,6 +1269,39 @@ describe("CompanyImport", () => {
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig);
|
||||
});
|
||||
|
||||
it.each(["deny-all", "approve-reads"])("preserves native Claude settings and saves an edited permission from %s", async permission => {
|
||||
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
|
||||
mockAdaptersApi.list.mockResolvedValue([
|
||||
{ type: "claude_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" },
|
||||
{ type: "paperclip_runner", disabled: false },
|
||||
]);
|
||||
const preview = buildPreviewResult();
|
||||
const source = { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", modelReasoningEffort: "high", acpxPermissionMode: permission, timeoutSec: 180, lifecycleMode: "per_turn" };
|
||||
preview.manifest.agents[0]!.adapterType = "paperclip_runner";
|
||||
preview.manifest.agents[0]!.runner = "paperclip";
|
||||
preview.manifest.agents[0]!.adapterConfig = source;
|
||||
mockCompaniesApi.importPreview.mockResolvedValue(preview);
|
||||
await renderPage();
|
||||
await enterGithubUrl();
|
||||
await clickButton(text => text === "Preview import");
|
||||
await clickButton(text => text.includes("configure adapter"));
|
||||
const select = container.querySelector<HTMLButtonElement>('[aria-label="Permission mode"]');
|
||||
expect(select?.textContent).toContain(permission === "deny-all" ? "Deny all" : "Allow Paperclip reads");
|
||||
await act(async () => select!.click());
|
||||
const nextPermission = permission === "deny-all" ? "approve-reads" : "deny-all";
|
||||
const option = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find(element => element.textContent === (nextPermission === "deny-all" ? "Deny all" : "Allow Paperclip reads"));
|
||||
expect(option).toBeTruthy();
|
||||
await act(async () => option!.click());
|
||||
await flushReact();
|
||||
await clickButton(text => text.startsWith("Import 3 file"));
|
||||
await settle();
|
||||
expect(lastImportMeta().adapterOverrides?.coder).toMatchObject({
|
||||
runner: "paperclip",
|
||||
adapterConfig: { ...source, acpxPermissionMode: nextPermission },
|
||||
});
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(source);
|
||||
});
|
||||
|
||||
it("does not reinterpret an unknown native provider as Codex when its configuration is edited", async () => {
|
||||
mockAdaptersApi.list.mockResolvedValue([{ type: "paperclip_runner", disabled: false }]);
|
||||
const preview = buildPreviewResult();
|
||||
|
||||
@@ -1735,7 +1735,9 @@ export function CompanyImport() {
|
||||
if (selectedType === "openai_dot" && configVals.adapterSchemaValues?.allowUnmeteredProvider !== true) {
|
||||
throw new Error("OpenAI Dot requires acknowledgement of external billing. Configure the Dot adapter and enable Allow externally billed provider.");
|
||||
}
|
||||
override.adapterConfig = getUIAdapter(configVals.adapterType).buildAdapterConfig(configVals);
|
||||
const retainsNativeHarness = agent.adapterType === "paperclip_runner"
|
||||
&& selectedType === agent.adapterChoice && ["codex_local", "claude_local"].includes(selectedType);
|
||||
override.adapterConfig = getUIAdapter(retainsNativeHarness ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig(configVals);
|
||||
}
|
||||
overrides[agent.slug] = override;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user