Skip to content

Compact mobile pairing QR ticket references - #7007

Closed
austinywang wants to merge 37 commits into
mainfrom
issue-5540-compact-mobile-pairing-qr-carry-a-ticket-refe
Closed

austinywang wants to merge 37 commits into
mainfrom
issue-5540-compact-mobile-pairing-qr-carry-a-ticket-refe

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5540

Summary

  • encode mobile pairing QR/deep links with a non-secret ticket reference instead of the bearer attach token
  • redeem the ticket reference over the authenticated mobile RPC path before sending authorized requests
  • persist the redeemed effective ticket on iOS and keep phone-safe scanned routes
  • add host ticket-reference storage plus regression coverage for QR size, redemption order, and ticket-ref expiry

Local validation

  • swift test (Packages/Shared/CMUXMobileCore)
  • swift test (Packages/iOS/CmuxMobileRPC)
  • swift test (Packages/iOS/CmuxMobileShell)
  • swift test (Packages/macOS/CmuxControlSocket)
  • bun test scripts/lib/attach-url.test.mjs
  • ./scripts/lint-pbxproj-test-wiring.sh
  • git diff --check

Note: ios/cmuxPackage swift test was attempted but local GhosttyKit.xcframework is absent in this checkout, so that suite is left to CI.


Summary by cubic

Switch mobile pairing to v3 QR/deep links that carry a non‑secret ticket reference (tr), redeemed over Stack‑authenticated RPC before any authorized request. Keeps QRs small, lets the Mac own TTL/revocation, and hardens redemption under retries/timeouts. Fixes #5540.

  • New Features

    • v3 QR uses tr=<ticket-ref> with plain host:port; compact JSON v2 adds q (no inline expiry).
    • iOS auto‑redeems tr before any authorized request, shares concurrent attempts behind a timeout gate, merges redeemed scope safely, exposes currentTicket(), and adds localized redeem‑failure messages.
    • Mac mints refs mapped to tokens, resolves until expiry, adds mobile.attach_ticket.redeem, returns a canonical attach_url with tr (preserves release/dev scheme); scripts/lib/attach-url.mjs prefers the canonical URL.
  • Bug Fixes

    • Reject empty ticket refs (emptyTicketRef), map ticket_expired; v3 QR decoder still blocks loopback.
    • Keep the scanned QR’s workspace/terminal scope authoritative; redemption only fills empty fields (v3 scans empty scope and adopts redeemed scope).
    • Redemption gate allows immediate retry after cancel, dedupes concurrent work, bounds abandoned tasks under repeated timeouts, and uses a gate‑governed deadline so a short‑deadline waiter can’t poison longer waiters.
    • Legacy compact v1: decode u as an opaque user id (no @) to avoid false account‑mismatch.
    • Docs: public CmxAttachTicketError cases; helper placement and file splits align with package conventions (no behavior change).

Written for commit 90dc248. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added support for pairing QR/URLs using a compact v3 grammar that requires a non-secret ticket reference.
    • Introduced attach-ticket redemption via ticket-reference flow, including coordination so concurrent requests share one redemption.
    • Added new localized messages for redeem failure scenarios.
    • Exposed the new redemption capability in advertised RPC methods.
  • Bug Fixes

    • Improved validation to reject missing/empty ticket references and preserve the ticket reference across pairing and retry paths.
    • Refined redemption/error handling, including clearer handling of “ticket expired” outcomes and inline expiry behavior.

@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 5, 2026 4:41am
cmux-staging Building Building Preview, Comment Jul 5, 2026 4:41am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds ticketRef to pairing QR and attach-ticket flows, introduces host-side redemption by reference, and updates iOS shell/client logic to redeem and use the returned ticket before authorization. It also revises canonical attach-URL handling, route validation, localization, and related tests.

Changes

Compact QR ticket reference flow

Layer / File(s) Summary
QR grammar and ticket model
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift, Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift, Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxAttachTicketCompactCoder.swift, Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift, Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/CmxAttachTicketInput.swift, Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/*, Packages/iOS/CmuxMobileCamera/Tests/CmuxMobileCameraTests/QRCodeFrameSelectionTests.swift
CmxAttachTicket gains ticketRef, compact QR encode/decode uses v3 grammar rules, and the related compact/full-round-trip and URL-scheme tests now expect q/tr and v=3.
Host minting and redeem RPC
Sources/Mobile/MobileAttachTicketStore.swift, Sources/Mobile/MobileHostService.swift, Sources/TerminalController.swift, Packages/macOS/CmuxControlSocket/..., Resources/Localizable.xcstrings, cmuxTests/..., Sources/Mobile/Pairing/MobilePairingModel.swift
The host stores and resolves ticketRef, exposes redeemAttachTicket(ticketRef:), routes mobile.attach_ticket.redeem, and updates related authorization, localization, and host-side tests.
iOS redemption state and client
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift, Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift, Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketState.swift, Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileAttachTicketRedeemResponse.swift, Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/*
The iOS RPC client now coalesces ticket redemption, tracks the current redeemed ticket, decodes redeem responses, and adds scripted transport and gate tests for concurrency and timeout behavior.
Shell connection and failure handling
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/CmxAttachTicket+ConstrainingRoutes.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift, cmuxTests/MobileHostAuthorizationTests.swift, ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift, Packages/iOS/CmuxMobileShell/Tests/..., Packages/iOS/CmuxMobileWorkspace/Tests/..., Packages/iOS/CmuxMobileShellModel/...
Shell connection now uses the redeemed ticket, adopted tickets preserve ticketRef, and pairing-failure and route-policy tests/docs are updated for v3 QR and ticket_expired.
Canonical attach URL script and tests
scripts/lib/attach-url.mjs, scripts/lib/attach-url.test.mjs
Canonical attach URLs are recognized structurally and the script tests update the canonical release/dev URL cases to include tr.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • manaflow-ai/cmux#5872: Same pairing-QR and compact attach-ticket surface; it updated the earlier minimal QR grammar that this PR extends to v3 with tr.

Sequence Diagram(s)

sequenceDiagram
  participant MobileCoreRPCClient
  participant MobileCoreRPCTicketRedemptionGate
  participant TerminalController
  participant MobileHostService

  MobileCoreRPCClient->>MobileCoreRPCTicketRedemptionGate: ticket(timeoutNanoseconds:provider:)
  MobileCoreRPCTicketRedemptionGate->>TerminalController: mobile.attach_ticket.redeem
  TerminalController->>MobileHostService: redeemAttachTicket(ticketRef:)
  MobileHostService-->>TerminalController: attach-ticket payload
  TerminalController-->>MobileCoreRPCClient: redeemed ticket response
  MobileCoreRPCClient->>MobileCoreRPCClient: ticketState.replace(with:)
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production MobileCoreRPCTicketRedemptionGate adds Task.sleep-backed timeouts and wall-clock cooldown gating for ticket redemption. Replace the timeout/cooldown path with a real completion signal or injected scheduler; keep Task.sleep and wall-clock retry gating out of shipped Swift.
Cmux Swift @Concurrent ❌ Error MobileCoreRPCClient.sendRequest now does ticket redemption/network auth work yet is awaited from @MainActor shell code without @concurrent or a detached hop. Mark the heavy RPC path @concurrent, or move the auth/redemption work to a detached/actor-hopped helper before calling it from MobileShellComposite.
Cmux Source Artifacts ❌ Error ghostty is a submodule gitlink bump (dependency checkout) with no product/docs/test-system rationale in the PR. Remove the submodule pointer change or explain and justify the dependency update with a deliberate repo reason.
Cmux No Test Or Debug Seam In Production Source ❌ Error MobileCoreRPCTicketRedemptionGate.swift adds waiterCount/abandonedCount accessors used only by tests, creating a production test-observation seam. Remove those helpers from production; let tests read internal state via @testable import, or keep the state private and move the checks into the test target.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers summary and local validation, but it omits the required Demo Video, Review Trigger, and Checklist sections. Add the missing template sections: Demo Video, Review Trigger block, and Checklist items; keep Summary and Testing under the required headings.
✅ Passed checks (19 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #5540 by switching QR/deeplink payloads to ticket references, adding redemption flow, and preserving routes and expiry handling.
Out of Scope Changes check ✅ Passed The diff stays focused on pairing QR, redemption, authorization, and tests/docs; no clear unrelated feature work stands out.
Cmux Swift Actor Isolation ✅ Passed The new mutable ticket state is actor-backed, the host service stays @MainActor with a lock-backed store, and no new unchecked shared Sendable refs were introduced.
Cmux Browser Automation Off-Main ✅ Passed No browser.* wait/callback command was routed to mainActor; worker policy still includes eval/wait/screenshot/cookies/storage and tests cover socket-worker routing.
Cmux Expensive Synchronous Load ✅ Passed No changed production Swift file adds/moves agent-history or transcript loads; the diff is ticket-ref/QR/RPC code, with only bounded request/response JSON parsing.
Cmux Cache Substitution Correctness ✅ Passed PASS: currentTicket() is fed by redeem responses and guarded host-status refreshes; persistence writes also re-check staleness with ifStillCurrent.
Cmux No Hacky Sleeps ✅ Passed scripts/lib/attach-url.mjs only parses and rewrites URLs; no setTimeout/sleep/polling was found, and the test file has none.
Cmux Algorithmic Complexity ✅ Passed New scans are only over tiny route lists or short-lived ticket caches; no nested rescans on scalable user-owned collections were introduced.
Cmux Swift Concurrency ✅ Passed New async work uses actors/async-await; no new Combine, background queues, or unmanaged fire-and-forget Tasks were introduced outside allowed boundaries.
Cmux Swift File And Package Boundaries ✅ Passed No new oversized Swift files or large growth; app-target edits are small glue/store updates around focused package logic.
Cmux Swiftpm Lockfiles ✅ Passed PASS: The commit only edits cmux.xcodeproj/project.pbxproj for source-file entries; no Package.resolved or cmux-owned .gitignore files changed, so the lockfile policy isn’t violated.
Cmux Swift Logging ✅ Passed No changed production Swift file adds print/NSLog/file logging; the only logger declarations are nonisolated private lets, with sensitive values redacted.
Cmux User-Facing Error Privacy ✅ Passed New user-facing copy is generic; no tokens, vendor names, raw upstream errors, or other prohibited details are exposed in alerts/API bodies.
Cmux Full Internationalization ✅ Passed New redeem UI strings use String(localized:) and the catalog entries cover all 20 supported locales; other changes are docs/comments or non-user-facing code.
Cmux Swiftui State Layout ✅ Passed PASS: The PR is models/services/tests plus incidental state wiring; I found no new SwiftUI view-state/layout patterns like @ObservableObject, GeometryReader, or render-time mutation.
Cmux Architecture Rethink ✅ Passed The new gate and ticketRef flow define clear ownership/invariants and mirror existing RPC gating patterns; no forbidden production polling/sleep workaround was introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The PR only changes pairing/RPC code; no new or materially changed standalone NSWindow/NSPanel/WindowGroup code, and no cmux.* identifier or cmuxAuxiliaryWindowIdentifiers edits.
Cmux No Ambient Global State ✅ Passed New state is scoped via injectable actors/methods; I found no newly introduced ambient singleton or file-scope API in the PR changes.
Title check ✅ Passed The title is concise and matches the main change: replacing mobile pairing QR bearer data with ticket references.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5540-compact-mobile-pairing-qr-carry-a-ticket-refe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR switches mobile pairing QR codes and deep links from v2 (bare routes, implicit bearer token) to v3 (non-secret ticket reference + bare routes), and adds the mobile.attach_ticket.redeem RPC so iOS can exchange a ticket reference for the full ticket payload after authenticating with its Stack access token. The Mac side mints ticket references mapped to auth tokens, resolves them until expiry, and returns the full ticket payload via the existing payload(for:) helper. The iOS side auto-redeems the reference before any authorized request and shares concurrent redemption attempts through a new MobileCoreRPCTicketRedemptionGate actor.

  • QR encoding (Mac): CmxPairingQRCode bumped to version=3; encoder requires a non-empty ticketRef and decoder accepts v2 codes for backwards compat; MobileAttachTicketStore mints ticketRef with 96-bit secure random, maintains a secondary authTokensByTicketRef map, and prunes it alongside recordsByAuthToken.
  • Ticket redemption (iOS): MobileCoreRPCTicketRedemptionGate deduplicates concurrent redemptions behind an actor gate, bounds abandoned tasks under repeated timeouts, and uses an unbounded deadline for the shared provider so a short-deadline waiter cannot poison longer-deadline waiters; MobileCoreRPCTicketState holds the evolving ticket atomically across the redemption lifecycle.
  • Scope merge: redeemedTicket(_:ticketRef:constrainedTo:) keeps the scanned QR's workspace/terminal scope authoritative; MobileShellComposite persists the redeemed ticket (with macDeviceID) rather than the anonymous v3 QR scan, and maps ticket_expired as an auth-class error code to trigger re-pairing.

Confidence Score: 5/5

Safe to merge. The concurrent redemption race from prior review is fully addressed by the new gate actor, and no regressions were found in auth, scope merge, or backwards compat.

The concurrent redemption gate correctly deduplicates in-flight RPC calls and handles timeout/cancellation/abandon without leaking tasks. Scope merge in redeemedTicket keeps scanned QR scope authoritative. Backwards compatibility for v2 QR codes is preserved in isPairingCodeURL. Internationalization is complete across all 20 locales. No test seams were added to production source — ticketRedemptionGate is widened to internal so the test target can observe it via @testable import, exactly the pattern the codebase requires.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Mobile/MobileAttachTicketStore.swift Adds secondary authTokensByTicketRef index, validAuthorization(ticketRef:), payload(forTicketRef:), and uniqueReferenceID() with 96-bit secure random. Pruning correctly keeps both maps consistent.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift New actor gate that deduplicates concurrent ticket-reference redemptions with correct timed-out/abandoned/completed state handling.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift Replaces direct ticket field with MobileCoreRPCTicketState actor; ticketRedemptionGate widened to internal for @testable import observability — correct pattern, no debug seam in production.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient+TicketRedemption.swift Scope-merge logic keeps scanned QR scope authoritative; redeemed scope only fills empty fields. Routes from the scan are preserved.
Sources/TerminalController.swift Adds mobile.attach_ticket.redeem to advertised methods and processV2Command switch. Error codes map cleanly: ticket_expired, invalid_request, internal_error.
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift Bumps grammar to v3 requiring tr=. isPairingCodeURL dispatches on version: v2 accepted without tr for backwards compat, v3 requires both tr and at least one r.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Uses redeemed connectionTicket for MacConnection, foreground-MAC ID, and persistPairedMacFromTicket. Maps ticket_expired into auth-failure list.
scripts/lib/attach-url.mjs Prefers canonical v3 attach_url only when not locally filtered and all r params match payload routes by endpoint key.
Resources/Localizable.xcstrings Three new keys fully translated across all 20 supported locales.
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift Adds q field for ticketRef; legacy @ heuristic correctly scoped to v1 grammar only.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift Adds mobile.attach_ticket.redeem to socketWorkerMethods, consistent with mobile.attach_ticket.create treatment.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Mac as Mac (MobileHostService)
    participant QR as v3 QR Code
    participant iOS as iOS (MobileCoreRPCClient)
    participant Gate as RedemptionGate (actor)

    Mac->>Mac: "createTicket() -> ticketRef + authToken"
    Mac->>QR: "encode(v=3&tr=ticketRef&r=host:port)"
    iOS->>iOS: "CmxPairingQRCode.decode() -> ticket{ticketRef, routes, no authToken}"
    Note over iOS,Gate: First authorized request triggers redemption
    iOS->>Gate: ticket(timeoutNs, provider)
    Gate->>Gate: "launch Task { provider() }"
    Note over iOS,Gate: Concurrent authorized requests join same task
    iOS->>Gate: ticket(timeoutNs, provider) waiter2
    Gate->>Gate: "waiters += 1, await same Task"
    Gate->>Mac: "mobile.attach_ticket.redeem {ticket_ref, stack_token}"
    Mac->>Mac: "validAuthorization(ticketRef) -> ticket{authToken}"
    Mac->>Gate: "{ticket: {ticketRef, authToken, macDeviceID}}"
    Gate->>Gate: ticketState.replace(with: redeemed)
    Gate->>iOS: redeemed ticket (waiter1 and waiter2)
    iOS->>Mac: "workspace.list {attach_token: authToken, stack_token}"
    Mac->>iOS: workspaces
    iOS->>iOS: persistPairedMacFromTicket(redeemed)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Mac as Mac (MobileHostService)
    participant QR as v3 QR Code
    participant iOS as iOS (MobileCoreRPCClient)
    participant Gate as RedemptionGate (actor)

    Mac->>Mac: "createTicket() -> ticketRef + authToken"
    Mac->>QR: "encode(v=3&tr=ticketRef&r=host:port)"
    iOS->>iOS: "CmxPairingQRCode.decode() -> ticket{ticketRef, routes, no authToken}"
    Note over iOS,Gate: First authorized request triggers redemption
    iOS->>Gate: ticket(timeoutNs, provider)
    Gate->>Gate: "launch Task { provider() }"
    Note over iOS,Gate: Concurrent authorized requests join same task
    iOS->>Gate: ticket(timeoutNs, provider) waiter2
    Gate->>Gate: "waiters += 1, await same Task"
    Gate->>Mac: "mobile.attach_ticket.redeem {ticket_ref, stack_token}"
    Mac->>Mac: "validAuthorization(ticketRef) -> ticket{authToken}"
    Mac->>Gate: "{ticket: {ticketRef, authToken, macDeviceID}}"
    Gate->>Gate: ticketState.replace(with: redeemed)
    Gate->>iOS: redeemed ticket (waiter1 and waiter2)
    iOS->>Mac: "workspace.list {attach_token: authToken, stack_token}"
    Mac->>iOS: workspaces
    iOS->>iOS: persistPairedMacFromTicket(redeemed)
Loading

Reviews (26): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/TerminalController.swift
cmux added 4 commits June 28, 2026 20:21
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Resources/Localizable.xcstrings
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
scripts/lib/attach-url.mjs (1)

89-112: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Tighten the canonical-URL check to the non-secret v2 shape.

Sources/Mobile/MobileAttachTicketStore.swift:165-193 still emits ...://attach?v=1&payload=... when the minimal grammar cannot represent the ticket. isCanonicalAttachURL() will accept that too, so this branch can preserve a bearer-carrying deep link through the "canonical/non-secret" path. Parse the URL and require the v2 bare-route form (v=2, tr, no payload) before reusing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/attach-url.mjs` around lines 89 - 112, The canonical attach-URL
check in isCanonicalAttachURL is too loose and currently accepts secret-bearing
v1 payload links as well as the intended non-secret form. Update the attach-url
handling in attach-url.mjs so the reuse branch only accepts the v2 bare-route
shape by parsing the URL and requiring v=2, a tr parameter, and no payload
before preserving payload.attach_url; keep the existing route-count guard and
leave the fallback encoding path for non-canonical links.
ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift (1)

1439-1461: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This test still exercises the legacy payload= attach URL, not the new QR grammar.

The updated comment says this covers the current QR path, but Lines 1458-1461 still build cmux-ios://attach?...&payload=.... That won't catch regressions in CmxPairingQRCode's tr= encoding/decoding or the ticket-ref redemption flow. Please either build this fixture through the real QR encoder or rename it as a legacy attach-URL test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift` around lines
1439 - 1461, The fixture in this test is still using the legacy attach URL with
payload= instead of the current QR grammar, so update it to exercise the real QR
path or rename it to reflect legacy behavior. Use the relevant symbols
CmxPairingQRCode and CmxAttachTicketCompactCoder to build the QR payload with
tr= encoding/decoding and ensure the ticket-ref redemption flow is covered. If
you keep the legacy URL shape, make the test name and comments explicit that it
only validates the old attach-link format.
cmuxTests/TerminalControllerSocketSecurityTests.swift (1)

621-626: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add redeem authz coverage
The capability-set assertion is fine, but there’s still no test showing mobile.attach_ticket.redeem rejects unauthenticated or cross-account callers. Add a focused authorization case alongside this coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/TerminalControllerSocketSecurityTests.swift` around lines 621 -
626, Add a focused authorization test for mobile.attach_ticket.redeem in
TerminalControllerSocketSecurityTests to cover unauthenticated and cross-account
callers. Extend the existing capability/auth coverage by adding a case that
invokes redeem through the same socket test harness and asserts it is rejected
when no valid auth context is present or when the caller belongs to a different
account. Use the existing TerminalControllerSocketSecurityTests helpers and the
mobile.attach_ticket.redeem method name to keep the new check aligned with the
current security coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift`:
- Around line 339-340: The redeem flow in MobileCoreRPCClient should fail closed
when the returned ticket does not match the requested ticketRef. In the
redemption path around MobileAttachTicketRedeemResponse.decode and
Self.redeemedTicket, validate the redeemed ticket against the original request
before updating ticketState, and do not fall back to the scanned reference
unless it is the only authoritative source. Make sure the logic in the fallback
location also preserves a single source of truth so a mismatched redeem response
cannot overwrite state for a different reference.

In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift`:
- Around line 38-40: The detached completion path in
MobileCoreRPCTicketRedemptionGate’s redemption tracking is clearing the gate
state too early, which drops the timeout cooldown set by timeoutWaiter() before
timedOutResetNanoseconds elapses. Update the Task.detached completion observer
and the related cleanup in clear(id:) / timeoutWaiter() so task completion only
removes abandoned work, while the timedOutUntil sentinel remains in place until
the reset window naturally expires. Ensure the next authorization check still
sees the timed-out state after a cancelled redemption finishes.

In
`@Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift`:
- Around line 162-168: The v2 pairing URL tests only cover the valid
ticket-reference path, so add a fail-closed assertion for decoding a v2 attach
URL without tr. Update the relevant decoder coverage in
CmxAttachTicketInputTests and the v2 pairing QR code tests around
CmxPairingQRCodeTests to verify that CmxAttachTicketInput/CmxPairingQRCode
rejects or returns nil for cmux-ios://attach?v=2&r=... when ticketRef is
missing, instead of treating it as valid input.

In
`@Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift`:
- Around line 416-423: The test in MobileCoreRPCClientTests around secondTask is
relying on Task.sleep polling, which can pass without proving the second caller
actually joined the in-flight redemption. Replace the fixed-wait loop with an
explicit synchronization signal from the redemption path, similar to the earlier
queuedRequestIDs check, and only release redeemRelease once that signal confirms
the second authorized call has entered the shared redemption flow.

In
`@Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/ScriptedRPCTransport.swift`:
- Around line 66-70: The scripted transport is swallowing serialization and
frame-encoding failures in enqueueResponse(_:), which hides bad fixtures and can
leave tests hanging. Update the ScriptedRPCTransport send(_:) flow to surface
these failures immediately by making the response-enqueue path throw instead of
returning silently, and propagate the JSONSerialization/MobileSyncFrameCodec
errors back to the caller so the failing scripted response is reported at the
source.

In `@Sources/TerminalController.swift`:
- Around line 13541-13548: Remove the raw error string from the redeem RPC
failure response in TerminalController’s catch block: keep the localized
internal_error message in the .err result, but omit the data entry that
serializes the caught error. Update the redeem path so the API body does not
expose String(describing: error) or any other internal failure details.

---

Outside diff comments:
In `@cmuxTests/TerminalControllerSocketSecurityTests.swift`:
- Around line 621-626: Add a focused authorization test for
mobile.attach_ticket.redeem in TerminalControllerSocketSecurityTests to cover
unauthenticated and cross-account callers. Extend the existing capability/auth
coverage by adding a case that invokes redeem through the same socket test
harness and asserts it is rejected when no valid auth context is present or when
the caller belongs to a different account. Use the existing
TerminalControllerSocketSecurityTests helpers and the
mobile.attach_ticket.redeem method name to keep the new check aligned with the
current security coverage.

In `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift`:
- Around line 1439-1461: The fixture in this test is still using the legacy
attach URL with payload= instead of the current QR grammar, so update it to
exercise the real QR path or rename it to reflect legacy behavior. Use the
relevant symbols CmxPairingQRCode and CmxAttachTicketCompactCoder to build the
QR payload with tr= encoding/decoding and ensure the ticket-ref redemption flow
is covered. If you keep the legacy URL shape, make the test name and comments
explicit that it only validates the old attach-link format.

In `@scripts/lib/attach-url.mjs`:
- Around line 89-112: The canonical attach-URL check in isCanonicalAttachURL is
too loose and currently accepts secret-bearing v1 payload links as well as the
intended non-secret form. Update the attach-url handling in attach-url.mjs so
the reuse branch only accepts the v2 bare-route shape by parsing the URL and
requiring v=2, a tr parameter, and no payload before preserving
payload.attach_url; keep the existing route-count guard and leave the fallback
encoding path for non-canonical links.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08e69469-6813-46c0-83af-04b58d7e06de

📥 Commits

Reviewing files that changed from the base of the PR and between 5265559 and 2efad57.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (34)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxAttachTicketCompactCoder.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxAttachTicketCompactCoderTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/CmxAttachTicketInput.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileAttachTicketRedeemResponse.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketState.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/ScriptedRPCTransport.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/ScriptedRPCTransportFactory.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/CmxAttachTicket+ConstrainingRoutes.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/MobileHost/ControlCommandCoordinator+MobileHost.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorMobileHostTests.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Resources/Localizable.xcstrings
  • Sources/Mobile/MobileAttachTicketStore.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/Pairing/MobilePairingModel.swift
  • Sources/TerminalController.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • cmuxTests/TerminalControllerSocketSecurityTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
  • scripts/lib/attach-url.mjs
  • scripts/lib/attach-url.test.mjs

Comment thread Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift Outdated
Comment thread Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/ScriptedRPCTransport.swift Outdated
Comment thread Sources/TerminalController.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift (1)

32-40: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

One stuck abandoned redeem can permanently wedge later retries.

After the first timed-out task is moved into abandoned, a second timed-out redeem leaves current on a new id while the old cancelled task is still hanging. When that second cooldown expires, Line 37 rejects every later call until the first abandoned task finally finishes. If the transport ignores cancellation, pairing is bricked forever after two timeouts. The rollover path needs to cap or replace stale abandoned work instead of treating any old abandoned task as a permanent blocker.

Also applies to: 80-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift`
around lines 32 - 40, The rollover logic in MobileCoreRPCTicketRedemptionGate’s
timeout handling currently treats any non-empty abandoned collection as a
permanent blocker, which can wedge later retries after multiple timeouts. Update
the timeout path that moves work into abandoned so stale abandoned tasks are
capped, replaced, or pruned before adding the next timed-out request, and adjust
the guard in the current/timedOutUntil flow so later calls can proceed once
prior abandoned work is no longer relevant. Use the existing symbols current,
abandoned, timedOutUntil, and isCompleted to locate and fix the retry gating
behavior consistently in both affected sections.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/MobileHostAuthorizationTests.swift`:
- Around line 401-419: The test uses the process-global MobileHostService.shared
and mutates its accepted Stack auth token before awaiting async work, so
concurrent authorization tests can interfere with each other. Update
testDebugConfiguredStackAuthTokenAuthorizesAttachTicketRedeem to avoid shared
mutable state by using a per-test isolated MobileHostService instance or by
serializing access around the override and restore. Make sure the
debugConfigureAcceptedStackAuthTokenForTesting reset is guaranteed within the
same isolated scope so other tests cannot observe the temporary token.

In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift`:
- Line 145: The pairing QR parser in CmxPairingQRCode is reusing grammar version
2 for a new format that now requires tr, which breaks previously valid v=2 URLs
that only contain r. Update the versioned decoding logic so the tr-required
ticket-reference format uses a new grammar version (or make the v=2 path
backwards-compatible), and keep the existing v=2 decoder accepting the older
cmux-ios://attach?v=2&r=... shape. Make the change in the validation/parse flow
around the query checks and the version dispatch so the version-specific
compatibility path remains intact.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift`:
- Around line 32-40: The rollover logic in MobileCoreRPCTicketRedemptionGate’s
timeout handling currently treats any non-empty abandoned collection as a
permanent blocker, which can wedge later retries after multiple timeouts. Update
the timeout path that moves work into abandoned so stale abandoned tasks are
capped, replaced, or pruned before adding the next timed-out request, and adjust
the guard in the current/timedOutUntil flow so later calls can proceed once
prior abandoned work is no longer relevant. Use the existing symbols current,
abandoned, timedOutUntil, and isCompleted to locate and fix the retry gating
behavior consistently in both affected sections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b3161482-d068-470f-b0a2-8ab075dc41dd

📥 Commits

Reviewing files that changed from the base of the PR and between 2efad57 and e7f094e.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (20)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRBitmapTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingURLSchemeTests.swift
  • Packages/iOS/CmuxMobileCamera/Tests/CmuxMobileCameraTests/QRCodeFrameSelectionTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/ScriptedRPCTransport.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingAttemptDeadlineTests.swift
  • Packages/iOS/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobilePairingScannerPolicyTests.swift
  • Packages/iOS/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift
  • Sources/TerminalController.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
  • scripts/lib/attach-url.mjs
  • scripts/lib/attach-url.test.mjs

Comment thread cmuxTests/MobileHostAuthorizationTests.swift
Comment thread Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
scripts/lib/attach-url.mjs (1)

90-92: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not reject canonical v3 URLs when Swift strips non-QR routes.

MobileAttachTicketStore.attachURL(for:) intentionally omits loopback/dev-only routes from the canonical v3 URL, but this guard still compares payload.attach_url against the raw payload.ticket.routes.length. For a ticket with one Tailscale route plus one loopback route, isCanonicalAttachURL(..., 2) returns false and buildAttachURL falls back to the v1 payload= URL, reintroducing the bearer token the new flow is meant to hide. It also leaves attach_url and result.routes free to disagree about the effective route set. Validate against the QR-eligible subset (or parse the canonical URL back into the returned route list) instead of the unfiltered ticket routes.

Also applies to: 126-129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/attach-url.mjs` around lines 90 - 92, The canonical v3 attach URL
check in buildAttachURL is using the full payload.ticket.routes length, which
causes valid Swift-generated URLs to fail when loopback/dev-only routes are
stripped. Update the validation around isCanonicalAttachURL and the routes
length comparison to use only the QR-eligible/effective route subset (or derive
the expected routes from the canonical URL) so attach_url and result.routes can
agree without falling back to the v1 payload URL.

Source: Path instructions

Sources/Mobile/MobileAttachTicketStore.swift (1)

25-25: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not grow this lock-owned store further.

This change adds another mutable index plus new lookup/write paths under NSLock. The repo’s Swift rules require shared runtime state like this to live behind actor isolation unless there is a documented low-level reason a lock is unavoidable. Please move MobileAttachTicketStore to an actor (or document that constraint here) before expanding the lock-based design. As per coding guidelines, "Do not add NSLock, pthread_mutex, or similar manual locking around shared mutable state when an actor or MainActor-isolated model would be safer; only allow very small lock usage around non-async low-level platform bridges when the code documents why an actor cannot be used."

Also applies to: 27-68, 112-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Mobile/MobileAttachTicketStore.swift` at line 25,
`MobileAttachTicketStore` is growing more shared mutable state under manual
locking, so move its runtime state behind actor isolation instead of expanding
the `NSLock`-based design. Refactor the store so the mutable dictionaries and
lookup/write paths are owned by an actor (or, if that is truly impossible, add a
clear documented justification for why `NSLock` is unavoidable). Update the
affected methods and access patterns in `MobileAttachTicketStore` to use the
actor’s isolated APIs, and remove the lock-based shared-state handling around
`authTokensByTicketRef` and the other mutable indices.

Source: Coding guidelines

Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift (1)

47-151: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Avoid wall-clock timing in these redemption tests.

These assertions depend on real 1ms/10ms timeout windows plus fixed Task.sleep delays, so they can fail under CI load even when the gate is correct. Inject a controllable clock into MobileCoreRPCTicketRedemptionGate and advance it from the test instead of sleeping real time. As per coding guidelines, "Tests must not depend on real wall-clock time" and timeout behavior must use a virtual/fake clock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift`
around lines 47 - 151, The redemption tests currently rely on real timeout
windows and Task.sleep, which makes them flaky under load. Update
MobileCoreRPCTicketRedemptionGate to accept a controllable clock and drive
timeout/cooldown behavior through that clock instead of wall-clock time. Then
rewrite timedOutTicketReferenceRedemptionKeepsCooldownAfterTaskCompletes and
repeatedTimedOutTicketReferenceRedemptionsDoNotWedgeRetry to advance the fake
clock and assert against the gate’s timeout behavior without sleeping.

Source: Coding guidelines

Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift (1)

102-145: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep canceled and superseded provider tasks tracked until they actually finish.

cancelWaiter() drops current after canceling its observer, and replaceAbandoned(with:) does the same to older abandoned entries. If the provider ignores cancellation, those redeem tasks keep running untracked, so a later caller can start another redeem concurrently while the old one is still in flight. Keep them in abandoned and let complete(id:) retire them instead of canceling the observer early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift`
around lines 102 - 145, The waiter cancellation and abandoned-task cleanup logic
in MobileCoreRPCTicketRedemptionGate is dropping in-flight provider work too
early. Update cancelWaiter(id:), clear(id:), replaceAbandoned(with:), and
complete(id:) so canceled or superseded redemption tasks remain tracked in
abandoned until complete(id:) retires them, instead of canceling
completionObserver and nil-ing current immediately. Use the existing current and
abandoned bookkeeping to preserve task tracking even when the provider ignores
cancellation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift`:
- Around line 145-159: The v3 pairing URL check in
CmxPairingQRCode.isPairingCodeURL(_:) is too permissive because it accepts any
URL with a non-empty tr even when there are no r route items. Tighten the
validator to require at least one route for Self.version, matching the legacy
branch, so isPairingCodeURLString(_:) in MobilePairingModel only treats
attach_url values as QR-safe when they are actually decodable.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift`:
- Around line 102-145: The waiter cancellation and abandoned-task cleanup logic
in MobileCoreRPCTicketRedemptionGate is dropping in-flight provider work too
early. Update cancelWaiter(id:), clear(id:), replaceAbandoned(with:), and
complete(id:) so canceled or superseded redemption tasks remain tracked in
abandoned until complete(id:) retires them, instead of canceling
completionObserver and nil-ing current immediately. Use the existing current and
abandoned bookkeeping to preserve task tracking even when the provider ignores
cancellation.

In
`@Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift`:
- Around line 47-151: The redemption tests currently rely on real timeout
windows and Task.sleep, which makes them flaky under load. Update
MobileCoreRPCTicketRedemptionGate to accept a controllable clock and drive
timeout/cooldown behavior through that clock instead of wall-clock time. Then
rewrite timedOutTicketReferenceRedemptionKeepsCooldownAfterTaskCompletes and
repeatedTimedOutTicketReferenceRedemptionsDoNotWedgeRetry to advance the fake
clock and assert against the gate’s timeout behavior without sleeping.

In `@scripts/lib/attach-url.mjs`:
- Around line 90-92: The canonical v3 attach URL check in buildAttachURL is
using the full payload.ticket.routes length, which causes valid Swift-generated
URLs to fail when loopback/dev-only routes are stripped. Update the validation
around isCanonicalAttachURL and the routes length comparison to use only the
QR-eligible/effective route subset (or derive the expected routes from the
canonical URL) so attach_url and result.routes can agree without falling back to
the v1 payload URL.

In `@Sources/Mobile/MobileAttachTicketStore.swift`:
- Line 25: `MobileAttachTicketStore` is growing more shared mutable state under
manual locking, so move its runtime state behind actor isolation instead of
expanding the `NSLock`-based design. Refactor the store so the mutable
dictionaries and lookup/write paths are owned by an actor (or, if that is truly
impossible, add a clear documented justification for why `NSLock` is
unavoidable). Update the affected methods and access patterns in
`MobileAttachTicketStore` to use the actor’s isolated APIs, and remove the
lock-based shared-state handling around `authTokensByTicketRef` and the other
mutable indices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fc51e0b9-71fe-4ea1-9d45-ee47ad9ed19e

📥 Commits

Reviewing files that changed from the base of the PR and between e7f094e and fab1e72.

📒 Files selected for processing (24)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRBitmapTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingURLSchemeTests.swift
  • Packages/iOS/CmuxMobileCamera/Tests/CmuxMobileCameraTests/QRCodeFrameSelectionTests.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/CmxAttachTicketInput.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCTicketRedemptionGate.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileHostStatusResponse.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCTicketRedemptionGateTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingAttemptDeadlineTests.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingFailureTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileShellRouteAuthPolicy.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileShellRouteAuthPolicyTests.swift
  • Packages/iOS/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobilePairingScannerPolicyTests.swift
  • Packages/iOS/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift
  • Sources/Mobile/MobileAttachTicketStore.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/Mobile/Pairing/MobilePairingModel.swift
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
  • scripts/lib/attach-url.mjs
  • scripts/lib/attach-url.test.mjs

austinywang and others added 3 commits June 29, 2026 16:43
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
Repeated timed-out redemptions could accumulate one retained task plus its
completion observer per attempt: `abandon(_:)` cancelled prior abandoned work
but never dropped it, so a non-cooperative provider that ignores cancellation
and never resolves `task.result` would grow memory without bound on this
long-lived RPC owner. Supersede prior abandoned work on each rollover so the
retained set stays bounded to the most recent attempt, and cover it with a
repeated-timeout assertion on the new `abandonedCount`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pairing-ticket-reference change adds the public `emptyTicketRef` case to
this shared-package error enum; document the type and each case so the public
API surface carries Swift-DocC comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
austinywang and others added 2 commits July 1, 2026 22:59
Older Macs stamp the compact pairing grammar as v == 1 while writing the
opaque Stack user id (no @) into `u`. The current decoder maps every v == 1
payload's `u` to macUserEmail, so a phone on the new grammar reads that
opaque id as an email and the account preflight rejects a still-valid
pairing before dialing. This test pins the expected behavior (u decodes to
macUserID) and fails without the accompanying fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
origin/main's compact encoder stamped v == 1 and wrote the opaque Stack
user id into `u`, and its decoder disambiguated email vs id with an `@`
probe. The new grammar-version routing dropped that probe and mapped every
v == 1 `u` to macUserEmail, so a phone on the new build scanning an older
Mac's still-valid QR set macUserEmail to an opaque id (macUserID nil) and
MobileShellComposite.emailFailure rejected the pairing during account
preflight before dialing.

Restore the `@` heuristic for the legacy v == 1 grammar while keeping the
explicit user-id interpretation for the current v == 2 grammar. Fixes the
regression pinned by the preceding test commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 45 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

2369-2832: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

File keeps absorbing unrelated responsibility; consider splitting before it grows further.

This diff adds a cancellation-aware mac-switch-attempt state machine (beginMacSwitchAttempt/switchToMac/restorePreviousMacIfNeeded/serialized paired-mac writes) and a large terminal-replay-barrier subsystem (beginTerminalReplayBarrier through cancelAllTerminalReplayTasks, plus hybrid-transport delivery decisions) into a file that is already far past 800 lines. Per the repo's own file/package-boundary rule, an already-oversized production file growing by hundreds of lines across independently-testable concerns (pairing/switch coordination, terminal replay protocol, persistence write serialization) should have that logic extracted behind a smaller, independently testable type or SwiftPM package boundary rather than accreting further into this one file.

As per path instructions: "Report a failure when a new production Swift file exceeds 400 lines without a clear single responsibility, or exceeds 800 lines even when the responsibility is mostly coherent... Report a failure when an existing production Swift file already over 800 lines grows by more than 250 lines, unless the PR removes or moves a mixed responsibility behind a new package boundary."

Also applies to: 6691-7371

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 2369 - 2832, The file is absorbing multiple unrelated
responsibilities and should be refactored to honor the repo’s file-boundary
rule. Extract the mac-switch attempt/state-machine logic centered on
switchToMac, promoteSecondaryToForeground, restorePreviousMacIfNeeded, and the
serialized paired-mac write helpers into a separate type or package boundary,
and do the same for the terminal replay/barrier flow if it remains mixed here.
Keep MobileShellComposite focused on orchestration only, with these subsystems
moved behind smaller, independently testable abstractions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 2369-2832: The file is absorbing multiple unrelated
responsibilities and should be refactored to honor the repo’s file-boundary
rule. Extract the mac-switch attempt/state-machine logic centered on
switchToMac, promoteSecondaryToForeground, restorePreviousMacIfNeeded, and the
serialized paired-mac write helpers into a separate type or package boundary,
and do the same for the terminal replay/barrier flow if it remains mixed here.
Keep MobileShellComposite focused on orchestration only, with these subsystems
moved behind smaller, independently testable abstractions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06d94ff6-014e-4e50-8320-eca47954fbbf

📥 Commits

Reviewing files that changed from the base of the PR and between 8a7e0e9 and e8033f6.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (9)
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxAttachTicketCompactCoder.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift
  • Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift
  • Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxAttachTicketCompactCoderTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Resources/Localizable.xcstrings
  • Sources/TerminalController.swift
  • cmuxTests/TerminalControllerSocketSecurityTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
💤 Files with no reviewable changes (3)
  • cmuxTests/TerminalControllerSocketSecurityTests.swift
  • Sources/TerminalController.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

austinywang and others added 2 commits July 2, 2026 03:31
Address the one-major-type-per-file and pure-helper conventions on the
ticket-redemption path:

- Move MobileCoreRPCTicketRedemptionGate's nested `Current`/`Abandoned`
  structs into MobileCoreRPCTicketRedemptionGate+Current.swift and
  +Abandoned.swift (unqualified references inside the actor still resolve).
- Extract the `ManualNanosecondClock` test clock and the `AsyncReleaseGate`
  test gate into their own files so each test file declares one major type.
- Convert MobileCoreRPCClient's two pure ticket-redemption helpers
  (`ticketReferenceRequiringRedemption`, `redeemedTicket`) from private
  static funcs into instance methods on the existing private extension.
- Replace MobileAttachTicketRedeemResponse's static `decode(_:)` namespace
  method with an `init(decoding:)` initializer.

No behavior change: the package builds and all 74 tests in 9 suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit moved `ticketReferenceRequiringRedemption` and
`redeemedTicket` onto MobileCoreRPCClient's private extension, which pushed
MobileCoreRPCClient.swift past its Swift file-length budget (519 > 513).

Move both helpers into MobileCoreRPCClient+TicketRedemption.swift (matching
the package's existing `+Feature.swift` convention). MobileCoreRPCClient.swift
drops to 480 lines, back under budget with no budget refresh or added debt.
No behavior change; the package builds and all 74 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

austinywang and others added 2 commits July 2, 2026 03:48
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
A redeem reply that omits its own workspace/terminal scope (empty or
whitespace-only workspaceID/terminalID) previously passed straight
through in redeemedTicket(_:ticketRef:constrainedTo:) and, because
CmxAttachTicket.validate() does not reject an empty workspace/terminal,
was stored via ticketState.replace without a scope check. A malformed or
partial redeem response could thereby widen the effective ticket past the
workspace/terminal the QR was scoped to.

Treat empty/whitespace-only workspaceID/terminalID in the reply as gaps
and fall back to the scanned scope, matching the existing mac* gap-fill
pattern in the same merge. A reply that carries its own scope still takes
precedence over the scan.

Regression test MobileCoreRPCRedeemScopeTests covers both directions.
Verified locally red/green on the CmuxMobileRPC package: with the fix
reverted, emptyReplyScopeFallsBackToScannedScope fails
(merged.workspaceID -> "   ", terminalID -> "  "); with the fix present
both tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

austinywang and others added 8 commits July 2, 2026 04:31
The prior scope merge only fell back to the scanned scope when the redeem
reply's workspace/terminal was empty. A reply carrying a *different*
non-empty workspaceID/terminalID still overwrote the scanned scope, so a
partial or hostile redeem response could retarget the ticket to a
workspace/terminal other than the QR the user actually scanned. The
redeem path only asserts ticketRef consistency, not scope, and
CmxAttachTicket.validate() does not constrain scope, so nothing else
caught this.

Make the scanned scope authoritative per field via
scopeFieldPreferringScanned: a non-empty scanned value always wins, and a
redeemed value only fills a field the scan left empty. This honors the
function's `constrainedTo scanned` contract and matches the compact v=3
grammar, which always scans empty scope (CmxPairingQRCode.canEncode
requires empty workspace/terminal), so v=3 still adopts the redeemed
scope while legacy non-empty scanned scope can no longer be retargeted.

Addresses cubic P1 on 19e634a. Regression coverage in
MobileCoreRPCRedeemScopeTests now asserts three directions: empty reply
-> scanned, mismatched non-empty reply -> scanned, and empty scanned
(v=3) -> redeemed. Verified red/green locally: with the guard reverted,
mismatchedReplyScopeFallsBackToScannedScope fails
(merged.workspaceID -> "ws-other", terminalID -> "term-other") while the
v=3 and empty-reply tests still pass; full CmuxMobileRPC suite green
(77 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
`scopeFieldPreferringScanned` is a pure, stateless helper. The cmux
Aziz package-design policy (static-as-namespace / pure-helper) flags a
`private static func` attached to a type that is called as `Type.method`
as a caseless namespace in disguise. Move it to a file-scope `private
func` and drop the `Self.` qualifier at its two call sites. Behavior is
unchanged; the full CmuxMobileRPC suite (77 tests) stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`waiterCount`/`abandonedCount` on MobileCoreRPCTicketRedemptionGate had
no production callers — they existed only for tests, a test-observation
seam in production source. Following the cmux precedent (PR #6452),
remove the accessors and widen the backing state to `private(set)`
(internal read, private write) so `@testable` tests read `current?.waiters`
and `abandoned.count` directly. The sibling RPCStackTokenGate keeps its
state private with no such accessors, so this also restores symmetry.
Behavior is unchanged; the full CmuxMobileRPC suite (77 tests) stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 3f1b92d moved `scopeFieldPreferringScanned` to a file-scope
`private func` to satisfy the Aziz static-as-namespace policy, but that
tripped the `package-conventions-lint` CI check (`free-function` rule:
no top-level free functions — scope functionality to a type).

The two rules only conflict on `static func` vs file-scope: the Aziz
grep matches `static func`/`class func`; the convention lint's regex is
anchored at column 0, so it only matches file-scope declarations. An
instance method on the extension (indented, non-static) satisfies both.
Keep it grouped with its sole caller `redeemedTicket`, and add a comment
so it is not "cleaned up" back into either flagged form.

Behavior is unchanged; the full CmuxMobileRPC suite (77 tests) stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression coverage for a concurrency bug in the ticket-reference
redemption path. `ticketForRequest` shares one redemption task across
concurrent authorized requests (via MobileCoreRPCTicketRedemptionGate),
but the provider captures the *triggering* request's `deadline` and
passes it into `redeemAttachTicket` → `session.send`. A short-deadline
request that times out while the redeem is in flight therefore times out
the shared `session.send`, so a concurrent longer-deadline waiter fails
spuriously even though it had ample budget.

This mirrors the existing `shortTokenTimeoutDoesNotCancelLongerTokenWaiter`
coverage for the Stack-token gate, whose provider correctly takes no
caller deadline. The test is red at this commit (the longer waiter throws
`requestTimedOut`); the fix follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ticketForRequest` deduplicates ticket-reference redemption across
concurrent authorized requests through MobileCoreRPCTicketRedemptionGate,
but the shared provider passed the triggering request's `deadline` into
`redeemAttachTicket` → `session.send`. When the first waiter had little
budget left, its deadline timed out the shared redeem and every waiter
failed — even one that joined with a much longer timeout.

Give the shared provider a gate-governed (unbounded) deadline instead.
Each waiter's own budget is already enforced by the gate
(`timeoutNanoseconds:` + cancellation once the last waiter leaves), and
`session.send` is cancellation-aware, so an abandoned redemption is still
torn down. This matches RPCStackTokenGate, whose shared provider likewise
carries no caller deadline.

Turns the regression test from the previous commit green; full
CmuxMobileRPC suite (78 tests) passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `shorterWaiterTimeoutDoesNotPoisonSharedTicketRedemption` regression
test grows MobileCoreRPCClientTests.swift by ~106 lines (621 -> 727),
which the file-length budget guard flags. Refresh the budget to accept
this test-coverage growth (the file is a test suite; the added coverage
proves a real concurrency fix). Regenerated with
`scripts/swift_file_length_budget.py --write-budget`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
…bile-pairing-qr-carry-a-ticket-refe

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 90dc248a Deployed Jul 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compact mobile-pairing QR: carry a ticket reference, not the full bearer token

3 participants