15 KiB
Filesystem Access for Instance AI
ADR: ADR-025 (gateway protocol), ADR-027 (auto-connect UX) Status: Implemented — gateway-only architecture via
@n8n/computer-usedaemon Depends on: ADR-002 (interface boundary)
Problem
The instance AI builds workflows generically. When a user says "sync my users to HubSpot", the agent guesses the data shape. If it could read the user's actual code — API routes, schemas, configs — it would build workflows that fit the project precisely.
Architecture Overview
Filesystem access is provided exclusively through the gateway protocol —
a lightweight daemon (@n8n/computer-use) runs on the user's machine and
bridges file access to the n8n server via SSE.
┌─────────────────────────────────┐
│ AI Agent Tools │
│ (created from MCP server) │
└──────────────┬──────────────────┘
│ calls
┌──────────────▼──────────────────┐
│ LocalMcpServer │ ← interface boundary
│ (getAvailableTools, callTool) │
└──────────────┬──────────────────┘
│ implemented by
▼
LocalGateway
(@n8n/computer-use daemon)
The gateway protocol provides filesystem access via a lightweight daemon running on the user's machine.
The protocol is simple:
- Daemon connects to
GET /instance-ai/gateway/events(SSE) - Server publishes
filesystem-requestevents when the agent needs files - Daemon reads the file from local disk
- Daemon POSTs the result to
POST /instance-ai/gateway/response/:requestId
Agent calls readFile("src/index.ts")
→ LocalGateway publishes filesystem-request to SSE subscriber
→ Daemon receives event, reads file from disk
→ Daemon POSTs content to /instance-ai/gateway/response/:requestId
→ Gateway resolves pending Promise → tool gets FileContent back
The @n8n/computer-use CLI daemon is one implementation of this protocol. Any
application that speaks SSE + HTTP POST can serve as a gateway — a Mac app,
an Electron desktop app, a VS Code extension, or a mobile companion.
Authentication: Gateway endpoints use a shared API key
(N8N_INSTANCE_AI_GATEWAY_API_KEY) or a one-time pairing token that gets
upgraded to a session key on init (see Authentication below).
Service Interface
Defined in packages/@n8n/instance-ai/src/types.ts:
interface LocalMcpServer {
getAvailableTools(): McpTool[];
getToolsByCategory(category: string): McpTool[];
callTool(req: McpToolCallRequest): Promise<McpToolCallResult>;
}
The localMcpServer field in InstanceAiContext is optional — when no
gateway is connected, filesystem tools are not registered with the agent.
Tools
Tools are dynamically created from the MCP server's advertised capabilities
when a gateway is connected. See create-tools-from-mcp-server.ts.
Frontend UX (ADR-027)
The LocalGatewaySection component has 3 states:
| State | Condition | UI |
|---|---|---|
| Connected | isGatewayConnected |
Green indicator with connected host and capabilities |
| Connecting | isDaemonConnecting |
Spinner: "Connecting..." |
| Setup needed | Default | npx @n8n/computer-use command + copy button + waiting spinner |
Auto-connect flow
The user runs npx @n8n/computer-use and everything connects automatically. No
URLs, no tokens, no buttons.
sequenceDiagram
participant UI as Frontend (browser)
participant Daemon as computer-use daemon (localhost:7655)
participant Server as n8n Backend
UI->>Daemon: GET localhost:7655/health (polling every 5s)
Daemon-->>UI: 200 OK
UI->>Server: Request pairing token
Server-->>UI: One-time token (5-min TTL)
UI->>Daemon: POST localhost:7655/connect (token + server URL)
Daemon->>Server: SSE subscribe + upload directory tree
Server-->>Daemon: Session key (token consumed)
Server-->>UI: Push: gateway connected
Note over UI: UI → "Connected"
The browser mediates the pairing — it is the only component with network
access to both the local daemon (localhost:7655) and the n8n server. The
pairing token is ephemeral (5-min TTL, single-use), and once consumed, all
subsequent communication uses a session key.
Auto-connect by deployment scenario
Self-hosted (bare metal / Docker / Kubernetes)
Whether n8n runs on bare metal or inside a container, it cannot directly read files from the user's project directory. The gateway bridge is required.
sequenceDiagram
participant Browser as Browser (host)
participant Daemon as computer-use daemon (host:7655)
participant Server as n8n server (container)
Note over Browser,Daemon: 1. User starts daemon
Daemon->>Daemon: npx @n8n/computer-use (scans project dir)
Note over Browser,Daemon: 2. Browser detects daemon
Browser->>Daemon: GET localhost:7655/health (polling every 5s)
Daemon-->>Browser: 200 OK
Note over Browser,Server: 3. Pairing
Browser->>Server: Request pairing token
Server-->>Browser: One-time token (5-min TTL)
Browser->>Daemon: POST localhost:7655/connect (token + server URL)
Note over Daemon,Server: 4. Daemon connects to server
Daemon->>Server: SSE subscribe + upload directory tree
Server-->>Daemon: Session key (token consumed)
Server-->>Browser: Push: gateway connected
Note over Browser: UI → "Connected"
Why this works: the browser is the only component that can see both the
daemon (localhost:7655 on the host) and the n8n server (container network or
mapped port). It brokers the pairing between the two.
Cloud (n8n Cloud)
The flow is identical to the Docker/K8s path. The n8n server is remote, so the gateway bridge is required.
sequenceDiagram
participant Browser as Browser (user's machine)
participant Daemon as computer-use daemon (localhost:7655)
participant Cloud as n8n Cloud server
Browser->>Daemon: GET localhost:7655/health
Daemon-->>Browser: 200 OK
Browser->>Cloud: Request pairing token
Cloud-->>Browser: One-time token
Browser->>Daemon: POST localhost:7655/connect (token + cloud URL)
Daemon->>Cloud: SSE subscribe (outbound HTTPS)
Cloud-->>Daemon: Session key
Cloud-->>Browser: Push: gateway connected
Note over Browser: UI → "Connected"
Key difference from Docker self-hosted: the daemon connects outbound to the cloud server over standard HTTPS. No ports need to be exposed, no firewall rules — SSE is a regular outbound connection.
Deployment summary
| Deployment | Access path | Daemon needed? | User action |
|---|---|---|---|
| Self-hosted | Gateway bridge | Yes | npx @n8n/computer-use on host |
| n8n Cloud | Gateway bridge | Yes | npx @n8n/computer-use on local machine |
Alternatively, setting N8N_INSTANCE_AI_GATEWAY_API_KEY on both the n8n
server and the daemon skips the pairing flow entirely — useful for permanent
daemon setups or headless environments.
Filesystem toggle
The UI includes a toggle switch to temporarily disable filesystem access
without disconnecting the gateway. This calls POST /filesystem/toggle and
the agent stops receiving filesystem tools until re-enabled.
Gateway Protocol
The protocol has three phases:
sequenceDiagram
participant Client as Client (user's machine)
participant GW as Gateway (n8n server)
participant Agent as AI Agent
Note over Client,GW: Phase 1: Connect
Client->>GW: Subscribe via SSE
Client->>GW: Upload initial state (directory tree)
GW-->>Client: Session key
Note over Agent,Client: Phase 2: Serve requests
Agent->>GW: Operation request
GW-->>Client: SSE event with request ID + operation + args
Client->>Client: Execute locally
Client->>GW: POST response with request ID
GW-->>Agent: Result
Note over Client,GW: Phase 3: Disconnect
Client->>GW: Graceful disconnect
GW->>GW: Clean up pending requests
- SSE for push: the server publishes operation requests to the client as events
- HTTP POST for responses: the client posts results back, keyed by request ID
- Timeout per request: 30 seconds; pending requests are rejected on disconnect
- Keep-alive pings: every 15 seconds to detect stale connections
- Exponential backoff: auto-reconnect from 1s up to 30s max
Endpoint reference
| Step | Method | Path | Auth | Body |
|---|---|---|---|---|
| Connect | GET |
/instance-ai/gateway/events?apiKey=<token> |
API key query param | — (SSE stream) |
| Init | POST |
/instance-ai/gateway/init |
X-Gateway-Key header |
{ rootPath, tree: [{path, type, sizeBytes}], treeText } |
| Respond | POST |
/instance-ai/gateway/response/:requestId |
X-Gateway-Key header |
{ data } or { error } |
| Create link | POST |
/instance-ai/gateway/create-link |
Session auth (cookie) | — |
| Status | GET |
/instance-ai/gateway/status |
Session auth (cookie) | — |
| Disconnect | POST |
/instance-ai/gateway/disconnect |
X-Gateway-Key header |
— |
| Toggle FS | POST |
/instance-ai/filesystem/toggle |
Session auth (cookie) | — |
SSE event format
{
"type": "filesystem-request",
"payload": {
"requestId": "gw_abc123",
"operation": "read-file",
"args": { "filePath": "src/index.ts", "maxLines": 500 }
}
}
Operations: read-file and search-files. Tree/list operations are served
from the cached directory tree uploaded during init — no round-trip needed.
Authentication
Two options:
- Static: Set
N8N_INSTANCE_AI_GATEWAY_API_KEYenv var on the n8n server. The static key is used for all requests — no pairing/session upgrade. - Dynamic (pairing → session key):
POST /instance-ai/gateway/create-link(requires session auth) → returns{ token, command }. The token is a one-time pairing token (5-min TTL).- Daemon calls
POST /instance-ai/gateway/initwith the pairing token → server consumes the token and returns{ ok: true, sessionKey }. - All subsequent requests (SSE, response) use the session key instead of the consumed pairing token.
create-link → pairingToken (5 min TTL, single-use)
│
▼
gateway/init ──► consumed → sessionKey issued
│
▼
SSE + response use sessionKey
This prevents token replay: the pairing token is visible in terminal output
and ps aux, but it becomes useless after the first successful init call.
All key comparisons use timingSafeEqual() to prevent timing attacks.
Extending the Gateway: Building Custom Clients
The gateway protocol is client-agnostic — @n8n/computer-use is just one
implementation. Any application that speaks the protocol can serve as a
filesystem provider: a desktop app (Electron, Tauri), a VS Code extension,
a Go binary, a mobile companion, etc.
Any client that implements three interactions is a valid gateway client:
- Subscribe: open an SSE connection to receive operation requests
- Initialize: upload initial state (for filesystem: the directory tree)
- Respond: handle each request locally and POST the result back
What you do NOT need to change
- No agent changes — tools call the interface, not the transport
- No gateway changes —
LocalGatewayis protocol-level - No controller changes — endpoints are client-agnostic
- No frontend changes — unless you want auto-connect (see below)
Optional: auto-connect support
The frontend probes http://127.0.0.1:7655/health every 5s to auto-detect
a running daemon. To support this for a custom client:
- Listen on port 7655 (or any port, but 7655 gets auto-detected)
- Respond to
GET /healthwith200 OK - Accept
POST /connectwith{ url, token }— then use those to connect to the gateway endpoints above
If your client has its own auth/connection flow (e.g., a desktop app that talks to n8n directly), you can skip auto-connect entirely and call the gateway endpoints with your own token.
No changes are needed on the n8n server. The protocol, auth, and lifecycle are client-agnostic.
Security
| Layer | Protection |
|---|---|
| Read-only | No write methods on interface |
| File size | 512 KB max per read |
| Line limits | 200 default, 500 max per read |
| Binary detection | Null-byte check in first 8 KB |
| Directory containment | path.resolve() + fs.realpath() when basePath is set |
| Auth | Timing-safe key comparison (timingSafeEqual()) |
| Pairing token | One-time use, 5-min TTL, consumed on init |
| Session key | Server-issued, replaces pairing token after init |
| Request timeout | 30s per gateway round-trip |
| Keep-alive | 15s ping interval to detect stale connections |
Directory exclusions
The daemon excludes common non-essential directories from the tree scan:
node_modules, .git, dist, build, .next, .nuxt, __pycache__,
.cache, .turbo, coverage, .venv, venv, .idea, .vscode,
.output, .svelte-kit
Entry count caps
| Component | Max entries | Default depth |
|---|---|---|
| Tree scanner (daemon) | 10,000 | 8 |
The daemon scans broadly (10,000 entries, depth 8) because it uploads the full tree on init for cached queries.
Configuration
| Env var | Default | Purpose |
|---|---|---|
N8N_INSTANCE_AI_GATEWAY_API_KEY |
none | Static auth key for gateway (skips pairing flow) |
No env vars needed for most deployments. The browser auto-connects the daemon via the pairing flow.
See docs/configuration.md for the full configuration reference.
Package Structure
| Package | Responsibility |
|---|---|
@n8n/instance-ai |
Agent core: service interfaces, tool definitions, data shapes. Framework-agnostic, zero n8n dependencies. |
packages/cli/.../instance-ai/ |
n8n backend: HTTP endpoints, gateway singleton, event bus. |
@n8n/computer-use |
Reference gateway client: standalone CLI daemon. HTTP server, SSE client, local file reader, directory scanner. Independently installable via npx. |
Tree scanner behavior
The reference daemon (@n8n/computer-use) scans the user's project directory on
startup:
- Algorithm: Breadth-first, broad top-level coverage before descending into deeply nested paths
- Depth limit: 8 levels (default)
- Entry cap: 10,000
- Sort order: Directories first, then files, alphabetical within each group
- Excluded directories: node_modules, .git, dist, build, coverage, __pycache__, .venv, venv, .vscode, .idea, .next, .nuxt, .cache, .turbo, .output, .svelte-kit