diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index ce05ae5d8..356ee3e5e 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -55,6 +55,10 @@ def build_delivery_closure_workbench( github_boundaries = _dict(github.get("operation_boundaries")) github_preflight = _dict(github.get("controlled_execution_preflight")) github_operator_unblock = _dict(github_preflight.get("operator_unblock")) + github_governance_writeback = _dict( + github_preflight.get("internal_governance_writeback") + or github.get("internal_governance_writeback") + ) gitea_status = _dict(gitea.get("program_status")) gitea_rollups = _dict(gitea.get("rollups")) runtime_status = _dict(runtime.get("program_status")) @@ -122,9 +126,20 @@ def build_delivery_closure_workbench( "github_account_suspended" ) is True, + "internal_governance_ready": github_governance_writeback.get("ready") + is True, + "km_writeback_ready": _int( + github_governance_writeback.get("km_writeback_ready_count") + ), + "playbook_writeback_ready": _int( + github_governance_writeback.get( + "playbook_writeback_ready_count" + ) + ), }, "href": "/governance?tab=automation-inventory", "operator_unblock": github_operator_unblock, + "internal_governance_writeback": github_governance_writeback, "next_action": str( _first_string(github_operator_unblock.get("required_actions")) or github_operator_unblock.get("safe_handoff") @@ -263,6 +278,28 @@ def build_delivery_closure_workbench( "github_operator_unblock_status": str( github_operator_unblock.get("status") or "" ), + "github_internal_governance_writeback_ready": github_governance_writeback.get( + "ready" + ) + is True, + "github_mcp_evidence_packet_count": _int( + github_governance_writeback.get("mcp_evidence_packet_count") + ), + "github_rag_evidence_ref_count": _int( + github_governance_writeback.get("rag_evidence_ref_count") + ), + "github_km_writeback_ready_count": _int( + github_governance_writeback.get("km_writeback_ready_count") + ), + "github_playbook_writeback_ready_count": _int( + github_governance_writeback.get("playbook_writeback_ready_count") + ), + "github_timeline_log_ready_count": _int( + github_governance_writeback.get("timeline_log_ready_count") + ), + "github_logbook_entry_count": _int( + github_governance_writeback.get("logbook_entry_count") + ), "secret_values_collected": False, }, "source_statuses": source_statuses, 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 0f818203b..87a24782a 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 @@ -29,12 +29,18 @@ _EXECUTION_AUTHORIZATION_FILE = ( _CONTROLLED_EXECUTION_PREFLIGHT_FILE = ( "github-target-controlled-execution-preflight.snapshot.json" ) +_OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_FILE = ( + "github-operator-unblock-governance-closure.snapshot.json" +) _EXECUTION_AUTHORIZATION_SCHEMA_VERSION = ( "github_target_owner_execution_authorization_v1" ) _CONTROLLED_EXECUTION_PREFLIGHT_SCHEMA_VERSION = ( "github_target_controlled_execution_preflight_v1" ) +_OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_SCHEMA_VERSION = ( + "github_operator_unblock_governance_closure_v1" +) _GITHUB_WRITE_CHANNEL_RECHECK_COMMANDS = [ "gh api /user --jq '{login:.login}'", "git push --dry-run origin HEAD:refs/heads/codex-github-write-channel-readonly-check", @@ -166,6 +172,9 @@ def load_latest_github_target_private_backup_evidence_gate( controlled_execution_preflight = _load_optional_snapshot( directory / _CONTROLLED_EXECUTION_PREFLIGHT_FILE ) + operator_unblock_governance_closure = _load_optional_snapshot( + directory / _OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_FILE + ) _require_source_contracts( decision=decision, @@ -176,6 +185,7 @@ def load_latest_github_target_private_backup_evidence_gate( missing_source_readiness=missing_source_readiness, execution_authorization=execution_authorization, controlled_execution_preflight=controlled_execution_preflight, + operator_unblock_governance_closure=operator_unblock_governance_closure, ) return build_github_target_private_backup_evidence_gate( decision=decision, @@ -186,6 +196,7 @@ def load_latest_github_target_private_backup_evidence_gate( missing_source_readiness=missing_source_readiness, execution_authorization=execution_authorization, controlled_execution_preflight=controlled_execution_preflight, + operator_unblock_governance_closure=operator_unblock_governance_closure, ) @@ -504,6 +515,7 @@ def build_github_target_private_backup_evidence_gate( missing_source_readiness: dict[str, Any] | None = None, execution_authorization: dict[str, Any] | None = None, controlled_execution_preflight: dict[str, Any] | None = None, + operator_unblock_governance_closure: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build the read-only gate response from source-control snapshots.""" connector_payload = _dict(connector_readback) @@ -512,6 +524,7 @@ def build_github_target_private_backup_evidence_gate( missing_summary = _dict(missing_payload.get("summary")) authorization_payload = _dict(execution_authorization) execution_preflight_payload = _dict(controlled_execution_preflight) + governance_payload = _dict(operator_unblock_governance_closure) authorization_summary = _execution_authorization_summary(authorization_payload) execution_authorized = ( authorization_summary["authorization_present"] @@ -615,6 +628,15 @@ def build_github_target_private_backup_evidence_gate( targets=targets, authorization_summary=authorization_summary, ) + internal_governance_writeback = ( + _github_operator_unblock_internal_governance_writeback( + payload=governance_payload, + controlled_preflight=controlled_preflight, + ) + ) + controlled_preflight["internal_governance_writeback"] = ( + internal_governance_writeback + ) private_backup_verified_count = sum( 1 for row in approval_required_targets if row["private_backup_verified"] @@ -647,6 +669,7 @@ def build_github_target_private_backup_evidence_gate( "github_missing_source_readiness": f"docs/security/{_MISSING_SOURCE_READINESS_FILE}", "github_owner_execution_authorization": f"docs/security/{_EXECUTION_AUTHORIZATION_FILE}", "github_controlled_execution_preflight": f"docs/security/{_CONTROLLED_EXECUTION_PREFLIGHT_FILE}", + "github_operator_unblock_governance_closure": f"docs/security/{_OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_FILE}", }, "summary": { "target_decision_count": len(targets), @@ -817,6 +840,30 @@ def build_github_target_private_backup_evidence_gate( "github_missing_target_github_404_count": controlled_preflight[ "github_connector_missing_target_404_count" ], + "github_operator_unblock_internal_governance_ready": internal_governance_writeback[ + "ready" + ], + "github_operator_unblock_mcp_evidence_packet_count": internal_governance_writeback[ + "mcp_evidence_packet_count" + ], + "github_operator_unblock_rag_evidence_ref_count": internal_governance_writeback[ + "rag_evidence_ref_count" + ], + "github_operator_unblock_km_writeback_ready_count": internal_governance_writeback[ + "km_writeback_ready_count" + ], + "github_operator_unblock_playbook_writeback_ready_count": internal_governance_writeback[ + "playbook_writeback_ready_count" + ], + "github_operator_unblock_timeline_log_ready_count": internal_governance_writeback[ + "timeline_log_ready_count" + ], + "github_operator_unblock_logbook_entry_count": internal_governance_writeback[ + "logbook_entry_count" + ], + "github_operator_unblock_github_write_performed": internal_governance_writeback[ + "github_write_performed" + ], "execution_ready_count": sum( 1 for row in approval_required_targets if row["execution_ready"] ), @@ -847,6 +894,7 @@ def build_github_target_private_backup_evidence_gate( "owner_response_intake_readiness": owner_response_intake_readiness, "safe_credential_evidence_intake_readiness": safe_credential_evidence_intake_readiness, "controlled_execution_preflight": controlled_preflight, + "internal_governance_writeback": internal_governance_writeback, "targets": targets, "acceptance_requirements": _acceptance_requirements(owner_response), "rejection_rules": _rejection_rules(owner_response), @@ -1375,6 +1423,7 @@ def _require_source_contracts( missing_source_readiness: dict[str, Any], execution_authorization: dict[str, Any], controlled_execution_preflight: dict[str, Any], + operator_unblock_governance_closure: dict[str, Any], ) -> None: _require_schema(decision, "github_target_decision_v1", _DECISION_FILE) _require_schema( @@ -1410,6 +1459,12 @@ def _require_source_contracts( _CONTROLLED_EXECUTION_PREFLIGHT_SCHEMA_VERSION, _CONTROLLED_EXECUTION_PREFLIGHT_FILE, ) + if operator_unblock_governance_closure: + _require_schema( + operator_unblock_governance_closure, + _OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_SCHEMA_VERSION, + _OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_FILE, + ) _require_decision_consistency(decision, _DECISION_FILE) _require_probe_consistency(probe, _PROBE_FILE) _require_approval_package_consistency(approval_package, _APPROVAL_PACKAGE_FILE) @@ -1434,6 +1489,11 @@ def _require_source_contracts( missing_source_readiness=missing_source_readiness, label=_CONTROLLED_EXECUTION_PREFLIGHT_FILE, ) + if operator_unblock_governance_closure: + _require_operator_unblock_governance_closure_consistency( + payload=operator_unblock_governance_closure, + label=_OPERATOR_UNBLOCK_GOVERNANCE_CLOSURE_FILE, + ) _require_owner_response_boundaries(owner_response, _OWNER_RESPONSE_FILE) @@ -1853,6 +1913,87 @@ def _require_controlled_execution_preflight_consistency( ) +def _require_operator_unblock_governance_closure_consistency( + *, payload: dict[str, Any], label: str +) -> None: + summary = _dict(payload.get("summary")) + boundaries = _dict(payload.get("operation_boundaries")) + mcp_packets = _list(payload.get("mcp_evidence_packets")) + rag_refs = _list(payload.get("rag_evidence_refs")) + km_items = [_dict(row) for row in _list(payload.get("km_writeback_items"))] + playbook_items = [ + _dict(row) for row in _list(payload.get("playbook_writeback_items")) + ] + timeline_items = [_dict(row) for row in _list(payload.get("timeline_log_items"))] + logbook_entries = _list(payload.get("logbook_entries")) + + count_pairs = { + "mcp_evidence_packet_count": len(mcp_packets), + "rag_evidence_ref_count": len(rag_refs), + "km_writeback_item_count": len(km_items), + "km_writeback_ready_count": sum( + 1 for row in km_items if row.get("ready_for_writeback") is True + ), + "playbook_writeback_item_count": len(playbook_items), + "playbook_writeback_ready_count": sum( + 1 for row in playbook_items if row.get("ready_for_writeback") is True + ), + "timeline_log_item_count": len(timeline_items), + "timeline_log_ready_count": sum( + 1 for row in timeline_items if row.get("ready_for_writeback") is True + ), + "logbook_entry_count": len(logbook_entries), + } + mismatched = sorted( + key for key, expected in count_pairs.items() if _int(summary.get(key)) != expected + ) + if mismatched: + raise ValueError(f"{label}: governance writeback count drift: {mismatched}") + + false_summary_flags = { + "github_write_performed", + "gh_cli_called", + "github_api_called", + "github_repo_creation_performed", + "github_refs_sync_performed", + "github_visibility_change_performed", + "secret_values_collected", + "raw_session_or_sqlite_read", + "runtime_write_performed", + "host_or_k8s_write_performed", + } + enabled = sorted( + flag for flag in false_summary_flags if summary.get(flag) is not False + ) + false_boundary_flags = { + "github_app_connector_mcp_allowed", + "gh_cli_allowed", + "github_api_allowed", + "github_actions_allowed", + "github_repo_creation_allowed", + "github_refs_sync_allowed", + "github_visibility_change_allowed", + "github_primary_switch_allowed", + "force_push_allowed", + "delete_refs_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + "raw_session_read_allowed", + "sqlite_read_allowed", + "runtime_write_allowed", + "host_or_k8s_write_allowed", + } + enabled.extend( + sorted( + flag for flag in false_boundary_flags if boundaries.get(flag) is not False + ) + ) + if enabled: + raise ValueError( + f"{label}: governance closure must stay local-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") @@ -2247,6 +2388,124 @@ def _github_write_channel_operator_unblock( } +def _github_operator_unblock_internal_governance_writeback( + *, + payload: dict[str, Any], + controlled_preflight: dict[str, Any], +) -> dict[str, Any]: + if not payload: + return { + "schema_version": "missing_github_operator_unblock_governance_closure_v1", + "generated_at": "", + "status": "missing_operator_unblock_governance_closure_snapshot", + "ready": False, + "mcp_evidence_packet_count": 0, + "rag_evidence_ref_count": 0, + "km_writeback_ready_count": 0, + "playbook_writeback_ready_count": 0, + "timeline_log_ready_count": 0, + "logbook_entry_count": 0, + "github_account_status": controlled_preflight.get( + "github_account_status" + ), + "github_account_suspended": controlled_preflight.get( + "github_account_suspended" + ) + is True, + "github_write_channel_ready": controlled_preflight.get( + "github_write_channel_ready" + ) + is True, + "github_write_performed": False, + "secret_values_collected": False, + "source_refs": {}, + "mcp_evidence_packets": [], + "rag_evidence_refs": [], + "km_writeback_items": [], + "playbook_writeback_items": [], + "timeline_log_items": [], + "logbook_entries": [], + "operation_boundaries": {}, + "still_forbidden": [], + "next_action": "add_committed_operator_unblock_governance_closure_snapshot", + } + + summary = _dict(payload.get("summary")) + boundaries = _dict(payload.get("operation_boundaries")) + return { + "schema_version": str(payload.get("schema_version") or ""), + "generated_at": str(payload.get("generated_at") or ""), + "status": str(payload.get("status") or ""), + "ready": summary.get("governance_writeback_ready") is True, + "mcp_evidence_packet_count": _int( + summary.get("mcp_evidence_packet_count") + ), + "rag_evidence_ref_count": _int(summary.get("rag_evidence_ref_count")), + "km_writeback_ready_count": _int( + summary.get("km_writeback_ready_count") + ), + "playbook_writeback_ready_count": _int( + summary.get("playbook_writeback_ready_count") + ), + "timeline_log_ready_count": _int( + summary.get("timeline_log_ready_count") + ), + "logbook_entry_count": _int(summary.get("logbook_entry_count")), + "github_account_status": str( + summary.get("github_account_status") + or controlled_preflight.get("github_account_status") + or "unknown" + ), + "github_account_suspended": summary.get("github_account_suspended") is True, + "github_write_channel_ready": summary.get("github_write_channel_ready") + is True, + "source_preflight_ready_count": _int( + summary.get("source_preflight_ready_count") + ), + "controlled_apply_ready_count": _int( + summary.get("controlled_apply_ready_count") + ), + "blocked_preflight_target_count": _int( + summary.get("blocked_preflight_target_count") + ), + "github_write_performed": summary.get("github_write_performed") is True, + "gh_cli_called": summary.get("gh_cli_called") is True, + "github_api_called": summary.get("github_api_called") is True, + "secret_values_collected": summary.get("secret_values_collected") is True, + "source_refs": _dict(payload.get("source_refs")), + "mcp_evidence_packets": _list(payload.get("mcp_evidence_packets")), + "rag_evidence_refs": _list(payload.get("rag_evidence_refs")), + "km_writeback_items": _list(payload.get("km_writeback_items")), + "playbook_writeback_items": _list(payload.get("playbook_writeback_items")), + "timeline_log_items": _list(payload.get("timeline_log_items")), + "logbook_entries": _list(payload.get("logbook_entries")), + "operation_boundaries": { + "local_source_snapshot_allowed": boundaries.get( + "local_source_snapshot_allowed" + ) + is True, + "github_app_connector_mcp_allowed": False, + "gh_cli_allowed": False, + "github_api_allowed": False, + "github_actions_allowed": False, + "github_repo_creation_allowed": False, + "github_refs_sync_allowed": False, + "github_visibility_change_allowed": False, + "github_primary_switch_allowed": False, + "force_push_allowed": False, + "delete_refs_allowed": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + "raw_session_read_allowed": False, + "sqlite_read_allowed": False, + "runtime_write_allowed": False, + "host_or_k8s_write_allowed": False, + }, + "still_forbidden": _strings(payload.get("still_forbidden")), + "next_action": str(payload.get("next_action") or ""), + } + + def _controlled_execution_target_summary(value: Any) -> dict[str, Any]: row = _dict(value) return { diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index 2a12d6ef7..4dc8af225 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -34,6 +34,13 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert data["summary"]["github_operator_unblock_status"] == ( "github_account_suspended_external_action_required" ) + assert data["summary"]["github_internal_governance_writeback_ready"] is True + assert data["summary"]["github_mcp_evidence_packet_count"] == 1 + assert data["summary"]["github_rag_evidence_ref_count"] == 5 + assert data["summary"]["github_km_writeback_ready_count"] == 1 + assert data["summary"]["github_playbook_writeback_ready_count"] == 1 + assert data["summary"]["github_timeline_log_ready_count"] == 1 + assert data["summary"]["github_logbook_entry_count"] == 1 assert data["summary"]["secret_values_collected"] is False assert data["summary"]["average_completion_percent"] >= 0 assert data["summary"]["high_risk_blocker_count"] > 0 @@ -68,6 +75,9 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert lanes["github"]["metric"]["write_channel_ready"] is False assert lanes["github"]["metric"]["github_account_status"] == "suspended" assert lanes["github"]["metric"]["github_account_suspended"] is True + assert lanes["github"]["metric"]["internal_governance_ready"] is True + assert lanes["github"]["metric"]["km_writeback_ready"] == 1 + assert lanes["github"]["metric"]["playbook_writeback_ready"] == 1 assert lanes["github"]["operator_unblock"]["required"] is True assert lanes["github"]["operator_unblock"]["status"] == ( "github_account_suspended_external_action_required" @@ -76,6 +86,17 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" in lanes["github"]["operator_unblock"]["required_actions"] ) + assert lanes["github"]["internal_governance_writeback"]["ready"] is True + assert ( + lanes["github"]["internal_governance_writeback"][ + "mcp_evidence_packet_count" + ] + == 1 + ) + assert ( + lanes["github"]["internal_governance_writeback"]["github_write_performed"] + is False + ) assert lanes["github"]["next_action"] == ( "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" ) 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 c8b367284..7c460d516 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 @@ -74,6 +74,26 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot assert snapshot["summary"]["github_account_status"] == "suspended" assert snapshot["summary"]["github_account_suspended"] is True assert snapshot["summary"]["github_api_forbidden_count"] == 6 + assert ( + snapshot["summary"]["github_operator_unblock_internal_governance_ready"] + is True + ) + assert ( + snapshot["summary"]["github_operator_unblock_mcp_evidence_packet_count"] == 1 + ) + assert snapshot["summary"]["github_operator_unblock_rag_evidence_ref_count"] == 5 + assert ( + snapshot["summary"]["github_operator_unblock_km_writeback_ready_count"] == 1 + ) + assert ( + snapshot["summary"]["github_operator_unblock_playbook_writeback_ready_count"] + == 1 + ) + assert ( + snapshot["summary"]["github_operator_unblock_timeline_log_ready_count"] == 1 + ) + assert snapshot["summary"]["github_operator_unblock_logbook_entry_count"] == 1 + assert snapshot["summary"]["github_operator_unblock_github_write_performed"] is False assert snapshot["summary"]["private_backup_verified_count"] == 4 assert snapshot["summary"]["private_visibility_verified_count"] == 4 assert snapshot["summary"]["safe_credential_required_count"] == 9 @@ -279,6 +299,35 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot ] is False ) + governance_writeback = controlled_preflight["internal_governance_writeback"] + assert ( + governance_writeback["schema_version"] + == "github_operator_unblock_governance_closure_v1" + ) + assert governance_writeback["ready"] is True + assert governance_writeback["github_account_status"] == "suspended" + assert governance_writeback["github_write_channel_ready"] is False + assert governance_writeback["mcp_evidence_packet_count"] == 1 + assert governance_writeback["rag_evidence_ref_count"] == 5 + assert governance_writeback["km_writeback_ready_count"] == 1 + assert governance_writeback["playbook_writeback_ready_count"] == 1 + assert governance_writeback["timeline_log_ready_count"] == 1 + assert governance_writeback["logbook_entry_count"] == 1 + assert governance_writeback["github_write_performed"] is False + assert governance_writeback["gh_cli_called"] is False + assert governance_writeback["github_api_called"] is False + assert governance_writeback["secret_values_collected"] is False + assert ( + governance_writeback["operation_boundaries"]["github_api_allowed"] is False + ) + assert governance_writeback["operation_boundaries"]["gh_cli_allowed"] is False + assert ( + governance_writeback["operation_boundaries"]["runtime_write_allowed"] is False + ) + assert ( + governance_writeback["operation_boundaries"]["raw_session_read_allowed"] + is False + ) targets = {target["github_repo"]: target for target in snapshot["targets"]} assert targets["owenhytsai/awoooi"]["visibility_evidence_status"] == ( 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 fefcfd1ca..a5fa1b490 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 @@ -143,6 +143,15 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g in controlled_preflight["operator_unblock"]["recheck_commands"] ) assert controlled_preflight["blocked_preflight_target_count"] == 5 + governance_writeback = controlled_preflight["internal_governance_writeback"] + assert governance_writeback["ready"] is True + assert governance_writeback["mcp_evidence_packet_count"] == 1 + assert governance_writeback["rag_evidence_ref_count"] == 5 + assert governance_writeback["km_writeback_ready_count"] == 1 + assert governance_writeback["playbook_writeback_ready_count"] == 1 + assert governance_writeback["github_write_performed"] is False + assert governance_writeback["operation_boundaries"]["github_api_allowed"] is False + assert governance_writeback["operation_boundaries"]["gh_cli_allowed"] is False assert "192.168.0." not in response.text @@ -183,6 +192,19 @@ def test_github_target_controlled_execution_preflight_endpoint_returns_write_gap assert data["operation_boundaries"]["controlled_apply_allowed"] is False assert data["operation_boundaries"]["secret_value_collection_allowed"] is False assert data["tool_channel_readback"]["gh_cli_write_ready"] is False + assert data["internal_governance_writeback"]["ready"] is True + assert data["internal_governance_writeback"]["km_writeback_ready_count"] == 1 + assert ( + data["internal_governance_writeback"]["playbook_writeback_ready_count"] == 1 + ) + assert data["internal_governance_writeback"]["github_api_called"] is False + assert data["internal_governance_writeback"]["gh_cli_called"] is False + assert ( + data["internal_governance_writeback"]["operation_boundaries"][ + "github_refs_sync_allowed" + ] + is False + ) assert data["targets"][0]["controlled_apply_ready"] is False assert "github_account_suspended_403" in data["targets"][0]["blockers"] assert "192.168.0." not in response.text diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 9c130d2fa..bbf49729f 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -48638,3 +48638,29 @@ production browser smoke: **下一個 P0**: - commit / push 到 Gitea main,完成 deployment readback 後驗證 `GET /api/v1/iwooos/wazuh-live-metadata-gate`、`GET /api/v1/iwooos/runtime-security-readback` 與 `/zh-TW/iwooos` desktop / mobile;下一關才是 controlled server-side env enable + post-enable readback,仍不得讀或提交 secret value。 + +## 2026-06-28 — 18:51 GitHub operator unblock 內部治理閉環 + +**背景**: +- GitHub missing targets 的 source readiness 與 controlled execution preflight 已能在 API 讀回;正式 production readback 已明確顯示 `github_account_status=suspended`、`github_write_channel_ready=false`、`controlled_apply_ready_count=0`、`blocked_preflight_target_count=5`。 +- 這代表真正阻擋「推上 GitHub」的是外部 GitHub 帳號 / write channel,不是 AWOOOI repo、Gitea CD、source selector 或本機治理 gate。 +- 2026-06-28 GitHub freeze 生效後,不再把 GitHub repo / refs / visibility / API / gh / Actions 當作可執行下一步;只能做本機安全整理與 Gitea/local source governance。 + +**完成內容**: +- 新增 `docs/security/github-operator-unblock-governance-closure.snapshot.json`,把 GitHub external channel blocker 轉成 MCP evidence packet、RAG evidence refs、KM writeback item、PlayBook writeback item、timeline log item 與 LOGBOOK entry。 +- `github_target_private_backup_evidence_gate` 新增 `internal_governance_writeback` 投影;`controlled_execution_preflight` 同步帶出同一份治理閉環,讓 API 消費者不必再從舊聊天或人工敘述還原阻擋原因。 +- `delivery_closure_workbench` 的 GitHub lane 新增 internal governance readiness 與 KM / PlayBook / RAG / timeline / LOG counters,讓產品交付總覽可直接看見 operator unblock 已被內部化。 + +**仍維持 0 / false**: +- `github_write_channel_ready=false`、`controlled_apply_ready_count=0`、`blocked_preflight_target_count=5`。 +- `github_api_called=false`、`gh_cli_called=false`、`github_write_performed=false`、`github_repo_creation_performed=false`、`github_refs_sync_performed=false`、`github_visibility_change_performed=false`。 +- `secret_values_collected=false`、`raw_session_or_sqlite_read=false`、`runtime_write_performed=false`、`host_or_k8s_write_performed=false`。 + +**未做**: +- 沒有使用 GitHub app / connector / MCP、沒有執行 `gh`、沒有呼叫 GitHub API、沒有 GitHub Actions / PR / issue / review / search。 +- 沒有建立 repo、同步 refs、修改 visibility、force push、刪 refs、要求 token / cookie / private key / authorization header。 +- 沒有讀 raw sessions / SQLite / auth / `.env`,沒有 host / Docker / K8s / firewall / Wazuh runtime 操作。 + +**本段驗證目標**: +- JSON parse、`py_compile`、focused GitHub gate API tests、Delivery Workbench test、`git diff --check`。 +- 此段只關閉內部 MCP / RAG / KM / PlayBook / LOG governance gap;GitHub executable channel 仍維持 freeze / external unblock。 diff --git a/docs/security/github-operator-unblock-governance-closure.snapshot.json b/docs/security/github-operator-unblock-governance-closure.snapshot.json new file mode 100644 index 000000000..6af2dc971 --- /dev/null +++ b/docs/security/github-operator-unblock-governance-closure.snapshot.json @@ -0,0 +1,223 @@ +{ + "schema_version": "github_operator_unblock_governance_closure_v1", + "generated_at": "2026-06-28T18:51:11+08:00", + "status": "operator_unblock_governance_writeback_ready_no_github_write", + "summary": { + "governance_writeback_ready": true, + "mcp_evidence_packet_count": 1, + "rag_evidence_ref_count": 5, + "km_writeback_item_count": 1, + "km_writeback_ready_count": 1, + "playbook_writeback_item_count": 1, + "playbook_writeback_ready_count": 1, + "timeline_log_item_count": 1, + "timeline_log_ready_count": 1, + "logbook_entry_count": 1, + "github_account_status": "suspended", + "github_account_suspended": true, + "github_write_channel_ready": false, + "source_preflight_ready_count": 5, + "controlled_apply_ready_count": 0, + "blocked_preflight_target_count": 5, + "secret_value_collection_allowed_count": 0, + "github_write_performed": false, + "gh_cli_called": false, + "github_api_called": false, + "github_repo_creation_performed": false, + "github_refs_sync_performed": false, + "github_visibility_change_performed": false, + "secret_values_collected": false, + "raw_session_or_sqlite_read": false, + "runtime_write_performed": false, + "host_or_k8s_write_performed": false + }, + "source_refs": { + "controlled_execution_preflight": "docs/security/github-target-controlled-execution-preflight.snapshot.json", + "missing_source_readiness": "docs/security/github-target-missing-source-readiness.snapshot.json", + "private_backup_gate_service": "apps/api/src/services/github_target_private_backup_evidence_gate.py", + "delivery_workbench_service": "apps/api/src/services/delivery_closure_workbench.py", + "logbook": "docs/LOGBOOK.md" + }, + "mcp_evidence_packets": [ + { + "packet_id": "github_operator_unblock_external_channel_blocker_mcp_packet_20260628", + "source_id": "github_target_controlled_execution_preflight", + "status": "ready_for_internal_tool_context", + "decision": "external_channel_blocker_not_repo_cd_or_local_governance_blocker", + "evidence_refs": [ + "docs/security/github-target-controlled-execution-preflight.snapshot.json", + "docs/security/github-target-missing-source-readiness.snapshot.json", + "apps/api/src/services/github_target_private_backup_evidence_gate.py", + "apps/api/src/services/delivery_closure_workbench.py" + ], + "safe_fields": { + "github_account_status": "suspended", + "github_write_channel_ready": false, + "source_preflight_ready_count": 5, + "controlled_apply_ready_count": 0, + "blocked_preflight_target_count": 5 + }, + "redaction_boundary": "metadata_and_committed_refs_only_no_tokens_no_private_clone_urls_no_raw_sessions" + } + ], + "rag_evidence_refs": [ + { + "ref_id": "github-operator-unblock-controlled-preflight", + "path": "docs/security/github-target-controlled-execution-preflight.snapshot.json", + "topic": "github_operator_unblock", + "keywords": [ + "github_account_suspended", + "write_channel_blocked", + "source_preflight_ready", + "controlled_apply_ready_zero" + ] + }, + { + "ref_id": "github-missing-source-readiness", + "path": "docs/security/github-target-missing-source-readiness.snapshot.json", + "topic": "source_of_truth_selection", + "keywords": [ + "missing_targets", + "gitea_source_candidate", + "internal_remote_source_candidate", + "no_repo_write" + ] + }, + { + "ref_id": "github-private-backup-gate-service", + "path": "apps/api/src/services/github_target_private_backup_evidence_gate.py", + "topic": "api_projection", + "keywords": [ + "controlled_execution_preflight", + "operator_unblock", + "internal_governance_writeback" + ] + }, + { + "ref_id": "delivery-workbench-github-lane", + "path": "apps/api/src/services/delivery_closure_workbench.py", + "topic": "delivery_workbench_projection", + "keywords": [ + "github_lane", + "operator_unblock", + "km_playbook_writeback" + ] + }, + { + "ref_id": "github-operator-unblock-logbook-entry", + "path": "docs/LOGBOOK.md", + "topic": "operator_unblock_timeline", + "keywords": [ + "github_freeze", + "mcp", + "rag", + "km", + "playbook", + "log" + ] + } + ], + "km_writeback_items": [ + { + "item_id": "km-github-operator-unblock-external-channel-blocker", + "title": "GitHub account suspension is an external write-channel blocker", + "status": "ready_for_km_trust_writeback", + "ready_for_writeback": true, + "category": "source_control_governance", + "safe_summary": "Five missing GitHub backup targets have source preflight evidence, but controlled apply remains blocked because the GitHub account/write channel is suspended. Treat this as an external channel blocker, not as a repo/CD/local-governance blocker.", + "trust_signal": "negative_channel_availability_evidence", + "writeback_allowed": true, + "runtime_write_performed": false + } + ], + "playbook_writeback_items": [ + { + "playbook_id": "playbook-github-operator-unblock-external-channel-blocker", + "trigger": "github_account_suspended_external_action_required", + "status": "ready_for_playbook_draft", + "ready_for_writeback": true, + "controlled_actions": [ + "record_external_channel_blocker_in_delivery_workbench", + "keep_source_preflight_and_rollback_plan_attached", + "rerun_local_and_gitea_focused_tests_after_source_changes" + ], + "blocked_actions": [ + "github_app_connector_mcp", + "gh_cli", + "github_api", + "github_actions", + "repo_creation", + "refs_sync", + "visibility_change", + "force_push", + "delete_refs", + "secret_or_session_collection" + ], + "rollback_plan_ref": "docs/security/github-target-controlled-execution-preflight.snapshot.json#rollback_plan", + "post_apply_verifier_refs": [ + "apps/api/tests/test_github_target_private_backup_evidence_gate.py", + "apps/api/tests/test_github_target_private_backup_evidence_gate_api.py", + "apps/api/tests/test_delivery_closure_workbench_api.py" + ] + } + ], + "timeline_log_items": [ + { + "event_id": "timeline-github-operator-unblock-governance-closure-20260628", + "event_type": "source_control_governance", + "status": "ready_for_log_projection", + "ready_for_writeback": true, + "message": "GitHub operator unblock is captured as an internal governance evidence packet while GitHub executable channels remain disabled.", + "evidence_refs": [ + "docs/security/github-operator-unblock-governance-closure.snapshot.json", + "docs/security/github-target-controlled-execution-preflight.snapshot.json", + "docs/LOGBOOK.md" + ] + } + ], + "logbook_entries": [ + { + "entry_id": "logbook-github-operator-unblock-governance-closure-20260628", + "path": "docs/LOGBOOK.md", + "status": "ready_and_written", + "entry_written": true + } + ], + "operation_boundaries": { + "local_source_snapshot_allowed": true, + "github_app_connector_mcp_allowed": false, + "gh_cli_allowed": false, + "github_api_allowed": false, + "github_actions_allowed": false, + "github_repo_creation_allowed": false, + "github_refs_sync_allowed": false, + "github_visibility_change_allowed": false, + "github_primary_switch_allowed": false, + "force_push_allowed": false, + "delete_refs_allowed": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false, + "raw_session_read_allowed": false, + "sqlite_read_allowed": false, + "runtime_write_allowed": false, + "host_or_k8s_write_allowed": false + }, + "still_forbidden": [ + "github_app_connector_mcp", + "gh_cli", + "github_api", + "github_actions", + "repo_creation", + "refs_sync", + "visibility_change", + "force_push", + "delete_refs", + "public_visibility", + "github_primary_switch", + "token_or_secret_value_collection", + "private_clone_url_collection", + "raw_session_or_sqlite_read", + "runtime_or_host_write" + ], + "next_action": "Keep GitHub as an external operator-unblock item and continue Gitea/local governance work only." +}