@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.
- package/LICENSE +202 -0
- package/README.md +239 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +43 -0
- package/dist/src/agent-tools.d.ts +31 -0
- package/dist/src/agent-tools.js +99 -0
- package/dist/src/agents-md.d.ts +9 -0
- package/dist/src/agents-md.js +83 -0
- package/dist/src/api-tools.d.ts +38 -0
- package/dist/src/api-tools.js +313 -0
- package/dist/src/approvals.d.ts +39 -0
- package/dist/src/approvals.js +103 -0
- package/dist/src/arrange.d.ts +8 -0
- package/dist/src/arrange.js +40 -0
- package/dist/src/catalog.d.ts +108 -0
- package/dist/src/catalog.js +255 -0
- package/dist/src/completions.d.ts +61 -0
- package/dist/src/completions.js +578 -0
- package/dist/src/confine.d.ts +31 -0
- package/dist/src/confine.js +298 -0
- package/dist/src/container.d.ts +30 -0
- package/dist/src/container.js +172 -0
- package/dist/src/context-tools.d.ts +81 -0
- package/dist/src/context-tools.js +347 -0
- package/dist/src/deploy.d.ts +100 -0
- package/dist/src/deploy.js +377 -0
- package/dist/src/egress.d.ts +75 -0
- package/dist/src/egress.js +133 -0
- package/dist/src/evals.d.ts +150 -0
- package/dist/src/evals.js +651 -0
- package/dist/src/flow-lint.d.ts +16 -0
- package/dist/src/flow-lint.js +177 -0
- package/dist/src/git-hook.d.ts +1 -0
- package/dist/src/git-hook.js +70 -0
- package/dist/src/git.d.ts +37 -0
- package/dist/src/git.js +101 -0
- package/dist/src/gitrepo.d.ts +95 -0
- package/dist/src/gitrepo.js +355 -0
- package/dist/src/history.d.ts +82 -0
- package/dist/src/history.js +259 -0
- package/dist/src/kinds.d.ts +95 -0
- package/dist/src/kinds.js +421 -0
- package/dist/src/library.d.ts +94 -0
- package/dist/src/library.js +341 -0
- package/dist/src/linkable.d.ts +20 -0
- package/dist/src/linkable.js +113 -0
- package/dist/src/model-probe.d.ts +21 -0
- package/dist/src/model-probe.js +81 -0
- package/dist/src/notify.d.ts +61 -0
- package/dist/src/notify.js +252 -0
- package/dist/src/oauth-presets.d.ts +13 -0
- package/dist/src/oauth-presets.js +39 -0
- package/dist/src/okf.d.ts +220 -0
- package/dist/src/okf.js +656 -0
- package/dist/src/openapi.d.ts +66 -0
- package/dist/src/openapi.js +342 -0
- package/dist/src/paths.d.ts +33 -0
- package/dist/src/paths.js +78 -0
- package/dist/src/platform.d.ts +32 -0
- package/dist/src/platform.js +48 -0
- package/dist/src/providers.d.ts +47 -0
- package/dist/src/providers.js +114 -0
- package/dist/src/refs.d.ts +20 -0
- package/dist/src/refs.js +78 -0
- package/dist/src/run-container.d.ts +227 -0
- package/dist/src/run-container.js +760 -0
- package/dist/src/runner.d.ts +241 -0
- package/dist/src/runner.js +3215 -0
- package/dist/src/runtime.d.ts +41 -0
- package/dist/src/runtime.js +357 -0
- package/dist/src/script-tools.d.ts +62 -0
- package/dist/src/script-tools.js +296 -0
- package/dist/src/secret-files.d.ts +12 -0
- package/dist/src/secret-files.js +92 -0
- package/dist/src/secrets.d.ts +147 -0
- package/dist/src/secrets.js +626 -0
- package/dist/src/starter.d.ts +21 -0
- package/dist/src/starter.js +399 -0
- package/dist/src/step-exec.d.ts +136 -0
- package/dist/src/step-exec.js +391 -0
- package/dist/src/storage.d.ts +123 -0
- package/dist/src/storage.js +640 -0
- package/dist/src/store.d.ts +1052 -0
- package/dist/src/store.js +2378 -0
- package/dist/src/tar.d.ts +30 -0
- package/dist/src/tar.js +111 -0
- package/dist/src/tool-names.d.ts +39 -0
- package/dist/src/tool-names.js +79 -0
- package/dist/src/tool-programs.d.ts +18 -0
- package/dist/src/tool-programs.js +52 -0
- package/dist/src/tool-test.d.ts +19 -0
- package/dist/src/tool-test.js +335 -0
- package/dist/src/translator.d.ts +159 -0
- package/dist/src/translator.js +986 -0
- package/dist/src/triggers.d.ts +60 -0
- package/dist/src/triggers.js +270 -0
- package/dist/src/webhook.d.ts +47 -0
- package/dist/src/webhook.js +148 -0
- package/index.ts +44 -0
- package/package.json +64 -0
- package/src/agent-tools.ts +138 -0
- package/src/agents-md.ts +81 -0
- package/src/api-tools.ts +389 -0
- package/src/approvals.ts +129 -0
- package/src/arrange.ts +42 -0
- package/src/catalog.ts +321 -0
- package/src/completions.ts +652 -0
- package/src/confine.ts +350 -0
- package/src/container.ts +220 -0
- package/src/context-tools.ts +435 -0
- package/src/deploy.ts +458 -0
- package/src/egress.ts +176 -0
- package/src/evals.ts +812 -0
- package/src/flow-lint.ts +215 -0
- package/src/git-hook.ts +73 -0
- package/src/git.ts +117 -0
- package/src/gitrepo.ts +391 -0
- package/src/history.ts +329 -0
- package/src/kinds.ts +507 -0
- package/src/library.ts +419 -0
- package/src/linkable.ts +123 -0
- package/src/model-probe.ts +112 -0
- package/src/notify.ts +281 -0
- package/src/oauth-presets.ts +54 -0
- package/src/okf.ts +768 -0
- package/src/openapi.ts +383 -0
- package/src/paths.ts +79 -0
- package/src/platform.ts +87 -0
- package/src/providers.ts +147 -0
- package/src/refs.ts +83 -0
- package/src/run-container.ts +883 -0
- package/src/runner.ts +3446 -0
- package/src/runtime.ts +404 -0
- package/src/script-tools.ts +383 -0
- package/src/secret-files.ts +101 -0
- package/src/secrets.ts +799 -0
- package/src/starter.ts +415 -0
- package/src/step-exec.ts +498 -0
- package/src/storage.ts +771 -0
- package/src/store.ts +2985 -0
- package/src/tar.ts +124 -0
- package/src/tool-names.ts +91 -0
- package/src/tool-programs.ts +65 -0
- package/src/tool-test.ts +401 -0
- package/src/translator.ts +1029 -0
- package/src/triggers.ts +287 -0
- package/src/webhook.ts +183 -0
- package/templates/blog-desk/AGENTS.md +13 -0
- package/templates/blog-desk/agents/researcher/agent.md +11 -0
- package/templates/blog-desk/agents/writer/agent.md +8 -0
- package/templates/blog-desk/evals/writer-quality.md +10 -0
- package/templates/blog-desk/flows/publish.md +10 -0
- package/templates/blog-desk/knowledge/house-style.md +9 -0
- package/templates/blog-desk/knowledge/index.md +9 -0
- package/templates/hello/AGENTS.md +18 -0
- package/templates/hello/agents/curator/agent.md +32 -0
- package/templates/hello/agents/notetaker/agent.md +24 -0
- package/templates/hello/evals/house-style.md +16 -0
- package/templates/hello/flows/memory-review.md +7 -0
- package/templates/hello/flows/note.md +7 -0
- package/templates/hello/knowledge/house-style.md +15 -0
- package/templates/hello/knowledge/index.md +9 -0
- package/templates/hello/skills/plain-english/SKILL.md +11 -0
- package/templates/hello/state/publishing.md +7 -0
- package/templates/hello/tools/wordcount/run.py +13 -0
- 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[];
|
package/dist/src/refs.js
ADDED
|
@@ -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>;
|