mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Contributors, agents, and image builds run `tsc` to check and compile the repository. > - The repository uses TypeScript 7, which is a native binary. Node heap flags do not limit it. > - The server is one large program, so one `tsc` process needs about 10 GiB. > - The operating system stops `tsc` under a 4 GiB memory limit. > - This pull request checks the server in layers, removes the server from the CLI program, and removes declarations and checks from the image emit. > - The benefit is that the full typecheck and the image `tsc` step now complete in 4 GiB. ## Linked Issues or Issue Description I found no duplicate pull request or issue. A search for `tsc`, memory, typecheck, and OOM found only runtime memory work, which is a different subject. **What happened?** `tsc` used too much memory, and the kernel stopped it with exit code 137 in resource-constrained environments. Measured peaks before this change: | Command | Peak | | --- | --- | | Server `tsc --noEmit` | About 10 GiB | | Server `tsc` emit, as in the Docker build | About 10.7 GiB | | CLI `tsc --noEmit` | About 7 GiB | | `pnpm -r typecheck`, whole process tree | About 10.2 GiB | **Expected behavior** The typecheck and the image `tsc` step complete under a 4 GiB memory limit. **Steps to reproduce** 1. Start a Linux container with `--memory=4g --memory-swap=4g --cpus=4`. 2. Install the dependencies on the base commit. 3. Run `pnpm -r typecheck`. The kernel stops the process with exit code 137. 4. Run `pnpm --filter @paperclipai/server build`. The `tsc` step needs more than 4 GiB. **Paperclip version or commit** Base commit `09162fe893`, with TypeScript 7.0.2. **Deployment mode** Not applicable. This issue applies to development, CI, and image builds. It does not change the server at runtime. ## What Changed - Add `server/scripts/typecheck-layers.mjs`. It reads the import graph on each run and runs one `tsc` process per layer. The layers are the files below the largest import cycle, the cycle, the files above the cycle, and the tests. Each layer reads the earlier layers as declarations. - Add `scripts/run-tsc.mjs`. It adds `--singleThreaded` when `PAPERCLIP_TYPECHECK_LOW_MEMORY=1` is set. The server layers and the UI typecheck use it. - Add `scripts/typecheck-low-memory.mjs`. It checks one package at a time in this mode. It has no `package.json` entry, as `.greptile/rules.md` requires. - Change the server `typecheck` script to build only the Runner TypeScript output. It does not build the Runner release binary now. `pnpm build` and `typecheck:build-gaps` still run the full Runner build and its generated-file checks. - Change the `tsc` step of the server `build` script to `scripts/run-tsc.mjs`. The `Dockerfile` sets `PAPERCLIP_TSC_EMIT_ONLY=1` for that build, so the image emits JavaScript without checks or declarations. Without this variable, `build` still does a full check for npm packages and CI. - Use a non-literal specifier for the `@paperclipai/server` import in `cli/src/commands/run.ts`. The import result already had no useful type, and the literal specifier added the full server to the CLI program. - Add a layered server typecheck step to the `pr-trusted` typecheck job, because `typecheck:build-gaps` does not run this script. - Update `AGENTS.md` and `doc/DEVELOPING.md`. ## Verification Results on Linux use a clean copy of this branch, a new `pnpm install`, Node 24, Rust 1.97.1, 4 CPUs, and a 4 GiB memory limit with no swap. Results before the merge of `master`: | Command | Result | Peak non-reclaimable memory | | --- | --- | --- | | `node scripts/typecheck-low-memory.mjs` | Exit 0, 0 OOM kills | 3,492 MiB | | `PAPERCLIP_TSC_EMIT_ONLY=1 pnpm --filter @paperclipai/server build` | Exit 0 | 3,656 MiB, from the Rust release build | | Server single-program `tsc --noEmit --singleThreaded` | Exit 137, 1 OOM kill | Reached the limit | Results on macOS on the merged head, with peak memory for the full process tree: | Command | Before | After | | --- | --- | --- | | `pnpm -r typecheck` | 10.2 GiB, exit 0 | 5.47 GiB, exit 0 | | `node scripts/typecheck-low-memory.mjs` | Not available | 3.81 GiB, exit 0 | | CLI typecheck | 7.06 GiB | 1.68 GiB, before the merge | Other checks: - Compiler tests: new tests run the real compiler on a small fixture. They prove that the layers emit declarations and pass, that a renamed export fails the layer that imports it, and that a changed type fails the test layer. - Declaration imports: a new compiler test proves that types from `.d.mts` and JSON imports stay correct in later layers. On the real server, a wrong use of `AgentFileManifest` in the cycle layer failed with `TS2353`. - Coverage: a test on the real server source asserts that each source file is in one layer and that no import goes to a later layer. The test layer contains the remaining files. `src/__tests__` stays out of the root list, as in `server/tsconfig.json`. - Error detection: I added one type error to one file in each of the four layers. The typecheck reported all four errors and exited with 1. - Cross-layer errors: I renamed an export in the lower layer. The cycle and upper layers reported the error and the typecheck exited with 1. I reverted these temporary edits. - Emit: full `tsc` and `--noCheck --declaration false --declarationMap false` gave byte-identical JavaScript and source maps for 2,249 files on the base commit. - Runtime: `scripts/server-boot-smoke.sh node server/dist-runtime/index.mjs` passed with a bundle from the emit-only build. - CLI: the esbuild bundle keeps the dynamic import, and `node --check` passed. - Tests on the merged head: `typecheck-layers`, `prepare-runner-vendor`, `cli-invocation-safety`, the CLI `runtime` tests, and all 503 `.github/scripts/tests` tests passed. Before the merge, `release-verify-workflow` and `runner-release-crate-cache` passed. - Full `pnpm --filter @paperclipai/server build` passed before the merge. Failures during this work: - The first low-memory run failed in the UI with `TS5023: Unknown compiler option '-b'`, because the flag came before `-b`. I corrected the argument order and added a test that runs build mode with the flag. - The first rename check did not rename the export, because BSD `sed` does not support `\b`. Its exit 0 was not evidence. I ran the check again with `perl`, and the result above is from that run. - The first version of the real-compiler fixture had a `TS2774` error in its own source. The test found this error, and I corrected the fixture. - CI failed `server-package-build-script.test.ts` on `1abc5e5a43`, because that test reads the steps of the server `build` script, and I had moved them to `build:prepare` and `build:assets`. My first search for readers of this script did not include `server/src`. I removed those scripts and `build:image`, restored one `build` script, and kept the test unchanged. - `GOMEMLIMIT` did not help enough, and I removed it from the plan. On these steps, `3GiB` saved at most about 300 MiB. With default checkers, the cycle layer was stopped with and without it, and the lower layer went from 7 seconds to 151 seconds. Not run or not proven: - I did not run the full `pnpm test:run`, a root `pnpm build`, or a full `docker build`. I expect CI to run the test and build gates. - I did not measure the Docker build steps other than the server build. - I did not run the 4 GiB checks on x86-64. - I did not measure the merged head in the 4 GiB container. The macOS result on the merged head is 3.81 GiB, and the merge added one source file to the lower layer. ## Risks - The margin is small. In the 4 GiB container, the cycle layer peaks about 600 MiB below the limit, and the Rust release build in the image build peaks about 440 MiB below it. Growth of the largest import cycle uses this margin first. - `node scripts/typecheck-low-memory.mjs` applies only to `pnpm -r` packages. These commands still check the server as one program and need more than 4 GiB: `pnpm build`, `test:e2e:runner:typecheck`, `test:lifecycle-baseline:typecheck`, and the `tests/speko-native` project. - A local flow that used the Runner binary from a previous typecheck must now run `pnpm --filter @paperclipai/paperclip-runner build`. - The image does not include server declarations now. I found no consumer in this repository. A downstream image that reads `server/dist/*.d.ts` must change. - An image build does not find type errors now. A typecheck in CI must find them before the image build. - The import scan uses a regular expression. A false edge can only make a layer larger. A missed edge causes a `TS6307` failure, so it cannot skip a file. - The new CI step adds about one minute to the typecheck job. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used Anthropic Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with reasoning, tool use, and code execution. The runtime does not show the context window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
344 lines
20 KiB
Docker
344 lines
20 KiB
Docker
# syntax=docker/dockerfile:1.20
|
|
FROM node:24-trixie-slim AS base
|
|
ARG USER_UID=1000
|
|
ARG USER_GID=1000
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates gosu curl gh git wget ripgrep python3 tini \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& corepack enable
|
|
|
|
# Modify the existing node user/group to have the specified UID/GID to match host user
|
|
RUN usermod -u $USER_UID --non-unique node \
|
|
&& groupmod -g $USER_GID --non-unique node \
|
|
&& usermod -g $USER_GID -d /paperclip node
|
|
|
|
FROM base AS deps
|
|
WORKDIR /app
|
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
|
|
COPY cli/package.json cli/
|
|
COPY server/package.json server/
|
|
COPY ui/package.json ui/
|
|
COPY packages/shared/package.json packages/shared/
|
|
COPY packages/db/package.json packages/db/
|
|
COPY packages/adapter-utils/package.json packages/adapter-utils/
|
|
COPY packages/google-sheets-mcp-server/package.json packages/google-sheets-mcp-server/
|
|
COPY packages/kv-demo-mcp-server/package.json packages/kv-demo-mcp-server/
|
|
COPY packages/mcp-server/package.json packages/mcp-server/
|
|
COPY packages/paperclip-eval-kernel/package.json packages/paperclip-eval-kernel/
|
|
COPY packages/paperclip-runner/package.json packages/paperclip-runner/
|
|
COPY packages/skills-catalog/package.json packages/skills-catalog/
|
|
COPY packages/tailscale-https-broker/package.json packages/tailscale-https-broker/
|
|
COPY packages/teams-catalog/package.json packages/teams-catalog/
|
|
COPY packages/adapters/claude-local/package.json packages/adapters/claude-local/
|
|
COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/
|
|
COPY packages/adapters/cursor-cloud/package.json packages/adapters/cursor-cloud/
|
|
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
|
|
COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/
|
|
COPY packages/adapters/grok-local/package.json packages/adapters/grok-local/
|
|
COPY packages/adapters/kimi-local/package.json packages/adapters/kimi-local/
|
|
COPY packages/adapters/hermes/package.json packages/adapters/hermes/
|
|
COPY packages/adapters/hermes-gateway/package.json packages/adapters/hermes-gateway/
|
|
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
|
|
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
|
|
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
|
|
COPY packages/plugins/sdk/package.json packages/plugins/sdk/
|
|
COPY --parents packages/plugins/sandbox-providers/./*/package.json packages/plugins/sandbox-providers/
|
|
COPY packages/plugins/paperclip-plugin-fake-sandbox/package.json packages/plugins/paperclip-plugin-fake-sandbox/
|
|
COPY packages/plugins/plugin-llm-wiki/package.json packages/plugins/plugin-llm-wiki/
|
|
COPY packages/plugins/plugin-workspace-diff/package.json packages/plugins/plugin-workspace-diff/
|
|
COPY patches/ patches/
|
|
COPY scripts/link-plugin-dev-sdk.mjs scripts/
|
|
COPY packages/paperclip-runner/scripts/install-copilot-assets.mjs packages/paperclip-runner/scripts/materialize-copilot-binary.mjs packages/paperclip-runner/scripts/copilot-inner-distribution.mjs packages/paperclip-runner/scripts/
|
|
COPY packages/paperclip-runner/src/drivers/acpx/copilot-installation.ts packages/paperclip-runner/src/drivers/acpx/
|
|
COPY server/scripts/install-copilot-assets.mjs server/scripts/
|
|
COPY server/src/services/copilot-connection-probe.ts server/src/services/
|
|
|
|
# CI owns repository lock updates. Resolve manifest changes in the image's
|
|
# private build tree, then install from that exact generated lock.
|
|
RUN pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile && pnpm install --frozen-lockfile
|
|
|
|
FROM base AS rust-toolchain
|
|
WORKDIR /app
|
|
# Debian's packaged rust lags the ecosystem (trixie ships 1.85) and the
|
|
# runner's dependency tree now requires a newer rustc. Install rustup from a
|
|
# version-pinned, checksum-verified installer and let the runner's own
|
|
# rust-toolchain.toml choose the compiler — one pin, owned by the runner
|
|
# package, shared by CI and image builds alike.
|
|
#
|
|
# The C toolchain is explicit: apt's cargo used to pull gcc in as a
|
|
# dependency, and rustup does not — without it every build script dies on
|
|
# "linker `cc` not found".
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends gcc libc6-dev pkg-config \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
ENV RUSTUP_HOME=/usr/local/rustup \
|
|
CARGO_HOME=/usr/local/cargo \
|
|
PATH=/usr/local/cargo/bin:$PATH
|
|
ARG RUSTUP_VERSION=1.29.0
|
|
ARG RUSTUP_SHA256_AMD64=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10
|
|
ARG RUSTUP_SHA256_ARM64=9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792
|
|
RUN set -eux; \
|
|
arch="$(dpkg --print-architecture)"; \
|
|
case "$arch" in \
|
|
amd64) rustTarget="x86_64-unknown-linux-gnu"; sha256="$RUSTUP_SHA256_AMD64" ;; \
|
|
arm64) rustTarget="aarch64-unknown-linux-gnu"; sha256="$RUSTUP_SHA256_ARM64" ;; \
|
|
*) echo "unsupported architecture: $arch" >&2; exit 1 ;; \
|
|
esac; \
|
|
curl -fsSLo /tmp/rustup-init "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustTarget}/rustup-init"; \
|
|
echo "${sha256} /tmp/rustup-init" | sha256sum -c -; \
|
|
chmod +x /tmp/rustup-init; \
|
|
/tmp/rustup-init -y --no-modify-path --profile minimal --default-toolchain none; \
|
|
rm /tmp/rustup-init
|
|
# Install the package-owned compiler before any application source enters the
|
|
# stage. rustup-init above installs rustup itself, not the selected compiler.
|
|
COPY packages/paperclip-runner/rust-toolchain.toml /tmp/runner-toolchain/rust-toolchain.toml
|
|
RUN cd /tmp/runner-toolchain && rustup show
|
|
|
|
# Pin the recipe generator and its dependency lockfile. It is a build-only tool
|
|
# and uses the same package-owned compiler as both native build stages.
|
|
FROM rust-toolchain AS rust-chef
|
|
RUN cd /tmp/runner-toolchain && cargo install cargo-chef --version 0.1.73 --locked
|
|
|
|
FROM rust-chef AS runner-plan
|
|
WORKDIR /app/packages/paperclip-runner
|
|
COPY packages/paperclip-runner/rust-toolchain.toml ./
|
|
COPY packages/paperclip-runner/runner ./runner
|
|
RUN cd runner && cargo chef prepare --recipe-path /tmp/runner-recipe.json
|
|
|
|
FROM rust-chef AS runner-deps
|
|
WORKDIR /app/packages/paperclip-runner/runner
|
|
COPY packages/paperclip-runner/rust-toolchain.toml ../
|
|
# The recipe changes only when dependency manifests, the lockfile, or target
|
|
# metadata change. Source edits can reuse this compiled dependency layer.
|
|
COPY --from=runner-plan /tmp/runner-recipe.json /tmp/runner-recipe.json
|
|
RUN cargo chef cook --release --locked --package paperclip-runner-core --bin paperclip-runnerd --recipe-path /tmp/runner-recipe.json \
|
|
&& find . -mindepth 1 -maxdepth 1 ! -name target -exec rm -rf {} +
|
|
|
|
FROM runner-deps AS runner-build
|
|
WORKDIR /app/packages/paperclip-runner
|
|
# Rust embeds protocol schemas and fixtures with include_str!. Keep those
|
|
# alongside the complete Cargo workspace so every compile-time input keys
|
|
# this layer. Ordinary server/UI edits can then reuse the native build.
|
|
COPY packages/paperclip-runner/rust-toolchain.toml ./
|
|
COPY packages/paperclip-runner/runner ./runner
|
|
COPY packages/paperclip-runner/protocol ./protocol
|
|
COPY packages/paperclip-runner/acpx-profiles.json packages/paperclip-runner/antigravity-distributions.json ./
|
|
# Cargo fingerprints source mtimes. Normalize them here and after the full
|
|
# source copy below so a fresh checkout cannot invalidate unchanged inputs.
|
|
RUN find runner protocol -type f -exec touch -d @0 {} + \
|
|
&& touch -d @0 rust-toolchain.toml acpx-profiles.json antigravity-distributions.json \
|
|
&& cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd
|
|
|
|
FROM runner-build AS build
|
|
WORKDIR /app
|
|
COPY --from=deps /app /app
|
|
COPY . .
|
|
RUN find packages/paperclip-runner/runner packages/paperclip-runner/protocol -type f -exec touch -d @0 {} + \
|
|
&& touch -d @0 packages/paperclip-runner/rust-toolchain.toml packages/paperclip-runner/acpx-profiles.json packages/paperclip-runner/antigravity-distributions.json
|
|
# Both the browser bundle and server stamp need the source commit. Declare it
|
|
# after the stable dependency layers, before either application build.
|
|
ARG PAPERCLIP_BUILD_COMMIT=""
|
|
RUN pnpm --filter @paperclipai/ui build
|
|
RUN pnpm --filter @paperclipai/plugin-sdk build
|
|
# The server build runs scripts/write-build-stamp.mjs, which stamps the built
|
|
# commit into dist/build-info.json. The build context has no .git, so the
|
|
# script reads PAPERCLIP_BUILD_COMMIT instead. Docker exposes an ARG to the
|
|
# next RUN as an environment variable. The production stage below declares the
|
|
# same ARG again for the runtime fallback; an ARG goes out of scope at the
|
|
# end of its stage. Empty for local `docker build`, which then writes no stamp.
|
|
# NODE_OPTIONS covers the Node build steps. tsc is a native binary and ignores it.
|
|
ENV NODE_OPTIONS=--max-old-space-size=4096
|
|
# The image needs no declarations, and CI type-checks this source separately.
|
|
# Emitting without either cuts the tsc step from more than 10 GiB to about 2.5 GiB.
|
|
RUN PAPERCLIP_TSC_EMIT_ONLY=1 pnpm --filter @paperclipai/server build
|
|
RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" && exit 1)
|
|
RUN pnpm --filter @paperclipai/server build:runtime \
|
|
&& test -f server/dist-runtime/index.mjs || (echo "ERROR: server runtime bundle missing" && exit 1)
|
|
RUN rm -rf packages/paperclip-runner/runner/target
|
|
|
|
# Remote OpenCode and ACPX runs require a controller-owned provider pack to
|
|
# verify the sandbox installation or stage matching assets. Ship it in the
|
|
# standard image so downstream Cloud compositions inherit the same artifacts.
|
|
# Grok's native executable stays an external sandbox prerequisite; this pack
|
|
# contains only its launcher.
|
|
FROM build AS runner-provider-pack
|
|
# Unstamped local builds remain usable, but cannot qualify a remote pack.
|
|
# Never invent a source revision to make an unqualified pack look verified.
|
|
RUN mkdir -p /provider-pack /provider-pack-transport \
|
|
&& if [ -n "${PAPERCLIP_BUILD_COMMIT}" ]; then \
|
|
PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_BUILD_COMMIT}" node packages/paperclip-runner/scripts/build-provider-pack.mjs /provider-pack \
|
|
&& node scripts/create-provider-pack-archive.mjs /provider-pack \
|
|
&& mv /provider-pack.archives /provider-pack.archive.json /provider-pack-transport/; \
|
|
else \
|
|
echo "Skipping remote provider pack: supply a full PAPERCLIP_BUILD_COMMIT to enable remote OpenCode/ACPX execution"; \
|
|
fi
|
|
|
|
FROM base AS production
|
|
ARG USER_UID=1000
|
|
ARG USER_GID=1000
|
|
# Refreshes the tool layer below when it changes (CI stamps an ISO week, so
|
|
# the @latest CLI tools advance weekly). Without it the cached layer would
|
|
# freeze the tools until an unrelated cache bust.
|
|
ARG CLI_TOOLS_CACHE_EPOCH=""
|
|
WORKDIR /app
|
|
# Tool and OS layer BEFORE the app copy: it references nothing from /app, and
|
|
# the app copy changes on every commit — ordered the other way around, this
|
|
# (the single most expensive layer: four CLI toolchains + apt, per arch) can
|
|
# never hit the layer cache and rebuilds on every build.
|
|
RUN echo "cli-tools-epoch: ${CLI_TOOLS_CACHE_EPOCH}" \
|
|
&& npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai @google/gemini-cli@latest @moonshot-ai/kimi-code@latest \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends openssh-client jq \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& mkdir -p /paperclip \
|
|
&& chown node:node /paperclip
|
|
|
|
COPY scripts/docker-entrypoint.sh /usr/local/bin/
|
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|
|
|
COPY --chown=node:node --from=build /app /app
|
|
|
|
COPY --chown=0:0 --from=runner-provider-pack /provider-pack /opt/paperclip-runner/provider-pack
|
|
COPY --from=runner-provider-pack /provider-pack-transport/ /opt/paperclip-runner/
|
|
# Managed deployments can remap node's UID at startup. This immutable pack
|
|
# contains public code and integrity metadata, never credentials; it must remain
|
|
# readable afterward.
|
|
# Keep it root-owned and verify access as an unrelated unprivileged UID.
|
|
RUN chmod -R a+rX /opt/paperclip-runner/provider-pack \
|
|
&& if [ -f /opt/paperclip-runner/provider-pack/provider-pack.json ]; then \
|
|
gosu 65534:65534 node -e 'const fs = require("node:fs"); const path = require("node:path"); const root = "/opt/paperclip-runner/provider-pack"; const manifest = JSON.parse(fs.readFileSync(path.join(root, "provider-pack.json"), "utf8")); for (const artifact of Object.values(manifest.payload.artifacts)) fs.readFileSync(path.join(root, artifact.path)); fs.accessSync(path.join(root, manifest.payload.artifacts.nodeCommand.path), fs.constants.X_OK);'; \
|
|
fi
|
|
ENV PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH=/opt/paperclip-runner/provider-pack
|
|
|
|
# Declare per-build metadata after the stable RUN layers. Docker includes
|
|
# in-scope ARG values in a RUN's environment even when its command does not
|
|
# mention them; declaring these earlier invalidates the weekly tool cache.
|
|
# The build stage still receives the commit before writing dist/build-info.json.
|
|
# Empty for local builds, preserving the server's normal version fallbacks.
|
|
ARG PAPERCLIP_BUILD_VERSION=""
|
|
ARG PAPERCLIP_BUILD_COMMIT=""
|
|
ENV NODE_ENV=production \
|
|
HOME=/paperclip \
|
|
HOST=0.0.0.0 \
|
|
PORT=3100 \
|
|
SERVE_UI=true \
|
|
PAPERCLIP_HOME=/paperclip \
|
|
PAPERCLIP_INSTANCE_ID=default \
|
|
PAPERCLIP_BUILD_VERSION=${PAPERCLIP_BUILD_VERSION} \
|
|
PAPERCLIP_BUILD_COMMIT=${PAPERCLIP_BUILD_COMMIT} \
|
|
USER_UID=${USER_UID} \
|
|
USER_GID=${USER_GID} \
|
|
PAPERCLIP_CONFIG=/paperclip/instances/default/config.json \
|
|
PAPERCLIP_DEPLOYMENT_MODE=authenticated \
|
|
PAPERCLIP_DEPLOYMENT_EXPOSURE=private \
|
|
OPENCODE_ALLOW_ALL_MODELS=true \
|
|
GEMINI_SANDBOX=false
|
|
|
|
EXPOSE 3100
|
|
|
|
# tini, not node, is PID 1. The entrypoint ends in `exec`, so without an init
|
|
# node inherits PID 1 and never wait()s the orphans the kernel re-parents onto
|
|
# it -- agent runs spawn git/claude/esbuild/sh descendants that outlive their
|
|
# leader, so they pile up as permanent zombies (~79/h measured) until the
|
|
# cgroup pid limit is exhausted and *every* fork() in the container fails.
|
|
# tini reaps adopted orphans and forwards signals, so the exec chain below and
|
|
# graceful shutdown are unchanged. Mirrors docker/agent-runtime/Dockerfile.base.
|
|
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "docker-entrypoint.sh"]
|
|
CMD ["node", "server/dist-runtime/index.mjs"]
|
|
|
|
# Cloud image variant (build with `--target cloud`): the production image
|
|
# plus built bundled sandbox-provider plugins. Managed instances receive a
|
|
# `plugins.autoInstall` key list through PAPERCLIP_MANAGED_CONFIG and
|
|
# install those plugins from the bundled catalog at boot
|
|
# (server/src/services/bundled-plugins.ts), which requires each plugin's
|
|
# dist/ to exist in the image — the default image ships only their source,
|
|
# so auto-install logs "bundle not present" and skips. The plugins are
|
|
# built in this separate target so the default (self-hosted) image stays
|
|
# lean; CI pins the default build to `--target production`. Both targets
|
|
# inherit the build-owned remote provider pack from the standard image.
|
|
#
|
|
# The sandbox providers are intentionally excluded from the pnpm workspace
|
|
# (see pnpm-workspace.yaml), so each installs standalone exactly as its
|
|
# README prescribes. Installing in a `build`-based stage (not `production`)
|
|
# keeps devDependencies available for tsc: `production` sets
|
|
# NODE_ENV=production, which would make pnpm skip them.
|
|
#
|
|
# CLOUD_BUNDLED_PLUGINS is the space-separated list of sandbox-provider
|
|
# directory names to build into the variant. Only what managed deployments
|
|
# actually auto-install belongs here — every entry adds its node_modules
|
|
# to the image. Growing the list is a one-line workflow change.
|
|
FROM build AS cloud-plugins
|
|
ARG CLOUD_BUNDLED_PLUGINS="daytona"
|
|
RUN set -eu; \
|
|
for name in $CLOUD_BUNDLED_PLUGINS; do \
|
|
dir="packages/plugins/sandbox-providers/$name"; \
|
|
test -d "$dir" || { echo "ERROR: unknown sandbox provider '$name'" >&2; exit 1; }; \
|
|
pnpm -C "$dir" install --ignore-workspace --no-lockfile; \
|
|
pnpm -C "$dir" build; \
|
|
test -f "$dir/dist/manifest.js" || { echo "ERROR: $dir is missing dist/manifest.js after build" >&2; exit 1; }; \
|
|
done
|
|
|
|
# The hosted image variant ships selected optional peer packages
|
|
# pre-installed. A managed tenant then needs no separate install step.
|
|
# The self-hosted image stays on the opt-in contract: it never runs this
|
|
# stage, so a package like `@sentry/node` stays a true optional peer
|
|
# dependency. A self-hosted operator installs it by hand (see
|
|
# doc/observability.md).
|
|
#
|
|
# CLOUD_BUNDLED_SERVER_DEPS names the optional peer packages to install.
|
|
# The value is a space-separated list, the same shape as
|
|
# CLOUD_BUNDLED_PLUGINS above. The stage reads each package's version
|
|
# from the `peerDependencies` block of `server/package.json` at build
|
|
# time, so the version has one committed home.
|
|
#
|
|
# The stage fails the build in three cases:
|
|
# - the argument is empty
|
|
# - server/package.json declares no version for a named package
|
|
# - the named package is not an optional peer
|
|
#
|
|
# This check keeps the argument limited to packages the server already
|
|
# treats as optional.
|
|
#
|
|
# The install happens in its own isolated directory, not inside
|
|
# `server`'s own workspace install. The self-hosted target above never
|
|
# gains these packages this way. The directory sits under `/app`, not
|
|
# `server/`, and `--ignore-workspace` below excludes it from the pnpm
|
|
# workspace. From that directory, pnpm still finds the `packageManager`
|
|
# pin in the repo's own `package.json` by walking up — the same pnpm
|
|
# version the rest of the build uses.
|
|
#
|
|
# The install writes no lock file (`--no-lockfile`, the same flag the
|
|
# `cloud-plugins` stage above uses). Two builds of the same commit can
|
|
# therefore install different transitive versions of a named package.
|
|
# Three facts make this an accepted trade-off:
|
|
# - the `cloud-plugins` stage above already has the same property, with
|
|
# the same flag
|
|
# - the direct version of each named package comes from one exact,
|
|
# single-sourced place: the `peerDependencies` block of
|
|
# `server/package.json`
|
|
# - an automated check asserts the installed direct version after every
|
|
# build, so a transitive drift that breaks the package still fails the
|
|
# build
|
|
FROM build AS cloud-server-deps
|
|
WORKDIR /app/.cloud-server-deps
|
|
ARG CLOUD_BUNDLED_SERVER_DEPS="@sentry/node"
|
|
RUN set -eu; \
|
|
test -n "$CLOUD_BUNDLED_SERVER_DEPS" || { echo "ERROR: CLOUD_BUNDLED_SERVER_DEPS is empty; name at least one optional peer package to install" >&2; exit 1; }; \
|
|
echo '{"name":"paperclip-cloud-server-deps","private":true}' > package.json; \
|
|
specifiers=""; \
|
|
for name in $CLOUD_BUNDLED_SERVER_DEPS; do \
|
|
version="$(node -e "const pkg=require('/app/server/package.json'); const name=process.argv[1]; const version=(pkg.peerDependencies||{})[name]; if(!version){console.error('ERROR: server/package.json declares no peerDependencies version for '+JSON.stringify(name));process.exit(1);} const meta=(pkg.peerDependenciesMeta||{})[name]; if(!meta||meta.optional!==true){console.error('ERROR: '+JSON.stringify(name)+' is not declared as an optional peer dependency in server/package.json; CLOUD_BUNDLED_SERVER_DEPS may name only optional peer packages');process.exit(1);} process.stdout.write(version);" "$name")"; \
|
|
test -n "$version" || { echo "ERROR: could not resolve a version for '$name'" >&2; exit 1; }; \
|
|
specifiers="$specifiers ${name}@${version}"; \
|
|
done; \
|
|
test -n "$specifiers" || { echo "ERROR: CLOUD_BUNDLED_SERVER_DEPS names no package" >&2; exit 1; }; \
|
|
pnpm add --ignore-workspace --no-lockfile $specifiers
|
|
|
|
FROM production AS cloud
|
|
COPY --chown=node:node --from=cloud-plugins /app/packages/plugins/sandbox-providers /app/packages/plugins/sandbox-providers
|
|
# Land the isolated install inside the server's own `node_modules`, the
|
|
# directory Node's module resolution walks up to from `/app/server` for
|
|
# both a CommonJS `require.resolve` and an ECMAScript `import` — an entry
|
|
# on `NODE_PATH` would satisfy only the first and silently fail the second.
|
|
COPY --chown=node:node --from=cloud-server-deps /app/.cloud-server-deps/node_modules /app/server/node_modules
|