|
| 1 | +# Agent Runtime Image Family |
| 2 | + |
| 3 | +Paperclip.inc publishes container images for remote agents. Images are named `agent-runtime-{adapterType}:{paperclipVersion}` and distributed via `ghcr.io/paperclipinc/`. |
| 4 | + |
| 5 | +## Image Lineup |
| 6 | + |
| 7 | +- **`agent-runtime-base`**: Foundation. Ubuntu 22.04 + Node 22 + git + tini + non-root user + shim + workspace-init. |
| 8 | +- **`agent-runtime-claude`**: Extends base with `@anthropic-ai/claude-code` CLI globally installed. |
| 9 | +- Future: Additional adapter-specific images follow the same pattern (e.g., `agent-runtime-go`, `agent-runtime-rust`). |
| 10 | + |
| 11 | +## Base Image Contents |
| 12 | + |
| 13 | +**OS & Runtime:** |
| 14 | +- Ubuntu 22.04 |
| 15 | +- Node.js 22 (via NodeSource APT repo) |
| 16 | +- git |
| 17 | +- tini (PID-1 init, ensures signal propagation) |
| 18 | +- Non-root user `paperclip` (uid/gid 1000) |
| 19 | + |
| 20 | +**Paperclip Binaries:** |
| 21 | +- `/usr/local/bin/paperclip-agent-shim` — Go binary compiled from `tools/agent-shim/`. Reads `/run/paperclip/runtime-command.json` and `syscall.Exec`s the adapter CLI. |
| 22 | +- `/usr/local/bin/paperclip-workspace-init` — Node script entry point. Used by init container to bootstrap the workspace. |
| 23 | + |
| 24 | +**Defaults:** |
| 25 | +- `USER`: 1000:1000 (paperclip, non-root) |
| 26 | +- `WORKDIR`: `/workspace` — PVCs are mounted here |
| 27 | +- `ENTRYPOINT`: `/usr/bin/tini --` (PID-1 reaper, forwards signals) |
| 28 | +- `CMD`: `/usr/local/bin/paperclip-agent-shim` |
| 29 | + |
| 30 | +## Building Locally |
| 31 | + |
| 32 | +### Multi-architecture (amd64 + arm64) |
| 33 | + |
| 34 | +```bash |
| 35 | +docker buildx bake -f docker/agent-runtime/buildx-bake.hcl --push |
| 36 | +``` |
| 37 | + |
| 38 | +### Host-only (faster iteration) |
| 39 | + |
| 40 | +Replace the architecture with your machine's native platform: |
| 41 | + |
| 42 | +```bash |
| 43 | +docker buildx bake -f docker/agent-runtime/buildx-bake.hcl \ |
| 44 | + --set "*.platforms=linux/$(uname -m | sed s/x86_64/amd64/)" \ |
| 45 | + --load |
| 46 | +``` |
| 47 | + |
| 48 | +### Custom tag or registry |
| 49 | + |
| 50 | +```bash |
| 51 | +docker buildx bake -f docker/agent-runtime/buildx-bake.hcl \ |
| 52 | + --set "*.tags=myregistry/agent-runtime-base:mytag" \ |
| 53 | + --load |
| 54 | +``` |
| 55 | + |
| 56 | +## Quickstart Smoke Test |
| 57 | + |
| 58 | +Build and verify the `agent-runtime-claude` image runs locally: |
| 59 | + |
| 60 | +```bash |
| 61 | +docker buildx bake -f docker/agent-runtime/buildx-bake.hcl \ |
| 62 | + --set "*.platforms=linux/$(uname -m | sed s/x86_64/amd64/)" \ |
| 63 | + --load |
| 64 | +docker run --rm ghcr.io/paperclipinc/agent-runtime-claude:dev claude-code --version |
| 65 | +``` |
| 66 | + |
| 67 | +## Init Container (workspace-init) |
| 68 | + |
| 69 | +The init container prepares the workspace before the agent starts. It reads environment variables, bootstraps the workspace directory tree, and exits. |
| 70 | + |
| 71 | +**Environment Variables:** |
| 72 | +- `PAPERCLIP_WORKSPACE_REQUEST` — JSON serialized workspace request (required) |
| 73 | +- `PAPERCLIP_WORKSPACE_ROOT` — Where to write workspace state (default: `/workspace`) |
| 74 | +- `BOOTSTRAP_TOKEN` — Authentication token for workspace API (required) |
| 75 | +- `PAPERCLIP_PUBLIC_URL` — Public endpoint for workspace callbacks (required) |
| 76 | + |
| 77 | +**Failure Modes:** |
| 78 | +Missing or invalid env vars → exit code 1. Pod init never repeats; failure blocks agent startup. |
| 79 | + |
| 80 | +## Agent Container (paperclip-agent-shim) |
| 81 | + |
| 82 | +The main agent runs as the shim process (PID 1 under tini). The shim: |
| 83 | + |
| 84 | +1. Reads `/run/paperclip/runtime-command.json` — a JSON file mounted by the Job controller |
| 85 | +2. Parses `{ command, args, ... }` — the adapter CLI and arguments |
| 86 | +3. `syscall.Exec`s the adapter process, replacing itself |
| 87 | +4. SIGTERM from kubelet propagates directly to the adapter (no process zombie) |
| 88 | + |
| 89 | +**runtime-command.json Contract:** |
| 90 | +```json |
| 91 | +{ |
| 92 | + "command": "claude-code", |
| 93 | + "args": ["--token", "xyz", "--workspace", "/workspace"] |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +The shim makes no assumptions about command structure; it is adapter-agnostic. Future adapters swap the command/args; the image remains the same. |
| 98 | + |
| 99 | +## Security Model |
| 100 | + |
| 101 | +- **Non-root execution** — user 1000:1000, no capability grant |
| 102 | +- **PSS Restricted compatible** — no privileged containers, no host mounts, read-only filesystem (except `/workspace` + `/tmp`) |
| 103 | +- **No secrets baked in** — API tokens, credentials come from per-Job ephemeral Secrets mounted as env vars or files |
| 104 | +- **Image signing** — cosign keyless OIDC in CI (see Task 29) |
| 105 | + |
| 106 | +## Versioning Policy |
| 107 | + |
| 108 | +**agent-runtime-base:** |
| 109 | +- Version tag `vX.Y.Z` published when the shim or workspace-init source changes |
| 110 | +- Includes all base layer content (OS, Node, git, tini, non-root user) |
| 111 | + |
| 112 | +**agent-runtime-claude:** |
| 113 | +- Builds on top of base at the same version tag |
| 114 | +- Version tag bumps independently when a new `@anthropic-ai/claude-code` release is pinned |
| 115 | +- Currently uses `npm install @anthropic-ai/claude-code@latest` for the `dev` tag; CI workflow (Task 29) will pin exact semver versions per release |
| 116 | + |
| 117 | +## Multi-arch Caveats |
| 118 | + |
| 119 | +- Both amd64 and arm64 images are built in CI; local builds require `--load` on single-arch or `--push` for multi-arch |
| 120 | +- Go shim cross-compilation is automatic via `GOARCH` (see Dockerfile.base Stage 1) |
| 121 | +- Node modules are platform-agnostic; workspace-init rebuilds without issues across architectures |
0 commit comments