From e9ed479b93b1196bf56d819723d2120bb96c184e Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 1 Aug 2026 01:20:11 -0700 Subject: [PATCH] [eric] runner: the image had node but no npm, so CreateApp scaffolded an app it could never build --- openswarm-runner/Dockerfile | 32 +++++++++++++++++++++++++-- openswarm-runner/README.md | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/openswarm-runner/Dockerfile b/openswarm-runner/Dockerfile index d5359e70..31fec01b 100644 --- a/openswarm-runner/Dockerfile +++ b/openswarm-runner/Dockerfile @@ -39,6 +39,21 @@ RUN printf '{"name":"router-stage","version":"0.0.0","private":true}\n' > packag && test -f node_modules/9router/app/server.js \ && test -z "$(find node_modules/9router -name '*.node' -print -quit)" +# The App Builder's template dependencies, installed once here and shipped ALREADY EXTRACTED at +# the digest path backend/apps/outputs/view_builder_templates.py already probes. Without it the +# first CreateApp in a run pays a cold npm install against the public registry, and a run with no +# egress just fails. NOT $BUILDPLATFORM: vite pulls in a platform-specific esbuild, so this has to +# resolve on the arch the container will actually run on. +FROM node:${NODE_VERSION}-bookworm-slim AS webapp-template +WORKDIR /stage +COPY backend/apps/outputs/webapp_template/frontend/package.json ./package.json +RUN set -eux; \ + npm install --no-audit --no-fund --loglevel=error --ignore-scripts; \ + test -x node_modules/.bin/vite; \ + digest="$(sha256sum package.json | cut -c1-12)"; \ + mkdir -p "/out/${digest}"; \ + mv node_modules "/out/${digest}/node_modules" + # Webpack output is architecture-independent, so this runs natively on the build host rather than under emulation. FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS frontend WORKDIR /src @@ -104,6 +119,10 @@ RUN set -eux; \ rm -rf /var/lib/apt/lists/* COPY --from=node /usr/local/bin/node /usr/local/bin/node +# npm and npx too, not just node. They are shims into lib/node_modules, so copying the tree and +# re-linking is the only way to get them; a `node` with no `npm` is what left the App Builder +# scaffolding an app it could never install, build or serve. +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=pydeps /opt/pydeps /usr/local # Numeric owner on every /app copy, because a `chown -R /app` afterwards rewrites the whole tree into a second layer and the image pays for it twice (that cost 493MB before this line existed). Numeric, not `runner`, because the user is created further down. @@ -119,8 +138,14 @@ COPY --from=frontend --chown=10001:10001 /src/dist /app/frontend COPY --from=uv --chown=10001:10001 /stage/uv /app/backend/uv-bin/uv COPY --from=uv --chown=10001:10001 /stage/uvx /app/backend/uv-bin/uvx +# Also after backend/, and at the exact path bundled_extracted_modules() looks for. +COPY --from=webapp-template --chown=10001:10001 /out /app/backend/apps/outputs/webapp_template_cache + RUN set -eux; \ if ls /app/backend/.env* >/dev/null 2>&1; then echo "a dotenv reached the image; fix Dockerfile.dockerignore" >&2; exit 1; fi; \ + ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm; \ + ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx; \ + npm --version >/dev/null; \ mkdir -p /app/python-env/bin; \ ln -s /usr/local/bin/python3 /app/python-env/bin/python3; \ find /app/backend -name '__pycache__' -type d -prune -exec rm -rf {} +; \ @@ -129,7 +154,8 @@ RUN set -eux; \ ln -s /data/openswarm /app/backend/data; \ mkdir -p /tmp/.X11-unix; \ chmod 1777 /tmp/.X11-unix; \ - chown runner:runner /data + chown runner:runner /data; \ + printf '[user]\n\tname = OpenSwarm Cloud Run\n\temail = cloud-run@openswarm.local\n[init]\n\tdefaultBranch = main\n[safe]\n\tdirectory = *\n' > /etc/gitconfig USER runner WORKDIR /app @@ -144,6 +170,8 @@ ENV HOME=/home/runner \ OPENSWARM_PORT=8324 \ DATA_DIR=/data/9router \ NODE_ENV=production \ - ELECTRON_BIN=/app/electron-runtime/electron + ELECTRON_BIN=/app/electron-runtime/electron \ + OPENSWARM_RUN_WORKSPACE=/data/workspace \ + OPENSWARM_NODE_PATH=/usr/local/bin/node ENTRYPOINT ["python3", "-m", "runner.main"] diff --git a/openswarm-runner/README.md b/openswarm-runner/README.md index 0bcdb45a..3e8606a5 100644 --- a/openswarm-runner/README.md +++ b/openswarm-runner/README.md @@ -42,6 +42,50 @@ Exit codes: `0` ok, `1` runner crash, `2` bad spec, `3` credential expired on ar `4` backend never came up, `5` workflow failed, `6` wall-clock cap hit, `7` no Electron window ever registered. +## Files the run makes + +`/data/workspace` is the agent's working directory and **the only path whose contents survive**. +It is seeded as `default_folder` before the backend boots, and the agent is told in its system +prompt that files saved there come back and everything else is destroyed. + +After the workflow reaches a terminal state (including a timeout, so partial work still lands) the +runner walks that directory and POSTs each file to `callback.artifacts_url`, then sends the +terminal report. That order is load-bearing: the per-run callback token is refused once the run is +closed, so uploading afterwards would be rejected. + +Caps, applied in the runner AND again at the control plane, which is the one that counts: + +| Limit | Value | +| --- | --- | +| One file | 20 MB | +| One run, all files | 50 MB | +| Files per run | 40 | + +Nothing is ever truncated. A file past a cap is not sent and instead arrives as a row in the +report's `files[]` carrying a written reason, so the user reads "your 512 MB render could not be +sent" rather than finding a 20 MB fragment. `.git`, `node_modules`, `__pycache__`, `.venv`, +`.claude` and the usual caches are skipped, and symlinks are never followed. + +## Skills and connected apps + +`skills[]` in the run spec is written to `~/.claude/skills//` before boot. This is not a +nicety: the backend registers the Skill tool only when at least one non-built-in skill exists on +disk, so a container without them has no Skill tool at all and answers from general knowledge in +the same confident voice it would use with the real thing. + +`unavailable_mcp_servers[]` is **names only**. The user's MCP credentials (Slack session cookies, +Notion and GitHub access tokens, Google refresh tokens) never leave their machine, so the names go +up purely so the run's system prompt can tell the agent which apps exist and are out of reach. +`McpServerNote` forbids extra fields, so there is no shape a secret could travel in. + +## What the image carries for the App Builder + +`node`, `npm` and `npx`, plus the App Builder template's `node_modules` pre-installed at the digest +path `bundled_extracted_modules()` probes. Without npm, `CreateApp` scaffolded an app that could +never install, build or serve; without the baked cache, the first `CreateApp` in a run would pay a +cold registry install. `git` also carries a system identity (`/etc/gitconfig`), so a workflow that +commits does not die on "Author identity unknown". + ## The renderer OpenSwarm's browser tier is not an HTTP client. Element serialization and every click,