diff --git a/.github/workflows/skill-eval-pr.yml b/.github/workflows/skill-eval-pr.yml index eefd7c3d..85141f06 100644 --- a/.github/workflows/skill-eval-pr.yml +++ b/.github/workflows/skill-eval-pr.yml @@ -140,6 +140,8 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.workflow_sha }} + # Report-only retries authenticate the original verifier as a main ancestor. + fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@v5 with: @@ -160,6 +162,7 @@ jobs: name: skill-eval-report path: | prepared/pr-plan.json + prepared/report-renderer.json prepared/*/report.json prepared/*/report.md retention-days: 30 diff --git a/docs/skill-eval-pr.md b/docs/skill-eval-pr.md index 43829b5d..147ac8cf 100644 --- a/docs/skill-eval-pr.md +++ b/docs/skill-eval-pr.md @@ -383,3 +383,32 @@ that a small case covers every mechanism in those files. Unmapped generated projections, navigation documents and tests remain visible coverage gaps. Source-only review dispositions must be explicit and must not be represented as model-evaluated passes. The maintained mapping is not expanded automatically. + +## Retained-result explanations + +The automated comment leads with bounded baseline/candidate components, submitted +finite diagnostic fields, and fixed failure reasons derived after trusted regrading. +A component improvement does not change the frozen aggregate: a strict citation +failure remains `FAIL` even when diagnosis and next-check components improve. +Missing, invalid or unverified components are unavailable, not failures. No raw +model explanation, hidden oracle answer or arbitrary answer field is published. +Synthetic diagnostic coverage is not an engine benchmark or general efficacy claim. + +An explicit `publish` retry with the original `source_run` can refresh presentation +without a new paid evaluation. It authenticates the original workflow, complete +plan digest and verified ledger record, current PR base/head, retention and frozen +inputs. Historical controller compatibility is established against the original +trusted-main ancestor, not by relaxing hashes: an isolated report-only subprocess +runs that Git source with no provider/GitHub environment and never executes +artifact Python. The comment distinguishes the original execution implementation +from the current presentation implementation. Current explanatory projection also +requires the identical builtin grader and schema; otherwise components are unavailable. +Failures stop the retry rather than replacing available evidence with a fabricated +new result. New-run authorization/accounting and retained-result validity are separate. + +Use the existing workflow dispatch with `action=publish`, the current PR number +and original `source_run`; do not request `execute` or provide a new approval. +This bridge supports completed first-attempt automatic (`pull_request_target`) +source runs with a verified ledger binding. Missing Git ancestry, mismatched live +revisions, expired artifacts or unauthenticated provenance stop publication. +The uploaded report contains the original plan and separate renderer metadata. diff --git a/tests/test_skill_eval_feedback.py b/tests/test_skill_eval_feedback.py new file mode 100644 index 00000000..88823514 --- /dev/null +++ b/tests/test_skill_eval_feedback.py @@ -0,0 +1,250 @@ +"""Offline public projection checks: no model/provider calls or grader changes.""" +import copy +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +from inferrsi.harness import skill_eval_diagnostic as diagnostic +from inferrsi.infra.artifacts import sha256, write_json +from tests.fixtures.skill_eval_compare_support import FixedOutputClient +from tests.fixtures.skill_eval_pr_support import approval, setup +from tools import skill_eval_feedback as feedback +from tools import skill_eval_pr as integration + +ROOT = Path(__file__).resolve().parents[1] +TASK_PATH = ROOT / 'docs/examples/skill-comparison/pr133-diagnostic-case05.json' +TASK = json.loads(TASK_PATH.read_text())[0] +OLD = dict(diagnosis='capacity', evidence=['E4'], next_check='inspect_allocator_accounting', + reuse_authorized=False) +NEW = dict(diagnosis='readiness', evidence=['E2', 'E4'], next_check='check_receive_completion', + reuse_authorized=False) + + +def trial(answer): + checks = diagnostic.verify(TASK, answer) + return dict(verdict='PASS' if checks['passed'] else 'FAIL', grader_trust='reverified', + feedback=feedback.projection(diagnostic.ADAPTER, TASK, answer, checks)) + + +def fixture(): + exam = dict(cases='f' * 64, schema='a' * 64, grader='9' * 64) + plan = dict(repository='example/fixture', pr=7, base='b' * 40, head='c' * 40, + version='d' * 40, model=dict(name='offline', effort='test'), + limits=dict(repeats=1, max_task_calls=2, timeout_seconds=60, + campaign_seconds=600, token_stop_new_trials=1000), + estimated_calls=2, coverage=[], comparisons=[dict( + skill='selected', mode='update', calls=2, plan_sha256='e' * 64, + cases=[dict(id='case-05', split='holdout')], exam=exam)]) + result = dict(skill='selected', plan_sha256='e' * 64, exam=exam, + arms={r: dict(git_revision=plan[k]) for r, k in + [('baseline', 'base'), ('candidate', 'head')]}, + status=dict(execution_state='completed'), discovery=[], + pairs=[dict(case_id='case-05', repeat=1, outcome='unchanged_incorrect', + baseline=trial(OLD), candidate=trial(NEW))], + counts={k: int(k == 'unchanged_incorrect') for k in + ('fix', 'regression', 'unchanged_correct', 'unchanged_incorrect', + 'inconclusive', 'missing_pair')}) + return plan, [result] + + +def render(plan, results, **kwargs): + return integration.summary(plan, results, 'https://example.invalid/evidence', 100, 1, **kwargs) + + +class FeedbackTests(unittest.TestCase): + def test_retained_case_changes_components_not_total(self): + plan, results = fixture() + text = render(plan, results) + self.assertIn('unchanged_incorrect', text) + self.assertEqual(text.count('diagnosis=FAIL'), 1) + self.assertEqual(text.count('diagnosis=PASS'), 1) + self.assertEqual(text.count('citation=FAIL'), 2) + self.assertEqual(text.count('reuse_authorized=false'), 2) + self.assertIn('Extra/nondecisive citation', text) + self.assertIn('capacity', text) + self.assertIn('readiness', text) + self.assertNotIn('expected', text.lower()) + self.assertLess(text.index('Retained baseline'), text.index('### Advisory')) + + def test_component_regression_with_unchanged_strict_failure(self): + plan, results = fixture() + pair = results[0]['pairs'][0] + pair['baseline'], pair['candidate'] = pair['candidate'], pair['baseline'] + text = render(plan, results) + self.assertLess(text.index('diagnosis=PASS'), text.index('diagnosis=FAIL')) + self.assertIn('unchanged_incorrect', text) + + def test_execution_block_does_not_obscure_evidence(self): + plan, results = fixture() + plan['execution_blocked'] = 'accounting unavailable' + text = render(plan, results) + self.assertLess(text.index('readiness'), text.index('Paid execution blocked')) + self.assertIn('does not invalidate available retained results', text) + + def test_bounded_block_reason_keeps_explicit_no_fallback_safety_clause(self): + plan, results = fixture() + plan["execution_blocked"] = "@someone `unsafe` " * 1000 + text = render(plan, results) + self.assertIn("no alternate provider/model is selected", text) + self.assertIn("does not invalidate available retained results", text) + self.assertEqual(text.count(""), 2) + self.assertNotIn("@someone", text) + self.assertNotIn("`unsafe`", text) + self.assertLess(len(text), 10000) + + def test_unavailable_and_schema_failure_are_not_component_failures(self): + for t in ({}, dict(verdict='INCONCLUSIVE', grader_trust='reverified'), + dict(verdict='FAIL', grader_trust='unverified')): + self.assertIn('unavailable', feedback.trial_text(t)) + self.assertNotIn('diagnosis=FAIL', feedback.trial_text(t)) + text = feedback.trial_text(trial({'reason': 'secret-canary'})) + self.assertIn('schema=FAIL', text) + self.assertIn('diagnosis=unavailable', text) + self.assertNotIn('secret-canary', text) + + def test_citation_reason_classification(self): + for ids, present, absent in [(['E2', 'E4'], 'extra_citation', 'missing_citation'), + ([], 'missing_citation', 'extra_citation'), + (['E2', 'E2'], 'duplicate_citation', 'extra_citation')]: + data = trial({**NEW, 'evidence': ids})['feedback'] + self.assertIn(present, data['reasons']) + self.assertNotIn(absent, data['reasons']) + + def test_oversized_and_arbitrary_fields_are_not_published(self): + for ids in [['E' + '1' * 10000], ['E2'] * 10000, ['secret-canary']]: + text = feedback.trial_text(trial({**NEW, 'evidence': ids})) + self.assertNotIn('secret-canary', text) + self.assertLess(len(text), 1500) + for field, value in [('diagnosis', '
@secret-canary'), + ('reuse_authorized', 1), ('next_check', {'secret-canary': 1})]: + text = feedback.trial_text(trial({**NEW, field: value})) + self.assertNotIn('secret-canary', text) + t = trial(NEW) + t['feedback']['components']['citation'] = 1 + self.assertIn('invalid projection', feedback.trial_text(t)) + + def test_mismatched_and_unavailable_evidence_suppresses_details_and_counts(self): + for mutation in ('head', 'base', 'exam', 'plan_sha256', 'duplicate'): + plan, results = fixture() + if mutation in ('head', 'base'): + plan[mutation] = 'f' * 40 + elif mutation == 'duplicate': + results.append(copy.deepcopy(results[0])) + else: + results[0][mutation] = 'wrong' + text = render(plan, results) + self.assertNotIn('readiness', text) + self.assertNotIn('"unchanged_incorrect":1', text) + for state in ('expired', 'missing', 'invalid'): + plan, results = fixture() + text = render(plan, results, evidence_state=state) + self.assertNotIn('readiness', text) + self.assertNotIn('"unchanged_incorrect":1', text) + + def test_projection_is_bounded_and_never_echoes_freeform_or_oracle(self): + t = trial(NEW) + t['feedback']['answer']['reason'] = 'secret-canary' + t['feedback']['oracle'] = 'hidden-secret-canary' + self.assertNotIn('secret-canary', feedback.trial_text(t)) + plan, results = fixture() + results[0]['pairs'][0]['case_id'] = '
@secret-canary' + self.assertNotIn('secret-canary', '\n'.join(feedback.comparison_lines( + plan, results, 'available'))) + self.assertEqual(feedback.code('@secret-canary'), 'unavailable') + + def test_actual_reports_regrade_and_replace_untrusted_projection(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + repo, mapping, identity = setup(root, mode="addition", ids=("one",)) + output = root / "evaluation" + plan = integration.prepare(repo, mapping, identity, output) + integration.execute(output, approval(plan), client=FixedOutputClient(root)) + result = integration.reports(output)[0] + self.assertEqual(result["pairs"][0]["candidate"]["feedback"]["adapter"], "audit") + text = render(plan, [result]) + self.assertIn("report_verdict=PASS", text) + path = output / "selected/results/one-candidate-1/result.json" + record = json.loads(path.read_text()) + record["feedback"] = {"answer": "secret-canary", "components": {"injected": True}} + write_json(path, record) + write_json(path.with_name("receipt.json"), {"result_sha256": sha256(path)}) + text = render(plan, integration.reports(output)) + self.assertNotIn("secret-canary", text) + self.assertNotIn("injected", text) + record["checks"]["assertions"]["report_verdict"] = False + write_json(path, record) + write_json(path.with_name("receipt.json"), {"result_sha256": sha256(path)}) + with self.assertRaisesRegex(ValueError, "Untrusted grading"): + integration.reports(output) + + def test_missing_pair_and_consistently_hostile_metadata(self): + plan, results = fixture() + results[0]["pairs"][0]["candidate"] = {"verdict": "INCONCLUSIVE", "missing": True} + text = render(plan, results) + self.assertIn("Candidate: unavailable", text) + self.assertNotIn("Candidate: FAIL", text) + bad = "
\n@someone `evil` | # forged" + plan["comparisons"][0]["skill"] = results[0]["skill"] = bad + plan["comparisons"][0]["cases"][0]["id"] = bad + results[0]["pairs"][0]["case_id"] = bad + text = render(plan, results) + self.assertEqual(text.count("
"), 2) + self.assertNotIn("@someone", text) + self.assertNotIn("`evil`", text) + self.assertNotIn("\n# forged", text) + + def test_changed_grader_cannot_generate_current_explanations(self): + result = {"pairs": [{"baseline": trial(OLD), "candidate": trial(NEW)}], + "discovery": []} + context = dict(adapter=diagnostic.ADAPTER, schema=diagnostic.SCHEMA, + exam={"grader": "f" * 64}, tasks={TASK["id"]: TASK}) + integration.attach_feedback(result, context) + self.assertNotIn("feedback", result["pairs"][0]["baseline"]) + + def test_historical_publish_preserves_original_plan_and_new_writer(self): + plan, results = fixture() + context = dict(adapter=diagnostic.ADAPTER, schema=diagnostic.SCHEMA, + exam={"grader": "f" * 64}, tasks={TASK["id"]: TASK}) + retained = dict(plan=plan, reports=results, contexts={"selected": context}, + expires_at=100, evidence_url="https://example.invalid/evidence") + api = Mock() + api.publish.return_value = {"html_url": "https://example.invalid/comment"} + with tempfile.TemporaryDirectory() as temporary, \ + patch("tools.skill_eval_retained.retained_reports", return_value=retained), \ + patch.object(integration, "git", return_value=b"a" * 40): + output = Path(temporary) + integration.publish_historical(api, 7, 10, 20, now=1, output=output) + self.assertEqual(json.loads((output / "pr-plan.json").read_text()), plan) + self.assertEqual(json.loads((output / "report-renderer.json").read_text())[ + "original_plan_sha256"], integration.fingerprint(plan)) + self.assertTrue((output / "selected/report.json").is_file()) + args = api.publish.call_args.args + self.assertIs(args[0], plan) + self.assertEqual(args[2], 20) + self.assertIn("Presentation implementation: `" + "a" * 40, args[1]) + self.assertIn("starts no model execution or budget reservation", args[1]) + + def test_copyable_json_and_metadata_escape_are_not_double_escaped(self): + value = [{"outcome": "unchanged_incorrect", "case": "[one]```\n@someone"}] + text = feedback.json_for_code(value) + self.assertEqual(json.loads(text), value) + self.assertIn("unchanged_incorrect", text) + self.assertNotIn("```", text) + self.assertNotIn("", text) + self.assertNotIn("@someone", text) + self.assertEqual(feedback.safe_text("@someone_[x]"), + "@someone_[x]") + + def test_historical_validation_failure_never_overwrites_comment(self): + api = Mock() + with patch("tools.skill_eval_retained.retained_reports", side_effect=ValueError("stale")), \ + patch.object(integration, "git", return_value=b"a" * 40): + with self.assertRaisesRegex(ValueError, "stale"): + integration.publish_historical(api, 7, 10, 20, now=1) + api.publish.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_skill_eval_pr.py b/tests/test_skill_eval_pr.py index f96bb335..71b46a4a 100644 --- a/tests/test_skill_eval_pr.py +++ b/tests/test_skill_eval_pr.py @@ -633,9 +633,10 @@ def test_publish_dispatch_rejects_planning_source_before_artifact_access( monkeypatch.setattr(integration, "GitHub", lambda repository, token: api) dispatch_event(monkeypatch, tmp_path, output, {"action": "publish", "pr": "7", "source_run": "10"}, "publish") - with pytest.raises(ValueError, match="Untrusted or mismatched execution workflow"): + with pytest.raises(ValueError, match="Untrusted retained source workflow"): integration.main() - request.assert_called_once_with("GET", "/actions/runs/10") + assert request.call_args_list[0].args == ("GET", "/actions/runs/10") + assert not any("/artifacts" in call.args[1] for call in request.call_args_list) assert api.writes == [] and api.comments == [] diff --git a/tests/test_skill_eval_pr_summary.py b/tests/test_skill_eval_pr_summary.py index a9cf07c7..ff70ba75 100644 --- a/tests/test_skill_eval_pr_summary.py +++ b/tests/test_skill_eval_pr_summary.py @@ -74,7 +74,15 @@ def test_planned_summary_keeps_visible_status_and_folds_full_approval_fields(pla def test_mixed_results_keep_all_aggregate_outcome_counts_visible(plan): - text = render(plan, [outcome(fix=1, inconclusive=2), outcome(regression=3, missing_pair=4)]) + first = {**outcome(fix=1, inconclusive=2), "skill": "selected", + "plan_sha256": plan["comparisons"][0]["plan_sha256"], + "exam": plan["comparisons"][0]["exam"], + "arms": {"baseline": {"git_revision": plan["base"]}, + "candidate": {"git_revision": plan["head"]}}} + second = {**copy.deepcopy(first), **outcome(regression=3, missing_pair=4), + "skill": "another"} + plan["comparisons"].append({**copy.deepcopy(plan["comparisons"][0]), "skill": "another"}) + text = render(plan, [first, second]) assert_boundaries(text) main = visible(text) assert "**RETAINED RESULTS**" in main and "**PLANNED:" not in main diff --git a/tests/test_skill_eval_retained.py b/tests/test_skill_eval_retained.py new file mode 100644 index 00000000..375993cc --- /dev/null +++ b/tests/test_skill_eval_retained.py @@ -0,0 +1,286 @@ +"""Offline security tests for the historical report-only bridge.""" +import copy +import io +import json +import os +import stat +import subprocess +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +from tools import skill_eval_retained as bridge + + +def zipped(files): + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + for name, value in files.items(): + archive.writestr(name, value) + return stream.getvalue() + + +class RetainedArchiveTests(unittest.TestCase): + def test_archive_reads_only_data(self): + raw = b"raise RuntimeError('never execute artifact code')" + self.assertEqual(bridge._archive(zipped({"validator/verify.py": raw})), + {"validator/verify.py": raw}) + + def test_unsafe_names(self): + for name in ("../escape", "/absolute", "a\\b", "a//b", "a/./b", "a/../b"): + with self.subTest(name=name), self.assertRaises(ValueError): + bridge._archive(zipped({name: b"x"})) + + def test_symlink_and_special(self): + for mode in (stat.S_IFLNK, stat.S_IFIFO, stat.S_IFSOCK): + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + item = zipfile.ZipInfo("entry") + item.external_attr = (mode | 0o600) << 16 + archive.writestr(item, "target") + with self.assertRaises(ValueError): + bridge._archive(stream.getvalue()) + + def test_duplicate_and_oversize(self): + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("one", "first") + archive.writestr("one", "second") + with self.assertRaises(ValueError): + bridge._archive(stream.getvalue()) + with patch.object(bridge, "MAX_BYTES", 1), self.assertRaises(ValueError): + bridge._archive(zipped({"one": b"xx"})) + + +class HistoricalGitTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.repo = self.root / "repo" + self.repo.mkdir() + self.git("init", "--quiet") + (self.repo / "tools").mkdir() + (self.repo / "src").mkdir() + (self.repo / "src/empty.py").write_text("") + self.adapter = b"# trusted historical adapter\n" + (self.repo / "tools/skill_eval_pr.py").write_bytes(self.adapter) + (self.repo / "tools/skill_eval_pr_cases.json").write_text("{}") + self.git("add", ".") + self.git("commit", "--quiet", "-m", "historical main") + self.old = self.git("rev-parse", "HEAD") + (self.repo / "src/empty.py").write_text("# new controller bytes\n") + self.git("add", ".") + self.git("commit", "--quiet", "-m", "current main") + self.current = self.git("rev-parse", "HEAD") + self.plan = {"adapter_sha256": bridge._hash(self.adapter), + "mapping_sha256": bridge._hash(b"{}")} + + def git(self, *args): + return subprocess.check_output(["git", "-c", "user.name=Offline Fixture", "-c", + "user.email=fixture@example.invalid", "-c", "commit.gpgsign=false", *args], + cwd=self.repo, text=True).strip() + + def test_historical_bytes_not_current_controller(self): + dest = self.root / "code" + dest.mkdir() + bridge._trusted_code(self.repo, self.old, self.current, self.plan, dest) + self.assertEqual((dest / "src/empty.py").read_bytes(), b"") + self.assertEqual((dest / "tools/skill_eval_pr.py").read_bytes(), self.adapter) + + def test_invalid_revision_and_blob_bindings(self): + for field in ("adapter_sha256", "mapping_sha256"): + changed = {**self.plan, field: "0" * 64} + with self.subTest(field=field), self.assertRaises(ValueError): + bridge._trusted_code(self.repo, self.old, self.current, changed, self.root / field) + with self.assertRaises(ValueError): + bridge._trusted_code(self.repo, "--help", self.current, self.plan, self.root / "bad") + with self.assertRaises(ValueError): + bridge._trusted_code(self.repo, self.old, self.old, self.plan, self.root / "bad") + + def test_nonancestor_rejected(self): + self.git("checkout", "--quiet", "--orphan", "untrusted") + self.git("commit", "--quiet", "-m", "unrelated") + unrelated = self.git("rev-parse", "HEAD") + self.git("checkout", "--quiet", self.current) + with self.assertRaises(ValueError): + bridge._trusted_code(self.repo, unrelated, self.current, self.plan, self.root / "bad") + + +class ReportOnlyTests(unittest.TestCase): + def test_clean_environment_no_provider_execute_network_or_artifact_import(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + code, data, home = (root / n for n in ("code", "data", "home")) + for directory in (code / "tools", code / "src", data, home): + directory.mkdir(parents=True) + (code / "tools/__init__.py").write_text("") + (code / "tools/skill_eval_compare.py").write_text("") + (data / "results.zip").write_bytes(zipped({})) + (data / "sitecustomize.py").write_text("raise AssertionError('artifact import')") + (code / "tools/skill_eval_pr.py").write_text(''' +import os, socket, subprocess, sys +from tools import skill_eval_compare as compare +def import_results(raw, output): + assert set(os.environ) <= {"HOME", "LANG", "LC_ALL", "LC_CTYPE"} + assert not any(str(output) == p for p in sys.path) +def reports(output): + for call in (lambda: execute(None), lambda: compare.provider_client(None), + lambda: subprocess.run([sys.executable, "-c", "pass"]), + lambda: socket.socket()): + try: + call() + except RuntimeError: + continue + raise AssertionError("report-only restriction bypassed") + return [{"verified": True}] +''') + with patch.dict(os.environ, {"GITHUB_TOKEN": "fake-secret", + "ANTHROPIC_API_KEY": "fake-key", + "PYTHONPATH": str(data), "HTTPS_PROXY": "fake-proxy"}): + self.assertEqual(bridge._regrade(code, data, home), [{"verified": True}]) + + +class FrozenInputTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.code, self.data = self.root / "code", self.root / "data" + (self.code / "tools").mkdir(parents=True) + self.data.mkdir() + (self.code / "tools/controller.py").write_bytes(b"trusted") + self.frozen = {"skill": "selected", "adapter": "audit", "exam": {"grader": "a" * 64}, + "arms": {"baseline": {"git_revision": "b" * 40}, + "candidate": {"git_revision": "c" * 40}}, + "controllers": {"tools/controller.py": bridge._hash(b"trusted")}, + "files": {"plan/tasks.json": bridge._hash(b"[]"), + "plan/schema.json": bridge._hash(b"{}"), + "validator/verify.py": bridge._hash(b"raise RuntimeError('do not execute')")}} + self.files = {"selected/plan/tasks.json": b"[]", "selected/plan/schema.json": b"{}", + "selected/validator/verify.py": b"raise RuntimeError('do not execute')"} + self.plan = {"head": "c" * 40, "base": "b" * 40, "comparisons": [{"skill": "selected", + "exam": self.frozen["exam"]}]} + self.freeze() + + def freeze(self): + raw = json.dumps(self.frozen).encode() + self.files["selected/plan/freeze.json"] = raw + self.plan["comparisons"][0]["plan_sha256"] = bridge._hash(raw) + self.files["pr-plan.json"] = json.dumps(self.plan).encode() + + def materialize(self): + return bridge._materialize(self.files, self.plan, self.data, self.code) + + def test_hash_verified_python_remains_data(self): + context = self.materialize() + self.assertEqual(context["selected"]["tasks"], []) + self.assertEqual((self.data / "selected/validator/verify.py").read_bytes(), + self.files["selected/validator/verify.py"]) + + def test_mismatched_freeze_input_controller_and_arm(self): + for kind in ("freeze", "input", "controller", "arm", "extra"): + original_files, original_frozen = copy.deepcopy(self.files), copy.deepcopy(self.frozen) + with self.subTest(kind=kind): + if kind == "freeze": + self.files["selected/plan/freeze.json"] += b" " + elif kind == "input": + self.files["selected/plan/tasks.json"] = b"[1]" + elif kind == "extra": + self.files["selected/plan/extra.py"] = b"malicious" + elif kind == "controller": + self.frozen["controllers"]["tools/controller.py"] = "0" * 64 + self.freeze() + else: + self.frozen["arms"]["baseline"]["git_revision"] = "d" * 40 + self.freeze() + with self.assertRaises(ValueError): + self.materialize() + self.files, self.frozen = original_files, original_frozen + self.freeze() + + def test_frozen_path_traversal(self): + for field, name in (("files", "../../escape"), ("controllers", "../escape")): + with self.subTest(field=field): + self.frozen[field][name] = "0" * 64 + self.freeze() + with self.assertRaises(ValueError): + self.materialize() + del self.frozen[field][name] + + +class SourceAuthenticationTests(unittest.TestCase): + def setUp(self): + from tests.fixtures.skill_eval_pr_support import SimulatedGitHub + self.plan = {"repository": "example/fixture", "pr": 7, "head": "b" * 40, + "base": "c" * 40, "version": "d" * 40, "comparisons": []} + self.api = SimulatedGitHub(self.plan, zipped({})) + self.api.run.update(event="pull_request_target", run_attempt=1, + display_title="skill-eval plan or publish PR 7", head_sha=self.plan["head"]) + self.patches = [patch("tools.skill_eval_auto.retained_source", return_value=10), + patch.object(bridge, "_trusted_code"), + patch.object(bridge, "_materialize", return_value={}), + patch.object(bridge, "_regrade", return_value=[])] + self.mocks = [p.start() for p in self.patches] + for p in self.patches: + self.addCleanup(p.stop) + + def call(self): + return bridge.retained_reports(self.api, 7, 10, trusted_repo=Path("."), + renderer_revision="e" * 40, now=100) + + def test_authenticated_source_read_only_and_separate_renderer(self): + result = self.call() + self.assertEqual(result["plan"], self.plan) + self.assertEqual(result["renderer_revision"], "e" * 40) + self.assertEqual(self.api.writes, []) + self.mocks[0].assert_called_once_with(self.api, self.plan) + self.mocks[1].assert_called_once() + + def test_bad_source_identity_fails_before_verifier(self): + fields = {"id": 11, "run_attempt": 2, "path": "untrusted.yml", + "event": "pull_request", "status": "in_progress", "display_title": "other", + "head_sha": "f" * 40, + "repository": {"id": 999, "full_name": "example/untrusted"}} + for name, value in fields.items(): + old = self.api.run[name] + self.api.run[name] = value + with self.subTest(name=name), self.assertRaises(ValueError): + self.call() + self.api.run[name] = old + self.mocks[1].assert_not_called() + self.assertEqual(self.api.writes, []) + + def test_stale_and_ledger_mismatch(self): + self.api.current["head"] = "f" * 40 + with self.assertRaises(ValueError): + self.call() + self.api.current["head"] = self.plan["head"] + for source in (None, 11): + self.mocks[0].return_value = source + with self.subTest(source=source), self.assertRaises(ValueError): + self.call() + self.mocks[0].side_effect = ValueError("ambiguous ledger") + with self.assertRaises(ValueError): + self.call() + self.mocks[1].assert_not_called() + + def test_expired_plan_and_results(self): + for artifact in (self.api.plan_artifact, self.api.artifact): + artifact["expired"] = True + with self.assertRaises(ValueError): + self.call() + artifact["expired"] = False + self.mocks[1].assert_not_called() + + def test_missing_source_plan(self): + self.api.plan_artifact = None + with self.assertRaises(ValueError): + self.call() + self.mocks[1].assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/skill_eval_feedback.py b/tools/skill_eval_feedback.py new file mode 100644 index 00000000..412409e7 --- /dev/null +++ b/tools/skill_eval_feedback.py @@ -0,0 +1,216 @@ +"""Bounded public projections, never raw answers or hidden expected answers. + +Projection is called only after trusted in-tree regrading. Rendering still +validates the projection: stored report dictionaries are not publication authority. +""" +import html +import json +import re + +from inferrsi.harness import skill_eval_diagnostic as diagnostic + +COMPONENTS = { + diagnostic.ADAPTER: ("schema", "diagnosis", "citation", "safe_next_check", + "no_invariant_violation"), + "audit": ("schema", "report_verdict", "claim_scope", "findings", "reduction", + "evidence", "explanation"), +} +REASONS = { + "schema": "Answer does not satisfy the frozen schema.", + "diagnosis": "Diagnosis does not match the recorded observations.", + "citation": "Citations do not satisfy the exact decisive-evidence requirement.", + "extra_citation": "Extra/nondecisive citation present; strict citation check fails.", + "missing_citation": "Required decisive citation missing.", + "duplicate_citation": "Duplicate citation present.", + "safe_next_check": "Next read-only check does not match the recorded observations.", + "no_invariant_violation": "Answer incorrectly authorizes reuse.", + "report_verdict": "Report verdict fails the frozen check.", + "claim_scope": "Claim scope fails the frozen check.", + "findings": "Findings fail the frozen check.", + "reduction": "Reduction fails the frozen numeric check.", + "evidence": "Evidence references fail the frozen check.", + "explanation": "Required explanation is absent or invalid.", +} + + +def projection(adapter, task, answer, checks): + """Select only finite fields from a trusted grade; no oracle answer is emitted.""" + adapter = diagnostic.ADAPTER if adapter == diagnostic.ADAPTER else "audit" + assertions = checks["assertions"] + components = {key: assertions[key] for key in COMPONENTS[adapter] if key in assertions} + reasons = [key for key, value in components.items() if value is False] + selected = {} + if adapter == diagnostic.ADAPTER and assertions.get("schema") is True: + selected = {key: answer[key] for key in ("diagnosis", "next_check", "reuse_authorized")} + citations = answer["evidence"] + # Never include arbitrary strings or identifiers that could carry secrets. + if (len(citations) <= 4 and all(isinstance(v, str) and + re.fullmatch(r"E[0-9]{1,4}", v) for v in citations)): + selected["evidence"] = citations + if assertions.get("citation") is False: + _, required, _ = diagnostic.expected(task) + cited = set(citations) + reasons += (["extra_citation"] if cited - required else []) + reasons += (["missing_citation"] if required - cited else []) + reasons += (["duplicate_citation"] if len(cited) != len(citations) else []) + return {"adapter": adapter, "components": components, "answer": selected, + "reasons": reasons} + + +def code(value): + """Render a bounded safe identifier, without Markdown or mentions.""" + if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_.:/-]{1,128}", value): + return "unavailable" + return "" + html.escape(value) + "" + + +def safe_text(value, limit=512): + """Bound presentation metadata and neutralize Markdown/HTML/mention syntax.""" + if not isinstance(value, str): + return "unavailable" + value = value[:limit].replace("\n", " ").replace("\r", " ") + return "".join("&#" + str(ord(char)) + ";" if char in "`@*_[\\]#|" + else html.escape(char) for char in value) + + +def json_for_code(value): + """Keep valid copyable JSON while neutralizing fence/HTML/mention characters.""" + text = json.dumps(value, sort_keys=True, separators=(",", ":")) + if len(text) > 12000: + return "[]" + for char in "`<>&@": + text = text.replace(char, "\\u" + format(ord(char), "04x")) + return text + + +def trial_text(trial): + if not isinstance(trial, dict) or trial.get("grader_trust") != "reverified": + return "unavailable (no trusted component evidence)" + verdict = trial.get("verdict") + if verdict not in ("PASS", "FAIL"): + return "unavailable (incomplete or invalid retained evidence)" + data = trial.get("feedback") + if (not isinstance(data, dict) or not isinstance(data.get("adapter"), str) + or data["adapter"] not in COMPONENTS): + return verdict + "; components unavailable" + adapter = data["adapter"] + checks = data.get("components") + reasons = data.get("reasons") + if (not isinstance(checks, dict) or not checks or + any(k not in COMPONENTS[adapter] or type(v) is not bool for k, v in checks.items()) + or not isinstance(reasons, list) or len(reasons) > 12 + or any(not isinstance(r, str) or r not in REASONS for r in reasons)): + return verdict + "; components unavailable (invalid projection)" + parts = [verdict] + answer = data.get("answer") + if adapter == diagnostic.ADAPTER and isinstance(answer, dict): + for key, allowed in (("diagnosis", diagnostic.DIAGNOSES), + ("next_check", diagnostic.CHECKS)): + value = answer.get(key) + if type(value) is str and value in allowed: + parts.append(key + "=" + code(value)) + citations = answer.get("evidence") + if (isinstance(citations, list) and len(citations) <= 4 and + all(isinstance(v, str) and re.fullmatch(r"E[0-9]{1,4}", v) for v in citations)): + parts.append("citations=" + (", ".join(code(v) for v in citations) or "none")) + if type(answer.get("reuse_authorized")) is bool: + parts.append("reuse_authorized=" + str(answer["reuse_authorized"]).lower()) + parts.append("components: " + ", ".join( + key + "=" + ("PASS" if checks[key] else "FAIL") if key in checks else key + "=unavailable" + for key in COMPONENTS[adapter])) + if reasons: + specific = any(r in reasons for r in ("extra_citation", "missing_citation", + "duplicate_citation")) + parts.append("Reasons: " + " ".join(REASONS[r] for r in dict.fromkeys(reasons) + if not (r == "citation" and specific))) + return "; ".join(parts) + + +def component_changes(pair): + before, after = (pair.get(role, {}) for role in ("baseline", "candidate")) + if any(not isinstance(t, dict) or t.get("grader_trust") != "reverified" or + t.get("verdict") not in ("PASS", "FAIL") for t in (before, after)): + return "Component changes unavailable." + old, new = (t.get("feedback", {}) for t in (before, after)) + if not isinstance(old, dict) or not isinstance(new, dict): + return "Component changes unavailable." + adapter = old.get("adapter") + if not isinstance(adapter, str) or adapter not in COMPONENTS or new.get("adapter") != adapter: + return "Component changes unavailable." + checks = [v.get("components") for v in (old, new)] + if any(not isinstance(c, dict) for c in checks): + return "Component changes unavailable." + changes = [] + for key in COMPONENTS[adapter]: + a, b = (c.get(key) for c in checks) + if type(a) is bool and type(b) is bool and a != b: + changes.append(key + ": " + ("PASS" if a else "FAIL") + " → " + + ("PASS" if b else "FAIL")) + return "Component changes: " + ("; ".join(changes) or "none observed") + "." + + +def comparison_lines(plan, results, evidence_state): + """Only exact, unique bindings may expose component explanations.""" + if evidence_state != "available": + return [] + lines, count = [], 0 + for item, result in bound_results(plan, results): + allowed = {(case["id"], repeat) for case in item["cases"] if case["split"] != "discovery" + for repeat in range(1, plan["limits"]["repeats"] + 1)} + pairs = result.get("pairs", []) + seen = [(p.get("case_id"), p.get("repeat")) for p in pairs] + if len(set(seen)) != len(seen): + continue + for pair in pairs: + if (pair.get("case_id"), pair.get("repeat")) not in allowed: + continue + count += 1 + if count > 8: + continue + lines += ["", "**" + code(item["skill"]) + " / " + code(pair["case_id"]) + + " · repeat " + str(pair["repeat"]) + "**", + component_changes(pair), + "- Baseline: " + trial_text(pair.get("baseline")), + "- Candidate: " + trial_text(pair.get("candidate"))] + if any(isinstance(pair.get(role, {}).get("feedback"), dict) and + pair[role]["feedback"].get("adapter") == diagnostic.ADAPTER + for role in ("baseline", "candidate")): + lines += ["Coverage: one synthetic diagnostic case per pair; not a production " + "trace, engine benchmark or general efficacy result."] + if lines: + lines = ["### Retained baseline → candidate", "Strict aggregate outcomes are unchanged " + "by component explanations; unavailable is not failure."] + lines + gaps = sum(item["coverage"] != "covered" for item in plan["coverage"]) + lines += ["", f"Showing {min(count, 8)} of {count} bound pairs; discovery excluded. " + f"Uncovered/unsupported changes: **{gaps}**; gaps never count as passes."] + return lines + + +def evidence_binding_lines(plan, results, evidence_state): + if evidence_state != "available": + return [] + lines = [] + for item, result in bound_results(plan, results): + lines += ["Frozen comparison " + code(item["skill"]) + ": " + code(item["plan_sha256"])] + for pair in result.get("pairs", [])[:8]: + lines += ["Case " + code(pair["case_id"]) + " answer SHA-256 (baseline / candidate): " + + " / ".join(code(pair.get(role, {}).get("output_sha256")) + for role in ("baseline", "candidate"))] + return lines[:40] + + +def bound_results(plan, results): + """No aggregate or detail can outlive its unique revision/plan/exam binding.""" + for item in plan["comparisons"]: + if "plan_sha256" not in item: + continue + matches = [r for r in results if r.get("skill") == item["skill"]] + if len(matches) != 1: + continue + result = matches[0] + if (result.get("plan_sha256") != item["plan_sha256"] or + result.get("exam") != item["exam"] or + any(result.get("arms", {}).get(role, {}).get("git_revision") != plan[key] + for role, key in (("baseline", "base"), ("candidate", "head")))): + continue + yield item, result diff --git a/tools/skill_eval_pr.py b/tools/skill_eval_pr.py index 1df0452b..107473d0 100644 --- a/tools/skill_eval_pr.py +++ b/tools/skill_eval_pr.py @@ -18,6 +18,15 @@ from inferrsi.model.anthropic_agent import money, reservation from tools import skill_eval_compare as compare from tools.check_knowledge_model_policy import GitHubHTTPError +from tools.skill_eval_feedback import ( + bound_results, + code, + comparison_lines, + evidence_binding_lines, + json_for_code, + projection, + safe_text, +) ROOT = Path(__file__).resolve().parents[1] MAPPING = ROOT / "tools/skill_eval_pr_cases.json" @@ -332,10 +341,75 @@ def reports(output): compare.load_plan(directory), [p[r] for p in result["pairs"] for r in compare.ROLES] + result["discovery"], tasks) + frozen = compare.load_plan(directory) + attach_feedback(result, {"adapter": frozen.get("adapter", "audit"), + "tasks": tasks, "exam": frozen["exam"], + "schema": compare.read_json(directory / "plan/schema.json")}) results.append(result) return results +def attach_feedback(result, context): + """Project only an exact current builtin exam, including historical reports. + + A historical report's trusted result remains intact when its grader has + changed; explanations are unavailable rather than inferred under new rules. + """ + diagnostic = compare.diagnostic + adapter = context["adapter"] + if adapter == diagnostic.ADAPTER: + verifier, schema, path = diagnostic.verify, diagnostic.SCHEMA, Path(diagnostic.__file__) + elif adapter == "audit": + verifier, schema = compare.verify, compare.SCHEMA + path = ROOT / "tools/skill_eval_verifier.py" + else: + return + trials = [p[r] for p in result["pairs"] for r in compare.ROLES] + result["discovery"] + for trial in trials: + trial.pop("feedback", None) + if context["exam"]["grader"] != sha256(path) or context["schema"] != schema: + return + tasks = context["tasks"] + if isinstance(tasks, list): + tasks = {task["id"]: task for task in tasks} + for trial in trials: + if trial.get("grader_trust") != "reverified" or trial["verdict"] not in ("PASS", "FAIL"): + continue + answer = json.loads(trial["output"]) + checks = verifier(tasks[trial["task_id"]], answer) + if checks != trial["checks"] or checks["passed"] != (trial["verdict"] == "PASS"): + raise ValueError("Historical grading disagrees with the current identical grader") + trial["feedback"] = projection(adapter, tasks[trial["task_id"]], answer, checks) + + +def publish_historical(api, pr, source_run, writer_run, *, now, output=None): + """Explicit no-execution retry; retain the original plan's complete identity.""" + from tools.skill_eval_retained import retained_reports + + revision = git(ROOT, "rev-parse", "HEAD").decode().strip() + retained = retained_reports(api, pr, source_run, trusted_repo=ROOT, + renderer_revision=revision, now=now) + plan, results = retained["plan"], retained["reports"] + for result in results: + attach_feedback(result, retained["contexts"][result["skill"]]) + from tools.skill_eval_auto import report_control + + body = summary(plan, results, retained["evidence_url"], retained["expires_at"], now, + evidence_state="available", execution_control=report_control(api, plan)) + body += ("\n\nPresentation implementation: `" + revision + "`. " + "Original execution plan and frozen verifier bindings are retained; " + "this report-only refresh starts no model execution or budget reservation.") + if output is not None: + write_json(output / "pr-plan.json", plan) + write_json(output / "report-renderer.json", {"renderer_revision": revision, + "source_run": source_run, "original_plan_sha256": fingerprint(plan)}) + for result in results: + directory = output / result["skill"] + write_json(directory / "report.json", result) + (directory / "report.md").write_text(body, encoding="utf-8") + return api.publish(plan, body, writer_run) + + def review_guidance(plan, results, *, evidence_state): """Advisory only, over trusted regenerated reports for this exact plan. @@ -414,27 +488,29 @@ def summary(plan, results, evidence_url, expires_at, now, *, evidence_state=None ("available" if results else "planned / unexecuted")) guidance = review_guidance(plan, results, evidence_state=evidence_state) lines = [MARKER, "## Skill evaluation"] + lines += comparison_lines(plan, results, evidence_state) if plan.get("execution_blocked"): - lines += ["", "Paid execution blocked: " + plan["execution_blocked"] + ". " - "This is free planning only; no alternate provider/model is selected."] + lines += ["", "Paid execution blocked: " + safe_text(plan["execution_blocked"]) + ". " + "New execution only: this does not invalidate available retained results; " + "no alternate provider/model is selected."] if execution_control is not None: lines += ["", "Automatic execution / cumulative PR budget: `" + canonical(execution_control) + "`", ""] lines += ["### Advisory: " + guidance["disposition"], "Retained evidence availability: **" + evidence_state + "**. " "No approval or merge authorization is issued."] - lines.extend("- " + reason for reason in guidance["reasons"][:6]) + lines.extend("- " + safe_text(reason) for reason in guidance["reasons"][:6]) if len(guidance["reasons"]) > 6: lines += [f"- {len(guidance['reasons']) - 6} additional reasons in details below."] lines += [""] counts = {k: 0 for k in ("fix", "regression", "unchanged_correct", "unchanged_incorrect", "inconclusive", "missing_pair")} - if expired: + if expired or evidence_state == "expired": lines += ["**EXPIRED: retained evidence is no longer verifiable.**"] elif evidence_state in ("missing", "invalid"): lines += ["**INSUFFICIENT EVIDENCE: retained artifacts are " + evidence_state + ".**"] elif results: - for result in results: + for _, result in bound_results(plan, results): for key in counts: counts[key] += result["counts"][key] lines += ["**RETAINED RESULTS**", "", "Outcomes: `" + canonical(counts) + "`", @@ -464,14 +540,15 @@ def summary(plan, results, evidence_url, expires_at, now, *, evidence_state=None "Content/source review and authorship admission remain separate.", "Trusted regrading checks retained answers, not model/hardware provenance or " "a hostile executor; this is not an efficacy certification.", - *["- " + reason for reason in guidance["reasons"][6:]], + *["- " + safe_text(reason) for reason in guidance["reasons"][6:26]], "", "Per-case outcomes (discovery excluded; regressions and gaps first):", - "```json", canonical(sorted(guidance["observations"], key=lambda item: + "```json", json_for_code(sorted(guidance["observations"], key=lambda item: (item["outcome"] not in ("regression", "inconclusive", "missing_pair", "unchanged_incorrect"), item["skill"]))[:20]), "```", f"Showing at most 20 of {len(guidance['observations'])} pairs; " "the evidence link contains all outcomes, including omitted regressions/gaps.", "", + *[line for line in evidence_binding_lines(plan, results, evidence_state)], f"Plan SHA-256: `{fingerprint(plan)}`", f"Base: `{plan['base']}` · Head: `{plan['head']}`", f"Trusted implementation: `{plan['version']}`", "", @@ -479,21 +556,25 @@ def summary(plan, results, evidence_url, expires_at, now, *, evidence_state=None "Budget: `" + canonical(budget(plan)) + "`", "", "| Skill | Comparison | Cases | Estimated calls |", "| --- | --- | --- | --- |"] - for item in plan["comparisons"]: + for item in plan["comparisons"][:50]: if "plan_sha256" in item: - cases = ", ".join(c["id"] + " (" + c["split"] + ")" for c in item["cases"]) - lines.append(f"| {item['skill']} | {item['mode']} | {cases} | {item['calls']} |") + cases = ", ".join(safe_text(c["id"], 128) + " (" + safe_text(c["split"], 24) + ")" + for c in item["cases"][:20]) + lines.append(f"| {code(item['skill'])} | {code(item['mode'])} | {cases} | " + f"{item['calls']} |") lines.append("\nFrozen exam hashes: `" + canonical(item["exam"]) + "`\n") else: - lines.append(f"| {item['skill']} | unsupported snapshot | — | 0 |") + lines.append(f"| {code(item['skill'])} | unsupported snapshot | — | 0 |") if "provider_config" in plan: lines += ["", "Provider/settings: `" + canonical(plan["provider_config"]) + "`"] lines += ["", "", "", "
", f"Uncovered / unsupported paths ({len(gaps)})", ""] # A collapsed HTML container still requires data escaping for untrusted Git paths. - for gap in gaps: - lines.append("- " + gap["coverage"] + ": " - + html.escape(canonical(gap["path"])).replace("@", "@") + "") + for gap in gaps[:100]: + lines.append("- " + safe_text(gap["coverage"], 24) + ": " + + html.escape(canonical(gap["path"][:1024])).replace("@", "@") + "") + if len(gaps) > 100: + lines.append("Additional paths omitted; see retained evidence.") if not gaps: lines.append("(none)") lines += ["", "
"] @@ -890,6 +971,13 @@ def main(): handle.write("client_kind=" + (plan["provider_config"]["client"]["kind"] if "provider_config" in plan else "codex") + "\n") return + if args.mode == "publish" and inputs.get("action") == "publish": + if not inputs.get("source_run"): + raise ValueError("Publish-only retry requires the original execution run ID") + receipt = publish_historical(api, pr, int(inputs["source_run"]), run_id, + now=time.time(), output=output) + print(receipt["html_url"]) + return plan = compare.read_json(output / "pr-plan.json") if args.mode == "authorize": actor = os.environ["GITHUB_ACTOR"] diff --git a/tools/skill_eval_retained.py b/tools/skill_eval_retained.py new file mode 100644 index 00000000..9c346250 --- /dev/null +++ b/tools/skill_eval_retained.py @@ -0,0 +1,258 @@ +"""Read-only bridge to an authenticated historical main-ancestor verifier. + +This is deliberately not an execution/reservation entry point. The caller must run +from a trusted main checkout and publish through GitHub.publish's live-PR guards. +Artifact Python is hash-checked data, never an import or executable. +""" + +import hashlib +import io +import json +import re +import stat +import subprocess +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +MAX_BYTES = 100 * 1024 * 1024 +MAX_FILES = 10000 +SHA = r"[0-9a-f]{40}" +DIGEST = r"[0-9a-f]{64}" +WORKFLOW = ".github/workflows/skill-eval-pr.yml" +GIT = ("git", "--no-replace-objects", "--no-lazy-fetch") + + +def _hash(raw): + return hashlib.sha256(raw).hexdigest() + + +def _path(name): + if (not isinstance(name, str) or not name or "\\" in name or "\x00" in name + or str(PurePosixPath(name)) != name or name.startswith("/") + or any(p in {"", ".", ".."} for p in name.split("/"))): + raise ValueError("Unsafe retained path") + return name + + +def _archive(raw): + if not isinstance(raw, bytes) or len(raw) > MAX_BYTES: + raise ValueError("Missing or oversized retained archive") + with zipfile.ZipFile(io.BytesIO(raw)) as zipped: + entries = zipped.infolist() + names = [m.orig_filename for m in entries] + if (len(entries) > MAX_FILES or len(set(names)) != len(names) + or sum(m.file_size for m in entries) > MAX_BYTES): + raise ValueError("Duplicate or oversized retained archive") + files = {} + for member in entries: + name = member.orig_filename + mode = stat.S_IFMT(member.external_attr >> 16) + if member.is_dir(): + _path(name[:-1]) + if mode not in (0, stat.S_IFDIR): + raise ValueError("Unsafe retained archive directory") + continue + _path(name) + if mode not in (0, stat.S_IFREG): + raise ValueError("Unsafe retained archive file") + files[name] = zipped.read(member) + return files + + +def _git(repo, *args): + return subprocess.check_output([*GIT, *args], cwd=repo, stderr=subprocess.DEVNULL) + + +def _trusted_code(repo, revision, renderer, plan, destination): + for value in (revision, renderer): + if not isinstance(value, str) or re.fullmatch(SHA, value) is None: + raise ValueError("Expected immutable trusted revision") + if _git(repo, "rev-parse", "HEAD").decode().strip() != renderer: + raise ValueError("Renderer is not the trusted checkout") + try: + _git(repo, "merge-base", "--is-ancestor", revision, renderer) + except subprocess.CalledProcessError as error: + raise ValueError("Verifier is not a trusted main ancestor") from error + for field, name in (("adapter_sha256", "tools/skill_eval_pr.py"), + ("mapping_sha256", "tools/skill_eval_pr_cases.json")): + if plan.get(field) != _hash(_git(repo, "show", f"{revision}:{name}")): + raise ValueError("Historical Git blob binding mismatch") + # Git archive is trusted source; avoid checkout hooks, filters and submodules. + raw = _git(repo, "archive", "--format=tar", revision, "src", "tools") + if len(raw) > MAX_BYTES: + raise ValueError("Oversized historical source archive") + with tarfile.open(fileobj=io.BytesIO(raw)) as archive: + members = archive.getmembers() + if len(members) > MAX_FILES or sum(m.size for m in members) > MAX_BYTES: + raise ValueError("Oversized historical source tree") + for member in members: + if not member.name.endswith(".py"): + continue + _path(member.name) + if not member.isfile(): + raise ValueError("Historical Python must be a regular Git blob") + path = destination / member.name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(archive.extractfile(member).read()) + + +def _materialize(files, plan, destination, code): + """Select only frozen inputs, bound to the authenticated original PR plan.""" + contexts = {} + for item in plan["comparisons"]: + skill = item["skill"] + if not isinstance(skill, str) or re.fullmatch(r"[a-z0-9][a-z0-9._-]*", skill) is None: + raise ValueError("Unsafe retained skill") + if skill in contexts: + raise ValueError("Duplicate retained comparison") + if "plan_sha256" not in item: + continue + name = skill + "/plan/freeze.json" + raw = files[name] + if _hash(raw) != item["plan_sha256"]: + raise ValueError("Retained freeze binding mismatch") + frozen = json.loads(raw) + if (frozen["skill"] != skill or frozen["exam"] != item["exam"] + or any(frozen["arms"][role]["git_revision"] != plan[key] + for role, key in (("baseline", "base"), ("candidate", "head")))): + raise ValueError("Retained comparison identity mismatch") + selected = {name: raw} + for path, digest in frozen["files"].items(): + _path(path) + if not path.startswith(("plan/", "validator/")) or path == "plan/freeze.json": + raise ValueError("Unsafe frozen input path") + payload = files[skill + "/" + path] + if (not isinstance(digest, str) or re.fullmatch(DIGEST, digest) is None + or _hash(payload) != digest): + raise ValueError("Frozen input hash mismatch") + selected[skill + "/" + path] = payload + for path, digest in frozen["controllers"].items(): + _path(path) + if not path.startswith(("src/", "tools/")) or not path.endswith(".py"): + raise ValueError("Unsafe historical controller") + if _hash((code / path).read_bytes()) != digest: + raise ValueError("Historical controller hash mismatch") + # Unexpected plan/validator files cannot hide behind selective extraction. + prefixes = (skill + "/plan/", skill + "/validator/") + actual = {name for name in files if name.startswith(prefixes)} + if actual != set(selected): + raise ValueError("Frozen input file set mismatch") + for name, payload in selected.items(): + path = destination / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(payload) + contexts[skill] = {"adapter": frozen.get("adapter", "audit"), "exam": frozen["exam"], + "tasks": json.loads(selected[skill + "/plan/tasks.json"]), + "schema": json.loads(selected[skill + "/plan/schema.json"])} + (destination / "pr-plan.json").write_bytes(files["pr-plan.json"]) + return contexts + + +# Fixed program: no artifact-derived command, imports, Python flags or executable. +# Ancestor source is trusted, not hostile sandbox input. The audit hook and denied +# entry points additionally prevent accidental execution/network in report mode. +REPORT_PROGRAM = r''' +import json, sys +from pathlib import Path +code, data, output = map(Path, sys.argv[1:]) +sys.dont_write_bytecode = True +sys.path[:0] = [str(code), str(code / "src")] +def deny(*args, **kwargs): + raise RuntimeError("Historical bridge is report-only") +def audit(event, args): + if (event.startswith(("socket.", "subprocess.", "os.exec", "os.spawn")) + or event in {"os.system", "os.fork", "os.posix_spawn"}): + deny() +sys.addaudithook(audit) +from tools import skill_eval_pr as pr +from tools import skill_eval_compare as compare +for module, names in ((pr, ("main", "prepare", "execute", "authorize", "claim")), + (compare, ("main", "prepare", "execute", "provider_client", "run_trial"))): + for name in names: + setattr(module, name, deny) +pr.import_results((data / "results.zip").read_bytes(), data) +results = pr.reports(data) +output.write_text(json.dumps(results, allow_nan=False), encoding="utf-8") +''' + + +def _regrade(code, data, home): + result = home / "report.json" + # -I ignores PYTHONPATH/user-site; a fresh HOME and explicit allowlist prevent + # all GitHub/provider/proxy/cloud credentials from entering the interpreter. + environment = {"HOME": str(home), "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"} + subprocess.run([sys.executable, "-I", "-c", REPORT_PROGRAM, str(code), str(data), str(result)], + cwd=code, env=environment, check=True, timeout=120, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if result.stat().st_size > MAX_BYTES: + raise ValueError("Oversized historical report") + reports = json.loads(result.read_text(encoding="utf-8")) + if not isinstance(reports, list): + raise ValueError("Invalid historical report") + return reports + + +def retained_reports(api, pr_number, source_run, *, trusted_repo, renderer_revision, now): + """Authenticate and regrade retained data; performs no GitHub writes or spend. + + ``trusted_repo``/``renderer_revision`` MUST be the caller's trusted main + checkout, never a PR checkout. Missing Git ancestry fails closed (no fetch). + Failures propagate so a valid existing comment is not replaced by PLANNED. + """ + from tools import skill_eval_pr as pr + from tools.skill_eval_auto import retained_source + + if (type(source_run) is not int or source_run <= 0 + or type(pr_number) is not int or pr_number <= 0): + raise ValueError("Expected positive retained run and PR IDs") + run = api.request("GET", f"/actions/runs/{source_run}") + repository = api.request("GET", "") + if (run.get("id") != source_run or run.get("run_attempt") != 1 + or run.get("path") != WORKFLOW or run.get("event") != "pull_request_target" + or run.get("status") != "completed" + or run.get("display_title") != f"skill-eval plan or publish PR {pr_number}" + or type(repository.get("id")) is not int + or run.get("repository", {}).get("id") != repository["id"] + or run.get("repository", {}).get("full_name") != repository.get("full_name")): + raise ValueError("Untrusted retained source workflow") + raw, expires = pr.artifact_download(api, source_run, "skill-eval-plan", now) + if raw is None or now >= expires: + raise ValueError("Expired retained plan") + files = _archive(raw) + if len(files.get("pr-plan.json", b"")) > 4_000_000: + raise ValueError("Oversized retained plan") + plan = json.loads(files["pr-plan.json"]) + if (plan["pr"] != pr_number or plan["repository"] != repository["full_name"] + or plan["head"] != run.get("head_sha") + or api.pr(pr_number) != {"head": plan["head"], "base": plan["base"]} + or retained_source(api, plan) != source_run): + raise ValueError("Stale or unauthenticated retained plan") + data, result_expiry = pr.artifact_zip(api, source_run, "skill-eval-results", now) + expires = min(expires, result_expiry) + if data is None or now >= expires: + raise ValueError("Expired retained results") + _archive(data) # Validate names/modes/size before historical import's whitelist. + with tempfile.TemporaryDirectory(prefix="skill-eval-retained-") as temporary: + root = Path(temporary) + code, inputs, home = (root / name for name in ("code", "inputs", "home")) + for path in (code, inputs, home): + path.mkdir() + _trusted_code(trusted_repo, plan["version"], renderer_revision, plan, code) + contexts = _materialize(files, plan, inputs, code) + (inputs / "results.zip").write_bytes(data) + reports = _regrade(code, inputs, home) + expected = {item["skill"]: item for item in plan["comparisons"] if "plan_sha256" in item} + if len(reports) != len(expected) or {r["skill"] for r in reports} != set(expected): + raise ValueError("Historical report comparison mismatch") + for report in reports: + item = expected[report["skill"]] + if (report["plan_sha256"] != item["plan_sha256"] or report["exam"] != item["exam"] + or any(report["arms"][role]["git_revision"] != plan[key] + for role, key in (("baseline", "base"), ("candidate", "head")))): + raise ValueError("Historical report binding mismatch") + return {"plan": plan, "reports": reports, "contexts": contexts, "expires_at": expires, + "evidence_url": f"https://github.com/{plan['repository']}/actions/runs/{source_run}", + "renderer_revision": renderer_revision}