Skip to content
Closed
83 changes: 79 additions & 4 deletions Sources/CodexAccountSwitcher/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ final class CodexAccountSwitcher {
try fileManager.removeItem(at: settings.loginBackupAuth)
}
try fileManager.copyItem(at: settings.activeAuth, to: settings.loginBackupAuth)
try enforceOwnerOnlyFilePermissions(settings.loginBackupAuth)
try fileManager.removeItem(at: settings.activeAuth)
}
loginCapturePending = true
Expand Down Expand Up @@ -1178,6 +1179,8 @@ final class CodexAccountSwitcher {
private func createFolders() {
try? fileManager.createDirectory(at: settings.accountsDir, withIntermediateDirectories: true)
try? fileManager.createDirectory(at: settings.backupsDir, withIntermediateDirectories: true)
try? fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: settings.accountsDir.path)
try? fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: settings.backupsDir.path)
}

private func recoverPendingLoginCapture() {
Expand Down Expand Up @@ -1537,6 +1540,11 @@ final class CodexAccountSwitcher {
let active = activeAccountName().replacingOccurrences(of: "/", with: "_")
let backup = settings.backupsDir.appendingPathComponent("\(stamp)-\(active)-auth.json")
try fileManager.copyItem(at: settings.activeAuth, to: backup)
try enforceOwnerOnlyFilePermissions(backup)
}

private func enforceOwnerOnlyFilePermissions(_ url: URL) throws {
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}

private func appendAudit(_ line: String) {
Expand Down Expand Up @@ -3280,6 +3288,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private lazy var macPeerConnection = MacPeerConnection { [weak self] event in
guard let self else { return nil }
let displayFrame = CGDisplayBounds(CGMainDisplayID())
try self.requireActiveRemoteDesktopLease(event.sessionId)
try self.remoteInputInjector.handle(event, displayFrame: displayFrame)
let cursor = CGEvent(source: nil)?.location ?? .zero
return CGPoint(
Expand All @@ -3289,6 +3298,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
}
private var remoteDesktopCoordinator: RemoteDesktopCoordinator?
private var remoteDesktopSocketServer: RemoteDesktopSocketServer?
private var remoteSessionLeaseStore: SessionLeaseStore?
private var statusItem: NSStatusItem!
private let menu = NSMenu()
private var setupWindowController: CodePilotSetupWindowController?
Expand All @@ -3302,10 +3312,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
statusItem.button?.title = "CodePilot"
menu.delegate = self
statusItem.menu = menu
if RemoteDesktopHostPolicy.isEnabled {
remoteDesktopCoordinator = try? RemoteDesktopCoordinator()
}
if RemoteDesktopHostPolicy.isEnabled, let remoteDesktopCoordinator {
remoteDesktopCoordinator = try? RemoteDesktopCoordinator()
if let remoteDesktopCoordinator {
remoteSessionLeaseStore = SessionLeaseStore { deviceID in
guard let device = remoteDesktopCoordinator.pairingStore.trustedDevice(id: deviceID) else {
throw RemoteDesktopSecurityError.untrustedDevice
}
guard device.revokedAt == nil else {
throw RemoteDesktopSecurityError.deviceRevoked
}
}
do {
let server = RemoteDesktopSocketServer { [weak self, weak remoteDesktopCoordinator] request in
guard let self, let remoteDesktopCoordinator else {
Expand Down Expand Up @@ -3553,12 +3569,42 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
])

case "frame.capture":
let payload = try Self.remoteDesktopRPCPayload(request.payload)
let sessionID = try Self.remoteDesktopString(payload["sessionId"])
try requireActiveRemoteDesktopLease(sessionID)
let frame = try remoteFrameCaptureService.captureMainDisplayJPEG()
return HostRPCResponse(id: request.id, status: 200, payload: frame, errorCode: nil)

case "session.nonce":
let payload = try Self.remoteDesktopRPCPayload(request.payload)
let deviceID = try Self.remoteDesktopString(payload["deviceId"])
guard let device = coordinator.pairingStore.trustedDevice(id: deviceID) else {
throw RemoteDesktopSecurityError.untrustedDevice
}
guard device.revokedAt == nil else {
throw RemoteDesktopSecurityError.deviceRevoked
}
let nonce = try remoteDesktopLeaseStore().issueNonce(for: deviceID)
return try Self.remoteDesktopRPCJSON(request.id, ["nonce": nonce])

case "session.start":
let payload = try Self.remoteDesktopRPCPayload(request.payload)
let deviceID = try Self.remoteDesktopString(payload["deviceId"])
let nonce = try Self.remoteDesktopString(payload["nonce"])
let signature = try Self.remoteDesktopBase64(payload["signature"])
let proof = try coordinator.pairingStore.verifyNonce(
deviceID: deviceID,
nonce: nonce,
signature: signature
)
let lease = try remoteDesktopLeaseStore().createLease(using: proof)
coordinator.registerActiveSession(deviceID: lease.deviceId, leaseID: lease.id)
return try Self.remoteDesktopRPCCodable(request.id, lease)

case "session.signal":
let payload = try Self.remoteDesktopRPCPayload(request.payload)
let sessionID = try Self.remoteDesktopString(payload["sessionId"])
try requireActiveRemoteDesktopLease(sessionID)
let sequence = try Self.remoteDesktopUInt64(payload["sequence"])
let kindRaw = try Self.remoteDesktopString(payload["kind"])
guard let kind = MacPeerSignal.Kind(rawValue: kindRaw) else {
Expand Down Expand Up @@ -3593,6 +3639,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
)

case "session.end":
let payload = try Self.remoteDesktopRPCPayload(request.payload)
let sessionID = try Self.remoteDesktopString(payload["sessionId"])
try requireActiveRemoteDesktopLease(sessionID)
remoteSessionLeaseStore?.endLease(leaseID: sessionID)
coordinator.emergencyDisconnect()
macPeerConnection.disconnect()
return try Self.remoteDesktopRPCJSON(request.id, ["ok": true])

Expand All @@ -3603,6 +3654,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
}
let decoder = JSONDecoder()
let event = try decoder.decode(RemoteInputEvent.self, from: request.payload)
try requireActiveRemoteDesktopLease(event.sessionId)
try remoteDesktopLeaseStore().validateSequence(event.sequence, for: event.sessionId)
let displayFrame = CGDisplayBounds(CGMainDisplayID())
try remoteInputInjector.handle(event, displayFrame: displayFrame)
let cursor = CGEvent(source: nil)?.location ?? .zero
Expand All @@ -3626,6 +3679,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
}
}

private func remoteDesktopLeaseStore() throws -> SessionLeaseStore {
guard let remoteSessionLeaseStore else {
throw RemoteDesktopSecurityError.leaseUnknown
}
return remoteSessionLeaseStore
}

private func requireActiveRemoteDesktopLease(_ leaseID: String) throws {
guard let activeLease = try remoteDesktopLeaseStore().activeLease(),
activeLease.id == leaseID else {
throw RemoteDesktopSecurityError.leaseUnknown
}
}

private static func remoteDesktopRPCPayload(_ data: Data) throws -> [String: Any] {
if data.isEmpty {
return [:]
Expand Down Expand Up @@ -3695,6 +3762,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
return "invalid_signature"
case .deviceRevoked, .untrustedDevice:
return "untrusted_device"
case .leaseExpired:
return "session_expired"
case .leaseUnknown:
return "unauthorized"
case .sequenceReplay:
return "sequence_replay"
case .controllerBusy:
return "controller_busy"
default:
return "invalid_request"
}
Expand Down
12 changes: 3 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@ The beta is focused on practical workflows for AI coding users:

- See active Codex usage, reset timing, available reset credits, and account status from the Mac and iPhone.
- Switch saved Codex account profiles after active turns finish.
- Connect the iPhone app to the Mac gateway through a user-owned Cloudflare Tunnel during the public beta.
- Upload files, inspect threads, steer or stop turns where supported, and receive turn-finished notifications when APNs is configured for the gateway.
- Open loopback web links from Codex output in the iPhone app through the authenticated Mac gateway.

## Beta Access

The Mac app currently builds from source on macOS 13 or later. The iPhone companion requires iOS 17 or later and access to an approved beta build; CodePilot does not yet advertise a public TestFlight invitation or App Store download.

Start with the [Mac installation guide](INSTALL_MAC.md). If you already have an approved iPhone beta build, continue with the [iOS installation guide](INSTALL_IOS.md).
- Connect the iPhone app through a user-owned Cloudflare Tunnel during the public beta.
- Upload files, inspect threads, steer or stop turns where supported, and receive turn-finished notifications.
- Use remote desktop features only after pairing and macOS permission checks.

CodePilot is source-available for noncommercial use. It is not OSI open source. See the repository license files before redistributing or using it commercially.

Expand Down
Loading
Loading