mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] runner: the image had node but no npm, so CreateApp scaffolded an app it could never build
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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/<id>/` 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,
|
||||
|
||||
Reference in New Issue
Block a user