From b28cc3eeaa45bfff02507f3f2b7b0528ac72b929 Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 3 Aug 2026 14:44:25 -0400 Subject: [PATCH] fix(13-02): retire the Python model-proxy sidecar and the OpenRouter open relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /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) --- .../src/api/rpc/system/handlers.rs | 22 +-- image-recipe/configs/nginx-archipelago.conf | 58 ++++---- scripts/deploy-to-target.sh | 105 +++----------- scripts/setup-aiui-server.sh | 131 +++--------------- 4 files changed, 76 insertions(+), 240 deletions(-) diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 30ed79da..72275f5b 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -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 })) } diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 72e55eeb..37a98f97 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -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/) { diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index dbecef51..f804cd28 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -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) diff --git a/scripts/setup-aiui-server.sh b/scripts/setup-aiui-server.sh index cbf3fc4a..fda5a849 100755 --- a/scripts/setup-aiui-server.sh +++ b/scripts/setup-aiui-server.sh @@ -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 @@ -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 " @@ -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= $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 /secrets/claude-api-key." echo " Access: http://$(echo $TARGET_HOST | cut -d@ -f2)"