Files
archy/docs/ai-quarantine-architecture.html
Dorian e55fd3baf0 feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- 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>
2026-03-06 12:23:57 +00:00

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 &mdash; a defense-in-depth approach across 6 layers</p>
<div class="toc">
<h3>Contents</h3>
<ol>
<li><a href="#overview">Architecture Overview &amp; 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 &amp; 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 &amp; 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 &mdash; 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>&lt;!-- neode-ui/src/views/Chat.vue --&gt;
&lt;iframe
:src="aiuiUrl"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="microphone"
/&gt;</code></pre>
<p><strong>The sandbox attribute restricts AIUI from:</strong></p>
<ul>
<li><strong>Navigating the parent page</strong> &mdash; cannot redirect Archy</li>
<li><strong>Opening popups/new windows</strong> &mdash; <code>allow-popups</code> is NOT granted</li>
<li><strong>Accessing parent DOM</strong> &mdash; cross-origin isolation is enforced</li>
<li><strong>Submitting forms to external URLs</strong> &mdash; forms are scoped to same origin</li>
<li><strong>Running plugins</strong> &mdash; 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">&rarr;</span>
<div class="flow-box secure">Origin validated<br><code>event.origin === allowedOrigin</code></div>
<span class="arrow">&rarr;</span>
<div class="flow-box secure">Permission checked<br><code>perms.isEnabled(category)</code></div>
<span class="arrow">&rarr;</span>
<div class="flow-box secure">Data fetched &amp;<br>sanitized</div>
<span class="arrow">&rarr;</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 &rarr; 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 &mdash; 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 &amp; 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 &rarr; <code>.env.local</code>)</li>
<li>Auto-refreshes tokens 5 minutes before expiry</li>
<li>Never exposes the token to the browser &mdash; 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 &rarr; 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 &rarr; 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 &mdash; 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 &amp; 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 &mdash; 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> &mdash; Podman with isolated network, read-only FS, zero capabilities</li>
<li><strong>Iframe sandbox</strong> &mdash; Browser-enforced isolation, no popups, no parent DOM access</li>
<li><strong>Context Broker</strong> &mdash; Single postMessage gate with origin validation</li>
<li><strong>Permissions</strong> &mdash; Per-category toggles, all OFF by default</li>
<li><strong>Sanitization</strong> &mdash; Dedicated functions strip sensitive fields per category</li>
<li><strong>Proxy auth</strong> &mdash; 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 &mdash; Generated 2026-03-06 &mdash; v1.0.0
</p>
</body>
</html>