From c78b19d1dc7aa9e654de57eeae0ab393ab2382c4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 11:32:05 +0800 Subject: [PATCH] feat(github): read back private backup visibility evidence --- ...hub_target_private_backup_evidence_gate.py | 93 ++++++++++++++- .../test_delivery_closure_workbench_api.py | 4 +- ...hub_target_private_backup_evidence_gate.py | 14 ++- ...target_private_backup_evidence_gate_api.py | 6 +- ...ub-target-connector-readback.snapshot.json | 108 ++++++++++++++++++ 5 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 docs/security/github-target-connector-readback.snapshot.json diff --git a/apps/api/src/services/github_target_private_backup_evidence_gate.py b/apps/api/src/services/github_target_private_backup_evidence_gate.py index 1a89ff35e..839bd3517 100644 --- a/apps/api/src/services/github_target_private_backup_evidence_gate.py +++ b/apps/api/src/services/github_target_private_backup_evidence_gate.py @@ -20,6 +20,7 @@ _DECISION_FILE = "github-target-decision.snapshot.json" _OWNER_RESPONSE_FILE = "github-target-owner-decision-response.snapshot.json" _APPROVAL_PACKAGE_FILE = "github-target-repo-approval-package.snapshot.json" _PROBE_FILE = "github-target-probe.snapshot.json" +_CONNECTOR_READBACK_FILE = "github-target-connector-readback.snapshot.json" def load_latest_github_target_private_backup_evidence_gate( @@ -31,18 +32,21 @@ def load_latest_github_target_private_backup_evidence_gate( owner_response = _load_snapshot(directory / _OWNER_RESPONSE_FILE) approval_package = _load_snapshot(directory / _APPROVAL_PACKAGE_FILE) probe = _load_snapshot(directory / _PROBE_FILE) + connector_readback = _load_optional_snapshot(directory / _CONNECTOR_READBACK_FILE) _require_source_contracts( decision=decision, owner_response=owner_response, approval_package=approval_package, probe=probe, + connector_readback=connector_readback, ) return build_github_target_private_backup_evidence_gate( decision=decision, owner_response=owner_response, approval_package=approval_package, probe=probe, + connector_readback=connector_readback, ) @@ -52,8 +56,10 @@ def build_github_target_private_backup_evidence_gate( owner_response: dict[str, Any], approval_package: dict[str, Any], probe: dict[str, Any], + connector_readback: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build the read-only gate response from source-control snapshots.""" + connector_payload = _dict(connector_readback) decisions = [_dict(row) for row in _list(decision.get("decisions"))] probe_by_repo = { str(row.get("github_repo")): _dict(row) @@ -65,6 +71,11 @@ def build_github_target_private_backup_evidence_gate( for row in _list(approval_package.get("approval_items")) if row.get("github_repo") } + connector_by_repo = { + str(row.get("github_repo")): _dict(row) + for row in _list(connector_payload.get("targets")) + if row.get("github_repo") + } owner_summary = _dict(owner_response.get("summary")) targets = [ @@ -72,6 +83,7 @@ def build_github_target_private_backup_evidence_gate( decision=row, probe=probe_by_repo.get(str(row.get("github_repo")), {}), approval_item=package_by_repo.get(str(row.get("github_repo")), {}), + connector_readback=connector_by_repo.get(str(row.get("github_repo")), {}), ) for row in decisions ] @@ -111,14 +123,27 @@ def build_github_target_private_backup_evidence_gate( "owner_decision_response": f"docs/security/{_OWNER_RESPONSE_FILE}", "approval_package": f"docs/security/{_APPROVAL_PACKAGE_FILE}", "github_target_probe": f"docs/security/{_PROBE_FILE}", + "github_connector_readback": f"docs/security/{_CONNECTOR_READBACK_FILE}", }, "summary": { "target_decision_count": len(targets), "approval_required_target_count": len(approval_required_targets), "approval_package_item_count": len(_list(approval_package.get("approval_items"))), + "github_connector_readback_count": len(_list(connector_payload.get("targets"))), + "github_connector_private_visibility_count": sum( + 1 + for row in _list(connector_payload.get("targets")) + if _dict(row).get("private_visibility_verified") is True + ), + "github_connector_not_found_or_inaccessible_count": sum( + 1 + for row in _list(connector_payload.get("targets")) + if _dict(row).get("readback_status") == "not_found_or_inaccessible" + ), "public_probe_visible_target_count": len(public_probe_visible_targets), "not_found_or_private_target_count": len(not_found_or_private_targets), "private_backup_verified_count": private_backup_verified_count, + "private_visibility_verified_count": private_backup_verified_count, "private_visibility_evidence_missing_count": len(approval_required_targets) - private_backup_verified_count, "safe_credential_required_count": len(approval_required_targets), "safe_credential_accepted_evidence_count": 0, @@ -173,17 +198,25 @@ def _build_target( decision: dict[str, Any], probe: dict[str, Any], approval_item: dict[str, Any], + connector_readback: dict[str, Any], ) -> dict[str, Any]: github_repo = str(decision.get("github_repo") or "") probe_status = str(probe.get("status") or decision.get("probe_status") or "unknown") target_state = str(decision.get("target_state") or "unknown") approval_required = decision.get("approval_required") is True external_scope = not approval_required or target_state == "external_scope" + private_visibility_verified = ( + not external_scope + and connector_readback.get("readback_status") == "visible" + and connector_readback.get("visibility") == "private" + and connector_readback.get("private_visibility_verified") is True + ) visibility_status = _visibility_evidence_status( external_scope=external_scope, probe_status=probe_status, + private_visibility_verified=private_visibility_verified, ) - blockers = _target_blockers(visibility_status, approval_required) + blockers = _target_blockers(visibility_status, approval_required, private_visibility_verified) forbidden_actions = _strings(approval_item.get("still_forbidden")) or [ "create_github_repo", "change_repo_visibility", @@ -203,8 +236,10 @@ def _build_target( "target_state": target_state, "risk": str(decision.get("risk") or "UNKNOWN"), "visibility_evidence_status": visibility_status, - "private_backup_verified": False, - "private_visibility_owner_evidence_ref": None, + "github_connector_readback_status": str(connector_readback.get("readback_status") or "not_checked"), + "github_connector_visibility": connector_readback.get("visibility"), + "private_backup_verified": private_visibility_verified, + "private_visibility_owner_evidence_ref": connector_readback.get("evidence_ref"), "safe_credential_evidence_status": "not_collected", "safe_credential_evidence_ref": None, "owner_response_accepted": False, @@ -222,9 +257,16 @@ def _build_target( } -def _visibility_evidence_status(*, external_scope: bool, probe_status: str) -> str: +def _visibility_evidence_status( + *, + external_scope: bool, + probe_status: str, + private_visibility_verified: bool, +) -> str: if external_scope: return "external_scope_not_backup_target" + if private_visibility_verified: + return "verified_private_by_github_connector_readback" if probe_status == "exists": return "blocked_public_probe_visible_private_evidence_required" if probe_status == "not_found_or_private": @@ -232,15 +274,20 @@ def _visibility_evidence_status(*, external_scope: bool, probe_status: str) -> s return "blocked_probe_status_unknown" -def _target_blockers(visibility_status: str, approval_required: bool) -> list[str]: +def _target_blockers( + visibility_status: str, + approval_required: bool, + private_visibility_verified: bool, +) -> list[str]: if not approval_required: return ["external_scope_not_backup_target"] blockers = [ - "private_visibility_owner_evidence_missing", "safe_credential_evidence_missing", "owner_response_not_accepted", "refs_sync_not_authorized", ] + if not private_visibility_verified: + blockers.insert(0, "private_visibility_owner_evidence_missing") if visibility_status == "blocked_public_probe_visible_private_evidence_required": blockers.insert(0, "public_probe_visible_not_private_backup") if visibility_status == "blocked_private_or_absent_not_verified": @@ -256,20 +303,31 @@ def _load_snapshot(path: Path) -> dict[str, Any]: return payload +def _load_optional_snapshot(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + return _load_snapshot(path) + + def _require_source_contracts( *, decision: dict[str, Any], owner_response: dict[str, Any], approval_package: dict[str, Any], probe: dict[str, Any], + connector_readback: dict[str, Any], ) -> None: _require_schema(decision, "github_target_decision_v1", _DECISION_FILE) _require_schema(owner_response, "github_target_owner_decision_response_v1", _OWNER_RESPONSE_FILE) _require_schema(approval_package, "github_target_repo_approval_package_v1", _APPROVAL_PACKAGE_FILE) _require_schema(probe, "github_target_probe_v1", _PROBE_FILE) + if connector_readback: + _require_schema(connector_readback, "github_target_connector_readback_v1", _CONNECTOR_READBACK_FILE) _require_decision_consistency(decision, _DECISION_FILE) _require_probe_consistency(probe, _PROBE_FILE) _require_approval_package_consistency(approval_package, _APPROVAL_PACKAGE_FILE) + if connector_readback: + _require_connector_readback_consistency(connector_readback, _CONNECTOR_READBACK_FILE) _require_owner_response_boundaries(owner_response, _OWNER_RESPONSE_FILE) @@ -306,6 +364,29 @@ def _require_approval_package_consistency(payload: dict[str, Any], label: str) - raise ValueError(f"{label}: package_count must equal approval_items length") +def _require_connector_readback_consistency(payload: dict[str, Any], label: str) -> None: + summary = _dict(payload.get("summary")) + targets = [_dict(row) for row in _list(payload.get("targets"))] + if _int(summary.get("target_count")) != len(targets): + raise ValueError(f"{label}: summary.target_count must equal targets length") + private_count = sum(1 for row in targets if row.get("private_visibility_verified") is True) + not_found_count = sum(1 for row in targets if row.get("readback_status") == "not_found_or_inaccessible") + if _int(summary.get("private_visibility_verified_count")) != private_count: + raise ValueError(f"{label}: private visibility count must match targets") + if _int(summary.get("not_found_or_inaccessible_count")) != not_found_count: + raise ValueError(f"{label}: not found count must match targets") + false_flags = { + "github_api_write_performed", + "repo_creation_performed", + "visibility_change_performed", + "refs_sync_performed", + "secret_values_collected", + } + enabled = sorted(flag for flag in false_flags if summary.get(flag) is not False) + if enabled: + raise ValueError(f"{label}: connector readback must remain read-only: {enabled}") + + def _require_owner_response_boundaries(payload: dict[str, Any], label: str) -> None: if payload.get("runtime_execution_authorized") is not False: raise ValueError(f"{label}: runtime_execution_authorized must be false") diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index a80eae3da..8819dda14 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -39,8 +39,8 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert sources["github_private_backup"]["schema_version"] == "github_target_private_backup_evidence_gate_v1" assert sources["github_private_backup"]["missing_reason"] == "" assert lanes["github"]["blocker_count"] == 9 - assert lanes["github"]["status"] == "blocked_public_visibility_and_safe_credential_evidence_required" - assert lanes["github"]["metric"]["verified"] == 0 + assert lanes["github"]["status"] == "blocked_private_visibility_and_safe_credential_evidence_required" + assert lanes["github"]["metric"]["verified"] == 4 assert lanes["github"]["metric"]["total"] == 9 assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values()) assert all(lane["tone"] in {"ok", "warn", "danger"} for lane in lanes.values()) diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate.py b/apps/api/tests/test_github_target_private_backup_evidence_gate.py index 4be3e2db3..102127997 100644 --- a/apps/api/tests/test_github_target_private_backup_evidence_gate.py +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate.py @@ -17,10 +17,14 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot assert snapshot["schema_version"] == "github_target_private_backup_evidence_gate_v1" assert snapshot["mode"] == "read_only_private_backup_evidence_gate" - assert snapshot["status"] == "blocked_public_visibility_and_safe_credential_evidence_required" + assert snapshot["status"] == "blocked_private_visibility_and_safe_credential_evidence_required" assert snapshot["summary"]["target_decision_count"] == 10 assert snapshot["summary"]["approval_required_target_count"] == 9 - assert snapshot["summary"]["private_backup_verified_count"] == 0 + assert snapshot["summary"]["github_connector_readback_count"] == 9 + assert snapshot["summary"]["github_connector_private_visibility_count"] == 4 + assert snapshot["summary"]["github_connector_not_found_or_inaccessible_count"] == 5 + assert snapshot["summary"]["private_backup_verified_count"] == 4 + assert snapshot["summary"]["private_visibility_verified_count"] == 4 assert snapshot["summary"]["blocked_target_count"] == 9 assert snapshot["summary"]["public_repo_allowed"] is False assert snapshot["summary"]["repo_creation_authorized"] is False @@ -35,11 +39,14 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot targets = {target["github_repo"]: target for target in snapshot["targets"]} assert targets["owenhytsai/awoooi"]["visibility_evidence_status"] == ( - "blocked_public_probe_visible_private_evidence_required" + "verified_private_by_github_connector_readback" ) + assert targets["owenhytsai/awoooi"]["private_backup_verified"] is True + assert targets["owenhytsai/awoooi"]["github_connector_visibility"] == "private" assert targets["owenhytsai/ewoooc"]["visibility_evidence_status"] == ( "blocked_private_or_absent_not_verified" ) + assert targets["owenhytsai/ewoooc"]["private_backup_verified"] is False assert targets["nexu-io/open-design"]["visibility_evidence_status"] == ( "external_scope_not_backup_target" ) @@ -74,5 +81,6 @@ def _copy_security_snapshots(tmp_path: Path) -> None: "github-target-owner-decision-response.snapshot.json", "github-target-repo-approval-package.snapshot.json", "github-target-probe.snapshot.json", + "github-target-connector-readback.snapshot.json", ): shutil.copy(source_dir / filename, tmp_path / filename) diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py index 0019f8328..f3c5e2ec6 100644 --- a/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py @@ -18,7 +18,11 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1" assert data["mode"] == "read_only_private_backup_evidence_gate" assert data["summary"]["approval_required_target_count"] == 9 - assert data["summary"]["private_backup_verified_count"] == 0 + assert data["summary"]["github_connector_readback_count"] == 9 + assert data["summary"]["github_connector_private_visibility_count"] == 4 + assert data["summary"]["github_connector_not_found_or_inaccessible_count"] == 5 + assert data["summary"]["private_backup_verified_count"] == 4 + assert data["summary"]["private_visibility_verified_count"] == 4 assert data["summary"]["blocked_target_count"] == 9 assert data["summary"]["public_repo_allowed"] is False assert data["summary"]["repo_creation_authorized"] is False diff --git a/docs/security/github-target-connector-readback.snapshot.json b/docs/security/github-target-connector-readback.snapshot.json new file mode 100644 index 000000000..47d233300 --- /dev/null +++ b/docs/security/github-target-connector-readback.snapshot.json @@ -0,0 +1,108 @@ +{ + "schema_version": "github_target_connector_readback_v1", + "generated_at": "2026-06-27T11:26:18+08:00", + "mode": "read_only_github_connector_metadata", + "source": "codex_github_connector_get_repository", + "summary": { + "target_count": 9, + "private_visibility_verified_count": 4, + "not_found_or_inaccessible_count": 5, + "github_api_write_performed": false, + "repo_creation_performed": false, + "visibility_change_performed": false, + "refs_sync_performed": false, + "secret_values_collected": false + }, + "targets": [ + { + "github_repo": "owenhytsai/awoooi", + "readback_status": "visible", + "visibility": "private", + "default_branch": "main", + "archived": false, + "repo_id": "1188603327", + "private_visibility_verified": true, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/awoooi:visibility=private" + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "readback_status": "visible", + "visibility": "private", + "default_branch": "main", + "archived": false, + "repo_id": "1177061851", + "private_visibility_verified": true, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/clawbot-v5:visibility=private" + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "readback_status": "visible", + "visibility": "private", + "default_branch": "main", + "archived": false, + "repo_id": "1175137404", + "private_visibility_verified": true, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/wooo-aiops:visibility=private" + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "readback_status": "visible", + "visibility": "private", + "default_branch": "main", + "archived": false, + "repo_id": "1177060194", + "private_visibility_verified": true, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/wooo-infra-config:visibility=private" + }, + { + "github_repo": "owenhytsai/ewoooc", + "readback_status": "not_found_or_inaccessible", + "visibility": null, + "default_branch": null, + "archived": null, + "repo_id": null, + "private_visibility_verified": false, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/ewoooc:404" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "readback_status": "not_found_or_inaccessible", + "visibility": null, + "default_branch": null, + "archived": null, + "repo_id": null, + "private_visibility_verified": false, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/bitan-pharmacy:404" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "readback_status": "not_found_or_inaccessible", + "visibility": null, + "default_branch": null, + "archived": null, + "repo_id": null, + "private_visibility_verified": false, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/tsenyang-website:404" + }, + { + "github_repo": "owenhytsai/VibeWork", + "readback_status": "not_found_or_inaccessible", + "visibility": null, + "default_branch": null, + "archived": null, + "repo_id": null, + "private_visibility_verified": false, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/VibeWork:404" + }, + { + "github_repo": "owenhytsai/agent-bounty-protocol", + "readback_status": "not_found_or_inaccessible", + "visibility": null, + "default_branch": null, + "archived": null, + "repo_id": null, + "private_visibility_verified": false, + "evidence_ref": "github_connector_repo_metadata:owenhytsai/agent-bounty-protocol:404" + } + ] +}