# CTM Remote Desktop Access Design

Status: Design update from user feedback
Date: 2026-05-27
Owner: CTM / Wall-E
Related docs:
- `claude-task-manager/docs/phone-access-design.md`
- `claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md`
- `claude-task-manager/docs/mobile-live-streaming.md`

External references:
- Microsoft Dev Tunnels security:
  `https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/security`
- Microsoft Dev Tunnels CLI reference:
  `https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/cli-commands`
- Tailscale quickstart:
  `https://tailscale.com/docs/how-to/quickstart`
- Tailscale access controls:
  `https://tailscale.com/docs/features/access-control/acls`

## Decisions

1. **Microsoft Dev Tunnel is the default browser remote-desktop transport.**
   It gives a second computer a normal browser URL without DNS setup, VPN setup,
   or Cloudflare Tunnel. It supports two explicit access modes:
   CTM-authenticated connect for phone-friendly pairing, and private
   Microsoft/GitHub gate when the user wants provider auth before CTM loads.
   Neither mode allows anonymous CTM app access.
2. **Cloudflare Tunnel is out of scope.** Do not add it to the remote desktop
   IA, default setup path, or MVP plan.
3. **Tailscale is the secure private-network alternative.** It requires both
   sides to be authenticated into the same tailnet, or an explicitly shared
   machine accepted by an authenticated Tailscale user. CTM still applies its
   own device auth after the network connection succeeds.
4. **No temporary desktop mode.** A remote computer is either not paired or is a
   trusted CTM client device. Avoid split policies such as "temporary desktop"
   or "lite desktop" unless a later product requirement justifies them.
5. **Remote desktop gets full CTM access after auth.** The UX should be almost
   the same as using CTM locally: full dashboard, terminal tabs, conversation
   tabs, prompt editor, model selectors, queue, setup, reviews, worktrees, and
   raw terminal input. The difference is that remote access passes through
   network identity, CTM device auth, origin checks, route/WS authorization,
   passkey step-up for high-risk actions, and audit.
6. **Mobile and desktop share one architecture.** The only difference is the
   surface: `/m/` is a mobile UX and `/` is the desktop UX. Pairing, device
   identity, scopes, WebAuthn credentials, origin binding, WebSocket tickets,
   revocation, audit, and route/WS policy should be shared.

## Product Shape

`Setup -> Access` should present:

1. `Microsoft Dev Tunnel` - default remote computer access.
2. `Tailscale` - private tailnet access.
3. `Walle Remote` - typed mobile/relay control, not full desktop mirroring.

Do not show Cloudflare Tunnel in the remote desktop flow.

Each direct transport publishes two URLs:

```text
desktop_url = <origin>/
mobile_url  = <origin>/m/
```

The same CTM instance, device store, auth rules, and WebSocket server back both
URLs. The surface only controls layout and UX.

## Architecture

```text
Remote browser
  -> Transport identity
     -> Microsoft Dev Tunnel CTM-authenticated connect
     -> or Microsoft Dev Tunnel private browser access
     -> or Tailscale tailnet identity + ACL/grant
  -> CTM same-origin HTTPS/WS endpoint
  -> CTM client-device token
  -> CTM origin allowlist + WebSocket ticket
  -> Auth rule registry for HTTP routes and WS message types
  -> Passkey step-up for high-risk remote actions
  -> Audit + revoke closes active WebSockets
```

The current code already has most of this shape:

- `auth-context.js` resolves loopback versus remote device auth.
- `auth-rules.js` classifies HTTP routes and WS message types.
- `mobile-auth-store.js` stores device tokens, WebAuthn credentials, step-up
  sessions, audit, and revocation.
- `transport-security.js` blocks wildcard binds and validates origins.
- `server.js` checks Cloudflare Access only when configured, validates CTM
  device tokens, checks origins, enforces step-up, and closes WebSockets when a
  device is revoked.

The needed architectural cleanup is to stop treating this as "mobile auth".
The shared layer should become `client-device` or `remote-client` internally:

```text
client_device
  id
  title
  surface_last_used: desktop | mobile
  transport_last_used: microsoft-dev-tunnel | tailscale | loopback
  scopes: read, respond, create, admin
  origins / rp_ids
  token hash
  WebAuthn credentials
  audit / revoke metadata
```

DB migration can preserve existing table names initially, but the code and docs
should use the product term `client device`. `mobile-*` names should become
compatibility aliases, not the mental model.

## Microsoft Dev Tunnel Default

Microsoft documents that, by default, hosting and connecting require
authentication with the same Microsoft, Entra, or GitHub account that created
the tunnel, and that anonymous access is an explicit opt-in. CTM should expose
that as an explicit private mode, not as a hidden failure state.

For phone access, CTM-authenticated mode is the better default: Dev Tunnels
allows anonymous `connect` to this one port so the browser can reach CTM, and
CTM then enforces client-device auth, origin checks, route/WS authorization,
passkey step-up, audit, and revocation.

Required flow:

1. The Mac signs into `devtunnel` with Microsoft or GitHub.
2. CTM creates or reuses a persistent tunnel ID.
3. CTM creates port `3456` with protocol `http`.
4. CTM applies the selected access mode:
   - CTM-authenticated mode creates/verifies anonymous `connect` on this
     tunnel/port.
   - Private Microsoft gate mode resets/removes anonymous `connect` access.
5. CTM starts `devtunnel host <tunnel-id>`.
6. The remote computer opens the `desktop_url`.
7. Microsoft/GitHub authentication happens before CTM loads only in private
   Microsoft gate mode.
8. After the request reaches CTM, CTM requires client-device pairing or an
   existing CTM device token.
9. The paired desktop gets full CTM access subject to CTM remote auth rules.

The remote browser should not receive a Dev Tunnel access token in a URL. If a
future headless/non-browser client needs tunnel access, use Microsoft’s
`X-Tunnel-Authorization` header flow, not CTM browser URLs.

Migration rule:

- If CTM detects anonymous Dev Tunnel access while the selected mode is private
  Microsoft gate, show a security warning and offer `Reset to private`.
- If CTM-authenticated mode is selected, anonymous `connect` is the intended
  transport state and should not be treated as stale.

## Tailscale Alternative

Tailscale is not the default for this desktop flow because it requires both
machines to be in an authenticated Tailscale relationship. That can be:

- both devices logged into the same tailnet;
- the Mac shared to another authenticated Tailscale user;
- a managed device/auth-key flow for machines you control.

For CTM this is a clean security boundary:

```text
Tailscale proves the remote machine/user can reach the Mac on port 3456.
CTM proves this browser is an approved CTM client device.
```

CTM should still bind only to loopback or a specific Tailscale IP. Do not bind
to `0.0.0.0`. Tailscale ACLs/grants should restrict `3456` to the intended
user/device where possible.

## Full Desktop Access Policy

There is one remote desktop policy: `trusted full desktop`.

Allowed after device pairing:

- load `/` desktop UI;
- open all session tabs;
- subscribe to terminal and conversation streams;
- type into terminal sessions;
- send Wall-E/coding messages;
- create sessions;
- use prompts, queues, reviews, model selectors, and setup pages.

Still protected:

- remote mutations must match `auth-rules.js`;
- routes/WS messages with `stepUp: remote` require a fresh passkey step-up;
- loopback-only routes remain loopback-only;
- `/hook/*`, `/v1/logs`, OAuth proxy routes, local telemetry ingress, and local
  permission helper endpoints stay local-only;
- every remote mutation is audited by device, route/message type, origin, IP,
  scope, and step-up state.

This gives local-like power without pretending a remote browser is loopback.

## Pairing UX

Use one pairing model for phone and desktop:

```text
Pair device
  Choose surface: Desktop or Phone
  Choose transport: Microsoft Dev Tunnel or Tailscale
  Choose device title
  Approve locally on the Mac
  Register passkey for this origin
  Issue CTM client-device token
```

Pairing URLs:

```text
desktop claim: <origin>/claim?claim=...&secret=...&surface=desktop
mobile claim:  <origin>/m/claim?claim=...&secret=...&surface=mobile
```

If a remote desktop opens `/` without a CTM token, it should show an auth gate
that can request pairing. Do not expose the dashboard until CTM auth succeeds.

## UI Requirements

Desktop remote banner:

```text
Remote Desktop
Microsoft Dev Tunnel · user@example.com · CTM device: Owner MacBook
Step-up: fresh / expired
[Step up] [Revoke this device] [Logout]
```

The banner should be compact and dismissible for the current session, but a
security status indicator must remain visible when remote.

Setup Access should show:

- `Desktop URL`;
- `Phone URL`;
- tunnel account;
- CTM device status;
- last used device;
- revoke controls;
- stale anonymous-access warning when detected.

## Test Plan

Required tests before implementation is accepted:

- Microsoft private tunnel flow does not call or recommend anonymous access
  creation.
- Stale anonymous access detection surfaces a warning and reset action.
- Remote desktop opening `/` without CTM token shows auth/pairing gate, not the
  dashboard.
- Remote desktop opening `/` with paired CTM token loads the same desktop UI.
- Remote WebSocket can read with `read` scope.
- Remote terminal `input` requires `respond` scope and fresh step-up when the
  route registry requires it.
- Admin/session/setup mutations require `admin` or `create` as defined by
  `auth-rules.js` and fresh step-up when remote.
- Revoke device closes active desktop WebSockets.
- Tailscale setup emits both `desktop_url` and `mobile_url`.
- Existing `/m/` phone flows still work from the same device/auth store.
- Loopback desktop behavior remains unchanged.
