docs(FN-2524): add remote access operator runbook

- Add docs/remote-access.md as the canonical runbook for dashboard, TUI, and headless remote access operations
- Document provider prerequisites, lifecycle restore gates, and status/diagnostic semantics for remote tunnel management
- Add remote auth security guidance for tokenized URLs/QR payloads and include troubleshooting-oriented API flow references
- Cross-link the runbook from docs index and related architecture, CLI, dashboard, getting-started, and settings docs
This commit is contained in:
Fusion
2026-04-26 05:38:18 -07:00
committed by gsxdsm
parent dd81a12e77
commit 6da077237c
7 changed files with 334 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
| [Getting Started](./getting-started.md) | Installation, first-run, first task, and daily workflow basics |
| [Dashboard Guide](./dashboard-guide.md) | Board/list views, terminal, git manager, files, planning, and UI tools |
| [CLI Reference](./cli-reference.md) | Complete `fn` command reference with subcommands, flags, and examples |
| [Remote Access](./remote-access.md) | Operator runbook for Tailscale/Cloudflare setup, tokenized login links, security caveats, and troubleshooting |
### Task & Project Management
| Guide | Description |

View File

@@ -351,6 +351,8 @@ Implemented in `agent-heartbeat.ts`:
- `PeerExchangeService` (`peer-exchange-service.ts`) — peer sync orchestration
### Remote access runtime
Operator setup + troubleshooting guide: **[Remote Access runbook](./remote-access.md)**.
- `remote-access/tunnel-process-manager.ts` owns tunnel lifecycle orchestration with `spawn`-based, non-blocking process supervision.
- `remote-access/types.ts` defines the runtime contract used by downstream API/TUI/headless layers:
- Providers: `"tailscale" | "cloudflare"`
@@ -392,6 +394,7 @@ Implemented in `agent-heartbeat.ts`:
Key server capabilities:
- REST APIs for tasks, git, GitHub, agents, missions, planning, automations/routines, settings
- Remote access APIs (`/api/remote/*`) for provider config, activation, tunnel lifecycle, status, token issuance, authenticated URL generation, and QR payload generation
- Operational runbook (prereqs/security/troubleshooting): [`docs/remote-access.md`](./remote-access.md)
- `/api/remote/tunnel/start` and `/api/remote/tunnel/stop` are the only lifecycle transition endpoints.
- `/api/remote/status` includes tunnel status plus restore diagnostics (`restore.outcome` + `restore.reason`) with parity between dashboard and headless `fn serve` runtimes.
- Remote auth handoff endpoints:

View File

@@ -87,6 +87,8 @@ Remote actions support:
- Short-lived token generation with TTL input and expiry display
- URL + QR hand-off (always shows full authenticated URL)
> ⚠️ Remote URL/QR payloads include tokenized query data. Treat them like credentials and avoid sharing them in screenshots/chat/logs. Prefer short-lived links for ad-hoc phone login.
Remote action keys in Settings detail pane:
- `C` activate selected provider
- `V` start tunnel
@@ -231,6 +233,20 @@ When remote access is enabled/configured, the headless server exposes `/api/remo
control/status endpoints and applies the same hybrid token validation rules for
remote routes (persistent token + optional short-lived token registry).
Headless operators should use the same lifecycle/API flow as dashboard mode:
- `POST /api/remote/provider/activate`
- `POST /api/remote/tunnel/start`
- `POST /api/remote/tunnel/stop`
- `GET /api/remote/status`
- `POST /api/remote-access/auth/login-url`
`GET /remote-login?rt=<token>` is intentionally public for phone-link handoff,
but token validity is still enforced server-side.
For end-to-end setup, risk guidance, and troubleshooting, see
**[docs/remote-access.md](./remote-access.md)**.
For programmatic consumers, these endpoints map to the engine tunnel manager contract:
- `getStatus()` for current snapshot
- `start(provider, config)` / `stop()` / `switchProvider(...)`

View File

@@ -161,6 +161,19 @@ Click the chevron next to the status indicator to open the node selector dropdow
The selected node persists across browser sessions via localStorage. If the selected remote node is unregistered, the dashboard automatically falls back to local mode.
## Remote Access (Settings)
Dashboard remote controls live in **Settings → Remote Access**.
From this section, operators can:
- Configure Tailscale and Cloudflare provider fields
- Activate the current provider
- Start/stop tunnel lifecycle manually
- Generate login URLs / QR payloads using persistent or short-lived token mode
For setup prerequisites, security caveats for tokenized URLs/QR links, and troubleshooting, use the canonical **[Remote Access runbook](./remote-access.md)**.
## Skills API
The Skills API provides endpoints for managing execution skills. Skills are toggled via project-scoped settings in `.fusion/settings.json`.

View File

@@ -181,6 +181,8 @@ fn daemon --port 5050 # daemon mode with token auth support
fn desktop # launch Electron desktop app
```
If you plan to expose Fusion remotely, read the **[Remote Access runbook](./remote-access.md)** before enabling tunnels. It covers provider prerequisites, tokenized login-link security caveats, and operational troubleshooting.
## Create Your First Task
You can create tasks from the board or CLI.

297
docs/remote-access.md Normal file
View File

@@ -0,0 +1,297 @@
# Remote Access Runbook
[← Docs index](./README.md)
This runbook is the canonical operator reference for Fusion Remote Access across:
- Dashboard (`fn dashboard`)
- Interactive TUI (`fn dashboard` in TTY mode)
- Headless node mode (`fn serve`)
It documents only behavior implemented in the current codebase:
- Project-scoped `remoteAccess` settings in `.fusion/config.json`
- API endpoints under `/api/remote/*` and `/api/remote-access/auth/login-url`
- Public login handoff route `GET /remote-login?rt=...`
- Engine tunnel lifecycle and safe restore diagnostics
---
## 1) Prerequisites
## 1.1 General requirements
- Remote Access is **project-scoped** (`ProjectSettings.remoteAccess` in `packages/core/src/types.ts`).
- Configure it in dashboard settings (Remote tab) or via `PUT /api/settings`.
- A provider must be selected (`remoteAccess.activeProvider`) before tunnel start.
- Start/stop is always manual through `/api/remote/tunnel/start` and `/api/remote/tunnel/stop`.
## 1.2 Tailscale Serve prerequisites
Tailscale provider startup gates:
- `remoteAccess.providers.tailscale.enabled = true`
- `remoteAccess.providers.tailscale.hostname` is non-empty
- `remoteAccess.providers.tailscale.targetPort` is a positive number
- `tailscale` executable is available on `PATH`
Runtime command used by engine:
- `tailscale funnel <targetPort>`
Operational notes:
- Fusion validates executable availability (`which tailscale` / `where tailscale`) before start.
- If prerequisites are missing, start returns a prerequisite/config error (HTTP 409 from API start route).
## 1.3 Cloudflare Tunnel prerequisites
Cloudflare provider startup gates:
- `remoteAccess.providers.cloudflare.enabled = true`
- `remoteAccess.providers.cloudflare.tunnelName` is non-empty
- `remoteAccess.providers.cloudflare.ingressUrl` is non-empty (must parse as `http://` or `https://` for login URL generation)
- `remoteAccess.providers.cloudflare.tunnelToken` is non-empty
- `cloudflared` executable is available on `PATH`
Runtime command used by engine:
- `cloudflared tunnel --no-autoupdate run <tunnelName>`
- Token is passed via env (`TUNNEL_TOKEN`), not as a plain CLI argument.
---
## 2) Configuration and provider operations
## 2.1 Configure both providers (recommended)
You can persist both provider configs and switch between them without rewriting settings.
Minimal `remoteAccess` shape (redacted placeholders):
```json
{
"remoteAccess": {
"enabled": true,
"activeProvider": "tailscale",
"providers": {
"tailscale": {
"enabled": true,
"hostname": "<host>",
"targetPort": 4040,
"acceptRoutes": false
},
"cloudflare": {
"enabled": true,
"tunnelName": "<tunnel-name>",
"tunnelToken": "<token>",
"ingressUrl": "https://<host>"
}
},
"tokenStrategy": {
"persistent": {
"enabled": true,
"token": null
},
"shortLived": {
"enabled": true,
"ttlMs": 900000,
"maxTtlMs": 86400000
}
},
"lifecycle": {
"rememberLastRunning": false,
"wasRunningOnShutdown": false,
"lastRunningProvider": null
}
}
}
```
## 2.2 Switch active provider
Switch provider (settings-level):
- `POST /api/remote/provider/activate` with `{ "provider": "tailscale" | "cloudflare" }`
Important behavior:
- Provider activation updates selected provider only.
- Activation does **not** start the tunnel by itself.
## 2.3 Start and stop tunnel manually
- Start: `POST /api/remote/tunnel/start`
- Stop: `POST /api/remote/tunnel/stop`
- Status: `GET /api/remote/status`
Returned status fields include:
- `state`: `stopped | starting | running | stopping | failed`
- `provider`, `url`, `lastError`, `lastErrorCode`
- `restore` diagnostics block (`outcome`, `reason`, `at`, `provider`, optional `message`)
---
## 3) Restore-on-restart semantics (remember last running)
Fusion attempts restore during engine startup only when all gates pass:
1. `remoteAccess.enabled = true`
2. `remoteAccess.lifecycle.rememberLastRunning = true`
3. `remoteAccess.lifecycle.wasRunningOnShutdown = true`
4. A valid provider is available (`lastRunningProvider` or `activeProvider`)
5. Provider config is complete and runtime prerequisites (binary on `PATH`) are satisfied
If any gate fails, startup continues and restore is skipped safely.
## 3.1 Restore outcomes
`GET /api/remote/status` includes restore diagnostics:
- `outcome: "applied" | "skipped" | "failed"`
- `reason` code (examples):
- `not_attempted`
- `remote_access_disabled`
- `remember_last_running_disabled`
- `no_prior_running_marker`
- `provider_missing`
- `provider_not_enabled`
- `provider_not_configured`
- `runtime_prerequisite_missing`
- `restore_started`
- `restore_start_failed`
## 3.2 Marker behavior and safety
- Explicit manual stop clears restart intent markers:
- `wasRunningOnShutdown = false`
- `lastRunningProvider = null`
- Failed/skipped restore clears stale running markers to avoid retry loops.
- Restore failure is non-fatal to engine startup.
---
## 4) Operation paths: Dashboard vs TUI vs headless
## 4.1 Dashboard (`fn dashboard`)
Path: **Settings → Remote Access**
Supported actions:
- Save Remote settings (provider config + token strategy)
- Activate provider
- Start/Stop tunnel
- Regenerate persistent token
- Generate short-lived token
- Show authenticated URL
- Generate QR payload (SVG preview + raw details)
## 4.2 TUI (`fn dashboard` in TTY mode)
Path: **Interactive → Settings**
Remote hotkeys:
- `C` activate selected provider
- `V` start tunnel
- `X` stop tunnel
- `P` regenerate persistent token
- `L` enter TTL flow + generate short-lived token
- `U` fetch authenticated URL
- `K` fetch QR payload
- `R` refresh remote status
## 4.3 Headless API (`fn serve`)
`fn serve` runs with `headless: true` and exposes the same remote endpoints:
- `/api/remote/*` status/control
- `/api/remote-access/auth/login-url`
- `/remote-login?rt=...` public login handoff route
Parity expectations:
- Remote control/status contracts are the same in dashboard and headless runtime.
- Root SPA route is not served in headless mode (API operation only).
---
## 5) API quick reference
| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/api/remote/settings` | Return summarized remote settings (masked persistent token) |
| `GET` | `/api/remote/status` | Return tunnel status + restore diagnostics |
| `POST` | `/api/remote/provider/activate` | Set active provider |
| `POST` | `/api/remote/tunnel/start` | Start active provider tunnel |
| `POST` | `/api/remote/tunnel/stop` | Stop current tunnel |
| `POST` | `/api/remote/token/persistent/regenerate` | Generate and persist new persistent token |
| `POST` | `/api/remote/token/short-lived/generate` | Generate short-lived token (`ttlMs` optional in body) |
| `POST` | `/api/remote-access/auth/login-url` | Build login URL (`mode: persistent | short-lived`) |
| `GET` | `/api/remote/url` | Build login URL payload (`tokenType` query) |
| `GET` | `/api/remote/qr` | Build QR payload (`tokenType`, optional `format=image/svg`) |
| `GET` | `/remote-login?rt=<token>` | Validate remote token and 302 redirect to `/` or `/?token=...` |
---
## 6) Security caveats for login URLs and QR flow
> ⚠️ **Critical:** Remote auth links and QR payloads contain the full authenticated login URL, including `rt` token data in the query string.
>
> Example shape (redacted): `https://<host>/remote-login?rt=<token>`
Treat these links as secrets.
- Do **not** post them in chat, tickets, screenshots, screen recordings, or logs.
- Do **not** paste them into shared documents.
- Assume any copied URL can be replayed until token expiry/rotation.
## 6.1 Hybrid token model
Fusion supports two token modes for remote login handoff:
1. **Persistent token** (`remoteAccess.tokenStrategy.persistent`)
- Stored in project settings.
- Reused across generated links until regenerated.
- `GET /api/remote/settings` returns a **masked** representation only.
2. **Short-lived token** (`remoteAccess.tokenStrategy.shortLived`)
- Issued in-memory by dashboard server runtime.
- Expires by TTL and is removed when expired.
- Registry is process-local and is cleared on server restart.
Recommended usage by risk level:
- **Low-risk/internal lab:** persistent links may be acceptable for convenience.
- **Shared environments / ad-hoc phone login:** prefer short-lived links.
- **High-risk or uncertain channel hygiene:** use short-lived links with minimal TTL and regenerate/rotate frequently.
## 6.2 Token hygiene and rotation practices
- Prefer short-lived mode for one-time phone scans.
- If exposure is suspected, regenerate persistent token immediately (`POST /api/remote/token/persistent/regenerate`).
- Keep short-lived TTL as small as practical for the operator workflow.
- Avoid storing tokenized URLs in shell history where possible.
- Redact secrets in examples and runbooks (`<host>`, `<token>`, `<expiresAt>`).
---
## 7) Troubleshooting matrix
| Symptom | Likely cause | What to check | Remediation |
|---|---|---|---|
| `POST /api/remote/tunnel/start` returns 409 with prerequisite/config message | Provider config incomplete or runtime binary missing | Verify active provider, required provider fields, and executable presence on `PATH` (`tailscale` or `cloudflared`) | Fix provider config (`PUT /api/settings`) and install/repair missing binary, then retry start |
| Tunnel remains `stopped` after provider switch | Provider was activated but tunnel start was never requested | `GET /api/remote/status` and confirm no start request was made | Run explicit start (`POST /api/remote/tunnel/start`) after activation |
| `GET /remote-login?rt=<token>` returns `401` `remote_token_missing` | Missing `rt` query token | Validate URL structure includes `?rt=<token>` | Regenerate/fetch URL via `/api/remote-access/auth/login-url`, `/api/remote/url`, or `/api/remote/qr` |
| `GET /remote-login?rt=<token>` returns `401` `remote_token_expired` | Short-lived token expired | Check `expiresAt` from generation response and local clock drift | Generate a new short-lived token/login URL and retry |
| `GET /remote-login?rt=<token>` returns `401` `remote_token_invalid` | Wrong/rotated token or disabled token strategy | Confirm token mode and whether persistent token was regenerated | Re-fetch a current URL; if needed re-enable token strategy and rotate token |
| Restart does not restore prior running tunnel even with remember enabled | Restore gates failed or stale marker reconciled | Inspect `/api/remote/status.restore` (`outcome`, `reason`, optional `message`) | Resolve reported reason (`provider_not_configured`, `runtime_prerequisite_missing`, etc.), then start manually |
| Dashboard and headless behavior appear different | Different runtime/auth context (project selection, bearer token, host) | Confirm same project config and same endpoint calls in both modes | Use `/api/remote/status` and `/api/remote/settings` to compare canonical state; align auth/token/project context |
### 7.1 Mode-specific checks
- **Dashboard/TUI:** Ensure you saved remote settings before lifecycle operations.
- **Headless (`fn serve`):** Ensure API auth context is valid for protected `/api/*` calls.
- **All modes:** `GET /remote-login` remains a public handoff route by design, but only accepts valid remote token material.

View File

@@ -181,6 +181,8 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
Remote access settings are project-only (stored in `.fusion/config.json`), not global.
The canonical persisted shape is a nested `remoteAccess` object.
Use **[Remote Access runbook](./remote-access.md)** for setup prerequisites (Tailscale/Cloudflare), tokenized login-link security caveats, and operational troubleshooting. Keep this section as a schema reference.
| Setting | Type | Default | Description |
|---|---|---:|---|
| `remoteAccess.enabled` | `boolean` | `false` | Master toggle for remote access orchestration. |