mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-02 02:07:25 +08:00
## Thinking Path > - Paperclip manages agents through a shared native runner. > - Built-in harness support should ship with Paperclip's public distribution. > - Grok already speaks ACP; it does not require a new public bridge package. > - Sandbox provisioning owns the native executable and its pinned version. > - The runner must verify that prerequisite without downloading it during npm installation. > - This change separates built-in launcher identity from external runtime identity. > - Clean npm installation and live staging checks verify the distribution boundary. ## Linked Issues or Issue Description Refs #13882, #13973, #13977, #13979. This follow-up now targets master after #13882 was squash-merged. It replaces the private `@paperclipai/grok-acp` workspace package with runner-owned assets. Current master is included so the branch also contains the merged scheduler, complete-event capture, and durable cleanup fixes. ## What Changed - Ship Grok launcher and qualification metadata inside the runner's compiled output and the public server's vendored runner tree. - Remove the separate Grok npm package and all package-manager install hooks for this runtime. - Require the checksum-verified Grok Build 1.0.13 binary at `/opt/paperclip/providers/grok/1.0.13/grok` in the selected execution environment. Provision it explicitly in the Daytona image and CI setup. - Keep native binaries outside the provider pack. Bind the built-in launcher into the pack manifest. - Preserve executable leases, descriptor-backed startup, credential fences, permissions, and exact ACP model admission. - Use `builtin:grok-acp` and `native:grok` as profile identities. Historical package-profile sessions fail closed on resume rather than being silently reinterpreted. - Resolve built-in assets from the authenticated sidecar location, including public server npm layouts. Keep the controller path out of provider environments. - Add clean npm tarball installation verification to the existing trusted canary CI job and the admitted manual EC2 verification path. It stages a unified release version and runs npm lifecycle scripts, then verifies missing-prerequisite rejection and admission after separate provisioning without credentials or inference. - Include the controller-owned provider pack in stamped Cloud images. Unstamped local images omit the pack and remain usable; remote ACPX requires full source provenance. - Correct CLI approval-page metadata for an already authenticated Cloud board user; approval authorization remains unchanged. - Honor explicit native-runner enablement in the Cloud agent picker and direct setup page, keeping the flag disabled by default. - Allow selecting the execution environment before connecting credentials. Include Grok in the existing authenticated hello-probe flow, targeting its pinned native prerequisite for runner setup. - Recover an existing subscription sign-in conflict through an explicit cancel-and-retry action, serialized after cancellation succeeds. - Preserve the selected ACPX harness before normalizing config fields, so new Grok agents use the Grok default model. - Keep the credential-free Cloud provider pack root-owned and readable after runtime UID remapping; verify manifest and referenced asset access under an unrelated unprivileged UID during image builds. - Archive prior failover backups alongside explicitly replaced harness state, preserving evidence while preventing stale backups from blocking a fresh replacement. - Update Daytona image content inputs and contract tests for the built-in assets and explicit provisioner. - Document and regression-test the shared `approve-all` default for Grok setup, saved configuration, and native execution. Explicitly saved restrictions remain unchanged. ## Verification Current merge-repair head `df09eb3e1a619430ad8419a0ee9aedd486689b05` incorporates master `f1a394bd30cb56fb9e479f98b9f50176fe921858` after the base PR was squash-merged. All 12 conflicts came from incoming files identical to the tested pre-squash base. The final tree exactly matches a three-way merge using that original base, preserving built-in Grok distribution and removal of the obsolete private package. All 252 focused runner/UI tests, six npm-isolation tests, and token gates pass. Fresh exact-head Greptile review is 5/5 with no outstanding findings; security scans and EC2 native compilation pass. All current-head CI is green: 56 successful checks/statuses and four intentional skips ([run 36468768035](https://github.com/paperclipai/paperclip/actions/runs/36468768035)). The repository owner explicitly authorized bypassing code-owner approval after all checks passed; no CI checks or repository protection settings are bypassed or changed. The only remaining PR was removed from the completed stack metadata to permit native auto-merge. Earlier integration head `78cb306ecc41b5c96577c26c1d89153b0ef865a1` includes master `3447609d2247e75e55d91493dda91a608364f672` (2026-09-28). Two master advances during verification overlapped the eval catalog; the final merge preserves Grok qualification, completion updates, and bounded API-response reading in all 348 cells. All 77 focused catalog/eval/workflow tests pass. Both native stack layers (#14397) are mergeable, and both exact-head Greptile reviews are 5/5 with successful security scans and no unresolved review threads. All current-head CI is green: 56 successful checks/statuses and four intentional skips ([CI attempts](https://github.com/paperclipai/paperclip/actions/runs/36447124691)). The initial attempt lost two EC2 runners to shutdown signals and stalled a third shard during dependency preparation; all three passed the same-commit failed-job-only retry. Trunk code-owner requirements remain enforced. The review summary’s non-blocking saved-asset offset classification note concerns code already merged in #14301; those runtime files are identical to master and outside this stack’s diff. Historical live evidence below retains its original source revisions. [Final public npm verification](https://github.com/paperclipai/paperclip/actions/runs/36445542764) passed on `76ea70cd4d13786a042af9df82f0fd7a8c85ae30`: 17 public packages, an executed offline lifecycle sentinel, unchanged consumer lock, built-in launcher, missing-prerequisite rejection, and verified separately provisioned binary/command lease. Provisioning and cleanup require no host privilege elevation; only the positive probe mounts the temporary native binary read-only. The verifier is unchanged by the final master merge. All six isolation tests and an offline npm smoke test pass. The prior head had 56 green CI checks and a 5/5 review after two unchanged tests timed out and passed a failed-job-only retry ([CI attempts](https://github.com/paperclipai/paperclip/actions/runs/36444597313)). All 56 recovery-display/lineage tests pass; re-review cleared the already-covered missed-retry concern. Earlier EC2 failures remain retained: [npm lockfile rejection](https://github.com/paperclipai/paperclip/actions/runs/36436311203), [missing compiler in the slim image](https://github.com/paperclipai/paperclip/actions/runs/36440210984), and the aggregate 15-minute test timeouts in those broad runs. Both broad attempts passed typecheck, token gates, Product E2E type/unit checks and build. The focused EC2 lane preserves the existing trusted-actor and immutable-source gates. Earlier documentation/test checkpoint `ff244c4fd78a7ede5a3e00efe09f475f133ef33e` leaves runtime behavior unchanged. 154 focused tests pass across configuration building, native provider resolution, permission policy, credentials, UI configuration, and new-agent setup (including both Grok auth modes); token gates pass. All fresh CI is green for this head: 56 successful checks/statuses and two intentional skips ([run 36367065119](https://github.com/paperclipai/paperclip/actions/runs/36367065119)). Greptile is 5/5 with no new findings. Grok already inherits the shared `approve-all` default, so unattended setup requires no manual permission change. Runtime head `bb5a9307991f1ac567b781970ef11b39d518e19b` fixes a final staging continuation failure before provider startup: explicit replacement archived the old harness but left its failover backups active, which caused `runner_harness_state_mismatch`. The regression fails before the fix and passes after it; all eight adjacent recovery-safety cases also pass. Old backups remain inspectable inside the continuity archive. All fresh CI is green at this head ([run 36360839248](https://github.com/paperclipai/paperclip/actions/runs/36360839248)), with a 5/5 review. One unrelated Cursor test timed out in the initial server shard; the same-commit failed-job rerun passed, and both attempts are retained. Staging deployment is confirmed healthy on this revision. The controller image is `ghcr.io/paperclipai/paperclip@sha256:6ad91c487910ccd2596ff7aed0a3a3ea5233d12b51b83cd6e1402237749b9673`. The final browser-created staging task passed on this exact revision with API authentication: context read → structured human question → controller restart → answer submission → same native provider session resumed → document saved → task Done. The two turns took approximately 119s and 77s. The actual write receipt was applied, and the saved document has exactly one revision containing the selected answer and requested marker. Usage and cost were not reported. [Controller image build](https://github.com/paperclipai/paperclip/actions/runs/36360889243). - Previous integration head `a44f7dbb6b6f77cd9ed893756ca453307f281e5f`: all CI green (53 successful checks/statuses, two intentional skips), including repository typecheck/build/tests, native Runner tests, browser shards, and canary installation checks. [CI run 36358672529](https://github.com/paperclipai/paperclip/actions/runs/36358672529). Greptile is 5/5 with no unresolved findings. - Focused checks cover Grok credentials, executable admission, launcher assets, provider-pack paths/permissions, workflow contracts, setup defaults, CLI authorization, and subscription conflict recovery. All 39 protocol definitions validate. Final integration checks pass 124 catalog/evidence/cache tests and nine project-form tests; token gates pass. Some local dependency checks could not load the stale installed dependency tree; the corresponding fresh EC2 checks pass. - Clean public npm installation passed on EC2 at `8b172ebcf8e02e30662d830c00f3961e3bd459ec` ([run 36164964900](https://github.com/paperclipai/paperclip/actions/runs/36164964900)): 17 unified-version packages, lifecycle scripts enabled, built-in launcher present, no separate Grok package or npm-downloaded binary, missing prerequisite rejected, separately provisioned native executable and command lease verified. No credentials or inference were used. Subsequent changes preserve this npm asset layout. - The immutable Daytona prerequisite image is `ghcr.io/paperclipai/paperclip-daytona-runner@sha256:98957d5be0ac774d086b6402b5849e8e6356fec70fb8c09fca6eb4ed6de918e0`, built from `5a2db471f3ddabe77f9f80e76ed27f996cb97fba`. The previous Cloud controller image was `ghcr.io/paperclipai/paperclip@sha256:fd914e1ab1e45f741e8e078ff452d16f082d7ac05f9b4b3506d3a3c64150d204`, built from `a44f7dbb6b6f77cd9ed893756ca453307f281e5f`; it is superseded by the latest image above. Its EC2 build verified provider-pack access under an unrelated unprivileged UID. - Browser staging at `40f898bc4cba73c1dff4e6344a3983ba0fb247ef` passed full Grok onboarding with the correct `grok-4.7` model, saved credential delivery, and pinned Daytona execution. A browser-created task read context and asked the structured human question. After a controller restart, answering the persisted question resumed the same native provider session, saved the requested document, and completed the task. Actual tool outcomes and durable state agree: one question and one document revision. The two successful turns took 42.7s and 63.1s; usage and cost were not reported. - Restricted policy returned the expected `approval_required` outcome. Functional staging tests explicitly selected `approve-all`; controller authorization and governed approvals remain enforced. Temporary board CLI access was revoked and verified rejected (HTTP 401), and the disposable onboarding agent was paused. Failures remain retained: the pre-fix continuation failure (its task remains blocked; the passing final task is fresh), the original Cloud provider-pack permission failure, the expected restricted-policy denial, the superseded npm staging failure, and an earlier monolithic CI infrastructure timeout. Browser CI exposed a project alias/form race; the final stack uses master's stronger draft-preservation fix and all browser shards pass. Historical full subscription/API protocol and Product rosters retain their original source revisions and do not qualify this packaging revision. No local Docker or Rust build was used. ## Risks The branch includes master’s draft-preservation fix for project URL aliases. It keeps the same project’s edit form mounted and clears prior data when the project or company changes. Custom sandboxes and local execution hosts must provision the pinned binary before Grok starts. Missing, changed, unsupported-platform, and symlinked executables fail admission. The new builtin profile cannot resume sessions created with the former private-package profile. Existing Claude/Codex npm bridge profiles retain their package pins. Grok restricted modes preserve the selected policy but cannot automatically admit Paperclip calls: ACP permission metadata does not independently bind tool authority, so those calls stop with `approval_required`. New Grok configurations default to `approve-all`, including API configurations that omit the mode. Existing explicitly restricted configurations remain restricted; controller authorization and governed approvals remain enforced. ## Model Used OpenAI GPT-6 through Codex, with tool use and code execution. The exact serving model identifier and context-window size are not exposed in this session. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
325 lines
18 KiB
Docker
325 lines
18 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/
|
|
|
|
RUN 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
|
|
# 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 \
|
|
&& 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
|
|
# 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.
|
|
ENV NODE_OPTIONS=--max-old-space-size=4096
|
|
RUN pnpm --filter @paperclipai/server build
|
|
RUN test -f server/dist/index.js || (echo "ERROR: server build output missing" && exit 1)
|
|
RUN rm -rf packages/paperclip-runner/runner/target
|
|
|
|
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
|
|
|
|
# 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", "--", "docker-entrypoint.sh"]
|
|
CMD ["node", "--import", "./server/node_modules/tsx/dist/loader.mjs", "server/dist/index.js"]
|
|
|
|
# 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`, which is
|
|
# byte-identical to before this stage existed.
|
|
#
|
|
# 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
|
|
|
|
# ACPX remote runs require a controller-owned provider pack to verify the
|
|
# sandbox installation or stage matching assets. Grok's native executable stays
|
|
# an external sandbox prerequisite; this pack contains only its launcher.
|
|
FROM build AS cloud-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 \
|
|
&& if [ -n "${PAPERCLIP_BUILD_COMMIT}" ]; then \
|
|
PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_BUILD_COMMIT}" node packages/paperclip-runner/scripts/build-provider-pack.mjs /provider-pack; \
|
|
else \
|
|
echo "Skipping remote provider pack: supply a full PAPERCLIP_BUILD_COMMIT to enable remote ACPX execution"; \
|
|
fi
|
|
|
|
FROM production AS cloud
|
|
COPY --from=cloud-provider-pack /provider-pack /opt/paperclip-runner/provider-pack
|
|
# Cloud remaps 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
|
|
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
|