How AIUI (Claude) is sandboxed from your node's sensitive data — a defense-in-depth approach across 6 layers
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.
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:
127.0.0.1, so only nginx (on the same machine) can reach itThe 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:
allow-popups is NOT grantedThe only communication channel is window.postMessage(), which is intercepted by the Context Broker.
The ContextBroker class validates origin, checks permissions, fetches data, strips sensitive fields, then responds. AIUI never directly calls any backend API.
context:requestevent.origin === allowedOriginperms.isEnabled(category)// 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,
})
}
event.origin !== this.allowedOriginevent.origin !== allowedOriginiframe.contentWindow.postMessage(msg, this.allowedOrigin)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 |
|---|---|---|---|
appsInstalled Apps |
App names, versions, running state, URLs | Config files, env vars, credentials | OFF |
systemSystem Stats |
CPU %, RAM used/total, disk used/total, uptime | File paths, IP addresses, hostnames, PIDs | OFF |
networkNetwork Status |
Connected (bool), Tor active (bool), Tailscale active (bool) | IP addresses, Tor .onion addresses, peer IPs, MAC addresses | OFF |
bitcoinBitcoin Node |
Block height, sync %, chain, difficulty, mempool size/count | Wallet keys, addresses, transaction history, RPC credentials | OFF |
walletWallet Overview |
Alias, channel count, peer count, balance (sats), sync status | Private keys, seed phrases, macaroons, channel secrets, addresses | OFF |
mediaMedia Libraries |
Which media apps are installed (Plex, Jellyfin, etc.) + status | Library contents, file paths, metadata | OFF |
filesFile Names |
Folder names, recent file names, sizes, dates from Cloud | File contents (unless read-file action is used with permission) | OFF |
notesDocuments |
Document titles (currently returns "not available") | Document contents | OFF |
searchWeb Search |
Whether SearXNG is installed + available | N/A | OFF |
ai-localLocal AI |
Whether Ollama is installed + running | Model details | OFF |
Permissions stored in localStorage key: archipelago-ai-permissions
Store: neode-ui/src/stores/aiPermissions.ts
The broker doesn't pass raw data through — it constructs new objects with only safe properties.
// 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
// 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
// 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
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):
.env.local)/aiui/api/claude/ goes through this proxyContent 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.
AIUI and Archy communicate via a strictly-typed protocol defined in neode-ui/src/types/aiui-protocol.ts.
| 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 |
| 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 } |
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).
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 |
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.
The AI sometimes says "I don't have access to your Bitcoin node" even though Bitcoin data may be permitted. This happens because:
bitcoin.getinfo RPC call may fail (e.g., Bitcoin Knots RPC not configured in the backend){ available: true, status: 'running', network: 'mainnet' }tail-logs action could fetch Bitcoin logs, but Claude may not know to use it| 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() |
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