Merge remote-tracking branch 'origin/codex/codex-runner-default' into codex/claude-runner-protocol

* origin/codex/codex-runner-default:
  docs: record final environment metadata review closure
  fix(environments): preserve measured proof atomically on metadata saves
  test(agents): distinguish measured and unknown sandbox defaults
  test(e2e): qualify managed Codex images in existing fixtures
  fix(agents): preserve legacy policy and require measured sandbox defaults
  fix(environments): bind platform facts to the effective immutable image
  fix(runner): close review gaps in setup and platform qualification
This commit is contained in:
Dotta committed 2026-10-12 16:48:56 -05:00
commit e23dd02651
33 files changed
+1104 -103

No files matched your search

+31 -4
View File
@@ -544,14 +544,41 @@ jobs:
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
node --input-type=module <<'NODE'
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { appendFileSync } from 'node:fs';
import { RELEASE_RUNNER_TARGETS } from './packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs';
const source = process.env.SOURCE_SHA;
const readBlob = file => {
const entries = execFileSync('git', ['ls-tree', '-z', source, '--', file], {
encoding: 'utf8', maxBuffer: 4096,
}).split('\0').filter(Boolean);
if (!entries.length) return undefined;
assert.equal(entries.length, 1, 'Ambiguous exact-source target contract');
assert.ok(entries[0].startsWith('100644 blob ') && entries[0].endsWith('\t' + file), 'Target contract must be a regular source file');
return execFileSync('git', ['show', source + ':' + file], { encoding: 'utf8', maxBuffer: 64 * 1024 });
};
const data = readBlob('packages/paperclip-runner/release-runner-targets.json');
let declared;
if (data !== undefined) {
const contract = JSON.parse(data);
assert.ok(contract && contract.schema === 'paperclip.runner.release-targets.v1'
&& Object.keys(contract).sort().join() === 'schema,targets', 'Unknown exact-source daemon platform contract');
declared = contract.targets;
} else {
// Frozen pre-contract sources used a literal export. Read that one
// declaration as JSON data; never import or evaluate preview code.
const legacy = readBlob('packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
const declarations = [...(legacy ?? '').matchAll(/^[ \t]*export const RELEASE_RUNNER_TARGETS = (?:Object\.freeze\()?(\[[^\r\n]*\])\)?;[ \t]*$/gm)];
assert.equal(declarations.length, 1, 'Unknown exact-source daemon platform contract');
declared = JSON.parse(declarations[0][1]);
}
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
const current = [...historical, 'linux-arm64'];
const targets = [...RELEASE_RUNNER_TARGETS].sort();
assert.ok(JSON.stringify(targets) === JSON.stringify(historical.sort()) || JSON.stringify(targets) === JSON.stringify(current.sort()), 'Unknown exact-source daemon platform contract');
assert.ok(Array.isArray(declared), 'Unknown exact-source daemon platform contract');
const matches = expected => JSON.stringify([...declared].sort()) === JSON.stringify([...expected].sort());
assert.ok(matches(historical) || matches(current), 'Unknown exact-source daemon platform contract');
const targets = matches(current) ? current : historical;
const hosts = { 'linux-x64': targets.length === current.length ? 'ubuntu-24.04' : 'ubuntu-latest', 'linux-arm64': 'ubuntu-24.04-arm', 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' };
const include = RELEASE_RUNNER_TARGETS.map(target => ({ target, host: hosts[target] }));
const include = targets.map(target => ({ target, host: hosts[target] }));
appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\nnative_consumers=${JSON.stringify({ include: include.filter(row => row.target !== 'linux-x64') })}\n`);
NODE
@@ -2,6 +2,140 @@
Updated 2026-10-12.
## Metadata review closure checkpoint
Runtime prerequisite source is `cae179c21412048d08aeb94e0fe8b0f7844119de`,
with 55 changed files; Codex remains 99 files. Two fresh metadata review P2s
are fixed: explicit null clears user metadata, and saves preserve only the
latest private platform proof through an atomic database update. Config/driver
updates invalidate proof in the same write; status cleanup retains its existing
archive-token behavior. Public callers cannot supply the proof.
The three existing environment suites pass 83 controls. Seven focused tests
exercise real PostgreSQL writes between request preparation and save; there are
42 name-filter exclusions and zero host-guard skips. Both canonical server
no-emit commands pass. Source hashes match in both worktrees and independent
review is clear. A separate read-only audit confirms no actual company/team
import bypass or proof export: portability does not copy environment rows, and
environment API responses strip the private subtree. No new infrastructure,
provider turn, Docker/Rust build or schema/API change was introduced.
Fresh exact-head full CI/review remain required. The verified Mac preview restart
at `64d0746f8a` preserved four execution configurations and two historical native
runs with the deprecated flag false and no provider actions. Refresh the idle
preview to the next recorded head; old measurements retain their literal source.
Merge the qualified prerequisite first, then reconcile Codex with actual master
before the final remote image and managed acceptance. No merge has occurred.
## Fixture closure checkpoint
The runtime prerequisite advances to `093b97aa12` with 54 files. The Codex
review remains 99 files. Existing managed fixtures now run the saved environment
probe before native-generation Codex creation and inspect server defaults only
for automatic requests. The owned setup probe lease joins the existing runtime
estimator, with unrelated leases excluded from task attribution. The fixture
contract records one setup lease per eligible cell, zero provider turns, zero
retries, a 330-second HTTP deadline and a 10-second platform command deadline.
Archive/release, expiry and failure-cost holds remain unchanged.
Both worktrees pass 36 existing fixture/billing controls and canonical E2E
no-emit. Existing catalog/report controls pass 46 cases, and core compatibility
discovery succeeds. Affected adapter discovery/setup routes pass 69 controls.
Unknown sandbox facts now expect Legacy; measured GNU2.39 ARM64 expects native.
All checks use fixtures, not paid provider proof. Exact-head hosted CI/review and
real managed onboarding/task/follow-up remain pending. No merge has occurred.
## Corrective source checkpoint: ready for fresh CI and review
Runtime prerequisite source is `cf19e086ae4a0142c8b4314771c55f73b294805b`
on `codex/runner-native-platform-prerequisites`, with 50 changed files. The
Codex-default branch `codex/codex-runner-default` is composed on it and retains
99 changed files. Both reviews remain under 100 files. All four original review
findings and the relevant independent-review follow-ups are fixed.
Existing controls pass: release workflow 70, assembly 7, registration 15,
measured environment/probe routes 32, UI permission/forms 207, and selection/import
140. Counts describe separate runs and can overlap. Canonical server, runner and
UI no-emit checks and token gates pass. Source-matching independent reviews
confirm cleanup, immutable-image binding and saved Legacy permission handling.
No local Docker/Rust build, new provider turn, or agent migration was used.
Remaining gates are fresh exact-head full CI/review and managed Codex acceptance.
The existing managed eval catalog already requires OCI digests. Its image-backed
fixture helper must use the existing explicit environment probe before automatic
Codex creation and inspect the stored default; explicit-runner coverage remains
separate. First-task onboarding fixtures are local-only, so fresh managed
onboarding still requires live proof. The pinned fleet tenant currently uses a
name-only snapshot and cannot establish native-default compatibility. Use a
qualified immutable image-backed QA environment, preserve the tenant pin, and
verify the serving commit and independently checked task/follow-up artifacts.
Merge the prerequisite after its applicable checks pass. Then reconcile the
Codex PR with actual master, verify its final source, and build the immutable
preview and DB/shared pair remotely once. All three cancelled previews remain
excluded. The original budget and cleanup holds remain unchanged. Claude owns
its separate bounded proof and downstream attestation reconciliation. No merge
or production deployment has occurred at this checkpoint.
## Final review checkpoint: shared fixes and sandbox qualification
The user authorizes normal sequential merges after the applicable gates pass.
Runtime prerequisite [#16028](https://github.com/paperclipai/paperclip/pull/16028)
and Codex defaults [#15683](https://github.com/paperclipai/paperclip/pull/15683)
remain draft. Their pre-fix sources are `00ac509b0a` and `8de43be4cd`.
The shared SSH argv and verified cold-resume corrections are already composed.
Six argv controls, fourteen recovery controls, and actual SSH subprocess checks
pass. The separate Claude owner reports real ARM and Linux x64 task/follow-up
pairs across controller restarts on its literal installed-package overlays;
those results do not qualify an unmodified final Codex package.
The Mac test-drive serves `8de43be4cd` at port 3125. A read-only restart comparison
preserves four full legacy/native execution configurations and two historical
native runs with the deprecated native flag false. Zero provider turns and
zero local native/Docker builds were used. The canonical server no-emit passes
on the composed root and actual prerequisite worktree.
Both shared fixes are implemented and frozen. The actual workflow selector
rejects candidate execution/output injection in real Git fixtures. Its full
existing release-workflow suite passes 70 tests; existing target assembly passes
7 tests. Registration passes 15 focused controls and canonical runner no-emit.
An independent scoped review confirms bounded teardown and retained private
state on unconfirmed release. The first broader local workflow attempt failed
only because mock loopback listeners were sandbox-blocked; its loopback-enabled
rerun passes. No daemon/provider execution was used by these controls.
The generic sandbox qualification support passes 30 existing probe and route
controls plus the canonical server no-emit command. It accepts only OCI digest
images, checks the returned cleanup receipt, and reports conditional-update races
without claiming a record. Mutable or name-only configurations retain existing
connection behavior and receive no native compatibility credit. The default
consumer and explicit-permission/import controls are being composed next.
Fresh review found four source defects: candidate-code execution during CI runner
selection, unbounded registration teardown, a lost explicit Codex permission
setting on automatic Legacy selection, and missing sandbox platform/libc facts.
Existing subagents own narrow fixes in parallel. Use inert target data and
workflow-owned public labels; preserve primary failures and private state on
unconfirmed cleanup; preserve explicit permissions; and use server-measured
sandbox qualification bound to its effective image/config and company scope.
The existing explicit environment-probe/setup flow owns qualification. Saving
and discovery must not provision resources or accept client-authored evidence.
Unknown targets retain automatic Legacy with an actionable explicit-native error.
No new product API or database migration is planned.
Preview runs [38303434211](https://github.com/paperclipai/paperclip/actions/runs/38303434211),
[38304889308](https://github.com/paperclipai/paperclip/actions/runs/38304889308), and
[38306885592](https://github.com/paperclipai/paperclip/actions/runs/38306885592)
were cancelled as shared source defects changed their candidates. They are
excluded from final qualification. Preserve historical evidence and all cost
and cleanup holds. The next action is to finish the four fixes, run affected
controls, obtain fresh exact-source checks/review, then build the final immutable
preview and matching DB/shared pair remotely for the pinned QA tenant.
Managed Codex retains its $2.50 setup/task envelope and $2 cleanup within the
original $250 ceiling. Claude retains its separate $50 ceiling. No merge or
production deployment has occurred.
## Latest checkpoint: runtime prerequisite and authorized merges
The user authorizes preparing and merging ready PRs in dependency order.
+2
View File
@@ -727,6 +727,8 @@ export interface CreateConfigValues {
runner?: "auto" | "paperclip" | "legacy";
adapterType: string;
codexPermissionMode?: "never" | "on-request" | "untrusted";
/** Explicit Legacy permission choice; never persisted as a runner setting. */
codexLegacySandboxBypass?: boolean;
paperclipRunnerLifecycleMode?: "per_turn" | "warm";
paperclipRunnerIdleTimeoutMs?: number;
cwd: string;
+10
View File
@@ -203,6 +203,16 @@ Apple Silicon and Intel hosts. Actual task and follow-up acceptance is performed
on Apple Silicon; Intel qualification covers build, installation, and protocol
startup.
For sandbox environments, the existing explicit environment probe can measure
OS, architecture and libc from its allocated lease. It records those facts only
for an image pinned with `@sha256`, after confirmed lease cleanup. The record is
server-owned and bound to the company, effective provider/image configuration
and active custom-image identity. Creation and discovery read this record without
allocating a sandbox. Configuration or image changes invalidate it. Mutable tags,
provider defaults and name-only snapshots do not establish compatibility; they
keep automatic Legacy selection until there is verifiable immutable image evidence.
A connection-only success does not establish native readiness.
On Linux, Codex's sandboxed filesystem modes also require the host to permit its
unprivileged namespaces. A no-privilege Docker container can prohibit nested
namespace creation even when installation and binary compatibility pass.
@@ -0,0 +1,4 @@
{
"schema": "paperclip.runner.release-targets.v1",
"targets": ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]
}
@@ -9,7 +9,19 @@ import { fileURLToPath } from "node:url";
import { runnerBinaryTarget } from "../src/live/runner-binary.ts";
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const RELEASE_RUNNER_TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]);
export function releaseRunnerTargets(contract) {
const supported = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"];
if (!contract || contract.schema !== "paperclip.runner.release-targets.v1"
|| Object.keys(contract).sort().join() !== "schema,targets"
|| !Array.isArray(contract.targets)
|| JSON.stringify([...contract.targets].sort()) !== JSON.stringify([...supported].sort())) {
throw Error("Unknown release daemon target contract");
}
return Object.freeze([...contract.targets]);
}
export const RELEASE_RUNNER_TARGETS = releaseRunnerTargets(JSON.parse(
readFileSync(new URL("../release-runner-targets.json", import.meta.url), "utf8"),
));
const required = RELEASE_RUNNER_TARGETS;
const digest = bytes => "sha256:" + createHash("sha256").update(bytes).digest("hex");
function regularFile(path, maximumSize) {
@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { stageBuiltRunnerBinary, stageRunnerBinary } from "./stage-runner-binary.mjs";
import { RELEASE_RUNNER_TARGETS, releaseDaemonManifest, verifyReleaseRunnerMetadata } from "./stage-release-runner-binaries.mjs";
import { RELEASE_RUNNER_TARGETS, releaseRunnerTargets, releaseDaemonManifest, verifyReleaseRunnerMetadata } from "./stage-release-runner-binaries.mjs";
import { paperclipRunnerBinaryNeedsBuild, resolveNativeRunnerDevBinary } from "../../../scripts/dev-runner-native-binary.mjs";
import { resolvePackagedRunnerBinary } from "../src/live/runner-binary.ts";
@@ -16,6 +16,18 @@ const contract = { package: { name: "@paperclipai/paperclip-runner", version: "0
const metadata = { binaryContractVersion: 2, nativeExecutionVersion: 1, harnessDriverVersion: 1,
packageName: "@paperclipai/paperclip-runner", packageVersion: "0.0.0", prp: { name: "paperclip.runner", minimumVersion: 1, maximumVersion: 2 } };
test("release assembly uses the inert four-target contract and rejects unexpected selection data", async () => {
const contract = JSON.parse(await readFile(new URL("../release-runner-targets.json", import.meta.url), "utf8"));
assert.deepEqual(RELEASE_RUNNER_TARGETS, ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]);
assert.deepEqual(releaseRunnerTargets(contract), RELEASE_RUNNER_TARGETS);
assert.ok(Object.isFrozen(RELEASE_RUNNER_TARGETS));
for (const invalid of [null, {}, { ...contract, schema: "other" }, { ...contract, host: "trusted-fleet" },
{ ...contract, targets: contract.targets.slice(0, 3) }, { ...contract, targets: [...contract.targets, "linux-arm64"] },
{ ...contract, targets: [...contract.targets.slice(0, 3), "win32-x64"] }, { ...contract, targets: "linux-arm64" }]) {
assert.throws(() => releaseRunnerTargets(invalid), /Unknown release daemon target contract/);
}
});
test("release assembly binds all four actual targets, source, hashes and compatible metadata", async () => {
const root = await realpath(await mkdtemp(path.join(tmpdir(), "runner-release-inputs-"))), source = "a".repeat(40);
const verify = value => verifyReleaseRunnerMetadata(value, contract);
@@ -2716,6 +2716,93 @@ const fakeCodex = resolve(
"../../runner/target/debug/fake-codex-app-server",
);
it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooperative-release", "launch-failure"] as const)(
"bounds setup registration teardown and retains unconfirmed state (%s)",
async mode => {
const root = await mkdtemp(join(tmpdir(), "runnerd-setup-registration-"));
const runtimeDirectory = join(root, "native-state");
await mkdir(runtimeDirectory, { mode: 0o700 });
let registrationStarted!: () => void;
const started = new Promise<void>(resolve => { registrationStarted = resolve; });
let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: string; listenPort: number; listenPath: string }; release: () => Promise<void> }) => void;
const registration = new Promise<Parameters<typeof returnRegistration>[0]>(resolve => { returnRegistration = resolve; });
let finishRelease!: () => void;
const releasing = new Promise<void>(resolve => { finishRelease = resolve; });
const releaseError = new Error("setup registration release failed");
const release = vi.fn(async () => {
if (mode === "stuck-release") await releasing;
if (mode === "late-release-error") throw releaseError;
});
const registered = {
connection: { mode: "listen" as const, listenAddress: "127.0.0.1", listenPort: 43127, listenPath: "/setup-probe" },
release,
};
let registrationSignal: AbortSignal | undefined;
const cleanup = vi.fn(async () => { await rm(runtimeDirectory, { recursive: true, force: true }); });
const launch = vi.fn(() => { throw new Error("owned launcher failed before execution"); });
let settled = false;
vi.useFakeTimers();
const result = probeNativeRunnerEnvironment({
runtimeDirectory, provider: "codex", model: "gpt-test", timeoutMs: 1_000,
environment: { PATH: "", OPENAI_API_KEY: "inert-test-key" },
transportOptions: {
// Registration and launch are external owners; hash real inert bytes
// without executing a daemon, Codex, or an ambient developer binary.
runnerBinary: resolve(import.meta.dirname, "../../package.json"),
codexCommand: resolve(import.meta.dirname, "../../package.json"),
sourceCodexHome: "", runnerProcessLauncher: launch,
controlPlaneRegistration: async (_authority, _identity, signal) => {
registrationSignal = signal;
if (mode === "cooperative-release") signal?.addEventListener("abort", () => returnRegistration(registered), { once: true });
registrationStarted();
return registration;
},
},
onCleanupConfirmed: cleanup,
}).then(value => { settled = true; return value; }, (error: unknown) => { settled = true; return error; });
try {
await started;
if (mode === "launch-failure") returnRegistration(registered);
await vi.advanceTimersByTimeAsync(1_000);
if (mode === "late-release" || mode === "late-release-error" || mode === "stuck-release") returnRegistration(registered);
await vi.advanceTimersByTimeAsync(10_000);
if (mode === "late-release" || mode === "cooperative-release" || mode === "launch-failure") await result;
expect(settled).toBe(true);
expect(registrationSignal?.aborted).toBe(true);
const failure = await result;
expect(failure).toBeInstanceOf(Error);
expect((failure as Error).message).toContain(mode === "launch-failure" ? "owned launcher failed before execution" : "Native provider hello probe timed out.");
if (mode === "stuck" || mode === "stuck-release" || mode === "late-release-error") {
expect(failure).toBeInstanceOf(AggregateError);
expect((failure as AggregateError).cause).toMatchObject({ message: "Native provider hello probe timed out." });
expect(cleanup).not.toHaveBeenCalled();
expect((await stat(runtimeDirectory)).isDirectory()).toBe(true);
if (mode === "late-release-error") expect((failure as AggregateError).errors).toContain(releaseError);
// A registration that finishes after the bounded return still releases
// its authority, without retroactively certifying or deleting state.
returnRegistration(registered);
finishRelease();
await vi.advanceTimersByTimeAsync(0);
expect(release).toHaveBeenCalledOnce();
expect(cleanup).not.toHaveBeenCalled();
} else {
expect(release).toHaveBeenCalledOnce();
expect(cleanup).toHaveBeenCalledOnce();
await expect(stat(runtimeDirectory)).rejects.toThrow();
}
expect(launch).toHaveBeenCalledTimes(mode === "launch-failure" ? 1 : 0);
expect(vi.getTimerCount()).toBe(0);
} finally {
returnRegistration(registered);
finishRelease();
await vi.advanceTimersByTimeAsync(0);
await result;
vi.useRealTimers();
await rm(root, { recursive: true, force: true });
}
},
);
function fakeCodexArgs(stateDirectory: string, ...args: string[]): string[] {
return [
"--state-file",
@@ -1338,6 +1338,8 @@ export interface CapabilityRunnerdCodexTransportOptions {
controlPlaneRegistration?: (
authority: DurablePrpControlPlane,
identity?: DurableRecoveryIdentity,
/** Setup owners abort preparation before draining or releasing its result. */
signal?: AbortSignal,
) => Promise<{
connectUrl?: string;
connection?: RunnerProcessConnection;
@@ -7101,6 +7103,7 @@ export async function probeNativeRunnerEnvironment(options: {
const controller = new AbortController();
let registrationTeardownError: unknown;
let registrationOperation: Promise<unknown> | undefined;
let registrationCleanupConfirmed = true;
const sourceCodexHome = options.transportOptions?.sourceCodexHome ?? resolveSourceCodexHome(environment);
// Never let an unbound home or an external-sandbox flag admit host state or
// weaken this read-only probe. The normal materializer copies bound auth and
@@ -7114,13 +7117,20 @@ export async function probeNativeRunnerEnvironment(options: {
...(options.transportOptions?.controlPlaneRegistration ? { controlPlaneRegistration: (authority, identity) => {
const operation = (async () => {
controller.signal.throwIfAborted();
const registration = await options.transportOptions!.controlPlaneRegistration!(authority, identity);
registrationCleanupConfirmed = false;
const registration = await options.transportOptions!.controlPlaneRegistration!(authority, identity, controller.signal);
let releaseOperation: Promise<void> | undefined;
const release = () => releaseOperation ??= Promise.resolve()
.then(() => registration.release())
.then(() => { registrationCleanupConfirmed = true; }, error => {
registrationTeardownError = error;
throw error;
});
if (controller.signal.aborted) {
try { await registration.release(); }
catch (error) { registrationTeardownError = error; throw error; }
await release();
controller.signal.throwIfAborted();
}
return registration;
return { ...registration, release };
})();
registrationOperation = operation;
return operation;
@@ -7215,17 +7225,34 @@ export async function probeNativeRunnerEnvironment(options: {
failure = error;
} finally {
clearTimeout(timer);
controller.abort(failure ?? new Error("Native provider hello probe complete."));
// Preserve private recovery state if the production close cannot confirm
// teardown; callers remove it only after an authoritative receipt.
const teardownErrors: unknown[] = [];
const teardownDeadline = Date.now() + 10_000;
const awaitTeardown = async (pending: Promise<unknown>, detail: string): Promise<void> => {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
pending,
new Promise<never>((_resolve, reject) => {
timeout = setTimeout(() => reject(new Error(`Native setup probe cleanup timed out while ${detail}; recovery state retained.`)), Math.max(0, teardownDeadline - Date.now()));
}),
]);
} finally { clearTimeout(timeout); }
};
try {
if (session) await session.close({ reason: "environment probe complete" });
else await bundle.transport.close("environment probe complete");
await awaitTeardown(session ? session.close({ reason: "environment probe complete" }) : bundle.transport.close("environment probe complete"), "closing the selected runtime");
if (bundle.evidence().runnerPid !== null && !bundle.evidence().runnerExited) throw new Error("Native setup probe cleanup is incomplete.");
} catch (error) { teardownErrors.push(error); }
await operation.catch(() => undefined);
await registrationOperation?.catch(() => undefined);
if (registrationTeardownError !== undefined) teardownErrors.push(registrationTeardownError);
try { await awaitTeardown(operation.catch(() => undefined), "draining native setup"); }
catch (error) { teardownErrors.push(error); }
if (registrationOperation) {
try { await awaitTeardown(registrationOperation.catch(() => undefined), "draining authority registration"); }
catch (error) { teardownErrors.push(error); }
}
if (registrationTeardownError !== undefined && !teardownErrors.includes(registrationTeardownError)) teardownErrors.push(registrationTeardownError);
if (!registrationCleanupConfirmed) teardownErrors.push(new Error("Native setup probe authority release is unconfirmed; recovery state retained."));
if (bundle.evidence().runnerPid !== null && bundle.evidence().runnerExited && options.provider === "codex" && options.onCodexCredentialRefresh) {
try { await options.onCodexCredentialRefresh(resolve(remoteFilesystemRoot ?? options.runtimeDirectory, "codex-home", "auth.json")); }
catch (error) { teardownErrors.push(error); }
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { tmpdir } from "node:os";
@@ -753,7 +753,9 @@ test("release daemon production uses exact-source native hosts and no publisher
}
assert.match(producer, /matrix: \$\{\{ fromJSON\(needs.runner_binary_targets.outputs.matrix\) \}\}/);
assert.match(targets, /Unknown exact-source daemon platform contract/);
assert.match(targets, /RELEASE_RUNNER_TARGETS/);
assert.match(targets, /release-runner-targets\.json/);
assert.match(targets, /execFileSync\('git', \['show', source \+ ':' \+ file\]/);
assert.doesNotMatch(targets, /import .*from '\.\/|await import\(|\beval\(|new Function/);
assert.doesNotMatch(targets, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
assert.match(producer, /persist-credentials: false/);
assert.match(producer, /contents: read/);
@@ -1064,39 +1066,96 @@ test("Linux ARM prerequisite setup rejects other hosts before invoking tools and
} finally { rmSync(root, { recursive: true, force: true }); }
});
test("exact-source daemon matrices preserve historical three-target sources and add native ARM only for new sources", () => {
test("exact-source daemon matrices read inert data and never execute preview runner selection", () => {
const workflow = readWorkflow("release-verify.yml");
const step = workflow.split(' - name: Preserve the immutable source\'s platform contract')[1].split('\n runner_binaries:')[0];
const script = step.split(' run: |\n')[1].split('\n').map(line => line.startsWith(' ') ? line.slice(10) : line).join('\n');
const root = mkdtempSync(path.join(tmpdir(), 'release-target-contract-'));
try {
const tools = path.join(root, 'tools'); mkdirSync(tools);
writeFileSync(path.join(tools, 'git'), '#!/bin/sh\nprintf "%s\\n" "$FIXTURE_HEAD"\n', { mode: 0o755 });
const environment = {
PATH: `${path.dirname(process.execPath)}:/usr/bin:/bin`, HOME: root,
GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null',
};
const git = args => {
const result = spawnSync('git', args, { cwd: root, env: environment, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
};
const template = path.join(root, 'empty-template'); mkdirSync(template);
git(['init', '-q', '--template=' + template]);
const source = path.join(root, 'packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
const dataFile = path.join(root, 'packages/paperclip-runner/release-runner-targets.json');
mkdirSync(path.dirname(source), { recursive: true });
const output = path.join(root, 'outputs');
const output = path.join(root, 'outputs'), marker = path.join(root, 'candidate-executed');
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
for (const targets of [historical, [...historical, 'linux-arm64']]) {
writeFileSync(source, `export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(targets)};`);
const current = [...historical, 'linux-arm64'];
const contract = targets => JSON.stringify({ schema: 'paperclip.runner.release-targets.v1', targets });
const malicious = `import { writeFileSync } from 'node:fs';\nwriteFileSync(${JSON.stringify(marker)}, 'executed');\nwriteFileSync(process.env.GITHUB_OUTPUT, 'matrix={"include":[{"target":"linux-x64","host":"runs-on/fleet=paperclip-post-merge-x64/env=public-ci"}]}\\n');\nprocess.exit(0);\n`;
const pin = (module, data) => {
rmSync(dataFile, { force: true });
writeFileSync(source, module);
if (data !== undefined) writeFileSync(dataFile, data);
git(['add', 'packages']);
git(['-c', 'user.name=Release fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false',
'commit', '-qm', 'Fixture', '--allow-empty']);
return git(['rev-parse', 'HEAD']);
};
const run = sha => {
writeFileSync(output, '');
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
...environment, SOURCE_SHA: sha, GITHUB_OUTPUT: output,
} });
assert.equal(result.status, 0, result.stderr);
assert.equal(existsSync(marker), false, 'Candidate code must never execute while selecting runners');
return result;
};
const assertMatrix = targets => {
const data = Object.fromEntries(readFileSync(output, 'utf8').trim().split('\n').map(line => {
const index = line.indexOf('='); return [line.slice(0, index), JSON.parse(line.slice(index + 1))];
}));
assert.deepEqual(data.matrix.include.map(row => row.target), targets);
assert.deepEqual(data.native_consumers.include.map(row => row.target), targets.filter(target => target !== 'linux-x64'));
assert.ok(data.matrix.include.every(row => row.target !== 'linux-arm64' || row.host === 'ubuntu-24.04-arm'));
assert.equal(data.matrix.include.find(row => row.target === 'linux-x64').host, targets.length === 4 ? 'ubuntu-24.04' : 'ubuntu-latest',
'new GNU libc baseline stays pinned while historical qualification keeps its producer mode');
const hosts = { 'linux-x64': targets.length === 4 ? 'ubuntu-24.04' : 'ubuntu-latest',
'linux-arm64': 'ubuntu-24.04-arm', 'darwin-arm64': 'macos-15', 'darwin-x64': 'macos-15-intel' };
for (const row of data.matrix.include) assert.equal(row.host, hosts[row.target], 'Only workflow-owned public hosts may be selected');
};
for (const [targets, module, data] of [
[historical, `export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(historical)};`, undefined],
[current, `${malicious}export const RELEASE_RUNNER_TARGETS = Object.freeze(${JSON.stringify(current)});`, undefined],
[historical, malicious, contract(historical)],
[current, malicious, contract(current)],
]) {
const result = run(pin(module, data));
assert.equal(result.status, 0, result.stderr);
assertMatrix(targets);
}
writeFileSync(source, 'export const RELEASE_RUNNER_TARGETS = ["linux-arm64"];'); writeFileSync(output, '');
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
} });
assert.equal(result.status, 1); assert.match(result.stderr, /Unknown exact-source daemon platform contract/);
const sha = pin(malicious, contract(current));
writeFileSync(dataFile, contract(['runs-on/fleet=paperclip-post-merge-x64/env=public-ci']));
assert.equal(run(sha).status, 0, 'Read the immutable git blob, not a changed checkout file');
assertMatrix(current);
assert.equal(run('a'.repeat(40)).status, 1, 'Wrong checked-out source must fail before output');
assert.equal(readFileSync(output, 'utf8'), '');
for (const data of [
contract(['linux-arm64']), contract([...current, 'linux-arm64']), contract([...current.slice(0, 3), 'win32-x64']),
JSON.stringify({ schema: 'paperclip.runner.release-targets.v1', targets: current, host: 'trusted-fleet' }),
JSON.stringify({ schema: 'other', targets: current }), contract('linux-arm64'), 'null', '{invalid', ' '.repeat(65 * 1024),
]) {
const result = run(pin(`export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(historical)};`, data));
assert.notEqual(result.status, 0, 'Malformed present data must never fall back to a legacy module');
assert.equal(readFileSync(output, 'utf8'), '');
}
for (const module of [malicious,
'export const RELEASE_RUNNER_TARGETS = getTargets();',
`export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(current)};\nexport const RELEASE_RUNNER_TARGETS = ${JSON.stringify(historical)};`,
'export const RELEASE_RUNNER_TARGETS = ["linux-arm64"];']) {
const result = run(pin(module, undefined));
assert.notEqual(result.status, 0, 'Unknown legacy declarations must fail closed');
assert.equal(readFileSync(output, 'utf8'), '');
}
pin(malicious, undefined);
symlinkSync('scripts/stage-release-runner-binaries.mjs', dataFile);
git(['add', 'packages']);
git(['-c', 'user.name=Release fixture', '-c', 'user.email=fixture@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'Symlink fixture']);
assert.notEqual(run(git(['rev-parse', 'HEAD'])).status, 0, 'Source contract symlinks must not be followed');
assert.equal(readFileSync(output, 'utf8'), '');
} finally { rmSync(root, { recursive: true, force: true }); }
});
+2 -1
View File
@@ -173,6 +173,7 @@ describe("adapter routes", () => {
[{ driver: "ssh", platform: "linux", architecture: "arm64" }, "paperclip"],
[{ driver: "ssh", platform: "linux", architecture: "arm" }, "legacy"],
[{ driver: "local", platform: "win32", architecture: "x64" }, "legacy"],
[{ driver: "sandbox", platform: "linux", architecture: "arm64", libc: "glibc", glibcVersion: "2.39" }, "paperclip"],
])("projects the selected environment once for the whole adapter inventory (%s)", async (target, defaultRunner) => {
const resolveRunnerTarget = vi.fn(async () => target);
const response = await request(createApp({}, { resolveRunnerTarget })).get("/api/adapters?companyId=company-1&environmentId=selected");
@@ -187,7 +188,7 @@ describe("adapter routes", () => {
const response = await request(createApp({}, { resolveRunnerTarget })).get("/api/adapters?companyId=company-1");
expect(response.status).toBe(200);
expect(resolveRunnerTarget).toHaveBeenCalledExactlyOnceWith("company-1", undefined);
expect(response.body.find((adapter: any) => adapter.type === "codex_local")).toMatchObject({ defaultRunner: "paperclip" });
expect(response.body.find((adapter: any) => adapter.type === "codex_local")).toMatchObject({ defaultRunner: "legacy" });
});
it("keeps unscoped inventory independent of company target discovery", async () => {
@@ -21,6 +21,7 @@ const mockEnvironmentService = vi.hoisted(() => ({
list: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
}));
const mockEnvironmentCustomImageService = vi.hoisted(() => ({
@@ -129,6 +130,26 @@ function createApp(actor: Record<string, unknown>) {
}
describe("environment instance routes", () => {
it.each(["post", "patch"])("rejects client-written sandbox platform evidence on %s", async method => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({ type: "board", source: "local_implicit", isInstanceAdmin: true });
const payload = { ...(method === "post" ? { name: "Fixture", driver: "local", config: {} } : {}), metadata: { paperclipSandboxPlatformProbes: { "company-1": { v: 1 } } } };
const res = method === "post" ? await request(app).post("/api/companies/company-1/environments").send(payload)
: await request(app).patch("/api/environments/env-1").send(payload);
expect(res.status).toBe(422);
expect(res.body.details?.code ?? res.body.code).toBe("environment_platform_probe_reserved");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it.each([null, { userNote: "replacement" }])("passes explicit user metadata replacement through without copying an old probe (%j)", async metadata => {
const existing = createEnvironment({ metadata: { userNote: "old", paperclipSandboxPlatformProbes: { "company-1": { v: 1, marker: "stale" } } } });
mockEnvironmentService.getById.mockResolvedValue(existing);
mockEnvironmentService.update.mockResolvedValue({ ...existing, metadata });
const app = createApp({ type: "board", source: "local_implicit", isInstanceAdmin: true });
const res = await request(app).patch("/api/environments/env-1").send({ metadata });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalledExactlyOnceWith("env-1", { metadata }, { db: expect.anything() });
expect(res.body.metadata).toEqual(metadata);
});
beforeEach(() => {
mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
@@ -138,6 +159,7 @@ describe("environment instance routes", () => {
mockEnvironmentService.list.mockReset();
mockEnvironmentService.getById.mockReset();
mockEnvironmentService.create.mockReset();
mockEnvironmentService.update.mockReset();
Object.values(mockEnvironmentCustomImageService).forEach((mock) => mock.mockReset());
mockEnvironmentCustomImageService.getOverview.mockResolvedValue({
activeTemplate: null,
+106 -2
View File
@@ -6,12 +6,16 @@ const mockProbePluginSandboxProviderDriver = vi.hoisted(() => vi.fn());
const mockResolvePluginSandboxProviderDriverByKey = vi.hoisted(() => vi.fn());
const mockRuntimeAcquireRunLease = vi.hoisted(() => vi.fn());
const mockRuntimeReleaseRunLease = vi.hoisted(() => vi.fn());
const mockRuntimeExecute = vi.hoisted(() => vi.fn());
const mockRecordPlatformProbe = vi.hoisted(() => vi.fn());
const mockEnvironmentRuntimeService = vi.hoisted(() => vi.fn(() => ({
acquireRunLease: mockRuntimeAcquireRunLease,
execute: mockRuntimeExecute,
getDriver: vi.fn(() => ({
releaseRunLease: mockRuntimeReleaseRunLease,
})),
})));
vi.mock("../services/environments.js", () => ({ environmentService: () => ({ recordSandboxPlatformProbe: mockRecordPlatformProbe }) }));
vi.mock("@paperclipai/adapter-utils/ssh", () => ({
ensureSshWorkspaceReady: mockEnsureSshWorkspaceReady,
@@ -27,20 +31,120 @@ vi.mock("../services/environment-runtime.js", () => ({
environmentRuntimeService: mockEnvironmentRuntimeService,
}));
import { probeEnvironment } from "../services/environment-probe.ts";
import { probeEnvironment, readSandboxPlatformQualification, SANDBOX_PLATFORM_PROBE_COMMAND } from "../services/environment-probe.ts";
import * as configRuntime from "../services/environment-config.js";
import { SANDBOX_PLATFORM_PROBE_METADATA_KEY, resolveActiveEnvironmentCustomImageTemplateForRuntime } from "../services/environment-custom-image-runtime.js";
describe("probeEnvironment", () => {
beforeEach(() => {
vi.restoreAllMocks();
mockRecordPlatformProbe.mockReset().mockResolvedValue(true);
mockRuntimeExecute.mockReset();
mockEnsureSshWorkspaceReady.mockReset();
mockProbePluginEnvironmentDriver.mockReset();
mockProbePluginSandboxProviderDriver.mockReset();
mockResolvePluginSandboxProviderDriverByKey.mockReset();
mockResolvePluginSandboxProviderDriverByKey.mockResolvedValue(null);
mockRuntimeAcquireRunLease.mockReset();
mockRuntimeReleaseRunLease.mockReset();
mockRuntimeReleaseRunLease.mockReset().mockResolvedValue({ id: "lease", companyId: "company", environmentId: "env", status: "released", cleanupStatus: "success" });
mockEnvironmentRuntimeService.mockClear();
});
it.each(["2.38", "2.39", "musl"])("records actual immutable-image facts for %s and reads them without another lease", async libc => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image, reuseLease: false }, metadata: null } as any;
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockResolvedValue({ driver: "sandbox", config: environment.config });
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { image, remoteCwd: "/workspace" } } });
mockRuntimeExecute.mockResolvedValue({ exitCode: 0, stdout: `Linux\naarch64\n${libc === "musl" ? "musl" : `glibc\nglibc ${libc}`}\n` });
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.ok).toBe(true);
expect(result.details?.platformQualificationRecorded).toBe(true);
expect(mockRuntimeExecute).toHaveBeenCalledWith(expect.objectContaining({ command: "sh", args: ["-c", SANDBOX_PLATFORM_PROBE_COMMAND], timeoutMs: 10_000, bypassSession: true }));
const proof = mockRecordPlatformProbe.mock.calls[1]![0].proof;
const measured = { platform: "linux", architecture: "arm64", libc: libc === "musl" ? "musl" : "glibc", ...(libc === "musl" ? {} : { glibcVersion: `glibc ${libc}` }) };
expect(proof.facts).toEqual(measured);
environment.metadata = { [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { company: proof } };
expect(await readSandboxPlatformQualification({} as any, environment, "company")).toEqual(measured);
expect(mockRuntimeAcquireRunLease).toHaveBeenCalledTimes(1);
expect(await readSandboxPlatformQualification({} as any, environment, "foreign-company")).toBeNull();
expect(await readSandboxPlatformQualification({} as any, { ...environment, config: { ...environment.config, image: `registry.example.test/runtime@sha256:${"b".repeat(64)}` } }, "company")).toBeNull();
vi.mocked(configRuntime.resolveEnvironmentDriverConfigForRuntime).mockImplementation(async (_db, _company, _env, options) => {
options?.onResolvedCustomImageTemplate?.({ id: "changed-template", capturedAt: null, templateKind: "image", bindingField: "image" });
return { driver: "sandbox", config: environment.config };
});
expect(await readSandboxPlatformQualification({} as any, environment, "company")).toBeNull();
expect(mockRuntimeAcquireRunLease).toHaveBeenCalledTimes(1);
});
it.each(["measurement", "cleanup", "config-race"])("does not claim qualification after %s failure while retaining connection readiness", async failure => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image }, metadata: null } as any;
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockResolvedValue({ driver: "sandbox", config: environment.config });
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { image } } });
mockRuntimeExecute.mockResolvedValue({ exitCode: failure === "measurement" ? 1 : 0, stdout: "Linux\nx86_64\nglibc\nglibc 2.39\n" });
if (failure === "cleanup") mockRuntimeReleaseRunLease.mockRejectedValueOnce(new Error("owned cleanup failed"));
if (failure === "config-race") mockRecordPlatformProbe.mockResolvedValue(false);
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.ok).toBe(true);
expect(result.details?.platformQualificationRecorded).toBe(false);
expect(mockRecordPlatformProbe.mock.calls[0]![0].proof).toBeNull();
if (failure !== "config-race") expect(mockRecordPlatformProbe).toHaveBeenCalledTimes(1);
});
it.each([
null,
{ id: "lease", companyId: "company", environmentId: "env", status: "released", cleanupStatus: "failed" },
{ id: "lease", companyId: "company", environmentId: "env", status: "pending_cleanup", cleanupStatus: "failed" },
{ id: "other-lease", companyId: "company", environmentId: "env", status: "released", cleanupStatus: "success" },
{ id: "lease", companyId: "foreign-company", environmentId: "env", status: "released", cleanupStatus: "success" },
{ id: "lease", companyId: "company", environmentId: "other-environment", status: "released", cleanupStatus: "success" },
])("does not record platform evidence without an owned successful cleanup receipt (%j)", async receipt => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image }, metadata: null } as any;
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockResolvedValue({ driver: "sandbox", config: environment.config });
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { image } } });
mockRuntimeExecute.mockResolvedValue({ exitCode: 0, stdout: "Linux\nx86_64\nglibc\nglibc 2.39\n" });
mockRuntimeReleaseRunLease.mockResolvedValue(receipt);
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.ok).toBe(true);
expect(result.details?.platformQualificationRecorded).toBe(false);
expect(mockRecordPlatformProbe).toHaveBeenCalledTimes(1);
expect(mockRecordPlatformProbe.mock.calls[0]![0].proof).toBeNull();
});
it("reads an effective custom image without touching its usage timestamp", async () => {
const update = vi.fn();
const capturedAt = new Date("2026-10-12T00:00:00Z");
const row = { id: "template", provider: "daytona", templateKind: "image", templateRef: `registry.example.test/runtime@sha256:${"a".repeat(64)}`, capturedAt, metadata: null };
const db = { select: () => ({ from: () => ({ where: () => ({ orderBy: () => Promise.resolve([row]) }) }) }), update };
const identity = vi.fn();
const result = await resolveActiveEnvironmentCustomImageTemplateForRuntime(db as never, { environmentId: "env", baseConfig: { provider: "daytona", reuseLease: false }, runtimeConfig: { provider: "daytona", reuseLease: false }, readOnly: true, onResolvedTemplate: identity });
expect(result).toMatchObject({ image: row.templateRef });
expect(identity).toHaveBeenCalledWith({ id: "template", capturedAt: capturedAt.toISOString(), templateKind: "image", bindingField: "image" });
expect(update).not.toHaveBeenCalled();
});
it.each(["provider_template", "unknown"])("does not qualify a named active %s boot binding over a pinned base image", async templateKind => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image }, metadata: null } as any;
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockImplementation(async (_db, _company, _env, options) => {
const bindingField = templateKind === "provider_template" ? "template" : "templateRef";
options?.onResolvedCustomImageTemplate?.({ id: "active-template", capturedAt: null, templateKind, bindingField });
return { driver: "sandbox", config: { ...environment.config, [bindingField]: "mutable-template-name" } };
});
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { image } } });
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.ok).toBe(true);
expect(result.details?.platformQualificationRecorded).toBe(false);
expect(mockRuntimeExecute).not.toHaveBeenCalled();
expect(mockRecordPlatformProbe).toHaveBeenCalledTimes(1);
});
it("returns false when an atomic probe record loses its config comparison", async () => {
const { environmentService } = await vi.importActual<typeof import("../services/environments.js")>("../services/environments.js");
const returning = vi.fn().mockResolvedValue([]);
const where = vi.fn(() => ({ returning }));
const db = { update: vi.fn(() => ({ set: vi.fn(() => ({ where })) })) };
expect(await environmentService(db as never).recordSandboxPlatformProbe({ environmentId: "env", companyId: "company", config: { provider: "daytona" }, proof: { v: 1 } })).toBe(false);
expect(where).toHaveBeenCalledOnce();
expect(returning).toHaveBeenCalledOnce();
});
it("reports local environments as immediately available", async () => {
const result = await probeEnvironment({} as any, {
id: "env-1",
@@ -1,6 +1,8 @@
import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { and, eq } from "drizzle-orm";
import { and, eq, type SQL } from "drizzle-orm";
import type { UpdateEnvironment } from "@paperclipai/shared";
import { SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "../services/environment-custom-image-runtime.js";
import {
activityLog,
agents,
@@ -130,6 +132,65 @@ describeEmbeddedPostgres("environmentService leases", () => {
return companyId;
}
it("clears user metadata to null when no private platform proof exists", async () => {
const created = await svc.create({ name: "User metadata clear", driver: "sandbox", config: { provider: "daytona" }, metadata: { userNote: "remove" } });
expect((await svc.update(created.id, { metadata: null }))?.metadata).toBeNull();
});
it.each(["clear", "replace", "unrelated", "status", "config", "driver"] as const)(
"handles %s saves atomically after a concurrent platform probe record", async operation => {
const companyId = await seedCompany();
const config = { provider: "daytona", image: `registry.example.test/runtime@sha256:${"a".repeat(64)}` };
const staleProof = { v: 1, marker: "old" };
const freshProof = { v: 1, marker: "new" };
const created = await svc.create({ name: "Concurrent metadata", driver: "sandbox", config,
metadata: { userNote: "old", _paperclipManagedArchiveToken: "old-archive-token",
[SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { [companyId]: staleProof } } });
const patches: Record<typeof operation, UpdateEnvironment> = {
clear: { metadata: null },
replace: { metadata: { userNote: "replacement", [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { [companyId]: staleProof } } },
unrelated: { description: "unrelated save" },
status: { status: "active" },
config: { config: { ...config, image: `registry.example.test/runtime@sha256:${"b".repeat(64)}` } },
driver: { driver: "ssh", config: { host: "fixture.example.test", username: "fixture" } },
};
// Interleave the real probe UPDATE after the save is prepared but before its
// real UPDATE executes. This reproduces the stale route-read/write window.
const delayedWriteDb = {
update(table: typeof environments) {
const prepared = db.update(table);
return {
set(values: Parameters<typeof prepared.set>[0]) {
const statement = prepared.set(values);
return {
where(condition: SQL) {
const pending = statement.where(condition);
return {
async returning() {
expect(await svc.recordSandboxPlatformProbe({
environmentId: created.id, companyId, config, proof: freshProof,
})).toBe(true);
return pending.returning();
},
};
},
};
},
};
},
};
const saved = await svc.update(created.id, patches[operation], { db: delayedWriteDb as never });
if (operation === "config" || operation === "driver") {
expect(saved?.metadata).not.toHaveProperty(SANDBOX_PLATFORM_PROBE_METADATA_KEY);
} else {
expect(saved?.metadata?.[SANDBOX_PLATFORM_PROBE_METADATA_KEY]).toEqual({ [companyId]: freshProof });
}
if (operation === "clear") expect(saved?.metadata).toEqual({ [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { [companyId]: freshProof } });
else expect(saved?.metadata?.userNote).toBe(operation === "replace" ? "replacement" : "old");
if (operation !== "unrelated") expect(saved?.metadata).not.toHaveProperty("_paperclipManagedArchiveToken");
},
);
it("acquires and releases a lease for a run", async () => {
const { companyId, environmentId, runId } = await seedEnvironment();
+8
View File
@@ -57,6 +57,7 @@ import { getConfiguredSecretProvider } from "../secrets/configured-provider.js";
import { assertBoardOrgAccess, getActorInfo } from "./authz.js";
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
import { environmentService } from "../services/environments.js";
import { SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "../services/environment-custom-image-runtime.js";
import { environmentRuntimeService } from "../services/environment-runtime.js";
import { executionWorkspaceService } from "../services/execution-workspaces.js";
import { closeWarmNativeSessionsForEnvironment } from "../services/native-runtime/native-session-executor.js";
@@ -311,6 +312,9 @@ async function assertPlatformProvisionedEnvironmentWritable(
* Clearing (null/false) is allowed so stale markers can be removed.
*/
function assertNoClientPlatformProvisionedMarkers(metadata: unknown): void {
if (isPlainRecord(metadata) && Object.prototype.hasOwnProperty.call(metadata, SANDBOX_PLATFORM_PROBE_METADATA_KEY)) {
throw unprocessable("Sandbox platform qualification is recorded only by an explicit environment probe.", { code: "environment_platform_probe_reserved" });
}
if (!isCloudManagedInstance() || !isPlainRecord(metadata)) return;
for (const key of PLATFORM_PROVISIONED_MARKER_KEYS) {
if (metadata[key] !== undefined && metadata[key] !== null && metadata[key] !== false) {
@@ -396,6 +400,10 @@ export function environmentRoutes(
envVars?: Record<string, unknown> | null;
metadata: Record<string, unknown> | null;
}>(req: Request, environment: T): T {
if (environment.metadata) {
const { [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: _probe, ...metadata } = environment.metadata;
environment = { ...environment, metadata };
}
// Floor: on cloud-managed instances, platform-provisioned rows use the
// floored view — credential-shaped config keys are never echoed to any
// reader, structural config stays visible, and tenant env vars on the
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { agentHarnessType, agentRunner, paperclipRunnerProfileForHarness, paperclipRunnerSupportsPlatform, PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION, createAgentSchema, updateAgentSchema, testAdapterEnvironmentSchema } from "@paperclipai/shared";
const state = vi.hoisted(() => ({ disabled: [] as string[], overridden: new Set<string>(), settings: {} as Record<string, unknown>, getEnvironment: vi.fn(), bindings: vi.fn(), managedEnvironment: vi.fn(), ssh: vi.fn(), resolveEnvironment: vi.fn() }));
const state = vi.hoisted(() => ({ disabled: [] as string[], overridden: new Set<string>(), settings: {} as Record<string, unknown>, getEnvironment: vi.fn(), bindings: vi.fn(), managedEnvironment: vi.fn(), ssh: vi.fn(), resolveEnvironment: vi.fn(), sandboxQualification: vi.fn() }));
vi.mock("../adapters/registry.js", () => ({ findActiveServerAdapter: (type: string) => ({ type }), hasActiveAdapterOverride: (type: string) => state.overridden.has(type), waitForExternalAdapters: async () => {} }));
vi.mock("./adapter-plugin-store.js", () => ({ getDisabledAdapterTypes: () => state.disabled }));
vi.mock("./instance-settings.js", () => ({ instanceSettingsService: () => ({ get: async () => state.settings }) }));
vi.mock("./environments.js", () => ({ environmentService: () => ({ getById: state.getEnvironment, listBoundCompanyIds: state.bindings, findManagedSandboxEnvironment: state.managedEnvironment, findKubernetesEnvironment: state.managedEnvironment }) }));
vi.mock("./environment-probe.js", () => ({ readSandboxPlatformQualification: state.sandboxQualification }));
vi.mock("./environment-config.js", () => ({ resolveEnvironmentDriverConfigForRuntime: state.resolveEnvironment }));
vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: state.ssh }));
import { AGENT_RUNNER_SSH_PLATFORM_COMMAND, agentRunnerAvailability, resolveAgentRunnerTargetForCompany, resolveNewAgentRunner, resolveNewAgentRunnerForCompany } from "./agent-runner-selection.js";
@@ -16,7 +17,7 @@ const originalArchitecture = Object.getOwnPropertyDescriptor(process, "arch")!;
afterEach(() => { vi.restoreAllMocks(); Object.defineProperty(process, "platform", originalPlatform); Object.defineProperty(process, "arch", originalArchitecture); });
describe("server-owned Codex runner selection", () => {
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.sandboxQualification.mockReset().mockResolvedValue(null); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
it.each(["x64", "arm64"])("keeps detected musl/unknown Linux %s legacy and preserves saved execution", architecture => {
for (const libc of ["musl", "unknown"] as const) {
const target = { driver: "local", platform: "linux", architecture, libc };
@@ -166,10 +167,30 @@ describe("server-owned Codex runner selection", () => {
expect(state.getEnvironment).not.toHaveBeenCalled();
expect(state.ssh).not.toHaveBeenCalled();
});
it.each([
[{ platform: "linux", architecture: "x64", libc: "glibc", glibcVersion: "2.39" }, "paperclip_runner"],
[{ platform: "linux", architecture: "arm64", libc: "glibc", glibcVersion: "3.0" }, "paperclip_runner"],
[{ platform: "linux", architecture: "arm64", libc: "glibc", glibcVersion: "2.38" }, "codex_local"],
[{ platform: "linux", architecture: "x64", libc: "glibc" }, "codex_local"],
[{ platform: "linux", architecture: "arm64", libc: "musl" }, "codex_local"],
[{ platform: "linux", architecture: "unknown", libc: "glibc", glibcVersion: "2.39" }, "codex_local"],
])("uses cached selected-image measurements for sandbox creation/discovery (%j)", async (facts, expected) => {
state.getEnvironment.mockResolvedValue({ id: "sandbox", driver: "sandbox", status: "active", config: { provider: "daytona" } });
state.sandboxQualification.mockResolvedValue(facts);
const selectedTarget = await resolveAgentRunnerTargetForCompany({} as never, "company", "sandbox");
expect(agentRunnerAvailability("codex_local", selectedTarget).defaultRunner).toBe(expected === "paperclip_runner" ? "paperclip" : "legacy");
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "sandbox" });
expect(result.adapterType).toBe(expected);
expect(state.ssh).not.toHaveBeenCalled();
expect(state.resolveEnvironment).not.toHaveBeenCalled();
if (expected === "codex_local") await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip", defaultEnvironmentId: "sandbox" })).rejects.toMatchObject({ status: 422 });
});
it("does not infer a sandbox image architecture from its driver", async () => {
state.getEnvironment.mockResolvedValue({ id: "sandbox", driver: "sandbox", status: "active", config: { provider: "daytona" } });
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "sandbox" });
expect(result.adapterType).toBe("paperclip_runner"); expect(state.ssh).not.toHaveBeenCalled();
expect(result.adapterType).toBe("codex_local"); expect(state.ssh).not.toHaveBeenCalled();
expect(state.sandboxQualification).toHaveBeenCalledWith({}, expect.objectContaining({ id: "sandbox" }), "company");
await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip", defaultEnvironmentId: "sandbox" })).rejects.toMatchObject({ status: 422, details: { code: "agent_runner_unavailable" } });
});
});
+11 -2
View File
@@ -25,6 +25,8 @@ export function agentRunnerAvailability(harness: string, target: RunnerTarget =
&& !!findActiveServerAdapter(harness) && !!findActiveServerAdapter("paperclip_runner")
&& !hasActiveAdapterOverride(harness) && !hasActiveAdapterOverride("paperclip_runner")
&& isEnvironmentDriverSupportedForAdapter("paperclip_runner", target.driver)
&& (target.driver !== "sandbox" || Boolean(target.platform && target.architecture
&& (target.platform !== "linux" || (target.libc === "glibc" && target.glibcVersion))))
&& (!target.platform || paperclipRunnerSupportsPlatform(harness, target.platform, target.architecture ?? "", target.libc, target.glibcVersion));
return { supportedRunners: supported ? ["paperclip", "legacy"] : ["legacy"], defaultRunner: supported ? "paperclip" : "legacy" };
}
@@ -55,7 +57,9 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp
return { adapterType: "codex_local", adapterConfig: { ...config, dangerouslyBypassApprovalsAndSandbox: config.dangerouslyBypassApprovalsAndSandbox ?? true } };
}
if (native && harness !== "codex_local" && choice === "paperclip") return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) };
if (availability.defaultRunner !== "paperclip") throw unprocessable("Paperclip Runner is unavailable for this harness and environment. Select Legacy runner in Advanced.", { code: "agent_runner_unavailable" });
if (availability.defaultRunner !== "paperclip") throw unprocessable(input.target?.driver === "sandbox"
? "Paperclip Runner is unavailable for this sandbox. Choose an immutable OCI image digest and run its environment connection probe to qualify the OS, architecture and libc, or select Legacy runner in Advanced."
: "Paperclip Runner is unavailable for this harness and environment. Select Legacy runner in Advanced.", { code: "agent_runner_unavailable" });
if (native) return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) };
const unsupported = ["agentCommand", "stateDir", "warmHandleIdleMs", "extraArgs", "args", "search", "fastMode", "variant", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "permissionMode", "maxTurns", "disableWebSearch", "outputInactivityTimeoutMs", "terminalResultCleanupGraceMs"]
.filter(key => config[key] !== undefined && config[key] !== false && config[key] !== "" && config[key] !== 0 && !(Array.isArray(config[key]) && config[key].length === 0));
@@ -112,7 +116,12 @@ export async function resolveAgentRunnerTargetForCompany(db: Db, companyId: stri
}
const driver = environment?.driver ?? "local";
let target: RunnerTarget = driver === "local" ? localAgentRunnerTarget()
: { driver }; // Sandbox platform/artifact identity is verified by selected-runtime setup; do not guess an image's architecture.
: { driver };
if (driver === "sandbox" && environment) {
const { readSandboxPlatformQualification } = await import("./environment-probe.js");
const qualified = await readSandboxPlatformQualification(db, environment, companyId);
if (qualified) target = { driver, ...qualified };
}
if (driver === "ssh" && environment && agentRunnerAvailability("codex_local", target).defaultRunner === "paperclip") {
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment);
if (parsed.driver !== "ssh") throw unprocessable("The selected SSH environment is unavailable.");
@@ -660,6 +660,8 @@ export async function resolveEnvironmentDriverConfigForRuntime(
// but must still resolve the active custom image as prepared runtime
// configuration and tooling so the test reflects what real agent runs use.
applyCustomImageTemplate?: boolean;
readOnlyCustomImageTemplate?: boolean;
onResolvedCustomImageTemplate?: (identity: { id: string; capturedAt: string | null; templateKind: string; bindingField: string }) => void;
},
): Promise<ParsedEnvironmentConfig> {
const parsed = parseEnvironmentDriverConfig(environment);
@@ -729,6 +731,8 @@ export async function resolveEnvironmentDriverConfigForRuntime(
// are excluded when the template's source fingerprint is computed,
// so they must be excluded when re-checking it here.
secretRefExcludePaths: collectSecretRefPaths(schema),
readOnly: context?.readOnlyCustomImageTemplate,
onResolvedTemplate: context?.onResolvedCustomImageTemplate,
})
: runtimeConfig,
};
@@ -13,6 +13,8 @@ import { readConfigValueAtPath, writeConfigValueAtPath } from "./json-schema-sec
type TemplateRow = typeof environmentCustomImageTemplates.$inferSelect;
export const ENVIRONMENT_CUSTOM_IMAGE_RUNTIME_CONFIG_BINDING_METADATA_KEY = "runtimeConfigBinding";
/** Measured platform evidence is reserved to explicit server-side environment probes. */
export const SANDBOX_PLATFORM_PROBE_METADATA_KEY = "paperclipSandboxPlatformProbes";
export const ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS = [
"timeoutMs",
"reuseLease",
@@ -557,6 +559,8 @@ export async function resolveActiveEnvironmentCustomImageTemplateForRuntime(
runtimeConfig: SandboxEnvironmentConfig;
secretRefExcludePaths?: Iterable<string>;
now?: Date;
readOnly?: boolean;
onResolvedTemplate?: (identity: { id: string; capturedAt: string | null; templateKind: string; bindingField: string }) => void;
},
): Promise<SandboxEnvironmentConfig> {
const row = await db
@@ -587,7 +591,9 @@ export async function resolveActiveEnvironmentCustomImageTemplateForRuntime(
// match. Runtime-only knobs such as lease reuse, timeouts, and resource hints
// are excluded from the fingerprint so those edits do not discard the capture.
const now = input.now ?? new Date();
await db
input.onResolvedTemplate?.({ id: row.id, capturedAt: row.capturedAt?.toISOString() ?? null,
templateKind: row.templateKind ?? "unknown", bindingField: resolveEnvironmentCustomImageRuntimeConfigBinding(row).field });
if (!input.readOnly) await db
.update(environmentCustomImageTemplates)
.set({ lastUsedAt: now, updatedAt: now })
.where(eq(environmentCustomImageTemplates.id, active.id));
+96 -8
View File
@@ -11,6 +11,56 @@ import { isBuiltinSandboxProvider, probeSandboxProvider } from "./sandbox-provid
import { probePluginEnvironmentDriver, probePluginSandboxProviderDriver } from "./plugin-environment-driver.js";
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
import { environmentRuntimeService } from "./environment-runtime.js";
import { environmentService } from "./environments.js";
import { fingerprintEnvironmentSandboxProviderConfig, SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "./environment-custom-image-runtime.js";
import type { SandboxEnvironmentConfig } from "@paperclipai/shared";
export const SANDBOX_PLATFORM_PROBE_COMMAND = "uname -s; uname -m; if version=$(getconf GNU_LIBC_VERSION 2>/dev/null) && [ -n \"$version\" ]; then printf 'glibc\\n%s\\n' \"$version\"; else case \"$(ldd --version 2>&1)\" in *musl*) printf 'musl\\n' ;; *) printf 'unknown\\n' ;; esac; fi";
type PlatformFacts = { platform: string; architecture: string; libc: "glibc" | "musl" | "unknown"; glibcVersion?: string };
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function fingerprint(config: Record<string, unknown>): string {
return fingerprintEnvironmentSandboxProviderConfig(config as SandboxEnvironmentConfig);
}
function isImmutableImage(value: unknown): value is string {
return typeof value === "string" && /^[^\s@]+@sha256:[a-f0-9]{64}$/.test(value);
}
export function parseSandboxPlatformFacts(output: string): PlatformFacts {
const [os, arch, libc, version] = output.trim().split(/\r?\n/);
return { platform: os === "Linux" ? "linux" : os === "Darwin" ? "darwin" : "unknown",
architecture: arch === "x86_64" ? "x64" : arch === "aarch64" || arch === "arm64" ? "arm64" : "unknown",
libc: libc === "glibc" || libc === "musl" ? libc : "unknown",
...(libc === "glibc" ? { glibcVersion: version?.trim() ?? "unknown" } : {}) };
}
async function sandboxBinding(db: Db, environment: Environment, companyId: string) {
const selectedTemplate: { value: { id: string; capturedAt: string | null; templateKind: string; bindingField: string } | null } = { value: null };
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment, {
applyCustomImageTemplate: true, readOnlyCustomImageTemplate: true,
onResolvedCustomImageTemplate: identity => { selectedTemplate.value = identity; },
});
if (parsed.driver !== "sandbox") return null;
const image = parsed.config.image;
// Names, mutable tags, provider defaults and snapshot names are not immutable identities.
const template = selectedTemplate.value;
if (!isImmutableImage(image) || ["snapshot", "template", "templateRef"].some(key => record(parsed.config)[key])
|| (template && (template.templateKind !== "image" || template.bindingField !== "image"))) return null;
return { baseConfigFingerprint: fingerprint(environment.config), runtimeConfigFingerprint: fingerprint(parsed.config as unknown as Record<string, unknown>), image, template };
}
/** Read measured, company-scoped image evidence without leasing or contacting a provider. */
export async function readSandboxPlatformQualification(db: Db, environment: Environment, companyId: string): Promise<PlatformFacts | null> {
const proof = record(record(environment.metadata?.[SANDBOX_PLATFORM_PROBE_METADATA_KEY])[companyId]);
if (proof.v !== 1 || proof.companyId !== companyId || proof.baseConfigFingerprint !== fingerprint(environment.config)) return null;
const binding = await sandboxBinding(db, environment, companyId);
if (!binding || proof.runtimeConfigFingerprint !== binding.runtimeConfigFingerprint || proof.image !== binding.image
|| JSON.stringify(proof.template) !== JSON.stringify(binding.template)) return null;
const facts = record(proof.facts);
if (typeof facts.platform !== "string" || typeof facts.architecture !== "string"
|| !["glibc", "musl", "unknown"].includes(String(facts.libc))) return null;
return { platform: facts.platform, architecture: facts.architecture, libc: facts.libc as PlatformFacts["libc"],
...(typeof facts.glibcVersion === "string" ? { glibcVersion: facts.glibcVersion } : {}) };
}
export async function probeEnvironment(
db: Db,
@@ -24,6 +74,10 @@ export async function probeEnvironment(
} = {},
): Promise<EnvironmentProbeResult> {
const resolvedCompanyId = options.companyId ?? null;
const savedSandboxProbe = environment.driver === "sandbox" && options.acquireSandboxRuntimeLease === true && resolvedCompanyId;
const environments = environmentService(db);
if (savedSandboxProbe) await environments.recordSandboxPlatformProbe({ environmentId: environment.id,
companyId: savedSandboxProbe, config: environment.config, proof: null });
const parsed = options.resolvedConfig ?? (
options.acquireSandboxRuntimeLease === true
? parseEnvironmentDriverConfig(environment)
@@ -77,6 +131,8 @@ export async function probeEnvironment(
};
let leaseRecord: Awaited<ReturnType<typeof runtime.acquireRunLease>> | null = null;
let releaseStatus: "released" | "failed" = "released";
let qualification: Record<string, unknown> | null = null;
let details: Record<string, unknown> | null = null;
try {
leaseRecord = await runtime.acquireRunLease({
companyId: resolvedCompanyId,
@@ -93,19 +149,37 @@ export async function probeEnvironment(
const sandboxName = typeof metadata.sandboxName === "string" && metadata.sandboxName.trim().length > 0
? metadata.sandboxName.trim()
: null;
details = { provider, providerLeaseId: leaseRecord.lease.providerLeaseId, leaseId: leaseRecord.lease.id,
leasePolicy: leaseRecord.lease.leasePolicy, metadata, platformQualificationRecorded: false,
platformQualificationDiagnostic: "An immutable OCI image digest is required; this connection probe does not qualify a mutable image or snapshot name." };
if (isImmutableImage(metadata.image) && !metadata.snapshot) {
try {
const binding = await sandboxBinding(db, environment, resolvedCompanyId);
if (binding && metadata.image === binding.image) {
const platform = await runtime.execute({ environment: probeEnvironmentConfig, lease: leaseRecord.lease,
command: "sh", args: ["-c", SANDBOX_PLATFORM_PROBE_COMMAND],
bypassSession: true, timeoutMs: 10_000, commandTimeoutMs: 10_000 });
if (platform.exitCode !== 0) throw new Error("platform_probe_failed");
const facts = parseSandboxPlatformFacts(platform.stdout);
details.platform = facts;
if (facts.platform !== "unknown" && facts.architecture !== "unknown"
&& (facts.platform !== "linux" || facts.libc !== "unknown")
&& (facts.libc !== "glibc" || /^(?:glibc\s+)?\d+\.\d+(?:\.\d+)?$/.test(facts.glibcVersion ?? ""))) {
qualification = { v: 1, companyId: resolvedCompanyId, ...binding, facts, probedAt: new Date().toISOString() };
delete details.platformQualificationDiagnostic;
} else details.platformQualificationDiagnostic = "The image's OS, architecture or libc could not be identified. Legacy remains the creation default.";
} else details.platformQualificationDiagnostic = "The live lease does not match the selected immutable image configuration. Probe again.";
} catch {
details.platformQualificationDiagnostic = "Platform qualification could not complete; connection readiness does not establish native compatibility. Check the image and probe again.";
}
}
return {
ok: true,
driver: "sandbox",
summary: sandboxName
? `Connected to ${provider} sandbox ${sandboxName}.`
: `Connected to ${provider} sandbox environment.`,
details: {
provider,
providerLeaseId: leaseRecord.lease.providerLeaseId,
leaseId: leaseRecord.lease.id,
leasePolicy: leaseRecord.lease.leasePolicy,
metadata,
},
details,
};
} catch (error) {
releaseStatus = "failed";
@@ -122,11 +196,25 @@ export async function probeEnvironment(
if (leaseRecord) {
const driver = runtime.getDriver(environment.driver);
try {
await driver?.releaseRunLease({
const releasedLease = await driver?.releaseRunLease({
environment: probeEnvironmentConfig,
lease: leaseRecord.lease,
status: releaseStatus,
});
if (qualification && driver && savedSandboxProbe
&& releasedLease?.id === leaseRecord.lease.id
&& releasedLease.companyId === resolvedCompanyId
&& releasedLease.environmentId === environment.id
&& releasedLease.status === "released" && releasedLease.cleanupStatus === "success") {
const recorded = await environments.recordSandboxPlatformProbe({ environmentId: environment.id, companyId: savedSandboxProbe,
config: environment.config, proof: qualification });
if (details) {
details.platformQualificationRecorded = recorded;
if (!recorded) details.platformQualificationDiagnostic = "The environment changed during the probe. Probe its current configuration again.";
}
} else if (qualification && details) {
details.platformQualificationDiagnostic = "The probe lease cleanup could not be verified. Resolve pending cleanup and probe again.";
}
} catch (releaseError) {
// Cleanup failures must not mask the connection result shown to
// the operator, but a leaked sandbox should still be traceable.
+37 -10
View File
@@ -1,5 +1,7 @@
import { randomUUID } from "node:crypto";
import { and, desc, eq, inArray, isNull, ne, sql } from "drizzle-orm";
import { SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "./environment-custom-image-runtime.js";
import { and, desc, eq, inArray, isNull, ne, sql, type SQL } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
agents,
@@ -859,6 +861,21 @@ export function environmentService(db: Db) {
};
return {
recordSandboxPlatformProbe: async (input: {
environmentId: string;
companyId: string;
config: Record<string, unknown>;
proof: Record<string, unknown> | null;
}): Promise<boolean> => {
const existing = sql`coalesce(${environments.metadata}->${SANDBOX_PLATFORM_PROBE_METADATA_KEY}, '{}'::jsonb)`;
const probes = input.proof === null
? sql`${existing} - ${input.companyId}`
: sql`${existing} || ${JSON.stringify({ [input.companyId]: input.proof })}::jsonb`;
const rows = await db.update(environments).set({
metadata: sql`jsonb_set(coalesce(${environments.metadata}, '{}'::jsonb), ARRAY[${SANDBOX_PLATFORM_PROBE_METADATA_KEY}]::text[], ${probes}, true)`,
}).where(and(eq(environments.id, input.environmentId), eq(environments.driver, "sandbox"), eq(environments.config, input.config))).returning({ id: environments.id });
return rows.length > 0;
},
list: async (
companyIdOrFilters?: string | EnvironmentListFilters,
maybeFilters?: EnvironmentListFilters,
@@ -1118,20 +1135,30 @@ export function environmentService(db: Db) {
if ("envVars" in patch && patch.envVars !== undefined) {
values.envVars = (patch.envVars ?? {}) as Record<string, unknown>;
}
const changesTarget = patch.config !== undefined || patch.driver !== undefined;
let metadataUpdate: SQL | undefined;
if (patch.metadata !== undefined) {
values.metadata = withoutManagedEnvironmentArchiveToken(patch.metadata ?? null);
} else if (patch.status !== undefined) {
const existingMetadata = await writeDb
.select({ metadata: environments.metadata })
.from(environments)
.where(eq(environments.id, id))
.then((rows) => rows[0]?.metadata ?? null);
values.metadata = withoutManagedEnvironmentArchiveToken(existingMetadata);
const userMetadata = withoutManagedEnvironmentArchiveToken(patch.metadata ?? null);
if (userMetadata) delete userMetadata[SANDBOX_PLATFORM_PROBE_METADATA_KEY];
if (changesTarget) {
values.metadata = userMetadata;
} else {
const replacement = userMetadata === null ? sql`null::jsonb` : sql`${JSON.stringify(userMetadata)}::jsonb`;
// Preserve the current private subtree in the same statement. A route's
// earlier read must never replace a newer concurrent probe receipt.
metadataUpdate = sql`case when ${environments.metadata} ? ${SANDBOX_PLATFORM_PROBE_METADATA_KEY}
then coalesce(${replacement}, '{}'::jsonb) || jsonb_build_object(
${SANDBOX_PLATFORM_PROBE_METADATA_KEY}::text, ${environments.metadata}->${SANDBOX_PLATFORM_PROBE_METADATA_KEY})
else ${replacement} end`;
}
} else if (changesTarget || patch.status !== undefined) {
metadataUpdate = sql`${environments.metadata} - ${MANAGED_ENVIRONMENT_ARCHIVE_TOKEN_METADATA_KEY}`;
if (changesTarget) metadataUpdate = sql`${metadataUpdate} - ${SANDBOX_PLATFORM_PROBE_METADATA_KEY}`;
}
const row = await writeDb
.update(environments)
.set(values)
.set({ ...values, ...(metadataUpdate ? { metadata: metadataUpdate } : {}) })
.where(eq(environments.id, id))
.returning()
.then((rows) => rows[0] ?? null)
+13
View File
@@ -111,6 +111,19 @@ registry therefore discovers that row through the public environments API.
This still provides full isolation because every cell starts a new Paperclip
instance and database.
Native-generation Codex on Daytona qualifies its immutable image through one
existing saved-environment probe before agent creation. Automatic Codex requests
also inspect the scoped server discovery default. Legacy and other provider
fixtures keep their existing setup. The probe uses no provider turn, permits one
setup lease per cell, has a 330-second HTTP deadline and a 10-second platform
command budget, and performs no retry. It uses the existing archive/release and
provider-expiry cleanup policy. The returned owned probe lease joins task leases
in the existing runtime-cost estimator; unrelated lease IDs do not gain task
attribution. Failed setup before fixture completion retains unavailable/unknown
cost coverage and its holds. A connection-only probe or name-only snapshot cannot
qualify native defaults.
Daytona creates sandbox environments through the public API. The core fixture
keeps `reuseLease:false` and `runnerLifecycleMode:"per_turn"`. The dedicated
warm-continuity fixture uses `reuseLease:true` and
+99 -7
View File
@@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import type { RunnerApi } from "./api.js";
import { buildRuntimeUsage } from "./billing.js";
import { runnerMatrix } from "./catalog.js";
import { setupLiveFixtures } from "./live-fixtures.js";
import { setupLiveFixtures, selectFixtureRuntimeLeases } from "./live-fixtures.js";
describe("live runner fixtures", () => {
it.each(["runner-codex", "runner-acpx-claude", "runner-opencode"].flatMap(profile =>
@@ -163,7 +164,7 @@ describe("live runner fixtures", () => {
},
);
it("installs the Daytona provider through the public API before creating its environment", async () => {
it.each(["legacy-codex", "runner-acpx-claude", "runner-opencode"])("installs Daytona before its %s environment without adding a Codex qualification probe", async profile => {
const calls: string[] = [];
const api = {
async post(path: string, data?: Record<string, unknown>) {
@@ -186,7 +187,7 @@ describe("live runner fixtures", () => {
}
if (path.endsWith("/environments")) {
expect(calls).toContain("POST /api/plugins/install");
return { id: "environment-1", driver: "sandbox" };
return { id: "environment-1", driver: "sandbox", config: data?.config };
}
if (path.endsWith("/agents")) {
return { id: "agent-1", name: "Agent", companyId: "company-1" };
@@ -204,7 +205,7 @@ describe("live runner fixtures", () => {
const execution = runnerMatrix.find(
(candidate) =>
candidate.id ===
"core-compatibility.legacy-codex.daytona.message-marker",
`core-compatibility.${profile}.daytona.message-marker`,
);
expect(execution).toBeDefined();
@@ -214,7 +215,7 @@ describe("live runner fixtures", () => {
executionNonce: "nonce",
workspacePath: "/tmp/workspace",
credentials: {
OPENAI_API_KEY: "openai-test-value",
[execution!.profile.credential]: "provider-test-value",
DAYTONA_API_KEY: "daytona-test-value",
},
daytonaImage:
@@ -224,12 +225,99 @@ describe("live runner fixtures", () => {
expect(calls.indexOf("POST /api/plugins/install")).toBeLessThan(
calls.indexOf("POST /api/companies/company-1/environments"),
);
expect(calls.some(call => call.includes("/probe"))).toBe(false);
await fixtures.teardown();
expect(calls).toContain(
"DELETE /api/environments/environment-1?destroyReusableSandboxLeases=true",
);
});
it.each(["explicit", "auto"])("qualifies the immutable Codex image once before %s agent creation", async mode => {
const original = runnerMatrix.find(row => row.id === "core-compatibility.runner-codex.daytona.message-marker")!;
const execution = { ...original, profile: { ...original.profile, buildAgent(input: Parameters<typeof original.profile.buildAgent>[0]) {
const payload = original.profile.buildAgent(input);
return { ...payload, adapterType: mode === "auto" ? "codex_local" : "paperclip_runner",
runner: mode === "auto" ? "auto" : "paperclip" };
} } };
const calls: string[] = [];
const api = {
async get(path: string) {
calls.push(`GET ${path}`);
return [{ type: "codex_local", defaultRunner: "paperclip", supportedRunners: ["paperclip", "legacy"] }];
},
async post(path: string, data: any, options: any) {
calls.push(`POST ${path}`);
if (path === "/api/plugins/install") return { id: "plugin", status: "ready" };
if (path === "/api/companies") return { id: "company", name: "Fixture" };
if (path.endsWith("/environments")) return { id: "environment", driver: "sandbox", config: data.config };
if (path === "/api/environments/environment/probe?companyId=company") {
expect(options).toEqual({ timeout: 330_000 });
return { ok: true, details: { platformQualificationRecorded: true, leaseId: "setup-probe" } };
}
if (path.endsWith("/agents")) {
expect(calls).toContain("POST /api/environments/environment/probe?companyId=company");
expect(data.adapterType).toBe(mode === "auto" ? "codex_local" : "paperclip_runner");
expect(data.runner).toBe(mode === "auto" ? "auto" : "paperclip");
if (mode === "auto") expect(calls).toContain("GET /api/adapters?companyId=company&environmentId=environment");
return { id: "agent", companyId: "company", ...data };
}
throw new Error(`Unexpected POST ${path}`);
},
async postSensitive() { return { id: "secret" }; },
async delete() {},
} as unknown as RunnerApi;
const fixtures = await setupLiveFixtures({ api, execution, executionNonce: "nonce", workspacePath: "/tmp/fixture",
credentials: { OPENAI_API_KEY: "fixture-key", DAYTONA_API_KEY: "fixture-provider-key" },
daytonaImage: `registry.example.test/runtime@sha256:${"a".repeat(64)}` });
expect(fixtures.setupProbe).toMatchObject({ leaseId: "setup-probe", ...(mode === "auto" ? { defaultRunner: "paperclip" } : {}) });
expect(calls.filter(call => call.includes("/probe?"))).toHaveLength(1);
expect(calls.filter(call => call.startsWith("GET"))).toHaveLength(mode === "auto" ? 1 : 0);
await fixtures.teardown();
});
it.each(["probe-failed", "not-recorded", "wrong-default"])("stops before automatic Codex creation when image qualification is %s", async failure => {
const original = runnerMatrix.find(row => row.id === "core-compatibility.runner-codex.daytona.message-marker")!;
const execution = { ...original, profile: { ...original.profile, buildAgent(input: Parameters<typeof original.profile.buildAgent>[0]) {
return { ...original.profile.buildAgent(input), adapterType: "codex_local", runner: "auto" };
} } };
const calls: string[] = [];
const api = {
async get() { return [{ type: "codex_local", defaultRunner: "legacy", supportedRunners: ["legacy"] }]; },
async post(path: string, data: any) {
calls.push(path);
if (path === "/api/plugins/install") return { id: "plugin", status: "ready" };
if (path === "/api/companies") return { id: "company", name: "Fixture" };
if (path.endsWith("/environments")) return { id: "environment", driver: "sandbox", config: data.config };
if (path.includes("/probe?")) return { ok: failure !== "probe-failed",
details: { platformQualificationRecorded: failure !== "not-recorded", leaseId: "setup-probe" } };
throw new Error(`Unexpected POST ${path}`);
},
async postSensitive() { return { id: "secret" }; },
async delete(path: string) { calls.push(`DELETE ${path}`); },
} as unknown as RunnerApi;
await expect(setupLiveFixtures({ api, execution, executionNonce: "nonce", workspacePath: "/tmp/fixture",
credentials: { OPENAI_API_KEY: "fixture-key", DAYTONA_API_KEY: "fixture-provider-key" },
daytonaImage: `registry.example.test/runtime@sha256:${"a".repeat(64)}` })).rejects.toThrow(failure === "wrong-default" ? "server-owned default" : "not recorded");
expect(calls.some(call => call.endsWith("/agents"))).toBe(false);
expect(calls.filter(call => call.includes("/probe?"))).toHaveLength(1);
expect(calls).toContain("DELETE /api/environments/environment?destroyReusableSandboxLeases=true");
});
it("includes the owned setup probe in existing runtime estimates while excluding unrelated leases", () => {
const lease = (id: string, duration: number, heartbeatRunId?: string) => ({ id, provider: "daytona", heartbeatRunId,
acquiredAt: "2026-10-12T00:00:00Z", releasedAt: new Date(Date.parse("2026-10-12T00:00:00Z") + duration).toISOString(),
metadata: { cpu: 4, memory: 4, disk: 10 } });
const leases = [lease("task", 20_000, "selected-run"), lease("setup-probe", 10_000), lease("unrelated", 600_000, "other-run")];
const selected = selectFixtureRuntimeLeases({ leases, runIds: ["selected-run"], setupProbeLeaseId: "setup-probe" });
expect(selected.map(value => value.id)).toEqual(["task", "setup-probe"]);
const usage = buildRuntimeUsage({ environmentId: "daytona", runs: [], leases: selected });
expect(usage.leaseCount).toBe(2);
expect(usage.leaseDurationMs).toBe(30_000);
expect(usage.costStatus).toBe("estimated");
expect(usage.estimatedListCostUsd).toBeGreaterThan(0);
expect(selectFixtureRuntimeLeases({ leases, runIds: ["selected-run"] }).map(value => value.id)).toEqual(["task"]);
});
it("creates a primary project workspace for reusable Daytona scope", async () => {
const calls: string[] = [];
const api = {
@@ -246,11 +334,14 @@ describe("live runner fixtures", () => {
return { id: "company-1", name: "Runner E2E" };
}
if (path.endsWith("/environments")) {
return { id: "environment-1", driver: "sandbox" };
return { id: "environment-1", driver: "sandbox", config: data?.config };
}
if (path.endsWith("/agents")) {
return { id: "agent-1", name: "Agent", companyId: "company-1" };
}
if (path.startsWith("/api/environments/environment-1/probe?")) {
return { ok: true, details: { platformQualificationRecorded: true, leaseId: "setup-probe" } };
}
if (path.endsWith("/projects")) {
expect(data).toMatchObject({
executionWorkspacePolicy: {
@@ -275,6 +366,7 @@ describe("live runner fixtures", () => {
}
throw new Error(`Unexpected POST ${path}`);
},
async get() { return [{ type: "codex_local", defaultRunner: "paperclip", supportedRunners: ["paperclip", "legacy"] }]; },
async postSensitive(_path: string, data?: Record<string, unknown>) {
return { id: `secret-${String(data?.key).toLowerCase()}` };
},
@@ -292,7 +384,7 @@ describe("live runner fixtures", () => {
executionNonce: "nonce",
workspacePath: "/tmp/workspace",
credentials: {
OPENAI_API_KEY: "openai-test-value",
[execution!.profile.credential]: "provider-test-value",
DAYTONA_API_KEY: "daytona-test-value",
},
daytonaImage:
+47
View File
@@ -4,6 +4,7 @@ import path from "node:path";
import { installedReleaseDaytonaPlugin } from "./installed-release.js";
import { isManagedHiringCase } from "./chat-cases.js";
import { FixtureRegistry } from "./fixture-registry.js";
import { isImmutableDaytonaImage } from "./catalog.js";
import { TASK_TITLE_BUDGET_CENTS } from "./task-titles.js";
import { stageGrokSubscriptionFixture } from "./grok-subscription-fixture.js";
import type { RunnerApi } from "./api.js";
@@ -63,6 +64,8 @@ export interface LiveFixtureValues {
agent: AgentRecord;
project?: ProjectRecord;
aiConnection?: ManagedAccountFixture;
/** Private setup receipt; its lease is included in existing runtime cost accounting. */
setupProbe?: { leaseId: string; durationMs: number; defaultRunner?: string };
onboardingRuntime?: {
mode: "production-wizard" | "post-onboarding-runtime-switch";
@@ -79,6 +82,19 @@ function value<T>(resolved: ReadonlyMap<string, unknown>, id: string): T {
return result as T;
}
/** Preserve task/run attribution and include only this fixture's explicit setup probe. */
export function selectFixtureRuntimeLeases<T extends {
id: string; provider?: string | null; heartbeatRunId?: string | null; issueId?: string | null;
}>(input: { leases: T[]; runIds: string[]; issueId?: string; setupProbeLeaseId?: string }): T[] {
const selectedRunIds = new Set(input.runIds);
const relevant = input.leases.filter(lease => lease.provider === "daytona" && (
lease.id === input.setupProbeLeaseId || selectedRunIds.size === 0
|| (lease.heartbeatRunId && selectedRunIds.has(lease.heartbeatRunId))
|| (input.issueId && lease.issueId === input.issueId)
));
return relevant.length > 0 ? relevant : input.leases;
}
async function deleteDaytonaEnvironment(api: RunnerApi, environmentId: string) {
const deadlineAt = Date.now() + 120_000;
let lastError: unknown;
@@ -111,6 +127,7 @@ export async function setupLiveFixtures(input: {
}): Promise<LiveFixtureValues> {
const { api, execution } = input;
const registry = new FixtureRegistry();
let setupProbe: LiveFixtureValues["setupProbe"];
if (execution.environment.id === "daytona") {
registry.register<PluginRecord>({
@@ -295,6 +312,35 @@ export async function setupLiveFixtures(input: {
secretRefs,
executionId: input.executionNonce,
});
const autoCodex = agent.adapterType === "codex_local" && (agent.runner === undefined || agent.runner === "auto");
const nativeCodex = agent.adapterType === "paperclip_runner" && (agent.adapterConfig as Record<string, unknown>).provider === "codex";
if (environment.driver === "sandbox" && execution.profile.generation === "native"
&& execution.profile.provider === "codex" && agent.runner !== "legacy" && (autoCodex || nativeCodex)) {
if (!isImmutableDaytonaImage(String(environment.config?.image ?? "")) || environment.config?.snapshot) {
throw new Error("Native Codex qualification requires this fixture's immutable image-backed environment.");
}
const startedAt = Date.now();
// One existing explicit connection probe: a fresh lease, bounded platform read,
// verified release, and server-owned image evidence before agent creation.
const probe = await api.post<{ ok: boolean; details?: { platformQualificationRecorded?: boolean; leaseId?: string } }>(
`/api/environments/${environment.id}/probe?companyId=${encodeURIComponent(company.id)}`,
{}, { timeout: 330_000 },
);
if (probe.ok !== true || probe.details?.platformQualificationRecorded !== true || !probe.details.leaseId) {
throw new Error("Native Codex image qualification was not recorded; agent creation was not attempted.");
}
setupProbe = { leaseId: probe.details.leaseId, durationMs: Date.now() - startedAt };
if (autoCodex) {
const adapters = await api.get<Array<{ type: string; defaultRunner?: string; supportedRunners?: string[] }>>(
`/api/adapters?companyId=${encodeURIComponent(company.id)}&environmentId=${encodeURIComponent(environment.id)}`,
);
const selected = adapters.find(adapter => adapter.type === "codex_local");
if (selected?.defaultRunner !== "paperclip" || !selected.supportedRunners?.includes("paperclip")) {
throw new Error("The qualified image does not advertise native Codex as its server-owned default.");
}
setupProbe.defaultRunner = selected.defaultRunner;
}
}
if (["stock-harness", "native-connection-guidance"].includes(execution.suite.id)
|| (execution.suite.id === "everyday-workflows" && ["hire-reuse", "delegate-feedback"].includes(execution.task.id))) {
agent.budgetMonthlyCents = 1_000;
@@ -362,6 +408,7 @@ export async function setupLiveFixtures(input: {
secretRefs: value<SecretReferenceMap>(setup.values, "secrets"),
environment: value<EnvironmentRecord>(setup.values, "environment"),
agent: value<AgentRecord>(setup.values, "agent"),
...(setupProbe ? { setupProbe } : {}),
...(setup.values.has("project")
? { project: value<ProjectRecord>(setup.values, "project") }
: {}),
+6 -11
View File
@@ -51,7 +51,7 @@ import { runnerExecutionById } from "./catalog.js";
import { classifyFailure } from "./failure-classifier.js";
import { runnerE2EServerControlPaths } from "./harness-env.js";
import { setupConnectionReview } from "./connection-reviews.js";
import { setupLiveFixtures, type LiveFixtureValues } from "./live-fixtures.js";
import { setupLiveFixtures, selectFixtureRuntimeLeases, type LiveFixtureValues } from "./live-fixtures.js";
import { evaluateMatcher, persistedFinalRunMessage, type MatcherResult } from "./matchers.js";
import { readRegisteredArtifacts } from "./registered-artifact.js";
import {
@@ -695,16 +695,9 @@ for (const execution of executions) {
const listed = await api.get<EnvironmentLeaseRecord[]>(
`/api/environments/${fixtures.environment.id}/leases`,
);
const selectedRunIds = new Set(selectedRuns.map((run) => run.id));
const relevant = listed.filter(
(lease) =>
lease.provider === "daytona" &&
(selectedRunIds.size === 0 ||
(lease.heartbeatRunId &&
selectedRunIds.has(lease.heartbeatRunId)) ||
(issue && lease.issueId === issue.id)),
);
runtimeLeases = relevant.length > 0 ? relevant : listed;
runtimeLeases = selectFixtureRuntimeLeases({ leases: listed,
runIds: selectedRuns.map(run => run.id), issueId: issue?.id,
setupProbeLeaseId: fixtures.setupProbe?.leaseId });
};
const cancelActiveRunsForCleanup = async () => {
@@ -969,6 +962,8 @@ for (const execution of executions) {
companyId: fixtures.company.id,
environmentId: fixtures.environment.id,
agentId: fixtures.agent.id,
...(fixtures.setupProbe ? { setupProbe: { durationMs: fixtures.setupProbe.durationMs,
...(fixtures.setupProbe.defaultRunner ? { defaultRunner: fixtures.setupProbe.defaultRunner } : {}) } } : {}),
secretIds: Object.fromEntries(
Object.entries(fixtures.secretRefs).map(([name, ref]) => [
name,
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { createRoot } from "react-dom/client";
import { act } from "react";
import { act, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
@@ -9,6 +9,9 @@ import { beforeAll, describe, expect, it } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { CodexLocalConfigFields } from "./config-fields";
import { buildCodexRunnerConfig } from "./config-fields";
import { defaultCreateValues } from "@/components/agent-config-defaults";
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
beforeAll(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); });
@@ -51,6 +54,47 @@ async function renderRunner(config: Record<string, unknown>, expand?: string, in
}
describe("Paperclip Runner Codex configuration", () => {
it.each(["", "--sandbox,read-only"])("shows actual automatic Legacy policy and preserves the explicit toggle (%s)", async (extraArgs) => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
let saved: typeof defaultCreateValues = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto", extraArgs };
function Fixture() {
const [values, setValues] = useState(saved);
saved = values;
return <CodexLocalConfigFields mode="create" isCreate adapterType="codex_local" values={values}
set={(patch) => setValues(current => ({ ...current, ...patch }))} config={{}}
eff={(_group, _field, original) => original} mark={() => undefined} models={[]} hideInstructionsFile />;
}
try {
await act(async () => root.render(<QueryClientProvider client={new QueryClient()}><TooltipProvider><Fixture /></TooltipProvider></QueryClientProvider>));
const toggle = container.querySelector<HTMLButtonElement>('[aria-label="Bypass sandbox"]');
expect(toggle).toBeDefined();
expect(toggle?.getAttribute("aria-checked")).toBe(String(resolveCodexLocalCliPolicy(buildCodexRunnerConfig(saved)).bypass));
await act(async () => toggle!.click());
const config = buildCodexRunnerConfig(saved);
expect(saved.runner).toBe("auto");
expect(config.dangerouslyBypassApprovalsAndSandbox).toBe(extraArgs !== "");
expect(toggle?.getAttribute("aria-checked")).toBe(String(resolveCodexLocalCliPolicy(config).bypass));
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it.each([
{},
{ dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true },
{ dangerouslyBypassSandbox: false },
{ extraArgs: ["--sandbox", "read-only"] },
])("shows the actual saved Legacy CLI policy while editing %j", async config => {
await renderMarkup(<TooltipProvider><CodexLocalConfigFields mode="edit" isCreate={false}
adapterType="codex_local" values={null} set={null} config={config}
eff={(_group, _field, original) => original} mark={() => undefined} models={[]} hideInstructionsFile />
</TooltipProvider>, undefined, container => {
expect(container.querySelector('[aria-label="Bypass sandbox"]')?.getAttribute("aria-checked"))
.toBe(String(resolveCodexLocalCliPolicy(config).bypass));
});
});
it("edits the legacy execution engine through the shared keyboard picker", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
+30 -10
View File
@@ -3,6 +3,8 @@ import { AdapterMark } from "../../components/AdapterMark";
import { DotRunnerConnection } from "../../components/DotRunnerConnection";
import { configFieldsForSection } from "../config-sections";
import type { AdapterConfigFieldsProps } from "../types";
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildCodexLocalConfig } from "@paperclipai/adapter-codex-local/ui";
import {
Field,
ToggleField,
@@ -17,6 +19,7 @@ import {
CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS,
isCodexLocalFastModeSupported,
isCodexLocalManualModel,
resolveCodexLocalCliPolicy,
} from "@paperclipai/adapter-codex-local";
import {
PAPERCLIP_RUNNER_IDLE_TIMEOUT_DEFAULT_MS,
@@ -47,6 +50,23 @@ const runnerHarnessOptions = [
{ value: "grok", label: "Grok Build", adapter: "grok_local" },
];
/** Keep explicit Legacy policy even when the server resolves an automatic choice. */
export function buildCodexRunnerConfig(values: CreateConfigValues): Record<string, unknown> {
const config = buildCodexLocalConfig(values);
if (values.codexLegacySandboxBypass !== undefined) {
config.dangerouslyBypassApprovalsAndSandbox = values.codexLegacySandboxBypass;
} else if (values.runner !== "legacy") {
// An untouched false form default is not an explicit native permission policy.
delete config.dangerouslyBypassApprovalsAndSandbox;
}
return {
...config,
...(values.codexPermissionMode !== undefined ? { codexPermissionMode: values.codexPermissionMode } : {}),
...(values.paperclipRunnerLifecycleMode !== undefined ? { lifecycleMode: values.paperclipRunnerLifecycleMode } : {}),
...(values.paperclipRunnerIdleTimeoutMs !== undefined ? { idleTimeoutMs: values.paperclipRunnerIdleTimeoutMs } : {}),
};
}
export function CodexLocalConfigFields({
section,
companyId,
@@ -149,9 +169,13 @@ export function CodexLocalConfigFields({
const engine =
rawEngine === "acp" || rawEngine === "cli" ? rawEngine : "auto";
const acpSelected = engine === "acp";
const bypassEnabled =
config.dangerouslyBypassApprovalsAndSandbox === true ||
config.dangerouslyBypassSandbox === true;
const bypassEnabled = resolveCodexLocalCliPolicy({
...config,
dangerouslyBypassApprovalsAndSandbox: eff("adapterConfig", "dangerouslyBypassApprovalsAndSandbox", config.dangerouslyBypassApprovalsAndSandbox),
dangerouslyBypassSandbox: eff("adapterConfig", "dangerouslyBypassSandbox", config.dangerouslyBypassSandbox),
extraArgs: eff("adapterConfig", "extraArgs", config.extraArgs),
args: eff("adapterConfig", "args", config.args),
}).bypass;
const fastModeEnabled = isCreate
? Boolean(values!.fastMode)
: eff("adapterConfig", "fastMode", Boolean(config.fastMode));
@@ -753,16 +777,12 @@ export function CodexLocalConfigFields({
hint={help.dangerouslyBypassSandbox}
checked={
isCreate
? values!.dangerouslyBypassSandbox
: eff(
"adapterConfig",
"dangerouslyBypassApprovalsAndSandbox",
bypassEnabled,
)
? resolveCodexLocalCliPolicy(buildCodexRunnerConfig(values!)).bypass
: bypassEnabled
}
onChange={(v) =>
isCreate
? set!({ dangerouslyBypassSandbox: v })
? set!({ dangerouslyBypassSandbox: v, codexLegacySandboxBypass: v })
: mark(
"adapterConfig",
"dangerouslyBypassApprovalsAndSandbox",
+2 -13
View File
@@ -1,22 +1,11 @@
import type { UIAdapterModule } from "../types";
import { parseCodexStdoutLine } from "@paperclipai/adapter-codex-local/ui";
import { CodexLocalConfigFields } from "./config-fields";
import { buildCodexLocalConfig } from "@paperclipai/adapter-codex-local/ui";
import { CodexLocalConfigFields, buildCodexRunnerConfig } from "./config-fields";
export const codexLocalUIAdapter: UIAdapterModule = {
type: "codex_local",
label: "Codex",
parseStdoutLine: parseCodexStdoutLine,
ConfigFields: CodexLocalConfigFields,
buildAdapterConfig: (values) => {
const config = buildCodexLocalConfig(values);
// Legacy permission flags cannot describe the native provider policy.
if (values.runner !== "legacy") delete config.dangerouslyBypassApprovalsAndSandbox;
return {
...config,
...(values.codexPermissionMode !== undefined ? { codexPermissionMode: values.codexPermissionMode } : {}),
...(values.paperclipRunnerLifecycleMode !== undefined ? { lifecycleMode: values.paperclipRunnerLifecycleMode } : {}),
...(values.paperclipRunnerIdleTimeoutMs !== undefined ? { idleTimeoutMs: values.paperclipRunnerIdleTimeoutMs } : {}),
};
},
buildAdapterConfig: buildCodexRunnerConfig,
};
@@ -660,6 +660,39 @@ async function flushUntil(check: () => boolean, timeoutMs = 4000) {
describe("AgentConfigForm environment selector", () => {
let roots: Root[] = [];
it("keeps an explicit Codex draft policy until a harness reset clears it", async () => {
mockEnvironmentsApi.list.mockResolvedValue([]);
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
let current = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto" as const,
dangerouslyBypassSandbox: false, codexLegacySandboxBypass: false };
function Fixture() {
const [values, setValues] = useState<typeof defaultCreateValues>(current);
current = values as typeof current;
return <AgentConfigForm mode="create" values={values}
onChange={patch => setValues(prev => ({ ...prev, ...patch }))} showAdapterTypeField hidePromptTemplate />;
}
try {
await act(async () => root.render(<QueryClientProvider client={queryClient}><ToastProvider><TooltipProvider>
<Fixture />
</TooltipProvider></ToastProvider></QueryClientProvider>));
await flushReact();
expect(current.codexLegacySandboxBypass).toBe(false);
for (const harness of ["claude_local", "codex_local"]) {
await clickElement(container.querySelector('[aria-label="Harness"]'));
await clickElement(document.querySelector(`[role="option"][data-value="${harness}"]`));
await flushReact();
expect(current.adapterType).toBe(harness);
expect(current).toHaveProperty("codexLegacySandboxBypass", undefined);
}
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it.each(["codex_local", "paperclip_runner"])("keeps a saved %s runner during unrelated edits", async adapterType => {
const adapterConfig = { model: "gpt-5.6-sol", ...(adapterType === "paperclip_runner" ? { provider: "codex", reasoningEffort: "high" } : { modelReasoningEffort: "high" }) };
const result = await renderForm([], { adapterType, adapterConfig }, { showAdapterTypeField: true });
@@ -12,6 +12,7 @@ export const defaultCreateValues: CreateConfigValues = {
search: false,
fastMode: false,
dangerouslyBypassSandbox: false,
codexLegacySandboxBypass: undefined,
command: "",
args: "",
extraArgs: "",
@@ -3,8 +3,17 @@ import { describe, expect, it } from "vitest";
import { buildNewAgentHirePayload } from "./new-agent-hire-payload";
import { defaultCreateValues } from "../components/agent-config-defaults";
import { codexLocalUIAdapter } from "../adapters/codex-local";
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
describe("buildNewAgentHirePayload", () => {
it.each([false, true])("preserves explicit automatic Legacy sandbox choice %s through the actual CLI policy", (bypass) => {
const values = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto" as const, dangerouslyBypassSandbox: bypass, codexLegacySandboxBypass: bypass };
const payload = buildNewAgentHirePayload({ name: "Codex", effectiveRole: "general", configValues: values, adapterConfig: codexLocalUIAdapter.buildAdapterConfig(values) });
expect(payload.runner).toBe("auto");
expect(payload.adapterConfig.dangerouslyBypassApprovalsAndSandbox).toBe(bypass);
expect(resolveCodexLocalCliPolicy(payload.adapterConfig).bypass).toBe(bypass);
expect(payload.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass");
});
it.each([undefined, "auto", "paperclip", "legacy"] as const)("keeps runner %s separate from Codex execution settings", (runner) => {
const values = { ...defaultCreateValues, adapterType: "codex_local", runner, model: "gpt-5.6-sol", thinkingEffort: "high" };
const payload = buildNewAgentHirePayload({ name: "Codex", effectiveRole: "general", configValues: values, adapterConfig: codexLocalUIAdapter.buildAdapterConfig(values) });
+32
View File
@@ -1269,6 +1269,38 @@ describe("CompanyImport", () => {
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig);
});
it.each([
{ dangerouslyBypassApprovalsAndSandbox: false },
{ dangerouslyBypassSandbox: false },
{ dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true },
])("keeps saved restrictive Legacy policy during an automatic import model edit (%j)", async policy => {
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
mockAdaptersApi.list.mockResolvedValue([
{ type: "codex_local", disabled: false, supportedRunners: ["legacy"], defaultRunner: "legacy" },
]);
const preview = buildPreviewResult();
preview.manifest.agents[0]!.adapterType = "codex_local";
preview.manifest.agents[0]!.adapterConfig = { model: "gpt-5.6-sol", ...policy };
mockCompaniesApi.importPreview.mockResolvedValue(preview);
await renderPage();
await enterGithubUrl();
await clickButton(text => text === "Preview import");
await clickButton(text => text.includes("configure adapter"));
await clickButton(text => text.includes("Advanced"));
expect(container.querySelector('[aria-label="Bypass sandbox"]')?.getAttribute("aria-checked")).toBe("false");
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Model"]')!.click());
const defaultModel = [...document.querySelectorAll<HTMLButtonElement>('[role="option"]')].find(option => option.textContent?.trim() === "Default");
expect(defaultModel).toBeTruthy();
await act(async () => defaultModel!.click());
await clickButton(text => text.startsWith("Import 3 file"));
await settle();
expect(lastImportMeta().adapterOverrides?.coder).toMatchObject({
adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false },
});
expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("model");
expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass");
});
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();
+1
View File
@@ -585,6 +585,7 @@ function importAdapterValues(
...(sourceConfig && choice === "codex_local" ? {
model: typeof sourceConfig.model === "string" ? sourceConfig.model : "",
thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""),
...(typeof savedBypass === "boolean" ? { codexLegacySandboxBypass: savedBypass } : {}),
// Runner-only imports preserve server-translated policy; display that policy too.
dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass
: nativeCodex