wip(13-02): checkpoint interrupted model-proxy work (session-gated forwarder)

Session died on a broken pipe with this work uncommitted in the executor
worktree. Committed verbatim, unverified — not a task completion. The
continuation executor may reset --soft this commit and recommit atomically
per task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 13:03:13 -04:00
co-authored by Claude Opus 5
parent 15774d266f
commit 13b576da26
6 changed files with 488 additions and 240 deletions
+21 -84
View File
@@ -396,7 +396,6 @@ deploy_secondary() {
ssh $SSH_OPTS "$SEC_TARGET" '
sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago
sudo rm -f /etc/nginx/conf.d/external-app-proxies.conf
sudo sed -i "s|proxy_pass http://127.0.0.1:3141/;|proxy_pass http://127.0.0.1:3142/;|g" /etc/nginx/sites-available/archipelago
rm -f /tmp/nginx-archipelago.conf
' 2>/dev/null || true
fi
@@ -775,9 +774,6 @@ if [ "$LIVE" = true ]; then
# Remove old port-based external app proxies config
ssh $SSH_OPTS "$TARGET_HOST" 'sudo rm -f /etc/nginx/conf.d/external-app-proxies.conf' 2>/dev/null || true
# Fix nginx Claude API proxy port (template uses 3141, proxy runs on 3142)
ssh $SSH_OPTS "$TARGET_HOST" 'sudo sed -i "s|proxy_pass http://127.0.0.1:3141/;|proxy_pass http://127.0.0.1:3142/;|g" /etc/nginx/sites-available/archipelago' 2>/dev/null || true
# Validate nginx config after all changes
ssh $SSH_OPTS "$TARGET_HOST" 'sudo nginx -t 2>&1 && echo " nginx config OK" || echo " ⚠️ nginx config test failed"' 2>/dev/null || true
@@ -873,87 +869,28 @@ if [ "$LIVE" = true ]; then
' 2>/dev/null || true
fi
# Deploy Claude API proxy (auto-install if missing)
progress "Setting up Claude API proxy"
# Retire the Claude API proxy sidecar (13-02-PLAN.md — closing a live
# production exposure). This used to install/restart a standalone Python
# process on port 3142 holding its OWN copy of ANTHROPIC_API_KEY, reachable
# with no session gate — anyone who could reach the node's web port could
# spend the owner's API budget (T-13-08/T-13-09). AIUI's Claude/Ollama
# calls now route through the Rust daemon (127.0.0.1:5678, see the nginx
# sync above), which enforces the session cookie and reads the node's
# single key ledger (data_dir/secrets/claude-api-key).
#
# This step must run unconditionally on every deploy, not just fresh
# installs: deploying the daemon fix without tearing down an
# already-provisioned node's sidecar leaves the old unauthenticated
# listener running right alongside the new authenticated one.
progress "Removing legacy Claude API proxy sidecar"
ssh $SSH_OPTS "$TARGET_HOST" '
echo " Updating Claude API proxy on port 3142..."
# Check for API key in existing service or setup-aiui-server.sh
EXISTING_KEY=$(grep -oP "ANTHROPIC_API_KEY=\K.*" /etc/systemd/system/claude-api-proxy.service 2>/dev/null || true)
if [ -z "$EXISTING_KEY" ]; then
echo " ⚠️ No ANTHROPIC_API_KEY found — run setup-aiui-server.sh first to configure"
else
# Proxy script
sudo tee /opt/archipelago/claude-api-proxy.py > /dev/null << '\''PYEOF'\''
#!/usr/bin/env python3
import http.server, json, ssl, sys, os, urllib.request, urllib.error
API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
PORT = 3142
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/health":
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers()
self.wfile.write(b"{\"status\":\"ok\"}"); return
cl = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(cl)
try: data = json.loads(body)
except: data = {}
if "max_tokens" not in data: data["max_tokens"] = 8096
for f in ["webSearch","web_search"]: data.pop(f, None)
# Normalize model IDs — map short/dotted names to full API model IDs
MODEL_MAP = {
"claude-haiku-4.5": "claude-haiku-4-5-20251001",
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
"claude-sonnet-4": "claude-sonnet-4-20250514",
"claude-sonnet-4.5": "claude-sonnet-4-5-20250514",
"claude-sonnet-4-5": "claude-sonnet-4-5-20250514",
"claude-opus-4": "claude-opus-4-20250514",
}
m = data.get("model", "")
if m in MODEL_MAP: data["model"] = MODEL_MAP[m]
body = json.dumps(data).encode()
if not API_KEY:
err = json.dumps({"type":"error","error":{"type":"auth_error","message":"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat."}}).encode()
self.send_response(401); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(err))); self.end_headers(); self.wfile.write(err); return
headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}
for h in ["anthropic-version","anthropic-beta"]:
if self.headers.get(h): headers[h] = self.headers[h]
req = urllib.request.Request("https://api.anthropic.com"+self.path, data=body, headers=headers, method="POST")
try:
ctx = ssl.create_default_context()
resp = urllib.request.urlopen(req, context=ctx, timeout=300)
self.send_response(resp.status)
is_stream = "text/event-stream" in (resp.headers.get("Content-Type","") or "")
for k,v in resp.headers.items():
if k.lower() not in ("transfer-encoding","connection"): self.send_header(k,v)
if is_stream: self.send_header("Transfer-Encoding","chunked")
self.end_headers()
if is_stream:
while True:
chunk = resp.read(4096)
if not chunk: break
self.wfile.write(b"%x\r\n" % len(chunk)); self.wfile.write(chunk); self.wfile.write(b"\r\n"); self.wfile.flush()
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
else: self.wfile.write(resp.read())
except urllib.error.HTTPError as e:
self.send_response(e.code); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(e.read())
except Exception as e:
self.send_response(502); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(json.dumps({"error":str(e)}).encode())
def do_GET(self):
if self.path == "/health":
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b"{\"status\":\"ok\"}")
else: self.send_response(404); self.end_headers()
def log_message(self, fmt, *args): pass
if not API_KEY: print("WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions")
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
print(f"Claude API proxy on port {PORT}")
server.serve_forever()
PYEOF
sudo systemctl daemon-reload
sudo systemctl enable claude-api-proxy
sudo systemctl restart claude-api-proxy
sleep 1
echo " Claude API proxy: $(systemctl is-active claude-api-proxy)"
fi
sudo systemctl stop claude-api-proxy 2>/dev/null || true
sudo systemctl disable claude-api-proxy 2>/dev/null || true
sudo rm -f /etc/systemd/system/claude-api-proxy.service
sudo rm -f /opt/archipelago/claude-api-proxy.py
sudo rm -f /var/lib/archipelago/secrets/claude-api-proxy.env
sudo systemctl daemon-reload 2>/dev/null || true
echo " claude-api-proxy: $(systemctl is-active claude-api-proxy 2>&1)"
' 2>/dev/null || true
# Dev mode for Tailscale HTTP access (cookies need Secure flag disabled over plain HTTP)