diff --git a/.codex/skills/babysit-pr/scripts/gh_pr_watch.py b/.codex/skills/babysit-pr/scripts/gh_pr_watch.py index f6398373b7..1572133c92 100755 --- a/.codex/skills/babysit-pr/scripts/gh_pr_watch.py +++ b/.codex/skills/babysit-pr/scripts/gh_pr_watch.py @@ -60,7 +60,9 @@ def parse_args(): ) parser.add_argument("--pr", default="auto", help="auto, PR number, or PR URL") parser.add_argument("--repo", help="Optional OWNER/REPO override") - parser.add_argument("--poll-seconds", type=int, default=30, help="Watch poll interval") + parser.add_argument( + "--poll-seconds", type=int, default=30, help="Watch poll interval" + ) parser.add_argument( "--max-flaky-retries", type=int, @@ -68,8 +70,12 @@ def parse_args(): help="Max rerun cycles per head SHA before stop recommendation", ) parser.add_argument("--state-file", help="Path to state JSON file") - parser.add_argument("--once", action="store_true", help="Emit one snapshot and exit") - parser.add_argument("--watch", action="store_true", help="Continuously emit JSONL snapshots") + parser.add_argument( + "--once", action="store_true", help="Emit one snapshot and exit" + ) + parser.add_argument( + "--watch", action="store_true", help="Continuously emit JSONL snapshots" + ) parser.add_argument( "--retry-failed-now", action="store_true", @@ -128,7 +134,9 @@ def gh_json(args, repo=None): try: return json.loads(raw) except json.JSONDecodeError as err: - raise GhCommandError(f"Failed to parse JSON from gh output for {' '.join(args)}") from err + raise GhCommandError( + f"Failed to parse JSON from gh output for {' '.join(args)}" + ) from err def parse_pr_spec(pr_spec): @@ -210,6 +218,8 @@ def extract_repo_from_pr_view(data): if owner and name: return f"{owner}/{name}" return None + + def extract_repo_from_pr_url(pr_url): parsed = urlparse(pr_url) parts = [p for p in parsed.path.split("/") if p] @@ -242,7 +252,9 @@ def load_state(path): def save_state(path, state): path.parent.mkdir(parents=True, exist_ok=True) payload = json.dumps(state, indent=2, sort_keys=True) + "\n" - fd, tmp_name = tempfile.mkstemp(prefix=f"{path.name}.", suffix=".tmp", dir=path.parent) + fd, tmp_name = tempfile.mkstemp( + prefix=f"{path.name}.", suffix=".tmp", dir=path.parent + ) tmp_path = Path(tmp_name) try: with os.fdopen(fd, "w", encoding="utf-8") as tmp_file: @@ -304,7 +316,16 @@ def summarize_checks(checks): def get_workflow_runs_for_sha(repo, head_sha): endpoint = f"repos/{repo}/actions/runs" data = gh_json( - ["api", endpoint, "-X", "GET", "-f", f"head_sha={head_sha}", "-f", "per_page=100"], + [ + "api", + endpoint, + "-X", + "GET", + "-f", + f"head_sha={head_sha}", + "-f", + "per_page=100", + ], repo=repo, ) if not isinstance(data, dict): @@ -334,7 +355,12 @@ def failed_runs_from_workflow_runs(runs, head_sha): "html_url": str(run.get("html_url") or ""), } ) - failed_runs.sort(key=lambda item: (str(item.get("workflow_name") or ""), str(item.get("run_id") or ""))) + failed_runs.sort( + key=lambda item: ( + str(item.get("workflow_name") or ""), + str(item.get("run_id") or ""), + ) + ) return failed_runs @@ -361,7 +387,10 @@ def failed_jobs_from_workflow_runs(repo, runs, head_sha): continue run_status = str(run.get("status") or "") run_conclusion = str(run.get("conclusion") or "") - if run_status.lower() == "completed" and run_conclusion not in FAILED_RUN_CONCLUSIONS: + if ( + run_status.lower() == "completed" + and run_conclusion not in FAILED_RUN_CONCLUSIONS + ): continue jobs = get_jobs_for_run(repo, run_id) for job in jobs: @@ -401,7 +430,9 @@ def failed_jobs_from_workflow_runs(repo, runs, head_sha): def get_authenticated_login(): data = gh_json(["api", "user"]) if not isinstance(data, dict) or not data.get("login"): - raise GhCommandError("Unable to determine authenticated GitHub login from `gh api user`") + raise GhCommandError( + "Unable to determine authenticated GitHub login from `gh api user`" + ) return str(data["login"]) @@ -492,7 +523,9 @@ def normalize_reviews(items): "id": str(item.get("id") or ""), "author": extract_login(item.get("user")), "author_association": str(item.get("author_association") or ""), - "created_at": str(item.get("submitted_at") or item.get("created_at") or ""), + "created_at": str( + item.get("submitted_at") or item.get("created_at") or "" + ), "body": str(item.get("body") or ""), "path": None, "line": None, @@ -535,7 +568,9 @@ def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None): endpoints = comment_endpoints(repo, pr_number) issue_payload = gh_api_list_paginated(endpoints["issue_comment"], repo=repo) - review_comment_payload = gh_api_list_paginated(endpoints["review_comment"], repo=repo) + review_comment_payload = gh_api_list_paginated( + endpoints["review_comment"], repo=repo + ) review_payload = gh_api_list_paginated(endpoints["review"], repo=repo) issue_items = normalize_issue_comments(issue_payload) @@ -545,7 +580,9 @@ def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None): if isinstance(item, dict) and item.get("id") not in (None, "") } pending_review_ids = { - review_id for review_id, review_state in review_states.items() if review_state == "PENDING" + review_id + for review_id, review_state in review_states.items() + if review_state == "PENDING" } pending_review_comment_ids = { str(item.get("id")) @@ -554,7 +591,9 @@ def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None): and item.get("id") not in (None, "") and str(item.get("pull_request_review_id") or "") in pending_review_ids } - review_comment_items = normalize_review_comments(review_comment_payload, review_states) + review_comment_items = normalize_review_comments( + review_comment_payload, review_states + ) review_items = normalize_reviews(review_payload) all_items = issue_items + review_comment_items + review_items @@ -598,7 +637,13 @@ def fetch_new_review_items(pr, state, fresh_state, authenticated_login=None): elif kind == "review": seen_review.add(item_id) - new_items.sort(key=lambda item: (item.get("created_at") or "", item.get("kind") or "", item.get("id") or "")) + new_items.sort( + key=lambda item: ( + item.get("created_at") or "", + item.get("kind") or "", + item.get("id") or "", + ) + ) state["seen_issue_comment_ids"] = sorted(seen_issue) state["seen_review_comment_ids"] = sorted(seen_review_comment) state["seen_review_ids"] = sorted(seen_review) @@ -650,7 +695,15 @@ def is_pr_ready_to_merge(pr, checks_summary, new_review_items): return True -def recommend_actions(pr, checks_summary, failed_runs, failed_jobs, new_review_items, retries_used, max_retries): +def recommend_actions( + pr, + checks_summary, + failed_runs, + failed_jobs, + new_review_items, + retries_used, + max_retries, +): actions = [] if pr["closed"] or pr["merged"]: if new_review_items: @@ -671,7 +724,11 @@ def recommend_actions(pr, checks_summary, failed_runs, failed_jobs, new_review_i actions.append("stop_exhausted_retries") else: actions.append("diagnose_ci_failure") - if checks_summary["all_terminal"] and failed_runs and retries_used < max_retries: + if ( + checks_summary["all_terminal"] + and failed_runs + and retries_used < max_retries + ): actions.append("retry_failed_checks") if not actions: @@ -681,7 +738,9 @@ def recommend_actions(pr, checks_summary, failed_runs, failed_jobs, new_review_i def collect_snapshot(args): pr = resolve_pr(args.pr, repo_override=args.repo) - state_path = Path(args.state_file) if args.state_file else default_state_file_for(pr) + state_path = ( + Path(args.state_file) if args.state_file else default_state_file_for(pr) + ) state, fresh_state = load_state(state_path) if not state.get("started_at"): @@ -703,7 +762,9 @@ def collect_snapshot(args): checks_summary = summarize_checks(checks) workflow_runs = get_workflow_runs_for_sha(pr["repo"], pr["head_sha"]) failed_runs = failed_runs_from_workflow_runs(workflow_runs, pr["head_sha"]) - failed_jobs = failed_jobs_from_workflow_runs(pr["repo"], workflow_runs, pr["head_sha"]) + failed_jobs = failed_jobs_from_workflow_runs( + pr["repo"], workflow_runs, pr["head_sha"] + ) retries_used = current_retry_count(state, pr["head_sha"]) actions = recommend_actions( @@ -845,11 +906,10 @@ def run_watch(args): }, ) actions = set(snapshot.get("actions") or []) - if ( - "stop_pr_closed" in actions - or "stop_exhausted_retries" in actions - ): - print_event("stop", {"actions": snapshot.get("actions"), "pr": snapshot.get("pr")}) + if "stop_pr_closed" in actions or "stop_exhausted_retries" in actions: + print_event( + "stop", {"actions": snapshot.get("actions"), "pr": snapshot.get("pr")} + ) return 0 current_change_key = snapshot_change_key(snapshot) diff --git a/.github/scripts/macos-signing/notarize_with_akv.py b/.github/scripts/macos-signing/notarize_with_akv.py index df4362e570..1929482cd4 100644 --- a/.github/scripts/macos-signing/notarize_with_akv.py +++ b/.github/scripts/macos-signing/notarize_with_akv.py @@ -49,10 +49,14 @@ class NotarizationConfiguration: "vault_name": "AZURE_KEYVAULT_NAME", "vault_key_name": "APPLE_NOTARIZATION_AKV_KEY_NAME", } - values = {field: os.environ.get(name, "").strip() for field, name in required.items()} + values = { + field: os.environ.get(name, "").strip() for field, name in required.items() + } missing = [name for field, name in required.items() if not values[field]] if missing: - raise NotarizationError("Missing notarization configuration: " + ", ".join(missing)) + raise NotarizationError( + "Missing notarization configuration: " + ", ".join(missing) + ) if not re.fullmatch(r"[A-Za-z][A-Za-z0-9-]{2,23}", values["vault_name"]): raise NotarizationError("Invalid signing vault name") if not re.fullmatch(r"[A-Za-z0-9-]+", values["vault_key_name"]): @@ -90,7 +94,9 @@ class NotarizationConfiguration: try: result = subprocess.run(command, check=True, capture_output=True, text=True) except (OSError, subprocess.CalledProcessError) as error: - raise NotarizationError("Notarization signing key could not be read") from error + raise NotarizationError( + "Notarization signing key could not be read" + ) from error try: metadata = json.loads(result.stdout) returned_key_id = metadata["id"] @@ -99,10 +105,10 @@ class NotarizationConfiguration: except (json.JSONDecodeError, KeyError, TypeError) as error: raise NotarizationError("Notarization key metadata is invalid") from error - key_id_prefix = ( - f"https://{values['vault_name']}.vault.azure.net/keys/{values['vault_key_name']}/" - ) - if not isinstance(returned_key_id, str) or not returned_key_id.startswith(key_id_prefix): + key_id_prefix = f"https://{values['vault_name']}.vault.azure.net/keys/{values['vault_key_name']}/" + if not isinstance(returned_key_id, str) or not returned_key_id.startswith( + key_id_prefix + ): raise NotarizationError("Unexpected notarization signing key") returned_key_version = returned_key_id[len(key_id_prefix) :] if not re.fullmatch(r"[0-9a-fA-F]{32}", returned_key_version): @@ -120,7 +126,9 @@ class NotarizationConfiguration: r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", apple_issuer_id.strip(), ): - raise NotarizationError("Notarization apple-issuer-id tag must contain a valid UUID") + raise NotarizationError( + "Notarization apple-issuer-id tag must contain a valid UUID" + ) values["issuer_id"] = apple_issuer_id.strip() values["apple_key_id"] = apple_key_id.strip() @@ -147,12 +155,18 @@ def base64url_decode(value: str) -> bytes: """Decode a base64url value and reject malformed input.""" try: - return base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True) + return base64.b64decode( + value + "=" * (-len(value) % 4), altchars=b"-_", validate=True + ) except (ValueError, UnicodeEncodeError) as error: - raise NotarizationError("Signing service returned an invalid signature") from error + raise NotarizationError( + "Signing service returned an invalid signature" + ) from error -def create_apple_jwt(configuration: NotarizationConfiguration, *, issued_at: int) -> str: +def create_apple_jwt( + configuration: NotarizationConfiguration, *, issued_at: int +) -> str: """Create an authentication token for notarization requests.""" header = {"alg": "ES256", "kid": configuration.apple_key_id, "typ": "JWT"} @@ -164,8 +178,12 @@ def create_apple_jwt(configuration: NotarizationConfiguration, *, issued_at: int "aud": "appstoreconnect-v1", "scope": ["/notary/v2"], } - encoded_header = base64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8")) - encoded_claims = base64url_encode(json.dumps(claims, separators=(",", ":")).encode("utf-8")) + encoded_header = base64url_encode( + json.dumps(header, separators=(",", ":")).encode("utf-8") + ) + encoded_claims = base64url_encode( + json.dumps(claims, separators=(",", ":")).encode("utf-8") + ) signing_input = f"{encoded_header}.{encoded_claims}" digest = hashlib.sha256(signing_input.encode("ascii")).digest() key_url = configuration.versioned_key_id @@ -206,7 +224,9 @@ def create_apple_jwt(configuration: NotarizationConfiguration, *, issued_at: int returned_key_id = response["kid"] encoded_signature = response["value"] except (json.JSONDecodeError, KeyError, TypeError) as error: - raise NotarizationError("Signing service returned an invalid response") from error + raise NotarizationError( + "Signing service returned an invalid response" + ) from error # Reject signatures generated with a different key version. if returned_key_id != key_url: @@ -220,7 +240,9 @@ def create_apple_jwt(configuration: NotarizationConfiguration, *, issued_at: int return f"{signing_input}.{base64url_encode(signature)}" -def json_request(url: str, token: str, *, body: dict[str, Any] | None = None) -> dict[str, Any]: +def json_request( + url: str, token: str, *, body: dict[str, Any] | None = None +) -> dict[str, Any]: """Send an authenticated JSON request to Apple's notarization service.""" data = None if body is None else json.dumps(body).encode("utf-8") @@ -239,7 +261,9 @@ def json_request(url: str, token: str, *, body: dict[str, Any] | None = None) -> result = json.load(response) except urllib.error.HTTPError as error: detail = error.read(1024).decode("utf-8", errors="replace") - raise NotarizationError(f"Apple Notary API returned HTTP {error.code}: {detail}") from error + raise NotarizationError( + f"Apple Notary API returned HTTP {error.code}: {detail}" + ) from error except (OSError, json.JSONDecodeError) as error: raise NotarizationError("Apple Notary API request failed") from error @@ -281,9 +305,12 @@ def upload_to_apple_s3( "object", ) if any( - not isinstance(credentials.get(name), str) or not credentials[name] for name in required + not isinstance(credentials.get(name), str) or not credentials[name] + for name in required ): - raise NotarizationError("Apple returned incomplete temporary upload credentials") + raise NotarizationError( + "Apple returned incomplete temporary upload credentials" + ) size = path.stat().st_size if not 0 < size <= MAX_SINGLE_UPLOAD_BYTES: @@ -293,7 +320,9 @@ def upload_to_apple_s3( amz_date = now.strftime("%Y%m%dT%H%M%SZ") date_stamp = now.strftime("%Y%m%d") host = f"{credentials['bucket']}.s3.{AWS_REGION}.amazonaws.com" - object_path = "/" + urllib.parse.quote(credentials["object"].lstrip("/"), safe="/-_.~") + object_path = "/" + urllib.parse.quote( + credentials["object"].lstrip("/"), safe="/-_.~" + ) headers = { "content-type": "application/octet-stream", "host": host, @@ -319,7 +348,9 @@ def upload_to_apple_s3( signing_key = ("AWS4" + credentials["awsSecretAccessKey"]).encode("utf-8") for component in (date_stamp, AWS_REGION, "s3", "aws4_request"): signing_key = hmac_sha256(signing_key, component) - signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() + signature = hmac.new( + signing_key, string_to_sign.encode("utf-8"), hashlib.sha256 + ).hexdigest() authorization = ( "AWS4-HMAC-SHA256 " f"Credential={credentials['awsAccessKeyId']}/{credential_scope}, " @@ -346,10 +377,14 @@ def upload_to_apple_s3( f"Apple notarization payload upload failed with HTTP {error.code}" ) from error except OSError as error: - raise NotarizationError("Apple notarization payload upload failed") from error + raise NotarizationError( + "Apple notarization payload upload failed" + ) from error -def write_developer_log(submission_id: str, token: str, destination: Path | None) -> None: +def write_developer_log( + submission_id: str, token: str, destination: Path | None +) -> None: """Write the submission's optional diagnostic report.""" if destination is None: @@ -358,9 +393,13 @@ def write_developer_log(submission_id: str, token: str, destination: Path | None try: log_url = response["data"]["attributes"]["developerLogUrl"] except (KeyError, TypeError) as error: - raise NotarizationError("Apple did not provide a notarization developer log") from error + raise NotarizationError( + "Apple did not provide a notarization developer log" + ) from error if not isinstance(log_url, str) or not log_url.startswith("https://"): - raise NotarizationError("Apple returned an invalid notarization developer log URL") + raise NotarizationError( + "Apple returned an invalid notarization developer log URL" + ) try: # Do not forward unrelated authorization headers to the download URL. with urllib.request.urlopen(log_url, timeout=60) as response: @@ -370,7 +409,9 @@ def write_developer_log(submission_id: str, token: str, destination: Path | None f"Apple notarization developer log download failed with HTTP {error.code}" ) from error except OSError as error: - raise NotarizationError("Apple notarization developer log download failed") from error + raise NotarizationError( + "Apple notarization developer log download failed" + ) from error def notarize( @@ -395,11 +436,15 @@ def notarize( submission_id = response["data"]["id"] upload_credentials = response["data"]["attributes"] except (KeyError, TypeError) as error: - raise NotarizationError("Apple returned an invalid notarization submission") from error + raise NotarizationError( + "Apple returned an invalid notarization submission" + ) from error if not isinstance(submission_id, str) or not submission_id: raise NotarizationError("Apple did not return a notarization submission ID") if not isinstance(upload_credentials, dict): - raise NotarizationError("Apple returned invalid notarization upload credentials") + raise NotarizationError( + "Apple returned invalid notarization upload credentials" + ) print(f"Uploading notarization submission {submission_id} for {path.name}") upload_to_apple_s3(path, file_digest, upload_credentials) @@ -416,7 +461,9 @@ def notarize( try: status = result["data"]["attributes"]["status"] except (KeyError, TypeError) as error: - raise NotarizationError("Apple returned an invalid notarization status") from error + raise NotarizationError( + "Apple returned an invalid notarization status" + ) from error if status in {"Accepted", "Invalid", "Rejected"}: # Save diagnostics for both accepted and rejected submissions. @@ -449,7 +496,9 @@ def main() -> int: try: if not arguments.file.is_file(): - raise NotarizationError(f"Notarization payload does not exist: {arguments.file}") + raise NotarizationError( + f"Notarization payload does not exist: {arguments.file}" + ) if arguments.max_wait_seconds < 0: raise NotarizationError("--max-wait-seconds must be non-negative") if arguments.report_log is not None: diff --git a/.github/scripts/macos-signing/test_notarize_with_akv.py b/.github/scripts/macos-signing/test_notarize_with_akv.py index 7f40c6a3eb..7c3aa50534 100644 --- a/.github/scripts/macos-signing/test_notarize_with_akv.py +++ b/.github/scripts/macos-signing/test_notarize_with_akv.py @@ -35,7 +35,9 @@ CREDENTIALS = { def response(payload): - return io.BytesIO(json.dumps(payload).encode() if isinstance(payload, dict) else payload) + return io.BytesIO( + json.dumps(payload).encode() if isinstance(payload, dict) else payload + ) def azure_response(payload): @@ -66,10 +68,16 @@ class NotarizationTest(unittest.TestCase): notary.NotarizationConfiguration.from_environment() for issuer_id in (None, "", " "): with self.subTest(issuer_id=issuer_id): - show.return_value = azure_response({**metadata, "apple_issuer_id": issuer_id}) - with self.assertRaisesRegex(notary.NotarizationError, "apple-issuer-id tag"): + show.return_value = azure_response( + {**metadata, "apple_issuer_id": issuer_id} + ) + with self.assertRaisesRegex( + notary.NotarizationError, "apple-issuer-id tag" + ): notary.NotarizationConfiguration.from_environment() - show.return_value = azure_response({**metadata, "apple_issuer_id": "invalid-issuer"}) + show.return_value = azure_response( + {**metadata, "apple_issuer_id": "invalid-issuer"} + ) with self.assertRaisesRegex(notary.NotarizationError, "valid UUID"): notary.NotarizationConfiguration.from_environment() self.assertIn( @@ -78,7 +86,10 @@ class NotarizationTest(unittest.TestCase): show.call_args.args[0], ) - invalid_environment = {**ENVIRONMENT, "APPLE_NOTARIZATION_AKV_KEY_NAME": "../bad"} + invalid_environment = { + **ENVIRONMENT, + "APPLE_NOTARIZATION_AKV_KEY_NAME": "../bad", + } with ( patch.dict(os.environ, invalid_environment, clear=True), self.assertRaisesRegex(notary.NotarizationError, "key name"), @@ -91,10 +102,14 @@ class NotarizationTest(unittest.TestCase): "kid": CONFIGURATION.versioned_key_id, "value": notary.base64url_encode(signature), } - with patch.object(notary.subprocess, "run", return_value=azure_response(payload)) as sign: + with patch.object( + notary.subprocess, "run", return_value=azure_response(payload) + ) as sign: token = notary.create_apple_jwt(CONFIGURATION, issued_at=1_780_000_000) header, claims, encoded_signature = token.split(".") - self.assertEqual(json.loads(notary.base64url_decode(header))["kid"], "APPLEKEY01") + self.assertEqual( + json.loads(notary.base64url_decode(header))["kid"], "APPLEKEY01" + ) self.assertEqual( json.loads(notary.base64url_decode(claims))["iss"], CONFIGURATION.issuer_id ) @@ -113,7 +128,11 @@ class NotarizationTest(unittest.TestCase): def test_rejects_wrong_key_versions_and_invalid_signatures(self) -> None: cases = ( - (CONFIGURATION.versioned_key_id + "wrong", bytes(64), "unexpected key version"), + ( + CONFIGURATION.versioned_key_id + "wrong", + bytes(64), + "unexpected key version", + ), (CONFIGURATION.versioned_key_id, b"short", "64 JOSE"), ) for key_id, signature, message in cases: @@ -141,7 +160,15 @@ class NotarizationTest(unittest.TestCase): response(b""), response({"data": {"attributes": {"status": "In Progress"}}}), response({"data": {"attributes": {"status": "Accepted"}}}), - response({"data": {"attributes": {"developerLogUrl": "https://logs.example.com/log"}}}), + response( + { + "data": { + "attributes": { + "developerLogUrl": "https://logs.example.com/log" + } + } + } + ), response(b'{"status":"Accepted"}'), ] with tempfile.TemporaryDirectory() as directory: @@ -153,7 +180,9 @@ class NotarizationTest(unittest.TestCase): output = io.StringIO() with ( patch.object(notary.subprocess, "run", return_value=signature), - patch.object(notary.urllib.request, "urlopen", side_effect=responses) as requests, + patch.object( + notary.urllib.request, "urlopen", side_effect=responses + ) as requests, patch.object(notary.time, "sleep") as sleep, contextlib.redirect_stdout(output), ): @@ -185,9 +214,15 @@ class NotarizationWrapperTest(unittest.TestCase): "CALL_LOG": str(call_log), } for name in ("az", "python3", "rcodesign"): - body = "exit 0" if name == "az" else f'printf "{name} %s\\n" "$*" >> "$CALL_LOG"' + body = ( + "exit 0" + if name == "az" + else f'printf "{name} %s\\n" "$*" >> "$CALL_LOG"' + ) executable = tools / name - executable.write_text(f"#!/usr/bin/env bash\nset -euo pipefail\n{body}\n") + executable.write_text( + f"#!/usr/bin/env bash\nset -euo pipefail\n{body}\n" + ) executable.chmod(0o755) for kind in ("binary", "dmg"): with self.subTest(kind=kind): @@ -195,7 +230,9 @@ class NotarizationWrapperTest(unittest.TestCase): artifact.write_bytes(b"signed release artifact") subprocess.run( [ - str(SIGNING_DIRECTORY / f"notarize_macos_{kind}_with_akv.sh"), + str( + SIGNING_DIRECTORY / f"notarize_macos_{kind}_with_akv.sh" + ), f"--{kind}", str(artifact), "--report-dir", diff --git a/.github/scripts/test_rusty_v8_bazel.py b/.github/scripts/test_rusty_v8_bazel.py index 47cf2f16a5..e0ed40b00e 100644 --- a/.github/scripts/test_rusty_v8_bazel.py +++ b/.github/scripts/test_rusty_v8_bazel.py @@ -236,12 +236,7 @@ class RustyV8BazelTest(unittest.TestCase): TemporaryDirectory() as target_dir, TemporaryDirectory() as output_dir, ): - gn_out = ( - Path(target_dir) - / "x86_64-pc-windows-msvc" - / "release" - / "gn_out" - ) + gn_out = Path(target_dir) / "x86_64-pc-windows-msvc" / "release" / "gn_out" (gn_out / "obj").mkdir(parents=True) (gn_out / "obj" / "rusty_v8.lib").write_bytes(b"archive") (gn_out / "src_binding.rs").write_text("binding") diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py index 958aa85432..e36e77316c 100644 --- a/.github/scripts/v8_canary_changes.py +++ b/.github/scripts/v8_canary_changes.py @@ -168,9 +168,7 @@ def main() -> None: canary = canary_required(files, base_version, head_version) windows_source = windows_source_required(files, base_version, head_version) if base_version != head_version: - canary_reason = ( - f"v8 version changed from {base_version} to {head_version}" - ) + canary_reason = f"v8 version changed from {base_version} to {head_version}" windows_source_reason = canary_reason else: canary_reason = ( diff --git a/.github/scripts/verify_cargo_workspace_manifests.py b/.github/scripts/verify_cargo_workspace_manifests.py index 93b41ea59f..645c6c59e5 100644 --- a/.github/scripts/verify_cargo_workspace_manifests.py +++ b/.github/scripts/verify_cargo_workspace_manifests.py @@ -167,7 +167,9 @@ def manifest_errors( "create crate features" ) - if not is_internal_dependency(path, dependency_name, dependency, internal_package_names): + if not is_internal_dependency( + path, dependency_name, dependency, internal_package_names + ): continue dependency_features = dependency.get("features") @@ -354,7 +356,9 @@ def add_unused_exception_errors( ) -def add_failure(failures_by_path: dict[str, list[str]], path_key: str, error: str) -> None: +def add_failure( + failures_by_path: dict[str, list[str]], path_key: str, error: str +) -> None: failures_by_path.setdefault(path_key, []).append(error) diff --git a/.github/scripts/verify_tui_core_boundary.py b/.github/scripts/verify_tui_core_boundary.py index e66afeca92..4c1b8d1e89 100644 --- a/.github/scripts/verify_tui_core_boundary.py +++ b/.github/scripts/verify_tui_core_boundary.py @@ -66,7 +66,7 @@ def dependency_sections(manifest: dict) -> list[tuple[str, dict]]: for section_name in ("dependencies", "dev-dependencies", "build-dependencies"): dependencies = target.get(section_name) if isinstance(dependencies, dict): - sections.append((f'target.{target_name}.{section_name}', dependencies)) + sections.append((f"target.{target_name}.{section_name}", dependencies)) return sections @@ -77,7 +77,9 @@ def source_failures() -> list[str]: text = path.read_text() for line_number, line in enumerate(text.splitlines(), start=1): if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS): - failures.append(f"{relative_path(path)}:{line_number} imports `codex_core`") + failures.append( + f"{relative_path(path)}:{line_number} imports `codex_core`" + ) return failures diff --git a/codex-cli/scripts/build_npm_package.py b/codex-cli/scripts/build_npm_package.py index 60f6ca7a9d..12f4f9dc89 100755 --- a/codex-cli/scripts/build_npm_package.py +++ b/codex-cli/scripts/build_npm_package.py @@ -88,8 +88,11 @@ PACKAGE_TARGET_FILTERS: dict[str, str] = { PACKAGE_CHOICES = tuple(PACKAGE_NATIVE_COMPONENTS) + def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Build or stage the Codex CLI npm package.") + parser = argparse.ArgumentParser( + description="Build or stage the Codex CLI npm package." + ) parser.add_argument( "--package", choices=PACKAGE_CHOICES, @@ -102,9 +105,7 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( "--release-version", - help=( - "Version to stage for npm release." - ), + help=("Version to stage for npm release."), ) parser.add_argument( "--staging-dir", @@ -141,7 +142,9 @@ def main() -> int: release_version = args.release_version if release_version: if version and version != release_version: - raise RuntimeError("--version and --release-version must match when both are provided.") + raise RuntimeError( + "--version and --release-version must match when both are provided." + ) version = release_version if not version: @@ -272,7 +275,9 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None: elif package == "codex-responses-api-proxy": bin_dir = staging_dir / "bin" bin_dir.mkdir(parents=True, exist_ok=True) - launcher_src = RESPONSES_API_PROXY_NPM_ROOT / "bin" / "codex-responses-api-proxy.js" + launcher_src = ( + RESPONSES_API_PROXY_NPM_ROOT / "bin" / "codex-responses-api-proxy.js" + ) shutil.copy2(launcher_src, bin_dir / "codex-responses-api-proxy.js") readme_src = RESPONSES_API_PROXY_NPM_ROOT / "README.md" @@ -405,7 +410,10 @@ def copy_native_binaries( missing_targets = sorted(target_filter - copied_targets) if missing_targets: missing_list = ", ".join(missing_targets) - raise RuntimeError(f"Missing target directories in vendor source: {missing_list}") + raise RuntimeError( + f"Missing target directories in vendor source: {missing_list}" + ) + def run_npm_pack(staging_dir: Path, output_path: Path) -> Path: output_path = output_path.resolve() diff --git a/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py b/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py index 69217206cc..f23da4fc86 100644 --- a/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py +++ b/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py @@ -33,9 +33,7 @@ def main() -> None: env = os.environ.copy() env["CODEX_APP_SERVER_SCHEMA_ROOT"] = str(schema_root) - env["CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL"] = ( - "1" if args.experimental else "0" - ) + env["CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL"] = "1" if args.experimental else "0" if args.prettier: env["CODEX_APP_SERVER_SCHEMA_PRETTIER"] = str(args.prettier) diff --git a/codex-rs/skills/src/assets/samples/imagegen/scripts/image_gen.py b/codex-rs/skills/src/assets/samples/imagegen/scripts/image_gen.py index 9e0ea6717c..ca78422d71 100644 --- a/codex-rs/skills/src/assets/samples/imagegen/scripts/image_gen.py +++ b/codex-rs/skills/src/assets/samples/imagegen/scripts/image_gen.py @@ -132,7 +132,9 @@ def _validate_gpt_image_2_size(size: str) -> None: total_pixels = width * height if max_edge > GPT_IMAGE_2_MAX_EDGE: - _die("gpt-image-2 size maximum edge length must be less than or equal to 3840px.") + _die( + "gpt-image-2 size maximum edge length must be less than or equal to 3840px." + ) if width % 16 != 0 or height % 16 != 0: _die("gpt-image-2 size width and height must be multiples of 16px.") if max_edge / min_edge > GPT_IMAGE_2_MAX_RATIO: @@ -257,7 +259,9 @@ def _augment_prompt(args: argparse.Namespace, prompt: str) -> str: return _augment_prompt_fields(args.augment, prompt, fields) -def _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str: +def _augment_prompt_fields( + augment: bool, prompt: str, fields: Dict[str, Optional[str]] +) -> str: if not augment: return prompt @@ -280,7 +284,7 @@ def _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optiona if fields.get("materials"): sections.append(f"Materials/textures: {fields['materials']}") if fields.get("text"): - sections.append(f"Text (verbatim): \"{fields['text']}\"") + sections.append(f'Text (verbatim): "{fields["text"]}"') if fields.get("constraints"): sections.append(f"Constraints: {fields['constraints']}") if fields.get("negative"): @@ -327,7 +331,9 @@ def _derive_downscale_path(path: Path, suffix: str) -> Path: return path.with_name(f"{path.stem}{suffix}{path.suffix}") -def _downscale_image_bytes(image_bytes: bytes, *, max_dim: int, output_format: str) -> bytes: +def _downscale_image_bytes( + image_bytes: bytes, *, max_dim: int, output_format: str +) -> bytes: try: from PIL import Image except Exception: @@ -342,16 +348,22 @@ def _downscale_image_bytes(image_bytes: bytes, *, max_dim: int, output_format: s scale = min(1.0, float(max_dim) / float(max(w, h))) target = (max(1, int(round(w * scale))), max(1, int(round(h * scale)))) - resized = img if target == (w, h) else img.resize(target, Image.Resampling.LANCZOS) + resized = ( + img if target == (w, h) else img.resize(target, Image.Resampling.LANCZOS) + ) fmt = output_format.lower() if fmt == "jpg": fmt = "jpeg" if fmt == "jpeg": - if resized.mode in ("RGBA", "LA") or ("transparency" in getattr(resized, "info", {})): + if resized.mode in ("RGBA", "LA") or ( + "transparency" in getattr(resized, "info", {}) + ): bg = Image.new("RGB", resized.size, (255, 255, 255)) - bg.paste(resized.convert("RGBA"), mask=resized.convert("RGBA").split()[-1]) + bg.paste( + resized.convert("RGBA"), mask=resized.convert("RGBA").split()[-1] + ) resized = bg else: resized = resized.convert("RGB") @@ -389,7 +401,9 @@ def _decode_write_and_downscale( if derived.exists() and not force: _die(f"Output already exists: {derived} (use --force to overwrite)") derived.parent.mkdir(parents=True, exist_ok=True) - resized = _downscale_image_bytes(raw, max_dim=downscale_max_dim, output_format=output_format) + resized = _downscale_image_bytes( + raw, max_dim=downscale_max_dim, output_format=output_format + ) derived.write_bytes(resized) print(f"Wrote {derived}") @@ -398,7 +412,9 @@ def _create_client(): try: from openai import OpenAI except ImportError: - _die(f"openai SDK not installed in the active environment. {_dependency_hint('openai')}") + _die( + f"openai SDK not installed in the active environment. {_dependency_hint('openai')}" + ) return OpenAI() @@ -500,10 +516,7 @@ def _job_output_paths( if n == 1: return [base] - return [ - base.with_name(f"{base.stem}-{i}{base.suffix}") - for i in range(1, n + 1) - ] + return [base.with_name(f"{base.stem}-{i}{base.suffix}") for i in range(1, n + 1)] def _extract_retry_after_seconds(exc: Exception) -> Optional[float]: @@ -589,17 +602,25 @@ async def _run_generate_batch(args: argparse.Namespace) -> int: prompt = str(job["prompt"]).strip() fields = _merge_non_null(base_fields, job.get("fields", {})) # Allow flat job keys as well (use_case, scene, etc.) - fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()}) + fields = _merge_non_null( + fields, {k: job.get(k) for k in base_fields.keys()} + ) augmented = _augment_prompt_fields(args.augment, prompt, fields) job_payload = dict(base_payload) job_payload["prompt"] = augmented - job_payload = _merge_non_null(job_payload, {k: job.get(k) for k in base_payload.keys()}) + job_payload = _merge_non_null( + job_payload, {k: job.get(k) for k in base_payload.keys()} + ) job_payload = {k: v for k, v in job_payload.items() if v is not None} _validate_generate_payload(job_payload) - effective_output_format = _normalize_output_format(job_payload.get("output_format")) - _validate_transparency(job_payload.get("background"), effective_output_format) + effective_output_format = _normalize_output_format( + job_payload.get("output_format") + ) + _validate_transparency( + job_payload.get("background"), effective_output_format + ) job_payload["output_format"] = effective_output_format n = int(job_payload.get("n", 1)) @@ -614,7 +635,8 @@ async def _run_generate_batch(args: argparse.Namespace) -> int: downscaled = None if args.downscale_max_dim is not None: downscaled = [ - str(_derive_downscale_path(p, args.downscale_suffix)) for p in outputs + str(_derive_downscale_path(p, args.downscale_suffix)) + for p in outputs ] _print_request( { @@ -688,7 +710,9 @@ async def _run_generate_batch(args: argparse.Namespace) -> int: raise return i, str(exc) - tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)] + tasks = [ + asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1) + ] try: await asyncio.gather(*tasks) @@ -730,7 +754,9 @@ def _generate(args: argparse.Namespace) -> None: output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir) downscaled = None if args.downscale_max_dim is not None: - downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths] + downscaled = [ + str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths + ] if args.dry_run: _print_request( @@ -799,7 +825,9 @@ def _edit(args: argparse.Namespace) -> None: output_paths = _build_output_paths(args.out, output_format, args.n, args.out_dir) downscaled = None if args.downscale_max_dim is not None: - downscaled = [str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths] + downscaled = [ + str(_derive_downscale_path(p, args.downscale_suffix)) for p in output_paths + ] if args.dry_run: payload_preview = dict(payload) @@ -949,7 +977,9 @@ def main() -> int: help="Generate multiple prompts concurrently (JSONL input)", ) _add_shared_args(batch_parser) - batch_parser.add_argument("--input", required=True, help="Path to JSONL file (one job per line)") + batch_parser.add_argument( + "--input", required=True, help="Path to JSONL file (one job per line)" + ) batch_parser.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY) batch_parser.add_argument("--max-attempts", type=int, default=3) batch_parser.add_argument("--fail-fast", action="store_true") @@ -969,11 +999,16 @@ def main() -> int: _die("--concurrency must be between 1 and 25") if getattr(args, "max_attempts", 3) < 1 or getattr(args, "max_attempts", 3) > 10: _die("--max-attempts must be between 1 and 10") - if args.output_compression is not None and not (0 <= args.output_compression <= 100): + if args.output_compression is not None and not ( + 0 <= args.output_compression <= 100 + ): _die("--output-compression must be between 0 and 100") if args.command == "generate-batch" and not args.out_dir: _die("generate-batch requires --out-dir") - if getattr(args, "downscale_max_dim", None) is not None and args.downscale_max_dim < 1: + if ( + getattr(args, "downscale_max_dim", None) is not None + and args.downscale_max_dim < 1 + ): _die("--downscale-max-dim must be >= 1") _validate_model(args.model) diff --git a/codex-rs/skills/src/assets/samples/imagegen/scripts/remove_chroma_key.py b/codex-rs/skills/src/assets/samples/imagegen/scripts/remove_chroma_key.py index 50539877b0..f14df35518 100644 --- a/codex-rs/skills/src/assets/samples/imagegen/scripts/remove_chroma_key.py +++ b/codex-rs/skills/src/assets/samples/imagegen/scripts/remove_chroma_key.py @@ -95,7 +95,9 @@ def _smoothstep(value: float) -> float: return value * value * (3.0 - 2.0 * value) -def _soft_alpha(distance: int, transparent_threshold: float, opaque_threshold: float) -> int: +def _soft_alpha( + distance: int, transparent_threshold: float, opaque_threshold: float +) -> int: if distance <= transparent_threshold: return 0 if distance >= opaque_threshold: @@ -132,7 +134,9 @@ def _spill_channels(key: Color) -> list[int]: key_max = max(key) if key_max < 128: return [] - return [idx for idx, value in enumerate(key) if value >= key_max - 16 and value >= 128] + return [ + idx for idx, value in enumerate(key) if value >= key_max - 16 and value >= 128 + ] def _key_channel_dominance(rgb: Color, key: Color) -> float: @@ -358,7 +362,10 @@ def _remove_chroma_key(args: argparse.Namespace) -> None: print(f"Transparent pixels: {transparent_after}/{total}") print(f"Partially transparent pixels: {partial_after}/{total}") if transparent == 0: - print("Warning: no pixels matched the key color before feathering.", file=sys.stderr) + print( + "Warning: no pixels matched the key color before feathering.", + file=sys.stderr, + ) def _build_parser() -> argparse.ArgumentParser: @@ -425,7 +432,9 @@ def _build_parser() -> argparse.ArgumentParser: action="store_true", help="Alias for --spill-cleanup; decontaminate key-color edge spill.", ) - parser.add_argument("--force", action="store_true", help="Overwrite an existing output file.") + parser.add_argument( + "--force", action="store_true", help="Overwrite an existing output file." + ) return parser diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py index 25a4e58b5e..40edaa8600 100755 --- a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py +++ b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py @@ -49,7 +49,9 @@ def display_name_from_plugin_name(plugin_name: str) -> str: return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name)) -def build_plugin_json(plugin_name: str, *, with_mcp: bool, with_apps: bool) -> dict[str, Any]: +def build_plugin_json( + plugin_name: str, *, with_mcp: bool, with_apps: bool +) -> dict[str, Any]: display_name = display_name_from_plugin_name(plugin_name) payload: dict[str, Any] = { "name": plugin_name, @@ -126,7 +128,9 @@ def load_validated_marketplace( if marketplace_path.exists(): payload = load_json(marketplace_path) else: - payload = build_default_marketplace(marketplace_name or DEFAULT_MARKETPLACE_NAME) + payload = build_default_marketplace( + marketplace_name or DEFAULT_MARKETPLACE_NAME + ) if not isinstance(payload, dict): raise ValueError(f"{marketplace_path} must contain a JSON object.") @@ -134,7 +138,10 @@ def load_validated_marketplace( validate_marketplace_interface(payload) existing_marketplace_name = payload.get("name") - if not isinstance(existing_marketplace_name, str) or not existing_marketplace_name.strip(): + if ( + not isinstance(existing_marketplace_name, str) + or not existing_marketplace_name.strip() + ): raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.") validate_marketplace_name(existing_marketplace_name) @@ -150,7 +157,8 @@ def load_validated_marketplace( if not isinstance(plugins, list): raise ValueError(f"{marketplace_path} field 'plugins' must be an array.") if not force and any( - isinstance(entry, dict) and entry.get("name") == plugin_name for entry in plugins + isinstance(entry, dict) and entry.get("name") == plugin_name + for entry in plugins ): raise FileExistsError( f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. " @@ -169,10 +177,14 @@ def update_marketplace_json( category: str, force: bool, ) -> None: - payload = load_validated_marketplace(marketplace_path, marketplace_name, plugin_name, force) + payload = load_validated_marketplace( + marketplace_path, marketplace_name, plugin_name, force + ) plugins = payload["plugins"] - new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category) + new_entry = build_marketplace_entry( + plugin_name, install_policy, auth_policy, category + ) for index, entry in enumerate(plugins): if isinstance(entry, dict) and entry.get("name") == plugin_name: @@ -215,12 +227,24 @@ def parse_args() -> argparse.Namespace: "Pass an explicit repo path only when a repo/team plugin is intended." ), ) - parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory") - parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory") - parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory") - parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory") - parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder") - parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder") + parser.add_argument( + "--with-skills", action="store_true", help="Create skills/ directory" + ) + parser.add_argument( + "--with-hooks", action="store_true", help="Create hooks/ directory" + ) + parser.add_argument( + "--with-scripts", action="store_true", help="Create scripts/ directory" + ) + parser.add_argument( + "--with-assets", action="store_true", help="Create assets/ directory" + ) + parser.add_argument( + "--with-mcp", action="store_true", help="Create .mcp.json placeholder" + ) + parser.add_argument( + "--with-apps", action="store_true", help="Create .app.json placeholder" + ) parser.add_argument( "--with-marketplace", action="store_true", @@ -271,7 +295,9 @@ def main() -> None: raw_plugin_name = args.plugin_name plugin_name = normalize_plugin_name(raw_plugin_name) if plugin_name != raw_plugin_name: - print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.") + print( + f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'." + ) validate_plugin_name(plugin_name) marketplace_name = None if args.marketplace_name is not None: @@ -280,15 +306,19 @@ def main() -> None: if args.with_marketplace: marketplace_path = Path(args.marketplace_path).expanduser().resolve() - load_validated_marketplace(marketplace_path, marketplace_name, plugin_name, args.force) + load_validated_marketplace( + marketplace_path, marketplace_name, plugin_name, args.force + ) - plugin_root = (Path(args.path).expanduser().resolve() / plugin_name) + plugin_root = Path(args.path).expanduser().resolve() / plugin_name plugin_root.mkdir(parents=True, exist_ok=True) plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json" write_json( plugin_json_path, - build_plugin_json(plugin_name, with_mcp=args.with_mcp, with_apps=args.with_apps), + build_plugin_json( + plugin_name, with_mcp=args.with_mcp, with_apps=args.with_apps + ), args.force, ) diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/update_plugin_cachebuster.py b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/update_plugin_cachebuster.py index 478fb9ea73..7818f15b5c 100644 --- a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/update_plugin_cachebuster.py +++ b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/update_plugin_cachebuster.py @@ -62,6 +62,8 @@ def load_manifest(manifest_path: Path) -> dict[str, object]: if not isinstance(payload, dict): raise ValueError(f"{manifest_path} must contain a JSON object.") return payload + + def sanitize_cachebuster(value: str) -> str: sanitized = re.sub(r"[^a-z0-9-]+", "-", value.strip().lower()) sanitized = re.sub(r"-{2,}", "-", sanitized).strip("-") diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py index 68ab10565f..b5be462c3b 100644 --- a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py +++ b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py @@ -187,7 +187,9 @@ def validate_manifest_shape( if not isinstance(capabilities, list) or not all( isinstance(value, str) and value.strip() for value in capabilities ): - errors.append("plugin.json field `interface.capabilities` must be an array of strings") + errors.append( + "plugin.json field `interface.capabilities` must be an array of strings" + ) for field in ("websiteURL", "privacyPolicyURL", "termsOfServiceURL"): validate_optional_https_url(interface, field, errors, prefix="interface") brand_color = interface.get("brandColor") @@ -260,7 +262,9 @@ def reject_unknown_fields( errors: list[str], ) -> None: for key in sorted(set(payload) - allowed_keys): - errors.append(f"plugin.json field `{prefix}.{key}` is not accepted by plugin validation") + errors.append( + f"plugin.json field `{prefix}.{key}` is not accepted by plugin validation" + ) def validate_optional_https_url( @@ -275,7 +279,9 @@ def validate_optional_https_url( return parsed = urlparse(value) if isinstance(value, str) else None if parsed is None or parsed.scheme != "https" or not parsed.netloc: - errors.append(f"plugin.json field `{prefix}.{key}` must be an absolute `https://` URL") + errors.append( + f"plugin.json field `{prefix}.{key}` must be an absolute `https://` URL" + ) def validate_optional_contract_path( @@ -344,9 +350,13 @@ def validate_app_manifest(path: Path, errors: list[str]) -> None: ) app_id = value.get("id") if not isinstance(app_id, str) or not app_id.strip(): - errors.append(f"`.app.json` app `{key}` field `id` must be a non-empty string") + errors.append( + f"`.app.json` app `{key}` field `id` must be a non-empty string" + ) category = value.get("category") - if category is not None and (not isinstance(category, str) or not category.strip()): + if category is not None and ( + not isinstance(category, str) or not category.strip() + ): errors.append( f"`.app.json` app `{key}` field `category` must be a non-empty string" ) @@ -448,7 +458,9 @@ def validate_skill_manifest(skill_root: Path, errors: list[str]) -> None: return skill_name = frontmatter.get("name") if not isinstance(skill_name, str) or not skill_name.strip(): - errors.append(f"skill `{skill_root.name}` frontmatter field `name` must be non-empty") + errors.append( + f"skill `{skill_root.name}` frontmatter field `name` must be non-empty" + ) description = frontmatter.get("description") if not isinstance(description, str) or not description.strip(): errors.append( @@ -498,7 +510,9 @@ def validate_skill_agent_manifest( ) interface = payload.get("interface") if not isinstance(interface, dict): - errors.append(f"skill `{skill_root.name}` agent field `interface` must be an object") + errors.append( + f"skill `{skill_root.name}` agent field `interface` must be an object" + ) return reject_skill_agent_unknown_fields( interface, @@ -547,7 +561,9 @@ def validate_skill_agent_manifest( policy = payload.get("policy") if policy is not None: if not isinstance(policy, dict): - errors.append(f"skill `{skill_root.name}` agent field `policy` must be an object") + errors.append( + f"skill `{skill_root.name}` agent field `policy` must be an object" + ) else: reject_skill_agent_unknown_fields( policy, @@ -624,7 +640,9 @@ def validate_asset_path( errors.append(f"{label} must be a non-empty relative path") return candidate = PurePosixPath(raw_path.replace("\\", "/")) - if candidate.is_absolute() or any(part in {"", ".", ".."} for part in candidate.parts): + if candidate.is_absolute() or any( + part in {"", ".", ".."} for part in candidate.parts + ): errors.append(f"{label} must stay inside the plugin archive") return resolved_path = (base_dir / candidate.as_posix()).resolve() diff --git a/codex-rs/skills/src/assets/samples/skill-creator/scripts/generate_openai_yaml.py b/codex-rs/skills/src/assets/samples/skill-creator/scripts/generate_openai_yaml.py index 3fd7405345..870eefcea9 100644 --- a/codex-rs/skills/src/assets/samples/skill-creator/scripts/generate_openai_yaml.py +++ b/codex-rs/skills/src/assets/samples/skill-creator/scripts/generate_openai_yaml.py @@ -148,7 +148,10 @@ def parse_interface_overrides(raw_overrides): print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}") return None, None overrides[key] = value - if key not in ("display_name", "short_description") and key not in optional_order: + if ( + key not in ("display_name", "short_description") + and key not in optional_order + ): optional_order.append(key) return overrides, optional_order @@ -159,7 +162,9 @@ def write_openai_yaml(skill_dir, skill_name, raw_overrides): return None display_name = overrides.get("display_name") or format_display_name(skill_name) - short_description = overrides.get("short_description") or generate_short_description(display_name) + short_description = overrides.get( + "short_description" + ) or generate_short_description(display_name) if not (25 <= len(short_description) <= 64): print( diff --git a/codex-rs/skills/src/assets/samples/skill-creator/scripts/init_skill.py b/codex-rs/skills/src/assets/samples/skill-creator/scripts/init_skill.py index 559e2caea5..2ed2fa3125 100644 --- a/codex-rs/skills/src/assets/samples/skill-creator/scripts/init_skill.py +++ b/codex-rs/skills/src/assets/samples/skill-creator/scripts/init_skill.py @@ -123,7 +123,9 @@ def parse_resources(raw_resources): return deduped -def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples): +def create_resource_dirs( + skill_dir, skill_name, skill_title, resources, include_examples +): for resource in resources: resource_dir = skill_dir / resource resource_dir.mkdir(exist_ok=True) @@ -138,7 +140,9 @@ def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_ elif resource == "references": if include_examples: example_reference = resource_dir / "api_reference.md" - example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) + example_reference.write_text( + EXAMPLE_REFERENCE.format(skill_title=skill_title) + ) print("[OK] Created references/api_reference.md") else: print("[OK] Created references/") @@ -182,7 +186,9 @@ def init_skill(skill_name, path, resources, include_examples, interface_override # Create SKILL.md from template skill_title = title_case_skill_name(skill_name) - skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title) + skill_content = SKILL_TEMPLATE.format( + skill_name=skill_name, skill_title=skill_title + ) skill_md_path = skill_dir / "SKILL.md" try: @@ -204,7 +210,9 @@ def init_skill(skill_name, path, resources, include_examples, interface_override # Create resource directories if requested if resources: try: - create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples) + create_resource_dirs( + skill_dir, skill_name, skill_title, resources, include_examples + ) except Exception as e: print(f"[ERROR] Error creating resource directories: {e}") return None @@ -215,14 +223,20 @@ def init_skill(skill_name, path, resources, include_examples, interface_override print("1. Edit SKILL.md to complete the TODO items and update the description") if resources: if include_examples: - print("2. Customize or delete the example files in scripts/, references/, and assets/") + print( + "2. Customize or delete the example files in scripts/, references/, and assets/" + ) else: print("2. Add resources to scripts/, references/, and assets/ as needed") else: - print("2. Create resource directories only if needed (scripts/, references/, assets/)") + print( + "2. Create resource directories only if needed (scripts/, references/, assets/)" + ) print("3. Update agents/openai.yaml if the UI metadata should differ") print("4. Run the validator when ready to check the skill structure") - print("5. Consider independent forward-testing only when complexity or risk warrants it") + print( + "5. Consider independent forward-testing only when complexity or risk warrants it" + ) return skill_dir diff --git a/codex-rs/skills/src/assets/samples/skill-creator/scripts/quick_validate.py b/codex-rs/skills/src/assets/samples/skill-creator/scripts/quick_validate.py index 877c9b384a..e27023ece4 100644 --- a/codex-rs/skills/src/assets/samples/skill-creator/scripts/quick_validate.py +++ b/codex-rs/skills/src/assets/samples/skill-creator/scripts/quick_validate.py @@ -109,7 +109,9 @@ def validate_skill(skill_path): fence_length = 0 continue - if fence_marker is None and re.fullmatch(r"[ ]{0,3}\[TODO:[^\n]*\][ \t]*", line): + if fence_marker is None and re.fullmatch( + r"[ ]{0,3}\[TODO:[^\n]*\][ \t]*", line + ): return False, "Skill instructions contain an unfinished TODO placeholder" return True, "Skill is valid!" diff --git a/codex-rs/skills/src/assets/samples/skill-installer/scripts/install-skill-from-github.py b/codex-rs/skills/src/assets/samples/skill-installer/scripts/install-skill-from-github.py index b7b8e6e8d4..2d05f74924 100755 --- a/codex-rs/skills/src/assets/samples/skill-installer/scripts/install-skill-from-github.py +++ b/codex-rs/skills/src/assets/samples/skill-installer/scripts/install-skill-from-github.py @@ -15,6 +15,7 @@ import urllib.parse import zipfile from github_utils import github_request + DEFAULT_REF = "main" @@ -97,7 +98,9 @@ def _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str: def _run_git(args: list[str]) -> None: - result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + result = subprocess.run( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) if result.returncode != 0: raise InstallError(result.stderr.strip() or "Git command failed.") @@ -125,7 +128,9 @@ def _validate_skill_name(name: str) -> None: raise InstallError("Invalid skill name.") -def _git_sparse_checkout(repo_url: str, ref: str, paths: list[str], dest_dir: str) -> str: +def _git_sparse_checkout( + repo_url: str, ref: str, paths: list[str], dest_dir: str +) -> str: repo_dir = os.path.join(dest_dir, "repo") clone_cmd = [ "git", @@ -165,7 +170,10 @@ def _validate_skill(path: str, repo_root: str) -> None: resolved_repo_root = os.path.realpath(repo_root) resolved_path = os.path.realpath(path) try: - inside_repo = os.path.commonpath([resolved_repo_root, resolved_path]) == resolved_repo_root + inside_repo = ( + os.path.commonpath([resolved_repo_root, resolved_path]) + == resolved_repo_root + ) except ValueError: inside_repo = False if not inside_repo: @@ -194,12 +202,15 @@ def _validate_skill(path: str, repo_root: str) -> None: resolved_entry = os.path.realpath(entry_path) try: inside_skill = ( - os.path.commonpath([resolved_path, resolved_entry]) == resolved_path + os.path.commonpath([resolved_path, resolved_entry]) + == resolved_path ) except ValueError: inside_skill = False if not inside_skill or not os.path.isfile(resolved_entry): - raise InstallError(f"Unsupported symbolic link in skill: {relative_entry}") + raise InstallError( + f"Unsupported symbolic link in skill: {relative_entry}" + ) continue if not os.path.isdir(entry_path) and not os.path.isfile(entry_path): raise InstallError(f"Unsupported file type in skill: {relative_entry}") diff --git a/codex-rs/skills/src/assets/samples/skill-installer/scripts/list-skills.py b/codex-rs/skills/src/assets/samples/skill-installer/scripts/list-skills.py index 0977c296ab..0015571c5a 100755 --- a/codex-rs/skills/src/assets/samples/skill-installer/scripts/list-skills.py +++ b/codex-rs/skills/src/assets/samples/skill-installer/scripts/list-skills.py @@ -54,8 +54,7 @@ def _list_skills(repo: str, path: str, ref: str) -> list[str]: except urllib.error.HTTPError as exc: if exc.code == 404: raise ListError( - "Skills path not found: " - f"https://github.com/{repo}/tree/{ref}/{path}" + f"Skills path not found: https://github.com/{repo}/tree/{ref}/{path}" ) from exc raise ListError(f"Failed to fetch skills: HTTP {exc.code}") from exc data = json.loads(payload.decode("utf-8")) diff --git a/codex-rs/skills/tests/test_plugin_creator.py b/codex-rs/skills/tests/test_plugin_creator.py index 38b4f798d3..ea930e13b9 100644 --- a/codex-rs/skills/tests/test_plugin_creator.py +++ b/codex-rs/skills/tests/test_plugin_creator.py @@ -33,7 +33,9 @@ class PluginCreatorSecurityTests(unittest.TestCase): self.marketplace_path = self.root / "marketplace.json" self.plugin_root = self.root / "plugins" / "demo" - def run_script(self, script: str, *arguments: str) -> subprocess.CompletedProcess[str]: + def run_script( + self, script: str, *arguments: str + ) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, "-B", str(SCRIPTS / script), *arguments], capture_output=True, @@ -65,7 +67,9 @@ class PluginCreatorSecurityTests(unittest.TestCase): scaffold = runpy.run_path(str(SCRIPTS / "create_basic_plugin.py")) validator = runpy.run_path(str(SCRIPTS / "validate_plugin.py")) - manifest = scaffold["build_plugin_json"]("demo", with_mcp=False, with_apps=False) + manifest = scaffold["build_plugin_json"]( + "demo", with_mcp=False, with_apps=False + ) manifest["name"] = name errors: list[str] = [] validator["validate_manifest_shape"](self.plugin_root, manifest, errors) @@ -163,7 +167,9 @@ class PluginCreatorSecurityTests(unittest.TestCase): with self.subTest(name=name): manifest_path = self.write_plugin(name) original = manifest_path.read_bytes() - result = self.run_script("update_plugin_cachebuster.py", str(self.plugin_root)) + result = self.run_script( + "update_plugin_cachebuster.py", str(self.plugin_root) + ) self.assertNotEqual(result.returncode, 0) self.assertEqual(result.stdout, "") self.assertEqual(manifest_path.read_bytes(), original) @@ -206,7 +212,12 @@ class PluginCreatorSecurityTests(unittest.TestCase): app_manifest.write_text('{"apps":{"existing":{}}}', encoding="utf-8") originals = { path: path.read_bytes() - for path in (self.marketplace_path, plugin_manifest, mcp_manifest, app_manifest) + for path in ( + self.marketplace_path, + plugin_manifest, + mcp_manifest, + app_manifest, + ) } result = self.run_script( @@ -241,7 +252,12 @@ class PluginCreatorSecurityTests(unittest.TestCase): app_manifest.write_text('{"apps":{"existing":{}}}', encoding="utf-8") originals = { path: path.read_bytes() - for path in (self.marketplace_path, plugin_manifest, mcp_manifest, app_manifest) + for path in ( + self.marketplace_path, + plugin_manifest, + mcp_manifest, + app_manifest, + ) } result = self.run_script( @@ -264,7 +280,9 @@ class PluginCreatorSecurityTests(unittest.TestCase): with self.subTest(path=path): self.assertEqual(path.read_bytes(), original) - def test_scaffold_rejects_duplicate_marketplace_entry_before_creating_files(self) -> None: + def test_scaffold_rejects_duplicate_marketplace_entry_before_creating_files( + self, + ) -> None: self.marketplace_path.write_text( json.dumps({"name": "team-local", "plugins": [{"name": "demo"}]}), encoding="utf-8", diff --git a/codex-rs/windows-sandbox-rs/sandbox_smoketests.py b/codex-rs/windows-sandbox-rs/sandbox_smoketests.py index f6689587f5..a1553e5e54 100644 --- a/codex-rs/windows-sandbox-rs/sandbox_smoketests.py +++ b/codex-rs/windows-sandbox-rs/sandbox_smoketests.py @@ -14,6 +14,7 @@ from pathlib import Path from typing import List, Optional, Tuple from urllib.parse import urlsplit + def _resolve_codex_cmd() -> List[str]: """Resolve the Codex CLI to invoke `codex sandbox windows`. @@ -30,10 +31,12 @@ def _resolve_codex_cmd() -> List[str]: ] if cargo_target: cargo_base = Path(cargo_target) - candidates.extend([ - cargo_base / "debug" / "codex.exe", - cargo_base / "release" / "codex.exe", - ]) + candidates.extend( + [ + cargo_base / "debug" / "codex.exe", + cargo_base / "release" / "codex.exe", + ] + ) for candidate in candidates: if candidate.exists(): @@ -49,20 +52,27 @@ def _resolve_codex_cmd() -> List[str]: " cargo build -p codex-cli\n" ) + CODEX_CMD = _resolve_codex_cmd() print(CODEX_CMD) TIMEOUT_SEC = 20 WS_ROOT = Path(os.environ["USERPROFILE"]) / "sbx_ws_tests" -OUTSIDE = Path(os.environ["USERPROFILE"]) / "sbx_ws_outside" # outside CWD for deny checks -EXTRA_ROOT = Path(os.environ["USERPROFILE"]) / "WorkspaceRoot" # additional writable root +OUTSIDE = ( + Path(os.environ["USERPROFILE"]) / "sbx_ws_outside" +) # outside CWD for deny checks +EXTRA_ROOT = ( + Path(os.environ["USERPROFILE"]) / "WorkspaceRoot" +) # additional writable root ENV_BASE = {} # extend if needed + class CaseResult: def __init__(self, name: str, ok: bool, detail: str = ""): self.name, self.ok, self.detail = name, ok, detail + def run_sbx( policy: str, cmd_argv: List[str], @@ -90,38 +100,61 @@ def run_sbx( f'sandbox_workspace_write.writable_roots=["{additional_root.as_posix()}"]', ] - argv = [*CODEX_CMD, "sandbox", "windows", *policy_flags, *overrides, "--", *cmd_argv] + argv = [ + *CODEX_CMD, + "sandbox", + "windows", + *policy_flags, + *overrides, + "--", + *cmd_argv, + ] print(cmd_argv) - cp = subprocess.run(argv, cwd=str(cwd), env=env, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - timeout=TIMEOUT_SEC, text=True) + cp = subprocess.run( + argv, + cwd=str(cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=TIMEOUT_SEC, + text=True, + ) return cp.returncode, cp.stdout, cp.stderr + def have(cmd: str) -> bool: return shutil.which(cmd) is not None + def make_dir_clean(p: Path) -> None: if p.exists(): shutil.rmtree(p, ignore_errors=True) p.mkdir(parents=True, exist_ok=True) + def write_file(p: Path, content: str = "x") -> None: p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") + def remove_if_exists(p: Path) -> None: try: - if p.is_dir(): shutil.rmtree(p, ignore_errors=True) - elif p.exists(): p.unlink(missing_ok=True) + if p.is_dir(): + shutil.rmtree(p, ignore_errors=True) + elif p.exists(): + p.unlink(missing_ok=True) except Exception: pass + def assert_exists(p: Path) -> bool: return p.exists() + def assert_not_exists(p: Path) -> bool: return not p.exists() + def make_junction(link: Path, target: Path) -> bool: """Create a directory junction; return True if it exists afterward.""" remove_if_exists(link) @@ -130,6 +163,7 @@ def make_junction(link: Path, target: Path) -> bool: cp = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) return cp.returncode == 0 and link.exists() + def make_symlink(link: Path, target: Path) -> bool: """Create a directory symlink; return True if it exists afterward.""" remove_if_exists(link) @@ -142,10 +176,12 @@ def make_symlink(link: Path, target: Path) -> bool: cp = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) return cp.returncode == 0 and link.exists() + class _QuietHandler(http.server.BaseHTTPRequestHandler): def log_message(self, format, *args): pass + class _TargetHandler(_QuietHandler): def do_GET(self): body = b"proxy-ok" @@ -155,6 +191,7 @@ class _TargetHandler(_QuietHandler): self.end_headers() self.wfile.write(body) + class _ProxyHandler(_QuietHandler): def do_GET(self): parsed = urlsplit(self.path) @@ -169,7 +206,9 @@ class _ProxyHandler(_QuietHandler): path = f"{path}?{parsed.query}" conn = None try: - conn = http.client.HTTPConnection(parsed.hostname, parsed.port or 80, timeout=2) + conn = http.client.HTTPConnection( + parsed.hostname, parsed.port or 80, timeout=2 + ) conn.request("GET", path) upstream = conn.getresponse() body = upstream.read() @@ -186,6 +225,7 @@ class _ProxyHandler(_QuietHandler): self.end_headers() self.wfile.write(body) + @contextlib.contextmanager def start_loopback_proxy_fixture(): target = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _TargetHandler) @@ -204,16 +244,21 @@ def start_loopback_proxy_fixture(): proxy.server_close() target.server_close() + def summarize(results: List[CaseResult]) -> int: ok = sum(1 for r in results if r.ok) total = len(results) print("\n" + "=" * 72) print(f"Sandbox smoke tests: {ok}/{total} passed") for r in results: - print(f"[{'PASS' if r.ok else 'FAIL'}] {r.name}" + (f" :: {r.detail.strip()}" if r.detail and not r.ok else "")) + print( + f"[{'PASS' if r.ok else 'FAIL'}] {r.name}" + + (f" :: {r.detail.strip()}" if r.detail and not r.ok else "") + ) print("=" * 72) return 0 if ok == total else 1 + def main() -> int: results: List[CaseResult] = [] make_dir_clean(WS_ROOT) @@ -229,26 +274,44 @@ def main() -> int: ro_temp_denied = probe_rc != 0 def add(name: str, ok: bool, detail: str = ""): - print('running', name) + print("running", name) results.append(CaseResult(name, ok, detail)) # 1. RO: deny write in CWD target = WS_ROOT / "ro_should_fail.txt" remove_if_exists(target) - rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo nope > ro_should_fail.txt"], WS_ROOT) - add("RO: write in CWD denied", rc != 0 and assert_not_exists(target), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "read-only", ["cmd", "/c", "echo nope > ro_should_fail.txt"], WS_ROOT + ) + add( + "RO: write in CWD denied", + rc != 0 and assert_not_exists(target), + f"rc={rc}, err={err}", + ) # 2. WS: allow write in CWD target = WS_ROOT / "ws_ok.txt" remove_if_exists(target) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo ok > ws_ok.txt"], WS_ROOT) - add("WS: write in CWD allowed", rc == 0 and assert_exists(target), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo ok > ws_ok.txt"], WS_ROOT + ) + add( + "WS: write in CWD allowed", + rc == 0 and assert_exists(target), + f"rc={rc}, err={err}", + ) # 3. WS: deny write outside workspace outside_file = OUTSIDE / "blocked.txt" remove_if_exists(outside_file) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", f"echo nope > {outside_file}"], WS_ROOT) - add("WS: write outside workspace denied", rc != 0 and assert_not_exists(outside_file), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", f"echo nope > {outside_file}"], WS_ROOT + ) + add( + "WS: write outside workspace denied", + rc != 0 and assert_not_exists(outside_file), + f"rc={rc}", + ) # 3b. WS: allow write in additional workspace root extra_target = EXTRA_ROOT / "extra_ok.txt" @@ -259,7 +322,11 @@ def main() -> int: WS_ROOT, additional_root=EXTRA_ROOT, ) - add("WS: write in additional root allowed", rc == 0 and assert_exists(extra_target), f"rc={rc}, err={err}") + add( + "WS: write in additional root allowed", + rc == 0 and assert_exists(extra_target), + f"rc={rc}, err={err}", + ) # 3c. RO: deny write in additional workspace root ro_extra_target = EXTRA_ROOT / "extra_ro.txt" @@ -277,11 +344,17 @@ def main() -> int: ) # 4. WS: allow TEMP write - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo tempok > %TEMP%\\ws_temp_ok.txt"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo tempok > %TEMP%\\ws_temp_ok.txt"], + WS_ROOT, + ) add("WS: TEMP write allowed", rc == 0, f"rc={rc}") # 5. RO: deny TEMP write - rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo tempno > %TEMP%\\ro_temp_fail.txt"], WS_ROOT) + rc, out, err = run_sbx( + "read-only", ["cmd", "/c", "echo tempno > %TEMP%\\ro_temp_fail.txt"], WS_ROOT + ) if ro_temp_denied: add("RO: TEMP write denied", rc != 0, f"rc={rc}") else: @@ -289,62 +362,145 @@ def main() -> int: # 6. WS: append OK in CWD target = WS_ROOT / "append.txt" - remove_if_exists(target); write_file(target, "line1\n") - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo line2 >> append.txt"], WS_ROOT) - add("WS: append allowed", rc == 0 and target.read_text().strip().endswith("line2"), f"rc={rc}") + remove_if_exists(target) + write_file(target, "line1\n") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo line2 >> append.txt"], WS_ROOT + ) + add( + "WS: append allowed", + rc == 0 and target.read_text().strip().endswith("line2"), + f"rc={rc}", + ) # 7. RO: append denied target = WS_ROOT / "ro_append.txt" write_file(target, "line1\n") - rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo line2 >> ro_append.txt"], WS_ROOT) + rc, out, err = run_sbx( + "read-only", ["cmd", "/c", "echo line2 >> ro_append.txt"], WS_ROOT + ) add("RO: append denied", rc != 0 and target.read_text() == "line1\n", f"rc={rc}") # 8. WS: PowerShell Set-Content in CWD (OK) target = WS_ROOT / "ps_ok.txt" remove_if_exists(target) - rc, out, err = run_sbx("workspace-write", - ["powershell", "-NoLogo", "-NoProfile", "-Command", - "Set-Content -LiteralPath ps_ok.txt -Value 'hello' -Encoding ASCII"], WS_ROOT) - add("WS: PowerShell Set-Content allowed", rc == 0 and assert_exists(target), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "Set-Content -LiteralPath ps_ok.txt -Value 'hello' -Encoding ASCII", + ], + WS_ROOT, + ) + add( + "WS: PowerShell Set-Content allowed", + rc == 0 and assert_exists(target), + f"rc={rc}, err={err}", + ) # 9. RO: PowerShell Set-Content denied target = WS_ROOT / "ps_ro_fail.txt" remove_if_exists(target) - rc, out, err = run_sbx("read-only", - ["powershell", "-NoLogo", "-NoProfile", "-Command", - "Set-Content -LiteralPath ps_ro_fail.txt -Value 'x'"], WS_ROOT) - add("RO: PowerShell Set-Content denied", rc != 0 and assert_not_exists(target), f"rc={rc}") + rc, out, err = run_sbx( + "read-only", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "Set-Content -LiteralPath ps_ro_fail.txt -Value 'x'", + ], + WS_ROOT, + ) + add( + "RO: PowerShell Set-Content denied", + rc != 0 and assert_not_exists(target), + f"rc={rc}", + ) # 10. WS: mkdir and write (OK) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "mkdir sub && echo hi > sub\\in_sub.txt"], WS_ROOT) - add("WS: mkdir+write allowed", rc == 0 and (WS_ROOT / "sub/in_sub.txt").exists(), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "mkdir sub && echo hi > sub\\in_sub.txt"], + WS_ROOT, + ) + add( + "WS: mkdir+write allowed", + rc == 0 and (WS_ROOT / "sub/in_sub.txt").exists(), + f"rc={rc}", + ) # 11. WS: rename (EXPECTED SUCCESS on this host) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo x > r.txt & ren r.txt r2.txt"], WS_ROOT) - add("WS: rename succeeds (expected on this host)", rc == 0 and (WS_ROOT / "r2.txt").exists(), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo x > r.txt & ren r.txt r2.txt"], WS_ROOT + ) + add( + "WS: rename succeeds (expected on this host)", + rc == 0 and (WS_ROOT / "r2.txt").exists(), + f"rc={rc}, err={err}", + ) # 12. WS: delete (EXPECTED SUCCESS on this host) - target = WS_ROOT / "delme.txt"; write_file(target, "x") - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "del /q delme.txt"], WS_ROOT) - add("WS: delete succeeds (expected on this host)", rc == 0 and not target.exists(), f"rc={rc}, err={err}") + target = WS_ROOT / "delme.txt" + write_file(target, "x") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "del /q delme.txt"], WS_ROOT + ) + add( + "WS: delete succeeds (expected on this host)", + rc == 0 and not target.exists(), + f"rc={rc}, err={err}", + ) # 13. RO: python tries to write (denied) - pyfile = WS_ROOT / "py_should_fail.txt"; remove_if_exists(pyfile) - rc, out, err = run_sbx("read-only", ["python", "-c", "open('py_should_fail.txt','w').write('x')"], WS_ROOT) - add("RO: python file write denied", rc != 0 and assert_not_exists(pyfile), f"rc={rc}") + pyfile = WS_ROOT / "py_should_fail.txt" + remove_if_exists(pyfile) + rc, out, err = run_sbx( + "read-only", + ["python", "-c", "open('py_should_fail.txt','w').write('x')"], + WS_ROOT, + ) + add( + "RO: python file write denied", + rc != 0 and assert_not_exists(pyfile), + f"rc={rc}", + ) # 14. WS: python writes file (OK) - pyfile = WS_ROOT / "py_ok.txt"; remove_if_exists(pyfile) - rc, out, err = run_sbx("workspace-write", ["python", "-c", "open('py_ok.txt','w').write('x')"], WS_ROOT) - add("WS: python file write allowed", rc == 0 and assert_exists(pyfile), f"rc={rc}, err={err}") + pyfile = WS_ROOT / "py_ok.txt" + remove_if_exists(pyfile) + rc, out, err = run_sbx( + "workspace-write", ["python", "-c", "open('py_ok.txt','w').write('x')"], WS_ROOT + ) + add( + "WS: python file write allowed", + rc == 0 and assert_exists(pyfile), + f"rc={rc}, err={err}", + ) # 15. WS: curl network blocked (short timeout) - rc, out, err = run_sbx("workspace-write", ["curl", "--connect-timeout", "1", "--max-time", "2", "https://example.com"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["curl", "--connect-timeout", "1", "--max-time", "2", "https://example.com"], + WS_ROOT, + ) add("WS: curl network blocked", rc != 0, f"rc={rc}") # 16. WS: iwr network blocked (HTTP) - rc, out, err = run_sbx("workspace-write", ["powershell", "-NoLogo", "-NoProfile", "-Command", - "try { iwr http://neverssl.com -TimeoutSec 2 } catch { exit 1 }"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "try { iwr http://neverssl.com -TimeoutSec 2 } catch { exit 1 }", + ], + WS_ROOT, + ) add("WS: iwr network blocked", rc != 0, f"rc={rc}") # 17. WS: direct loopback blocked, proxy loopback allowed via env proxy @@ -401,14 +557,28 @@ def main() -> int: WS_ROOT, env_extra={"CODEX_HOME": str(proxy_home)}, ) - add("WS: direct loopback blocked", rc_direct != 0, f"rc={rc_direct}, err={err_direct}") + add( + "WS: direct loopback blocked", + rc_direct != 0, + f"rc={rc_direct}, err={err_direct}", + ) else: - add("WS: direct/proxy loopback tests (curl missing)", True, "curl not installed") + add( + "WS: direct/proxy loopback tests (curl missing)", True, "curl not installed" + ) # 18. RO: deny TEMP writes via PowerShell - rc, out, err = run_sbx("read-only", - ["powershell", "-NoLogo", "-NoProfile", "-Command", - "Set-Content -LiteralPath $env:TEMP\\ro_tmpfail.txt -Value 'x'"], WS_ROOT) + rc, out, err = run_sbx( + "read-only", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "Set-Content -LiteralPath $env:TEMP\\ro_tmpfail.txt -Value 'x'", + ], + WS_ROOT, + ) if ro_temp_denied: add("RO: TEMP write denied (PS)", rc != 0, f"rc={rc}") else: @@ -416,14 +586,18 @@ def main() -> int: # 19. WS: curl version check — don't rely on stub, just succeed if have("curl"): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "curl --version"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "curl --version"], WS_ROOT + ) add("WS: curl present (version prints)", rc == 0, f"rc={rc}, err={err}") else: add("WS: curl present (optional, skipped)", True) # 20. Optional: ripgrep version if have("rg"): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "rg --version"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "rg --version"], WS_ROOT + ) add("WS: rg --version (optional)", rc == 0, f"rc={rc}, err={err}") else: add("WS: rg --version (optional, skipped)", True) @@ -436,30 +610,71 @@ def main() -> int: add("WS: git --version (optional, skipped)", True) # 24. WS: PS bytes write (OK) - rc, out, err = run_sbx("workspace-write", - ["powershell", "-NoLogo", "-NoProfile", "-Command", - "[IO.File]::WriteAllBytes('bytes_ok.bin',[byte[]](0..255))"], WS_ROOT) - add("WS: PS bytes write allowed", rc == 0 and (WS_ROOT / "bytes_ok.bin").exists(), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "[IO.File]::WriteAllBytes('bytes_ok.bin',[byte[]](0..255))", + ], + WS_ROOT, + ) + add( + "WS: PS bytes write allowed", + rc == 0 and (WS_ROOT / "bytes_ok.bin").exists(), + f"rc={rc}", + ) # 25. RO: PS bytes write denied - rc, out, err = run_sbx("read-only", - ["powershell", "-NoLogo", "-NoProfile", "-Command", - "[IO.File]::WriteAllBytes('bytes_fail.bin',[byte[]](0..10))"], WS_ROOT) - add("RO: PS bytes write denied", rc != 0 and not (WS_ROOT / "bytes_fail.bin").exists(), f"rc={rc}") + rc, out, err = run_sbx( + "read-only", + [ + "powershell", + "-NoLogo", + "-NoProfile", + "-Command", + "[IO.File]::WriteAllBytes('bytes_fail.bin',[byte[]](0..10))", + ], + WS_ROOT, + ) + add( + "RO: PS bytes write denied", + rc != 0 and not (WS_ROOT / "bytes_fail.bin").exists(), + f"rc={rc}", + ) # 26. WS: deep mkdir and write (OK) - rc, out, err = run_sbx("workspace-write", - ["cmd", "/c", "mkdir deep\\nest && echo ok > deep\\nest\\f.txt"], WS_ROOT) - add("WS: deep mkdir+write allowed", rc == 0 and (WS_ROOT / "deep/nest/f.txt").exists(), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "mkdir deep\\nest && echo ok > deep\\nest\\f.txt"], + WS_ROOT, + ) + add( + "WS: deep mkdir+write allowed", + rc == 0 and (WS_ROOT / "deep/nest/f.txt").exists(), + f"rc={rc}", + ) # 27. WS: move (EXPECTED SUCCESS on this host) - rc, out, err = run_sbx("workspace-write", - ["cmd", "/c", "echo x > m1.txt & move /y m1.txt m2.txt"], WS_ROOT) - add("WS: move succeeds (expected on this host)", rc == 0 and (WS_ROOT / "m2.txt").exists(), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo x > m1.txt & move /y m1.txt m2.txt"], + WS_ROOT, + ) + add( + "WS: move succeeds (expected on this host)", + rc == 0 and (WS_ROOT / "m2.txt").exists(), + f"rc={rc}, err={err}", + ) # 28. RO: cmd redirection denied - target = WS_ROOT / "cmd_ro.txt"; remove_if_exists(target) - rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo nope > cmd_ro.txt"], WS_ROOT) + target = WS_ROOT / "cmd_ro.txt" + remove_if_exists(target) + rc, out, err = run_sbx( + "read-only", ["cmd", "/c", "echo nope > cmd_ro.txt"], WS_ROOT + ) add("RO: cmd redirection denied", rc != 0 and not target.exists(), f"rc={rc}") # 29. WS: CWD junction poisoning denied (allowlist should not follow to OUTSIDE) @@ -467,10 +682,20 @@ def main() -> int: if make_junction(poison_cwd, OUTSIDE): target = OUTSIDE / "poisoned.txt" remove_if_exists(target) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo poison > poisoned.txt"], poison_cwd) - add("WS: junction poisoning via CWD denied", rc != 0 and assert_not_exists(target), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo poison > poisoned.txt"], poison_cwd + ) + add( + "WS: junction poisoning via CWD denied", + rc != 0 and assert_not_exists(target), + f"rc={rc}, err={err}", + ) else: - add("WS: junction poisoning via CWD denied (setup skipped)", True, "junction creation failed") + add( + "WS: junction poisoning via CWD denied (setup skipped)", + True, + "junction creation failed", + ) # 30. WS: junction into Windows denied sys_link = WS_ROOT / "sys_link" @@ -479,44 +704,76 @@ def main() -> int: if sys_file.exists(): remove_if_exists(sys_file) if make_junction(sys_link, sys_target): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo bad > sys_link\\system32\\sbx_junc.txt"], WS_ROOT) - add("WS: junction into Windows denied", rc != 0 and not sys_file.exists(), f"rc={rc}, err={err}") + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo bad > sys_link\\system32\\sbx_junc.txt"], + WS_ROOT, + ) + add( + "WS: junction into Windows denied", + rc != 0 and not sys_file.exists(), + f"rc={rc}, err={err}", + ) else: - add("WS: junction into Windows denied (setup skipped)", True, "junction creation failed") + add( + "WS: junction into Windows denied (setup skipped)", + True, + "junction creation failed", + ) # 31. WS: device/pipe access blocked - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "type \\\\.\\PhysicalDrive0"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "type \\\\.\\PhysicalDrive0"], WS_ROOT + ) add("WS: raw device access denied", rc != 0, f"rc={rc}") - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo hi > \\\\.\\pipe\\codex_testpipe"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo hi > \\\\.\\pipe\\codex_testpipe"], + WS_ROOT, + ) add("WS: named pipe creation denied", rc != 0, f"rc={rc}") # 32. WS: ADS/long-path escape denied ads_base = WS_ROOT / "ads_base.txt" remove_if_exists(ads_base) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo secret > ads_base.txt:stream"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo secret > ads_base.txt:stream"], WS_ROOT + ) add("WS: ADS write denied", rc != 0 and assert_not_exists(ads_base), f"rc={rc}") lp_target = Path(r"\\?\C:\sbx_longpath_test.txt") - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo long > \\\\?\\C:\\sbx_longpath_test.txt"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo long > \\\\?\\C:\\sbx_longpath_test.txt"], + WS_ROOT, + ) add("WS: long-path escape denied", rc != 0 and not lp_target.exists(), f"rc={rc}") # 33. WS: case-insensitive protected path bypass denied (.GiT) git_variation = WS_ROOT / ".GiT" / "config" remove_if_exists(git_variation.parent) git_variation.parent.mkdir(exist_ok=True) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo hack > .GiT\\config"], WS_ROOT) - add("WS: protected path case-variation denied", rc != 0 and assert_not_exists(git_variation), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo hack > .GiT\\config"], WS_ROOT + ) + add( + "WS: protected path case-variation denied", + rc != 0 and assert_not_exists(git_variation), + f"rc={rc}", + ) # 34. WS: policy tamper (.codex artifacts) denied codex_home = Path(os.environ["USERPROFILE"]) / ".codex" cap_sid_target = codex_home / "cap_sid" rc, out, err = run_sbx( "workspace-write", - ["cmd", "/c", f"echo tamper > \"{cap_sid_target}\""], + ["cmd", "/c", f'echo tamper > "{cap_sid_target}"'], WS_ROOT, ) - rc2, out2, err2 = run_sbx("workspace-write", ["cmd", "/c", "echo tamper > .codex\\policy.json"], WS_ROOT) + rc2, out2, err2 = run_sbx( + "workspace-write", ["cmd", "/c", "echo tamper > .codex\\policy.json"], WS_ROOT + ) add("WS: .codex cap_sid tamper denied", rc != 0, f"rc={rc}, err={err}") add("WS: .codex policy tamper denied", rc2 != 0, f"rc={rc2}, err={err2}") @@ -531,7 +788,9 @@ def main() -> int: shim = tools_dir / "ssh.bat" shim.write_text("@echo off\r\necho stubbed\r\n", encoding="utf-8") env = {"PATH": f"{tools_dir};%PATH%"} - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "ssh"], WS_ROOT, env_extra=env) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "ssh"], WS_ROOT, env_extra=env + ) add("WS: PATH stub bypass denied", "stubbed" in out, f"rc={rc}, out={out}") else: add("WS: PATH stub bypass denied (ssh missing)", True, "ssh not installed") @@ -553,44 +812,84 @@ def main() -> int: "/c", f'for /L %i in (1,1,400) do (rmdir flip & mklink /D flip "{inside_abs}" >NUL & rmdir flip & mklink /D flip "{outside_abs}" >NUL)', ] - subprocess.Popen(toggle, cwd=str(race_root), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo race > flip\\race.txt"], race_root) - add("WS: symlink race write denied (best-effort)", rc != 0 and not (outside / "race.txt").exists(), f"rc={rc}") + subprocess.Popen( + toggle, cwd=str(race_root), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo race > flip\\race.txt"], race_root + ) + add( + "WS: symlink race write denied (best-effort)", + rc != 0 and not (outside / "race.txt").exists(), + f"rc={rc}", + ) # 37. WS: audit blind spots – deep junction/world-writable denied deep = WS_ROOT / "deep" / "redir" unsafe_dir = WS_ROOT / "deep" / "unsafe" make_junction(deep, Path("C:/Windows")) unsafe_dir.mkdir(parents=True, exist_ok=True) - subprocess.run(["icacls", str(unsafe_dir), "/grant", "Everyone:(F)"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo probe > deep\\redir\\system32\\audit_gap.txt"], WS_ROOT) - add("WS: deep junction/world-writable escape denied", rc != 0, f"rc={rc}, err={err}") + subprocess.run( + ["icacls", str(unsafe_dir), "/grant", "Everyone:(F)"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo probe > deep\\redir\\system32\\audit_gap.txt"], + WS_ROOT, + ) + add( + "WS: deep junction/world-writable escape denied", rc != 0, f"rc={rc}, err={err}" + ) # 38. WS: policy poisoning via workspace symlink root denied # Simulate workspace replaced by symlink to C:\; expect writes to be denied. fake_root = WS_ROOT / "fake_root" if make_symlink(fake_root, Path("C:/")): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo owned > codex_escape.txt"], fake_root) + rc, out, err = run_sbx( + "workspace-write", ["cmd", "/c", "echo owned > codex_escape.txt"], fake_root + ) add("WS: workspace-root symlink poisoning denied", rc != 0, f"rc={rc}") else: - add("WS: workspace-root symlink poisoning denied (setup skipped)", True, "symlink creation failed") + add( + "WS: workspace-root symlink poisoning denied (setup skipped)", + True, + "symlink creation failed", + ) # 39. WS: UNC/other-drive canonicalization denied unc_link = WS_ROOT / "unc_link" other_to = Path(r"\\\\localhost\\C$") if make_symlink(unc_link, other_to): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo unc > unc_link\\unc_test.txt"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo unc > unc_link\\unc_test.txt"], + WS_ROOT, + ) add("WS: UNC link escape denied", rc != 0, f"rc={rc}") else: - add("WS: UNC link escape denied (setup skipped)", True, "symlink creation failed") + add( + "WS: UNC link escape denied (setup skipped)", + True, + "symlink creation failed", + ) other_drive = WS_ROOT / "other_drive" other_target = Path("D:/") # best-effort; may not exist if make_symlink(other_drive, other_target): - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo drive > other_drive\\drive.txt"], WS_ROOT) + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", "echo drive > other_drive\\drive.txt"], + WS_ROOT, + ) add("WS: other-drive link escape denied", rc != 0, f"rc={rc}") else: - add("WS: other-drive link escape denied (setup skipped)", True, "symlink creation failed") + add( + "WS: other-drive link escape denied (setup skipped)", + True, + "symlink creation failed", + ) # 40. WS: timeout cleanup still denies outside write slow_ps = WS_ROOT / "sleep.ps1" @@ -601,8 +900,16 @@ def main() -> int: pass outside_after_timeout = OUTSIDE / "timeout_leak.txt" remove_if_exists(outside_after_timeout) - rc, out, err = run_sbx("workspace-write", ["cmd", "/c", f"echo leak > {outside_after_timeout}"], WS_ROOT) - add("WS: post-timeout outside write still denied", rc != 0 and assert_not_exists(outside_after_timeout), f"rc={rc}") + rc, out, err = run_sbx( + "workspace-write", + ["cmd", "/c", f"echo leak > {outside_after_timeout}"], + WS_ROOT, + ) + add( + "WS: post-timeout outside write still denied", + rc != 0 and assert_not_exists(outside_after_timeout), + f"rc={rc}", + ) # 41. RO: Start-Process https blocked (KNOWN FAIL until GUI escape fixed) rc, out, err = run_sbx( @@ -624,5 +931,6 @@ def main() -> int: return summarize(results) + if __name__ == "__main__": sys.exit(main()) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000..fc155aff18 --- /dev/null +++ b/ruff.toml @@ -0,0 +1 @@ +extend-exclude = ["sdk/python", "codex-rs/vendor"] diff --git a/scripts/format.py b/scripts/format.py index 9310254f52..9b408e1c7c 100644 --- a/scripts/format.py +++ b/scripts/format.py @@ -116,20 +116,10 @@ def python_sdk_formatter_group(*, check: bool) -> FormatterGroup: def python_scripts_formatter_group(*, check: bool) -> FormatterGroup: - # The SDK and internal scripts intentionally use separate project roots so - # uv and Ruff retain each project's configuration context. - args = [ - "uv", - "run", - "--frozen", - "--project", - "scripts", - "ruff", - "format", - ] + args = ["uv", "run", "--frozen", "--project", "scripts", "ruff", "format"] if check: args.append("--check") - args.append("scripts") + args.append(".") return FormatterGroup("Python scripts", (Command(tuple(args)),)) diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 459e32a545..936e85d179 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -272,11 +272,11 @@ def test_root_format_driver_covers_all_formatter_groups( ) assert [group.commands[-1].args[-3:] for group in formatters[3:]] == [ ("ruff", "format", "sdk/python"), - ("ruff", "format", "scripts"), + ("ruff", "format", "."), ] assert [group.commands[-1].args[-4:] for group in checks[3:]] == [ ("ruff", "format", "--check", "sdk/python"), - ("ruff", "format", "--check", "scripts"), + ("ruff", "format", "--check", "."), ] diff --git a/third_party/voice/assemble_package.py b/third_party/voice/assemble_package.py index 060da50384..74a4908552 100644 --- a/third_party/voice/assemble_package.py +++ b/third_party/voice/assemble_package.py @@ -11,17 +11,26 @@ import shutil def assemble(package: Path, helper: Path, voice_target: str, commit: str, output: Path): package, helper = package.resolve(strict=True), helper.resolve(strict=True) output = output.absolute() - if output.exists() or output.is_symlink() or output.resolve().is_relative_to(package): + if ( + output.exists() + or output.is_symlink() + or output.resolve().is_relative_to(package) + ): raise ValueError("output must be fresh and outside the input package") if not re.fullmatch(r"[0-9a-f]{40}", commit): - raise ValueError("a full build commit is required; dev builds are not distributable") + raise ValueError( + "a full build commit is required; dev builds are not distributable" + ) metadata = json.loads((package / "codex-package.json").read_text()) app_target = metadata["target"] targets = { f"{arch}-{suffix}": f"{arch}-{suffix.replace('musl', 'gnu')}" for arch in ("aarch64", "x86_64") for suffix in ( - "apple-darwin", "unknown-linux-gnu", "unknown-linux-musl", "pc-windows-msvc" + "apple-darwin", + "unknown-linux-gnu", + "unknown-linux-musl", + "pc-windows-msvc", ) } if targets.get(app_target) != voice_target: @@ -29,8 +38,11 @@ def assemble(package: Path, helper: Path, voice_target: str, commit: str, output suffix = ".exe" if app_target.endswith("windows-msvc") else "" entrypoint = f"bin/codex{suffix}" expected = { - "layoutVersion": 1, "variant": "codex", "entrypoint": entrypoint, - "resourcesDir": "codex-resources", "pathDir": "codex-path", + "layoutVersion": 1, + "variant": "codex", + "entrypoint": entrypoint, + "resourcesDir": "codex-resources", + "pathDir": "codex-path", } if any(metadata.get(key) != value for key, value in expected.items()): raise ValueError("input is not a canonical Codex package") @@ -57,8 +69,12 @@ def assemble(package: Path, helper: Path, voice_target: str, commit: str, output with (output / relative).open("rb") as source: digests[relative] = hashlib.file_digest(source, "sha256").hexdigest() manifest = { - "schemaVersion": 1, "buildCommit": commit, "appTarget": app_target, - "voiceTarget": voice_target, "appVersion": metadata["version"], "sha256": digests, + "schemaVersion": 1, + "buildCommit": commit, + "appTarget": app_target, + "voiceTarget": voice_target, + "appVersion": metadata["version"], + "sha256": digests, } (destination.parent.parent / "manifest.json").write_text( json.dumps(manifest, indent=2) + "\n", encoding="utf-8" @@ -76,4 +92,6 @@ if __name__ == "__main__": parser.add_argument("--build-commit", required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() - assemble(args.package, args.helper, args.voice_target, args.build_commit, args.output) + assemble( + args.package, args.helper, args.voice_target, args.build_commit, args.output + ) diff --git a/third_party/voice/test_assemble_package.py b/third_party/voice/test_assemble_package.py index 9a759eeadb..149a2138b6 100644 --- a/third_party/voice/test_assemble_package.py +++ b/third_party/voice/test_assemble_package.py @@ -21,9 +21,13 @@ class AssembleTests(unittest.TestCase): (self.package / "codex-path").mkdir() self.commit = "a" * 40 self.metadata = { - "layoutVersion": 1, "version": f"0.0.0+{self.commit}", - "target": "aarch64-unknown-linux-musl", "variant": "codex", - "entrypoint": "bin/codex", "resourcesDir": "codex-resources", "pathDir": "codex-path", + "layoutVersion": 1, + "version": f"0.0.0+{self.commit}", + "target": "aarch64-unknown-linux-musl", + "variant": "codex", + "entrypoint": "bin/codex", + "resourcesDir": "codex-resources", + "pathDir": "codex-path", } (self.package / "codex-package.json").write_text(json.dumps(self.metadata)) (self.package / "bin/codex").write_bytes(b"unchanged app") @@ -33,7 +37,13 @@ class AssembleTests(unittest.TestCase): self.output = self.root / "installed copy" def test_copies_app_unchanged_and_records_distinct_linux_targets(self): - assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, self.output) + assemble( + self.package, + self.helper, + "aarch64-unknown-linux-gnu", + self.commit, + self.output, + ) self.assertEqual((self.output / "bin/codex").read_bytes(), b"unchanged app") self.assertEqual((self.package / "bin/codex").read_bytes(), b"unchanged app") self.assertFalse((self.package / "codex-resources/voice").exists()) @@ -42,13 +52,20 @@ class AssembleTests(unittest.TestCase): (self.package / "codex-package.json").read_bytes(), ) self.assertEqual( - json.loads((self.output / "codex-resources/voice/manifest.json").read_text()), + json.loads( + (self.output / "codex-resources/voice/manifest.json").read_text() + ), { - "schemaVersion": 1, "buildCommit": self.commit, - "appTarget": self.metadata["target"], "voiceTarget": "aarch64-unknown-linux-gnu", - "appVersion": self.metadata["version"], "sha256": { + "schemaVersion": 1, + "buildCommit": self.commit, + "appTarget": self.metadata["target"], + "voiceTarget": "aarch64-unknown-linux-gnu", + "appVersion": self.metadata["version"], + "sha256": { "bin/codex": hashlib.sha256(b"unchanged app").hexdigest(), - "codex-resources/voice/bin/codex-voice-host": hashlib.sha256(b"private helper").hexdigest(), + "codex-resources/voice/bin/codex-voice-host": hashlib.sha256( + b"private helper" + ).hexdigest(), }, }, ) @@ -60,7 +77,10 @@ class AssembleTests(unittest.TestCase): ("aarch64-unknown-linux-gnu", "dev"), ("aarch64-unknown-linux-gnu", "b" * 40), ]: - with self.subTest(target=target, commit=commit), self.assertRaises(ValueError): + with ( + self.subTest(target=target, commit=commit), + self.assertRaises(ValueError), + ): assemble(self.package, self.helper, target, commit, self.output) self.assertFalse(self.output.exists()) @@ -69,38 +89,61 @@ class AssembleTests(unittest.TestCase): target = f"{architecture}-unknown-linux-gnu" with self.subTest(target=target): self.metadata["target"] = target - (self.package / "codex-package.json").write_text(json.dumps(self.metadata)) + (self.package / "codex-package.json").write_text( + json.dumps(self.metadata) + ) output = self.root / target assemble(self.package, self.helper, target, self.commit, output) - manifest = json.loads((output / "codex-resources/voice/manifest.json").read_text()) + manifest = json.loads( + (output / "codex-resources/voice/manifest.json").read_text() + ) self.assertEqual( manifest, { - "schemaVersion": 1, "buildCommit": self.commit, - "appTarget": target, "voiceTarget": target, - "appVersion": self.metadata["version"], "sha256": { + "schemaVersion": 1, + "buildCommit": self.commit, + "appTarget": target, + "voiceTarget": target, + "appVersion": self.metadata["version"], + "sha256": { "bin/codex": hashlib.sha256(b"unchanged app").hexdigest(), - "codex-resources/voice/bin/codex-voice-host": hashlib.sha256(b"private helper").hexdigest(), + "codex-resources/voice/bin/codex-voice-host": hashlib.sha256( + b"private helper" + ).hexdigest(), }, }, ) self.assertEqual((output / "bin/codex").read_bytes(), b"unchanged app") self.assertEqual( - (output / "codex-resources/voice/bin/codex-voice-host").read_bytes(), + ( + output / "codex-resources/voice/bin/codex-voice-host" + ).read_bytes(), self.helper.read_bytes(), ) def test_never_replaces_existing_or_nested_outputs(self): for output in (self.package, self.package / "nested", self.helper): with self.subTest(output=output), self.assertRaises(ValueError): - assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, output) + assemble( + self.package, + self.helper, + "aarch64-unknown-linux-gnu", + self.commit, + output, + ) self.assertEqual(self.helper.read_bytes(), b"private helper") self.assertFalse((self.package / "nested").exists()) def test_cleans_only_its_new_copy_on_failure(self): with patch("assemble_package.shutil.copy2", side_effect=OSError("copy failed")): with self.assertRaises(OSError): - assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, self.output) + assemble( + self.package, + self.helper, + "aarch64-unknown-linux-gnu", + self.commit, + self.output, + ) self.assertFalse(self.output.exists()) self.assertEqual((self.package / "bin/codex").read_bytes(), b"unchanged app") diff --git a/third_party/voice/test_build_native.py b/third_party/voice/test_build_native.py index 1befd5e4f7..a0d1d54467 100644 --- a/third_party/voice/test_build_native.py +++ b/third_party/voice/test_build_native.py @@ -207,9 +207,10 @@ class NativeBuildTests(unittest.TestCase): with self.subTest(architecture=architecture): self.args.target = f"{architecture}-pc-windows-msvc" self.args.output = self.root / architecture - with patch( - "build_native.platform.system", return_value="Windows" - ), patch("build_native.platform.machine", return_value=architecture): + with ( + patch("build_native.platform.system", return_value="Windows"), + patch("build_native.platform.machine", return_value=architecture), + ): build = NativeBuild(self.args, self.environment) self.assertEqual( build.environment["USERPROFILE"], self.environment["USERPROFILE"] @@ -222,20 +223,23 @@ class NativeBuildTests(unittest.TestCase): "-ID:/private/include -LD:/private/lib -lffi\n" ) - with patch( - "build_native.prepare_sources", - side_effect=lambda *args: (build.output / "build").mkdir(), - ), patch.object(build, "cmake") as cmake, patch.object( - build, "meson" - ), patch.object( - build, "run", side_effect=record - ), patch.object( - build, - "posix_path", - side_effect=lambda path: "/cygdrive/d/" + path.name, - ), patch( - "build_native.subprocess.check_output", - return_value="/usr/share/automake-1.18\n", + with ( + patch( + "build_native.prepare_sources", + side_effect=lambda *args: (build.output / "build").mkdir(), + ), + patch.object(build, "cmake") as cmake, + patch.object(build, "meson"), + patch.object(build, "run", side_effect=record), + patch.object( + build, + "posix_path", + side_effect=lambda path: "/cygdrive/d/" + path.name, + ), + patch( + "build_native.subprocess.check_output", + return_value="/usr/share/automake-1.18\n", + ), ): build.build() opus_options = next( diff --git a/third_party/voice/test_prepare_sources.py b/third_party/voice/test_prepare_sources.py index ea011414b2..60edb72bad 100644 --- a/third_party/voice/test_prepare_sources.py +++ b/third_party/voice/test_prepare_sources.py @@ -80,6 +80,7 @@ class PreparationTests(unittest.TestCase): }, }, ) + def test_rejects_changed_archive_before_extracting(self): source = self.source([("fixture-1/file", tarfile.REGTYPE, b"original")]) (self.root / source["archive"]).write_bytes(b"changed") @@ -106,9 +107,7 @@ class PreparationTests(unittest.TestCase): ] ) with patch.object(preparation, "MAX_SOURCE_BYTES", 10): - with self.assertRaisesRegex( - ValueError, "Expanded source exceeds limits" - ): + with self.assertRaisesRegex(ValueError, "Expanded source exceeds limits"): self.prepare(source) self.assertFalse(self.output.exists()) diff --git a/tools/argument-comment-lint/run.py b/tools/argument-comment-lint/run.py index c6247c44b3..49d0417e47 100755 --- a/tools/argument-comment-lint/run.py +++ b/tools/argument-comment-lint/run.py @@ -27,7 +27,12 @@ def main() -> "Never": ensure_source_prerequisites(env) set_default_lint_env(env) - command = ["cargo", "dylint", "--path", str(root / "tools" / "argument-comment-lint")] + command = [ + "cargo", + "dylint", + "--path", + str(root / "tools" / "argument-comment-lint"), + ] if not parsed.has_library_selection: command.append("--all") command.extend(final_args) diff --git a/tools/argument-comment-lint/test_wrapper_common.py b/tools/argument-comment-lint/test_wrapper_common.py index 3998f2e41a..bab6e6f84d 100644 --- a/tools/argument-comment-lint/test_wrapper_common.py +++ b/tools/argument-comment-lint/test_wrapper_common.py @@ -14,7 +14,9 @@ import wrapper_common class WrapperCommonTest(unittest.TestCase): def test_defaults_to_workspace_and_all_targets(self) -> None: parsed = wrapper_common.parse_wrapper_args([]) - final_args = wrapper_common.build_final_args(parsed, Path("/repo/codex-rs/Cargo.toml")) + final_args = wrapper_common.build_final_args( + parsed, Path("/repo/codex-rs/Cargo.toml") + ) self.assertEqual( final_args, @@ -29,8 +31,12 @@ class WrapperCommonTest(unittest.TestCase): ) def test_forwarded_cargo_args_keep_single_separator(self) -> None: - parsed = wrapper_common.parse_wrapper_args(["-p", "codex-core", "--", "--tests"]) - final_args = wrapper_common.build_final_args(parsed, Path("/repo/codex-rs/Cargo.toml")) + parsed = wrapper_common.parse_wrapper_args( + ["-p", "codex-core", "--", "--tests"] + ) + final_args = wrapper_common.build_final_args( + parsed, Path("/repo/codex-rs/Cargo.toml") + ) self.assertEqual( final_args, @@ -47,7 +53,9 @@ class WrapperCommonTest(unittest.TestCase): def test_fix_does_not_add_all_targets(self) -> None: parsed = wrapper_common.parse_wrapper_args(["--fix", "-p", "codex-core"]) - final_args = wrapper_common.build_final_args(parsed, Path("/repo/codex-rs/Cargo.toml")) + final_args = wrapper_common.build_final_args( + parsed, Path("/repo/codex-rs/Cargo.toml") + ) self.assertEqual( final_args, @@ -72,7 +80,9 @@ class WrapperCommonTest(unittest.TestCase): "--bins", ] ) - final_args = wrapper_common.build_final_args(parsed, Path("/repo/codex-rs/Cargo.toml")) + final_args = wrapper_common.build_final_args( + parsed, Path("/repo/codex-rs/Cargo.toml") + ) self.assertEqual( final_args, @@ -93,7 +103,9 @@ class WrapperCommonTest(unittest.TestCase): "/tmp/custom/Cargo.toml", ] ) - final_args = wrapper_common.build_final_args(parsed, Path("/repo/codex-rs/Cargo.toml")) + final_args = wrapper_common.build_final_args( + parsed, Path("/repo/codex-rs/Cargo.toml") + ) self.assertEqual( final_args, diff --git a/tools/argument-comment-lint/wrapper_common.py b/tools/argument-comment-lint/wrapper_common.py index 2473029b8e..382ba7bcf1 100644 --- a/tools/argument-comment-lint/wrapper_common.py +++ b/tools/argument-comment-lint/wrapper_common.py @@ -31,9 +31,7 @@ _TARGET_SELECTION_ARGS = { } _TARGET_SELECTION_PREFIXES = ("--bin=", "--test=", "--example=", "--bench=") _TARGET_SELECTION_WITH_VALUE = {"--bin", "--test", "--example", "--bench"} -_NIGHTLY_LIBRARY_PATTERN = re.compile( - r"^(.+@nightly-[0-9]{4}-[0-9]{2}-[0-9]{2})-.+$" -) +_NIGHTLY_LIBRARY_PATTERN = re.compile(r"^(.+@nightly-[0-9]{4}-[0-9]{2}-[0-9]{2})-.+$") @dataclass @@ -153,7 +151,9 @@ def require_command(name: str, install_message: str | None = None) -> str: return executable -def run_capture(args: Sequence[str], env: MutableMapping[str, str] | None = None) -> str: +def run_capture( + args: Sequence[str], env: MutableMapping[str, str] | None = None +) -> str: try: completed = subprocess.run( list(args), @@ -230,14 +230,18 @@ def prefer_rustup_shims(env: MutableMapping[str, str]) -> None: env["RUSTUP_HOME"] = rustup_home -def fetch_packaged_entrypoint(dotslash_manifest: Path, env: MutableMapping[str, str]) -> Path: +def fetch_packaged_entrypoint( + dotslash_manifest: Path, env: MutableMapping[str, str] +) -> Path: require_command( "dotslash", "argument-comment-lint prebuilt wrapper requires dotslash.\n" "Install dotslash, or use:\n" " ./tools/argument-comment-lint/run.py ...", ) - entrypoint = run_capture(["dotslash", "--", "fetch", str(dotslash_manifest)], env=env) + entrypoint = run_capture( + ["dotslash", "--", "fetch", str(dotslash_manifest)], env=env + ) return Path(entrypoint).resolve()