48 KiB
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(branchfeat/docker-deployment). Every Bash command starts withcd /d/Projects/Names/wt-docker-deployment && ...; Read/Edit/Write use absolute paths underD:\Projects\Names\wt-docker-deployment. Never switch branches, nevergit worktree/checkout/reset --hard/push. Never touchD:\Projects\Names\VolcanicAgenticCoreor anotherwt-*directory. - Wave E runs alone, but keep edits to shared host files minimal. This plan changes no file under
src/,extensions/orhosts/; if a task finds it has to, it edits the spec in the same commit and says so in the commit body. Directory.Build.propssetsTreatWarningsAsErrors=truefor non-test projects; the image build runsdotnet 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.Testsfail onmainbefore 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=1in front (otherwise Git Bash rewrites container paths such as/data/...into Windows paths), or from PowerShell. Foregroundsleepis blocked in the Bash tool; wait with PowerShellStart-Sleep -Seconds 20or the Monitor tool. - Docker hygiene: never use real secrets (only
TELEGRAM_BOT_TOKEN=123:fake,TELEGRAM_ALLOWED_USER_IDS=1); never push images; neverdocker system prune,docker volume pruneor 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 arevolcanic-test_workspaces,volcanic-test_agent,volcanic-test_claude; its imagevolcanic-test-bot). Container names:volcanic-probe,volcanic-probe-pw. T6 removes all of them. - Never commit
.env(a dummy one is created fordocker 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 equalPlaywrightPreset.PackageVersion); - apt tools:
bash git ripgrep python3 python-is-python3 python3-venv curl ca-certificates tzdata jq less procps openssh-client(+gnupgfor 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 everySection__Keyform throughHost.CreateApplicationBuilder's unprefixed environment provider (soTelegram__WorkspaceRootandTelegram__Modelbind toTelegramOptionsexactly as the spec's compose assumes;TELEGRAM_MODEL,LOG_LEVEL,INSTALL_PLAYWRIGHT,CLAUDE_CODE_VERSIONare compose-level variables only); - the
@playwright/mcp@0.0.82package's binary isplaywright-mcp(npm view @playwright/mcp@0.0.82 bin→{ 'playwright-mcp': 'cli.js' }); it depends onplaywright1.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 noThe .NET diagnostic IPC socket is on ...warning.
- base images
- Deviation from the spec found while planning (T1 edits the spec): the spec runs the image as
USER $APP_UIDwith compose'sinit: 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'printsSECRET_PROBE=x, becausedocker-initruns as the container user and is dumpable. The agent'sbashcould read every secret from/proc/1/environ, defeating process-environment-hook §8. Fix (also checked): leave the image user as root and useENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]; with--init,docker-initis PID 1 as root andcat /proc/1/environfrom uid 1654 fails withPermission denied.setprivexecsdotnet, soEnvironment.ProcessPathis still the muxer.docker execthen defaults to root, so the docs usedocker 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
Dockerfilewith the lineARG PLAYWRIGHT_MCP_VERSION=0.0.82, aENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwrightline followed by the comment# INSTALL_PLAYWRIGHT block (T2)that T2 replaces, and imagevolcanic-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,.dockerignoreandDockerfile
.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,
Entryrow →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 toappand execsdotnet, soEnvironment.ProcessPathis still the muxerBridgeLocator.FindDotnetwants. No entrypoint script | -
Userrow → The image's default user stays root so that PID 1 (compose'sinit: true, i.e.docker-init) is root-owned; the bot and everything it spawns run as the base image's non-rootappuser (1654,$APP_UID),HOME=/home/app. WithUSER app,docker-initruns asappand its/proc/1/environ(the whole container environment, secrets included) is readable by the agent'sbash(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. NoUSER(see the User row); an earlier step checks thatappis 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/environis root-only". -
First run §Fallback:
docker exec -it <container> claude auth login --claudeai→docker exec -u app -it <container> claude auth login --claudeai(docker execdefaults 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-mcpon PATH (/usr/bin/playwright-mcp), Chromium under/opt/ms-playwrightreadable by everyone, imagevolcanic-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, volumesworkspaces,agent,claude; the variable names that T4'sdocs/docker.mddocuments. -
Step 1:
.gitignore
Append:
# Local compose configuration (secrets)
.env
- Step 2:
docker-compose.yml(the spec's file verbatim, plus one comment oninit)
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 configwith 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 (
/procand 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.mdwith the anchors#configuration,#first-run-and-claude-login,#troubleshootingthat 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"):
# Running the Telegram bot in Dockerintro: one container from the repo'sDockerfile+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(seetelegram-bot.md) stays the development path.## 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/mcp0.0.82 + Chromium under/opt/ms-playwright. No .NET SDK. The bot runs asapp(uid 1654); PID 1 (docker-init) stays root so the agent can't read the container's environment from/proc/1/environ. Don't setuser:in compose (setprivneeds root to switch users).## Build argumentstable:INSTALL_PLAYWRIGHT(Dockerfilefalse, composetrue),CLAUDE_CODE_VERSION(2.1; exact version to pin),NODE_MAJOR(24),PLAYWRIGHT_MCP_VERSION(0.0.82; must equalPlaywrightPreset.DefaultPackage, a unit test checks it; compose doesn't override it).## Configuration: environment only. TableVariable | Default | Purpose | Agent's processes see itwith 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(followsINSTALL_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,*_PASSWORDand theirSection__Keyforms, plus the named secrets), any variable whose value is a configured secret, and[REDACTED]in tool output;PROCESS_ENV_ALLOW=GH_TOKENlets one through (an explicit name on the deny list, such asTELEGRAM_BOT_TOKEN, can't be allowed). Other settings: anySection__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 toenvironment: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, withProcessEnvironment__HardenHostProcess=false, only to attachdotnet-dump/dotnet-trace). A warning block: Never setANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL,CLAUDE_CODE_OAUTH_TOKEN,CLAUDE_CODE_USE_BEDROCKorCLAUDE_CODE_USE_VERTEX. The provider is subscription-only and refuses to run. In Coolify, check shared/team variables too.## 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.## Deploying on Coolify: new resource from this repo, build pack Docker Compose, setTELEGRAM_BOT_TOKENandTELEGRAM_ALLOWED_USER_IDS(owner first) plus any optional ones, no domain, deploy. The log showsUsing Claude Code at /usr/bin/claude, ...andAllowed users: .... No healthcheck: the bot serves no HTTP; lost connectivity backs off, a401exits 1 and invalid config exits 2, whichrestart: unless-stoppedturns into a visible restart loop.## 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-doctorshould 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 appmatters:docker execdefaults to root and root-owned credentials are unreadable by the bot).## Updating: redeploy (rebuild) to get the newestclaude2.1.x; the CLI can't update itself in place. SetCLAUDE_CODE_VERSIONto an exact version to pin. Bumping Playwright means changingPlaywrightPreset.DefaultPackageand the Dockerfile ARG in one commit.## Extensions and project context:docker cp my.dll <container>:/data/agent/extensions/+ restart (built against the sameVolcanic.AgenticCore.Abstractionsversion), or/data/workspaces/<chatId>/.volcanic/extensions/. Context files live in the chat workspace.## Troubleshooting:[executable_missing]→ image built withoutclaude? rebuild, checkclaude --versionin the build log;[auth]→/claude-code-login(or thedocker exec -u appfallback);[mcp_startup]→ bridge files missing from/app(Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll+.runtimeconfig.json; the build guard should have caught it);Permission deniedunder/datawith bind mounts →chown -R 1654:1654;409 Conflictin the log → another instance or a webhook polls the same bot token; browser errors →INSTALL_PLAYWRIGHT=truebuild,shm_size,--no-sandboxkept.## Security: short; the container is the isolation boundary; the process environment hook, non-dumpable bot, root PID 1, redaction; link totelegram-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"
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.mdfrom 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.mdline 23: "Out of scope: webhooks, Docker/Coolify, voice and ..." → "Out of scope: webhooks, Docker/Coolify (see2026-09-26-docker-deployment-design.md), voice and ...". -
2026-09-26-mcp-client-playwright-design.mdline 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.