Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions .github/workflows/global_etf_research_codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
85 changes: 76 additions & 9 deletions scripts/run_global_etf_research_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -365,15 +366,81 @@ 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"


_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:
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)
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):
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]:
Expand Down Expand Up @@ -483,9 +550,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
Expand Down
119 changes: 113 additions & 6 deletions tests/test_global_etf_research_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,40 +66,146 @@ 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"])

self.assertEqual(status, 0)
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", {}, 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:
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_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

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]
Expand Down