# 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/aspnet: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
# 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

# The entrypoint hard-codes uid/gid 1654. Named volumes copy this ownership on first mount; bind-mounted roots are
# chowned by the entrypoint (not recursively), since Docker creates missing host directories as root.
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

# The A2A server (A2A__Enabled=true); with A2A off the bot opens no port at all.
EXPOSE 8080

# 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.
# An empty bounding set and no_new_privs keep the agent's processes from regaining root through setuid binaries.
# The shell only chowns the mount roots and execs setpriv, so the process tree is unchanged.
ENTRYPOINT ["/bin/sh", "-c", "chown 1654:1654 /data/workspaces /data/agent /data/claude; exec setpriv --reuid=1654 --regid=1654 --init-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- dotnet Volcanic.AgenticCore.Telegram.dll"]
