Files
Playa/docs/superpowers/plans/2026-09-26-docker-deployment.md
2026-09-27 11:58:42 +02:00

48 KiB
Raw Blame History

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" </proc/1/environ | grep SECRET_PROBE' prints SECRET_PROBE=x, because docker-init runs as the container user and is dumpable. The agent's bash could read every secret from /proc/1/environ, defeating process-environment-hook §8. Fix (also checked): leave the image user as root and use ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]; with --init, docker-init is PID 1 as root and cat /proc/1/environ from uid 1654 fails with Permission denied. setpriv execs dotnet, so Environment.ProcessPath is still the muxer. docker exec then defaults to root, so the docs use docker exec -u app.
  • The spec is the source of truth; when the implementation has to differ, edit the spec in the same commit and say so in the commit body.
  • Commits: Conventional Commits, scope docker (feat(docker): ..., docs(docker): ...). Stage only the files the task lists.
  • Test commands (from the worktree root): dotnet test tests/Volcanic.AgenticCore.Extensions.Tests --filter "FullyQualifiedName~PlaywrightPresetTests"; end of T1 and T6: dotnet build && dotnet test (green = only the three baseline failures).

File map

File Change Task
tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/PlaywrightPresetTests.cs Dockerfile_version_matches_DefaultPackage + RepoFile helper T1
Dockerfile new: build stage, runtime stage, tools, Node, Claude CLI, env, setpriv entrypoint T1
.dockerignore new T1
.gitattributes new: LF for the Docker files T1
docs/superpowers/specs/2026-09-26-docker-deployment-design.md root PID 1 + setpriv deviation, csproj list T1
Dockerfile INSTALL_PLAYWRIGHT block T2
docker-compose.yml, .env.example new T3
.gitignore .env T3
docs/docker.md new user doc T4
README.md, docs/telegram-bot.md, docs/mcp.md links, container recommendation, DOTNET_EnableDiagnostics wording T5
docs/superpowers/specs/2026-09-25-telegram-host-design.md, docs/superpowers/specs/2026-09-26-mcp-client-playwright-design.md out-of-scope line, Status note, deferred test bullet T5
docs/superpowers/specs/2026-09-26-docker-deployment-design.md Status, Verification results T6

Spec Testing / Verification coverage

Spec item Where
MCP spec's deferred test: Dockerfile PLAYWRIGHT_MCP_VERSION equals DefaultPackage T1 PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage
Verification 1: docker compose build with INSTALL_PLAYWRIGHT=false and true; bridge guard and claude --version pass T1 (false), T2 (true), T3 (docker compose up --build), T6 (re-run from cache)
Verification 2: uid 1654, secrets not in the agent's env, /proc/1/environ denied, no dotnet-diagnostic-* socket T3 probe container (--network none, docker exec -u app); the chat-driven parts (real bot, /claude-code-login, /timezone) are manual → report openIssues
Verification 3: git, rg, python3, node, npx; playwright-mcp --version offline; browser works T1 tool check, T2 offline --version + headless Chromium launch as uid 1654; "tell me the H1" in a real chat is manual → openIssues
Verification 4: redeploy keeps chats, login, workspace files T3 checks volume ownership/writability; a real redeploy with a logged-in bot is manual → openIssues
Verification 5: ANTHROPIC_API_KEY makes the provider refuse Manual (needs a real chat) → openIssues; covered in code by existing ClaudeCode tests
Docs section (docker.md, README, telegram-bot.md, telegram-host spec line) T4, T5

Task 1: Dockerfile, .dockerignore and the Playwright version pin test

Files:

  • Modify: tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/PlaywrightPresetTests.cs
  • Create: Dockerfile, .dockerignore, .gitattributes
  • Modify: docs/superpowers/specs/2026-09-26-docker-deployment-design.md

Interfaces:

  • Consumes: PlaywrightPreset.PackageVersion ("0.0.82", extensions/Volcanic.AgenticCore.Extensions.Mcp/PlaywrightPreset.cs).

  • Produces: repo-root Dockerfile with the line ARG PLAYWRIGHT_MCP_VERSION=0.0.82, a ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright line followed by the comment # INSTALL_PLAYWRIGHT block (T2) that T2 replaces, and image volcanic-agentic-core:test.

  • Step 1: Write the failing test

Append to PlaywrightPresetTests (the file already has using System.Text.RegularExpressions;):

    [Fact]
    public void Dockerfile_version_matches_DefaultPackage()
    {
        var dockerfile = File.ReadAllText(RepoFile("Dockerfile"));
        var matches = Regex.Matches(dockerfile, @"^ARG PLAYWRIGHT_MCP_VERSION=(?<version>\S+)\s*$", RegexOptions.Multiline);

        var match = Assert.Single(matches);
        Assert.Equal(PlaywrightPreset.PackageVersion, match.Groups["version"].Value);
    }

    /// <summary>A file in the repository root, found by walking up from the test output to <c>VolcanicAgenticCore.slnx</c>.</summary>
    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:

# 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 <Bridge>.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 <container> claude auth login --claudeai → docker exec -u app -it <container> 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).

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

# 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("<h1>ok</h1>"); 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
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)
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)
# 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):

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
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 <dir>). 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 <container> 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 <container>:/data/agent/extensions/ + restart (built against the same Volcanic.AgenticCore.Abstractions version), or /data/workspaces/<chatId>/.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
cd /d/Projects/Names/wt-docker-deployment && git add docs/docker.md && git commit -m "docs(docker): Docker and Coolify deployment guide"

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:

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), 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, 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, 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.

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)
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
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.