@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.
- package/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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/browser-pod.d.ts +20 -1
- package/dist/src/browser-pod.js +58 -9
- 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/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +9 -0
- package/dist/src/deploy.js +31 -3
- package/dist/src/evals.js +24 -4
- package/dist/src/host-env.d.ts +14 -0
- package/dist/src/host-env.js +31 -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/mail.js +3 -1
- package/dist/src/model-credential.d.ts +30 -0
- package/dist/src/model-credential.js +98 -0
- package/dist/src/model-env.d.ts +17 -0
- package/dist/src/model-env.js +57 -0
- 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/run-container.d.ts +6 -1
- package/dist/src/run-container.js +93 -20
- package/dist/src/runner.d.ts +239 -0
- package/dist/src/runner.js +380 -72
- package/dist/src/runtime.d.ts +77 -0
- package/dist/src/runtime.js +230 -5
- package/dist/src/script-tools.d.ts +16 -0
- package/dist/src/script-tools.js +50 -8
- 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 +36 -4
- package/dist/src/step-exec.d.ts +22 -2
- package/dist/src/step-exec.js +70 -7
- package/dist/src/storage.d.ts +7 -1
- package/dist/src/storage.js +14 -1
- package/dist/src/store.d.ts +24 -0
- package/dist/src/store.js +91 -10
- package/dist/src/subagents.js +1 -1
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- 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 +4 -0
- package/package.json +1 -1
- package/src/agent-import.ts +154 -0
- package/src/api-tools.ts +16 -1
- package/src/approvals.ts +10 -3
- package/src/browser-pod.ts +46 -9
- package/src/completions.ts +1 -0
- package/src/confine.ts +36 -5
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +28 -3
- package/src/evals.ts +23 -2
- package/src/host-env.ts +32 -0
- package/src/install-helper.ts +143 -0
- package/src/install-tool.ts +154 -0
- package/src/mail.ts +3 -1
- package/src/model-credential.ts +112 -0
- package/src/model-env.ts +66 -0
- package/src/package.ts +664 -0
- package/src/platform.ts +6 -0
- package/src/provider-health.ts +3 -0
- package/src/run-container.ts +92 -19
- package/src/runner.ts +385 -72
- package/src/runtime.ts +235 -5
- package/src/script-tools.ts +48 -6
- package/src/secrets.ts +34 -6
- package/src/starter.ts +42 -4
- package/src/step-exec.ts +75 -10
- package/src/storage.ts +13 -0
- package/src/store.ts +91 -9
- package/src/subagents.ts +1 -1
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +4 -2
- package/src/tool-test.ts +8 -1
- package/src/zip.ts +189 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +3 -3
- package/templates/hello/agents/notetaker/agent.md +2 -2
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/run-container.ts
CHANGED
|
@@ -25,10 +25,10 @@ import crypto from "node:crypto";
|
|
|
25
25
|
import { spawn, spawnSync } from "node:child_process";
|
|
26
26
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
27
27
|
import { isPlatformPath, type ApiSpec, type Effort } from "./store.ts";
|
|
28
|
-
import { divertedSummary, isDivertedPath } from "./test-mode.ts";
|
|
28
|
+
import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.ts";
|
|
29
29
|
import type { EventExtra } from "./step-exec.ts";
|
|
30
30
|
import { isFileValue, fileContent } from "./secrets.ts";
|
|
31
|
-
import { safeTenantSegment, type RuntimeSpec } from "./runtime.ts";
|
|
31
|
+
import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET, type RuntimeSpec } from "./runtime.ts";
|
|
32
32
|
import type { ScriptSpec } from "./script-tools.ts";
|
|
33
33
|
import type { ConsultSpec } from "./agent-tools.ts";
|
|
34
34
|
import type { SubagentSpec } from "./subagents.ts";
|
|
@@ -293,18 +293,37 @@ export function mergeAppends(was: Buffer | string | null, host: Buffer, next: Bu
|
|
|
293
293
|
let common = 0;
|
|
294
294
|
const max = Math.min(h.length, n.length);
|
|
295
295
|
while (common < max && h.charCodeAt(common) === n.charCodeAt(common)) common++;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
// One pass over the shared prefix. Rebuilding and hashing the prefix at
|
|
297
|
+
// every line boundary, from the longest down, was quadratic: on 6 Oct 2026
|
|
298
|
+
// a conflicted file of a few MB held the worker's event loop for minutes,
|
|
299
|
+
// the liveness probe killed it every 4.5 minutes, and every step on the box
|
|
300
|
+
// lost its model connection.
|
|
301
|
+
let end = -1;
|
|
302
|
+
if (typeof was === "string") {
|
|
303
|
+
const run = crypto.createHash("sha256");
|
|
304
|
+
if (run.copy().digest("hex") === was) end = 0;
|
|
305
|
+
let from = 0;
|
|
306
|
+
for (let nl = h.indexOf("\n"); nl !== -1 && nl < common; nl = h.indexOf("\n", nl + 1)) {
|
|
307
|
+
run.update(h.slice(from, nl + 1), "utf8");
|
|
308
|
+
from = nl + 1;
|
|
309
|
+
if (run.copy().digest("hex") === was) end = from;
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// The base's bytes are known, so it can only end at their length.
|
|
313
|
+
const base = was.toString("utf8");
|
|
314
|
+
if (
|
|
315
|
+
base.length <= common &&
|
|
316
|
+
(base.length === 0 || base.endsWith("\n")) &&
|
|
317
|
+
h.startsWith(base) &&
|
|
318
|
+
Buffer.from(base, "utf8").equals(was)
|
|
319
|
+
)
|
|
320
|
+
end = base.length;
|
|
306
321
|
}
|
|
307
|
-
return null;
|
|
322
|
+
if (end < 0) return null;
|
|
323
|
+
const hostAdded = h.slice(end);
|
|
324
|
+
const stepAdded = n.slice(end);
|
|
325
|
+
const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
|
|
326
|
+
return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
|
|
308
327
|
}
|
|
309
328
|
|
|
310
329
|
export function applyContainerChanges(
|
|
@@ -408,6 +427,13 @@ export function applyContainerChanges(
|
|
|
408
427
|
*/
|
|
409
428
|
export const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
|
|
410
429
|
|
|
430
|
+
/** Starts the install helper as root, in the background, before the drop to
|
|
431
|
+
* the agent user: the root half of the `install` tool (install-helper.ts).
|
|
432
|
+
* The agent reaches it only through the socket; it installs only validated
|
|
433
|
+
* Debian package names, and dies with the container. */
|
|
434
|
+
export const INSTALL_HELPER_SH = `FOLDRUN_INSTALL_SOCKET=${INSTALL_SOCKET}; export FOLDRUN_INSTALL_SOCKET
|
|
435
|
+
node /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js "$FOLDRUN_INSTALL_SOCKET" ${RUNTIME_CACHE} >/dev/null 2>&1 &`;
|
|
436
|
+
|
|
411
437
|
/**
|
|
412
438
|
* Where the platform's shared runtime layer is mounted, read-only, in a run
|
|
413
439
|
* pod — and read-write, one entry at a time, in the pod that builds an entry
|
|
@@ -548,6 +574,25 @@ try {
|
|
|
548
574
|
// meet the packages its runtime declared, not the image's bare python.
|
|
549
575
|
Object.assign(env, runtime.env);
|
|
550
576
|
|
|
577
|
+
// install: what the agent finds it needs mid-step. apt goes through the
|
|
578
|
+
// root start-up's helper (install-helper.ts); pip and npm land in a
|
|
579
|
+
// directory of this container's own, on the paths from the first turn.
|
|
580
|
+
const { buildInstallTool, INSTALL_TOOL } = await import("@foldrun/core/install-tool");
|
|
581
|
+
const { stepInstallEnv, INSTALL_SOCKET } = await import("@foldrun/core/runtime");
|
|
582
|
+
const installDir = "/home/agent/.foldrun/step";
|
|
583
|
+
const installing = (input.allowed ?? []).includes(INSTALL_TOOL);
|
|
584
|
+
if (installing) Object.assign(env, stepInstallEnv(installDir, env));
|
|
585
|
+
const install = installing
|
|
586
|
+
? buildInstallTool({
|
|
587
|
+
dir: installDir,
|
|
588
|
+
socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,
|
|
589
|
+
cacheRoot: "${RUNTIME_CACHE}",
|
|
590
|
+
python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + "/bin/python" : "python3",
|
|
591
|
+
env,
|
|
592
|
+
emit,
|
|
593
|
+
})
|
|
594
|
+
: { server: null };
|
|
595
|
+
|
|
551
596
|
// API and script tools, rebuilt in here from their specs. Secrets were
|
|
552
597
|
// substituted into API headers before the input crossed the boundary, so
|
|
553
598
|
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
@@ -598,6 +643,7 @@ try {
|
|
|
598
643
|
...(history.server ? { foldrun_history: history.server } : {}),
|
|
599
644
|
...(desks.server ? { foldrun_desks: desks.server } : {}),
|
|
600
645
|
...(ask.server ? { foldrun_ask: ask.server } : {}),
|
|
646
|
+
...(install.server ? { foldrun_install: install.server } : {}),
|
|
601
647
|
...input.mcpServers,
|
|
602
648
|
},
|
|
603
649
|
...(inbox ? { inbox } : {}),
|
|
@@ -617,7 +663,7 @@ try {
|
|
|
617
663
|
// through the account's pod, and named the file the web tool logs its
|
|
618
664
|
// reconnects to. A lost pod stops the step; what it wrote decides what
|
|
619
665
|
// the runner does next (browser-pod.ts).
|
|
620
|
-
...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env) } } : {}),
|
|
666
|
+
...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env), ...(env.FOLDRUN_BROWSER_INIT ? { browseInit: env.FOLDRUN_BROWSER_INIT } : {}) } } : {}),
|
|
621
667
|
// The container is the boundary; the SDK's own bash sandbox here would
|
|
622
668
|
// only block declared network use (SSH, curl) for no added safety.
|
|
623
669
|
sandboxBash: false,
|
|
@@ -647,17 +693,30 @@ try {
|
|
|
647
693
|
}
|
|
648
694
|
`;
|
|
649
695
|
|
|
696
|
+
// Core itself is installed in a stage of its own (CORE_STAGE) and only the
|
|
697
|
+
// result is copied in: the tarball and the package README — which explains
|
|
698
|
+
// foldrun to a person reading the repo, and which no step reads — stay in
|
|
699
|
+
// that stage and never become a layer of the image. Owner rule, 2026-10-02:
|
|
700
|
+
// foldrun's own docs live in git, not in containers.
|
|
701
|
+
const CORE_STAGE = `FROM base AS core
|
|
702
|
+
WORKDIR /opt/runner
|
|
703
|
+
COPY foldrun-core.tgz ./
|
|
704
|
+
RUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\
|
|
705
|
+
&& npm cache clean --force >/dev/null 2>&1 \\
|
|
706
|
+
&& rm -f foldrun-core.tgz node_modules/@foldrun/core/README.md
|
|
707
|
+
`;
|
|
708
|
+
|
|
650
709
|
// docker cp writes root-owned files, so the entrypoint starts as root for
|
|
651
710
|
// exactly two commands — chown the copied-in trees, drop to the agent user —
|
|
652
711
|
// and the run flags grant only the three capabilities those two commands
|
|
653
712
|
// need. By the time any model-directed code executes, the process is uid
|
|
654
713
|
// 10001 with no capabilities at all.
|
|
655
714
|
const CORE_INSTALL = `WORKDIR /opt/runner
|
|
656
|
-
COPY
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
&&
|
|
660
|
-
&& chown -R agent:agent /workspace /library /opt/runner /home/agent/.foldrun /opt/foldrun-runtimes \\
|
|
715
|
+
COPY --from=core --chown=agent:agent /opt/runner/ ./
|
|
716
|
+
COPY --chown=agent:agent driver.mjs entry.sh ./
|
|
717
|
+
RUN mkdir -p /workspace /library /opt/runner/job ${RUNTIME_CACHE} ${SHARED_RUNTIMES} \\
|
|
718
|
+
&& chown agent:agent /opt/runner \\
|
|
719
|
+
&& chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\
|
|
661
720
|
&& chmod +x /opt/runner/entry.sh
|
|
662
721
|
ENTRYPOINT ["/opt/runner/entry.sh"]
|
|
663
722
|
`;
|
|
@@ -724,6 +783,7 @@ RUN apt-get update \\
|
|
|
724
783
|
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /usr/local/bin/
|
|
725
784
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules
|
|
726
785
|
|
|
786
|
+
${CORE_STAGE}
|
|
727
787
|
FROM base AS slim
|
|
728
788
|
# Fonts, because a step that draws text without a browser still needs them:
|
|
729
789
|
# the full image gets 51 from Playwright's --with-deps, slim had none, so from
|
|
@@ -879,7 +939,14 @@ RUN node -e ' \\
|
|
|
879
939
|
FROM browsers AS full
|
|
880
940
|
${CORE_INSTALL}`;
|
|
881
941
|
|
|
942
|
+
// runtime: system: — apt as root, before the drop (runtime.ts systemInstallSh).
|
|
943
|
+
// set -e is off around it: a failed install is reported by the driver, which
|
|
944
|
+
// reads the status file and fails the step with apt's own words.
|
|
882
945
|
const ENTRY = `#!/bin/sh
|
|
946
|
+
set +e
|
|
947
|
+
${SYSTEM_FROM_INPUT_SH}
|
|
948
|
+
${systemInstallSh(RUNTIME_CACHE)}
|
|
949
|
+
${INSTALL_HELPER_SH}
|
|
883
950
|
set -e
|
|
884
951
|
chown -R agent:agent /workspace /library /opt/runner/job
|
|
885
952
|
# The runtime cache, when one is mounted. NOT recursive: a warm cache is tens
|
|
@@ -1299,6 +1366,12 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
1299
1366
|
preserveTimestamps: true,
|
|
1300
1367
|
filter: (src) => !isPlatformPath(path.relative(args.workspaceRoot, src)),
|
|
1301
1368
|
});
|
|
1369
|
+
// A test run: what its earlier steps wrote, over the real files (the
|
|
1370
|
+
// copy is the baseline too, so only this step's own writes come back).
|
|
1371
|
+
if (args.divert) {
|
|
1372
|
+
const laid = overlayTestWrites(wsIn, args.divert.to);
|
|
1373
|
+
if (laid.length) args.emit("info", `test: this step sees ${laid.length} file${laid.length === 1 ? "" : "s"} earlier steps of this run wrote`);
|
|
1374
|
+
}
|
|
1302
1375
|
const libIn = path.join(staging, "library");
|
|
1303
1376
|
if (args.galleryRoot && fs.existsSync(args.galleryRoot)) fs.cpSync(args.galleryRoot, libIn, { recursive: true });
|
|
1304
1377
|
if (fs.existsSync(args.libraryRoot)) fs.cpSync(args.libraryRoot, libIn, { recursive: true });
|