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:
|
||||
build:
|
||||
context: docker/edge
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
dockerfile: docker/edge/Dockerfile
|
||||
image: disinto/edge:latest
|
||||
container_name: disinto-edge
|
||||
security_opt:
|
||||
|
|
@ -220,6 +220,8 @@ services:
|
|||
- ${CLAUDE_CONFIG_FILE:-${HOME}/.claude.json}:/root/.claude.json:ro
|
||||
- ${CLAUDE_DIR:-${HOME}/.claude}:/root/.claude:ro
|
||||
- disinto-logs:/opt/disinto-logs
|
||||
# Chat history persistence (merged from chat container, #1083)
|
||||
- ${CHAT_HISTORY_DIR:-./state/chat-history}:/var/lib/chat/history
|
||||
environment:
|
||||
- FORGE_SUPERVISOR_TOKEN=${FORGE_SUPERVISOR_TOKEN:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
|
|
@ -231,6 +233,16 @@ services:
|
|||
- PRIMARY_BRANCH=main
|
||||
- DISINTO_CONTAINER=1
|
||||
- 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:
|
||||
- "80:80"
|
||||
- "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
|
||||
|
||||
# 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))
|
||||
UI_DIR = "/var/chat/ui"
|
||||
STATIC_DIR = os.path.join(UI_DIR, "static")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
FROM caddy:latest
|
||||
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
|
||||
RUN apk add --no-cache bash jq curl git docker-cli python3 openssh-client autossh \
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -244,6 +244,9 @@ else
|
|||
echo "edge: collect-engagement cron skipped (EDGE_ENGAGEMENT_READY=0)" >&2
|
||||
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;
|
||||
# copy it into the expected location if present (compose uses the mounted path).
|
||||
if [ -f /local/Caddyfile ]; then
|
||||
|
|
|
|||
|
|
@ -326,7 +326,6 @@ _generate_compose_impl() {
|
|||
_record_service "edge" "base compose template" || return 1
|
||||
_record_service "staging" "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
|
||||
local wp_repo_id
|
||||
|
|
@ -615,6 +614,16 @@ COMPOSEEOF
|
|||
- EDGE_TUNNEL_FQDN_CHAT=${EDGE_TUNNEL_FQDN_CHAT:-}
|
||||
# Shared secret for Caddy ↔ chat forward_auth (#709)
|
||||
- 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:
|
||||
- ./docker/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
|
|
@ -622,6 +631,8 @@ COMPOSEEOF
|
|||
- ./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_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:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:2019/config/"]
|
||||
interval: 30s
|
||||
|
|
@ -670,64 +681,12 @@ COMPOSEEOF
|
|||
- disinto-net
|
||||
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:
|
||||
forgejo-data:
|
||||
woodpecker-data:
|
||||
agent-data:
|
||||
project-repos:
|
||||
caddy_data:
|
||||
chat-config:
|
||||
|
||||
networks:
|
||||
disinto-net:
|
||||
|
|
@ -786,7 +745,7 @@ COMPOSEEOF
|
|||
# In build mode, replace image: with build: for locally-built images
|
||||
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\/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
|
||||
|
||||
echo "Created: ${compose_file}"
|
||||
|
|
@ -864,22 +823,22 @@ _generate_caddyfile_subpath() {
|
|||
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)
|
||||
handle /chat/login {
|
||||
reverse_proxy chat:8080
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
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)
|
||||
handle /chat/* {
|
||||
forward_auth chat:8080 {
|
||||
forward_auth 127.0.0.1:8080 {
|
||||
uri /chat/auth/verify
|
||||
copy_headers X-Forwarded-User
|
||||
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
||||
}
|
||||
reverse_proxy chat:8080
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
}
|
||||
CADDYFILEEOF
|
||||
|
|
@ -912,18 +871,18 @@ _generate_caddyfile_subdomain() {
|
|||
# Chat — with forward_auth (#709, on its own host)
|
||||
{$EDGE_TUNNEL_FQDN_CHAT} {
|
||||
handle /login {
|
||||
reverse_proxy chat:8080
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
handle /oauth/callback {
|
||||
reverse_proxy chat:8080
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
handle /* {
|
||||
forward_auth chat:8080 {
|
||||
forward_auth 127.0.0.1:8080 {
|
||||
uri /auth/verify
|
||||
copy_headers X-Forwarded-User
|
||||
header_up X-Forward-Auth-Secret {$FORWARD_AUTH_SECRET}
|
||||
}
|
||||
reverse_proxy chat:8080
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
}
|
||||
CADDYFILEEOF
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue