diff --git a/docs/superpowers/plans/2026-09-26-docker-deployment.md b/docs/superpowers/plans/2026-09-26-docker-deployment.md new file mode 100644 index 0000000..a43bb6d --- /dev/null +++ b/docs/superpowers/plans/2026-09-26-docker-deployment.md @@ -0,0 +1,596 @@ +# Docker Deployment (Coolify) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Run the Telegram host as one non-root container, built from this repo's `Dockerfile` and started by `docker-compose.yml` on Coolify or any Docker host. The container ships the agent's tools, Node 24, the `claude` CLI and optionally Playwright MCP + Chromium, and keeps workspaces, sessions and the Claude login in three named volumes. + +**Architecture:** A two-stage `Dockerfile` (`sdk:10.0-noble` publishes the host framework-dependent; `runtime:10.0-noble` adds apt tools, NodeSource Node, `@anthropic-ai/claude-code`, optional `@playwright/mcp` + Chromium). The image's default user stays root so that PID 1 (compose's `init: true`, i.e. `docker-init`) is root-owned; the entrypoint `setpriv` drops to `app` (uid 1654) and execs `dotnet Volcanic.AgenticCore.Telegram.dll`. `docker-compose.yml` passes configuration through environment variables only. No C# behaviour changes; the one new unit test pins the Dockerfile's `PLAYWRIGHT_MCP_VERSION` to `PlaywrightPreset.DefaultPackage`. + +**Tech Stack:** Docker (BuildKit, Compose v2), `mcr.microsoft.com/dotnet/sdk:10.0-noble` and `runtime:10.0-noble`, NodeSource Node.js 24, `@anthropic-ai/claude-code@2.1`, `@playwright/mcp@0.0.82`, `util-linux` `setpriv`, xunit 2. Spec: `docs/superpowers/specs/2026-09-26-docker-deployment-design.md`. + +## Global Constraints + +- Work only in the worktree `D:\Projects\Names\wt-docker-deployment` (branch `feat/docker-deployment`). Every Bash command starts with `cd /d/Projects/Names/wt-docker-deployment && ...`; Read/Edit/Write use absolute paths under `D:\Projects\Names\wt-docker-deployment`. Never switch branches, never `git worktree`/`checkout`/`reset --hard`/`push`. Never touch `D:\Projects\Names\VolcanicAgenticCore` or another `wt-*` directory. +- Wave E runs alone, but keep edits to shared host files minimal. This plan changes **no** file under `src/`, `extensions/` or `hosts/`; if a task finds it has to, it edits the spec in the same commit and says so in the commit body. +- `Directory.Build.props` sets `TreatWarningsAsErrors=true` for non-test projects; the image build runs `dotnet publish -c Release`, so a Release-only warning fails the image build too. +- Tests: xunit 2. No test calls a paid API or any network endpoint, and no test runs Docker. Known baseline on this Windows machine: 3 tests in `Volcanic.AgenticCore.Tests` fail on `main` before any change (`SystemPromptBuilderTests.Loads_AGENTS_md_in_preference_to_CLAUDE_md_then_persona_and_memory`, `ExtensionLoaderTests.Discover_loads_a_real_assembly_from_the_user_directory`, `BuiltInToolTests.Ls_find_and_grep`). "Green" = no other failures. +- Docker on this machine: Docker Desktop 29, Linux containers. Run docker commands from Git Bash with `MSYS_NO_PATHCONV=1` in front (otherwise Git Bash rewrites container paths such as `/data/...` into Windows paths), or from PowerShell. Foreground `sleep` is blocked in the Bash tool; wait with PowerShell `Start-Sleep -Seconds 20` or the Monitor tool. +- Docker hygiene: never use real secrets (only `TELEGRAM_BOT_TOKEN=123:fake`, `TELEGRAM_ALLOWED_USER_IDS=1`); never push images; never `docker system prune`, `docker volume prune` or remove volumes/images you didn't create. Image tags this plan creates: `volcanic-agentic-core:test` (no Playwright), `volcanic-agentic-core:test-pw` (Playwright). Compose project name for test runs: `volcanic-test` (its volumes are `volcanic-test_workspaces`, `volcanic-test_agent`, `volcanic-test_claude`; its image `volcanic-test-bot`). Container names: `volcanic-probe`, `volcanic-probe-pw`. T6 removes all of them. +- Never commit `.env` (a dummy one is created for `docker compose config`/`up`), `bin/`, `obj/`, `launchSettings.json`, `hosts/Volcanic.AgenticCore.Telegram/Properties/`. +- Exact values from the spec and the code (copy verbatim): + - base images `mcr.microsoft.com/dotnet/sdk:10.0-noble`, `mcr.microsoft.com/dotnet/runtime:10.0-noble`; + - build args: `NODE_MAJOR=24`, `CLAUDE_CODE_VERSION=2.1`, `INSTALL_PLAYWRIGHT=false` (Dockerfile) / `true` (compose default), `PLAYWRIGHT_MCP_VERSION=0.0.82` (must equal `PlaywrightPreset.PackageVersion`); + - apt tools: `bash git ripgrep python3 python-is-python3 python3-venv curl ca-certificates tzdata jq less procps openssh-client` (+ `gnupg` for the NodeSource key); + - user `app`, uid/gid **1654**, `HOME=/home/app`; volumes `/data/workspaces`, `/data/agent`, `/data/claude`; `PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`; + - image env: `Telegram__WorkspaceRoot=/data/workspaces`, `VOLCANIC_AGENT_DIR=/data/agent`, `CLAUDE_CONFIG_DIR=/data/claude`, `HOME=/home/app`, `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_NOLOGO=1`, `TZ=UTC`, `DOTNET_EnableDiagnostics=0`; + - environment variables the host reads (verified in code): `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_IDS` (`TelegramOptions.ApplyEnvironment`), `OPENAI_API_KEY`, `STT_MODEL`, `DEFAULT_TIMEZONE` (same), `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (`GoogleCalendarSetup.Options`), `PROCESS_ENV_ALLOW` (`TelegramProcessEnvironment.AllowVariable`), `PLAYWRIGHT_ENABLED`, `PLAYWRIGHT_HEADLESS`, `PLAYWRIGHT_BROWSER`, `PLAYWRIGHT_EXTRA_ARGS`, `PLAYWRIGHT_COMMAND` (`McpOptions.ApplyEnvironment`), `VOLCANIC_AGENT_DIR` (`VolcanicPaths.DefaultAgentDir`, `GoogleCalendarOptions`), plus every `Section__Key` form through `Host.CreateApplicationBuilder`'s unprefixed environment provider (so `Telegram__WorkspaceRoot` and `Telegram__Model` bind to `TelegramOptions` exactly as the spec's compose assumes; `TELEGRAM_MODEL`, `LOG_LEVEL`, `INSTALL_PLAYWRIGHT`, `CLAUDE_CODE_VERSION` are compose-level variables only); + - the `@playwright/mcp@0.0.82` package's binary is `playwright-mcp` (`npm view @playwright/mcp@0.0.82 bin` → `{ 'playwright-mcp': 'cli.js' }`); it depends on `playwright` `1.64.0-alpha-1789764292000`; + - startup log lines to expect (from `Program.cs`): `Process environment: 15 deny rules, allow: none`, `MCP: playwright disabled (set PLAYWRIGHT_ENABLED=true)` / `MCP: playwright enabled (headless, chromium)`, `Speech to text: off (no OPENAI_API_KEY)`, `Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5`, `Scheduler: /data/agent/scheduler/jobs.json`, `Default time zone: ...`, `Allowed users: 1. Workspaces under /data/workspaces`, and **no** `The .NET diagnostic IPC socket is on ...` warning. +- **Deviation from the spec found while planning (T1 edits the spec):** the spec runs the image as `USER $APP_UID` with compose's `init: true`. Checked on Docker 29: `docker run --rm --init -u 1654 -e SECRET_PROBE=x mcr.microsoft.com/dotnet/runtime:10.0-noble sh -c 'tr "\0" "\n" \S+)\s*$", RegexOptions.Multiline); + + var match = Assert.Single(matches); + Assert.Equal(PlaywrightPreset.PackageVersion, match.Groups["version"].Value); + } + + /// A file in the repository root, found by walking up from the test output to VolcanicAgenticCore.slnx. + private static string RepoFile(string name) + { + for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) + { + if (File.Exists(Path.Combine(dir.FullName, "VolcanicAgenticCore.slnx"))) return Path.Combine(dir.FullName, name); + } + throw new InvalidOperationException("VolcanicAgenticCore.slnx not found above " + AppContext.BaseDirectory); + } +``` + +- [ ] **Step 2: Run it to verify it fails** + +Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet test tests/Volcanic.AgenticCore.Extensions.Tests --filter "FullyQualifiedName~PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage"` +Expected: FAIL with `FileNotFoundException` for `...\Dockerfile`. + +- [ ] **Step 3: Create `.gitattributes`, `.dockerignore` and `Dockerfile`** + +`.gitattributes` (the machine has `core.autocrlf=true`; keep these LF everywhere): + +``` +Dockerfile text eol=lf +.dockerignore text eol=lf +docker-compose.yml text eol=lf +.env.example text eol=lf +``` + +`.dockerignore` (Windows `obj/project.assets.json` in the context would break the Linux restore; `.env` must never reach the build): + +``` +**/bin +**/obj +**/TestResults +**/*.user +**/Properties/launchSettings.json +.git +.vs +.idea +.claude +.volcanic +.env +.env.* +tests +samples +docs +*.md +docker-compose.yml +``` + +`Dockerfile`: + +```dockerfile +# The Telegram host as one container. User guide: docs/docker.md. +# Spec: docs/superpowers/specs/2026-09-26-docker-deployment-design.md + +FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build +WORKDIR /src + +# Restore layer: only the project files the host needs, cached until one of them changes. +COPY Directory.Build.props VolcanicAgenticCore.slnx ./ +COPY src/Volcanic.AgenticCore.Abstractions/Volcanic.AgenticCore.Abstractions.csproj src/Volcanic.AgenticCore.Abstractions/ +COPY src/Volcanic.AgenticCore/Volcanic.AgenticCore.csproj src/Volcanic.AgenticCore/ +COPY extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/Volcanic.AgenticCore.Extensions.ClaudeCode.csproj extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/ +COPY extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.csproj extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/ +COPY extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/Volcanic.AgenticCore.Extensions.GoogleCalendar.csproj extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/ +COPY extensions/Volcanic.AgenticCore.Extensions.Mcp/Volcanic.AgenticCore.Extensions.Mcp.csproj extensions/Volcanic.AgenticCore.Extensions.Mcp/ +COPY extensions/Volcanic.AgenticCore.Extensions.Scheduler/Volcanic.AgenticCore.Extensions.Scheduler.csproj extensions/Volcanic.AgenticCore.Extensions.Scheduler/ +COPY hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj hosts/Volcanic.AgenticCore.Telegram/ +RUN dotnet restore hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj + +COPY src/ src/ +COPY extensions/ extensions/ +COPY hosts/ hosts/ +# Framework-dependent: claude starts the bridge as `dotnet .dll`, so the loose dll and its runtimeconfig must exist. +RUN dotnet publish hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj -c Release --no-restore -o /out \ + && test -f /out/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll \ + && test -f /out/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json + +FROM mcr.microsoft.com/dotnet/runtime:10.0-noble +ARG DEBIAN_FRONTEND=noninteractive +ARG NODE_MAJOR=24 +ARG CLAUDE_CODE_VERSION=2.1 +ARG INSTALL_PLAYWRIGHT=false +# Must equal the version in PlaywrightPreset.DefaultPackage (the single source of truth); +# PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage fails when they differ. +ARG PLAYWRIGHT_MCP_VERSION=0.0.82 + +# What the agent's bash tool can run, then Node from NodeSource (Ubuntu noble's nodejs is too old). +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash git ripgrep python3 python-is-python3 python3-venv curl ca-certificates tzdata jq less procps openssh-client gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# The global prefix stays root-owned, so claude's auto-updater can't replace it at runtime: an upgrade changes what +# system/init validation sees and invalidates the prompt cache, so it only arrives with a rebuild. +RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ + && claude --version \ + && npm cache clean --force + +# Set even without Playwright, so an MCP server started with the host environment finds browsers here. +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright +# INSTALL_PLAYWRIGHT block (T2) + +# The entrypoint hard-codes uid/gid 1654. Named volumes copy this ownership on first mount. +RUN test "$(id -u app)" = 1654 && test "$(id -g app)" = 1654 \ + && mkdir -p /data/workspaces /data/agent /data/claude /home/app \ + && chown -R app:app /data /home/app + +COPY --from=build /out /app +WORKDIR /app + +ENV Telegram__WorkspaceRoot=/data/workspaces \ + VOLCANIC_AGENT_DIR=/data/agent \ + CLAUDE_CONFIG_DIR=/data/claude \ + HOME=/home/app \ + DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + DOTNET_NOLOGO=1 \ + TZ=UTC \ + DOTNET_EnableDiagnostics=0 + +# No USER: PID 1 (docker-init with compose's init: true) must stay root, or the agent could read every secret from +# /proc/1/environ. setpriv drops to app and execs dotnet, so Environment.ProcessPath is the muxer the bridge needs. +ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"] +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet test tests/Volcanic.AgenticCore.Extensions.Tests --filter "FullyQualifiedName~PlaywrightPresetTests"` +Expected: all `PlaywrightPresetTests` PASS. Sanity-check the test bites: temporarily change the ARG to `0.0.81`, re-run, expect FAIL (`Expected: 0.0.82 Actual: 0.0.81`), change it back. + +- [ ] **Step 5: Build the image without Playwright** + +Run (allow ~10 min; network needed for base images, apt, NodeSource and npm): +`cd /d/Projects/Names/wt-docker-deployment && MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=false -t volcanic-agentic-core:test .` +Expected: success; the log shows the bridge `test -f` step and `claude --version` (2.1.x) passing. If `publish` drops the bridge runtimeconfig, fix the ClaudeCode csproj, not the Dockerfile (a local `dotnet publish` on Windows does produce `Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json`). If `dotnet restore` misses a project, check every `ProjectReference` again (host → core, ClaudeCode, GoogleCalendar, Mcp, Scheduler; all → Abstractions; ClaudeCode → Bridge). + +- [ ] **Step 6: Image sanity checks (no secrets)** + +Run the orchestrator's check as given (the entrypoint shell runs as root now; that is expected): +`MSYS_NO_PATHCONV=1 docker run --rm --entrypoint sh volcanic-agentic-core:test -c 'id; claude --version; test -f /app/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json && echo bridge-ok; echo $DOTNET_EnableDiagnostics'` +Expected: `uid=0(root)`, a `2.1.x` version, `bridge-ok`, `0`. + +Run the same as the bot's user, plus the agent tools: +`MSYS_NO_PATHCONV=1 docker run --rm --entrypoint setpriv volcanic-agentic-core:test --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- sh -c 'id; echo HOME=$HOME; claude --version; git --version; rg --version | head -1; python --version; python3 -m venv /tmp/v && echo venv-ok; node --version; npx --version; jq --version; ssh -V; ps --version; less --version | head -1; date; ls -ld /home/app /data /data/*; touch /data/workspaces/x /data/agent/x /data/claude/x && echo data-writable'` +Expected: `uid=1654(app) gid=1654(app)`, `HOME=/home/app`, node `v24.*`, `venv-ok`, `/home/app` and `/data/*` owned by `app`, `data-writable`. + +PID 1 check (the deviation above): +`MSYS_NO_PATHCONV=1 docker run --rm --init -e SECRET_PROBE=x --entrypoint setpriv volcanic-agentic-core:test --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- sh -c 'ps -o pid,user,comm -p 1; cat /proc/1/environ >/dev/null && echo READABLE || echo denied'` +Expected: PID 1 `root docker-init`, then `Permission denied` / `denied`. + +Record each command and its result for the report. + +- [ ] **Step 7: Edit the spec for the deviation** + +In `docs/superpowers/specs/2026-09-26-docker-deployment-design.md`: +- Decisions table, `Entry` row → ``ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]``: `setpriv` (util-linux, already in the base image) drops to `app` and execs `dotnet`, so `Environment.ProcessPath` is still the muxer `BridgeLocator.FindDotnet` wants. No entrypoint script | +- `User` row → The image's default user stays root so that PID 1 (compose's `init: true`, i.e. `docker-init`) is root-owned; the bot and everything it spawns run as the base image's non-root `app` user (1654, `$APP_UID`), `HOME=/home/app`. With `USER app`, `docker-init` runs as `app` and its `/proc/1/environ` (the whole container environment, secrets included) is readable by the agent's `bash` (checked on Docker 29) | +- Dockerfile §Build stage step 1: replace "(Abstractions, core, ClaudeCode, ClaudeCode.Bridge, Telegram, plus the Scheduler and GoogleCalendar extensions once the host references them)" with "(Abstractions, core, ClaudeCode, ClaudeCode.Bridge, GoogleCalendar, Mcp, Scheduler, Telegram)". +- Runtime step 5 → "`COPY --from=build /out /app`, `WORKDIR /app`. No `USER` (see the User row); an earlier step checks that `app` is uid/gid 1654, which the entrypoint hard-codes." +- Env table: add a row `HOME` | `/home/app` | The image user is root, so Docker would otherwise set `/root`. +- docker-compose §secrets bullet: "the bot (PID 1 in the container) makes itself non-dumpable" → "the bot (the only child of the root-owned PID 1) makes itself non-dumpable, and PID 1's own `/proc/1/environ` is root-only". +- First run §Fallback: `docker exec -it claude auth login --claudeai` → `docker exec -u app -it claude auth login --claudeai` (`docker exec` defaults to root, whose credentials the bot couldn't read). + +- [ ] **Step 8: Build, full tests, commit** + +Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet build && dotnet test` → green (only the 3 baseline failures). + +```bash +cd /d/Projects/Names/wt-docker-deployment && git add Dockerfile .dockerignore .gitattributes tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/PlaywrightPresetTests.cs docs/superpowers/specs/2026-09-26-docker-deployment-design.md && git commit -m "feat(docker): Dockerfile for the Telegram host" -m "Two-stage build, agent tools, Node 24, claude CLI, data dirs owned by app, DOTNET_EnableDiagnostics=0. Adds the MCP spec's deferred test that the Dockerfile's PLAYWRIGHT_MCP_VERSION equals PlaywrightPreset.DefaultPackage. + +Spec change: the image keeps root as its user and the entrypoint drops to app with setpriv. With USER app, compose's docker-init (PID 1) runs as app and its /proc/1/environ exposes every secret to the agent's bash." +``` + +--- + +### Task 2: Playwright in the image (`INSTALL_PLAYWRIGHT=true`) + +**Files:** +- Modify: `Dockerfile` (replace the `# INSTALL_PLAYWRIGHT block (T2)` line) + +**Interfaces:** +- Consumes: T1's `Dockerfile` (`ARG INSTALL_PLAYWRIGHT`, `ARG PLAYWRIGHT_MCP_VERSION=0.0.82`, `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`). +- Produces: `playwright-mcp` on PATH (`/usr/bin/playwright-mcp`), Chromium under `/opt/ms-playwright` readable by everyone, image `volcanic-agentic-core:test-pw` (kept until T6). + +- [ ] **Step 1: Replace the placeholder comment with the install block** + +```dockerfile +# Playwright MCP at the pinned version, and Chromium installed by that package's own playwright dependency, so browser +# build and library version match. Roughly +400 MB. +RUN if [ "$INSTALL_PLAYWRIGHT" = "true" ]; then \ + npm install -g "@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}" \ + && pw="$(npm root -g)/@playwright/mcp/node_modules/playwright/cli.js" \ + && test -f "$pw" \ + && node "$pw" install --with-deps chromium \ + && chmod -R a+rX "$PLAYWRIGHT_BROWSERS_PATH" \ + && playwright-mcp --version \ + && npm cache clean --force \ + && rm -rf /var/lib/apt/lists/*; \ + fi +``` + +If `test -f "$pw"` fails, list `$(npm root -g)/@playwright/mcp/node_modules` in a throwaway build step and point `pw` at the `playwright` package that `@playwright/mcp` itself resolves (never a separately installed `playwright`). If `playwright-mcp --version` isn't supported by 0.0.82, replace it with `npm ls -g @playwright/mcp` and grep for `@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}`, and note that in the report. + +- [ ] **Step 2: Test stays green** + +Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet test tests/Volcanic.AgenticCore.Extensions.Tests --filter "FullyQualifiedName~PlaywrightPresetTests"` → PASS (the ARG line is untouched and still unique). + +- [ ] **Step 3: Build both variants** + +Run: `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=false -t volcanic-agentic-core:test .` (fast; confirms the `if` is a no-op). +Run (background or with a long timeout; up to ~20 min): `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core:test-pw .` +Expected: both succeed; the second prints `0.0.82` from `playwright-mcp --version`. Record `docker image ls volcanic-agentic-core` sizes. + +- [ ] **Step 4: Offline checks as the bot's user** + +`MSYS_NO_PATHCONV=1 docker run --rm --network none --entrypoint setpriv volcanic-agentic-core:test-pw --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- sh -c 'command -v playwright-mcp; playwright-mcp --version; ls /opt/ms-playwright'` +Expected: `/usr/bin/playwright-mcp`, `0.0.82`, a `chromium-*` directory (and `chromium_headless_shell-*` if this Playwright version installs it). + +Headless Chromium launch as uid 1654 with `--no-sandbox`, offline: +`MSYS_NO_PATHCONV=1 docker run --rm --network none --entrypoint setpriv volcanic-agentic-core:test-pw --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- node -e 'const { chromium } = require("/usr/lib/node_modules/@playwright/mcp/node_modules/playwright"); (async () => { const b = await chromium.launch({ args: ["--no-sandbox"] }); const p = await b.newPage(); await p.setContent("

ok

"); console.log(await p.textContent("h1")); await b.close(); })().catch(e => { console.error(e); process.exit(1); });'` +Expected: `ok`. (Adjust the `require` path to what Step 1 found.) + +- [ ] **Step 5: Host startup with Playwright enabled, offline** + +`MSYS_NO_PATHCONV=1 docker run -d --name volcanic-probe-pw --init --network none -e TELEGRAM_BOT_TOKEN=123:fake -e TELEGRAM_ALLOWED_USER_IDS=1 -e PLAYWRIGHT_ENABLED=true -e PLAYWRIGHT_COMMAND=playwright-mcp -e PLAYWRIGHT_EXTRA_ARGS=--no-sandbox volcanic-agentic-core:test-pw`, wait ~15 s, `docker logs volcanic-probe-pw`. +Expected: `MCP: playwright enabled (headless, chromium)` and the other startup lines from Global Constraints; afterwards `Telegram unreachable (attempt n)` warnings (no network), no crash. Then `docker rm -f volcanic-probe-pw`. + +- [ ] **Step 6: Commit** + +```bash +cd /d/Projects/Names/wt-docker-deployment && git add Dockerfile && git commit -m "feat(docker): optional Playwright MCP and Chromium in the image" +``` + +--- + +### Task 3: docker-compose.yml, .env.example and a real host start + +**Files:** +- Create: `docker-compose.yml`, `.env.example` +- Modify: `.gitignore` (add `.env`) + +**Interfaces:** +- Consumes: T1's image contract (entrypoint, `/data/*` volumes, image env). +- Produces: compose service `bot`, volumes `workspaces`, `agent`, `claude`; the variable names that T4's `docs/docker.md` documents. + +- [ ] **Step 1: `.gitignore`** + +Append: + +``` +# Local compose configuration (secrets) +.env +``` + +- [ ] **Step 2: `docker-compose.yml`** (the spec's file verbatim, plus one comment on `init`) + +```yaml +services: + bot: + build: + context: . + args: + INSTALL_PLAYWRIGHT: ${INSTALL_PLAYWRIGHT:-true} + CLAUDE_CODE_VERSION: ${CLAUDE_CODE_VERSION:-2.1} + restart: unless-stopped + # dotnet does not reap orphans; killed `claude` trees and bash tool children would pile up as zombies. + # docker-init runs as root (the image user), so the agent can't read its /proc/1/environ. + init: true + # Chromium crashes with Docker's 64 MB /dev/shm. + shm_size: 1gb + stop_grace_period: 30s + environment: + - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + - TELEGRAM_ALLOWED_USER_IDS=${TELEGRAM_ALLOWED_USER_IDS} + - Telegram__Model=${TELEGRAM_MODEL:-claude-code/claude-opus-5-5} + - TZ=${TZ:-UTC} + - Logging__LogLevel__Default=${LOG_LEVEL:-Information} + # Optional features; empty means off / default. + - OPENAI_API_KEY=${OPENAI_API_KEY:-} # speech to text (attachments spec) + - STT_MODEL=${STT_MODEL:-} + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-} # Google Calendar (calendar spec) + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-} + - DEFAULT_TIMEZONE=${DEFAULT_TIMEZONE:-} # chats' default zone (scheduler spec §7); empty → TZ + - PROCESS_ENV_ALLOW=${PROCESS_ENV_ALLOW:-} # deny-listed variables the agent may see, e.g. GH_TOKEN + # Playwright MCP preset (MCP spec). Enabled when the image has it; the preinstalled binary, no npx download. + - PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-${INSTALL_PLAYWRIGHT:-true}} + - PLAYWRIGHT_HEADLESS=${PLAYWRIGHT_HEADLESS:-true} # no display in the container + - PLAYWRIGHT_BROWSER=${PLAYWRIGHT_BROWSER:-} + - PLAYWRIGHT_EXTRA_ARGS=${PLAYWRIGHT_EXTRA_ARGS:---no-sandbox} + - PLAYWRIGHT_COMMAND=${PLAYWRIGHT_COMMAND:-playwright-mcp} + volumes: + - workspaces:/data/workspaces + - agent:/data/agent + - claude:/data/claude + +volumes: + workspaces: + agent: + claude: +``` + +No `ports`, `expose`, `user:`, domain or `SERVICE_FQDN_*`; no `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_*` entries. + +- [ ] **Step 3: `.env.example`** (comments on their own lines; every value empty or a default) + +```dotenv +# Copy to .env for `docker compose up -d --build`. On Coolify, set these as the resource's environment variables. +# Never add ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, CLAUDE_CODE_OAUTH_TOKEN, +# CLAUDE_CODE_USE_BEDROCK or CLAUDE_CODE_USE_VERTEX: the Claude Code provider is subscription-only and refuses to run. + +# Required. Allowed Telegram user ids, comma separated; the first one is the owner (re-login prompts, "Online"). +TELEGRAM_BOT_TOKEN= +TELEGRAM_ALLOWED_USER_IDS= + +# Model every chat uses, log level, container time zone. +TELEGRAM_MODEL=claude-code/claude-opus-5-5 +LOG_LEVEL=Information +TZ=UTC + +# Build: Playwright MCP + Chromium in the image (~+400 MB); Claude Code version (2.1 = newest 2.1.x at build time). +INSTALL_PLAYWRIGHT=true +CLAUDE_CODE_VERSION=2.1 + +# Speech to text for voice notes (empty = off). +OPENAI_API_KEY= +STT_MODEL= + +# Google Calendar (both set = on; each chat connects its own account with /calendar_connect). +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# Default time zone for chats that never ran /timezone (IANA name, e.g. Europe/Berlin; empty = TZ). +DEFAULT_TIMEZONE= + +# Deny-listed variables the agent's processes may see anyway, comma separated (e.g. GH_TOKEN). +PROCESS_ENV_ALLOW= + +# Playwright MCP preset; empty = compose defaults (enabled if built in, headless, chromium, --no-sandbox, playwright-mcp). +# An override of PLAYWRIGHT_EXTRA_ARGS must keep --no-sandbox. +PLAYWRIGHT_ENABLED= +PLAYWRIGHT_HEADLESS= +PLAYWRIGHT_BROWSER= +PLAYWRIGHT_EXTRA_ARGS= +PLAYWRIGHT_COMMAND= +``` + +- [ ] **Step 4: `docker compose config` with a dummy `.env`** + +Create `.env` (never committed; `git status` must not list it): + +```dotenv +TELEGRAM_BOT_TOKEN=123:fake +TELEGRAM_ALLOWED_USER_IDS=1 +INSTALL_PLAYWRIGHT=false +``` + +Run: `cd /d/Projects/Names/wt-docker-deployment && docker compose -p volcanic-test config` +Expected: valid output with `INSTALL_PLAYWRIGHT: "false"` build arg, `PLAYWRIGHT_ENABLED: "false"` (nested default follows the build arg), `PLAYWRIGHT_EXTRA_ARGS: --no-sandbox`, `PLAYWRIGHT_COMMAND: playwright-mcp`, `Telegram__Model: claude-code/claude-opus-5-5`, `init: true`, `shm_size`, three named volumes, no `ports`. Also run it once with `.env.example` copied to `.env` plus the two required values, and check `PLAYWRIGHT_ENABLED: "true"`. + +- [ ] **Step 5: Real host start through compose (fake token)** + +Keep the dummy `.env` (`INSTALL_PLAYWRIGHT=false`). Run `docker compose -p volcanic-test up -d --build`, wait ~20 s, then `docker compose -p volcanic-test logs bot` and `docker compose -p volcanic-test ps -a`. +Expected in the log: the startup lines from Global Constraints (`Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5`, `Allowed users: 1. Workspaces under /data/workspaces`, `Process environment: 15 deny rules, allow: none`, `MCP: playwright disabled (set PLAYWRIGHT_ENABLED=true)`), no diagnostic-socket warning, then a `401`/unauthorized failure and exit code 1; `restart: unless-stopped` restarts it (visible restart loop). Nothing else may fail. +Then: `docker compose -p volcanic-test down -v --rmi local` (removes only this project's container, its three `volcanic-test_*` volumes and the `volcanic-test-bot` image). + +- [ ] **Step 6: Running-bot probe offline (`/proc` and diagnostics checks)** + +`MSYS_NO_PATHCONV=1 docker run -d --name volcanic-probe --init --network none -e TELEGRAM_BOT_TOKEN=123:fake -e TELEGRAM_ALLOWED_USER_IDS=1 -e OPENAI_API_KEY=sk-fake-probe-key volcanic-agentic-core:test`, wait ~10 s (no network → the poller backs off and keeps running), then: + +`MSYS_NO_PATHCONV=1 docker exec -u app volcanic-probe sh -c 'id; ps -o pid,user,comm -A; pid=$(pgrep -f Volcanic.AgenticCore.Telegram.dll); echo bot=$pid; cat /proc/1/environ >/dev/null 2>&1 && echo pid1-READABLE || echo pid1-denied; cat /proc/$pid/environ >/dev/null 2>&1 && echo bot-READABLE || echo bot-denied; ls /tmp/dotnet-diagnostic-* 2>/dev/null || echo no-diagnostic-socket'` +Expected: `uid=1654(app)`; PID 1 `docker-init` as `root`, the bot's `dotnet` as `app`; `pid1-denied`; `bot-denied`; `no-diagnostic-socket`. `docker logs volcanic-probe` shows the startup lines, `Speech to text: gpt-4o-transcribe`, and `Telegram unreachable` warnings. (The env filter itself, i.e. `TELEGRAM_BOT_TOKEN`/`OPENAI_API_KEY` missing from the agent's `bash`, needs a real chat; `docker exec` doesn't go through the spawn policy. Put it in `openIssues`.) +Then `docker rm -f volcanic-probe`, and delete the dummy `.env`. + +- [ ] **Step 7: Commit** + +```bash +cd /d/Projects/Names/wt-docker-deployment && git status --short && git add docker-compose.yml .env.example .gitignore && git commit -m "feat(docker): compose file and .env.example for Coolify" +``` + +--- + +### Task 4: `docs/docker.md` + +**Files:** +- Create: `docs/docker.md` + +**Interfaces:** +- Consumes: T1–T3's file names, build args, variable names, volume names, the `setpriv`/root PID 1 design. +- Produces: `docs/docker.md` with the anchors `#configuration`, `#first-run-and-claude-login`, `#troubleshooting` that T5 links to. + +- [ ] **Step 1: Write the doc** + +Sections and required content (plain prose, short; use the unslop skill's rules: no filler, no "robust/seamless"): + +1. `# Running the Telegram bot in Docker` intro: one container from the repo's `Dockerfile` + `docker-compose.yml`; Coolify or any Docker host; long polling, so no ports and no domain. Local run: `cp .env.example .env`, fill the two required values, `docker compose up -d --build`. `dotnet run` (see `telegram-bot.md`) stays the development path. +2. `## What's in the image`: .NET 10 runtime + the published host; agent tools (the apt list); Node 24 (NodeSource); `claude` (`@anthropic-ai/claude-code`); optionally `@playwright/mcp` 0.0.82 + Chromium under `/opt/ms-playwright`. No .NET SDK. The bot runs as `app` (uid 1654); PID 1 (`docker-init`) stays root so the agent can't read the container's environment from `/proc/1/environ`. Don't set `user:` in compose (`setpriv` needs root to switch users). +3. `## Build arguments` table: `INSTALL_PLAYWRIGHT` (Dockerfile `false`, compose `true`), `CLAUDE_CODE_VERSION` (`2.1`; exact version to pin), `NODE_MAJOR` (`24`), `PLAYWRIGHT_MCP_VERSION` (`0.0.82`; must equal `PlaywrightPreset.DefaultPackage`, a unit test checks it; compose doesn't override it). +4. `## Configuration`: environment only. Table `Variable | Default | Purpose | Agent's processes see it` with rows: `TELEGRAM_BOT_TOKEN` (required; no, redacted), `TELEGRAM_ALLOWED_USER_IDS` (required, comma separated, first = owner; yes), `TELEGRAM_MODEL` → `Telegram__Model` (`claude-code/claude-opus-5-5`), `TZ` (`UTC`), `LOG_LEVEL` → `Logging__LogLevel__Default` (`Information`), `OPENAI_API_KEY` (empty = speech to text off; no, redacted), `STT_MODEL` (`gpt-4o-transcribe`), `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` (both = calendar on; secret: no, redacted), `DEFAULT_TIMEZONE` (empty = `TZ`), `PROCESS_ENV_ALLOW`, `PLAYWRIGHT_ENABLED` (follows `INSTALL_PLAYWRIGHT`), `PLAYWRIGHT_HEADLESS` (`true`), `PLAYWRIGHT_BROWSER` (`chromium`), `PLAYWRIGHT_EXTRA_ARGS` (`--no-sandbox`, keep it when overriding), `PLAYWRIGHT_COMMAND` (`playwright-mcp`). Then: what the agent can't see: the default deny rules (`*_API_KEY`, `*_APIKEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` and their `Section__Key` forms, plus the named secrets), any variable whose value is a configured secret, and `[REDACTED]` in tool output; `PROCESS_ENV_ALLOW=GH_TOKEN` lets one through (an explicit name on the deny list, such as `TELEGRAM_BOT_TOKEN`, can't be allowed). Other settings: any `Section__Key` (`Telegram__ReconnectMaxDelay=00:02:00`, `Scheduler__*`, `GoogleCalendar__*`, `ProcessEnvironment__*`, `Telegram__Speech__*`, `Telegram__InputDebounce`, `VOLCANIC_CLAUDE_CODE_IDLE_TIMEOUT_MS`/`..._TOTAL_TIMEOUT_MS`) added in Coolify and to `environment:` in the compose file. Set in the image, normally left alone: `Telegram__WorkspaceRoot=/data/workspaces`, `VOLCANIC_AGENT_DIR=/data/agent`, `CLAUDE_CONFIG_DIR=/data/claude`, `DOTNET_EnableDiagnostics=0` (unset it, with `ProcessEnvironment__HardenHostProcess=false`, only to attach `dotnet-dump`/`dotnet-trace`). + A warning block: **Never set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_BEDROCK` or `CLAUDE_CODE_USE_VERTEX`.** The provider is subscription-only and refuses to run. In Coolify, check shared/team variables too. +5. `## Volumes`: `workspaces` → `/data/workspaces` (one directory per chat, `AGENTS.md`/`CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, `inbox/`, `browser/`, project-level `.volcanic/extensions/`), `agent` → `/data/agent` (sessions, `settings.json`, `extensions/*.dll`, chat settings, `scheduler/jobs.json`, `google-calendar/` tokens), `claude` → `/data/claude` (Claude login). Named volumes survive redeploys; Coolify prefixes them with the resource uuid and deletes them only with the resource's volumes. Bind mounts work if the host directories are owned by uid 1654 (`chown -R 1654:1654 `). `HOME` (`~/.gitconfig`, `~/.ssh`, npm cache) isn't persisted. +6. `## Deploying on Coolify`: new resource from this repo, build pack *Docker Compose*, set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_ALLOWED_USER_IDS` (owner first) plus any optional ones, no domain, deploy. The log shows `Using Claude Code at /usr/bin/claude, ...` and `Allowed users: ...`. No healthcheck: the bot serves no HTTP; lost connectivity backs off, a `401` exits 1 and invalid config exits 2, which `restart: unless-stopped` turns into a visible restart loop. +7. `## First run and Claude login`: owner sends `/start`, then `/claude-code-login`; the link and code prompt arrive as one message; paste the code; credentials land in `/data/claude`; `/claude-code-doctor` should report logged in, `claude.ai`, bridge ok. Later expiries use the owner re-login prompt. Fallback: `docker exec -u app -it claude auth login --claudeai` (`-u app` matters: `docker exec` defaults to root and root-owned credentials are unreadable by the bot). +8. `## Updating`: redeploy (rebuild) to get the newest `claude` 2.1.x; the CLI can't update itself in place. Set `CLAUDE_CODE_VERSION` to an exact version to pin. Bumping Playwright means changing `PlaywrightPreset.DefaultPackage` and the Dockerfile ARG in one commit. +9. `## Extensions and project context`: `docker cp my.dll :/data/agent/extensions/` + restart (built against the same `Volcanic.AgenticCore.Abstractions` version), or `/data/workspaces//.volcanic/extensions/`. Context files live in the chat workspace. +10. `## Troubleshooting`: `[executable_missing]` → image built without `claude`? rebuild, check `claude --version` in the build log; `[auth]` → `/claude-code-login` (or the `docker exec -u app` fallback); `[mcp_startup]` → bridge files missing from `/app` (`Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll` + `.runtimeconfig.json`; the build guard should have caught it); `Permission denied` under `/data` with bind mounts → `chown -R 1654:1654`; `409 Conflict` in the log → another instance or a webhook polls the same bot token; browser errors → `INSTALL_PLAYWRIGHT=true` build, `shm_size`, `--no-sandbox` kept. +11. `## Security`: short; the container is the isolation boundary; the process environment hook, non-dumpable bot, root PID 1, redaction; link to `telegram-bot.md#security`. + +- [ ] **Step 2: Check names against the code and the compose file** + +Run: `cd /d/Projects/Names/wt-docker-deployment && grep -o '[A-Z][A-Z_]*[A-Z]' docs/docker.md | sort -u` and confirm every variable name exists in `docker-compose.yml`, the `Dockerfile`, or the Global Constraints list; fix typos. + +- [ ] **Step 3: Commit** + +```bash +cd /d/Projects/Names/wt-docker-deployment && git add docs/docker.md && git commit -m "docs(docker): Docker and Coolify deployment guide" +``` + +--- + +### Task 5: Cross-links and the sibling specs + +**Files:** +- Modify: `README.md`, `docs/telegram-bot.md`, `docs/mcp.md`, `docs/superpowers/specs/2026-09-25-telegram-host-design.md`, `docs/superpowers/specs/2026-09-26-mcp-client-playwright-design.md` + +**Interfaces:** +- Consumes: `docs/docker.md` from T4. +- Produces: nothing code-facing. + +- [ ] **Step 1: `README.md`** + +After the Quick start code block (right after the line `TELEGRAM_BOT_TOKEN=123:abc ... dotnet run --project hosts/Volcanic.AgenticCore.Telegram` and the closing fence), add: + +```markdown +To run the bot in a container (Coolify or any Docker host): `cp .env.example .env`, fill in the two required values, +`docker compose up -d --build`. See [docs/docker.md](docs/docker.md). +``` + +In `## Documentation`, after the Telegram bot line, add: `- [Docker and Coolify](docs/docker.md): the container image, configuration, volumes, first login.` + +- [ ] **Step 2: `docs/telegram-bot.md`** + +In `## Security`, first paragraph: replace "Run the bot as a dedicated user or in a container, and treat the workspace root as writable by anyone on the allow-list." with "Run it in the container image ([docker.md](docker.md)), which is the recommended setup, or at least as a dedicated user, and treat the workspace root as writable by anyone on the allow-list." +Replace "Set `DOTNET_EnableDiagnostics=0` in the bot's environment too (a container image should set it): otherwise" with "Set `DOTNET_EnableDiagnostics=0` in the bot's environment too (the Docker image, [docker.md](docker.md), sets it): otherwise". + +- [ ] **Step 3: `docs/mcp.md`** + +Replace "The Docker image (`docker.md`, when the docker-deployment spec lands) preinstalls both" with "The Docker image ([docker.md](docker.md), built with `INSTALL_PLAYWRIGHT=true`) preinstalls both". + +- [ ] **Step 4: Sibling specs** + +- `2026-09-25-telegram-host-design.md` line 23: "Out of scope: webhooks, Docker/Coolify, voice and ..." → "Out of scope: webhooks, Docker/Coolify (see `2026-09-26-docker-deployment-design.md`), voice and ...". +- `2026-09-26-mcp-client-playwright-design.md` line 4: `Status: implemented (the Dockerfile version test arrives with 2026-09-26-docker-deployment-design.md)` → `Status: implemented (the Dockerfile version test, PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage, landed with 2026-09-26-docker-deployment-design.md)`. In the Testing bullet (around line 395), replace "(deferred: the Dockerfile and that test come with the docker-deployment spec)" with "(`Dockerfile_version_matches_DefaultPackage`, added with the docker-deployment spec)". + +- [ ] **Step 5: Verify links and commit** + +Run: `cd /d/Projects/Names/wt-docker-deployment && grep -n "docker.md" README.md docs/*.md` (each link target exists) and `grep -rn "a container image should set\|when the docker-deployment spec lands\|deferred: the Dockerfile" docs README.md` → no hits. + +```bash +cd /d/Projects/Names/wt-docker-deployment && git add README.md docs/telegram-bot.md docs/mcp.md docs/superpowers/specs/2026-09-25-telegram-host-design.md docs/superpowers/specs/2026-09-26-mcp-client-playwright-design.md && git commit -m "docs(docker): link the container setup from the README, bot and MCP docs" +``` + +--- + +### Task 6: Final verification, spec status and cleanup + +**Files:** +- Modify: `docs/superpowers/specs/2026-09-26-docker-deployment-design.md` (Status line, Verification section) + +**Interfaces:** +- Consumes: everything above. +- Produces: the verification record for the report. + +- [ ] **Step 1: Full .NET build and tests** + +Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet build && dotnet test` → green (only the 3 baseline failures), including `PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage`. + +- [ ] **Step 2: Rebuild both images from the final tree** + +`MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=false -t volcanic-agentic-core:test .` and `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core:test-pw .` (mostly cached). Re-run T1 Step 6's three checks against `:test` and T2 Step 4's `playwright-mcp --version` against `:test-pw`. Any failure → fix in the owning file, commit as `fix(docker): ...`. + +- [ ] **Step 3: Spec Status and Verification** + +In `2026-09-26-docker-deployment-design.md`: `Status: draft.` → `Status: implemented (plan: docs/superpowers/plans/2026-09-26-docker-deployment.md).` Replace the Verification intro "No unit tests (nothing in C# changes). Manual checklist in the implementation plan:" with "One unit test (`PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage`); no C# behaviour changes. Checked during implementation without a real bot: both image variants build; bridge guard, `claude --version`, the agent tools, uid 1654, `/data` ownership; root-owned PID 1 whose `/proc/1/environ` and the bot's are unreadable to `app`; no `dotnet-diagnostic-*` socket; `playwright-mcp --version` offline and a headless Chromium launch as `app`; `docker compose config`; host startup log and the `401` → exit 1 path. The rest of this list needs a real bot and stays a manual checklist:" and keep items 1–5. + +- [ ] **Step 4: Cleanup (only what this plan created)** + +```bash +cd /d/Projects/Names/wt-docker-deployment && docker rm -f volcanic-probe volcanic-probe-pw 2>/dev/null; docker compose -p volcanic-test down -v --rmi local 2>/dev/null; docker rmi volcanic-agentic-core:test volcanic-agentic-core:test-pw; docker volume ls --filter name=volcanic-test; docker image ls volcanic-agentic-core; rm -f .env; git status --short +``` + +Expected: no `volcanic-test_*` volumes, no `volcanic-agentic-core` images, no `.env`, a clean tree. Do not prune build cache or other images (base images pulled by the build may stay; list them in the report). + +- [ ] **Step 5: Commit** + +```bash +cd /d/Projects/Names/wt-docker-deployment && git add docs/superpowers/specs/2026-09-26-docker-deployment-design.md && git commit -m "docs(docker): mark the deployment spec implemented" +``` + +Report every verification step with its result. `openIssues` must list at least: the chat-driven checks (Verification 2 env/`date`/`/timezone`, `/claude-code-login` + `/claude-code-doctor` green, Verification 3 "tell me the H1" in a chat, Verification 4 redeploy with a logged-in bot, Verification 5 `ANTHROPIC_API_KEY` refusal), a real Coolify deploy, and anything from T1–T3 that could not be checked.