@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
|
@@ -201,6 +201,11 @@ export declare function applyContainerChanges(hostWs: string, containerWs: strin
|
|
|
201
201
|
* that fills it.
|
|
202
202
|
*/
|
|
203
203
|
export declare const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
|
|
204
|
+
/** Starts the install helper as root, in the background, before the drop to
|
|
205
|
+
* the agent user: the root half of the `install` tool (install-helper.ts).
|
|
206
|
+
* The agent reaches it only through the socket; it installs only validated
|
|
207
|
+
* Debian package names, and dies with the container. */
|
|
208
|
+
export declare const INSTALL_HELPER_SH = "FOLDRUN_INSTALL_SOCKET=/opt/runner/job/install.sock; export FOLDRUN_INSTALL_SOCKET\nnode /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js \"$FOLDRUN_INSTALL_SOCKET\" /home/agent/.foldrun/runner/.runtimes >/dev/null 2>&1 &";
|
|
204
209
|
/**
|
|
205
210
|
* Where the platform's shared runtime layer is mounted, read-only, in a run
|
|
206
211
|
* pod — and read-write, one entry at a time, in the pod that builds an entry
|
|
@@ -271,7 +276,7 @@ export declare const RETRY_SH = "retry() { what=$1; shift; n=1; until \"$@\"; do
|
|
|
271
276
|
* The digest names the multi-arch index, so the one pin serves amd64 and
|
|
272
277
|
* arm64 alike. */
|
|
273
278
|
export declare const NODE_BASE = "node:22-trixie-slim@sha256:b26b04c123d9ff8ab646ceb18b9d75a1173acf64b9a401094b906d27b29338d4";
|
|
274
|
-
export declare const DOCKERFILE = "FROM node:22-trixie-slim@sha256:b26b04c123d9ff8ab646ceb18b9d75a1173acf64b9a401094b906d27b29338d4 AS base\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends python3 python3-venv ca-certificates bash util-linux tar openssh-client sshpass git curl \\\n && rm -rf /var/lib/apt/lists/* \\\n && useradd -m -u 10001 agent\n# uv builds the declared python environments (runtime.ts): a fraction of\n# pip's time, and it can fetch a pinned python the image does not carry.\nCOPY --from=ghcr.io/astral-sh/uv:0.11.25 /uv /uvx /usr/local/bin/\nENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules\n\nFROM base AS slim\n# Fonts, because a step that draws text without a browser still needs them:\n# the full image gets 51 from Playwright's --with-deps, slim had none, so from\n# 2026-09-29 (when non-browsing steps moved to slim) every resvg/sharp diagram\n# rendered its labels as empty boxes \u2014 expert-witness-desk's validator failed\n# four passes on a post image that was \"three empty boxes\". DejaVu covers\n# Latin, Greek and Cyrillic in a few MB; fontconfig is how renderers find it.\n# DejaVu alone still drew emoji, Chinese, Japanese, Korean, Devanagari and\n# Thai as empty boxes (reproduced 2026-10-01 with resvg-js 2.6 and sharp\n# 0.34), so four more, the smallest set that drew all of them \u2014 8 MB:\n# Symbola emoji and symbols, as outlines. NOT Noto Color Emoji (10 MB):\n# resvg draws nothing from its bitmap glyphs, so a resvg\n# diagram kept its empty boxes.\n# WenQuanYi Micro Hei Chinese, Japanese kana and Korean Hangul, one file.\n# Lohit Devanagari, Loma Hindi and Thai.\n# Here, not in base, so the full image's browser layers stay cached.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends fonts-dejavu-core fontconfig fonts-symbola fonts-wqy-microhei fonts-lohit-deva fonts-tlwg-loma-otf \\\n && rm -rf /var/lib/apt/lists/* \\\n && fc-cache -f >/dev/null\n# The Playwright client only, the same pinned version as the browser pod's\n# server (the wire protocol is version-locked), so a tool that imports it\n# still loads; there is nothing here for it to launch.\n# npm's download cache is dropped in the same layer: it was 190 MB of every\n# image, read by nothing after the build.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null && npm cache clean --force >/dev/null 2>&1\nWORKDIR /opt/runner\nCOPY foldrun-core.tgz driver.mjs entry.sh ./\nRUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\\n && npm cache clean --force >/dev/null 2>&1 \\\n && mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown -R agent:agent /workspace /library /opt/runner /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n\nFROM base AS browsers\n# Real browsers, because directories and portals increasingly render with\n# JavaScript and a plain fetch sees only the empty shell. Chromium is the one the\n# browser tool drives by default; Firefox and WebKit (Safari's engine) are\n# there for an agent asked to see a page the way those render it. Installed\n# to a fixed path the agent user can read (the default cache would be\n# root's HOME). The browsers' own sandboxes are disabled at launch time \u2014 in\n# this platform the container/gVisor IS the sandbox, and the two fight.\n# Pinned exactly, not @1: web browse's numbered elements use ariaSnapshot's\n# \"ai\" mode (1.59+), and the per-account browser pod runs this same image, so\n# a floating version would move both under a working tool on any rebuild.\n# Raise it on purpose, with the gallery tests and one real call.\n# axe-core beside it is web browse's mode=a11y: one file the tool injects\n# into the page; without it the mode falls back to its own shorter checks.\n# Xvfb by name: the browser pod starts it at boot and web browse starts one\n# per call for headless: false. It used to arrive only as one of Playwright's\n# --with-deps packages, so a Playwright bump that dropped it would have\n# broken every windowed browser without a word. No xauth: nothing here runs\n# xvfb-run, and Xvfb started without -auth takes the local client as it is.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends xvfb \\\n && rm -rf /var/lib/apt/lists/*\n# Google Chrome and Chrome beta come from Google's apt repository, which\n# Playwright adds; each is tried ENGINE_DOWNLOAD_ATTEMPTS times. Only on\n# x86_64: Google ships no Linux Chrome for arm64, so there it is not tried.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null \\\n && playwright install --with-deps chromium firefox webkit >/dev/null \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && if [ \"$(uname -m)\" = x86_64 ]; then \\\n retry chrome playwright install --with-deps chrome >/dev/null || echo \"runner image: chrome not installed - engine: chrome would fall back to chromium\" >&2; \\\n retry chrome-beta playwright install --with-deps chrome-beta >/dev/null || echo \"runner image: chrome-beta not installed\" >&2; \\\n else echo \"runner image: no Google Chrome for $(uname -m) - engine: chrome falls back to chromium\"; fi \\\n && chmod -R a+rX /opt/browser \\\n && npm cache clean --force >/dev/null 2>&1\n# Lightpanda (engine: lightpanda): a browser that runs the JavaScript and\n# never draws \u2014 a fraction of Chromium's memory for reading and filling\n# pages. Its own program, which web browse starts beside a call and drives\n# over the DevTools protocol; shipped unmodified (AGPL-3.0). Pinned by\n# checksum (GitHub's own asset digests, checked again here). 0.4.1 since the\n# move to Debian 13: 0.3.7 on wants glibc 2.38, and Debian 12's 2.36 held it\n# at 0.3.6. An arch without a build leaves the engine out, and the tool says\n# so when asked for it. Each download (with its checksum) is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times before the engine is left out.\n# Raise it on purpose: the new release's two sha256s, then one\n# engine=lightpanda call on each arch.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1d40801e72c0bc61b2cbd3f3562bcfc46de7b79e0568f33f686b64f2e587610a ;; \\\n aarch64) sum=664775c7f5ab69cc3189954c7f9345e25c167cb4dace016173e629f9a5e82c42 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/lightpanda \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /opt/browser/lightpanda/lightpanda \"https://github.com/lightpanda-io/browser/releases/download/0.4.1/lightpanda-$arch-linux\" \\\n && echo \"$sum /opt/browser/lightpanda/lightpanda\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry lightpanda fetch; then \\\n chmod a+rx /opt/browser/lightpanda/lightpanda && /opt/browser/lightpanda/lightpanda version; \\\n else rm -f /opt/browser/lightpanda/lightpanda; echo \"runner image: lightpanda not installed for $arch - engine: lightpanda says so when asked\" >&2; fi\n# Obscura (engine: obscura): a headless browser in Rust around V8 that does\n# draw \u2014 screenshots, a raster PDF, a screencast \u2014 light like Lightpanda.\n# Apache-2.0. The plain build, not -stealth: this platform does not help a\n# page not see automation. Two binaries, obscura and obscura-worker, which\n# must sit side by side. Pinned by checksum; glibc 2.35 is its floor, so\n# Debian 13 (2.41) runs it. As above, a download is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times, then a failed one leaves the engine out.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1534d1e6ddaf3d080ec4091eb41d0a4d8cc042a48b607d3c410fc13b482a9eec ;; \\\n aarch64) sum=5ecf980bca3060236a7a86ec7ed83d943e6598ee87caa46d20325d90bc75f979 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/obscura \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /tmp/obscura.tgz \"https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.3/obscura-$arch-linux.tar.gz\" \\\n && echo \"$sum /tmp/obscura.tgz\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry obscura fetch \\\n && tar -xzf /tmp/obscura.tgz -C /opt/browser/obscura obscura obscura-worker; then \\\n chmod a+rx /opt/browser/obscura/obscura /opt/browser/obscura/obscura-worker && /opt/browser/obscura/obscura --version; \\\n else rm -rf /opt/browser/obscura/*; echo \"runner image: obscura not installed for $arch - engine: obscura says so when asked\" >&2; fi \\\n && rm -f /tmp/obscura.tgz\n# What this image actually has, written down: /opt/browser/engines.json, one\n# entry per engine (its path and version, or null when the install above\n# left it out) plus Xvfb. Chromium's version is its own --version (dotted,\n# so FOLDRUN_RUNNER_ENGINES carries it: a pinned user_agent on engine\n# chromium is compared with it, not with Google Chrome's) then the build.\n# web browse reads it for its \"built without\" answer, and the deploy reads\n# it to report the engines beside the version.\n# On x86_64 a missing engine FAILS the build: every engine ships for that\n# arch, so a gap there is a failed download, and an image that quietly lacks\n# the Chrome or Lightpanda a desk names would fail those runs instead. A\n# failed build leaves the previous image running and says why. Elsewhere\n# (arm64 has no Google Chrome) the gap is recorded and the image ships.\nRUN node -e ' \\\n const fs = require(\"fs\"), cp = require(\"child_process\"), pw = require(\"playwright\"); \\\n const at = (p) => (p && fs.existsSync(p) ? p : null); \\\n const ver = (bin, args) => { try { return cp.execFileSync(bin, args, { timeout: 30000, stdio: [\"ignore\", \"pipe\", \"ignore\"] }).toString().trim().split(\"\\n\")[0] || null; } catch { return null; } }; \\\n const one = (p, v) => (p ? { path: p, version: v } : null); \\\n const pwBuild = (p) => (p ? (p.match(/(chromium|firefox|webkit)-(\\d+)/) || [])[0] || null : null); \\\n const lp = at(\"/opt/browser/lightpanda/lightpanda\"); \\\n const cr = at(pw.chromium.executablePath()); \\\n const ob = at(\"/opt/browser/obscura/obscura-worker\") && at(\"/opt/browser/obscura/obscura\"); \\\n const engines = { \\\n chromium: one(cr, cr && [ver(cr, [\"--version\"]), pwBuild(cr)].filter(Boolean).join(\" \")), \\\n chrome: one(at(\"/opt/google/chrome/chrome\"), ver(\"/opt/google/chrome/chrome\", [\"--version\"])), \\\n \"chrome-beta\": one(at(\"/opt/google/chrome-beta/chrome\"), ver(\"/opt/google/chrome-beta/chrome\", [\"--version\"])), \\\n firefox: one(at(pw.firefox.executablePath()), pwBuild(pw.firefox.executablePath())), \\\n webkit: one(at(pw.webkit.executablePath()), pwBuild(pw.webkit.executablePath())), \\\n lightpanda: one(lp, lp && ver(lp, [\"version\"])), \\\n obscura: one(ob, ob && ver(ob, [\"--version\"])), \\\n }; \\\n const xvfb = ver(\"sh\", [\"-c\", \"command -v Xvfb\"]); \\\n const arch = cp.execFileSync(\"uname\", [\"-m\"]).toString().trim(); \\\n const manifest = { arch, playwright: require(\"playwright/package.json\").version, engines, xvfb }; \\\n fs.writeFileSync(\"/opt/browser/engines.json\", JSON.stringify(manifest, null, 2) + \"\\n\"); \\\n console.log(JSON.stringify(manifest)); \\\n const missing = [\"chromium\", \"chrome\", \"firefox\", \"webkit\", \"lightpanda\", \"obscura\"].filter((e) => !engines[e]).concat(xvfb ? [] : [\"Xvfb\"]); \\\n if (missing.length && arch === \"x86_64\") { \\\n console.error(\"runner image: MISSING ENGINES on x86_64: \" + missing.join(\", \")); \\\n console.error(\"runner image: every engine ships for x86_64, so a missing one is a failed download (the retry lines above say which) - failing the build\"); \\\n process.exit(1); \\\n } \\\n if (missing.length) console.error(\"runner image: built without \" + missing.join(\", \") + \" on \" + arch); \\\n ' \\\n && chmod a+r /opt/browser/engines.json\n\nFROM browsers AS full\nWORKDIR /opt/runner\nCOPY foldrun-core.tgz driver.mjs entry.sh ./\nRUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\\n && npm cache clean --force >/dev/null 2>&1 \\\n && mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown -R agent:agent /workspace /library /opt/runner /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n";
|
|
279
|
+
export declare const DOCKERFILE = "FROM node:22-trixie-slim@sha256:b26b04c123d9ff8ab646ceb18b9d75a1173acf64b9a401094b906d27b29338d4 AS base\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends python3 python3-venv ca-certificates bash util-linux tar openssh-client sshpass git curl \\\n && rm -rf /var/lib/apt/lists/* \\\n && useradd -m -u 10001 agent\n# uv builds the declared python environments (runtime.ts): a fraction of\n# pip's time, and it can fetch a pinned python the image does not carry.\nCOPY --from=ghcr.io/astral-sh/uv:0.11.25 /uv /uvx /usr/local/bin/\nENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules\n\nFROM base AS core\nWORKDIR /opt/runner\nCOPY foldrun-core.tgz ./\nRUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\\n && npm cache clean --force >/dev/null 2>&1 \\\n && rm -f foldrun-core.tgz node_modules/@foldrun/core/README.md\n\nFROM base AS slim\n# Fonts, because a step that draws text without a browser still needs them:\n# the full image gets 51 from Playwright's --with-deps, slim had none, so from\n# 2026-09-29 (when non-browsing steps moved to slim) every resvg/sharp diagram\n# rendered its labels as empty boxes \u2014 expert-witness-desk's validator failed\n# four passes on a post image that was \"three empty boxes\". DejaVu covers\n# Latin, Greek and Cyrillic in a few MB; fontconfig is how renderers find it.\n# DejaVu alone still drew emoji, Chinese, Japanese, Korean, Devanagari and\n# Thai as empty boxes (reproduced 2026-10-01 with resvg-js 2.6 and sharp\n# 0.34), so four more, the smallest set that drew all of them \u2014 8 MB:\n# Symbola emoji and symbols, as outlines. NOT Noto Color Emoji (10 MB):\n# resvg draws nothing from its bitmap glyphs, so a resvg\n# diagram kept its empty boxes.\n# WenQuanYi Micro Hei Chinese, Japanese kana and Korean Hangul, one file.\n# Lohit Devanagari, Loma Hindi and Thai.\n# Here, not in base, so the full image's browser layers stay cached.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends fonts-dejavu-core fontconfig fonts-symbola fonts-wqy-microhei fonts-lohit-deva fonts-tlwg-loma-otf \\\n && rm -rf /var/lib/apt/lists/* \\\n && fc-cache -f >/dev/null\n# The Playwright client only, the same pinned version as the browser pod's\n# server (the wire protocol is version-locked), so a tool that imports it\n# still loads; there is nothing here for it to launch.\n# npm's download cache is dropped in the same layer: it was 190 MB of every\n# image, read by nothing after the build.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null && npm cache clean --force >/dev/null 2>&1\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n\nFROM base AS browsers\n# Real browsers, because directories and portals increasingly render with\n# JavaScript and a plain fetch sees only the empty shell. Chromium is the one the\n# browser tool drives by default; Firefox and WebKit (Safari's engine) are\n# there for an agent asked to see a page the way those render it. Installed\n# to a fixed path the agent user can read (the default cache would be\n# root's HOME). The browsers' own sandboxes are disabled at launch time \u2014 in\n# this platform the container/gVisor IS the sandbox, and the two fight.\n# Pinned exactly, not @1: web browse's numbered elements use ariaSnapshot's\n# \"ai\" mode (1.59+), and the per-account browser pod runs this same image, so\n# a floating version would move both under a working tool on any rebuild.\n# Raise it on purpose, with the gallery tests and one real call.\n# axe-core beside it is web browse's mode=a11y: one file the tool injects\n# into the page; without it the mode falls back to its own shorter checks.\n# Xvfb by name: the browser pod starts it at boot and web browse starts one\n# per call for headless: false. It used to arrive only as one of Playwright's\n# --with-deps packages, so a Playwright bump that dropped it would have\n# broken every windowed browser without a word. No xauth: nothing here runs\n# xvfb-run, and Xvfb started without -auth takes the local client as it is.\nRUN apt-get update \\\n && apt-get install -y --no-install-recommends xvfb \\\n && rm -rf /var/lib/apt/lists/*\n# Google Chrome and Chrome beta come from Google's apt repository, which\n# Playwright adds; each is tried ENGINE_DOWNLOAD_ATTEMPTS times. Only on\n# x86_64: Google ships no Linux Chrome for arm64, so there it is not tried.\nRUN npm install -g playwright@1.63.0 axe-core@4.13.0 >/dev/null \\\n && playwright install --with-deps chromium firefox webkit >/dev/null \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && if [ \"$(uname -m)\" = x86_64 ]; then \\\n retry chrome playwright install --with-deps chrome >/dev/null || echo \"runner image: chrome not installed - engine: chrome would fall back to chromium\" >&2; \\\n retry chrome-beta playwright install --with-deps chrome-beta >/dev/null || echo \"runner image: chrome-beta not installed\" >&2; \\\n else echo \"runner image: no Google Chrome for $(uname -m) - engine: chrome falls back to chromium\"; fi \\\n && chmod -R a+rX /opt/browser \\\n && npm cache clean --force >/dev/null 2>&1\n# Lightpanda (engine: lightpanda): a browser that runs the JavaScript and\n# never draws \u2014 a fraction of Chromium's memory for reading and filling\n# pages. Its own program, which web browse starts beside a call and drives\n# over the DevTools protocol; shipped unmodified (AGPL-3.0). Pinned by\n# checksum (GitHub's own asset digests, checked again here). 0.4.1 since the\n# move to Debian 13: 0.3.7 on wants glibc 2.38, and Debian 12's 2.36 held it\n# at 0.3.6. An arch without a build leaves the engine out, and the tool says\n# so when asked for it. Each download (with its checksum) is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times before the engine is left out.\n# Raise it on purpose: the new release's two sha256s, then one\n# engine=lightpanda call on each arch.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1d40801e72c0bc61b2cbd3f3562bcfc46de7b79e0568f33f686b64f2e587610a ;; \\\n aarch64) sum=664775c7f5ab69cc3189954c7f9345e25c167cb4dace016173e629f9a5e82c42 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/lightpanda \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /opt/browser/lightpanda/lightpanda \"https://github.com/lightpanda-io/browser/releases/download/0.4.1/lightpanda-$arch-linux\" \\\n && echo \"$sum /opt/browser/lightpanda/lightpanda\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry lightpanda fetch; then \\\n chmod a+rx /opt/browser/lightpanda/lightpanda && /opt/browser/lightpanda/lightpanda version; \\\n else rm -f /opt/browser/lightpanda/lightpanda; echo \"runner image: lightpanda not installed for $arch - engine: lightpanda says so when asked\" >&2; fi\n# Obscura (engine: obscura): a headless browser in Rust around V8 that does\n# draw \u2014 screenshots, a raster PDF, a screencast \u2014 light like Lightpanda.\n# Apache-2.0. The plain build, not -stealth: this platform does not help a\n# page not see automation. Two binaries, obscura and obscura-worker, which\n# must sit side by side. Pinned by checksum; glibc 2.35 is its floor, so\n# Debian 13 (2.41) runs it. As above, a download is tried\n# ENGINE_DOWNLOAD_ATTEMPTS times, then a failed one leaves the engine out.\nRUN arch=$(uname -m) \\\n && case \"$arch\" in \\\n x86_64) sum=1534d1e6ddaf3d080ec4091eb41d0a4d8cc042a48b607d3c410fc13b482a9eec ;; \\\n aarch64) sum=5ecf980bca3060236a7a86ec7ed83d943e6598ee87caa46d20325d90bc75f979 ;; \\\n *) sum= ;; \\\n esac \\\n && mkdir -p /opt/browser/obscura \\\n && retry() { what=$1; shift; n=1; until \"$@\"; do if [ \"$n\" -ge 3 ]; then echo \"runner image: $what - download failed $n times, giving up\" >&2; return 1; fi; echo \"runner image: $what - download failed (attempt $n of 3), retrying in $((n * 15))s\" >&2; sleep $((n * 15)); n=$((n + 1)); done; } \\\n && fetch() { curl -fsSL -o /tmp/obscura.tgz \"https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.3/obscura-$arch-linux.tar.gz\" \\\n && echo \"$sum /tmp/obscura.tgz\" | sha256sum -c - >/dev/null; } \\\n && if [ -n \"$sum\" ] && retry obscura fetch \\\n && tar -xzf /tmp/obscura.tgz -C /opt/browser/obscura obscura obscura-worker; then \\\n chmod a+rx /opt/browser/obscura/obscura /opt/browser/obscura/obscura-worker && /opt/browser/obscura/obscura --version; \\\n else rm -rf /opt/browser/obscura/*; echo \"runner image: obscura not installed for $arch - engine: obscura says so when asked\" >&2; fi \\\n && rm -f /tmp/obscura.tgz\n# What this image actually has, written down: /opt/browser/engines.json, one\n# entry per engine (its path and version, or null when the install above\n# left it out) plus Xvfb. Chromium's version is its own --version (dotted,\n# so FOLDRUN_RUNNER_ENGINES carries it: a pinned user_agent on engine\n# chromium is compared with it, not with Google Chrome's) then the build.\n# web browse reads it for its \"built without\" answer, and the deploy reads\n# it to report the engines beside the version.\n# On x86_64 a missing engine FAILS the build: every engine ships for that\n# arch, so a gap there is a failed download, and an image that quietly lacks\n# the Chrome or Lightpanda a desk names would fail those runs instead. A\n# failed build leaves the previous image running and says why. Elsewhere\n# (arm64 has no Google Chrome) the gap is recorded and the image ships.\nRUN node -e ' \\\n const fs = require(\"fs\"), cp = require(\"child_process\"), pw = require(\"playwright\"); \\\n const at = (p) => (p && fs.existsSync(p) ? p : null); \\\n const ver = (bin, args) => { try { return cp.execFileSync(bin, args, { timeout: 30000, stdio: [\"ignore\", \"pipe\", \"ignore\"] }).toString().trim().split(\"\\n\")[0] || null; } catch { return null; } }; \\\n const one = (p, v) => (p ? { path: p, version: v } : null); \\\n const pwBuild = (p) => (p ? (p.match(/(chromium|firefox|webkit)-(\\d+)/) || [])[0] || null : null); \\\n const lp = at(\"/opt/browser/lightpanda/lightpanda\"); \\\n const cr = at(pw.chromium.executablePath()); \\\n const ob = at(\"/opt/browser/obscura/obscura-worker\") && at(\"/opt/browser/obscura/obscura\"); \\\n const engines = { \\\n chromium: one(cr, cr && [ver(cr, [\"--version\"]), pwBuild(cr)].filter(Boolean).join(\" \")), \\\n chrome: one(at(\"/opt/google/chrome/chrome\"), ver(\"/opt/google/chrome/chrome\", [\"--version\"])), \\\n \"chrome-beta\": one(at(\"/opt/google/chrome-beta/chrome\"), ver(\"/opt/google/chrome-beta/chrome\", [\"--version\"])), \\\n firefox: one(at(pw.firefox.executablePath()), pwBuild(pw.firefox.executablePath())), \\\n webkit: one(at(pw.webkit.executablePath()), pwBuild(pw.webkit.executablePath())), \\\n lightpanda: one(lp, lp && ver(lp, [\"version\"])), \\\n obscura: one(ob, ob && ver(ob, [\"--version\"])), \\\n }; \\\n const xvfb = ver(\"sh\", [\"-c\", \"command -v Xvfb\"]); \\\n const arch = cp.execFileSync(\"uname\", [\"-m\"]).toString().trim(); \\\n const manifest = { arch, playwright: require(\"playwright/package.json\").version, engines, xvfb }; \\\n fs.writeFileSync(\"/opt/browser/engines.json\", JSON.stringify(manifest, null, 2) + \"\\n\"); \\\n console.log(JSON.stringify(manifest)); \\\n const missing = [\"chromium\", \"chrome\", \"firefox\", \"webkit\", \"lightpanda\", \"obscura\"].filter((e) => !engines[e]).concat(xvfb ? [] : [\"Xvfb\"]); \\\n if (missing.length && arch === \"x86_64\") { \\\n console.error(\"runner image: MISSING ENGINES on x86_64: \" + missing.join(\", \")); \\\n console.error(\"runner image: every engine ships for x86_64, so a missing one is a failed download (the retry lines above say which) - failing the build\"); \\\n process.exit(1); \\\n } \\\n if (missing.length) console.error(\"runner image: built without \" + missing.join(\", \") + \" on \" + arch); \\\n ' \\\n && chmod a+r /opt/browser/engines.json\n\nFROM browsers AS full\nWORKDIR /opt/runner\nCOPY --from=core --chown=agent:agent /opt/runner/ ./\nCOPY --chown=agent:agent driver.mjs entry.sh ./\nRUN mkdir -p /workspace /library /opt/runner/job /home/agent/.foldrun/runner/.runtimes /opt/foldrun-runtimes/shared/.runtimes \\\n && chown agent:agent /opt/runner \\\n && chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\\n && chmod +x /opt/runner/entry.sh\nENTRYPOINT [\"/opt/runner/entry.sh\"]\n";
|
|
275
280
|
/** Which build target of the runner image (see DOCKERFILE). */
|
|
276
281
|
export type RunnerVariant = "full" | "slim";
|
|
277
282
|
export declare function runnerImageTag(variant?: RunnerVariant): string;
|
|
@@ -23,9 +23,9 @@ import path from "node:path";
|
|
|
23
23
|
import crypto from "node:crypto";
|
|
24
24
|
import { spawn, spawnSync } from "node:child_process";
|
|
25
25
|
import { isPlatformPath } from "./store.js";
|
|
26
|
-
import { divertedSummary, isDivertedPath } from "./test-mode.js";
|
|
26
|
+
import { divertedSummary, isDivertedPath, overlayTestWrites } from "./test-mode.js";
|
|
27
27
|
import { isFileValue, fileContent } from "./secrets.js";
|
|
28
|
-
import { safeTenantSegment } from "./runtime.js";
|
|
28
|
+
import { safeTenantSegment, systemInstallSh, SYSTEM_FROM_INPUT_SH, INSTALL_SOCKET } from "./runtime.js";
|
|
29
29
|
import { isOperatorEvent } from "./operator.js";
|
|
30
30
|
import { parseBrowserPod } from "./browser-pod.js";
|
|
31
31
|
/**
|
|
@@ -173,19 +173,39 @@ export function mergeAppends(was, host, next) {
|
|
|
173
173
|
const max = Math.min(h.length, n.length);
|
|
174
174
|
while (common < max && h.charCodeAt(common) === n.charCodeAt(common))
|
|
175
175
|
common++;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
// One pass over the shared prefix. Rebuilding and hashing the prefix at
|
|
177
|
+
// every line boundary, from the longest down, was quadratic: on 6 Oct 2026
|
|
178
|
+
// a conflicted file of a few MB held the worker's event loop for minutes,
|
|
179
|
+
// the liveness probe killed it every 4.5 minutes, and every step on the box
|
|
180
|
+
// lost its model connection.
|
|
181
|
+
let end = -1;
|
|
182
|
+
if (typeof was === "string") {
|
|
183
|
+
const run = crypto.createHash("sha256");
|
|
184
|
+
if (run.copy().digest("hex") === was)
|
|
185
|
+
end = 0;
|
|
186
|
+
let from = 0;
|
|
187
|
+
for (let nl = h.indexOf("\n"); nl !== -1 && nl < common; nl = h.indexOf("\n", nl + 1)) {
|
|
188
|
+
run.update(h.slice(from, nl + 1), "utf8");
|
|
189
|
+
from = nl + 1;
|
|
190
|
+
if (run.copy().digest("hex") === was)
|
|
191
|
+
end = from;
|
|
192
|
+
}
|
|
187
193
|
}
|
|
188
|
-
|
|
194
|
+
else {
|
|
195
|
+
// The base's bytes are known, so it can only end at their length.
|
|
196
|
+
const base = was.toString("utf8");
|
|
197
|
+
if (base.length <= common &&
|
|
198
|
+
(base.length === 0 || base.endsWith("\n")) &&
|
|
199
|
+
h.startsWith(base) &&
|
|
200
|
+
Buffer.from(base, "utf8").equals(was))
|
|
201
|
+
end = base.length;
|
|
202
|
+
}
|
|
203
|
+
if (end < 0)
|
|
204
|
+
return null;
|
|
205
|
+
const hostAdded = h.slice(end);
|
|
206
|
+
const stepAdded = n.slice(end);
|
|
207
|
+
const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
|
|
208
|
+
return Buffer.from(h.slice(0, end) + joined + stepAdded, "utf8");
|
|
189
209
|
}
|
|
190
210
|
export function applyContainerChanges(hostWs, containerWs, baseline, note, divert) {
|
|
191
211
|
const applied = [];
|
|
@@ -283,6 +303,12 @@ export function applyContainerChanges(hostWs, containerWs, baseline, note, diver
|
|
|
283
303
|
* that fills it.
|
|
284
304
|
*/
|
|
285
305
|
export const RUNTIME_CACHE = "/home/agent/.foldrun/runner/.runtimes";
|
|
306
|
+
/** Starts the install helper as root, in the background, before the drop to
|
|
307
|
+
* the agent user: the root half of the `install` tool (install-helper.ts).
|
|
308
|
+
* The agent reaches it only through the socket; it installs only validated
|
|
309
|
+
* Debian package names, and dies with the container. */
|
|
310
|
+
export const INSTALL_HELPER_SH = `FOLDRUN_INSTALL_SOCKET=${INSTALL_SOCKET}; export FOLDRUN_INSTALL_SOCKET
|
|
311
|
+
node /opt/runner/node_modules/@foldrun/core/dist/src/install-helper.js "$FOLDRUN_INSTALL_SOCKET" ${RUNTIME_CACHE} >/dev/null 2>&1 &`;
|
|
286
312
|
/**
|
|
287
313
|
* Where the platform's shared runtime layer is mounted, read-only, in a run
|
|
288
314
|
* pod — and read-write, one entry at a time, in the pod that builds an entry
|
|
@@ -420,6 +446,25 @@ try {
|
|
|
420
446
|
// meet the packages its runtime declared, not the image's bare python.
|
|
421
447
|
Object.assign(env, runtime.env);
|
|
422
448
|
|
|
449
|
+
// install: what the agent finds it needs mid-step. apt goes through the
|
|
450
|
+
// root start-up's helper (install-helper.ts); pip and npm land in a
|
|
451
|
+
// directory of this container's own, on the paths from the first turn.
|
|
452
|
+
const { buildInstallTool, INSTALL_TOOL } = await import("@foldrun/core/install-tool");
|
|
453
|
+
const { stepInstallEnv, INSTALL_SOCKET } = await import("@foldrun/core/runtime");
|
|
454
|
+
const installDir = "/home/agent/.foldrun/step";
|
|
455
|
+
const installing = (input.allowed ?? []).includes(INSTALL_TOOL);
|
|
456
|
+
if (installing) Object.assign(env, stepInstallEnv(installDir, env));
|
|
457
|
+
const install = installing
|
|
458
|
+
? buildInstallTool({
|
|
459
|
+
dir: installDir,
|
|
460
|
+
socket: env.FOLDRUN_INSTALL_SOCKET || INSTALL_SOCKET,
|
|
461
|
+
cacheRoot: "${RUNTIME_CACHE}",
|
|
462
|
+
python: env.VIRTUAL_ENV ? env.VIRTUAL_ENV + "/bin/python" : "python3",
|
|
463
|
+
env,
|
|
464
|
+
emit,
|
|
465
|
+
})
|
|
466
|
+
: { server: null };
|
|
467
|
+
|
|
423
468
|
// API and script tools, rebuilt in here from their specs. Secrets were
|
|
424
469
|
// substituted into API headers before the input crossed the boundary, so
|
|
425
470
|
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
@@ -470,6 +515,7 @@ try {
|
|
|
470
515
|
...(history.server ? { foldrun_history: history.server } : {}),
|
|
471
516
|
...(desks.server ? { foldrun_desks: desks.server } : {}),
|
|
472
517
|
...(ask.server ? { foldrun_ask: ask.server } : {}),
|
|
518
|
+
...(install.server ? { foldrun_install: install.server } : {}),
|
|
473
519
|
...input.mcpServers,
|
|
474
520
|
},
|
|
475
521
|
...(inbox ? { inbox } : {}),
|
|
@@ -489,7 +535,7 @@ try {
|
|
|
489
535
|
// through the account's pod, and named the file the web tool logs its
|
|
490
536
|
// reconnects to. A lost pod stops the step; what it wrote decides what
|
|
491
537
|
// the runner does next (browser-pod.ts).
|
|
492
|
-
...(env.FOLDRUN_BROWSER_POD_EVENTS ? { browserPod: { events: env.FOLDRUN_BROWSER_POD_EVENTS, methods: api.methods ?? {}, paidWeb: paidWebActions(env) } } : {}),
|
|
538
|
+
...(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 } : {}) } } : {}),
|
|
493
539
|
// The container is the boundary; the SDK's own bash sandbox here would
|
|
494
540
|
// only block declared network use (SSH, curl) for no added safety.
|
|
495
541
|
sandboxBash: false,
|
|
@@ -518,17 +564,29 @@ try {
|
|
|
518
564
|
process.exit(0);
|
|
519
565
|
}
|
|
520
566
|
`;
|
|
567
|
+
// Core itself is installed in a stage of its own (CORE_STAGE) and only the
|
|
568
|
+
// result is copied in: the tarball and the package README — which explains
|
|
569
|
+
// foldrun to a person reading the repo, and which no step reads — stay in
|
|
570
|
+
// that stage and never become a layer of the image. Owner rule, 2026-10-02:
|
|
571
|
+
// foldrun's own docs live in git, not in containers.
|
|
572
|
+
const CORE_STAGE = `FROM base AS core
|
|
573
|
+
WORKDIR /opt/runner
|
|
574
|
+
COPY foldrun-core.tgz ./
|
|
575
|
+
RUN npm init -y >/dev/null && npm install ./foldrun-core.tgz --omit=dev \\
|
|
576
|
+
&& npm cache clean --force >/dev/null 2>&1 \\
|
|
577
|
+
&& rm -f foldrun-core.tgz node_modules/@foldrun/core/README.md
|
|
578
|
+
`;
|
|
521
579
|
// docker cp writes root-owned files, so the entrypoint starts as root for
|
|
522
580
|
// exactly two commands — chown the copied-in trees, drop to the agent user —
|
|
523
581
|
// and the run flags grant only the three capabilities those two commands
|
|
524
582
|
// need. By the time any model-directed code executes, the process is uid
|
|
525
583
|
// 10001 with no capabilities at all.
|
|
526
584
|
const CORE_INSTALL = `WORKDIR /opt/runner
|
|
527
|
-
COPY
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
&&
|
|
531
|
-
&& chown -R agent:agent /workspace /library /opt/runner /home/agent/.foldrun /opt/foldrun-runtimes \\
|
|
585
|
+
COPY --from=core --chown=agent:agent /opt/runner/ ./
|
|
586
|
+
COPY --chown=agent:agent driver.mjs entry.sh ./
|
|
587
|
+
RUN mkdir -p /workspace /library /opt/runner/job ${RUNTIME_CACHE} ${SHARED_RUNTIMES} \\
|
|
588
|
+
&& chown agent:agent /opt/runner \\
|
|
589
|
+
&& chown -R agent:agent /workspace /library /opt/runner/job /home/agent/.foldrun /opt/foldrun-runtimes \\
|
|
532
590
|
&& chmod +x /opt/runner/entry.sh
|
|
533
591
|
ENTRYPOINT ["/opt/runner/entry.sh"]
|
|
534
592
|
`;
|
|
@@ -590,6 +648,7 @@ RUN apt-get update \\
|
|
|
590
648
|
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /usr/local/bin/
|
|
591
649
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/browser NODE_PATH=/usr/local/lib/node_modules
|
|
592
650
|
|
|
651
|
+
${CORE_STAGE}
|
|
593
652
|
FROM base AS slim
|
|
594
653
|
# Fonts, because a step that draws text without a browser still needs them:
|
|
595
654
|
# the full image gets 51 from Playwright's --with-deps, slim had none, so from
|
|
@@ -744,7 +803,14 @@ RUN node -e ' \\
|
|
|
744
803
|
|
|
745
804
|
FROM browsers AS full
|
|
746
805
|
${CORE_INSTALL}`;
|
|
806
|
+
// runtime: system: — apt as root, before the drop (runtime.ts systemInstallSh).
|
|
807
|
+
// set -e is off around it: a failed install is reported by the driver, which
|
|
808
|
+
// reads the status file and fails the step with apt's own words.
|
|
747
809
|
const ENTRY = `#!/bin/sh
|
|
810
|
+
set +e
|
|
811
|
+
${SYSTEM_FROM_INPUT_SH}
|
|
812
|
+
${systemInstallSh(RUNTIME_CACHE)}
|
|
813
|
+
${INSTALL_HELPER_SH}
|
|
748
814
|
set -e
|
|
749
815
|
chown -R agent:agent /workspace /library /opt/runner/job
|
|
750
816
|
# The runtime cache, when one is mounted. NOT recursive: a warm cache is tens
|
|
@@ -1100,6 +1166,13 @@ export async function runStepInContainer(args) {
|
|
|
1100
1166
|
preserveTimestamps: true,
|
|
1101
1167
|
filter: (src) => !isPlatformPath(path.relative(args.workspaceRoot, src)),
|
|
1102
1168
|
});
|
|
1169
|
+
// A test run: what its earlier steps wrote, over the real files (the
|
|
1170
|
+
// copy is the baseline too, so only this step's own writes come back).
|
|
1171
|
+
if (args.divert) {
|
|
1172
|
+
const laid = overlayTestWrites(wsIn, args.divert.to);
|
|
1173
|
+
if (laid.length)
|
|
1174
|
+
args.emit("info", `test: this step sees ${laid.length} file${laid.length === 1 ? "" : "s"} earlier steps of this run wrote`);
|
|
1175
|
+
}
|
|
1103
1176
|
const libIn = path.join(staging, "library");
|
|
1104
1177
|
if (args.galleryRoot && fs.existsSync(args.galleryRoot))
|
|
1105
1178
|
fs.cpSync(args.galleryRoot, libIn, { recursive: true });
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type SubagentSpec } from "./subagents.ts";
|
|
2
2
|
import { type FlowStep, type RunRecord, type StepRecord } from "./store.ts";
|
|
3
3
|
import { type Catalog } from "./catalog.ts";
|
|
4
|
+
import { type SearchRoot, type RunDigest } from "./context-tools.ts";
|
|
5
|
+
import { type ScriptSpec } from "./script-tools.ts";
|
|
4
6
|
import { type RuntimeSpec } from "./runtime.ts";
|
|
7
|
+
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
5
8
|
import { type LanguageChoice } from "./language.ts";
|
|
6
9
|
import { type RegionChoice } from "./locale.ts";
|
|
7
10
|
import { type ClockChoice } from "./clock.ts";
|
|
@@ -190,6 +193,163 @@ export declare function applicableSkills<T extends {
|
|
|
190
193
|
*/
|
|
191
194
|
export declare function saysUntilMarker(result: string | null, until: string): boolean;
|
|
192
195
|
export declare function resolveDocLinks(text: string, workspaceRoot: string): string;
|
|
196
|
+
export declare function agentContext(agentDir: string, tenant: string, tags?: string[],
|
|
197
|
+
/** Who is running, so scripts can stamp what they write with its provenance. */
|
|
198
|
+
identity?: {
|
|
199
|
+
runId?: string;
|
|
200
|
+
agent?: string;
|
|
201
|
+
},
|
|
202
|
+
/** The flow's own `timezone:`, when this step belongs to one. Between the
|
|
203
|
+
* agent's frontmatter and the workspace's AGENTS.md in the cascade. */
|
|
204
|
+
flowTimezone?: string | null,
|
|
205
|
+
/** Only what the agent is granted — no runtime prepared. For a sub-agent,
|
|
206
|
+
* whose tools are cut to its parent's and run in the parent's runtime. */
|
|
207
|
+
grantsOnly?: boolean): {
|
|
208
|
+
front: {
|
|
209
|
+
[key: string]: any;
|
|
210
|
+
};
|
|
211
|
+
clockEnv: {
|
|
212
|
+
FOLDRUN_LANGUAGE: string;
|
|
213
|
+
FOLDRUN_REGION?: string | undefined;
|
|
214
|
+
FOLDRUN_CURRENCY?: string | undefined;
|
|
215
|
+
FOLDRUN_UNITS: import("./locale.ts").Units;
|
|
216
|
+
FOLDRUN_CALENDAR: string;
|
|
217
|
+
FOLDRUN_DATE_LOCAL?: string | undefined;
|
|
218
|
+
FOLDRUN_RTL: string;
|
|
219
|
+
TZ: string;
|
|
220
|
+
FOLDRUN_DATE: string;
|
|
221
|
+
FOLDRUN_SEARCH_URL?: string | undefined;
|
|
222
|
+
FOLDRUN_API_URL?: string | undefined;
|
|
223
|
+
FOLDRUN_WEB_SEARCH_VIA?: string | undefined;
|
|
224
|
+
FOLDRUN_WEB_SEARCH_SECRET?: string | undefined;
|
|
225
|
+
FOLDRUN_WEB_FETCH_VIA?: string | undefined;
|
|
226
|
+
FOLDRUN_WEB_FETCH_SECRET?: string | undefined;
|
|
227
|
+
FOLDRUN_WEB_ACTIONS?: string | undefined;
|
|
228
|
+
FOLDRUN_BROWSER_VENDOR?: string | undefined;
|
|
229
|
+
FOLDRUN_BROWSER_SECRET?: string | undefined;
|
|
230
|
+
FOLDRUN_BROWSER_VENDOR_SESSION?: string | undefined;
|
|
231
|
+
FOLDRUN_BROWSER_SESSION?: string | undefined;
|
|
232
|
+
FOLDRUN_BROWSER_ENGINE?: "chrome" | "chromium" | "firefox" | "lightpanda" | "obscura" | "webkit" | undefined;
|
|
233
|
+
FOLDRUN_BROWSER_USER_AGENT?: string | undefined;
|
|
234
|
+
FOLDRUN_BROWSER_DEVICE?: string | undefined;
|
|
235
|
+
FOLDRUN_BROWSER_LOCALE?: string | undefined;
|
|
236
|
+
FOLDRUN_BROWSER_TIMEZONE?: string | undefined;
|
|
237
|
+
FOLDRUN_BROWSER_COOKIES?: string | undefined;
|
|
238
|
+
FOLDRUN_BROWSER_COOKIE_DOMAIN?: string | undefined;
|
|
239
|
+
FOLDRUN_BROWSER_STORAGE?: string | undefined;
|
|
240
|
+
FOLDRUN_BROWSER_STORAGE_ORIGIN?: string | undefined;
|
|
241
|
+
FOLDRUN_BROWSER_IDENTITIES?: string | undefined;
|
|
242
|
+
FOLDRUN_BROWSER_HEADLESS?: string | undefined;
|
|
243
|
+
FOLDRUN_BROWSER_VERSION?: string | undefined;
|
|
244
|
+
FOLDRUN_BROWSER_LIVE?: string | undefined;
|
|
245
|
+
FOLDRUN_BROWSER_ALLOWED_DOMAINS?: string | undefined;
|
|
246
|
+
FOLDRUN_BROWSER_DENY?: string | undefined;
|
|
247
|
+
FOLDRUN_BROWSER_BOUNDARIES?: string | undefined;
|
|
248
|
+
FOLDRUN_BROWSER_INIT?: string | undefined;
|
|
249
|
+
FOLDRUN_BROWSER_EXTENSIONS?: string | undefined;
|
|
250
|
+
FOLDRUN_BROWSER_WEBGPU?: string | undefined;
|
|
251
|
+
FOLDRUN_BROWSER_IGNORE_HTTPS_ERRORS?: string | undefined;
|
|
252
|
+
FOLDRUN_BROWSER_STATE_KEY?: string | undefined;
|
|
253
|
+
FOLDRUN_BROWSER_VIDEO?: string | undefined;
|
|
254
|
+
FOLDRUN_BROWSER_LIVE_VIEW?: string | undefined;
|
|
255
|
+
};
|
|
256
|
+
/** The zone, where it came from, and anything unreadable on the way —
|
|
257
|
+
* reported by the step, not here, so it lands in the run's trace. */
|
|
258
|
+
clock: ClockChoice;
|
|
259
|
+
systemPrompt: string;
|
|
260
|
+
allowed: string[];
|
|
261
|
+
disabled: string[];
|
|
262
|
+
unknownTools: string[];
|
|
263
|
+
shadowed: string[];
|
|
264
|
+
legacyUse: string[];
|
|
265
|
+
retiredTools: string[];
|
|
266
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
267
|
+
mcpNames: string[];
|
|
268
|
+
apiTools: import("./api-tools.ts").ApiToolResult;
|
|
269
|
+
scriptTools: import("./script-tools.ts").ScriptToolResult;
|
|
270
|
+
apiSpecs: import("./store.ts").ApiSpec[];
|
|
271
|
+
scriptSpecs: ScriptSpec[];
|
|
272
|
+
outward: boolean;
|
|
273
|
+
searchRoots: SearchRoot[];
|
|
274
|
+
historyDigest: RunDigest[];
|
|
275
|
+
deskDigest: RunDigest[];
|
|
276
|
+
searchTools: import("./context-tools.ts").ContextToolResult;
|
|
277
|
+
historyTools: import("./context-tools.ts").ContextToolResult;
|
|
278
|
+
deskTools: import("./context-tools.ts").ContextToolResult;
|
|
279
|
+
ask: {
|
|
280
|
+
timeoutSec: number;
|
|
281
|
+
} | null;
|
|
282
|
+
runtime: {
|
|
283
|
+
log: string[];
|
|
284
|
+
error: string | null;
|
|
285
|
+
executor: import("./container.ts").Executor;
|
|
286
|
+
spec: RuntimeSpec | null;
|
|
287
|
+
dispose: (() => void) | undefined;
|
|
288
|
+
};
|
|
289
|
+
secretEnv: {
|
|
290
|
+
[x: string]: string;
|
|
291
|
+
};
|
|
292
|
+
formatWarning: string | null;
|
|
293
|
+
providerEnv: Record<string, string>;
|
|
294
|
+
providerLabel: string | null;
|
|
295
|
+
providerSecrets: Record<string, string>;
|
|
296
|
+
providerWarnings: string[];
|
|
297
|
+
fallbackEnv: Record<string, string> | null;
|
|
298
|
+
region: RegionChoice;
|
|
299
|
+
searchChoice: import("./providers.ts").SearchChoice;
|
|
300
|
+
fetchChoice: import("./providers.ts").SearchChoice;
|
|
301
|
+
actionChoices: ({
|
|
302
|
+
provider: null;
|
|
303
|
+
error?: undefined;
|
|
304
|
+
action: "answer" | "crawl" | "extract" | "map" | "monitor";
|
|
305
|
+
} | {
|
|
306
|
+
provider: string;
|
|
307
|
+
secret: string;
|
|
308
|
+
host: string;
|
|
309
|
+
error?: undefined;
|
|
310
|
+
action: "answer" | "crawl" | "extract" | "map" | "monitor";
|
|
311
|
+
} | {
|
|
312
|
+
provider: null;
|
|
313
|
+
error: string;
|
|
314
|
+
action: "answer" | "crawl" | "extract" | "map" | "monitor";
|
|
315
|
+
})[];
|
|
316
|
+
web: {
|
|
317
|
+
actions: import("./providers.ts").WebAction[] | null;
|
|
318
|
+
raw: Record<import("./providers.ts").WebAction, unknown>;
|
|
319
|
+
problems: string[];
|
|
320
|
+
};
|
|
321
|
+
language: LanguageChoice;
|
|
322
|
+
translator: {
|
|
323
|
+
upstreamBase: string;
|
|
324
|
+
wire?: import("./translator.ts").TranslatorWire;
|
|
325
|
+
upstreamKey: string;
|
|
326
|
+
headers?: Record<string, string>;
|
|
327
|
+
maxTokensParam?: "max_tokens" | "max_completion_tokens";
|
|
328
|
+
reasoningEffort?: boolean;
|
|
329
|
+
params?: Record<string, unknown>;
|
|
330
|
+
label?: string;
|
|
331
|
+
egress?: string;
|
|
332
|
+
timezone: string;
|
|
333
|
+
} | null;
|
|
334
|
+
fallbackTranslator: {
|
|
335
|
+
upstreamBase: string;
|
|
336
|
+
wire?: import("./translator.ts").TranslatorWire;
|
|
337
|
+
upstreamKey: string;
|
|
338
|
+
headers?: Record<string, string>;
|
|
339
|
+
maxTokensParam?: "max_tokens" | "max_completion_tokens";
|
|
340
|
+
reasoningEffort?: boolean;
|
|
341
|
+
params?: Record<string, unknown>;
|
|
342
|
+
label?: string;
|
|
343
|
+
egress?: string;
|
|
344
|
+
timezone: string;
|
|
345
|
+
} | null;
|
|
346
|
+
apiWarnings: string[];
|
|
347
|
+
secretScopes: Record<string, import("./secrets.ts").SecretScope>;
|
|
348
|
+
missingTools: string[];
|
|
349
|
+
size: "heavy" | "large" | "small";
|
|
350
|
+
brokenTools: string[];
|
|
351
|
+
missingSecrets: string[];
|
|
352
|
+
};
|
|
193
353
|
export declare function repriced(catalog: Catalog | null, model: string, usage: {
|
|
194
354
|
inputTokens: number;
|
|
195
355
|
outputTokens: number;
|
|
@@ -214,6 +374,28 @@ attempt: number, status: "completed" | "failed", startedAt: string, eventsBefore
|
|
|
214
374
|
/** The attempt re-attached to a sandbox a previous driver left: same
|
|
215
375
|
* attempt, so its row is replaced rather than repeated. */
|
|
216
376
|
reattach?: boolean): void;
|
|
377
|
+
/**
|
|
378
|
+
* What the person at the gate said, framed for the step's prompt — or "".
|
|
379
|
+
*
|
|
380
|
+
* The note is the freshest instruction the step has — later than the flow
|
|
381
|
+
* file, aimed at this exact run. When the gate asked a question (ask:), the
|
|
382
|
+
* note IS the answer and is framed as one.
|
|
383
|
+
*
|
|
384
|
+
* Both halves go through resolveDocLinks for the same reason the
|
|
385
|
+
* instruction does: this is authored text on its way to becoming a prompt,
|
|
386
|
+
* and the rule is that ALL of it expands. A person answering "which list?"
|
|
387
|
+
* with [[dead-ends]] was sending the model two literal brackets — the one
|
|
388
|
+
* reference syntax the workspace teaches, and the one place it silently did
|
|
389
|
+
* nothing. The previous step's results are NOT resolved, deliberately: that
|
|
390
|
+
* is model output, and a model that happens to emit [[brackets]] must not
|
|
391
|
+
* thereby name a file.
|
|
392
|
+
*
|
|
393
|
+
* An ask: gate approved with no note still says so. It used to add nothing,
|
|
394
|
+
* so the step never learned its question had been answered: gbp-desk's
|
|
395
|
+
* rivals sender, told "a plain approval means all offices", saw no answer
|
|
396
|
+
* at all and stopped BLOCKED (run-muu6r18y-417e, 2026-10-05).
|
|
397
|
+
*/
|
|
398
|
+
export declare function operatorBlock(step: Pick<StepRecord, "ask" | "approvalNote" | "approvedAt">, workspaceRoot: string): string;
|
|
217
399
|
/**
|
|
218
400
|
* Did this attempt call its tools and then fail only on the check after —
|
|
219
401
|
* `verify:`, `output: json`, a schema? For a step whose tools act outward
|
|
@@ -244,6 +426,11 @@ export declare function actedThenFailedCheck(events: {
|
|
|
244
426
|
* happened to create it.
|
|
245
427
|
*/
|
|
246
428
|
export declare function copyTreeBytes(from: string, to: string): void;
|
|
429
|
+
/** A refusal of the credential itself, in any of the wordings Claude Code
|
|
430
|
+
* has used. "Failed to authenticate: OAuth token revoked" (9 Oct 2026, a
|
|
431
|
+
* step started one second before the 09:07:52Z refresh) matched none of the
|
|
432
|
+
* older two, so the step failed instead of waiting for the new token. */
|
|
433
|
+
export declare function isAuthRefusal(text: string): boolean;
|
|
247
434
|
/**
|
|
248
435
|
* Wait, briefly, for the mounted credential to become one we have not
|
|
249
436
|
* already been refused on. Returns the new value, or null when the window
|
|
@@ -251,6 +438,24 @@ export declare function copyTreeBytes(from: string, to: string): void;
|
|
|
251
438
|
* wrong", not "this key is stale".
|
|
252
439
|
*/
|
|
253
440
|
export declare function awaitRotatedCredential(before: string | undefined, now?: () => number, read?: () => string | undefined, waitMs?: number, pollMs?: number): Promise<string | null>;
|
|
441
|
+
/** A step's own model connection cut: the egress proxy or the worker it
|
|
442
|
+
* runs through went away. Only the run's final error, never a tool's — a
|
|
443
|
+
* WebFetch that could not connect is the step's business, not ours. */
|
|
444
|
+
export declare function isModelConnectionLost(text: string): boolean;
|
|
445
|
+
/** A tool call written out as text rather than made: the model's last
|
|
446
|
+
* message holds `<invoke name=…>` / `<function_calls>` markup. Only the tail
|
|
447
|
+
* counts — a reply that quotes such markup mid-way and goes on is fine. */
|
|
448
|
+
export declare function wroteToolCallAsText(text: string): boolean;
|
|
449
|
+
/** Runs again after a tool call written as text. */
|
|
450
|
+
export declare const TEXT_CALL_RETRIES = 1;
|
|
451
|
+
export declare const TEXT_CALL_NOTE: string;
|
|
452
|
+
/** How many times a step whose model connection was cut runs again. */
|
|
453
|
+
export declare const DROP_RETRIES = 2;
|
|
454
|
+
/** May a step whose connection dropped run again? Yes, unless its tools can
|
|
455
|
+
* act outward and one ran in this attempt (actedThenFailedCheck's rule). */
|
|
456
|
+
export declare function dropRetrySafe(canActOutward: boolean, attemptEvents: {
|
|
457
|
+
type: string;
|
|
458
|
+
}[]): boolean;
|
|
254
459
|
/** How many times a busy provider is given another moment, and the longest
|
|
255
460
|
* any one wait may be. Three is the industry's habit and the point where
|
|
256
461
|
* waiting longer stops being kinder than failing. */
|
|
@@ -408,3 +613,37 @@ export declare function waitForRun(tenant: string, workspace: string, runId: str
|
|
|
408
613
|
* nothing is what makes `?wait=true` useful rather than merely blocking.
|
|
409
614
|
*/
|
|
410
615
|
export declare function runResult(run: RunRecord): string | null;
|
|
616
|
+
/**
|
|
617
|
+
* What a Docker-run script sees of the workspace and the account: their
|
|
618
|
+
* scripts/ and their tools/, read-only. A folder tool's code lives in its
|
|
619
|
+
* tools/<name>/ folder, not in scripts/ — without those two mounts every
|
|
620
|
+
* folder tool (the hello template's wordcount among them) was handed a host
|
|
621
|
+
* path the container did not have, and failed on every call. A gallery tool
|
|
622
|
+
* nobody installed runs from the gallery's own shelf, so that is mounted too
|
|
623
|
+
* when there is one. A folder that does not exist is skipped by the copy.
|
|
624
|
+
*/
|
|
625
|
+
export declare function scriptMounts(agentDir: string, tenant: string): Record<string, string>;
|
|
626
|
+
/**
|
|
627
|
+
* The skills an agent can see, nearest first, each with the path the agent
|
|
628
|
+
* is told to read it at. Own skills, then the workspace's, then the
|
|
629
|
+
* workspace's `.agents/skills/` (the cross-client convention, scanned after
|
|
630
|
+
* the native location so a foldrun-native skill wins a name clash), then the
|
|
631
|
+
* account library. The nearest definition of a name wins.
|
|
632
|
+
*
|
|
633
|
+
* The workspace's are written `workspace/skills/…`, the one spelling for the
|
|
634
|
+
* workspace root, which the file tools expand themselves. `../../skills/…`
|
|
635
|
+
* left the model to resolve it against a path it had to guess, and on the
|
|
636
|
+
* hello template it climbed one level too far and was refused.
|
|
637
|
+
*/
|
|
638
|
+
export declare function skillsInScope(agentDir: string, tenant: string): DiscoveredSkill[];
|
|
639
|
+
/**
|
|
640
|
+
* The interpreters a set of scripts needs, read from what they are: the
|
|
641
|
+
* extension of `run:` (or of inline code), or an explicit `interpreter:`.
|
|
642
|
+
* Docker's script image is built from the runtime spec, and the base one is
|
|
643
|
+
* Python only, so a JavaScript tool that declared no `runtime:` (the demo's
|
|
644
|
+
* inline crm-upload, any folder tool's run.mjs) failed with no node to run
|
|
645
|
+
* it. Python is named too whenever a .py script is present, because asking
|
|
646
|
+
* for node alone builds on node:22-slim, which has no python3. Null when
|
|
647
|
+
* nothing beyond the default is needed.
|
|
648
|
+
*/
|
|
649
|
+
export declare function scriptLanguages(scripts: ScriptSpec[]): RuntimeSpec | null;
|