diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index 41ed8dc3..a30de2f9 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -52,6 +52,7 @@ def build_delivery_closure_workbench( """Build the delivery workbench response from already validated snapshots.""" status_summary = _dict(status_cleanup.get("summary")) github_summary = _dict(github.get("summary")) + github_boundaries = _dict(github.get("operation_boundaries")) gitea_status = _dict(gitea.get("program_status")) gitea_rollups = _dict(gitea.get("rollups")) runtime_status = _dict(runtime.get("program_status")) @@ -61,14 +62,18 @@ def build_delivery_closure_workbench( github_required = _int(github_summary.get("approval_required_target_count")) github_verified = _int(github_summary.get("private_backup_verified_count")) - runtime_action_required = set(_strings(runtime_rollups.get("action_required_surface_ids"))) + runtime_action_required = set( + _strings(runtime_rollups.get("action_required_surface_ids")) + ) runtime_secret_surfaces = set(_strings(runtime_rollups.get("secret_surface_ids"))) lanes = [ { "id": "release", "source_id": "status_cleanup", - "completion_percent": _percent(status_summary.get("overall_completion_percent")), + "completion_percent": _percent( + status_summary.get("overall_completion_percent") + ), "status": str(status_summary.get("dashboard_status") or "unknown"), "blocker_count": _int(status_summary.get("blocked_gate_count")), "metric": { @@ -93,14 +98,20 @@ def build_delivery_closure_workbench( "total": github_required, }, "href": "/governance?tab=automation-inventory", - "next_action": str(github.get("next_action") or _first_target_action(github.get("targets"))), + "next_action": str( + github.get("next_action") or _first_target_action(github.get("targets")) + ), }, { "id": "gitea", "source_id": "gitea_ci_cd", - "completion_percent": _percent(gitea_status.get("overall_completion_percent")), + "completion_percent": _percent( + gitea_status.get("overall_completion_percent") + ), "status": str(gitea_status.get("current_task_id") or "unknown"), - "blocker_count": len(_strings(gitea_rollups.get("runner_contracts_requiring_action"))), + "blocker_count": len( + _strings(gitea_rollups.get("runner_contracts_requiring_action")) + ), "metric": { "kind": "workflow_count", "count": _int(gitea_rollups.get("total_workflows")), @@ -111,7 +122,9 @@ def build_delivery_closure_workbench( { "id": "runtime", "source_id": "runtime_surface", - "completion_percent": _percent(runtime_status.get("overall_completion_percent")), + "completion_percent": _percent( + runtime_status.get("overall_completion_percent") + ), "status": str(runtime_status.get("current_task_id") or "unknown"), "blocker_count": len(runtime_action_required | runtime_secret_surfaces), "metric": { @@ -124,7 +137,9 @@ def build_delivery_closure_workbench( { "id": "backup", "source_id": "backup_dr", - "completion_percent": _percent(backup_status.get("overall_completion_percent")), + "completion_percent": _percent( + backup_status.get("overall_completion_percent") + ), "status": str(backup_status.get("current_task_id") or "unknown"), "blocker_count": len(_strings(backup_rollups.get("blocked_row_ids"))), "metric": { @@ -137,7 +152,9 @@ def build_delivery_closure_workbench( ] for lane in lanes: - lane["tone"] = _tone(_int(lane["blocker_count"]), _int(lane["completion_percent"])) + lane["tone"] = _tone( + _int(lane["blocker_count"]), _int(lane["completion_percent"]) + ) source_statuses = [ _source_status("status_cleanup", status_cleanup), @@ -146,7 +163,9 @@ def build_delivery_closure_workbench( _source_status("runtime_surface", runtime), _source_status("backup_dr", backup), ] - generated_candidates = [source["generated_at"] for source in source_statuses if source["generated_at"]] + generated_candidates = [ + source["generated_at"] for source in source_statuses if source["generated_at"] + ] loaded_source_count = sum(1 for source in source_statuses if source["loaded"]) high_risk_blocker_count = sum(_int(lane["blocker_count"]) for lane in lanes) average_completion = _percent( @@ -166,17 +185,28 @@ def build_delivery_closure_workbench( return { "schema_version": _SCHEMA_VERSION, "generated_at": max(generated_candidates) if generated_candidates else "", - "status": "blocked_delivery_actions_required" if high_risk_blocker_count else "ready", + "status": "blocked_delivery_actions_required" + if high_risk_blocker_count + else "ready", "summary": { "source_count": len(source_statuses), "loaded_source_count": loaded_source_count, "average_completion_percent": average_completion, "high_risk_blocker_count": high_risk_blocker_count, "runtime_execution_authorized": False, - "remote_write_authorized": False, - "repo_creation_authorized": False, - "refs_sync_authorized": False, - "workflow_trigger_authorized": False, + "remote_write_authorized": github_boundaries.get("github_api_write_allowed") + is True, + "repo_creation_authorized": github_summary.get("repo_creation_authorized") + is True, + "visibility_change_authorized": github_summary.get( + "visibility_change_authorized" + ) + is True, + "refs_sync_authorized": github_summary.get("refs_sync_authorized") is True, + "workflow_trigger_authorized": github_summary.get( + "workflow_trigger_authorized" + ) + is True, "secret_values_collected": False, }, "source_statuses": source_statuses, @@ -185,11 +215,19 @@ def build_delivery_closure_workbench( "operation_boundaries": { "read_only_api_allowed": True, "runtime_write_allowed": False, - "remote_write_allowed": False, - "repo_creation_allowed": False, - "visibility_change_allowed": False, - "refs_sync_allowed": False, - "workflow_trigger_allowed": False, + "remote_write_allowed": github_boundaries.get("github_api_write_allowed") + is True, + "repo_creation_allowed": github_boundaries.get("repo_creation_allowed") + is True, + "visibility_change_allowed": github_boundaries.get( + "visibility_change_allowed" + ) + is True, + "refs_sync_allowed": github_boundaries.get("refs_sync_allowed") is True, + "workflow_trigger_allowed": github_boundaries.get( + "workflow_trigger_allowed" + ) + is True, "secret_value_collection_allowed": False, "backup_restore_execution_allowed": False, "active_scan_allowed": False, @@ -204,7 +242,9 @@ def _source_status(source_id: str, payload: dict[str, Any]) -> dict[str, Any]: "loaded": not source_missing, "schema_version": str(payload.get("schema_version") or ""), "generated_at": str(payload.get("generated_at") or ""), - "missing_reason": str(payload.get("missing_reason") or "") if source_missing else "", + "missing_reason": str(payload.get("missing_reason") or "") + if source_missing + else "", } @@ -218,7 +258,9 @@ def _load_github_private_backup_evidence_gate() -> dict[str, Any]: except ModuleNotFoundError as exc: if exc.name != "src.services.github_target_private_backup_evidence_gate": raise - return _missing_github_private_backup_source("service_module_missing_on_release_base") + return _missing_github_private_backup_source( + "service_module_missing_on_release_base" + ) except FileNotFoundError: return _missing_github_private_backup_source("snapshot_missing_on_release_base") @@ -255,7 +297,7 @@ def _dict(value: Any) -> dict[str, Any]: def _int(value: Any) -> int: if isinstance(value, bool): return int(value) - if isinstance(value, (int, float)): + if isinstance(value, int | float): return int(value) return 0 @@ -307,7 +349,10 @@ def _first_backup_action(value: Any) -> str: if not isinstance(value, list): return "" for row in value: - if isinstance(row, dict) and row.get("overall_readiness") in {"blocked", "action_required"}: + if isinstance(row, dict) and row.get("overall_readiness") in { + "blocked", + "action_required", + }: return str(row.get("next_action") or "") return _first_row_action(value) 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 7d488f54..7fd07a45 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 @@ -23,6 +23,12 @@ _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" _MISSING_SOURCE_READINESS_FILE = "github-target-missing-source-readiness.snapshot.json" +_EXECUTION_AUTHORIZATION_FILE = ( + "github-target-owner-execution-authorization.snapshot.json" +) +_EXECUTION_AUTHORIZATION_SCHEMA_VERSION = ( + "github_target_owner_execution_authorization_v1" +) _PREFLIGHT_SCHEMA_VERSION = "github_target_owner_response_intake_preflight_v1" _PREFLIGHT_MODE = "validate_owner_response_only_no_persist_no_github_write" _SAFE_CREDENTIAL_REVIEW_SCHEMA_VERSION = ( @@ -134,6 +140,9 @@ def load_latest_github_target_private_backup_evidence_gate( missing_source_readiness = _load_optional_snapshot( directory / _MISSING_SOURCE_READINESS_FILE ) + execution_authorization = _load_optional_snapshot( + directory / _EXECUTION_AUTHORIZATION_FILE + ) _require_source_contracts( decision=decision, @@ -142,6 +151,7 @@ def load_latest_github_target_private_backup_evidence_gate( probe=probe, connector_readback=connector_readback, missing_source_readiness=missing_source_readiness, + execution_authorization=execution_authorization, ) return build_github_target_private_backup_evidence_gate( decision=decision, @@ -150,6 +160,7 @@ def load_latest_github_target_private_backup_evidence_gate( probe=probe, connector_readback=connector_readback, missing_source_readiness=missing_source_readiness, + execution_authorization=execution_authorization, ) @@ -457,12 +468,22 @@ def build_github_target_private_backup_evidence_gate( probe: dict[str, Any], connector_readback: dict[str, Any] | None = None, missing_source_readiness: dict[str, Any] | None = None, + execution_authorization: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build the read-only gate response from source-control snapshots.""" connector_payload = _dict(connector_readback) owner_readback = _dict(connector_payload.get("owner_readback")) missing_payload = _dict(missing_source_readiness) missing_summary = _dict(missing_payload.get("summary")) + authorization_payload = _dict(execution_authorization) + authorization_summary = _execution_authorization_summary(authorization_payload) + execution_authorized = ( + authorization_summary["authorization_present"] + and authorization_summary["repo_creation_authorized"] + and authorization_summary["visibility_change_authorized"] + and authorization_summary["refs_sync_authorized"] + and authorization_summary["workflow_trigger_authorized"] + ) decisions = [_dict(row) for row in _list(decision.get("decisions"))] probe_by_repo = { str(row.get("github_repo")): _dict(row) @@ -484,6 +505,11 @@ def build_github_target_private_backup_evidence_gate( for row in _list(missing_payload.get("targets")) if row.get("github_repo") } + authorization_by_repo = { + str(row.get("github_repo")): _dict(row) + for row in _list(authorization_payload.get("authorized_targets")) + if row.get("github_repo") + } owner_summary = _dict(owner_response.get("summary")) owner_response_intake_readiness = _owner_response_intake_readiness(owner_response) safe_credential_evidence_intake_readiness = ( @@ -512,6 +538,10 @@ def build_github_target_private_backup_evidence_gate( owner_response_template=owner_template_by_repo.get( str(row.get("github_repo")), {} ), + execution_authorization=authorization_by_repo.get( + str(row.get("github_repo")), {} + ), + global_execution_authorized=execution_authorized, ) for row in decisions ] @@ -534,9 +564,7 @@ def build_github_target_private_backup_evidence_gate( if row["visibility_evidence_status"] == "external_scope_not_backup_target" ] blocked_targets = [ - row - for row in approval_required_targets - if not row["private_backup_verified"] or not row["execution_ready"] + row for row in approval_required_targets if not row["execution_ready"] ] private_backup_verified_count = sum( @@ -548,11 +576,19 @@ def build_github_target_private_backup_evidence_gate( return { "schema_version": _SCHEMA_VERSION, - "generated_at": _generated_at(owner_response), - "status": "blocked_public_visibility_and_safe_credential_evidence_required" - if public_probe_visible_targets - else "blocked_private_visibility_and_safe_credential_evidence_required", - "mode": "read_only_private_backup_evidence_gate", + "generated_at": str( + authorization_payload.get("generated_at") or _generated_at(owner_response) + ), + "status": "owner_authorized_controlled_execution_preflight_ready" + if execution_authorized + else ( + "blocked_public_visibility_and_safe_credential_evidence_required" + if public_probe_visible_targets + else "blocked_private_visibility_and_safe_credential_evidence_required" + ), + "mode": "owner_authorized_controlled_execution_no_secret_plaintext" + if execution_authorized + else "read_only_private_backup_evidence_gate", "source_reviews": { "target_decision": f"docs/security/{_DECISION_FILE}", "owner_decision_response": f"docs/security/{_OWNER_RESPONSE_FILE}", @@ -560,6 +596,7 @@ def build_github_target_private_backup_evidence_gate( "github_target_probe": f"docs/security/{_PROBE_FILE}", "github_connector_readback": f"docs/security/{_CONNECTOR_READBACK_FILE}", "github_missing_source_readiness": f"docs/security/{_MISSING_SOURCE_READINESS_FILE}", + "github_owner_execution_authorization": f"docs/security/{_EXECUTION_AUTHORIZATION_FILE}", }, "summary": { "target_decision_count": len(targets), @@ -615,6 +652,8 @@ def build_github_target_private_backup_evidence_gate( - private_backup_verified_count, "safe_credential_required_count": len(approval_required_targets), "safe_credential_accepted_evidence_count": 0, + "safe_credential_execution_override_authorized": execution_authorized, + "safe_credential_evidence_requirement_overridden_for_execution": execution_authorized, "safe_credential_evidence_intake_ready": safe_credential_evidence_intake_readiness[ "intake_ready" ], @@ -674,21 +713,57 @@ def build_github_target_private_backup_evidence_gate( ], "owner_response_request_execution_authorized": False, "github_target_owner_response_handoff_not_approval": True, + "owner_execution_authorization_received_count": authorization_summary[ + "authorization_received_count" + ], + "owner_execution_authorized_target_count": authorization_summary[ + "authorized_target_count" + ], + "owner_execution_authorization_status": authorization_summary["status"], + "owner_execution_authorization_source": authorization_summary[ + "authorization_source" + ], + "owner_execution_controlled_preflight_required_count": sum( + 1 + for row in approval_required_targets + if row["controlled_preflight_required"] is True + ), + "post_execution_readback_required_count": sum( + 1 + for row in approval_required_targets + if row["post_execution_readback_required"] is True + ), + "github_missing_target_create_private_repo_authorized_count": authorization_summary[ + "github_missing_target_create_private_repo_authorized_count" + ], + "github_missing_target_refs_sync_authorized_count": authorization_summary[ + "github_missing_target_refs_sync_authorized_count" + ], "execution_ready_count": sum( 1 for row in approval_required_targets if row["execution_ready"] ), "blocked_target_count": len(blocked_targets), "external_scope_target_count": len(external_scope_targets), "forbidden_action_count": len(forbidden_actions), - "repo_creation_authorized": False, - "visibility_change_authorized": False, - "refs_sync_authorized": False, - "github_primary_switch_authorized": False, + "repo_creation_authorized": authorization_summary[ + "repo_creation_authorized" + ], + "visibility_change_authorized": authorization_summary[ + "visibility_change_authorized" + ], + "refs_sync_authorized": authorization_summary["refs_sync_authorized"], + "github_primary_switch_authorized": authorization_summary[ + "github_primary_switch_authorized" + ], "workflow_modification_authorized": False, - "workflow_trigger_authorized": False, + "workflow_trigger_authorized": authorization_summary[ + "workflow_trigger_authorized" + ], "secret_value_collection_allowed": False, "private_clone_url_collection_allowed": False, - "not_found_or_private_as_absent_allowed": False, + "not_found_or_private_as_absent_allowed": authorization_summary[ + "not_found_or_private_as_absent_allowed" + ], "public_repo_allowed": False, }, "owner_response_intake_readiness": owner_response_intake_readiness, @@ -698,24 +773,41 @@ def build_github_target_private_backup_evidence_gate( "rejection_rules": _rejection_rules(owner_response), "operation_boundaries": { "read_only_api_allowed": True, - "github_api_write_allowed": False, + "github_api_write_allowed": authorization_summary[ + "github_api_write_allowed" + ], "gitea_api_write_allowed": False, - "repo_creation_allowed": False, - "visibility_change_allowed": False, - "refs_sync_allowed": False, + "repo_creation_allowed": authorization_summary["repo_creation_authorized"], + "visibility_change_allowed": authorization_summary[ + "visibility_change_authorized" + ], + "refs_sync_allowed": authorization_summary["refs_sync_authorized"], "workflow_modification_allowed": False, - "workflow_trigger_allowed": False, + "workflow_trigger_allowed": authorization_summary[ + "workflow_trigger_authorized" + ], "github_primary_switch_allowed": False, "secret_value_collection_allowed": False, "private_clone_url_collection_allowed": False, }, "authorization_flags": { - "repo_creation_authorized": False, - "visibility_change_authorized": False, - "refs_sync_authorized": False, - "github_primary_switch_authorized": False, + "owner_execution_authorization_present": authorization_summary[ + "authorization_present" + ], + "repo_creation_authorized": authorization_summary[ + "repo_creation_authorized" + ], + "visibility_change_authorized": authorization_summary[ + "visibility_change_authorized" + ], + "refs_sync_authorized": authorization_summary["refs_sync_authorized"], + "github_primary_switch_authorized": authorization_summary[ + "github_primary_switch_authorized" + ], "workflow_modification_authorized": False, - "workflow_trigger_authorized": False, + "workflow_trigger_authorized": authorization_summary[ + "workflow_trigger_authorized" + ], "secret_value_collection_allowed": False, "private_clone_url_collection_allowed": False, }, @@ -911,6 +1003,8 @@ def _build_target( connector_readback: dict[str, Any], missing_source_readiness: dict[str, Any], owner_response_template: dict[str, Any], + execution_authorization: dict[str, Any], + global_execution_authorized: bool, ) -> dict[str, Any]: github_repo = str(decision.get("github_repo") or "") probe_status = str(probe.get("status") or decision.get("probe_status") or "unknown") @@ -928,9 +1022,38 @@ def _build_target( probe_status=probe_status, private_visibility_verified=private_visibility_verified, ) + owner_execution_authorized = ( + approval_required + and global_execution_authorized + and execution_authorization.get("target_execution_authorized") is True + ) + repo_creation_authorized = ( + owner_execution_authorized + and execution_authorization.get("create_private_repo_authorized") is True + ) + visibility_change_authorized = ( + owner_execution_authorized + and execution_authorization.get("visibility_change_authorized") is True + ) + refs_sync_authorized = ( + owner_execution_authorized + and execution_authorization.get("refs_sync_authorized") is True + ) + workflow_trigger_authorized = ( + owner_execution_authorized + and execution_authorization.get("workflow_trigger_authorized") is True + ) + execution_ready = ( + owner_execution_authorized + and visibility_change_authorized + and refs_sync_authorized + and workflow_trigger_authorized + ) blockers = _target_blockers( visibility_status, approval_required, private_visibility_verified ) + if execution_ready: + blockers = [] forbidden_actions = _strings(approval_item.get("still_forbidden")) or [ "create_github_repo", "change_repo_visibility", @@ -978,10 +1101,15 @@ def _build_target( "safe_credential_evidence_status": "not_collected", "safe_credential_evidence_ref": None, "safe_credential_evidence_intake_ready": approval_required, - "safe_credential_evidence_submission_status": "waiting_redacted_evidence_ref" - if approval_required - else "not_required", + "safe_credential_evidence_submission_status": ( + "owner_execution_authorized_post_apply_readback_pending" + if owner_execution_authorized + else ( + "waiting_redacted_evidence_ref" if approval_required else "not_required" + ) + ), "safe_credential_required_redacted_evidence_ref": approval_required, + "safe_credential_execution_override_authorized": owner_execution_authorized, "safe_credential_allowed_evidence_ref_types": [ "repo_path", "snapshot_path", @@ -1018,10 +1146,29 @@ def _build_target( owner_response_template.get("allowed_outputs") ), "owner_response_execution_authorized": False, + "owner_execution_authorized": owner_execution_authorized, + "owner_execution_authorization_source": execution_authorization.get( + "source_disposition" + ), + "controlled_preflight_required": bool( + owner_execution_authorized + and execution_authorization.get("controlled_preflight_required") is True + ), + "post_execution_readback_required": bool( + owner_execution_authorized + and execution_authorization.get("post_execution_readback_required") is True + ), "owner_response_accepted": False, - "refs_sync_ready": False, - "execution_ready": False, + "refs_sync_ready": refs_sync_authorized, + "execution_ready": execution_ready, + "controlled_execution_ready": execution_ready, "blockers": blockers, + "execution_pending_checks": [ + "controlled_preflight", + "post_execution_private_visibility_and_refs_readback", + ] + if execution_ready + else [], "evidence_refs": _strings(decision.get("evidence_refs")), "next_action": str( approval_item.get("approval_action") @@ -1029,9 +1176,10 @@ def _build_target( or "" ), "forbidden_actions": forbidden_actions, - "repo_creation_authorized": False, - "visibility_change_authorized": False, - "refs_sync_authorized": False, + "repo_creation_authorized": repo_creation_authorized, + "visibility_change_authorized": visibility_change_authorized, + "refs_sync_authorized": refs_sync_authorized, + "workflow_trigger_authorized": workflow_trigger_authorized, "github_primary_switch_authorized": False, "secret_values_collected": False, } @@ -1097,6 +1245,7 @@ def _require_source_contracts( probe: dict[str, Any], connector_readback: dict[str, Any], missing_source_readiness: dict[str, Any], + execution_authorization: dict[str, Any], ) -> None: _require_schema(decision, "github_target_decision_v1", _DECISION_FILE) _require_schema( @@ -1120,6 +1269,12 @@ def _require_source_contracts( "github_target_missing_source_readiness_v1", _MISSING_SOURCE_READINESS_FILE, ) + if execution_authorization: + _require_schema( + execution_authorization, + _EXECUTION_AUTHORIZATION_SCHEMA_VERSION, + _EXECUTION_AUTHORIZATION_FILE, + ) _require_decision_consistency(decision, _DECISION_FILE) _require_probe_consistency(probe, _PROBE_FILE) _require_approval_package_consistency(approval_package, _APPROVAL_PACKAGE_FILE) @@ -1131,6 +1286,13 @@ def _require_source_contracts( _require_missing_source_readiness_consistency( missing_source_readiness, _MISSING_SOURCE_READINESS_FILE ) + if execution_authorization: + _require_execution_authorization_consistency( + authorization=execution_authorization, + decision=decision, + missing_source_readiness=missing_source_readiness, + label=_EXECUTION_AUTHORIZATION_FILE, + ) _require_owner_response_boundaries(owner_response, _OWNER_RESPONSE_FILE) @@ -1276,6 +1438,160 @@ def _require_missing_source_readiness_consistency( ) +def _require_execution_authorization_consistency( + *, + authorization: dict[str, Any], + decision: dict[str, Any], + missing_source_readiness: dict[str, Any], + label: str, +) -> None: + summary = _dict(authorization.get("summary")) + boundaries = _dict(authorization.get("operation_boundaries")) + authorized_targets = [ + _dict(row) for row in _list(authorization.get("authorized_targets")) + ] + approval_required_repos = { + str(row.get("github_repo")) + for row in _list(decision.get("decisions")) + if _dict(row).get("approval_required") is True + } + authorized_repos = { + str(row.get("github_repo")) + for row in authorized_targets + if row.get("github_repo") + } + if authorized_repos != approval_required_repos: + raise ValueError( + f"{label}: authorized targets must match approval-required targets" + ) + if _int(summary.get("authorized_target_count")) != len(authorized_targets): + raise ValueError(f"{label}: authorized target count must match targets") + if _int(summary.get("authorization_received_count")) < 1: + raise ValueError(f"{label}: authorization_received_count must be >= 1") + + required_true_flags = { + "repo_creation_authorized", + "visibility_change_authorized", + "refs_sync_authorized", + "workflow_trigger_authorized", + } + missing_true_flags = sorted( + flag for flag in required_true_flags if summary.get(flag) is not True + ) + if missing_true_flags: + raise ValueError( + f"{label}: controlled execution flags must be true: {missing_true_flags}" + ) + + required_boundary_true_flags = { + "github_api_write_allowed", + "repo_creation_allowed", + "visibility_change_allowed", + "refs_sync_allowed", + "workflow_trigger_allowed", + } + missing_boundary_true_flags = sorted( + flag + for flag in required_boundary_true_flags + if boundaries.get(flag) is not True + ) + if missing_boundary_true_flags: + raise ValueError( + f"{label}: controlled operation boundaries must be true: {missing_boundary_true_flags}" + ) + + forbidden_true_flags = { + "github_primary_switch_authorized", + "workflow_modification_authorized", + "delete_refs_authorized", + "force_push_authorized", + "public_repo_allowed", + "public_visibility_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + "credential_value_collection_allowed", + "raw_payload_storage_allowed", + "write_performed", + "repo_creation_performed", + "visibility_change_performed", + "refs_sync_performed", + "workflow_trigger_performed", + } + enabled = sorted( + flag for flag in forbidden_true_flags if summary.get(flag) is not False + ) + forbidden_boundary_true_flags = { + "workflow_modification_allowed", + "github_primary_switch_allowed", + "delete_refs_allowed", + "force_push_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + "credential_value_collection_allowed", + "raw_payload_storage_allowed", + } + enabled.extend( + sorted( + flag + for flag in forbidden_boundary_true_flags + if boundaries.get(flag) is not False + ) + ) + if enabled: + raise ValueError(f"{label}: forbidden authorization flags enabled: {enabled}") + + required_still_forbidden = { + "secret_value", + "token_value", + "private_clone_url_credential", + "repo_archive", + "git_object_pack", + "force_push", + "delete_refs", + "github_primary_switch", + } + missing_forbidden = sorted( + required_still_forbidden - set(_strings(authorization.get("still_forbidden"))) + ) + if missing_forbidden: + raise ValueError( + f"{label}: still_forbidden missing critical boundaries: {missing_forbidden}" + ) + + missing_targets = { + str(row.get("github_repo")) + for row in _list(missing_source_readiness.get("targets")) + if _dict(row).get("github_repo") + } + create_authorized_repos = { + str(row.get("github_repo")) + for row in authorized_targets + if row.get("create_private_repo_authorized") is True + } + refs_authorized_missing_repos = { + str(row.get("github_repo")) + for row in authorized_targets + if row.get("github_repo") in missing_targets + and row.get("refs_sync_authorized") is True + } + if create_authorized_repos != missing_targets: + raise ValueError( + f"{label}: create authorization must match missing-source targets" + ) + if refs_authorized_missing_repos != missing_targets: + raise ValueError( + f"{label}: refs sync authorization must cover missing-source targets" + ) + if _int( + summary.get("github_missing_target_create_private_repo_authorized_count") + ) != len(missing_targets): + raise ValueError(f"{label}: missing target create authorization count drift") + if _int(summary.get("github_missing_target_refs_sync_authorized_count")) != len( + missing_targets + ): + raise ValueError(f"{label}: missing target refs authorization count drift") + + 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") @@ -1468,6 +1784,48 @@ def _owner_response_intake_readiness(owner_response: dict[str, Any]) -> dict[str } +def _execution_authorization_summary( + execution_authorization: dict[str, Any], +) -> dict[str, Any]: + summary = _dict(execution_authorization.get("summary")) + boundaries = _dict(execution_authorization.get("operation_boundaries")) + authorization_present = bool(execution_authorization) + repo_creation_authorized = summary.get("repo_creation_authorized") is True + visibility_change_authorized = summary.get("visibility_change_authorized") is True + refs_sync_authorized = summary.get("refs_sync_authorized") is True + workflow_trigger_authorized = summary.get("workflow_trigger_authorized") is True + return { + "authorization_present": authorization_present, + "status": str( + execution_authorization.get("status") + or "owner_execution_authorization_not_received" + ), + "mode": str(execution_authorization.get("mode") or ""), + "authorization_source": str( + execution_authorization.get("authorization_source") or "" + ), + "authorization_received_count": _int( + summary.get("authorization_received_count") + ), + "authorized_target_count": _int(summary.get("authorized_target_count")), + "github_missing_target_create_private_repo_authorized_count": _int( + summary.get("github_missing_target_create_private_repo_authorized_count") + ), + "github_missing_target_refs_sync_authorized_count": _int( + summary.get("github_missing_target_refs_sync_authorized_count") + ), + "github_api_write_allowed": boundaries.get("github_api_write_allowed") is True, + "repo_creation_authorized": repo_creation_authorized, + "visibility_change_authorized": visibility_change_authorized, + "refs_sync_authorized": refs_sync_authorized, + "workflow_trigger_authorized": workflow_trigger_authorized, + "github_primary_switch_authorized": False, + "not_found_or_private_as_absent_allowed": repo_creation_authorized + and visibility_change_authorized + and refs_sync_authorized, + } + + def _safe_credential_evidence_intake_readiness( *, owner_response: dict[str, Any], diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index 8819dda1..cfafb0a7 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -19,10 +19,11 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert data["summary"]["source_count"] == 5 assert data["summary"]["loaded_source_count"] == 5 assert data["summary"]["runtime_execution_authorized"] is False - assert data["summary"]["remote_write_authorized"] is False - assert data["summary"]["repo_creation_authorized"] is False - assert data["summary"]["refs_sync_authorized"] is False - assert data["summary"]["workflow_trigger_authorized"] is False + assert data["summary"]["remote_write_authorized"] is True + assert data["summary"]["repo_creation_authorized"] is True + assert data["summary"]["visibility_change_authorized"] is True + assert data["summary"]["refs_sync_authorized"] is True + assert data["summary"]["workflow_trigger_authorized"] is True assert data["summary"]["secret_values_collected"] is False assert data["summary"]["average_completion_percent"] >= 0 assert data["summary"]["high_risk_blocker_count"] > 0 @@ -36,10 +37,16 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert lanes["runtime"]["metric"]["kind"] == "surface_count" assert lanes["backup"]["metric"]["kind"] == "readiness_row_count" assert sources["github_private_backup"]["loaded"] is True - assert sources["github_private_backup"]["schema_version"] == "github_target_private_backup_evidence_gate_v1" + 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_private_visibility_and_safe_credential_evidence_required" + assert lanes["github"]["blocker_count"] == 0 + assert ( + lanes["github"]["status"] + == "owner_authorized_controlled_execution_preflight_ready" + ) assert lanes["github"]["metric"]["verified"] == 4 assert lanes["github"]["metric"]["total"] == 9 assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values()) @@ -48,11 +55,11 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): boundaries = data["operation_boundaries"] assert boundaries["read_only_api_allowed"] is True assert boundaries["runtime_write_allowed"] is False - assert boundaries["remote_write_allowed"] is False - assert boundaries["repo_creation_allowed"] is False - assert boundaries["visibility_change_allowed"] is False - assert boundaries["refs_sync_allowed"] is False - assert boundaries["workflow_trigger_allowed"] is False + assert boundaries["remote_write_allowed"] is True + assert boundaries["repo_creation_allowed"] is True + assert boundaries["visibility_change_allowed"] is True + assert boundaries["refs_sync_allowed"] is True + assert boundaries["workflow_trigger_allowed"] is True assert boundaries["secret_value_collection_allowed"] is False assert boundaries["backup_restore_execution_allowed"] is False assert boundaries["active_scan_allowed"] is False 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 a9e1981d..b0feaead 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 @@ -18,11 +18,10 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot snapshot = load_latest_github_target_private_backup_evidence_gate() assert snapshot["schema_version"] == "github_target_private_backup_evidence_gate_v1" - assert snapshot["mode"] == "read_only_private_backup_evidence_gate" assert ( - snapshot["status"] - == "blocked_private_visibility_and_safe_credential_evidence_required" + snapshot["mode"] == "owner_authorized_controlled_execution_no_secret_plaintext" ) + assert snapshot["status"] == "owner_authorized_controlled_execution_preflight_ready" assert snapshot["summary"]["target_decision_count"] == 10 assert snapshot["summary"]["approval_required_target_count"] == 9 assert snapshot["summary"]["github_connector_owner_visible_repository_count"] == 4 @@ -55,6 +54,13 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot == 0 ) assert snapshot["summary"]["github_missing_target_refs_sync_ready_count"] == 0 + assert ( + snapshot["summary"][ + "github_missing_target_create_private_repo_authorized_count" + ] + == 5 + ) + assert snapshot["summary"]["github_missing_target_refs_sync_authorized_count"] == 5 assert snapshot["summary"]["private_backup_verified_count"] == 4 assert snapshot["summary"]["private_visibility_verified_count"] == 4 assert snapshot["summary"]["safe_credential_required_count"] == 9 @@ -90,17 +96,48 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot assert ( snapshot["summary"]["github_target_owner_response_handoff_not_approval"] is True ) - assert snapshot["summary"]["blocked_target_count"] == 9 + assert snapshot["summary"]["owner_execution_authorization_received_count"] == 1 + assert snapshot["summary"]["owner_execution_authorized_target_count"] == 9 + assert ( + snapshot["summary"]["owner_execution_authorization_status"] + == "owner_authorized_controlled_execution" + ) + assert ( + snapshot["summary"]["owner_execution_authorization_source"] + == "chat_authorization_2026-06-28_full_hard_gate_open" + ) + assert ( + snapshot["summary"]["owner_execution_controlled_preflight_required_count"] == 9 + ) + assert snapshot["summary"]["post_execution_readback_required_count"] == 9 + assert snapshot["summary"]["execution_ready_count"] == 9 + assert snapshot["summary"]["blocked_target_count"] == 0 assert snapshot["summary"]["public_repo_allowed"] is False - assert snapshot["summary"]["repo_creation_authorized"] is False - assert snapshot["summary"]["visibility_change_authorized"] is False - assert snapshot["summary"]["refs_sync_authorized"] is False + assert snapshot["summary"]["repo_creation_authorized"] is True + assert snapshot["summary"]["visibility_change_authorized"] is True + assert snapshot["summary"]["refs_sync_authorized"] is True + assert snapshot["summary"]["workflow_trigger_authorized"] is True + assert snapshot["summary"]["github_primary_switch_authorized"] is False assert snapshot["summary"]["secret_value_collection_allowed"] is False assert snapshot["operation_boundaries"]["read_only_api_allowed"] is True - assert snapshot["operation_boundaries"]["repo_creation_allowed"] is False - assert snapshot["operation_boundaries"]["visibility_change_allowed"] is False - assert snapshot["operation_boundaries"]["refs_sync_allowed"] is False + assert snapshot["operation_boundaries"]["github_api_write_allowed"] is True + assert snapshot["operation_boundaries"]["repo_creation_allowed"] is True + assert snapshot["operation_boundaries"]["visibility_change_allowed"] is True + assert snapshot["operation_boundaries"]["refs_sync_allowed"] is True + assert snapshot["operation_boundaries"]["workflow_trigger_allowed"] is True assert snapshot["operation_boundaries"]["secret_value_collection_allowed"] is False + assert ( + snapshot["operation_boundaries"]["private_clone_url_collection_allowed"] + is False + ) + assert snapshot["authorization_flags"]["repo_creation_authorized"] is True + assert snapshot["authorization_flags"]["visibility_change_authorized"] is True + assert snapshot["authorization_flags"]["refs_sync_authorized"] is True + assert snapshot["authorization_flags"]["workflow_trigger_authorized"] is True + assert snapshot["authorization_flags"]["github_primary_switch_authorized"] is False + assert ( + snapshot["authorization_flags"]["private_clone_url_collection_allowed"] is False + ) intake = snapshot["owner_response_intake_readiness"] assert ( intake["status"] @@ -189,7 +226,7 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot ) assert ( targets["owenhytsai/awoooi"]["safe_credential_evidence_submission_status"] - == "waiting_redacted_evidence_ref" + == "owner_execution_authorized_post_apply_readback_pending" ) assert ( "redacted_metadata_pointer" @@ -210,6 +247,10 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot is False ) assert targets["owenhytsai/awoooi"]["owner_response_execution_authorized"] is False + assert targets["owenhytsai/awoooi"]["owner_execution_authorized"] is True + assert targets["owenhytsai/awoooi"]["controlled_execution_ready"] is True + assert targets["owenhytsai/awoooi"]["refs_sync_ready"] is True + assert targets["owenhytsai/awoooi"]["blockers"] == [] assert ( "canonical_source" in targets["owenhytsai/awoooi"]["owner_response_required_fields"] @@ -226,6 +267,9 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot is False ) assert targets["owenhytsai/ewoooc"]["missing_target_refs_sync_ready"] is False + assert targets["owenhytsai/ewoooc"]["repo_creation_authorized"] is True + assert targets["owenhytsai/ewoooc"]["refs_sync_authorized"] is True + assert targets["owenhytsai/ewoooc"]["execution_ready"] is True assert targets["owenhytsai/ewoooc"]["private_backup_verified"] is False assert ( targets["owenhytsai/ewoooc"]["owner_response_template_id"] @@ -352,6 +396,21 @@ def test_github_target_private_backup_gate_rejects_missing_source_write_flags(tm load_latest_github_target_private_backup_evidence_gate(tmp_path) +def test_github_target_private_backup_gate_rejects_execution_authorization_secrets( + tmp_path, +): + _copy_security_snapshots(tmp_path) + authorization_path = ( + tmp_path / "github-target-owner-execution-authorization.snapshot.json" + ) + authorization = json.loads(authorization_path.read_text(encoding="utf-8")) + authorization["summary"]["secret_value_collection_allowed"] = True + authorization_path.write_text(json.dumps(authorization), encoding="utf-8") + + with pytest.raises(ValueError, match="forbidden authorization flags"): + load_latest_github_target_private_backup_evidence_gate(tmp_path) + + def test_github_target_owner_response_preflight_accepts_redacted_evidence_refs(): preflight = preflight_github_target_owner_response_submission( _valid_owner_response_submission() @@ -439,8 +498,8 @@ def test_github_target_safe_credential_evidence_review_accepts_redacted_refs(): assert payload["summary"]["safe_credential_evidence_submission_accepted_count"] == 1 assert payload["summary"]["safe_credential_accepted_evidence_count"] == 0 assert payload["summary"]["private_backup_verified_count"] == 4 - assert payload["summary"]["execution_ready_count"] == 0 - assert payload["summary"]["blocked_target_count"] == 9 + assert payload["summary"]["execution_ready_count"] == 9 + assert payload["summary"]["blocked_target_count"] == 0 assert payload["boundaries"]["payload_persisted"] is False assert payload["boundaries"]["safe_credential_accepted_updated"] is False assert payload["boundaries"]["runtime_execution_authorized"] is False @@ -510,6 +569,7 @@ def _copy_security_snapshots(tmp_path: Path) -> None: "github-target-probe.snapshot.json", "github-target-connector-readback.snapshot.json", "github-target-missing-source-readiness.snapshot.json", + "github-target-owner-execution-authorization.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 198d99ad..02625ef0 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 @@ -16,7 +16,8 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert response.status_code == 200 data = response.json() assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1" - assert data["mode"] == "read_only_private_backup_evidence_gate" + assert data["mode"] == "owner_authorized_controlled_execution_no_secret_plaintext" + assert data["status"] == "owner_authorized_controlled_execution_preflight_ready" assert data["summary"]["approval_required_target_count"] == 9 assert data["summary"]["github_connector_readback_count"] == 9 assert data["summary"]["github_connector_private_visibility_count"] == 4 @@ -25,6 +26,11 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert data["summary"]["github_missing_target_gitea_source_candidate_count"] == 3 assert data["summary"]["github_missing_target_create_private_repo_ready_count"] == 0 assert data["summary"]["github_missing_target_refs_sync_ready_count"] == 0 + assert ( + data["summary"]["github_missing_target_create_private_repo_authorized_count"] + == 5 + ) + assert data["summary"]["github_missing_target_refs_sync_authorized_count"] == 5 assert data["summary"]["private_backup_verified_count"] == 4 assert data["summary"]["private_visibility_verified_count"] == 4 assert data["summary"]["safe_credential_evidence_intake_ready"] is True @@ -46,18 +52,22 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert data["summary"]["owner_response_collection_check_count"] == 6 assert data["summary"]["owner_response_intake_preflight_check_count"] == 6 assert data["summary"]["owner_response_request_execution_authorized"] is False - assert data["summary"]["blocked_target_count"] == 9 + assert data["summary"]["owner_execution_authorization_received_count"] == 1 + assert data["summary"]["owner_execution_authorized_target_count"] == 9 + assert data["summary"]["execution_ready_count"] == 9 + assert data["summary"]["blocked_target_count"] == 0 assert data["summary"]["public_repo_allowed"] is False - assert data["summary"]["repo_creation_authorized"] is False - assert data["summary"]["visibility_change_authorized"] is False - assert data["summary"]["refs_sync_authorized"] is False + assert data["summary"]["repo_creation_authorized"] is True + assert data["summary"]["visibility_change_authorized"] is True + assert data["summary"]["refs_sync_authorized"] is True + assert data["summary"]["workflow_trigger_authorized"] is True assert data["summary"]["secret_value_collection_allowed"] is False assert data["operation_boundaries"]["read_only_api_allowed"] is True - assert data["operation_boundaries"]["github_api_write_allowed"] is False - assert data["operation_boundaries"]["repo_creation_allowed"] is False - assert data["operation_boundaries"]["visibility_change_allowed"] is False - assert data["operation_boundaries"]["refs_sync_allowed"] is False - assert data["operation_boundaries"]["workflow_trigger_allowed"] is False + assert data["operation_boundaries"]["github_api_write_allowed"] is True + assert data["operation_boundaries"]["repo_creation_allowed"] is True + assert data["operation_boundaries"]["visibility_change_allowed"] is True + assert data["operation_boundaries"]["refs_sync_allowed"] is True + assert data["operation_boundaries"]["workflow_trigger_allowed"] is True assert data["operation_boundaries"]["private_clone_url_collection_allowed"] is False intake = data["owner_response_intake_readiness"] assert ( @@ -92,9 +102,11 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g == "/api/v1/agents/github-target-safe-credential-evidence-reviewer-validation/validate-redacted-refs" ) assert data["targets"][0]["owner_response_execution_authorized"] is False + assert data["targets"][0]["owner_execution_authorized"] is True + assert data["targets"][0]["controlled_execution_ready"] is True assert ( data["targets"][0]["safe_credential_evidence_submission_status"] - == "waiting_redacted_evidence_ref" + == "owner_execution_authorized_post_apply_readback_pending" ) assert data["targets"][0]["safe_credential_raw_payload_storage_allowed"] is False assert ( @@ -211,5 +223,5 @@ def test_github_target_safe_credential_evidence_review_api_does_not_persist(): == 0 ) assert readback["summary"]["github_missing_target_refs_sync_ready_count"] == 0 - assert readback["summary"]["execution_ready_count"] == 0 + assert readback["summary"]["execution_ready_count"] == 9 assert "192.168.0." not in response.text diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 18d833d6..4cb4d3cb 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,40 @@ +## 2026-06-28 — GitHub private backup controlled execution 授權 gate 本地完成 + +**背景**:統帥明確要求「硬閘全部打開、完全授權、全面快速推進」。本段把舊 GitHub private backup `blocked/read-only` gate 改成可審計的 owner controlled execution authorization;這是授權 gate 變更,不是秘密值收件,也不是已完成 GitHub 寫入。 + +**完成內容**: +- 新增 `docs/security/github-target-owner-execution-authorization.snapshot.json`,記錄本次授權來源 `chat_authorization_2026-06-28_full_hard_gate_open`。 +- 9 個 approval-required GitHub targets 全部列為 `target_execution_authorized=true`,`execution_ready_count=9`、`blocked_target_count=0`。 +- 5 個 missing targets 新增受控授權計數:`github_missing_target_create_private_repo_authorized_count=5`、`github_missing_target_refs_sync_authorized_count=5`。 +- `apps/api/src/services/github_target_private_backup_evidence_gate.py` 讀取授權 snapshot 後,將主 gate status 改為 `owner_authorized_controlled_execution_preflight_ready`,mode 改為 `owner_authorized_controlled_execution_no_secret_plaintext`。 +- `apps/api/src/services/delivery_closure_workbench.py` 將 GitHub lane blocker 從 9 降為 0,並把 `remote_write_authorized`、`repo_creation_authorized`、`visibility_change_authorized`、`refs_sync_authorized`、`workflow_trigger_authorized` 投影為 `true`。 +- 新增 fail-closed consistency guard:授權 snapshot 若開啟 secret value、private clone URL、raw payload、force push、delete refs、GitHub primary switch、public visibility,API 會直接 fail。 + +**本地 readback**: +- GitHub gate:`owner_execution_authorization_received_count=1`、`owner_execution_authorized_target_count=9`。 +- GitHub gate:`repo_creation_authorized=true`、`visibility_change_authorized=true`、`refs_sync_authorized=true`、`workflow_trigger_authorized=true`。 +- GitHub gate:`private_backup_verified_count=4`、`safe_credential_accepted_evidence_count=0`,未把尚未完成的 evidence 偽造成已驗收。 +- GitHub gate:原始 source readiness 仍保留 `github_missing_target_create_private_repo_ready_count=0`、`github_missing_target_refs_sync_ready_count=0`;新授權以 authorized count 表示。 +- Delivery Workbench GitHub lane:`status=owner_authorized_controlled_execution_preflight_ready`、`blocker_count=0`、metric 仍為 `private_backup_verified 4/9`。 + +**本地驗證結果**: +- `python3.11 -m ruff format ...`:通過,`5 files left unchanged`。 +- `python3.11 -m ruff check ...`:通過。 +- `python3 -m py_compile apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/src/services/delivery_closure_workbench.py apps/api/src/api/v1/agents.py`:通過。 +- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest 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 -q`:`17 passed`。 +- `python3 -m json.tool docs/security/github-target-owner-execution-authorization.snapshot.json`:通過。 +- `git diff --check`:通過。 + +**仍維持 false / 未做**: +- `secret_value_collection_allowed=false`、`private_clone_url_collection_allowed=false`、`credential_value_collection_allowed=false`、`raw_payload_storage_allowed=false`。 +- `force_push_authorized=false`、`delete_refs_authorized=false`、`github_primary_switch_authorized=false`、`workflow_modification_authorized=false`、`public_repo_allowed=false`、`public_visibility_allowed=false`。 +- 本段未建立 GitHub repo、未改 repo visibility、未同步 refs、未觸發 workflow、未讀或保存 secret / private clone URL;未碰 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime;未 force push。 + +**下一個 P0**: +- commit 並 normal push feature;確認 `gitea/main` 最新後 normal push `HEAD:main`,等待 Gitea CD 成功,再做 production readback。 +- production 目標讀回:`owner_execution_authorization_received_count=1`、`execution_ready_count=9`、`blocked_target_count=0`、repo / visibility / refs / workflow authorization 皆 `true`,同時 secret / private clone / force / delete / primary switch 維持 `false`。 +- 授權 gate 上線後再進入實際 GitHub controlled execution:先 collision preflight,再 create private repo / set private / normal refs sync / workflow verification / production readback。 + ## 2026-06-27 — 22:51 AwoooP controlled automation copy guard 進 main **背景**:上一段已把正式 AwoooP Approvals / Runs / Work Items / Alerts HTML payload 中殘留的舊 manual gate 語意清零;本段不是再做文案文件,而是把防回歸規則寫成 repo guard,避免 `待人工決策`、`阻塞與人工閘門`、`人工接手`、`manual gate`、`owner review` 等語意再次回到低 / 中 / 高風險流程。 diff --git a/docs/security/github-target-owner-execution-authorization.snapshot.json b/docs/security/github-target-owner-execution-authorization.snapshot.json new file mode 100644 index 00000000..652c5bc1 --- /dev/null +++ b/docs/security/github-target-owner-execution-authorization.snapshot.json @@ -0,0 +1,198 @@ +{ + "schema_version": "github_target_owner_execution_authorization_v1", + "generated_at": "2026-06-28T00:00:00+08:00", + "status": "owner_authorized_controlled_execution", + "mode": "controlled_github_private_backup_execution_no_secret_plaintext", + "authorization_source": "chat_authorization_2026-06-28_full_hard_gate_open", + "scope": "github_private_backup_targets_only", + "summary": { + "authorization_received_count": 1, + "authorized_target_count": 9, + "github_missing_target_create_private_repo_authorized_count": 5, + "github_missing_target_refs_sync_authorized_count": 5, + "repo_creation_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "github_primary_switch_authorized": false, + "workflow_modification_authorized": false, + "delete_refs_authorized": false, + "force_push_authorized": false, + "public_repo_allowed": false, + "public_visibility_allowed": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false, + "credential_value_collection_allowed": false, + "raw_payload_storage_allowed": false, + "write_performed": false, + "repo_creation_performed": false, + "visibility_change_performed": false, + "refs_sync_performed": false, + "workflow_trigger_performed": false + }, + "authorized_actions": [ + "create_private_repo_for_missing_targets_after_collision_preflight", + "set_or_verify_private_visibility", + "sync_refs_from_approved_source_candidate", + "trigger_post_sync_verification_workflow" + ], + "controlled_preflight_requirements": [ + "confirm_target_owner_scope_is_owenhytsai", + "verify_no_existing_private_repo_collision_before_create", + "select_best_available_source_candidate_without_copying_secret_values", + "perform_normal_push_or_sync_only_no_force", + "run_post_execution_private_visibility_and_refs_readback" + ], + "still_forbidden": [ + "secret_value", + "token_value", + "private_key", + "cookie_or_session", + "authorization_header", + "private_clone_url_credential", + "repo_archive", + "git_object_pack", + "db_dump", + "force_push", + "delete_refs", + "tag_rewrite", + "repo_delete", + "github_primary_switch", + "public_visibility", + "raw_runtime_secret_volume", + "unrelated_history_merge" + ], + "authorized_targets": [ + { + "github_repo": "owenhytsai/awoooi", + "template_id": "target-awoooi-refs-blocked", + "target_execution_authorized": true, + "create_private_repo_authorized": false, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "existing_private_target_use_gitea_main_refs", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "template_id": "target-clawbot-v5-refs-blocked", + "target_execution_authorized": true, + "create_private_repo_authorized": false, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "existing_private_target_use_gitea_main_refs", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "template_id": "target-wooo-aiops-refs-blocked", + "target_execution_authorized": true, + "create_private_repo_authorized": false, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "existing_private_target_use_gitea_main_refs", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "template_id": "target-wooo-infra-config-internal-remote", + "target_execution_authorized": true, + "create_private_repo_authorized": false, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "existing_private_target_verify_internal_remote_refs", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/ewoooc", + "template_id": "target-ewoooc-private-or-new", + "target_execution_authorized": true, + "create_private_repo_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "owner_authorized_use_best_available_source_candidate_pending_preflight", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "template_id": "target-bitan-pharmacy-private-or-new", + "target_execution_authorized": true, + "create_private_repo_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "owner_authorized_use_best_available_source_candidate_pending_preflight", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "template_id": "target-tsenyang-website-private-or-new", + "target_execution_authorized": true, + "create_private_repo_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "owner_authorized_use_best_available_source_candidate_pending_preflight", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/VibeWork", + "template_id": "target-vibework-private-or-new", + "target_execution_authorized": true, + "create_private_repo_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "owner_authorized_use_best_available_source_candidate_pending_preflight", + "controlled_preflight_required": true, + "post_execution_readback_required": true + }, + { + "github_repo": "owenhytsai/agent-bounty-protocol", + "template_id": "target-agent-bounty-protocol-private-or-new", + "target_execution_authorized": true, + "create_private_repo_authorized": true, + "visibility_change_authorized": true, + "refs_sync_authorized": true, + "workflow_trigger_authorized": true, + "source_disposition": "owner_authorized_use_best_available_source_candidate_pending_preflight", + "controlled_preflight_required": true, + "post_execution_readback_required": true + } + ], + "operation_boundaries": { + "read_only_api_allowed": true, + "github_api_write_allowed": true, + "repo_creation_allowed": true, + "visibility_change_allowed": true, + "refs_sync_allowed": true, + "workflow_trigger_allowed": true, + "workflow_modification_allowed": false, + "github_primary_switch_allowed": false, + "delete_refs_allowed": false, + "force_push_allowed": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false, + "credential_value_collection_allowed": false, + "raw_payload_storage_allowed": false + }, + "evidence_refs": [ + "docs/HARD_RULES.md#ai-agent-controlled-runtime-authorization", + "docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md#15-2026-06-26", + "docs/security/github-target-owner-decision-response.snapshot.json", + "docs/security/github-target-missing-source-readiness.snapshot.json" + ], + "next_gate": "perform_controlled_github_private_backup_execution_and_post_readback" +}