n8n/packages/@n8n/instance-ai/docs/filesystem-access.md

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-use daemon 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:

  1. Daemon connects to GET /instance-ai/gateway/events (SSE)
  2. Server publishes filesystem-request events when the agent needs files
  3. Daemon reads the file from local disk
  4. 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_KEY env var on the n8n server. The static key is used for all requests — no pairing/session upgrade.
  • Dynamic (pairing → session key):
    1. POST /instance-ai/gateway/create-link (requires session auth) → returns { token, command, expiresAt, ttlSeconds }. The token is a one-time pairing token (5-min TTL).
    2. Daemon calls POST /instance-ai/gateway/init with the pairing token → server consumes the token and returns { ok: true, sessionKey }.
    3. 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. The resulting session key has no time-based expiry and remains valid until explicit disconnect/revocation. 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:

  1. Subscribe: open an SSE connection to receive operation requests
  2. Initialize: upload initial state (for filesystem: the directory tree)
  3. 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 changesLocalGateway is 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:

  1. Listen on port 7655 (or any port, but 7655 gets auto-detected)
  2. Respond to GET /health with 200 OK
  3. Accept POST /connect with { 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