@foldrun/core 0.5.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 (99) hide show
  1. package/README.md +7 -4
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +4 -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/browser-pod.d.ts +20 -1
  10. package/dist/src/browser-pod.js +58 -9
  11. package/dist/src/completions.js +1 -0
  12. package/dist/src/confine.d.ts +1 -0
  13. package/dist/src/confine.js +33 -4
  14. package/dist/src/container.d.ts +9 -0
  15. package/dist/src/container.js +115 -5
  16. package/dist/src/context-tools.d.ts +1 -1
  17. package/dist/src/deploy.d.ts +9 -0
  18. package/dist/src/deploy.js +31 -3
  19. package/dist/src/evals.js +24 -4
  20. package/dist/src/host-env.d.ts +14 -0
  21. package/dist/src/host-env.js +31 -0
  22. package/dist/src/install-helper.d.ts +17 -0
  23. package/dist/src/install-helper.js +134 -0
  24. package/dist/src/install-tool.d.ts +128 -0
  25. package/dist/src/install-tool.js +133 -0
  26. package/dist/src/mail.js +3 -1
  27. package/dist/src/model-credential.d.ts +30 -0
  28. package/dist/src/model-credential.js +98 -0
  29. package/dist/src/model-env.d.ts +17 -0
  30. package/dist/src/model-env.js +57 -0
  31. package/dist/src/package.d.ts +67 -0
  32. package/dist/src/package.js +594 -0
  33. package/dist/src/platform.d.ts +5 -0
  34. package/dist/src/platform.js +1 -0
  35. package/dist/src/provider-health.js +3 -0
  36. package/dist/src/run-container.d.ts +6 -1
  37. package/dist/src/run-container.js +93 -20
  38. package/dist/src/runner.d.ts +239 -0
  39. package/dist/src/runner.js +380 -72
  40. package/dist/src/runtime.d.ts +77 -0
  41. package/dist/src/runtime.js +230 -5
  42. package/dist/src/script-tools.d.ts +16 -0
  43. package/dist/src/script-tools.js +50 -8
  44. package/dist/src/secrets.d.ts +5 -0
  45. package/dist/src/secrets.js +37 -6
  46. package/dist/src/starter.d.ts +12 -0
  47. package/dist/src/starter.js +36 -4
  48. package/dist/src/step-exec.d.ts +22 -2
  49. package/dist/src/step-exec.js +70 -7
  50. package/dist/src/storage.d.ts +7 -1
  51. package/dist/src/storage.js +14 -1
  52. package/dist/src/store.d.ts +24 -0
  53. package/dist/src/store.js +91 -10
  54. package/dist/src/subagents.js +1 -1
  55. package/dist/src/test-mode.d.ts +17 -2
  56. package/dist/src/test-mode.js +53 -13
  57. package/dist/src/tool-names.d.ts +3 -1
  58. package/dist/src/tool-names.js +4 -2
  59. package/dist/src/tool-test.js +8 -1
  60. package/dist/src/zip.d.ts +17 -0
  61. package/dist/src/zip.js +184 -0
  62. package/index.ts +4 -0
  63. package/package.json +1 -1
  64. package/src/agent-import.ts +154 -0
  65. package/src/api-tools.ts +16 -1
  66. package/src/approvals.ts +10 -3
  67. package/src/browser-pod.ts +46 -9
  68. package/src/completions.ts +1 -0
  69. package/src/confine.ts +36 -5
  70. package/src/container.ts +112 -5
  71. package/src/context-tools.ts +1 -1
  72. package/src/deploy.ts +28 -3
  73. package/src/evals.ts +23 -2
  74. package/src/host-env.ts +32 -0
  75. package/src/install-helper.ts +143 -0
  76. package/src/install-tool.ts +154 -0
  77. package/src/mail.ts +3 -1
  78. package/src/model-credential.ts +112 -0
  79. package/src/model-env.ts +66 -0
  80. package/src/package.ts +664 -0
  81. package/src/platform.ts +6 -0
  82. package/src/provider-health.ts +3 -0
  83. package/src/run-container.ts +92 -19
  84. package/src/runner.ts +385 -72
  85. package/src/runtime.ts +235 -5
  86. package/src/script-tools.ts +48 -6
  87. package/src/secrets.ts +34 -6
  88. package/src/starter.ts +42 -4
  89. package/src/step-exec.ts +75 -10
  90. package/src/storage.ts +13 -0
  91. package/src/store.ts +91 -9
  92. package/src/subagents.ts +1 -1
  93. package/src/test-mode.ts +51 -11
  94. package/src/tool-names.ts +4 -2
  95. package/src/tool-test.ts +8 -1
  96. package/src/zip.ts +189 -0
  97. package/templates/blog-desk/agents/writer/agent.md +1 -1
  98. package/templates/hello/agents/curator/agent.md +3 -3
  99. package/templates/hello/agents/notetaker/agent.md +2 -2
@@ -0,0 +1,133 @@
1
+ // The `install` tool: an agent installs what it finds it needs, mid-step,
2
+ // without anyone having declared it. Built in and granted to every agent
3
+ // (take it away with `disallowedTools: [install]`).
4
+ //
5
+ // install(manager: "apt", packages: ["ffmpeg"]) — Debian programs and libraries
6
+ // install(manager: "pip", packages: ["manim>=0.19"]) — Python packages
7
+ // install(manager: "npm", packages: ["sharp"]) — Node packages
8
+ //
9
+ // apt needs root, which the agent never has: it asks the root start-up's
10
+ // install helper over a unix socket (install-helper.ts), which installs only
11
+ // validated names through the same cache and snapshots as `runtime: system:`.
12
+ // pip and npm need no root and run here, as the agent, into a per-step
13
+ // directory already on PATH / PYTHONPATH / NODE_PATH (stepInstallEnv), so the
14
+ // next command finds what this one installed. Inside a run container all of
15
+ // it goes when the container does. Every install is logged with the line to
16
+ // add to the agent's `runtime:` to have it before the step starts next time.
17
+ import fs from "node:fs";
18
+ import net from "node:net";
19
+ import path from "node:path";
20
+ import { spawnSync } from "node:child_process";
21
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
22
+ import { z } from "zod";
23
+ import { DEB_PACKAGE, PIP_REQUIREMENT, NPM_REQUIREMENT, summarizeInstallFailure } from "./runtime.js";
24
+ export const INSTALL_TOOL = "mcp__foldrun_install__install";
25
+ const PATTERNS = { apt: DEB_PACKAGE, pip: PIP_REQUIREMENT, npm: NPM_REQUIREMENT };
26
+ const RUNTIME_KEY = { apt: "system", pip: "packages", npm: "npm" };
27
+ function findUv() {
28
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
29
+ if (dir && fs.existsSync(path.join(dir, "uv")))
30
+ return path.join(dir, "uv");
31
+ }
32
+ return null;
33
+ }
34
+ /** Ask the root helper; resolves with its reply or an error. */
35
+ export function askHelper(socket, packages, timeoutMs = 600_000) {
36
+ return new Promise((resolve) => {
37
+ const conn = net.createConnection(socket);
38
+ let buf = "";
39
+ const timer = setTimeout(() => {
40
+ conn.destroy();
41
+ resolve({ ok: false, error: `the install helper did not answer in ${Math.round(timeoutMs / 1000)}s` });
42
+ }, timeoutMs);
43
+ conn.on("connect", () => conn.write(JSON.stringify({ manager: "apt", packages }) + "\n"));
44
+ conn.on("data", (c) => (buf += c.toString("utf8")));
45
+ conn.on("end", () => {
46
+ clearTimeout(timer);
47
+ try {
48
+ resolve(JSON.parse(buf.trim()));
49
+ }
50
+ catch {
51
+ resolve({ ok: false, error: "the install helper sent no answer" });
52
+ }
53
+ });
54
+ conn.on("error", (err) => {
55
+ clearTimeout(timer);
56
+ resolve({ ok: false, error: `the install helper is not reachable (${err.message})` });
57
+ });
58
+ });
59
+ }
60
+ export function buildInstallTool(opts) {
61
+ const installed = [];
62
+ const text = (t, isError = false) => ({ content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) });
63
+ const install = tool("install", "Install a program or library this step needs and does not have: Debian packages with manager \"apt\" (ffmpeg, imagemagick, tesseract-ocr, libcairo2-dev, build-essential), Python packages with \"pip\", Node packages with \"npm\". Use this instead of apt-get, pip or npm in Bash — you are not root and those would fail. What you install is usable by your next command (on PATH, importable) and lasts for this step. Name real packages only; versions as the manager writes them (\"pandas>=2\", \"sharp@^0.34\").", {
64
+ manager: z.enum(["apt", "pip", "npm"]).describe("apt for Debian programs and system libraries, pip for Python, npm for Node"),
65
+ packages: z.array(z.string()).min(1).max(30).describe("Package names, e.g. [\"ffmpeg\"] or [\"manim\", \"numpy<2\"]"),
66
+ }, async ({ manager, packages }) => {
67
+ const names = packages.map((p) => String(p).trim()).filter(Boolean);
68
+ const bad = names.filter((p) => !PATTERNS[manager].test(p));
69
+ if (bad.length)
70
+ return text(`Not ${manager} package names: ${bad.join(", ")}. Nothing was installed.`, true);
71
+ const t0 = Date.now();
72
+ let how = "";
73
+ if (manager === "apt") {
74
+ if (!opts.socket || !fs.existsSync(opts.socket)) {
75
+ return text("Debian packages can only be installed inside a run container, and this step is not running in one. Nothing was installed.", true);
76
+ }
77
+ const r = await askHelper(opts.socket, names);
78
+ if (!r.ok) {
79
+ opts.emit("error", `install: apt ${names.join(" ")} — ${String(r.error).split("\n")[0]}`);
80
+ return text(r.error ?? "apt failed", true);
81
+ }
82
+ how = `${r.mode ?? "apt"}, ${r.seconds ?? Math.round((Date.now() - t0) / 1000)}s`;
83
+ }
84
+ else if (manager === "pip") {
85
+ const target = path.join(opts.dir, "py");
86
+ fs.mkdirSync(target, { recursive: true });
87
+ const uv = findUv();
88
+ const res = uv
89
+ ? spawnSync(uv, ["pip", "install", "-q", "--python", opts.python, "--target", target, ...names], {
90
+ env: { ...opts.env, UV_CACHE_DIR: path.join(opts.cacheRoot, ".uv-cache"), UV_LINK_MODE: "copy", UV_NO_PROGRESS: "1" },
91
+ encoding: "utf8",
92
+ timeout: 600_000,
93
+ })
94
+ : spawnSync(opts.python, ["-m", "pip", "install", "-q", "--disable-pip-version-check", "--target", target, ...names], {
95
+ env: opts.env,
96
+ encoding: "utf8",
97
+ timeout: 600_000,
98
+ });
99
+ if (res.status !== 0) {
100
+ const msg = summarizeInstallFailure(uv ? "uv pip install" : "pip install", `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? res.error.message : ""}`);
101
+ opts.emit("error", `install: pip ${names.join(" ")} — ${msg.split("\n")[0]}`);
102
+ return text(msg, true);
103
+ }
104
+ how = `pip, ${Math.round((Date.now() - t0) / 1000)}s`;
105
+ }
106
+ else {
107
+ const prefix = path.join(opts.dir, "npm");
108
+ fs.mkdirSync(prefix, { recursive: true });
109
+ const res = spawnSync("npm", ["install", "--no-fund", "--no-audit", "--no-progress", "--prefix", prefix, ...names], {
110
+ env: { ...opts.env, npm_config_cache: path.join(opts.cacheRoot, ".npm-cache") },
111
+ encoding: "utf8",
112
+ timeout: 600_000,
113
+ });
114
+ if (res.status !== 0) {
115
+ const msg = summarizeInstallFailure("npm install", `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? res.error.message : ""}`);
116
+ opts.emit("error", `install: npm ${names.join(" ")} — ${msg.split("\n")[0]}`);
117
+ return text(msg, true);
118
+ }
119
+ how = `npm, ${Math.round((Date.now() - t0) / 1000)}s`;
120
+ }
121
+ installed.push(...names.map((n) => `${manager}:${n}`));
122
+ opts.emit("info", `install: ${manager} ${names.join(" ")} — ready (${how}). To have it before the step starts: runtime: ${RUNTIME_KEY[manager]}: [${names.join(", ")}]`);
123
+ return text(`Installed ${names.join(", ")} (${manager}). Usable now in this step — run it or import it directly.`);
124
+ });
125
+ return {
126
+ server: createSdkMcpServer({ name: "foldrun_install", version: "1.0.0", tools: [install] }),
127
+ toolName: INSTALL_TOOL,
128
+ /** What this step installed, `manager:name`, for the run record. */
129
+ installed: () => [...installed],
130
+ /** The handler, for tests. */
131
+ call: (args) => install.handler(args, {}),
132
+ };
133
+ }
package/dist/src/mail.js CHANGED
@@ -125,7 +125,9 @@ export function readUnsubscribeToken(token, now = Date.now()) {
125
125
  if (!payload || !sig || extra !== undefined)
126
126
  return { error: "invalid token", status: 401 };
127
127
  const want = sign(payload);
128
- if (sig.length !== want.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want))) {
128
+ // Only base64url reaches the compare: a multibyte character would make the
129
+ // byte buffers differ in length, and timingSafeEqual throws on that.
130
+ if (!/^[A-Za-z0-9_-]+$/.test(sig) || sig.length !== want.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want))) {
129
131
  return { error: "invalid token", status: 401 };
130
132
  }
131
133
  let raw;
@@ -0,0 +1,30 @@
1
+ export type ModelCredentialKind = "api-key" | "auth-token" | "claude-login" | "none";
2
+ export interface ModelCredential {
3
+ kind: ModelCredentialKind;
4
+ /** What the run record and the CLI say: which credential, by its variable. */
5
+ label: string;
6
+ /** The variables the step's Claude Code is started with. */
7
+ env: Record<string, string>;
8
+ }
9
+ export declare function resolveModelCredential(source?: Record<string, string | undefined>): ModelCredential;
10
+ /** The line a step's trail carries, and what goes wrong when there is none. */
11
+ export declare function credentialLine(c: ModelCredential): {
12
+ type: "info" | "error";
13
+ text: string;
14
+ };
15
+ /** The account vault names the account's own model key lives under. */
16
+ export declare const ACCOUNT_MODEL_SECRETS: {
17
+ readonly provider: "FOLDRUN_MODEL_PROVIDER";
18
+ readonly key: "FOLDRUN_MODEL_API_KEY";
19
+ readonly baseUrl: "FOLDRUN_MODEL_BASE_URL";
20
+ readonly format: "FOLDRUN_MODEL_FORMAT";
21
+ };
22
+ /** A provider: block built from the account's own model secrets, or
23
+ * undefined when it has set no key. `read` is the account-scope secret
24
+ * reader (getSecret without a workspace). */
25
+ export declare function accountModelBlock(read: (name: string) => string | null): Record<string, unknown> | undefined;
26
+ /** A Claude login token (`claude setup-token`, sk-ant-oat…), not an API key.
27
+ * Anthropic's terms do not let a third-party product run customers on
28
+ * claude.ai logins, so on a hosted platform only the operator's own
29
+ * accounts may; a customer's key must be an API key. */
30
+ export declare function isClaudeLoginToken(value: string | null | undefined): boolean;
@@ -0,0 +1,98 @@
1
+ // Which Anthropic credential a step runs on, decided once and said on the
2
+ // record — never whatever happens to be signed in on the machine.
3
+ //
4
+ // In order: an API key (ANTHROPIC_API_KEY), a bearer for a gateway
5
+ // (ANTHROPIC_AUTH_TOKEN), then a Claude login token (CLAUDE_CODE_OAUTH_TOKEN,
6
+ // made by `claude setup-token`). Before this, an in-process step was handed
7
+ // none of them — the host allowlist drops credentials — so Claude Code fell
8
+ // back to the machine's own claude.ai login whatever key was set: a key the
9
+ // CLI insisted on was checked and then ignored.
10
+ //
11
+ // Note on terms: Anthropic does not allow third-party products built on the
12
+ // Agent SDK to offer claude.ai login unless Anthropic has approved it. The
13
+ // login token is accepted here because the account owner chose to support
14
+ // it; offering it to customers needs that approval.
15
+ //
16
+ // What an agent's own shell sees: Claude Code keeps CLAUDE_CODE_OAUTH_TOKEN
17
+ // out of its Bash by itself; an API key is visible there, as it is when you
18
+ // run Claude Code with one in your shell. Its scrub switch
19
+ // (CLAUDE_CODE_SUBPROCESS_ENV_SCRUB) is not used: it also strips the agent's
20
+ // own declared secrets (RESEND_API_KEY, DATAFORSEO_PASSWORD) from Bash, and on
21
+ // Linux it refuses to start without bubblewrap. Script tools and shell
22
+ // `verify:` never see any of these: they start from the host allowlist.
23
+ const set = (v) => typeof v === "string" && v.trim() !== "";
24
+ export function resolveModelCredential(source = process.env) {
25
+ const base = set(source.ANTHROPIC_BASE_URL) ? { ANTHROPIC_BASE_URL: source.ANTHROPIC_BASE_URL } : {};
26
+ if (set(source.ANTHROPIC_API_KEY)) {
27
+ return {
28
+ kind: "api-key",
29
+ label: "API key (ANTHROPIC_API_KEY)",
30
+ env: { ...base, ANTHROPIC_API_KEY: source.ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN: "" },
31
+ };
32
+ }
33
+ if (set(source.ANTHROPIC_AUTH_TOKEN)) {
34
+ return {
35
+ kind: "auth-token",
36
+ label: `bearer token (ANTHROPIC_AUTH_TOKEN)${base.ANTHROPIC_BASE_URL ? ` to ${base.ANTHROPIC_BASE_URL}` : ""}`,
37
+ env: { ...base, ANTHROPIC_AUTH_TOKEN: source.ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY: "", CLAUDE_CODE_OAUTH_TOKEN: "" },
38
+ };
39
+ }
40
+ if (set(source.CLAUDE_CODE_OAUTH_TOKEN)) {
41
+ return {
42
+ kind: "claude-login",
43
+ label: "Claude login token (CLAUDE_CODE_OAUTH_TOKEN)",
44
+ env: { ...base, CLAUDE_CODE_OAUTH_TOKEN: source.CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY: "" },
45
+ };
46
+ }
47
+ return { kind: "none", label: "none set", env: {} };
48
+ }
49
+ /** The line a step's trail carries, and what goes wrong when there is none. */
50
+ export function credentialLine(c) {
51
+ if (c.kind !== "none")
52
+ return { type: "info", text: `model credential: ${c.label}` };
53
+ return {
54
+ type: "error",
55
+ text: "model credential: none set — Claude Code will fall back to whatever login this machine has, if any. " +
56
+ "Set ANTHROPIC_API_KEY, or CLAUDE_CODE_OAUTH_TOKEN from `claude setup-token`",
57
+ };
58
+ }
59
+ // ---------------------------------------------------------------- the account's own key
60
+ //
61
+ // An account brings its own model key without writing a provider: block:
62
+ // three secrets in the ACCOUNT vault (set from Settings → Model, the API's
63
+ // /api/account/model, or `foldrun model set`). A provider: block at any
64
+ // level still wins; this is what a step falls to when none names one, before
65
+ // the platform's own credential.
66
+ /** The account vault names the account's own model key lives under. */
67
+ export const ACCOUNT_MODEL_SECRETS = {
68
+ provider: "FOLDRUN_MODEL_PROVIDER",
69
+ key: "FOLDRUN_MODEL_API_KEY",
70
+ baseUrl: "FOLDRUN_MODEL_BASE_URL",
71
+ format: "FOLDRUN_MODEL_FORMAT",
72
+ };
73
+ /** A provider: block built from the account's own model secrets, or
74
+ * undefined when it has set no key. `read` is the account-scope secret
75
+ * reader (getSecret without a workspace). */
76
+ export function accountModelBlock(read) {
77
+ const key = read(ACCOUNT_MODEL_SECRETS.key);
78
+ if (!key)
79
+ return undefined;
80
+ const block = { token: `\${${ACCOUNT_MODEL_SECRETS.key}}` };
81
+ const name = read(ACCOUNT_MODEL_SECRETS.provider);
82
+ const baseUrl = read(ACCOUNT_MODEL_SECRETS.baseUrl);
83
+ const format = read(ACCOUNT_MODEL_SECRETS.format);
84
+ if (name)
85
+ block.name = name;
86
+ if (baseUrl)
87
+ block.base_url = baseUrl;
88
+ if (format)
89
+ block.format = format;
90
+ return block;
91
+ }
92
+ /** A Claude login token (`claude setup-token`, sk-ant-oat…), not an API key.
93
+ * Anthropic's terms do not let a third-party product run customers on
94
+ * claude.ai logins, so on a hosted platform only the operator's own
95
+ * accounts may; a customer's key must be an API key. */
96
+ export function isClaudeLoginToken(value) {
97
+ return typeof value === "string" && /^sk-ant-oat\d*-/.test(value.trim());
98
+ }
@@ -0,0 +1,17 @@
1
+ /** Refused for want of a usable key; `status` is what an HTTP layer answers. */
2
+ export declare class ModelKeyError extends Error {
3
+ readonly status: number;
4
+ constructor(message: string, status?: number);
5
+ }
6
+ export declare const NO_MODEL_KEY: string;
7
+ /**
8
+ * `platform` is a function: the caller's own way to build the platform's env
9
+ * (a step's, or the SDK's host env), called only when this account may use
10
+ * it. `format` says what the endpoint speaks — a caller with no translator
11
+ * refuses anything but anthropic.
12
+ */
13
+ export declare function accountModelEnv(tenant: string, workspace: string, platformEnv: () => Record<string, string | undefined>): {
14
+ env: Record<string, string | undefined>;
15
+ supply: "provider" | "platform";
16
+ format: string;
17
+ };
@@ -0,0 +1,57 @@
1
+ // The model environment for work an ACCOUNT asks for outside a step: an eval's
2
+ // judge, the dashboard's "draft a flow with AI". Resolved the way a step
3
+ // resolves it — the workspace's provider: block, else the account's, else the
4
+ // account's own key from Settings → Model — and only then the platform's own
5
+ // credential, which a hosted platform keeps for its operator's accounts
6
+ // (platform.platformKeyAllowed). Every other account brings an API key;
7
+ // asking for the platform's here is refused, not quietly billed to it.
8
+ import { accountDir, workspaceDir, parseProvider, providerEnvFor } from "./store.js";
9
+ import { readAgentsMd } from "./runner.js";
10
+ import { getSecret } from "./secrets.js";
11
+ import { platform } from "./platform.js";
12
+ import { accountModelBlock, isClaudeLoginToken } from "./model-credential.js";
13
+ /** Refused for want of a usable key; `status` is what an HTTP layer answers. */
14
+ export class ModelKeyError extends Error {
15
+ status;
16
+ constructor(message, status = 402) {
17
+ super(message);
18
+ this.status = status;
19
+ }
20
+ }
21
+ export const NO_MODEL_KEY = "no model key: this account runs on its own API key, and none is set — add one in Settings → Model " +
22
+ "(or `foldrun model set <provider> --key …`), or write a provider: block in AGENTS.md";
23
+ /**
24
+ * `platform` is a function: the caller's own way to build the platform's env
25
+ * (a step's, or the SDK's host env), called only when this account may use
26
+ * it. `format` says what the endpoint speaks — a caller with no translator
27
+ * refuses anything but anthropic.
28
+ */
29
+ export function accountModelEnv(tenant, workspace, platformEnv) {
30
+ const raw = readAgentsMd(workspaceDir(tenant, workspace))?.data?.provider ??
31
+ readAgentsMd(accountDir(tenant))?.data?.provider ??
32
+ accountModelBlock((name) => getSecret(tenant, name)?.value ?? null);
33
+ const spec = parseProvider(raw);
34
+ if (!spec?.baseUrl) {
35
+ if (!platform.platformKeyAllowed(tenant))
36
+ throw new ModelKeyError(NO_MODEL_KEY);
37
+ return { env: platformEnv(), supply: "platform", format: "anthropic" };
38
+ }
39
+ const sub = (text) => text.replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => getSecret(tenant, name, workspace)?.value ?? whole);
40
+ const token = sub(spec.token);
41
+ if (token.includes("${"))
42
+ throw new ModelKeyError("the provider's token secret is not set — `foldrun secrets` sets it", 422);
43
+ if (!platform.platformKeyAllowed(tenant) && isClaudeLoginToken(token)) {
44
+ throw new ModelKeyError("model key: a Claude login token (sk-ant-oat…) cannot be used here — use an API key from your provider", 422);
45
+ }
46
+ return {
47
+ env: providerEnvFor({
48
+ baseUrl: spec.baseUrl,
49
+ token,
50
+ auth: spec.auth,
51
+ models: spec.models,
52
+ headers: Object.fromEntries(Object.entries(spec.headers).map(([k, v]) => [k, sub(v)])),
53
+ }),
54
+ supply: "provider",
55
+ format: spec.format,
56
+ };
57
+ }
@@ -0,0 +1,67 @@
1
+ import { type DeployFile } from "./store.ts";
2
+ export type PackageKind = "workspace" | "flow" | "agent";
3
+ export interface PackageManifest {
4
+ format: "foldrun-package";
5
+ version: 1;
6
+ kind: PackageKind;
7
+ /** The workspace's, flow's or agent's name. */
8
+ name: string;
9
+ /** The workspace it was exported from. */
10
+ workspace: string;
11
+ exportedAt: string;
12
+ files: string[];
13
+ }
14
+ export interface Package {
15
+ manifest: PackageManifest;
16
+ files: DeployFile[];
17
+ /** Entries in the zip that are not authored source, left out (readPackage). */
18
+ skipped?: string[];
19
+ }
20
+ /** An error the API layer turns into a status rather than a 500. */
21
+ export declare class PackageError extends Error {
22
+ readonly status: number;
23
+ constructor(message: string, status: number);
24
+ }
25
+ export declare const MANIFEST_FILE = "foldrun-package.json";
26
+ export declare function isPackagedPath(rel: string): boolean;
27
+ export declare function packageOf(tenant: string, workspace: string, kind: PackageKind, name?: string): Package;
28
+ export declare function packageZip(pkg: Package): Buffer;
29
+ /** What the download is called: `<workspace>.zip`, `<workspace>-flow-<name>.zip`. */
30
+ export declare function packageFilename(m: PackageManifest): string;
31
+ export declare function readPackage(buf: Buffer): Package;
32
+ export interface ImportPlan {
33
+ kind: PackageKind;
34
+ name: string;
35
+ /** Where it came from, as the package says. */
36
+ from: string;
37
+ workspace: string;
38
+ /** True when the workspace does not exist yet and the import makes it. */
39
+ creates: boolean;
40
+ added: string[];
41
+ /** Files that exist with different text — written only with `overwrite`. */
42
+ overwritten: string[];
43
+ unchanged: string[];
44
+ /** In the zip, not written: memory, state, .gitignore, anything outside
45
+ * the authored folders. */
46
+ skipped: string[];
47
+ /** What the imported files name that the target cannot provide. The import
48
+ * still happens — a missing secret is set in a minute. */
49
+ needs: {
50
+ secrets: string[];
51
+ tools: string[];
52
+ skills: string[];
53
+ scripts: string[];
54
+ agents: string[];
55
+ flows: string[];
56
+ };
57
+ }
58
+ export interface ImportResult extends ImportPlan {
59
+ written: string[];
60
+ revision: string | null;
61
+ }
62
+ export declare function planImport(tenant: string, workspace: string, pkg: Package): ImportPlan;
63
+ export declare function applyImport(tenant: string, workspace: string, pkg: Package, opts?: {
64
+ overwrite?: boolean;
65
+ by?: string;
66
+ expect?: "create" | "merge";
67
+ }): ImportResult;