- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305 encrypted secret storage, QR code generation, and bcrypt-hashed backup codes - API key switcher: OAuth vs personal API key toggle in AIUI chat settings with status indicator, key validation, and help text - Login progress bar: server startup detection with health check polling, form disabled until server is ready - AI quarantine docs: comprehensive HTML page documenting all 6 security layers - Settings: AI Data Access permission toggles with per-category control - Alpha hardening plan: 28-task overnight automation plan across 7 phases (onboarding, login, app install, AIUI, UI polish, security, ISO build) - Backlog: node discovery spatial map feature for alpha demo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
759 lines
33 KiB
HTML
759 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Archipelago AI Quarantine Architecture</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface-2: #1c2333;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-muted: #8b949e;
|
|
--accent: #fb923c;
|
|
--green: #4ade80;
|
|
--red: #ef4444;
|
|
--blue: #58a6ff;
|
|
--purple: #bc8cff;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.7;
|
|
padding: 2rem;
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
font-size: 2.2rem;
|
|
margin-bottom: 0.5rem;
|
|
background: linear-gradient(135deg, var(--accent), #f59e0b);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
.subtitle {
|
|
color: var(--text-muted);
|
|
font-size: 1.1rem;
|
|
margin-bottom: 2.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 1.5rem;
|
|
}
|
|
h2 {
|
|
font-size: 1.5rem;
|
|
margin: 2.5rem 0 1rem;
|
|
color: var(--accent);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
h2 .num {
|
|
background: var(--accent);
|
|
color: var(--bg);
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
h3 {
|
|
font-size: 1.15rem;
|
|
margin: 1.5rem 0 0.5rem;
|
|
color: var(--blue);
|
|
}
|
|
p { margin-bottom: 1rem; color: var(--text); }
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
margin: 1rem 0;
|
|
}
|
|
.card-green { border-left: 4px solid var(--green); }
|
|
.card-red { border-left: 4px solid var(--red); }
|
|
.card-blue { border-left: 4px solid var(--blue); }
|
|
.card-orange { border-left: 4px solid var(--accent); }
|
|
.card-purple { border-left: 4px solid var(--purple); }
|
|
.card h4 {
|
|
font-size: 1rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
code {
|
|
background: var(--surface-2);
|
|
padding: 0.15rem 0.45rem;
|
|
border-radius: 4px;
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 0.88rem;
|
|
color: var(--accent);
|
|
}
|
|
pre {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1rem 1.25rem;
|
|
overflow-x: auto;
|
|
margin: 1rem 0;
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 0.85rem;
|
|
line-height: 1.6;
|
|
color: var(--text);
|
|
}
|
|
pre code { background: none; padding: 0; color: inherit; }
|
|
.label {
|
|
display: inline-block;
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 999px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.label-green { background: rgba(74, 222, 128, 0.15); color: var(--green); }
|
|
.label-red { background: rgba(239, 68, 68, 0.15); color: var(--red); }
|
|
.label-blue { background: rgba(88, 166, 255, 0.15); color: var(--blue); }
|
|
.label-orange { background: rgba(251, 146, 60, 0.15); color: var(--accent); }
|
|
ul { margin: 0.5rem 0 1rem 1.5rem; }
|
|
li { margin-bottom: 0.4rem; }
|
|
li code { font-size: 0.82rem; }
|
|
.diagram {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
margin: 1.5rem 0;
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 0.82rem;
|
|
line-height: 1.8;
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
color: var(--text-muted);
|
|
}
|
|
.diagram .highlight { color: var(--accent); font-weight: 600; }
|
|
.diagram .green { color: var(--green); }
|
|
.diagram .red { color: var(--red); }
|
|
.diagram .blue { color: var(--blue); }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1rem 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
th {
|
|
background: var(--surface-2);
|
|
text-align: left;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 2px solid var(--border);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
td {
|
|
padding: 0.6rem 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: top;
|
|
}
|
|
tr:hover td { background: rgba(251, 146, 60, 0.03); }
|
|
.flow-arrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin: 1rem 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
.flow-box {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
.flow-box.secure {
|
|
border-color: var(--green);
|
|
color: var(--green);
|
|
}
|
|
.flow-box.blocked {
|
|
border-color: var(--red);
|
|
color: var(--red);
|
|
}
|
|
.arrow { color: var(--text-muted); font-size: 1.2rem; }
|
|
.toc {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
margin: 1.5rem 0;
|
|
}
|
|
.toc h3 { margin-top: 0; color: var(--text); }
|
|
.toc ol { margin-left: 1.5rem; }
|
|
.toc li { margin-bottom: 0.3rem; }
|
|
.toc a { color: var(--blue); text-decoration: none; }
|
|
.toc a:hover { text-decoration: underline; }
|
|
.files-ref {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.5rem;
|
|
}
|
|
.files-ref code {
|
|
color: var(--text-muted);
|
|
font-size: 0.8rem;
|
|
}
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 1rem;
|
|
margin: 1.5rem 0;
|
|
}
|
|
@media (max-width: 600px) {
|
|
body { padding: 1rem; }
|
|
h1 { font-size: 1.6rem; }
|
|
pre { font-size: 0.78rem; padding: 0.75rem; }
|
|
.diagram { font-size: 0.7rem; padding: 1rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Archipelago AI Quarantine Architecture</h1>
|
|
<p class="subtitle">How AIUI (Claude) is sandboxed from your node's sensitive data — a defense-in-depth approach across 6 layers</p>
|
|
|
|
<div class="toc">
|
|
<h3>Contents</h3>
|
|
<ol>
|
|
<li><a href="#overview">Architecture Overview & Diagram</a></li>
|
|
<li><a href="#layer1">Layer 1: Container Isolation (Podman)</a></li>
|
|
<li><a href="#layer2">Layer 2: Iframe Sandbox (Browser)</a></li>
|
|
<li><a href="#layer3">Layer 3: postMessage Gate (Context Broker)</a></li>
|
|
<li><a href="#layer4">Layer 4: Per-Category Permissions (User Toggles)</a></li>
|
|
<li><a href="#layer5">Layer 5: Data Sanitization (Field Stripping)</a></li>
|
|
<li><a href="#layer6">Layer 6: Proxy & Nginx Authentication</a></li>
|
|
<li><a href="#protocol">The postMessage Protocol</a></li>
|
|
<li><a href="#context">What the AI System Prompt Sees</a></li>
|
|
<li><a href="#never">What the AI Can NEVER See</a></li>
|
|
<li><a href="#actions">Permitted Actions (Limited)</a></li>
|
|
<li><a href="#bugs">Current Bugs & Issues</a></li>
|
|
<li><a href="#files">Source File Reference</a></li>
|
|
</ol>
|
|
</div>
|
|
|
|
<!-- ───────────────── OVERVIEW ───────────────── -->
|
|
<h2 id="overview"><span class="num">0</span> Architecture Overview</h2>
|
|
|
|
<p>The AI is treated as <strong>untrusted code in a hostile environment</strong>. It runs inside an iframe with sandbox restrictions, inside a Podman container with no outbound network. All data it receives passes through a <strong>Context Broker</strong> that checks user permissions and strips sensitive fields before anything reaches Claude's API.</p>
|
|
|
|
<div class="diagram"><span class="highlight">User's Browser</span>
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ <span class="blue">Archy (neode-ui)</span> — Vue.js Host Application │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────┐ │
|
|
│ │ <span class="green">Context Broker</span> │ │
|
|
│ │ - Checks aiPermissions store │ │
|
|
│ │ - Validates postMessage origin │ │
|
|
│ │ - Fetches data from Pinia stores / RPC │ │
|
|
│ │ - <span class="red">Strips sensitive fields</span> (sanitize*) │ │
|
|
│ │ - Returns only permitted, sanitized data │ │
|
|
│ └──────────────┬────────────────────────────────┘ │
|
|
│ │ postMessage (origin-validated) │
|
|
│ ┌──────────────▼────────────────────────────────┐ │
|
|
│ │ <span class="highlight">AIUI iframe</span> │ │
|
|
│ │ sandbox="allow-scripts allow-same-origin │ │
|
|
│ │ allow-forms" │ │
|
|
│ │ │ │
|
|
│ │ <span class="green">archyBridge</span> ──postMessage──▶ Context Broker │ │
|
|
│ │ <span class="red">✗ Cannot</span> call /rpc/ directly │ │
|
|
│ │ <span class="red">✗ Cannot</span> access host DOM │ │
|
|
│ │ <span class="red">✗ Cannot</span> open popups │ │
|
|
│ └───────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────┘
|
|
│ HTTPS (session cookie required)
|
|
▼
|
|
┌──────────────────────────────────┐
|
|
│ <span class="blue">Nginx</span> (/aiui/api/claude/) │ ◀── cookie check gate
|
|
│ proxy_pass → 127.0.0.1:3141 │
|
|
└──────────────┬───────────────────┘
|
|
▼
|
|
┌──────────────────────────────────┐
|
|
│ <span class="highlight">Claude Proxy</span> (port 3141) │
|
|
│ OAuth token from macOS keychain │
|
|
│ → Anthropic API │
|
|
└──────────────────────────────────┘
|
|
|
|
<span class="red">BLOCKED paths</span> (AI cannot reach):
|
|
✗ /rpc/ (backend API) ✗ Container exec
|
|
✗ /ws (WebSocket) ✗ File system
|
|
✗ SSH ✗ Outbound network (from container)</div>
|
|
|
|
<!-- ───────────────── LAYER 1 ───────────────── -->
|
|
<h2 id="layer1"><span class="num">1</span> Layer 1: Container Isolation (Podman)</h2>
|
|
|
|
<div class="card card-green">
|
|
<h4>AIUI runs in a locked-down Podman container</h4>
|
|
<p>Even if the AIUI web app were compromised, the container itself has no way to reach the rest of the system.</p>
|
|
</div>
|
|
|
|
<pre><code># apps/aiui/manifest.yml
|
|
security:
|
|
capabilities: [] # No Linux capabilities at all
|
|
readonly_root: true # Read-only filesystem
|
|
no_new_privileges: true # Cannot escalate privileges
|
|
network_policy: isolated # NO outbound network access
|
|
|
|
ports:
|
|
- host: 5180
|
|
container: 80
|
|
bind: 127.0.0.1 # Only reachable via nginx, not externally</code></pre>
|
|
|
|
<p><strong>What this means:</strong></p>
|
|
<ul>
|
|
<li>The AIUI container <strong>cannot make HTTP requests to the internet</strong> or to other containers</li>
|
|
<li>It serves static files only — the actual Claude API calls happen in the <em>browser</em>, not the container</li>
|
|
<li>Even with root access in the container, you can't escalate or modify the filesystem</li>
|
|
<li>The container port (5180) is bound to <code>127.0.0.1</code>, so only nginx (on the same machine) can reach it</li>
|
|
</ul>
|
|
|
|
<!-- ───────────────── LAYER 2 ───────────────── -->
|
|
<h2 id="layer2"><span class="num">2</span> Layer 2: Iframe Sandbox (Browser)</h2>
|
|
|
|
<div class="card card-blue">
|
|
<h4>AIUI loads inside a sandboxed iframe</h4>
|
|
<p>The browser enforces strict boundaries between the host Archy app and the AIUI iframe.</p>
|
|
</div>
|
|
|
|
<pre><code><!-- neode-ui/src/views/Chat.vue -->
|
|
<iframe
|
|
:src="aiuiUrl"
|
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
|
allow="microphone"
|
|
/></code></pre>
|
|
|
|
<p><strong>The sandbox attribute restricts AIUI from:</strong></p>
|
|
<ul>
|
|
<li><strong>Navigating the parent page</strong> — cannot redirect Archy</li>
|
|
<li><strong>Opening popups/new windows</strong> — <code>allow-popups</code> is NOT granted</li>
|
|
<li><strong>Accessing parent DOM</strong> — cross-origin isolation is enforced</li>
|
|
<li><strong>Submitting forms to external URLs</strong> — forms are scoped to same origin</li>
|
|
<li><strong>Running plugins</strong> — no plugin execution</li>
|
|
</ul>
|
|
|
|
<p>The only communication channel is <code>window.postMessage()</code>, which is intercepted by the Context Broker.</p>
|
|
|
|
<!-- ───────────────── LAYER 3 ───────────────── -->
|
|
<h2 id="layer3"><span class="num">3</span> Layer 3: The Context Broker (postMessage Gate)</h2>
|
|
|
|
<div class="card card-orange">
|
|
<h4>Every data request goes through a single gatekeeper</h4>
|
|
<p>The <code>ContextBroker</code> class validates origin, checks permissions, fetches data, strips sensitive fields, then responds. AIUI never directly calls any backend API.</p>
|
|
</div>
|
|
|
|
<h3>How it works</h3>
|
|
|
|
<div class="flow-arrow">
|
|
<div class="flow-box">AIUI sends<br><code>context:request</code></div>
|
|
<span class="arrow">→</span>
|
|
<div class="flow-box secure">Origin validated<br><code>event.origin === allowedOrigin</code></div>
|
|
<span class="arrow">→</span>
|
|
<div class="flow-box secure">Permission checked<br><code>perms.isEnabled(category)</code></div>
|
|
<span class="arrow">→</span>
|
|
<div class="flow-box secure">Data fetched &<br>sanitized</div>
|
|
<span class="arrow">→</span>
|
|
<div class="flow-box">Response sent<br>to iframe</div>
|
|
</div>
|
|
|
|
<pre><code>// contextBroker.ts — the critical permission check
|
|
private async handleContextRequest(id, category, query?) {
|
|
const perms = useAIPermissionsStore()
|
|
|
|
if (!perms.isEnabled(category)) {
|
|
// DENIED — send empty response, no data
|
|
this.postToIframe({
|
|
type: 'context:response', id,
|
|
data: null,
|
|
permitted: false, // ← AIUI knows it was denied
|
|
})
|
|
return
|
|
}
|
|
|
|
// ALLOWED — fetch and sanitize before sending
|
|
const data = await this.fetchAndSanitize(category, query)
|
|
this.postToIframe({
|
|
type: 'context:response', id,
|
|
data, // ← sanitized data only
|
|
permitted: true,
|
|
})
|
|
}</code></pre>
|
|
|
|
<h3>Origin Validation (both sides)</h3>
|
|
<ul>
|
|
<li><strong>Context Broker</strong> (host): Rejects any message where <code>event.origin !== this.allowedOrigin</code></li>
|
|
<li><strong>archyBridge</strong> (AIUI): Rejects any message where <code>event.origin !== allowedOrigin</code></li>
|
|
<li><strong>Responses</strong> use explicit target origin: <code>iframe.contentWindow.postMessage(msg, this.allowedOrigin)</code></li>
|
|
</ul>
|
|
|
|
<!-- ───────────────── LAYER 4 ───────────────── -->
|
|
<h2 id="layer4"><span class="num">4</span> Layer 4: Per-Category Permission Toggles</h2>
|
|
|
|
<div class="card card-purple">
|
|
<h4>All categories are OFF by default</h4>
|
|
<p>The user must explicitly enable each data category in Settings → AI Data Access. The AI sees nothing until you flip the switch.</p>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Category</th>
|
|
<th>What AI Sees</th>
|
|
<th>What's Stripped</th>
|
|
<th>Default</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>apps</code><br><span class="label label-blue">Installed Apps</span></td>
|
|
<td>App names, versions, running state, URLs</td>
|
|
<td>Config files, env vars, credentials</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>system</code><br><span class="label label-blue">System Stats</span></td>
|
|
<td>CPU %, RAM used/total, disk used/total, uptime</td>
|
|
<td>File paths, IP addresses, hostnames, PIDs</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>network</code><br><span class="label label-blue">Network Status</span></td>
|
|
<td>Connected (bool), Tor active (bool), Tailscale active (bool)</td>
|
|
<td>IP addresses, Tor .onion addresses, peer IPs, MAC addresses</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>bitcoin</code><br><span class="label label-orange">Bitcoin Node</span></td>
|
|
<td>Block height, sync %, chain, difficulty, mempool size/count</td>
|
|
<td>Wallet keys, addresses, transaction history, RPC credentials</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>wallet</code><br><span class="label label-orange">Wallet Overview</span></td>
|
|
<td>Alias, channel count, peer count, balance (sats), sync status</td>
|
|
<td><strong>Private keys, seed phrases, macaroons, channel secrets, addresses</strong></td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>media</code><br><span class="label label-blue">Media Libraries</span></td>
|
|
<td>Which media apps are installed (Plex, Jellyfin, etc.) + status</td>
|
|
<td>Library contents, file paths, metadata</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>files</code><br><span class="label label-blue">File Names</span></td>
|
|
<td>Folder names, recent file names, sizes, dates from Cloud</td>
|
|
<td>File contents (unless read-file action is used with permission)</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>notes</code><br><span class="label label-blue">Documents</span></td>
|
|
<td>Document titles (currently returns "not available")</td>
|
|
<td>Document contents</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>search</code><br><span class="label label-green">Web Search</span></td>
|
|
<td>Whether SearXNG is installed + available</td>
|
|
<td>N/A</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>ai-local</code><br><span class="label label-green">Local AI</span></td>
|
|
<td>Whether Ollama is installed + running</td>
|
|
<td>Model details</td>
|
|
<td><span class="label label-red">OFF</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p class="files-ref">Permissions stored in <code>localStorage</code> key: <code>archipelago-ai-permissions</code></p>
|
|
<p class="files-ref">Store: <code>neode-ui/src/stores/aiPermissions.ts</code></p>
|
|
|
|
<!-- ───────────────── LAYER 5 ───────────────── -->
|
|
<h2 id="layer5"><span class="num">5</span> Layer 5: Data Sanitization</h2>
|
|
|
|
<div class="card card-green">
|
|
<h4>Each category has a dedicated sanitize function that extracts only whitelisted fields</h4>
|
|
<p>The broker doesn't pass raw data through — it constructs new objects with only safe properties.</p>
|
|
</div>
|
|
|
|
<h3>Example: Bitcoin sanitization</h3>
|
|
<pre><code>// contextBroker.ts — sanitizeBitcoin()
|
|
// ONLY these fields are extracted and sent to AI:
|
|
return {
|
|
available: true,
|
|
status: 'running',
|
|
block_height: info.block_height,
|
|
sync_progress: info.sync_progress,
|
|
chain: info.chain,
|
|
difficulty: info.difficulty,
|
|
mempool_size: info.mempool_size,
|
|
mempool_tx_count: info.mempool_tx_count,
|
|
verification_progress: info.verification_progress,
|
|
}
|
|
// NOT included: wallet data, addresses, keys, RPC auth, raw responses</code></pre>
|
|
|
|
<h3>Example: Wallet sanitization</h3>
|
|
<pre><code>// contextBroker.ts — sanitizeWallet()
|
|
// ONLY these safe summary fields:
|
|
return {
|
|
available: true,
|
|
status: 'running',
|
|
alias: info.alias,
|
|
num_active_channels: info.num_active_channels,
|
|
num_peers: info.num_peers,
|
|
synced_to_chain: info.synced_to_chain,
|
|
block_height: info.block_height,
|
|
balance_sats: info.balance_sats,
|
|
channel_balance_sats: info.channel_balance_sats,
|
|
pending_open_balance: info.pending_open_balance,
|
|
}
|
|
// NEVER included: private keys, seed phrases, macaroons,
|
|
// channel points, backup data, node pubkeys</code></pre>
|
|
|
|
<h3>Example: Network sanitization</h3>
|
|
<pre><code>// contextBroker.ts — sanitizeNetwork()
|
|
// Only booleans — no addresses:
|
|
return {
|
|
connected: store.isConnected, // true/false
|
|
torConnected: hasTor, // true/false
|
|
tailscaleActive: tailscale?.state === 'running', // true/false
|
|
}
|
|
// NEVER: IP addresses, .onion addresses, peer info, MAC addresses</code></pre>
|
|
|
|
<!-- ───────────────── LAYER 6 ───────────────── -->
|
|
<h2 id="layer6"><span class="num">6</span> Layer 6: Proxy & Nginx Authentication</h2>
|
|
|
|
<div class="card card-blue">
|
|
<h4>Claude API requests require a valid Archy session</h4>
|
|
<p>Nginx rejects unauthenticated API calls. The Claude Proxy on port 3141 manages OAuth tokens securely.</p>
|
|
</div>
|
|
|
|
<pre><code># nginx-archipelago.conf
|
|
location /aiui/api/claude/ {
|
|
if ($cookie_session = "") {
|
|
return 401 '{"error":"Unauthorized"}'; # No session = blocked
|
|
}
|
|
proxy_pass http://127.0.0.1:3141/; # → Claude Proxy
|
|
}</code></pre>
|
|
|
|
<p><strong>The Claude Proxy (port 3141):</strong></p>
|
|
<ul>
|
|
<li>OAuth token stored securely (macOS keychain → <code>.env.local</code>)</li>
|
|
<li>Auto-refreshes tokens 5 minutes before expiry</li>
|
|
<li>Never exposes the token to the browser — the proxy adds auth headers server-side</li>
|
|
<li>Only the browser's fetch to <code>/aiui/api/claude/</code> goes through this proxy</li>
|
|
</ul>
|
|
|
|
<p><strong>Content Security Policy (CSP):</strong></p>
|
|
<pre><code>Content-Security-Policy: default-src 'self';
|
|
connect-src 'self' ws: wss:;
|
|
frame-src 'self' http://127.0.0.1:* http://localhost:*;</code></pre>
|
|
<p>The CSP restricts the AIUI iframe to only connect to the same origin and local addresses. No external fetch calls are possible.</p>
|
|
|
|
<!-- ───────────────── PROTOCOL ───────────────── -->
|
|
<h2 id="protocol"><span class="num">7</span> The postMessage Protocol</h2>
|
|
|
|
<p>AIUI and Archy communicate via a strictly-typed protocol defined in <code>neode-ui/src/types/aiui-protocol.ts</code>.</p>
|
|
|
|
<h3>AIUI → Archy (Requests)</h3>
|
|
<table>
|
|
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>ready</code></td><td>Signals iframe is loaded</td><td>None</td></tr>
|
|
<tr><td><code>context:request</code></td><td>Request node data</td><td><code>id</code>, <code>category</code>, <code>query?</code></td></tr>
|
|
<tr><td><code>action:request</code></td><td>Request an action</td><td><code>id</code>, <code>action</code>, <code>params</code></td></tr>
|
|
<tr><td><code>theme:request</code></td><td>Request UI theme</td><td>None</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>Archy → AIUI (Responses)</h3>
|
|
<table>
|
|
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>context:response</code></td><td>Sanitized data or denial</td><td><code>id</code>, <code>data</code>, <code>permitted</code> (bool)</td></tr>
|
|
<tr><td><code>action:response</code></td><td>Action result</td><td><code>id</code>, <code>success</code>, <code>error?</code>, <code>data?</code></td></tr>
|
|
<tr><td><code>permissions:update</code></td><td>Push new permissions</td><td><code>categories[]</code></td></tr>
|
|
<tr><td><code>theme:response</code></td><td>Theme colors</td><td><code>theme { accent, mode }</code></td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- ───────────────── CONTEXT ───────────────── -->
|
|
<h2 id="context"><span class="num">8</span> What the AI System Prompt Sees</h2>
|
|
|
|
<p>The <code>buildArchyContext()</code> function in AIUI constructs a context string that gets appended to Claude's system prompt. It only includes data for <strong>permitted categories</strong>:</p>
|
|
|
|
<pre><code>// Example output when apps + bitcoin + wallet are enabled:
|
|
|
|
**Archy Node Context** (this user is running AIUI on their Archipelago node):
|
|
**Installed apps on this node:**
|
|
- Bitcoin Knots (installed, running)
|
|
- LND (installed, running)
|
|
- Mempool (installed, running)
|
|
- File Browser (installed, running)
|
|
**Bitcoin:** Block 890,123, 99.99% synced, mainnet, mempool: 42,815 txs
|
|
**Lightning (LND):** MyNode | 5 channels | 3 peers | On-chain: 150,000 sats
|
|
|
|
You can help the user manage their node. Available actions: open an app
|
|
(open-app), install an app (install-app), navigate in Archy (navigate).</code></pre>
|
|
|
|
<div class="card card-red">
|
|
<h4>What's NOT in the system prompt — ever</h4>
|
|
<ul>
|
|
<li>Private keys, seed phrases, HD derivation paths</li>
|
|
<li>Macaroons, auth tokens, API keys</li>
|
|
<li>IP addresses (.onion, LAN, WAN, Tailscale)</li>
|
|
<li>File contents, log contents</li>
|
|
<li>SSH credentials, RPC passwords</li>
|
|
<li>Transaction history, UTXO set, address lists</li>
|
|
<li>Container configs, environment variables</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- ───────────────── NEVER ───────────────── -->
|
|
<h2 id="never"><span class="num">9</span> What the AI Can NEVER See</h2>
|
|
|
|
<div class="summary-grid">
|
|
<div class="card card-red">
|
|
<h4>Cryptographic Material</h4>
|
|
<ul>
|
|
<li>Private keys (BTC, LN)</li>
|
|
<li>Seed phrases / BIP39 mnemonics</li>
|
|
<li>LND macaroons</li>
|
|
<li>Channel backup data</li>
|
|
<li>HD derivation paths</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card card-red">
|
|
<h4>Network Identity</h4>
|
|
<ul>
|
|
<li>IP addresses (LAN, WAN)</li>
|
|
<li>Tor .onion addresses</li>
|
|
<li>Tailscale IPs</li>
|
|
<li>Peer connection details</li>
|
|
<li>MAC addresses</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card card-red">
|
|
<h4>Credentials</h4>
|
|
<ul>
|
|
<li>SSH passwords / keys</li>
|
|
<li>RPC usernames/passwords</li>
|
|
<li>API tokens</li>
|
|
<li>Session cookies</li>
|
|
<li>OAuth tokens</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card card-red">
|
|
<h4>Sensitive Data</h4>
|
|
<ul>
|
|
<li>Transaction history</li>
|
|
<li>Bitcoin addresses (receive/change)</li>
|
|
<li>UTXO set</li>
|
|
<li>File contents (unless explicitly permitted)</li>
|
|
<li>Environment variables</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ───────────────── ACTIONS ───────────────── -->
|
|
<h2 id="actions"><span class="num">10</span> Permitted Actions</h2>
|
|
|
|
<p>The AI can request a limited set of actions through the Context Broker. Each action is validated and requires the relevant permission category to be enabled.</p>
|
|
|
|
<table>
|
|
<thead><tr><th>Action</th><th>What It Does</th><th>Requires Permission</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>open-app</code></td><td>Dispatches event to open an installed app</td><td><em>None (navigation)</em></td></tr>
|
|
<tr><td><code>navigate</code></td><td>Navigate to a path within Archy UI</td><td><em>None (navigation)</em></td></tr>
|
|
<tr><td><code>install-app</code></td><td>Installs an app from marketplace</td><td><em>None</em></td></tr>
|
|
<tr><td><code>search-web</code></td><td>Searches via local SearXNG instance</td><td><code>search</code></td></tr>
|
|
<tr><td><code>read-file</code></td><td>Reads a file from FileBrowser (Cloud)</td><td><code>files</code></td></tr>
|
|
<tr><td><code>tail-logs</code></td><td>Gets recent log lines for an app</td><td><code>apps</code></td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="card card-red">
|
|
<h4>Actions the AI CANNOT perform</h4>
|
|
<ul>
|
|
<li>Execute shell commands</li>
|
|
<li>Call backend RPC endpoints directly</li>
|
|
<li>Modify container configs</li>
|
|
<li>Access the filesystem outside FileBrowser</li>
|
|
<li>Send Bitcoin transactions</li>
|
|
<li>Open/close Lightning channels</li>
|
|
<li>Modify system settings</li>
|
|
<li>Access other users' data</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- ───────────────── BUGS ───────────────── -->
|
|
<h2 id="bugs"><span class="num">11</span> Current Bugs & Issues</h2>
|
|
|
|
<div class="card card-red">
|
|
<h4>"messages.6: user messages must have non-empty content" error</h4>
|
|
<p>This Anthropic API 400 error occurs when replying in the chat. The AIUI client is sending a message array where one of the user messages has empty content (likely an empty string or the reply content isn't being properly included in the messages array). This is a bug in the AIUI chat message construction, not a quarantine issue.</p>
|
|
</div>
|
|
|
|
<div class="card card-orange">
|
|
<h4>Inconsistent node awareness</h4>
|
|
<p>The AI sometimes says "I don't have access to your Bitcoin node" even though Bitcoin data may be permitted. This happens because:</p>
|
|
<ul>
|
|
<li>The <code>bitcoin.getinfo</code> RPC call may fail (e.g., Bitcoin Knots RPC not configured in the backend)</li>
|
|
<li>When the RPC fails, the broker returns a minimal fallback: <code>{ available: true, status: 'running', network: 'mainnet' }</code></li>
|
|
<li>The system prompt context then shows limited info, and Claude responds conservatively</li>
|
|
<li>The <code>tail-logs</code> action could fetch Bitcoin logs, but Claude may not know to use it</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- ───────────────── FILES ───────────────── -->
|
|
<h2 id="files"><span class="num">12</span> Source File Reference</h2>
|
|
|
|
<table>
|
|
<thead><tr><th>File</th><th>Role</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>neode-ui/src/services/contextBroker.ts</code></td><td>The quarantine gate — validates, checks permissions, sanitizes all data</td></tr>
|
|
<tr><td><code>neode-ui/src/types/aiui-protocol.ts</code></td><td>Strict TypeScript protocol definition for all messages</td></tr>
|
|
<tr><td><code>neode-ui/src/stores/aiPermissions.ts</code></td><td>Pinia store for per-category permission toggles</td></tr>
|
|
<tr><td><code>neode-ui/src/views/Chat.vue</code></td><td>Iframe host with sandbox attribute</td></tr>
|
|
<tr><td><code>neode-ui/src/views/Settings.vue</code></td><td>AI Data Access toggles UI</td></tr>
|
|
<tr><td><code>apps/aiui/manifest.yml</code></td><td>Container security config (isolated network, readonly root)</td></tr>
|
|
<tr><td><code>image-recipe/configs/nginx-archipelago.conf</code></td><td>Nginx routes with session cookie auth gate</td></tr>
|
|
<tr><td><code>AIUI/packages/app/src/services/archyBridge.ts</code></td><td>AIUI-side postMessage client (the only way AIUI talks to Archy)</td></tr>
|
|
<tr><td><code>AIUI/packages/app/src/composables/useArchy.ts</code></td><td>Vue composable wrapping archyBridge + <code>buildArchyContext()</code></td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="card card-green" style="margin-top: 2rem;">
|
|
<h4>Summary: 6 Layers of Defense</h4>
|
|
<ol>
|
|
<li><strong>Container</strong> — Podman with isolated network, read-only FS, zero capabilities</li>
|
|
<li><strong>Iframe sandbox</strong> — Browser-enforced isolation, no popups, no parent DOM access</li>
|
|
<li><strong>Context Broker</strong> — Single postMessage gate with origin validation</li>
|
|
<li><strong>Permissions</strong> — Per-category toggles, all OFF by default</li>
|
|
<li><strong>Sanitization</strong> — Dedicated functions strip sensitive fields per category</li>
|
|
<li><strong>Proxy auth</strong> — Nginx session cookie check + CSP headers</li>
|
|
</ol>
|
|
<p style="margin-top: 1rem; color: var(--text-muted);">The AI is treated as untrusted. It can only see what you explicitly permit, and even then, sensitive fields are stripped before the data ever reaches Claude's API.</p>
|
|
</div>
|
|
|
|
<p style="text-align: center; color: var(--text-muted); margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.85rem;">
|
|
Archipelago AI Quarantine Architecture — Generated 2026-03-06 — v1.0.0
|
|
</p>
|
|
|
|
</body>
|
|
</html>
|