Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
372d2a1
security: constrain gateway capability surfaces
codepilotios Jul 17, 2026
4f8e051
security: fail closed on unsafe gateway paths
codepilotios Jul 17, 2026
a5f2b7f
security: store gateway token in device keychain
codepilotios Jul 17, 2026
05f2b12
security: prevent gateway output and log leaks
codepilotios Jul 17, 2026
1f76a23
security: hide APNs credentials from process arguments
codepilotios Jul 17, 2026
3aab19d
security: keep gateway token out of process arguments
codepilotios Jul 17, 2026
61cb083
docs: refresh security history scan totals
codepilotios Jul 17, 2026
0fc22c2
security: harden gateway and public data handling
codepilotios Jul 17, 2026
33ae979
security: validate iOS gateway origins
codepilotios Jul 17, 2026
ae13fe3
security: protect cached thread messages
codepilotios Jul 17, 2026
a354a6f
docs: refresh security history audit counts
codepilotios Jul 17, 2026
f67676a
security: constrain Cloudflare tunnel setup
codepilotios Jul 17, 2026
8d3dc9c
security: require explicit background agent opt-in
codepilotios Jul 17, 2026
a2ccc04
security: protect launch service logs
codepilotios Jul 17, 2026
4dc2116
docs: refresh security history scan counts
codepilotios Jul 17, 2026
3906c62
security: constrain continuous agent public writes
codepilotios Jul 17, 2026
c60a963
docs: refresh security history coverage
codepilotios Jul 17, 2026
e026d8c
security: disable unsafe native remote host
codepilotios Jul 17, 2026
f96d0da
security: validate iOS loopback endpoints
codepilotios Jul 17, 2026
b1df9d1
docs: refresh security scan coverage
codepilotios Jul 17, 2026
e6255d8
security: redact push notification details
codepilotios Jul 17, 2026
7bf916a
security: expire private uploads safely
codepilotios Jul 17, 2026
1188c23
security: bound remote credential flows
codepilotios Jul 17, 2026
4c9cd1e
security: broaden public repo credential guards
codepilotios Jul 17, 2026
71f0754
security: expand public credential leak guards
codepilotios Jul 17, 2026
6bf7976
security: isolate OAuth callback listener
codepilotios Jul 18, 2026
1626254
security: require unique OAuth callback state
codepilotios Jul 18, 2026
65724fe
security: isolate gateway credentials and write guards
codepilotios Jul 18, 2026
86b71dc
security: require explicit agent branch pushes
codepilotios Jul 18, 2026
2f6ade9
security: audit outbound agent writes
codepilotios Jul 18, 2026
db86fc4
security: bound gateway request concurrency
codepilotios Jul 18, 2026
c386305
security: pin guarded pushes to public repository
codepilotios Jul 18, 2026
f468060
security: isolate gateway and public write credentials
codepilotios Jul 18, 2026
5c8253a
ci: update checkout action runtime
codepilotios Jul 18, 2026
c780eb8
security: roll back rejected upload batches
codepilotios Jul 18, 2026
3ebf949
security: harden desktop sync archive imports
codepilotios Jul 18, 2026
0277c71
security: bound desktop sync archive extraction
codepilotios Jul 18, 2026
9ba2662
security: reject weak gateway tokens
codepilotios Jul 18, 2026
7a75b28
security: update release tooling dependencies
codepilotios Jul 18, 2026
de1a68b
security: validate desktop sync destinations
codepilotios Jul 18, 2026
2f352b2
fix: restore current privacy audit script
codepilotios Jul 19, 2026
31de5d6
test: keep privacy audit fixtures scan-safe
codepilotios Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ on:
branches:
- main

permissions:
contents: read

jobs:
test:
name: Test and Audit
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Privacy audit
run: scripts/privacy-audit.sh

- name: Public presence audit
run: scripts/public-presence-audit.sh
- name: Privacy audit regression tests
run: scripts/tests/privacy-audit-test.sh

- name: SwiftPM tests
run: swift test
Expand All @@ -27,6 +30,27 @@ jobs:
working-directory: gateway
run: python3 -m unittest test_codex_phone_gateway.py test_remote_desktop_gateway.py

- name: Gateway token process-argument guard
run: scripts/tests/gateway-token-argv-test.sh

- name: LaunchAgent owner-only umask guard
run: scripts/tests/launch-agent-umask-test.sh

- name: Cloudflare setup security tests
run: scripts/test-cloudflare-setup.sh

- name: Background-agent opt-in guard
run: scripts/tests/background-agent-opt-in-test.sh

- name: Continuous-agent public-write guard
run: scripts/tests/codepilot-agent-public-write-guard-test.sh

- name: Desktop sync security tests
run: python3 -m unittest scripts/tests/test_desktop_sync_security.py

- name: Desktop sync transport security tests
run: scripts/tests/push-desktop-sync-security-test.sh

- name: Fastlane syntax
run: ruby -c ios/CodexPhone/fastlane/Fastfile

Expand Down
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,34 @@ AGENTS.md
docs/superpowers/
.env
*.env
.env.*
!.env.example
.envrc
.dev.vars
*.credentials
*.secrets
auth.json
credentials.json
service-account*.json
*.p8
*.key
*.jks
*.keystore
*.sqlite
*.sqlite3
*.db
.netrc
.npmrc
.pypirc
phone-gateway-token
phone-gateway.env
AuthKey_*.p8
*.pem
*.p12
*.mobileprovision
*.provisionprofile
.cloudflared/
.wrangler/
*.log
logs/
uploads/
Expand Down
38 changes: 25 additions & 13 deletions Sources/CodexAccountSwitcher/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ enum CodePilotHostServicesManager {
}

private static func unloadAndRemoveLegacyLaunchAgents() {
[
"com.tony.codex-phone-gateway",
"com.tony.codex-phone-cloudflared"
].forEach { label in
let legacyLabels = ProcessInfo.processInfo.environment["CODEPILOT_LEGACY_LAUNCHD_LABELS"]?
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty } ?? []
legacyLabels.forEach { label in
let plist = launchAgents.appendingPathComponent("\(label).plist")
runLaunchctl(["unload", plist.path])
try? fileManager.removeItem(at: plist)
Expand Down Expand Up @@ -173,8 +174,12 @@ enum CodePilotHostServicesManager {
let scheduler = bundledResource("scripts/codepilot-agent-scheduler.sh")
?? repoRoot.appendingPathComponent("scripts/codepilot-agent-scheduler.sh")
guard fileManager.fileExists(atPath: scheduler.path) else { return }
let threadID = (try? String(contentsOf: appDir.appendingPathComponent("agents/thread-id"), encoding: .utf8))
?? "019e2d20-3695-7423-be21-968f544d2b20"
let threadIDFile = appDir.appendingPathComponent("agents/thread-id")
guard let threadID = try? String(contentsOf: threadIDFile, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines),
!threadID.isEmpty else {
return
}
let codex = preferredCodexURL()?.path ?? executable(named: "codex")?.path ?? "codex"
try writeLaunchAgent(
label: "io.codepilot.agents.scheduler",
Expand All @@ -190,7 +195,7 @@ enum CodePilotHostServicesManager {
"CODEPILOT_AGENT_MODEL": "gpt-5.6-sol",
"CODEPILOT_AGENT_REASONING_EFFORT": "medium",
"CODEPILOT_CODEX_BIN": codex,
"CODEPILOT_AGENT_THREAD_ID": threadID.trimmingCharacters(in: .whitespacesAndNewlines)
"CODEPILOT_AGENT_THREAD_ID": threadID
],
keepAlive: false,
startInterval: 60
Expand All @@ -214,6 +219,7 @@ enum CodePilotHostServicesManager {
"Label": label,
"ProgramArguments": programArguments,
"RunAtLoad": true,
"Umask": 0o077,
"WorkingDirectory": workingDirectory,
"StandardOutPath": stdout,
"StandardErrorPath": stderr,
Expand All @@ -227,6 +233,7 @@ enum CodePilotHostServicesManager {
}
let data = try PropertyListSerialization.data(fromPropertyList: payload, format: .xml, options: 0)
try data.write(to: plist, options: .atomic)
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: plist.path)
runLaunchctl(["unload", plist.path])
runLaunchctl(["load", plist.path])
}
Expand Down Expand Up @@ -3259,6 +3266,13 @@ private final class GatewayRemoteInputValidator: RemoteInputLeaseValidating {
}
}

enum RemoteDesktopHostPolicy {
// The native RPC host still lacks lease authorization for screen capture,
// input injection, and WebRTC signaling. Keep it unreachable until those
// checks are enforced end to end.
static let isEnabled = false
}

final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private let switcher = CodexAccountSwitcher()
private let remoteFrameCaptureService = RemoteFrameCaptureService()
Expand All @@ -3284,16 +3298,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
NSApp.setActivationPolicy(.accessory)
CodePilotHostServicesManager.ensureConfiguredOnLaunch()

if !CGPreflightScreenCaptureAccess() {
_ = CGRequestScreenCaptureAccess()
}

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.title = "CodePilot"
menu.delegate = self
statusItem.menu = menu
remoteDesktopCoordinator = try? RemoteDesktopCoordinator()
if let remoteDesktopCoordinator {
if RemoteDesktopHostPolicy.isEnabled {
remoteDesktopCoordinator = try? RemoteDesktopCoordinator()
}
if RemoteDesktopHostPolicy.isEnabled, let remoteDesktopCoordinator {
do {
let server = RemoteDesktopSocketServer { [weak self, weak remoteDesktopCoordinator] request in
guard let self, let remoteDesktopCoordinator else {
Expand Down
14 changes: 7 additions & 7 deletions Tests/CodexAccountSwitcherTests/PairingStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class PairingStoreTests: XCTestCase {

let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand All @@ -35,7 +35,7 @@ final class PairingStoreTests: XCTestCase {
let expiredStore = try makeStore(clock: { expiredNow })
let expiredChallenge = expiredStore.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand All @@ -56,7 +56,7 @@ final class PairingStoreTests: XCTestCase {
let privateKey = P256.Signing.PrivateKey()
let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand Down Expand Up @@ -93,7 +93,7 @@ final class PairingStoreTests: XCTestCase {
let wrongPrivateKey = P256.Signing.PrivateKey()
let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand All @@ -120,7 +120,7 @@ final class PairingStoreTests: XCTestCase {
let privateKey = P256.Signing.PrivateKey()
let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand Down Expand Up @@ -148,7 +148,7 @@ final class PairingStoreTests: XCTestCase {
let privateKey = P256.Signing.PrivateKey()
let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand All @@ -171,7 +171,7 @@ final class PairingStoreTests: XCTestCase {
let privateKey = P256.Signing.PrivateKey()
let challenge = store.issueChallenge(
deviceID: "device-1",
name: "Taylor's iPhone",
name: "Test iPhone",
publicKeyRawRepresentation: privateKey.publicKey.rawRepresentation,
macName: "Studio Mac"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import XCTest
@testable import CodexAccountSwitcher

final class RemoteDesktopCoordinatorTests: XCTestCase {
func testUnsafeNativeRemoteDesktopHostIsDisabled() {
XCTAssertFalse(RemoteDesktopHostPolicy.isEnabled)
}

func testSnapshotReflectsPermissionState() throws {
let permissions = FakeRemoteDesktopPermissions(
screenRecordingGranted: false,
Expand Down
2 changes: 1 addition & 1 deletion docs/PRODUCTION_READINESS_IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ curl -fsS -X POST http://127.0.0.1:8787/codexphone/api/build
curl -fsS http://127.0.0.1:8787/codexphone/api/status
```

Wait until `current.state == "complete"`, then verify the manifest and IPA from the approved OTA host, for example `https://ota.example.com`, return HTTP 200.
Wait until `current.state == "complete"`, then verify the manifest and IPA from `https://ota.example.com` return HTTP 200.

- [ ] **Step 8: Commit**

Expand Down
20 changes: 20 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,31 @@ Recommended:

- Use HTTPS when remote.
- Use a long random bearer token.
- The gateway refuses existing tokens shorter than 32 characters or containing non-URL-safe characters.
- Rotate the token if a device is lost.
- Restrict Cloudflare access where practical.

The iOS client refuses non-HTTPS gateway URLs except for `localhost`, `::1`, and syntactically valid `127.x.x.x` loopback addresses. DNS names that merely begin with `127.` are not loopback exceptions and still require HTTPS. The client also rejects gateway URLs containing user information, query strings, or fragments, and credential-bearing requests do not follow cross-origin redirects.

The gateway health response is intentionally public-safe, but it is still operational metadata. Treat the gateway URL and token as private.

Do not place infrastructure or connector management tokens in `phone-gateway.env`. The gateway service deliberately does not load `SUPABASE_ACCESS_TOKEN`, and gateway-launched Codex processes scrub gateway-only credentials from their environment. Configure sensitive connectors in a separately trusted local Codex session instead of exposing their bearer tokens to remotely initiated turns.

Cloudflare remote verification sends the gateway token only to the HTTPS origin stored by the permanent-tunnel setup. It rejects alternate hosts, URL credentials, nonstandard ports, and URL extras. Reconfigure the permanent tunnel instead of overriding verification with another URL.

Localhost web sessions use short-lived capability URLs so WebView subresources can load without exposing the gateway bearer token to page content. Do not share those URLs. Sessions are restricted to the selected loopback origin, expire after ten minutes, and have bounded request counts.

Remote file previews are restricted to files uploaded through CodePilot by default. Advanced local setups can add explicit roots with the `CODEPILOT_FILE_DOWNLOAD_ROOTS` environment variable, using the platform path separator between roots. Never add a credential, SSH, cloud configuration, or account-auth directory.

Uploaded attachment batches are retained for seven days. The gateway removes expired batches at startup and before saving another upload. Storage and cleanup stay within a private CodePilot upload directory and refuse symbolic links.

Desktop sync transfers accept only a plain host or `user@host` destination. SSH options, remote paths, and whitespace are rejected before CodePilot exports project metadata.

Remote account-login sessions expire after ten minutes, are limited to four at a time, and terminate their temporary login process when they expire or fail.
The temporary iOS OAuth callback listener binds only to IPv4 loopback and accepts a callback only when its path and single, non-empty OAuth state exactly match the active login request.

The native remote-desktop host is disabled while screen capture, input injection, and signaling still lack end-to-end lease authorization. CodePilot does not request Screen Recording permission at launch. Do not re-enable the host until the documented trusted-device and active-lease checks are enforced on every privileged operation.

## Account Switching

CodePilot swaps local provider auth files. It waits for active turns to finish before automatic switching, but users should still avoid manually editing auth files while turns are running.
Expand Down
2 changes: 1 addition & 1 deletion docs/superpowers/plans/2026-06-27-secure-remote-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ cd /path/to/ios-release-worktree
python3 scripts/dke_ota_build.py build --app codexphone --branch release
```

Expected: status reaches `complete`; output reports the existing configured bundle ID; both public manifest and IPA return HTTP 200; the OTA install updates the configured CodePilot app rather than creating another instance.
Expected: status reaches `complete`; output reports bundle ID `com.example.codepilot`; both public manifest and IPA return HTTP 200; the OTA install updates the configured CodePilot app rather than creating another instance.

- [ ] **Step 7: Commit**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Do not ping the maintainer for:

4. **Prepare community launch**
- Community Growth Agent drafts Reddit account profile and launch posts.
- Maintainer approves account/posting strategy.
- The maintainer approves account/posting strategy.
- Agent schedules staged, rule-compliant posts.

5. **Start beta loop**
Expand All @@ -229,7 +229,7 @@ Do not ping the maintainer for:
- Revenue Agent proposes tiers and pricing.
- Presence Agent updates website and App Store copy.
- Security Agent signs off on public release.
- Maintainer approves pricing, legal, and App Store submission.
- The maintainer approves pricing, legal, and App Store submission.

## Proposed Recurring Automations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ POST to `http://127.0.0.1:8787/codexphone/api/build`, poll `/codexphone/api/stat

- [ ] **Step 4: Verify public OTA artifacts**

Fetch the generated tokenized manifest and IPA URLs from the approved OTA host, for example `https://ota.example.com`.
Fetch the generated tokenized manifest and IPA URLs from `https://ota.example.com`.

Expected: HTTP 200 for both, manifest bundle identifier `io.codepilot.iOS`, and the widget extension embedded in the IPA.

Expand Down
Loading