From 01c66b0a363a653ac877dffb2ef5ee0051efc1f4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 17 Sep 2026 02:26:27 +0800 Subject: [PATCH 1/2] Diagnose Global review authentication without research execution Co-Authored-By: Codex --- .../workflows/global_etf_research_codegen.yml | 21 +++- README.md | 3 + scripts/run_global_etf_research_codegen.py | 56 +++++++++-- tests/test_global_etf_research_codegen.py | 98 +++++++++++++++++-- 4 files changed, 160 insertions(+), 18 deletions(-) diff --git a/.github/workflows/global_etf_research_codegen.yml b/.github/workflows/global_etf_research_codegen.yml index 92b9bc5..1dc62ed 100644 --- a/.github/workflows/global_etf_research_codegen.yml +++ b/.github/workflows/global_etf_research_codegen.yml @@ -8,6 +8,11 @@ on: required: true default: false type: boolean + auth_only: + description: Check the audit-service authentication path only; no research work + required: true + default: false + type: boolean permissions: contents: read @@ -37,7 +42,16 @@ jobs: with: persist-credentials: false + - name: Reject conflicting execution modes + run: | + set -euo pipefail + if [ "${{ inputs.execute }}" = "true" ] && [ "${{ inputs.auth_only }}" = "true" ]; then + echo "execute_and_auth_only_are_mutually_exclusive" >&2 + exit 2 + fi + - name: Checkout frozen UsEquityStrategies base + if: inputs.execute == true || inputs.auth_only == true uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: QuantStrategyLab/UsEquityStrategies @@ -46,7 +60,7 @@ jobs: persist-credentials: false - name: Initialize the bounded execute workspace - if: inputs.execute == true + if: inputs.execute == true || inputs.auth_only == true run: | set -euo pipefail umask 077 @@ -57,7 +71,7 @@ jobs: run: docker info --format '{{.ServerVersion}}' - name: Install the bounded codegen runtime - if: inputs.execute == true + if: inputs.execute == true || inputs.auth_only == true run: | set -euo pipefail umask 077 @@ -75,11 +89,12 @@ jobs: ./ues-source ./ - name: Verify audit-service authentication before research entry - if: inputs.execute == true + if: inputs.execute == true || inputs.auth_only == true run: | "$CASE_ROOT/venv/bin/python" -m scripts.run_global_etf_research_codegen --auth-preflight - name: Run the fixed plan or execute path + if: inputs.auth_only != true run: | set -euo pipefail if [ "${{ inputs.execute }}" = "true" ]; then diff --git a/README.md b/README.md index 73a737a..19a49d6 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ The old `global-etf-review-20260917` claim, result, and response are retained as authorization failure record; this explicitly authorized recovery uses `~/.local/state/aiauditbridge/global-etf-review-20260917-auth-recovery-35124525442`. The older `global-etf-codegen-20260916` directory is not changed or replayed. +The manual workflow also has an `auth_only` path for its existing OIDC and +audit-service health check. It cannot be combined with `execute`, and it does +not read the research source, create a claim, or call a model. A claim without a terminal result remains unknown and is never retried automatically. Only the advisory result is projected to a seven-day GitHub artifact; source body and raw response stay private on VPS. No deployment, diff --git a/scripts/run_global_etf_research_codegen.py b/scripts/run_global_etf_research_codegen.py index 951a18c..a8721ad 100644 --- a/scripts/run_global_etf_research_codegen.py +++ b/scripts/run_global_etf_research_codegen.py @@ -22,6 +22,7 @@ import subprocess import tempfile import urllib.error +import urllib.parse import urllib.request from collections.abc import Callable, Mapping from typing import Any @@ -365,15 +366,52 @@ def execute(prompt: str): return execute -def _auth_preflight() -> bool: - """Check the audit-service route before any bounded research work begins.""" +def _auth_preflight_http_stage(url: object, service_url: object) -> str: + """Return a fixed stage label without retaining an error URL or query.""" + if isinstance(url, str) and isinstance(service_url, str): + if url == f"{service_url}/v1/ai/health": + return "service" + oidc_url = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL", "") + try: + actual = urllib.parse.urlsplit(url) + oidc = urllib.parse.urlsplit(oidc_url) + except ValueError: + return "unknown" + if oidc.scheme and oidc.netloc and oidc.path and ( + actual.scheme, actual.netloc, actual.path + ) == (oidc.scheme, oidc.netloc, oidc.path): + return "oidc" + return "unknown" + + +def _auth_preflight() -> str: + """Check the audit-service route without exposing credentials or response content.""" try: - from ai_gateway_client import AiGatewayClient, GatewayConfig + from ai_gateway_client import AiGatewayClient, AuthenticationError, GatewayConfig + except ImportError: + return "import" - AiGatewayClient(GatewayConfig.from_env()).get_health() + try: + config = GatewayConfig.from_env() + except ValueError: + return "config" + try: + health = AiGatewayClient(config).get_health() + except AuthenticationError: + return "oidc" + except urllib.error.HTTPError as exc: + try: + stage = _auth_preflight_http_stage(exc.url, config.service_url) + return f"{stage}_http_{int(exc.code)}" + except (TypeError, ValueError): + return "unknown" + except (urllib.error.URLError, TimeoutError, OSError): + return "transport" + except (json.JSONDecodeError, UnicodeError, AttributeError, TypeError): + return "invalid_response" except Exception: - return False - return True + return "unknown" + return "passed" if isinstance(health, Mapping) else "invalid_response" def _run_global_candidate_tests(candidate_root: Path, *, baseline_root: Path) -> dict[str, str]: @@ -483,9 +521,9 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--ues-repo-root", type=Path, default=Path("/opt/ues-source")) args = parser.parse_args(argv) if args.auth_preflight: - passed = _auth_preflight() - print("auth_preflight_passed" if passed else "auth_preflight_failed") - return 0 if passed else 1 + outcome = _auth_preflight() + print(f"auth_preflight_{outcome}") + return 0 if outcome == "passed" else 1 if not args.execute: print(json.dumps(plan(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))) return 0 diff --git a/tests/test_global_etf_research_codegen.py b/tests/test_global_etf_research_codegen.py index 1ce35ef..af47c35 100644 --- a/tests/test_global_etf_research_codegen.py +++ b/tests/test_global_etf_research_codegen.py @@ -8,6 +8,7 @@ import subprocess import sys import textwrap +import urllib.error from contextlib import redirect_stdout from tempfile import TemporaryDirectory from types import SimpleNamespace @@ -65,33 +66,55 @@ def test_global_codegen_docker_integration_fixture(tmp_path): class GlobalResearchCodegenTests(TestCase): - def test_auth_preflight_is_an_execute_only_gate_before_research_entry(self): + def test_auth_preflight_is_a_bounded_execute_or_auth_only_gate_before_research_entry(self): workflow = (Path(__file__).parents[1] / ".github/workflows/global_etf_research_codegen.yml").read_text() preflight = " - name: Verify audit-service authentication before research entry\n" entry = " - name: Run the fixed plan or execute path\n" self.assertIn(preflight, workflow) - self.assertIn(" if: inputs.execute == true\n", workflow.split(preflight, 1)[1].split(entry, 1)[0]) + self.assertIn(" auth_only:\n", workflow) + self.assertIn(" default: false\n", workflow.split(" auth_only:\n", 1)[1].split("\n\npermissions:", 1)[0]) + self.assertIn(" if: inputs.execute == true || inputs.auth_only == true\n", workflow.split(preflight, 1)[1].split(entry, 1)[0]) + self.assertIn(" if: inputs.auth_only != true\n", workflow.split(entry, 1)[1].split("\n - name:", 1)[0]) self.assertLess(workflow.index(preflight), workflow.index(entry)) + def test_workflow_rejects_execute_and_auth_only_together(self): + workflow = (Path(__file__).parents[1] / ".github/workflows/global_etf_research_codegen.yml").read_text() + step = workflow.split(" - name: Reject conflicting execution modes\n", 1)[1] + run_block = step.split(" run: |\n", 1)[1].split("\n - name:", 1)[0] + script = textwrap.dedent(run_block).replace("${{ inputs.execute }}", "true").replace( + "${{ inputs.auth_only }}", "true") + result = subprocess.run(["bash", "-euo", "pipefail", "-c", script], capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn("execute_and_auth_only_are_mutually_exclusive", result.stderr) + def test_auth_preflight_hides_exception_and_stops_before_research(self): + class FakeAuthenticationError(Exception): + pass + client = SimpleNamespace(get_health=lambda: (_ for _ in ()).throw(RuntimeError("sensitive detail"))) output = io.StringIO() with patch.dict(sys.modules, {"ai_gateway_client": SimpleNamespace( - AiGatewayClient=lambda config: client, GatewayConfig=SimpleNamespace(from_env=lambda: object()), + AiGatewayClient=lambda config: client, AuthenticationError=FakeAuthenticationError, + GatewayConfig=SimpleNamespace(from_env=lambda: object()), )}), patch.object(codegen, "run_global_etf_research_codegen_case") as run_research, redirect_stdout(output): status = codegen.main(["--auth-preflight"]) self.assertEqual(status, 1) - self.assertEqual(output.getvalue().strip(), "auth_preflight_failed") + self.assertEqual(output.getvalue().strip(), "auth_preflight_unknown") + self.assertNotIn("sensitive detail", output.getvalue()) run_research.assert_not_called() def test_auth_preflight_only_reports_passed_after_health_check(self): + class FakeAuthenticationError(Exception): + pass + calls = [] - client = SimpleNamespace(get_health=lambda: calls.append("health")) + client = SimpleNamespace(get_health=lambda: calls.append("health") or {}) output = io.StringIO() with patch.dict(sys.modules, {"ai_gateway_client": SimpleNamespace( - AiGatewayClient=lambda config: client, GatewayConfig=SimpleNamespace(from_env=lambda: object()), + AiGatewayClient=lambda config: client, AuthenticationError=FakeAuthenticationError, + GatewayConfig=SimpleNamespace(from_env=lambda: object()), )}), redirect_stdout(output): status = codegen.main(["--auth-preflight"]) @@ -99,6 +122,69 @@ def test_auth_preflight_only_reports_passed_after_health_check(self): self.assertEqual(calls, ["health"]) self.assertEqual(output.getvalue().strip(), "auth_preflight_passed") + def test_auth_preflight_classifies_failures_without_leaking_exception_content(self): + class FakeAuthenticationError(Exception): + pass + + failures = ( + (FakeAuthenticationError("canary"), "oidc"), + (urllib.error.URLError("canary"), "transport"), + ) + for failure, category in failures: + with self.subTest(category=category): + client = SimpleNamespace(get_health=lambda failure=failure: (_ for _ in ()).throw(failure)) + output = io.StringIO() + with patch.dict(sys.modules, {"ai_gateway_client": SimpleNamespace( + AiGatewayClient=lambda config: client, AuthenticationError=FakeAuthenticationError, + GatewayConfig=SimpleNamespace(from_env=lambda: object()), + )}), redirect_stdout(output): + status = codegen.main(["--auth-preflight"]) + self.assertEqual(status, 1) + self.assertEqual(output.getvalue().strip(), f"auth_preflight_{category}") + self.assertNotIn("canary", output.getvalue()) + + def test_auth_preflight_classifies_http_service_and_oidc_stages_without_urls(self): + class FakeAuthenticationError(Exception): + pass + + config = SimpleNamespace(service_url="https://audit.example") + failures = ( + (urllib.error.HTTPError("https://audit.example/v1/ai/health", 401, "canary", None, None), "service_http_401"), + (urllib.error.HTTPError("https://oidc.example/token?canary", 403, "canary", None, None), "oidc_http_403"), + ) + for failure, category in failures: + with self.subTest(category=category): + client = SimpleNamespace(get_health=lambda failure=failure: (_ for _ in ()).throw(failure)) + output = io.StringIO() + with patch.dict(os.environ, {"ACTIONS_ID_TOKEN_REQUEST_URL": "https://oidc.example/token?secret=canary"}), patch.dict( + sys.modules, {"ai_gateway_client": SimpleNamespace( + AiGatewayClient=lambda _: client, AuthenticationError=FakeAuthenticationError, + GatewayConfig=SimpleNamespace(from_env=lambda: config), + )} + ), redirect_stdout(output): + status = codegen.main(["--auth-preflight"]) + self.assertEqual(status, 1) + self.assertEqual(output.getvalue().strip(), f"auth_preflight_{category}") + self.assertNotIn("canary", output.getvalue()) + + def test_auth_preflight_classifies_config_and_invalid_response(self): + class FakeAuthenticationError(Exception): + pass + + cases = ( + (SimpleNamespace(from_env=lambda: (_ for _ in ()).throw(ValueError("canary"))), None, "config"), + (SimpleNamespace(from_env=lambda: object()), SimpleNamespace(get_health=lambda: []), "invalid_response"), + ) + for config, client, category in cases: + with self.subTest(category=category): + output = io.StringIO() + with patch.dict(sys.modules, {"ai_gateway_client": SimpleNamespace( + AiGatewayClient=lambda _: client, AuthenticationError=FakeAuthenticationError, GatewayConfig=config, + )}), redirect_stdout(output): + status = codegen.main(["--auth-preflight"]) + self.assertEqual(status, 1) + self.assertEqual(output.getvalue().strip(), f"auth_preflight_{category}") + def test_workflow_plan_branch_runs_without_execute_venv(self): workflow = (Path(__file__).parents[1] / ".github/workflows/global_etf_research_codegen.yml").read_text() step = workflow.split(" - name: Run the fixed plan or execute path\n", 1)[1] From f5b58a2662ccc439426675bce6be5bc9ebc746db Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 17 Sep 2026 02:29:17 +0800 Subject: [PATCH 2/2] Classify exact service authentication rejections without exposing response text Co-Authored-By: Codex --- scripts/run_global_etf_research_codegen.py | 31 +++++++++++++++++++++- tests/test_global_etf_research_codegen.py | 23 +++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/scripts/run_global_etf_research_codegen.py b/scripts/run_global_etf_research_codegen.py index a8721ad..a38caf6 100644 --- a/scripts/run_global_etf_research_codegen.py +++ b/scripts/run_global_etf_research_codegen.py @@ -384,6 +384,32 @@ def _auth_preflight_http_stage(url: object, service_url: object) -> str: return "unknown" +_AUTH_PREFLIGHT_SERVICE_ERROR_MARKERS = { + "OIDC workflow_ref is not allowed": "workflow_ref_not_allowed", + "OIDC job workflow ref is not allowed": "job_workflow_ref_not_allowed", + "OIDC repository is not allowed": "repository_not_allowed", + "OIDC ref is not allowed": "ref_not_allowed", + "OIDC audience is not allowed": "audience_not_allowed", + "OIDC token is expired": "token_expired", + "OIDC signature length is invalid": "signature_invalid", + "OIDC signature padding is invalid": "signature_invalid", + "OIDC signature padding separator is missing": "signature_invalid", + "OIDC signature digest does not match": "signature_invalid", +} + + +def _auth_preflight_service_error_marker(exc: urllib.error.HTTPError) -> str: + """Map only exact service error values; never return the response content.""" + try: + payload = json.loads(exc.read(4096).decode("utf-8")) + except (AttributeError, OSError, UnicodeError, json.JSONDecodeError): + return "unknown" + if not isinstance(payload, dict): + return "unknown" + error = payload.get("error") + return _AUTH_PREFLIGHT_SERVICE_ERROR_MARKERS.get(error, "unknown") if isinstance(error, str) else "unknown" + + def _auth_preflight() -> str: """Check the audit-service route without exposing credentials or response content.""" try: @@ -402,7 +428,10 @@ def _auth_preflight() -> str: except urllib.error.HTTPError as exc: try: stage = _auth_preflight_http_stage(exc.url, config.service_url) - return f"{stage}_http_{int(exc.code)}" + code = int(exc.code) + if stage == "service" and code in {401, 403}: + return f"{stage}_http_{code}_{_auth_preflight_service_error_marker(exc)}" + return f"{stage}_http_{code}" except (TypeError, ValueError): return "unknown" except (urllib.error.URLError, TimeoutError, OSError): diff --git a/tests/test_global_etf_research_codegen.py b/tests/test_global_etf_research_codegen.py index af47c35..7b75baa 100644 --- a/tests/test_global_etf_research_codegen.py +++ b/tests/test_global_etf_research_codegen.py @@ -149,7 +149,7 @@ class FakeAuthenticationError(Exception): config = SimpleNamespace(service_url="https://audit.example") failures = ( - (urllib.error.HTTPError("https://audit.example/v1/ai/health", 401, "canary", None, None), "service_http_401"), + (urllib.error.HTTPError("https://audit.example/v1/ai/health", 401, "canary", {}, io.BytesIO(b'{"error":"canary"}')), "service_http_401_unknown"), (urllib.error.HTTPError("https://oidc.example/token?canary", 403, "canary", None, None), "oidc_http_403"), ) for failure, category in failures: @@ -167,6 +167,27 @@ class FakeAuthenticationError(Exception): self.assertEqual(output.getvalue().strip(), f"auth_preflight_{category}") self.assertNotIn("canary", output.getvalue()) + def test_auth_preflight_exactly_classifies_known_service_claim_error(self): + class FakeAuthenticationError(Exception): + pass + + error = urllib.error.HTTPError( + "https://audit.example/v1/ai/health", 401, "canary", {}, + io.BytesIO(b'{"error":"OIDC job workflow ref is not allowed","detail":"canary"}'), + ) + client = SimpleNamespace(get_health=lambda: (_ for _ in ()).throw(error)) + output = io.StringIO() + config = SimpleNamespace(service_url="https://audit.example") + with patch.dict(sys.modules, {"ai_gateway_client": SimpleNamespace( + AiGatewayClient=lambda _: client, AuthenticationError=FakeAuthenticationError, + GatewayConfig=SimpleNamespace(from_env=lambda: config), + )}), redirect_stdout(output): + status = codegen.main(["--auth-preflight"]) + + self.assertEqual(status, 1) + self.assertEqual(output.getvalue().strip(), "auth_preflight_service_http_401_job_workflow_ref_not_allowed") + self.assertNotIn("canary", output.getvalue()) + def test_auth_preflight_classifies_config_and_invalid_response(self): class FakeAuthenticationError(Exception): pass