@foldrun/core 2.14.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/confine.d.ts +3 -0
- package/dist/src/confine.js +14 -0
- package/dist/src/copy-back.d.ts +17 -0
- package/dist/src/copy-back.js +74 -0
- package/dist/src/deploy.d.ts +2 -1
- package/dist/src/deploy.js +4 -6
- package/dist/src/install-tool.js +15 -6
- package/dist/src/kinds.d.ts +14 -0
- package/dist/src/kinds.js +18 -0
- package/dist/src/run-container.d.ts +44 -2
- package/dist/src/run-container.js +82 -5
- package/dist/src/runner.js +32 -0
- package/dist/src/step-exec.d.ts +3 -0
- package/dist/src/step-exec.js +3 -3
- package/dist/src/store.d.ts +11 -0
- package/package.json +1 -1
- package/src/confine.ts +19 -0
- package/src/copy-back.ts +87 -0
- package/src/deploy.ts +4 -6
- package/src/install-tool.ts +15 -6
- package/src/kinds.ts +31 -0
- package/src/run-container.ts +105 -3
- package/src/runner.ts +30 -0
- package/src/step-exec.ts +6 -3
- package/src/store.ts +8 -0
package/src/run-container.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
29
29
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
30
30
|
import { CAPTURE_CAP, isPlatformPath, type ApiSpec, type Effort, type ModelCall } from "./store.ts";
|
|
31
31
|
import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.ts";
|
|
32
|
+
import { refusedBack, refusedLine, keptLine, type RefusedPath } from "./copy-back.ts";
|
|
32
33
|
import type { CallIo, EventExtra, ExecUsage } from "./step-exec.ts";
|
|
33
34
|
import type { TokenPrice } from "./catalog.ts";
|
|
34
35
|
import { isFileValue, fileContent } from "./secrets.ts";
|
|
@@ -47,6 +48,9 @@ import { parseBrowserPod, type BrowserPodOutcome } from "./browser-pod.ts";
|
|
|
47
48
|
* secrets in headers, assembled prompt, serializable MCP configs. */
|
|
48
49
|
export interface ContainerStepInput {
|
|
49
50
|
agentRel: string; // "agents/<name>" inside the workspace
|
|
51
|
+
/** A person approved this step before it ran: its file tools may write
|
|
52
|
+
* configuration and code inside the sandbox too (FOL-96). */
|
|
53
|
+
configWrites?: boolean;
|
|
50
54
|
prompt: string;
|
|
51
55
|
model: string;
|
|
52
56
|
effort?: Effort | null;
|
|
@@ -229,6 +233,9 @@ export interface ContainerStepOutcome {
|
|
|
229
233
|
* applyContainerChanges returned. An isolated sub-agent's reply names
|
|
230
234
|
* them to the step that delegated (delegate-tools.ts). */
|
|
231
235
|
applied?: string[];
|
|
236
|
+
/** What the step changed that was not taken back: configuration and code
|
|
237
|
+
* (copy-back.ts). On the step record and in the trail. */
|
|
238
|
+
refused?: RefusedPath[];
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
/**
|
|
@@ -333,6 +340,45 @@ export function hashTree(dir: string): Record<string, string> {
|
|
|
333
340
|
return out;
|
|
334
341
|
}
|
|
335
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Where the host keeps what a sandbox was handed (hashTree), for a driver
|
|
345
|
+
* that inherits the step: under the run's own folder, which is platform-
|
|
346
|
+
* owned — never staged into a sandbox, never copied back, erased with the
|
|
347
|
+
* run (deleteRun). It used to ride into the pod beside the input and be
|
|
348
|
+
* read back from there, where the step could rewrite it (FOL-96).
|
|
349
|
+
*/
|
|
350
|
+
export function handedInPath(workspaceRoot: string, runId: string, ref: string): string | null {
|
|
351
|
+
const safe = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/;
|
|
352
|
+
if (!safe.test(runId) || !safe.test(ref)) return null;
|
|
353
|
+
return path.join(workspaceRoot, "runs", runId, "sandbox", `${ref}.json`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Note what a sandbox was handed, on the host. */
|
|
357
|
+
export function writeHandedIn(workspaceRoot: string, runId: string, ref: string, hashes: Record<string, string>): void {
|
|
358
|
+
const file = handedInPath(workspaceRoot, runId, ref);
|
|
359
|
+
if (!file) return;
|
|
360
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
361
|
+
fs.writeFileSync(file, JSON.stringify(hashes));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** What the host noted a sandbox was handed, or undefined. */
|
|
365
|
+
export function readHandedIn(workspaceRoot: string, runId: string, ref: string): Record<string, string> | undefined {
|
|
366
|
+
const file = handedInPath(workspaceRoot, runId, ref);
|
|
367
|
+
if (!file) return undefined;
|
|
368
|
+
try {
|
|
369
|
+
const m = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
370
|
+
return m && typeof m === "object" && !Array.isArray(m) ? (m as Record<string, string>) : undefined;
|
|
371
|
+
} catch {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Forget it once the step's copy-out is done. */
|
|
377
|
+
export function dropHandedIn(workspaceRoot: string, runId: string, ref: string): void {
|
|
378
|
+
const file = handedInPath(workspaceRoot, runId, ref);
|
|
379
|
+
if (file) fs.rmSync(file, { force: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
336
382
|
/**
|
|
337
383
|
* Every file's mtime under `dir`, relative path to milliseconds — so a copy
|
|
338
384
|
* that cannot carry times can have them put back. `kubectl cp` unpacks with
|
|
@@ -412,12 +458,28 @@ export function mergeAppends(was: Buffer | string | null, host: Buffer, next: Bu
|
|
|
412
458
|
return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
|
|
413
459
|
}
|
|
414
460
|
|
|
461
|
+
export interface ApplyOptions {
|
|
462
|
+
/** What the host handed back into this sandbox mid-step — a `sandbox: own`
|
|
463
|
+
* job's files (delegate-tools.ts), path → sha256 — as the HOST recorded
|
|
464
|
+
* it (StepRecord.handedBack). The sandbox's own note of the same is only
|
|
465
|
+
* believed where it agrees with this: a step can write that note. */
|
|
466
|
+
handedBack?: Record<string, string | string[]>;
|
|
467
|
+
/** Told of every path the step changed that was not taken back because it
|
|
468
|
+
* is configuration or code (copy-back.ts). */
|
|
469
|
+
onRefused?: (refused: RefusedPath) => void;
|
|
470
|
+
/** A person approved the step before it ran: configuration and code it
|
|
471
|
+
* changed come back, each told to onKept instead of refused. */
|
|
472
|
+
allowConfig?: boolean;
|
|
473
|
+
onKept?: (kept: RefusedPath) => void;
|
|
474
|
+
}
|
|
475
|
+
|
|
415
476
|
export function applyContainerChanges(
|
|
416
477
|
hostWs: string,
|
|
417
478
|
containerWs: string,
|
|
418
479
|
baseline?: string | Record<string, string>,
|
|
419
480
|
note?: (message: string) => void,
|
|
420
481
|
divert?: WriteDivert,
|
|
482
|
+
opts: ApplyOptions = {},
|
|
421
483
|
): string[] {
|
|
422
484
|
const applied: string[] = [];
|
|
423
485
|
// Files a sandbox: own job's reply copied into this sandbox mid-step
|
|
@@ -425,6 +487,10 @@ export function applyContainerChanges(
|
|
|
425
487
|
// those, that is what the step started from. The job's change is already on
|
|
426
488
|
// the workspace; read against the older baseline it came back as the
|
|
427
489
|
// step's own and was merged in a second time (Codex review, 23 Oct 2026).
|
|
490
|
+
// The sandbox notes which it actually wrote (a conflict goes beside the
|
|
491
|
+
// file instead), but the step can write that note too, so an entry counts
|
|
492
|
+
// only where the host handed exactly those bytes back (FOL-96).
|
|
493
|
+
const handedBack = opts.handedBack ?? {};
|
|
428
494
|
let imported: Record<string, string> = {};
|
|
429
495
|
try {
|
|
430
496
|
// Read here, on the host: a link the step planted is never followed,
|
|
@@ -433,7 +499,15 @@ export function applyContainerChanges(
|
|
|
433
499
|
const file = path.join(dir, "copied-back.json");
|
|
434
500
|
if (fs.lstatSync(dir).isDirectory() && fs.lstatSync(file).isFile()) {
|
|
435
501
|
const m = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
436
|
-
if (m && typeof m === "object" && !Array.isArray(m))
|
|
502
|
+
if (m && typeof m === "object" && !Array.isArray(m)) {
|
|
503
|
+
for (const [rel, h] of Object.entries(m as Record<string, unknown>)) {
|
|
504
|
+
// Every version the host handed back for the path counts: two jobs
|
|
505
|
+
// can return the same file, and the sandbox may hold the first
|
|
506
|
+
// (the second went beside a parent edit as a conflict copy).
|
|
507
|
+
const ok = handedBack[rel];
|
|
508
|
+
if (typeof h === "string" && (Array.isArray(ok) ? ok.includes(h) : ok === h)) imported[rel] = h;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
437
511
|
}
|
|
438
512
|
} catch {
|
|
439
513
|
/* none copied back */
|
|
@@ -474,6 +548,17 @@ export function applyContainerChanges(
|
|
|
474
548
|
const target = path.join(hostWs, rel);
|
|
475
549
|
const hostNow = fs.existsSync(target) ? fs.readFileSync(target) : null;
|
|
476
550
|
if (hostNow && hostNow.equals(next)) continue;
|
|
551
|
+
// Configuration and code are a person's (copy-back.ts): refused here,
|
|
552
|
+
// after "did the step change it" and before any merge, conflict copy
|
|
553
|
+
// or test divert — so none of those can carry it to the workspace,
|
|
554
|
+
// and the next step is staged from the workspace's own copy.
|
|
555
|
+
const refused = refusedBack(rel);
|
|
556
|
+
if (refused && opts.allowConfig) {
|
|
557
|
+
opts.onKept?.({ path: rel.replaceAll("\\", "/"), kind: refused });
|
|
558
|
+
} else if (refused) {
|
|
559
|
+
opts.onRefused?.({ path: rel.replaceAll("\\", "/"), kind: refused });
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
477
562
|
// The host moved too since the step was handed its copy — another run
|
|
478
563
|
// or a person wrote it. Writing the step's copy over it erased the
|
|
479
564
|
// other run's ledger rows on 2026-09-14. Merge appends; keep both
|
|
@@ -806,6 +891,7 @@ try {
|
|
|
806
891
|
agentDir,
|
|
807
892
|
workspaceRoot: "/workspace",
|
|
808
893
|
libraryRoot: "/library",
|
|
894
|
+
configWrites: input.configWrites === true,
|
|
809
895
|
prompt: input.prompt,
|
|
810
896
|
model: input.model,
|
|
811
897
|
effort: input.effort ?? null,
|
|
@@ -1491,6 +1577,9 @@ export function heldCores(cpus: string, isolation: string | undefined, available
|
|
|
1491
1577
|
|
|
1492
1578
|
export interface RunInContainerArgs {
|
|
1493
1579
|
workspaceRoot: string;
|
|
1580
|
+
/** A person approved this step before it ran: configuration and code it
|
|
1581
|
+
* changed are kept, not refused (copy-back.ts, FOL-96). */
|
|
1582
|
+
configWrites?: boolean;
|
|
1494
1583
|
libraryRoot: string;
|
|
1495
1584
|
/** The platform's gallery, staged under /library BEFORE the account library so the account's copies win. */
|
|
1496
1585
|
galleryRoot?: string;
|
|
@@ -1543,6 +1632,10 @@ export interface RunInContainerArgs {
|
|
|
1543
1632
|
/** A test run: where state/ and storage/ changes go instead of the
|
|
1544
1633
|
* workspace, and who to tell about each. See test-mode.ts. */
|
|
1545
1634
|
divert?: WriteDivert;
|
|
1635
|
+
/** What the host handed back into this sandbox mid-step (StepRecord.
|
|
1636
|
+
* handedBack), read at copy-out — kept on the host, never read back from
|
|
1637
|
+
* the sandbox alone (FOL-96). */
|
|
1638
|
+
handedBack?: () => Record<string, string | string[]> | undefined;
|
|
1546
1639
|
/** The run is a test run. An isolated executor labels the sandbox so the
|
|
1547
1640
|
* cluster can cut its network down to the egress proxy: a script tool
|
|
1548
1641
|
* calls the network directly, not through the proxy where the test-mode
|
|
@@ -1912,8 +2005,17 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
1912
2005
|
fs.mkdirSync(wsOut);
|
|
1913
2006
|
const back = spawnSync(cli(), ["cp", `${containerId}:/workspace/.`, wsOut], { encoding: "utf8" });
|
|
1914
2007
|
let applied: string[] = [];
|
|
2008
|
+
const refused: RefusedPath[] = [];
|
|
2009
|
+
const kept: RefusedPath[] = [];
|
|
1915
2010
|
if (back.status === 0) {
|
|
1916
|
-
applied = applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert
|
|
2011
|
+
applied = applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert, {
|
|
2012
|
+
handedBack: args.handedBack?.(),
|
|
2013
|
+
onRefused: (r) => refused.push(r),
|
|
2014
|
+
allowConfig: args.configWrites === true,
|
|
2015
|
+
onKept: (r) => kept.push(r),
|
|
2016
|
+
});
|
|
2017
|
+
if (refused.length) args.emit("info", refusedLine(refused));
|
|
2018
|
+
if (kept.length) args.emit("info", keptLine(kept));
|
|
1917
2019
|
} else {
|
|
1918
2020
|
args.emit("error", `copy-out failed — the step's file changes were lost:\n${back.stderr.slice(0, 500)}`);
|
|
1919
2021
|
}
|
|
@@ -1927,7 +2029,7 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
1927
2029
|
phases,
|
|
1928
2030
|
};
|
|
1929
2031
|
args.emit("info", timingLine(timing));
|
|
1930
|
-
return { ...outcome, timing, applied };
|
|
2032
|
+
return { ...outcome, timing, applied, ...(refused.length ? { refused } : {}) };
|
|
1931
2033
|
} finally {
|
|
1932
2034
|
if (containerId) spawnSync(cli(), ["rm", "-f", containerId], { stdio: "ignore" });
|
|
1933
2035
|
fs.rmSync(staging, { recursive: true, force: true });
|
package/src/runner.ts
CHANGED
|
@@ -3036,6 +3036,8 @@ async function runStep(
|
|
|
3036
3036
|
tenant,
|
|
3037
3037
|
input: {
|
|
3038
3038
|
agentRel: path.relative(workspaceRoot, agentDir).replaceAll("\\", "/"),
|
|
3039
|
+
// Its file tools inside the sandbox get the same allowance (FOL-96).
|
|
3040
|
+
configWrites: step.approve === true && Boolean(step.approvedAt),
|
|
3039
3041
|
prompt,
|
|
3040
3042
|
model,
|
|
3041
3043
|
effort,
|
|
@@ -3127,6 +3129,11 @@ async function runStep(
|
|
|
3127
3129
|
network: { level: network.level, enforced: forward?.enforce === true },
|
|
3128
3130
|
// A test run's state/ and storage/ changes come out under the run.
|
|
3129
3131
|
divert: testRun && runId ? divertFor(runId) : undefined,
|
|
3132
|
+
// Read at copy-out: what the host handed back into the sandbox.
|
|
3133
|
+
handedBack: () => step.handedBack,
|
|
3134
|
+
// A person approved this step before it ran: configuration and code
|
|
3135
|
+
// it changes are kept, listed on the trail (FOL-96).
|
|
3136
|
+
configWrites: step.approve === true && Boolean(step.approvedAt),
|
|
3130
3137
|
test: testRun,
|
|
3131
3138
|
// Resume: a sandbox a previous driver left running, recorded on the
|
|
3132
3139
|
// step. Consumed once — a fresh attempt after this one (a retry, a
|
|
@@ -3332,6 +3339,11 @@ async function runStep(
|
|
|
3332
3339
|
step.image = outcome.image ?? null;
|
|
3333
3340
|
step.browserPod = outcome.browserPod ? { ...outcome.browserPod } : null;
|
|
3334
3341
|
if (step.subagentOf && outcome.applied?.length) step.wrote = [...new Set([...(step.wrote ?? []), ...outcome.applied])];
|
|
3342
|
+
if (outcome.refused?.length) {
|
|
3343
|
+
const seen = new Map((step.refused ?? []).map((r) => [r.path, r]));
|
|
3344
|
+
for (const r of outcome.refused) seen.set(r.path, r);
|
|
3345
|
+
step.refused = [...seen.values()];
|
|
3346
|
+
}
|
|
3335
3347
|
if (outcome.status === "failed" && outcome.browserPod?.lost) {
|
|
3336
3348
|
const decision = podLossDecision(outcome.browserPod);
|
|
3337
3349
|
if (decision.rerun) {
|
|
@@ -3503,6 +3515,9 @@ async function runStep(
|
|
|
3503
3515
|
const attemptInProcess = (modelEnv: Record<string, string | undefined>) => executeStep({
|
|
3504
3516
|
agentDir,
|
|
3505
3517
|
workspaceRoot,
|
|
3518
|
+
// A person approved this step (its gate) before it ran: it may change
|
|
3519
|
+
// configuration and code (FOL-96) — the desks' retro applies fixes so.
|
|
3520
|
+
configWrites: step.approve === true && Boolean(step.approvedAt),
|
|
3506
3521
|
libraryRoot: libraryDir(tenant),
|
|
3507
3522
|
...(subagents.length ? { subagents } : {}),
|
|
3508
3523
|
// The colleagues' inbox, read after every tool call — the same hook
|
|
@@ -5241,6 +5256,21 @@ function driveRunInner(
|
|
|
5241
5256
|
// Its files go back with the reply, so the delegating step's
|
|
5242
5257
|
// sandbox has them too (delegate-tools.ts copyBackFiles).
|
|
5243
5258
|
const back = readCopyBack(pDir, child.wrote ?? []);
|
|
5259
|
+
// What goes back into the parent's sandbox, as the host sends it:
|
|
5260
|
+
// its copy-out reads this, not the sandbox's own note (FOL-96).
|
|
5261
|
+
if (back.files.length) {
|
|
5262
|
+
parent.handedBack = { ...(parent.handedBack ?? {}) };
|
|
5263
|
+
for (const f of back.files) {
|
|
5264
|
+
const h = crypto.createHash("sha256").update(Buffer.from(f.base64, "base64")).digest("hex");
|
|
5265
|
+
// Kept beside any earlier job's for the same path, never over
|
|
5266
|
+
// it: either may be what the sandbox ends up holding.
|
|
5267
|
+
const prev = parent.handedBack[f.path];
|
|
5268
|
+
const all = prev === undefined ? [] : Array.isArray(prev) ? prev : [prev];
|
|
5269
|
+
if (!all.includes(h)) all.push(h);
|
|
5270
|
+
parent.handedBack[f.path] = all.length === 1 ? all[0] : all;
|
|
5271
|
+
}
|
|
5272
|
+
save();
|
|
5273
|
+
}
|
|
5244
5274
|
return {
|
|
5245
5275
|
status,
|
|
5246
5276
|
result: child.result ?? (errors || "(no reply)"),
|
package/src/step-exec.ts
CHANGED
|
@@ -87,6 +87,9 @@ export interface ExecOutcome {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export interface ExecOptions {
|
|
90
|
+
/** A person approved this step before it ran: its file tools may write
|
|
91
|
+
* the workspace's configuration and code (confine.ts, FOL-96). */
|
|
92
|
+
configWrites?: boolean;
|
|
90
93
|
/** What the step reports beyond its events (FOL-54 step 4): `metadata`
|
|
91
94
|
* sends each model call (`turn`); `full` also sends each tool call's input
|
|
92
95
|
* and output (`io`). Unset sends neither — the consult, the probe and any
|
|
@@ -520,7 +523,7 @@ async function executeStepInner(opts: ExecOptions, runQuery: QueryFn): Promise<E
|
|
|
520
523
|
}
|
|
521
524
|
}
|
|
522
525
|
const verdict = isFilesystemTool(hookInput.tool_name)
|
|
523
|
-
? checkPaths(hookInput.tool_name, hookInput.tool_input as Record<string, unknown>, { agentDir, workspaceRoot, libraryRoot })
|
|
526
|
+
? checkPaths(hookInput.tool_name, hookInput.tool_input as Record<string, unknown>, { agentDir, workspaceRoot, libraryRoot, configWrites: opts.configWrites })
|
|
524
527
|
: null;
|
|
525
528
|
if (verdict && !verdict.ok) {
|
|
526
529
|
emit("error", verdict.reason!, via ? { subagent: via } : undefined);
|
|
@@ -560,7 +563,7 @@ async function executeStepInner(opts: ExecOptions, runQuery: QueryFn): Promise<E
|
|
|
560
563
|
const again = hookInput.tool_name === "Bash"
|
|
561
564
|
? checkBash(String(v.input.command ?? ""))
|
|
562
565
|
: isFilesystemTool(hookInput.tool_name)
|
|
563
|
-
? checkPaths(hookInput.tool_name, v.input, { agentDir, workspaceRoot, libraryRoot })
|
|
566
|
+
? checkPaths(hookInput.tool_name, v.input, { agentDir, workspaceRoot, libraryRoot, configWrites: opts.configWrites })
|
|
564
567
|
: { ok: true as const };
|
|
565
568
|
const granted = opts.allowed.includes(hookInput.tool_name) || opts.mcpNames.some((n) => hookInput.tool_name.startsWith(`mcp__${n}__`));
|
|
566
569
|
// The sub-agent guard read the ORIGINAL input: a rewrite of a
|
|
@@ -628,7 +631,7 @@ async function executeStepInner(opts: ExecOptions, runQuery: QueryFn): Promise<E
|
|
|
628
631
|
toolName === "Bash"
|
|
629
632
|
? checkBash(String(input.command ?? ""))
|
|
630
633
|
: isFilesystemTool(toolName)
|
|
631
|
-
? checkPaths(toolName, input, { agentDir, workspaceRoot, libraryRoot })
|
|
634
|
+
? checkPaths(toolName, input, { agentDir, workspaceRoot, libraryRoot, configWrites: opts.configWrites })
|
|
632
635
|
: { ok: true as const };
|
|
633
636
|
if (!verdict.ok) {
|
|
634
637
|
refund();
|
package/src/store.ts
CHANGED
|
@@ -3533,6 +3533,14 @@ export interface StepRecord {
|
|
|
3533
3533
|
/** On an isolated sub-agent's run: the workspace paths its sandbox's
|
|
3534
3534
|
* changes were applied to — what its delegating step is told it wrote. */
|
|
3535
3535
|
wrote?: string[];
|
|
3536
|
+
/** What this step's sandbox changed that was not taken back to the
|
|
3537
|
+
* workspace: configuration and code, which a step proposes and a person
|
|
3538
|
+
* applies (copy-back.ts, FOL-96). */
|
|
3539
|
+
refused?: { path: string; kind: "config" | "code" }[];
|
|
3540
|
+
/** What the host handed back into this step's sandbox mid-step — its
|
|
3541
|
+
* `sandbox: own` jobs' files, path → sha256 — recorded HERE, on the host,
|
|
3542
|
+
* so the copy-out never has to take the sandbox's word for it (FOL-96). */
|
|
3543
|
+
handedBack?: Record<string, string | string[]>;
|
|
3536
3544
|
/**
|
|
3537
3545
|
* Sandbox seconds this step rented, and how many of them were cold start.
|
|
3538
3546
|
* Set only on the isolated path — an in-process step rents nothing, so
|