Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124# radial-turn: the sandboxed image `radiald run` launches (via `docker run`, see# packages/daemon/src/container.ts's `dockerRunArgs`) once per turn.## Build from the repo root:# docker build -f docker/Dockerfile -t radial-turn:latest .# (scripts/build-images.mjs / `pnpm images` does exactly this, after `pnpm build`.)## Assumptions, called out because Docker cannot be run in this development environment to verify# them directly (see the phase-3 workstream E-final report for what was and wasn't exercised):# - The build stage needs network access to the npm registry (`pnpm install`); the runtime stage# needs it too, to `npm install -g` pnpm and the three agent CLIs. Both are operator/build-time# concerns, not exercised by this repo's own `pnpm build`/`test` gate.# - `pnpm deploy --legacy --prod` was verified locally (outside Docker) against this workspace:# it assembles a self-contained `@radial/sidecar` install, resolving its workspace dependencies# (@radial/core, @radial/atproto, @radial/ingest — none of which declare any *runtime* npm# dependencies of their own) into a real `node_modules`. That means the runtime stage needs# none of the rest of the monorepo, nor a pnpm virtual store — just the deployed directory.# (`--legacy` is required: pnpm v10's default injected-workspace deploy needs# `inject-workspace-packages=true`, which this workspace does not set.)# - The daemon supplies mounts (/work rw for implementation turns and ro otherwise, /bundle ro,# /run/radial rw), env (RADIAL_SIDECAR_SOCKET, RADIAL_TURN_TOKEN, whichever provider# credential names the acting profile's harness declares — `Harness.credentialEnv` plus# `run.modelEnv`, so ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN for claude, any of pi's# provider keys for pi, and CODEX_API_KEY / CODEX_ACCESS_TOKEN or a scratch managed-auth home# for codex —# GH_TOKEN or, on a tangled project, GIT_SSH_COMMAND +# RADIAL_PUSH_REMOTE alongside a read-only /run/radial-forge mount, see# turn.ts), and the argv of whichever harness the profile names (`claude -p ...`,# `pi --mode json ...` or `codex exec --json ...`, see harness.ts) at# `docker run` time. This image deliberately sets no ENTRYPOINT/CMD and no opinionated WORKDIR# (turn.ts passes `-w /work` on every run).# - `/tmp` and `/home/radial` are tmpfs-mounted by the daemon at run time# (ContainerSpec.tmpfs); the rootfs is otherwise read-only (ContainerSpec.readOnlyRootfs), so# this image must not need to write anywhere else (npm/claude/pi/codex config, git's global# config, etc. all resolve under $HOME=/home/radial — codex's $CODEX_HOME defaults to# $HOME/.codex).# - All three agent CLIs are installed unpinned, matching how `@anthropic-ai/claude-code` has# always been installed here. Pinning them is the follow-up if a bad upstream release lands.
FROM node:24-slim AS build# CI=true: pnpm must never prompt (no TTY in a docker build); COREPACK_ENABLE_DOWNLOAD_PROMPT# likewise silences corepack's download confirmation. The root .dockerignore keeps the host's# node_modules/dist out of the build context so `pnpm install` starts from a clean slate.ENV CI=true COREPACK_ENABLE_DOWNLOAD_PROMPT=0RUN corepack enableWORKDIR /repoCOPY . .RUN pnpm install --frozen-lockfileRUN pnpm buildRUN pnpm --filter=@radial/sidecar deploy /out --prod --legacy
FROM node:24-slim AS runtime
# Implementation turns use git directly. On GitHub they also use gh, with the operator's ephemeral# GH_TOKEN; on tangled they push over ssh with the per-turn key the daemon bind-mounts at# /run/radial-forge (turn.ts) — hence openssh-client, which node:24-slim does not ship.## ripgrep is pi's: its `grep` tool shells out to `rg` and DOWNLOADS one at first use when the# binary is missing — which, under a read-only rootfs with a tmpfs $HOME, means a download per# turn at best. Installing it here is what pi's own containerization guide recommends.RUN apt-get update \ && apt-get install -y --no-install-recommends git gh openssh-client ripgrep ca-certificates \ && rm -rf /var/lib/apt/lists/*
# Project checks in a turn invoke pnpm directly. Corepack was enabled in the discarded build stage# only, so without a runtime install agents first fail with "pnpm: not found" and have to discover# an incidental package-manager path. Install the exact workspace version as a global executable;# unlike a Corepack shim this never needs to download a package manager after the read-only# container has started. The smoke test below exercises it as uid 1000, the same user as a turn.RUN npm install -g --ignore-scripts pnpm@10.28.2 \ && pnpm --version \ && npm cache clean --force
# @anthropic-ai/claude-code: harness.ts's ClaudeCodeHarness shells out to `claude --print ...`.# Installed globally so it's on PATH for every user of the image, not just whoever installs it.## install.cjs (the package's postinstall) is what copies the platform-native binary from the# optional dependency over the bin/claude.exe placeholder. npm >= 11.16 blocks install scripts by# default and only *warns*, so leaving it implicit makes the build succeed while shipping a stub# that exits 1 with "claude native binary not installed" on the first turn. Running it explicitly# is also idempotent if npm's policy changes and the postinstall does fire.# `claude --version` then fails the build rather than deferring the failure to run time.RUN npm install -g @anthropic-ai/claude-code \ && node "$(npm root -g)/@anthropic-ai/claude-code/install.cjs" \ && claude --version \ && npm cache clean --force
# @earendil-works/pi-coding-agent: harness.ts's PiHarness shells out to `pi --mode json ...`.# --ignore-scripts is what pi's own containerization guide installs with (it needs no lifecycle# script, unlike the claude package above); `pi --version` fails the build here rather than# deferring a broken install to the first turn.RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent \ && pi --version \ && npm cache clean --force
# @openai/codex: harness.ts's CodexHarness shells out to `codex exec --json ...`. The npm package# is a thin wrapper around a platform-native Rust binary shipped in optional dependencies, which# npm resolves from the platform triple at install time — no lifecycle script is involved, so# --ignore-scripts costs nothing here and keeps the install policy uniform with pi's.# `codex --version` fails the build rather than deferring a broken install to the first turn.RUN npm install -g --ignore-scripts @openai/codex \ && codex --version \ && npm cache clean --force
# The self-contained sidecar deploy from the build stage: dist/ for @radial/sidecar plus# node_modules holding @radial/core, @radial/atproto, @radial/ingest (each dependency-free at# runtime beyond node builtins and each other).COPY --from=build /out /opt/radial/sidecarRUN chmod -R a+rX /opt/radial/sidecar
# A thin wrapper rather than a raw symlink onto PATH: this doesn't depend on cli.js's own# `#!/usr/bin/env node` shebang or execute bit surviving the multi-stage COPY unchanged.RUN printf '#!/bin/sh\nexec node /opt/radial/sidecar/dist/cli.js "$@"\n' > /usr/local/bin/radial \ && chmod 0755 /usr/local/bin/radial
# Non-root uid/gid 1000, matching ContainerSpec's default `--user 1000:1000` (container.ts).# node:24-slim already ships uid/gid 1000 (the `node` user); the runtime only needs the uid and a# home directory at /home/radial ($HOME below) — the username is never referenced.RUN mkdir -p /home/radial && chown 1000:1000 /home/radial
USER 1000:1000ENV HOME=/home/radial# No WORKDIR: neutral by design (see header comment) — `docker run -w /work ...` supplies it.