diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9a3bd1d8..6e8f56d2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Architecture -> **Nerve** is a web interface for OpenClaw — chat, voice input, TTS, and agent monitoring in the browser. It connects to the OpenClaw gateway over WebSocket and provides a rich UI for interacting with AI agents. +> **Nerve** is a local-first web interface for OpenClaw: chat, voice input, TTS, workspace tooling, and agent monitoring in the browser. It usually talks to the OpenClaw gateway over WebSocket, but some UI surfaces also depend on whether the Nerve server can reach the agent workspace on its own filesystem. ## System Diagram @@ -38,13 +38,50 @@ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────┬───────────────────────────────────┘ │ HTTP / WS - ┌──────────┴──────────┐ - │ OpenClaw Gateway │ - │ (ws://127.0.0.1: │ - │ 18789) │ - └─────────────────────┘ + ┌──────────┴───────────────┐ + │ OpenClaw Gateway │ + │ (default: │ + │ ws://127.0.0.1:18789, │ + │ but may be remote) │ + └──────────────────────────┘ ``` +## Locality model + +Nerve is easiest to reason about if you separate three boundaries: + +1. **Browser ↔ Nerve server** +2. **Nerve server ↔ OpenClaw gateway** +3. **Nerve server ↔ agent workspace filesystem** + +The biggest docs trap is assuming only the gateway matters. It does not. + +Full deployment-A behavior depends on **Nerve having local access to the same workspace filesystem as the gateway-backed agent**. That is true in deployment A, and usually also in same-host deployment C. It is **not** true in deployment B, or in split-host deployment C, unless you deliberately mount or replicate the workspace onto the Nerve host. + +### What each boundary changes + +| Boundary | Local case | Remote case | +|----------|------------|-------------| +| Browser ↔ Nerve | Loopback browser access is implicitly trusted for official gateway auto-connect flows | Remote browsers should use `NERVE_AUTH=true`. Trusted authenticated sessions can still get server-side gateway token injection for the official gateway URL | +| Nerve ↔ Gateway | Default path. Minimal config, loopback origin, simplest support surface | Requires `WS_ALLOWED_HOSTS`, correct gateway `controlUi.allowedOrigins`, and in some cases `NERVE_PUBLIC_ORIGIN` so server-side gateway RPC uses the real browser-facing origin | +| Nerve ↔ workspace filesystem | Full file browser, file mutations, raw previews, local watchers, and normal workspace UX | Falls back to gateway RPC only where specific routes support it. This is partial, not full parity | + +### Remote workspace fallback boundaries + +When the Nerve server cannot access the workspace directory locally, the app enters a partial fallback mode. + +| Surface | Local workspace | Remote workspace | +|---------|-----------------|------------------| +| Allowlisted config files (`SOUL.md`, `TOOLS.md`, `USER.md`, `AGENTS.md`, `HEARTBEAT.md`, `IDENTITY.md`) | Normal local read/write | Gateway fallback read/write for allowlisted top-level files only | +| File tree | Full nested tree | Top-level listing only | +| Text file read/write | Any allowed path in the workspace | Top-level text files only | +| Rename / move / trash / restore | Supported | `501 Not supported for remote workspaces` | +| Raw image / binary preview | Supported | Not available | +| Memory data | Full `MEMORY.md` plus recent daily files | Limited fallback for `MEMORY.md`; daily files are local-only, and the current UI treats remote workspaces as a constrained path rather than deployment-A parity | +| Skills tab | Local `openclaw skills list` scoped to the selected workspace | No dedicated gateway file fallback. Verify behavior in split-topology installs instead of assuming parity | + +This is why deployment B and split-host deployment C feel different from deployment A even when chat itself is healthy. + ## Frontend Structure Built with **React 19**, **TypeScript**, **Vite**, and **Tailwind CSS v4**. @@ -344,8 +381,8 @@ Applied in order in `app.ts`: | `/api/voice-phrases/:lang` | `routes/voice-phrases.ts` | GET, PUT | Read/save language-specific stop/cancel/wake phrase overrides | | `/api/agentlog` | `routes/agent-log.ts` | GET, POST | Agent activity log persistence. Zod-validated entries. Mutex-protected file I/O | | `/api/tokens` | `routes/tokens.ts` | GET | Token usage statistics — scans session transcripts, persists high water mark | -| `/api/memories` | `routes/memories.ts` | GET, POST, DELETE | Agent-scoped memory management — reads `MEMORY.md` + daily files, stores/deletes via gateway tool invocation | -| `/api/memories/section` | `routes/memories.ts` | GET, PUT | Read/replace a specific memory section by title, scoped via `agentId` | +| `/api/memories` | `routes/memories.ts` | GET, POST, DELETE | Agent-scoped memory management. Full local behavior reads `MEMORY.md` plus daily files. Remote fallback is limited to `MEMORY.md` because gateway file access does not cover subdirectories | +| `/api/memories/section` | `routes/memories.ts` | GET, PUT | Read/replace a specific memory section by title, scoped via `agentId`. Remote fallback applies to `MEMORY.md`, not daily files | | `/api/gateway/models` | `routes/gateway.ts` | GET | Config-backed model catalog from the active OpenClaw config. Returns `{ models, error, source: "config" }` | | `/api/gateway/session-info` | `routes/gateway.ts` | GET | Current session model/thinking level | | `/api/gateway/session-patch` | `routes/gateway.ts` | POST | HTTP fallback for model changes. Thinking changes belong on WS `sessions.patch` | @@ -356,23 +393,23 @@ Applied in order in `app.ts`: | `/api/gateway/restart` | `routes/gateway.ts` | POST | Restart the OpenClaw gateway service and verify readiness | | `/api/sessions/hidden` | `routes/sessions.ts` | GET | List hidden cron-like sessions from stored session metadata | | `/api/sessions/:id/model` | `routes/sessions.ts` | GET | Read the actual model used by a session from its transcript | -| `/api/workspace` | `routes/workspace.ts` | GET | List allowlisted workspace files for the selected agent workspace | -| `/api/workspace/:key` | `routes/workspace.ts` | GET, PUT | Read/write allowlisted workspace files (`soul`, `tools`, `identity`, `user`, `agents`, `heartbeat`) via `agentId` | +| `/api/workspace` | `routes/workspace.ts` | GET | List allowlisted workspace files for the selected agent workspace. Falls back to gateway file RPC when the workspace is not local | +| `/api/workspace/:key` | `routes/workspace.ts` | GET, PUT | Read/write allowlisted workspace files (`soul`, `tools`, `identity`, `user`, `agents`, `heartbeat`) via `agentId`. Remote fallback is top-level allowlisted files only | | `/api/crons` | `routes/crons.ts` | GET, POST, PATCH, DELETE | Cron job CRUD via gateway tool invocation | | `/api/crons/:id/toggle` | `routes/crons.ts` | POST | Toggle cron enabled/disabled | | `/api/crons/:id/run` | `routes/crons.ts` | POST | Run cron job immediately | | `/api/crons/:id/runs` | `routes/crons.ts` | GET | Cron run history | -| `/api/skills` | `routes/skills.ts` | GET | List skills for the selected agent workspace via a scoped OpenClaw config | +| `/api/skills` | `routes/skills.ts` | GET | List skills for the selected agent workspace via a scoped local OpenClaw config. This route does not use the remote file-browser fallback path | | `/api/keys` | `routes/api-keys.ts` | GET, PUT | Read API-key presence and persist updated key values to `.env` | | `/api/files` | `routes/files.ts` | GET | Serve local image files (MIME-type restricted, directory traversal blocked) | -| `/api/files/tree` | `routes/file-browser.ts` | GET | Agent-scoped workspace directory tree (excludes node_modules, .git, etc.) | -| `/api/files/read` | `routes/file-browser.ts` | GET | Read scoped file contents with mtime for conflict detection | -| `/api/files/write` | `routes/file-browser.ts` | PUT | Write scoped file contents with optimistic concurrency (409 on conflict) | -| `/api/files/rename` | `routes/file-browser.ts` | POST | Rename a file or directory within the selected workspace | -| `/api/files/move` | `routes/file-browser.ts` | POST | Move a file or directory within the selected workspace | -| `/api/files/trash` | `routes/file-browser.ts` | POST | Trash a file or directory, or permanently delete when using `FILE_BROWSER_ROOT` | -| `/api/files/restore` | `routes/file-browser.ts` | POST | Restore a trashed file or directory | -| `/api/files/raw` | `routes/file-browser.ts` | GET | Serve scoped image previews from the selected workspace | +| `/api/files/tree` | `routes/file-browser.ts` | GET | Agent-scoped workspace directory tree. Full nested tree locally, top-level-only fallback remotely | +| `/api/files/read` | `routes/file-browser.ts` | GET | Read scoped file contents with mtime for conflict detection. Remote fallback is top-level text files only | +| `/api/files/write` | `routes/file-browser.ts` | PUT | Write scoped file contents with optimistic concurrency (409 on conflict). Remote fallback is top-level text files only | +| `/api/files/rename` | `routes/file-browser.ts` | POST | Rename a file or directory within the selected workspace. Local workspaces only | +| `/api/files/move` | `routes/file-browser.ts` | POST | Move a file or directory within the selected workspace. Local workspaces only | +| `/api/files/trash` | `routes/file-browser.ts` | POST | Trash a file or directory, or permanently delete when using `FILE_BROWSER_ROOT`. Local workspaces only | +| `/api/files/restore` | `routes/file-browser.ts` | POST | Restore a trashed file or directory. Local workspaces only | +| `/api/files/raw` | `routes/file-browser.ts` | GET | Serve scoped image previews from the selected workspace. No remote fallback | | `/api/claude-code-limits` | `routes/claude-code-limits.ts` | GET | Claude Code rate limits via PTY + CLI parsing | | `/api/codex-limits` | `routes/codex-limits.ts` | GET | Codex rate limits via OpenAI API with local file fallback | | `/api/kanban/tasks` | `routes/kanban.ts` | GET, POST | Task CRUD -- list (with filters/pagination) and create | @@ -453,7 +490,7 @@ Browser WS → /ws?target=ws://gateway:18789/ws → ws-proxy.ts → OpenClaw Gat 1. Client connects to `/ws` endpoint on Nerve server 2. When auth is enabled, the session cookie is verified on the HTTP upgrade request (rejects with 401 if invalid) -3. Proxy validates target URL against `WS_ALLOWED_HOSTS` allowlist +3. Proxy validates target URL against `WS_ALLOWED_HOSTS` allowlist. This matters in hybrid and split-host deployments where the gateway is not on loopback 4. Proxy opens upstream WebSocket to the gateway 5. On `connect.challenge` event, proxy intercepts the client's `connect` request and injects Ed25519 device identity (`device` block with signed nonce) 6. If the gateway rejects the device (close code 1008), proxy retries without device identity (reduced scopes) @@ -701,9 +738,9 @@ npm test # Run all tests npm run test:coverage # With V8 coverage ``` -### Test Files +### Test Coverage Areas -48 test files, 692 tests. Key areas: +The Vitest suite spans server code, client features, hooks, and shared utilities. Representative areas: | Area | Files | Coverage | |------|-------|----------| diff --git a/docs/DEPLOYMENT-B.md b/docs/DEPLOYMENT-B.md index 8f476aa5..10e3ed52 100644 --- a/docs/DEPLOYMENT-B.md +++ b/docs/DEPLOYMENT-B.md @@ -1,29 +1,62 @@ # Deployment: Remote Gateway + Local Nerve -Nerve runs on your laptop, Gateway runs on a cloud host. Good when you want local UI responsiveness but your OpenClaw runtime lives in the cloud. +Nerve runs on your laptop, while the OpenClaw gateway runs somewhere else. + +This gives you a fast local UI, but it is **not** full deployment-A parity. The missing piece is workspace locality: the agent workspaces live on the remote gateway host, not on the Nerve host. ## Topology +```text +Browser (localhost) → Nerve local (127.0.0.1:3080) → Gateway remote (:18789) ``` -Browser (localhost) → Nerve local (127.0.0.1:3080) → Gateway cloud (:18789) -``` + +## Reality check + +This topology splits three things: + +- **browser ↔ Nerve** is local +- **Nerve ↔ gateway** is remote +- **Nerve ↔ workspace filesystem** is remote + +Chat, sessions, cron, and Kanban can work well here. + +Workspace-heavy features do **not** have full parity, because Nerve cannot directly walk or mutate the remote filesystem. Some routes fall back to gateway file RPC, but that fallback is intentionally narrow. + +## What works, what degrades + +| Surface | Status | Notes | +|---|---|---| +| Chat, session list, live agent status | Full | Requires the WebSocket proxy, gateway origins, and device identity path to be configured correctly | +| Official gateway auto-connect without manual token entry | Usually full | Works well on the normal localhost browser path. Custom gateway URLs or untrusted paths still require manual token entry | +| Allowlisted top-level workspace files (`SOUL.md`, `TOOLS.md`, `USER.md`, `AGENTS.md`, `HEARTBEAT.md`, `IDENTITY.md`) | Partial | Read/write fallback exists through gateway file RPC | +| File browser | Partial | Top-level listing plus top-level text read/write only | +| Nested directories | Not supported | No remote tree walk through subdirectories | +| Rename / move / trash / restore | Not supported | Remote workspace routes return `501 Not supported for remote workspaces` | +| Raw image / binary preview | Not supported | No remote raw-file fallback | +| Memory | Limited | `MEMORY.md` has some backend fallback, but daily files are local-only and the current UI treats remote workspaces as constrained, not as deployment-A parity | +| Crons | Full with extra config | Gateway must allow `cron`, `gateway`, and `sessions_spawn` on `/tools/invoke` | +| Kanban execution | Full with extra config | Same allowlist requirement as crons. Assignee execution still depends on the remote gateway having the right sessions available | +| Skills tab | Verify in your environment | Uses local `openclaw skills list`, not the remote file-browser fallback path | + +If you need full Files, Memory, raw previews, and file mutation behavior, move Nerve onto the same machine as the gateway and workspace, or use same-host cloud deployment instead. ## Prerequisites - Nerve installed on your laptop -- Cloud Gateway reachable from your laptop -- Gateway token from the cloud host -- Access to cloud host config (`~/.openclaw/openclaw.json`) +- OpenClaw gateway running on the remote host +- A private network path to the gateway host (Tailscale, WireGuard, SSH tunnel, private VPC, etc.) +- Gateway token from the remote host +- Access to the remote host's OpenClaw config (`~/.openclaw/openclaw.json`) ## Recommended network approach -Use a private network path (Tailscale, WireGuard, SSH tunnel, or private VPC). Avoid exposing port `18789` publicly. +Use a private network path. Do **not** expose gateway port `18789` publicly unless you have a very specific reason. ## Setup -### 1. Prepare cloud gateway +### 1. Prepare the remote gateway -On the cloud host: +On the remote host: ```bash openclaw gateway status @@ -32,38 +65,55 @@ curl -sS http://127.0.0.1:18789/health ### 2. Configure Nerve locally +If you are installing fresh, either run the installer and then the setup wizard, or point the installer at the remote gateway up front. + ```bash cd ~/nerve npm run setup ``` When prompted: -- Set **Gateway URL** to your cloud gateway URL -- Set **Gateway token** from cloud host -- Keep access mode as **localhost** unless you need LAN access -### 3. Allow gateway host in WS proxy +- set **Gateway URL** to the remote gateway URL +- set **Gateway token** from the remote host +- keep access mode as **localhost** unless you intentionally want LAN / Tailscale access to the Nerve UI itself -If your gateway hostname isn't localhost, add it to `.env`: +### 3. Allow the remote gateway host in the WS proxy + +Add the gateway hostname or IP to `.env` on the Nerve host: ```bash WS_ALLOWED_HOSTS= ``` -Restart Nerve after. +Restart Nerve after changing it. + +### 4. Allow the Nerve origin on the remote gateway -### 4. Patch remote gateway allowed origins +On the remote gateway host, add the browser-facing Nerve origin to `gateway.controlUi.allowedOrigins` in `~/.openclaw/openclaw.json`. -On the cloud host, add your local Nerve origin to the gateway allowlist in `~/.openclaw/openclaw.json`: +For the normal localhost path, add both: - `http://localhost:3080` - `http://127.0.0.1:3080` -Restart the gateway. +Then restart the gateway. -### 5. Optional: allow required gateway tools +### 5. If Nerve is not being accessed via localhost, set `NERVE_PUBLIC_ORIGIN` -On the cloud host config: +If the browser reaches Nerve through anything other than localhost, set the exact browser origin in `.env` on the Nerve host: + +```bash +NERVE_PUBLIC_ORIGIN=https://nerve.example.com +``` + +Add that same origin to `gateway.controlUi.allowedOrigins` on the remote gateway. + +Why this matters: some workspace fallback paths open their own server-side WebSocket to the gateway. Those paths need the real browser-facing origin, not an invented loopback default. + +### 6. Ensure the gateway tool allowlist is complete + +On the remote gateway host, `gateway.tools.allow` must include: ```json "gateway": { @@ -73,38 +123,54 @@ On the cloud host config: } ``` +Restart the gateway after updating it. + ## Validation ```bash -# On laptop +# On the Nerve host curl -sS http://127.0.0.1:3080/health -# Connectivity to cloud gateway +# Connectivity to the remote gateway curl -sS /health ``` -In the browser: connect succeeds, session list loads, messages send/receive. +In the browser, verify these separately: + +1. connect succeeds +2. session list loads +3. messages send and receive +4. Crons and Kanban load without `Tool not available` errors +5. the file browser only shows top-level remote files, which is expected in this topology ## Common issues -### "Target not allowed" WebSocket error +### `Target not allowed` on WebSocket connect + +The remote gateway host is missing from `WS_ALLOWED_HOSTS`. + +**Fix:** add the hostname or IP to `WS_ALLOWED_HOSTS`, then restart Nerve. -The gateway hostname isn't in `WS_ALLOWED_HOSTS`. +### Chat works, but Files / Config / workspace-adjacent panels fail with `origin not allowed` -**Fix:** Add the hostname to `WS_ALLOWED_HOSTS` in `.env`. +The browser-facing Nerve origin is missing from `gateway.controlUi.allowedOrigins`, or `NERVE_PUBLIC_ORIGIN` is not set correctly for a non-localhost access path. -### Scope or pairing errors +**Fix:** set `NERVE_PUBLIC_ORIGIN` to the exact browser origin and add that same origin to the gateway allowlist. -Connection works but actions fail with scope errors. +### Cron or Kanban says a tool is unavailable -**Fix:** Repair pairing/scopes on the gateway host. Re-run setup flows on the gateway host itself. +The remote gateway is missing required HTTP tool allowlist entries. -## Security notes +**Fix:** add `cron`, `gateway`, and `sessions_spawn` to `gateway.tools.allow`, then restart the gateway. -- Use private addressing and strict firewall rules -- Rotate gateway token if it's been shared -- If you expose local Nerve to LAN, enable `NERVE_AUTH=true` +### The file browser looks broken because directories are missing + +That is expected in this topology. + +Remote workspace fallback is top-level only. Nested directory browsing, file moves, trash/restore, and raw previews are not available unless Nerve can access the workspace locally. ## Recommendation -This works today but has manual steps. If you want low maintenance and multi-device access, consider [cloud deployment](DEPLOYMENT-C.md) instead. +Choose this topology when you want a **local UI with a remote runtime** and you can live with partial workspace tooling. + +If you want Nerve to behave like deployment A, move Nerve onto the same host as the gateway and workspace, or use same-host deployment C. diff --git a/docs/DEPLOYMENT-C.md b/docs/DEPLOYMENT-C.md index 53b49619..d366d25e 100644 --- a/docs/DEPLOYMENT-C.md +++ b/docs/DEPLOYMENT-C.md @@ -1,29 +1,39 @@ # Deployment: Cloud (Remote Access) -Both Nerve and Gateway hosted remotely. Access from any device, anywhere. +This guide covers two very different cloud topologies. + +If you only remember one thing, remember this: + +> **Remote browser access does not automatically mean limited Nerve. Split-host workspace locality does.** + +Same-host cloud deployment can preserve near-full deployment-A behavior. Split-host cloud deployment cannot, unless the Nerve host also has direct access to the same workspace filesystem. ## Topology options -### Same host (recommended) +### Same host, recommended -``` +```text Browser (remote) → Nerve cloud → Gateway cloud (same machine) ``` -### Split hosts +This is the cloud topology with the best feature parity. Nerve, the gateway, and the workspace live together. -``` -Browser (remote) → Nerve (host A) → Gateway (host B) +### Split hosts, partial parity + +```text +Browser (remote) → Nerve host A → Gateway host B ``` -Same-host is simpler and has fewer failure points. Use split hosts only if you have a specific reason. +This behaves much more like deployment B. Chat can still work well, but workspace-heavy features degrade unless host A also has the workspace mounted locally. -## Prerequisites +## Choose based on what you need -- Cloud Linux host with Node.js 22+ -- OpenClaw gateway running -- Domain or stable IP for Nerve -- TLS termination plan (reverse proxy or direct certs) +| Cloud mode | Nerve ↔ gateway | Nerve ↔ workspace | Result | +|---|---|---|---| +| Same host | Local | Local | Best cloud experience. Closest to deployment A | +| Split hosts | Remote | Usually remote | Partial parity only. Inherits deployment-B style limitations | + +If you care about full Files, Memory, Config, raw previews, and normal file operations, choose **same host**. ## Same-host setup @@ -33,7 +43,7 @@ Same-host is simpler and has fewer failure points. Use split hosts only if you h curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh | bash ``` -### 2. Run setup with network access +### 2. Run setup for remote browser access ```bash cd ~/nerve @@ -41,10 +51,11 @@ npm run setup ``` Recommended choices: + - Access mode: **Network** or **Custom** - `HOST=0.0.0.0` -- **Enable authentication** and set a password -- Enable HTTPS if serving directly +- **Enable authentication** +- use HTTPS directly or put Nerve behind a reverse proxy with TLS ### 3. Start the service @@ -55,49 +66,90 @@ sudo systemctl status nerve.service ### 4. Set up TLS -Put Nerve behind a reverse proxy (Nginx, Caddy, or Traefik) that handles HTTPS and forwards HTTP + WebSocket traffic to Nerve. +Put Nerve behind a reverse proxy such as Nginx, Caddy, or Traefik, or serve HTTPS directly with local certs. + +If you terminate TLS in a reverse proxy, also set `TRUSTED_PROXIES` in `.env` so rate limiting and client-IP resolution use the real client address instead of the proxy hop. -Or generate certs directly: +### 5. Keep the gateway local to the host + +On same-host installs, keep the gateway on loopback if possible: ```bash -mkdir -p certs -openssl req -x509 -newkey rsa:2048 -nodes \ - -keyout certs/key.pem -out certs/cert.pem -days 365 \ - -subj "/CN=your-domain.com" +GATEWAY_URL=http://127.0.0.1:18789 ``` -Nerve auto-detects certificates at `certs/cert.pem` and `certs/key.pem`. +This is the simplest and safest cloud path. + +## Same-host behavior + +This is the important part: remote browser access changes the **trust and auth model**, not the workspace model. + +### What changes from deployment A + +- the browser is no longer loopback, so **auth should be on** +- the browser should reach Nerve over HTTPS +- if you use a reverse proxy, set `TRUSTED_PROXIES` correctly +- server-side gateway token injection depends on the authenticated Nerve session for remote clients + +### What does **not** need to degrade + +Because Nerve and the gateway share the same host and workspace, these can still have normal deployment-A style behavior: + +- full file browser +- nested directories +- rename / move / trash / restore +- raw image previews +- normal workspace config editing +- full memory parsing from local `MEMORY.md` plus daily files +- local watcher-based workspace updates + +In other words: **same-host deployment C is the recommended remote-access topology if you want real Nerve, not a reduced control panel.** ## Split-host setup -Follow the same-host steps for Nerve, then add: +Use this only when you have a specific infrastructure reason to separate Nerve and the gateway. -### Install with remote gateway settings up front +### 1. Install Nerve with remote gateway settings ```bash curl -fsSL https://raw.githubusercontent.com/daggerhashimoto/openclaw-nerve/master/install.sh \ | bash -s -- --gateway-url https://gw.example.com --gateway-token --skip-setup ``` -### Point Nerve to remote gateway +Then: + +```bash +cd ~/nerve +npm run setup +``` + +Recommended choices: + +- Access mode: **Network** or **Custom** +- **Enable authentication** +- configure TLS or a reverse proxy + +### 2. Point Nerve at the remote gateway -In `.env`: +In `.env` on the Nerve host: ```bash -GATEWAY_URL= -WS_ALLOWED_HOSTS= +GATEWAY_URL=https://gw.example.com +WS_ALLOWED_HOSTS=gw.example.com NERVE_PUBLIC_ORIGIN=https://nerve.example.com ``` -### Patch remote gateway allowed origins +### 3. Allow the public Nerve origin on the gateway host -On the gateway host, add Nerve's public origin to `gateway.controlUi.allowedOrigins`: +On the gateway host, add the Nerve origin to `gateway.controlUi.allowedOrigins`: -``` +```text https://nerve.example.com ``` -### Ensure gateway tools allowlist +### 4. Ensure the gateway HTTP tool allowlist is complete + +On the gateway host: ```json "gateway": { @@ -107,41 +159,91 @@ https://nerve.example.com } ``` -Restart both services. +Restart both services after making the changes. + +## Split-host behavior + +This is **not** the same as same-host cloud deployment. + +Because the Nerve host usually cannot reach the gateway host's workspace filesystem directly, split-host deployment inherits the same core limits as deployment B. + +### What still works well + +- chat +- session list and live session state +- cron management, if the gateway tool allowlist is correct +- Kanban execution, if the gateway tool allowlist is correct +- top-level allowlisted workspace-file fallback (`SOUL.md`, `TOOLS.md`, etc.) + +### What becomes partial or unavailable + +- file browser becomes top-level only +- nested directories are unavailable +- rename / move / trash / restore are unavailable +- raw image / binary previews are unavailable +- memory behavior is limited compared with same-host and deployment A +- some workspace-adjacent flows depend on the exact public origin being configured correctly on both sides + +If you need these features, do one of the following instead: + +1. move Nerve onto the same host as the gateway +2. mount the same workspace filesystem onto the Nerve host +3. stop using split-host and use deployment A or same-host deployment C ## Validation +### Same host + ```bash -# Nerve host curl -sS http://127.0.0.1:3080/health +curl -sS https:///health +``` + +Verify in the browser: + +1. login appears and succeeds +2. connect succeeds without manual token entry on the normal official-gateway path +3. file browser, memory, config, and raw previews all work normally +4. Crons and Kanban load without tool-availability errors -# Public endpoint -curl -sS https:///health +### Split hosts + +```bash +curl -sS http://127.0.0.1:3080/health +curl -sS https:///health +curl -sS https:///health ``` -In the browser: login screen appears, connect succeeds, sessions load, messages work. +Verify in the browser: + +1. login succeeds +2. connect succeeds +3. Crons and Kanban load +4. workspace file access is limited in the ways documented above, which is expected in this topology ## Common issues -### Remote clients may still need manual credentials +### Remote clients still see the token field in the connect dialog + +This can still happen when: -Remote clients can still auto-connect when Nerve trusts the request and the browser is using the official gateway URL. In that case `/api/connect-defaults` reports `serverSideAuth=true`, the browser sends an empty token, and Nerve injects `GATEWAY_TOKEN` server-side during the WebSocket handshake. +- the browser is pointed at a custom gateway URL instead of the official Nerve-managed one +- the request path is not trusted for server-side token injection +- stale browser config is overriding the official URL path -Manual token entry is only required for custom gateway URLs or untrusted access paths. +### Chat works, but workspace-adjacent panels fail with `origin not allowed` -### Reverse proxy and trusted proxy settings +This is usually an origin mismatch between Nerve's public URL and `gateway.controlUi.allowedOrigins` on the gateway host. -Wrong IP detection affects rate limiting and logs. +**Fix:** set `NERVE_PUBLIC_ORIGIN` to the exact public Nerve origin and add that same origin to the gateway allowlist. -**Fix:** Set `TRUSTED_PROXIES` in `.env` to your reverse proxy addresses. +### Split-host install feels weaker than expected -## Security notes +That is not your imagination. -- **Always** enable `NERVE_AUTH=true` for remote access -- Use HTTPS end-to-end or at least at the edge -- Keep the gateway on loopback when Nerve and Gateway share a host -- Rotate gateway token on access changes +Split-host cloud deployment loses local workspace access unless you provide it yourself. If you want full Nerve behavior, use same-host cloud deployment. ## Recommendation -Choose same-host unless you have a hard requirement for split hosts. It's easier to secure and support. +- **Want the best remote-access experience?** Use **same-host deployment C**. +- **Want a split topology anyway?** Accept deployment-B style workspace limits, or provide your own shared filesystem.