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:
co-authored by
Claude Opus 5
parent
97921d9932
commit
b28cc3eeaa
@@ -1049,22 +1049,12 @@ impl RpcHandler {
|
||||
info!("Claude API key saved");
|
||||
}
|
||||
|
||||
// Update the claude-api-proxy environment and restart
|
||||
let env_line = format!("ANTHROPIC_API_KEY={}", value);
|
||||
let env_file = self.config.data_dir.join("secrets/claude-api-proxy.env");
|
||||
tokio::fs::write(&env_file, &env_line).await.ok();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&env_file, std::fs::Permissions::from_mode(0o600))
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Restart the proxy to pick up the new key
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["systemctl", "restart", "claude-api-proxy"])
|
||||
.output()
|
||||
.await;
|
||||
// `secrets/claude-api-key` (above) is deliberately the ONLY
|
||||
// Claude key ledger on this node (13-02-PLAN.md). A second
|
||||
// copy used to be written alongside it for a standalone,
|
||||
// unauthenticated sidecar process on port 3142 — that
|
||||
// sidecar and its key copy are retired; the session-gated
|
||||
// Rust daemon reads this one file directly.
|
||||
|
||||
Ok(serde_json::json!({ "saved": true }))
|
||||
}
|
||||
|
||||
@@ -46,12 +46,21 @@ server {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
# AIUI Claude API proxy (API key managed by proxy, no session gate needed)
|
||||
# AIUI Claude API proxy — re-pointed to the Rust daemon (127.0.0.1:5678),
|
||||
# which enforces the session cookie itself and reads the node's single
|
||||
# key ledger (data_dir/secrets/claude-api-key). The old comment here said
|
||||
# "API key managed by proxy, no session gate needed" — that confuses key
|
||||
# *secrecy* with spend *authorization* and is the reasoning error that
|
||||
# made this an unauthenticated door into a paid API (T-13-08/T-13-09).
|
||||
# Do not point this at a standalone process again. No trailing path on
|
||||
# proxy_pass: nginx forwards the request URI unmodified so the daemon's
|
||||
# own prefix match sees the full /aiui/api/claude/... path.
|
||||
location /aiui/api/claude/ {
|
||||
proxy_pass http://127.0.0.1:3142/;
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_connect_timeout 120s;
|
||||
@@ -59,25 +68,18 @@ server {
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
|
||||
# AIUI OpenRouter API proxy (API key managed by proxy, no session gate needed)
|
||||
location /aiui/api/openrouter/ {
|
||||
set $upstream_1 "https://openrouter.ai/api/";
|
||||
|
||||
proxy_pass $upstream_1;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host openrouter.ai;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_connect_timeout 120s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
|
||||
# AIUI Ollama (local AI) proxy — localhost:11434
|
||||
# AIUI Ollama (local AI) proxy — same daemon, same session gate as above.
|
||||
# The standalone AIUI OpenRouter relay that used to live here is deleted
|
||||
# outright: the node holds no key for that backend, it is not in the
|
||||
# model backend chain, and an unauthenticated proxy_pass to a paid
|
||||
# third-party API from the node's IP was a plain open relay (T-13-10).
|
||||
# AIUI's own standalone/dev mode keeps its own proxy and is unaffected.
|
||||
location /aiui/api/ollama/ {
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_connect_timeout 120s;
|
||||
@@ -958,11 +960,17 @@ server {
|
||||
try_files $uri $uri/ /aiui/index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
# See the HTTP server block above for the full rationale: re-pointed to
|
||||
# the session-gated Rust daemon (T-13-08/T-13-09), OpenRouter relay
|
||||
# deleted outright (T-13-10). Both server blocks must carry this fix —
|
||||
# a change applied to only one leaves the exposure live on whichever
|
||||
# block actually serves the request (T-13-15).
|
||||
location /aiui/api/claude/ {
|
||||
proxy_pass http://127.0.0.1:3142/;
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_connect_timeout 120s;
|
||||
@@ -970,27 +978,17 @@ server {
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
location /aiui/api/ollama/ {
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_connect_timeout 120s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
location /aiui/api/openrouter/ {
|
||||
set $upstream_6 "https://openrouter.ai/api/";
|
||||
|
||||
proxy_pass $upstream_6;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host openrouter.ai;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_connect_timeout 120s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
|
||||
# Icons, favicon, manifest — always revalidate (no heuristic caching)
|
||||
location ~* ^/(favicon\.ico|manifest\.webmanifest|assets/icon/) {
|
||||
|
||||
+21
-84
@@ -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)
|
||||
|
||||
+21
-110
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user