@foldrun/core 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/agent-import.d.ts +21 -0
- package/dist/src/agent-import.js +116 -0
- package/dist/src/api-tools.js +14 -2
- package/dist/src/approvals.d.ts +8 -1
- package/dist/src/approvals.js +10 -5
- package/dist/src/completions.js +1 -0
- package/dist/src/confine.d.ts +1 -0
- package/dist/src/confine.js +33 -4
- package/dist/src/deploy.d.ts +9 -0
- package/dist/src/deploy.js +31 -3
- package/dist/src/evals.js +22 -5
- package/dist/src/hooks.d.ts +114 -0
- package/dist/src/hooks.js +370 -0
- package/dist/src/host-env.d.ts +14 -0
- package/dist/src/host-env.js +26 -0
- package/dist/src/install-helper.d.ts +17 -0
- package/dist/src/install-helper.js +134 -0
- package/dist/src/install-tool.d.ts +128 -0
- package/dist/src/install-tool.js +133 -0
- package/dist/src/kinds.d.ts +1 -1
- package/dist/src/kinds.js +26 -0
- package/dist/src/library.d.ts +1 -1
- package/dist/src/library.js +17 -1
- package/dist/src/mail.js +1 -1
- package/dist/src/model-credential.d.ts +16 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/model-env.d.ts +17 -0
- package/dist/src/model-env.js +57 -0
- package/dist/src/notify.d.ts +8 -0
- package/dist/src/notify.js +10 -1
- package/dist/src/package.d.ts +67 -0
- package/dist/src/package.js +594 -0
- package/dist/src/platform.d.ts +5 -0
- package/dist/src/platform.js +1 -0
- package/dist/src/provider-health.js +3 -0
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +21 -1
- package/dist/src/run-container.d.ts +10 -0
- package/dist/src/run-container.js +72 -14
- package/dist/src/runner.d.ts +205 -0
- package/dist/src/runner.js +273 -36
- package/dist/src/runtime.d.ts +77 -0
- package/dist/src/runtime.js +230 -5
- package/dist/src/script-tools.d.ts +2 -0
- package/dist/src/script-tools.js +3 -1
- package/dist/src/secrets.d.ts +5 -0
- package/dist/src/secrets.js +37 -6
- package/dist/src/starter.d.ts +12 -0
- package/dist/src/starter.js +38 -4
- package/dist/src/step-exec.d.ts +21 -2
- package/dist/src/step-exec.js +165 -7
- package/dist/src/storage.d.ts +7 -1
- package/dist/src/storage.js +14 -1
- package/dist/src/store.d.ts +31 -1
- package/dist/src/store.js +90 -8
- package/dist/src/tool-names.d.ts +3 -1
- package/dist/src/tool-names.js +4 -2
- package/dist/src/tool-test.js +8 -1
- package/dist/src/zip.d.ts +17 -0
- package/dist/src/zip.js +184 -0
- package/index.ts +3 -0
- package/package.json +18 -1
- package/src/agent-import.ts +154 -0
- package/src/api-tools.ts +16 -1
- package/src/approvals.ts +10 -3
- package/src/completions.ts +1 -0
- package/src/confine.ts +36 -5
- package/src/deploy.ts +28 -3
- package/src/evals.ts +21 -3
- package/src/hooks.ts +406 -0
- package/src/host-env.ts +27 -0
- package/src/install-helper.ts +143 -0
- package/src/install-tool.ts +154 -0
- package/src/kinds.ts +28 -0
- package/src/library.ts +18 -2
- package/src/mail.ts +1 -1
- package/src/model-credential.ts +41 -0
- package/src/model-env.ts +66 -0
- package/src/notify.ts +11 -1
- package/src/package.ts +664 -0
- package/src/platform.ts +6 -0
- package/src/provider-health.ts +3 -0
- package/src/providers.ts +27 -1
- package/src/run-container.ts +75 -14
- package/src/runner.ts +279 -36
- package/src/runtime.ts +235 -5
- package/src/script-tools.ts +5 -1
- package/src/secrets.ts +34 -6
- package/src/starter.ts +44 -4
- package/src/step-exec.ts +171 -11
- package/src/storage.ts +13 -0
- package/src/store.ts +89 -7
- package/src/tool-names.ts +4 -2
- package/src/tool-test.ts +8 -1
- package/src/zip.ts +189 -0
package/src/provider-health.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// It reads the same provider block a run reads, resolved the same way, so a
|
|
19
19
|
// pass here means a run would reach the same place with the same key.
|
|
20
20
|
|
|
21
|
+
import { accountModelBlock } from "./model-credential.ts";
|
|
21
22
|
import fs from "node:fs";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import matter from "gray-matter";
|
|
@@ -236,6 +237,8 @@ export function providersInUse(tenant: string): ProviderInUse[] {
|
|
|
236
237
|
};
|
|
237
238
|
|
|
238
239
|
add(frontmatterOf(path.join(accountDir(tenant), "AGENTS.md")).provider, null, "account");
|
|
240
|
+
// The account's own key from Settings → Model, which needs no block.
|
|
241
|
+
add(accountModelBlock((name) => getSecret(tenant, name)?.value ?? null), null, "account (Settings → Model)");
|
|
239
242
|
for (const ws of safeList(() => listWorkspaces(tenant))) {
|
|
240
243
|
const dir = workspaceDir(tenant, ws.name);
|
|
241
244
|
add(frontmatterOf(path.join(dir, "AGENTS.md")).provider, ws.name, ws.name);
|
package/src/providers.ts
CHANGED
|
@@ -591,6 +591,11 @@ export interface BrowseSettings {
|
|
|
591
591
|
ignore_https_errors?: boolean;
|
|
592
592
|
/** The NAME of a vault secret whose value encrypts state= logins at rest. */
|
|
593
593
|
state_key?: string;
|
|
594
|
+
/** A saved login every call uses by default: loaded from
|
|
595
|
+
* workspace/state/browser/<name>.json and saved back when the call ends.
|
|
596
|
+
* Beside `cookies:` (with `state_key:`), the secret seeds it once and the
|
|
597
|
+
* session the site keeps rotating is what later runs load. */
|
|
598
|
+
state?: string;
|
|
594
599
|
/** true records every call, so the run page always has the recording to play. */
|
|
595
600
|
video?: boolean;
|
|
596
601
|
/** false stops the tool streaming its page to the run page while the step
|
|
@@ -685,7 +690,7 @@ export function browseEngineRefusal(engine: string, what: string): string | null
|
|
|
685
690
|
}
|
|
686
691
|
const BROWSE_SETTING_KEYS = [
|
|
687
692
|
"engine", "user_agent", "device", "locale", "timezone", "cookies", "cookie_domain", "storage", "storage_origin", "identities", "headless", "version", "live",
|
|
688
|
-
"allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key",
|
|
693
|
+
"allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key", "state",
|
|
689
694
|
"video", "live_view", "vendor_session",
|
|
690
695
|
// Deprecated spelling of vendor_session (read, and warned about).
|
|
691
696
|
"session",
|
|
@@ -964,6 +969,11 @@ export function readBrowseSettings(raw: unknown): { settings: BrowseSettings; re
|
|
|
964
969
|
if (list.length) settings[k] = list;
|
|
965
970
|
continue;
|
|
966
971
|
}
|
|
972
|
+
if (k === "state") {
|
|
973
|
+
if (typeof v !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(v)) return { settings, rest: left(rest), error: "web.browse.state is a plain name (letters, digits, dot, dash, underscore), like youtube — the login is kept in workspace/state/browser/<name>.json." };
|
|
974
|
+
settings.state = v;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
967
977
|
if (k === "state_key") {
|
|
968
978
|
if (typeof v !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(v)) return { settings, rest: left(rest), error: "web.browse.state_key must be the NAME of a vault secret (CAPITALS), never the key — store it with `foldrun secrets set NAME` and declare it under secrets:." };
|
|
969
979
|
settings.state_key = v;
|
|
@@ -1033,6 +1043,22 @@ export function readBrowseSettings(raw: unknown): { settings: BrowseSettings; re
|
|
|
1033
1043
|
};
|
|
1034
1044
|
}
|
|
1035
1045
|
|
|
1046
|
+
// A secret's cookies seeding a saved login: the login then sits in state/,
|
|
1047
|
+
// a workspace file, so it must be encrypted with a key from the vault.
|
|
1048
|
+
if (settings.state && settings.cookies && !settings.state_key) {
|
|
1049
|
+
return {
|
|
1050
|
+
settings,
|
|
1051
|
+
rest: left(rest),
|
|
1052
|
+
error:
|
|
1053
|
+
`web.browse.state beside cookies needs web.browse.state_key — the secret's session would be saved ` +
|
|
1054
|
+
`to state/ where any step can read it. Store a key (\`foldrun secrets set BROWSER_STATE_KEY\`), ` +
|
|
1055
|
+
`write \`state_key: BROWSER_STATE_KEY\` and declare it under secrets:.`,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (settings.state && settings.storage) {
|
|
1059
|
+
return { settings, rest: left(rest), error: "web.browse.state cannot be combined with storage: a secret's tokens would be written to state/." };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1036
1062
|
// Storage is per origin, and seeding it into the wrong one would hand a
|
|
1037
1063
|
// site another site's signed-in state — the same mistake `cookie_domain`
|
|
1038
1064
|
// exists to prevent, one storage area over.
|
package/src/run-container.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// The runner image bundles @foldrun/core itself, so the driver runs the same
|
|
19
19
|
// executeStep the server would — one implementation, two homes.
|
|
20
20
|
|
|
21
|
+
import { isHookRecord, type HookRecord } from "./hooks.ts";
|
|
21
22
|
import fs from "node:fs";
|
|
22
23
|
import os from "node:os";
|
|
23
24
|
import path from "node:path";
|
|
@@ -28,7 +29,7 @@ import { isPlatformPath, type ApiSpec, type Effort } from "./store.ts";
|
|
|
28
29
|
import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.ts";
|
|
29
30
|
import type { EventExtra } from "./step-exec.ts";
|
|
30
31
|
import { isFileValue, fileContent } from "./secrets.ts";
|
|
31
|
-
import { safeTenantSegment, type RuntimeSpec } from "./runtime.ts";
|
|
32
|
+
import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET, type RuntimeSpec } from "./runtime.ts";
|
|
32
33
|
import type { ScriptSpec } from "./script-tools.ts";
|
|
33
34
|
import type { ConsultSpec } from "./agent-tools.ts";
|
|
34
35
|
import type { SubagentSpec } from "./subagents.ts";
|
|
@@ -77,6 +78,9 @@ export interface ContainerStepInput {
|
|
|
77
78
|
/** `limits:` resolved host-side, and the SDK-name → foldrun-name map the
|
|
78
79
|
* count reads (limits.ts) — enforced in the pod by the same hook. */
|
|
79
80
|
limits?: Record<string, number>;
|
|
81
|
+
/** `hooks:` names, resolved inside the sandbox against its own roots. */
|
|
82
|
+
hookNames?: string[];
|
|
83
|
+
hookBase?: Record<string, unknown>;
|
|
80
84
|
toolOwners?: Record<string, string>;
|
|
81
85
|
/** tools: [search] — the directories to search, as paths INSIDE the
|
|
82
86
|
* container (/workspace/…, /library/…). */
|
|
@@ -172,7 +176,9 @@ const cli = () => process.env.FOLDRUN_CONTAINER_CLI ?? "docker";
|
|
|
172
176
|
// there: everything platform-owned (store.ts#isPlatformPath — also never
|
|
173
177
|
// copied in), plus knowledge/, which goes in for reading and is dropped on
|
|
174
178
|
// the way out, and .git/.
|
|
175
|
-
|
|
179
|
+
// hooks/: a step must not rewrite the code that guards it — for this run (the
|
|
180
|
+
// code is read into memory before the model starts, hooks.ts) or the next.
|
|
181
|
+
const DENY_BACK = ["knowledge/", "hooks/", ".git/"];
|
|
176
182
|
// A .git ANYWHERE in the tree, not just at the root. A step that clones a
|
|
177
183
|
// repository into a shared area (files/site, say) was shipping the entire
|
|
178
184
|
// object store back to the workspace, where the harvest then filed thousands
|
|
@@ -293,18 +299,37 @@ export function mergeAppends(was: Buffer | string | null, host: Buffer, next: Bu
|
|
|
293
299
|
let common = 0;
|
|
294
300
|
const max = Math.min(h.length, n.length);
|
|
295
301
|
while (common < max && h.charCodeAt(common) === n.charCodeAt(common)) common++;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
// One pass over the shared prefix. Rebuilding and hashing the prefix at
|
|
303
|
+
// every line boundary, from the longest down, was quadratic: on 6 Oct 2026
|
|
304
|
+
// a conflicted file of a few MB held the worker's event loop for minutes,
|
|
305
|
+
// the liveness probe killed it every 4.5 minutes, and every step on the box
|
|
306
|
+
// lost its model connection.
|
|
307
|
+
let end = -1;
|
|
308
|
+
if (typeof was === "string") {
|
|
309
|
+
const run = crypto.createHash("sha256");
|
|
310
|
+
if (run.copy().digest("hex") === was) end = 0;
|
|
311
|
+
let from = 0;
|
|
312
|
+
for (let nl = h.indexOf("\n"); nl !== -1 && nl < common; nl = h.indexOf("\n", nl + 1)) {
|
|
313
|
+
run.update(h.slice(from, nl + 1), "utf8");
|
|
314
|
+
from = nl + 1;
|
|
315
|
+
if (run.copy().digest("hex") === was) end = from;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
// The base's bytes are known, so it can only end at their length.
|
|
319
|
+
const base = was.toString("utf8");
|
|
320
|
+
if (
|
|
321
|
+
base.length <= common &&
|
|
322
|
+
(base.length === 0 || base.endsWith("\n")) &&
|
|
323
|
+
h.startsWith(base) &&
|
|
324
|
+
Buffer.from(base, "utf8").equals(was)
|
|
325
|
+
)
|
|
326
|
+
end = base.length;
|
|
306
327
|
}
|
|
307
|
-
return null;
|
|
328
|
+
if (end < 0) return null;
|
|
329
|
+
const hostAdded = h.slice(end);
|
|
330
|
+
const stepAdded = n.slice(end);
|
|
331
|
+
const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
|
|
332
|
+
return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
|
|
308
333
|
}
|
|
309
334
|
|
|
310
335
|
export function applyContainerChanges(
|
|
@@ -408,6 +433,13 @@ export function applyContainerChanges(
|
|
|
408
433
|
*/
|
|
409
434
|
export const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
|
|
410
435
|
|
|
436
|
+
/** Starts the install helper as root, in the background, before the drop to
|
|
437
|
+
* the agent user: the root half of the `install` tool (install-helper.ts).
|
|
438
|
+
* The agent reaches it only through the socket; it installs only validated
|
|
439
|
+
* Debian package names, and dies with the container. */
|
|
440
|
+
export const INSTALL_HELPER_SH = `FOLDRUN_INSTALL_SOCKET=${INSTALL_SOCKET}; export FOLDRUN_INSTALL_SOCKET
|
|
441
|
+
node /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js "$FOLDRUN_INSTALL_SOCKET" ${RUNTIME_CACHE} >/dev/null 2>&1 &`;
|
|
442
|
+
|
|
411
443
|
/**
|
|
412
444
|
* Where the platform's shared runtime layer is mounted, read-only, in a run
|
|
413
445
|
* pod — and read-write, one entry at a time, in the pod that builds an entry
|
|
@@ -548,6 +580,25 @@ try {
|
|
|
548
580
|
// meet the packages its runtime declared, not the image's bare python.
|
|
549
581
|
Object.assign(env, runtime.env);
|
|
550
582
|
|
|
583
|
+
// install: what the agent finds it needs mid-step. apt goes through the
|
|
584
|
+
// root start-up's helper (install-helper.ts); pip and npm land in a
|
|
585
|
+
// directory of this container's own, on the paths from the first turn.
|
|
586
|
+
const { buildInstallTool, INSTALL_TOOL } = await import("@foldrun/core/install-tool");
|
|
587
|
+
const { stepInstallEnv, INSTALL_SOCKET } = await import("@foldrun/core/runtime");
|
|
588
|
+
const installDir = "/home/agent/.foldrun/step";
|
|
589
|
+
const installing = (input.allowed ?? []).includes(INSTALL_TOOL);
|
|
590
|
+
if (installing) Object.assign(env, stepInstallEnv(installDir, env));
|
|
591
|
+
const install = installing
|
|
592
|
+
? buildInstallTool({
|
|
593
|
+
dir: installDir,
|
|
594
|
+
socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,
|
|
595
|
+
cacheRoot: "${RUNTIME_CACHE}",
|
|
596
|
+
python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + "/bin/python" : "python3",
|
|
597
|
+
env,
|
|
598
|
+
emit,
|
|
599
|
+
})
|
|
600
|
+
: { server: null };
|
|
601
|
+
|
|
551
602
|
// API and script tools, rebuilt in here from their specs. Secrets were
|
|
552
603
|
// substituted into API headers before the input crossed the boundary, so
|
|
553
604
|
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
@@ -598,6 +649,7 @@ try {
|
|
|
598
649
|
...(history.server ? { foldrun_history: history.server } : {}),
|
|
599
650
|
...(desks.server ? { foldrun_desks: desks.server } : {}),
|
|
600
651
|
...(ask.server ? { foldrun_ask: ask.server } : {}),
|
|
652
|
+
...(install.server ? { foldrun_install: install.server } : {}),
|
|
601
653
|
...input.mcpServers,
|
|
602
654
|
},
|
|
603
655
|
...(inbox ? { inbox } : {}),
|
|
@@ -613,6 +665,7 @@ try {
|
|
|
613
665
|
schema: input.schema,
|
|
614
666
|
maxTurns: input.maxTurns,
|
|
615
667
|
...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),
|
|
668
|
+
...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),
|
|
616
669
|
// Slim browsing: the platform put this step on the slim image to browse
|
|
617
670
|
// through the account's pod, and named the file the web tool logs its
|
|
618
671
|
// reconnects to. A lost pod stops the step; what it wrote decides what
|
|
@@ -893,7 +946,14 @@ RUN node -e ' \\
|
|
|
893
946
|
FROM browsers AS full
|
|
894
947
|
${CORE_INSTALL}`;
|
|
895
948
|
|
|
949
|
+
// runtime: system: — apt as root, before the drop (runtime.ts systemInstallSh).
|
|
950
|
+
// set -e is off around it: a failed install is reported by the driver, which
|
|
951
|
+
// reads the status file and fails the step with apt's own words.
|
|
896
952
|
const ENTRY = `#!/bin/sh
|
|
953
|
+
set +e
|
|
954
|
+
${SYSTEM_FROM_INPUT_SH}
|
|
955
|
+
${systemInstallSh(RUNTIME_CACHE)}
|
|
956
|
+
${INSTALL_HELPER_SH}
|
|
897
957
|
set -e
|
|
898
958
|
chown -R agent:agent /workspace /library /opt/runner/job
|
|
899
959
|
# The runtime cache, when one is mounted. NOT recursive: a warm cache is tens
|
|
@@ -1054,7 +1114,7 @@ export function buildFailureSummary(log: string, opts: { tail?: number; maxLines
|
|
|
1054
1114
|
|
|
1055
1115
|
export function parseDriverLine(
|
|
1056
1116
|
line: string,
|
|
1057
|
-
): { e: "event"; type: "text" | "tool" | "info" | "error"; text: string; call?: string; ms?: number; err?: boolean; operator?: OperatorEvent; subagent?: string; check?: boolean } | { e: "done" } & ContainerStepOutcome | null {
|
|
1117
|
+
): { e: "event"; type: "text" | "tool" | "info" | "error"; text: string; call?: string; ms?: number; err?: boolean; operator?: OperatorEvent; subagent?: string; check?: boolean; hook?: HookRecord } | { e: "done" } & ContainerStepOutcome | null {
|
|
1058
1118
|
const trimmed = line.trim();
|
|
1059
1119
|
if (!trimmed.startsWith("{")) return null;
|
|
1060
1120
|
try {
|
|
@@ -1070,6 +1130,7 @@ export function parseDriverLine(
|
|
|
1070
1130
|
...(isOperatorEvent(parsed.operator) ? { operator: parsed.operator } : {}),
|
|
1071
1131
|
...(typeof parsed.subagent === "string" && /^[a-z0-9-]{1,64}$/.test(parsed.subagent) ? { subagent: parsed.subagent } : {}),
|
|
1072
1132
|
...(parsed.check === true ? { check: true } : {}),
|
|
1133
|
+
...(isHookRecord(parsed.hook) ? { hook: parsed.hook } : {}),
|
|
1073
1134
|
};
|
|
1074
1135
|
}
|
|
1075
1136
|
if (parsed.e === "done") {
|