fix: feat: merge chat container into edge — run chat server inside edge container with full permissions (reverts sandbox from #706) (#1083)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
398a7398a9
commit
bcdf33e68a
7 changed files with 48 additions and 142 deletions
|
|
@ -208,8 +208,8 @@ services:
|
||||||
|
|
||||||
edge:
|
edge:
|
||||||
build:
|
build:
|
||||||
context: docker/edge
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: docker/edge/Dockerfile
|
||||||
image: disinto/edge:latest
|
image: disinto/edge:latest
|
||||||
container_name: disinto-edge
|
container_name: disinto-edge
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|
@ -220,6 +220,8 @@ services:
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/root/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/root/.claude.json:ro
|
||||||
- ${CLAUDE_DIR:-${HOME}/.claude}:/root/.claude:ro
|
- ${CLAUDE_DIR:-${HOME}/.claude}:/root/.claude:ro
|
||||||
- disinto-logs:/opt/disinto-logs
|
- disinto-logs:/opt/disinto-logs
|
||||||
|
# Chat history persistence (merged from chat container, #1083)
|
||||||
|
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
|
||||||
environment:
|
environment:
|
||||||
- FORGE_SUPERVISOR_TOKEN=${FORGE_SUPERVISOR_TOKEN:-}
|
- FORGE_SUPERVISOR_TOKEN=${FORGE_SUPERVISOR_TOKEN:-}
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
|
|
@ -231,6 +233,16 @@ services:
|
||||||
- PRIMARY_BRANCH=main
|
- PRIMARY_BRANCH=main
|
||||||
- DISINTO_CONTAINER=1
|
- DISINTO_CONTAINER=1
|
||||||
- FORGE_ADMIN_USERS=disinto-admin,vault-bot,admin
|
- FORGE_ADMIN_USERS=disinto-admin,vault-bot,admin
|
||||||
|
# Chat env vars (merged from chat container into edge, #1083)
|
||||||
|
- CHAT_HOST=127.0.0.1
|
||||||
|
- CHAT_PORT=8080
|
||||||
|
- CHAT_OAUTH_CLIENT_ID=${CHAT_OAUTH_CLIENT_ID:-}
|
||||||
|
- CHAT_OAUTH_CLIENT_SECRET=${CHAT_OAUTH_CLIENT_SECRET:-}
|
||||||
|
- DISINTO_CHAT_ALLOWED_USERS=${DISINTO_CHAT_ALLOWED_USERS:-}
|
||||||
|
- FORWARD_AUTH_SECRET=${FORWARD_AUTH_SECRET:-}
|
||||||
|
- EDGE_TUNNEL_FQDN=${EDGE_TUNNEL_FQDN:-}
|
||||||
|
- EDGE_TUNNEL_FQDN_CHAT=${EDGE_TUNNEL_FQDN_CHAT:-}
|
||||||
|
- EDGE_ROUTING_MODE=${EDGE_ROUTING_MODE:-subpath}
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# disinto-chat — minimal HTTP backend for Claude chat UI
|
|
||||||
#
|
|
||||||
# Small Debian slim base with Python runtime and Node.js.
|
|
||||||
# Chosen for simplicity and small image size (~100MB).
|
|
||||||
#
|
|
||||||
# Image size: ~100MB (well under the 200MB ceiling)
|
|
||||||
#
|
|
||||||
# Claude CLI is baked into the image — same pattern as the agents container.
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
# Install Node.js (required for Claude CLI) and Python
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
nodejs npm python3 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Claude Code CLI — chat backend runtime
|
|
||||||
RUN npm install -g @anthropic-ai/claude-code@2.1.84
|
|
||||||
|
|
||||||
# Non-root user — fixed UID 10001 for sandbox hardening (#706)
|
|
||||||
RUN useradd -m -u 10001 -s /bin/bash chat
|
|
||||||
|
|
||||||
# Copy application files
|
|
||||||
COPY server.py /usr/local/bin/server.py
|
|
||||||
COPY entrypoint-chat.sh /entrypoint-chat.sh
|
|
||||||
COPY ui/ /var/chat/ui/
|
|
||||||
|
|
||||||
RUN chmod +x /entrypoint-chat.sh /usr/local/bin/server.py
|
|
||||||
|
|
||||||
USER chat
|
|
||||||
WORKDIR /var/chat
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
||||||
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint-chat.sh"]
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# entrypoint-chat.sh — Start the disinto-chat backend server
|
|
||||||
#
|
|
||||||
# Exec-replace pattern: this script is the container entrypoint and runs
|
|
||||||
# the server directly (no wrapper needed). Logs to stdout for docker logs.
|
|
||||||
|
|
||||||
LOGFILE="/tmp/chat.log"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" | tee -a "$LOGFILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sandbox sanity checks (#706) — fail fast if isolation is broken
|
|
||||||
if [ -e /var/run/docker.sock ]; then
|
|
||||||
log "FATAL: /var/run/docker.sock is accessible — sandbox violation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$(id -u)" = "0" ]; then
|
|
||||||
log "FATAL: running as root (uid 0) — sandbox violation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify Claude CLI is available (expected via volume mount from host).
|
|
||||||
if ! command -v claude &>/dev/null; then
|
|
||||||
log "FATAL: claude CLI not found in PATH"
|
|
||||||
log "Mount the host binary into the container, e.g.:"
|
|
||||||
log " volumes:"
|
|
||||||
log " - /usr/local/bin/claude:/usr/local/bin/claude:ro"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "Claude CLI: $(claude --version 2>&1 || true)"
|
|
||||||
|
|
||||||
# Start the Python server (exec-replace so signals propagate correctly)
|
|
||||||
log "Starting disinto-chat server on port 8080..."
|
|
||||||
exec python3 /usr/local/bin/server.py
|
|
||||||
|
|
@ -41,7 +41,7 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
HOST = os.environ.get("CHAT_HOST", "0.0.0.0")
|
HOST = os.environ.get("CHAT_HOST", "127.0.0.1")
|
||||||
PORT = int(os.environ.get("CHAT_PORT", 8080))
|
PORT = int(os.environ.get("CHAT_PORT", 8080))
|
||||||
UI_DIR = "/var/chat/ui"
|
UI_DIR = "/var/chat/ui"
|
||||||
STATIC_DIR = os.path.join(UI_DIR, "static")
|
STATIC_DIR = os.path.join(UI_DIR, "static")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
FROM caddy:latest
|
FROM caddy:latest
|
||||||
RUN apk add --no-cache bash jq curl git docker-cli python3 openssh-client autossh
|
RUN apk add --no-cache bash jq curl git docker-cli python3 openssh-client autossh \
|
||||||
COPY entrypoint-edge.sh /usr/local/bin/entrypoint-edge.sh
|
nodejs npm
|
||||||
|
# Claude Code CLI — chat backend runtime (merged from docker/chat, #1083)
|
||||||
|
RUN npm install -g @anthropic-ai/claude-code@2.1.84
|
||||||
|
COPY docker/edge/entrypoint-edge.sh /usr/local/bin/entrypoint-edge.sh
|
||||||
|
# Chat server and UI (merged from docker/chat into edge, #1083)
|
||||||
|
COPY docker/chat/server.py /usr/local/bin/chat-server.py
|
||||||
|
COPY docker/chat/ui/ /var/chat/ui/
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,9 @@ else
|
||||||
echo "edge: collect-engagement cron skipped (EDGE_ENGAGEMENT_READY=0)" >&2
|
echo "edge: collect-engagement cron skipped (EDGE_ENGAGEMENT_READY=0)" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Start chat server in background (#1083 — merged from docker/chat into edge)
|
||||||
|
(python3 /usr/local/bin/chat-server.py 2>&1 | tee -a /opt/disinto-logs/chat.log) &
|
||||||
|
|
||||||
# Nomad template renders Caddyfile to /local/Caddyfile via service discovery;
|
# Nomad template renders Caddyfile to /local/Caddyfile via service discovery;
|
||||||
# copy it into the expected location if present (compose uses the mounted path).
|
# copy it into the expected location if present (compose uses the mounted path).
|
||||||
if [ -f /local/Caddyfile ]; then
|
if [ -f /local/Caddyfile ]; then
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,6 @@ _generate_compose_impl() {
|
||||||
_record_service "edge" "base compose template" || return 1
|
_record_service "edge" "base compose template" || return 1
|
||||||
_record_service "staging" "base compose template" || return 1
|
_record_service "staging" "base compose template" || return 1
|
||||||
_record_service "staging-deploy" "base compose template" || return 1
|
_record_service "staging-deploy" "base compose template" || return 1
|
||||||
_record_service "chat" "base compose template" || return 1
|
|
||||||
|
|
||||||
# Extract primary woodpecker_repo_id from project TOML files
|
# Extract primary woodpecker_repo_id from project TOML files
|
||||||
local wp_repo_id
|
local wp_repo_id
|
||||||
|
|
@ -615,6 +614,16 @@ COMPOSEEOF
|
||||||
- EDGE_TUNNEL_FQDN_CHAT=${EDGE_TUNNEL_FQDN_CHAT:-}
|
- EDGE_TUNNEL_FQDN_CHAT=${EDGE_TUNNEL_FQDN_CHAT:-}
|
||||||
# Shared secret for Caddy ↔ chat forward_auth (#709)
|
# Shared secret for Caddy ↔ chat forward_auth (#709)
|
||||||
- FORWARD_AUTH_SECRET=${FORWARD_AUTH_SECRET:-}
|
- FORWARD_AUTH_SECRET=${FORWARD_AUTH_SECRET:-}
|
||||||
|
# Chat env vars (merged from chat container into edge, #1083)
|
||||||
|
- CHAT_HOST=127.0.0.1
|
||||||
|
- CHAT_PORT=8080
|
||||||
|
- CHAT_OAUTH_CLIENT_ID=${CHAT_OAUTH_CLIENT_ID:-}
|
||||||
|
- CHAT_OAUTH_CLIENT_SECRET=${CHAT_OAUTH_CLIENT_SECRET:-}
|
||||||
|
- DISINTO_CHAT_ALLOWED_USERS=${DISINTO_CHAT_ALLOWED_USERS:-}
|
||||||
|
# Cost caps / rate limiting (#711)
|
||||||
|
- CHAT_MAX_REQUESTS_PER_HOUR=${CHAT_MAX_REQUESTS_PER_HOUR:-60}
|
||||||
|
- CHAT_MAX_REQUESTS_PER_DAY=${CHAT_MAX_REQUESTS_PER_DAY:-500}
|
||||||
|
- CHAT_MAX_TOKENS_PER_DAY=${CHAT_MAX_TOKENS_PER_DAY:-1000000}
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/Caddyfile:/etc/caddy/Caddyfile
|
- ./docker/Caddyfile:/etc/caddy/Caddyfile
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
|
|
@ -622,6 +631,8 @@ COMPOSEEOF
|
||||||
- ./secrets/tunnel_key:/run/secrets/tunnel_key:ro
|
- ./secrets/tunnel_key:/run/secrets/tunnel_key:ro
|
||||||
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
- ${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}
|
||||||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/home/agent/.claude.json:ro
|
||||||
|
# Chat history persistence (merged from chat container, #1083)
|
||||||
|
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-fsS", "http://localhost:2019/config/"]
|
test: ["CMD", "curl", "-fsS", "http://localhost:2019/config/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
@ -670,64 +681,12 @@ COMPOSEEOF
|
||||||
- disinto-net
|
- disinto-net
|
||||||
command: ["echo", "staging slot — replace with project image"]
|
command: ["echo", "staging slot — replace with project image"]
|
||||||
|
|
||||||
# Chat container — Claude chat UI backend (#705)
|
|
||||||
# Internal service only; edge proxy routes to chat:8080
|
|
||||||
# Sandbox hardened per #706 — no docker.sock, read-only rootfs, minimal caps
|
|
||||||
chat:
|
|
||||||
build:
|
|
||||||
context: ./docker/chat
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: disinto-chat
|
|
||||||
restart: unless-stopped
|
|
||||||
read_only: true
|
|
||||||
tmpfs:
|
|
||||||
- /tmp:size=64m
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
pids_limit: 128
|
|
||||||
mem_limit: 512m
|
|
||||||
memswap_limit: 512m
|
|
||||||
volumes:
|
|
||||||
# Mount claude binary from host (same as agents)
|
|
||||||
- ${CLAUDE_BIN_DIR}:/usr/local/bin/claude:ro
|
|
||||||
# Throwaway named volume for chat config (isolated from host ~/.claude)
|
|
||||||
- chat-config:/var/chat/config
|
|
||||||
# Chat history persistence: per-user NDJSON files on bind-mounted host volume
|
|
||||||
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
|
|
||||||
environment:
|
|
||||||
CHAT_HOST: "0.0.0.0"
|
|
||||||
CHAT_PORT: "8080"
|
|
||||||
FORGE_URL: http://forgejo:3000
|
|
||||||
CHAT_OAUTH_CLIENT_ID: ${CHAT_OAUTH_CLIENT_ID:-}
|
|
||||||
CHAT_OAUTH_CLIENT_SECRET: ${CHAT_OAUTH_CLIENT_SECRET:-}
|
|
||||||
EDGE_TUNNEL_FQDN: ${EDGE_TUNNEL_FQDN:-}
|
|
||||||
EDGE_TUNNEL_FQDN_CHAT: ${EDGE_TUNNEL_FQDN_CHAT:-}
|
|
||||||
EDGE_ROUTING_MODE: ${EDGE_ROUTING_MODE:-subpath}
|
|
||||||
DISINTO_CHAT_ALLOWED_USERS: ${DISINTO_CHAT_ALLOWED_USERS:-}
|
|
||||||
# Shared secret for Caddy forward_auth verify endpoint (#709)
|
|
||||||
FORWARD_AUTH_SECRET: ${FORWARD_AUTH_SECRET:-}
|
|
||||||
# Cost caps / rate limiting (#711)
|
|
||||||
CHAT_MAX_REQUESTS_PER_HOUR: ${CHAT_MAX_REQUESTS_PER_HOUR:-60}
|
|
||||||
CHAT_MAX_REQUESTS_PER_DAY: ${CHAT_MAX_REQUESTS_PER_DAY:-500}
|
|
||||||
CHAT_MAX_TOKENS_PER_DAY: ${CHAT_MAX_TOKENS_PER_DAY:-1000000}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
networks:
|
|
||||||
- disinto-net
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
forgejo-data:
|
forgejo-data:
|
||||||
woodpecker-data:
|
woodpecker-data:
|
||||||
agent-data:
|
agent-data:
|
||||||
project-repos:
|
project-repos:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
chat-config:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
disinto-net:
|
disinto-net:
|
||||||
|
|
@ -786,7 +745,7 @@ COMPOSEEOF
|
||||||
# In build mode, replace image: with build: for locally-built images
|
# In build mode, replace image: with build: for locally-built images
|
||||||
if [ "$use_build" = true ]; then
|
if [ "$use_build" = true ]; then
|
||||||
sed -i '/^ image: ghcr\.io\/disinto\/agents:/{s|image: ghcr\.io/disinto/agents:.*|build:\n context: .\n dockerfile: docker/agents/Dockerfile\n pull_policy: build|}' "$compose_file"
|
sed -i '/^ image: ghcr\.io\/disinto\/agents:/{s|image: ghcr\.io/disinto/agents:.*|build:\n context: .\n dockerfile: docker/agents/Dockerfile\n pull_policy: build|}' "$compose_file"
|
||||||
sed -i '/^ image: ghcr\.io\/disinto\/edge:/{s|image: ghcr\.io/disinto/edge:.*|build: ./docker/edge\n pull_policy: build|}' "$compose_file"
|
sed -i '/^ image: ghcr\.io\/disinto\/edge:/{s|image: ghcr\.io/disinto/edge:.*|build:\n context: .\n dockerfile: docker/edge/Dockerfile\n pull_policy: build|}' "$compose_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Created: ${compose_file}"
|
echo "Created: ${compose_file}"
|
||||||
|
|
@ -864,22 +823,22 @@ _generate_caddyfile_subpath() {
|
||||||
reverse_proxy staging:80
|
reverse_proxy staging:80
|
||||||
}
|
}
|
||||||
|
|
||||||
# Chat service — reverse proxy to disinto-chat backend (#705)
|
# Chat service — reverse proxy to in-process chat server (#705, #1083)
|
||||||
# OAuth routes bypass forward_auth — unauthenticated users need these (#709)
|
# OAuth routes bypass forward_auth — unauthenticated users need these (#709)
|
||||||
handle /chat/login {
|
handle /chat/login {
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
handle /chat/oauth/callback {
|
handle /chat/oauth/callback {
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
# Defense-in-depth: forward_auth stamps X-Forwarded-User from session (#709)
|
# Defense-in-depth: forward_auth stamps X-Forwarded-User from session (#709)
|
||||||
handle /chat/* {
|
handle /chat/* {
|
||||||
forward_auth chat:8080 {
|
forward_auth 127.0.0.1:8080 {
|
||||||
uri /chat/auth/verify
|
uri /chat/auth/verify
|
||||||
copy_headers X-Forwarded-User
|
copy_headers X-Forwarded-User
|
||||||
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
||||||
}
|
}
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CADDYFILEEOF
|
CADDYFILEEOF
|
||||||
|
|
@ -912,18 +871,18 @@ _generate_caddyfile_subdomain() {
|
||||||
# Chat — with forward_auth (#709, on its own host)
|
# Chat — with forward_auth (#709, on its own host)
|
||||||
{$EDGE_TUNNEL_FQDN_CHAT} {
|
{$EDGE_TUNNEL_FQDN_CHAT} {
|
||||||
handle /login {
|
handle /login {
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
handle /oauth/callback {
|
handle /oauth/callback {
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
handle /* {
|
handle /* {
|
||||||
forward_auth chat:8080 {
|
forward_auth 127.0.0.1:8080 {
|
||||||
uri /auth/verify
|
uri /auth/verify
|
||||||
copy_headers X-Forwarded-User
|
copy_headers X-Forwarded-User
|
||||||
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
||||||
}
|
}
|
||||||
reverse_proxy chat:8080
|
reverse_proxy 127.0.0.1:8080
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CADDYFILEEOF
|
CADDYFILEEOF
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue