Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Tests/CodexAccountSwitcherTests/CloudflareSetupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class CloudflareSetupTests: XCTestCase {
hostname: "codepilot.example.com",
tunnelName: "codepilot",
tunnelId: "tun_123",
configPath: "/Users/test/.cloudflared/codepilot-config.yaml",
configPath: "/tmp/codepilot-test/.cloudflared/codepilot-config.yaml",
launchAgentLabel: "io.codepilot.phone-cloudflared",
lastVerifiedAt: nil
)
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 @@ -632,7 +632,7 @@ Verify pairing approval, Face ID session start, Wi-Fi direct path, cellular TURN
Run:

```bash
cd /Users/example/CodePilot/ios
cd ios
python3 scripts/dke_ota_build.py build --app codexphone --branch release
```

Expand Down
3 changes: 1 addition & 2 deletions docs/superpowers/plans/2026-06-30-cloudflare-setup-wizard.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ final class CloudflareSetupTests: XCTestCase {
hostname: "codepilot.example.com",
tunnelName: "codepilot",
tunnelId: "tun_123",
configPath: "/Users/test/.cloudflared/codepilot-config.yaml",
configPath: "/tmp/codepilot-test/.cloudflared/codepilot-config.yaml",
launchAgentLabel: "io.codepilot.phone-cloudflared",
lastVerifiedAt: nil
)
Expand Down Expand Up @@ -904,4 +904,3 @@ If verification caused small fixes:
git add Sources/CodexAccountSwitcher/main.swift Tests/CodexAccountSwitcherTests scripts docs
git commit -m "test: verify Cloudflare setup wizard"
```

9 changes: 6 additions & 3 deletions gateway/codex_phone_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,15 @@ def __init__(
id_factory=None,
notification_handler=None,
env: dict | None = None,
allow_dangerous: bool = False,
):
self.codex_path = codex_path
self.cwd = cwd
self.process_factory = process_factory
self.id_factory = id_factory or (lambda: str(uuid.uuid4()))
self.notification_handler = notification_handler
self.env = env
self.allow_dangerous = allow_dangerous
self.process = None
self.lock = threading.Lock()
self.write_lock = threading.Lock()
Expand Down Expand Up @@ -202,8 +204,8 @@ def close(self):
def thread_start(self, cwd: str | None = None, reasoning_effort: str | None = None) -> dict:
params = {
"cwd": cwd,
"approvalPolicy": "never",
"sandbox": "danger-full-access",
"approvalPolicy": "never" if self.allow_dangerous else "on-request",
"sandbox": "danger-full-access" if self.allow_dangerous else "workspace-write",
"threadSource": "user",
}
if reasoning_effort:
Expand Down Expand Up @@ -1967,6 +1969,7 @@ def app_server_client(self) -> CodexAppServerClient:
id_factory=self.app_server_id_factory,
notification_handler=self.handle_app_server_notification,
env=env,
allow_dangerous=self.allow_dangerous,
)
self._app_server_auth_fingerprint = auth_fingerprint
return self._app_server_client
Expand Down Expand Up @@ -4391,7 +4394,7 @@ def main():
parser.add_argument("--codex-home", default=str(DEFAULT_CODEX_HOME))
parser.add_argument("--codex-path", default=str(DEFAULT_CODEX))
parser.add_argument("--token-file", default=str(DEFAULT_TOKEN_FILE))
parser.add_argument("--allow-dangerous", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--allow-dangerous", action=argparse.BooleanOptionalAction, default=False)
args = parser.parse_args()

token = read_or_create_token(Path(args.token_file))
Expand Down
41 changes: 41 additions & 0 deletions gateway/test_codex_phone_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,47 @@ def test_thread_start_sends_reasoning_effort_config(self):
self.assertEqual(start["method"], "thread/start")
self.assertEqual(start["params"]["config"], {"model_reasoning_effort": "high"})

def test_thread_start_uses_safe_policy_by_default(self):
process = FakeAppServerProcess([
json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n",
json.dumps({"id": "start-id", "result": {"thread": {"id": "thread-1"}}}) + "\n",
])
client = CodexAppServerClient(
codex_path=Path("/usr/local/bin/codex"),
cwd=Path("/tmp/workspace"),
process_factory=lambda args, **kwargs: process,
id_factory=iter(["init-id", "start-id"]).__next__,
)

client.start()
client.thread_start("/tmp/workspace")
sent = [json.loads(line) for line in process.stdin.getvalue().splitlines()]
start = sent[2]

self.assertEqual(start["params"]["approvalPolicy"], "on-request")
self.assertEqual(start["params"]["sandbox"], "workspace-write")

def test_thread_start_allows_dangerous_mode_when_explicitly_enabled(self):
process = FakeAppServerProcess([
json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n",
json.dumps({"id": "start-id", "result": {"thread": {"id": "thread-1"}}}) + "\n",
])
client = CodexAppServerClient(
codex_path=Path("/usr/local/bin/codex"),
cwd=Path("/tmp/workspace"),
process_factory=lambda args, **kwargs: process,
id_factory=iter(["init-id", "start-id"]).__next__,
allow_dangerous=True,
)

client.start()
client.thread_start("/tmp/workspace")
sent = [json.loads(line) for line in process.stdin.getvalue().splitlines()]
start = sent[2]

self.assertEqual(start["params"]["approvalPolicy"], "never")
self.assertEqual(start["params"]["sandbox"], "danger-full-access")

def test_turn_start_sends_reasoning_effort(self):
process = FakeAppServerProcess([
json.dumps({"id": "init-id", "result": {"userAgent": "ua", "codexHome": "/tmp/codex", "platformFamily": "unix", "platformOs": "macos"}}) + "\n",
Expand Down
14 changes: 7 additions & 7 deletions ios/CodexPhone/CodexPhoneTests/RemoteDesktopTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,28 +69,28 @@ final class RemoteDesktopTests: XCTestCase {
}

func testRemoteFilePathAcceptsCustomPreviewURL() throws {
let url = try XCTUnwrap(remoteFilePreviewURL(path: "/Users/example/CodePilot/README.md"))
let url = try XCTUnwrap(remoteFilePreviewURL(path: "/tmp/codepilot-sample/README.md"))

XCTAssertEqual(remoteFilePath(from: url), "/Users/example/CodePilot/README.md")
XCTAssertEqual(remoteFilePath(from: url), "/tmp/codepilot-sample/README.md")
}

func testRemoteFilePathAcceptsMarkdownAbsolutePathURL() throws {
let url = try XCTUnwrap(URL(string: "/Users/example/CodePilot/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md"))
let url = try XCTUnwrap(URL(string: "/tmp/codepilot-sample/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md"))

XCTAssertEqual(
remoteFilePath(from: url),
"/Users/example/CodePilot/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md"
"/tmp/codepilot-sample/docs/superpowers/plans/2026-07-01-codepilot-launch-agent-system.md"
)
}

func testRemoteFilePathAcceptsFileURLAndStripsLineSuffix() throws {
let url = URL(fileURLWithPath: "/Users/example/CodePilot/Sources/App.swift:42")
let url = URL(fileURLWithPath: "/tmp/codepilot-sample/Sources/App.swift:42")

XCTAssertEqual(remoteFilePath(from: url), "/Users/example/CodePilot/Sources/App.swift")
XCTAssertEqual(remoteFilePath(from: url), "/tmp/codepilot-sample/Sources/App.swift")
}

func testRemoteFilePathRejectsWebURLs() throws {
let url = try XCTUnwrap(URL(string: "https://example.com/Users/example/README.md"))
let url = try XCTUnwrap(URL(string: "https://example.com/tmp/codepilot-sample/README.md"))

XCTAssertNil(remoteFilePath(from: url))
}
Expand Down
21 changes: 21 additions & 0 deletions ops/agents/escalations/security-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# CodePilot Security Scan Escalation

Maintainer intervention is needed before public launch for these product-boundary decisions:

1. Authenticated arbitrary file download remains enabled.
- Surface: `gateway/codex_phone_gateway.py` exposes `/api/files/download?path=...`.
- Current behavior: any client with the gateway bearer token can request any absolute file path readable by the gateway process.
- Risk: a leaked or phished gateway token becomes direct local file disclosure, independent of Codex approval/sandbox controls.
- Decision needed: define the allowed file scope for iPhone previews, such as uploaded files, explicit per-thread workspace roots, or short-lived file-preview grants.

2. Localhost proxy sessions are bearerless capability URLs.
- Surface: `POST /api/local-web/sessions` requires the bearer token, but the returned `/api/local-web/<session>/...` URL is fetched without bearer auth.
- Current behavior: the random session id gates access for the session lifetime.
- Risk: anyone who obtains a live session URL can proxy GET requests to the selected loopback port through the public gateway until expiry.
- Decision needed: decide whether this is acceptable for WebView compatibility, should have a shorter lifetime, or should move to an authenticated browser/proxy design.

Local hardening already applied in this branch:

- Removed public embedded private identifier patterns from the privacy audit and made local denylist patterns opt-in via `CODEPILOT_PRIVACY_PATTERNS_FILE`.
- Removed machine-specific default repository paths from local agent scripts.
- Changed gateway-created Codex app-server threads to use safe approval/sandbox settings unless dangerous mode is explicitly enabled.
4 changes: 3 additions & 1 deletion scripts/codepilot-agent-escalate.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT="${CODEPILOT_REPO_ROOT:-$DEFAULT_ROOT}"
STATE_DIR="${CODEPILOT_AGENT_STATE_DIR:-$HOME/.codex-account-switcher/agents}"
THREAD_ID_FILE="$STATE_DIR/thread-id"
CODEX_BIN="${CODEPILOT_CODEX_BIN:-codex}"
Expand Down
4 changes: 3 additions & 1 deletion scripts/codepilot-agent-runner.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT="${CODEPILOT_REPO_ROOT:-$DEFAULT_ROOT}"
JOB="${1:-}"

if [[ -z "$JOB" ]]; then
Expand Down
3 changes: 2 additions & 1 deletion scripts/install-codepilot-local-agents.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env zsh
set -euo pipefail

ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="${CODEPILOT_REPO_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
PLIST_DIR="$HOME/Library/LaunchAgents"
RUNNER="$ROOT/scripts/codepilot-agent-runner.sh"

Expand Down
53 changes: 21 additions & 32 deletions scripts/privacy-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,21 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 2
fi

pattern="$(python3 - <<'PY'
entries = [
"546f6e79",
"746f6e79",
"537072656e6b656c696e67",
"737072656e6b656c696e67",
"686f6d65736572766572",
"686f6d655c2e686f6d65",
"4d6163206d696e69",
"6d6163206d696e69",
"444b45",
"64652d6b6c65696e65",
"65656b686f6f726e",
"636f6d5c2e746f6e79",
"636f5c2e737072656e6b656c696e67",
"394b3734443657324252",
"514836354232",
"515a334d364137324747",
"70726f7065726c69",
"676d61696c",
"69636c6f7564",
"40737072656e6b656c696e67",
]
print("|".join(bytes.fromhex(entry).decode("utf-8") for entry in entries))
PY
)"
private_patterns_file="${CODEPILOT_PRIVACY_PATTERNS_FILE:-$HOME/.codepilot-privacy-patterns}"
users_dir_pattern="/$(printf %s Users)/[^[:space:]\"']+"
generic_private_patterns=(
"$users_dir_pattern"
'[A-Za-z][A-Za-z0-9._%+-]*@[A-Za-z][A-Za-z0-9.-]*\.[A-Za-z]{2,}'
)

pattern="$(IFS='|'; echo "${generic_private_patterns[*]}")"

if [[ -f "$private_patterns_file" ]]; then
while IFS= read -r private_pattern; do
[[ -n "$private_pattern" && "${private_pattern:0:1}" != "#" ]] || continue
pattern="$pattern|$private_pattern"
done < "$private_patterns_file"
fi

tracked_files=("${(@f)$(git ls-files)}")
audit_files=()
Expand All @@ -49,13 +38,13 @@ if git grep -n -I -E "$pattern" -- "${audit_files[@]}"; then
fi

secret_patterns=(
'ghp_[A-Za-z0-9_]+'
'github_pat_[A-Za-z0-9_]+'
'sk-[A-Za-z0-9]{20,}'
'-----BEGIN (RSA|OPENSSH|PRIVATE) KEY'
"g""hp_[A-Za-z0-9_]+"
"github_""pat_[A-Za-z0-9_]+"
"s""k-[A-Za-z0-9]{20,}"
"-----BEGIN (RSA|OPENSSH|PRIVATE) ""KEY"
'Bearer [A-Za-z0-9._-]{20,}'
'client_secret'
'private_key'
"client_""secret"
"private_""key"
)

secret_pattern="$(IFS='|'; echo "${secret_patterns[*]}")"
Expand Down
Loading