Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the subtle compliance case. The -full base bundles Claude, so the built snapshot contains Claude. It stays compliant only because we ship this script and each operator builds the snapshot in their own account; we distribute nothing. The cleaner-provenance follow-up (daemon-only base + install from Anthropic at build) is parked pending a live Daytona check that the daemon-only tag ships the ACP adapters.


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
Expand Down
7 changes: 5 additions & 2 deletions hosting/docker-compose/ee/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ============================================== #
Expand Down
55 changes: 55 additions & 0 deletions services/agent/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Load-bearing: ca-certificates is here because the daemon fetches Claude from Anthropic over TLS at runtime. node:*-slim omits the trust store, so without this install-agent claude fails verification. This is the mechanism that keeps Anthropic as the distributor and the image Claude-free.

&& 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 ./

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full dependency set is installed (not --prod) on purpose: the runtime uses tsx and the extension build uses esbuild, both devDependencies. Worth confirming this stays intentional if anyone later tries to slim the image.

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"]
Comment on lines +16 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the final container as a non-root user.

The image still launches the HTTP server as root. For a network-facing agent runner, that widens the blast radius of any compromise.

🔧 Suggested fix
 RUN pnpm run build:extension
 
 ENV NODE_ENV=production \
     PORT=8765
 
 EXPOSE 8765
+USER node
 
 CMD ["node_modules/.bin/tsx", "src/server.ts"]

Source: Linters/SAST tools

66 changes: 66 additions & 0 deletions services/agent/docker/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading