Files
fusion/docs/remote-access.md
Fusion ed9827a1bf docs(FN-4266): complete Step 4 — document headless auto-register
Ref: Runfusion/Fusion#216
Fusion-Task-Id: FN-4266
Fusion-Task-Lineage: 4105d2eb-f3d8-4297-9b49-bf7e5a2906be
2026-05-13 06:29:33 -07:00

332 lines
13 KiB
Markdown

# 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:
- Global-scoped `remoteAccess` settings in `~/.fusion/settings.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 **global-scoped** (`GlobalSettings.remoteAccess` in `packages/core/src/types.ts`).
- Configure it in dashboard settings (Remote tab) or via `PUT /api/settings/global` or `PUT /api/remote/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 named tunnel prerequisites
Cloudflare **named tunnel** startup gates (`quickTunnel = false`):
- `remoteAccess.providers.cloudflare.enabled = true`
- `remoteAccess.providers.cloudflare.quickTunnel = false` (default)
- `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.
## 1.4 Cloudflare Quick Tunnel prerequisites
Cloudflare **Quick Tunnel** startup gates (`quickTunnel = true`):
- `remoteAccess.providers.cloudflare.enabled = true`
- `remoteAccess.providers.cloudflare.quickTunnel = true`
- `cloudflared` executable is available on `PATH`
No Cloudflare account, tunnel token, named tunnel, or pre-created ingress URL is required.
Dashboard note: in Settings → Remote Access, selecting Cloudflare now performs a proactive `cloudflared` CLI detection check and shows a one-click **Install cloudflared** action (with manual command fallback) if the binary is missing.
Runtime command used by engine:
- `cloudflared tunnel --url http://localhost:<dashboardPort>`
Operational notes:
- `trycloudflare.com` URLs are ephemeral and typically change every tunnel restart.
- Login URL generation for quick tunnel mode uses the **live runtime URL** reported by the running tunnel.
- If the tunnel is not started yet, login URL generation cannot resolve a remote base URL.
---
## 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,
"quickTunnel": false,
"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`
- Kill external funnel bindings: `POST /api/remote/tunnel/kill-external`
- Status: `GET /api/remote/status`
Returned status fields include:
- `state`: `stopped | starting | running | stopping | failed`
- `provider`, `url`, `lastError`, `lastErrorCode`
- `externalTunnel` (nullable): detected externally-running tunnel metadata (`provider`, `url`) when Fusion-managed tunnel is stopped
- `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
- Detect externally-running Tailscale funnel sessions when opening Remote Access settings
- Use Existing (adopt existing tunnel) or Start Fresh (kill external funnel bindings then start a managed 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
Headless startup now auto-registers the current working directory as a Fusion project when it is not already in the central project registry. On first run you'll see:
- `[serve] Auto-registered project "<name>" at <cwd>`
This removes the old requirement to run `fn init`/`fn project add` first in CI, Docker, cron, and other non-interactive environments. Use `fn serve --no-auto-register` to keep the previous strict behavior.
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.