fix(13-02): retire the Python model-proxy sidecar and the OpenRouter open relay

Closes the live production exposure this plan targets: /aiui/api/claude/
and /aiui/api/ollama/ proxied straight through with "no session gate
needed", and /aiui/api/openrouter/ was a plain unauthenticated relay to a
paid third-party API the node holds no key for (T-13-08/T-13-09/T-13-10).

image-recipe/configs/nginx-archipelago.conf (BOTH server blocks, ~line 49
and ~line 961 — a fix applied to only one leaves the exposure live on
whichever block serves the request, T-13-15):
- /aiui/api/claude/ and /aiui/api/ollama/ proxy_pass re-pointed from
  127.0.0.1:3142 / 127.0.0.1:11434 to the Rust daemon at 127.0.0.1:5678
  (no trailing path component, so the daemon's own prefix match sees the
  full request URI)
- Forward the session Cookie header to the daemon so it can re-derive auth
- location /aiui/api/openrouter/ deleted outright in both blocks
- Old comment "API key managed by proxy, no session gate needed" (the
  reasoning error that produced the exposure) replaced with rationale

scripts/deploy-to-target.sh: deleted the embedded claude-api-proxy.py
heredoc, its systemd unit creation/enable/restart, the ANTHROPIC_API_KEY
extraction, and the 3141->3142 sed fixups. Added an unconditional step that
stops/disables/removes any pre-existing claude-api-proxy unit and deletes
/opt/archipelago/claude-api-proxy.py and
<data_dir>/secrets/claude-api-proxy.env on every deploy — so
already-provisioned nodes actually lose the old unauthenticated listener,
not just newly-deployed ones.

scripts/setup-aiui-server.sh: dropped the hard ANTHROPIC_API_KEY
requirement and the patch-nginx-claude.py step; the script's remaining job
is the AIUI dist rsync. (Also drops the FileBrowser-fix step that lived
here — that logic already exists, and is kept, in deploy-to-target.sh; this
script narrows to exactly what its rewritten header now says it does.)

core/archipelago/src/api/rpc/system/handlers.rs: `claude_api_key` setting
branch no longer writes a second key copy to secrets/claude-api-proxy.env
or restarts claude-api-proxy. secrets/claude-api-key (0600) remains the
single ledger, with a comment naming it as such.

`cargo build --package archipelago` succeeds. Verified via grep against
every acceptance criterion in 13-02-PLAN.md's Task 2 (openrouter count 0,
3142 gone from nginx, both location blocks present, PORT=3142 gone,
claude-api-proxy gone from handlers.rs, secrets/claude-api-key present).
Task 3 (real-node curl/systemd verification, S-15) is NOT done in this
commit — see 13-02-SUMMARY.md.

Continues WIP checkpoint 13b576da (reset --soft, recommitted atomically
per task per plan protocol).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 14:44:25 -04:00
co-authored by Claude Opus 5
parent 97921d9932
commit b28cc3eeaa
4 changed files with 76 additions and 240 deletions
+21 -110
View File
@@ -1,6 +1,6 @@
#!/bin/bash
#
# Setup AIUI + Claude API proxy + FileBrowser on any Archipelago server
# Deploy the AIUI (Chat mode iframe) build to an Archipelago server.
#
# Usage:
# ./scripts/setup-aiui-server.sh <host>
@@ -8,10 +8,20 @@
# ./scripts/setup-aiui-server.sh archipelago@192.168.1.228
#
# What it does:
# 1. Deploys AIUI files (from local build)
# 2. Configures nginx Claude API proxy (direct to Anthropic with API key)
# 3. Fixes FileBrowser container (removes read-only root if needed)
# 4. Reloads nginx
# Rsyncs (or tar+scp, if rsync is unavailable on the target) a locally
# built AIUI dist/ into /opt/archipelago/web-ui/aiui/ on the target node.
#
# What it no longer does (13-02-PLAN.md — closing a live production
# exposure): it used to also patch nginx to route /aiui/api/claude/ to a
# standalone Python proxy holding its own ANTHROPIC_API_KEY, with no session
# gate — anyone who could reach the node's web port could spend the owner's
# API budget. That proxy, its systemd unit, and this script's nginx-patch
# step are all deleted (see scripts/deploy-to-target.sh's "Removing legacy
# Claude API proxy sidecar" step). AIUI's Claude/Ollama calls now route
# through the Rust daemon (127.0.0.1:5678), which enforces the session
# cookie itself and reads the node's single key ledger. Set the key via
# `system.settings.set claude_api_key` (Settings > AIUI in neode-ui) — this
# script has nothing to do with the key anymore.
#
# Prerequisites:
# - AIUI must be built locally first: cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build
@@ -24,10 +34,6 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
SSH_OPTS="-o StrictHostKeyChecking=no -i $SSH_KEY"
# Anthropic API key used by the AIUI Claude chat proxy. Keep this in the
# caller's environment or scripts/deploy-config.sh; never commit live keys.
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"
TARGET_HOST="$1"
if [ -z "$TARGET_HOST" ]; then
echo "Usage: $0 <user@host>"
@@ -35,12 +41,6 @@ if [ -z "$TARGET_HOST" ]; then
exit 1
fi
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "ERROR: ANTHROPIC_API_KEY must be set in the environment."
echo "Example: ANTHROPIC_API_KEY=<key> $0 $TARGET_HOST"
exit 1
fi
AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist"
if [ ! -f "$AIUI_DIST/index.html" ]; then
echo "ERROR: AIUI build not found at $AIUI_DIST"
@@ -51,15 +51,14 @@ fi
timestamp() { echo "[$(date +%H:%M:%S)]"; }
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Archipelago AIUI + Claude API Setup ║"
echo "║ Archipelago AIUI deploy ║"
echo "║ Target: $TARGET_HOST"
echo "╚════════════════════════════════════════════════════════════╝"
# --- Step 1: Deploy AIUI files ---
# --- Deploy AIUI files ---
echo ""
echo "$(timestamp) 📦 Deploying AIUI files..."
# Check if rsync is available on remote
if ssh $SSH_OPTS "$TARGET_HOST" "which rsync" &>/dev/null; then
rsync -avz --delete -e "ssh $SSH_OPTS" "$AIUI_DIST/" "$TARGET_HOST:/opt/archipelago/web-ui/aiui/" 2>&1 | tail -3
else
@@ -72,105 +71,17 @@ else
fi
echo " AIUI deployed."
# --- Step 2: Configure nginx Claude API proxy ---
echo ""
echo "$(timestamp) 🔧 Configuring nginx Claude API proxy..."
# Create a Python script to patch nginx config
cat << 'PYSCRIPT' > /tmp/patch-nginx-claude.py
import sys
import re
API_KEY = sys.argv[1]
with open("/etc/nginx/sites-available/archipelago") as f:
content = f.read()
# The new Claude API proxy block
new_block = '''location /aiui/api/claude/ {
if ($cookie_session = "") {
return 401 '{"error":"Unauthorized"}';
}
proxy_pass https://api.anthropic.com/;
proxy_http_version 1.1;
proxy_set_header Host api.anthropic.com;
proxy_set_header x-api-key "''' + API_KEY + '''";
proxy_set_header anthropic-version "2023-06-01";
proxy_set_header anthropic-dangerous-direct-browser-access "true";
proxy_ssl_server_name on;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_cache off;
proxy_connect_timeout 120s;
proxy_read_timeout 300s;
proxy_send_timeout 120s;
}'''
# Replace existing Claude API proxy blocks (handles both old proxy and direct patterns)
pattern = r'location /aiui/api/claude/ \{[^}]*(?:\{[^}]*\}[^}]*)*\}'
content = re.sub(pattern, new_block, content)
with open("/etc/nginx/sites-available/archipelago", "w") as f:
f.write(content)
# Verify
count = content.count("api.anthropic.com")
print(f" Patched {count // 2} Claude API proxy blocks (HTTP + HTTPS)")
PYSCRIPT
scp $SSH_OPTS /tmp/patch-nginx-claude.py "$TARGET_HOST:/tmp/patch-nginx-claude.py"
ssh $SSH_OPTS "$TARGET_HOST" "sudo python3 /tmp/patch-nginx-claude.py '$ANTHROPIC_API_KEY'"
# Test and reload nginx
echo " Testing nginx config..."
ssh $SSH_OPTS "$TARGET_HOST" "sudo nginx -t 2>&1 && sudo systemctl reload nginx && echo ' Nginx reloaded OK'" || {
echo " ERROR: nginx config test failed!"
exit 1
}
# --- Step 3: Fix FileBrowser container ---
echo ""
echo "$(timestamp) 📁 Checking FileBrowser..."
FB_STATUS=$(ssh $SSH_OPTS "$TARGET_HOST" "podman inspect filebrowser 2>/dev/null | grep -oP '\"ReadonlyRootfs\":\s*\K\w+'" 2>/dev/null || echo "not_found")
if [ "$FB_STATUS" = "true" ]; then
echo " FileBrowser has read-only root — recreating..."
ssh $SSH_OPTS "$TARGET_HOST" "
podman stop filebrowser 2>/dev/null
podman rm filebrowser 2>/dev/null
sudo mkdir -p /var/lib/archipelago/filebrowser
podman run -d --name filebrowser --restart=always \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
filebrowser/filebrowser:v2.27.0
" 2>&1 | tail -2
echo " FileBrowser recreated."
elif [ "$FB_STATUS" = "not_found" ]; then
echo " FileBrowser not found — creating..."
ssh $SSH_OPTS "$TARGET_HOST" "
sudo mkdir -p /var/lib/archipelago/filebrowser
podman run -d --name filebrowser --restart=always \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
filebrowser/filebrowser:v2.27.0
" 2>&1 | tail -2
echo " FileBrowser created."
else
echo " FileBrowser OK (ReadonlyRootfs: $FB_STATUS)"
fi
# --- Step 4: Verify ---
# --- Verify ---
echo ""
echo "$(timestamp) ✅ Verification..."
ssh $SSH_OPTS "$TARGET_HOST" "
echo \" AIUI index: \$(ls -la /opt/archipelago/web-ui/aiui/index.html 2>/dev/null | awk '{print \$6,\$7,\$8}')\"
echo \" FileBrowser: \$(podman ps --format '{{.Names}} {{.Status}}' | grep filebrowser)\"
echo \" Nginx: \$(systemctl is-active nginx)\"
echo \" Backend: \$(systemctl is-active archipelago)\"
echo \" Claude API test: \$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost/aiui/api/claude/v1/messages -H 'Content-Type: application/json' -H 'Cookie: session=test' -d '{\"model\":\"claude-sonnet-4-20250514\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}')\"
"
echo ""
echo "$(timestamp) Done! Server configured."
echo "$(timestamp) Done! AIUI deployed."
echo " Set the Claude API key (if not already set) via Settings > AIUI in"
echo " neode-ui — it now lives only at <data_dir>/secrets/claude-api-key."
echo " Access: http://$(echo $TARGET_HOST | cut -d@ -f2)"