Merge reviewed runtime and measured-platform fixes before Codex defaults

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/runner-native-platform-prerequisites:
  fix(runner): close review gaps in setup and platform qualification
This commit is contained in:
DottaandPaperclip committed 2026-10-12 16:14:48 -05:00
commit a1bfe8f5be
16 files changed
+568 -44

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,65 @@
Updated 2026-10-12.
## 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.
+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",
@@ -1335,6 +1335,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;
@@ -7094,6 +7096,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
@@ -7107,13 +7110,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;
@@ -7208,17 +7218,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 }); }
});
@@ -129,6 +129,16 @@ 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();
});
beforeEach(() => {
mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
+91 -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,105 @@ 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 });
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() });
expect(update).not.toHaveBeenCalled();
});
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",
+16
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
@@ -1208,6 +1216,14 @@ export function environmentRoutes(
// Persist the config change and its binding rows atomically: a binding
// ref that fails validation (e.g. a deleted secret) must roll the whole
// save back instead of leaving the config re-pointed with stale bindings.
if (changesProviderTarget || patch.metadata !== undefined) {
const metadata = { ...(patch.metadata ?? existing.metadata ?? {}) };
if (changesProviderTarget) delete metadata[SANDBOX_PLATFORM_PROBE_METADATA_KEY];
else if (existing.metadata?.[SANDBOX_PLATFORM_PROBE_METADATA_KEY] !== undefined) {
metadata[SANDBOX_PLATFORM_PROBE_METADATA_KEY] = existing.metadata[SANDBOX_PLATFORM_PROBE_METADATA_KEY];
}
patch.metadata = metadata;
}
const environment = await db.transaction(async (tx) => {
const updated = await svc.update(existing.id, patch, { db: tx });
if (!updated) return null;
@@ -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 }) => 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 }) => void;
},
): Promise<SandboxEnvironmentConfig> {
const row = await db
@@ -587,7 +591,8 @@ 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 });
if (!input.readOnly) await db
.update(environmentCustomImageTemplates)
.set({ lastUsedAt: now, updatedAt: now })
.where(eq(environmentCustomImageTemplates.id, active.id));
+94 -8
View File
@@ -11,6 +11,54 @@ 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) {
let template: { id: string; capturedAt: string | null } | null = null;
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment, {
applyCustomImageTemplate: true, readOnlyCustomImageTemplate: true,
onResolvedCustomImageTemplate: identity => { template = identity; },
});
if (parsed.driver !== "sandbox") return null;
const image = parsed.config.image;
// Names, mutable tags, provider defaults and snapshot names are not immutable identities.
if (!isImmutableImage(image) || record(parsed.config).snapshot) 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 +72,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 +129,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 +147,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 +194,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.
+17
View File
@@ -1,4 +1,6 @@
import { randomUUID } from "node:crypto";
import { SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "./environment-custom-image-runtime.js";
import { and, desc, eq, inArray, isNull, ne, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
@@ -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,