Remote Access Runbook

May 21, 2026 · View on GitHub

← Docs index

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 performs a proactive cloudflared CLI detection check and shows a one-click Install cloudflared action (with manual command fallback) if the binary is missing. The direct-download path is pinned-manifest gated: until maintainers flip the shipped manifest from upstream-pending-verification to a verified tagged release with per-asset .sha256 sidecars, auto-download fails closed and the UI surfaces package-manager/manual fallback commands.

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

You can persist both provider configs and switch between them without rewriting settings.

Minimal remoteAccess shape (redacted placeholders):

{
  "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

MethodEndpointPurpose
GET/api/remote/settingsReturn summarized remote settings (masked persistent token)
GET/api/remote/statusReturn tunnel status + restore diagnostics
POST/api/remote/provider/activateSet active provider
POST/api/remote/tunnel/startStart active provider tunnel
POST/api/remote/tunnel/stopStop current tunnel
POST/api/remote/token/persistent/regenerateGenerate and persist new persistent token
POST/api/remote/token/short-lived/generateGenerate short-lived token (ttlMs optional in body)
POST/api/remote-access/auth/login-urlBuild login URL (`mode: persistent
GET/api/remote/urlBuild login URL payload (tokenType query)
GET/api/remote/qrBuild 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

SymptomLikely causeWhat to checkRemediation
POST /api/remote/tunnel/start returns 409 with prerequisite/config messageProvider config incomplete or runtime binary missingVerify 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 switchProvider was activated but tunnel start was never requestedGET /api/remote/status and confirm no start request was madeRun explicit start (POST /api/remote/tunnel/start) after activation
GET /remote-login?rt=<token> returns 401 remote_token_missingMissing rt query tokenValidate 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_expiredShort-lived token expiredCheck expiresAt from generation response and local clock driftGenerate a new short-lived token/login URL and retry
GET /remote-login?rt=<token> returns 401 remote_token_invalidWrong/rotated token or disabled token strategyConfirm token mode and whether persistent token was regeneratedRe-fetch a current URL; if needed re-enable token strategy and rotate token
Restart does not restore prior running tunnel even with remember enabledRestore gates failed or stale marker reconciledInspect /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 differentDifferent runtime/auth context (project selection, bearer token, host)Confirm same project config and same endpoint calls in both modesUse /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.