fix: preserve release artifacts during dev builds and display imported policy

Keep source-built daemons separate from source-bound release targets and select
fresh compatible source output only inside the development supervisor. Preserve
release manifests, running executable inodes, and explicit operator overrides.

Show the existing translated Codex permission policy in import controls while
preserving runner-only override payloads. Record real Apple Silicon task and
paused import proof alongside exact qualification sources.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 09:32:28 -05:00
1 parent d522822a5d
commit 058af8f1b6
8 files changed
+240 -72

No files matched your search

+70 -10
View File
@@ -14,20 +14,24 @@ an attended Intel provider journey is not required. Reuse the healthy test drive
and the existing Product E2E helpers rather than create another test system.
Keep the current PR and branch: [#15683](https://github.com/paperclipai/paperclip/pull/15683),
`codex/codex-runner-default`. The last pushed source is
`dc7304d7c4db1c9c9ee2ed80a98d004371372771`; its ordinary CI is green.
`codex/codex-runner-default`. The Mac qualification source is
`d522822a5daeb72d124360b576b82a8bb52b43f4`; its
[ordinary CI](https://github.com/paperclipai/paperclip/actions/runs/38259205327)
is green. Fresh automated review is 5/5. The development release-manifest
finding is fixed with focused tests and awaits fresh final-head review.
Earlier Linux-only source `6ca302426b768791b499d2d563bbca4276f2c908` had fresh
5/5 review and eight resolved threads. That historical review does not qualify
the macOS amendment. Master integration conflicts are resolved.
The [test-drive server](http://127.0.0.1:3125/COD/agents/all) is running through
the normal dev supervisor at the pushed source. Health and bootstrap are ready;
the actual listener belongs to this worktree. The preserved Codex QA agent is
idle on `codex_local`, with its saved unrelated title edit intact. Dependencies
the normal dev supervisor at the Mac qualification source. Health and bootstrap
are ready; the actual listener belongs to this worktree. The preserved legacy
and new native Codex QA agents are idle, with unrelated title edits intact. Dependencies
and ignored configuration had been cleared, so startup restored them and the
normal CLI signing keys while retaining the database and encrypted account key.
The existing cached development daemon is from a different Rust tree. It avoids
a local Rust build and is not macOS qualification evidence for this candidate.
The initial borrowed development daemon was removed before paid testing.
The production resolver selects the source-bound CI arm64 daemon. Both native
task runs and restart preservation use the verified runtime below.
Implementation ownership:
@@ -91,6 +95,36 @@ and reload preserved complete legacy/native execution digests; the shared runner
picker passed light/dark, mobile and keyboard checks. Screenshots and sanitized
receipts are under `/private/tmp/pr2-f4-mac-ui/`.
The corrected qualifier's exact `d522822` Apple Silicon artifact independently
matches those same bytes and metadata. Artifact `11752770004` passed native
header, source receipt and strict ad-hoc signature checks. Rust tree
`6ca713d5f1f0a059c7f01a37d21f268817e0c93c` and runner TypeScript tree
`ba9744db7fe79301cb0ab442e9546de3282a4bf4` are identical to the paid-task source.
The receipt is `/private/tmp/pr2-d522-mac-remote-daemon/sanitized-verification-receipt.json`.
Restarting the test-drive at `d522822` preserved both complete execution digests,
titles and both native run records, with no additional provider calls.
The deprecated `enableNativeRunner` field is now false on this QA instance.
The native export/import UI journey passes with that field disabled. Import
submission omitted adapter overrides, preserved native Codex/model identity,
and left the new agent paused with its import hold. The explicit Legacy round
trip also passes: the request sends `runner: legacy` without replacing source
configuration, and the saved agent is `codex_local` with the import hold intact.
Both survive reload; the source agent remains unchanged. These imports do not
run setup probes or provider turns.
Evidence is under `/private/tmp/codex-default-dc-import-live/`.
The live import journey found one incorrect policy display. Native Codex's
preserved `never` policy translated to Legacy bypass true, while its checkbox
showed false. A narrow hydration fix uses the existing policy defaults and
canonical booleans; it does not change server permission translation. Against
the pre-fix page, three display cases fail and four safe controls pass. The
fixed page passes 42 component cases and 40 selection controls. The no-provider
preview check passes: the corrected control matches the already-saved paused
agent. Exactly two paused imports were submitted, with no third Apply. The
combined receipt is `/private/tmp/codex-default-dc-import-live/receipt.json`;
it separates `d522822` persistence proof from the corrected UI's content hashes.
[Three-target qualification](https://github.com/paperclipai/paperclip/actions/runs/38252137381)
built all three daemons, including actual Intel Mac. Its clean Linux consumer
passed the 18-package installation and Codex ownership controls, then a Pi probe
@@ -118,9 +152,35 @@ The complete $3 setup/task hold remains reserved as unpriced. No historical
unknown or cleanup hold was released, no automatic retry ran, and no additional
paid Linux campaign started. The aggregate ceiling remains $250.
Next action: commit the verified import, dev-stager and installation-probe fixes
together, then run the existing three-target qualifier once for the frozen head.
Verify restart preservation and update the same PR with its final evidence.
The import, dev-stager and installation-probe fixes are pushed at `d522822`.
The [corrected three-target qualifier](https://github.com/paperclipai/paperclip/actions/runs/38259364638)
has passed all three daemon producers and the real Linux clean installation,
including the corrected Pi probe. Both Mac consumers are checking the same
18 tarballs. Ordinary CI reports 53 successful check runs, Snyk success and four
conditional skips. Fresh review reports 5/5, with one new development-only
manifest issue: rebuilding a host daemon must not corrupt released artifact
identity. Its fix separates local development selection from release assets;
it does not alter the Rust runner, production selector or release assembler.
Fourteen focused controls prove preserved release targets/hash manifest, running
old inodes, correct Mac host selection and future source freshness. Independent
review verifies that the atomic flat publisher and qualified release components
remain unchanged. The actual Mac still selects the verified release target and
does not require Cargo. Receipts are
`/private/tmp/pr2-dev-release-preservation-checkpoint.json` and
`/private/tmp/ci-final-d522-dev-equivalence-audit.json`.
Next action: commit the verified development/import display fixes and this state
record together. Run fresh final-head CI/review in parallel with the remaining
clean package consumers, then record their exact conclusions in the PR evidence
index. Keep `d522822` as the qualification source; subsequent UI/development
checks have their own source identity. Keep exact sources in every receipt.
Do not repeat unchanged paid task turns or qualification components merely for
the development-only fix.
This is the 2026-10-12 source-freeze snapshot. The PR verification section is
the linked evidence index for final CI, review and consumer conclusions. Current
qualification links above retain their literal tested source rather than claim
that an earlier workflow ran on a later development/UI commit.
Managed staging execution/access renewal, code-owner approval and actual
post-merge Cloud composition remain unverified. Prior staging preflight evidence
is retained at [37962635959](https://github.com/paperclipai/paperclip/actions/runs/37962635959)
@@ -1,6 +1,6 @@
import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { chmod, copyFile, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
import { chmod, copyFile, mkdir, mkdtemp, rename, rm } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
@@ -32,15 +32,9 @@ export async function stageBuiltRunnerBinary(packageRoot) {
const executable = process.platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd";
const source = path.join(packageRoot, "runner", "target", "release", executable);
const binaryRoot = path.join(packageRoot, "dist", "bin");
// Source builds are separate from the source-bound release targets and their
// manifest. The dev supervisor selects this fresh output for its server.
await stageRunnerBinary(source, path.join(binaryRoot, executable));
const hostTarget = path.join(binaryRoot, `${process.platform}-${process.arch}`, executable);
const targetStat = await stat(hostTarget).catch(error => {
if (error.code === "ENOENT") return null;
throw error;
});
// The production resolver prefers an existing platform target. A source
// rebuild must refresh it too, using the same atomic, signed publication.
if (targetStat?.isFile()) await stageRunnerBinary(source, hostTarget);
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { execFileSync, spawn } from "node:child_process";
import { once } from "node:events";
import { copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
@@ -8,7 +8,8 @@ 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 { paperclipRunnerBinaryNeedsBuild } from "../../../scripts/dev-runner-native-binary.mjs";
import { paperclipRunnerBinaryNeedsBuild, resolveNativeRunnerDevBinary } from "../../../scripts/dev-runner-native-binary.mjs";
import { resolvePackagedRunnerBinary } from "../src/live/runner-binary.ts";
const contract = { package: { name: "@paperclipai/paperclip-runner", version: "0.0.0" },
contracts: { runnerdArtifact: 2, nativeExecution: 1, harnessDriver: 1 }, prp: { name: "paperclip.runner", minimumVersion: 1, maximumVersion: 3 } };
@@ -109,27 +110,56 @@ test("atomically publishes while the previous executable is running", { skip: pr
});
});
test("a source rebuild refreshes the preferred host target without replacing another architecture", { skip: process.platform === "win32", timeout: 15_000 }, async () => {
test("a source rebuild selects fresh dev bytes while preserving release binaries and their manifest", { skip: process.platform === "win32", timeout: 15_000 }, async () => {
const root = await mkdtemp(path.join(tmpdir(), "runnerd-stage-rebuild-"));
const packageRoot = path.join(root, "packages", "paperclip-runner");
const binaryRoot = path.join(packageRoot, "dist", "bin");
const hostTarget = path.join(binaryRoot, `${process.platform}-${process.arch}`, "paperclip-runnerd");
const otherTarget = path.join(binaryRoot, `${process.platform}-${process.arch === "x64" ? "arm64" : "x64"}`, "paperclip-runnerd");
const source = path.join(packageRoot, "runner", "crates", "runner-core", "src", "main.rs");
const built = path.join(packageRoot, "runner", "target", "release", "paperclip-runnerd");
await withOwnedChildren(root, async start => {
const copyHostFixture = async (input, output) => {
await copyFile(input, output);
if (process.platform === "darwin") {
// System fixtures can be universal; Cargo release daemons are thin.
// Extract only this host slice before exercising the native header gate.
const slices = execFileSync("/usr/bin/lipo", ["-archs", input], { encoding: "utf8" }).trim().split(/\s+/);
const architecture = slices.find(value => process.arch === "arm64" ? value.startsWith("arm64") : value === "x86_64");
assert.ok(architecture);
if (slices.length > 1) execFileSync("/usr/bin/lipo", [input, "-thin", architecture, "-output", output]);
}
};
await mkdir(path.dirname(source), { recursive: true });
await writeFile(source, "fn main() {}\n");
await mkdir(path.dirname(built), { recursive: true });
await copyFile("/bin/echo", built);
await stageRunnerBinary("/bin/sleep", hostTarget);
await stageRunnerBinary("/bin/echo", path.join(binaryRoot, "paperclip-runnerd"));
await mkdir(path.dirname(otherTarget), { recursive: true });
await writeFile(otherTarget, "other architecture artifact");
await copyHostFixture("/bin/echo", built);
const previousBuild = path.join(root, "previous-build");
await copyHostFixture("/bin/sleep", previousBuild);
await stageRunnerBinary(previousBuild, hostTarget);
await stageRunnerBinary(built, path.join(binaryRoot, "paperclip-runnerd"));
const platforms = {}, releaseBytes = new Map();
for (const target of RELEASE_RUNNER_TARGETS) {
const artifact = path.join(binaryRoot, target, "paperclip-runnerd");
if (artifact !== hostTarget) {
const header = Buffer.alloc(64);
if (target === "linux-x64") {
header.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); header.writeUInt16LE(62, 18);
} else { header.writeUInt32LE(0xfeedfacf); header.writeUInt32LE(target === "darwin-x64" ? 0x01000007 : 0x0100000c, 4); }
await mkdir(path.dirname(artifact), { recursive: true });
await writeFile(artifact, header);
}
const bytes = await readFile(artifact);
releaseBytes.set(artifact, bytes);
platforms[target] = { path: `${target}/paperclip-runnerd`, sha256: "sha256:" + createHash("sha256").update(bytes).digest("hex") };
}
const oldTime = new Date(Date.now() - 10_000);
await utimes(hostTarget, oldTime, oldTime);
await utimes(path.join(binaryRoot, "paperclip-runnerd"), oldTime, oldTime);
const oldArtifact = await readFile(hostTarget);
const oldIdentity = await stat(hostTarget);
const manifestPath = path.join(binaryRoot, "release-manifest.json");
const manifest = JSON.stringify({ schema: "paperclip.runner.release-binaries.v1", sourceRevision: "a".repeat(40), platforms });
await writeFile(manifestPath, manifest);
const old = start(hostTarget, ["30"]);
await old.spawned;
const requirement = { repoRoot: root, nativeRunnerRequired: true };
@@ -137,16 +167,20 @@ test("a source rebuild refreshes the preferred host target without replacing ano
await stageBuiltRunnerBinary(packageRoot);
const published = await stat(hostTarget);
assert.notEqual(published.ino, oldIdentity.ino);
const selected = resolveNativeRunnerDevBinary(requirement);
assert.deepEqual(selected, { binary: path.join(binaryRoot, "paperclip-runnerd"), sourceBuild: true });
const published = await stat(selected.binary);
assert.equal(published.mode & 0o777, 0o755);
assert.notDeepEqual(await readFile(hostTarget), oldArtifact);
assert.deepEqual(await readFile(hostTarget), await readFile(path.join(binaryRoot, "paperclip-runnerd")));
assert.equal(await readFile(otherTarget, "utf8"), "other architecture artifact");
assert.notDeepEqual(await readFile(selected.binary), oldArtifact);
assert.equal((await stat(hostTarget)).ino, oldIdentity.ino);
assert.deepEqual(await readFile(hostTarget), oldArtifact);
assert.equal(await readFile(manifestPath, "utf8"), manifest);
for (const [artifact, bytes] of releaseBytes) assert.deepEqual(await readFile(artifact), bytes);
assert.equal(resolvePackagedRunnerBinary(path.join(packageRoot, "dist")), hostTarget);
assert.equal(paperclipRunnerBinaryNeedsBuild(requirement), false);
assert.equal(old.child.exitCode, null);
assert.equal(old.child.signalCode, null);
const next = start(hostTarget, ["rebuilt"]);
const next = start(selected.binary, ["rebuilt"]);
await next.spawned; await next.closed;
assert.equal(next.child.exitCode, 0);
});
+26 -14
View File
@@ -1,5 +1,6 @@
import { readdirSync, statSync } from "node:fs";
import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { runnerBinaryTarget } from "../packages/paperclip-runner/src/live/runner-binary.ts";
export function resolveNativeRunnerRequirement({ exitCode, stdout }) {
if (exitCode !== 0) {
@@ -38,6 +39,27 @@ function newestMtimeMs(target) {
return newest;
}
export function resolveNativeRunnerDevBinary({
repoRoot,
platform = process.platform,
architecture = process.arch,
}) {
const executable = platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd";
const packageRoot = path.join(repoRoot, "packages", "paperclip-runner");
const target = path.join(packageRoot, "dist", "bin", `${platform}-${architecture}`, executable);
const flat = path.join(packageRoot, "dist", "bin", executable);
const targetStat = statSync(target, { throwIfNoEntry: false });
const flatStat = statSync(flat, { throwIfNoEntry: false });
// Release targets remain authoritative unless a newer source build matches
// this host. In particular, a build-host flat artifact cannot mask a remotely
// staged Mac target or force a local Cargo build.
if (flatStat?.isFile() && (!targetStat?.isFile() || flatStat.mtimeMs > targetStat.mtimeMs)
&& runnerBinaryTarget(readFileSync(flat)) === `${platform}-${architecture}`) {
return { binary: flat, sourceBuild: true };
}
return targetStat?.isFile() ? { binary: target, sourceBuild: false } : null;
}
export function paperclipRunnerBinaryNeedsBuild({
repoRoot,
nativeRunnerRequired,
@@ -48,20 +70,10 @@ export function paperclipRunnerBinaryNeedsBuild({
if (!nativeRunnerRequired) return false;
if (configuredBinary?.trim()) return false;
const executable = platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd";
const selected = resolveNativeRunnerDevBinary({ repoRoot, platform, architecture });
if (!selected) return true;
const binaryStat = statSync(selected.binary);
const packageRoot = path.join(repoRoot, "packages", "paperclip-runner");
// Match production resolution: a staged release target takes precedence over
// the flat source-build output. Remote Mac builds must not trigger Cargo just
// because only the normal darwin-<arch> release executable is present.
let binaryStat;
for (const binary of [
path.join(packageRoot, "dist", "bin", `${platform}-${architecture}`, executable),
path.join(packageRoot, "dist", "bin", executable),
]) {
const candidate = statSync(binary, { throwIfNoEntry: false });
if (candidate?.isFile()) { binaryStat = candidate; break; }
}
if (!binaryStat) return true;
const runnerRoot = path.join(packageRoot, "runner");
const buildInputs = [
+25 -17
View File
@@ -8,6 +8,7 @@ import { stdin, stdout } from "node:process";
import { createCapturedOutputBuffer, parseJsonResponseWithLimit } from "./dev-runner-output.ts";
import {
paperclipRunnerBinaryNeedsBuild,
resolveNativeRunnerDevBinary,
resolveNativeRunnerRequirement,
} from "./dev-runner-native-binary.mjs";
import { applyDevRunnerOptions } from "./dev-runner-options.ts";
@@ -171,6 +172,9 @@ const env: NodeJS.ProcessEnv = {
...process.env,
PAPERCLIP_UI_DEV_MIDDLEWARE: explicitUiDevMiddleware ?? (serveBuiltUiForManagedRuntime ? "false" : "true"),
};
// An automatic source-build override must not hide subsequent Rust edits from
// the watcher. Keep the operator's original choice separate from server env.
const configuredRunnerBinary = env.PAPERCLIP_RUNNER_BINARY;
if (mode === "dev") {
env.PAPERCLIP_DEV_SERVER_STATUS_FILE = devServerStatusFilePath;
@@ -573,28 +577,32 @@ async function buildPaperclipRunner() {
process.exit(typescriptResult.code);
}
const nativeRunnerRequired = await getNativeRunnerRequired();
if (
!paperclipRunnerBinaryNeedsBuild({
paperclipRunnerBinaryNeedsBuild({
repoRoot,
nativeRunnerRequired: await getNativeRunnerRequired(),
configuredBinary: env.PAPERCLIP_RUNNER_BINARY,
nativeRunnerRequired,
configuredBinary: configuredRunnerBinary,
})
) {
return;
console.log("[paperclip] building paperclip runner native binary...");
const binaryResult = await runPnpm(
["--filter", "@paperclipai/paperclip-runner", "build:binary"],
{ stdio: "inherit" },
);
if (binaryResult.signal) {
exitForSignal(binaryResult.signal);
return;
}
if (binaryResult.code !== 0) {
console.error("[paperclip] paperclip runner native binary build failed");
process.exit(binaryResult.code);
}
}
console.log("[paperclip] building paperclip runner native binary...");
const binaryResult = await runPnpm(
["--filter", "@paperclipai/paperclip-runner", "build:binary"],
{ stdio: "inherit" },
);
if (binaryResult.signal) {
exitForSignal(binaryResult.signal);
return;
}
if (binaryResult.code !== 0) {
console.error("[paperclip] paperclip runner native binary build failed");
process.exit(binaryResult.code);
if (!configuredRunnerBinary?.trim()) {
const selected = nativeRunnerRequired ? resolveNativeRunnerDevBinary({ repoRoot }) : null;
if (selected?.sourceBuild) env.PAPERCLIP_RUNNER_BINARY = selected.binary;
else delete env.PAPERCLIP_RUNNER_BINARY;
}
}
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
paperclipRunnerBinaryNeedsBuild,
resolveNativeRunnerDevBinary,
resolveNativeRunnerRequirement,
} from "../../../scripts/dev-runner-native-binary.mjs";
@@ -36,10 +37,24 @@ function createRunnerCheckout(): { root: string; source: string; binary: string
fs.writeFileSync(path.join(runnerRoot, "Cargo.toml"), "[workspace]\n", "utf8");
fs.writeFileSync(path.join(runnerRoot, "Cargo.lock"), "", "utf8");
fs.writeFileSync(source, "fn main() {}\n", "utf8");
fs.writeFileSync(binary, "runnerd", "utf8");
fs.writeFileSync(binary, nativeHeader(process.platform, process.arch));
return { root, source, binary };
}
function nativeHeader(platform: string, architecture: string): Buffer {
const bytes = Buffer.alloc(128);
if (platform === "darwin") {
bytes.writeUInt32LE(0xfeedfacf);
bytes.writeUInt32LE(architecture === "x64" ? 0x01000007 : 0x0100000c, 4);
} else if (platform === "win32") {
bytes.set([0x4d, 0x5a]); bytes.writeUInt32LE(64, 60);
bytes.writeUInt32LE(0x00004550, 64); bytes.writeUInt16LE(architecture === "x64" ? 0x8664 : 0xaa64, 68);
} else {
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(architecture === "x64" ? 62 : 183, 18);
}
return bytes;
}
describe("paperclip runner native dev prerequisite", () => {
it("uses an explicit status response and fails safe when status is unknown", () => {
expect(
@@ -114,12 +129,18 @@ describe("paperclip runner native dev prerequisite", () => {
const checkout = createRunnerCheckout();
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, "target-runnerd");
fs.writeFileSync(target, nativeHeader("darwin", architecture));
const old = new Date(Date.now() - 2000), current = new Date(Date.now() + 2000);
fs.utimesSync(checkout.binary, old, old);
fs.utimesSync(target, current, current);
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
// A newer Linux build-host output cannot mask the remote Mac artifact.
fs.writeFileSync(checkout.binary, nativeHeader("linux", "x64"));
const wrongHostNewer = new Date(Date.now() + 4000);
fs.utimesSync(checkout.binary, wrongHostNewer, wrongHostNewer);
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: target, sourceBuild: false });
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
fs.rmSync(checkout.binary);
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
fs.utimesSync(target, old, old);
@@ -128,6 +149,26 @@ describe("paperclip runner native dev prerequisite", () => {
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
});
it.each(["arm64", "x64"])("uses only a newer matching Mac %s source build without changing release selection", architecture => {
const checkout = createRunnerCheckout();
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, nativeHeader("darwin", architecture));
fs.writeFileSync(checkout.binary, nativeHeader("darwin", architecture));
const old = new Date(Date.now() - 2000), fresh = new Date(Date.now() + 2000), changed = new Date(Date.now() + 4000);
fs.utimesSync(target, old, old); fs.utimesSync(checkout.binary, fresh, fresh);
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: checkout.binary, sourceBuild: true });
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
fs.utimesSync(checkout.source, changed, changed);
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
fs.writeFileSync(checkout.binary, nativeHeader("darwin", architecture === "x64" ? "arm64" : "x64"));
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: target, sourceBuild: false });
fs.rmSync(target);
expect(resolveNativeRunnerDevBinary(input)).toBeNull();
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
});
it("does not build a workspace binary when an explicit binary is configured", () => {
expect(
paperclipRunnerBinaryNeedsBuild({
+15 -4
View File
@@ -1214,10 +1214,14 @@ describe("CompanyImport", () => {
});
it.each([
{ sourceType: "codex_local", sourceRunner: "legacy" as const, runner: "paperclip" as const },
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const },
{ sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const },
])("submits the explicit $runner import override from $sourceType without replacing saved settings", async ({ sourceType, sourceRunner, runner }) => {
{ sourceType: "codex_local", sourceRunner: "legacy" as const, runner: "paperclip" as const, sourcePolicy: {}, expectedBypass: undefined },
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false },
{ sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: { codexPermissionMode: "never" }, expectedBypass: true },
{ sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: {}, expectedBypass: true },
{ sourceType: "paperclip_runner", sourceRunner: "paperclip" as const, runner: "legacy" as const, sourcePolicy: { codexPermissionMode: "never", dangerouslyBypassApprovalsAndSandbox: false }, expectedBypass: false },
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassSandbox: false }, expectedBypass: false },
{ sourceType: "codex_local", sourceRunner: undefined, runner: "legacy" as const, sourcePolicy: { dangerouslyBypassApprovalsAndSandbox: true }, expectedBypass: true },
])("submits $runner from $sourceType with visible bypass=$expectedBypass while preserving saved policy", async ({ sourceType, sourceRunner, runner, sourcePolicy, expectedBypass }) => {
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
mockAdaptersApi.list.mockResolvedValue([
{ type: "claude_local", disabled: false },
@@ -1230,7 +1234,9 @@ describe("CompanyImport", () => {
preview.manifest.agents[0]!.adapterConfig = {
...(sourceType === "paperclip_runner" ? { provider: "codex" } : {}),
model: "gpt-5.6-sol", modelReasoningEffort: "high", env: { CODEX_HOME: "/saved/account" },
...sourcePolicy,
};
const savedSourceConfig = structuredClone(preview.manifest.agents[0]!.adapterConfig);
mockCompaniesApi.importPreview.mockResolvedValue(preview);
await renderPage();
await enterGithubUrl();
@@ -1245,9 +1251,14 @@ describe("CompanyImport", () => {
expect(option!.disabled).toBe(false);
await act(async () => option!.click());
await flushReact();
if (expectedBypass !== undefined) {
expect(container.querySelector('[role="switch"][aria-label="Bypass sandbox"]')?.getAttribute("aria-checked"))
.toBe(String(expectedBypass));
}
await clickButton(text => text.startsWith("Import 3 file"));
await settle();
expect(lastImportMeta().adapterOverrides).toEqual({ coder: { adapterType: "codex_local", runner } });
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig);
});
it("does not reinterpret an unknown native provider as Codex when its configuration is edited", async () => {
+9 -1
View File
@@ -42,7 +42,8 @@ import { Field, adapterLabels } from "../components/agent-config-primitives";
import { getAdapterLabel } from "../adapters/adapter-display-registry";
import { defaultCreateValues } from "../components/agent-config-defaults";
import { getUIAdapter, listUIAdapters } from "../adapters";
import { isPaperclipRunnerProvider, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { isPaperclipRunnerProvider, PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local";
import {
type FileTreeNode,
type FrontmatterData,
@@ -575,6 +576,8 @@ function importAdapterValues(
runner?: AgentRunnerChoice,
): CreateConfigValues {
const nativeCodex = sourceAdapterType === "paperclip_runner" && choice === "codex_local";
const savedBypass = sourceConfig?.dangerouslyBypassApprovalsAndSandbox
?? (nativeCodex ? undefined : sourceConfig?.dangerouslyBypassSandbox);
return {
...defaultCreateValues,
adapterType: choice === "openai_dot" ? "paperclip_runner" : choice,
@@ -582,6 +585,11 @@ function importAdapterValues(
...(sourceConfig && choice === "codex_local" ? {
model: typeof sourceConfig.model === "string" ? sourceConfig.model : "",
thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""),
// Runner-only imports preserve server-translated policy; display that policy too.
dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass
: nativeCodex
? (sourceConfig.codexPermissionMode ?? PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES.codex.defaultMode) === "never"
: DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
} : {}),
...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot"
? { adapterSchemaValues: { ...sourceConfig } } : {}),