From 42963a6ba3c94597150da66367e3e5774e9b7f2e Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 16:22:30 +0300 Subject: [PATCH 1/7] docs: outer builder container design spec Co-Authored-By: Claude Fable 5 --- ...26-06-10-outer-builder-container-design.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-10-outer-builder-container-design.md diff --git a/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md new file mode 100644 index 0000000..707826d --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md @@ -0,0 +1,168 @@ +# Outer Builder Container — Design + +Date: 2026-06-10 +Status: approved (approach A) +Scope: items 1, 2, and 4 of "What Needs to Be Built" in +`docs/architecture/important/builder-container-architecture.md`. Item 3 +(project provisioning) already exists (`POST /v1/projects`); item 5 +(idle eviction) stays out of scope. + +## Goal + +A self-contained Docker image and run script so that one `docker run` on any +Linux host yields the outer builder container from the architecture doc: +agent-server (project registry + shared auth) plus rootless podman, with +project sources on a volume and agent-built apps running as inner containers. + +Out of scope for this slice: orchestrator (appx) spawn integration, the chat +web shell (stays outside the container and talks REST/SSE to port 4001), +per-project port registries, and multi-user isolation. + +## Topology + +``` +Linux host (verification: Ubuntu arm64 VM via OrbStack, Docker installed) +└── docker run appx-builder ← OUTER container, unprivileged + ├── agent-server (node, :4001) ← WORKSPACE_DIR=/workspace + ├── rootless podman (user: builder) + ├── /workspace ← named volume `appx-workspace` + │ ├── .pi-global/ ← auth.json, projects.json, sessions/ + │ └── / ← created by POST /v1/projects + └── inner containers (podman) ← built/run by the builder agent, + ports published inside the outer + container's net ns → reachable via + the outer container's -p mappings +``` + +Trust zones follow the architecture doc: host trusted; outer container holds +LLM credentials in process memory; inner containers run generated code and +get no credentials. + +## Deliverables (all in this repo) + +``` +docker/builder/ +├── Dockerfile # multi-stage: build agent-server → runtime image +├── entrypoint.sh # podman warmup + exec agent-server +├── containers.conf # rootless podman defaults for nested operation +├── storage.conf # overlay + fuse-overlayfs storage for user `builder` +├── AGENTS.builder.md # builder-agent system prompt (podman, /workspace, ports) +└── run.sh # canonical build+run wrapper (the "spawn" contract) +docs/architecture/important/builder-container-architecture.md # status update +README.md # "Run it in Docker" section +``` + +## Dockerfile + +Multi-stage: + +1. **build** — `node:22-bookworm`: `npm ci && npm run build` of this repo + (`dist/` + production `node_modules`). +2. **runtime** — `ubuntu:24.04`: + - apt: `nodejs` (NodeSource 22), `podman`, `fuse-overlayfs`, `uidmap`, + `slirp4netns`, `netavark`/`aardvark-dns` (noble defaults), `passt`, + `ca-certificates`, `git`, `curl`. + - non-root user `builder` (uid/gid 1000) with `/etc/subuid` and + `/etc/subgid` ranges (`builder:100000:65536`) for rootless podman. + - `containers.conf` + `storage.conf` baked into + `/home/builder/.config/containers/`: overlay driver with + `fuse-overlayfs`, network backend left at noble defaults with + `slirp4netns` as the rootless network fallback (most reliable nested). + - builder system prompt baked at `/home/builder/.pi/agent/AGENTS.md` + (pi's user-level context discovery applies it to every project; the + acceptance run must confirm pickup — fallback is copying it into each + project's `.pi/AGENTS.md` at provisioning time). + - `ENV WORKSPACE_DIR=/workspace AGENT_SERVER_HOST=0.0.0.0`. + - `USER builder`, `EXPOSE 4001 3000-3010`, entrypoint below. + +`.dockerignore` keeps the context small (node_modules, docs, test). + +## entrypoint.sh + +1. `mkdir -p "$WORKSPACE_DIR"` (volume may mount empty; agent-server requires + the directory to exist). +2. `podman info >/dev/null 2>&1 || true` — first-run storage init warmup + (non-fatal: REST surface must come up even if nesting is broken; the + failure then surfaces in agent tool calls and logs). +3. `exec node /app/dist/server.js`. + +## run.sh — the spawn contract + +The single canonical way to launch (and later the exact contract appx will +implement): + +```bash +docker build -t appx-builder -f docker/builder/Dockerfile . +docker run -d --name appx-builder \ + --device /dev/fuse \ + --security-opt seccomp=unconfined \ + --security-opt apparmor=unconfined \ + -v appx-workspace:/workspace \ + -v appx-podman:/home/builder/.local/share/containers \ + -p 4001:4001 -p 3000-3010:3000-3010 \ + -e ANTHROPIC_API_KEY -e AGENT_SERVER_TOKEN \ + -e LITELLM_BASE_URL -e LITELLM_API_KEY -e LITELLM_MODELS \ + -e LITELLM_MODELS_JSON -e LITELLM_DEFAULT_MODEL -e LITELLM_DEFAULT_THINKING \ + appx-builder +``` + +Notes: + +- **No `--privileged`.** `--device /dev/fuse` is required for fuse-overlayfs; + the relaxed seccomp/apparmor profiles are required for nested user + namespaces on stock Docker (documented in run.sh; a future hardening pass + can replace them with a custom seccomp profile). +- `appx-podman` volume keeps inner-container images/state across outer + restarts (limitation 6 in the architecture doc). +- Flags are parameterized via env (`BUILDER_IMAGE`, `BUILDER_NAME`, + `BUILDER_PORTS`), with the above as defaults. + +## Builder system prompt (AGENTS.builder.md) + +Short, factual contract for every builder agent: + +- you work in a project directory under `/workspace/` +- `podman` is available for building and running app containers + (`podman build`, `podman run -d -p :`) +- publish app ports in the 3000–3010 range; they are forwarded to the host +- never pass provider API keys or env secrets into containers you run +- long-running apps: run detached, verify with `curl`, check `podman ps`/logs + +## Verification (acceptance on the OrbStack VM) + +Environment: new arm64 Ubuntu noble VM `appx-builder-vm` via +`orb create ubuntu appx-builder-vm`, Docker installed inside. + +1. **Image builds**: `docker build` succeeds from a clean checkout. +2. **REST up**: `run.sh`, then `curl :4001/v1/healthz` → ok; + `POST /v1/projects {"name":"demo"}` twice → idempotent; restart container + → project still listed (volume-backed registry). +3. **Nesting works**: `docker exec -u builder appx-builder podman run --rm + docker.io/library/alpine echo nested-ok` → prints `nested-ok`. +4. **Inner app reachable**: inside the outer container run a podman container + publishing :3000 (static http server), then `curl :3000` from the VM → + responds. This proves the diagram's port-forward chain + (host → outer → inner). +5. **Prompt pickup**: create a session in `demo`, `GET` history after a + prompt; with LLM credentials available, ask the agent to run an inner + container itself (full diagram walkthrough). Without credentials this step + degrades to checking the system prompt is loaded (server logs). +6. **No credential leak**: `docker exec ... podman run --rm alpine env` shows + no `ANTHROPIC_API_KEY`/`LITELLM_API_KEY` (they live in the agent-server + process env, which the bash tool inherits — the prompt forbids passing + them on; this check documents the current boundary rather than enforcing + it. A `spawnHook` env-strip is a named follow-up, not in this slice). + +## Risks / open points + +- **Nested rootless podman quirks** (network backend, cgroups v2 delegation) + are the main unknown; that is exactly what the VM acceptance run flushes + out. Fallbacks: `--network slirp4netns` per inner container; switching + storage to `vfs` (slow but always works) as a last resort. +- **Prompt discovery**: if pi does not auto-load + `/home/builder/.pi/agent/AGENTS.md`, fall back to copying the builder + prompt into each project at provisioning (template copy in entrypoint is + not possible per-project; the fallback would be an agent-server change — + flagged during implementation if needed). +- Port range 3000–3010 is a fixed convention for this slice; a port registry + is an explicit non-goal (limitation 5 in the architecture doc). From 89a528378487201fdd6b70a43aa6fa3ab2f2d895 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 16:33:31 +0300 Subject: [PATCH 2/7] feat: outer builder container image (agent-server + rootless podman) Implements items 1, 2 and 4 of builder-container-architecture.md's 'What Needs to Be Built': the Dockerfile, the canonical run script (spawn contract for orchestrators), and the builder system prompt. Co-Authored-By: Claude Fable 5 --- .dockerignore | 8 ++++ docker/builder/AGENTS.builder.md | 16 ++++++++ docker/builder/Dockerfile | 70 ++++++++++++++++++++++++++++++++ docker/builder/containers.conf | 14 +++++++ docker/builder/entrypoint.sh | 16 ++++++++ docker/builder/run.sh | 42 +++++++++++++++++++ docker/builder/storage.conf | 9 ++++ 7 files changed, 175 insertions(+) create mode 100644 .dockerignore create mode 100644 docker/builder/AGENTS.builder.md create mode 100644 docker/builder/Dockerfile create mode 100644 docker/builder/containers.conf create mode 100755 docker/builder/entrypoint.sh create mode 100755 docker/builder/run.sh create mode 100644 docker/builder/storage.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ce1d11f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +docs +test +.git +.gen +*.md +!docker/builder/AGENTS.builder.md diff --git a/docker/builder/AGENTS.builder.md b/docker/builder/AGENTS.builder.md new file mode 100644 index 0000000..35a0d8b --- /dev/null +++ b/docker/builder/AGENTS.builder.md @@ -0,0 +1,16 @@ +# Appx Builder Environment + +You are a builder agent inside the Appx outer builder container. + +- Your project lives in a directory under `/workspace/` — treat it + as the normal editable project root. +- `podman` is available for building and running app containers: + `podman build -t -app .`, `podman run -d --name -app -p 3000:3000 -app`. +- Publish app ports in the **3000–3010** range only; those ports are forwarded + out of this container to the host. +- Run long-lived apps detached (`-d`), then verify them with `curl` and check + `podman ps` / `podman logs ` instead of blocking the shell. +- Rebuild + restart flow: `podman build ... && podman rm -f && podman run ...`. +- NEVER pass provider credentials (`ANTHROPIC_API_KEY`, `LITELLM_API_KEY`, or + any other secret from your environment) into containers, Dockerfiles, or + generated app code. diff --git a/docker/builder/Dockerfile b/docker/builder/Dockerfile new file mode 100644 index 0000000..034e84c --- /dev/null +++ b/docker/builder/Dockerfile @@ -0,0 +1,70 @@ +# Outer builder container: agent-server + rootless podman. +# See docs/superpowers/specs/2026-06-10-outer-builder-container-design.md and +# docs/architecture/important/builder-container-architecture.md. + +# --- Stage 1: build agent-server from this repo ---------------------------- +FROM node:22-bookworm AS build +WORKDIR /src +COPY package.json package-lock.json tsconfig.json ./ +ENV HUSKY=0 +RUN npm ci +COPY src ./src +RUN npm run build && npm prune --omit=dev + +# --- Stage 2: runtime ------------------------------------------------------- +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Node 22 (NodeSource) + rootless podman stack. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl gnupg \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends \ + nodejs \ + podman \ + fuse-overlayfs \ + uidmap \ + slirp4netns \ + passt \ + netavark \ + aardvark-dns \ + catatonit \ + git \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Non-root builder user with subordinate id ranges for rootless podman. +# ubuntu:24.04 ships a default "ubuntu" uid-1000 user; replace it. +RUN userdel -r ubuntu 2>/dev/null || true \ + && useradd -m -u 1000 -s /bin/bash builder \ + && echo "builder:100000:65536" > /etc/subuid \ + && echo "builder:100000:65536" > /etc/subgid + +# Rootless podman defaults tuned for running nested inside Docker. +COPY docker/builder/containers.conf /home/builder/.config/containers/containers.conf +COPY docker/builder/storage.conf /home/builder/.config/containers/storage.conf + +# Builder-agent system prompt: pi user-level context discovery applies it to +# every project this server runs. +COPY docker/builder/AGENTS.builder.md /home/builder/.pi/agent/AGENTS.md + +# agent-server runtime. +COPY --from=build /src/dist /app/dist +COPY --from=build /src/node_modules /app/node_modules +COPY package.json /app/package.json +COPY docker/builder/entrypoint.sh /usr/local/bin/builder-entrypoint + +RUN mkdir -p /workspace /home/builder/.local/share/containers \ + && chown -R builder:builder /home/builder /workspace /app \ + && chmod +x /usr/local/bin/builder-entrypoint + +USER builder +ENV HOME=/home/builder \ + WORKSPACE_DIR=/workspace \ + AGENT_SERVER_HOST=0.0.0.0 \ + AGENT_SERVER_PORT=4001 + +WORKDIR /workspace +EXPOSE 4001 3000-3010 + +ENTRYPOINT ["/usr/local/bin/builder-entrypoint"] diff --git a/docker/builder/containers.conf b/docker/builder/containers.conf new file mode 100644 index 0000000..7a4fe2d --- /dev/null +++ b/docker/builder/containers.conf @@ -0,0 +1,14 @@ +# Rootless podman defaults for running nested inside an unprivileged Docker +# container. slirp4netns is the most reliable rootless network backend in +# nested user namespaces; cgroupfs avoids requiring a systemd user session. +[containers] +netns = "slirp4netns" +cgroup_manager = "cgroupfs" +log_driver = "k8s-file" + +[engine] +events_logger = "file" +runtime = "crun" + +[network] +default_rootless_network_cmd = "slirp4netns" diff --git a/docker/builder/entrypoint.sh b/docker/builder/entrypoint.sh new file mode 100755 index 0000000..8b3206e --- /dev/null +++ b/docker/builder/entrypoint.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# The workspace volume may mount empty; agent-server requires the dir to exist. +mkdir -p "${WORKSPACE_DIR:-/workspace}" + +# First-run rootless storage init is slow; warm it up. Non-fatal: the REST +# surface must come up even if nesting is broken — the failure then surfaces +# in agent tool calls instead of a crash loop. +if podman info >/dev/null 2>&1; then + echo "[builder] rootless podman ready" +else + echo "[builder] WARNING: podman info failed — inner containers will not work" >&2 +fi + +exec node /app/dist/server.js diff --git a/docker/builder/run.sh b/docker/builder/run.sh new file mode 100755 index 0000000..18c4bc3 --- /dev/null +++ b/docker/builder/run.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Build and run the outer builder container. This script is the canonical +# spawn contract — an orchestrator (appx) should replicate exactly these +# flags. See docs/superpowers/specs/2026-06-10-outer-builder-container-design.md. +set -euo pipefail + +cd "$(dirname "$0")/../.." + +IMAGE="${BUILDER_IMAGE:-appx-builder}" +NAME="${BUILDER_NAME:-appx-builder}" +AGENT_PORT="${BUILDER_AGENT_PORT:-4001}" +APP_PORTS="${BUILDER_APP_PORTS:-3000-3010}" +WORKSPACE_VOLUME="${BUILDER_WORKSPACE_VOLUME:-appx-workspace}" +PODMAN_VOLUME="${BUILDER_PODMAN_VOLUME:-appx-podman}" + +docker build -t "$IMAGE" -f docker/builder/Dockerfile . + +docker rm -f "$NAME" >/dev/null 2>&1 || true + +# Notes on flags (no --privileged): +# --device /dev/fuse fuse-overlayfs storage for nested podman +# seccomp/apparmor unconfined required for nested user namespaces on +# stock Docker; a tailored seccomp profile +# is a follow-up hardening task +# $PODMAN_VOLUME keeps inner images/containers across +# outer restarts +docker run -d --name "$NAME" \ + --device /dev/fuse \ + --security-opt seccomp=unconfined \ + --security-opt apparmor=unconfined \ + -v "$WORKSPACE_VOLUME":/workspace \ + -v "$PODMAN_VOLUME":/home/builder/.local/share/containers \ + -p "$AGENT_PORT":4001 \ + -p "$APP_PORTS":"$APP_PORTS" \ + -e ANTHROPIC_API_KEY \ + -e AGENT_SERVER_TOKEN \ + -e LITELLM_BASE_URL -e LITELLM_API_KEY \ + -e LITELLM_MODELS -e LITELLM_MODELS_JSON \ + -e LITELLM_DEFAULT_MODEL -e LITELLM_DEFAULT_THINKING \ + "$IMAGE" + +echo "builder container '$NAME' is up — agent-server on :$AGENT_PORT, app ports $APP_PORTS" diff --git a/docker/builder/storage.conf b/docker/builder/storage.conf new file mode 100644 index 0000000..8a1d323 --- /dev/null +++ b/docker/builder/storage.conf @@ -0,0 +1,9 @@ +# Rootless storage for the builder user. overlay + fuse-overlayfs works in +# nested user namespaces (requires --device /dev/fuse on the outer container). +[storage] +driver = "overlay" +runroot = "/tmp/podman-run" +graphroot = "/home/builder/.local/share/containers/storage" + +[storage.options.overlay] +mount_program = "/usr/bin/fuse-overlayfs" From 715b771d239e27a8c8408f3bf940f66dd2e5b927 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 16:34:51 +0300 Subject: [PATCH 3/7] docs: builder container run section + status in architecture doc Co-Authored-By: Claude Fable 5 --- README.md | 20 +++++++++++++++++++ .../builder-container-architecture.md | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b4c8513..55407aa 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,26 @@ WORKSPACE_DIR=/abs/path/to/workspace npm start Dev with watch: `WORKSPACE_DIR=/abs/path/to/workspace npm run dev`. +## Run it in Docker (outer builder container) + +The repo ships the outer builder container from +[`docs/architecture/important/builder-container-architecture.md`](docs/architecture/important/builder-container-architecture.md): +agent-server plus **rootless podman**, so builder agents can build and run app +containers (`podman build` / `podman run`) isolated from the host. + +```bash +./docker/builder/run.sh +# → agent-server on :4001, inner-app ports 3000-3010 forwarded +``` + +`run.sh` is the canonical spawn contract (no `--privileged`; `--device +/dev/fuse` + relaxed seccomp/apparmor for nested user namespaces; named +volumes for `/workspace` and podman storage). Orchestrators should replicate +its flags. Provider credentials are passed with `-e` and live only in the +agent-server process — never in inner containers. The builder system prompt +(`docker/builder/AGENTS.builder.md`) is baked at +`/home/builder/.pi/agent/AGENTS.md`. + ## Configuration All via env vars (see `.env.example`): diff --git a/docs/architecture/important/builder-container-architecture.md b/docs/architecture/important/builder-container-architecture.md index f48edc6..1bd6ffc 100644 --- a/docs/architecture/important/builder-container-architecture.md +++ b/docs/architecture/important/builder-container-architecture.md @@ -176,12 +176,14 @@ No host-level work happens for any of this beyond running the outer container. * ## What Needs to Be Built -1. **The outer container's Dockerfile** — Ubuntu/Alpine + podman + nodejs + agent-server (~10 lines, draft in `rootless-podman-isolation.md`) -2. **A run script / docker-compose** that launches the outer container with the right flags (`--device /dev/fuse`, port forwards, volume mount, env vars) +1. ✅ **The outer container's Dockerfile** — built: `docker/builder/Dockerfile` (Ubuntu 24.04 + NodeSource 22 + rootless podman, fuse-overlayfs, builder user with subuid/subgid) +2. ✅ **A run script** — built: `docker/builder/run.sh` is the canonical spawn contract (`--device /dev/fuse`, relaxed seccomp/apparmor for nested userns, workspace + podman-storage volumes, port forwards, env passthrough) 3. **Project provisioning logic** — `POST /v1/projects { name }` already creates `WORKSPACE_DIR//` and registers the project; provisioning is just calling that endpoint (plus any product-specific scaffolding the caller layers on top) -4. **System prompt for the builder agent** — telling it that `podman` is available, where projects live, how to expose ports +4. ✅ **System prompt for the builder agent** — built: `docker/builder/AGENTS.builder.md`, baked at `/home/builder/.pi/agent/AGENTS.md` (pi user-level context discovery); prompt-engineering iteration remains open 5. **(Optional) An idle-eviction sweep** — if many projects exist and stopping unused `ProjectRuntime`s would free memory; not needed for one admin user +See `docs/superpowers/specs/2026-06-10-outer-builder-container-design.md` for the design and acceptance criteria. + That's it. Maybe 1-2 days of work for the outer container + provisioning, plus prompt engineering iteration on point 4. ## What This Architecture Buys You From aaee29ee03a6ae66454cda65934ccbd3d1e5bbd8 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 16:36:07 +0300 Subject: [PATCH 4/7] feat: builder container acceptance script Co-Authored-By: Claude Fable 5 --- docker/builder/verify.sh | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100755 docker/builder/verify.sh diff --git a/docker/builder/verify.sh b/docker/builder/verify.sh new file mode 100755 index 0000000..384461f --- /dev/null +++ b/docker/builder/verify.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Acceptance checks for the outer builder container (run on the docker host). +# Expects the container started via run.sh. See the design spec for context. +set -uo pipefail + +NAME="${BUILDER_NAME:-appx-builder}" +PORT="${BUILDER_AGENT_PORT:-4001}" +BASE="http://127.0.0.1:$PORT" +fail=0 + +check() { + local label="$1"; shift + if "$@" >/dev/null 2>&1; then + echo "ok - $label" + else + echo "not ok - $label" + fail=1 + fi +} + +body() { curl -fsS --max-time 10 "$@"; } + +# 1. REST surface up +check "healthz" body "$BASE/v1/healthz" + +# 2. Idempotent project creation +P1=$(body -X POST "$BASE/v1/projects" -H 'content-type: application/json' -d '{"name":"demo"}') +P2=$(body -X POST "$BASE/v1/projects" -H 'content-type: application/json' -d '{"name":"demo"}') +if [ -n "$P1" ] && [ "$P1" = "$P2" ]; then echo "ok - project create idempotent"; else echo "not ok - project create idempotent"; fail=1; fi + +# 3. Session creation (runtime boots, builder prompt loads — see container logs) +check "session create" body -X POST "$BASE/v1/projects/demo/sessions" + +# 4. Nested rootless podman works +if docker exec -u builder "$NAME" podman run --rm docker.io/library/alpine echo nested-ok 2>/dev/null | grep -q nested-ok; then + echo "ok - nested podman run" +else + echo "not ok - nested podman run"; fail=1 +fi + +# 5. Inner app port chain: inner :3000 → outer -p → host +docker exec -u builder "$NAME" podman rm -f verify-web >/dev/null 2>&1 +if docker exec -u builder "$NAME" podman run -d --name verify-web -p 3000:80 docker.io/library/nginx:alpine >/dev/null 2>&1; then + sleep 3 + check "inner app reachable from host (:3000)" body "http://127.0.0.1:3000" + docker exec -u builder "$NAME" podman rm -f verify-web >/dev/null 2>&1 +else + echo "not ok - inner app container start"; fail=1 +fi + +# 6. Registry survives an outer restart +docker restart "$NAME" >/dev/null +sleep 5 +if body "$BASE/v1/projects" | grep -q '"demo"'; then + echo "ok - project registry survives restart" +else + echo "not ok - project registry survives restart"; fail=1 +fi + +# 7. No credential leak into inner containers +LEAK=$(docker exec -u builder "$NAME" podman run --rm docker.io/library/alpine env 2>/dev/null | grep -cE "ANTHROPIC_API_KEY|LITELLM_API_KEY") +if [ "${LEAK:-0}" = "0" ]; then echo "ok - no credentials in inner env"; else echo "not ok - credentials leaked into inner env"; fail=1; fi + +exit $fail From 95d5e0e02b89c782318588c60a0b2cab60fd7535 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 16:45:12 +0300 Subject: [PATCH 5/7] fix: spawn contract flags required for nested rootless podman Verified on Ubuntu noble (arm64): --cap-add SYS_ADMIN for newuidmap, --device /dev/net/tun for slirp4netns, systempaths=unconfined for crun sysctl writes. All verify.sh acceptance checks pass. Co-Authored-By: Claude Fable 5 --- docker/builder/run.sh | 10 ++++++++++ .../2026-06-10-outer-builder-container-design.md | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docker/builder/run.sh b/docker/builder/run.sh index 18c4bc3..53b7b19 100755 --- a/docker/builder/run.sh +++ b/docker/builder/run.sh @@ -19,15 +19,25 @@ docker rm -f "$NAME" >/dev/null 2>&1 || true # Notes on flags (no --privileged): # --device /dev/fuse fuse-overlayfs storage for nested podman +# --device /dev/net/tun slirp4netns/pasta tap device for +# rootless container networking # seccomp/apparmor unconfined required for nested user namespaces on # stock Docker; a tailored seccomp profile # is a follow-up hardening task +# --cap-add SYS_ADMIN required for newuidmap to write the +# rootless uid/gid maps (verified: without +# it nested podman fails with EPERM) +# systempaths=unconfined unmask /proc so crun can set per-container +# sysctls (ping_group_range) for inner nets # $PODMAN_VOLUME keeps inner images/containers across # outer restarts docker run -d --name "$NAME" \ --device /dev/fuse \ + --device /dev/net/tun \ --security-opt seccomp=unconfined \ --security-opt apparmor=unconfined \ + --security-opt systempaths=unconfined \ + --cap-add SYS_ADMIN \ -v "$WORKSPACE_VOLUME":/workspace \ -v "$PODMAN_VOLUME":/home/builder/.local/share/containers \ -p "$AGENT_PORT":4001 \ diff --git a/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md index 707826d..45d25cd 100644 --- a/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md +++ b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md @@ -1,7 +1,17 @@ # Outer Builder Container — Design Date: 2026-06-10 -Status: approved (approach A) +Status: implemented & verified (2026-06-10, Ubuntu noble arm64 VM, Docker 29) + +Acceptance results: all checks in `docker/builder/verify.sh` pass — REST up, +idempotent project create, session create, nested `podman run`, inner app +published on :3000 reachable from the host, registry survives container +restart, no credentials in inner env. Two flags were added to the spawn +contract during verification: `--cap-add SYS_ADMIN` (newuidmap EPERM on +uid/gid maps without it) and `--security-opt systempaths=unconfined` + +`--device /dev/net/tun` (crun sysctl writes and slirp4netns tap device). +Open item: user-level builder prompt pickup needs one live LLM turn to +confirm pi context discovery. Scope: items 1, 2, and 4 of "What Needs to Be Built" in `docs/architecture/important/builder-container-architecture.md`. Item 3 (project provisioning) already exists (`POST /v1/projects`); item 5 From 4fab5372dac38b5c2e74bf5fa1d2a3ba232fdfd6 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 17:45:09 +0300 Subject: [PATCH 6/7] fix: restart policy so the builder container survives host reboots Co-Authored-By: Claude Fable 5 --- docker/builder/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/builder/run.sh b/docker/builder/run.sh index 53b7b19..84eb912 100755 --- a/docker/builder/run.sh +++ b/docker/builder/run.sh @@ -32,6 +32,7 @@ docker rm -f "$NAME" >/dev/null 2>&1 || true # $PODMAN_VOLUME keeps inner images/containers across # outer restarts docker run -d --name "$NAME" \ + --restart unless-stopped \ --device /dev/fuse \ --device /dev/net/tun \ --security-opt seccomp=unconfined \ From a0ffb13b57053b1d9a98304a9c71c30ec5c3aa4c Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 10 Jun 2026 17:48:43 +0300 Subject: [PATCH 7/7] Install builder prompt in shared agent dir Builder images copied AGENTS.builder.md under /home/builder/.pi/agent, but ProjectRegistry passes /workspace/.pi-global as the runtime agentDir. New projects without their own .pi/AGENTS.md therefore could start without the podman, port, and secret-handling instructions required by the builder container. Bake the prompt as an image template and have the entrypoint install it into WORKSPACE_DIR/.pi-global/AGENTS.md when the mounted workspace does not already provide one. Update the Docker docs to describe the actual lookup path and the project-local override behavior. --- README.md | 6 ++++-- docker/builder/Dockerfile | 6 +++--- docker/builder/entrypoint.sh | 10 +++++++++- .../builder-container-architecture.md | 2 +- ...26-06-10-outer-builder-container-design.md | 19 ++++++++++--------- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 55407aa..2f92597 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ volumes for `/workspace` and podman storage). Orchestrators should replicate its flags. Provider credentials are passed with `-e` and live only in the agent-server process — never in inner containers. The builder system prompt (`docker/builder/AGENTS.builder.md`) is baked at -`/home/builder/.pi/agent/AGENTS.md`. +`/usr/local/share/appx-builder/AGENTS.md` and installed at +`${WORKSPACE_DIR}/.pi-global/AGENTS.md` when the container starts. ## Configuration @@ -91,7 +92,8 @@ WORKSPACE_DIR/ are centralised under `.pi-global/sessions/{id}/`, so they never leak into a project's git history and survive independently on the volume. - A project with no `.pi/AGENTS.md` starts with no pinned prompt (silent skip); - Pi's normal context-file discovery then applies. + Pi's normal context-file discovery then applies, including any global + `.pi-global/AGENTS.md` installed by the deployment. - LLM credentials are injected from env into memory at startup and are **not** the job of the volume to persist (`auth.json` holds only non-secret/OAuth state). diff --git a/docker/builder/Dockerfile b/docker/builder/Dockerfile index 034e84c..f1626e6 100644 --- a/docker/builder/Dockerfile +++ b/docker/builder/Dockerfile @@ -44,9 +44,9 @@ RUN userdel -r ubuntu 2>/dev/null || true \ COPY docker/builder/containers.conf /home/builder/.config/containers/containers.conf COPY docker/builder/storage.conf /home/builder/.config/containers/storage.conf -# Builder-agent system prompt: pi user-level context discovery applies it to -# every project this server runs. -COPY docker/builder/AGENTS.builder.md /home/builder/.pi/agent/AGENTS.md +# Builder-agent system prompt template; the entrypoint installs it into +# WORKSPACE_DIR/.pi-global/AGENTS.md, the shared agentDir used by runtimes. +COPY docker/builder/AGENTS.builder.md /usr/local/share/appx-builder/AGENTS.md # agent-server runtime. COPY --from=build /src/dist /app/dist diff --git a/docker/builder/entrypoint.sh b/docker/builder/entrypoint.sh index 8b3206e..fc85a5c 100755 --- a/docker/builder/entrypoint.sh +++ b/docker/builder/entrypoint.sh @@ -2,7 +2,15 @@ set -euo pipefail # The workspace volume may mount empty; agent-server requires the dir to exist. -mkdir -p "${WORKSPACE_DIR:-/workspace}" +workspace_dir="${WORKSPACE_DIR:-/workspace}" +global_agent_dir="${workspace_dir}/.pi-global" +builder_agents_template="/usr/local/share/appx-builder/AGENTS.md" + +mkdir -p "${global_agent_dir}" + +if [ ! -e "${global_agent_dir}/AGENTS.md" ]; then + cp "${builder_agents_template}" "${global_agent_dir}/AGENTS.md" +fi # First-run rootless storage init is slow; warm it up. Non-fatal: the REST # surface must come up even if nesting is broken — the failure then surfaces diff --git a/docs/architecture/important/builder-container-architecture.md b/docs/architecture/important/builder-container-architecture.md index 1bd6ffc..fc162f1 100644 --- a/docs/architecture/important/builder-container-architecture.md +++ b/docs/architecture/important/builder-container-architecture.md @@ -179,7 +179,7 @@ No host-level work happens for any of this beyond running the outer container. * 1. ✅ **The outer container's Dockerfile** — built: `docker/builder/Dockerfile` (Ubuntu 24.04 + NodeSource 22 + rootless podman, fuse-overlayfs, builder user with subuid/subgid) 2. ✅ **A run script** — built: `docker/builder/run.sh` is the canonical spawn contract (`--device /dev/fuse`, relaxed seccomp/apparmor for nested userns, workspace + podman-storage volumes, port forwards, env passthrough) 3. **Project provisioning logic** — `POST /v1/projects { name }` already creates `WORKSPACE_DIR//` and registers the project; provisioning is just calling that endpoint (plus any product-specific scaffolding the caller layers on top) -4. ✅ **System prompt for the builder agent** — built: `docker/builder/AGENTS.builder.md`, baked at `/home/builder/.pi/agent/AGENTS.md` (pi user-level context discovery); prompt-engineering iteration remains open +4. ✅ **System prompt for the builder agent** — built: `docker/builder/AGENTS.builder.md`, installed at `WORKSPACE_DIR/.pi-global/AGENTS.md` on container startup so runtimes using the shared agent dir can load it; prompt-engineering iteration remains open 5. **(Optional) An idle-eviction sweep** — if many projects exist and stopping unused `ProjectRuntime`s would free memory; not needed for one admin user See `docs/superpowers/specs/2026-06-10-outer-builder-container-design.md` for the design and acceptance criteria. diff --git a/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md index 45d25cd..7eb64a9 100644 --- a/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md +++ b/docs/superpowers/specs/2026-06-10-outer-builder-container-design.md @@ -78,9 +78,9 @@ Multi-stage: `/home/builder/.config/containers/`: overlay driver with `fuse-overlayfs`, network backend left at noble defaults with `slirp4netns` as the rootless network fallback (most reliable nested). - - builder system prompt baked at `/home/builder/.pi/agent/AGENTS.md` - (pi's user-level context discovery applies it to every project; the - acceptance run must confirm pickup — fallback is copying it into each + - builder system prompt baked at `/usr/local/share/appx-builder/AGENTS.md` + and installed at `/workspace/.pi-global/AGENTS.md` on startup (the + shared agent dir that runtimes pass to Pi; fallback is copying it into each project's `.pi/AGENTS.md` at provisioning time). - `ENV WORKSPACE_DIR=/workspace AGENT_SERVER_HOST=0.0.0.0`. - `USER builder`, `EXPOSE 4001 3000-3010`, entrypoint below. @@ -90,7 +90,8 @@ Multi-stage: ## entrypoint.sh 1. `mkdir -p "$WORKSPACE_DIR"` (volume may mount empty; agent-server requires - the directory to exist). + the directory to exist) and install the builder prompt into + `$WORKSPACE_DIR/.pi-global/AGENTS.md` if the volume does not already provide one. 2. `podman info >/dev/null 2>&1 || true` — first-run storage init warmup (non-fatal: REST surface must come up even if nesting is broken; the failure then surfaces in agent tool calls and logs). @@ -169,10 +170,10 @@ Environment: new arm64 Ubuntu noble VM `appx-builder-vm` via are the main unknown; that is exactly what the VM acceptance run flushes out. Fallbacks: `--network slirp4netns` per inner container; switching storage to `vfs` (slow but always works) as a last resort. -- **Prompt discovery**: if pi does not auto-load - `/home/builder/.pi/agent/AGENTS.md`, fall back to copying the builder - prompt into each project at provisioning (template copy in entrypoint is - not possible per-project; the fallback would be an agent-server change — - flagged during implementation if needed). +- **Prompt discovery**: the entrypoint installs the builder prompt into + `/workspace/.pi-global/AGENTS.md`, matching the shared `agentDir` + (`WORKSPACE_DIR/.pi-global`) that `ProjectRegistry` passes to every runtime. + A project-local `.pi/AGENTS.md` can still replace the global default when a + project needs specialised instructions. - Port range 3000–3010 is a fixed convention for this slice; a port registry is an explicit non-goal (limitation 5 in the architecture doc).