@foldrun/core 2.6.0 → 2.8.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.
@@ -477,6 +477,7 @@ try {
477
477
  const { buildConsultTools } = await import("@foldrun/core/agent-tools");
478
478
  const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import("@foldrun/core/context-tools");
479
479
  const { httpChannel, buildAskTool, buildPeerTools, inboxReader } = await import("@foldrun/core/operator");
480
+ const { buildDelegateTools } = await import("@foldrun/core/delegate-tools");
480
481
  const { startTranslator } = await import("@foldrun/core/translator");
481
482
  const { prepareRuntime } = await import("@foldrun/core/runtime");
482
483
  const { materializeFileSecrets } = await import("@foldrun/core/secret-files");
@@ -576,6 +577,39 @@ try {
576
577
  const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };
577
578
  const peers = input.peers ? buildPeerTools({ channel, self: input.peers.self, siblings: input.peers.siblings, emit }) : { server: null };
578
579
  const inbox = channel ? inboxReader(channel, emit) : undefined;
580
+ // sandbox: own sub-agents: a tool each, whose job runs in its own sandbox
581
+ // on the worker, reached over the same proxy door.
582
+ const delegate = input.delegates?.length ? buildDelegateTools({ specs: input.delegates, channel, emit, timeoutSec: input.timeoutSec }) : { server: null };
583
+
584
+ // Keyless cloud identities (FOLDRUN_IDENTITIES): each token file kept
585
+ // fresh through the proxy's /identity door, which mints only what this
586
+ // step's lease was granted. Fetched before the step starts, renewed five
587
+ // minutes before it expires (with jitter); a failure is said and retried.
588
+ // Plain JS: this text is the driver, not TypeScript.
589
+ const identityNames = (env.FOLDRUN_IDENTITIES || "").split(",").filter(Boolean);
590
+ if (identityNames.length) {
591
+ // No backslashes: this sits inside a template literal, which eats them.
592
+ const ep = new RegExp("^(https?://[^/]+)/e/([A-Za-z0-9_-]{16,})/?$").exec(env.FOLDRUN_EGRESS || "");
593
+ const dir = env.FOLDRUN_IDENTITY_DIR || "/tmp/foldrun-identity";
594
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
595
+ const keepFresh = async (name) => {
596
+ let wait = 30000;
597
+ try {
598
+ if (!ep) throw new Error("no egress proxy to ask");
599
+ const res = await fetch(ep[1] + "/identity/" + ep[2] + "/" + name, { signal: AbortSignal.timeout(15000) });
600
+ const body = await res.json().catch(() => ({}));
601
+ if (!res.ok || !body.token) throw new Error((body.error && body.error.message) || ("proxy answered " + res.status));
602
+ const file = dir + "/" + name.toLowerCase() + ".jwt";
603
+ fs.writeFileSync(file + ".tmp", body.token, { mode: 0o600 });
604
+ fs.renameSync(file + ".tmp", file);
605
+ wait = Math.max(30000, body.expiresAt - Date.now() - 300000 + Math.floor(Math.random() * 60000) - 30000);
606
+ } catch (err) {
607
+ emit("error", "identity " + name + ": " + (err && err.message ? err.message : String(err)) + " — trying again in 30s");
608
+ }
609
+ setTimeout(() => { void keepFresh(name); }, wait).unref();
610
+ };
611
+ await Promise.all(identityNames.map(keepFresh));
612
+ }
579
613
 
580
614
  mark("tools");
581
615
  const outcome = await executeStep({
@@ -597,6 +631,7 @@ try {
597
631
  ...(desks.server ? { foldrun_desks: desks.server } : {}),
598
632
  ...(ask.server ? { foldrun_ask: ask.server } : {}),
599
633
  ...(peers.server ? { foldrun_peers: peers.server } : {}),
634
+ ...(delegate.server ? { foldrun_delegate: delegate.server } : {}),
600
635
  ...(install.server ? { foldrun_install: install.server } : {}),
601
636
  ...input.mcpServers,
602
637
  },
@@ -1457,8 +1492,9 @@ export async function runStepInContainer(args) {
1457
1492
  const wsOut = path.join(staging, "out");
1458
1493
  fs.mkdirSync(wsOut);
1459
1494
  const back = spawnSync(cli(), ["cp", `${containerId}:/workspace/.`, wsOut], { encoding: "utf8" });
1495
+ let applied = [];
1460
1496
  if (back.status === 0) {
1461
- applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert);
1497
+ applied = applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert);
1462
1498
  }
1463
1499
  else {
1464
1500
  args.emit("error", `copy-out failed — the step's file changes were lost:\n${back.stderr.slice(0, 500)}`);
@@ -1472,7 +1508,7 @@ export async function runStepInContainer(args) {
1472
1508
  phases,
1473
1509
  };
1474
1510
  args.emit("info", timingLine(timing));
1475
- return { ...outcome, timing };
1511
+ return { ...outcome, timing, applied };
1476
1512
  }
1477
1513
  finally {
1478
1514
  if (containerId)
@@ -1,4 +1,4 @@
1
- import { type SubagentSpec } from "./subagents.ts";
1
+ import { type DelegateSpec, type SubagentSpec } from "./subagents.ts";
2
2
  import { type FlowStep, type RunRecord, type StepRecord } from "./store.ts";
3
3
  import { type Catalog } from "./catalog.ts";
4
4
  import { type SearchRoot, type RunDigest } from "./context-tools.ts";
@@ -189,6 +189,26 @@ export declare function buildSubagentSpecs(o: {
189
189
  parentAllowed: string[];
190
190
  push: (type: "info" | "error", text: string) => void;
191
191
  }): SubagentSpec[];
192
+ /**
193
+ * The `sandbox: own` sub-agents a step may hand jobs to: a name and a
194
+ * description each, all the parent's sandbox needs. What they may hold is
195
+ * read from their own file when a job arrives — never cut to the parent's,
196
+ * and never seen by it. `canRun` is false where nothing can start their
197
+ * sandbox (a sub-agent's own step: depth one; a step outside a run).
198
+ */
199
+ export declare function buildDelegateSpecs(o: {
200
+ names: unknown;
201
+ self: string;
202
+ workspaceRoot: string;
203
+ canRun: boolean;
204
+ push: (type: "info" | "error", text: string) => void;
205
+ /** Told, per offered sub-agent, whether a tool it holds acts outside the
206
+ * workspace — a delegating step whose job may have sent something must
207
+ * not be re-run as if it had only read (launchStep's retry policy). */
208
+ outward?: (name: string, outward: boolean) => void;
209
+ tenant?: string;
210
+ tags?: string[];
211
+ }): DelegateSpec[];
192
212
  export declare function sharedInstructions(agentDir: string, tenant: string): string | null;
193
213
  /**
194
214
  * Which of an agent's in-scope skills load for this run.