diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py b/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py index 5e85e7b491..1b27067f98 100644 --- a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py +++ b/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py @@ -2,7 +2,7 @@ # requires-python = ">=3.11" # dependencies = ["daytona"] # /// -"""Build a Daytona snapshot for the WP-8 rivet agent runtime. +"""Build a Daytona snapshot for the WP-8 sandbox-agent runtime. Bakes the `pi` CLI into rivet's `-full` image (which already ships the sandbox-agent daemon, the Claude CLI, and CA certs) so Daytona runs don't pay a ~150s per-invoke @@ -12,6 +12,20 @@ AGENTA_RIVET_DAYTONA_INSTALL_PI=false Run: DAYTONA_API_KEY=... DAYTONA_TARGET=eu uv run build_rivet_snapshot.py [--force] + +Licensing (see services/agent/docker/README.md): + This script is the build recipe we ship, NOT a snapshot we distribute. Whoever + runs it builds the snapshot in their own Daytona account: Agenta Cloud builds + its own for internal use; self-hosters build their own. We never hand anyone a + Claude-containing image, so this is compliant even though the `-full` base bundles + Claude (Anthropic's Commercial Terms forbid us *distributing* Claude Code, not + building/using it). + + Cleaner-provenance follow-up (needs a live Daytona build to verify): base on a + daemon-only rivet image and install Claude from Anthropic at build (npm + `@anthropic-ai/claude-code` or `claude.ai/install.sh`), so the snapshot's Claude + comes straight from Anthropic instead of from a third party's bundled image. Pin + that only after confirming the daemon-only tag also ships the ACP adapters. """ import sys diff --git a/hosting/docker-compose/ee/docker-compose.dev.yml b/hosting/docker-compose/ee/docker-compose.dev.yml index b1f9a2d773..a5ca2c7ec5 100644 --- a/hosting/docker-compose/ee/docker-compose.dev.yml +++ b/hosting/docker-compose/ee/docker-compose.dev.yml @@ -450,8 +450,11 @@ services: DAYTONA_API_KEY: ${DAYTONA_API_KEY:-} DAYTONA_API_URL: ${DAYTONA_API_URL:-} DAYTONA_TARGET: ${DAYTONA_TARGET:-} - # Pre-baked snapshot (rivet daemon + Pi + Claude + certs) so Daytona runs skip - # the ~150s per-invoke `npm install pi`. Built by poc/build_rivet_snapshot.py. + # Pre-baked snapshot (rivet daemon + Pi + certs; Claude inherited from rivet's + # -full base) so Daytona runs skip the ~150s per-invoke `npm install pi`. We + # ship the builder (poc/build_rivet_snapshot.py), not the snapshot itself: each + # operator builds their own, so we never distribute a Claude-containing image. + # See services/agent/docker/README.md for the licensing posture. AGENTA_RIVET_DAYTONA_SNAPSHOT: ${AGENTA_RIVET_DAYTONA_SNAPSHOT:-agenta-rivet-pi} AGENTA_RIVET_DAYTONA_INSTALL_PI: ${AGENTA_RIVET_DAYTONA_INSTALL_PI:-false} # === STORAGE ============================================== # diff --git a/services/agent/docker/Dockerfile b/services/agent/docker/Dockerfile new file mode 100644 index 0000000000..687fea4347 --- /dev/null +++ b/services/agent/docker/Dockerfile @@ -0,0 +1,55 @@ +# Agent runner sidecar (sandbox-agent server), production image. +# +# Runs the TypeScript runner (src/server.ts) as a long-lived HTTP server on :8765. +# The Python agent service calls it in-network. Unlike Dockerfile.dev there is no +# `tsx watch` and no bind mount: the source is baked in. +# +# Licensing posture (see docker/README.md): +# - Pi (@earendil-works/pi-coding-agent, MIT) is baked via the npm dependencies. +# - Claude Code is proprietary (Anthropic Commercial Terms). It is NEVER baked into +# this image. The sandbox-agent daemon installs it at runtime from Anthropic over +# HTTPS (the reason ca-certificates is installed). That keeps Anthropic as the +# distributor, the only compliant path for an image we build and ship. +# - No credential is baked: no API key, no OAuth login. Auth is injected at runtime +# (ANTHROPIC_API_KEY / request secrets; OAuth self-host is a mounted opt-in only). + +FROM node:24-slim + +WORKDIR /app + +# CA certificates: the sandbox-agent daemon (Rust) downloads harness CLIs (e.g. Claude +# Code) over HTTPS using the system trust store, which node:*-slim omits — without this +# the daemon's `install-agent claude` fails TLS verification. git lets npm/installers +# fetch git deps. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +RUN corepack enable + +# Install deps as a cached layer (manifest + lockfile only). The full dependency set is +# installed (not --prod): the runtime uses `tsx` and the extension build uses `esbuild`, +# both devDependencies. +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Bake the source (no bind mount in production). +COPY tsconfig.json ./ +COPY scripts ./scripts +COPY src ./src +COPY config ./config +COPY skills ./skills + +# Bundle the Agenta Pi extension (tracing + tools) into dist/. runSandboxAgent installs +# this baked copy into Pi's agent dir on every run. Rebuild the image after editing +# src/extensions/agenta.ts or the tracer. +RUN pnpm run build:extension + +ENV NODE_ENV=production \ + PORT=8765 + +EXPOSE 8765 + +# Call the local tsx binary directly to avoid pnpm/corepack HOME writes when the +# container runs as a non-root host uid. +CMD ["node_modules/.bin/tsx", "src/server.ts"] diff --git a/services/agent/docker/README.md b/services/agent/docker/README.md new file mode 100644 index 0000000000..63895b109a --- /dev/null +++ b/services/agent/docker/README.md @@ -0,0 +1,66 @@ +# Agent sidecar images + +Images for the agent runner sidecar (the `sandbox-agent server` runtime in +`services/agent/src/server.ts`). The Python service calls it in-network at +`:8765`. + +- `Dockerfile.dev` — dev image. `tsx watch`, source bind-mounted, hot reload. +- `Dockerfile` — production image. Source baked in, no watcher. + +## Licensing posture (read before changing any image or build recipe) + +The rule that shapes every image here: + +> **We ship build recipes, not Claude-containing images, and we never bake a +> credential into any image.** + +Why: + +- **Pi** (`@earendil-works/pi-coding-agent`) is MIT. We bake it freely via the npm + dependencies, in every image and snapshot. +- **Claude Code** is proprietary (© Anthropic PBC, governed by Anthropic's + [Commercial Terms](https://www.anthropic.com/legal/commercial-terms); + [legal & compliance](https://code.claude.com/docs/en/legal-and-compliance)). The + Commercial Terms grant a usage license only. They do not grant any right to + redistribute, resell, sublicense, or repackage the Services. So an image **we + build and distribute must not contain Claude Code.** +- Claude Code is installed **from Anthropic** (`npm install -g + @anthropic-ai/claude-code`, `https://claude.ai/install.sh`, or the daemon's + `install-agent claude`). That keeps Anthropic as the distributor, which is the + permitted path. The production sidecar does this at runtime; a snapshot we build + for our own use does it at build time. + +## Authentication + +Auth is injected at runtime, never baked into a layer. + +- **API key (default, and the only option for cloud / multi-tenant).** Set + `ANTHROPIC_API_KEY` (or pass provider keys as request secrets from the vault). + Anthropic directs products and services that interact with Claude to use API key + auth, so this is the path for any Agenta-orchestrated run that serves users. +- **OAuth subscription (self-host opt-in only).** An individual operator may mount + their own Claude login (e.g. `~/.claude`) into the container and run with their + own subscription. This is for personal, individual use of Claude Code, never for + serving other users, and it is the operator's responsibility. Anthropic restricts + Free/Pro/Max OAuth to first-party use and forbids third parties routing requests + through it (enforced since 2026-03). Cloud and multi-tenant deployments must stay + API-key only. + +We never bake an OAuth login or an API key into an image. + +## Build recipes (two paths) + +- **Cloud / Daytona (API key).** The Daytona snapshot recipe bakes Pi. Agenta Cloud + builds and uses its own snapshot internally; self-hosters run the same recipe + against their own Daytona account. We ship the build script (the recipe), not the + built snapshot, so we never distribute a Claude-containing artifact. Snapshot + builder: `docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py`. + Today it bases on rivet's `-full` image, which already bundles Claude. That is + compliant under the recipe-not-image model. **Cleaner-provenance follow-up + (needs a live Daytona build to verify):** base on a daemon-only rivet image and + install Claude from Anthropic at build, so the snapshot's Claude comes straight + from Anthropic rather than from a third party's bundled image. Relocation of the + builder into this folder is a follow-up. +- **Self-host (API key, OAuth optional).** Build the production `Dockerfile` (it + bakes neither Claude nor a credential), then supply auth at runtime: an + `ANTHROPIC_API_KEY` env var, or, for individual use, a mounted OAuth login dir.