# Archipelago AI Quarantine Architecture

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

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

- The AIUI container cannot make HTTP requests to the internet or to other containers
- It serves static files only — the actual Claude API calls happen in the browser, not the container
- Even with root access in the container, you can't escalate or modify the filesystem
- The container port (5180) is bound to `127.0.0.1`, so only nginx (on the same machine) can reach it

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

- Navigating the parent page — cannot redirect Archy
- Opening popups/new windows — `allow-popups` is NOT granted
- Accessing parent DOM — cross-origin isolation is enforced
- Submitting forms to external URLs — forms are scoped to same origin
- Running plugins — no plugin execution

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)

- Context Broker (host): Rejects any message where `event.origin !== this.allowedOrigin`
- archyBridge (AIUI): Rejects any message where `event.origin !== allowedOrigin`
- Responses use explicit target origin: `iframe.contentWindow.postMessage(msg, this.allowedOrigin)`

## 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):

- OAuth token stored securely (macOS keychain → `.env.local`)
- Auto-refreshes tokens 5 minutes before expiry
- Never exposes the token to the browser — the proxy adds auth headers server-side
- Only the browser's fetch to `/aiui/api/claude/` goes through this proxy

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 Type | Purpose | Fields
--- | --- | ---
`ready` | Signals iframe is loaded | None
`context:request` | Request node data | `id`, `category`, `query?`
`action:request` | Request an action | `id`, `action`, `params`
`theme:request` | Request UI theme | None

### Archy → AIUI (Responses)

Message Type | Purpose | Fields
--- | --- | ---
`context:response` | Sanitized data or denial | `id`, `data`, `permitted` (bool)
`action:response` | Action result | `id`, `success`, `error?`, `data?`
`permissions:update` | Push new permissions | `categories[]`
`theme:response` | Theme colors | `theme { 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

- Private keys, seed phrases, HD derivation paths
- Macaroons, auth tokens, API keys
- IP addresses (.onion, LAN, WAN, Tailscale)
- File contents, log contents
- SSH credentials, RPC passwords
- Transaction history, UTXO set, address lists
- Container configs, environment variables

## 9 What the AI Can NEVER See

#### Cryptographic Material

- Private keys (BTC, LN)
- Seed phrases / BIP39 mnemonics
- LND macaroons
- Channel backup data
- HD derivation paths

#### Network Identity

- IP addresses (LAN, WAN)
- Tor .onion addresses
- Tailscale IPs
- Peer connection details
- MAC addresses

#### Credentials

- SSH passwords / keys
- RPC usernames/passwords
- API tokens
- Session cookies
- OAuth tokens

#### Sensitive Data

- Transaction history
- Bitcoin addresses (receive/change)
- UTXO set
- File contents (unless explicitly permitted)
- Environment variables

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

Action | What It Does | Requires Permission
--- | --- | ---
`open-app` | Dispatches event to open an installed app | None (navigation)
`navigate` | Navigate to a path within Archy UI | None (navigation)
`install-app` | Installs an app from marketplace | None
`search-web` | Searches via local SearXNG instance | `search`
`read-file` | Reads a file from FileBrowser (Cloud) | `files`
`tail-logs` | Gets recent log lines for an app | `apps`

#### Actions the AI CANNOT perform

- Execute shell commands
- Call backend RPC endpoints directly
- Modify container configs
- Access the filesystem outside FileBrowser
- Send Bitcoin transactions
- Open/close Lightning channels
- Modify system settings
- Access other users' data

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

- The `bitcoin.getinfo` RPC call may fail (e.g., Bitcoin Knots RPC not configured in the backend)
- When the RPC fails, the broker returns a minimal fallback: `{ available: true, status: 'running', network: 'mainnet' }`
- The system prompt context then shows limited info, and Claude responds conservatively
- The `tail-logs` action could fetch Bitcoin logs, but Claude may not know to use it

## 12 Source File Reference

File | Role
--- | ---
`neode-ui/src/services/contextBroker.ts` | The quarantine gate — validates, checks permissions, sanitizes all data
`neode-ui/src/types/aiui-protocol.ts` | Strict TypeScript protocol definition for all messages
`neode-ui/src/stores/aiPermissions.ts` | Pinia store for per-category permission toggles
`neode-ui/src/views/Chat.vue` | Iframe host with sandbox attribute
`neode-ui/src/views/Settings.vue` | AI Data Access toggles UI
`apps/aiui/manifest.yml` | Container security config (isolated network, readonly root)
`image-recipe/configs/nginx-archipelago.conf` | Nginx routes with session cookie auth gate
`AIUI/packages/app/src/services/archyBridge.ts` | AIUI-side postMessage client (the only way AIUI talks to Archy)
`AIUI/packages/app/src/composables/useArchy.ts` | Vue 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