From 34867d784f24ae855e7bc5efd01c7ec4ee38ba89 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 23 Aug 2026 01:39:38 +0200 Subject: [PATCH] Expose SBOM controlled source evidence Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f --- src/activity_core/rules/executor.py | 45 ++++++++++++++----- tests/test_sbom_nexus_context_resolver.py | 18 ++++++++ ...P-0034-sbom-controlled-source-reference.md | 8 ++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/activity_core/rules/executor.py b/src/activity_core/rules/executor.py index 0cdf0f9..556ce90 100644 --- a/src/activity_core/rules/executor.py +++ b/src/activity_core/rules/executor.py @@ -762,6 +762,22 @@ def _sbom_catchup_report(instr: Any, catchup: dict) -> InstructionResult: updated = [r for r in catchup.get("updated", []) if isinstance(r, dict)] skipped = [r for r in catchup.get("skipped", []) if isinstance(r, dict)] limit = catchup.get("limit") + selected_repos = [] + controlled_source_count = 0 + for repo in selected: + item = { + "repo_slug": repo.get("repo_slug"), + "sbom_age_days": repo.get("sbom_age_days"), + "has_sbom": repo.get("has_sbom"), + "last_sbom_at": repo.get("last_sbom_at"), + "checkout_available": repo.get("checkout_available"), + } + source_ref = _safe_sbom_source_ref(repo.get("source_ref")) + if source_ref is not None: + item["source_ref"] = source_ref + controlled_source_count += 1 + selected_repos.append(item) + report: dict[str, Any] = { "summary": ( f"SBOM catch-up: {len(selected)} selected (limit {limit}), " @@ -773,19 +789,11 @@ def _sbom_catchup_report(instr: Any, catchup: dict) -> InstructionResult: "deterministic": True, "limit": limit, "selected_count": len(selected), + "controlled_source_count": controlled_source_count, "stale_count": catchup.get("stale_count"), "never_count": catchup.get("never_count"), "total_count": catchup.get("total_count"), - "selected_repos": [ - { - "repo_slug": r.get("repo_slug"), - "sbom_age_days": r.get("sbom_age_days"), - "has_sbom": r.get("has_sbom"), - "last_sbom_at": r.get("last_sbom_at"), - "checkout_available": r.get("checkout_available"), - } - for r in selected - ], + "selected_repos": selected_repos, "updated_repos": [r.get("repo_slug") for r in updated], "skipped_repos": [ {"repo_slug": r.get("repo_slug"), "reason": r.get("reason")} @@ -803,6 +811,23 @@ def _sbom_catchup_report(instr: Any, catchup: dict) -> InstructionResult: ) +def _safe_sbom_source_ref(raw: Any) -> dict[str, str] | None: + """Keep only the reviewable controlled-source identity in progress evidence.""" + if not isinstance(raw, dict) or raw.get("kind") != "forgejo-archive-v1": + return None + repository = raw.get("repository") + revision = raw.get("revision") + if not isinstance(repository, str) or not repository.strip(): + return None + if not isinstance(revision, str) or re.fullmatch(r"[0-9a-f]{40}", revision) is None: + return None + return { + "kind": "forgejo-archive-v1", + "repository": repository, + "revision": revision, + } + + def _validate_output( raw_output: Any, instr: Any, diff --git a/tests/test_sbom_nexus_context_resolver.py b/tests/test_sbom_nexus_context_resolver.py index c446346..88ecd71 100644 --- a/tests/test_sbom_nexus_context_resolver.py +++ b/tests/test_sbom_nexus_context_resolver.py @@ -640,6 +640,12 @@ def test_deterministic_report_names_selected_repos() -> None: "sbom_age_days": 9999, "has_sbom": False, "checkout_available": True, + "source_ref": { + "kind": "forgejo-archive-v1", + "repository": "coulomb/never-scanned", + "revision": "a" * 40, + "archive_url": "must-not-enter-progress-evidence", + }, }, { "repo_slug": "no-checkout", @@ -647,6 +653,11 @@ def test_deterministic_report_names_selected_repos() -> None: "sbom_age_days": 112, "has_sbom": True, "checkout_available": False, + "source_ref": { + "kind": "forgejo-archive-v1", + "repository": "coulomb/no-checkout", + "revision": "not-a-full-sha", + }, }, ], "selected_count": 2, @@ -663,6 +674,7 @@ def test_deterministic_report_names_selected_repos() -> None: report = result.report assert report["deterministic"] is True assert report["never_count"] == 93 + assert report["controlled_source_count"] == 1 assert [r["repo_slug"] for r in report["selected_repos"]] == [ "never-scanned", "no-checkout", @@ -670,6 +682,12 @@ def test_deterministic_report_names_selected_repos() -> None: assert "93 never scanned of 111 repos" in report["summary"] assert report["updated_repos"] == [] assert report["skipped_repos"] == [] + assert report["selected_repos"][0]["source_ref"] == { + "kind": "forgejo-archive-v1", + "repository": "coulomb/never-scanned", + "revision": "a" * 40, + } + assert "source_ref" not in report["selected_repos"][1] def test_deterministic_report_carries_t02_ingest_outcomes() -> None: diff --git a/workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md b/workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md index ab3a367..f27c246 100644 --- a/workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md +++ b/workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md @@ -61,3 +61,11 @@ processed exactly that frozen set, spawned zero tasks, and produced three provenance-bearing `no-manifest` snapshots. `never_count` advanced 94 to 91. Keep open for the first unassisted weekday fire and sustainable projection of each newly exposed oldest-N batch. + +Preflight 2026-08-23: after the controlled batch advanced, the newly exposed +oldest three (`citation-work`, `clay-borg`, `config-atlas`) each carried a +matching `forgejo-archive-v1` reference and a 40-character revision, confirming +projection is not limited to the attended batch. The deterministic catch-up +report now retains only the allowlisted kind/repository/revision fields and a +`controlled_source_count`, so the first unassisted fire can prove provenance +directly in State Hub without copying Nexus payloads or archive URLs.