# Remote Session UI Security Proposal

The current Linux remote-session-ui bridge in
`tools/remote-session-client/src/bin/remote_session_ui.rs` is a trusted
local web bridge: a loopback HTTP listener whose Rust backend owns the
TCP connection to the capOS gateway and the upstream session, while
browser JavaScript receives only DTOs (view models, call results,
denial diagnostics, and redacted transcript rows). This document
describes the web-security posture required before that bridge ships
beyond research use, and how the Tauri desktop wrapper inherits the
controls. It also records which browser-facing controls carry over to the
capOS-served `remote-session-web-ui` service and which public-origin controls
belong to the selected GCE provider-terminated HTTPS policy, without
authorizing public exposure. It is cross-linked from
`docs/proposals/security-and-verification-proposal.md`,
`docs/proposals/remote-session-capset-client-proposal.md` (the parent
proposal that defines the remote session CapSet wire and host-client
shape this bridge instantiates), and the design risks register entry
[R17 -- Remote-session UI bridge and Tauri wrapper are research-only](../design-risks-register.md#r17----remote-session-ui-bridge-and-tauri-wrapper-are-research-only),
which routes long-horizon residual risk (distributable packaging,
desktop automation, non-loopback exposure) back to this proposal.


## Threat Model

The bridge holds the operator's authority to drive the capOS gateway.
Anything that can issue HTTP requests to the loopback listener inherits
that authority. The original bridge shape had:

- A single shared `Arc<Mutex<AppState>>` constructed once in `run()`
  (around line 1606) and cloned to every accepted connection.
- No per-browser session cookie, no per-tab token, no per-origin
  isolation, no proof-of-possession of the original operator login.
- An origin allow-list that returns `true` when the `Origin` header is
  absent (`origin_allowed`, line 2163-2169), which lets non-browser
  POSTs bypass the only state-change guard.
- Plain `http://127.0.0.1:<port>/` transport.

Already closed:

- The previous non-constant-time `!=` comparison on the automation
  token has been replaced with `constant_time_eq` in
  `automation_report` and `set_automation_report` (see
  `tools/remote-session-client/src/bin/remote_session_ui.rs:1378` and
  `:1392`). Future secret comparisons must use the same comparator.
- The loopback bridge now mints per-browser `BrowserSession` cookies,
  requires CSRF tokens on state-changing `/api/*` routes, validates
  `Host` / `Origin` / JSON content type before route work, and enforces
  first-wins bridge ownership through an atomic tentative reservation.
- The local HTTP parser now bounds request-line length, header-line
  length, header count, aggregate header bytes, body size, slow reads,
  and concurrent handler threads before gateway or authentication work.

**Gateway-host redirect scope.** `POST /api/config` is intentionally
operator-controlled: it allows an authenticated operator to point the
bridge at a different `gateway_host`. This is bounded by the
operator-console trust boundary — only a caller who has already passed
the `BrowserSession` cookie guard and the CSRF double-submit check
(i.e., the bridge-owning operator session) can invoke it. The
capability model provides the deeper guarantee: the bridge holds a
single capOS gateway connection at a time; redirecting to an arbitrary
host replaces that connection but does not grant new capability
authority that wasn't already present in an authenticated operator
session. No arbitrary-host proxy to untrusted endpoints is possible
without an authenticated operator action.

Treating `127.0.0.1` as a trust boundary repeats the failure pattern of
historic Docker, Jupyter, and Electron loopback CVEs: any local user,
another OS account, a malicious browser extension, a locally-running
package install script, or any other process that can `connect(2)` to
the listener can drive the upstream capOS gateway with the operator's
authority. Two browsers today silently share one upstream session;
there is no way for an operator or audit log to distinguish them.


## Required Posture

### Per-browser BrowserSession

Mint a high-entropy opaque session id at the first browser hit and
store it server-side as a `BrowserSession` record distinct from the
upstream capOS session. The cookie is the only thing the browser
holds; everything else stays in `AppState`. Two browsers must end up
with two `BrowserSession` records.

Cookie attribute target:

- `HttpOnly`
- `SameSite=Strict` (the loopback bridge has no cross-site sign-in
  redirect, so `Strict` is unconditional here; the capOS-served
  `remote-session-web-ui` behind public ingress selects the posture
  from the boot manifest instead -- `Strict` by default, `Lax` only
  when an IAP-fronted deployment manifest grants the
  `iap_fronted_ingress` marker, per the selected policy in
  `cloud-deployment-proposal.md` -- and applies it uniformly to the
  session, CSRF, and clear-cookie headers)
- `Path=/`
- Host-only: no `Domain` attribute.
- `__Host-` name prefix when transport allows it (requires `Secure`
  and `Path=/` and forbids `Domain`).
- `Max-Age=...` plus an absolute upper bound enforced server-side.

`Secure` cookie attribute over plaintext loopback is browser- and
version-specific. Modern browsers do treat `127.0.0.1` and `::1` as
potentially trustworthy origins for some Secure-Context APIs, but
acceptance and sending of `Secure`-flagged cookies over plaintext
loopback is not uniform across vendors and versions. Two acceptable
deployment paths:

1. Move the bridge to HTTPS or to the Tauri custom-scheme secure
   origin before requiring `Secure` and `__Host-`.
2. Run on plaintext loopback as an interim with `HttpOnly;
   SameSite=Strict; Path=/; Max-Age=...` and no `Secure` /
   `__Host-`, with a documented support matrix and a test that proves
   browsers retain and resend the cookie across the supported range.

**Decision: option 2 (plaintext loopback, no `Secure`, no
`__Host-`).** This matches the current research-stage operator-bridge
deployment, which only listens on `127.0.0.1` and is not reachable
from the network. Cookie attributes are therefore `HttpOnly;
SameSite=Strict; Path=/; Max-Age=<absolute-timeout-secs>` exactly --
no `Secure` and no `__Host-` prefix. The follow-on Tauri / HTTPS
track will switch to option 1 (with `Secure` and `__Host-`) before
shipping beyond research use; the cookie-emit code carves out one
place to flip both attributes when transport changes.

Browser support matrix verified for option 2 (cookies retained and
resent across loopback HTTP without `Secure`):

| Browser    | Min version | Notes                                           |
| ---------- | ----------- | ----------------------------------------------- |
| Chromium   | 96+         | `127.0.0.1` is a potentially-trustworthy origin |
| Firefox    | 96+         | same; `SameSite=Strict` enforced for loopback   |
| Safari     | 15.4+       | macOS 12.3+ / iOS 15.4+                         |
| Edge       | 96+         | matches Chromium                                |

The verification host test in iter7 round-trips a cookie through a
synthetic loopback request to assert browsers within this matrix
retain and resend it. Older browsers (pre-Same-Site-Strict
enforcement on loopback) are not supported.

The design must not silently rely on a `Secure` flag that some target
browsers drop.

Server-side requirements:

- High-entropy opaque ids; never derived from user-controlled input.
- Server-side rotation: regenerate the `BrowserSession` id on
  successful login and on privilege transitions, and invalidate the
  prior anonymous/pre-auth record. (Session-fixation defense.)
- Server-side invalidation on logout, idle timeout, absolute timeout,
  and explicit revoke; wipe the record from `AppState`.
- Cookie value must never be logged, never written to the transcript,
  and never included in any DTO returned to the browser.

### Multi-browser policy

Pick one and document it here:

- **(a) Independent logins.** Each `BrowserSession` carries its own
  upstream capOS session; logging in from a second browser opens a
  second upstream session.
- **(b) First-wins exclusivity.** The first authenticated
  `BrowserSession` owns the upstream session; subsequent browsers see
  an explicit "session already in use" denial DTO rather than silent
  piggy-backing.

Either is acceptable if explicit and audit-logged. Silent shared
state is not.

**Decision: option (b), first-wins exclusivity.** The bridge today
holds exactly one upstream capOS session per process, and the
research-stage operator boot does not have a clean way to multiplex
two operator-authority sessions through a single capOS gateway
connection. First-wins is also the auditor-friendlier path: every
denied "session already in use" carries the active
`BrowserSession`'s timestamped lineage, so the operator and audit log
see the rejection rather than silently sharing state. Concretely:

- The first browser that starts `/api/login/password`,
  `/api/login/anonymous`, or `/api/login/guest` after passing local
  request guards reserves the owner slot before upstream gateway
  authentication. Successful login rotates the BrowserSession id,
  marks that slot authenticated, and keeps the upstream capOS session
  handle in `AppState`.
- Failed local login validation, bad credentials, and gateway denials
  release the tentative reservation. An already authenticated owner is
  not released by a later bad retry from the same browser session.
- Subsequent `BrowserSession`s authenticating against the same
  bridge get a typed `sessionAlreadyInUse` denial DTO rather than an
  upstream login attempt, including while the first session is still
  authenticating upstream. The denial includes the owner's claim or
  authentication timestamp so the second operator sees when the bridge
  was claimed.
- Logout / idle-timeout / absolute-timeout on the owner releases the
  upstream session and clears `owner_session_id`; the next
  authenticator wins.
- Every transition (claim / denial / release) emits a structured
  audit event into the same stream as upstream capOS session events
  so an operator looking back can see the bridge contention pattern.

The Tauri wrapper inherits this rule per-window unless the
wrapper introduces an explicit multi-window upstream-fanout authority
the loopback bridge does not have.

### CSRF and origin discipline

- Require a valid `BrowserSession` cookie on every `/api/*` route, not
  only state-changing routes. Today's `GET /api/state`,
  `GET /api/transcript/redacted`, and `GET /api/automation/report`
  expose state, transcript, and automation surfaces and must not rely
  on SOP/loopback assumptions alone.
- Reject state-changing requests when `Origin` is missing. The
  current `origin_allowed` short-circuit on missing `Origin` (line
  2164) must be removed for state-changing methods. Validate `Origin`
  against the listener's expected loopback origin set, and validate
  `Referer` as a fallback only when `Origin` is absent on legacy paths.
- Add a double-submit CSRF token bound to the `BrowserSession` cookie
  and required on every state-changing POST. `SameSite=Strict` is not
  sufficient defense in depth on its own.
- Defense-in-depth via Fetch Metadata: reject browser POSTs whose
  `Sec-Fetch-Site` is `cross-site` or whose `Sec-Fetch-Mode` is not in
  the expected set for the route. This is not a replacement for
  CSRF/Origin, but adds another layer.

### DNS-rebinding hardening

Validate the `Host` header against the loopback set
`{127.0.0.1:<port>, localhost:<port>, [::1]:<port>}`. Without this,
DNS-rebinding from a malicious public site can use the victim's
browser as a proxy into the loopback bridge.

### Content-Type enforcement

Reject POSTs whose `Content-Type` is not `application/json` (or the
specific expected type for the route). This blocks `text/plain` /
form-urlencoded cross-origin form submits that bypass preflight.

Implemented on both surfaces. The capOS-served `remote-session-web-ui`
normalizes the header (casing and `;`-parameters stripped) and requires
the `application/json` media type on every state-changing `/api/*` POST
class -- login-family and authenticated -- before route work, with a
typed `415` denial (`missingContentType` / `unsupportedContentType`).
The more specific Host/Origin denials keep precedence, and the fixed
non-JSON routes (`/healthz`, bundle assets, the scoped ACME `http-01`
challenge path) are unaffected. `make
run-cloud-prod-remote-session-web-ui-l4` proves the negative matrix
(missing, `text/plain`, form-encoded, multipart, malformed, mixed-case
parameterized non-JSON) and the parameterized/mixed-case JSON positives
over the real ingress path. This is local request-shape hardening only;
it is not public ingress or TLS readiness.

### Local HTTP request and handler bounds

The host bridge remains a trusted local development bridge. These
bounds reduce local resource-exhaustion and confused-client failure
modes; they do not make the UI a public network service.

The HTTP parser must reject overlong request lines, overlong header
lines, too many headers, excessive aggregate header bytes, and
overlarge bodies before route dispatch, JSON parsing, authentication,
or gateway I/O. Incomplete or slow request lines, headers, and bodies
must time out under a fixed read deadline. The accept loop must also
cap concurrent request handler threads and fail closed with a typed
local denial rather than spawning one thread per accepted connection
without bound.

### CORS stance

Emit no `Access-Control-Allow-Origin` by default. If a future route
ever needs CORS, allow only the exact same-origin echo of the listener
URL. Refuse wildcards. Refuse `Access-Control-Allow-Credentials: true`
combined with permissive origins. Document the rule in code so future
contributors do not accidentally widen it.

### Security response headers

Implemented in the in-guest `remote-session-web-ui` service
(`SECURITY_RESPONSE_HEADERS` / `CONTENT_SECURITY_POLICY` in
`demos/remote-session-web-ui/src/main.rs`), emitted on every response
class -- HTML, static assets, JSON API, `/healthz`, the ACME `http-01`
challenge route, and every denial:

- `X-Frame-Options: DENY` (anti-clickjacking).
- `X-Content-Type-Options: nosniff`.
- `Referrer-Policy: no-referrer`.
- `Cross-Origin-Opener-Policy: same-origin`.
- `Cross-Origin-Embedder-Policy: require-corp`.
- `Cross-Origin-Resource-Policy: same-origin`.
- `Cache-Control: no-store`.

The implemented shape applies `Cross-Origin-Resource-Policy:
same-origin` and `Cache-Control: no-store` to every response, not only
API responses: every asset is consumed same-origin by the operator app,
non-browser consumers (provider health checkers, ACME validators)
ignore browser embedding policy, and serving the fixed boot-resource
bundle uncached is acceptable for the operator UI. Relaxing caching for
static assets would be a deliberate future change, not a default.

The implemented `Content-Security-Policy` meets the no-`unsafe-inline`
target for both `script-src` and `style-src`:

```text
default-src 'none'; script-src 'self'; style-src 'self';
img-src 'self' data:; connect-src 'self'; base-uri 'none';
form-action 'self'; frame-ancestors 'none'
```

`img-src` allows `data:` in addition to `'self'` because the committed
stylesheet's hacker-theme dashed border is a `data:image/svg+xml`
background image; a `data:` image cannot execute script under this
policy, and folding it into the pinned bundle as a file asset would be
a separate reviewed bundle change. The earlier inline feature-flag
script and inline `style="..."` attributes in
`tools/remote-session-client/ui/index.html` were moved into static
bundle assets (`/feature-flags.js`, the stylesheet) before the CSP
landed, so the strict policy serves the fixed bundle without nonces or
hashes. The local QEMU proof
(`make run-cloud-prod-remote-session-web-ui-l4`) asserts the header set
and CSP on every response class over the real ingress, boots the served
root document in a real browser under the strict CSP with zero
`securitypolicyviolation` events, and asserts no `Access-Control-*`
header is emitted on any probed route.

### Constant-time secret comparison

The automation-token check has been migrated to `constant_time_eq`
(`automation_report` and `set_automation_report` in
`tools/remote-session-client/src/bin/remote_session_ui.rs`). Apply
the same comparator to the future `BrowserSession` cookie value
lookup, the CSRF token check, and any future bearer/HMAC validations.

### Auth-endpoint rate limiting and lockout

Add per-`BrowserSession` and per-listener rate limits to
`/api/login/password` and any future credential-handling routes.
Exponential backoff on failure. Audit-logged lockout. Wire into the
same audit stream as upstream session events so the operator sees
failed attempts.

### Idle and absolute timeouts

Independent of the upstream capOS session expiry, expire
`BrowserSession` cookies on idle and on absolute lifetime. Force
re-auth on resume. Rotate the cookie id on re-auth.

### Log injection / transcript safety

Sanitize browser-supplied strings routed into the transcript or
stderr for CRLF, ANSI escape sequences, and control bytes so a
hostile client cannot forge transcript rows or terminal control on
operator stderr.

### DTO-only-to-webview discipline

Keep the existing `*Vm` DTO boundary in
`tools/remote-session-client/src/bin/remote_session_ui.rs` (lines
~199-382). The browser must never receive raw cap handles, raw
interface ids, or unredacted session ids. The `CapVm.interface_id`
field is already `#[serde(skip_serializing)]`; preserve that pattern
for any new fields.


## Self-Served And Public-Origin Carry-Over

The host-local `remote-session-ui` bridge and the capOS-served
`remote-session-web-ui` service are different deployment surfaces. The host
bridge is a trusted Linux loopback development tool whose backend owns the TCP
gateway connection. The self-served service is a capOS userspace HTTP service
that owns its `TcpListenAuthority`, session-manager login flow, authority-broker
bundle, and remote CapSet/proxy state inside the guest. The host bridge is not
the self-served service moved into the guest.

The authority boundary is the shared rule. Browser JavaScript receives only
view models, typed commands, typed results, denials, redacted transcript/status
rows, and fixed UI assets. It must not receive raw capOS capabilities, raw cap
ids, endpoint-owner authority, `ProcessSpawner`, socket factories,
`NetworkManager`, `TcpListenAuthority`, `TcpListener`, `TcpSocket`, key
material, remote CapSet handles, result-cap slots, process handles, host
usernames, host paths, host environment markers, or QEMU-forwarding identity
hints. These exclusions match the self-served Gate 1B boundary in
[Remote Session CapSet Client](remote-session-capset-client-proposal.md) and
the implementation proof records under
[`remote-session-self-served-full-ui-bundle`](../tasks/done/2026-06-04/remote-session-self-served-full-ui-bundle.md).

Forbidden browser-visible surface matrix:

| Forbidden browser-visible class | Trusted owner or denial boundary | Proof / denial expectation |
| --- | --- | --- |
| Raw capOS capabilities, raw cap handles, raw interface ids, and local cap ids | Held only by the `remote-session-web-ui` backend, its server-side proxy state, or the upstream gateway connection. | Browser envelopes, DOM state, diagnostics, transcripts, and JSON contain only DTO names and redacted labels; any browser request that tries to name a cap id fails before backend dispatch. |
| Endpoint-owner authority and arbitrary endpoint creation | Owned by the backend service runner and `AuthorityBroker` policy, not by browser state. | Browser launch forms name only approved service descriptors; denied launches return typed denial DTOs without endpoint-owner tokens or creation handles. |
| Process handles, raw `ProcessSpawner`, and shell launcher authority | Kept behind `AuthorityBroker`-approved remote-client bundle policy. | Status and transcript rows expose only redacted process/service state; process handles and spawner markers are absent from browser-visible data. |
| `NetworkManager` and `TcpListenAuthority` | `remote-session-web-ui` owns only the manifest-scoped UI listener for the selected proof target; the open cloudboot L4 task must source that listener through the Phase C userspace network path rather than browser or raw manager authority. | Listener/source metadata is service-derived from the accepted socket plus a service event id; browser requests cannot supply trusted source, route, or listener authority. |
| `TcpListener`, `TcpSocket`, and socket factories | The HTTP accept loop owns accepted sockets and per-connection state server-side. | Browser JavaScript uses ordinary same-origin HTTP commands only; socket factory names, accepted-socket handles, and backend connection handles never appear in DTOs. |
| Key material, TLS private keys, certificates, public IPs, and firewall rules | Public-origin TLS and ingress remain in the on-hold provider-terminated HTTPS task; local and private proofs do not hold these secrets in the browser or capOS Web UI. | Local self-served and cloudboot proofs must not emit TLS key/certificate material, provider resource ids, public addresses, or firewall rule names as browser-readable state. |
| Remote CapSet handles, backend cap holders, session-global ids, and result-cap slots | Stored in server-side remote-session proxy tables and invalidated through backend logout/stale-call rules. | Browser commands reference typed route/request ids only; stale calls and unauthorized result access fail closed without leaking slot numbers or remote handles. |
| Host paths, host usernames, host environment markers, and QEMU-forwarding identity hints | Limited to development harness/operator context and not part of the capOS-served browser contract. | DOM state, JSON responses, diagnostics, and transcripts use redacted service labels; source metadata is backend-derived and cannot be replayed from browser-supplied fields. |

The matrix is a review checklist, not the enforcement mechanism. The browser
boundary is acceptable only when the backend also rejects stale, unauthorized,
or client-supplied authority selectors before any capability dispatch.

The carry-over controls are backend-held session state, server-side
`BrowserSession` records, CSRF tokens on state-changing JSON routes,
`Host`/`Origin`/`Referer`/content-type validation, no wildcard CORS, security
response headers, request and handler bounds, per-session rate and resource
limits, idle and absolute lifetime enforcement, logout that drops server-side
authority, transcript sanitization, constant-time comparisons for secrets, and
audit-visible denials. Those controls are required for the capOS-served service
as well as the loopback bridge, but their concrete transport assumptions differ.

On the capOS-served `remote-session-web-ui`, the browser-boundary baseline is
implemented and locally proven on
`make run-cloud-prod-remote-session-web-ui-l4`: server-side session hardening
(unpredictable rotated session ids, a domain-separated double-submit CSRF
token, `Host`/`Origin` validation, and idle/absolute lifetime enforcement),
GFE-range-pinned forwarded-scheme trust, the manifest-selected single public
origin, the IAP-aware SameSite cookie posture, JSON content-type rejection on
state-changing `/api/*` POSTs, the uniform security response headers with the
strict no-`unsafe-inline` CSP, in-guest login peer-gating with failure
backoff, and the public `/healthz` health-check contract. All of that evidence
is local QEMU/cloudboot proof only; none of it claims private GCE
reachability, public ingress, TLS custody, or operator exposure.

Two browser-boundary local proofs remain open as dispatchable task records
under `docs/tasks/`, not done: a public-deployment loopback gate that rejects
loopback `Host`/`Origin`/`Referer` acceptance and loopback-shaped source hints
when the public-origin load-balancer posture is configured (the landed local
proofs intentionally preserve the QEMU loopback posture), and a consolidated
browser-visible forbidden-marker matrix proof that scans every response class
-- success, denial, health, manual, and error bodies -- for the forbidden
surface above and proves hostile browser-supplied authority fields fail closed
before backend-held capability dispatch.

Loopback-only decisions do not carry to a public origin. The plaintext
`http://127.0.0.1` cookie exception above is only for the trusted local bridge.
A public operator endpoint must use the selected policy in
[Cloud Deployment](cloud-deployment-proposal.md#public-web-ui-ingress-policy-first-operator-access-proof):
one HTTPS origin at a GCP external Application Load Balancer, no wildcard CORS
or cross-origin credentialed requests, provider-terminated TLS with no capOS or
harness private-key custody for the bootstrap proof, capOS serving only
plain HTTP/1.1 on the backend port, no public IP on the VM, and firewall-bounded
trust in the load balancer's forwarded-scheme headers. Public sessions use
`Secure`/`HttpOnly`/`SameSite` cookies, HSTS at the HTTPS edge, CSRF
`Origin`/`Referer` checks against the known public origin, bounded idle and
absolute lifetimes, and server-side logout.

The forwarded-scheme half of that trust boundary is already implemented and
locally proven on the capOS-served service: `remote-session-web-ui` honors
`X-Forwarded-Proto` only from the recorded GCP front-end source ranges
(`130.211.0.0/22`, `35.191.0.0/16`) and treats the header from any other peer
-- or any unknown peer-address format -- as absent, so a direct client cannot
forge secure-context cookie posture. `make
run-cloud-prod-remote-session-web-ui-l4` drives both the forged-header negative
over the real ingress path and the trusted-forwarder fixture positive.

The single-public-origin half is also implemented and locally proven:
`remote-session-web-ui` reads exactly one `public_origin.<host>` manifest
marker cap (fail-closed on a second marker, a malformed, loopback-named, or
IP-literal-shaped host, or any unrecognized extra grant) and accepts the
configured
`https://<host>` origin in its `Host`/`Origin`/`Referer` gates only for
requests arriving through the trusted forwarded-scheme HTTPS path.
Cross-origin, mixed-scheme, wildcard, and missing-origin state changes fail
closed before backend-held capability dispatch, browser-supplied
principal/source hint headers are rejected on the public-origin path, no CORS
headers are ever emitted, and the loopback proof posture is unchanged. The
same proof drives a direct-client forged public Host/Origin negative over the
real ingress and the trusted-forwarder fixture positive in-process. This is
local public-origin readiness only -- no DNS name, load balancer, TLS
endpoint, or live public exposure is claimed.

Keep the proof classes separate. The landed local/QEMU self-served UI bundle
proof does not prove local cloudboot L4 over the Phase C userspace network
stack. The local cloudboot L4 proof does not prove private GCE reachability.
The private GCE proof does not authorize public IPs, firewall exposure, DNS,
TLS certificates, or operator browser exposure from the internet. The later
[`cloud-gce-public-self-hosted-webui-ingress-tls`](../tasks/on-hold/cloud-gce-public-self-hosted-webui-ingress-tls.md)
task remains on hold for explicit public-ingress/TLS authorization and must
build against the selected provider-terminated HTTPS policy rather than raw
public HTTP.


## Tauri Wrapper

The repository now contains a check/dev Tauri wrapper scaffold under
`tools/remote-session-client/src-tauri/`. It does not introduce a new
remote-session authority boundary: `make remote-session-tauri` checks
the wrapper and host Tauri prerequisites by default, and
`CAPOS_REMOTE_SESSION_TAURI_MODE=dev make remote-session-tauri`
launches `cargo tauri dev`. The webview loads
`http://127.0.0.1:3337/` from the existing `remote-session-ui` Rust
backend, so the backend still owns the gateway TCP connection, remote
session state, remote caps, and worker proxies. Webview JavaScript
receives only the same view models, user events, typed results,
denials, and redacted transcript rows as the trusted local web bridge.

The wrapper command also has a policy-only preflight:
`CAPOS_REMOTE_SESSION_TAURI_MODE=policy tools/remote-session-tauri.sh`.
That preflight runs before Tauri dependency/build checks in the normal
`check` path and does not require Tauri Linux packages or a desktop
session. It fails closed if the reviewed scaffold drifts: bundling must
stay disabled, both the Tauri `devUrl` and the single `main` window
URL must remain `http://127.0.0.1:3337`, the default capability must
grant only `core:default` to the `main` window, and the wrapper must
not add app-specific Tauri commands, invoke handlers, generate
handlers, or `tauri-plugin-*` dependencies/uses. This is a guardrail
over the current check/dev scaffold only; it is not evidence that
distributable packaging or desktop automation is reviewed.

The current check/dev wrapper therefore inherits the loopback HTTP
bridge threat model:

- **Loopback HTTP controls apply.** Host validation, Origin checks,
  CSRF tokens, per-`BrowserSession` cookies, request bounds,
  first-wins ownership, rate limiting, transcript sanitization, and
  DTO-only-to-webview discipline apply to the Tauri webview path
  unchanged because the webview talks to the same loopback backend.
- **No custom Tauri invoke authority.** The current scaffold has no
  app-specific `invoke` commands for remote-session actions. Do not
  add Tauri commands that expose raw caps, cap ids, process handles,
  endpoint owner caps, result slots, host usernames, host paths, or
  gateway connection internals to the webview.
- **Distributable packaging is still residual.** Bundling is disabled
  until the backend lifecycle is reviewed. A future packaged wrapper
  may keep a reviewed loopback sidecar or migrate to Tauri command IPC
  / custom-protocol assets, but that change must update this proposal
  and re-evaluate which loopback controls still apply. The wrapper's
  `package` mode is intentionally blocked until that review is done.
- **Webview content is the attacker.** If any non-trusted asset can
  ever load (remote frame, broken integrity check, mis-scoped asset
  protocol), webview JavaScript becomes the attacker. CSP, asset
  scope discipline, no remote frames, no `eval`-style hatches still
  apply.
- **Capability/allowlist minimization.** Lock the Tauri capability
  manifest tightly. Every `invoke` command and every core API (fs,
  shell, http, dialog, process, window, clipboard, ...) the frontend
  may call must be enumerated and minimized before distributable
  packaging is enabled. Misconfigured Tauri allowlists are the
  dominant Tauri CVE pattern; prefer per-window capability scoping over
  global allow.
- **Per-window BrowserSession isolation.** If multiple windows are
  spawned over a shared Rust state, keep per-window `BrowserSession`
  isolation matching the loopback design.
- **Carry-over controls.** Constant-time secret comparison,
  rate-limiting, idle/absolute timeouts, transcript-injection
  sanitization, DTO-only-to-webview discipline, and audit logging
  apply to the Tauri wrapper unchanged.
- **Desktop automation remains unreviewed.** The wrapper's
  `automation` mode is intentionally blocked until screenshot/input
  authority, automation-token handling, UI-smoke oracle scope, desktop
  session isolation, and fail-closed teardown have a reviewed design.


## Verification

Before the corresponding review-finding task is closed:

- Host tests cover each control above (cookie attributes, CSRF
  guard, Origin/Host validation, Content-Type rejection, CSP
  surface, header set, constant-time compare, rate limit, timeouts,
  log injection sanitization).
- The CSP refactor of `tools/remote-session-client/ui/index.html`
  ships in the same change set as the CSP header.
- The cookie-transport choice (HTTPS/secure-origin vs. interim
  plaintext-loopback no-Secure) is recorded in this proposal and the
  matching browser support matrix is documented.
- The multi-browser policy choice is recorded in this proposal and
  reflected in audit logs and DTO denial diagnostics.
- The Tauri wrapper check/dev scaffold keeps the existing loopback
  bridge controls in force, has no app-specific remote-session
  `invoke` commands, leaves distributable packaging disabled until the
  sidecar/custom-protocol/backend lifecycle is reviewed, and keeps the
  policy preflight passing as a narrow guardrail over that scaffold.
