@foldrun/core 0.6.0 → 1.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.
Files changed (98) hide show
  1. package/README.md +14 -1
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/src/agent-import.d.ts +21 -0
  5. package/dist/src/agent-import.js +116 -0
  6. package/dist/src/api-tools.js +14 -2
  7. package/dist/src/approvals.d.ts +8 -1
  8. package/dist/src/approvals.js +10 -5
  9. package/dist/src/completions.js +1 -0
  10. package/dist/src/confine.d.ts +1 -0
  11. package/dist/src/confine.js +33 -4
  12. package/dist/src/deploy.d.ts +9 -0
  13. package/dist/src/deploy.js +31 -3
  14. package/dist/src/evals.js +22 -5
  15. package/dist/src/hooks.d.ts +114 -0
  16. package/dist/src/hooks.js +370 -0
  17. package/dist/src/host-env.d.ts +14 -0
  18. package/dist/src/host-env.js +26 -0
  19. package/dist/src/install-helper.d.ts +17 -0
  20. package/dist/src/install-helper.js +134 -0
  21. package/dist/src/install-tool.d.ts +128 -0
  22. package/dist/src/install-tool.js +133 -0
  23. package/dist/src/kinds.d.ts +1 -1
  24. package/dist/src/kinds.js +26 -0
  25. package/dist/src/library.d.ts +1 -1
  26. package/dist/src/library.js +17 -1
  27. package/dist/src/mail.js +1 -1
  28. package/dist/src/model-credential.d.ts +16 -0
  29. package/dist/src/model-credential.js +40 -0
  30. package/dist/src/model-env.d.ts +17 -0
  31. package/dist/src/model-env.js +57 -0
  32. package/dist/src/notify.d.ts +8 -0
  33. package/dist/src/notify.js +10 -1
  34. package/dist/src/package.d.ts +67 -0
  35. package/dist/src/package.js +594 -0
  36. package/dist/src/platform.d.ts +5 -0
  37. package/dist/src/platform.js +1 -0
  38. package/dist/src/provider-health.js +3 -0
  39. package/dist/src/providers.d.ts +5 -0
  40. package/dist/src/providers.js +21 -1
  41. package/dist/src/run-container.d.ts +10 -0
  42. package/dist/src/run-container.js +72 -14
  43. package/dist/src/runner.d.ts +205 -0
  44. package/dist/src/runner.js +273 -36
  45. package/dist/src/runtime.d.ts +77 -0
  46. package/dist/src/runtime.js +230 -5
  47. package/dist/src/script-tools.d.ts +2 -0
  48. package/dist/src/script-tools.js +3 -1
  49. package/dist/src/secrets.d.ts +5 -0
  50. package/dist/src/secrets.js +37 -6
  51. package/dist/src/starter.d.ts +12 -0
  52. package/dist/src/starter.js +38 -4
  53. package/dist/src/step-exec.d.ts +21 -2
  54. package/dist/src/step-exec.js +165 -7
  55. package/dist/src/storage.d.ts +7 -1
  56. package/dist/src/storage.js +14 -1
  57. package/dist/src/store.d.ts +31 -1
  58. package/dist/src/store.js +90 -8
  59. package/dist/src/tool-names.d.ts +3 -1
  60. package/dist/src/tool-names.js +4 -2
  61. package/dist/src/tool-test.js +8 -1
  62. package/dist/src/zip.d.ts +17 -0
  63. package/dist/src/zip.js +184 -0
  64. package/index.ts +3 -0
  65. package/package.json +18 -1
  66. package/src/agent-import.ts +154 -0
  67. package/src/api-tools.ts +16 -1
  68. package/src/approvals.ts +10 -3
  69. package/src/completions.ts +1 -0
  70. package/src/confine.ts +36 -5
  71. package/src/deploy.ts +28 -3
  72. package/src/evals.ts +21 -3
  73. package/src/hooks.ts +406 -0
  74. package/src/host-env.ts +27 -0
  75. package/src/install-helper.ts +143 -0
  76. package/src/install-tool.ts +154 -0
  77. package/src/kinds.ts +28 -0
  78. package/src/library.ts +18 -2
  79. package/src/mail.ts +1 -1
  80. package/src/model-credential.ts +41 -0
  81. package/src/model-env.ts +66 -0
  82. package/src/notify.ts +11 -1
  83. package/src/package.ts +664 -0
  84. package/src/platform.ts +6 -0
  85. package/src/provider-health.ts +3 -0
  86. package/src/providers.ts +27 -1
  87. package/src/run-container.ts +75 -14
  88. package/src/runner.ts +279 -36
  89. package/src/runtime.ts +235 -5
  90. package/src/script-tools.ts +5 -1
  91. package/src/secrets.ts +34 -6
  92. package/src/starter.ts +44 -4
  93. package/src/step-exec.ts +171 -11
  94. package/src/storage.ts +13 -0
  95. package/src/store.ts +89 -7
  96. package/src/tool-names.ts +4 -2
  97. package/src/tool-test.ts +8 -1
  98. package/src/zip.ts +189 -0
@@ -352,7 +352,7 @@ export function browseEngineRefusal(engine, what) {
352
352
  }
353
353
  const BROWSE_SETTING_KEYS = [
354
354
  "engine", "user_agent", "device", "locale", "timezone", "cookies", "cookie_domain", "storage", "storage_origin", "identities", "headless", "version", "live",
355
- "allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key",
355
+ "allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key", "state",
356
356
  "video", "live_view", "vendor_session",
357
357
  // Deprecated spelling of vendor_session (read, and warned about).
358
358
  "session",
@@ -696,6 +696,12 @@ export function readBrowseSettings(raw) {
696
696
  settings[k] = list;
697
697
  continue;
698
698
  }
699
+ if (k === "state") {
700
+ if (typeof v !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(v))
701
+ return { settings, rest: left(rest), error: "web.browse.state is a plain name (letters, digits, dot, dash, underscore), like youtube — the login is kept in workspace/state/browser/<name>.json." };
702
+ settings.state = v;
703
+ continue;
704
+ }
699
705
  if (k === "state_key") {
700
706
  if (typeof v !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(v))
701
707
  return { settings, rest: left(rest), error: "web.browse.state_key must be the NAME of a vault secret (CAPITALS), never the key — store it with `foldrun secrets set NAME` and declare it under secrets:." };
@@ -762,6 +768,20 @@ export function readBrowseSettings(raw) {
762
768
  `would be sent to whatever site the call opens. Write \`cookie_domain: .example.com\`.`,
763
769
  };
764
770
  }
771
+ // A secret's cookies seeding a saved login: the login then sits in state/,
772
+ // a workspace file, so it must be encrypted with a key from the vault.
773
+ if (settings.state && settings.cookies && !settings.state_key) {
774
+ return {
775
+ settings,
776
+ rest: left(rest),
777
+ error: `web.browse.state beside cookies needs web.browse.state_key — the secret's session would be saved ` +
778
+ `to state/ where any step can read it. Store a key (\`foldrun secrets set BROWSER_STATE_KEY\`), ` +
779
+ `write \`state_key: BROWSER_STATE_KEY\` and declare it under secrets:.`,
780
+ };
781
+ }
782
+ if (settings.state && settings.storage) {
783
+ return { settings, rest: left(rest), error: "web.browse.state cannot be combined with storage: a secret's tokens would be written to state/." };
784
+ }
765
785
  // Storage is per origin, and seeding it into the wrong one would hand a
766
786
  // site another site's signed-in state — the same mistake `cookie_domain`
767
787
  // exists to prevent, one storage area over.
@@ -1,3 +1,4 @@
1
+ import { type HookRecord } from "./hooks.ts";
1
2
  import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
2
3
  import { type ApiSpec, type Effort } from "./store.ts";
3
4
  import type { EventExtra } from "./step-exec.ts";
@@ -52,6 +53,9 @@ export interface ContainerStepInput {
52
53
  /** `limits:` resolved host-side, and the SDK-name → foldrun-name map the
53
54
  * count reads (limits.ts) — enforced in the pod by the same hook. */
54
55
  limits?: Record<string, number>;
56
+ /** `hooks:` names, resolved inside the sandbox against its own roots. */
57
+ hookNames?: string[];
58
+ hookBase?: Record<string, unknown>;
55
59
  toolOwners?: Record<string, string>;
56
60
  /** tools: [search] — the directories to search, as paths INSIDE the
57
61
  * container (/workspace/…, /library/…). */
@@ -201,6 +205,11 @@ export declare function applyContainerChanges(hostWs: string, containerWs: strin
201
205
  * that fills it.
202
206
  */
203
207
  export declare const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
208
+ /** Starts the install helper as root, in the background, before the drop to
209
+ * the agent user: the root half of the `install` tool (install-helper.ts).
210
+ * The agent reaches it only through the socket; it installs only validated
211
+ * Debian package names, and dies with the container. */
212
+ export declare const INSTALL_HELPER_SH = "FOLDRUN_INSTALL_SOCKET=/opt/runner/job/install.sock; export FOLDRUN_INSTALL_SOCKET\nnode /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js \"$FOLDRUN_INSTALL_SOCKET\" /home/agent/.foldrun/runner/.runtimes >/dev/null 2>&1 &";
204
213
  /**
205
214
  * Where the platform's shared runtime layer is mounted, read-only, in a run
206
215
  * pod — and read-write, one entry at a time, in the pod that builds an entry
@@ -335,6 +344,7 @@ export declare function parseDriverLine(line: string): {
335
344
  operator?: OperatorEvent;
336
345
  subagent?: string;
337
346
  check?: boolean;
347
+ hook?: HookRecord;
338
348
  } | ({
339
349
  e: "done";
340
350
  } & ContainerStepOutcome) | null;
@@ -17,6 +17,7 @@
17
17
  //
18
18
  // The runner image bundles @foldrun/core itself, so the driver runs the same
19
19
  // executeStep the server would — one implementation, two homes.
20
+ import { isHookRecord } from "./hooks.js";
20
21
  import fs from "node:fs";
21
22
  import os from "node:os";
22
23
  import path from "node:path";
@@ -25,7 +26,7 @@ import { spawn, spawnSync } from "node:child_process";
25
26
  import { isPlatformPath } from "./store.js";
26
27
  import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.js";
27
28
  import { isFileValue, fileContent } from "./secrets.js";
28
- import { safeTenantSegment } from "./runtime.js";
29
+ import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET } from "./runtime.js";
29
30
  import { isOperatorEvent } from "./operator.js";
30
31
  import { parseBrowserPod } from "./browser-pod.js";
31
32
  /**
@@ -42,7 +43,9 @@ const cli = () => process.env.FOLDRUN_CONTAINER_CLI ?? "docker";
42
43
  // there: everything platform-owned (store.ts#isPlatformPath — also never
43
44
  // copied in), plus knowledge/, which goes in for reading and is dropped on
44
45
  // the way out, and .git/.
45
- const DENY_BACK = ["knowledge/", ".git/"];
46
+ // hooks/: a step must not rewrite the code that guards it — for this run (the
47
+ // code is read into memory before the model starts, hooks.ts) or the next.
48
+ const DENY_BACK = ["knowledge/", "hooks/", ".git/"];
46
49
  // A .git ANYWHERE in the tree, not just at the root. A step that clones a
47
50
  // repository into a shared area (files/site, say) was shipping the entire
48
51
  // object store back to the workspace, where the harvest then filed thousands
@@ -173,19 +176,39 @@ export function mergeAppends(was, host, next) {
173
176
  const max = Math.min(h.length, n.length);
174
177
  while (common < max && h.charCodeAt(common) === n.charCodeAt(common))
175
178
  common++;
176
- for (let end = common; end >= 0; end--) {
177
- if (end > 0 && h[end - 1] !== "\n")
178
- continue;
179
- const prefix = h.slice(0, end);
180
- const matches = typeof was === "string" ? sha(Buffer.from(prefix, "utf8")) === was : Buffer.from(prefix, "utf8").equals(was);
181
- if (!matches)
182
- continue;
183
- const hostAdded = h.slice(end);
184
- const stepAdded = n.slice(end);
185
- const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
186
- return Buffer.from(prefix + joined + stepAdded, "utf8");
179
+ // One pass over the shared prefix. Rebuilding and hashing the prefix at
180
+ // every line boundary, from the longest down, was quadratic: on 6 Oct 2026
181
+ // a conflicted file of a few MB held the worker's event loop for minutes,
182
+ // the liveness probe killed it every 4.5 minutes, and every step on the box
183
+ // lost its model connection.
184
+ let end = -1;
185
+ if (typeof was === "string") {
186
+ const run = crypto.createHash("sha256");
187
+ if (run.copy().digest("hex") === was)
188
+ end = 0;
189
+ let from = 0;
190
+ for (let nl = h.indexOf("\n"); nl !== -1 && nl < common; nl = h.indexOf("\n", nl + 1)) {
191
+ run.update(h.slice(from, nl + 1), "utf8");
192
+ from = nl + 1;
193
+ if (run.copy().digest("hex") === was)
194
+ end = from;
195
+ }
187
196
  }
188
- return null;
197
+ else {
198
+ // The base's bytes are known, so it can only end at their length.
199
+ const base = was.toString("utf8");
200
+ if (base.length <= common &&
201
+ (base.length === 0 || base.endsWith("\n")) &&
202
+ h.startsWith(base) &&
203
+ Buffer.from(base, "utf8").equals(was))
204
+ end = base.length;
205
+ }
206
+ if (end < 0)
207
+ return null;
208
+ const hostAdded = h.slice(end);
209
+ const stepAdded = n.slice(end);
210
+ const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
211
+ return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
189
212
  }
190
213
  export function applyContainerChanges(hostWs, containerWs, baseline, note, divert) {
191
214
  const applied = [];
@@ -283,6 +306,12 @@ export function applyContainerChanges(hostWs, containerWs, baseline, note, diver
283
306
  * that fills it.
284
307
  */
285
308
  export const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
309
+ /** Starts the install helper as root, in the background, before the drop to
310
+ * the agent user: the root half of the `install` tool (install-helper.ts).
311
+ * The agent reaches it only through the socket; it installs only validated
312
+ * Debian package names, and dies with the container. */
313
+ export const INSTALL_HELPER_SH = `FOLDRUN_INSTALL_SOCKET=${INSTALL_SOCKET}; export FOLDRUN_INSTALL_SOCKET
314
+ node /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js "$FOLDRUN_INSTALL_SOCKET" ${RUNTIME_CACHE} >/dev/null 2>&1 &`;
286
315
  /**
287
316
  * Where the platform's shared runtime layer is mounted, read-only, in a run
288
317
  * pod — and read-write, one entry at a time, in the pod that builds an entry
@@ -420,6 +449,25 @@ try {
420
449
  // meet the packages its runtime declared, not the image's bare python.
421
450
  Object.assign(env, runtime.env);
422
451
 
452
+ // install: what the agent finds it needs mid-step. apt goes through the
453
+ // root start-up's helper (install-helper.ts); pip and npm land in a
454
+ // directory of this container's own, on the paths from the first turn.
455
+ const { buildInstallTool, INSTALL_TOOL } = await import("@foldrun/core/install-tool");
456
+ const { stepInstallEnv, INSTALL_SOCKET } = await import("@foldrun/core/runtime");
457
+ const installDir = "/home/agent/.foldrun/step";
458
+ const installing = (input.allowed ?? []).includes(INSTALL_TOOL);
459
+ if (installing) Object.assign(env, stepInstallEnv(installDir, env));
460
+ const install = installing
461
+ ? buildInstallTool({
462
+ dir: installDir,
463
+ socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,
464
+ cacheRoot: "${RUNTIME_CACHE}",
465
+ python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + "/bin/python" : "python3",
466
+ env,
467
+ emit,
468
+ })
469
+ : { server: null };
470
+
423
471
  // API and script tools, rebuilt in here from their specs. Secrets were
424
472
  // substituted into API headers before the input crossed the boundary, so
425
473
  // buildApiTools gets an already-resolved environment. verify: gets the
@@ -470,6 +518,7 @@ try {
470
518
  ...(history.server ? { foldrun_history: history.server } : {}),
471
519
  ...(desks.server ? { foldrun_desks: desks.server } : {}),
472
520
  ...(ask.server ? { foldrun_ask: ask.server } : {}),
521
+ ...(install.server ? { foldrun_install: install.server } : {}),
473
522
  ...input.mcpServers,
474
523
  },
475
524
  ...(inbox ? { inbox } : {}),
@@ -485,6 +534,7 @@ try {
485
534
  schema: input.schema,
486
535
  maxTurns: input.maxTurns,
487
536
  ...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),
537
+ ...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),
488
538
  // Slim browsing: the platform put this step on the slim image to browse
489
539
  // through the account's pod, and named the file the web tool logs its
490
540
  // reconnects to. A lost pod stops the step; what it wrote decides what
@@ -757,7 +807,14 @@ RUN node -e ' \\
757
807
 
758
808
  FROM browsers AS full
759
809
  ${CORE_INSTALL}`;
810
+ // runtime: system: — apt as root, before the drop (runtime.ts systemInstallSh).
811
+ // set -e is off around it: a failed install is reported by the driver, which
812
+ // reads the status file and fails the step with apt's own words.
760
813
  const ENTRY = `#!/bin/sh
814
+ set +e
815
+ ${SYSTEM_FROM_INPUT_SH}
816
+ ${systemInstallSh(RUNTIME_CACHE)}
817
+ ${INSTALL_HELPER_SH}
761
818
  set -e
762
819
  chown -R agent:agent /workspace /library /opt/runner/job
763
820
  # The runtime cache, when one is mounted. NOT recursive: a warm cache is tens
@@ -930,6 +987,7 @@ export function parseDriverLine(line) {
930
987
  ...(isOperatorEvent(parsed.operator) ? { operator: parsed.operator } : {}),
931
988
  ...(typeof parsed.subagent === "string" && /^[a-z0-9-]{1,64}$/.test(parsed.subagent) ? { subagent: parsed.subagent } : {}),
932
989
  ...(parsed.check === true ? { check: true } : {}),
990
+ ...(isHookRecord(parsed.hook) ? { hook: parsed.hook } : {}),
933
991
  };
934
992
  }
935
993
  if (parsed.e === "done") {
@@ -1,8 +1,10 @@
1
1
  import { 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 SearchRoot, type RunDigest } from "./context-tools.ts";
4
5
  import { type ScriptSpec } from "./script-tools.ts";
5
6
  import { type RuntimeSpec } from "./runtime.ts";
7
+ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
6
8
  import { type LanguageChoice } from "./language.ts";
7
9
  import { type RegionChoice } from "./locale.ts";
8
10
  import { type ClockChoice } from "./clock.ts";
@@ -191,6 +193,164 @@ export declare function applicableSkills<T extends {
191
193
  */
192
194
  export declare function saysUntilMarker(result: string | null, until: string): boolean;
193
195
  export declare function resolveDocLinks(text: string, workspaceRoot: string): string;
196
+ export declare function agentContext(agentDir: string, tenant: string, tags?: string[],
197
+ /** Who is running, so scripts can stamp what they write with its provenance. */
198
+ identity?: {
199
+ runId?: string;
200
+ agent?: string;
201
+ },
202
+ /** The flow's own `timezone:`, when this step belongs to one. Between the
203
+ * agent's frontmatter and the workspace's AGENTS.md in the cascade. */
204
+ flowTimezone?: string | null,
205
+ /** Only what the agent is granted — no runtime prepared. For a sub-agent,
206
+ * whose tools are cut to its parent's and run in the parent's runtime. */
207
+ grantsOnly?: boolean): {
208
+ front: {
209
+ [key: string]: any;
210
+ };
211
+ clockEnv: {
212
+ FOLDRUN_LANGUAGE: string;
213
+ FOLDRUN_REGION?: string | undefined;
214
+ FOLDRUN_CURRENCY?: string | undefined;
215
+ FOLDRUN_UNITS: import("./locale.ts").Units;
216
+ FOLDRUN_CALENDAR: string;
217
+ FOLDRUN_DATE_LOCAL?: string | undefined;
218
+ FOLDRUN_RTL: string;
219
+ TZ: string;
220
+ FOLDRUN_DATE: string;
221
+ FOLDRUN_SEARCH_URL?: string | undefined;
222
+ FOLDRUN_API_URL?: string | undefined;
223
+ FOLDRUN_WEB_SEARCH_VIA?: string | undefined;
224
+ FOLDRUN_WEB_SEARCH_SECRET?: string | undefined;
225
+ FOLDRUN_WEB_FETCH_VIA?: string | undefined;
226
+ FOLDRUN_WEB_FETCH_SECRET?: string | undefined;
227
+ FOLDRUN_WEB_ACTIONS?: string | undefined;
228
+ FOLDRUN_BROWSER_VENDOR?: string | undefined;
229
+ FOLDRUN_BROWSER_SECRET?: string | undefined;
230
+ FOLDRUN_BROWSER_VENDOR_SESSION?: string | undefined;
231
+ FOLDRUN_BROWSER_SESSION?: string | undefined;
232
+ FOLDRUN_BROWSER_ENGINE?: "chrome" | "chromium" | "firefox" | "lightpanda" | "obscura" | "webkit" | undefined;
233
+ FOLDRUN_BROWSER_USER_AGENT?: string | undefined;
234
+ FOLDRUN_BROWSER_DEVICE?: string | undefined;
235
+ FOLDRUN_BROWSER_LOCALE?: string | undefined;
236
+ FOLDRUN_BROWSER_TIMEZONE?: string | undefined;
237
+ FOLDRUN_BROWSER_COOKIES?: string | undefined;
238
+ FOLDRUN_BROWSER_COOKIE_DOMAIN?: string | undefined;
239
+ FOLDRUN_BROWSER_STORAGE?: string | undefined;
240
+ FOLDRUN_BROWSER_STORAGE_ORIGIN?: string | undefined;
241
+ FOLDRUN_BROWSER_IDENTITIES?: string | undefined;
242
+ FOLDRUN_BROWSER_HEADLESS?: string | undefined;
243
+ FOLDRUN_BROWSER_VERSION?: string | undefined;
244
+ FOLDRUN_BROWSER_LIVE?: string | undefined;
245
+ FOLDRUN_BROWSER_ALLOWED_DOMAINS?: string | undefined;
246
+ FOLDRUN_BROWSER_DENY?: string | undefined;
247
+ FOLDRUN_BROWSER_BOUNDARIES?: string | undefined;
248
+ FOLDRUN_BROWSER_INIT?: string | undefined;
249
+ FOLDRUN_BROWSER_EXTENSIONS?: string | undefined;
250
+ FOLDRUN_BROWSER_WEBGPU?: string | undefined;
251
+ FOLDRUN_BROWSER_IGNORE_HTTPS_ERRORS?: string | undefined;
252
+ FOLDRUN_BROWSER_STATE_KEY?: string | undefined;
253
+ FOLDRUN_BROWSER_STATE?: string | undefined;
254
+ FOLDRUN_BROWSER_VIDEO?: string | undefined;
255
+ FOLDRUN_BROWSER_LIVE_VIEW?: string | undefined;
256
+ };
257
+ /** The zone, where it came from, and anything unreadable on the way —
258
+ * reported by the step, not here, so it lands in the run's trace. */
259
+ clock: ClockChoice;
260
+ systemPrompt: string;
261
+ allowed: string[];
262
+ disabled: string[];
263
+ unknownTools: string[];
264
+ shadowed: string[];
265
+ legacyUse: string[];
266
+ retiredTools: string[];
267
+ mcpServers: Record<string, McpServerConfig>;
268
+ mcpNames: string[];
269
+ apiTools: import("./api-tools.ts").ApiToolResult;
270
+ scriptTools: import("./script-tools.ts").ScriptToolResult;
271
+ apiSpecs: import("./store.ts").ApiSpec[];
272
+ scriptSpecs: ScriptSpec[];
273
+ outward: boolean;
274
+ searchRoots: SearchRoot[];
275
+ historyDigest: RunDigest[];
276
+ deskDigest: RunDigest[];
277
+ searchTools: import("./context-tools.ts").ContextToolResult;
278
+ historyTools: import("./context-tools.ts").ContextToolResult;
279
+ deskTools: import("./context-tools.ts").ContextToolResult;
280
+ ask: {
281
+ timeoutSec: number;
282
+ } | null;
283
+ runtime: {
284
+ log: string[];
285
+ error: string | null;
286
+ executor: import("./container.ts").Executor;
287
+ spec: RuntimeSpec | null;
288
+ dispose: (() => void) | undefined;
289
+ };
290
+ secretEnv: {
291
+ [x: string]: string;
292
+ };
293
+ formatWarning: string | null;
294
+ providerEnv: Record<string, string>;
295
+ providerLabel: string | null;
296
+ providerSecrets: Record<string, string>;
297
+ providerWarnings: string[];
298
+ fallbackEnv: Record<string, string> | null;
299
+ region: RegionChoice;
300
+ searchChoice: import("./providers.ts").SearchChoice;
301
+ fetchChoice: import("./providers.ts").SearchChoice;
302
+ actionChoices: ({
303
+ provider: null;
304
+ error?: undefined;
305
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
306
+ } | {
307
+ provider: string;
308
+ secret: string;
309
+ host: string;
310
+ error?: undefined;
311
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
312
+ } | {
313
+ provider: null;
314
+ error: string;
315
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
316
+ })[];
317
+ web: {
318
+ actions: import("./providers.ts").WebAction[] | null;
319
+ raw: Record<import("./providers.ts").WebAction, unknown>;
320
+ problems: string[];
321
+ };
322
+ language: LanguageChoice;
323
+ translator: {
324
+ upstreamBase: string;
325
+ wire?: import("./translator.ts").TranslatorWire;
326
+ upstreamKey: string;
327
+ headers?: Record<string, string>;
328
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
329
+ reasoningEffort?: boolean;
330
+ params?: Record<string, unknown>;
331
+ label?: string;
332
+ egress?: string;
333
+ timezone: string;
334
+ } | null;
335
+ fallbackTranslator: {
336
+ upstreamBase: string;
337
+ wire?: import("./translator.ts").TranslatorWire;
338
+ upstreamKey: string;
339
+ headers?: Record<string, string>;
340
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
341
+ reasoningEffort?: boolean;
342
+ params?: Record<string, unknown>;
343
+ label?: string;
344
+ egress?: string;
345
+ timezone: string;
346
+ } | null;
347
+ apiWarnings: string[];
348
+ secretScopes: Record<string, import("./secrets.ts").SecretScope>;
349
+ missingTools: string[];
350
+ size: "heavy" | "large" | "small";
351
+ brokenTools: string[];
352
+ missingSecrets: string[];
353
+ };
194
354
  export declare function repriced(catalog: Catalog | null, model: string, usage: {
195
355
  inputTokens: number;
196
356
  outputTokens: number;
@@ -215,6 +375,28 @@ attempt: number, status: "completed" | "failed", startedAt: string, eventsBefore
215
375
  /** The attempt re-attached to a sandbox a previous driver left: same
216
376
  * attempt, so its row is replaced rather than repeated. */
217
377
  reattach?: boolean): void;
378
+ /**
379
+ * What the person at the gate said, framed for the step's prompt — or "".
380
+ *
381
+ * The note is the freshest instruction the step has — later than the flow
382
+ * file, aimed at this exact run. When the gate asked a question (ask:), the
383
+ * note IS the answer and is framed as one.
384
+ *
385
+ * Both halves go through resolveDocLinks for the same reason the
386
+ * instruction does: this is authored text on its way to becoming a prompt,
387
+ * and the rule is that ALL of it expands. A person answering "which list?"
388
+ * with [[dead-ends]] was sending the model two literal brackets — the one
389
+ * reference syntax the workspace teaches, and the one place it silently did
390
+ * nothing. The previous step's results are NOT resolved, deliberately: that
391
+ * is model output, and a model that happens to emit [[brackets]] must not
392
+ * thereby name a file.
393
+ *
394
+ * An ask: gate approved with no note still says so. It used to add nothing,
395
+ * so the step never learned its question had been answered: gbp-desk's
396
+ * rivals sender, told "a plain approval means all offices", saw no answer
397
+ * at all and stopped BLOCKED (run-muu6r18y-417e, 2026-10-05).
398
+ */
399
+ export declare function operatorBlock(step: Pick<StepRecord, "ask" | "approvalNote" | "approvedAt">, workspaceRoot: string): string;
218
400
  /**
219
401
  * Did this attempt call its tools and then fail only on the check after —
220
402
  * `verify:`, `output: json`, a schema? For a step whose tools act outward
@@ -245,6 +427,11 @@ export declare function actedThenFailedCheck(events: {
245
427
  * happened to create it.
246
428
  */
247
429
  export declare function copyTreeBytes(from: string, to: string): void;
430
+ /** A refusal of the credential itself, in any of the wordings Claude Code
431
+ * has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
432
+ * step started one second before the 09:07:52Z refresh) matched none of the
433
+ * older two, so the step failed instead of waiting for the new token. */
434
+ export declare function isAuthRefusal(text: string): boolean;
248
435
  /**
249
436
  * Wait, briefly, for the mounted credential to become one we have not
250
437
  * already been refused on. Returns the new value, or null when the window
@@ -252,6 +439,24 @@ export declare function copyTreeBytes(from: string, to: string): void;
252
439
  * wrong", not "this key is stale".
253
440
  */
254
441
  export declare function awaitRotatedCredential(before: string | undefined, now?: () => number, read?: () => string | undefined, waitMs?: number, pollMs?: number): Promise<string | null>;
442
+ /** A step's own model connection cut: the egress proxy or the worker it
443
+ * runs through went away. Only the run's final error, never a tool's — a
444
+ * WebFetch that could not connect is the step's business, not ours. */
445
+ export declare function isModelConnectionLost(text: string): boolean;
446
+ /** A tool call written out as text rather than made: the model's last
447
+ * message holds `<invoke name=…>` / `<function_calls>` markup. Only the tail
448
+ * counts — a reply that quotes such markup mid-way and goes on is fine. */
449
+ export declare function wroteToolCallAsText(text: string): boolean;
450
+ /** Runs again after a tool call written as text. */
451
+ export declare const TEXT_CALL_RETRIES = 1;
452
+ export declare const TEXT_CALL_NOTE: string;
453
+ /** How many times a step whose model connection was cut runs again. */
454
+ export declare const DROP_RETRIES = 2;
455
+ /** May a step whose connection dropped run again? Yes, unless its tools can
456
+ * act outward and one ran in this attempt (actedThenFailedCheck's rule). */
457
+ export declare function dropRetrySafe(canActOutward: boolean, attemptEvents: {
458
+ type: string;
459
+ }[]): boolean;
255
460
  /** How many times a busy provider is given another moment, and the longest
256
461
  * any one wait may be. Three is the industry's habit and the point where
257
462
  * waiting longer stops being kinder than failing. */