forked from admin/VolcanicAgenticCore
docs(plan): docker-deployment implementation plan
This commit is contained in:
596
docs/superpowers/plans/2026-09-26-docker-deployment.md
Normal file
596
docs/superpowers/plans/2026-09-26-docker-deployment.md
Normal file
@@ -0,0 +1,596 @@
|
||||
# Docker Deployment (Coolify) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Run the Telegram host as one non-root container, built from this repo's `Dockerfile` and started by `docker-compose.yml` on Coolify or any Docker host. The container ships the agent's tools, Node 24, the `claude` CLI and optionally Playwright MCP + Chromium, and keeps workspaces, sessions and the Claude login in three named volumes.
|
||||
|
||||
**Architecture:** A two-stage `Dockerfile` (`sdk:10.0-noble` publishes the host framework-dependent; `runtime:10.0-noble` adds apt tools, NodeSource Node, `@anthropic-ai/claude-code`, optional `@playwright/mcp` + Chromium). The image's default user stays root so that PID 1 (compose's `init: true`, i.e. `docker-init`) is root-owned; the entrypoint `setpriv` drops to `app` (uid 1654) and execs `dotnet Volcanic.AgenticCore.Telegram.dll`. `docker-compose.yml` passes configuration through environment variables only. No C# behaviour changes; the one new unit test pins the Dockerfile's `PLAYWRIGHT_MCP_VERSION` to `PlaywrightPreset.DefaultPackage`.
|
||||
|
||||
**Tech Stack:** Docker (BuildKit, Compose v2), `mcr.microsoft.com/dotnet/sdk:10.0-noble` and `runtime:10.0-noble`, NodeSource Node.js 24, `@anthropic-ai/claude-code@2.1`, `@playwright/mcp@0.0.82`, `util-linux` `setpriv`, xunit 2. Spec: `docs/superpowers/specs/2026-09-26-docker-deployment-design.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work only in the worktree `D:\Projects\Names\wt-docker-deployment` (branch `feat/docker-deployment`). Every Bash command starts with `cd /d/Projects/Names/wt-docker-deployment && ...`; Read/Edit/Write use absolute paths under `D:\Projects\Names\wt-docker-deployment`. Never switch branches, never `git worktree`/`checkout`/`reset --hard`/`push`. Never touch `D:\Projects\Names\VolcanicAgenticCore` or another `wt-*` directory.
|
||||
- Wave E runs alone, but keep edits to shared host files minimal. This plan changes **no** file under `src/`, `extensions/` or `hosts/`; if a task finds it has to, it edits the spec in the same commit and says so in the commit body.
|
||||
- `Directory.Build.props` sets `TreatWarningsAsErrors=true` for non-test projects; the image build runs `dotnet publish -c Release`, so a Release-only warning fails the image build too.
|
||||
- Tests: xunit 2. No test calls a paid API or any network endpoint, and no test runs Docker. Known baseline on this Windows machine: 3 tests in `Volcanic.AgenticCore.Tests` fail on `main` before any change (`SystemPromptBuilderTests.Loads_AGENTS_md_in_preference_to_CLAUDE_md_then_persona_and_memory`, `ExtensionLoaderTests.Discover_loads_a_real_assembly_from_the_user_directory`, `BuiltInToolTests.Ls_find_and_grep`). "Green" = no other failures.
|
||||
- Docker on this machine: Docker Desktop 29, Linux containers. Run docker commands from Git Bash with `MSYS_NO_PATHCONV=1` in front (otherwise Git Bash rewrites container paths such as `/data/...` into Windows paths), or from PowerShell. Foreground `sleep` is blocked in the Bash tool; wait with PowerShell `Start-Sleep -Seconds 20` or the Monitor tool.
|
||||
- Docker hygiene: never use real secrets (only `TELEGRAM_BOT_TOKEN=123:fake`, `TELEGRAM_ALLOWED_USER_IDS=1`); never push images; never `docker system prune`, `docker volume prune` or remove volumes/images you didn't create. Image tags this plan creates: `volcanic-agentic-core:test` (no Playwright), `volcanic-agentic-core:test-pw` (Playwright). Compose project name for test runs: `volcanic-test` (its volumes are `volcanic-test_workspaces`, `volcanic-test_agent`, `volcanic-test_claude`; its image `volcanic-test-bot`). Container names: `volcanic-probe`, `volcanic-probe-pw`. T6 removes all of them.
|
||||
- Never commit `.env` (a dummy one is created for `docker compose config`/`up`), `bin/`, `obj/`, `launchSettings.json`, `hosts/Volcanic.AgenticCore.Telegram/Properties/`.
|
||||
- Exact values from the spec and the code (copy verbatim):
|
||||
- base images `mcr.microsoft.com/dotnet/sdk:10.0-noble`, `mcr.microsoft.com/dotnet/runtime:10.0-noble`;
|
||||
- build args: `NODE_MAJOR=24`, `CLAUDE_CODE_VERSION=2.1`, `INSTALL_PLAYWRIGHT=false` (Dockerfile) / `true` (compose default), `PLAYWRIGHT_MCP_VERSION=0.0.82` (must equal `PlaywrightPreset.PackageVersion`);
|
||||
- apt tools: `bash git ripgrep python3 python-is-python3 python3-venv curl ca-certificates tzdata jq less procps openssh-client` (+ `gnupg` for the NodeSource key);
|
||||
- user `app`, uid/gid **1654**, `HOME=/home/app`; volumes `/data/workspaces`, `/data/agent`, `/data/claude`; `PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`;
|
||||
- image env: `Telegram__WorkspaceRoot=/data/workspaces`, `VOLCANIC_AGENT_DIR=/data/agent`, `CLAUDE_CONFIG_DIR=/data/claude`, `HOME=/home/app`, `DOTNET_CLI_TELEMETRY_OPTOUT=1`, `DOTNET_NOLOGO=1`, `TZ=UTC`, `DOTNET_EnableDiagnostics=0`;
|
||||
- environment variables the host reads (verified in code): `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_IDS` (`TelegramOptions.ApplyEnvironment`), `OPENAI_API_KEY`, `STT_MODEL`, `DEFAULT_TIMEZONE` (same), `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (`GoogleCalendarSetup.Options`), `PROCESS_ENV_ALLOW` (`TelegramProcessEnvironment.AllowVariable`), `PLAYWRIGHT_ENABLED`, `PLAYWRIGHT_HEADLESS`, `PLAYWRIGHT_BROWSER`, `PLAYWRIGHT_EXTRA_ARGS`, `PLAYWRIGHT_COMMAND` (`McpOptions.ApplyEnvironment`), `VOLCANIC_AGENT_DIR` (`VolcanicPaths.DefaultAgentDir`, `GoogleCalendarOptions`), plus every `Section__Key` form through `Host.CreateApplicationBuilder`'s unprefixed environment provider (so `Telegram__WorkspaceRoot` and `Telegram__Model` bind to `TelegramOptions` exactly as the spec's compose assumes; `TELEGRAM_MODEL`, `LOG_LEVEL`, `INSTALL_PLAYWRIGHT`, `CLAUDE_CODE_VERSION` are compose-level variables only);
|
||||
- the `@playwright/mcp@0.0.82` package's binary is `playwright-mcp` (`npm view @playwright/mcp@0.0.82 bin` → `{ 'playwright-mcp': 'cli.js' }`); it depends on `playwright` `1.64.0-alpha-1789764292000`;
|
||||
- startup log lines to expect (from `Program.cs`): `Process environment: 15 deny rules, allow: none`, `MCP: playwright disabled (set PLAYWRIGHT_ENABLED=true)` / `MCP: playwright enabled (headless, chromium)`, `Speech to text: off (no OPENAI_API_KEY)`, `Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5`, `Scheduler: /data/agent/scheduler/jobs.json`, `Default time zone: ...`, `Allowed users: 1. Workspaces under /data/workspaces`, and **no** `The .NET diagnostic IPC socket is on ...` warning.
|
||||
- **Deviation from the spec found while planning (T1 edits the spec):** the spec runs the image as `USER $APP_UID` with compose's `init: true`. Checked on Docker 29: `docker run --rm --init -u 1654 -e SECRET_PROBE=x mcr.microsoft.com/dotnet/runtime:10.0-noble sh -c 'tr "\0" "\n" </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;`):
|
||||
|
||||
```csharp
|
||||
[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`:
|
||||
|
||||
```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).
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git add Dockerfile .dockerignore .gitattributes tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/PlaywrightPresetTests.cs docs/superpowers/specs/2026-09-26-docker-deployment-design.md && git commit -m "feat(docker): Dockerfile for the Telegram host" -m "Two-stage build, agent tools, Node 24, claude CLI, data dirs owned by app, DOTNET_EnableDiagnostics=0. Adds the MCP spec's deferred test that the Dockerfile's PLAYWRIGHT_MCP_VERSION equals PlaywrightPreset.DefaultPackage.
|
||||
|
||||
Spec change: the image keeps root as its user and the entrypoint drops to app with setpriv. With USER app, compose's docker-init (PID 1) runs as app and its /proc/1/environ exposes every secret to the agent's bash."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Playwright in the image (`INSTALL_PLAYWRIGHT=true`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `Dockerfile` (replace the `# INSTALL_PLAYWRIGHT block (T2)` line)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: T1's `Dockerfile` (`ARG INSTALL_PLAYWRIGHT`, `ARG PLAYWRIGHT_MCP_VERSION=0.0.82`, `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`).
|
||||
- Produces: `playwright-mcp` on PATH (`/usr/bin/playwright-mcp`), Chromium under `/opt/ms-playwright` readable by everyone, image `volcanic-agentic-core:test-pw` (kept until T6).
|
||||
|
||||
- [ ] **Step 1: Replace the placeholder comment with the install block**
|
||||
|
||||
```dockerfile
|
||||
# Playwright MCP at the pinned version, and Chromium installed by that package's own playwright dependency, so browser
|
||||
# build and library version match. Roughly +400 MB.
|
||||
RUN if [ "$INSTALL_PLAYWRIGHT" = "true" ]; then \
|
||||
npm install -g "@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}" \
|
||||
&& pw="$(npm root -g)/@playwright/mcp/node_modules/playwright/cli.js" \
|
||||
&& test -f "$pw" \
|
||||
&& node "$pw" install --with-deps chromium \
|
||||
&& chmod -R a+rX "$PLAYWRIGHT_BROWSERS_PATH" \
|
||||
&& playwright-mcp --version \
|
||||
&& npm cache clean --force \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
```
|
||||
|
||||
If `test -f "$pw"` fails, list `$(npm root -g)/@playwright/mcp/node_modules` in a throwaway build step and point `pw` at the `playwright` package that `@playwright/mcp` itself resolves (never a separately installed `playwright`). If `playwright-mcp --version` isn't supported by 0.0.82, replace it with `npm ls -g @playwright/mcp` and grep for `@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}`, and note that in the report.
|
||||
|
||||
- [ ] **Step 2: Test stays green**
|
||||
|
||||
Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet test tests/Volcanic.AgenticCore.Extensions.Tests --filter "FullyQualifiedName~PlaywrightPresetTests"` → PASS (the ARG line is untouched and still unique).
|
||||
|
||||
- [ ] **Step 3: Build both variants**
|
||||
|
||||
Run: `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=false -t volcanic-agentic-core:test .` (fast; confirms the `if` is a no-op).
|
||||
Run (background or with a long timeout; up to ~20 min): `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core:test-pw .`
|
||||
Expected: both succeed; the second prints `0.0.82` from `playwright-mcp --version`. Record `docker image ls volcanic-agentic-core` sizes.
|
||||
|
||||
- [ ] **Step 4: Offline checks as the bot's user**
|
||||
|
||||
`MSYS_NO_PATHCONV=1 docker run --rm --network none --entrypoint setpriv volcanic-agentic-core:test-pw --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- sh -c 'command -v playwright-mcp; playwright-mcp --version; ls /opt/ms-playwright'`
|
||||
Expected: `/usr/bin/playwright-mcp`, `0.0.82`, a `chromium-*` directory (and `chromium_headless_shell-*` if this Playwright version installs it).
|
||||
|
||||
Headless Chromium launch as uid 1654 with `--no-sandbox`, offline:
|
||||
`MSYS_NO_PATHCONV=1 docker run --rm --network none --entrypoint setpriv volcanic-agentic-core:test-pw --reuid=1654 --regid=1654 --init-groups --inh-caps=-all -- node -e 'const { chromium } = require("/usr/lib/node_modules/@playwright/mcp/node_modules/playwright"); (async () => { const b = await chromium.launch({ args: ["--no-sandbox"] }); const p = await b.newPage(); await p.setContent("<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**
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git add Dockerfile && git commit -m "feat(docker): optional Playwright MCP and Chromium in the image"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: docker-compose.yml, .env.example and a real host start
|
||||
|
||||
**Files:**
|
||||
- Create: `docker-compose.yml`, `.env.example`
|
||||
- Modify: `.gitignore` (add `.env`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: T1's image contract (entrypoint, `/data/*` volumes, image env).
|
||||
- Produces: compose service `bot`, volumes `workspaces`, `agent`, `claude`; the variable names that T4's `docs/docker.md` documents.
|
||||
|
||||
- [ ] **Step 1: `.gitignore`**
|
||||
|
||||
Append:
|
||||
|
||||
```
|
||||
# Local compose configuration (secrets)
|
||||
.env
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `docker-compose.yml`** (the spec's file verbatim, plus one comment on `init`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
INSTALL_PLAYWRIGHT: ${INSTALL_PLAYWRIGHT:-true}
|
||||
CLAUDE_CODE_VERSION: ${CLAUDE_CODE_VERSION:-2.1}
|
||||
restart: unless-stopped
|
||||
# dotnet does not reap orphans; killed `claude` trees and bash tool children would pile up as zombies.
|
||||
# docker-init runs as root (the image user), so the agent can't read its /proc/1/environ.
|
||||
init: true
|
||||
# Chromium crashes with Docker's 64 MB /dev/shm.
|
||||
shm_size: 1gb
|
||||
stop_grace_period: 30s
|
||||
environment:
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- TELEGRAM_ALLOWED_USER_IDS=${TELEGRAM_ALLOWED_USER_IDS}
|
||||
- Telegram__Model=${TELEGRAM_MODEL:-claude-code/claude-opus-5-5}
|
||||
- TZ=${TZ:-UTC}
|
||||
- Logging__LogLevel__Default=${LOG_LEVEL:-Information}
|
||||
# Optional features; empty means off / default.
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-} # speech to text (attachments spec)
|
||||
- STT_MODEL=${STT_MODEL:-}
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-} # Google Calendar (calendar spec)
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||
- DEFAULT_TIMEZONE=${DEFAULT_TIMEZONE:-} # chats' default zone (scheduler spec §7); empty → TZ
|
||||
- PROCESS_ENV_ALLOW=${PROCESS_ENV_ALLOW:-} # deny-listed variables the agent may see, e.g. GH_TOKEN
|
||||
# Playwright MCP preset (MCP spec). Enabled when the image has it; the preinstalled binary, no npx download.
|
||||
- PLAYWRIGHT_ENABLED=${PLAYWRIGHT_ENABLED:-${INSTALL_PLAYWRIGHT:-true}}
|
||||
- PLAYWRIGHT_HEADLESS=${PLAYWRIGHT_HEADLESS:-true} # no display in the container
|
||||
- PLAYWRIGHT_BROWSER=${PLAYWRIGHT_BROWSER:-}
|
||||
- PLAYWRIGHT_EXTRA_ARGS=${PLAYWRIGHT_EXTRA_ARGS:---no-sandbox}
|
||||
- PLAYWRIGHT_COMMAND=${PLAYWRIGHT_COMMAND:-playwright-mcp}
|
||||
volumes:
|
||||
- workspaces:/data/workspaces
|
||||
- agent:/data/agent
|
||||
- claude:/data/claude
|
||||
|
||||
volumes:
|
||||
workspaces:
|
||||
agent:
|
||||
claude:
|
||||
```
|
||||
|
||||
No `ports`, `expose`, `user:`, domain or `SERVICE_FQDN_*`; no `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_*` entries.
|
||||
|
||||
- [ ] **Step 3: `.env.example`** (comments on their own lines; every value empty or a default)
|
||||
|
||||
```dotenv
|
||||
# Copy to .env for `docker compose up -d --build`. On Coolify, set these as the resource's environment variables.
|
||||
# Never add ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, CLAUDE_CODE_OAUTH_TOKEN,
|
||||
# CLAUDE_CODE_USE_BEDROCK or CLAUDE_CODE_USE_VERTEX: the Claude Code provider is subscription-only and refuses to run.
|
||||
|
||||
# Required. Allowed Telegram user ids, comma separated; the first one is the owner (re-login prompts, "Online").
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_ALLOWED_USER_IDS=
|
||||
|
||||
# Model every chat uses, log level, container time zone.
|
||||
TELEGRAM_MODEL=claude-code/claude-opus-5-5
|
||||
LOG_LEVEL=Information
|
||||
TZ=UTC
|
||||
|
||||
# Build: Playwright MCP + Chromium in the image (~+400 MB); Claude Code version (2.1 = newest 2.1.x at build time).
|
||||
INSTALL_PLAYWRIGHT=true
|
||||
CLAUDE_CODE_VERSION=2.1
|
||||
|
||||
# Speech to text for voice notes (empty = off).
|
||||
OPENAI_API_KEY=
|
||||
STT_MODEL=
|
||||
|
||||
# Google Calendar (both set = on; each chat connects its own account with /calendar_connect).
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Default time zone for chats that never ran /timezone (IANA name, e.g. Europe/Berlin; empty = TZ).
|
||||
DEFAULT_TIMEZONE=
|
||||
|
||||
# Deny-listed variables the agent's processes may see anyway, comma separated (e.g. GH_TOKEN).
|
||||
PROCESS_ENV_ALLOW=
|
||||
|
||||
# Playwright MCP preset; empty = compose defaults (enabled if built in, headless, chromium, --no-sandbox, playwright-mcp).
|
||||
# An override of PLAYWRIGHT_EXTRA_ARGS must keep --no-sandbox.
|
||||
PLAYWRIGHT_ENABLED=
|
||||
PLAYWRIGHT_HEADLESS=
|
||||
PLAYWRIGHT_BROWSER=
|
||||
PLAYWRIGHT_EXTRA_ARGS=
|
||||
PLAYWRIGHT_COMMAND=
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `docker compose config` with a dummy `.env`**
|
||||
|
||||
Create `.env` (never committed; `git status` must not list it):
|
||||
|
||||
```dotenv
|
||||
TELEGRAM_BOT_TOKEN=123:fake
|
||||
TELEGRAM_ALLOWED_USER_IDS=1
|
||||
INSTALL_PLAYWRIGHT=false
|
||||
```
|
||||
|
||||
Run: `cd /d/Projects/Names/wt-docker-deployment && docker compose -p volcanic-test config`
|
||||
Expected: valid output with `INSTALL_PLAYWRIGHT: "false"` build arg, `PLAYWRIGHT_ENABLED: "false"` (nested default follows the build arg), `PLAYWRIGHT_EXTRA_ARGS: --no-sandbox`, `PLAYWRIGHT_COMMAND: playwright-mcp`, `Telegram__Model: claude-code/claude-opus-5-5`, `init: true`, `shm_size`, three named volumes, no `ports`. Also run it once with `.env.example` copied to `.env` plus the two required values, and check `PLAYWRIGHT_ENABLED: "true"`.
|
||||
|
||||
- [ ] **Step 5: Real host start through compose (fake token)**
|
||||
|
||||
Keep the dummy `.env` (`INSTALL_PLAYWRIGHT=false`). Run `docker compose -p volcanic-test up -d --build`, wait ~20 s, then `docker compose -p volcanic-test logs bot` and `docker compose -p volcanic-test ps -a`.
|
||||
Expected in the log: the startup lines from Global Constraints (`Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5`, `Allowed users: 1. Workspaces under /data/workspaces`, `Process environment: 15 deny rules, allow: none`, `MCP: playwright disabled (set PLAYWRIGHT_ENABLED=true)`), no diagnostic-socket warning, then a `401`/unauthorized failure and exit code 1; `restart: unless-stopped` restarts it (visible restart loop). Nothing else may fail.
|
||||
Then: `docker compose -p volcanic-test down -v --rmi local` (removes only this project's container, its three `volcanic-test_*` volumes and the `volcanic-test-bot` image).
|
||||
|
||||
- [ ] **Step 6: Running-bot probe offline (`/proc` and diagnostics checks)**
|
||||
|
||||
`MSYS_NO_PATHCONV=1 docker run -d --name volcanic-probe --init --network none -e TELEGRAM_BOT_TOKEN=123:fake -e TELEGRAM_ALLOWED_USER_IDS=1 -e OPENAI_API_KEY=sk-fake-probe-key volcanic-agentic-core:test`, wait ~10 s (no network → the poller backs off and keeps running), then:
|
||||
|
||||
`MSYS_NO_PATHCONV=1 docker exec -u app volcanic-probe sh -c 'id; ps -o pid,user,comm -A; pid=$(pgrep -f Volcanic.AgenticCore.Telegram.dll); echo bot=$pid; cat /proc/1/environ >/dev/null 2>&1 && echo pid1-READABLE || echo pid1-denied; cat /proc/$pid/environ >/dev/null 2>&1 && echo bot-READABLE || echo bot-denied; ls /tmp/dotnet-diagnostic-* 2>/dev/null || echo no-diagnostic-socket'`
|
||||
Expected: `uid=1654(app)`; PID 1 `docker-init` as `root`, the bot's `dotnet` as `app`; `pid1-denied`; `bot-denied`; `no-diagnostic-socket`. `docker logs volcanic-probe` shows the startup lines, `Speech to text: gpt-4o-transcribe`, and `Telegram unreachable` warnings. (The env filter itself, i.e. `TELEGRAM_BOT_TOKEN`/`OPENAI_API_KEY` missing from the agent's `bash`, needs a real chat; `docker exec` doesn't go through the spawn policy. Put it in `openIssues`.)
|
||||
Then `docker rm -f volcanic-probe`, and delete the dummy `.env`.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git status --short && git add docker-compose.yml .env.example .gitignore && git commit -m "feat(docker): compose file and .env.example for Coolify"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: `docs/docker.md`
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/docker.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: T1–T3's file names, build args, variable names, volume names, the `setpriv`/root PID 1 design.
|
||||
- Produces: `docs/docker.md` with the anchors `#configuration`, `#first-run-and-claude-login`, `#troubleshooting` that T5 links to.
|
||||
|
||||
- [ ] **Step 1: Write the doc**
|
||||
|
||||
Sections and required content (plain prose, short; use the unslop skill's rules: no filler, no "robust/seamless"):
|
||||
|
||||
1. `# Running the Telegram bot in Docker` intro: one container from the repo's `Dockerfile` + `docker-compose.yml`; Coolify or any Docker host; long polling, so no ports and no domain. Local run: `cp .env.example .env`, fill the two required values, `docker compose up -d --build`. `dotnet run` (see `telegram-bot.md`) stays the development path.
|
||||
2. `## What's in the image`: .NET 10 runtime + the published host; agent tools (the apt list); Node 24 (NodeSource); `claude` (`@anthropic-ai/claude-code`); optionally `@playwright/mcp` 0.0.82 + Chromium under `/opt/ms-playwright`. No .NET SDK. The bot runs as `app` (uid 1654); PID 1 (`docker-init`) stays root so the agent can't read the container's environment from `/proc/1/environ`. Don't set `user:` in compose (`setpriv` needs root to switch users).
|
||||
3. `## Build arguments` table: `INSTALL_PLAYWRIGHT` (Dockerfile `false`, compose `true`), `CLAUDE_CODE_VERSION` (`2.1`; exact version to pin), `NODE_MAJOR` (`24`), `PLAYWRIGHT_MCP_VERSION` (`0.0.82`; must equal `PlaywrightPreset.DefaultPackage`, a unit test checks it; compose doesn't override it).
|
||||
4. `## Configuration`: environment only. Table `Variable | Default | Purpose | Agent's processes see it` with rows: `TELEGRAM_BOT_TOKEN` (required; no, redacted), `TELEGRAM_ALLOWED_USER_IDS` (required, comma separated, first = owner; yes), `TELEGRAM_MODEL` → `Telegram__Model` (`claude-code/claude-opus-5-5`), `TZ` (`UTC`), `LOG_LEVEL` → `Logging__LogLevel__Default` (`Information`), `OPENAI_API_KEY` (empty = speech to text off; no, redacted), `STT_MODEL` (`gpt-4o-transcribe`), `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` (both = calendar on; secret: no, redacted), `DEFAULT_TIMEZONE` (empty = `TZ`), `PROCESS_ENV_ALLOW`, `PLAYWRIGHT_ENABLED` (follows `INSTALL_PLAYWRIGHT`), `PLAYWRIGHT_HEADLESS` (`true`), `PLAYWRIGHT_BROWSER` (`chromium`), `PLAYWRIGHT_EXTRA_ARGS` (`--no-sandbox`, keep it when overriding), `PLAYWRIGHT_COMMAND` (`playwright-mcp`). Then: what the agent can't see: the default deny rules (`*_API_KEY`, `*_APIKEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` and their `Section__Key` forms, plus the named secrets), any variable whose value is a configured secret, and `[REDACTED]` in tool output; `PROCESS_ENV_ALLOW=GH_TOKEN` lets one through (an explicit name on the deny list, such as `TELEGRAM_BOT_TOKEN`, can't be allowed). Other settings: any `Section__Key` (`Telegram__ReconnectMaxDelay=00:02:00`, `Scheduler__*`, `GoogleCalendar__*`, `ProcessEnvironment__*`, `Telegram__Speech__*`, `Telegram__InputDebounce`, `VOLCANIC_CLAUDE_CODE_IDLE_TIMEOUT_MS`/`..._TOTAL_TIMEOUT_MS`) added in Coolify and to `environment:` in the compose file. Set in the image, normally left alone: `Telegram__WorkspaceRoot=/data/workspaces`, `VOLCANIC_AGENT_DIR=/data/agent`, `CLAUDE_CONFIG_DIR=/data/claude`, `DOTNET_EnableDiagnostics=0` (unset it, with `ProcessEnvironment__HardenHostProcess=false`, only to attach `dotnet-dump`/`dotnet-trace`).
|
||||
A warning block: **Never set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CODE_USE_BEDROCK` or `CLAUDE_CODE_USE_VERTEX`.** The provider is subscription-only and refuses to run. In Coolify, check shared/team variables too.
|
||||
5. `## Volumes`: `workspaces` → `/data/workspaces` (one directory per chat, `AGENTS.md`/`CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, `inbox/`, `browser/`, project-level `.volcanic/extensions/`), `agent` → `/data/agent` (sessions, `settings.json`, `extensions/*.dll`, chat settings, `scheduler/jobs.json`, `google-calendar/` tokens), `claude` → `/data/claude` (Claude login). Named volumes survive redeploys; Coolify prefixes them with the resource uuid and deletes them only with the resource's volumes. Bind mounts work if the host directories are owned by uid 1654 (`chown -R 1654:1654 <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**
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git add docs/docker.md && git commit -m "docs(docker): Docker and Coolify deployment guide"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Cross-links and the sibling specs
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`, `docs/telegram-bot.md`, `docs/mcp.md`, `docs/superpowers/specs/2026-09-25-telegram-host-design.md`, `docs/superpowers/specs/2026-09-26-mcp-client-playwright-design.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `docs/docker.md` from T4.
|
||||
- Produces: nothing code-facing.
|
||||
|
||||
- [ ] **Step 1: `README.md`**
|
||||
|
||||
After the Quick start code block (right after the line `TELEGRAM_BOT_TOKEN=123:abc ... dotnet run --project hosts/Volcanic.AgenticCore.Telegram` and the closing fence), add:
|
||||
|
||||
```markdown
|
||||
To run the bot in a container (Coolify or any Docker host): `cp .env.example .env`, fill in the two required values,
|
||||
`docker compose up -d --build`. See [docs/docker.md](docs/docker.md).
|
||||
```
|
||||
|
||||
In `## Documentation`, after the Telegram bot line, add: `- [Docker and Coolify](docs/docker.md): the container image, configuration, volumes, first login.`
|
||||
|
||||
- [ ] **Step 2: `docs/telegram-bot.md`**
|
||||
|
||||
In `## Security`, first paragraph: replace "Run the bot as a dedicated user or in a container, and treat the workspace root as writable by anyone on the allow-list." with "Run it in the container image ([docker.md](docker.md)), which is the recommended setup, or at least as a dedicated user, and treat the workspace root as writable by anyone on the allow-list."
|
||||
Replace "Set `DOTNET_EnableDiagnostics=0` in the bot's environment too (a container image should set it): otherwise" with "Set `DOTNET_EnableDiagnostics=0` in the bot's environment too (the Docker image, [docker.md](docker.md), sets it): otherwise".
|
||||
|
||||
- [ ] **Step 3: `docs/mcp.md`**
|
||||
|
||||
Replace "The Docker image (`docker.md`, when the docker-deployment spec lands) preinstalls both" with "The Docker image ([docker.md](docker.md), built with `INSTALL_PLAYWRIGHT=true`) preinstalls both".
|
||||
|
||||
- [ ] **Step 4: Sibling specs**
|
||||
|
||||
- `2026-09-25-telegram-host-design.md` line 23: "Out of scope: webhooks, Docker/Coolify, voice and ..." → "Out of scope: webhooks, Docker/Coolify (see `2026-09-26-docker-deployment-design.md`), voice and ...".
|
||||
- `2026-09-26-mcp-client-playwright-design.md` line 4: `Status: implemented (the Dockerfile version test arrives with 2026-09-26-docker-deployment-design.md)` → `Status: implemented (the Dockerfile version test, PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage, landed with 2026-09-26-docker-deployment-design.md)`. In the Testing bullet (around line 395), replace "(deferred: the Dockerfile and that test come with the docker-deployment spec)" with "(`Dockerfile_version_matches_DefaultPackage`, added with the docker-deployment spec)".
|
||||
|
||||
- [ ] **Step 5: Verify links and commit**
|
||||
|
||||
Run: `cd /d/Projects/Names/wt-docker-deployment && grep -n "docker.md" README.md docs/*.md` (each link target exists) and `grep -rn "a container image should set\|when the docker-deployment spec lands\|deferred: the Dockerfile" docs README.md` → no hits.
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git add README.md docs/telegram-bot.md docs/mcp.md docs/superpowers/specs/2026-09-25-telegram-host-design.md docs/superpowers/specs/2026-09-26-mcp-client-playwright-design.md && git commit -m "docs(docker): link the container setup from the README, bot and MCP docs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Final verification, spec status and cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-09-26-docker-deployment-design.md` (Status line, Verification section)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: everything above.
|
||||
- Produces: the verification record for the report.
|
||||
|
||||
- [ ] **Step 1: Full .NET build and tests**
|
||||
|
||||
Run: `cd /d/Projects/Names/wt-docker-deployment && dotnet build && dotnet test` → green (only the 3 baseline failures), including `PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage`.
|
||||
|
||||
- [ ] **Step 2: Rebuild both images from the final tree**
|
||||
|
||||
`MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=false -t volcanic-agentic-core:test .` and `MSYS_NO_PATHCONV=1 docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core:test-pw .` (mostly cached). Re-run T1 Step 6's three checks against `:test` and T2 Step 4's `playwright-mcp --version` against `:test-pw`. Any failure → fix in the owning file, commit as `fix(docker): ...`.
|
||||
|
||||
- [ ] **Step 3: Spec Status and Verification**
|
||||
|
||||
In `2026-09-26-docker-deployment-design.md`: `Status: draft.` → `Status: implemented (plan: docs/superpowers/plans/2026-09-26-docker-deployment.md).` Replace the Verification intro "No unit tests (nothing in C# changes). Manual checklist in the implementation plan:" with "One unit test (`PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage`); no C# behaviour changes. Checked during implementation without a real bot: both image variants build; bridge guard, `claude --version`, the agent tools, uid 1654, `/data` ownership; root-owned PID 1 whose `/proc/1/environ` and the bot's are unreadable to `app`; no `dotnet-diagnostic-*` socket; `playwright-mcp --version` offline and a headless Chromium launch as `app`; `docker compose config`; host startup log and the `401` → exit 1 path. The rest of this list needs a real bot and stays a manual checklist:" and keep items 1–5.
|
||||
|
||||
- [ ] **Step 4: Cleanup (only what this plan created)**
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && docker rm -f volcanic-probe volcanic-probe-pw 2>/dev/null; docker compose -p volcanic-test down -v --rmi local 2>/dev/null; docker rmi volcanic-agentic-core:test volcanic-agentic-core:test-pw; docker volume ls --filter name=volcanic-test; docker image ls volcanic-agentic-core; rm -f .env; git status --short
|
||||
```
|
||||
|
||||
Expected: no `volcanic-test_*` volumes, no `volcanic-agentic-core` images, no `.env`, a clean tree. Do not prune build cache or other images (base images pulled by the build may stay; list them in the report).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /d/Projects/Names/wt-docker-deployment && git add docs/superpowers/specs/2026-09-26-docker-deployment-design.md && git commit -m "docs(docker): mark the deployment spec implemented"
|
||||
```
|
||||
|
||||
Report every verification step with its result. `openIssues` must list at least: the chat-driven checks (Verification 2 env/`date`/`/timezone`, `/claude-code-login` + `/claude-code-doctor` green, Verification 3 "tell me the H1" in a chat, Verification 4 redeploy with a logged-in bot, Verification 5 `ANTHROPIC_API_KEY` refusal), a real Coolify deploy, and anything from T1–T3 that could not be checked.
|
||||
Reference in New Issue
Block a user