# Microsoft Dev Tunnel Client Access Design

Status: Design update
Date: 2026-05-30
Owner: CTM / Wall-E
Related docs:
- `claude-task-manager/docs/remote-desktop-access-design.md`
- `claude-task-manager/docs/phone-access-design.md`
- `claude-task-manager/docs/walle-relay-phone-access-design.md`
- `claude-task-manager/docs/phone-setup.md`

External references:
- Microsoft Dev Tunnels quickstart:
  `https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started`
- 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`

## Summary

Microsoft Dev Tunnel is the default direct browser transport for secure CTM
access from another browser client, including phone and desktop. CTM supports
two explicit tunnel access modes:

1. **CTM-authenticated** - recommended for phones. Microsoft Dev Tunnels allows
   anonymous `connect` to the CTM port, and CTM remains the application auth
   gate with pairing, client-device token, origin checks, route scopes, passkey
   step-up, audit, and revocation.
2. **Private Microsoft gate** - requires Microsoft/GitHub Dev Tunnel browser
   auth before the request reaches CTM. This is useful when the user wants a
   provider gate in front of CTM, but it can fail on mobile browsers before CTM
   can display its pairing UI.

> **Invariant (2026-05-30): CTM-authenticated is the default; private is an
> explicit opt-in.** Dev Tunnels "private" mode enforces auth by redirecting the
> *initial request* to an interactive Microsoft/GitHub login page. The phone
> client is a PWA that reaches CTM via `fetch()` + a `WebSocket` on the same
> tunnel origin, and programmatic XHR/fetch/WebSocket cannot follow that
> cross-origin redirect — so forcing private silently bricks remote access
> ("can't connect / times out"). A brief earlier experiment that made private the
> default was reverted for this reason. Private is therefore honored only as a
> deliberate opt-in (an explicit UI selection, `CTM_MS_TUNNEL_ACCESS_MODE=private`,
> or `options.allowPrivateAccess`); a private value coming only from persisted
> state is treated as stale and migrated back to CTM-authenticated so the phone
> keeps working across headless restarts. If you want a true edge sign-in that is
> compatible with the PWA/WebSocket, use Cloudflare Access (GitHub/Google SSO via
> a JWT/cookie) or Tailscale (device identity) instead — both are already wired.

The CTM-authenticated flow exposes the local CTM host through a
Microsoft-managed tunnel like this:

```text
remote browser
  -> private devtunnels.ms URL
  -> Dev Tunnels connect-only forwarding
  -> Azure Dev Tunnels service
  -> devtunnel host process on the Mac
  -> local CTM on 127.0.0.1:3456
  -> CTM client-device auth, scopes, step-up, audit, and revocation
```

Anonymous tunnel `connect` is not the same as anonymous CTM access. It only
lets a browser reach CTM; the CTM app must still reject unpaired devices and
unauthorized routes. The setup UI must name the selected mode clearly and show
what is protecting the connection.

This doc covers the transport. The access-control architecture is shared with
mobile and desktop remote access in `remote-desktop-access-design.md`.

## Product Positioning

Direct browser access methods should be presented in this order:

1. **Microsoft Dev Tunnel** - default. CTM-authenticated phone-friendly browser
   tunnel by default, with an optional private Microsoft/GitHub-gated mode.
2. **Tailscale** - private-network alternative when both devices are in the
   user's tailnet or the Mac is shared into a tailnet.
3. **Walle Remote** - typed mobile relay for request/response control when a
   full browser UI is not required.

Cloudflare Tunnel is out of scope for the CTM remote-desktop plan and should
not be shown in the main remote-access setup flow.

Microsoft Dev Tunnel is best when:

- the user wants full CTM browser access from another computer or phone;
- the user wants CTM's own pairing flow to load on the remote browser without
  first completing Microsoft/GitHub browser auth;
- the user does not want a VPN or domain;
- CTM still owns app-level permissions, pairing, passkey step-up, and audit.

Tailscale is best when:

- both machines can be authenticated into the same tailnet;
- the user wants direct private-network access instead of a hosted tunnel;
- ACLs/grants can restrict CTM to the intended devices and port.

## Design Principles

1. **Name the transport gate explicitly.** CTM-authenticated mode lets the
   browser reach CTM directly and relies on CTM app auth. Private Microsoft
   gate mode requires Microsoft/GitHub tunnel auth before CTM loads.
2. **No anonymous CTM access.** CTM-authenticated mode may create a narrow
   anonymous Dev Tunnel `connect` rule, but CTM must still require pairing,
   device token, origin checks, route/WS authorization, passkey step-up, audit,
   and revocation before exposing app data or controls.
3. **One client-device architecture.** Phone and desktop use the same device
   identity, pairing, passkey step-up, scopes, audit log, origin binding, and
   revocation model. The only difference is the UI surface: `/m/` for mobile,
   `/` for desktop.
4. **Full desktop means full CTM UI.** A trusted desktop client opens the same
   dashboard as local use: sessions, terminal, conversation view, prompts,
   queue, reviews, models, setup, and raw terminal input where permitted.
5. **No temporary desktop mode.** Do not create a separate reduced desktop
   product. Use normal CTM with remote auth and policy gates.
6. **Stable origin required.** CTM cookies and passkeys are origin-scoped.
   Reuse a persistent tunnel ID and URL instead of temporary URLs.
7. **Make access failures precise.** Distinguish Microsoft/GitHub auth,
   tunnel process, CTM origin, CTM device token, route scope, and passkey
   step-up failures.
8. **Revocation must close live access.** Revoking a CTM client device closes
   existing WebSockets and invalidates future HTTP/WS access.

## Recommended Setup Flow

### Desktop First-Time Setup

1. User opens `Setup -> Access`.
2. User selects `Microsoft Dev Tunnel`.
3. CTM shows one setup rail:

   ```text
   1 Install   2 Sign in   3 Start tunnel   4 Pair browser
   ```

4. CTM checks for the `devtunnel` CLI.
5. If missing, CTM offers install instructions and a refresh action.
6. CTM checks signed-in status with `devtunnel user show`.
7. If signed out, CTM offers Microsoft, GitHub, and device-code sign-in.
8. CTM creates or reuses a persistent CTM tunnel ID.
9. CTM creates or verifies port `3456` with protocol `http`.
10. User chooses the access mode:
    - CTM-authenticated: create or verify anonymous `connect` access for this
      tunnel port and rely on CTM app auth after forwarding.
    - Private Microsoft gate: verify no anonymous `connect` access exists, and
      offer `Reset to private` if stale anonymous access is detected.
11. CTM starts `devtunnel host <tunnel-id>` as a managed child process.
12. CTM parses the `https://...devtunnels.ms` URL from stdout.
13. CTM updates the origin allowlist for that exact URL.
14. CTM enables:
    - `Pair Desktop Browser`
    - `Pair Phone`
    - `Copy Desktop URL`
    - `Copy Mobile URL`

### Remote Browser Pairing

1. User opens the desktop or mobile URL:
   - desktop: `https://<tunnel>-3456.<region>.devtunnels.ms/`
   - mobile: `https://<tunnel>-3456.<region>.devtunnels.ms/m/`
2. In CTM-authenticated mode, the browser should reach CTM directly. In private
   Microsoft gate mode, Microsoft may show anti-phishing and Microsoft/GitHub
   sign-in before CTM receives the request.
3. After the request reaches CTM, CTM shows a pairing gate if the browser has
   no CTM client-device token.
4. CTM creates a pending pairing request for the current origin and surface.
5. The Mac setup page shows the pending request with device hint, origin,
   requested surface, scopes, and short code.
6. Local user approves or rejects.
7. Approval creates a CTM client-device token and registers or verifies a
   passkey for high-risk step-up.
8. The browser lands on `/` for desktop or `/m/` for mobile.

### Returning Use

1. User opens the saved URL.
2. CTM-authenticated mode forwards to CTM directly; private Microsoft gate mode
   first gates network access with Microsoft/GitHub tunnel auth.
3. CTM client-device token gates app access in both modes.
4. If the tunnel origin is stable, CTM cookies and passkeys remain valid.
5. If CTM token is missing or expired, CTM shows the pairing gate again.

### Stop and Resume

1. User clicks `Stop Tunnel`.
2. CTM stops the `devtunnel host` process.
3. Remote browsers can no longer reach CTM.
4. CTM keeps paired client-device records.
5. User clicks `Start Tunnel`.
6. CTM reuses the same persistent tunnel ID and origin.
7. Existing remote browsers resume without re-pairing unless their CTM token or
   passkey state expired.

### Expiration And Renewal

The `devtunnel` CLI accepts a maximum tunnel/access expiration of 30 days. CTM
therefore cannot create a never-expiring Microsoft tunnel; it should make the
remote URL stable by reusing the same tunnel ID and proactively renewing the
service-side expiration.

CTM policy:

- create persistent tunnels with `--expiration 30d`;
- renew the tunnel in place with `devtunnel update <tunnel-id> --expiration 30d`;
- mark renewal due after 25 days, leaving a buffer before the 30-day service
  limit;
- for CTM-authenticated mode, recreate the port-level anonymous `connect` access
  rule on the same 25-day cadence because the CLI exposes `access create` and
  `access delete`, but not `access update`;
- keep renewal checks cheap in the normal watchdog path by reading local renewal
  timestamps first and only calling the CLI when renewal is due, manual setup is
  run, or access repair is requested.

Renewal does not bypass Microsoft/GitHub account requirements for managing the
tunnel. If the local `devtunnel` credential expires, CTM should surface a
sign-in-required state and resume renewal after the user signs in again.

## Tailscale Alternative

Tailscale is not anonymous. For the normal case, both sides must be signed into
the same tailnet. Other secure variants are also possible, such as sharing the
Mac into another authenticated tailnet user or using managed/auth-key device
enrollment, but they are still authenticated tailnet flows.

CTM should treat Tailscale as a transport, not as the only auth layer:

- bind only to loopback and approved private/Tailscale interfaces;
- rely on Tailscale ACLs/grants to restrict network reachability;
- still require CTM client-device pairing for `/` and `/m/`;
- still enforce CTM route/WS scopes and passkey step-up.

## Access Policy

### Trusted Desktop Client

A paired trusted desktop client can:

- load the full `/` CTM dashboard;
- open session tabs;
- view terminal and conversation history;
- send raw terminal input to sessions where CTM policy allows it;
- send Wall-E/coding messages;
- create sessions and prompts;
- use queue, review, model, and setup pages subject to scopes and step-up.

Protected operations still require additional gates:

- route/WS authorization from `auth-rules`;
- passkey step-up for high-risk remote actions;
- local-only restrictions for hooks, telemetry ingest, OAuth callbacks, and
  other machine-local endpoints;
- audit events for auth, remote input, model changes, terminal control, and
  revocation.

### Mobile Client

A paired mobile client uses the same auth stack, but the `/m/` UI remains
optimized for phone-sized interaction. Mobile may hide or reshape some controls,
but that is a UX choice, not a separate auth architecture.

## Desktop UX

### Access Method Card

Title:

```text
Microsoft Dev Tunnel
```

Badge:

```text
Default secure browser access
```

Body:

```text
Open CTM from another computer or phone through a private Microsoft Dev Tunnel.
The remote browser signs into Microsoft/GitHub before CTM loads, then CTM
pairs and authorizes the browser.
```

Security note:

```text
No anonymous tunnel access. CTM still controls device pairing, permissions,
passkey step-up, audit, and revocation.
```

Primary button by state:

| State | Primary CTA |
| --- | --- |
| CLI missing | `Install CLI` |
| Signed out | `Sign in` |
| Private tunnel ready | `Start Tunnel` |
| Running | `Pair Browser` |
| Paired desktop | `Open Desktop URL` |
| Paired phone | `Open Phone URL` |
| Anonymous access detected | `Reset to Private` |
| Error | `Retry` |

Secondary buttons:

- `Stop Tunnel`
- `Refresh`
- `Diagnostics`
- `Copy Desktop URL`
- `Copy Mobile URL`

## Failure States

| State | User-facing copy | Recovery |
| --- | --- | --- |
| CLI missing | `Microsoft devtunnel CLI is not installed.` | Install, then refresh |
| Auth missing | `Sign into Microsoft or GitHub before starting a tunnel.` | Sign in / device code |
| Provider sign-in required | `Sign into the private Microsoft tunnel before CTM can load.` | Complete provider sign-in |
| Anonymous access detected | `This tunnel allows anonymous browser access. Reset it to private before using CTM remote access.` | Reset to private |
| Tunnel start failed | `Tunnel did not start.` | Retry, copy diagnostics |
| Temporary URL changed | `This browser link belongs to an old tunnel.` | Reuse persistent tunnel or re-pair |
| Origin not allowed | `CTM has not trusted this tunnel URL yet.` | Refresh setup, save origin |
| CTM device missing | `Pair this browser before using CTM remotely.` | Approve pairing on Mac |
| Claim expired | `Pairing code expired.` | Generate a new code |
| Microsoft service blocked | `This network blocks Microsoft Dev Tunnels.` | Use Tailscale |
| Tunnel stopped | `Microsoft tunnel is stopped.` | Start tunnel on Mac |
| Tunnel expiring | `Tunnel expires soon.` | Renew or recreate before expiry |

## Security Posture

Required:

- Microsoft/GitHub account required for Dev Tunnel management and remote
  browser access.
- No anonymous CTM app access. CTM-authenticated mode may use a connect-only
  anonymous Dev Tunnel rule, but CTM pairing, device token, route scopes, and
  passkey step-up remain mandatory app gates.
- CTM client-device token required for app access.
- Local Mac approval required for first browser pairing.
- CTM route authorization registry applies to HTTP and WebSocket actions.
- High-risk remote actions require passkey step-up.
- No tunnel access tokens in QR codes or browser URLs.
- Revocation closes active WebSockets and blocks future HTTP/WS requests.
- Audit records include transport, origin, surface, device, scope, and action.

Microsoft Dev Tunnel does not provide relay-blind payload privacy. Microsoft is
part of the trusted transport path. That is acceptable for this product shape
because the user explicitly selected Microsoft as the default secure transport
and CTM still owns app-layer authorization.

## Implementation Notes

### Local State

Store:

- tunnel ID;
- tunnel URL;
- port;
- protocol;
- signed-in account display;
- private access status;
- anonymous-access drift status;
- expiration timestamp if available;
- managed process PID;
- last stdout/stderr lines for diagnostics;
- exact CTM allowed origin;
- paired client-device IDs for desktop and mobile surfaces.

Do not store Microsoft/GitHub passwords or login tokens. The `devtunnel` CLI
uses the system credential store.

### Process Management

CTM should manage `devtunnel host <tunnel-id>` as a child process:

- start with fixed argv, not shell strings;
- capture stdout and stderr;
- parse the public URL;
- detect exit;
- expose `Stop Tunnel`;
- restore desired-running tunnel state after CTM restart or Mac wake;
- do not restart CTM unless local bind/origin config requires it.

### Readiness

`Ready` means:

- CLI installed;
- user signed in;
- persistent tunnel exists;
- port `3456` configured;
- tunnel access is private;
- no anonymous connect access is detected;
- host process running;
- CTM allowed origin includes the tunnel URL;
- CTM client-device pairing can be generated for that origin.

Do not show remote URLs before these checks pass.

## Test Plan

Manual happy path:

1. Install `devtunnel`.
2. Sign into Microsoft or GitHub.
3. Start persistent private tunnel.
4. Confirm CTM shows both desktop and mobile URLs.
5. Open desktop URL from another browser/computer.
6. Complete Microsoft/GitHub tunnel auth if prompted.
7. Approve CTM browser pairing on the Mac.
8. Open a session and send a low-risk terminal/message action.
9. Open mobile URL and confirm the same auth architecture with `/m/`.
10. Stop tunnel and confirm remote clients lose access.
11. Start tunnel again and confirm clients resume without re-pairing.
12. Revoke the remote browser and confirm existing WebSocket closes.

Negative cases:

- anonymous tunnel access detected;
- expired claim;
- tunnel host process crash;
- tunnel URL changes;
- CTM allowed origin missing;
- provider sign-in appears before CTM;
- CTM device token missing;
- high-risk action without CTM step-up.

Automated coverage should include:

- tunnel state normalization;
- anonymous-access drift detection;
- origin allowlist generation;
- pairing request lifecycle for desktop and mobile surfaces;
- HTTP route auth regression;
- WebSocket auth and revoke-close regression;
- audit event shape for remote desktop and mobile.

## MVP Cut Line

MVP includes:

- setup card and stepper;
- CLI detection;
- sign-in status and account display;
- persistent private tunnel create/reuse;
- anonymous-access drift detection and reset action;
- managed `devtunnel host` process;
- exact origin allowlist;
- desktop and mobile pairing links;
- shared client-device auth for `/` and `/m/`;
- stop tunnel;
- diagnostics copy.

MVP excludes:

- Cloudflare Tunnel;
- anonymous tunnel access;
- separate temporary desktop mode;
- team/tenant/org access controls;
- tunnel access tokens in browser URLs;
- traffic inspection UI.

## Recommendation

Use Microsoft Dev Tunnel as the default direct browser remote-access option.
Use Tailscale as the private-network alternative. Keep Walle Remote for typed
mobile relay workflows. Do not add Cloudflare Tunnel or a separate temporary
desktop mode to this plan.
