mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Format Python scripts across the repository (#42109)
## Why The root Python formatting pass only covered `scripts/`, leaving Python utilities elsewhere in the repository outside `just fmt` and `just fmt-check`. ## What changed - Run the scripts Ruff formatter from the repository root. - Exclude `sdk/python`, which has its own formatter pass, and `codex-rs/vendor`. - Apply Ruff formatting to the newly covered Python files and update the formatter coverage test. GitOrigin-RevId: 73ac82b8bb9c713d589724f6c4f926399febb513
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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("-")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
+16
-5
@@ -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}")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
+2
-12
@@ -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)),))
|
||||
|
||||
|
||||
|
||||
@@ -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", "."),
|
||||
]
|
||||
|
||||
|
||||
|
||||
+26
-8
@@ -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
|
||||
)
|
||||
|
||||
+62
-19
@@ -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")
|
||||
|
||||
|
||||
+21
-17
@@ -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(
|
||||
|
||||
+2
-3
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user