Archipelago AI Quarantine Architecture

How AIUI (Claude) is sandboxed from your node's sensitive data — a defense-in-depth approach across 6 layers

Contents

  1. Architecture Overview & Diagram
  2. Layer 1: Container Isolation (Podman)
  3. Layer 2: Iframe Sandbox (Browser)
  4. Layer 3: postMessage Gate (Context Broker)
  5. Layer 4: Per-Category Permissions (User Toggles)
  6. Layer 5: Data Sanitization (Field Stripping)
  7. Layer 6: Proxy & Nginx Authentication
  8. The postMessage Protocol
  9. What the AI System Prompt Sees
  10. What the AI Can NEVER See
  11. Permitted Actions (Limited)
  12. Current Bugs & Issues
  13. Source File Reference

0 Architecture Overview

The AI is treated as untrusted code in a hostile environment. It runs inside an iframe with sandbox restrictions, inside a Podman container with no outbound network. All data it receives passes through a Context Broker that checks user permissions and strips sensitive fields before anything reaches Claude's API.

User's Browser ┌─────────────────────────────────────────────────────┐ │ Archy (neode-ui) — Vue.js Host Application │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ Context Broker │ │ │ │ - Checks aiPermissions store │ │ │ │ - Validates postMessage origin │ │ │ │ - Fetches data from Pinia stores / RPC │ │ │ │ - Strips sensitive fields (sanitize*) │ │ │ │ - Returns only permitted, sanitized data │ │ │ └──────────────┬────────────────────────────────┘ │ │ │ postMessage (origin-validated) │ │ ┌──────────────▼────────────────────────────────┐ │ │ │ AIUI iframe │ │ │ │ sandbox="allow-scripts allow-same-origin │ │ │ │ allow-forms" │ │ │ │ │ │ │ │ archyBridge ──postMessage──▶ Context Broker │ │ │ │ ✗ Cannot call /rpc/ directly │ │ │ │ ✗ Cannot access host DOM │ │ │ │ ✗ Cannot open popups │ │ │ └───────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ │ HTTPS (session cookie required) ▼ ┌──────────────────────────────────┐ │ Nginx (/aiui/api/claude/) │ ◀── cookie check gate │ proxy_pass → 127.0.0.1:3141 │ └──────────────┬───────────────────┘ ▼ ┌──────────────────────────────────┐ │ Claude Proxy (port 3141) │ │ OAuth token from macOS keychain │ │ → Anthropic API │ └──────────────────────────────────┘ BLOCKED paths (AI cannot reach): ✗ /rpc/ (backend API) ✗ Container exec ✗ /ws (WebSocket) ✗ File system ✗ SSH ✗ Outbound network (from container)

1 Layer 1: Container Isolation (Podman)

AIUI runs in a locked-down Podman container

Even if the AIUI web app were compromised, the container itself has no way to reach the rest of the system.

# 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

What this means:

2 Layer 2: Iframe Sandbox (Browser)

AIUI loads inside a sandboxed iframe

The browser enforces strict boundaries between the host Archy app and the AIUI iframe.

<!-- neode-ui/src/views/Chat.vue -->
<iframe
  :src="aiuiUrl"
  sandbox="allow-scripts allow-same-origin allow-forms"
  allow="microphone"
/>

The sandbox attribute restricts AIUI from:

The only communication channel is window.postMessage(), which is intercepted by the Context Broker.

3 Layer 3: The Context Broker (postMessage Gate)

Every data request goes through a single gatekeeper

The ContextBroker class validates origin, checks permissions, fetches data, strips sensitive fields, then responds. AIUI never directly calls any backend API.

How it works

AIUI sends
context:request
Origin validated
event.origin === allowedOrigin
Permission checked
perms.isEnabled(category)
Data fetched &
sanitized
Response sent
to iframe
// 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,
  })
}

Origin Validation (both sides)

4 Layer 4: Per-Category Permission Toggles

All categories are OFF by default

The user must explicitly enable each data category in Settings → AI Data Access. The AI sees nothing until you flip the switch.

Category What AI Sees What's Stripped Default
apps
Installed Apps
App names, versions, running state, URLs Config files, env vars, credentials OFF
system
System Stats
CPU %, RAM used/total, disk used/total, uptime File paths, IP addresses, hostnames, PIDs OFF
network
Network Status
Connected (bool), Tor active (bool), Tailscale active (bool) IP addresses, Tor .onion addresses, peer IPs, MAC addresses OFF
bitcoin
Bitcoin Node
Block height, sync %, chain, difficulty, mempool size/count Wallet keys, addresses, transaction history, RPC credentials OFF
wallet
Wallet Overview
Alias, channel count, peer count, balance (sats), sync status Private keys, seed phrases, macaroons, channel secrets, addresses OFF
media
Media Libraries
Which media apps are installed (Plex, Jellyfin, etc.) + status Library contents, file paths, metadata OFF
files
File Names
Folder names, recent file names, sizes, dates from Cloud File contents (unless read-file action is used with permission) OFF
notes
Documents
Document titles (currently returns "not available") Document contents OFF
search
Web Search
Whether SearXNG is installed + available N/A OFF
ai-local
Local AI
Whether Ollama is installed + running Model details OFF

Permissions stored in localStorage key: archipelago-ai-permissions

Store: neode-ui/src/stores/aiPermissions.ts

5 Layer 5: Data Sanitization

Each category has a dedicated sanitize function that extracts only whitelisted fields

The broker doesn't pass raw data through — it constructs new objects with only safe properties.

Example: Bitcoin sanitization

// 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

Example: Wallet sanitization

// 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

Example: Network sanitization

// 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

6 Layer 6: Proxy & Nginx Authentication

Claude API requests require a valid Archy session

Nginx rejects unauthenticated API calls. The Claude Proxy on port 3141 manages OAuth tokens securely.

# 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
}

The Claude Proxy (port 3141):

Content Security Policy (CSP):

Content-Security-Policy: default-src 'self';
  connect-src 'self' ws: wss:;
  frame-src 'self' http://127.0.0.1:* http://localhost:*;

The CSP restricts the AIUI iframe to only connect to the same origin and local addresses. No external fetch calls are possible.

7 The postMessage Protocol

AIUI and Archy communicate via a strictly-typed protocol defined in neode-ui/src/types/aiui-protocol.ts.

AIUI → Archy (Requests)

Message TypePurposeFields
readySignals iframe is loadedNone
context:requestRequest node dataid, category, query?
action:requestRequest an actionid, action, params
theme:requestRequest UI themeNone

Archy → AIUI (Responses)

Message TypePurposeFields
context:responseSanitized data or denialid, data, permitted (bool)
action:responseAction resultid, success, error?, data?
permissions:updatePush new permissionscategories[]
theme:responseTheme colorstheme { accent, mode }

8 What the AI System Prompt Sees

The buildArchyContext() function in AIUI constructs a context string that gets appended to Claude's system prompt. It only includes data for permitted categories:

// 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).

What's NOT in the system prompt — ever

9 What the AI Can NEVER See

Cryptographic Material

Network Identity

Credentials

Sensitive Data

10 Permitted Actions

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.

ActionWhat It DoesRequires Permission
open-appDispatches event to open an installed appNone (navigation)
navigateNavigate to a path within Archy UINone (navigation)
install-appInstalls an app from marketplaceNone
search-webSearches via local SearXNG instancesearch
read-fileReads a file from FileBrowser (Cloud)files
tail-logsGets recent log lines for an appapps

Actions the AI CANNOT perform

11 Current Bugs & Issues

"messages.6: user messages must have non-empty content" error

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.

Inconsistent node awareness

The AI sometimes says "I don't have access to your Bitcoin node" even though Bitcoin data may be permitted. This happens because:

12 Source File Reference

FileRole
neode-ui/src/services/contextBroker.tsThe quarantine gate — validates, checks permissions, sanitizes all data
neode-ui/src/types/aiui-protocol.tsStrict TypeScript protocol definition for all messages
neode-ui/src/stores/aiPermissions.tsPinia store for per-category permission toggles
neode-ui/src/views/Chat.vueIframe host with sandbox attribute
neode-ui/src/views/Settings.vueAI Data Access toggles UI
apps/aiui/manifest.ymlContainer security config (isolated network, readonly root)
image-recipe/configs/nginx-archipelago.confNginx routes with session cookie auth gate
AIUI/packages/app/src/services/archyBridge.tsAIUI-side postMessage client (the only way AIUI talks to Archy)
AIUI/packages/app/src/composables/useArchy.tsVue composable wrapping archyBridge + buildArchyContext()

Summary: 6 Layers of Defense

  1. Container — Podman with isolated network, read-only FS, zero capabilities
  2. Iframe sandbox — Browser-enforced isolation, no popups, no parent DOM access
  3. Context Broker — Single postMessage gate with origin validation
  4. Permissions — Per-category toggles, all OFF by default
  5. Sanitization — Dedicated functions strip sensitive fields per category
  6. Proxy auth — Nginx session cookie check + CSP headers

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.

Archipelago AI Quarantine Architecture — Generated 2026-03-06 — v1.0.0