@foldrun/core 0.2.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 (166) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +239 -0
  3. package/dist/index.d.ts +32 -0
  4. package/dist/index.js +43 -0
  5. package/dist/src/agent-tools.d.ts +31 -0
  6. package/dist/src/agent-tools.js +99 -0
  7. package/dist/src/agents-md.d.ts +9 -0
  8. package/dist/src/agents-md.js +83 -0
  9. package/dist/src/api-tools.d.ts +38 -0
  10. package/dist/src/api-tools.js +313 -0
  11. package/dist/src/approvals.d.ts +39 -0
  12. package/dist/src/approvals.js +103 -0
  13. package/dist/src/arrange.d.ts +8 -0
  14. package/dist/src/arrange.js +40 -0
  15. package/dist/src/catalog.d.ts +108 -0
  16. package/dist/src/catalog.js +255 -0
  17. package/dist/src/completions.d.ts +61 -0
  18. package/dist/src/completions.js +578 -0
  19. package/dist/src/confine.d.ts +31 -0
  20. package/dist/src/confine.js +298 -0
  21. package/dist/src/container.d.ts +30 -0
  22. package/dist/src/container.js +172 -0
  23. package/dist/src/context-tools.d.ts +81 -0
  24. package/dist/src/context-tools.js +347 -0
  25. package/dist/src/deploy.d.ts +100 -0
  26. package/dist/src/deploy.js +377 -0
  27. package/dist/src/egress.d.ts +75 -0
  28. package/dist/src/egress.js +133 -0
  29. package/dist/src/evals.d.ts +150 -0
  30. package/dist/src/evals.js +651 -0
  31. package/dist/src/flow-lint.d.ts +16 -0
  32. package/dist/src/flow-lint.js +177 -0
  33. package/dist/src/git-hook.d.ts +1 -0
  34. package/dist/src/git-hook.js +70 -0
  35. package/dist/src/git.d.ts +37 -0
  36. package/dist/src/git.js +101 -0
  37. package/dist/src/gitrepo.d.ts +95 -0
  38. package/dist/src/gitrepo.js +355 -0
  39. package/dist/src/history.d.ts +82 -0
  40. package/dist/src/history.js +259 -0
  41. package/dist/src/kinds.d.ts +95 -0
  42. package/dist/src/kinds.js +421 -0
  43. package/dist/src/library.d.ts +94 -0
  44. package/dist/src/library.js +341 -0
  45. package/dist/src/linkable.d.ts +20 -0
  46. package/dist/src/linkable.js +113 -0
  47. package/dist/src/model-probe.d.ts +21 -0
  48. package/dist/src/model-probe.js +81 -0
  49. package/dist/src/notify.d.ts +61 -0
  50. package/dist/src/notify.js +252 -0
  51. package/dist/src/oauth-presets.d.ts +13 -0
  52. package/dist/src/oauth-presets.js +39 -0
  53. package/dist/src/okf.d.ts +220 -0
  54. package/dist/src/okf.js +656 -0
  55. package/dist/src/openapi.d.ts +66 -0
  56. package/dist/src/openapi.js +342 -0
  57. package/dist/src/paths.d.ts +33 -0
  58. package/dist/src/paths.js +78 -0
  59. package/dist/src/platform.d.ts +32 -0
  60. package/dist/src/platform.js +48 -0
  61. package/dist/src/providers.d.ts +47 -0
  62. package/dist/src/providers.js +114 -0
  63. package/dist/src/refs.d.ts +20 -0
  64. package/dist/src/refs.js +78 -0
  65. package/dist/src/run-container.d.ts +227 -0
  66. package/dist/src/run-container.js +760 -0
  67. package/dist/src/runner.d.ts +241 -0
  68. package/dist/src/runner.js +3215 -0
  69. package/dist/src/runtime.d.ts +41 -0
  70. package/dist/src/runtime.js +357 -0
  71. package/dist/src/script-tools.d.ts +62 -0
  72. package/dist/src/script-tools.js +296 -0
  73. package/dist/src/secret-files.d.ts +12 -0
  74. package/dist/src/secret-files.js +92 -0
  75. package/dist/src/secrets.d.ts +147 -0
  76. package/dist/src/secrets.js +626 -0
  77. package/dist/src/starter.d.ts +21 -0
  78. package/dist/src/starter.js +399 -0
  79. package/dist/src/step-exec.d.ts +136 -0
  80. package/dist/src/step-exec.js +391 -0
  81. package/dist/src/storage.d.ts +123 -0
  82. package/dist/src/storage.js +640 -0
  83. package/dist/src/store.d.ts +1052 -0
  84. package/dist/src/store.js +2378 -0
  85. package/dist/src/tar.d.ts +30 -0
  86. package/dist/src/tar.js +111 -0
  87. package/dist/src/tool-names.d.ts +39 -0
  88. package/dist/src/tool-names.js +79 -0
  89. package/dist/src/tool-programs.d.ts +18 -0
  90. package/dist/src/tool-programs.js +52 -0
  91. package/dist/src/tool-test.d.ts +19 -0
  92. package/dist/src/tool-test.js +335 -0
  93. package/dist/src/translator.d.ts +159 -0
  94. package/dist/src/translator.js +986 -0
  95. package/dist/src/triggers.d.ts +60 -0
  96. package/dist/src/triggers.js +270 -0
  97. package/dist/src/webhook.d.ts +47 -0
  98. package/dist/src/webhook.js +148 -0
  99. package/index.ts +44 -0
  100. package/package.json +64 -0
  101. package/src/agent-tools.ts +138 -0
  102. package/src/agents-md.ts +81 -0
  103. package/src/api-tools.ts +389 -0
  104. package/src/approvals.ts +129 -0
  105. package/src/arrange.ts +42 -0
  106. package/src/catalog.ts +321 -0
  107. package/src/completions.ts +652 -0
  108. package/src/confine.ts +350 -0
  109. package/src/container.ts +220 -0
  110. package/src/context-tools.ts +435 -0
  111. package/src/deploy.ts +458 -0
  112. package/src/egress.ts +176 -0
  113. package/src/evals.ts +812 -0
  114. package/src/flow-lint.ts +215 -0
  115. package/src/git-hook.ts +73 -0
  116. package/src/git.ts +117 -0
  117. package/src/gitrepo.ts +391 -0
  118. package/src/history.ts +329 -0
  119. package/src/kinds.ts +507 -0
  120. package/src/library.ts +419 -0
  121. package/src/linkable.ts +123 -0
  122. package/src/model-probe.ts +112 -0
  123. package/src/notify.ts +281 -0
  124. package/src/oauth-presets.ts +54 -0
  125. package/src/okf.ts +768 -0
  126. package/src/openapi.ts +383 -0
  127. package/src/paths.ts +79 -0
  128. package/src/platform.ts +87 -0
  129. package/src/providers.ts +147 -0
  130. package/src/refs.ts +83 -0
  131. package/src/run-container.ts +883 -0
  132. package/src/runner.ts +3446 -0
  133. package/src/runtime.ts +404 -0
  134. package/src/script-tools.ts +383 -0
  135. package/src/secret-files.ts +101 -0
  136. package/src/secrets.ts +799 -0
  137. package/src/starter.ts +415 -0
  138. package/src/step-exec.ts +498 -0
  139. package/src/storage.ts +771 -0
  140. package/src/store.ts +2985 -0
  141. package/src/tar.ts +124 -0
  142. package/src/tool-names.ts +91 -0
  143. package/src/tool-programs.ts +65 -0
  144. package/src/tool-test.ts +401 -0
  145. package/src/translator.ts +1029 -0
  146. package/src/triggers.ts +287 -0
  147. package/src/webhook.ts +183 -0
  148. package/templates/blog-desk/AGENTS.md +13 -0
  149. package/templates/blog-desk/agents/researcher/agent.md +11 -0
  150. package/templates/blog-desk/agents/writer/agent.md +8 -0
  151. package/templates/blog-desk/evals/writer-quality.md +10 -0
  152. package/templates/blog-desk/flows/publish.md +10 -0
  153. package/templates/blog-desk/knowledge/house-style.md +9 -0
  154. package/templates/blog-desk/knowledge/index.md +9 -0
  155. package/templates/hello/AGENTS.md +18 -0
  156. package/templates/hello/agents/curator/agent.md +32 -0
  157. package/templates/hello/agents/notetaker/agent.md +24 -0
  158. package/templates/hello/evals/house-style.md +16 -0
  159. package/templates/hello/flows/memory-review.md +7 -0
  160. package/templates/hello/flows/note.md +7 -0
  161. package/templates/hello/knowledge/house-style.md +15 -0
  162. package/templates/hello/knowledge/index.md +9 -0
  163. package/templates/hello/skills/plain-english/SKILL.md +11 -0
  164. package/templates/hello/state/publishing.md +7 -0
  165. package/templates/hello/tools/wordcount/run.py +13 -0
  166. package/templates/hello/tools/wordcount/tool.md +22 -0
@@ -0,0 +1,114 @@
1
+ // The providers this runtime knows by name.
2
+ //
3
+ // provider:
4
+ // name: groq
5
+ // token: ${GROQ_API_KEY}
6
+ // models: { fast: llama-3.3-70b-versatile }
7
+ //
8
+ // A name resolves to three facts: which wire format the endpoint speaks,
9
+ // where it is, and which header the key travels in. Everything here is
10
+ // bring-your-own-key — the platform never holds a provider credential on a
11
+ // customer's behalf — and a name is a convenience over `base_url:` +
12
+ // `format:` + `auth:`, never a requirement: an endpoint this table has
13
+ // never heard of works the same way by spelling those three out.
14
+ //
15
+ // Three formats. `responses` is OpenAI's newer API — the same translator
16
+ // with a second pair of mappings, for an OpenAI key that wants what only
17
+ // that shape carries (a PDF as input, the reasoning knobs). Nobody else
18
+ // implements it, so no preset defaults to it; a block asks for it by name.
19
+ // Two formats, deliberately. `anthropic` is what the runtime speaks, so
20
+ // those endpoints are reached directly. `openai` endpoints are reached
21
+ // through the runtime's own translator (translator.ts), which runs on
22
+ // localhost inside the run sandbox and rewrites Anthropic Messages to Chat
23
+ // Completions and back. Every provider without an Anthropic-shaped endpoint
24
+ // speaks Chat Completions — OpenAI, Gemini, xAI, Groq, Mistral, Hugging
25
+ // Face, Cloudflare's own models — so one translation reaches all of them.
26
+ //
27
+ // URLs and header shapes were checked against each provider's own
28
+ // documentation on 2026-09-02 and again on 2026-09-06 (see docs/providers.md). `verified` says
29
+ // whether a tool loop was actually driven through the endpoint from here,
30
+ // which is the only claim that matters for an agent runtime; a name without
31
+ // it is documented, not proven.
32
+ export const PROVIDERS = [
33
+ // ------------------------------------------------ Anthropic-shaped, direct
34
+ { name: "anthropic", title: "Anthropic", format: "anthropic", baseUrl: "https://api.anthropic.com", auth: "x-api-key", verified: true,
35
+ note: "Models newer than Opus 4.6 reject top_k with a 400; the runtime never sends it unless a params: block does." },
36
+ { name: "openrouter", title: "OpenRouter", format: "anthropic", baseUrl: "https://openrouter.ai/api", auth: "bearer", verified: true,
37
+ note: "Hundreds of models behind one key. Its own docs disagree on how well non-Anthropic models hold a tool loop on this endpoint — probe the model you mean to use." },
38
+ { name: "deepseek", title: "DeepSeek", format: "anthropic", baseUrl: "https://api.deepseek.com/anthropic", auth: "x-api-key",
39
+ note: "Ignores top_k, cache_control and thinking budgets; Claude model names are remapped to DeepSeek's." },
40
+ { name: "kimi", title: "Moonshot Kimi", format: "anthropic", baseUrl: "https://api.moonshot.ai/anthropic", auth: "bearer",
41
+ note: "Own model ids only. Known bug (2026-09): K3 reuses one tool_use id across separate calls, which breaks a tool loop." },
42
+ { name: "moonshot", title: "Moonshot Kimi", format: "anthropic", baseUrl: "https://api.moonshot.ai/anthropic", auth: "bearer",
43
+ note: "Same endpoint as kimi." },
44
+ { name: "zai", title: "z.ai (GLM)", format: "anthropic", baseUrl: "https://api.z.ai/api/anthropic", auth: "bearer" },
45
+ { name: "minimax", title: "MiniMax", format: "anthropic", baseUrl: "https://api.minimax.io/anthropic", auth: "bearer" },
46
+ { name: "qwen", title: "Alibaba Qwen (Model Studio)", format: "anthropic", auth: "x-api-key",
47
+ note: "base_url depends on the plan: pay-as-you-go https://<workspace>.<region>.maas.aliyuncs.com/apps/anthropic; Coding Plan https://coding-intl.dashscope.aliyuncs.com/apps/anthropic. Own model ids (qwen3.7-max …), no remap; no reasoning_effort, use thinking." },
48
+ { name: "fireworks", title: "Fireworks AI", format: "anthropic", baseUrl: "https://api.fireworks.ai/inference", auth: "bearer",
49
+ note: "No server-side tools; no adaptive thinking." },
50
+ { name: "deepinfra", title: "DeepInfra", format: "anthropic", baseUrl: "https://api.deepinfra.com/anthropic", auth: "bearer" },
51
+ { name: "sambanova", title: "SambaNova", format: "anthropic", baseUrl: "https://api.sambanova.ai", auth: "x-api-key",
52
+ note: "No server-side tools, base64 images only." },
53
+ { name: "vercel", title: "Vercel AI Gateway", format: "anthropic", baseUrl: "https://ai-gateway.vercel.sh", auth: "bearer",
54
+ note: "Model ids are namespaced, e.g. openai/gpt-5." },
55
+ { name: "litellm", title: "LiteLLM (yours)", format: "anthropic", auth: "x-api-key",
56
+ note: "base_url is your proxy, e.g. http://litellm.internal:4000. It presents an Anthropic endpoint and speaks anything behind it." },
57
+ { name: "cloudflare-gateway", title: "Cloudflare AI Gateway", format: "anthropic", auth: "x-api-key",
58
+ note: "base_url is https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic — a pass-through to Anthropic with logging; for Cloudflare's own models use name: cloudflare." },
59
+ { name: "ollama", title: "Ollama (local)", format: "anthropic", baseUrl: "http://localhost:11434", auth: "x-api-key",
60
+ note: "v0.14+. Ignores tool_choice and cache_control; accepts but does not enforce thinking budgets; base64 images only. Any key value is accepted." },
61
+ { name: "lmstudio", title: "LM Studio (local)", format: "anthropic", baseUrl: "http://localhost:1234", auth: "x-api-key" },
62
+ { name: "vllm", title: "vLLM (yours)", format: "anthropic", auth: "bearer",
63
+ note: "base_url is your server (default :8000). Start it with --enable-auto-tool-choice and a --tool-call-parser or no tool loop closes." },
64
+ // ------------------------------------------- Chat-Completions, translated
65
+ { name: "openai", title: "OpenAI", format: "openai", baseUrl: "https://api.openai.com/v1", auth: "bearer", verified: true,
66
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true,
67
+ note: "Reached over Chat Completions, which OpenAI keeps supporting; the Responses API is not spoken here, so a reasoning model's reasoning does not carry across tool calls." },
68
+ { name: "gemini", title: "Google Gemini", format: "openai", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", auth: "bearer",
69
+ note: "Google's OpenAI-compatible route. Unknown parameters are ignored silently; reasoning cannot be switched off on the newest models." },
70
+ { name: "xai", title: "xAI Grok", format: "openai", baseUrl: "https://api.x.ai/v1", auth: "bearer",
71
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
72
+ { name: "groq", title: "Groq", format: "openai", baseUrl: "https://api.groq.com/openai/v1", auth: "bearer",
73
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
74
+ { name: "mistral", title: "Mistral", format: "openai", baseUrl: "https://api.mistral.ai/v1", auth: "bearer" },
75
+ { name: "together", title: "Together AI", format: "openai", baseUrl: "https://api.together.ai/v1", auth: "bearer" },
76
+ { name: "cerebras", title: "Cerebras", format: "openai", baseUrl: "https://api.cerebras.ai/v1", auth: "bearer",
77
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
78
+ { name: "huggingface", title: "Hugging Face", format: "openai", baseUrl: "https://router.huggingface.co/v1", auth: "bearer",
79
+ note: "The Inference Providers router; model ids are Hub ids, e.g. meta-llama/Llama-3.3-70B-Instruct." },
80
+ { name: "cloudflare", title: "Cloudflare Workers AI", format: "openai", auth: "bearer",
81
+ note: "base_url is https://api.cloudflare.com/client/v4/accounts/<account>/ai/v1" },
82
+ { name: "nebius", title: "Nebius Token Factory", format: "openai", baseUrl: "https://api.tokenfactory.nebius.com/v1", auth: "bearer" },
83
+ { name: "novita", title: "Novita", format: "openai", baseUrl: "https://api.novita.ai/openai", auth: "bearer",
84
+ note: "Also has an Anthropic-shaped route at https://api.novita.ai/anthropic — spell it out with format: anthropic to skip the translator (header shape unverified)." },
85
+ { name: "hyperbolic", title: "Hyperbolic", format: "openai", baseUrl: "https://api.hyperbolic.xyz/v1", auth: "bearer" },
86
+ ];
87
+ /**
88
+ * Request fields `params:` may not set.
89
+ *
90
+ * These are not the provider's knobs, they are the conversation: the
91
+ * messages, the tools the agent declared, whether the reply streams, and
92
+ * which model the tier resolved to. A file that could overwrite them could
93
+ * silently break the agent loop — or make `model: fast` mean something else
94
+ * — from a line that looks like a tuning option. Everything else passes
95
+ * through untouched, because a vendor's own parameters are the vendor's
96
+ * business and this format will never model them.
97
+ */
98
+ export const PROTECTED_PARAMS = ["messages", "tools", "stream", "stream_options", "model"];
99
+ /** A preset by any spelling a person reaches for: case-insensitive, and
100
+ * "z.ai" / "z-ai" / "hugging-face" all land. */
101
+ export function providerPreset(name) {
102
+ if (typeof name !== "string")
103
+ return null;
104
+ const key = name.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
105
+ if (!key)
106
+ return null;
107
+ return PROVIDERS.find((p) => p.name.replace(/[^a-z0-9]/g, "") === key) ?? null;
108
+ }
109
+ /** Does this URL look like a Chat-Completions endpoint a person pasted
110
+ * without saying `format: openai`? The heuristic the checker uses to say
111
+ * so before a run fails with a 404 from the wrong path. */
112
+ export function looksOpenAiShaped(baseUrl) {
113
+ return /\/v1\/?$|\/chat\/completions|api\.openai\.com|\/openai\b/i.test(baseUrl);
114
+ }
@@ -0,0 +1,20 @@
1
+ /** One entry of a file-naming list. `mode` is the value of a map-form
2
+ * entry — `{bash: ask}` — and null for a plain name. */
3
+ export interface Ref {
4
+ name: string;
5
+ linked: boolean;
6
+ mode: string | null;
7
+ }
8
+ /** The name inside `[[…]]`, with `flow:` and surrounding space kept. */
9
+ export declare function stripLink(raw: string): {
10
+ name: string;
11
+ linked: boolean;
12
+ };
13
+ /**
14
+ * Every entry of a list field, in order, empty names dropped. Accepts the
15
+ * shapes a file can carry: an inline or block list of names, links or maps;
16
+ * a single name; or Claude Code's comma string (`tools: Read, Grep`).
17
+ */
18
+ export declare function refList(raw: unknown): Ref[];
19
+ /** Just the names — for a reader that does not care how they were spelt. */
20
+ export declare function refNames(raw: unknown): string[];
@@ -0,0 +1,78 @@
1
+ // A frontmatter list that names files — `tools:`, `skills:`, `agents:`,
2
+ // `after:` — read the same way whether an entry is bare or a `[[link]]`.
3
+ //
4
+ // `[[name]]` is the format's one reference syntax, and until now the
5
+ // frontmatter was the last place a name had to be bare. The rule is one
6
+ // sentence: if it names a file, you can link it. Agents, flows, skills,
7
+ // tools, knowledge and memory are files; secrets, model and size are values.
8
+ //
9
+ // YAML is the complication. To YAML, `- [[site_repo]]` is a nested list
10
+ // (`[["site_repo"]]`), and `tools: [read, [[site_repo]]]` is a list holding
11
+ // a string and a list. So an entry that arrives as an array IS a link — the
12
+ // brackets were consumed by the parser — and one that arrives as the string
13
+ // "[[x]]" (quoted, or from a step option line) is the same link. Both read
14
+ // as the name with `linked: true`; a bare string reads as `linked: false`.
15
+ // The distinction matters once: a linked name can only mean one of the
16
+ // author's files, so `[[search]]` is your tool and `search` is the runtime's
17
+ // group. Everywhere else the two are the same name.
18
+ /** The name inside `[[…]]`, with `flow:` and surrounding space kept. */
19
+ export function stripLink(raw) {
20
+ const s = raw.trim();
21
+ const m = s.match(/^\[\[(.*)\]\]$/);
22
+ return m ? { name: m[1].trim(), linked: true } : { name: s, linked: false };
23
+ }
24
+ /** The first string anywhere inside a nested array — what YAML made of
25
+ * `[[x]]`. Deeper nesting (`[[[x]]]`) collapses to the same name. */
26
+ function innermost(value) {
27
+ if (typeof value === "string" || typeof value === "number")
28
+ return String(value);
29
+ if (Array.isArray(value)) {
30
+ for (const v of value) {
31
+ const s = innermost(v);
32
+ if (s !== null)
33
+ return s;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Every entry of a list field, in order, empty names dropped. Accepts the
40
+ * shapes a file can carry: an inline or block list of names, links or maps;
41
+ * a single name; or Claude Code's comma string (`tools: Read, Grep`).
42
+ */
43
+ export function refList(raw) {
44
+ if (raw === undefined || raw === null)
45
+ return [];
46
+ const entries = Array.isArray(raw) ? raw : typeof raw === "string" && raw.includes(",") ? raw.split(",") : [raw];
47
+ const out = [];
48
+ for (const entry of entries) {
49
+ if (entry === null || entry === undefined)
50
+ continue;
51
+ if (Array.isArray(entry)) {
52
+ const name = innermost(entry);
53
+ if (name !== null) {
54
+ const inner = stripLink(name);
55
+ if (inner.name)
56
+ out.push({ name: inner.name, linked: true, mode: null });
57
+ }
58
+ continue;
59
+ }
60
+ if (typeof entry === "object") {
61
+ const [key, value] = Object.entries(entry)[0] ?? [];
62
+ if (!key)
63
+ continue;
64
+ const { name, linked } = stripLink(key);
65
+ if (name)
66
+ out.push({ name, linked, mode: value === undefined || value === null ? null : String(value) });
67
+ continue;
68
+ }
69
+ const { name, linked } = stripLink(String(entry));
70
+ if (name)
71
+ out.push({ name, linked, mode: null });
72
+ }
73
+ return out;
74
+ }
75
+ /** Just the names — for a reader that does not care how they were spelt. */
76
+ export function refNames(raw) {
77
+ return refList(raw).map((r) => r.name);
78
+ }
@@ -0,0 +1,227 @@
1
+ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
2
+ import { type ApiSpec, type Effort } from "./store.ts";
3
+ import type { EventExtra } from "./step-exec.ts";
4
+ import { type RuntimeSpec } from "./runtime.ts";
5
+ import type { ScriptSpec } from "./script-tools.ts";
6
+ import type { ConsultSpec } from "./agent-tools.ts";
7
+ import type { SearchRoot, RunDigest } from "./context-tools.ts";
8
+ import type { TranslatorSpec } from "./translator.ts";
9
+ /** What crosses the boundary. Everything in here is values — pre-resolved
10
+ * secrets in headers, assembled prompt, serializable MCP configs. */
11
+ export interface ContainerStepInput {
12
+ agentRel: string;
13
+ prompt: string;
14
+ model: string;
15
+ effort?: Effort | null;
16
+ systemPrompt: string;
17
+ allowed: string[];
18
+ mcpNames: string[];
19
+ /** http/stdio configs only — in-process servers are rebuilt inside. */
20
+ mcpServers: Record<string, McpServerConfig>;
21
+ apis: ApiSpec[];
22
+ scripts: ScriptSpec[];
23
+ /** The agent's `runtime:` declaration — the driver builds the venv/npm
24
+ * prefix inside the container, where the network is. Without this,
25
+ * dependency-using scripts worked locally and failed only in production
26
+ * isolation, which is the worst place for a difference. */
27
+ runtime: RuntimeSpec | null;
28
+ /** Colleagues this agent may consult — persona + model, gathered from
29
+ * their agent.md files host-side. The driver rebuilds the consult tools
30
+ * in here; the callees run toolless, so nothing else need cross. */
31
+ consults: ConsultSpec[];
32
+ timeoutSec?: number;
33
+ /** The step's spend ceiling and the model's per-token price — see
34
+ * step-exec.ts. Values, so the driver can enforce them in the pod. */
35
+ budgetUsd?: number | null;
36
+ price?: {
37
+ input: number;
38
+ output: number;
39
+ } | null;
40
+ verify?: string;
41
+ /** `output: json` — checked inside, where the reply is. */
42
+ output?: "json";
43
+ /** tools: [search] — the directories to search, as paths INSIDE the
44
+ * container (/workspace/…, /library/…). */
45
+ search?: SearchRoot[];
46
+ /** tools: [history] — the workspace's recent runs, gathered host-side. */
47
+ history?: RunDigest[];
48
+ /** tools: [desks] — the account's other workspaces' runs, gathered host-side. */
49
+ desks?: RunDigest[];
50
+ /** A Chat-Completions provider: the driver starts the runtime's translator
51
+ * on loopback and points the SDK at it. Null or absent: the env's
52
+ * ANTHROPIC_BASE_URL is spoken to directly. */
53
+ translator?: TranslatorSpec | null;
54
+ }
55
+ /**
56
+ * How long the sandbox actually existed, measured host-side — the driver
57
+ * cannot time its own cold start, because it isn't running yet.
58
+ *
59
+ * The split is where the two executors differ, so read it per executor:
60
+ * `sandboxMs` is scheduling and admission (docker `create` returns almost
61
+ * at once; a gVisor pod's Ready is the real cold-start number), and
62
+ * `firstOutputMs` is guest boot to the driver's first line (for docker
63
+ * that's where the cold start actually lands, since `start` is when the
64
+ * container boots). `totalMs` is what the platform rented either way, and
65
+ * is the only one billing should ever use.
66
+ */
67
+ export interface StepTiming {
68
+ sandboxMs: number;
69
+ firstOutputMs: number | null;
70
+ totalMs: number;
71
+ }
72
+ export interface StepActuals {
73
+ busyCpuSecs: number | null;
74
+ peakMemBytes: number | null;
75
+ rxBytes: number | null;
76
+ txBytes: number | null;
77
+ }
78
+ export interface ContainerStepOutcome {
79
+ status: "completed" | "failed";
80
+ result: string | null;
81
+ /** The model's final text block — see StepRecord.conclusion. Crosses the
82
+ * boundary with the rest of the outcome; the driver spreads it. */
83
+ conclusion?: string | null;
84
+ /** The parsed JSON of an `output: json` step, crossing back as a value. */
85
+ data?: unknown;
86
+ costUsd: number | null;
87
+ usage?: {
88
+ inputTokens: number;
89
+ outputTokens: number;
90
+ } | null;
91
+ /** What the sandbox actually touched — see the driver's readActuals. */
92
+ res?: StepActuals | null;
93
+ /** Set by the isolated executors only — an in-process step rents no
94
+ * sandbox, so it has no compute seconds to bill. */
95
+ timing?: StepTiming | null;
96
+ }
97
+ /**
98
+ * The one place the timing line is worded, so the docker and k8s traces
99
+ * read the same and a cold-start regression is greppable across both.
100
+ */
101
+ export declare function timingLine(t: StepTiming): string;
102
+ /** Should this container-side path be applied back to the host workspace? */
103
+ export declare function allowedBack(rel: string): boolean;
104
+ /**
105
+ * Apply what the STEP changed onto the real workspace — additive and
106
+ * overwriting, never deleting: an agent organising its own files gains
107
+ * nothing from deleting on the host that it can't get by writing, and a bug
108
+ * in a deletion sync destroys authored work.
109
+ *
110
+ * `baseline` is the copy that was handed to the container at the start of the
111
+ * step. Without it this function compares the container against the HOST, and
112
+ * that is a different question with a worse answer: a file the step never
113
+ * touched, edited on the host while the step ran, differs — so it was written
114
+ * back from the container's stale copy and the edit vanished. It happened on
115
+ * 2026-09-03: a tool.md fixed mid-run was reverted eight minutes later by a
116
+ * step that had never read it, and the repository kept the fix while the
117
+ * workspace lost it.
118
+ *
119
+ * With the baseline the rule is simply: identical to what we handed in means
120
+ * the step did not touch it, so it is not ours to write back. Concurrent
121
+ * edits survive, and a step that genuinely changed a file still wins.
122
+ */
123
+ export declare function applyContainerChanges(hostWs: string, containerWs: string, baseline?: string): string[];
124
+ /**
125
+ * Where the driver builds venvs and npm prefixes, as an absolute path *inside*
126
+ * the runner. Fixed, and it has to be: a venv bakes its own absolute path into
127
+ * every shebang and into pyvenv.cfg, so a cache reused across containers is
128
+ * only valid if it is mounted at the identical path every time. It is the same
129
+ * path prepareRuntime picks on its own — FOLDRUN_DATA is /home/agent/.foldrun
130
+ * in the driver, and the in-container tenant is "runner" — so mounting here
131
+ * turns the existing cache into a persistent one with no change to the code
132
+ * that fills it.
133
+ */
134
+ export declare const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
135
+ /**
136
+ * The per-tenant directory backing that mount, or null when there is none.
137
+ *
138
+ * Per-tenant, not shared. A shared cache would store one copy of pandas
139
+ * instead of one per account, but it would also be mutable state that one
140
+ * tenant's step writes and another tenant's step then *executes* — a poisoned
141
+ * venv arriving as a trusted local file, which is precisely the path gVisor,
142
+ * the capability drop and the egress policy all exist to close. Disk is the
143
+ * cheaper thing to spend. If the duplication ever stops being cheap, the fix
144
+ * is a read-only mount filled by a trusted builder, not a writable shared one.
145
+ *
146
+ * Deliberately NOT under `<tenant>/.runtimes`, which is the *host* cache: the
147
+ * venvs there carry host absolute paths and would be unusable inside a runner.
148
+ * Two roots, so a hit is always a real hit.
149
+ */
150
+ export declare function runtimeCacheDir(tenant: string | undefined): string | null;
151
+ export declare function runnerImageTag(): string;
152
+ /**
153
+ * Build the runner image if this exact (core version, driver, dockerfile)
154
+ * has not been built before. `npm pack` of the installed core, so the image
155
+ * runs the same bytes the server does.
156
+ */
157
+ export declare function ensureRunnerImage(): {
158
+ tag: string;
159
+ log: string[];
160
+ };
161
+ export declare function parseDriverLine(line: string): {
162
+ e: "event";
163
+ type: "text" | "tool" | "info" | "error";
164
+ text: string;
165
+ call?: string;
166
+ ms?: number;
167
+ err?: boolean;
168
+ } | {
169
+ e: "done";
170
+ } & ContainerStepOutcome | null;
171
+ export interface RunInContainerArgs {
172
+ workspaceRoot: string;
173
+ libraryRoot: string;
174
+ /** The account this step belongs to — the key for its dependency cache.
175
+ * Optional so an embedder that has no tenancy simply gets no cache. */
176
+ tenant?: string;
177
+ input: ContainerStepInput;
178
+ /** Secret + provider env — crosses as an env file, never argv. */
179
+ env: Record<string, string>;
180
+ emit: (type: "text" | "tool" | "info" | "error", text: string, extra?: EventExtra) => void;
181
+ /** The run this step belongs to. Stamped on the sandbox as a label so a
182
+ * person stopping the run can reach the thing actually burning money —
183
+ * without it, "stop" could only mean "stop after this step", and a
184
+ * browser step has fifteen minutes left to spend. */
185
+ runId?: string;
186
+ /** The reservation class — which limits this step's sandbox holds. */
187
+ size?: "small" | "large" | "heavy";
188
+ }
189
+ /** The limits a size class reserves. Large is the install's configured
190
+ * limits unchanged, so existing installs bill and behave identically;
191
+ * small defaults to a quarter-ish slice and is env-tunable. */
192
+ /**
193
+ * A memory figure Kubernetes will accept.
194
+ *
195
+ * `sizeLimits` speaks Docker, because that is who asked first: `--memory 2g`.
196
+ * Kubernetes wants a quantity — `2Gi` — and rejects the lowercase form with
197
+ * "quantities must match the regular expression", which names neither the
198
+ * field nor the value. The default size returns `2g`, so every default-size
199
+ * step failed to create a pod on any install that had not overridden
200
+ * FOLDRUN_RUNNER_MEMORY. These manifests do override it, which is why this
201
+ * survived: it broke for self-hosters and for nobody testing here.
202
+ *
203
+ * Docker's suffixes are binary, so `g` is `Gi` and not `G`. A value that is
204
+ * already a Kubernetes quantity passes through, and an unrecognised one is
205
+ * handed over unchanged so the cluster's own error is the one you read.
206
+ *
207
+ * Case decides, because one suffix is ambiguous: `k` is a legal Kubernetes
208
+ * decimal suffix AND Docker's kibibytes. Everything this is given comes from
209
+ * sizeLimits, which speaks Docker, so lowercase is read as Docker's — the
210
+ * difference is 2.4% on a memory limit and the alternative is silently
211
+ * misreading the default.
212
+ */
213
+ export declare function k8sMemory(value: string): string;
214
+ export declare function sizeLimits(size?: "small" | "large" | "heavy"): {
215
+ cpus: string;
216
+ memory: string;
217
+ };
218
+ /** The label both executors stamp, so one name means one thing. */
219
+ export declare const RUN_LABEL = "foldrun-run-id";
220
+ /**
221
+ * Destroy any sandbox still running for this run. Best effort by design:
222
+ * the step may have finished a moment ago, the daemon may be unreachable,
223
+ * and neither is a reason to refuse to stop a run. Returns how many it
224
+ * removed, which is what the trace reports.
225
+ */
226
+ export declare function killRunSandboxes(runId: string): number;
227
+ export declare function runStepInContainer(args: RunInContainerArgs): Promise<ContainerStepOutcome>;