perf(adapter-utils): content-hash-skip process-session remote script write + lock inbound round-trip count (#10377)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The sandbox runtime depends on efficiently staging files into remote
environments
> - The process-session bridge was rewriting a static remote script on
every start, even when the remote copy already matched
> - That caused unnecessary round trips and made the startup path
noisier than it needed to be
> - This pull request adds a hash-skip path for the process-session
script write and keeps the existing bridge-entrypoint hash gate on the
same helper
> - It also locks in the reduced inbound round-trip count so the
collapse from the prior PR cannot silently regress
> - The benefit is fewer remote execs on warm starts and a stronger
guard against performance regressions

## Linked Issues or Issue Description

- Refs: https://github.com/paperclipai/paperclip/pull/10354

### Feature request-style description

**Subsystem affected**
- Cross-cutting (packages/adapter-utils and sandbox staging helpers)

**Problem or motivation**
- The process-session bootstrap path was rewriting a static remote
script on every start even when the remote file already matched the host
content.
- That added avoidable remote execs and latency to warm starts.
- The inbound staging collapse from the previous PR also needed a
regression lock so it could not silently drift back to extra round
trips.

**Proposed solution**
- Route the process-session remote script upload through the existing
hash-skip helper used by the sandbox callback bridge entrypoint.
- Keep the bridge-entrypoint behavior unchanged by delegating it to the
same helper.
- Add a regression test that asserts the inbound staging path still
collapses to the expected round-trip count.

**Alternatives considered**
- Keep the existing unconditional write path and accept the extra execs
on warm starts. Rejected because it preserves avoidable overhead.
- Add a second specialized helper just for process-session scripts.
Rejected because the bridge-entrypoint logic already solved the same
problem and should stay aligned.

**Roadmap alignment**
- This fits the roadmap items around cloud/sandbox agents and enforced
outcomes by reducing bootstrap waste and preventing performance
regressions.
- It does not introduce new product surface area, telemetry, or
user-facing workflow changes.

**Additional context**
- The implementation preserves the existing upload/verify/rename
behavior when the remote content differs and only skips the write when
the hash already matches.
- PR #10354 collapsed the inbound staging path; this PR preserves that
win.

## What Changed

- Added a shared hash-skip helper for remote text-file synchronization
so unchanged content skips the write path after a single remote hash
check.
- Switched the process-session remote script upload to use that helper,
preserving the existing write/verify/rename behavior when the remote
content differs.
- Refactored the sandbox callback bridge entrypoint sync to use the same
helper without changing its observable behavior.
- Added a mocked-native-runner regression test that locks the inbound
staging path to one `client.syncIn` round-trip per step and zero direct
write/run execs.

## Verification

- `tsc --noEmit`
- `pnpm --filter @paperclipai/adapter-utils exec vitest run`
- Full adapter-utils vitest sweep: 338 passed / 4 skipped

## Risks

- Low risk: the helper changes when a write occurs, not the script
contents or the remote execution surface.
- A hash-check failure now fails loudly instead of silently rewriting,
which is safer but could surface provider-side issues earlier than
before.
- The regression test is intentionally specific to the current collapsed
staging path, so future architectural changes will need test updates.

## Model Used

OpenAI Codex (GPT-5, tool-using coding agent)

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Harold Kim <harold@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky Leach 2026-07-28 08:33:12 -07:00 committed by GitHub
parent 341993ebae
commit 9ff7e1caf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 351 additions and 25 deletions

View File

@ -22,6 +22,7 @@ import {
sandboxCallbackBridgeDirectories,
startSandboxCallbackBridgeServer,
startSandboxCallbackBridgeWorker,
syncRemoteTextFileWithHashSkip,
} from "./sandbox-callback-bridge.js";
import {
createSandboxRunLogTailFactory,
@ -1258,11 +1259,33 @@ async function writeProcessSessionProxyScript(dir: string, port: number, token:
return proxyPath;
}
// Content-hash-skip the process-session remote script write, mirroring the
// sandbox callback bridge entrypoint sha256 gate. The script is a static
// Paperclip-authored `.mjs` that only changes when the build changes, so on a
// warm start (same sandbox, script already present) the single sha-gate exec
// skips the ~3-exec base64 upload entirely. `syncRemoteTextFileWithHashSkip`
// fails loud on a check error rather than silently re-uploading.
async function syncProcessSessionRemoteScript(input: {
client: ReturnType<typeof createCommandManagedSandboxCallbackBridgeQueueClient>;
runner: CommandManagedRuntimeRunner;
remoteCwd: string;
remoteScriptDir: string;
remoteScriptPath: string;
}): Promise<void> {
await input.client.writeTextFile(input.remoteScriptPath, getProcessSessionRemoteSource());
timeoutMs?: number | null;
shellCommand?: "bash" | "sh" | null;
}): Promise<{ uploaded: boolean }> {
const { uploaded } = await syncRemoteTextFileWithHashSkip({
runner: input.runner,
remoteCwd: input.remoteCwd,
remoteDir: input.remoteScriptDir,
remotePath: input.remoteScriptPath,
body: getProcessSessionRemoteSource(),
label: "Process session remote script",
action: "sync process session remote script",
lockDir: path.posix.join(input.remoteScriptDir, ".paperclip-process-session-script.lock"),
timeoutMs: input.timeoutMs,
shellCommand: input.shellCommand,
});
return { uploaded };
}
async function readRemoteJsonFiles(input: {
@ -1342,7 +1365,14 @@ export async function startAdapterExecutionTargetProcessSessionBridge(input: {
await client.makeDir(stdinDir);
await client.makeDir(eventsDir);
await syncProcessSessionRemoteScript({ client, remoteScriptPath });
await syncProcessSessionRemoteScript({
runner,
remoteCwd: target.remoteCwd,
remoteScriptDir: bridgeRuntimeDir,
remoteScriptPath,
timeoutMs,
shellCommand,
});
// Resolve the launch env AFTER the env-independent setup above, so a caller
// can defer it until an upstream dependency (e.g. the paperclip bridge's env)

View File

@ -13,6 +13,7 @@ import {
createSandboxCallbackBridgeAsset,
createSandboxCallbackBridgeToken,
sandboxCallbackBridgeDirectories,
syncRemoteTextFileWithHashSkip,
syncSandboxCallbackBridgeEntrypoint,
startSandboxCallbackBridgeServer,
startSandboxCallbackBridgeWorker,
@ -862,6 +863,135 @@ describe("sandbox callback bridge", () => {
).resolves.toEqual([]);
});
// The process-session remote script is a static, Paperclip-authored `.mjs`
// written into the sandbox on every bridge start. `syncRemoteTextFileWithHashSkip`
// (which now backs that write, mirroring the bridge-entrypoint sha256 gate)
// content-hash-skips it so a warm start where the remote script already matches
// costs ZERO write execs instead of the prior ~3 (prepare/append/finalize base64
// upload).
it("test_process_session_script_skipped_when_remote_hash_matches: warm start with a matching remote hash writes 0 execs", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-warm-"));
cleanupDirs.push(rootDir);
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
const body = "console.log('process session remote script v1');\n";
let execCount = 0;
const inner = createExecRunner();
const runner = {
execute: async (input: Parameters<typeof inner.execute>[0]) => {
execCount += 1;
return inner.execute(input);
},
};
const args = {
runner,
remoteCwd: rootDir,
remoteDir,
remotePath,
body,
label: "Process session remote script",
action: "sync process session remote script",
lockDir,
timeoutMs: 30_000,
} as const;
// Cold start: the script is uploaded (single sha-gate exec that writes).
const first = await syncRemoteTextFileWithHashSkip(args);
expect(first.uploaded).toBe(true);
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
// Warm start: the remote hash matches, so the write is skipped entirely.
execCount = 0;
const second = await syncRemoteTextFileWithHashSkip(args);
expect(second.uploaded).toBe(false);
// A single hash-gate round-trip that performed 0 writes (down from ~3 execs).
expect(execCount).toBe(1);
// sha is still returned on the skip path so callers get a well-formed result.
expect(second.sha256).toBe(first.sha256);
// The remote file is unchanged and no upload/partial/lock leftovers remain.
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
await expect(
readdir(remoteDir).then((entries) =>
entries.filter(
(entry) =>
entry.endsWith(".paperclip-upload.b64") ||
entry.endsWith(".partial") ||
entry === ".paperclip-process-session-script.lock",
),
),
).resolves.toEqual([]);
});
it("test_process_session_script_rewritten_on_hash_mismatch: a mismatched remote hash still rewrites the script", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-cold-"));
cleanupDirs.push(rootDir);
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
const body = "console.log('process session remote script v2');\n";
// Pre-seed the remote with a DIFFERENT script (a prior/stale build).
await mkdir(remoteDir, { recursive: true });
await writeFile(remotePath, "console.log('stale remote script');\n", "utf8");
const result = await syncRemoteTextFileWithHashSkip({
runner: createExecRunner(),
remoteCwd: rootDir,
remoteDir,
remotePath,
body,
label: "Process session remote script",
action: "sync process session remote script",
lockDir,
timeoutMs: 30_000,
});
expect(result.uploaded).toBe(true);
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
});
it("fails loud when the hash-skip sync exec errors instead of silently re-uploading", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-fail-"));
cleanupDirs.push(rootDir);
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
// A runner whose exec fails: the hash-gate cannot be evaluated. The write
// must surface the failure, never swallow it and re-upload behind a green
// return value.
const runner = {
execute: async () => ({
exitCode: 1,
signal: null,
timedOut: false,
stdout: "",
stderr: "hash gate boom",
pid: null,
startedAt: new Date().toISOString(),
}),
};
await expect(
syncRemoteTextFileWithHashSkip({
runner,
remoteCwd: rootDir,
remoteDir,
remotePath,
body: "console.log('never written');\n",
label: "Process session remote script",
action: "sync process session remote script",
lockDir,
timeoutMs: 30_000,
}),
).rejects.toThrow(/sync process session remote script/i);
// Nothing was written to the remote path on the failure path.
await expect(readFile(remotePath, "utf8")).rejects.toThrow();
});
it("permits the documented heartbeat surface and denies unrelated routes", () => {
const allowed: Array<{ method: string; path: string }> = [
{ method: "GET", path: "/api/agents/me" },

View File

@ -777,34 +777,54 @@ export async function startSandboxCallbackBridgeWorker(input: {
};
}
export async function syncSandboxCallbackBridgeEntrypoint(input: {
/**
* Content-hash-skip write of a Paperclip-authored text file into the sandbox, in
* a SINGLE remote exec. The body's sha256 is computed on the host; the one shell
* round-trip skips the write entirely when the remote file already hashes to the
* same value (warm start 0 write execs), otherwise it uploads (base64 over
* stdin), verifies the decoded bytes, and atomically renames into place. A
* PID-liveness lock serializes concurrent writers to the same path and the
* verify step guards against a torn upload.
*
* Fail loudly: a non-zero remote exit (surfaced by `requireSuccessfulResult`) or
* malformed result JSON throws rather than silently re-uploading and masking a
* failed check. The only intentional degradation is when the remote has neither
* `sha256sum` nor `shasum` then the skip cannot be proven and we conservatively
* re-upload (and the post-upload verify is best-effort, as noted inline).
*/
export async function syncRemoteTextFileWithHashSkip(input: {
runner: CommandManagedRuntimeRunner;
remoteCwd: string;
assetRemoteDir: string;
bridgeAsset: SandboxCallbackBridgeAsset;
remoteDir: string;
remotePath: string;
body: string;
// Human-readable noun phrase used in fail-loud messages, e.g.
// "Sandbox callback bridge entrypoint" / "Process session remote script".
label: string;
// Short action label for `requireSuccessfulResult`, e.g.
// "sync sandbox callback bridge entrypoint".
action: string;
lockDir: string;
timeoutMs?: number | null;
shellCommand?: "bash" | "sh" | null;
}): Promise<{ remoteEntrypoint: string; sha256: string; uploaded: boolean }> {
}): Promise<{ uploaded: boolean; sha256: string }> {
const timeoutMs = normalizeTimeoutMs(input.timeoutMs, DEFAULT_BRIDGE_RESPONSE_TIMEOUT_MS);
const shellCommand = preferredShellForSandbox(input.shellCommand);
const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT);
const remoteEntrypointPartial = `${remoteEntrypoint}.partial`;
const remoteUploadPath = `${remoteEntrypoint}.paperclip-upload.b64`;
const remoteLockDir = path.posix.join(input.assetRemoteDir, ".paperclip-bridge-upload.lock");
const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8");
const entrypointBase64 = toBuffer(Buffer.from(entrypointSource, "utf8")).toString("base64");
const sha256 = createHash("sha256").update(entrypointSource, "utf8").digest("hex");
const remotePartial = `${input.remotePath}.partial`;
const remoteUploadPath = `${input.remotePath}.paperclip-upload.b64`;
const base64Body = toBuffer(Buffer.from(input.body, "utf8")).toString("base64");
const sha256 = createHash("sha256").update(input.body, "utf8").digest("hex");
const syncResult = await runShell(
input.runner,
input.remoteCwd,
[
"set -eu",
`remote_dir=${shellQuote(input.assetRemoteDir)}`,
`remote_path=${shellQuote(remoteEntrypoint)}`,
`remote_partial=${shellQuote(remoteEntrypointPartial)}`,
`remote_dir=${shellQuote(input.remoteDir)}`,
`remote_path=${shellQuote(input.remotePath)}`,
`remote_partial=${shellQuote(remotePartial)}`,
`remote_upload=${shellQuote(remoteUploadPath)}`,
`lock_dir=${shellQuote(remoteLockDir)}`,
`lock_dir=${shellQuote(input.lockDir)}`,
`expected_sha=${shellQuote(sha256)}`,
"hash_file() {",
" if command -v sha256sum >/dev/null 2>&1; then",
@ -818,7 +838,7 @@ export async function syncSandboxCallbackBridgeEntrypoint(input: {
" return 127",
"}",
"mkdir -p \"$remote_dir\"",
...buildRemotePidLockAcquireScript("\"$lock_dir\"", "Timed out acquiring sandbox callback bridge upload lock."),
...buildRemotePidLockAcquireScript("\"$lock_dir\"", `Timed out acquiring ${input.label} upload lock.`),
...buildRemotePidLockCleanupScript("\"$lock_dir\"", [
"rm -f \"$remote_upload\" \"$remote_partial\"",
]),
@ -839,30 +859,56 @@ export async function syncSandboxCallbackBridgeEntrypoint(input: {
// best-effort and we trust base64-decode + atomic rename below.
"if partial_sha=\"$(hash_file \"$remote_partial\" 2>/dev/null)\"; then",
" if [ \"$partial_sha\" != \"$expected_sha\" ]; then",
" echo \"Sandbox callback bridge entrypoint upload sha mismatch.\" >&2",
` echo ${shellQuote(`${input.label} upload sha mismatch.`)} >&2`,
" exit 1",
" fi",
"else",
" echo \"Sandbox callback bridge entrypoint sha verify skipped: no sha256sum/shasum on remote.\" >&2",
` echo ${shellQuote(`${input.label} sha verify skipped: no sha256sum/shasum on remote.`)} >&2`,
"fi",
"mv \"$remote_partial\" \"$remote_path\"",
"printf '{\"uploaded\":true}\\n'",
].join("\n"),
timeoutMs,
shellCommand,
entrypointBase64,
base64Body,
);
requireSuccessfulResult("sync sandbox callback bridge entrypoint", syncResult);
requireSuccessfulResult(input.action, syncResult);
let uploaded = false;
try {
uploaded = JSON.parse(syncResult.stdout.trim())?.uploaded === true;
} catch (error) {
throw new Error(
`Sandbox callback bridge sync wrote invalid result JSON: ${error instanceof Error ? error.message : String(error)}`,
`${input.label} sync wrote invalid result JSON: ${error instanceof Error ? error.message : String(error)}`,
);
}
return { uploaded, sha256 };
}
export async function syncSandboxCallbackBridgeEntrypoint(input: {
runner: CommandManagedRuntimeRunner;
remoteCwd: string;
assetRemoteDir: string;
bridgeAsset: SandboxCallbackBridgeAsset;
timeoutMs?: number | null;
shellCommand?: "bash" | "sh" | null;
}): Promise<{ remoteEntrypoint: string; sha256: string; uploaded: boolean }> {
const remoteEntrypoint = path.posix.join(input.assetRemoteDir, SANDBOX_CALLBACK_BRIDGE_ENTRYPOINT);
const entrypointSource = await fs.readFile(input.bridgeAsset.entrypoint, "utf8");
const { uploaded, sha256 } = await syncRemoteTextFileWithHashSkip({
runner: input.runner,
remoteCwd: input.remoteCwd,
remoteDir: input.assetRemoteDir,
remotePath: remoteEntrypoint,
body: entrypointSource,
label: "Sandbox callback bridge entrypoint",
action: "sync sandbox callback bridge entrypoint",
lockDir: path.posix.join(input.assetRemoteDir, ".paperclip-bridge-upload.lock"),
timeoutMs: input.timeoutMs,
shellCommand: input.shellCommand,
});
return {
remoteEntrypoint,
sha256,

View File

@ -1363,6 +1363,126 @@ describe("sandbox managed runtime", () => {
expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
});
// Regression lock: a representative `codex_local` start stages its inbound
// bytes — git history, workspace overlay, and the managed Codex `home` asset
// (auth.json merge) — as EXACTLY ONE `syncIn` operation each. Every inbound step
// is routed through `client.syncIn` (one native `uploadFiles` round-trip per
// operation, with the extract/merge carried as provider-executed
// `postUploadCommands`), with no separate custom-provision diversion. Assert
// the collapsed round-trip count so a future change that re-inlines a
// `writeFile`+`run` sequence — or fans one staging step across multiple
// operations — fails loudly here instead of silently regressing the start path.
it("collapses a representative codex_local start to one syncIn round-trip per inbound staging step", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-roundtrip-"));
cleanupDirs.push(rootDir);
const sourceRepoDir = path.join(rootDir, "source-repo");
const localWorkspaceDir = path.join(rootDir, "local-workspace");
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
const homeDir = path.join(rootDir, "codex-home");
// Git-backed workspace → git history + workspace overlay are two staging steps.
await mkdir(sourceRepoDir, { recursive: true });
await git(sourceRepoDir, ["init"]);
await git(sourceRepoDir, ["checkout", "-b", "main"]);
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
await git(sourceRepoDir, ["add", "tracked.txt"]);
await git(sourceRepoDir, ["commit", "-m", "base"]);
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
// Managed Codex home with an auth.json that a custom post-upload command
// merges in-sandbox — the credential path is routed onto native uploadFiles.
await mkdir(homeDir, { recursive: true });
await writeFile(path.join(homeDir, "auth.json"), "{\"OPENAI_API_KEY\":\"sk-test\"}\n", "utf8");
await writeFile(path.join(homeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
// A native runner delegates every staging step to `syncIn`; ANY direct
// writeFile/run exec is a collapse regression.
const directWrites: string[] = [];
const directRuns: string[] = [];
const client: SandboxManagedRuntimeClient = {
makeDir: async (remotePath) => {
await mkdir(remotePath, { recursive: true });
},
writeFile: async (remotePath, bytes) => {
directWrites.push(remotePath);
await mkdir(path.dirname(remotePath), { recursive: true });
await writeFile(remotePath, Buffer.from(bytes));
},
readFile: async (remotePath) => await readFile(remotePath),
listFiles: async () => [],
remove: async (remotePath) => {
await rm(remotePath, { recursive: true, force: true });
},
run: async (command) => {
directRuns.push(command);
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
},
};
const captured: SandboxSyncOperation[] = [];
attachNativeRecordingSyncIn(client, captured);
const q = (value: string) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
const prepared = await prepareSandboxManagedRuntime({
spec: {
transport: "sandbox",
provider: "test",
sandboxId: "sandbox-1",
remoteCwd: remoteWorkspaceDir,
timeoutMs: 30_000,
apiKey: null,
},
adapterKey: "codex",
client,
workspaceLocalDir: localWorkspaceDir,
assets: [{
key: "home",
localDir: homeDir,
provision: {
stageFiles: [{ name: "home-merge.sh", contents: "#!/bin/sh\ntar -xf \"$2\" -C \"$1\"\n" }],
postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) =>
`mkdir -p ${q(assetDir)} && ` +
`sh ${q(path.posix.join(runtimeRootDir, "home-merge.sh"))} ${q(assetDir)} ${q(assetTarPath)} && ` +
`rm -f ${q(assetTarPath)}`,
},
}],
});
// The orchestrator delegated everything to syncIn: no re-inlined writeFile/run.
expect(directWrites).toEqual([]);
expect(directRuns).toEqual([]);
// The collapsed count: exactly three inbound round-trips — git, workspace, home.
expect(captured).toHaveLength(3);
const gitOp = captured.find((op) =>
op.files.some((mapping) => mapping.targetPath.endsWith("git-workspace-upload.tar")),
);
const workspaceOp = captured.find((op) =>
op.files.some((mapping) => mapping.targetPath.endsWith("workspace-upload.tar")),
);
const homeOp = captured.find((op) =>
op.files.some((mapping) => mapping.targetPath.endsWith("home-upload.tar")),
);
expect(gitOp).toBeDefined();
expect(workspaceOp).toBeDefined();
expect(homeOp).toBeDefined();
// Every operation is a single native uploadFiles (all `file` mappings) whose
// extract/merge rides as an ordered provider-executed post-upload command.
for (const op of captured) {
expect(op.files.length).toBeGreaterThanOrEqual(1);
expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
expect(op.postUploadCommands ?? []).not.toHaveLength(0);
}
// Operation ids are distinct, so "3 operations" is 3 real round-trips.
expect(new Set(captured.map((op) => op.operationId)).size).toBe(3);
// The credential asset actually materialized through the native seam.
await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8"))
.resolves.toBe("{\"OPENAI_API_KEY\":\"sk-test\"}\n");
});
it("keeps the sandbox runtime core free of Codex-specific string literals", async () => {
const coreSource = await readFile(new URL("./sandbox-managed-runtime.ts", import.meta.url), "utf8");
// The seam must be generic: no adapter (Codex) knowledge may live in the core.