From 9149162a52be1c491f99a281b8b3da726b2947cd Mon Sep 17 00:00:00 2001 From: CodePilot Date: Sat, 4 Jul 2026 05:32:01 +0200 Subject: [PATCH] fix: require approved remote pairing for beta --- .../RemoteDesktopCoordinator.swift | 42 +++++++++++++++---- .../RemoteDesktop/RemoteDesktopModels.swift | 7 ++++ .../RemoteDesktopWindowController.swift | 1 + Sources/CodexAccountSwitcher/main.swift | 11 ++--- .../RemoteDesktopCoordinatorTests.swift | 39 ++++++++++++++++- .../RemoteDesktopModelsTests.swift | 7 ++++ docs/FAQ.md | 2 +- docs/INSTALL_IOS.md | 6 +-- ios/CodexPhone/CodexPhone/CodexPhoneApp.swift | 22 +++++++++- .../RemoteDesktop/RemoteDesktopAPI.swift | 3 +- .../RemoteDesktop/RemoteDesktopModels.swift | 7 ++++ .../RemoteDesktop/RemotePairingView.swift | 22 ++++++---- .../CodexPhoneTests/RemoteDesktopTests.swift | 23 +++++++++- ops/agents/escalations/issue-triage.md | 7 ++-- ops/agents/triage/2026-07-04-issue-triage.md | 37 ++++++++++++++++ 15 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 ops/agents/triage/2026-07-04-issue-triage.md diff --git a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopCoordinator.swift b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopCoordinator.swift index 9f1a6610..288ea4a3 100644 --- a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopCoordinator.swift +++ b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopCoordinator.swift @@ -38,6 +38,7 @@ struct PendingPairing: Equatable { let challenge: RemotePairingChallenge let publicKeyRawRepresentation: Data let keyFingerprint: String + var approvalToken: VerifiedPairingApprovalToken? } struct ActiveRemoteSession: Equatable { @@ -139,7 +140,8 @@ final class RemoteDesktopCoordinator { name: name, challenge: challenge, publicKeyRawRepresentation: publicKeyRawRepresentation, - keyFingerprint: Self.keyFingerprint(publicKeyRawRepresentation) + keyFingerprint: Self.keyFingerprint(publicKeyRawRepresentation), + approvalToken: nil ) pendingPairing = pending try appendAuditLocked(.pairingChallengeIssued, deviceID: deviceID, sessionID: nil, leaseID: nil, reason: nil) @@ -147,16 +149,42 @@ final class RemoteDesktopCoordinator { return pending } - func approvePendingPairing() throws { + func verifyPendingPairing( + challengeID: String, + deviceID: String, + signature: Data + ) throws -> RemotePairingApprovalStatus { lock.lock() defer { lock.unlock() } - guard let pendingPairing else { return } - _ = try pairingStore.trustPreverifiedDevice( - id: pendingPairing.deviceID, - name: pendingPairing.name, - publicKeyRawRepresentation: pendingPairing.publicKeyRawRepresentation + guard var pendingPairing, + pendingPairing.challenge.id == challengeID, + pendingPairing.deviceID == deviceID else { + throw RemoteDesktopSecurityError.challengeUnknown + } + let token = try pairingStore.verifyChallenge( + pendingPairing.challenge, + deviceID: deviceID, + signature: signature + ) + pendingPairing.approvalToken = token + self.pendingPairing = pendingPairing + try appendAuditLocked(.pairingChallengeVerified, deviceID: deviceID, sessionID: nil, leaseID: nil, reason: nil) + rebuildSnapshotLocked() + return RemotePairingApprovalStatus( + status: "pending_mac_approval", + challengeID: challengeID, + deviceID: deviceID, + macName: pendingPairing.challenge.macName ) + } + + func approvePendingPairing() throws { + lock.lock() + defer { lock.unlock() } + + guard let pendingPairing, let approvalToken = pendingPairing.approvalToken else { return } + _ = try pairingStore.approveDevice(using: approvalToken) self.pendingPairing = nil try appendAuditLocked(.pairingApproved, deviceID: pendingPairing.deviceID, sessionID: nil, leaseID: nil, reason: nil) rebuildSnapshotLocked() diff --git a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopModels.swift b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopModels.swift index 988d16ab..6e82238f 100644 --- a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopModels.swift +++ b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopModels.swift @@ -39,6 +39,13 @@ struct RemotePairingChallenge: Codable, Equatable, Identifiable { let expiresAt: Date } +struct RemotePairingApprovalStatus: Codable, Equatable { + let status: String + let challengeID: String + let deviceID: String + let macName: String +} + struct TrustedRemoteDevice: Codable, Equatable, Identifiable { let id: String var name: String diff --git a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopWindowController.swift b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopWindowController.swift index 076fca40..3105df5a 100644 --- a/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopWindowController.swift +++ b/Sources/CodexAccountSwitcher/RemoteDesktop/RemoteDesktopWindowController.swift @@ -65,6 +65,7 @@ final class RemoteDesktopWindowController: NSWindowController { if let pending = snapshot.pendingPairing { addSection("Pending Pairing", rows: [ "Device: \(pending.name)", + "Status: \(pending.approvalToken == nil ? "Waiting for iPhone proof" : "Ready for Mac approval")", "Code: \(pending.challenge.code)", "Expires: \(Self.dateFormatter.string(from: pending.challenge.expiresAt))", "Key: \(pending.keyFingerprint)" diff --git a/Sources/CodexAccountSwitcher/main.swift b/Sources/CodexAccountSwitcher/main.swift index eec8eb05..18d83e04 100644 --- a/Sources/CodexAccountSwitcher/main.swift +++ b/Sources/CodexAccountSwitcher/main.swift @@ -3354,17 +3354,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { let challengeID = try Self.remoteDesktopString(payload["challengeId"]) let deviceID = try Self.remoteDesktopString(payload["deviceId"]) let signature = try Self.remoteDesktopBase64(payload["signature"]) - guard let challenge = coordinator.pairingStore.challenge(id: challengeID) else { - return Self.remoteDesktopRPCError(request.id, status: 410, code: "pairing_expired") - } - let token = try coordinator.pairingStore.verifyChallenge( - challenge, + let approval = try coordinator.verifyPendingPairing( + challengeID: challengeID, deviceID: deviceID, signature: signature ) - let device = try coordinator.pairingStore.approveDevice(using: token) - coordinator.refreshStatus() - return try Self.remoteDesktopRPCCodable(request.id, device) + return try Self.remoteDesktopRPCCodable(request.id, approval) case "devices.list": coordinator.refreshStatus() diff --git a/Tests/CodexAccountSwitcherTests/RemoteDesktopCoordinatorTests.swift b/Tests/CodexAccountSwitcherTests/RemoteDesktopCoordinatorTests.swift index eb7283d0..60c3b849 100644 --- a/Tests/CodexAccountSwitcherTests/RemoteDesktopCoordinatorTests.swift +++ b/Tests/CodexAccountSwitcherTests/RemoteDesktopCoordinatorTests.swift @@ -24,7 +24,8 @@ final class RemoteDesktopCoordinatorTests: XCTestCase { func testPairingCanBeApprovedOrRejectedLocally() throws { let coordinator = try makeCoordinator() - let publicKey = P256.Signing.PrivateKey().publicKey.rawRepresentation + let privateKey = P256.Signing.PrivateKey() + let publicKey = privateKey.publicKey.rawRepresentation let pending = try coordinator.beginPairing( deviceID: "phone-1", @@ -39,12 +40,46 @@ final class RemoteDesktopCoordinatorTests: XCTestCase { coordinator.rejectPendingPairing() XCTAssertNil(coordinator.snapshot.pendingPairing) - _ = try coordinator.beginPairing( + let approvalPending = try coordinator.beginPairing( deviceID: "phone-1", name: "Beta iPhone", publicKeyRawRepresentation: publicKey, macName: "Mac host" ) + _ = try coordinator.verifyPendingPairing( + challengeID: approvalPending.challenge.id, + deviceID: "phone-1", + signature: try privateKey.signature(for: Data(approvalPending.challenge.code.utf8)).derRepresentation + ) + try coordinator.approvePendingPairing() + + XCTAssertNil(coordinator.snapshot.pendingPairing) + XCTAssertEqual(coordinator.snapshot.trustedDevices.map(\.id), ["phone-1"]) + } + + func testVerifiedPairingWaitsForMacApprovalBeforeTrustingDevice() throws { + let coordinator = try makeCoordinator() + let privateKey = P256.Signing.PrivateKey() + let pending = try coordinator.beginPairing( + deviceID: "phone-1", + name: "Beta iPhone", + publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation, + macName: "Mac host" + ) + + let approval = try coordinator.verifyPendingPairing( + challengeID: pending.challenge.id, + deviceID: "phone-1", + signature: try privateKey.signature(for: Data(pending.challenge.code.utf8)).derRepresentation + ) + + XCTAssertEqual(approval.status, "pending_mac_approval") + XCTAssertEqual(approval.challengeID, pending.challenge.id) + XCTAssertEqual(approval.deviceID, "phone-1") + XCTAssertEqual(approval.macName, "Mac host") + XCTAssertEqual(coordinator.snapshot.pendingPairing?.deviceID, "phone-1") + XCTAssertEqual(coordinator.snapshot.trustedDevices, []) + try coordinator.approvePendingPairing() XCTAssertNil(coordinator.snapshot.pendingPairing) diff --git a/Tests/CodexAccountSwitcherTests/RemoteDesktopModelsTests.swift b/Tests/CodexAccountSwitcherTests/RemoteDesktopModelsTests.swift index 3e506ae6..3ca21ad8 100644 --- a/Tests/CodexAccountSwitcherTests/RemoteDesktopModelsTests.swift +++ b/Tests/CodexAccountSwitcherTests/RemoteDesktopModelsTests.swift @@ -46,6 +46,12 @@ final class RemoteDesktopModelsTests: XCTestCase { sequence: 11, reason: .busy ) + let approvalStatus = RemotePairingApprovalStatus( + status: "pending_mac_approval", + challengeID: "challenge-1", + deviceID: device.id, + macName: "Office Mac" + ) try assertRoundTrip(RemoteInputKind.keyDown) try assertRoundTrip(RemoteInputEvent( @@ -74,6 +80,7 @@ final class RemoteDesktopModelsTests: XCTestCase { macName: "Office Mac", expiresAt: date )) + try assertRoundTrip(approvalStatus) try assertRoundTrip(device) try assertRoundTrip(RemoteDesktopLease( id: "lease-1", diff --git a/docs/FAQ.md b/docs/FAQ.md index cc838cbe..1329ec66 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -18,7 +18,7 @@ Not yet. The name is provider-neutral, but the first beta is Codex-focused. Futu ## Do I need Cloudflare? -Only for access away from your local network. Same-network iPhone access can use a local gateway URL. Remote access should use HTTPS and a private gateway token. +Yes for the public beta iPhone setup path. The Mac gateway listens on local loopback by default, so the iPhone app uses a Cloudflare Tunnel URL until CodePilot provides an explicit LAN-binding mode with firewall and trust guidance. ## Does CodePilot send my Codex credentials to a hosted service? diff --git a/docs/INSTALL_IOS.md b/docs/INSTALL_IOS.md index 55f84e9e..eee51c21 100644 --- a/docs/INSTALL_IOS.md +++ b/docs/INSTALL_IOS.md @@ -7,7 +7,7 @@ CodePilot iOS connects to a Mac running the CodePilot gateway. - A Mac running the CodePilot gateway. - A reachable gateway URL. - The gateway bearer token from the Mac. -- Optional: Cloudflare Tunnel for remote access outside the local network. +- Cloudflare Tunnel for the public beta connection path. ## Beta Installation @@ -19,7 +19,7 @@ Developers building from source can open the Xcode project under `ios/CodexPhone On first launch: -1. Choose **Same Network** or **Cloudflare**. +1. Choose **Cloudflare**. 2. Enter the gateway URL. 3. Enter the token from `~/.codex-account-switcher/phone-gateway-token` on the Mac. 4. Tap **Test Connection**. @@ -28,7 +28,7 @@ Success means the app shows the active account from the Mac gateway. The app stores these values locally on the device. -For the default Mac setup, choose **Cloudflare** and enter the Cloudflare Tunnel URL. The Mac gateway listens on `127.0.0.1:18790` by default, which is only reachable from the Mac itself. Use **Same Network** only after configuring the gateway to listen on a Mac LAN address. +For the public beta, enter the Cloudflare Tunnel URL. The Mac gateway listens on `127.0.0.1:18790` by default, which is only reachable from the Mac itself. Same Network setup is disabled until CodePilot provides an explicit LAN-binding mode with firewall and trust guidance. The gateway URL must include `http://` or `https://`. diff --git a/ios/CodexPhone/CodexPhone/CodexPhoneApp.swift b/ios/CodexPhone/CodexPhone/CodexPhoneApp.swift index 29c4c8f5..303d27e8 100644 --- a/ios/CodexPhone/CodexPhone/CodexPhoneApp.swift +++ b/ios/CodexPhone/CodexPhone/CodexPhoneApp.swift @@ -267,7 +267,7 @@ struct EmptySettingsView: View { Section("Gateway") { Picker("Connection", selection: $gatewayConnectionKind) { - ForEach(GatewayConnectionKind.allCases) { kind in + ForEach(GatewayConnectionKind.publicBetaCases) { kind in Text(kind.title).tag(kind.rawValue) } } @@ -308,6 +308,11 @@ struct EmptySettingsView: View { } } .navigationTitle("CodePilot") + .onAppear { + if !selectedConnectionKind.isPublicBetaAvailable { + gatewayConnectionKind = GatewayConnectionKind.cloudflare.rawValue + } + } } private var canTestConnection: Bool { @@ -361,6 +366,19 @@ enum GatewayConnectionKind: String, CaseIterable, Identifiable { var id: String { rawValue } + static var publicBetaCases: [GatewayConnectionKind] { + allCases.filter(\.isPublicBetaAvailable) + } + + var isPublicBetaAvailable: Bool { + switch self { + case .local: + false + case .cloudflare: + true + } + } + var title: String { switch self { case .local: @@ -373,7 +391,7 @@ enum GatewayConnectionKind: String, CaseIterable, Identifiable { var helpText: String { switch self { case .local: - "Use only after configuring the Mac gateway to listen on a LAN address." + "Same Network is disabled for public beta until LAN binding has explicit firewall and trust guidance." case .cloudflare: "Use your Cloudflare Tunnel hostname for access when you are away from the Mac." } diff --git a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopAPI.swift b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopAPI.swift index 80414194..837b810c 100644 --- a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopAPI.swift +++ b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopAPI.swift @@ -35,7 +35,7 @@ struct RemoteDesktopAPI { ) } - func completePairing(challengeID: String, deviceID: String, signature: Data) async throws -> TrustedRemoteDevice { + func completePairing(challengeID: String, deviceID: String, signature: Data) async throws -> RemotePairingApprovalStatus { try await request( "POST", path: "/api/remote/pairing/complete", @@ -191,6 +191,7 @@ struct RemoteDesktopHostStatus: Codable, Equatable { let screenRecordingGranted: Bool? let accessibilityGranted: Bool? let macUnlocked: Bool? + let trustedDeviceCount: Int? let displayFrame: RemoteDisplayFrame? let cursor: RemoteCursorPosition? let iceServers: [RemoteIceServer]? diff --git a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopModels.swift b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopModels.swift index 85200438..727fb49c 100644 --- a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopModels.swift +++ b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemoteDesktopModels.swift @@ -39,6 +39,13 @@ struct RemotePairingChallenge: Codable, Equatable, Identifiable { let expiresAt: Date } +struct RemotePairingApprovalStatus: Codable, Equatable { + let status: String + let challengeID: String + let deviceID: String + let macName: String +} + struct TrustedRemoteDevice: Codable, Equatable, Identifiable { let id: String var name: String diff --git a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemotePairingView.swift b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemotePairingView.swift index c7087db1..facb30a3 100644 --- a/ios/CodexPhone/CodexPhone/RemoteDesktop/RemotePairingView.swift +++ b/ios/CodexPhone/CodexPhone/RemoteDesktop/RemotePairingView.swift @@ -7,7 +7,6 @@ struct RemotePairingView: View { @AppStorage("gatewayToken") private var gatewayToken = "" @State private var statusText = "Not paired" @State private var isLoading = false - @State private var manualCode = "" @State private var identity = SoftwareRemoteDeviceIdentity(deviceID: UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString) var body: some View { @@ -15,9 +14,6 @@ struct RemotePairingView: View { Form { Section("Mac") { Text(statusText) - TextField("Pairing code", text: $manualCode) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() } Section("Device") { @@ -32,7 +28,7 @@ struct RemotePairingView: View { } label: { Label("Start Session", systemImage: "play.rectangle") } - .disabled(statusText == "Not paired") + .disabled(!canStartRemoteDesktop(statusText: statusText)) Button { Task { await refreshStatus() } @@ -78,10 +74,12 @@ struct RemotePairingView: View { status.accessibilityGranted == false ? "Accessibility" : nil ].compactMap { $0 }.joined(separator: " and ") statusText = "Allow \(missing) for CodePilot on the Mac" + } else if (status.trustedDeviceCount ?? 0) > 0 { + statusText = "Paired with this Mac" } else if status.capabilities?.relayAvailable == true { - statusText = "Host reachable, relay available" + statusText = "Not paired" } else { - statusText = "Host reachable, local/STUN only" + statusText = "Not paired" } } catch { statusText = "Host unavailable" @@ -99,12 +97,14 @@ struct RemotePairingView: View { publicKey: identity.publicKeyRawRepresentation ) let signature = try identity.sign(Data(challenge.code.utf8)) - _ = try await api.completePairing( + let approval = try await api.completePairing( challengeID: challenge.id, deviceID: identity.deviceID, signature: signature ) - statusText = "Paired with \(challenge.macName)" + statusText = approval.status == "pending_mac_approval" + ? "Waiting for approval on \(approval.macName)" + : "Pairing pending" } catch RemoteDesktopAPIError.server(_, let code) { statusText = code } catch { @@ -120,3 +120,7 @@ struct RemotePairingView: View { return RemoteDesktopAPI(baseURL: url, token: gatewayToken.trimmingCharacters(in: .whitespacesAndNewlines)) } } + +func canStartRemoteDesktop(statusText: String) -> Bool { + statusText.hasPrefix("Paired with ") +} diff --git a/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift b/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift index 11262566..564946b8 100644 --- a/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift +++ b/ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift @@ -15,12 +15,33 @@ final class RemoteDesktopTests: XCTestCase { } func testGatewayConnectionKindUsesUserFacingTitles() { + XCTAssertEqual(GatewayConnectionKind.publicBetaCases, [.cloudflare]) XCTAssertEqual(GatewayConnectionKind.local.title, "Same Network") XCTAssertEqual(GatewayConnectionKind.cloudflare.title, "Cloudflare") - XCTAssertTrue(GatewayConnectionKind.local.helpText.contains("LAN address")) + XCTAssertTrue(GatewayConnectionKind.local.isPublicBetaAvailable == false) + XCTAssertTrue(GatewayConnectionKind.cloudflare.isPublicBetaAvailable) + XCTAssertTrue(GatewayConnectionKind.local.helpText.contains("disabled for public beta")) XCTAssertTrue(GatewayConnectionKind.cloudflare.helpText.contains("Cloudflare Tunnel")) } + func testRemotePairingApprovalStatusRoundTrips() throws { + let status = RemotePairingApprovalStatus( + status: "pending_mac_approval", + challengeID: "challenge-1", + deviceID: "device-1", + macName: "Office Mac" + ) + + try assertRoundTrip(status) + } + + func testRemoteDesktopStartRequiresPairedStatus() { + XCTAssertTrue(canStartRemoteDesktop(statusText: "Paired with Office Mac")) + XCTAssertFalse(canStartRemoteDesktop(statusText: "Not paired")) + XCTAssertFalse(canStartRemoteDesktop(statusText: "Waiting for approval on Office Mac")) + XCTAssertFalse(canStartRemoteDesktop(statusText: "Host reachable, relay available")) + } + func testGatewayRootURLRequiresHTTPOrHTTPSWithHost() throws { let url = try gatewayRootURL(from: " https://codepilot.example.com ") diff --git a/ops/agents/escalations/issue-triage.md b/ops/agents/escalations/issue-triage.md index 535fc4f1..b89f8285 100644 --- a/ops/agents/escalations/issue-triage.md +++ b/ops/agents/escalations/issue-triage.md @@ -1,7 +1,6 @@ # Issue Triage Escalations -Date: 2026-07-03 +Date: 2026-07-04 -- #2 Same Network setup path: needs a product/security decision to either support LAN binding explicitly or disable the iOS Same Network path until it is safe and documented. -- #3 Remote Desktop pairing approval: needs maintainer decision on the trust model before public beta. Either require explicit Mac approval in the pairing flow or remove unused pairing-code UI and document bearer-token possession as the trust boundary. -- #8 Public screenshot set: maintainer approval is required before screenshots are committed, used in README/GitHub Pages, or reused in TestFlight/App Store metadata. +- No active maintainer decision escalations for issue triage. +- OTA build and public OTA asset verification are still required for the iOS changes, but were not run in this unattended pass because non-GitHub external system mutation is prohibited by the current public write policy. diff --git a/ops/agents/triage/2026-07-04-issue-triage.md b/ops/agents/triage/2026-07-04-issue-triage.md new file mode 100644 index 00000000..7697ee32 --- /dev/null +++ b/ops/agents/triage/2026-07-04-issue-triage.md @@ -0,0 +1,37 @@ +# CodePilot Issue Triage - 2026-07-04 + +Remote write policy: public GitHub writes are allowed after privacy audit when they directly advance launch readiness. Non-GitHub external system mutation is prohibited in this unattended run. + +## Reviewed Issues + +- #2 Support or disable the iOS Same Network setup path + - Current labels: `setup`, `ios`, `gateway`, `severity: medium`. + - Maintainer decision found: hide/disable Same Network for public beta; Cloudflare remains the supported default remote connection path. + - Action: implemented on branch `agent/issue-2-3-public-beta-setup`. +- #3 Require clear Remote Desktop pairing approval in setup + - Current labels: `setup`, `remote-desktop`, `mac`, `ios`, `severity: high`. + - Maintainer decision found: require explicit approval on the Mac; iOS must remain pending until approval or rejection. + - Action: implemented on branch `agent/issue-2-3-public-beta-setup`. +- #8 Prepare sanitized public screenshot set + - Current labels: `documentation`, `severity: low`. + - Maintainer approval found for sanitized demo-only screenshots. + - Action: not captured in this pass; no screenshot assets were created or published. + +## Local Changes + +- Disabled Same Network from the iOS public beta connection picker and updated setup docs to point users to Cloudflare. +- Changed Remote Desktop pairing so `pairing.complete` verifies the phone proof and returns `pending_mac_approval`; Mac approval is now required before the device is trusted. +- Removed the unused iOS pairing-code field and kept Start Session disabled until refresh sees a paired Mac state. +- Added focused Mac and iOS tests for public-beta connection modes and pending Mac approval. + +## Verification + +- `swift test`: passed. +- Focused iOS simulator tests for RemoteDesktopTests: passed. +- iOS simulator build with code signing disabled: passed. +- OTA build: not run because it would mutate non-GitHub OTA distribution systems, which this unattended run is not allowed to do. + +## Blockers + +- OTA build and public OTA asset verification require an allowed release/OTA run outside this issue-triage policy. +- #8 screenshot capture remains launch work, but no maintainer decision is blocking it after the 2026-07-03 approval comment.