@foldrun/core 2.14.2 → 3.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/frontmatter-fields.js +1 -1
- 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/dist/src/store.js +15 -1
- 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/frontmatter-fields.ts +1 -1
- 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 +22 -1
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
|
@@ -2107,7 +2107,7 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
2107
2107
|
// A header name is case-insensitive on the wire; a JSON field is not.
|
|
2108
2108
|
// `body:eventId` lower-cased to `body:eventid` matched nothing, and a
|
|
2109
2109
|
// dedupe that matches nothing admits everything.
|
|
2110
|
-
idempotency:
|
|
2110
|
+
idempotency: idempotencyOf(data.idempotency, data.trigger, data.signature),
|
|
2111
2111
|
debounce: durationOf(data.debounce),
|
|
2112
2112
|
throttle: durationOf(data.throttle),
|
|
2113
2113
|
catchup: data.catchup === "none" || data.catchup === "last" ? data.catchup : null,
|
|
@@ -2144,6 +2144,19 @@ export function parseSchemaText(text: string): { schema: Record<string, unknown>
|
|
|
2144
2144
|
return { schema: value as Record<string, unknown> };
|
|
2145
2145
|
}
|
|
2146
2146
|
|
|
2147
|
+
/**
|
|
2148
|
+
* The flow's `idempotency:`, or its default. A GitHub-signed webhook dedupes
|
|
2149
|
+
* on `x-github-delivery` unless it says otherwise: GitHub's signature has no
|
|
2150
|
+
* timestamp, so a delivery captured once could be replayed for ever, and
|
|
2151
|
+
* every GitHub delivery carries that id (FOL-94). Any `idempotency:` the
|
|
2152
|
+
* file sets wins; `none` (or `false`, `off`) turns dedupe off.
|
|
2153
|
+
*/
|
|
2154
|
+
function idempotencyOf(raw: unknown, trigger: unknown, signature: unknown): string | null {
|
|
2155
|
+
if (raw === false || (typeof raw === "string" && /^(none|off|false)$/i.test(raw.trim()))) return null;
|
|
2156
|
+
if (typeof raw === "string") return idempotencyName(raw);
|
|
2157
|
+
return trigger === "webhook" && String(signature) === "github" ? "x-github-delivery" : null;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2147
2160
|
function idempotencyName(raw: string): string | null {
|
|
2148
2161
|
const v = raw.trim();
|
|
2149
2162
|
if (!v) return null;
|
|
@@ -3520,6 +3533,14 @@ export interface StepRecord {
|
|
|
3520
3533
|
/** On an isolated sub-agent's run: the workspace paths its sandbox's
|
|
3521
3534
|
* changes were applied to — what its delegating step is told it wrote. */
|
|
3522
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[]>;
|
|
3523
3544
|
/**
|
|
3524
3545
|
* Sandbox seconds this step rented, and how many of them were cold start.
|
|
3525
3546
|
* Set only on the isolated path — an in-process step rents nothing, so
|