@foldrun/core 0.6.0 → 0.7.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 (80) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.js +3 -0
  3. package/dist/src/agent-import.d.ts +21 -0
  4. package/dist/src/agent-import.js +116 -0
  5. package/dist/src/api-tools.js +14 -2
  6. package/dist/src/approvals.d.ts +8 -1
  7. package/dist/src/approvals.js +10 -5
  8. package/dist/src/completions.js +1 -0
  9. package/dist/src/confine.d.ts +1 -0
  10. package/dist/src/confine.js +33 -4
  11. package/dist/src/deploy.d.ts +9 -0
  12. package/dist/src/deploy.js +31 -3
  13. package/dist/src/evals.js +22 -5
  14. package/dist/src/host-env.d.ts +14 -0
  15. package/dist/src/host-env.js +26 -0
  16. package/dist/src/install-helper.d.ts +17 -0
  17. package/dist/src/install-helper.js +134 -0
  18. package/dist/src/install-tool.d.ts +128 -0
  19. package/dist/src/install-tool.js +133 -0
  20. package/dist/src/model-credential.d.ts +16 -0
  21. package/dist/src/model-credential.js +40 -0
  22. package/dist/src/model-env.d.ts +17 -0
  23. package/dist/src/model-env.js +57 -0
  24. package/dist/src/package.d.ts +67 -0
  25. package/dist/src/package.js +594 -0
  26. package/dist/src/platform.d.ts +5 -0
  27. package/dist/src/platform.js +1 -0
  28. package/dist/src/provider-health.js +3 -0
  29. package/dist/src/run-container.d.ts +5 -0
  30. package/dist/src/run-container.js +66 -13
  31. package/dist/src/runner.d.ts +204 -0
  32. package/dist/src/runner.js +257 -36
  33. package/dist/src/runtime.d.ts +77 -0
  34. package/dist/src/runtime.js +230 -5
  35. package/dist/src/script-tools.d.ts +2 -0
  36. package/dist/src/script-tools.js +3 -1
  37. package/dist/src/secrets.d.ts +5 -0
  38. package/dist/src/secrets.js +37 -6
  39. package/dist/src/starter.d.ts +12 -0
  40. package/dist/src/starter.js +36 -4
  41. package/dist/src/step-exec.d.ts +11 -2
  42. package/dist/src/step-exec.js +52 -5
  43. package/dist/src/storage.d.ts +7 -1
  44. package/dist/src/storage.js +14 -1
  45. package/dist/src/store.d.ts +20 -0
  46. package/dist/src/store.js +89 -8
  47. package/dist/src/tool-names.d.ts +3 -1
  48. package/dist/src/tool-names.js +4 -2
  49. package/dist/src/tool-test.js +8 -1
  50. package/dist/src/zip.d.ts +17 -0
  51. package/dist/src/zip.js +184 -0
  52. package/index.ts +3 -0
  53. package/package.json +1 -1
  54. package/src/agent-import.ts +154 -0
  55. package/src/api-tools.ts +16 -1
  56. package/src/approvals.ts +10 -3
  57. package/src/completions.ts +1 -0
  58. package/src/confine.ts +36 -5
  59. package/src/deploy.ts +28 -3
  60. package/src/evals.ts +21 -3
  61. package/src/host-env.ts +27 -0
  62. package/src/install-helper.ts +143 -0
  63. package/src/install-tool.ts +154 -0
  64. package/src/model-credential.ts +41 -0
  65. package/src/model-env.ts +66 -0
  66. package/src/package.ts +664 -0
  67. package/src/platform.ts +6 -0
  68. package/src/provider-health.ts +3 -0
  69. package/src/run-container.ts +65 -12
  70. package/src/runner.ts +264 -36
  71. package/src/runtime.ts +235 -5
  72. package/src/script-tools.ts +5 -1
  73. package/src/secrets.ts +34 -6
  74. package/src/starter.ts +42 -4
  75. package/src/step-exec.ts +56 -8
  76. package/src/storage.ts +13 -0
  77. package/src/store.ts +85 -7
  78. package/src/tool-names.ts +4 -2
  79. package/src/tool-test.ts +8 -1
  80. package/src/zip.ts +189 -0
@@ -25,7 +25,7 @@ import { spawn, spawnSync } from "node:child_process";
25
25
  import { isPlatformPath } from "./store.js";
26
26
  import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.js";
27
27
  import { isFileValue, fileContent } from "./secrets.js";
28
- import { safeTenantSegment } from "./runtime.js";
28
+ import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET } from "./runtime.js";
29
29
  import { isOperatorEvent } from "./operator.js";
30
30
  import { parseBrowserPod } from "./browser-pod.js";
31
31
  /**
@@ -173,19 +173,39 @@ export function mergeAppends(was, host, next) {
173
173
  const max = Math.min(h.length, n.length);
174
174
  while (common < max && h.charCodeAt(common) === n.charCodeAt(common))
175
175
  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");
176
+ // One pass over the shared prefix. Rebuilding and hashing the prefix at
177
+ // every line boundary, from the longest down, was quadratic: on 6 Oct 2026
178
+ // a conflicted file of a few MB held the worker's event loop for minutes,
179
+ // the liveness probe killed it every 4.5 minutes, and every step on the box
180
+ // lost its model connection.
181
+ let end = -1;
182
+ if (typeof was === "string") {
183
+ const run = crypto.createHash("sha256");
184
+ if (run.copy().digest("hex") === was)
185
+ end = 0;
186
+ let from = 0;
187
+ for (let nl = h.indexOf("\n"); nl !== -1 && nl < common; nl = h.indexOf("\n", nl + 1)) {
188
+ run.update(h.slice(from, nl + 1), "utf8");
189
+ from = nl + 1;
190
+ if (run.copy().digest("hex") === was)
191
+ end = from;
192
+ }
187
193
  }
188
- return null;
194
+ else {
195
+ // The base's bytes are known, so it can only end at their length.
196
+ const base = was.toString("utf8");
197
+ if (base.length <= common &&
198
+ (base.length === 0 || base.endsWith("\n")) &&
199
+ h.startsWith(base) &&
200
+ Buffer.from(base, "utf8").equals(was))
201
+ end = base.length;
202
+ }
203
+ if (end < 0)
204
+ return null;
205
+ const hostAdded = h.slice(end);
206
+ const stepAdded = n.slice(end);
207
+ const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
208
+ return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
189
209
  }
190
210
  export function applyContainerChanges(hostWs, containerWs, baseline, note, divert) {
191
211
  const applied = [];
@@ -283,6 +303,12 @@ export function applyContainerChanges(hostWs, containerWs, baseline, note, diver
283
303
  * that fills it.
284
304
  */
285
305
  export const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
306
+ /** Starts the install helper as root, in the background, before the drop to
307
+ * the agent user: the root half of the `install` tool (install-helper.ts).
308
+ * The agent reaches it only through the socket; it installs only validated
309
+ * Debian package names, and dies with the container. */
310
+ export const INSTALL_HELPER_SH = `FOLDRUN_INSTALL_SOCKET=${INSTALL_SOCKET}; export FOLDRUN_INSTALL_SOCKET
311
+ node /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js "$FOLDRUN_INSTALL_SOCKET" ${RUNTIME_CACHE} >/dev/null 2>&1 &`;
286
312
  /**
287
313
  * Where the platform's shared runtime layer is mounted, read-only, in a run
288
314
  * pod — and read-write, one entry at a time, in the pod that builds an entry
@@ -420,6 +446,25 @@ try {
420
446
  // meet the packages its runtime declared, not the image's bare python.
421
447
  Object.assign(env, runtime.env);
422
448
 
449
+ // install: what the agent finds it needs mid-step. apt goes through the
450
+ // root start-up's helper (install-helper.ts); pip and npm land in a
451
+ // directory of this container's own, on the paths from the first turn.
452
+ const { buildInstallTool, INSTALL_TOOL } = await import("@foldrun/core/install-tool");
453
+ const { stepInstallEnv, INSTALL_SOCKET } = await import("@foldrun/core/runtime");
454
+ const installDir = "/home/agent/.foldrun/step";
455
+ const installing = (input.allowed ?? []).includes(INSTALL_TOOL);
456
+ if (installing) Object.assign(env, stepInstallEnv(installDir, env));
457
+ const install = installing
458
+ ? buildInstallTool({
459
+ dir: installDir,
460
+ socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,
461
+ cacheRoot: "${RUNTIME_CACHE}",
462
+ python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + "/bin/python" : "python3",
463
+ env,
464
+ emit,
465
+ })
466
+ : { server: null };
467
+
423
468
  // API and script tools, rebuilt in here from their specs. Secrets were
424
469
  // substituted into API headers before the input crossed the boundary, so
425
470
  // buildApiTools gets an already-resolved environment. verify: gets the
@@ -470,6 +515,7 @@ try {
470
515
  ...(history.server ? { foldrun_history: history.server } : {}),
471
516
  ...(desks.server ? { foldrun_desks: desks.server } : {}),
472
517
  ...(ask.server ? { foldrun_ask: ask.server } : {}),
518
+ ...(install.server ? { foldrun_install: install.server } : {}),
473
519
  ...input.mcpServers,
474
520
  },
475
521
  ...(inbox ? { inbox } : {}),
@@ -757,7 +803,14 @@ RUN node -e ' \\
757
803
 
758
804
  FROM browsers AS full
759
805
  ${CORE_INSTALL}`;
806
+ // runtime: system: — apt as root, before the drop (runtime.ts systemInstallSh).
807
+ // set -e is off around it: a failed install is reported by the driver, which
808
+ // reads the status file and fails the step with apt's own words.
760
809
  const ENTRY = `#!/bin/sh
810
+ set +e
811
+ ${SYSTEM_FROM_INPUT_SH}
812
+ ${systemInstallSh(RUNTIME_CACHE)}
813
+ ${INSTALL_HELPER_SH}
761
814
  set -e
762
815
  chown -R agent:agent /workspace /library /opt/runner/job
763
816
  # The runtime cache, when one is mounted. NOT recursive: a warm cache is tens
@@ -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,163 @@ 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_VIDEO?: string | undefined;
254
+ FOLDRUN_BROWSER_LIVE_VIEW?: string | undefined;
255
+ };
256
+ /** The zone, where it came from, and anything unreadable on the way —
257
+ * reported by the step, not here, so it lands in the run's trace. */
258
+ clock: ClockChoice;
259
+ systemPrompt: string;
260
+ allowed: string[];
261
+ disabled: string[];
262
+ unknownTools: string[];
263
+ shadowed: string[];
264
+ legacyUse: string[];
265
+ retiredTools: string[];
266
+ mcpServers: Record<string, McpServerConfig>;
267
+ mcpNames: string[];
268
+ apiTools: import("./api-tools.ts").ApiToolResult;
269
+ scriptTools: import("./script-tools.ts").ScriptToolResult;
270
+ apiSpecs: import("./store.ts").ApiSpec[];
271
+ scriptSpecs: ScriptSpec[];
272
+ outward: boolean;
273
+ searchRoots: SearchRoot[];
274
+ historyDigest: RunDigest[];
275
+ deskDigest: RunDigest[];
276
+ searchTools: import("./context-tools.ts").ContextToolResult;
277
+ historyTools: import("./context-tools.ts").ContextToolResult;
278
+ deskTools: import("./context-tools.ts").ContextToolResult;
279
+ ask: {
280
+ timeoutSec: number;
281
+ } | null;
282
+ runtime: {
283
+ log: string[];
284
+ error: string | null;
285
+ executor: import("./container.ts").Executor;
286
+ spec: RuntimeSpec | null;
287
+ dispose: (() => void) | undefined;
288
+ };
289
+ secretEnv: {
290
+ [x: string]: string;
291
+ };
292
+ formatWarning: string | null;
293
+ providerEnv: Record<string, string>;
294
+ providerLabel: string | null;
295
+ providerSecrets: Record<string, string>;
296
+ providerWarnings: string[];
297
+ fallbackEnv: Record<string, string> | null;
298
+ region: RegionChoice;
299
+ searchChoice: import("./providers.ts").SearchChoice;
300
+ fetchChoice: import("./providers.ts").SearchChoice;
301
+ actionChoices: ({
302
+ provider: null;
303
+ error?: undefined;
304
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
305
+ } | {
306
+ provider: string;
307
+ secret: string;
308
+ host: string;
309
+ error?: undefined;
310
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
311
+ } | {
312
+ provider: null;
313
+ error: string;
314
+ action: "answer" | "crawl" | "extract" | "map" | "monitor";
315
+ })[];
316
+ web: {
317
+ actions: import("./providers.ts").WebAction[] | null;
318
+ raw: Record<import("./providers.ts").WebAction, unknown>;
319
+ problems: string[];
320
+ };
321
+ language: LanguageChoice;
322
+ translator: {
323
+ upstreamBase: string;
324
+ wire?: import("./translator.ts").TranslatorWire;
325
+ upstreamKey: string;
326
+ headers?: Record<string, string>;
327
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
328
+ reasoningEffort?: boolean;
329
+ params?: Record<string, unknown>;
330
+ label?: string;
331
+ egress?: string;
332
+ timezone: string;
333
+ } | null;
334
+ fallbackTranslator: {
335
+ upstreamBase: string;
336
+ wire?: import("./translator.ts").TranslatorWire;
337
+ upstreamKey: string;
338
+ headers?: Record<string, string>;
339
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
340
+ reasoningEffort?: boolean;
341
+ params?: Record<string, unknown>;
342
+ label?: string;
343
+ egress?: string;
344
+ timezone: string;
345
+ } | null;
346
+ apiWarnings: string[];
347
+ secretScopes: Record<string, import("./secrets.ts").SecretScope>;
348
+ missingTools: string[];
349
+ size: "heavy" | "large" | "small";
350
+ brokenTools: string[];
351
+ missingSecrets: string[];
352
+ };
194
353
  export declare function repriced(catalog: Catalog | null, model: string, usage: {
195
354
  inputTokens: number;
196
355
  outputTokens: number;
@@ -215,6 +374,28 @@ attempt: number, status: "completed" | "failed", startedAt: string, eventsBefore
215
374
  /** The attempt re-attached to a sandbox a previous driver left: same
216
375
  * attempt, so its row is replaced rather than repeated. */
217
376
  reattach?: boolean): void;
377
+ /**
378
+ * What the person at the gate said, framed for the step's prompt — or "".
379
+ *
380
+ * The note is the freshest instruction the step has — later than the flow
381
+ * file, aimed at this exact run. When the gate asked a question (ask:), the
382
+ * note IS the answer and is framed as one.
383
+ *
384
+ * Both halves go through resolveDocLinks for the same reason the
385
+ * instruction does: this is authored text on its way to becoming a prompt,
386
+ * and the rule is that ALL of it expands. A person answering "which list?"
387
+ * with [[dead-ends]] was sending the model two literal brackets — the one
388
+ * reference syntax the workspace teaches, and the one place it silently did
389
+ * nothing. The previous step's results are NOT resolved, deliberately: that
390
+ * is model output, and a model that happens to emit [[brackets]] must not
391
+ * thereby name a file.
392
+ *
393
+ * An ask: gate approved with no note still says so. It used to add nothing,
394
+ * so the step never learned its question had been answered: gbp-desk's
395
+ * rivals sender, told "a plain approval means all offices", saw no answer
396
+ * at all and stopped BLOCKED (run-muu6r18y-417e, 2026-10-05).
397
+ */
398
+ export declare function operatorBlock(step: Pick<StepRecord, "ask" | "approvalNote" | "approvedAt">, workspaceRoot: string): string;
218
399
  /**
219
400
  * Did this attempt call its tools and then fail only on the check after —
220
401
  * `verify:`, `output: json`, a schema? For a step whose tools act outward
@@ -245,6 +426,11 @@ export declare function actedThenFailedCheck(events: {
245
426
  * happened to create it.
246
427
  */
247
428
  export declare function copyTreeBytes(from: string, to: string): void;
429
+ /** A refusal of the credential itself, in any of the wordings Claude Code
430
+ * has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
431
+ * step started one second before the 09:07:52Z refresh) matched none of the
432
+ * older two, so the step failed instead of waiting for the new token. */
433
+ export declare function isAuthRefusal(text: string): boolean;
248
434
  /**
249
435
  * Wait, briefly, for the mounted credential to become one we have not
250
436
  * already been refused on. Returns the new value, or null when the window
@@ -252,6 +438,24 @@ export declare function copyTreeBytes(from: string, to: string): void;
252
438
  * wrong", not "this key is stale".
253
439
  */
254
440
  export declare function awaitRotatedCredential(before: string | undefined, now?: () => number, read?: () => string | undefined, waitMs?: number, pollMs?: number): Promise<string | null>;
441
+ /** A step's own model connection cut: the egress proxy or the worker it
442
+ * runs through went away. Only the run's final error, never a tool's — a
443
+ * WebFetch that could not connect is the step's business, not ours. */
444
+ export declare function isModelConnectionLost(text: string): boolean;
445
+ /** A tool call written out as text rather than made: the model's last
446
+ * message holds `<invoke name=…>` / `<function_calls>` markup. Only the tail
447
+ * counts — a reply that quotes such markup mid-way and goes on is fine. */
448
+ export declare function wroteToolCallAsText(text: string): boolean;
449
+ /** Runs again after a tool call written as text. */
450
+ export declare const TEXT_CALL_RETRIES = 1;
451
+ export declare const TEXT_CALL_NOTE: string;
452
+ /** How many times a step whose model connection was cut runs again. */
453
+ export declare const DROP_RETRIES = 2;
454
+ /** May a step whose connection dropped run again? Yes, unless its tools can
455
+ * act outward and one ran in this attempt (actedThenFailedCheck's rule). */
456
+ export declare function dropRetrySafe(canActOutward: boolean, attemptEvents: {
457
+ type: string;
458
+ }[]): boolean;
255
459
  /** How many times a busy provider is given another moment, and the longest
256
460
  * any one wait may be. Three is the industry's habit and the point where
257
461
  * waiting longer stops being kinder than failing. */