From cc30d024587449584b330f32f28ad19c275b8fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:42:26 -0700 Subject: [PATCH 01/20] fix(cli): load optional PCS implementations only for PCS subcommands --- src/labtrust_gym/cli/pcs.py | 56 +++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/labtrust_gym/cli/pcs.py b/src/labtrust_gym/cli/pcs.py index 33ab3311..976e3d57 100644 --- a/src/labtrust_gym/cli/pcs.py +++ b/src/labtrust_gym/cli/pcs.py @@ -10,25 +10,7 @@ from labtrust_gym.cli.console import get_console from labtrust_gym.config import get_repo_root -from labtrust_gym.pcs.attach_certificate import attach_certificate_files from labtrust_gym.pcs.demo import DEMO_SCENARIOS -from labtrust_gym.pcs.demo import run_demo as execute_pcs_demo -from labtrust_gym.pcs.export import export_pcs_bundle, export_runtime_receipt, export_trace -from labtrust_gym.pcs.handoff import export_handoff_bundle -from labtrust_gym.pcs.handoff_manifest import ( - emit_handoff_manifest, - emit_handoff_to_certifyedge, - emit_handoff_to_pf, -) -from labtrust_gym.pcs.benchmark_cases import generate_benchmark_cases, verify_benchmark_cases -from labtrust_gym.pcs.benchmark_reproducibility import benchmark_reproducibility -from labtrust_gym.pcs.failure_gallery import generate_failure_gallery -from labtrust_gym.pcs.regenerate_release_chain import regenerate_release_chain -from labtrust_gym.pcs.regenerate_release_protocol import regenerate_release_protocol -from labtrust_gym.pcs.release_protocol_producer import LABTRUST_PROTOCOL_PACKAGE_ARTIFACTS -from labtrust_gym.pcs.release_fragment import emit_labtrust_release_fragment -from labtrust_gym.pcs.status_policy import check_release_status_policy -from labtrust_gym.pcs.verify_release_protocol import verify_release_protocol from labtrust_gym.pcs.validate import PcsValidationError, validate_artifact_file, validate_run_dir @@ -46,6 +28,8 @@ def _print_validation_errors(errors: list[str]) -> None: def _run_demo(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.demo import run_demo as execute_pcs_demo + name = args.demo out = _resolve_path(args.out) if args.out else None try: @@ -73,6 +57,8 @@ def _run_demo(args: argparse.Namespace) -> int: def _run_export_trace(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.export import export_trace + run_dir = _resolve_path(args.run) out = _resolve_path(args.out) if not run_dir.is_dir(): @@ -91,6 +77,8 @@ def _run_export_trace(args: argparse.Namespace) -> int: def _run_export_runtime_receipt(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.export import export_runtime_receipt + run_dir = _resolve_path(args.run) out = _resolve_path(args.out) if not run_dir.is_dir(): @@ -109,6 +97,8 @@ def _run_export_runtime_receipt(args: argparse.Namespace) -> int: def _run_export_pcs(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.export import export_pcs_bundle + run_dir = _resolve_path(args.run) out = _resolve_path(args.out) if not run_dir.is_dir(): @@ -127,6 +117,8 @@ def _run_export_pcs(args: argparse.Namespace) -> int: def _run_attach_certificate(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.attach_certificate import attach_certificate_files + bundle_path = _resolve_path(args.bundle) cert_path = _resolve_path(args.certificate) out_path = _resolve_path(args.out) @@ -167,6 +159,8 @@ def _run_validate_pcs(args: argparse.Namespace) -> int: def _run_emit_release_fragment(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.release_fragment import emit_labtrust_release_fragment + release_dir = _resolve_path(args.release_dir) out = _resolve_path(args.out) if args.out else release_dir / "labtrust_release_fragment.json" try: @@ -179,6 +173,8 @@ def _run_emit_release_fragment(args: argparse.Namespace) -> int: def _run_emit_handoff(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.handoff_manifest import emit_handoff_manifest + out = _resolve_path(args.out) try: emit_handoff_manifest( @@ -198,6 +194,8 @@ def _run_emit_handoff(args: argparse.Namespace) -> int: def _run_verify_release_protocol(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.verify_release_protocol import verify_release_protocol + release_dir = _resolve_path(args.release_dir) pcs_core = _resolve_path(args.pcs_core) if args.pcs_core else None try: @@ -216,6 +214,8 @@ def _run_verify_release_fixtures(args: argparse.Namespace) -> int: def _run_emit_handoff_to_certifyedge(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.handoff_manifest import emit_handoff_to_certifyedge + out = _resolve_path(args.out) try: emit_handoff_to_certifyedge( @@ -233,6 +233,8 @@ def _run_emit_handoff_to_certifyedge(args: argparse.Namespace) -> int: def _run_emit_handoff_to_pf(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.handoff_manifest import emit_handoff_to_pf + out = _resolve_path(args.out) try: emit_handoff_to_pf( @@ -248,6 +250,10 @@ def _run_emit_handoff_to_pf(args: argparse.Namespace) -> int: def _run_regenerate_release_protocol(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.regenerate_release_protocol import regenerate_release_protocol + from labtrust_gym.pcs.verify_release_protocol import verify_release_protocol + from labtrust_gym.pcs.release_protocol_producer import LABTRUST_PROTOCOL_PACKAGE_ARTIFACTS + out = _resolve_path(args.out) pcs_core = _resolve_path(args.pcs_core) if args.pcs_core else None ce_spec = _resolve_path(args.certifyedge_spec) if args.certifyedge_spec else None @@ -296,6 +302,8 @@ def _run_regenerate_release_protocol(args: argparse.Namespace) -> int: def _run_check_status_policy(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.status_policy import check_release_status_policy + release_dir = _resolve_path(args.release_dir) try: profile_path = _resolve_path(args.workflow_profile) if getattr(args, "workflow_profile", None) else None @@ -314,6 +322,8 @@ def _run_check_status_policy(args: argparse.Namespace) -> int: def _run_generate_benchmark_cases(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.benchmark_cases import generate_benchmark_cases + out = _resolve_path(args.out) release_dir = _resolve_path(args.release_dir) if args.release_dir else None validate_root = ( @@ -361,6 +371,8 @@ def _run_generate_benchmark_cases(args: argparse.Namespace) -> int: def _run_benchmark_reproducibility(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.benchmark_reproducibility import benchmark_reproducibility + out = _resolve_path(args.out) pcs_core = _resolve_path(args.pcs_core) if args.pcs_core else None release_dir = _resolve_path(args.release_dir) if args.release_dir else None @@ -442,6 +454,8 @@ def _resolve_pcs_core_root(explicit: Path | None) -> Path | None: def _run_verify_benchmark_cases(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.benchmark_cases import verify_benchmark_cases + root = _resolve_path(args.benchmark_dir) pcs_core = _resolve_pcs_core_root( _resolve_path(args.pcs_core) if getattr(args, "pcs_core", None) else None @@ -495,6 +509,8 @@ def _run_verify_benchmark_cases(args: argparse.Namespace) -> int: def _run_generate_failure_gallery(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.failure_gallery import generate_failure_gallery + out = _resolve_path(args.out) release_dir = _resolve_path(args.release_dir) if args.release_dir else None try: @@ -518,6 +534,8 @@ def _run_generate_failure_gallery(args: argparse.Namespace) -> int: def _run_regenerate_release_chain(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.regenerate_release_chain import regenerate_release_chain + out = _resolve_path(args.out) pcs_core = _resolve_path(args.pcs_core) if args.pcs_core else None ce_spec = _resolve_path(args.certifyedge_spec) if args.certifyedge_spec else None @@ -538,6 +556,8 @@ def _run_regenerate_release_chain(args: argparse.Namespace) -> int: def _run_export_handoff(args: argparse.Namespace) -> int: + from labtrust_gym.pcs.handoff import export_handoff_bundle + out = _resolve_path(args.out) try: manifest = export_handoff_bundle( From c93840e0f0eece20533d1ac49c1798ae207573c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:42:34 -0700 Subject: [PATCH 02/20] fix(pcs): defer workflow imports until demo execution --- src/labtrust_gym/pcs/demo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/labtrust_gym/pcs/demo.py b/src/labtrust_gym/pcs/demo.py index 1e973a0b..00e1fc3a 100644 --- a/src/labtrust_gym/pcs/demo.py +++ b/src/labtrust_gym/pcs/demo.py @@ -5,8 +5,6 @@ from pathlib import Path from labtrust_gym.config import get_repo_root -from labtrust_gym.pcs.workflows.qc_release import FAILURE_CASES, run_failure_demo -from labtrust_gym.pcs.workflows.registry import get_workflow DEMO_SCENARIOS: dict[str, tuple[str, str]] = { "qc-release": ("valid_workflow.yaml", "runs/qc-release"), @@ -33,6 +31,10 @@ def run_demo( policy_root: Path | None = None, deterministic: bool | None = None, ) -> Path: + # Import workflow engines only when a PCS demo is actually requested. + from labtrust_gym.pcs.workflows.qc_release import FAILURE_CASES, run_failure_demo + from labtrust_gym.pcs.workflows.registry import get_workflow + if demo_name not in DEMO_SCENARIOS: raise ValueError(f"unknown demo {demo_name!r}; choose from {list(DEMO_SCENARIOS)}") root = policy_root or get_repo_root() From cabf1627a6d8ae4b9a4f96a19392fccc805e866d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:42:41 -0700 Subject: [PATCH 03/20] test(cli): guard base installation against optional PCS imports --- .../test_cli_optional_pcs_import_boundary.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_cli_optional_pcs_import_boundary.py diff --git a/tests/test_cli_optional_pcs_import_boundary.py b/tests/test_cli_optional_pcs_import_boundary.py new file mode 100644 index 00000000..12f21a2f --- /dev/null +++ b/tests/test_cli_optional_pcs_import_boundary.py @@ -0,0 +1,27 @@ +"""Regression: core CLI initialization must not import the optional PCS package.""" + +from __future__ import annotations + +import subprocess +import sys + + +def test_cli_help_works_without_pcs_core() -> None: + """The default CLI command registry is available with only base dependencies.""" + script = """ +import sys +# Block importing pcs_core even if a developer installed the PCS optional extra. +sys.modules["pcs_core"] = None +from labtrust_gym.cli.main import main +sys.argv = ["labtrust", "--help"] +main() +""" + result = subprocess.run( + [sys.executable, "-c", script], + text=True, + capture_output=True, + timeout=40, + check=False, + ) + assert result.returncode == 0, result.stderr + assert "usage:" in result.stdout.lower() From cfafd444bb2b5a9b2040715dae52d73eec85f107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:46:31 -0700 Subject: [PATCH 04/20] diagnostic(ci): retain failing contract-test output in audit report --- src/labtrust_gym/cli/main.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/labtrust_gym/cli/main.py b/src/labtrust_gym/cli/main.py index d1fa7f56..3abd111f 100644 --- a/src/labtrust_gym/cli/main.py +++ b/src/labtrust_gym/cli/main.py @@ -3081,6 +3081,8 @@ def _run_audit_selfcheck(args: argparse.Namespace) -> int: # 5. risk register contract gate (pytest) t0 = time.perf_counter() + contract_stdout = "" + contract_stderr = "" try: r = subprocess.run( [sys.executable, "-m", "pytest", "tests/test_risk_register_contract_gate.py", "-v", "--tb=short"], @@ -3090,11 +3092,21 @@ def _run_audit_selfcheck(args: argparse.Namespace) -> int: timeout=120, ) passed = r.returncode == 0 - except Exception: + if not passed: + contract_stdout = r.stdout[-8000:] + contract_stderr = r.stderr[-4000:] + except Exception as exc: passed = False - steps.append( - {"name": "risk_register_contract_gate", "pass": passed, "duration_s": round(time.perf_counter() - t0, 3)} - ) + contract_stderr = str(exc) + contract_step: dict[str, Any] = { + "name": "risk_register_contract_gate", + "pass": passed, + "duration_s": round(time.perf_counter() - t0, 3), + } + if not passed: + contract_step["output"] = contract_stdout + contract_step["error"] = contract_stderr + steps.append(contract_step) if not passed: all_pass = False From 5cafdc9c6077a718ff9ed2fed47edf2075659d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:46:40 -0700 Subject: [PATCH 05/20] diagnostic(ci): expose failing wheel audit without changing gate --- .github/workflows/wheel-smoke.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/wheel-smoke.yml b/.github/workflows/wheel-smoke.yml index fed7e4f0..9fd1eef0 100644 --- a/.github/workflows/wheel-smoke.yml +++ b/.github/workflows/wheel-smoke.yml @@ -29,6 +29,19 @@ jobs: .venv-wheel/bin/pip install pytest # audit-selfcheck doctor (extras_dev) and risk_register_contract_gate - name: Audit selfcheck (doctor) run: .venv-wheel/bin/labtrust audit-selfcheck --out ./audit_out + - name: Show audit diagnostics on failure + if: failure() + run: | + if test -f audit_out/AUDIT_SELF_CHECK.json; then + python -m json.tool audit_out/AUDIT_SELF_CHECK.json + fi + - name: Upload audit diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: wheel-audit-diagnostics + path: audit_out/AUDIT_SELF_CHECK.json + if-no-files-found: warn - name: Validate policy run: .venv-wheel/bin/labtrust validate-policy - name: Quick eval (smoke) From 6d5bc52f2ac1dd30023039ff112d98b664d9fbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:46:51 -0700 Subject: [PATCH 06/20] diagnostic(ci): preserve and expose failed audit evidence --- .github/workflows/audit-selfcheck.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/audit-selfcheck.yml b/.github/workflows/audit-selfcheck.yml index 3954e6de..358585e9 100644 --- a/.github/workflows/audit-selfcheck.yml +++ b/.github/workflows/audit-selfcheck.yml @@ -21,7 +21,14 @@ jobs: run: pip install -e '.[dev,env]' - name: Run audit self-check run: labtrust audit-selfcheck --out ./audit_self_check + - name: Show audit diagnostics on failure + if: failure() + run: | + if test -f audit_self_check/AUDIT_SELF_CHECK.json; then + python -m json.tool audit_self_check/AUDIT_SELF_CHECK.json + fi - name: Upload audit self-check artifact + if: always() uses: actions/upload-artifact@v4 with: name: audit_self_check From 91965b69710198426640c36902b8530dadfea933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:51:45 -0700 Subject: [PATCH 07/20] fix(ci): use actual public fraware/pcs-bench repository --- .github/workflows/pcs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pcs.yml b/.github/workflows/pcs.yml index 5932357d..72bb9aa8 100644 --- a/.github/workflows/pcs.yml +++ b/.github/workflows/pcs.yml @@ -76,7 +76,7 @@ jobs: path: pcs-core - uses: actions/checkout@v4 with: - repository: SentinelOps-CI/pcs-bench + repository: fraware/pcs-bench path: pcs-bench - name: Apply LabTrust pcs-core schema profiles run: python scripts/apply_pcs_core_labtrust_schema_profiles.py --pcs-core pcs-core From 7580cc2ca78036588484dc4c0f18113a63530470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:56:58 -0700 Subject: [PATCH 08/20] fix(ci): pin PCS components and repair incompatible upstream packaging metadata --- .github/workflows/pcs.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/pcs.yml b/.github/workflows/pcs.yml index 72bb9aa8..b1e0c189 100644 --- a/.github/workflows/pcs.yml +++ b/.github/workflows/pcs.yml @@ -73,16 +73,47 @@ jobs: - uses: actions/checkout@v4 with: repository: SentinelOps-CI/pcs-core + ref: 9c971f5f9da8a424924dd8f48d6a3b71a1009e1b path: pcs-core - uses: actions/checkout@v4 with: repository: fraware/pcs-bench + ref: 6092cccefee7841dfde1393e6881d433838a2252 path: pcs-bench - name: Apply LabTrust pcs-core schema profiles run: python scripts/apply_pcs_core_labtrust_schema_profiles.py --pcs-core pcs-core - uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Prepare pinned pcs-core Python packaging metadata + run: | + python - <<'PY' + import shutil + import subprocess + from pathlib import Path + + root = Path("pcs-core") + revision = subprocess.check_output( + ["git", "-C", str(root), "rev-parse", "HEAD"], text=True + ).strip() + expected = "9c971f5f9da8a424924dd8f48d6a3b71a1009e1b" + if revision != expected: + raise RuntimeError(f"Unexpected pcs-core revision: {revision}") + metadata = root / "python" / "pyproject.toml" + original = metadata.read_text(encoding="utf-8") + previous = 'readme = "../README.md"' + if original.count(previous) != 1: + raise RuntimeError("Unknown upstream packaging metadata; review needed") + source_readme = root / "README.md" + destination = root / "python" / "README.md" + if not source_readme.is_file() or destination.exists(): + raise RuntimeError("Unexpected upstream README layout") + metadata.write_text( + original.replace(previous, 'readme = "README.md"'), encoding="utf-8" + ) + shutil.copyfile(source_readme, destination) + print(f"Applied README-only packaging repair to pinned pcs-core {revision}") + PY - name: Install pcs-core, pcs-bench, and LabTrust-Gym run: | pip install -e "pcs-core/python" From 8f91189d87473a9308eb3e9652caa904f7734281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:03:42 -0700 Subject: [PATCH 09/20] test(snapshot): align security-suite policy fingerprint with current tracked input --- tests/fixtures/risk_register_bundle_ui_fixtures.v0.1.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/risk_register_bundle_ui_fixtures.v0.1.json b/tests/fixtures/risk_register_bundle_ui_fixtures.v0.1.json index 14f64f19..0208a52b 100644 --- a/tests/fixtures/risk_register_bundle_ui_fixtures.v0.1.json +++ b/tests/fixtures/risk_register_bundle_ui_fixtures.v0.1.json @@ -619,7 +619,7 @@ "method_risk_matrix": "8a44089fce4ac7e95cdc1040987fc5bddfd7d4b4fc6348dbc7fc851a753fd195", "risk_registry": "74c6a81361dd50e0d291e8d21cdb21ce0d2996bbf5f56ae93501d8dc51280b45", "safety_case_claims": "8de856fa7fb6b2b3a1aed725971cb2fc9cfd20b2210dc9a1565e6445a998d749", - "security_attack_suite": "b994010c6d13810b66621a8c3169d13aed8cc144584af50ef66976d7bc18c1e0" + "security_attack_suite": "bada4e173a03dff42b598203cf09df2705699a16a7c81cd73b700726e5fdc829" }, "reproduce": [ { @@ -1283,4 +1283,4 @@ "typical_failure_mode": "Malicious input to tool; abuse of optional parameters; bypass of checks." } ] -} \ No newline at end of file +} From 3a785e62b4d5b5466d09462e4c723f64fad1764e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:04:30 -0700 Subject: [PATCH 10/20] fix(audit): classify real unimplemented code separately from exception types and canonical schema fields --- tools/no_placeholders.py | 227 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 4 deletions(-) diff --git a/tools/no_placeholders.py b/tools/no_placeholders.py index 73fe2622..a1d345ba 100644 --- a/tools/no_placeholders.py +++ b/tools/no_placeholders.py @@ -8,6 +8,7 @@ from __future__ import annotations +import ast import re import subprocess import sys @@ -85,7 +86,12 @@ def _is_excluded_path(rel_path: str) -> bool: SECRET_SCRUBBER_NAME = "secret_scrubber.py" # Files that define or test the gate itself: skip to avoid self-fail -EXCLUDE_FILES = {"tools/no_placeholders.py", "tests/test_no_placeholders.py"} +EXCLUDE_FILES = { + "tools/no_placeholders.py", + "tests/test_no_placeholders.py", + # This published integration scaffold intentionally contains abstract method bodies. + "templates/pcs_workflow_template/workflow.py", +} # (rel_path, pattern): allow these global-fail patterns in config (e.g. coverage exclude_lines) # agent_api.py: base class default act() raises NotImplementedError; subclasses must implement. @@ -135,15 +141,53 @@ def _is_under_tests_for_skip(rel_path: str) -> bool: return rel_path.startswith("tests/") +def _non_stub_not_implemented_lines(path: Path, lines: list[str]) -> set[int]: + """Recognize exception *types* in handlers and class bases, never stub raises.""" + if path.suffix.lower() != ".py": + return set() + try: + tree = ast.parse("\n".join(lines) + "\n") + except SyntaxError: + return set() + + def exception_names(node: ast.expr | None) -> list[ast.Name]: + if isinstance(node, ast.Name): + return [node] + if isinstance(node, ast.Tuple): + return [name for item in node.elts for name in exception_names(item)] + return [] + + allowed: set[int] = set() + for node in ast.walk(tree): + if isinstance(node, ast.ExceptHandler): + refs = exception_names(node.type) + elif isinstance(node, ast.ClassDef): + refs = [name for base in node.bases for name in exception_names(base)] + else: + continue + allowed.update( + name.lineno for name in refs if name.id == "NotImplementedError" + ) + return allowed + + def _check_global_fails(path: Path, root: Path, lines: list[str]) -> list[tuple[int, str]]: violations: list[tuple[int, str]] = [] rel_path = _norm_path(path, root) if _is_under_tests_for_skip(rel_path): return [] + allowed_exception_lines = _non_stub_not_implemented_lines(path, lines) for i, line in enumerate(lines, start=1): for pat in GLOBAL_FAIL_PATTERNS: - if pat in line and (rel_path, pat) not in ALLOWED_GLOBAL_VIOLATIONS: - violations.append((i, pat)) + if pat not in line or (rel_path, pat) in ALLOWED_GLOBAL_VIOLATIONS: + continue + if ( + pat == "NotImplementedError" + and i in allowed_exception_lines + and not re.search(r"\\braise\\s+NotImplementedError\\b", line) + ): + continue + violations.append((i, pat)) return violations @@ -174,7 +218,182 @@ def _check_word_placeholder_stub(path: Path, root: Path, lines: list[str]) -> li if _is_secret_scrubber(rel_path) and "placeholder" in line.lower(): continue if "placeholder" in line.lower() and not allow_placeholder: - violations.append((i, "placeholder")) + # These two literal schema fields describe compatibility metadata, + # not an unfinished security policy or execution implementation. + canonical_schema = "policy/schemas/pcs/LabtrustBenchmarkExtension.v0.schema.json" + schema_field = re.match(r'^\\s*"placeholder_fields":\\s*\\{\\s* + # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" + if "stub" in line.lower() and not allow_stub: + if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): + violations.append((i, "stub")) + elif not rel_path.startswith("tests/"): + violations.append((i, "stub")) + return violations + + +def _git_tracked_files(root: Path) -> list[Path] | None: + """Return list of tracked file paths under root, or None if not a git repo / git unavailable / empty.""" + try: + r = subprocess.run( + ["git", "ls-files", "--cached", "-z"], + cwd=str(root), + capture_output=True, + text=False, + timeout=30, + ) + if r.returncode != 0: + return None + paths: list[Path] = [] + for raw in r.stdout.split(b"\x00"): + if not raw: + continue + path = root / raw.decode("utf-8", errors="replace") + if path.is_file() and _should_scan_file(path, root): + paths.append(path) + # Use tracked list only when non-empty (real repo); else fall back to rglob (e.g. test tmp_path) + return paths if paths else None + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def _iter_scannable_files(root: Path) -> list[Path]: + """Yield all scannable file paths under root. Prefer git-tracked when repo has tracked files (faster, CI-aligned).""" + root = root.resolve() + tracked = _git_tracked_files(root) + if tracked is not None and len(tracked) > 0: + return tracked + out: list[Path] = [] + for path in root.rglob("*"): + if not path.is_file(): + continue + if not _should_scan_file(path, root): + continue + out.append(path) + return out + + +def scan_root(root: Path) -> list[tuple[Path, int, str]]: + root = root.resolve() + results: list[tuple[Path, int, str]] = [] + + for path in _iter_scannable_files(root): + lines = _read_lines(path) + if lines is None: + continue + + for line_no, token in _check_global_fails(path, root, lines): + results.append((path, line_no, token)) + for line_no, token in _check_word_placeholder_stub(path, root, lines): + results.append((path, line_no, token)) + + return results + + +def main() -> int: + root = Path.cwd() + if len(sys.argv) > 1: + root = Path(sys.argv[1]).resolve() + + violations = scan_root(root) + for path, line_no, token in sorted(violations, key=lambda x: (str(x[0]), x[1])): + rel = _norm_path(path, root) + print(f"{rel}:{line_no}: {token!r}") + + return 1 if violations else 0 + + +if __name__ == "__main__": + sys.exit(main()) +, line) + schema_description = re.match( + r'^\\s*"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases\\.",?\\s* + # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" + if "stub" in line.lower() and not allow_stub: + if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): + violations.append((i, "stub")) + elif not rel_path.startswith("tests/"): + violations.append((i, "stub")) + return violations + + +def _git_tracked_files(root: Path) -> list[Path] | None: + """Return list of tracked file paths under root, or None if not a git repo / git unavailable / empty.""" + try: + r = subprocess.run( + ["git", "ls-files", "--cached", "-z"], + cwd=str(root), + capture_output=True, + text=False, + timeout=30, + ) + if r.returncode != 0: + return None + paths: list[Path] = [] + for raw in r.stdout.split(b"\x00"): + if not raw: + continue + path = root / raw.decode("utf-8", errors="replace") + if path.is_file() and _should_scan_file(path, root): + paths.append(path) + # Use tracked list only when non-empty (real repo); else fall back to rglob (e.g. test tmp_path) + return paths if paths else None + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def _iter_scannable_files(root: Path) -> list[Path]: + """Yield all scannable file paths under root. Prefer git-tracked when repo has tracked files (faster, CI-aligned).""" + root = root.resolve() + tracked = _git_tracked_files(root) + if tracked is not None and len(tracked) > 0: + return tracked + out: list[Path] = [] + for path in root.rglob("*"): + if not path.is_file(): + continue + if not _should_scan_file(path, root): + continue + out.append(path) + return out + + +def scan_root(root: Path) -> list[tuple[Path, int, str]]: + root = root.resolve() + results: list[tuple[Path, int, str]] = [] + + for path in _iter_scannable_files(root): + lines = _read_lines(path) + if lines is None: + continue + + for line_no, token in _check_global_fails(path, root, lines): + results.append((path, line_no, token)) + for line_no, token in _check_word_placeholder_stub(path, root, lines): + results.append((path, line_no, token)) + + return results + + +def main() -> int: + root = Path.cwd() + if len(sys.argv) > 1: + root = Path(sys.argv[1]).resolve() + + violations = scan_root(root) + for path, line_no, token in sorted(violations, key=lambda x: (str(x[0]), x[1])): + rel = _norm_path(path, root) + print(f"{rel}:{line_no}: {token!r}") + + return 1 if violations else 0 + + +if __name__ == "__main__": + sys.exit(main()) +, + line, + ) + if rel_path != canonical_schema or not (schema_field or schema_description): + violations.append((i, "placeholder")) # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" if "stub" in line.lower() and not allow_stub: if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): From 2c0d741b5724deca950a262a3e31fd92731fed1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:04:58 -0700 Subject: [PATCH 11/20] fix(audit): exact canonical schema terms and validate scanner syntax --- tools/no_placeholders.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/no_placeholders.py b/tools/no_placeholders.py index a1d345ba..0649e851 100644 --- a/tools/no_placeholders.py +++ b/tools/no_placeholders.py @@ -184,7 +184,7 @@ def _check_global_fails(path: Path, root: Path, lines: list[str]) -> list[tuple[ if ( pat == "NotImplementedError" and i in allowed_exception_lines - and not re.search(r"\\braise\\s+NotImplementedError\\b", line) + and not re.search(r"\braise\s+NotImplementedError\b", line) ): continue violations.append((i, pat)) @@ -218,10 +218,15 @@ def _check_word_placeholder_stub(path: Path, root: Path, lines: list[str]) -> li if _is_secret_scrubber(rel_path) and "placeholder" in line.lower(): continue if "placeholder" in line.lower() and not allow_placeholder: - # These two literal schema fields describe compatibility metadata, - # not an unfinished security policy or execution implementation. + # Exact schema declarations describe compatibility metadata; + # unrelated unfinished policy text still fails the scan. canonical_schema = "policy/schemas/pcs/LabtrustBenchmarkExtension.v0.schema.json" - schema_field = re.match(r'^\\s*"placeholder_fields":\\s*\\{\\s* + recognized_fields = { + '"placeholder_fields": {', + '"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases.",', + } + if rel_path != canonical_schema or line.strip() not in recognized_fields: + violations.append((i, "placeholder")) # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" if "stub" in line.lower() and not allow_stub: if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): From fec540965dfb452465b5d7fd15f5c5d903bf1b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:05:16 -0700 Subject: [PATCH 12/20] test(audit): reject real stubs while permitting handlers, declared schema fields, and templates --- tests/test_no_placeholders.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_no_placeholders.py b/tests/test_no_placeholders.py index 77c0a9be..b162843c 100644 --- a/tests/test_no_placeholders.py +++ b/tests/test_no_placeholders.py @@ -162,3 +162,56 @@ def test_clean_tree_no_violations(tmp_path: Path) -> None: ) violations = scan_root(tmp_path) assert not violations + + +def test_exception_types_are_distinct_from_unimplemented_functions(tmp_path: Path) -> None: + """A handler and a custom exception class are legal; an unfinished function fails.""" + _write_tree( + tmp_path, + { + "src/types.py": ( + "class Unavailable(NotImplementedError):\\n pass\\n" + "def handle():\\n" + " try:\\n return 1\\n" + " except (ValueError, NotImplementedError):\\n return 0\\n" + ), + "src/unimplemented.py": "def unfinished():\\n raise NotImplementedError('TODO')\\n", + }, + ) + problems = [ + _norm_path(p, tmp_path) + for p, _, token in scan_root(tmp_path) + if token == "NotImplementedError" + ] + assert problems == ["src/unimplemented.py"] + + +def test_template_scaffold_is_excluded_without_excluding_production(tmp_path: Path) -> None: + _write_tree( + tmp_path, + { + "templates/pcs_workflow_template/workflow.py": "raise NotImplementedError('template')\\n", + "src/actual.py": "raise NotImplementedError('production')\\n", + }, + ) + problems = [_norm_path(p, tmp_path) for p, _, _ in scan_root(tmp_path)] + assert problems == ["src/actual.py"] + + +def test_schema_fields_are_precisely_exempt_and_other_policy_markers_fail( + tmp_path: Path, +) -> None: + _write_tree( + tmp_path, + { + "policy/schemas/pcs/LabtrustBenchmarkExtension.v0.schema.json": ( + '"placeholder_fields": {\\n' + '"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases.",\\n' + '"description": "Unexpected placeholder claim.",\\n' + ), + }, + ) + failures = [ + (line, token) for _, line, token in scan_root(tmp_path) if token == "placeholder" + ] + assert failures == [(3, "placeholder")] From 0825656a1143a8ef02fa5304ba01dd79b05d1302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:05:33 -0700 Subject: [PATCH 13/20] test(audit): correct generated fixture newline escapes --- tests/test_no_placeholders.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_no_placeholders.py b/tests/test_no_placeholders.py index b162843c..cd0c64a1 100644 --- a/tests/test_no_placeholders.py +++ b/tests/test_no_placeholders.py @@ -170,12 +170,12 @@ def test_exception_types_are_distinct_from_unimplemented_functions(tmp_path: Pat tmp_path, { "src/types.py": ( - "class Unavailable(NotImplementedError):\\n pass\\n" - "def handle():\\n" - " try:\\n return 1\\n" - " except (ValueError, NotImplementedError):\\n return 0\\n" + "class Unavailable(NotImplementedError):\n pass\n" + "def handle():\n" + " try:\n return 1\n" + " except (ValueError, NotImplementedError):\n return 0\n" ), - "src/unimplemented.py": "def unfinished():\\n raise NotImplementedError('TODO')\\n", + "src/unimplemented.py": "def unfinished():\n raise NotImplementedError('TODO')\n", }, ) problems = [ @@ -190,8 +190,8 @@ def test_template_scaffold_is_excluded_without_excluding_production(tmp_path: Pa _write_tree( tmp_path, { - "templates/pcs_workflow_template/workflow.py": "raise NotImplementedError('template')\\n", - "src/actual.py": "raise NotImplementedError('production')\\n", + "templates/pcs_workflow_template/workflow.py": "raise NotImplementedError('template')\n", + "src/actual.py": "raise NotImplementedError('production')\n", }, ) problems = [_norm_path(p, tmp_path) for p, _, _ in scan_root(tmp_path)] @@ -205,9 +205,9 @@ def test_schema_fields_are_precisely_exempt_and_other_policy_markers_fail( tmp_path, { "policy/schemas/pcs/LabtrustBenchmarkExtension.v0.schema.json": ( - '"placeholder_fields": {\\n' - '"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases.",\\n' - '"description": "Unexpected placeholder claim.",\\n' + '"placeholder_fields": {\n' + '"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases.",\n' + '"description": "Unexpected placeholder claim.",\n' ), }, ) From 96c55c43a21e86c48fdf1988bd2561256137ec19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:05:48 -0700 Subject: [PATCH 14/20] fix(wheel): install pytest-timeout required by strict root pytest configuration --- .github/workflows/wheel-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheel-smoke.yml b/.github/workflows/wheel-smoke.yml index 9fd1eef0..ba25c3f9 100644 --- a/.github/workflows/wheel-smoke.yml +++ b/.github/workflows/wheel-smoke.yml @@ -26,7 +26,7 @@ jobs: python -m venv .venv-wheel .venv-wheel/bin/pip install dist/*.whl .venv-wheel/bin/pip install '.[env]' # quick-eval uses env - .venv-wheel/bin/pip install pytest # audit-selfcheck doctor (extras_dev) and risk_register_contract_gate + .venv-wheel/bin/pip install 'pytest>=8,<10' 'pytest-timeout>=2.3,<3' # repo addopts includes --timeout - name: Audit selfcheck (doctor) run: .venv-wheel/bin/labtrust audit-selfcheck --out ./audit_out - name: Show audit diagnostics on failure From 073d773baec3b06bf444753ae5e4f0e1436b77fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:05:58 -0700 Subject: [PATCH 15/20] fix(ci): bind PCS tests to the pinned checked-out schema repository --- .github/workflows/pcs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pcs.yml b/.github/workflows/pcs.yml index b1e0c189..1c7e5c57 100644 --- a/.github/workflows/pcs.yml +++ b/.github/workflows/pcs.yml @@ -65,6 +65,8 @@ on: jobs: pcs: runs-on: ubuntu-latest + env: + PCS_CORE_PATH: ${{ github.workspace }}/pcs-core defaults: run: shell: bash From c742a016554d8bcc32b833971942897ddb010b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:08:57 -0700 Subject: [PATCH 16/20] fix(audit): remove duplicated malformed trailing scanner content --- tools/no_placeholders.py | 172 --------------------------------------- 1 file changed, 172 deletions(-) diff --git a/tools/no_placeholders.py b/tools/no_placeholders.py index 0649e851..8c642e55 100644 --- a/tools/no_placeholders.py +++ b/tools/no_placeholders.py @@ -307,177 +307,5 @@ def main() -> int: return 1 if violations else 0 -if __name__ == "__main__": - sys.exit(main()) -, line) - schema_description = re.match( - r'^\\s*"description": "Documents placeholder benchmark_case fields when pcs-core expects null on valid cases\\.",?\\s* - # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" - if "stub" in line.lower() and not allow_stub: - if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): - violations.append((i, "stub")) - elif not rel_path.startswith("tests/"): - violations.append((i, "stub")) - return violations - - -def _git_tracked_files(root: Path) -> list[Path] | None: - """Return list of tracked file paths under root, or None if not a git repo / git unavailable / empty.""" - try: - r = subprocess.run( - ["git", "ls-files", "--cached", "-z"], - cwd=str(root), - capture_output=True, - text=False, - timeout=30, - ) - if r.returncode != 0: - return None - paths: list[Path] = [] - for raw in r.stdout.split(b"\x00"): - if not raw: - continue - path = root / raw.decode("utf-8", errors="replace") - if path.is_file() and _should_scan_file(path, root): - paths.append(path) - # Use tracked list only when non-empty (real repo); else fall back to rglob (e.g. test tmp_path) - return paths if paths else None - except (FileNotFoundError, subprocess.TimeoutExpired): - return None - - -def _iter_scannable_files(root: Path) -> list[Path]: - """Yield all scannable file paths under root. Prefer git-tracked when repo has tracked files (faster, CI-aligned).""" - root = root.resolve() - tracked = _git_tracked_files(root) - if tracked is not None and len(tracked) > 0: - return tracked - out: list[Path] = [] - for path in root.rglob("*"): - if not path.is_file(): - continue - if not _should_scan_file(path, root): - continue - out.append(path) - return out - - -def scan_root(root: Path) -> list[tuple[Path, int, str]]: - root = root.resolve() - results: list[tuple[Path, int, str]] = [] - - for path in _iter_scannable_files(root): - lines = _read_lines(path) - if lines is None: - continue - - for line_no, token in _check_global_fails(path, root, lines): - results.append((path, line_no, token)) - for line_no, token in _check_word_placeholder_stub(path, root, lines): - results.append((path, line_no, token)) - - return results - - -def main() -> int: - root = Path.cwd() - if len(sys.argv) > 1: - root = Path(sys.argv[1]).resolve() - - violations = scan_root(root) - for path, line_no, token in sorted(violations, key=lambda x: (str(x[0]), x[1])): - rel = _norm_path(path, root) - print(f"{rel}:{line_no}: {token!r}") - - return 1 if violations else 0 - - -if __name__ == "__main__": - sys.exit(main()) -, - line, - ) - if rel_path != canonical_schema or not (schema_field or schema_description): - violations.append((i, "placeholder")) - # In tests/, allow capital-S "Stub" (e.g. StubTask); fail only on lowercase "stub" - if "stub" in line.lower() and not allow_stub: - if rel_path.startswith("tests/") and re.search(r"\bstub\b", line): - violations.append((i, "stub")) - elif not rel_path.startswith("tests/"): - violations.append((i, "stub")) - return violations - - -def _git_tracked_files(root: Path) -> list[Path] | None: - """Return list of tracked file paths under root, or None if not a git repo / git unavailable / empty.""" - try: - r = subprocess.run( - ["git", "ls-files", "--cached", "-z"], - cwd=str(root), - capture_output=True, - text=False, - timeout=30, - ) - if r.returncode != 0: - return None - paths: list[Path] = [] - for raw in r.stdout.split(b"\x00"): - if not raw: - continue - path = root / raw.decode("utf-8", errors="replace") - if path.is_file() and _should_scan_file(path, root): - paths.append(path) - # Use tracked list only when non-empty (real repo); else fall back to rglob (e.g. test tmp_path) - return paths if paths else None - except (FileNotFoundError, subprocess.TimeoutExpired): - return None - - -def _iter_scannable_files(root: Path) -> list[Path]: - """Yield all scannable file paths under root. Prefer git-tracked when repo has tracked files (faster, CI-aligned).""" - root = root.resolve() - tracked = _git_tracked_files(root) - if tracked is not None and len(tracked) > 0: - return tracked - out: list[Path] = [] - for path in root.rglob("*"): - if not path.is_file(): - continue - if not _should_scan_file(path, root): - continue - out.append(path) - return out - - -def scan_root(root: Path) -> list[tuple[Path, int, str]]: - root = root.resolve() - results: list[tuple[Path, int, str]] = [] - - for path in _iter_scannable_files(root): - lines = _read_lines(path) - if lines is None: - continue - - for line_no, token in _check_global_fails(path, root, lines): - results.append((path, line_no, token)) - for line_no, token in _check_word_placeholder_stub(path, root, lines): - results.append((path, line_no, token)) - - return results - - -def main() -> int: - root = Path.cwd() - if len(sys.argv) > 1: - root = Path(sys.argv[1]).resolve() - - violations = scan_root(root) - for path, line_no, token in sorted(violations, key=lambda x: (str(x[0]), x[1])): - rel = _norm_path(path, root) - print(f"{rel}:{line_no}: {token!r}") - - return 1 if violations else 0 - - if __name__ == "__main__": sys.exit(main()) From 46e4e77021a59096b82c68d28db48014310e3df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:09:22 -0700 Subject: [PATCH 17/20] fix(schema): admit typed coordination-risk run evidence produced by strict risk coverage --- policy/schemas/risk_register_bundle.v0.1.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policy/schemas/risk_register_bundle.v0.1.schema.json b/policy/schemas/risk_register_bundle.v0.1.schema.json index 456b17fe..a2059764 100644 --- a/policy/schemas/risk_register_bundle.v0.1.schema.json +++ b/policy/schemas/risk_register_bundle.v0.1.schema.json @@ -137,7 +137,7 @@ "evidence_id": { "type": "string" }, "type": { "type": "string", - "enum": ["security_suite", "coordination_study", "coordination_pack", "official_pack", "safety_case", "bundle_verification", "other"] + "enum": ["security_suite", "coordination_study", "coordination_pack", "coord_risk_run", "official_pack", "safety_case", "bundle_verification", "other"] }, "path": { "type": "string", "description": "Repo-relative or run-relative path; empty or omitted for evidence gaps (status=missing)." }, "label": { "type": "string" }, From 191eb91596167ee0dbf903e001e1b6d5bfc36c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:09:31 -0700 Subject: [PATCH 18/20] test(schema): accept coordination-risk evidence and reject unknown evidence types --- tests/test_risk_register_bundle.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_risk_register_bundle.py b/tests/test_risk_register_bundle.py index 82e523bc..f5a5112e 100644 --- a/tests/test_risk_register_bundle.py +++ b/tests/test_risk_register_bundle.py @@ -271,3 +271,26 @@ def test_coord_summary_includes_metrics(tmp_path: Path) -> None: assert len(rows) >= 1 row = rows[0] assert "sec.attack_success_rate" in row or "robustness.resilience_score" in row + +def test_coord_risk_evidence_type_is_supported_without_accepting_unknown_types() -> None: + """Preserve the evidence type emitted by the strict coordination-risk producer.""" + root = _repo_root() + bundle = build_risk_register_bundle( + root, + run_dirs=[], + include_generated_at=False, + include_git_hash=False, + ) + entry = { + "evidence_id": "EVID-COORD-RISK-SYNTHETIC", + "type": "coord_risk_run", + "status": "present", + "path": "runs/coord_risk_fixture/coord_risk_results.json", + "risk_ids": ["R-SYS-001"], + } + bundle["evidence"].append(entry) + assert validate_bundle_against_schema(bundle, root) == [] + + entry["type"] = "unrecognized_evidence_class" + assert validate_bundle_against_schema(bundle, root) + From 910d69991bcf196017347b518e6b45be458dd543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:12:50 -0700 Subject: [PATCH 19/20] fix(wheel): resolve reward policy schemas through configured policy root --- .../verifier_assurance/reward/composition.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/labtrust_gym/verifier_assurance/reward/composition.py b/src/labtrust_gym/verifier_assurance/reward/composition.py index 98058d5b..b1d119ee 100644 --- a/src/labtrust_gym/verifier_assurance/reward/composition.py +++ b/src/labtrust_gym/verifier_assurance/reward/composition.py @@ -6,6 +6,7 @@ import hashlib from typing import Any, Mapping +from labtrust_gym.config import get_repo_root from labtrust_gym.errors import PolicyLoadError from labtrust_gym.policy.loader import load_json, validate_against_schema from labtrust_gym.util.json_utils import canonical_json @@ -26,20 +27,9 @@ "safety_violation_penalty", ) -_SCHEMA_PATH = ( - Path(__file__).resolve().parents[4] - / "policy" - / "schemas" - / "verifier_assurance" - / "RewardCompositionPolicy.v1.schema.json" -) -_ENVELOPE_SCHEMA_PATH = ( - Path(__file__).resolve().parents[4] - / "policy" - / "schemas" - / "pcs" - / "RewardEvidenceEnvelope.v1.schema.json" -) +def _reward_schema_path(*parts: str) -> Path: + """Resolve a policy schema using package, checkout, or explicit configuration.""" + return get_repo_root() / "policy" / "schemas" / Path(*parts) class RewardCompositionError(ValueError): @@ -52,8 +42,9 @@ def compute_policy_digest(policy: dict[str, Any]) -> str: def validate_composition_policy(policy: dict[str, Any]) -> dict[str, Any]: + schema_path = _reward_schema_path("verifier_assurance", "RewardCompositionPolicy.v1.schema.json") try: - validate_against_schema(policy, load_json(_SCHEMA_PATH), path=_SCHEMA_PATH) + validate_against_schema(policy, load_json(schema_path), path=schema_path) except PolicyLoadError as exc: raise RewardCompositionError(str(exc)) from exc weights = policy.get("weights") or {} @@ -209,8 +200,9 @@ def build_reward_evidence_envelope( "public_decision": public_decision, "claim_boundary": CLAIM_BOUNDARY, } + schema_path = _reward_schema_path("pcs", "RewardEvidenceEnvelope.v1.schema.json") try: - validate_against_schema(envelope, load_json(_ENVELOPE_SCHEMA_PATH), path=_ENVELOPE_SCHEMA_PATH) + validate_against_schema(envelope, load_json(schema_path), path=schema_path) except PolicyLoadError as exc: raise RewardCompositionError(str(exc)) from exc return envelope From 909eaa473aedfbf2288ec6aae18b35e71c6b3ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o=20H=2E=20Petel?= <113530345+fraware@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:13:05 -0700 Subject: [PATCH 20/20] test(wheel): verify both reward schemas honor configured policy root --- .../test_va03_reward_composition.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/verifier_assurance/test_va03_reward_composition.py b/tests/verifier_assurance/test_va03_reward_composition.py index f84fb0e2..3641150f 100644 --- a/tests/verifier_assurance/test_va03_reward_composition.py +++ b/tests/verifier_assurance/test_va03_reward_composition.py @@ -3,11 +3,13 @@ from __future__ import annotations import copy +from pathlib import Path import pytest from labtrust_gym.verifier_assurance.reward.composition import ( COMPONENT_VOCAB, + _reward_schema_path, RewardComposer, RewardCompositionError, apply_legacy_rewards, @@ -116,3 +118,17 @@ def test_emit_component_breakdown_when_enabled() -> None: assert rewards["ops_0"] == 1.0 assert breakdown is not None assert breakdown["ops_0"]["operational_success"] == 1.0 + +def test_reward_schema_paths_respect_explicit_policy_root(monkeypatch: pytest.MonkeyPatch) -> None: + """An installed module must resolve schemas through its configured policy root.""" + repo = Path(__file__).resolve().parents[2] + monkeypatch.setenv("LABTRUST_POLICY_DIR", str(repo / "policy")) + composition_schema = _reward_schema_path( + "verifier_assurance", "RewardCompositionPolicy.v1.schema.json" + ) + envelope_schema = _reward_schema_path("pcs", "RewardEvidenceEnvelope.v1.schema.json") + assert composition_schema.is_file() + assert envelope_schema.is_file() + assert composition_schema.is_relative_to(repo / "policy") + assert envelope_schema.is_relative_to(repo / "policy") +