@foldrun/core 9.1.0 → 9.2.1

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.
Files changed (44) hide show
  1. package/dist/src/a2a-client.d.ts +165 -0
  2. package/dist/src/a2a-client.js +440 -0
  3. package/dist/src/a2a-publish.d.ts +37 -0
  4. package/dist/src/a2a-publish.js +116 -0
  5. package/dist/src/a2a-remotes.d.ts +73 -0
  6. package/dist/src/a2a-remotes.js +192 -0
  7. package/dist/src/a2a-tools.d.ts +148 -0
  8. package/dist/src/a2a-tools.js +833 -0
  9. package/dist/src/api-tools.js +15 -10
  10. package/dist/src/browser-pod.js +4 -0
  11. package/dist/src/completions.js +8 -8
  12. package/dist/src/frontmatter-fields.js +2 -0
  13. package/dist/src/frontmatter-schema.js +4 -0
  14. package/dist/src/limits.js +22 -4
  15. package/dist/src/run-container.d.ts +10 -1
  16. package/dist/src/run-container.js +17 -0
  17. package/dist/src/runner.d.ts +10 -0
  18. package/dist/src/runner.js +158 -14
  19. package/dist/src/scan.js +102 -5
  20. package/dist/src/secrets.d.ts +30 -0
  21. package/dist/src/secrets.js +41 -1
  22. package/dist/src/starter.js +6 -1
  23. package/dist/src/store.d.ts +34 -0
  24. package/dist/src/store.js +28 -7
  25. package/dist/src/subagents.d.ts +16 -0
  26. package/dist/src/subagents.js +23 -2
  27. package/package.json +4 -1
  28. package/src/a2a-client.ts +517 -0
  29. package/src/a2a-publish.ts +120 -0
  30. package/src/a2a-remotes.ts +225 -0
  31. package/src/a2a-tools.ts +919 -0
  32. package/src/api-tools.ts +20 -8
  33. package/src/browser-pod.ts +3 -0
  34. package/src/completions.ts +8 -8
  35. package/src/frontmatter-fields.ts +2 -0
  36. package/src/frontmatter-schema.ts +4 -0
  37. package/src/limits.ts +22 -4
  38. package/src/run-container.ts +22 -0
  39. package/src/runner.ts +152 -11
  40. package/src/scan.ts +123 -6
  41. package/src/secrets.ts +43 -1
  42. package/src/starter.ts +6 -1
  43. package/src/store.ts +47 -2
  44. package/src/subagents.ts +29 -2
@@ -17,7 +17,7 @@ import { EGRESS_ENV, viaEgress } from "./egress.js";
17
17
  import { z } from "zod";
18
18
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
19
19
  import { previewOrKeep, RESULT_KEEP_CHARS } from "./tool-output.js";
20
- import { resolveSecrets, materializeSecrets } from "./secrets.js";
20
+ import { resolveSecrets, materializeCurrent } from "./secrets.js";
21
21
  // Collected whole up to this; the model sees a preview and where the rest is
22
22
  // kept (tool-output.ts), not a cut with the rest gone.
23
23
  const MAX_BODY_CHARS = RESULT_KEEP_CHARS;
@@ -97,7 +97,7 @@ export function typedToolName(apiName, opId) {
97
97
  */
98
98
  async function performRequest(api, env, bucket, log, req,
99
99
  /** Where a long body is kept, when the agent can read it back (tool-output.ts). */
100
- keepIn) {
100
+ keepIn, scope = null) {
101
101
  const started = Date.now();
102
102
  try {
103
103
  const waited = bucket ? await bucket.take() : 0;
@@ -105,8 +105,11 @@ keepIn) {
105
105
  // Per call, not per build: an oauth2 secret's recipe becomes a
106
106
  // live access token here, so a step that runs for an hour keeps
107
107
  // getting fresh tokens (the exchange is cached until near expiry)
108
- // — and a recipe can never end up inside a header.
109
- const live = await materializeSecrets(env);
108
+ // — and a recipe can never end up inside a header. On a host, the
109
+ // vault's recipe as it is now, made live in the account's scope: a
110
+ // rotating refresh token the runner (or an earlier call) spent is never
111
+ // sent again (secrets.ts materializeCurrent).
112
+ const live = await materializeCurrent(env, scope);
110
113
  const path = req.path.startsWith("/") ? req.path : `/${req.path}`;
111
114
  const url = new URL(api.base + path);
112
115
  // Base-URL confinement: a path can't escape the declared host.
@@ -183,7 +186,7 @@ function argKeys(op) {
183
186
  }
184
187
  return out;
185
188
  }
186
- function typedTool(api, op, env, bucket, log, keepIn) {
189
+ function typedTool(api, op, env, bucket, log, keepIn, scope = null) {
187
190
  const keys = argKeys(op);
188
191
  const shape = {};
189
192
  for (const [key, p] of keys)
@@ -235,10 +238,10 @@ function typedTool(api, op, env, bucket, log, keepIn) {
235
238
  query,
236
239
  headers,
237
240
  body: typeof values.body === "string" ? values.body : undefined,
238
- }, keepIn);
241
+ }, keepIn, scope);
239
242
  });
240
243
  }
241
- function genericTool(api, env, bucket, log, keepIn) {
244
+ function genericTool(api, env, bucket, log, keepIn, scope = null) {
242
245
  const typed = api.resolvedOperations?.length
243
246
  ? ` Prefer the typed ${api.name}_* tools; this one is the escape hatch for operations they do not cover.`
244
247
  : "";
@@ -258,7 +261,7 @@ function genericTool(api, env, bucket, log, keepIn) {
258
261
  query: args.query ?? {},
259
262
  headers: {},
260
263
  body: args.body,
261
- }, keepIn));
264
+ }, keepIn, scope));
262
265
  }
263
266
  export function buildApiTools(tenant, apis, workspace,
264
267
  // The runner container passes this: secrets were resolved before the specs
@@ -277,6 +280,8 @@ keepIn) {
277
280
  missing: resolved.missing,
278
281
  }
279
282
  : resolveSecrets(tenant, neededSecrets, workspace);
283
+ // Read from the vault here: made live per call in its account's scope.
284
+ const scope = !resolved && tenant ? { tenant, ...(workspace ? { workspace } : {}) } : null;
280
285
  const log = [];
281
286
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the SDK types its own list this way
282
287
  const tools = [];
@@ -311,12 +316,12 @@ keepIn) {
311
316
  }
312
317
  const typedNames = [];
313
318
  for (const op of ops) {
314
- tools.push(typedTool(api, op, env, bucket, log, keepIn));
319
+ tools.push(typedTool(api, op, env, bucket, log, keepIn, scope));
315
320
  typedNames.push(typedToolName(api.name, op.id));
316
321
  methods[`mcp__foldrun_apis__${typedToolName(api.name, op.id)}`] = String(op.method ?? "").toUpperCase();
317
322
  }
318
323
  if (withGeneric)
319
- tools.push(genericTool(api, env, bucket, log, keepIn));
324
+ tools.push(genericTool(api, env, bucket, log, keepIn, scope));
320
325
  const names = [...typedNames, ...(withGeneric ? [`call_${api.name}`] : [])];
321
326
  for (const n of names) {
322
327
  toolNames.push(`mcp__foldrun_apis__${n}`);
@@ -173,6 +173,10 @@ export function classifyCall(tool, input, ctx = {}) {
173
173
  // colleague is said, and a re-run would say it twice.
174
174
  if (server === "foldrun_peers")
175
175
  return name === "peers_list" ? { write: false } : write("a message to a colleague");
176
+ // remotes: — reading where a request got to changes nothing; a send, a
177
+ // reply or a cancel acts on an outside agent's system.
178
+ if (server === "foldrun_a2a")
179
+ return name === "a2a_status" ? { write: false } : write(`${name.replace(/^a2a_/, "")} to an outside agent`);
176
180
  if (server === "foldrun_scripts") {
177
181
  if (name !== "web")
178
182
  return write(`script ${name}`);
@@ -331,9 +331,9 @@ export function completionsAt(path, text, cursor, vocab) {
331
331
  // that field's files and nothing else. `[[search]]` in tools: is how a
332
332
  // tool named after a runtime group gets granted; offering the group
333
333
  // here would defeat the point of the brackets.
334
- const inlineOwner = line.match(/^\s*([a-zA-Z_]+):\s*(?:\[.*)?\[\[[^\]]*$/)?.[1];
334
+ const inlineOwner = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*):\s*(?:\[.*)?\[\[[^\]]*$/)?.[1];
335
335
  const blockOwner = /^\s+-\s*\[\[/.test(line)
336
- ? before.slice(0, lineStart).split("\n").reverse().find((l) => /^\s*[a-zA-Z_]+:\s*$/.test(l))?.trim().replace(":", "")
336
+ ? before.slice(0, lineStart).split("\n").reverse().find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))?.trim().replace(":", "")
337
337
  : undefined;
338
338
  const owner = inlineOwner ?? blockOwner;
339
339
  const byField = {
@@ -391,7 +391,7 @@ export function completionsAt(path, text, cursor, vocab) {
391
391
  // been chosen yet, one at a time. Fixed combinations were the wrong
392
392
  // model: nobody wants a menu of every permutation, they want to add the
393
393
  // next verb.
394
- const inlineList = line.match(/^(\s*)([a-zA-Z_]+):\s*\[([^\]]*)$/);
394
+ const inlineList = line.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):\s*\[([^\]]*)$/);
395
395
  if (inlineList) {
396
396
  const [, , key, inside] = inlineList;
397
397
  // The inline and block forms of a list are the same field — `tools: [a, b]`
@@ -435,7 +435,7 @@ export function completionsAt(path, text, cursor, vocab) {
435
435
  }
436
436
  }
437
437
  // 4. `key: value` — the value side of a known field.
438
- const kv = line.match(/^(\s*)([a-zA-Z_]+):\s*(\S*)$/);
438
+ const kv = line.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):\s*(\S*)$/);
439
439
  if (kv) {
440
440
  const [, , key, partial] = kv;
441
441
  const from = cursor - partial.length;
@@ -517,7 +517,7 @@ export function completionsAt(path, text, cursor, vocab) {
517
517
  const partial = listItem[2];
518
518
  const from = cursor - partial.length;
519
519
  const priorLines = before.slice(0, lineStart).split("\n").reverse();
520
- const owner = priorLines.find((l) => /^\s*[a-zA-Z_]+:\s*$/.test(l))?.trim().replace(":", "");
520
+ const owner = priorLines.find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))?.trim().replace(":", "");
521
521
  const byOwner = {
522
522
  // One list now: built-ins and your own tools are granted the same way.
523
523
  // Groups first (they cover most needs), then your own tools — the ones
@@ -558,7 +558,7 @@ export function completionsAt(path, text, cursor, vocab) {
558
558
  const partial = mapping[2] ?? "";
559
559
  const priorLines = before.slice(0, lineStart).split("\n").reverse();
560
560
  const owner = priorLines
561
- .find((l) => /^\s*[a-zA-Z_]+:\s*$/.test(l))
561
+ .find((l) => /^\s*[a-zA-Z_][a-zA-Z0-9_]*:\s*$/.test(l))
562
562
  ?.trim()
563
563
  .replace(":", "");
564
564
  const byOwner = {
@@ -593,7 +593,7 @@ export function completionsAt(path, text, cursor, vocab) {
593
593
  // kind of file — the block takes nothing else.
594
594
  const fmClose = text.indexOf("\n---", text.indexOf("---") + 3);
595
595
  const insideFrontmatter = text.startsWith("---") && (fmClose === -1 || cursor <= fmClose);
596
- const nested = line.match(/^(\s+)([a-zA-Z_]*)$/);
596
+ const nested = line.match(/^(\s+)([a-zA-Z_][a-zA-Z0-9_]*|)$/);
597
597
  if (insideFrontmatter && nested) {
598
598
  const header = before.slice(0, lineStart).split("\n").reverse().find((l) => /^\S/.test(l));
599
599
  const job = JOBS.find((j) => new RegExp(`^${j.name}\\s*:\\s*$`).test(header ?? ""));
@@ -624,7 +624,7 @@ export function completionsAt(path, text, cursor, vocab) {
624
624
  // this kind of file. This is where you learn the format by writing it.
625
625
  const fmEnd = text.indexOf("\n---", text.indexOf("---") + 3);
626
626
  const inFrontmatter = text.startsWith("---") && (fmEnd === -1 || cursor <= fmEnd);
627
- if (inFrontmatter && /^[a-zA-Z_][a-zA-Z_-]*$|^$/.test(line)) {
627
+ if (inFrontmatter && /^[a-zA-Z_][a-zA-Z0-9_-]*$|^$/.test(line)) {
628
628
  const from = cursor - line.length;
629
629
  return { from, query: line, items: filter(fieldsFor(path, kind), line), title: `${kind} fields` };
630
630
  }
@@ -43,6 +43,7 @@ const AGENT = [
43
43
  { key: "scripts", type: "list", help: "programs in scripts/, each a tool" },
44
44
  { key: "apis", type: "list", help: "an HTTP API as a tool" },
45
45
  { key: "mcpServers", type: "object", help: "MCP servers, name → {command | url, args, env, headers}" },
46
+ { key: "remotes", type: "list", help: "outside A2A agents it may call — [{name, card: the https URL of its agent card, secret?: a vault NAME sent as a bearer token, description?}]" },
46
47
  { key: "agents", type: ["list", "string"], help: "colleagues it may consult" },
47
48
  { key: "subagents", type: ["list", "string"], help: "colleagues it may delegate to" },
48
49
  { key: "sandbox", type: "string", values: ["own", "parent"], help: "when delegated to as a sub-agent: own = its own sandbox, tools, secrets and network; parent (default) = inside the delegating step's" },
@@ -127,6 +128,7 @@ const FLOW = [
127
128
  { key: "budget", type: ["number", "string"], help: "USD per run" },
128
129
  { key: "compute", type: "object", help: "{vcpu, memory, disk} one run's steps may hold together at once — only narrows the workspace's and account's" },
129
130
  { key: "live", type: "boolean", help: "true: steps may act for real in a test run" },
131
+ { key: "a2a", type: "object", help: "serve this flow to outside agents over A2A 1.0 — {publish: true, name?, description?, examples?, public_card?, input?}; changing it needs workspace:manage" },
130
132
  ];
131
133
  const TOOL = [
132
134
  { key: "name", type: "string", help: "defaults to the file or folder name" },
@@ -9,6 +9,8 @@ import { cronProblem, parseCron, cronCanFire } from "./cron.js";
9
9
  import { SECRET_NAME } from "./secrets.js";
10
10
  import { modelSecretCollision } from "./model-credential.js";
11
11
  import { resourcesProblem, computeProblem } from "./resources.js";
12
+ import { remotesProblem } from "./a2a-remotes.js";
13
+ import { a2aPublishProblem } from "./a2a-publish.js";
12
14
  export { FRONTMATTER, schemaKindOf } from "./frontmatter-fields.js";
13
15
  /** A value for a message: a scalar as written, anything else by its shape.
14
16
  * Never String() of a map — its `toString` is whatever the YAML says. */
@@ -31,6 +33,7 @@ const CHECKS = {
31
33
  "agent": {
32
34
  effort: (v) => (resolveEffort(v) ? null : `"${String(v)}" is not an effort — low, medium, high, xhigh or max; the step runs at the model's default`),
33
35
  apis: apisProblem,
36
+ remotes: remotesProblem,
34
37
  budget: (v) => budgetProblem(v, ["run"]),
35
38
  ask: (v) => askProblem(v),
36
39
  resources: resourcesProblem,
@@ -55,6 +58,7 @@ const CHECKS = {
55
58
  pause_when: (v) => (String(v).length <= 60 ? null : "is longer than 60 characters"),
56
59
  budget: (v) => budgetProblem(v, ["run"]),
57
60
  compute: computeProblem,
61
+ a2a: a2aPublishProblem,
58
62
  },
59
63
  "tool": {
60
64
  transport: (v) => (["http", "script", "mcp"].includes(String(v).toLowerCase()) ? null : `"${String(v)}" is not a transport — http, script or mcp`),
@@ -5,6 +5,8 @@
5
5
  // web.search: 40 # one action of the web tool
6
6
  // web: 150 # every web action together
7
7
  // crm: 20 # an API, script or MCP tool, by the name it is granted as
8
+ // a2a: 10 # messages to outside agents (a2a_send and a2a_reply)
9
+ // a2a.status: 30 # polls of them, apart from the sends
8
10
  // calls: 300 # every tool call in the step, whatever the tool
9
11
  // repeat: 5 # the same call — one tool, the same input — at most this often
10
12
  //
@@ -21,6 +23,7 @@
21
23
  import crypto from "node:crypto";
22
24
  import { WEB_ACTIONS } from "./providers.js";
23
25
  import { DELEGATE_TOOLS } from "./subagents.js";
26
+ import { A2A_SERVER, A2A_LIMIT_KEY, A2A_LIMIT_ACTIONS } from "./a2a-remotes.js";
24
27
  /** The key that caps every call in the step. */
25
28
  export const TOTAL_KEY = "calls";
26
29
  /** The key that caps one identical call: a model asking the same tool the
@@ -96,8 +99,11 @@ export function readLimits(raw, where = "limits") {
96
99
  }
97
100
  if (k.includes(".")) {
98
101
  const [head, action] = k.split(".");
99
- if (head !== "web" || !WEB_ACTIONS.includes(action)) {
100
- problems.push(`${where}: "${k}" is not a key — the dotted keys are web.<action>: ${WEB_ACTIONS.map((a) => `web.${a}`).join(", ")}.`);
102
+ const web = head === "web" && WEB_ACTIONS.includes(action);
103
+ const a2a = head === A2A_LIMIT_KEY && A2A_LIMIT_ACTIONS.includes(action);
104
+ if (!web && !a2a) {
105
+ problems.push(`${where}: "${k}" is not a key — the dotted keys are web.<action>: ${WEB_ACTIONS.map((a) => `web.${a}`).join(", ")}, ` +
106
+ `and ${A2A_LIMIT_ACTIONS.map((a) => `${A2A_LIMIT_KEY}.${a}`).join(", ")}.`);
101
107
  continue;
102
108
  }
103
109
  }
@@ -166,6 +172,16 @@ export function limitKeysFor(toolName, input, owners = {}) {
166
172
  }
167
173
  if (DELEGATE_TOOLS.includes(toolName))
168
174
  return keys;
175
+ // remotes: — a message to an outside agent (a2a_send, a2a_reply) counts
176
+ // under `a2a`; every tool also under its own `a2a.<action>`, so polling
177
+ // (`a2a.status`) is capped apart from sending.
178
+ const a2a = new RegExp(`^mcp__${A2A_SERVER}__a2a_([a-z]+)$`).exec(toolName);
179
+ if (a2a) {
180
+ if (a2a[1] === "send" || a2a[1] === "reply")
181
+ keys.push(A2A_LIMIT_KEY);
182
+ keys.push(`${A2A_LIMIT_KEY}.${a2a[1]}`);
183
+ return keys;
184
+ }
169
185
  const owner = owners[toolName];
170
186
  const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
171
187
  const name = owner
@@ -263,7 +279,7 @@ export class CallCounter {
263
279
  */
264
280
  export function limitKeyProblems(limits, ctx) {
265
281
  const out = [];
266
- const builtinKeys = ["read", "write", "code", ...Object.keys(BUILTIN_GROUP), "TodoWrite", "search", "history", "desks", "ask", "web"];
282
+ const builtinKeys = ["read", "write", "code", ...Object.keys(BUILTIN_GROUP), "TodoWrite", "search", "history", "desks", "ask", "web", A2A_LIMIT_KEY];
267
283
  const known = new Set([...builtinKeys, ...ctx.known].map(normKey));
268
284
  const granted = new Set([...ctx.granted].flatMap((g) => {
269
285
  const n = normKey(g);
@@ -281,10 +297,12 @@ export function limitKeyProblems(limits, ctx) {
281
297
  for (const key of Object.keys(limits)) {
282
298
  if (key === TOTAL_KEY || key === REPEAT_KEY)
283
299
  continue;
284
- const base = key.startsWith("web.") ? "web" : key;
300
+ const base = key.startsWith("web.") ? "web" : key.startsWith(`${A2A_LIMIT_KEY}.`) ? A2A_LIMIT_KEY : key;
285
301
  const n = normKey(base);
286
302
  if (!known.has(n)) {
287
303
  const valid = [TOTAL_KEY, "web", ...WEB_ACTIONS.map((a) => `web.${a}`), ...[...new Set([...ctx.granted].map(String))].filter((g) => g !== "web")];
304
+ if ([...ctx.granted].map(String).includes(A2A_LIMIT_KEY))
305
+ valid.push(...A2A_LIMIT_ACTIONS.map((a) => `${A2A_LIMIT_KEY}.${a}`));
288
306
  out.push({ level: "error", message: `limits: ${key} — not a tool this agent could call. The keys: ${[...new Set(valid)].join(", ")}.` });
289
307
  }
290
308
  else if (!granted.has(n)) {
@@ -16,6 +16,7 @@ import { type OperatorEvent } from "./operator.ts";
16
16
  import type { SearchRoot, RunDigest } from "./context-tools.ts";
17
17
  import type { TranslatorSpec } from "./translator.ts";
18
18
  import { type BrowserPodOutcome } from "./browser-pod.ts";
19
+ import type { RemoteSpec } from "./a2a-remotes.ts";
19
20
  /** What crosses the boundary. Everything in here is values — pre-resolved
20
21
  * secrets in headers, assembled prompt, serializable MCP configs. */
21
22
  export interface ContainerStepInput {
@@ -32,6 +33,14 @@ export interface ContainerStepInput {
32
33
  /** http/stdio configs only — in-process servers are rebuilt inside. */
33
34
  mcpServers: Record<string, McpServerConfig>;
34
35
  apis: ApiSpec[];
36
+ /** `remotes:` — outside A2A agents, each spec's `token` filled host-side
37
+ * (`${NAME}` for the egress proxy, or the value with no proxy), and the
38
+ * run and step its handles are kept under (a2a-tools.ts). */
39
+ a2a?: {
40
+ remotes: RemoteSpec[];
41
+ runId: string | null;
42
+ step: number | null;
43
+ };
35
44
  scripts: ScriptSpec[];
36
45
  /** The agent's `runtime:` declaration — the driver builds the venv/npm
37
46
  * prefix inside the container, where the network is. Without this,
@@ -339,7 +348,7 @@ export declare function runtimeCacheMount(tenant: string | undefined): {
339
348
  target: string;
340
349
  } | null;
341
350
  /** The runner container's entrypoint, as JS text: written into the image as driver.mjs, so it must be plain JS — a TypeScript cast here broke every step on v2026.10.21.5 (tests/run-container.test.ts checks it). */
342
- export declare const DRIVER = "// The runner container's entrypoint. Reads one step, executes it with the\n// same core the server uses, speaks JSONL on stdout, exits.\nimport fs from \"node:fs\";\nconst input = JSON.parse(fs.readFileSync(\"/opt/runner/job/input.json\", \"utf8\"));\n// Start-up marks (run-container.ts#STARTUP_PHASES): which phase just ended,\n// on the sandbox's clock. A worker that does not know a mark skips the line.\nconst mark = (name) => process.stdout.write(JSON.stringify({ e: \"mark\", name, at: Date.now() }) + \"\\n\");\nmark(\"driver\");\nlet modelSpoke = false;\nconst emit = (type, text, extra) => {\n // The model's first text or tool call is where start-up ends and the\n // step's own work begins \u2014 runtime and tool lines before it are set-up.\n if (!modelSpoke && (type === \"text\" || type === \"tool\")) { modelSpoke = true; mark(\"model\"); }\n process.stdout.write(JSON.stringify({ e: \"event\", type, text, ...extra }) + \"\\n\");\n};\n// What this sandbox actually touched, read at the last moment from the\n// kernel's own books: cgroup v2 for CPU busy-time and peak memory,\n// /proc/net/dev for bytes on the wire. Every read is best-effort \u2014 gVisor\n// exposes some of these files and not others depending on version, and a\n// metric we cannot read is null, never guessed. Reservation is what is\n// billed; these exist so the reservation can be right-sized.\nfunction readActuals() {\n // fs from the module's own import \u2014 the driver is ESM, and a require()\n // here threw on every call, silently nulling every metric it guarded.\n const read = (p) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return null; } };\n // Two cgroup dialects: v2 (cpu.stat, microseconds; memory.peak) on a\n // plain runtime, v1-style controllers under gVisor (cpuacct.usage,\n // NANOseconds; memory.max_usage_in_bytes) \u2014 probed on the real cluster,\n // not assumed. Whichever answers wins; both silent means null.\n const cpuStat = read(\"/sys/fs/cgroup/cpu.stat\");\n const usage = cpuStat?.match(/^usage_usec (\\d+)/m);\n const cpuacctNs = usage ? NaN : Number(read(\"/sys/fs/cgroup/cpuacct/cpuacct.usage\")?.trim());\n // Time the CPU cap held the sandbox back: v2's throttled_usec in the same\n // cpu.stat, v1's throttled_time (nanoseconds) under gVisor. Large against\n // busy time means the step wanted more cores than its class gives.\n const throttledUs = cpuStat?.match(/^throttled_usec (\\d+)/m);\n const throttledNs = throttledUs ? null : read(\"/sys/fs/cgroup/cpu/cpu.stat\")?.match(/^throttled_time (\\d+)/m);\n const peak =\n read(\"/sys/fs/cgroup/memory.peak\") ??\n read(\"/sys/fs/cgroup/memory/memory.max_usage_in_bytes\") ??\n read(\"/sys/fs/cgroup/memory/memory.usage_in_bytes\");\n let rx = 0, tx = 0, sawNet = false;\n const net = read(\"/proc/net/dev\");\n if (net) {\n for (const line of net.split(\"\\n\").slice(2)) {\n const m = line.trim().match(/^(\\S+):\\s+(\\d+)(?:\\s+\\d+){7}\\s+(\\d+)/);\n if (m && m[1] !== \"lo\") { rx += Number(m[2]); tx += Number(m[3]); sawNet = true; }\n }\n }\n return {\n busyCpuSecs: usage\n ? Number(usage[1]) / 1e6\n : Number.isFinite(cpuacctNs) && cpuacctNs > 0\n ? cpuacctNs / 1e9\n : null,\n peakMemBytes: peak ? Number(peak.trim()) || null : null,\n throttledSecs: throttledUs ? Number(throttledUs[1]) / 1e6 : throttledNs ? Number(throttledNs[1]) / 1e9 : null,\n rxBytes: sawNet ? rx : null,\n txBytes: sawNet ? tx : null,\n };\n}\n\ntry {\n // Runtime state (venvs, npm prefixes) lands in the agent user's real home\n // \u2014 writable, inside the container, gone with it.\n process.env.FOLDRUN_DATA = \"/home/agent/.foldrun\";\n const { executeStep } = await import(\"@foldrun/core/step-exec\");\n const { buildApiTools } = await import(\"@foldrun/core/api-tools\");\n const { buildScriptTools } = await import(\"@foldrun/core/script-tools\");\n const { buildConsultTools } = await import(\"@foldrun/core/agent-tools\");\n const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import(\"@foldrun/core/context-tools\");\n const { httpChannel, buildAskTool, buildPeerTools, inboxReader } = await import(\"@foldrun/core/operator\");\n const { buildDelegateTools } = await import(\"@foldrun/core/delegate-tools\");\n const { startTranslator } = await import(\"@foldrun/core/translator\");\n const { prepareRuntime } = await import(\"@foldrun/core/runtime\");\n const { materializeFileSecrets } = await import(\"@foldrun/core/secret-files\");\n const { paidWebActions } = await import(\"@foldrun/core/browser-pod\");\n mark(\"imports\");\n\n const agentDir = \"/workspace/\" + input.agentRel;\n // HOME is where a bare relative path lands when anything resolves one\n // outside cwd \u2014 and root's /root is outside the workspace, so such a read\n // was refused by confinement with a path the agent never typed. Its own\n // directory is both the truthful answer and the safe one.\n // @file secrets crossed as content; materialise them to 0600 paths inside\n // the container (a host path from the other side would not exist here).\n const env = materializeFileSecrets(agentDir, { ...process.env, HOME: agentDir }).env;\n // Caches go to the sandbox user's own home, not to HOME: the Claude CLI's\n // logs, fontconfig, Mesa and gstreamer all follow XDG_CACHE_HOME, and under\n // agents/<name>/.cache they were copied back after every step \u2014 3,855 files\n // across 179 agent folders on the box by 17 Oct 2026, read by nothing.\n env.XDG_CACHE_HOME = \"/home/agent/.cache\";\n\n // The declared runtime, built in here \u2014 pip and npm have the network, which\n // the host side may not. It lands in .foldrun/runner/.runtimes, which the\n // executors mount from a per-tenant directory (run-container.ts#RUNTIME_CACHE):\n // the first step for a given fingerprint installs, every later one finds\n // .ready and skips. With no mount it still works, just fresh every time \u2014\n // which is what this used to be unconditionally. The log says which happened.\n const runtime = input.runtime\n ? prepareRuntime(\"runner\", input.runtime)\n : { interpreters: {}, env: {}, log: [], error: null };\n for (const line of runtime.log) emit(\"info\", \"runtime: \" + line);\n if (runtime.error) emit(\"error\", \"runtime: \" + runtime.error);\n // The step's own environment gets it too, not only the script tools: an\n // agent that runs \"python3 x.py\" or a shebang script through Bash should\n // meet the packages its runtime declared, not the image's bare python.\n Object.assign(env, runtime.env);\n\n // install: what the agent finds it needs mid-step. apt goes through the\n // root start-up's helper (install-helper.ts); pip and npm land in a\n // directory of this container's own, on the paths from the first turn.\n const { buildInstallTool, INSTALL_TOOL } = await import(\"@foldrun/core/install-tool\");\n const { stepInstallEnv, INSTALL_SOCKET } = await import(\"@foldrun/core/runtime\");\n const installDir = \"/home/agent/.foldrun/step\";\n const installing = (input.allowed ?? []).includes(INSTALL_TOOL);\n if (installing) Object.assign(env, stepInstallEnv(installDir, env));\n const install = installing\n ? buildInstallTool({\n dir: installDir,\n socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,\n cacheRoot: \"/home/agent/.foldrun/runner/.runtimes\",\n python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + \"/bin/python\" : \"python3\",\n env,\n emit,\n })\n : { server: null };\n\n // API and script tools, rebuilt in here from their specs. Secrets were\n // substituted into API headers before the input crossed the boundary, so\n // buildApiTools gets an already-resolved environment. verify: gets the\n // container's own environment, whole: it is the boundary, assembled on\n // the host from an allowlist, and the declared secrets are in it.\n // A long result is kept in a file only when the agent can read it back.\n // (tool-output.ts canReadBack, inlined: the driver imports core by subpath.)\n const keepIn = { dir: agentDir, readable: () => (input.allowed ?? []).some((t) => t === \"Read\" || t === \"Grep\") };\n // A declared secret named like a model variable crosses as\n // FOLDRUN_DECLARED_<NAME> (runner.ts, FOL-19): the model keeps its own\n // settings, and the agent's API and script tools and verify: get the value\n // back under the name it declared. Built here, after the runtime and any\n // mid-step install have added their paths to env.\n const toolEnv = { ...env };\n for (const [k, v] of Object.entries(env)) if (k.startsWith(\"FOLDRUN_DECLARED_\")) toolEnv[k.slice(\"FOLDRUN_DECLARED_\".length)] = v;\n const api = buildApiTools(\"\", input.apis, undefined, { env: toolEnv, missing: [] }, keepIn);\n const script = buildScriptTools(\n agentDir,\n input.scripts,\n toolEnv,\n \"/library/scripts\",\n runtime.interpreters,\n null,\n keepIn.readable,\n );\n // The translator, when the provider speaks Chat Completions: a loopback\n // server for the length of this step. Its env replaces the endpoint the\n // host assembled; the provider's real URL and key stay inside it.\n const translator = input.translator ? await startTranslator(input.translator) : null;\n if (translator) {\n Object.assign(env, translator.env);\n emit(\"info\", \"translator: listening on \" + translator.baseUrl + \" for \" + (input.translator.label ?? input.translator.upstreamBase));\n }\n const consult = buildConsultTools(input.consults ?? [], env, emit);\n // The platform's own two groups, rebuilt from values like everything else.\n const search = buildSearchTools(input.search ?? []);\n const history = input.history?.length ? buildHistoryTools(input.history) : { server: null };\n const desks = input.desks?.length ? buildDeskTools(input.desks) : { server: null };\n // A person in the loop, over the proxy this step's lease already names:\n // ask_person when tools: [ask], and the run's inbox after every tool call.\n const channel = httpChannel(env.FOLDRUN_EGRESS);\n const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };\n const peers = input.peers ? buildPeerTools({ channel, self: input.peers.self, siblings: input.peers.siblings, emit }) : { server: null };\n const inbox = channel ? inboxReader(channel, emit) : undefined;\n // sandbox: own sub-agents: a tool each, whose job runs in its own sandbox\n // on the worker, reached over the same proxy door.\n const delegate = input.delegates?.length ? buildDelegateTools({ specs: input.delegates, channel, emit, timeoutSec: input.timeoutSec, workspaceRoot: \"/workspace\" }) : { server: null };\n\n // Keyless cloud identities (FOLDRUN_IDENTITIES): each token file kept\n // fresh through the proxy's /identity door, which mints only what this\n // step's lease was granted. Fetched before the step starts, renewed five\n // minutes before it expires (with jitter); a failure is said and retried.\n // Plain JS: this text is the driver, not TypeScript.\n const identityNames = (env.FOLDRUN_IDENTITIES || \"\").split(\",\").filter(Boolean);\n if (identityNames.length) {\n // No backslashes: this sits inside a template literal, which eats them.\n const ep = new RegExp(\"^(https?://[^/]+)/e/([A-Za-z0-9_-]{16,})/?$\").exec(env.FOLDRUN_EGRESS || \"\");\n const dir = env.FOLDRUN_IDENTITY_DIR || \"/tmp/foldrun-identity\";\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const keepFresh = async (name) => {\n let wait = 30000;\n try {\n if (!ep) throw new Error(\"no egress proxy to ask\");\n const res = await fetch(ep[1] + \"/identity/\" + ep[2] + \"/\" + name, { signal: AbortSignal.timeout(15000) });\n const body = await res.json().catch(() => ({}));\n if (!res.ok || !body.token) throw new Error((body.error && body.error.message) || (\"proxy answered \" + res.status));\n const file = dir + \"/\" + name.toLowerCase() + \".jwt\";\n fs.writeFileSync(file + \".tmp\", body.token, { mode: 0o600 });\n fs.renameSync(file + \".tmp\", file);\n wait = Math.max(30000, body.expiresAt - Date.now() - 300000 + Math.floor(Math.random() * 60000) - 30000);\n } catch (err) {\n emit(\"error\", \"identity \" + name + \": \" + (err && err.message ? err.message : String(err)) + \" \u2014 trying again in 30s\");\n }\n setTimeout(() => { void keepFresh(name); }, wait).unref();\n };\n await Promise.all(identityNames.map(keepFresh));\n }\n\n mark(\"tools\");\n const outcome = await executeStep({\n agentDir,\n workspaceRoot: \"/workspace\",\n libraryRoot: \"/library\",\n configWrites: input.configWrites === true,\n prompt: input.prompt,\n model: input.model,\n effort: input.effort ?? null,\n systemPrompt: input.systemPrompt,\n allowed: input.allowed,\n mcpNames: input.mcpNames,\n mcpServers: {\n ...(api.server ? { foldrun_apis: api.server } : {}),\n ...(script.server ? { foldrun_scripts: script.server } : {}),\n ...(consult.server ? { foldrun_agents: consult.server } : {}),\n ...(search.server ? { foldrun_search: search.server } : {}),\n ...(history.server ? { foldrun_history: history.server } : {}),\n ...(desks.server ? { foldrun_desks: desks.server } : {}),\n ...(ask.server ? { foldrun_ask: ask.server } : {}),\n ...(peers.server ? { foldrun_peers: peers.server } : {}),\n ...(delegate.server ? { foldrun_delegate: delegate.server } : {}),\n ...(install.server ? { foldrun_install: install.server } : {}),\n ...input.mcpServers,\n },\n ...(inbox ? { inbox } : {}),\n ...(input.subagents?.length ? { subagents: input.subagents } : {}),\n env,\n timeoutSec: input.timeoutSec,\n budgetUsd: input.budgetUsd,\n budgetNote: input.budgetNote,\n price: input.price,\n verify: input.verify,\n verifyEnv: toolEnv,\n ...(typeof input.judgeModel === \"string\" ? { judgeModel: input.judgeModel } : {}),\n ...(typeof input.judgeEffort === \"string\" ? { judgeEffort: input.judgeEffort } : {}),\n output: input.output,\n schema: input.schema,\n maxTurns: input.maxTurns,\n ...(input.capture === \"metadata\" || input.capture === \"full\" ? { capture: input.capture } : {}),\n ...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),\n ...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),\n // Slim browsing: the platform put this step on the slim image to browse\n // through the account's pod, and named the file the web tool logs its\n // reconnects to. A lost pod stops the step; what it wrote decides what\n // the runner does next (browser-pod.ts).\n ...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env), ...(env.FOLDRUN_BROWSER_INIT ? { browseInit: env.FOLDRUN_BROWSER_INIT } : {}) } } : {}),\n // The container is the boundary; the SDK's own bash sandbox here would\n // only block declared network use (SSH, curl) for no added safety.\n sandboxBash: false,\n emit,\n });\n for (const line of api.drainLog()) emit(\"info\", \"api: \" + line);\n for (const line of script.drainLog()) emit(\"info\", \"script: \" + line);\n if (translator) {\n for (const line of translator.drainLog()) emit(\"info\", line.startsWith(\"translator\") ? line : \"translator: \" + line);\n await translator.close();\n }\n // A consult's spend belongs to the step that asked.\n const consultCost = consult.drainCost();\n if (consultCost > 0) outcome.costUsd = (outcome.costUsd ?? 0) + consultCost;\n if (consultCost > 0 && typeof outcome.turnsCostUsd === \"number\") outcome.turnsCostUsd += consultCost;\n process.stdout.write(JSON.stringify({ e: \"done\", ...outcome, res: readActuals() }) + \"\\n\");\n process.exit(0);\n} catch (err) {\n emit(\"error\", err instanceof Error ? err.message : String(err));\n // The failure path reports actuals too \u2014 a step that died eleven minutes\n // in still held its slice for eleven minutes, and the failed runs are\n // exactly the ones whose resource story gets read.\n let res = null;\n try { res = readActuals(); } catch { /* the reading must never mask the error */ }\n process.stdout.write(JSON.stringify({ e: \"done\", status: \"failed\", result: null, costUsd: null, res }) + \"\\n\");\n process.exit(0);\n}\n";
351
+ export declare const DRIVER = "// The runner container's entrypoint. Reads one step, executes it with the\n// same core the server uses, speaks JSONL on stdout, exits.\nimport fs from \"node:fs\";\nconst input = JSON.parse(fs.readFileSync(\"/opt/runner/job/input.json\", \"utf8\"));\n// Start-up marks (run-container.ts#STARTUP_PHASES): which phase just ended,\n// on the sandbox's clock. A worker that does not know a mark skips the line.\nconst mark = (name) => process.stdout.write(JSON.stringify({ e: \"mark\", name, at: Date.now() }) + \"\\n\");\nmark(\"driver\");\nlet modelSpoke = false;\nconst emit = (type, text, extra) => {\n // The model's first text or tool call is where start-up ends and the\n // step's own work begins \u2014 runtime and tool lines before it are set-up.\n if (!modelSpoke && (type === \"text\" || type === \"tool\")) { modelSpoke = true; mark(\"model\"); }\n process.stdout.write(JSON.stringify({ e: \"event\", type, text, ...extra }) + \"\\n\");\n};\n// What this sandbox actually touched, read at the last moment from the\n// kernel's own books: cgroup v2 for CPU busy-time and peak memory,\n// /proc/net/dev for bytes on the wire. Every read is best-effort \u2014 gVisor\n// exposes some of these files and not others depending on version, and a\n// metric we cannot read is null, never guessed. Reservation is what is\n// billed; these exist so the reservation can be right-sized.\nfunction readActuals() {\n // fs from the module's own import \u2014 the driver is ESM, and a require()\n // here threw on every call, silently nulling every metric it guarded.\n const read = (p) => { try { return fs.readFileSync(p, \"utf8\"); } catch { return null; } };\n // Two cgroup dialects: v2 (cpu.stat, microseconds; memory.peak) on a\n // plain runtime, v1-style controllers under gVisor (cpuacct.usage,\n // NANOseconds; memory.max_usage_in_bytes) \u2014 probed on the real cluster,\n // not assumed. Whichever answers wins; both silent means null.\n const cpuStat = read(\"/sys/fs/cgroup/cpu.stat\");\n const usage = cpuStat?.match(/^usage_usec (\\d+)/m);\n const cpuacctNs = usage ? NaN : Number(read(\"/sys/fs/cgroup/cpuacct/cpuacct.usage\")?.trim());\n // Time the CPU cap held the sandbox back: v2's throttled_usec in the same\n // cpu.stat, v1's throttled_time (nanoseconds) under gVisor. Large against\n // busy time means the step wanted more cores than its class gives.\n const throttledUs = cpuStat?.match(/^throttled_usec (\\d+)/m);\n const throttledNs = throttledUs ? null : read(\"/sys/fs/cgroup/cpu/cpu.stat\")?.match(/^throttled_time (\\d+)/m);\n const peak =\n read(\"/sys/fs/cgroup/memory.peak\") ??\n read(\"/sys/fs/cgroup/memory/memory.max_usage_in_bytes\") ??\n read(\"/sys/fs/cgroup/memory/memory.usage_in_bytes\");\n let rx = 0, tx = 0, sawNet = false;\n const net = read(\"/proc/net/dev\");\n if (net) {\n for (const line of net.split(\"\\n\").slice(2)) {\n const m = line.trim().match(/^(\\S+):\\s+(\\d+)(?:\\s+\\d+){7}\\s+(\\d+)/);\n if (m && m[1] !== \"lo\") { rx += Number(m[2]); tx += Number(m[3]); sawNet = true; }\n }\n }\n return {\n busyCpuSecs: usage\n ? Number(usage[1]) / 1e6\n : Number.isFinite(cpuacctNs) && cpuacctNs > 0\n ? cpuacctNs / 1e9\n : null,\n peakMemBytes: peak ? Number(peak.trim()) || null : null,\n throttledSecs: throttledUs ? Number(throttledUs[1]) / 1e6 : throttledNs ? Number(throttledNs[1]) / 1e9 : null,\n rxBytes: sawNet ? rx : null,\n txBytes: sawNet ? tx : null,\n };\n}\n\ntry {\n // Runtime state (venvs, npm prefixes) lands in the agent user's real home\n // \u2014 writable, inside the container, gone with it.\n process.env.FOLDRUN_DATA = \"/home/agent/.foldrun\";\n const { executeStep } = await import(\"@foldrun/core/step-exec\");\n const { buildApiTools } = await import(\"@foldrun/core/api-tools\");\n const { buildA2ATools } = await import(\"@foldrun/core/a2a-tools\");\n const { buildScriptTools } = await import(\"@foldrun/core/script-tools\");\n const { buildConsultTools } = await import(\"@foldrun/core/agent-tools\");\n const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import(\"@foldrun/core/context-tools\");\n const { httpChannel, buildAskTool, buildPeerTools, inboxReader } = await import(\"@foldrun/core/operator\");\n const { buildDelegateTools } = await import(\"@foldrun/core/delegate-tools\");\n const { startTranslator } = await import(\"@foldrun/core/translator\");\n const { prepareRuntime } = await import(\"@foldrun/core/runtime\");\n const { materializeFileSecrets } = await import(\"@foldrun/core/secret-files\");\n const { paidWebActions } = await import(\"@foldrun/core/browser-pod\");\n mark(\"imports\");\n\n const agentDir = \"/workspace/\" + input.agentRel;\n // HOME is where a bare relative path lands when anything resolves one\n // outside cwd \u2014 and root's /root is outside the workspace, so such a read\n // was refused by confinement with a path the agent never typed. Its own\n // directory is both the truthful answer and the safe one.\n // @file secrets crossed as content; materialise them to 0600 paths inside\n // the container (a host path from the other side would not exist here).\n const env = materializeFileSecrets(agentDir, { ...process.env, HOME: agentDir }).env;\n // Caches go to the sandbox user's own home, not to HOME: the Claude CLI's\n // logs, fontconfig, Mesa and gstreamer all follow XDG_CACHE_HOME, and under\n // agents/<name>/.cache they were copied back after every step \u2014 3,855 files\n // across 179 agent folders on the box by 17 Oct 2026, read by nothing.\n env.XDG_CACHE_HOME = \"/home/agent/.cache\";\n\n // The declared runtime, built in here \u2014 pip and npm have the network, which\n // the host side may not. It lands in .foldrun/runner/.runtimes, which the\n // executors mount from a per-tenant directory (run-container.ts#RUNTIME_CACHE):\n // the first step for a given fingerprint installs, every later one finds\n // .ready and skips. With no mount it still works, just fresh every time \u2014\n // which is what this used to be unconditionally. The log says which happened.\n const runtime = input.runtime\n ? prepareRuntime(\"runner\", input.runtime)\n : { interpreters: {}, env: {}, log: [], error: null };\n for (const line of runtime.log) emit(\"info\", \"runtime: \" + line);\n if (runtime.error) emit(\"error\", \"runtime: \" + runtime.error);\n // The step's own environment gets it too, not only the script tools: an\n // agent that runs \"python3 x.py\" or a shebang script through Bash should\n // meet the packages its runtime declared, not the image's bare python.\n Object.assign(env, runtime.env);\n\n // install: what the agent finds it needs mid-step. apt goes through the\n // root start-up's helper (install-helper.ts); pip and npm land in a\n // directory of this container's own, on the paths from the first turn.\n const { buildInstallTool, INSTALL_TOOL } = await import(\"@foldrun/core/install-tool\");\n const { stepInstallEnv, INSTALL_SOCKET } = await import(\"@foldrun/core/runtime\");\n const installDir = \"/home/agent/.foldrun/step\";\n const installing = (input.allowed ?? []).includes(INSTALL_TOOL);\n if (installing) Object.assign(env, stepInstallEnv(installDir, env));\n const install = installing\n ? buildInstallTool({\n dir: installDir,\n socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,\n cacheRoot: \"/home/agent/.foldrun/runner/.runtimes\",\n python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + \"/bin/python\" : \"python3\",\n env,\n emit,\n })\n : { server: null };\n\n // API and script tools, rebuilt in here from their specs. Secrets were\n // substituted into API headers before the input crossed the boundary, so\n // buildApiTools gets an already-resolved environment. verify: gets the\n // container's own environment, whole: it is the boundary, assembled on\n // the host from an allowlist, and the declared secrets are in it.\n // A long result is kept in a file only when the agent can read it back.\n // (tool-output.ts canReadBack, inlined: the driver imports core by subpath.)\n const keepIn = { dir: agentDir, readable: () => (input.allowed ?? []).some((t) => t === \"Read\" || t === \"Grep\") };\n // A declared secret named like a model variable crosses as\n // FOLDRUN_DECLARED_<NAME> (runner.ts, FOL-19): the model keeps its own\n // settings, and the agent's API and script tools and verify: get the value\n // back under the name it declared. Built here, after the runtime and any\n // mid-step install have added their paths to env.\n const toolEnv = { ...env };\n for (const [k, v] of Object.entries(env)) if (k.startsWith(\"FOLDRUN_DECLARED_\")) toolEnv[k.slice(\"FOLDRUN_DECLARED_\".length)] = v;\n const api = buildApiTools(\"\", input.apis, undefined, { env: toolEnv, missing: [] }, keepIn);\n // remotes: \u2014 the four a2a tools, rebuilt from values. Each token crossed in\n // its spec (a placeholder the proxy fills for the card's origin alone), so\n // nothing here asks a vault; handles go under the workspace's state/.\n const a2a = input.a2a && Array.isArray(input.a2a.remotes) && input.a2a.remotes.length\n ? buildA2ATools(\"\", input.a2a.remotes, undefined, { env: {}, missing: [] }, {\n stateDir: \"/workspace/state/a2a/\" + input.agentRel.split(\"/\").pop(),\n runId: input.a2a.runId,\n step: input.a2a.step,\n egress: env.FOLDRUN_EGRESS || null,\n test: env.FOLDRUN_TEST_MODE === \"1\",\n keepIn,\n emit,\n })\n : { server: null, drainLog: () => [] };\n const script = buildScriptTools(\n agentDir,\n input.scripts,\n toolEnv,\n \"/library/scripts\",\n runtime.interpreters,\n null,\n keepIn.readable,\n );\n // The translator, when the provider speaks Chat Completions: a loopback\n // server for the length of this step. Its env replaces the endpoint the\n // host assembled; the provider's real URL and key stay inside it.\n const translator = input.translator ? await startTranslator(input.translator) : null;\n if (translator) {\n Object.assign(env, translator.env);\n emit(\"info\", \"translator: listening on \" + translator.baseUrl + \" for \" + (input.translator.label ?? input.translator.upstreamBase));\n }\n const consult = buildConsultTools(input.consults ?? [], env, emit);\n // The platform's own two groups, rebuilt from values like everything else.\n const search = buildSearchTools(input.search ?? []);\n const history = input.history?.length ? buildHistoryTools(input.history) : { server: null };\n const desks = input.desks?.length ? buildDeskTools(input.desks) : { server: null };\n // A person in the loop, over the proxy this step's lease already names:\n // ask_person when tools: [ask], and the run's inbox after every tool call.\n const channel = httpChannel(env.FOLDRUN_EGRESS);\n const ask = input.ask ? buildAskTool({ channel, timeoutSec: input.ask.timeoutSec, emit }) : { server: null };\n const peers = input.peers ? buildPeerTools({ channel, self: input.peers.self, siblings: input.peers.siblings, emit }) : { server: null };\n const inbox = channel ? inboxReader(channel, emit) : undefined;\n // sandbox: own sub-agents: a tool each, whose job runs in its own sandbox\n // on the worker, reached over the same proxy door.\n const delegate = input.delegates?.length ? buildDelegateTools({ specs: input.delegates, channel, emit, timeoutSec: input.timeoutSec, workspaceRoot: \"/workspace\" }) : { server: null };\n\n // Keyless cloud identities (FOLDRUN_IDENTITIES): each token file kept\n // fresh through the proxy's /identity door, which mints only what this\n // step's lease was granted. Fetched before the step starts, renewed five\n // minutes before it expires (with jitter); a failure is said and retried.\n // Plain JS: this text is the driver, not TypeScript.\n const identityNames = (env.FOLDRUN_IDENTITIES || \"\").split(\",\").filter(Boolean);\n if (identityNames.length) {\n // No backslashes: this sits inside a template literal, which eats them.\n const ep = new RegExp(\"^(https?://[^/]+)/e/([A-Za-z0-9_-]{16,})/?$\").exec(env.FOLDRUN_EGRESS || \"\");\n const dir = env.FOLDRUN_IDENTITY_DIR || \"/tmp/foldrun-identity\";\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const keepFresh = async (name) => {\n let wait = 30000;\n try {\n if (!ep) throw new Error(\"no egress proxy to ask\");\n const res = await fetch(ep[1] + \"/identity/\" + ep[2] + \"/\" + name, { signal: AbortSignal.timeout(15000) });\n const body = await res.json().catch(() => ({}));\n if (!res.ok || !body.token) throw new Error((body.error && body.error.message) || (\"proxy answered \" + res.status));\n const file = dir + \"/\" + name.toLowerCase() + \".jwt\";\n fs.writeFileSync(file + \".tmp\", body.token, { mode: 0o600 });\n fs.renameSync(file + \".tmp\", file);\n wait = Math.max(30000, body.expiresAt - Date.now() - 300000 + Math.floor(Math.random() * 60000) - 30000);\n } catch (err) {\n emit(\"error\", \"identity \" + name + \": \" + (err && err.message ? err.message : String(err)) + \" \u2014 trying again in 30s\");\n }\n setTimeout(() => { void keepFresh(name); }, wait).unref();\n };\n await Promise.all(identityNames.map(keepFresh));\n }\n\n mark(\"tools\");\n const outcome = await executeStep({\n agentDir,\n workspaceRoot: \"/workspace\",\n libraryRoot: \"/library\",\n configWrites: input.configWrites === true,\n prompt: input.prompt,\n model: input.model,\n effort: input.effort ?? null,\n systemPrompt: input.systemPrompt,\n allowed: input.allowed,\n mcpNames: input.mcpNames,\n mcpServers: {\n ...(api.server ? { foldrun_apis: api.server } : {}),\n ...(a2a.server ? { foldrun_a2a: a2a.server } : {}),\n ...(script.server ? { foldrun_scripts: script.server } : {}),\n ...(consult.server ? { foldrun_agents: consult.server } : {}),\n ...(search.server ? { foldrun_search: search.server } : {}),\n ...(history.server ? { foldrun_history: history.server } : {}),\n ...(desks.server ? { foldrun_desks: desks.server } : {}),\n ...(ask.server ? { foldrun_ask: ask.server } : {}),\n ...(peers.server ? { foldrun_peers: peers.server } : {}),\n ...(delegate.server ? { foldrun_delegate: delegate.server } : {}),\n ...(install.server ? { foldrun_install: install.server } : {}),\n ...input.mcpServers,\n },\n ...(inbox ? { inbox } : {}),\n ...(input.subagents?.length ? { subagents: input.subagents } : {}),\n env,\n timeoutSec: input.timeoutSec,\n budgetUsd: input.budgetUsd,\n budgetNote: input.budgetNote,\n price: input.price,\n verify: input.verify,\n verifyEnv: toolEnv,\n ...(typeof input.judgeModel === \"string\" ? { judgeModel: input.judgeModel } : {}),\n ...(typeof input.judgeEffort === \"string\" ? { judgeEffort: input.judgeEffort } : {}),\n output: input.output,\n schema: input.schema,\n maxTurns: input.maxTurns,\n ...(input.capture === \"metadata\" || input.capture === \"full\" ? { capture: input.capture } : {}),\n ...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),\n ...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),\n // Slim browsing: the platform put this step on the slim image to browse\n // through the account's pod, and named the file the web tool logs its\n // reconnects to. A lost pod stops the step; what it wrote decides what\n // the runner does next (browser-pod.ts).\n ...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env), ...(env.FOLDRUN_BROWSER_INIT ? { browseInit: env.FOLDRUN_BROWSER_INIT } : {}) } } : {}),\n // The container is the boundary; the SDK's own bash sandbox here would\n // only block declared network use (SSH, curl) for no added safety.\n sandboxBash: false,\n emit,\n });\n for (const line of api.drainLog()) emit(\"info\", \"api: \" + line);\n for (const line of a2a.drainLog()) emit(\"info\", \"a2a: \" + line);\n for (const line of script.drainLog()) emit(\"info\", \"script: \" + line);\n if (translator) {\n for (const line of translator.drainLog()) emit(\"info\", line.startsWith(\"translator\") ? line : \"translator: \" + line);\n await translator.close();\n }\n // A consult's spend belongs to the step that asked.\n const consultCost = consult.drainCost();\n if (consultCost > 0) outcome.costUsd = (outcome.costUsd ?? 0) + consultCost;\n if (consultCost > 0 && typeof outcome.turnsCostUsd === \"number\") outcome.turnsCostUsd += consultCost;\n process.stdout.write(JSON.stringify({ e: \"done\", ...outcome, res: readActuals() }) + \"\\n\");\n process.exit(0);\n} catch (err) {\n emit(\"error\", err instanceof Error ? err.message : String(err));\n // The failure path reports actuals too \u2014 a step that died eleven minutes\n // in still held its slice for eleven minutes, and the failed runs are\n // exactly the ones whose resource story gets read.\n let res = null;\n try { res = readActuals(); } catch { /* the reading must never mask the error */ }\n process.stdout.write(JSON.stringify({ e: \"done\", status: \"failed\", result: null, costUsd: null, res }) + \"\\n\");\n process.exit(0);\n}\n";
343
352
  /** The uv release copied into the image. Pinned: a floating uv is a floating
344
353
  * resolver, and the runtime cache is keyed on the declaration, not on it. */
345
354
  export declare const UV_VERSION = "0.11.25";
@@ -569,6 +569,7 @@ try {
569
569
  process.env.FOLDRUN_DATA = "/home/agent/.foldrun";
570
570
  const { executeStep } = await import("@foldrun/core/step-exec");
571
571
  const { buildApiTools } = await import("@foldrun/core/api-tools");
572
+ const { buildA2ATools } = await import("@foldrun/core/a2a-tools");
572
573
  const { buildScriptTools } = await import("@foldrun/core/script-tools");
573
574
  const { buildConsultTools } = await import("@foldrun/core/agent-tools");
574
575
  const { buildSearchTools, buildHistoryTools, buildDeskTools } = await import("@foldrun/core/context-tools");
@@ -645,6 +646,20 @@ try {
645
646
  const toolEnv = { ...env };
646
647
  for (const [k, v] of Object.entries(env)) if (k.startsWith("FOLDRUN_DECLARED_")) toolEnv[k.slice("FOLDRUN_DECLARED_".length)] = v;
647
648
  const api = buildApiTools("", input.apis, undefined, { env: toolEnv, missing: [] }, keepIn);
649
+ // remotes: — the four a2a tools, rebuilt from values. Each token crossed in
650
+ // its spec (a placeholder the proxy fills for the card's origin alone), so
651
+ // nothing here asks a vault; handles go under the workspace's state/.
652
+ const a2a = input.a2a && Array.isArray(input.a2a.remotes) && input.a2a.remotes.length
653
+ ? buildA2ATools("", input.a2a.remotes, undefined, { env: {}, missing: [] }, {
654
+ stateDir: "/workspace/state/a2a/" + input.agentRel.split("/").pop(),
655
+ runId: input.a2a.runId,
656
+ step: input.a2a.step,
657
+ egress: env.FOLDRUN_EGRESS || null,
658
+ test: env.FOLDRUN_TEST_MODE === "1",
659
+ keepIn,
660
+ emit,
661
+ })
662
+ : { server: null, drainLog: () => [] };
648
663
  const script = buildScriptTools(
649
664
  agentDir,
650
665
  input.scripts,
@@ -721,6 +736,7 @@ try {
721
736
  mcpNames: input.mcpNames,
722
737
  mcpServers: {
723
738
  ...(api.server ? { foldrun_apis: api.server } : {}),
739
+ ...(a2a.server ? { foldrun_a2a: a2a.server } : {}),
724
740
  ...(script.server ? { foldrun_scripts: script.server } : {}),
725
741
  ...(consult.server ? { foldrun_agents: consult.server } : {}),
726
742
  ...(search.server ? { foldrun_search: search.server } : {}),
@@ -760,6 +776,7 @@ try {
760
776
  emit,
761
777
  });
762
778
  for (const line of api.drainLog()) emit("info", "api: " + line);
779
+ for (const line of a2a.drainLog()) emit("info", "a2a: " + line);
763
780
  for (const line of script.drainLog()) emit("info", "script: " + line);
764
781
  if (translator) {
765
782
  for (const line of translator.drainLog()) emit("info", line.startsWith("translator") ? line : "translator: " + line);
@@ -1,6 +1,7 @@
1
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
+ import { type RemoteSpec } from "./a2a-remotes.ts";
4
5
  import { type SearchRoot, type RunDigest } from "./context-tools.ts";
5
6
  import { type ScriptSpec } from "./script-tools.ts";
6
7
  import { type RuntimeSpec } from "./runtime.ts";
@@ -331,6 +332,9 @@ grantsOnly?: boolean): {
331
332
  apiTools: import("./api-tools.ts").ApiToolResult;
332
333
  scriptTools: import("./script-tools.ts").ScriptToolResult;
333
334
  apiSpecs: import("./store.ts").ApiSpec[];
335
+ remotes: RemoteSpec[];
336
+ remoteOnlySecrets: string[];
337
+ remoteSecretEnv: Record<string, string>;
334
338
  scriptSpecs: ScriptSpec[];
335
339
  outward: boolean;
336
340
  searchRoots: SearchRoot[];
@@ -579,6 +583,12 @@ export declare function platformModelEnv(): Record<string, string | undefined>;
579
583
  export declare function readSchemaFile(workspaceRoot: string, rel: string): Record<string, unknown> | boolean | null;
580
584
  export declare function createFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, status: "queued" | "running", tags?: string[], startedBy?: string | null, opts?: {
581
585
  test?: boolean;
586
+ },
587
+ /** An A2A task's correlation (foldrun-platform flow-admission.ts) — its
588
+ * own trailing argument, so the signature only widened. */
589
+ a2a?: {
590
+ task: string;
591
+ key: string;
582
592
  }): RunRecord;
583
593
  export declare function startFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, modelOverride?: string | null, tags?: string[], effortOverride?: string | null, startedBy?: string | null, opts?: {
584
594
  test?: boolean;