diff --git a/SECURITY_AUDIT_ISSUE_33.md b/SECURITY_AUDIT_ISSUE_33.md deleted file mode 100644 index 97c88f5..0000000 --- a/SECURITY_AUDIT_ISSUE_33.md +++ /dev/null @@ -1,407 +0,0 @@ -# Security Audit — Issue #33: WebSocket Authentication - -> Operational note (2026-05-15): this file is retained as third-party audit evidence and claim verification. The remediation section later in this document proposed an older raw-token/hash design; that design is superseded by `SECURITY_REMEDIATION_PLAN_ISSUE_33.md` and `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md`, which use auth-v2 challenge-response, credential scopes, filesystem hardening, and per-workspace release gates. - -**Audited:** 2026-05-15 -**Branch:** `main` (HEAD) -**Issue:** https://github.com/MobileCLI/mobilecli/issues/33 -**Auditor:** Claude Code (claude-sonnet-4-6), multi-agent codebase analysis - ---- - -## Executive Summary - -GitHub issue #33 alleges 6 security findings against the MobileCLI daemon. After a full source code audit, the **code-level findings are largely accurate** for the current `main` branch. However, the framing partially misses context: - -- The README's auth token claims are **aspirational documentation that was never implemented** — not a deliberate deception. -- The actual security model is **network-level isolation** (Tailscale VPN or LAN trust), which is intentional and documented in code comments, but not communicated to users anywhere they would read it. -- The daemon source explicitly states this intent in `daemon.rs:394`: - > *"Security model: Access is controlled at the network level via: Local network: Only devices on same WiFi can connect / Tailscale: Only authenticated Tailscale network members can connect"* - -This means: for Tailscale users, the security posture is reasonable (Tailscale membership = authentication). For local-network users on untrusted WiFi, it is not. The README is wrong either way. - ---- - -## Finding-by-Finding Verdict - -### Finding 1 — No Authentication (Critical) — **ACCURATE, missing context** - -**Issue claim:** `handle_connection` accepts any WebSocket with no token check. `Config` has no `auth_token`. `ConnectionInfo.encryption_key` is always `None`. QR code contains no secret. - -**Code confirms:** - -- `daemon.rs:394`: `TcpListener::bind(format!("0.0.0.0:{}", port))` — binds all interfaces unconditionally -- `daemon.rs:497–535`: `handle_connection` calls `rx.next().await` as first action — no auth check before or after -- `setup.rs:22–30`: `Config` struct fields are `device_id`, `device_name`, `connection_mode`, `tailscale_ip`, `local_ip` — **no `auth_token` field** -- `protocol.rs:602–619`: `ConnectionInfo` has `encryption_key: Option` that is **always `None`**, never set, never checked -- `protocol.rs:630–661`: QR encodes `mobilecli://host:port?device_id=UUID&device_name=HOSTNAME[&wss=1]` — **no secret of any kind** - -**What the README says (false):** -- Line 86: *"generates a cryptographic auth token… The QR encodes a `ws://` URL with your token"* -- Line 129: *"Auth token required"* (architecture diagram) -- Line 199: *"Every WebSocket connection requires a cryptographic token generated during `mobilecli setup`. The token is stored in iOS Keychain and `~/.mobilecli/config.json`."* -- Line 200: *"Token stripping. Auth tokens are scrubbed from session output before streaming."* -- Line 322: *"config.json | Device identity, connection URL, **auth token hash**"* -- `docs/ARCHITECTURE_QUICK_REFERENCE.md:84`: *"Pairing Token (Optional): A per-device `auth_token` is generated during setup"* - -**Actual `~/.mobilecli/config.json` on disk:** -```json -{ - "connection_mode": "tailscale", - "device_id": "4fbff292-8275-473c-b958-144198219418", - "device_name": "Sandman", - "local_ip": null, - "tailscale_ip": "100.120.208.57" -} -``` -No token. Never generated. - -**Verdict:** All README auth token claims are false. The security model is network isolation, not application-layer tokens. The daemon binds `0.0.0.0` and accepts all connections with zero application-layer checks. - ---- - -### Finding 2 — Unauthenticated RCE (Critical) — **ACCURATE, minor mechanism nuance** - -**Issue claim:** `is_shell_safe` (line 1139) only blocks `` ` ``, `$(`, `\n`, `\r`, `\0`. Attacker can spawn `bash` with `args: ["-c", "malicious"]`. - -**Code confirms — `daemon.rs:1139–1145`:** -```rust -fn is_shell_safe(s: &str) -> bool { - !s.contains('\n') - && !s.contains('\r') - && !s.contains('\0') - && !s.contains('`') - && !s.contains("$(") -} -``` -Semicolons, pipes, redirects, `&`, `||`, `&&` are **not blocked**. - -**Full allowed command list (`daemon.rs:1112–1128`):** -``` -claude, codex, gemini, opencode, bash, zsh, sh, fish, nu, pwsh, powershell, python, python3, node, ruby -``` - -**Nuance on `-c` flag:** The issue says pass `args: ["-c", "malicious"]`. In practice, `shell_args_for_command` (`daemon.rs:1173–1188`) internally appends `-l -i -c ` for shell commands. However, **the command string passed to `-c` is not sanitized for semicolons/pipes**. An attacker passes `bash` with a command string containing `;` to chain arbitrary commands. The exact mechanism is slightly different from what the issue describes but the RCE risk is real and confirmed. - -**Additional finding — `SendInput` (daemon.rs:2129–2160):** -- Any connected client can inject arbitrary keystrokes into **any session by session ID** -- **No session ownership check** exists -- No restriction on which session a client can target -- This includes injecting "yes" into AI tool approval prompts (Claude Code, Codex, Gemini) - -**Verdict:** RCE risk is real. `is_shell_safe` is insufficient. `SendInput` has no ownership enforcement. - ---- - -### Finding 3 — Unauthenticated File System Access (High) — **ACCURATE** - -**Code confirms — `cli/src/filesystem/config.rs:33–50`:** - -`allowed_roots` defaults to `home_dir()` — **full home directory access**. - -Current `denied_patterns`: -``` -**/.ssh/* *.pem *.key **/id_rsa* -**/.gnupg/* **/.aws/credentials -**/.env **/.env.* **/secrets.* *.secret -**/token* **/.npmrc **/.pypirc -``` - -Supported operations: **list, read (50 MB), read_chunk, write (50 MB), create_directory, delete (recursive), rename, copy, get_file_info** - -**Confirmed denylist gaps (not blocked):** -- `~/.kube/config` — Kubernetes cluster credentials -- `~/.docker/config.json` — Docker registry credentials -- `~/.config/gcloud/` — GCP credentials -- `~/.bash_history`, `~/.zsh_history` — shell command history -- `~/.psql_history`, `~/.python_history`, `~/.node_repl_history` — REPL histories -- `~/.config/google-chrome/Default/Login Data` — Chrome saved passwords -- `~/.mozilla/firefox/**/*.sqlite` — Firefox credentials -- `~/.git-credentials`, `~/.netrc` — git/HTTP credentials -- `~/.vault-token` — HashiCorp Vault token -- `~/*.tfstate` — Terraform state (contains secrets) -- `~/.aws/**` (only `credentials` is blocked, not the whole directory) - -**Verdict:** Issue is accurate. Delete is enabled, write is enabled. Denylist approach has significant gaps. - ---- - -### Finding 4 — Listens on All Interfaces (High) — **ACCURATE, INTENTIONAL** - -`daemon.rs:394`: `TcpListener::bind(format!("0.0.0.0:{}", port))` — no conditions, always all interfaces. - -The code comment explicitly justifies this with the network-isolation security model. The concern about public WiFi/shared office networks is valid for users in local mode who did not choose Tailscale. - -**Verdict:** Accurate and intentional. Risk depends on user's network context. - ---- - -### Finding 5 — Push Notification Hijacking (Medium) — **ACCURATE** - -**Code confirms:** -- `daemon.rs:2646–2670`: `RegisterPushToken` — any connected client can register, no verification, no limit -- `daemon.rs:4500–4549`: Sends to `https://exp.host/--/api/v2/push/send` (Expo Push API) -- Push payload includes: session ID, title, body, "waiting_for_input" type - -**README line 50:** *"No cloud. No accounts. No relay servers. Just a direct WebSocket between your machine and your phone."* - -This is **false** — Expo push uses an external cloud relay service. - -**Verdict:** Any client can register push tokens. "No cloud" claim is false for users with push notifications. The prompt content risk is slightly overstated — push body says "waiting for input" with session ID, not the actual prompt text. - ---- - -### Finding 6 — No Binary Integrity Verification (Medium) — **ACCURATE** - -- `install.sh:94`: Downloads binary from GitHub releases -- No SHA-256 verification anywhere in the script -- `install.sh:117–119`: Escalates to `sudo` with only a printed warning, no explicit confirmation prompt - -**Verdict:** Accurate. No checksum verification exists. - ---- - -## Summary Table - -| Finding | Verdict | Notes | -|---------|---------|-------| -| No auth token in code | TRUE | Never implemented | -| README auth token claims | TRUE — README is outdated/false | Aspirational docs, never built | -| RCE via SpawnSession + `is_shell_safe` gaps | TRUE | Semicolons/pipes not blocked | -| `-c` flag injection specifically | PARTIALLY TRUE | `-c` is added internally, but semicolons in command string still allow injection | -| `SendInput` injects into any session | TRUE | No ownership check | -| Filesystem: home dir default | TRUE | `allowed_roots = [home_dir()]` | -| Filesystem: denylist gaps | TRUE | Many sensitive paths uncovered | -| `0.0.0.0` binding | TRUE, INTENTIONAL | Necessary for mobile; risk depends on network | -| Push token registration unvalidated | TRUE | Any client registers | -| "No cloud" contradicted by Expo push | TRUE | Expo uses `exp.host` external service | -| `install.sh` no checksums | TRUE | Confirmed | -| `install.sh` silent sudo | PARTIALLY TRUE | Prints warning, no "proceed?" prompt | - ---- - ---- - -# Remediation Plan - -## Fix 1 — Implement Application-Layer Auth Token - -**Goal:** Build what the README claims. Generate a real token during setup, embed it in the QR code, verify it on every WebSocket connection. - -### CLI changes - -**`cli/src/setup.rs`** -- Add `auth_token_hash: String` field to `Config` struct -- During setup wizard: generate 32 cryptographically random bytes (`rand` or `getrandom` crate), hex-encode as raw token -- SHA-256 hash the raw token, store hex-encoded hash in `config.json` as `auth_token_hash` - -**`cli/src/protocol.rs`** -- Update `ConnectionInfo.to_compact_qr()` to append `&token={raw_token}` to QR URL -- Add `token: Option` field to `ConnectionInfo` -- Add `ClientMessage::Auth { token: String }` variant to client message enum - -**`cli/src/daemon.rs`** -- Load `config.json` at startup, read `auth_token_hash` -- In `handle_connection` (~line 497): expect first client message to be `ClientMessage::Auth { token }`. Compute `sha256hex(token)`, compare to stored hash. Close connection if mismatch. -- Add ~1 second constant-time delay on auth failure to resist timing attacks - -**`cli/Cargo.toml`** -- Add `sha2 = "0.10"` (or `ring`) -- Confirm `rand` or `getrandom` present - -### Mobile app changes (`mobile/`) - -- Parse `token` query param from deep-link/QR URL -- Store raw token in iOS Keychain (`kSecAttrService = "mobilecli"`) and Android Keystore -- On WebSocket connect, send `{ "type": "auth", "token": "" }` as **first message** before any other message - ---- - -## Fix 2 — Session Ownership for SendInput - -**Goal:** Only the client that spawned a session can inject input into it. - -**`cli/src/daemon.rs`** -- Generate `connection_id: Uuid` per WebSocket connection at start of `handle_connection` -- Add `owner_connection_id: Uuid` to the session struct -- In `SpawnSession` handler: set `session.owner_connection_id = connection_id` -- In `SendInput` handler (~line 2129): verify `connection_id == session.owner_connection_id`, reject if mismatch -- Apply same ownership check to `ResizeSession` and `KillSession` - ---- - -## Fix 3 — Strengthen `is_shell_safe` - -**`cli/src/daemon.rs` (line 1139)** - -Replace current implementation with: -```rust -fn is_shell_safe(s: &str) -> bool { - !s.contains('\n') - && !s.contains('\r') - && !s.contains('\0') - && !s.contains('`') - && !s.contains("$(") - && !s.contains(';') // command chaining - && !s.contains("||") // OR chaining - && !s.contains("&&") // AND chaining - && !s.contains('>') // stdout redirect - && !s.contains('<') // stdin redirect - && !s.contains('&') // background / AND - && !s.contains('|') // pipe -} -``` - -Also audit every call site to confirm `is_shell_safe` is applied to both the command string **and** each arg in `SpawnSession`. Apply it to args if not already done. - ---- - -## Fix 4 — Tighten Filesystem Denylist - -**`cli/src/filesystem/config.rs` (lines 36–50)** - -Append to `denied_patterns`: -```rust -// Kubernetes -"**/.kube/**", -// Docker -"**/.docker/config.json", -"**/.docker/contexts/**", -// Google Cloud -"**/.config/gcloud/**", -// Shell histories -"**/.bash_history", -"**/.zsh_history", -"**/.sh_history", -"**/.fish/fish_history", -"**/.local/share/fish/fish_history", -// REPL histories -"**/.psql_history", -"**/.python_history", -"**/.node_repl_history", -"**/.irb_history", -// Browser credentials (Linux) -"**/.config/google-chrome/Default/Login Data", -"**/.config/chromium/Default/Login Data", -"**/.mozilla/firefox/**/*.sqlite", -// macOS Keychain -"**/Library/Keychains/**", -// Git credentials -"**/.git-credentials", -"**/.config/git/credentials", -// netrc -"**/.netrc", -// Vault / 1Password -"**/.vault-token", -"**/.config/op/**", -// yarn tokens -"**/.yarnrc", -"**/.config/yarn/**", -// Terraform state (contains secrets) -"**/*.tfstate", -"**/*.tfstate.backup", -// Broader AWS (not just credentials file) -"**/.aws/**", -// age encryption keys -"**/*.age", -``` - ---- - -## Fix 5 — Add Checksum Verification to install.sh - -**`install.sh` (after the download block, ~line 94)** - -```bash -# Download checksum file published alongside the release binary -checksum_url="${download_url}.sha256" -download_file "${checksum_url}" "${tmp_dir}/${archive_name}.sha256" - -# Verify integrity -if command -v sha256sum >/dev/null 2>&1; then - (cd "${tmp_dir}" && sha256sum --check "${archive_name}.sha256") \ - || die "SHA-256 checksum verification failed — binary may be corrupt or tampered with" -elif command -v shasum >/dev/null 2>&1; then - (cd "${tmp_dir}" && shasum -a 256 -c "${archive_name}.sha256") \ - || die "SHA-256 checksum verification failed — binary may be corrupt or tampered with" -else - warn "Neither sha256sum nor shasum found — skipping checksum verification (not recommended)" -fi -``` - -**Also update the GitHub Actions release workflow** (`.github/workflows/release.yml` or equivalent) to generate and upload `{archive}.sha256` files alongside each release binary. - ---- - -## Fix 6 — Update README and Docs - -**`README.md`** — remove/replace these specific false claims: - -| Location | Remove | Replace with | -|----------|--------|--------------| -| Line 86 | "generates a cryptographic auth token… QR encodes a `ws://` URL with your token" | Accurate description after Fix 1 is live | -| Line 129 | "Auth token required" in architecture diagram | "Auth token required (256-bit, verified on connect)" | -| Line 199 | Entire auth token paragraph | Accurate description of the token system after Fix 1 | -| Line 200 | "Token stripping. Auth tokens are scrubbed from session output…" | Remove entirely | -| Line 322 | "auth token hash" in config.json table | Remove or update | - -**Add a "Security Model" section** explaining: -- **Token auth:** 256-bit token generated during `mobilecli setup`. Raw token embedded in QR. Hash stored in `config.json`. Required for every WebSocket connection. -- **Local mode:** Token auth + LAN access. Suitable for trusted home/office networks. -- **Tailscale mode:** Token auth + Tailscale membership. Recommended for remote/public access. -- **Push notifications:** The only outbound network call is Expo push (`exp.host`) for "waiting for input" alerts. No terminal output leaves your machine. - -**`docs/ARCHITECTURE_QUICK_REFERENCE.md:84`** — replace false "Pairing Token (Optional)" claim with accurate post-Fix 1 description. - ---- - -## Fix 7 — Push Token Rate Limiting - -**`cli/src/daemon.rs` (~line 2646)** - -- After Fix 1 lands, push token registration is already gated behind auth -- Add a cap: maximum 3 push tokens per authenticated `device_id` -- Scope push tokens to `device_id` so re-registration replaces rather than accumulates - ---- - -## Implementation Order - -| Priority | Fix | Scope | Notes | -|----------|-----|-------|-------| -| 1 | Fix 3 — `is_shell_safe` | Small, 1 function | Highest bang-for-buck, isolated change | -| 2 | Fix 4 — filesystem denylist | Small, 1 file | Low risk, append-only | -| 3 | Fix 6 — README/docs | No code changes | Fix the false claims immediately | -| 4 | Fix 5 — install.sh checksums | install.sh + release workflow | Self-contained | -| 5 | Fix 2 — session ownership | daemon.rs only | Moderate scope, no protocol change | -| 6 | Fix 1 — auth token system | daemon + protocol + mobile app | Largest; breaks existing clients; do last | -| 7 | Fix 7 — push rate limit | After Fix 1 | Trivial once Fix 1 is done | - ---- - -## Critical Files - -| File | Relevant Fixes | -|------|---------------| -| `cli/src/daemon.rs` | Fix 1, 2, 3, 7 — auth, ownership, is_shell_safe, push limit | -| `cli/src/setup.rs` | Fix 1 — token generation, Config struct | -| `cli/src/protocol.rs` | Fix 1 — QR encoding, ClientMessage::Auth | -| `cli/src/filesystem/config.rs` | Fix 4 — denylist extension | -| `cli/Cargo.toml` | Fix 1 — sha2/rand deps | -| `install.sh` | Fix 5 — checksum verification | -| `.github/workflows/release.yml` | Fix 5 — publish .sha256 files | -| `README.md` | Fix 6 — remove false claims, add Security Model | -| `docs/ARCHITECTURE_QUICK_REFERENCE.md` | Fix 6 — remove false claim line 84 | -| `mobile/` (WebSocket connect code) | Fix 1 — send Auth message, store token | - ---- - -## Verification Checklist - -- [ ] **Fix 1:** `websocat ws://localhost:9847` closes immediately with error. Client with correct token gets `Welcome`. -- [ ] **Fix 2:** Client A spawns session. Client B sends `SendInput` to that session ID → rejected. -- [ ] **Fix 3:** `SpawnSession { command: "bash", args: ["; echo pwned"] }` → rejected by `is_shell_safe`. -- [ ] **Fix 4:** Read `~/.kube/config` and `~/.bash_history` via file bridge → denied. -- [ ] **Fix 5:** Corrupt downloaded binary, run `install.sh` → fails with checksum error message. -- [ ] **Fix 6:** Full README read — no false auth token claims remain. -- [ ] **End-to-end:** Setup → QR scan → mobile connects with token → sessions work normally. diff --git a/SECURITY_COMPLETION_AUDIT_ISSUE_33.md b/SECURITY_COMPLETION_AUDIT_ISSUE_33.md deleted file mode 100644 index 3eb4851..0000000 --- a/SECURITY_COMPLETION_AUDIT_ISSUE_33.md +++ /dev/null @@ -1,86 +0,0 @@ -# Security Completion Audit - Issue #33 - -Date: 2026-05-15 -Status: not complete for release; local implementation and local verification are complete for the current checkout - -## Objective Restated - -The user asked for a thorough issue #33 remediation plan and implementation path that covers: - -- the truth of the GitHub issue #33 claims and Claude audit notes -- a completed remediation plan document -- all desktop daemon/CLI/security components -- all mobile app/security components -- docs, website, installer, and release workflows -- Linux, macOS, Windows, iOS, and Android compatibility -- use of subagents for speed/depth -- no premature coding-only conclusion before release/device gates are known - -## Prompt-To-Artifact Checklist - -| Requirement | Artifact or evidence | Current state | -| --- | --- | --- | -| Audit issue #33 claims | `SECURITY_AUDIT_ISSUE_33.md` plus local code review | Historical audit retained; operational note says raw-token remediation is superseded by auth-v2 plan | -| Complete remediation plan | `SECURITY_REMEDIATION_PLAN_ISSUE_33.md` | Updated with implemented local state, remaining release gates, transport limitations, and verification status | -| Executable release/device plan | `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md` | Added host/mobile/network matrix, EAS/TestFlight/internal build order, evidence tables, and final sign-off table | -| Release evidence capture | `SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md` | Added a manifest/evidence template for commits, builds, artifacts, platform smoke, security smoke, and release sign-off | -| Desktop auth-v2 | CLI protocol/auth/daemon changes | Implemented locally with challenge-response, credential scopes, revocation, timeout/size cap, and unit coverage | -| Local PTY registration hardening | CLI daemon/wrapper/auth changes | Implemented local PTY proof so loopback clients cannot register unauthenticated PTY sessions; daemon-side validation has focused unit coverage | -| Desktop filesystem hardening | CLI filesystem/daemon/setup changes | Implemented project roots, denied secrets, destructive opt-in, safe empty roots, Windows-style pattern normalization, copy/search clamps | -| Desktop spawn hardening | CLI daemon changes | Mobile spawn uses supported profiles, rejects args/paths/interpreter flags, and defaults to approved roots instead of home | -| Desktop config secret handling | CLI setup/platform changes | Unix private writes, Windows ACL failures surfaced, no project-local config fallback when home is missing | -| Mobile auth-v2 | Mobile QR/auth/sync changes | Implemented QR auth fields, proof generation, pre-auth message gating, auth-state tests | -| Mobile credential storage | Mobile devices/storage changes | Pairing tokens are SecureStore-only per device; AsyncStorage stores metadata only; old inline tokens migrate/scrub | -| Mobile push behavior | Mobile sync/push changes | Push registration waits for auth; opt-out unregister state persists across app termination | -| Mobile native build need | Plan/runbook/checklist | New build required; current configured floor is iOS build `112`, Android versionCode `92` | -| Android release signing | Android Gradle/preflight | Release no longer signs with debug config; preflight fails if that pattern returns | -| Installer integrity | Root/website installer and release workflow | Installer verifies GitHub Release checksums; root and website scripts are byte-for-byte identical locally | -| Root release workflow | `.github/workflows/release.yml` | Adds CLI gates, tag/version sync, native release-runner tests, archive completeness, checksum completeness | -| Per-workspace CI | root/mobile/website workflows | Added/updated local workflow files; note mobile and website are separate nested workspaces and must be committed/released separately | -| Public docs accuracy | README/docs/website/LLM artifacts | Updated auth-v2/manual pairing/Tailscale/Expo/filesystem/install claims; stale command scan has no unsupported flag hits except intentional warnings | -| Subagent depth | Subagent results in session | Desktop, mobile, and docs/release subagents found residual blockers; local patch set addresses all actionable code findings except transport confidentiality | -| Linux compatibility | Local target check and CI matrix | `cargo check --target x86_64-unknown-linux-gnu` passed locally; release/manual Linux ARM64 still external | -| Windows compatibility | Local target check and docs/workflow | `cargo check --target x86_64-pc-windows-gnu` passed locally; real Windows smoke and ACL inspection still external | -| macOS compatibility | Workflow/runbook | Planned in CI/release and runbook; no local macOS runner evidence in this sandbox | -| iOS compatibility | Mobile build metadata/runbook | Build numbers updated and TypeScript/preflight pass; real TestFlight/device smoke still external | -| Android compatibility | Mobile build metadata/runbook | versionCode updated and preflight passes; production cleartext and signing evidence still external | - -## Fresh Local Verification Evidence - -Commands passed after the latest edits: - -- `cargo fmt --check` -- `cargo clippy --all-targets -- -D warnings -A dead-code` -- `cargo test` (`74 passed`) -- `cargo check --target x86_64-unknown-linux-gnu` -- `cargo check --target x86_64-pc-windows-gnu` -- `npx tsc --noEmit` in `mobile/` -- `npm run test:security` in `mobile/` -- `npm run preflight:release` in `mobile/` with expected warnings for unavailable EAS env access and dirty worktree -- `npm run build` in `website/` -- `bash -n install.sh` -- `bash -n website/public/install.sh` -- `diff -u install.sh website/public/install.sh` -- `bash scripts/test-installer-checksum.sh` -- workflow YAML parse across root, mobile, and website workflows -- `git diff --check`, `git -C mobile diff --check`, `git -C website diff --check` - -## Not Yet Achieved - -The overall release objective is not complete until these are done outside this sandbox: - -- iOS physical-device smoke on LAN and Tailscale -- Android physical-device smoke if Android is in release scope -- EAS/TestFlight/internal build evidence for the new mobile app build -- production Android cleartext decision and smoke evidence -- macOS Apple Silicon and Intel release-runner evidence -- Linux x86_64 and ARM64 release artifact smoke -- Windows x64 release artifact smoke and config ACL inspection -- actual GitHub Release `SHA256SUMS.txt` verification -- installer corrupt/missing checksum negative tests against draft release artifacts -- final transport security stance: trusted LAN/Tailscale only, or add `wss://`/message encryption before claiming MITM resistance -- final sign-off table in `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md` - -## Completion Decision - -Do not mark the active goal complete yet. The local implementation and local verification are substantially complete, but the stated objective includes all devices and cross-platform compatibility. Those requirements still need physical-device, EAS/store, release-artifact, and real platform evidence. diff --git a/SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md b/SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md deleted file mode 100644 index 8b32d51..0000000 --- a/SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md +++ /dev/null @@ -1,120 +0,0 @@ -# Security Release Evidence Template - Issue #33 - -Copy this template for the actual release candidate evidence packet. Do not use it as a substitute for running `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md`; this file is where the resulting evidence is recorded. - -## Release Manifest - -| Surface | Value | Evidence | -| --- | --- | --- | -| Root repository commit | | `git rev-parse HEAD` | -| Root release tag | | `git tag --points-at HEAD` or release URL | -| CLI crate version | | `cli/Cargo.toml` package version | -| Mobile repository commit | | `git -C mobile rev-parse HEAD` | -| iOS build | | EAS/TestFlight build URL and build number | -| Android build | | EAS/internal build URL and versionCode, or deferred decision | -| Website repository commit | | `git -C website rev-parse HEAD` | -| Website deploy URL | | deploy preview or production URL | -| Transport stance | | trusted LAN/Tailscale only, Tailscale-only, or `wss://`/message encryption | -| Android release stance | | public, internal-only, or deferred | - -## Local Preflight Evidence - -| Gate | Command | Result | Evidence path/link | -| --- | --- | --- | --- | -| CLI formatting | `cd cli && cargo fmt --check` | | | -| CLI lint | `cd cli && cargo clippy --all-targets -- -D warnings -A dead-code` | | | -| CLI tests | `cd cli && cargo test` | | | -| Linux target check | `cd cli && cargo check --target x86_64-unknown-linux-gnu` | | | -| Windows target check | `cd cli && cargo check --target x86_64-pc-windows-gnu` | | | -| Mobile TypeScript | `cd mobile && npx tsc --noEmit` | | | -| Mobile security tests | `cd mobile && npm run test:security` | | | -| Mobile release preflight | `cd mobile && npm run preflight:release` | | | -| Website build | `cd website && npm run build` | | | -| Installer syntax | `bash -n install.sh && bash -n website/public/install.sh` | | | -| Installer parity | `diff -u install.sh website/public/install.sh` | | | -| Installer checksum tests | `bash scripts/test-installer-checksum.sh` | | | -| Workflow YAML parse | see runbook command | | | -| Whitespace diff check | root/mobile/website `git diff --check` | | | - -## Mobile Build Evidence - -| Platform | Required value | Recorded value | Result | -| --- | --- | --- | --- | -| iOS marketing version | matches app release | | | -| iOS build number | `112` or higher | | | -| iOS EAS build URL | production/TestFlight candidate | | | -| iOS App Store/TestFlight status | installable by testers | | | -| Android versionName | matches app release if shipped | | | -| Android versionCode | `92` or higher if shipped | | | -| Android EAS build URL | internal candidate or deferred | | | -| Android signing evidence | not debug-signed if shipped | | | -| Android production cleartext decision | verified or deferred | | | - -## Desktop Release Artifact Evidence - -| Archive | Present in release | SHA256SUMS entry | Install smoke | Result | -| --- | --- | --- | --- | --- | -| Linux x86_64 `.tar.gz` | | | | | -| Linux ARM64 `.tar.gz` | | | | | -| macOS Intel `.tar.gz` | | | | | -| macOS Apple Silicon `.tar.gz` | | | | | -| Windows x64 `.zip` | | | | | - -Negative installer tests against draft release: - -| Scenario | Expected | Result | Evidence | -| --- | --- | --- | --- | -| Corrupted archive | fails before extraction | | | -| Missing checksum entry | fails before extraction | | | -| Invalid checksum entry | fails before extraction | | | -| Wrong archive name | fails before extraction | | | - -## Physical Device Smoke Evidence - -| Host | Mobile | Network | Auth pair | Restart reconnect | Bad token | Revoked credential | Sessions/control | Filesystem | Push | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| macOS Apple Silicon | iOS | LAN | | | | | | | | | -| macOS Apple Silicon | iOS | Tailscale | | | | | | | | | -| macOS Apple Silicon | Android | LAN/Tailscale or deferred | | | | | | | | | -| macOS Intel | iOS | LAN/Tailscale | | | | | | | | | -| Linux x86_64 | iOS | LAN/Tailscale | | | | | | | | | -| Linux ARM64 | iOS | LAN/Tailscale | | | | | | | | | -| Windows x64 | iOS | LAN/Tailscale | | | | | | | | | - -## Security-Specific Evidence - -| Check | Expected | Result | Evidence | -| --- | --- | --- | --- | -| Legacy `hello` first message | `auth_required`, no sensitive data | | | -| Invalid proof | `auth_invalid`, no client registration | | | -| Revoked credential | no reconnect, no PTY/filesystem/push data | | | -| PTY registration without local proof | rejected | | | -| Mobile spawn without working dir | uses approved root or fails | | | -| MobileCLI config path read | denied | | | -| OS secret paths | denied | | | -| Delete/rename default | denied | | | -| Delete/rename opt-in | allowed only in approved throwaway root | | | -| Push opt-out while offline | unregister drains after reconnect | | | -| AsyncStorage device metadata | no `authToken` or `auth_token` | | | - -## Final Sign-Off - -| Area | Owner | Status | Evidence | -| --- | --- | --- | --- | -| Desktop CLI release | | | | -| Mobile iOS release | | | | -| Mobile Android release/defer decision | | | | -| Website/docs release | | | | -| Transport stance approved | | | | -| Security smoke complete | | | | -| Rollback plan ready | | | | - -Release decision: - -- [ ] Ship stable release -- [ ] Ship prerelease only -- [ ] Block release pending fixes - -Decision notes: - -- diff --git a/SECURITY_RELEASE_RUNBOOK_ISSUE_33.md b/SECURITY_RELEASE_RUNBOOK_ISSUE_33.md deleted file mode 100644 index 8f284f0..0000000 --- a/SECURITY_RELEASE_RUNBOOK_ISSUE_33.md +++ /dev/null @@ -1,327 +0,0 @@ -# Security Release Runbook - Issue #33 - -Date: 2026-05-15 -Audience: MobileCLI maintainers running the release candidate on real desktop and mobile devices - -## Purpose - -This runbook turns the issue #33 remediation plan into an executable release checklist. The release is not ready until every supported host/mobile/network cell below has a recorded pass or an explicit product decision that the cell is out of scope for this release. - -The reader should be able to take a desktop release candidate, a mobile build candidate, and real devices, then prove the new auth-v2, bind, filesystem, push, installer, and documentation behavior before shipping. - -Use `SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md` to record the evidence produced while running this checklist. - -## Release Candidates Under Test - -Record the exact artifacts before testing starts. - -| Component | Candidate | Required evidence | -| --- | --- | --- | -| Desktop CLI | Git SHA and release archive name | `mobilecli --version`, archive checksum, host OS | -| iOS app | TestFlight/App Store candidate | marketing version, build number, device model, iOS version | -| Android app | Internal/App Bundle candidate | marketing version, versionCode, device model, Android version | -| Website/docs | Deploy preview or commit SHA | URL or commit SHA | - -Expected mobile build floor for this remediation: - -- iOS build: `112` or higher. -- Android versionCode: `92` or higher. - -## Cross-Repository Ownership - -This checkout contains three separate release surfaces. The root repository release workflow does not automatically publish or gate the nested mobile and website workspaces. - -| Workspace | Release owner | Required before stable desktop release | -| --- | --- | --- | -| Root/CLI | Desktop release owner | Root CI green, tag matches `cli/Cargo.toml`, release artifacts and checksums verified | -| Mobile | Mobile release owner | Mobile CI green, EAS/TestFlight/internal build available, physical smoke complete | -| Website | Website release owner | Website CI green, deploy preview/stable deploy uses matching installer and security docs | - -Record the root commit/tag, mobile commit/build IDs, and website commit/deploy URL together. Do not ship the desktop auth-enforcing stable release before the compatible mobile build is installable by the intended testers/users. - -## Transport Security Decision - -Auth-v2 authenticates the paired app but does not make `ws://` confidential. Record one release stance before manual testing starts: - -| Stance | Allowed release claim | -| --- | --- | -| LAN plus Tailscale, no message-layer encryption | Safe on trusted LANs and Tailnets; not safe against hostile same-LAN MITM | -| Tailscale-only for remote/untrusted networks | Remote access requires Tailnet membership; same-LAN remains trusted-network only | -| `wss://` or encrypted message layer added | May claim protection against network observers only after MITM tests pass | - -For the current remediation, use Tailscale for untrusted networks and do not recommend public tunnels or untrusted WiFi LAN use. - -## Mobile Compatibility Release Order - -Run this before the stable desktop release: - -1. Verify mobile build numbers are at or above the floor listed above. -2. Run mobile CI and local preflight. -3. Confirm EAS production environment values and signing credentials. -4. Produce iOS TestFlight/App Store candidate. -5. Produce Android internal candidate only if Android is in scope for this release; otherwise record Android as deferred/internal-only in the final sign-off. -6. Install the candidate on real devices and complete the pairing/auth smoke matrix below. -7. Only after compatible mobile builds pass, publish the desktop release that enforces auth-v2. - -## Preflight Gates - -Run these before manual device testing. Failures block release-candidate testing unless the maintainer records why the failed gate is unrelated. - -Desktop CLI: - -```bash -cd cli -cargo fmt --check -cargo clippy --all-targets -- -D warnings -A dead-code -cargo test -cargo check --target x86_64-unknown-linux-gnu -``` - -Mobile app: - -```bash -cd mobile -npx tsc --noEmit -npm run test:security -npm run preflight:release -``` - -Android release candidates must not use `signingConfigs.debug`. If Android is in scope, run a signing report or EAS build evidence check and attach the result. - -Website/docs: - -```bash -cd website -npm run build -``` - -Installer parity: - -```bash -bash -n install.sh -bash -n website/public/install.sh -diff -u install.sh website/public/install.sh -``` - -Workflow syntax: - -```bash -python3 -c 'import pathlib, yaml; [yaml.safe_load(p.read_text()) for p in pathlib.Path(".github/workflows").glob("*.yml")]' -python3 -c 'import pathlib, yaml; [yaml.safe_load(p.read_text()) for p in pathlib.Path("mobile/.github/workflows").glob("*.yml")]' -python3 -c 'import pathlib, yaml; [yaml.safe_load(p.read_text()) for p in pathlib.Path("website/.github/workflows").glob("*.yml")]' -``` - -## Host And Device Matrix - -Supported host cells: - -| Host | Required? | Notes | -| --- | --- | --- | -| macOS Apple Silicon | Yes | Primary local/iMac path. | -| macOS Intel | Yes, if release archive is shipped | Can be CI plus manual smoke if no physical host is available. | -| Linux x86_64 | Yes | Include a machine without Tailscale and one with Tailscale where possible. | -| Linux ARM64 | Yes, if release archive is shipped | CI build plus at least install/start smoke on real hardware or VM. | -| Windows x86_64 | Yes | Must run in user session, not as a Windows service. | - -Supported mobile cells: - -| Mobile | Required? | Notes | -| --- | --- | --- | -| iOS candidate | Yes | Test LAN and Tailscale. | -| Android candidate | Yes if Android ships; otherwise document internal-only status | Must verify production `ws://` behavior. | - -Network cells: - -| Network | Required? | Expected security property | -| --- | --- | --- | -| Same LAN | Yes | App-layer auth blocks unauthenticated clients on reachable LAN. | -| Tailscale | Yes | Tailnet limits reachability, auth-v2 remains enforced. | - -## Desktop Install And Setup - -Run once per host OS. - -1. Install the desktop candidate from the release archive or local build. -2. Confirm `mobilecli --version` reports the candidate version. -3. Run `mobilecli setup`. -4. Select the intended connection mode for the current test cell. -5. Confirm the daemon does not silently bind `0.0.0.0` in local or Tailscale mode. -6. Confirm setup creates a first mobile credential and shows an auth-v2 QR. -7. Run `mobilecli credentials list` and confirm the credential appears without a raw token. -8. Confirm the config file contains credentials/verifiers but no raw `auth_token`. -9. On Unix hosts, confirm the config file is owner-only readable/writable. -10. On Windows, confirm the config file is under the user profile and not readable by ordinary unrelated users. - -Evidence to record: - -| Host | Mode | Bound addresses | Credential ID | Config permission checked | Result | -| --- | --- | --- | --- | --- | --- | - -## Pairing And Auth - -Run for every host/mobile/network cell. - -1. Start the daemon. -2. Pair by scanning the QR code. -3. Confirm the mobile app does not show connected until after the daemon sends authenticated welcome. -4. Confirm sessions are listed after auth succeeds. -5. Restart the mobile app and confirm it reconnects without rescanning. -6. Restart the daemon and confirm the mobile app reconnects without rescanning. -7. Pair manually using URL, credential ID, server ID, and token. Confirm URL-only manual pairing is rejected or impossible. -8. Attempt to connect with an altered token. Expected: no welcome, no sessions, no push registration, and a visible auth error. -9. Run `mobilecli credentials revoke ` for the paired device, then attempt reconnect. Expected: revoked device cannot reconnect or receive further data. -10. Run `mobilecli pair --rotate`, pair again, and confirm the old credential remains revoked. - -Evidence to record: - -| Host | Mobile | Network | QR pair | Manual pair | App restart | Daemon restart | Bad token rejected | Revoked rejected | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | - -## Session And Terminal Controls - -Run for every host/mobile/network cell where pairing passes. - -1. Start an existing terminal session from desktop. -2. Confirm the session appears on mobile only after auth. -3. Subscribe from mobile and confirm PTY output appears. -4. Send input from mobile. -5. Resize/rotate the mobile terminal and confirm the host session remains usable. -6. Trigger a tool-approval or wait-state notification path and confirm the mobile prompt flow works. -7. Spawn each supported profile from mobile: Claude, Codex, Gemini, OpenCode where installed, and shell. -8. Confirm unsupported absolute paths and interpreter flags cannot be spawned from mobile. -9. Close a session from mobile and confirm the desktop state updates. -10. Confirm a second mobile device with a revoked or missing credential cannot receive PTY bytes for subscribed sessions. - -Evidence to record: - -| Host | Mobile | Network | Session list | Subscribe | Input | Resize | Approval | Spawn profiles | Unauthorized blocked | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | - -## Push Notifications - -Run on iOS and Android if that platform ships. - -1. Grant push permission on the mobile app. -2. Confirm push token registration happens only after auth. -3. Trigger a waiting state and confirm a notification arrives. -4. Re-register after app restart and confirm duplicate tokens do not accumulate for the same installation. -5. Revoke the credential and trigger another waiting state. Expected: no push to the revoked installation. -6. Unregister/disable push from mobile and confirm future waiting states do not notify that device. - -Evidence to record: - -| Mobile | Host | Network | Register after auth | Notification received | No duplicate token | Revocation suppresses push | Unregister works | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | - -## Filesystem Bridge - -Run for at least one host per OS family, and for every mobile platform that ships. - -1. Confirm fresh setup exposes only approved/project roots, not the entire home directory. -2. List an allowed root. -3. Read an allowed file. -4. Write a small allowed file. -5. Upload an attachment into the project upload cache. -6. Attempt to read MobileCLI config/auth files. Expected: denied. -7. Attempt to read common secret paths for that OS, such as SSH keys, shell history, cloud credentials, keychains, or Windows credential locations. Expected: denied. -8. Attempt search with an excessive result limit. Expected: server clamps to configured maximum. -9. Attempt copy with content over the configured limit. Expected: denied. -10. Confirm delete and rename are unavailable or denied by default. -11. Explicitly enable destructive operations in setup/config on a throwaway directory, then confirm delete/rename work only inside the approved root. -12. If whole-home access is enabled for testing, confirm the warning is shown and sensitive denylist paths remain blocked. -13. On Windows, test drive-prefix, case-insensitive, UNC/verbatim, junction, and reserved-name behavior where practical. - -Evidence to record: - -| Host | Mobile | Allowed read/write | Upload | Config denied | Secret denied | Search clamped | Copy capped | Delete default denied | Opt-in delete scoped | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | - -## Network And Bind Behavior - -Run once per host OS, then spot-check with mobile. - -1. Local mode binds loopback plus the selected LAN address. -2. Local mode does not bind all interfaces unless explicitly configured as advanced opt-in. -3. Tailscale mode binds loopback plus the selected Tailscale address. -4. Tailscale mode fails closed if Tailscale is unavailable or disconnected. -5. Custom mode warns when the address is public or all-interface. -6. PTY registration is accepted from loopback and rejected from non-loopback. -7. A legacy client that sends `hello` first receives `auth_required` and no sensitive data. -8. An idle unauthenticated socket closes after the first-message timeout. -9. An oversized first message is rejected before auth. - -Evidence to record: - -| Host | Mode | Loopback | Selected bind | No implicit all-interface | Tailscale fail-closed | Legacy hello rejected | Idle socket closed | Oversized first message rejected | Result | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | - -## Installer And Release Integrity - -Run against the actual draft GitHub Release before publishing. - -1. Confirm `SHA256SUMS.txt` exists in the release. -2. Confirm every published archive appears exactly once in `SHA256SUMS.txt`. -3. Download the installer from the website path and root path if both are distributed. -4. Install a valid archive and confirm checksum verification passes before extraction. -5. Corrupt the archive and confirm install fails before extraction. -6. Remove or rename the archive checksum entry and confirm install fails. -7. Confirm Linux, macOS, and Windows archives unpack to the expected binary name. -8. Confirm no install path executes downloaded archive contents before hash verification. - -Evidence to record: - -| Archive | Checksum present | Valid install | Corrupt fails | Missing checksum fails | Binary smoke | Result | -| --- | --- | --- | --- | --- | --- | --- | - -## Upgrade And Migration - -Run with backups of old configs/devices. - -1. Upgrade a desktop config that has no credentials. -2. Confirm network/device fields are preserved. -3. Confirm remote mobile bind is locked until `mobilecli pair` or `mobilecli setup` creates credentials. -4. Upgrade a mobile app with an existing linked device that has no auth-v2 fields. -5. Confirm the app does not send privileged queued messages to the auth-v2 daemon before re-pairing. -6. Re-pair and confirm old mobile settings are replaced with auth-v2 fields. -7. Confirm uninstall/reinstall behavior is acceptable for SecureStore and AsyncStorage fallback cases on both iOS and Android. - -Evidence to record: - -| Scenario | Desktop state preserved | Remote bind locked | Mobile queue gated | Re-pair succeeds | Result | -| --- | --- | --- | --- | --- | --- | - -## Docs And Privacy Review - -Before stable release: - -1. Read root README, CLI README, architecture docs, Windows docs, website docs, website privacy/terms, and LLM artifacts. -2. Confirm no page claims that auth was already token-based before this release. -3. Confirm no page claims URL-only manual pairing is sufficient. -4. Confirm no page recommends public tunnels for normal use. -5. Confirm push documentation names Expo push while Expo remains in use. -6. Confirm filesystem docs say whole-home and destructive operations are opt-in. -7. Confirm installer docs say archives are checksum verified. - -Evidence to record: - -| Surface | Auth-v2 accurate | Tailscale/LAN accurate | Expo push disclosed | Filesystem accurate | Installer accurate | Result | -| --- | --- | --- | --- | --- | --- | --- | - -## Final Sign-Off - -Release may proceed only after this table is complete. - -| Area | Owner | Evidence link/path | Status | -| --- | --- | --- | --- | -| Desktop CLI local checks | | | | -| Desktop cross-platform build/test | | | | -| iOS physical smoke | | | | -| Android physical smoke | | | | -| LAN auth/bind smoke | | | | -| Tailscale auth/bind smoke | | | | -| Filesystem bridge smoke | | | | -| Push notification smoke | | | | -| Installer checksum smoke | | | | -| Website/docs truth sweep | | | | - -Any failed row must produce either a fix before release or an explicit release-scope decision recorded in the remediation plan. diff --git a/SECURITY_REMEDIATION_PLAN_ISSUE_33.md b/SECURITY_REMEDIATION_PLAN_ISSUE_33.md deleted file mode 100644 index bbc84e0..0000000 --- a/SECURITY_REMEDIATION_PLAN_ISSUE_33.md +++ /dev/null @@ -1,534 +0,0 @@ -# Security Remediation Plan - Issue #33 - -Date: 2026-05-15 -Status: local remediation implemented in this checkout; release/manual-device gates still required before shipping -Audience: MobileCLI maintainers and implementation agents - -## Summary - -Issue #33 was substantially true against the pre-remediation code. The desktop daemon treated network reachability as the security boundary, while public docs described application-layer token authentication that did not exist. The fix needed to be coordinated across the desktop daemon, protocol, mobile app, installer/release pipeline, and public docs. - -This is not a daemon-only change. A new mobile build is required because the bundled app code controls QR parsing, token storage, WebSocket handshake order, queued message flushing, push-token registration, and reconnect behavior. OTA updates are not available in the current native app configuration, so the mobile compatibility release must ship before the desktop daemon enforces fail-closed auth. - -The implemented local end state is protocol v2 challenge-response auth, not a raw bearer token sent over plaintext `ws://`. Tailscale remains recommended transport isolation, but Tailscale is not the only auth boundary. - -## Local Implementation Status - -Implemented locally on 2026-05-15: - -- Desktop auth-v2 protocol messages, credential generation, verifier storage, fail-closed mobile handshake, and post-auth welcome/session flow. -- Authenticated local PTY registration for desktop wrapper sessions so arbitrary loopback WebSocket clients cannot register fake sessions or poison project roots. -- First-message timeout and first-message size cap for unauthenticated WebSockets. -- `mobilecli pair`, `mobilecli pair --rotate`, `mobilecli credentials list`, and `mobilecli credentials revoke `. -- Mobile QR parsing for auth-v2 fields, SecureStore-only per-device pairing-token storage, metadata-only AsyncStorage fallback, challenge-response proof generation, authenticated connection state, queued-message gating, and push registration after auth. -- Durable mobile push opt-out state so offline unregister requests survive app termination and drain after the next authenticated reconnect. -- Loopback plus configured LAN/Tailscale bind policy; no default all-interface bind. -- Active-credential filtering on sensitive broadcasts, PTY output, and file-watch sends so revoked credentials stop receiving data after revocation. -- Session-control messages now require both the `session:control` scope and an active subscription to the target session. -- Filesystem hardening defaults, expanded sensitive-path denylist including MobileCLI auth config, safe project-root inclusion for active sessions, search clamp, copy-size enforcement, destructive-operation config enforcement with delete/rename off by default, explicit empty-root deny-all preservation, and mobile spawn profile normalization. -- Push-token ownership, per-credential caps, and token format validation. -- Unix private config writes and Windows ACL tightening for config files/directories; Windows save now fails visibly if ACL hardening fails. -- Windows filesystem pattern matching now normalizes verbatim paths, separators, and case before denied/read-only glob checks. -- Auth material no longer falls back to a project-local `./.mobilecli` directory when the home directory is unavailable; the CLI uses a platform config directory or fails closed. -- Android release builds no longer use the debug signing config; mobile release preflight fails if that pattern returns. -- Linux Tailscale setup now gives manual installation instructions instead of running `curl | sh`. -- Installer checksum verification in both root and website installer scripts. -- Root release workflow gates for CLI fmt/clippy/test, root installer syntax, tag/version sync, native runner tests on release builders, and checksum completeness across expected Linux/macOS/Windows archives. -- Installer checksum helper now has a local shell test covering valid, star-prefixed, missing, invalid, wrong, and empty checksum manifests. -- Per-workspace CI gates: root Linux/macOS/Windows desktop compile checks; mobile TypeScript/security/preflight checks; website build and public installer syntax. -- Public docs/website/LLM artifacts updated from temporary network-only language to the implemented auth-v2 model. -- Mobile native build numbers synchronized to iOS build 112 and Android versionCode 92. - -Still required before shipping: - -- Physical-device smoke tests on iOS and Android for LAN and Tailscale, including app restart, daemon restart, bad token, revoked credential, push registration, attachments, filesystem allowed/denied roots, and production Android cleartext behavior. -- Cross-platform desktop test/build matrix on Linux, macOS Apple Silicon/Intel, and Windows x64. -- Release-artifact verification against the actual GitHub Release `SHA256SUMS.txt`. -- Transport-confidentiality release decision: auth-v2 authenticates the mobile client but does not encrypt `ws://` terminal/filesystem frames. Same-LAN use must be limited to trusted networks, Tailscale must be required for untrusted networks, or an encrypted/authenticated message layer must be added before claiming protection against LAN MITM. -- Dedicated full hook/integration tests for mobile WebSocket behavior and SecureStore migration are still recommended; the current local security test script covers QR parsing, auth-v2 proof fixtures, redundant storage fallback semantics, and pure auth state-machine gates used by `useSync`. -- Execute the release-candidate runbook in `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md` and record pass/fail evidence for every supported host/mobile/network cell. -- Record release-candidate evidence in `SECURITY_RELEASE_EVIDENCE_TEMPLATE_ISSUE_33.md`. -- Use `SECURITY_COMPLETION_AUDIT_ISSUE_33.md` as the prompt-to-artifact checklist before marking the remediation complete. - -## Pre-Remediation Truths Confirmed - -- Desktop WebSocket auth is missing. The daemon accepts mobile clients, registers them, and sends `welcome`, session lists, and waiting states before any credential check. -- QR pairing currently contains connection metadata only. It does not contain a secret. -- `Config` stores device/network fields only. It does not store an auth token hash or verifier. -- Mobile already has partial `authToken` plumbing, but it marks the raw socket as connected and sends `hello`, `get_sessions`, push registration, and queued commands before auth is proven. -- Mobile parses `auth_token`, not Claude's proposed `token`. The daemon should emit `auth_token`; the app should accept both `auth_token` and `token` for migration tolerance. -- Push notifications use Expo push service. Docs claiming direct APNs or no third-party service are false while Expo remains the push path. -- The mobile app, website, and root CLI are separate workspaces in this checkout. Root GitHub CI currently gates only the CLI repository surface. - -## Target Architecture - -### Auth Model - -Implement protocol v2 challenge-response authentication for all non-PTY mobile WebSocket clients. - -Pairing creates a credential: - -- `server_id`: stable desktop daemon identity. -- `credential_id`: stable ID for one paired mobile installation. -- `auth_token`: 32 bytes from OS RNG, base64url encoded, shown only in the QR/manual pairing flow. -- `verifier`: derived from `auth_token` and stored on desktop. Treat this verifier as sensitive because it is pass-the-hash material for challenge-response verification. -- `scopes`: explicit allowed capabilities for that credential. -- `created_at`, `last_used_at`, `revoked_at`, and display name. - -Protocol flow: - -1. Client opens WebSocket. -2. Client sends `auth_start` with `auth_version: 2`, `credential_id`, `client_nonce`, `mobile_installation_id`, `sender_id`, `client_version`, and `client_capabilities`. -3. Server replies with `auth_challenge` containing `server_nonce`, `server_id`, and `credential_id`. -4. Client sends `auth_response` with `proof = HMAC-SHA256(verifier, transcript)`. -5. Server constant-time compares the proof, updates credential last-used metadata, registers the mobile client, and only then sends `welcome`, session list, waiting states, and any queued authorized data. - -Fail closed: - -- No `welcome`, sessions, waiting states, filesystem watcher, push registration, PTY output, or client registration before auth succeeds. -- Legacy first-message `hello` without v2 auth gets `auth_required` and a policy-violation close. -- Wrong proof gets `auth_invalid` and a policy-violation close. -- Missing local credential storage or tokenless migrated config locks remote mobile bind until the user runs `mobilecli setup` or `mobilecli pair`. - -Compatibility: - -- The new mobile app should support legacy daemons and auth-v2 daemons. -- The new desktop daemon should not support unauthenticated mobile clients. -- If an emergency server-only bridge is needed, `hello.auth_token` can be considered as a temporary auth-v1 path, but it should not be the final design because it sends a reusable bearer token over `ws://`. - -### Transport Confidentiality - -Auth-v2 proves that a paired mobile installation knows the pairing secret, but `ws://` is still cleartext. A hostile same-LAN intermediary could read or tamper with post-auth terminal and filesystem frames unless the network itself is trusted or protected by Tailscale/WireGuard. - -Release posture: - -- Same-LAN mode is acceptable only as a trusted-network convenience, not as protection against a LAN MITM. -- Tailscale is the recommended remote/untrusted-network transport because it gives confidentiality and peer authentication below the WebSocket. -- Public tunnels and untrusted WiFi must not be recommended unless the connection is `wss://` with a trusted endpoint or MobileCLI adds its own encrypted/authenticated message layer after pairing. -- Android cleartext support must remain a deliberate compatibility decision for LAN/Tailscale `ws://`, not an accidental blanket security claim. - -### Desktop Config - -Extend the desktop config format: - -- Add `config_version`. -- Add `server_id`. -- Add `auth_version`. -- Add `credentials`. -- Add `filesystem.allowed_roots` and destructive-operation policy. -- Preserve existing `device_id`, `device_name`, `connection_mode`, `tailscale_ip`, and `local_ip`. - -Persistence requirements: - -- Atomic writes. -- Unix config files written with owner-only permissions. -- Best-effort private ACLs on Windows. -- No raw `auth_token` stored on desktop. -- `mobilecli pair` creates a fresh credential every time because old raw tokens cannot be redisplayed safely. - -New CLI operations: - -- `mobilecli pair`: create and show a new credential QR. -- `mobilecli pair --rotate`: revoke old mobile credentials for this desktop and create a new one. -- `mobilecli credentials list`: show paired devices without secrets. -- `mobilecli credentials revoke `: revoke one paired mobile device. -- `mobilecli setup`: migrate tokenless config, select connection mode, create first credential, and show QR. - -### Bind Policy - -Replace unconditional `0.0.0.0:` mobile exposure with explicit bind selection. - -Required behavior: - -- PTY registration remains loopback-only. -- The daemon always has a loopback listener for desktop wrapper/link traffic. -- Local mode binds the selected LAN IP, not every interface. -- Tailscale mode binds the Tailscale IP and fails closed if Tailscale is unavailable. -- Custom mode binds only according to explicit user configuration and warns if public exposure is possible. -- `0.0.0.0` becomes explicit advanced opt-in, not the default. - -Cross-platform considerations: - -- Linux/macOS/Windows must all support loopback plus selected mobile bind. -- IP changes require a clear restart/re-pair message. -- Windows users must keep the daemon in the user session, not a Windows service, so spawned terminals remain visible. -- Firewall docs should say exactly which address and port are bound. - -## Implementation Slices - -### Slice 0 - Release And Branch Hygiene - -- Freeze feature work touching daemon protocol, mobile sync, QR pairing, filesystem bridge, installer, and docs until the remediation lands. -- Treat current docs as unsafe until updated. -- Keep `SECURITY_AUDIT_ISSUE_33.md` as evidence and use this file as the execution plan. -- Decide release numbers before code starts. Assuming iOS build `111` has already been used, the next mobile build should be at least `112`; Android versionCode should be at least `92`. Use the next marketing version that matches App Store / Play Console state. - -### Slice 1 - Mobile Compatibility Release - -Goal: ship an app that can talk to both legacy daemons and auth-v2 daemons before the desktop daemon starts enforcing auth. - -Mobile changes: - -- Clean up QR parsing so special `mobilecli://relay`, `mobilecli://tailscale`, and `mobilecli://direct` formats are not swallowed by the generic compact parser. -- Parse `auth=v2`, `credential_id`, `server_id`, `auth_token`, `token`, `wss=1`, and `ws_url`. -- Avoid logging full QR payloads or auth tokens, even in dev logs. -- Store pairing secrets with existing `expo-secure-store` keychain service. Do not enable biometric `requireAuthentication`; reconnect/background behavior depends on silent access. -- Add a SecureStore-backed `mobile_installation_id` that survives app restarts and is distinct from the desktop `device_id`. -- Add HMAC-SHA256 support for auth-v2, likely with a small audited JS dependency such as `@noble/hashes` unless the native stack already provides a reliable HMAC API. -- Change connection state from `socket open == connected` to `authenticated welcome == connected`. -- On WebSocket open, send auth-v2 messages when the active device has v2 fields. -- Wait for auth success / `welcome` before setting `isConnected`, requesting sessions, registering/unregistering push tokens, sending filesystem requests, or flushing queued messages. -- Clear queued privileged messages on auth failure. -- Handle `auth_required`, `auth_invalid`, `auth_revoked`, JSON error messages, and close-code-only failures without retry loops. -- Add a visible manual token entry field in Settings or make manual URL pairing explicitly QR-only. Recommended: add manual fields for URL, credential ID, and token. -- Register push tokens only after auth succeeds and include `mobile_installation_id`. -- Fix the current TypeScript blocker: `TMUX_SWIPE_COOLDOWN_MS` undefined. -- Fix native version/build sync before any build: app config, iOS project, iOS Info.plist, and Android Gradle must agree. -- Test Android production `ws://` behavior. Debug allows cleartext; production must be verified for LAN/Tailscale WebSockets. - -Mobile acceptance tests: - -- QR parser unit tests for JSON, compact URL, `auth_token`, `token`, `auth=v2`, `wss=1`, relay/tailscale/direct, IPv6, and malformed values. -- Mock WebSocket tests proving auth is sent first, `isConnected` stays false until `welcome`, push registration waits for auth, queued messages flush only after auth, and auth failure clears privileged queues. -- SecureStore migration tests for existing linked devices without tokens and rescans with new credentials. -- Physical-device smoke tests for iOS LAN, iOS Tailscale, Android production LAN, Android production Tailscale, app restart, daemon restart, and push opt-in. - -Release order: - -- Build locally on the iMac for smoke testing if useful. -- Use the same signing/env/version inputs as production EAS. -- EAS remains the release path unless the team explicitly switches release process. -- Ship TestFlight/internal build first; only enforce desktop auth after the compatible mobile build is available. - -### Slice 2 - Desktop Auth And Pairing - -Goal: the daemon must reject unauthenticated mobile clients before any sensitive data leaves the desktop. - -Desktop changes: - -- Add auth-v2 protocol messages and server messages. -- Refactor `handle_connection` so mobile auth completes before calling the current mobile client loop. -- Do not create filesystem watcher subscriptions, broadcast receivers, mobile client map entries, sender IDs, or capability records until auth succeeds. -- Reduce unauthenticated WebSocket message/frame limits. The current 96 MB cap should apply only after auth or be replaced by authenticated chunked upload. -- Add token generation, verifier derivation, credential storage, credential list/revoke/rotate, config migration, and secure config persistence. -- `mobilecli setup` must create credentials before daemon state is initialized, or restart/reload the daemon after setup so the running daemon has the new auth state. -- Existing tokenless configs preserve network/device fields but lock remote mobile bind until pairing creates credentials. -- `mobilecli pair` should create a new credential and show a QR once; it should not attempt to redisplay old secrets. -- Never log full QR URLs, raw tokens, HMAC proofs, or verifiers. -- Return stable auth errors: `auth_required`, `auth_invalid`, `auth_revoked`, `auth_unsupported`, and `auth_locked`. - -Desktop auth tests: - -- No-token client gets no `welcome`, no sessions, no waiting states, and closes. -- Bad proof closes without registering a mobile client. -- Good proof gets `welcome` and can request sessions. -- Legacy `hello` gets `auth_required` and closes. -- Revoked credential cannot reconnect. -- Config migration locks remote bind until pairing. -- Config writes contain no raw token and use expected permissions. -- PTY `register_pty` remains loopback-only. - -### Slice 3 - Authorization And Capability Scopes - -Goal: after auth, the daemon still enforces least privilege by credential/scope instead of trusting arbitrary session IDs. - -Scopes: - -- `session:read` -- `session:control` -- `session:spawn` -- `fs:read` -- `fs:write` -- `fs:delete` -- `fs:watch` -- `fs:upload` -- `push:register` - -Session controls: - -- Gate `Subscribe`, `GetSessionHistory`, `SendInput`, `ToolApproval`, `PtyResize`, `TmuxViewport`, `RenameSession`, and `CloseSession`. -- Do not use `owner_connection_id` as the primary authorization model. It breaks reconnects and multi-device use. Use authenticated `credential_id`, scopes, and session ACLs where needed. -- For input/resize/viewport, require the connection to be subscribed or explicitly controlling the session. -- Ensure old/no-capability clients do not receive `PtyBytes` for sessions they did not subscribe to. - -Push tokens: - -- Bind tokens to `credential_id` and `mobile_installation_id`. -- Validate `token_type` and token format. -- Cap tokens per credential. -- Re-registration replaces the same mobile installation's token. -- Unregister only removes tokens owned by the authenticated credential. -- Credential revocation purges related push tokens. -- Rate-limit by credential and IP. -- Keep docs honest that Expo push is used unless the implementation changes. - -### Slice 4 - Filesystem Hardening - -Goal: the filesystem bridge should be useful without defaulting to whole-home access. - -Desktop changes: - -- Change default roots from whole home to active session project roots plus explicit setup-approved roots. -- Make whole-home access an explicit opt-in with a clear warning. -- Add per-root permissions: read, write, delete, watch, upload. -- Keep an expanded denylist as defense-in-depth, not as the primary security boundary. -- Block known secret locations across Linux, macOS, and Windows by default, including SSH, GPG, AWS, kube, Docker, gcloud, shell histories, netrc, Git credentials, Vault, Terraform state, browser credential stores, keychains, and password-manager config. -- Clamp client-provided search result limits to the configured maximum. -- Enforce write-size limits for copy operations too. -- Validate mobile `working_dir` for spawned sessions against approved roots or session project roots. -- Harden Windows paths: case-insensitive matching, drive prefixes, UNC/verbatim paths, junctions/reparse points, reserved names, and symlink race behavior. - -Mobile changes: - -- Treat denied roots as normal UX, not connection failure. -- Show explicit root/permission state in the file browser where needed. -- Do not offer destructive file actions when the authenticated credential lacks `fs:delete`. - -Filesystem tests: - -- Fresh config cannot read arbitrary home files. -- Session project roots are readable where intended. -- Whole-home opt-in works only after explicit setup approval. -- Sensitive paths are denied even under allowed roots. -- Copy honors size limits. -- Search clamps `max_results`. -- Windows junction/reparse/UNC cases are denied or handled as specified. - -### Slice 5 - Command Spawn Hardening - -Goal: mobile spawn should start supported tools, not arbitrary interpreter payloads. - -Desktop changes: - -- Replace free-form mobile `command,args` with server-defined spawn profiles: `claude`, `codex`, `gemini`, `opencode`, and `shell`. -- Existing mobile requests with `command` and empty `args` may be translated to profiles for compatibility. -- Reject absolute command paths from mobile clients. -- Reject non-empty args by default unless a profile explicitly allows them. -- For the `shell` profile, spawn the user's default shell without accepting client-provided flags. -- If advanced arbitrary command spawning is kept, put it behind an explicit config flag and a separate high-risk scope. -- Fix basename allowlist bypasses such as `/tmp/bash`. -- Do not rely on `is_shell_safe` for security. Escaping is still useful, but interpreter flags are the real risk. - -Tests: - -- Allowed profile spawns work on Linux, macOS, and Windows. -- `/tmp/bash`, `bash -c`, `python -c`, `node -e`, `powershell -Command`, and `powershell -EncodedCommand` are rejected. -- Working directories outside approved roots are rejected. -- Existing mobile UI spawn buttons continue to work. - -### Slice 6 - Bind, Setup, Autostart, And Cross-Platform UX - -Goal: safe defaults work on Linux, macOS, and Windows. - -Desktop changes: - -- Implement pure bind-address selection logic and test it independently. -- Local mode binds selected LAN IP. -- Tailscale mode binds selected Tailscale IP and fails closed if disconnected. -- Custom mode warns and requires explicit confirmation for public or all-interface exposure. -- Never silently fall back from Tailscale to LAN when Tailscale is selected. -- Setup should default to Tailscale as the recommended remote mode and explain LAN risk plainly. -- Auto-launch shell hook should remain opt-in and default to `No` during setup. -- Tailscale Linux install helper should give manual package-manager/download instructions and not run `curl | sh`. -- Audit autostart outputs while touching setup: macOS launchd plist, Windows Task Scheduler command, Linux systemd user service. - -Tests: - -- Bind selection: loopback always available, LAN binds selected local IP, Tailscale binds selected `100.x` address, no implicit `0.0.0.0`. -- Tailscale disconnected in Tailscale mode refuses pairing instead of showing LAN QR. -- Windows setup docs and behavior keep daemon in the user session. -- Shell hook install/uninstall remains reversible. - -### Slice 7 - Installer And Release Integrity - -Goal: downloads are verified before execution or privileged install. - -Changes: - -- Update root `install.sh` and `website/public/install.sh` together. -- Use the existing `SHA256SUMS.txt` from GitHub Releases or switch release workflow to per-archive `.sha256`; pick one and test it. Recommended: use existing `SHA256SUMS.txt` to minimize release workflow churn. -- Download checksum file before extraction. -- Verify the selected archive hash before `tar`/`unzip` and before `sudo`. -- Fail closed if checksum is missing or verification fails. -- Prefer fail-closed if no checksum tool exists; if a bypass flag is added, make it explicit and noisy. -- Keep the sudo warning, but the main supply-chain fix is checksum verification. -- Add release workflow checks that every archive appears in checksum output. -- Add signing/notarization as a follow-up hardening track: cosign/Sigstore for artifacts, macOS codesign/notarization if distributing outside Homebrew, Windows Authenticode if distributing `.exe`. - -Tests: - -- Valid checksum installs. -- Corrupted archive fails. -- Missing checksum fails. -- Wrong archive name fails. -- Website installer copy matches root installer. - -### Slice 8 - Docs, Website, Privacy, And Disclosure - -Goal: docs must describe the implemented security model, not the intended one. - -Before code ships: - -- Replace current auth-token claims with a temporary warning: current released daemon relies on network isolation and should be used only on trusted LANs or locked-down Tailnets. -- Remove public tunnel recommendations until app-layer auth is released. -- State that QR currently contains connection metadata only. -- State that the daemon exposes high-privilege terminal and filesystem capabilities to reachable clients. -- State that push notifications use Expo push service. -- State that installer checksum verification is being added if not yet shipped. - -After fixes ship: - -- Add a canonical Security and Privacy Model section reused across root README, CLI README, docs, and website. -- Document auth-v2 pairing, token storage, QR/manual entry, rotation, revocation, failed-auth logs, and re-pairing. -- Document LAN, Tailscale, and custom modes honestly. -- Keep Expo in privacy/docs unless push implementation changes. -- Document filesystem root approval and whole-home opt-in. -- Document release checksum verification. - -Sweep areas: - -- `README.md` -- `cli/README.md` -- `docs/ARCHITECTURE_QUICK_REFERENCE.md` -- `docs/WINDOWS_SETUP.md` -- `website/src/pages/index.astro` -- `website/src/pages/docs/*` -- `website/src/pages/features.astro` -- `website/src/pages/pricing.astro` -- `website/src/pages/privacy.astro` -- `website/src/pages/terms.astro` -- `website/src/lib/constants.ts` -- `website/public/llms.txt` -- `website/public/llms-full.txt` -- blog and comparison pages with durable no-cloud/no-third-party/no-shell claims -- `website/README.md` - -## Verification Gates - -### CLI Gates - -- `cargo fmt --check` -- `cargo clippy --all-targets -- -D warnings` -- `cargo clippy --all-targets -- -D warnings -A dead-code` -- `cargo test` -- `cargo check --target x86_64-unknown-linux-gnu` -- `cargo check --target x86_64-pc-windows-gnu` when the target is available locally. -- Cross-platform build/test on Linux, macOS, and Windows. -- Tmux-dependent tests skip cleanly or run in an environment where tmux sockets are permitted. - -Local status: - -- `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings -A dead-code`, and `cargo test` passed in this checkout on 2026-05-15. -- `cargo check --target x86_64-unknown-linux-gnu` and `cargo check --target x86_64-pc-windows-gnu` passed locally on 2026-05-15. -- Desktop auth parsing/verification now has pure unit coverage for legacy first-message rejection, unsupported auth versions, unknown/revoked credentials, transcript mismatch, invalid proof, and a valid bound proof. -- Local PTY registration proof, daemon-side PTY registration validation, and safe session-root filtering have focused unit coverage; explicit empty filesystem roots stay deny-all. -- Windows-style filesystem match normalization and config-directory fallback behavior have focused unit coverage. -- Root PR CI now includes `cargo check` on `ubuntu-latest`, `macos-latest`, and `windows-latest`. -- Full cross-platform Linux/macOS/Windows release build/test still needs to run outside this sandbox because macOS and Windows MSVC runners are not available here. - -### Mobile Gates - -- `npx tsc --noEmit` -- `npm run preflight:release` -- `npm run test:security` for QR parser, auth-v2 proof fixtures, redundant storage fallback semantics, and pure auth state-machine gates. -- WebSocket auth state machine tests. -- SecureStore/AsyncStorage fallback tests. -- Physical iOS and Android smoke tests. - -Local status: - -- `TMUX_SWIPE_COOLDOWN_MS` is defined. -- Native versions/build numbers are synchronized to iOS build 112 and Android versionCode 92. -- `npx tsc --noEmit`, `npm run test:security`, and `npm run preflight:release` passed locally. -- Pairing tokens are stored per device in SecureStore only; AsyncStorage stores device metadata without `authToken`/`auth_token`. Old inline token JSON is migrated and scrubbed. -- Pending push-token unregister state is persisted so notification opt-out can be drained after app restart and the next authenticated reconnect. -- Full mock WebSocket hook tests and SecureStore migration integration tests are not yet automated, but pure state gates, token-free metadata serialization, and redundant storage behavior are covered by `npm run test:security`. -- EAS production env var checks still warn if EAS auth/network is unavailable. -- Mobile workspace PR CI now runs dependency install, TypeScript check, `npm run test:security`, and release preflight. - -### Website Gates - -- `npm run build` -- No false security/privacy claims after the docs sweep. -- LLM artifacts regenerated or updated if they are generated manually. -- Website install script matches root install script. - -Local status: - -- `npm run build` passed in `website/`. -- Root and website installer scripts are byte-for-byte identical and pass `bash -n`. -- Targeted stale-claim scan found no remaining network-only auth claims or URL-only manual-pairing instructions in root docs, CLI docs, website source, or LLM artifacts. -- Website workspace PR CI now checks public installer syntax and website build before merge. -- Cross-repository installer parity still requires local/release coordination because root, website, and mobile are separate Git workspaces in this checkout. - -### Manual E2E Matrix - -The executable release checklist lives in `SECURITY_RELEASE_RUNBOOK_ISSUE_33.md`. The matrix below is the release-blocking coverage summary; use the runbook for exact commands, scenarios, and evidence tables. - -Host operating systems: - -- macOS Apple Silicon -- macOS Intel -- Linux x86_64 -- Linux ARM64 -- Windows x86_64 - -Mobile clients: - -- iOS TestFlight/App Store candidate -- Android internal candidate if Android is still in scope - -Networks: - -- Same LAN -- Tailscale - -Per-cell scenarios: - -- Fresh setup -- QR scan -- token persistence -- app restart reconnect -- daemon restart reconnect -- bad token rejection -- revoked credential rejection -- session list -- subscribe to existing session -- spawn Claude/Codex/Gemini/shell profile -- send input -- approval flow -- close session -- push register/unregister -- file list/read/write in allowed root -- denied sensitive path -- whole-home opt-in if enabled -- desktop upgrade from tokenless config -- mobile upgrade from existing pairing - -Release is blocked unless every supported cell passes or the unsupported cell is explicitly documented. - -## Rollout Order - -1. Patch public docs with temporary truth if we need to reduce user risk before code ships. -2. Build and release the mobile compatibility app first. Use at least the next unused iOS build number after `111` and the next unused Android versionCode after `91`. -3. Ship a desktop prerelease with auth-v2 enforced, safe migration, credential pairing, bind policy, command spawn hardening, filesystem root changes, push-token ownership, and installer checksum verification. -4. Run the full CLI, mobile, website, and manual E2E gates. -5. Update docs from temporary warnings to the final auth-v2 security model. -6. Publish stable CLI release. -7. Monitor support channels for auth failures, pairing confusion, Tailscale bind failures, mobile reconnect loops, and installer verification failures. - -## Explicit Non-Goals For The First Remediation Release - -- Do not implement a cloud relay. -- Do not claim end-to-end encryption beyond the chosen transport and auth properties. -- Do not claim “no third-party services” while Expo push, App Store, RevenueCat, or other processors remain in use. -- Do not implement arbitrary mobile command execution as a default feature. -- Do not expose public tunnels or reverse proxies as recommended setup paths. -- Do not silently preserve unauthenticated legacy mobile access for convenience. - -## Open Decisions To Confirm Before Release - -- Whether to ship an emergency desktop-only `hello.auth_token` hotfix before the full auth-v2 rollout. Recommended default: skip unless there is an immediate release pressure. -- Whether Android is part of this security release or remains internal only. If it is included, production cleartext WebSocket behavior must be verified. -- Whether whole-home filesystem access should remain available as an advanced opt-in. Recommended default: yes, but off by default with a strong warning. -- Whether artifact signing is required in the same release as checksum verification. Recommended default: checksum verification now, signing as the next hardening milestone.