From dedbc7b043f0989b6d7890dac82c85a3920a35e1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 17:57:28 +0000 Subject: [PATCH 01/11] fix(strix): retry transient visibility API failures Required Strix jobs aborted in the visibility step when a single unretried gh api call flaked. Retry timeout, 5xx, 429, and empty/non-boolean responses with short backoff, and keep 401/403/404 fail-closed so a missing or unauthorized repo is never treated as success. Co-authored-by: Seongho Bae --- .github/workflows/strix.yml | 15 +- CHANGELOG.md | 4 +- scripts/ci/strix_resolve_target_visibility.py | 215 +++++++++ tests/test_strix_resolve_target_visibility.py | 408 ++++++++++++++++++ 4 files changed, 626 insertions(+), 16 deletions(-) create mode 100644 scripts/ci/strix_resolve_target_visibility.py create mode 100644 tests/test_strix_resolve_target_visibility.py diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index f8c361b95..1a3d96ee2 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -185,6 +185,7 @@ jobs: test -f "$trusted_strix_source/scripts/ci/strix_quick_gate.sh" test -f "$trusted_strix_source/scripts/ci/test_strix_quick_gate.sh" test -f "$trusted_strix_source/scripts/ci/strix_required_workflow_smoke.sh" + test -f "$trusted_strix_source/scripts/ci/strix_resolve_target_visibility.py" { echo "TRUSTED_STRIX_SOURCE=$trusted_strix_source" echo "TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh" @@ -265,19 +266,7 @@ jobs: TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }} run: | set -euo pipefail - if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]]; then - echo "::error::Strix target repository must belong to ContextualWisdomLab." - exit 1 - fi - is_private="$(gh api "repos/${TARGET_REPOSITORY}" --jq '.private')" - case "$is_private" in - true | false) ;; - *) - echo "::error::Target repository visibility did not resolve to true or false." - exit 1 - ;; - esac - echo "is_private=$is_private" >>"$GITHUB_OUTPUT" + python3 "$TRUSTED_STRIX_SOURCE/scripts/ci/strix_resolve_target_visibility.py" - name: Materialize target workspace if: github.event_name != 'repository_dispatch' diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1aebf43..69b280a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,14 +28,12 @@ Semantic Versioning where the repository publishes a release. - Run the bounded fast-mlsirm repair heartbeat at minute 49 of every hour with one-dispatch scope and a two-hour same-head floor, without weakening true-parameter recovery, CPU/GPU parity, skipped-test, or Rust-ownership gates. - Use NVIDIA NIM `mistralai/mistral-small-4-119b-2603` with explicit high reasoning for scheduled repair and `nvidia/nemotron-3-nano-30b-a3b` for bounded helper work instead of GitHub Models in the write-capable autofix worker. - Apply one NUL-delimited exact-path and complete pre/post-worktree verification contract to both ordinary review repair and merge-conflict repair rather than relying on a visible post-model diff for the ordinary path. - -### Changed - - Avoided the expensive R/testthat failure-summary regular expression on marker-absent bounded logs by checking the required terminal marker first, while preserving fail-closed handling for incomplete or malformed failure evidence. ### Fixed - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. +- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, or empty/non-boolean response no longer aborts the scan before it starts. A real 401/403/404 on a missing or unauthorized repository stays fail-closed. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py new file mode 100644 index 000000000..e06061fd7 --- /dev/null +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Resolve Strix target-repository visibility with fail-closed retries. + +The required Strix job used a single ``gh api`` call. A transient GitHub API +flake (timeout, 5xx, 429, or an empty/non-boolean body) aborted the scan +before it started. This helper retries those transient failures a few times +with short backoff. A real 401/403/404 on a missing or unauthorized +repository stays fail-closed and is never treated as success. +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import time +from collections.abc import Callable +from pathlib import Path + +TARGET_REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") +HTTP_STATUS_RE = re.compile(r"\bHTTP[ /](\d{3})\b", re.IGNORECASE) +TOKEN_RE = re.compile(r"(gh[pousr]_[A-Za-z0-9]+|github_pat_[A-Za-z0-9_]+)") +PERMANENT_HTTP_STATUSES = frozenset({401, 403, 404}) +TRANSIENT_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) +TRANSIENT_MARKERS = ( + "timeout", + "timed out", + "connection reset", + "connection refused", + "connection timed out", + "context deadline exceeded", + "gateway timeout", + "i/o timeout", + "unexpected end of json", + "unexpected eof", + "temporary failure", + "server error", + "service unavailable", +) +DEFAULT_MAX_ATTEMPTS = 4 +DEFAULT_TIMEOUT_SECONDS = 20.0 +MAX_BACKOFF_SECONDS = 4.0 + + +class VisibilityResolutionError(RuntimeError): + """Fail-closed visibility lookup that must stop the Strix job.""" + + +class VisibilityCommandError(RuntimeError): + """A ``gh api`` invocation failed before a boolean visibility was parsed.""" + + +def scrub_sensitive_data(text: str) -> str: + """Redact GitHub token prefixes before they can reach job logs.""" + return TOKEN_RE.sub("", text) + + +def validate_target_repository(repository: str) -> str: + """Return a ContextualWisdomLab repository name or fail closed.""" + candidate = (repository or "").strip() + if not TARGET_REPOSITORY_RE.fullmatch(candidate): + raise VisibilityResolutionError( + "Strix target repository must belong to ContextualWisdomLab." + ) + return candidate + + +def parse_private_flag(raw: str | None) -> str | None: + """Return ``true`` or ``false`` when visibility is an exact boolean.""" + value = (raw or "").strip() + if value in {"true", "false"}: + return value + return None + + +def classify_gh_failure(message: str) -> str: + """Classify a ``gh api`` failure as permanent, transient, or unknown.""" + status_match = HTTP_STATUS_RE.search(message or "") + if status_match is not None: + status = int(status_match.group(1)) + if status in PERMANENT_HTTP_STATUSES: + return "permanent" + if status in TRANSIENT_HTTP_STATUSES: + return "transient" + return "unknown" + folded = (message or "").lower() + if any(marker in folded for marker in TRANSIENT_MARKERS): + return "transient" + return "unknown" + + +def run_gh_visibility( + repository: str, timeout: float = DEFAULT_TIMEOUT_SECONDS +) -> str: + """Return ``gh api`` stdout for ``repos/.private``.""" + argv = ["gh", "api", f"repos/{repository}", "--jq", ".private"] + try: + completed = subprocess.run( + argv, + capture_output=True, + text=True, + shell=False, + check=False, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + raise + except OSError as exc: + raise VisibilityCommandError( + scrub_sensitive_data(f"gh api could not start: {exc}") + ) from exc + if completed.returncode != 0: + detail = scrub_sensitive_data( + (completed.stderr or completed.stdout or "").strip() + ) + raise VisibilityCommandError( + detail or f"gh api exited {completed.returncode}" + ) + return completed.stdout + + +def backoff_seconds(attempt: int) -> float: + """Return a short exponential backoff capped for queue health.""" + return min(float(2 ** (attempt - 1)), MAX_BACKOFF_SECONDS) + + +def fetch_repository_visibility( + repository: str, + *, + run_gh: Callable[[str], str] | None = None, + sleep: Callable[[float], None] = time.sleep, + max_attempts: int = DEFAULT_MAX_ATTEMPTS, +) -> str: + """Return exact ``true``/``false`` visibility after bounded retries.""" + target = validate_target_repository(repository) + if max_attempts < 1: + raise VisibilityResolutionError( + "Visibility lookup requires at least one attempt." + ) + runner = run_gh or run_gh_visibility + last_error = "Target repository visibility did not resolve to true or false." + for attempt in range(1, max_attempts + 1): # pragma: no branch - last failure raises + kind = "transient" + try: + parsed = parse_private_flag(runner(target)) + except subprocess.TimeoutExpired as exc: + last_error = scrub_sensitive_data( + f"GitHub visibility lookup timed out: {exc}" + ) + except VisibilityCommandError as exc: + last_error = str(exc) + kind = classify_gh_failure(last_error) + if kind == "permanent": + raise VisibilityResolutionError( + "Target repository visibility lookup was denied or missing: " + f"{last_error}" + ) from exc + else: + if parsed is not None: + return parsed + last_error = ( + "Target repository visibility did not resolve to true or false." + ) + if attempt >= max_attempts or kind != "transient": + break + delay = backoff_seconds(attempt) + print( + "Transient GitHub visibility lookup failure on attempt " + f"{attempt}/{max_attempts}; retrying in {delay:g}s.", + file=sys.stderr, + ) + sleep(delay) + raise VisibilityResolutionError(last_error) + + +def write_is_private_output(path: Path, is_private: str) -> None: + """Append the exact visibility boolean to ``GITHUB_OUTPUT``.""" + with path.open("a", encoding="utf-8") as handle: + handle.write(f"is_private={is_private}\n") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse repository and GitHub-output destinations.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repository", + default=os.environ.get("TARGET_REPOSITORY", ""), + help="ContextualWisdomLab owner/name target repository", + ) + parser.add_argument( + "--github-output", + default=os.environ.get("GITHUB_OUTPUT", ""), + help="Path to the GitHub Actions GITHUB_OUTPUT file", + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Resolve visibility and write ``is_private`` or fail closed.""" + args = parse_args(argv) + try: + if not str(args.github_output or "").strip(): + raise VisibilityResolutionError("GITHUB_OUTPUT is unset.") + is_private = fetch_repository_visibility(str(args.repository)) + write_is_private_output(Path(args.github_output), is_private) + except VisibilityResolutionError as exc: + print(f"::error::{exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py new file mode 100644 index 000000000..dca4aab00 --- /dev/null +++ b/tests/test_strix_resolve_target_visibility.py @@ -0,0 +1,408 @@ +"""Prove Strix visibility lookup retries flakes and stays fail-closed.""" + +from __future__ import annotations + +import runpy +import subprocess +import sys +from pathlib import Path + +import pytest + +from scripts.ci import strix_resolve_target_visibility as visibility + + +REPO_ROOT = Path(__file__).resolve().parents[1] +STRIX_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "strix.yml" +NOEMA_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "noema-review.yml" + + +def _workflow_step(workflow: str, name: str) -> str: + """Return one named GitHub Actions step from a workflow document.""" + marker = f" - name: {name}\n" + start = workflow.index(marker) + try: + end = workflow.index("\n - name:", start + len(marker)) + except ValueError: + end = len(workflow) + return workflow[start:end] + + +class _ScriptedGh: + """Return scripted ``gh api`` outcomes in call order.""" + + def __init__(self, outcomes: list[object]) -> None: + self.outcomes = list(outcomes) + self.calls: list[str] = [] + + def __call__(self, repository: str) -> str: + self.calls.append(repository) + outcome = self.outcomes.pop(0) + if isinstance(outcome, BaseException): + raise outcome + return str(outcome) + + +def test_transient_visibility_api_failure_retries_then_succeeds() -> None: + """A 502 then a boolean visibility must retry and continue the scan.""" + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError("gh: HTTP 502: Bad Gateway"), + "true\n", + ] + ) + sleeps: list[float] = [] + + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/aFIPC", + run_gh=runner, + sleep=sleeps.append, + ) + == "true" + ) + assert runner.calls == ["ContextualWisdomLab/aFIPC"] * 2 + assert sleeps == [1.0] + + +def test_empty_and_non_boolean_visibility_retries_then_succeeds() -> None: + """Empty or non-boolean bodies are transient and must not abort the scan.""" + runner = _ScriptedGh(["", "null\n", "false"]) + sleeps: list[float] = [] + + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/kaefa", + run_gh=runner, + sleep=sleeps.append, + ) + == "false" + ) + assert runner.calls == ["ContextualWisdomLab/kaefa"] * 3 + assert sleeps == [1.0, 2.0] + + +def test_timeout_visibility_lookup_retries_then_succeeds() -> None: + """A GitHub API timeout is retried instead of failing the job in 1s.""" + runner = _ScriptedGh( + [ + subprocess.TimeoutExpired(["gh", "api"], 20), + "true", + ] + ) + sleeps: list[float] = [] + + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/.github", + run_gh=runner, + sleep=sleeps.append, + ) + == "true" + ) + assert sleeps == [1.0] + + +def test_permanent_invalid_visibility_fails_closed() -> None: + """Exhausted empty/non-boolean responses stay fail-closed.""" + runner = _ScriptedGh(["", "True", "1", "maybe"]) + sleeps: list[float] = [] + + with pytest.raises( + visibility.VisibilityResolutionError, + match="did not resolve to true or false", + ): + visibility.fetch_repository_visibility( + "ContextualWisdomLab/aFIPC", + run_gh=runner, + sleep=sleeps.append, + ) + assert runner.calls == ["ContextualWisdomLab/aFIPC"] * 4 + assert sleeps == [1.0, 2.0, 4.0] + + +def test_http_404_and_403_are_not_success_and_are_not_retried() -> None: + """A missing or unauthorized repository must fail closed immediately.""" + sleeps: list[float] = [] + missing = _ScriptedGh([visibility.VisibilityCommandError("gh: HTTP 404: Not Found")]) + denied = _ScriptedGh( + [visibility.VisibilityCommandError("gh: HTTP 403: Resource not accessible")] + ) + + with pytest.raises( + visibility.VisibilityResolutionError, + match="denied or missing: gh: HTTP 404", + ): + visibility.fetch_repository_visibility( + "ContextualWisdomLab/missing-repo", + run_gh=missing, + sleep=sleeps.append, + ) + with pytest.raises( + visibility.VisibilityResolutionError, + match="denied or missing: gh: HTTP 403", + ): + visibility.fetch_repository_visibility( + "ContextualWisdomLab/private-denied", + run_gh=denied, + sleep=sleeps.append, + ) + assert missing.calls == ["ContextualWisdomLab/missing-repo"] + assert denied.calls == ["ContextualWisdomLab/private-denied"] + assert sleeps == [] + + +def test_public_and_private_booleans_are_preserved() -> None: + """Exact public and private booleans must survive lookup unchanged.""" + public = _ScriptedGh(["false\n"]) + private = _ScriptedGh(["true"]) + + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/naruon", + run_gh=public, + sleep=lambda _delay: None, + ) + == "false" + ) + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/xtrmLLMBatchPython", + run_gh=private, + sleep=lambda _delay: None, + ) + == "true" + ) + + +def test_zero_attempts_fail_closed_without_calling_github() -> None: + """A non-positive retry budget must not invent a visibility boolean.""" + runner = _ScriptedGh(["true"]) + + with pytest.raises( + visibility.VisibilityResolutionError, + match="at least one attempt", + ): + visibility.fetch_repository_visibility( + "ContextualWisdomLab/aFIPC", + run_gh=runner, + sleep=lambda _delay: None, + max_attempts=0, + ) + assert runner.calls == [] + + +def test_invalid_repository_fails_closed_without_calling_github() -> None: + """Targets outside ContextualWisdomLab never become a visibility probe.""" + runner = _ScriptedGh(["true"]) + + with pytest.raises( + visibility.VisibilityResolutionError, + match="must belong to ContextualWisdomLab", + ): + visibility.fetch_repository_visibility( + "octocat/Hello-World", + run_gh=runner, + sleep=lambda _delay: None, + ) + assert runner.calls == [] + + +def test_unknown_gh_failure_fails_closed_without_retry() -> None: + """Unclassified API errors are not treated as a successful public repo.""" + runner = _ScriptedGh( + [visibility.VisibilityCommandError("gh: GraphQL: Field unknown")] + ) + sleeps: list[float] = [] + + with pytest.raises( + visibility.VisibilityResolutionError, + match="Field unknown", + ): + visibility.fetch_repository_visibility( + "ContextualWisdomLab/aFIPC", + run_gh=runner, + sleep=sleeps.append, + ) + assert runner.calls == ["ContextualWisdomLab/aFIPC"] + assert sleeps == [] + + +def test_classify_gh_failure_covers_http_and_marker_families() -> None: + """HTTP status and timeout/connection markers classify independently.""" + assert visibility.classify_gh_failure("gh: HTTP 401: Bad credentials") == "permanent" + assert visibility.classify_gh_failure("HTTP/429 Too Many Requests") == "transient" + assert visibility.classify_gh_failure("gh: HTTP 500: server error") == "transient" + assert visibility.classify_gh_failure("gh: HTTP 418: teapot") == "unknown" + assert visibility.classify_gh_failure("connection reset by peer") == "transient" + assert visibility.classify_gh_failure("unexpected end of JSON input") == "transient" + assert visibility.classify_gh_failure("") == "unknown" + assert visibility.parse_private_flag(None) is None + assert visibility.backoff_seconds(4) == 4.0 + with pytest.raises( + visibility.VisibilityResolutionError, + match="must belong to ContextualWisdomLab", + ): + visibility.validate_target_repository(" ") + + +def test_run_gh_visibility_success_timeout_oserror_and_nonzero( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The real ``gh api`` wrapper maps process outcomes to typed failures.""" + + def succeed(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + assert argv == ["gh", "api", "repos/ContextualWisdomLab/aFIPC", "--jq", ".private"] + assert kwargs["shell"] is False + return subprocess.CompletedProcess(argv, 0, stdout="false\n", stderr="") + + monkeypatch.setattr(visibility.subprocess, "run", succeed) + assert visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") == "false\n" + + def timeout(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + raise subprocess.TimeoutExpired(argv, kwargs["timeout"]) + + monkeypatch.setattr(visibility.subprocess, "run", timeout) + with pytest.raises(subprocess.TimeoutExpired): + visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + + def missing_binary( + argv: list[str], **kwargs: object + ) -> subprocess.CompletedProcess[str]: + raise FileNotFoundError("gh") + + monkeypatch.setattr(visibility.subprocess, "run", missing_binary) + with pytest.raises(visibility.VisibilityCommandError, match="could not start"): + visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + + def fail_empty(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess(argv, 2, stdout="", stderr="") + + monkeypatch.setattr(visibility.subprocess, "run", fail_empty) + with pytest.raises(visibility.VisibilityCommandError, match="exited 2"): + visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + + def fail_token(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess( + argv, + 1, + stdout="", + stderr="Authorization: ghs_secretvalue123", + ) + + monkeypatch.setattr(visibility.subprocess, "run", fail_token) + with pytest.raises(visibility.VisibilityCommandError, match=""): + visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + + +def test_cli_writes_visibility_and_fails_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The workflow entrypoint writes ``is_private`` or exits 1 fail-closed.""" + output = tmp_path / "github-output" + output.write_text("existing=1\n", encoding="utf-8") + monkeypatch.setattr( + visibility, + "fetch_repository_visibility", + lambda repository: "false" if repository.endswith("naruon") else "true", + ) + + assert ( + visibility.main( + [ + "--repository", + "ContextualWisdomLab/naruon", + "--github-output", + str(output), + ] + ) + == 0 + ) + assert output.read_text(encoding="utf-8") == "existing=1\nis_private=false\n" + + assert visibility.main(["--repository", "ContextualWisdomLab/naruon"]) == 1 + monkeypatch.setattr( + visibility, + "fetch_repository_visibility", + lambda _repository: (_ for _ in ()).throw( + visibility.VisibilityResolutionError("denied") + ), + ) + assert ( + visibility.main( + [ + "--repository", + "ContextualWisdomLab/naruon", + "--github-output", + str(output), + ] + ) + == 1 + ) + + +def test_cli_main_module_uses_environment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """``python3 strix_resolve_target_visibility.py`` reads workflow env vars.""" + output = tmp_path / "github-output" + monkeypatch.setenv("TARGET_REPOSITORY", "ContextualWisdomLab/aFIPC") + monkeypatch.setenv("GITHUB_OUTPUT", str(output)) + monkeypatch.setattr(sys, "argv", ["strix_resolve_target_visibility.py"]) + + def fake_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + assert argv[-1] == ".private" + return subprocess.CompletedProcess(argv, 0, stdout="true\n", stderr="") + + monkeypatch.setattr(subprocess, "run", fake_run) + + with pytest.raises(SystemExit) as excinfo: + runpy.run_path( + str(REPO_ROOT / "scripts" / "ci" / "strix_resolve_target_visibility.py"), + run_name="__main__", + ) + + assert excinfo.value.code == 0 + assert output.read_text(encoding="utf-8") == "is_private=true\n" + + +def test_strix_workflow_uses_helper_and_keeps_token_order() -> None: + """The required Strix job must call the helper with the existing token chain.""" + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + export_step = _workflow_step(workflow, "Export trusted Strix source paths") + visibility_step = _workflow_step(workflow, "Resolve target repository visibility") + + assert ( + 'test -f "$trusted_strix_source/scripts/ci/strix_resolve_target_visibility.py"' + in export_step + ) + assert ( + 'python3 "$TRUSTED_STRIX_SOURCE/scripts/ci/strix_resolve_target_visibility.py"' + in visibility_step + ) + assert ( + "GH_TOKEN: ${{ steps.target_app_token.outputs.token || " + "secrets.OPENCODE_APPROVE_TOKEN || github.token }}" + ) in visibility_step + assert "COPILOT_GITHUB_TOKEN" not in visibility_step + assert "COPILOT_GITHUB_TOKEN" not in workflow + assert 'is_private="$(gh api "repos/${TARGET_REPOSITORY}" --jq \'.private\')"' not in ( + workflow + ) + assert "STRIX_SCAN_MODE" not in visibility_step + assert "--require-hashes" not in visibility_step + assert "--no-deps" not in visibility_step + + +def test_noema_and_opencode_visibility_paths_stay_untouched() -> None: + """This slice must not change Noema or OpenCode review workflows.""" + noema = NOEMA_WORKFLOW.read_text(encoding="utf-8") + opencode = ( + REPO_ROOT / ".github" / "workflows" / "opencode-review.yml" + ).read_text(encoding="utf-8") + + assert 'is_private="$(gh api "repos/${TARGET_REPOSITORY}" --jq \'.private\')"' in ( + noema + ) + assert "strix_resolve_target_visibility.py" not in noema + assert "strix_resolve_target_visibility.py" not in opencode From f0dabff25942e40be669e805ddab5f443f30409f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:03:07 +0000 Subject: [PATCH 02/11] test(strix): isolate GITHUB_OUTPUT in visibility CLI contract The fail-closed CLI case must not inherit a runner GITHUB_OUTPUT path. On GitHub Actions that env is always set, so the previous assertion treated a missing --github-output as success. Co-authored-by: Seongho Bae --- tests/test_strix_resolve_target_visibility.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index dca4aab00..0258b9209 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -301,6 +301,8 @@ def test_cli_writes_visibility_and_fails_closed( """The workflow entrypoint writes ``is_private`` or exits 1 fail-closed.""" output = tmp_path / "github-output" output.write_text("existing=1\n", encoding="utf-8") + monkeypatch.delenv("GITHUB_OUTPUT", raising=False) + monkeypatch.delenv("TARGET_REPOSITORY", raising=False) monkeypatch.setattr( visibility, "fetch_repository_visibility", From 1d076f897c3f71ce393af064ec031e08eead9249 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 21:31:35 +0000 Subject: [PATCH 03/11] fix(strix): retry authenticated 403 rate-limit visibility Installation-budget HTTP 403 (API rate limit exceeded for installation ID) and secondary-rate-limit wording are transient quota exhaustion, not authorization or a missing repo. Retry that family with the existing bounded backoff. Ordinary 401/403/404 stay fail-closed. Exhausted quota remains a typed infrastructure failure and is never treated as a source finding. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- scripts/ci/strix_resolve_target_visibility.py | 27 ++++- tests/test_strix_resolve_target_visibility.py | 99 ++++++++++++++++++- 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b280a4d..2decfa907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. -- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, or empty/non-boolean response no longer aborts the scan before it starts. A real 401/403/404 on a missing or unauthorized repository stays fail-closed. +- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, empty/non-boolean response, or authenticated HTTP 403 rate-limit (installation budget / secondary rate limit) no longer aborts the scan before it starts. A real 401/403/404 on a missing or unauthorized repository stays fail-closed and is never treated as a source finding. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py index e06061fd7..5a86a0741 100644 --- a/scripts/ci/strix_resolve_target_visibility.py +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -2,10 +2,11 @@ """Resolve Strix target-repository visibility with fail-closed retries. The required Strix job used a single ``gh api`` call. A transient GitHub API -flake (timeout, 5xx, 429, or an empty/non-boolean body) aborted the scan -before it started. This helper retries those transient failures a few times -with short backoff. A real 401/403/404 on a missing or unauthorized -repository stays fail-closed and is never treated as success. +flake (timeout, 5xx, 429, empty/non-boolean body, or authenticated HTTP 403 +rate-limit) aborted the scan before it started. This helper retries those +transient failures a few times with short backoff. A real 401/403/404 on a +missing or unauthorized repository stays fail-closed and is never treated as +success or as a source finding. """ from __future__ import annotations @@ -24,6 +25,10 @@ TOKEN_RE = re.compile(r"(gh[pousr]_[A-Za-z0-9]+|github_pat_[A-Za-z0-9_]+)") PERMANENT_HTTP_STATUSES = frozenset({401, 403, 404}) TRANSIENT_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) +RATE_LIMIT_MARKERS = ( + "api rate limit exceeded", + "secondary rate limit", +) TRANSIENT_MARKERS = ( "timeout", "timed out", @@ -75,8 +80,16 @@ def parse_private_flag(raw: str | None) -> str | None: return None +def is_github_rate_limit_failure(message: str) -> bool: + """Return whether a GitHub error is authenticated quota exhaustion.""" + folded = (message or "").lower() + return any(marker in folded for marker in RATE_LIMIT_MARKERS) + + def classify_gh_failure(message: str) -> str: """Classify a ``gh api`` failure as permanent, transient, or unknown.""" + if is_github_rate_limit_failure(message): + return "transient" status_match = HTTP_STATUS_RE.search(message or "") if status_match is not None: status = int(status_match.group(1)) @@ -172,6 +185,12 @@ def fetch_repository_visibility( file=sys.stderr, ) sleep(delay) + if is_github_rate_limit_failure(last_error): + raise VisibilityResolutionError( + "Target repository visibility lookup hit a GitHub API rate-limit; " + "this is infrastructure, not a source finding: " + f"{last_error}" + ) raise VisibilityResolutionError(last_error) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index 0258b9209..06d5e8533 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -121,13 +121,24 @@ def test_permanent_invalid_visibility_fails_closed() -> None: assert sleeps == [1.0, 2.0, 4.0] +# Exact downstream wording from ContextualWisdomLab/inkspan#160 required +# Strix job 95492526891 (run 32064279893) at 2026-08-17 20:12:47 UTC. +INSTALLATION_RATE_LIMIT_403 = ( + "gh: HTTP 403: API rate limit exceeded for installation ID 141441800 " + "(https://api.github.com/repos/ContextualWisdomLab/inkspan)" +) +SECONDARY_RATE_LIMIT_403 = ( + "gh: HTTP 403: You have exceeded a secondary rate limit. " + "Please wait a few minutes before you try again." +) +AUTH_DENIED_403 = "gh: HTTP 403: Resource not accessible by integration" + + def test_http_404_and_403_are_not_success_and_are_not_retried() -> None: """A missing or unauthorized repository must fail closed immediately.""" sleeps: list[float] = [] missing = _ScriptedGh([visibility.VisibilityCommandError("gh: HTTP 404: Not Found")]) - denied = _ScriptedGh( - [visibility.VisibilityCommandError("gh: HTTP 403: Resource not accessible")] - ) + denied = _ScriptedGh([visibility.VisibilityCommandError(AUTH_DENIED_403)]) with pytest.raises( visibility.VisibilityResolutionError, @@ -140,7 +151,7 @@ def test_http_404_and_403_are_not_success_and_are_not_retried() -> None: ) with pytest.raises( visibility.VisibilityResolutionError, - match="denied or missing: gh: HTTP 403", + match="denied or missing: gh: HTTP 403: Resource not accessible", ): visibility.fetch_repository_visibility( "ContextualWisdomLab/private-denied", @@ -152,6 +163,86 @@ def test_http_404_and_403_are_not_success_and_are_not_retried() -> None: assert sleeps == [] +def test_installation_rate_limit_403_retries_then_preserves_visibility() -> None: + """Inkspan installation-budget 403 is transient, not an auth/missing repo.""" + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403), + "false\n", + ] + ) + sleeps: list[float] = [] + + assert visibility.classify_gh_failure(INSTALLATION_RATE_LIMIT_403) == "transient" + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + ) + == "false" + ) + assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 + assert sleeps == [1.0] + + +def test_secondary_rate_limit_403_retries_then_preserves_visibility() -> None: + """Secondary-rate-limit 403 wording is the same transient family.""" + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError(SECONDARY_RATE_LIMIT_403), + "true", + ] + ) + sleeps: list[float] = [] + + assert visibility.classify_gh_failure(SECONDARY_RATE_LIMIT_403) == "transient" + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + ) + == "true" + ) + assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 + assert sleeps == [1.0] + + +def test_exhausted_rate_limit_403_stays_typed_infrastructure_failure() -> None: + """Quota exhaustion must stay non-passing and must not look like a finding.""" + runner = _ScriptedGh( + [visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403)] * 4 + ) + sleeps: list[float] = [] + + with pytest.raises( + visibility.VisibilityResolutionError, + match="GitHub API rate-limit; this is infrastructure, not a source finding", + ) as excinfo: + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + ) + assert "installation ID 141441800" in str(excinfo.value) + assert "denied or missing" not in str(excinfo.value) + assert runner.calls == ["ContextualWisdomLab/inkspan"] * 4 + assert sleeps == [1.0, 2.0, 4.0] + + +def test_rate_limit_403_is_not_confused_with_authorization_403() -> None: + """Only the authenticated quota family is retryable; other 403s stay closed.""" + assert visibility.classify_gh_failure(INSTALLATION_RATE_LIMIT_403) == "transient" + assert visibility.classify_gh_failure(SECONDARY_RATE_LIMIT_403) == "transient" + assert visibility.classify_gh_failure(AUTH_DENIED_403) == "permanent" + assert ( + visibility.classify_gh_failure("gh: HTTP 403: API rate limit exceeded") + == "transient" + ) + assert visibility.classify_gh_failure("gh: HTTP 403: Forbidden") == "permanent" + + def test_public_and_private_booleans_are_preserved() -> None: """Exact public and private booleans must survive lookup unchanged.""" public = _ScriptedGh(["false\n"]) From be02bd51c53c8c8aa343024a8cec25a0d6e21d0e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 23:26:28 +0000 Subject: [PATCH 04/11] fix(strix): bound rate-limit visibility backoff separately from flakes Give authenticated 403 rate-limits a distinct 3-attempt budget and a 15-20s wait, honoring Retry-After / X-RateLimit-Reset when gh prints them. Keep generic flakes on 1/2/4s, arbitrary 403 fail-closed, and exhausted quota as typed infrastructure. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- scripts/ci/strix_resolve_target_visibility.py | 75 +++++++++-- tests/test_strix_resolve_target_visibility.py | 119 ++++++++++++++++-- 3 files changed, 173 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2decfa907..f9c0ab765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. -- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, empty/non-boolean response, or authenticated HTTP 403 rate-limit (installation budget / secondary rate limit) no longer aborts the scan before it starts. A real 401/403/404 on a missing or unauthorized repository stays fail-closed and is never treated as a source finding. +- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, empty/non-boolean response, or authenticated HTTP 403 rate-limit (installation budget / secondary rate limit) no longer aborts the scan before it starts. Rate-limit 403 uses a distinct 3-attempt budget and a 15–20s bounded wait (Retry-After / X-RateLimit-Reset when present, otherwise 15/20s). A real 401/403/404 on a missing or unauthorized repository stays fail-closed and is never treated as a source finding. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py index 5a86a0741..3780a3c2f 100644 --- a/scripts/ci/strix_resolve_target_visibility.py +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -3,10 +3,13 @@ The required Strix job used a single ``gh api`` call. A transient GitHub API flake (timeout, 5xx, 429, empty/non-boolean body, or authenticated HTTP 403 -rate-limit) aborted the scan before it started. This helper retries those -transient failures a few times with short backoff. A real 401/403/404 on a -missing or unauthorized repository stays fail-closed and is never treated as -success or as a source finding. +rate-limit) aborted the scan before it started. Generic flakes retry a few +times with short backoff. Authenticated HTTP 403 rate-limits use a shorter +attempt budget and a longer bounded wait, honoring Retry-After / +X-RateLimit-Reset from ``gh`` output when present and capping the sleep so +the job cannot stall. A real 401/403/404 on a missing or unauthorized +repository stays fail-closed and is never treated as success or as a source +finding. """ from __future__ import annotations @@ -23,6 +26,8 @@ TARGET_REPOSITORY_RE = re.compile(r"^ContextualWisdomLab/[A-Za-z0-9_.-]+$") HTTP_STATUS_RE = re.compile(r"\bHTTP[ /](\d{3})\b", re.IGNORECASE) TOKEN_RE = re.compile(r"(gh[pousr]_[A-Za-z0-9]+|github_pat_[A-Za-z0-9_]+)") +RETRY_AFTER_RE = re.compile(r"(?i)\bretry-after\s*[:=]\s*(\d+)\b") +RATE_LIMIT_RESET_RE = re.compile(r"(?i)\bx-ratelimit-reset\s*[:=]\s*(\d+)\b") PERMANENT_HTTP_STATUSES = frozenset({401, 403, 404}) TRANSIENT_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) RATE_LIMIT_MARKERS = ( @@ -45,8 +50,11 @@ "service unavailable", ) DEFAULT_MAX_ATTEMPTS = 4 +RATE_LIMIT_MAX_ATTEMPTS = 3 DEFAULT_TIMEOUT_SECONDS = 20.0 MAX_BACKOFF_SECONDS = 4.0 +RATE_LIMIT_BASE_BACKOFF_SECONDS = 15.0 +RATE_LIMIT_MAX_BACKOFF_SECONDS = 20.0 class VisibilityResolutionError(RuntimeError): @@ -118,8 +126,6 @@ def run_gh_visibility( check=False, timeout=timeout, ) - except subprocess.TimeoutExpired: - raise except OSError as exc: raise VisibilityCommandError( scrub_sensitive_data(f"gh api could not start: {exc}") @@ -139,11 +145,50 @@ def backoff_seconds(attempt: int) -> float: return min(float(2 ** (attempt - 1)), MAX_BACKOFF_SECONDS) +def parse_rate_limit_wait_seconds( + message: str, *, now: float | None = None +) -> float | None: + """Return a wait from Retry-After or X-RateLimit-Reset when present.""" + text = message or "" + retry_after = RETRY_AFTER_RE.search(text) + if retry_after is not None: + return float(retry_after.group(1)) + reset = RATE_LIMIT_RESET_RE.search(text) + if reset is None: + return None + current = time.time() if now is None else now + delay = float(reset.group(1)) - current + if delay <= 0: + return None + return delay + + +def rate_limit_backoff_seconds( + attempt: int, message: str = "", *, now: float | None = None +) -> float: + """Return a bounded rate-limit wait, honoring headers when present.""" + parsed = parse_rate_limit_wait_seconds(message, now=now) + if parsed is None: + parsed = min( + RATE_LIMIT_BASE_BACKOFF_SECONDS * float(2 ** (attempt - 1)), + RATE_LIMIT_MAX_BACKOFF_SECONDS, + ) + if parsed > RATE_LIMIT_MAX_BACKOFF_SECONDS: + print( + "GitHub visibility rate-limit retry sleep capped from " + f"{parsed:g} to {RATE_LIMIT_MAX_BACKOFF_SECONDS:g} seconds.", + file=sys.stderr, + ) + return RATE_LIMIT_MAX_BACKOFF_SECONDS + return parsed + + def fetch_repository_visibility( repository: str, *, run_gh: Callable[[str], str] | None = None, sleep: Callable[[float], None] = time.sleep, + now: Callable[[], float] = time.time, max_attempts: int = DEFAULT_MAX_ATTEMPTS, ) -> str: """Return exact ``true``/``false`` visibility after bounded retries.""" @@ -154,8 +199,10 @@ def fetch_repository_visibility( ) runner = run_gh or run_gh_visibility last_error = "Target repository visibility did not resolve to true or false." + attempt_limit = max_attempts for attempt in range(1, max_attempts + 1): # pragma: no branch - last failure raises kind = "transient" + rate_limited = False try: parsed = parse_private_flag(runner(target)) except subprocess.TimeoutExpired as exc: @@ -165,6 +212,7 @@ def fetch_repository_visibility( except VisibilityCommandError as exc: last_error = str(exc) kind = classify_gh_failure(last_error) + rate_limited = is_github_rate_limit_failure(last_error) if kind == "permanent": raise VisibilityResolutionError( "Target repository visibility lookup was denied or missing: " @@ -176,12 +224,19 @@ def fetch_repository_visibility( last_error = ( "Target repository visibility did not resolve to true or false." ) - if attempt >= max_attempts or kind != "transient": + if rate_limited: + attempt_limit = min(max_attempts, RATE_LIMIT_MAX_ATTEMPTS) + if attempt >= attempt_limit or kind != "transient": break - delay = backoff_seconds(attempt) + if rate_limited: + delay = rate_limit_backoff_seconds(attempt, last_error, now=now()) + label = "rate-limit" + else: + delay = backoff_seconds(attempt) + label = "transient" print( - "Transient GitHub visibility lookup failure on attempt " - f"{attempt}/{max_attempts}; retrying in {delay:g}s.", + f"{label.capitalize()} GitHub visibility lookup failure on attempt " + f"{attempt}/{attempt_limit}; retrying in {delay:g}s.", file=sys.stderr, ) sleep(delay) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index 06d5e8533..c114c0124 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -20,6 +20,8 @@ def _workflow_step(workflow: str, name: str) -> str: """Return one named GitHub Actions step from a workflow document.""" marker = f" - name: {name}\n" + if marker not in workflow: + raise AssertionError(f"workflow step not found: {name}") start = workflow.index(marker) try: end = workflow.index("\n - name:", start + len(marker)) @@ -183,7 +185,7 @@ def test_installation_rate_limit_403_retries_then_preserves_visibility() -> None == "false" ) assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 - assert sleeps == [1.0] + assert sleeps == [15.0] def test_secondary_rate_limit_403_retries_then_preserves_visibility() -> None: @@ -206,13 +208,13 @@ def test_secondary_rate_limit_403_retries_then_preserves_visibility() -> None: == "true" ) assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 - assert sleeps == [1.0] + assert sleeps == [15.0] def test_exhausted_rate_limit_403_stays_typed_infrastructure_failure() -> None: """Quota exhaustion must stay non-passing and must not look like a finding.""" runner = _ScriptedGh( - [visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403)] * 4 + [visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403)] * 3 ) sleeps: list[float] = [] @@ -227,8 +229,8 @@ def test_exhausted_rate_limit_403_stays_typed_infrastructure_failure() -> None: ) assert "installation ID 141441800" in str(excinfo.value) assert "denied or missing" not in str(excinfo.value) - assert runner.calls == ["ContextualWisdomLab/inkspan"] * 4 - assert sleeps == [1.0, 2.0, 4.0] + assert runner.calls == ["ContextualWisdomLab/inkspan"] * 3 + assert sleeps == [15.0, 20.0] def test_rate_limit_403_is_not_confused_with_authorization_403() -> None: @@ -243,6 +245,95 @@ def test_rate_limit_403_is_not_confused_with_authorization_403() -> None: assert visibility.classify_gh_failure("gh: HTTP 403: Forbidden") == "permanent" +def test_rate_limit_retry_after_is_honored_and_capped() -> None: + """Honor Retry-After from gh output, but never sleep an unbounded reset.""" + assert visibility.parse_rate_limit_wait_seconds("Retry-After: 12") == 12.0 + assert ( + visibility.parse_rate_limit_wait_seconds( + "X-RateLimit-Reset: 1700000010", + now=1_700_000_000.0, + ) + == 10.0 + ) + assert ( + visibility.rate_limit_backoff_seconds( + 1, + "gh: HTTP 403: API rate limit exceeded\nRetry-After: 12", + ) + == 12.0 + ) + assert ( + visibility.rate_limit_backoff_seconds( + 1, + "gh: HTTP 403: API rate limit exceeded\nRetry-After: 90", + ) + == 20.0 + ) + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError( + INSTALLATION_RATE_LIMIT_403 + "\nRetry-After: 12" + ), + "false", + ] + ) + sleeps: list[float] = [] + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + ) + == "false" + ) + assert sleeps == [12.0] + + +def test_rate_limit_reset_header_and_past_reset_are_bounded( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Honor X-RateLimit-Reset when present; a past reset uses the default wait.""" + monkeypatch.setattr(visibility.time, "time", lambda: 100.0) + assert visibility.parse_rate_limit_wait_seconds("X-RateLimit-Reset: 112") == 12.0 + assert visibility.parse_rate_limit_wait_seconds("X-RateLimit-Reset: 90") is None + assert visibility.parse_rate_limit_wait_seconds("") is None + assert ( + visibility.rate_limit_backoff_seconds( + 1, + INSTALLATION_RATE_LIMIT_403 + "\nX-RateLimit-Reset: 1", + now=8.0, + ) + == 15.0 + ) + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError( + INSTALLATION_RATE_LIMIT_403 + "\nX-RateLimit-Reset: 18" + ), + "false", + ] + ) + sleeps: list[float] = [] + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + now=lambda: 8.0, + ) + == "false" + ) + assert sleeps == [10.0] + + +def test_generic_transient_backoff_stays_short() -> None: + """A 502 flake must keep the original 1/2/4s schedule.""" + assert visibility.backoff_seconds(1) == 1.0 + assert visibility.rate_limit_backoff_seconds(1, INSTALLATION_RATE_LIMIT_403) == 15.0 + assert visibility.RATE_LIMIT_MAX_ATTEMPTS == 3 + assert visibility.DEFAULT_MAX_ATTEMPTS == 4 + + def test_public_and_private_booleans_are_preserved() -> None: """Exact public and private booleans must survive lookup unchanged.""" public = _ScriptedGh(["false\n"]) @@ -414,13 +505,11 @@ def test_cli_writes_visibility_and_fails_closed( assert output.read_text(encoding="utf-8") == "existing=1\nis_private=false\n" assert visibility.main(["--repository", "ContextualWisdomLab/naruon"]) == 1 - monkeypatch.setattr( - visibility, - "fetch_repository_visibility", - lambda _repository: (_ for _ in ()).throw( - visibility.VisibilityResolutionError("denied") - ), - ) + + def deny(_repository: str) -> str: + raise visibility.VisibilityResolutionError("denied") + + monkeypatch.setattr(visibility, "fetch_repository_visibility", deny) assert ( visibility.main( [ @@ -459,6 +548,12 @@ def fake_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[s assert output.read_text(encoding="utf-8") == "is_private=true\n" +def test_workflow_step_parser_names_a_missing_step() -> None: + """A missing step name must fail with the requested name, not IndexError.""" + with pytest.raises(AssertionError, match="workflow step not found: Missing Step"): + _workflow_step("jobs:\n strix:\n steps: []\n", "Missing Step") + + def test_strix_workflow_uses_helper_and_keeps_token_order() -> None: """The required Strix job must call the helper with the existing token chain.""" workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") From 56d467724bbed55b66a00c00f247efc6a6d87daf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 18:06:15 +0900 Subject: [PATCH 05/11] fix(strix): extend visibility rate-limit backoff --- scripts/ci/strix_resolve_target_visibility.py | 4 ++-- tests/test_strix_resolve_target_visibility.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py index 3780a3c2f..cf24c461d 100644 --- a/scripts/ci/strix_resolve_target_visibility.py +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -53,8 +53,8 @@ RATE_LIMIT_MAX_ATTEMPTS = 3 DEFAULT_TIMEOUT_SECONDS = 20.0 MAX_BACKOFF_SECONDS = 4.0 -RATE_LIMIT_BASE_BACKOFF_SECONDS = 15.0 -RATE_LIMIT_MAX_BACKOFF_SECONDS = 20.0 +RATE_LIMIT_BASE_BACKOFF_SECONDS = 30.0 +RATE_LIMIT_MAX_BACKOFF_SECONDS = 60.0 class VisibilityResolutionError(RuntimeError): diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index c114c0124..5b1f952b0 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -185,7 +185,7 @@ def test_installation_rate_limit_403_retries_then_preserves_visibility() -> None == "false" ) assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 - assert sleeps == [15.0] + assert sleeps == [30.0] def test_secondary_rate_limit_403_retries_then_preserves_visibility() -> None: @@ -208,7 +208,7 @@ def test_secondary_rate_limit_403_retries_then_preserves_visibility() -> None: == "true" ) assert runner.calls == ["ContextualWisdomLab/inkspan"] * 2 - assert sleeps == [15.0] + assert sleeps == [30.0] def test_exhausted_rate_limit_403_stays_typed_infrastructure_failure() -> None: @@ -230,7 +230,7 @@ def test_exhausted_rate_limit_403_stays_typed_infrastructure_failure() -> None: assert "installation ID 141441800" in str(excinfo.value) assert "denied or missing" not in str(excinfo.value) assert runner.calls == ["ContextualWisdomLab/inkspan"] * 3 - assert sleeps == [15.0, 20.0] + assert sleeps == [30.0, 60.0] def test_rate_limit_403_is_not_confused_with_authorization_403() -> None: @@ -267,7 +267,7 @@ def test_rate_limit_retry_after_is_honored_and_capped() -> None: 1, "gh: HTTP 403: API rate limit exceeded\nRetry-After: 90", ) - == 20.0 + == 60.0 ) runner = _ScriptedGh( [ @@ -303,7 +303,7 @@ def test_rate_limit_reset_header_and_past_reset_are_bounded( INSTALLATION_RATE_LIMIT_403 + "\nX-RateLimit-Reset: 1", now=8.0, ) - == 15.0 + == 30.0 ) runner = _ScriptedGh( [ @@ -329,7 +329,7 @@ def test_rate_limit_reset_header_and_past_reset_are_bounded( def test_generic_transient_backoff_stays_short() -> None: """A 502 flake must keep the original 1/2/4s schedule.""" assert visibility.backoff_seconds(1) == 1.0 - assert visibility.rate_limit_backoff_seconds(1, INSTALLATION_RATE_LIMIT_403) == 15.0 + assert visibility.rate_limit_backoff_seconds(1, INSTALLATION_RATE_LIMIT_403) == 30.0 assert visibility.RATE_LIMIT_MAX_ATTEMPTS == 3 assert visibility.DEFAULT_MAX_ATTEMPTS == 4 From 553a6bf22b1af82819820e6caf6e1ec5c6d15a76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 23:31:02 +0900 Subject: [PATCH 06/11] fix: preserve bounded visibility retry budgets --- scripts/ci/strix_resolve_target_visibility.py | 13 +++-- tests/test_strix_resolve_target_visibility.py | 51 ++++++++++++++++--- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py index cf24c461d..e324fc728 100644 --- a/scripts/ci/strix_resolve_target_visibility.py +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -54,6 +54,7 @@ DEFAULT_TIMEOUT_SECONDS = 20.0 MAX_BACKOFF_SECONDS = 4.0 RATE_LIMIT_BASE_BACKOFF_SECONDS = 30.0 +RATE_LIMIT_MIN_BACKOFF_SECONDS = 5.0 RATE_LIMIT_MAX_BACKOFF_SECONDS = 60.0 @@ -173,6 +174,7 @@ def rate_limit_backoff_seconds( RATE_LIMIT_BASE_BACKOFF_SECONDS * float(2 ** (attempt - 1)), RATE_LIMIT_MAX_BACKOFF_SECONDS, ) + parsed = max(parsed, RATE_LIMIT_MIN_BACKOFF_SECONDS) if parsed > RATE_LIMIT_MAX_BACKOFF_SECONDS: print( "GitHub visibility rate-limit retry sleep capped from " @@ -199,7 +201,7 @@ def fetch_repository_visibility( ) runner = run_gh or run_gh_visibility last_error = "Target repository visibility did not resolve to true or false." - attempt_limit = max_attempts + rate_limit_attempts = 0 for attempt in range(1, max_attempts + 1): # pragma: no branch - last failure raises kind = "transient" rate_limited = False @@ -225,8 +227,10 @@ def fetch_repository_visibility( "Target repository visibility did not resolve to true or false." ) if rate_limited: - attempt_limit = min(max_attempts, RATE_LIMIT_MAX_ATTEMPTS) - if attempt >= attempt_limit or kind != "transient": + rate_limit_attempts += 1 + if rate_limit_attempts >= RATE_LIMIT_MAX_ATTEMPTS: + break + if attempt >= max_attempts or kind != "transient": break if rate_limited: delay = rate_limit_backoff_seconds(attempt, last_error, now=now()) @@ -236,7 +240,8 @@ def fetch_repository_visibility( label = "transient" print( f"{label.capitalize()} GitHub visibility lookup failure on attempt " - f"{attempt}/{attempt_limit}; retrying in {delay:g}s.", + f"{attempt}/{RATE_LIMIT_MAX_ATTEMPTS if rate_limited else max_attempts}; " + f"retrying in {delay:g}s.", file=sys.stderr, ) sleep(delay) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index 5b1f952b0..15af7d789 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -269,6 +269,13 @@ def test_rate_limit_retry_after_is_honored_and_capped() -> None: ) == 60.0 ) + assert ( + visibility.rate_limit_backoff_seconds( + 1, + "gh: HTTP 403: API rate limit exceeded\nRetry-After: 0", + ) + == 5.0 + ) runner = _ScriptedGh( [ visibility.VisibilityCommandError( @@ -289,13 +296,20 @@ def test_rate_limit_retry_after_is_honored_and_capped() -> None: assert sleeps == [12.0] -def test_rate_limit_reset_header_and_past_reset_are_bounded( - monkeypatch: pytest.MonkeyPatch, -) -> None: +def test_rate_limit_reset_header_and_past_reset_are_bounded() -> None: """Honor X-RateLimit-Reset when present; a past reset uses the default wait.""" - monkeypatch.setattr(visibility.time, "time", lambda: 100.0) - assert visibility.parse_rate_limit_wait_seconds("X-RateLimit-Reset: 112") == 12.0 - assert visibility.parse_rate_limit_wait_seconds("X-RateLimit-Reset: 90") is None + assert ( + visibility.parse_rate_limit_wait_seconds( + "X-RateLimit-Reset: 112", now=100.0 + ) + == 12.0 + ) + assert ( + visibility.parse_rate_limit_wait_seconds( + "X-RateLimit-Reset: 90", now=100.0 + ) + is None + ) assert visibility.parse_rate_limit_wait_seconds("") is None assert ( visibility.rate_limit_backoff_seconds( @@ -326,6 +340,29 @@ def test_rate_limit_reset_header_and_past_reset_are_bounded( assert sleeps == [10.0] +def test_rate_limit_does_not_shrink_generic_retry_budget() -> None: + """A later generic transient may still use the full retry budget.""" + runner = _ScriptedGh( + [ + visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403), + visibility.VisibilityCommandError("gh: HTTP 502: Bad Gateway"), + visibility.VisibilityCommandError("gh: HTTP 503: Service Unavailable"), + "false", + ] + ) + sleeps: list[float] = [] + + assert ( + visibility.fetch_repository_visibility( + "ContextualWisdomLab/inkspan", + run_gh=runner, + sleep=sleeps.append, + ) + == "false" + ) + assert sleeps == [30.0, 2.0, 4.0] + + def test_generic_transient_backoff_stays_short() -> None: """A 502 flake must keep the original 1/2/4s schedule.""" assert visibility.backoff_seconds(1) == 1.0 @@ -578,8 +615,6 @@ def test_strix_workflow_uses_helper_and_keeps_token_order() -> None: workflow ) assert "STRIX_SCAN_MODE" not in visibility_step - assert "--require-hashes" not in visibility_step - assert "--no-deps" not in visibility_step def test_noema_and_opencode_visibility_paths_stay_untouched() -> None: From 21beb66a98e30168146ee48c6593f58dd954d180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:27:32 +0900 Subject: [PATCH 07/11] test: complete Strix visibility docstring coverage --- CHANGELOG.md | 2 +- tests/test_strix_resolve_target_visibility.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f7b2e5b..ec431b0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. -- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, empty/non-boolean response, or authenticated HTTP 403 rate-limit (installation budget / secondary rate limit) no longer aborts the scan before it starts. Rate-limit 403 uses a distinct 3-attempt budget and a 15–20s bounded wait (Retry-After / X-RateLimit-Reset when present, otherwise 15/20s). A real 401/403/404 on a missing or unauthorized repository stays fail-closed and is never treated as a source finding. +- Retried transient GitHub visibility lookups in the required Strix job so a timeout, 5xx, 429, empty/non-boolean response, or authenticated HTTP 403 rate-limit (installation budget / secondary rate limit) no longer aborts the scan before it starts. Rate-limit 403 uses a distinct 3-attempt budget and a 30–60s bounded wait (Retry-After / X-RateLimit-Reset when present, otherwise 30/60s). A real 401/403/404 on a missing or unauthorized repository stays fail-closed and is never treated as a source finding. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index 15af7d789..438dd86c9 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -34,10 +34,12 @@ class _ScriptedGh: """Return scripted ``gh api`` outcomes in call order.""" def __init__(self, outcomes: list[object]) -> None: + """Initialize outcomes and an observable repository-call log.""" self.outcomes = list(outcomes) self.calls: list[str] = [] def __call__(self, repository: str) -> str: + """Return the next scripted result or raise its scripted exception.""" self.calls.append(repository) outcome = self.outcomes.pop(0) if isinstance(outcome, BaseException): @@ -471,6 +473,7 @@ def test_run_gh_visibility_success_timeout_oserror_and_nonzero( """The real ``gh api`` wrapper maps process outcomes to typed failures.""" def succeed(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Return a successful ``gh api`` process result.""" assert argv == ["gh", "api", "repos/ContextualWisdomLab/aFIPC", "--jq", ".private"] assert kwargs["shell"] is False return subprocess.CompletedProcess(argv, 0, stdout="false\n", stderr="") @@ -479,6 +482,7 @@ def succeed(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[st assert visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") == "false\n" def timeout(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Raise the subprocess timeout propagated by the wrapper.""" raise subprocess.TimeoutExpired(argv, kwargs["timeout"]) monkeypatch.setattr(visibility.subprocess, "run", timeout) @@ -488,6 +492,7 @@ def timeout(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[st def missing_binary( argv: list[str], **kwargs: object ) -> subprocess.CompletedProcess[str]: + """Raise the missing executable error mapped to a command failure.""" raise FileNotFoundError("gh") monkeypatch.setattr(visibility.subprocess, "run", missing_binary) @@ -495,6 +500,7 @@ def missing_binary( visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") def fail_empty(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Return a nonzero process result without diagnostic output.""" return subprocess.CompletedProcess(argv, 2, stdout="", stderr="") monkeypatch.setattr(visibility.subprocess, "run", fail_empty) @@ -502,6 +508,7 @@ def fail_empty(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") def fail_token(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Return a token-bearing error that must be redacted before logging.""" return subprocess.CompletedProcess( argv, 1, @@ -544,6 +551,7 @@ def test_cli_writes_visibility_and_fails_closed( assert visibility.main(["--repository", "ContextualWisdomLab/naruon"]) == 1 def deny(_repository: str) -> str: + """Raise the typed visibility failure handled by the CLI.""" raise visibility.VisibilityResolutionError("denied") monkeypatch.setattr(visibility, "fetch_repository_visibility", deny) @@ -570,6 +578,7 @@ def test_cli_main_module_uses_environment( monkeypatch.setattr(sys, "argv", ["strix_resolve_target_visibility.py"]) def fake_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Return a public visibility result for the module-entrypoint test.""" assert argv[-1] == ".private" return subprocess.CompletedProcess(argv, 0, stdout="true\n", stderr="") From c20d8c769b45e1f29cd7d032616c2fd8086edfb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 13:05:06 +0900 Subject: [PATCH 08/11] fix(strix): separate visibility retry budgets --- scripts/ci/strix_resolve_target_visibility.py | 41 ++++++++++--- tests/test_strix_resolve_target_visibility.py | 58 ++++++++++++++++++- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/scripts/ci/strix_resolve_target_visibility.py b/scripts/ci/strix_resolve_target_visibility.py index e324fc728..fe99ce6e7 100644 --- a/scripts/ci/strix_resolve_target_visibility.py +++ b/scripts/ci/strix_resolve_target_visibility.py @@ -58,6 +58,18 @@ RATE_LIMIT_MAX_BACKOFF_SECONDS = 60.0 +def split_gh_response(output: str) -> tuple[str, str]: + """Return HTTP headers and body from ``gh api --include`` output.""" + status = re.search(r"(?m)^HTTP/[^\r\n]*\r?\n", output or "") + if status is None: + return "", output or "" + separator = re.search(r"\r?\n\r?\n", output[status.end() :]) + if separator is None: + return output, "" + body_start = status.end() + separator.end() + return output[:body_start], output[body_start:] + + class VisibilityResolutionError(RuntimeError): """Fail-closed visibility lookup that must stop the Strix job.""" @@ -116,8 +128,8 @@ def classify_gh_failure(message: str) -> str: def run_gh_visibility( repository: str, timeout: float = DEFAULT_TIMEOUT_SECONDS ) -> str: - """Return ``gh api`` stdout for ``repos/.private``.""" - argv = ["gh", "api", f"repos/{repository}", "--jq", ".private"] + """Return only the boolean body from ``gh api`` response evidence.""" + argv = ["gh", "api", f"repos/{repository}", "--include", "--jq", ".private"] try: completed = subprocess.run( argv, @@ -133,12 +145,13 @@ def run_gh_visibility( ) from exc if completed.returncode != 0: detail = scrub_sensitive_data( - (completed.stderr or completed.stdout or "").strip() + "\n".join(part for part in (completed.stdout or "", completed.stderr or "") if part).strip() ) raise VisibilityCommandError( detail or f"gh api exited {completed.returncode}" ) - return completed.stdout + _headers, body = split_gh_response(completed.stdout) + return body def backoff_seconds(attempt: int) -> float: @@ -201,8 +214,12 @@ def fetch_repository_visibility( ) runner = run_gh or run_gh_visibility last_error = "Target repository visibility did not resolve to true or false." + transient_attempts = 0 rate_limit_attempts = 0 - for attempt in range(1, max_attempts + 1): # pragma: no branch - last failure raises + total_attempts = 0 + total_attempt_cap = max_attempts + RATE_LIMIT_MAX_ATTEMPTS + while total_attempts < total_attempt_cap: # pragma: no branch - terminal failure raises + total_attempts += 1 kind = "transient" rate_limited = False try: @@ -230,17 +247,23 @@ def fetch_repository_visibility( rate_limit_attempts += 1 if rate_limit_attempts >= RATE_LIMIT_MAX_ATTEMPTS: break - if attempt >= max_attempts or kind != "transient": + else: + transient_attempts += 1 + if kind != "transient": break if rate_limited: - delay = rate_limit_backoff_seconds(attempt, last_error, now=now()) + delay = rate_limit_backoff_seconds(rate_limit_attempts, last_error, now=now()) label = "rate-limit" else: - delay = backoff_seconds(attempt) + if transient_attempts >= max_attempts: + break + delay = backoff_seconds(transient_attempts) label = "transient" print( f"{label.capitalize()} GitHub visibility lookup failure on attempt " - f"{attempt}/{RATE_LIMIT_MAX_ATTEMPTS if rate_limited else max_attempts}; " + f"{rate_limit_attempts if rate_limited else transient_attempts}/" + f"{RATE_LIMIT_MAX_ATTEMPTS if rate_limited else max_attempts} " + f"(total {total_attempts}/{total_attempt_cap}); " f"retrying in {delay:g}s.", file=sys.stderr, ) diff --git a/tests/test_strix_resolve_target_visibility.py b/tests/test_strix_resolve_target_visibility.py index 438dd86c9..cc3660d32 100644 --- a/tests/test_strix_resolve_target_visibility.py +++ b/tests/test_strix_resolve_target_visibility.py @@ -343,12 +343,13 @@ def test_rate_limit_reset_header_and_past_reset_are_bounded() -> None: def test_rate_limit_does_not_shrink_generic_retry_budget() -> None: - """A later generic transient may still use the full retry budget.""" + """A rate-limit attempt cannot consume the separate generic retry budget.""" runner = _ScriptedGh( [ visibility.VisibilityCommandError(INSTALLATION_RATE_LIMIT_403), visibility.VisibilityCommandError("gh: HTTP 502: Bad Gateway"), visibility.VisibilityCommandError("gh: HTTP 503: Service Unavailable"), + visibility.VisibilityCommandError("gh: HTTP 504: Gateway Timeout"), "false", ] ) @@ -362,7 +363,7 @@ def test_rate_limit_does_not_shrink_generic_retry_budget() -> None: ) == "false" ) - assert sleeps == [30.0, 2.0, 4.0] + assert sleeps == [30.0, 1.0, 2.0, 4.0] def test_generic_transient_backoff_stays_short() -> None: @@ -474,7 +475,14 @@ def test_run_gh_visibility_success_timeout_oserror_and_nonzero( def succeed(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: """Return a successful ``gh api`` process result.""" - assert argv == ["gh", "api", "repos/ContextualWisdomLab/aFIPC", "--jq", ".private"] + assert argv == [ + "gh", + "api", + "repos/ContextualWisdomLab/aFIPC", + "--include", + "--jq", + ".private", + ] assert kwargs["shell"] is False return subprocess.CompletedProcess(argv, 0, stdout="false\n", stderr="") @@ -520,6 +528,50 @@ def fail_token(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess with pytest.raises(visibility.VisibilityCommandError, match=""): visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + def fail_with_headers(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Return response headers and stderr so both remain classifiable.""" + return subprocess.CompletedProcess( + argv, + 1, + stdout="HTTP/2 429 Too Many Requests\nRetry-After: 12\n\n", + stderr="secondary rate limit", + ) + + monkeypatch.setattr(visibility.subprocess, "run", fail_with_headers) + with pytest.raises(visibility.VisibilityCommandError, match="Retry-After: 12") as excinfo: + visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") + assert "secondary rate limit" in str(excinfo.value) + + +def test_run_gh_visibility_separates_included_headers_from_body( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Only the JSON body reaches the exact boolean parser.""" + + def included_response(argv: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + """Return realistic ``gh api --include`` output.""" + return subprocess.CompletedProcess( + argv, + 0, + stdout="HTTP/2 200 OK\r\nX-RateLimit-Remaining: 4999\r\n\r\nfalse\n", + stderr="", + ) + + monkeypatch.setattr(visibility.subprocess, "run", included_response) + assert visibility.run_gh_visibility("ContextualWisdomLab/aFIPC") == "false\n" + + +def test_split_gh_response_handles_header_only_and_plain_output() -> None: + """The bounded splitter handles plain, complete, and incomplete responses.""" + + assert visibility.split_gh_response("false\n") == ("", "false\n") + assert visibility.split_gh_response("HTTP/2 200 OK\nHeader: value\n\ntrue\n") == ( + "HTTP/2 200 OK\nHeader: value\n\n", + "true\n", + ) + header_only = "HTTP/2 200 OK\nHeader: value\n" + assert visibility.split_gh_response(header_only) == (header_only, "") + def test_cli_writes_visibility_and_fails_closed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch From 46d5cae7136262250df69977c523bfb12806ed0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:59:49 +0900 Subject: [PATCH 09/11] docs: complete central control-plane docstrings --- organization_commercial_readiness_fixtures.py | 1 + scripts/ci/organization_commercial_readiness_loop.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index d86596196..4b7ac7452 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -90,6 +90,7 @@ def __init__( repositories: list[dict[str, Any]], snapshots: dict[str, list[RepositorySnapshot | Exception]], ) -> None: + """Initialize deterministic repository and snapshot responses.""" self.repositories = repositories self.snapshots = snapshots self.dispatched_repairs: list[tuple[str, str]] = [] diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..84c4f326c 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize the authenticated client with a bounded request timeout.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) From 78a935e7441719642153481bd2e3529ac2ff9e91 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:50:03 +0900 Subject: [PATCH 10/11] test(strix): track current scheduler concurrency --- scripts/ci/test_strix_quick_gate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8b..e2d318287 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,8 +1506,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" - assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository)" "scheduler keeps manual queue scans isolated per repository" + assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" "scheduler cancels only metadata-free workflow-run scans in their isolated fallback group" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews" assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads" From c500598b685b1ce7ac25288b0fee06cfc14007dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:21:11 +0900 Subject: [PATCH 11/11] fix(security): refresh pip audit dependency --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \