Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
artifacts/*
!artifacts/.gitkeep
50 changes: 50 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:1
# Isolated first-use environment for @pacphi/agentic-kit.
#
# Deliberately does NOT bake the kit into the image: the entrypoint installs
# @pacphi/agentic-kit@next at container start, so every fresh container
# exercises the true first-install path against whatever `next` currently is —
# no image rebuild per release. Apt + Node layers stay cached.
#
# See USER-GUIDE.md (running it) and MAINTAINER-GUIDE.md (design + knobs).

ARG UBUNTU_VERSION=26.04
FROM ubuntu:${UBUNTU_VERSION}

ARG NODE_MAJOR=24
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8

# procps: ruflo daemon discovery shells out to `ps -eo` (absent on slim bases).
# build-essential/python3: node-gyp fallback when a native prebuild is missing.
# socat: re-publishes the loopback-only dashboard on the container interface.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl git less procps socat sudo xz-utils \
build-essential python3 pkg-config \
&& rm -rf /var/lib/apt/lists/*

# NodeSource Node (kit engines: node >=22). Override: --build-arg NODE_MAJOR=26
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# Non-root: the claude CLI refuses permission-bypass modes as root, and a
# user-owned npm prefix (no sudo for `npm -g`) is the realistic first-use shape.
# Passwordless sudo stays available for in-container debugging.
RUN useradd -m -s /bin/bash tester \
&& echo 'tester ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/tester

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

USER tester
ENV NPM_CONFIG_PREFIX=/home/tester/.npm-global \
PATH=/home/tester/.npm-global/bin:$PATH
WORKDIR /home/tester/work

# 7432 is the socat bridge, NOT the dashboard itself — the dashboard binds
# container-loopback 7431 by design and is unreachable via port publishing.
EXPOSE 7432

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["dashboard"]
92 changes: 92 additions & 0 deletions docker/MAINTAINER-GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# First-use environment — maintainer guide

Audience: agentic-kit maintainers using this environment to validate releases,
reproduce first-install bugs, and test upgrade paths. For "how do I run it,"
see [USER-GUIDE.md](USER-GUIDE.md); this page is the *why* and the knobs.

## Design decisions

- **The kit is installed at container start, not image build.** The
entrypoint runs `npm i -g @pacphi/agentic-kit@$AK_DIST_TAG` on first boot,
so every ephemeral run exercises the true install path against the live
dist-tag — publishing a new alpha requires **no image rebuild** to be
covered. The image layers (apt, NodeSource) are pure cache.
- **Non-root `tester` user, user-owned npm prefix.** The claude CLI refuses
permission-bypass modes as root, and a no-sudo `npm -g` is the realistic
first-use shape. `NPM_CONFIG_PREFIX=/home/tester/.npm-global` is set in the
image — the container's global root is always known and self-contained.
- **The dashboard needs a bridge, not a port mapping.** `ak dashboard`
listens on a loopback literal with a per-session token
([ADR-0014](../docs/adr/0014-dashboard-auth-and-remediation.md)); Docker
port publishing can't reach a container-loopback listener. The entrypoint
runs `socat` (container `0.0.0.0:7432` → `127.0.0.1:7431`) and compose maps
it back to **host-loopback only** (`127.0.0.1:7431:7432`) — the security
envelope (local browsers + token) is preserved, and the printed `#token`
URL works verbatim on the host because the host port mirrors 7431. Do not
"fix" this by adding a bind flag to the dashboard; the loopback literal is
deliberate.
- **Isolation is absolute, both directions.** No host path is mounted except
`./artifacts`. Named volumes carry all persistent state, namespaced
`agentic-kit-firstuse_*`. A host agentic-kit install (any version, any
prefix) and this environment cannot see each other. Keep it that way when
extending: auth material enters via `docker cp` or env, never a mount.
- **`--yes` in the default setup flags.** Compose `up` has no interactive
stdin; `ak setup` prompts would either hang (TTY allocated) or silently
default (no TTY). `--yes` makes the accept-defaults choice explicit and
deterministic. Interactive prompt testing: `docker compose run --rm -e
AK_SKIP_SETUP=1 ak bash`, then run setup by hand.

## Knobs

| Knob | Default | Purpose |
| --- | --- | --- |
| `AK_DIST_TAG` (env) | `next` | Which dist-tag/version the entrypoint installs — pin an exact alpha to bisect a regression |
| `AK_INSTALL_SPEC` (env) | `@pacphi/agentic-kit@$AK_DIST_TAG` | Full npm spec override — test an unpublished build: `npm pack` the checkout, drop the tarball in `./artifacts`, set `AK_INSTALL_SPEC=/artifacts/<name>.tgz` |
| `AK_SETUP_FLAGS` (env) | `--codex --opencode --yes` | Full setup surface; add `--no-ruvnet-brain` to skip the ~2 GB KB, `--minimal` for the smallest footprint |
| `AK_SKIP_SETUP` (env) | `0` | `1` = install the kit but stop before setup (bare-kit debugging) |
| `AK_DASHBOARD_PORT` / `AK_BRIDGE_PORT` (env) | `7431` / `7432` | Only needed if you change the compose port mapping too |
| `UBUNTU_VERSION` (build arg) | `26.04` | OS matrix testing |
| `NODE_MAJOR` (build arg) | `24` | Node matrix testing (kit engines: `>=22`) |

## Standard maintainer workflows

```bash
# Validate the current `next` end-to-end (the release smoke)
docker compose up --build ak

# Bisect: does alpha.31 also fail?
AK_DIST_TAG=4.0.0-alpha.31 docker compose up ak

# Upgrade-path test: converge on alpha.N, then sync to alpha.N+1
docker compose --profile persistent run --rm -e AK_DIST_TAG=4.0.0-alpha.32 ak-persistent bash
# … let setup converge, exit …
docker compose --profile persistent run --rm ak-persistent bash
# inside: npm i -g @pacphi/agentic-kit@next && ak sync

# Architecture matrix (CI or a beefy host)
docker buildx build --platform linux/amd64,linux/arm64 .
```

## Regression artifacts

Each run that completes setup writes `artifacts/first-use-status.json` —
`ak status --json` as seen by a brand-new machine. Diff it across releases to
catch first-use regressions (a subsystem newly failing on clean install is
exactly the class of bug maintainers' converged machines can't see). This is
the seam for a future nightly job: GitHub Actions runs this same compose file
natively on Linux; compare the JSON against the previous run and alert on new
`fail` rows.

## Maintenance duties

- Keep `AK_SETUP_FLAGS` in step with `ak setup`'s option surface
(`src/commands/setup.mjs`) — a renamed flag here fails loudly at entrypoint.
- Bump `NODE_MAJOR` when the kit's `engines` floor moves; bump
`UBUNTU_VERSION` on new LTS. Both are build args — CI can matrix them
without file edits.
- The healthcheck allows `start_period: 600s` because install + full setup
precede the dashboard; if setup grows meaningfully slower, raise it rather
than letting orchestrators flap the container.
- `docker/*.md` is deliberately outside the repo's markdownlint globs
(`.markdownlint-cli2.jsonc` covers `docs/**`); keep these guides tidy by
hand.
106 changes: 106 additions & 0 deletions docker/USER-GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# First-use environment — user guide

You want to try `agentic-kit` the way a brand-new user would — on a clean OS,
with nothing pre-installed — without touching the tooling already on your
machine. This directory gives you that as one command, identically on macOS,
Windows (Docker Desktop / WSL2), and Linux.

**Isolation promise:** the container never mounts or reads your host's
`~/.claude`, `~/.codex`, `~/.npmrc`, npm prefix, or any installed CLI. A host
with any version of agentic-kit installed cannot conflict with these
containers, and the containers cannot alter your host install. Persistent
state lives only in Docker-managed named volumes; the single bind mount is
`./artifacts` inside this directory.

## Prerequisites

- Docker Desktop (macOS/Windows) or Docker Engine + Compose v2 (Linux).
- Nothing else — no Node, no npm, no agentic-kit on the host.

## Quick start

```bash
cd docker
docker compose up --build ak
```

What happens, in order — expect **10–15 minutes, network-dependent, on
*every* run of this service**: a true first-use re-downloads and re-installs
everything by design (only the image build itself is cached):

1. Ubuntu 26.04 + Node image builds (cached on later runs).
2. The container installs `@pacphi/agentic-kit@next` — the real first-install
path, against whatever `next` currently is.
3. `ak setup --codex --opencode --yes` runs: installs ruflo, agentic-qe, and
the claude/codex/opencode CLIs inside the container, wires everything.
4. The dashboard starts. **Watch the logs for a URL like:**

```text
http://127.0.0.1:7431/#token=…
```

Copy it into your **host** browser — it works verbatim. (The token is
required; the bare URL without the `#token` fragment is turned away.)

Stop with Ctrl-C (or `docker compose down`). Because this service keeps no
volumes, the next `up` is a genuine first-use again.

## Interactive exploration instead of the dashboard

```bash
docker compose run --rm ak bash # install + setup, then a shell
docker compose run --rm -e AK_SKIP_SETUP=1 ak bash # skip setup, bare kit
```

Inside: a sandbox git repo at `~/work/sandbox` is the project ak operates on.
`ak status`, `ak sync --dry-run`, `ak x verify all` etc. all work there.

## Keeping state between runs

```bash
docker compose --profile persistent up ak-persistent
```

Same environment, but `/home/tester` lives in a named volume, so the
converged install (and the ~2 GB RuvNet Brain KB, if you enable it) survives
restarts. Only the volume's first run pays the 10–15 minute install + setup;
converged restarts come up in seconds. Reset to factory:
`docker compose down --volumes`. Don't run both services at once — they
share the host port.

## Signing in to the AI CLIs (optional)

Everything infrastructural — setup, status, sync, dashboard, statusline —
works with **no** AI login. This is verified, not assumed: a zero-credential
container completes `ak setup --codex --opencode --yes` (including both MCP
bridge registrations, `ruflo init`/memory/swarm/daemon with a verified memory
write, and `aqe init --with-codex`), and a follow-up `ak sync` reports
**converged — no failing subsystems**. You only need auth to actually drive
sessions:

| CLI | Headless-container strategy |
| --- | --- |
| `claude` | `claude` login supports a paste-a-code flow in the terminal — run it inside `docker compose run --rm ak bash`. |
| `codex` | Easiest: `export OPENAI_API_KEY=…` before starting. The OAuth flow's `localhost:1455` callback can't cross the container boundary without extra bridging. |
| `opencode` | API keys via `opencode auth login` in the container shell. |

Never bind-mount host credential dirs into the container — if you must reuse
a login, `docker cp` the specific file in, deliberately.

## Common issues

- **Dashboard URL doesn't load** — use the exact printed URL (with `#token=`)
and confirm the container is still up. Plain `docker run -p` without this
compose file will *never* work: the dashboard binds loopback inside the
container by design; the bridge in this setup is what makes it reachable.
- **Port 7431 busy on the host** — another dashboard (maybe your host ak!) is
using it. Edit the left side of the port mapping in `compose.yaml`.
- **Apple Silicon vs Intel** — the image builds for your machine's native
architecture automatically. Don't force `--platform linux/amd64` on an
arm64 Mac; emulation breaks native Node modules and produces false
failures.
- **`artifacts/` permission errors (Linux)** — if Docker created the dir
root-owned, `sudo chown $USER docker/artifacts`.

Maintainers: design rationale, knobs, upgrade-path testing, and CI notes are
in [MAINTAINER-GUIDE.md](MAINTAINER-GUIDE.md).
Empty file added docker/artifacts/.gitkeep
Empty file.
47 changes: 47 additions & 0 deletions docker/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Isolated first-use environments for @pacphi/agentic-kit.
#
# Isolation contract: NOTHING from the host's own agentic-kit install is ever
# mounted or touched — no ~/.claude, ~/.codex, ~/.npmrc, no host binaries, no
# host npm prefix. The only bind mount is the repo-local ./artifacts dir; all
# persistent state lives in Docker-managed named volumes (namespaced
# agentic-kit-firstuse_*), so a host with any agentic-kit version installed
# cannot conflict with these containers, and vice versa.
name: agentic-kit-firstuse

services:
# True first-use: nothing persists. Every `up`/`run` starts from a bare
# Ubuntu + Node image, installs @next, runs setup, serves the dashboard.
ak:
build: .
image: agentic-kit-firstuse
init: true # reap daemon zombies; ak setup starts background workers
ports:
# HOST-loopback only — preserves the dashboard's localhost-only posture.
# Host 7431 → container 7432 (socat bridge) → container-loopback 7431,
# so the printed #token URL works verbatim in the host browser.
- "127.0.0.1:7431:7432"
environment:
AK_DIST_TAG: ${AK_DIST_TAG:-next}
AK_SETUP_FLAGS: ${AK_SETUP_FLAGS:---codex --opencode --yes}
volumes:
- ./artifacts:/artifacts
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/7431"]
interval: 15s
retries: 3
start_period: 600s # install + setup runs before the dashboard comes up

# Converged install that survives restarts — for iterating on a completed
# setup and for UPGRADE-path testing (install alpha.N, restart with
# AK_DIST_TAG pinned higher, run `ak sync` inside). Same port mapping, so
# don't run both profiles at once.
ak-persistent:
profiles: ["persistent"]
extends:
service: ak
volumes:
- home:/home/tester # named volume: seeds from the image on first run
- ./artifacts:/artifacts

volumes:
home:
65 changes: 65 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# First-use entrypoint: install the kit → run setup → serve the dashboard
# (or exec whatever command was given, e.g. `bash` for an interactive shell).
# Every knob is an env var so `docker compose run -e ...` can retune a run
# without an image rebuild. See MAINTAINER-GUIDE.md for the full knob table.
set -euo pipefail

AK_DIST_TAG="${AK_DIST_TAG:-next}"
# Full install spec override — lets maintainers test an UNPUBLISHED build:
# npm pack the checkout, drop the tarball into ./artifacts, and set
# AK_INSTALL_SPEC=/artifacts/<name>.tgz (mounted read-write by compose).
AK_INSTALL_SPEC="${AK_INSTALL_SPEC:-@pacphi/agentic-kit@${AK_DIST_TAG}}"
AK_SETUP_FLAGS="${AK_SETUP_FLAGS:---codex --opencode --yes}"
AK_SKIP_SETUP="${AK_SKIP_SETUP:-0}"
AK_DASHBOARD_PORT="${AK_DASHBOARD_PORT:-7431}" # container-loopback (ak's default)
AK_BRIDGE_PORT="${AK_BRIDGE_PORT:-7432}" # socat re-publish; the EXPOSEd port

# Sample project: ak is project-aware (statusline, settings.local.json, and the
# aqe router all anchor at a repo root), so first-use runs from a real git repo.
mkdir -p "$HOME/work/sandbox"
cd "$HOME/work/sandbox"
if [ ! -d .git ]; then
git init -q
git config user.email "tester@firstuse.invalid"
git config user.name "First-Use Tester"
echo "# sandbox — agentic-kit first-use project" > README.md
git add README.md && git commit -qm "init sandbox"
fi

if ! command -v ak >/dev/null 2>&1; then
echo "▶ installing ${AK_INSTALL_SPEC} (the real first-install path)"
npm install -g "${AK_INSTALL_SPEC}"
fi
echo "▶ agentic-kit $(ak --version 2>/dev/null || echo '(version probe failed)')"

if [ "${AK_SKIP_SETUP}" != "1" ]; then
echo "▶ ak setup ${AK_SETUP_FLAGS}"
# shellcheck disable=SC2086 # flags are deliberately word-split
ak setup ${AK_SETUP_FLAGS} \
|| echo "⚠ ak setup exited $? — continuing so the state stays inspectable"
fi

# Regression artifact: a machine-readable snapshot of what first-use produced.
if [ -d /artifacts ] && [ -w /artifacts ]; then
ak status --json > /artifacts/first-use-status.json 2>/dev/null \
&& echo "▶ wrote /artifacts/first-use-status.json"
fi

case "${1:-dashboard}" in
dashboard)
# ak's dashboard binds 127.0.0.1 by design (loopback + per-session token;
# docs/adr/0014-dashboard-auth-and-remediation.md). Docker port publishing
# cannot reach a container-loopback listener, so socat re-publishes it on
# the container interface; compose maps that back to HOST-loopback only —
# same security envelope, one hop longer.
socat "TCP-LISTEN:${AK_BRIDGE_PORT},fork,reuseaddr" \
"TCP:127.0.0.1:${AK_DASHBOARD_PORT}" &
echo "▶ copy the #token URL below into your HOST browser — it works verbatim"
echo " (host 127.0.0.1:${AK_DASHBOARD_PORT} → bridge :${AK_BRIDGE_PORT} → dashboard)"
exec ak dashboard --no-open --port "${AK_DASHBOARD_PORT}"
;;
*)
exec "$@"
;;
esac
Loading
Loading