From 96dfb535508eaa1fdb37bbe6e4b9c059e6cdc74d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 14:55:18 +0800 Subject: [PATCH] fix(github): expose operator unblock actions --- .../services/delivery_closure_workbench.py | 17 ++++- ...hub_target_private_backup_evidence_gate.py | 66 ++++++++++++++++++- .../test_delivery_closure_workbench_api.py | 15 +++++ ...hub_target_private_backup_evidence_gate.py | 19 ++++++ ...target_private_backup_evidence_gate_api.py | 25 ++++++- apps/web/src/lib/api-client.ts | 66 ++++++++++++++----- 6 files changed, 184 insertions(+), 24 deletions(-) diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index 3d906fe3..ce05ae5d 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -54,6 +54,7 @@ def build_delivery_closure_workbench( github_summary = _dict(github.get("summary")) github_boundaries = _dict(github.get("operation_boundaries")) github_preflight = _dict(github.get("controlled_execution_preflight")) + github_operator_unblock = _dict(github_preflight.get("operator_unblock")) gitea_status = _dict(gitea.get("program_status")) gitea_rollups = _dict(gitea.get("rollups")) runtime_status = _dict(runtime.get("program_status")) @@ -123,7 +124,14 @@ def build_delivery_closure_workbench( is True, }, "href": "/governance?tab=automation-inventory", + "operator_unblock": github_operator_unblock, "next_action": str( + _first_string(github_operator_unblock.get("required_actions")) + or github_operator_unblock.get("safe_handoff") + or github_preflight.get("operator_unblock_status") + or "" + ) + or str( _first_target_action(github_preflight.get("targets")) or github.get("next_action") or _first_target_action(github.get("targets")) @@ -241,9 +249,7 @@ def build_delivery_closure_workbench( "github_account_status": str( github_preflight.get("github_account_status") or "unknown" ), - "github_account_suspended": github_preflight.get( - "github_account_suspended" - ) + "github_account_suspended": github_preflight.get("github_account_suspended") is True, "github_api_forbidden_count": _int( github_preflight.get("github_api_forbidden_count") @@ -252,6 +258,11 @@ def build_delivery_closure_workbench( github_preflight.get("controlled_apply_ready_count") ), "github_blocked_preflight_target_count": github_preflight_blockers, + "github_operator_unblock_required": github_operator_unblock.get("required") + is True, + "github_operator_unblock_status": str( + github_operator_unblock.get("status") or "" + ), "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 a3257e3f..0f818203 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 @@ -35,6 +35,20 @@ _EXECUTION_AUTHORIZATION_SCHEMA_VERSION = ( _CONTROLLED_EXECUTION_PREFLIGHT_SCHEMA_VERSION = ( "github_target_controlled_execution_preflight_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", +] +_GITHUB_OPERATOR_UNBLOCK_ACTIONS = [ + "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace", + "refresh_local_github_cli_login_without_sharing_tokens_or_cookies", + "rerun_github_write_channel_dry_run_before_create_or_refs_sync", +] +_GITHUB_OPERATOR_STILL_FORBIDDEN = [ + "do_not_paste_pat_token_private_key_cookie_session_or_authorization_header", + "do_not_collect_private_clone_url_or_credential_value", + "do_not_force_push_delete_refs_or_change_public_visibility", +] _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 = ( @@ -2092,6 +2106,9 @@ def _controlled_execution_preflight_readiness( create_channel_ready = summary.get("github_create_repo_channel_ready") is True refs_channel_ready = summary.get("github_refs_sync_channel_ready") is True write_channel_ready = create_channel_ready and refs_channel_ready + github_account_status = str(summary.get("github_account_status") or "unknown") + github_account_suspended = summary.get("github_account_suspended") is True + github_api_forbidden_count = _int(summary.get("github_api_forbidden_count")) preflight_ready = ( bool(preflight_targets) and authorization_summary["authorization_present"] is True @@ -2110,6 +2127,12 @@ def _controlled_execution_preflight_readiness( for row in preflight_targets if row.get("github_repo") } + operator_unblock = _github_write_channel_operator_unblock( + github_account_status=github_account_status, + github_account_suspended=github_account_suspended, + github_api_forbidden_count=github_api_forbidden_count, + github_write_channel_ready=write_channel_ready, + ) return { "schema_version": str( payload.get("schema_version") @@ -2129,9 +2152,9 @@ def _controlled_execution_preflight_readiness( and authorization_summary["repo_creation_authorized"] is True and authorization_summary["refs_sync_authorized"] is True, "github_write_channel_ready": write_channel_ready, - "github_account_status": str(summary.get("github_account_status") or "unknown"), - "github_account_suspended": summary.get("github_account_suspended") is True, - "github_api_forbidden_count": _int(summary.get("github_api_forbidden_count")), + "github_account_status": github_account_status, + "github_account_suspended": github_account_suspended, + "github_api_forbidden_count": github_api_forbidden_count, "github_create_repo_channel_ready": create_channel_ready, "github_refs_sync_channel_ready": refs_channel_ready, "github_connector_repo_creation_tool_available": summary.get( @@ -2161,6 +2184,9 @@ def _controlled_execution_preflight_readiness( "required_preflight_checks": _strings(payload.get("required_preflight_checks")), "rollback_plan": _dict(payload.get("rollback_plan")), "post_apply_verifiers": _strings(payload.get("post_apply_verifiers")), + "operator_unblock_required": operator_unblock["required"], + "operator_unblock_status": operator_unblock["status"], + "operator_unblock": operator_unblock, "operation_boundaries": { "read_only_api_allowed": boundaries.get("read_only_api_allowed") is True, "github_api_write_allowed_by_authorization": boundaries.get( @@ -2187,6 +2213,40 @@ def _controlled_execution_preflight_readiness( } +def _github_write_channel_operator_unblock( + *, + github_account_status: str, + github_account_suspended: bool, + github_api_forbidden_count: int, + github_write_channel_ready: bool, +) -> dict[str, Any]: + required = not github_write_channel_ready + if github_account_suspended: + status = "github_account_suspended_external_action_required" + elif required: + status = "github_write_channel_reauthentication_or_namespace_required" + else: + status = "github_write_channel_ready_no_operator_action" + + return { + "required": required, + "status": status, + "github_account_status": github_account_status, + "github_account_suspended": github_account_suspended, + "github_api_forbidden_count": github_api_forbidden_count, + "required_actions": _GITHUB_OPERATOR_UNBLOCK_ACTIONS if required else [], + "recheck_commands": _GITHUB_WRITE_CHANNEL_RECHECK_COMMANDS if required else [], + "still_forbidden": _GITHUB_OPERATOR_STILL_FORBIDDEN, + "safe_handoff": ( + "GitHub owner must restore the suspended account or provide a writable " + "private GitHub namespace. Do not share tokens, cookies, private keys, " + "authorization headers, or private clone URLs." + ) + if required + else "GitHub write channel is ready for controlled apply preflight.", + } + + 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 e2b3fd7b..2a12d6ef 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -30,6 +30,10 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert data["summary"]["github_api_forbidden_count"] == 6 assert data["summary"]["github_controlled_apply_ready_count"] == 0 assert data["summary"]["github_blocked_preflight_target_count"] == 5 + assert data["summary"]["github_operator_unblock_required"] is True + assert data["summary"]["github_operator_unblock_status"] == ( + "github_account_suspended_external_action_required" + ) assert data["summary"]["secret_values_collected"] is False assert data["summary"]["average_completion_percent"] >= 0 assert data["summary"]["high_risk_blocker_count"] > 0 @@ -64,6 +68,17 @@ 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"]["operator_unblock"]["required"] is True + assert lanes["github"]["operator_unblock"]["status"] == ( + "github_account_suspended_external_action_required" + ) + assert ( + "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" + in lanes["github"]["operator_unblock"]["required_actions"] + ) + assert lanes["github"]["next_action"] == ( + "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" + ) assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values()) assert all(lane["tone"] in {"ok", "warn", "danger"} for lane in lanes.values()) diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate.py b/apps/api/tests/test_github_target_private_backup_evidence_gate.py index d032e924..c8b36728 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 @@ -245,6 +245,25 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot assert controlled_preflight["github_connector_missing_target_404_count"] == 0 assert controlled_preflight["blocked_preflight_target_count"] == 5 assert controlled_preflight["controlled_apply_ready_count"] == 0 + assert controlled_preflight["operator_unblock_required"] is True + assert ( + controlled_preflight["operator_unblock_status"] + == "github_account_suspended_external_action_required" + ) + operator_unblock = controlled_preflight["operator_unblock"] + assert operator_unblock["required"] is True + assert operator_unblock["github_account_status"] == "suspended" + assert operator_unblock["github_account_suspended"] is True + assert operator_unblock["github_api_forbidden_count"] == 6 + assert ( + "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" + in operator_unblock["required_actions"] + ) + assert "gh api /user --jq '{login:.login}'" in operator_unblock["recheck_commands"] + assert ( + "do_not_paste_pat_token_private_key_cookie_session_or_authorization_header" + in operator_unblock["still_forbidden"] + ) assert ( controlled_preflight["operation_boundaries"]["controlled_apply_allowed"] is False 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 3568fe2d..fefcfd1c 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 @@ -129,6 +129,19 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert controlled_preflight["authorization_ready"] is True assert controlled_preflight["preflight_ready"] is False assert controlled_preflight["github_write_channel_ready"] is False + assert controlled_preflight["operator_unblock_required"] is True + assert ( + controlled_preflight["operator_unblock_status"] + == "github_account_suspended_external_action_required" + ) + assert ( + "complete_github_account_suspension_appeal_or_provide_authorized_writable_namespace" + in controlled_preflight["operator_unblock"]["required_actions"] + ) + assert ( + "git push --dry-run origin HEAD:refs/heads/codex-github-write-channel-readonly-check" + in controlled_preflight["operator_unblock"]["recheck_commands"] + ) assert controlled_preflight["blocked_preflight_target_count"] == 5 assert "192.168.0." not in response.text @@ -144,8 +157,7 @@ def test_github_target_controlled_execution_preflight_endpoint_returns_write_gap data = response.json() assert data["schema_version"] == "github_target_controlled_execution_preflight_v1" assert ( - data["status"] - == "blocked_github_account_suspended_and_write_channel_required" + data["status"] == "blocked_github_account_suspended_and_write_channel_required" ) assert data["authorization_ready"] is True assert data["preflight_ready"] is False @@ -153,6 +165,15 @@ def test_github_target_controlled_execution_preflight_endpoint_returns_write_gap assert data["github_account_status"] == "suspended" assert data["github_account_suspended"] is True assert data["github_api_forbidden_count"] == 6 + assert data["operator_unblock_required"] is True + assert data["operator_unblock_status"] == ( + "github_account_suspended_external_action_required" + ) + assert data["operator_unblock"]["github_account_suspended"] is True + assert ( + "do_not_paste_pat_token_private_key_cookie_session_or_redacted_authorization_header" + in data["operator_unblock"]["still_forbidden"] + ) assert data["github_create_repo_channel_ready"] is False assert data["github_refs_sync_channel_ready"] is False assert data["source_preflight_ready_count"] == 5 diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 769d57ab..acf8d341 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -2429,6 +2429,18 @@ export interface AwoooIStatusCleanupDashboardSnapshot { ui_implementation_authorized: false } +export interface DeliveryOperatorUnblock { + required: boolean + status: string + github_account_status: string + github_account_suspended: boolean + github_api_forbidden_count: number + required_actions: string[] + recheck_commands: string[] + still_forbidden: string[] + safe_handoff: string +} + export interface DeliveryClosureWorkbenchSnapshot { schema_version: 'delivery_closure_workbench_v1' generated_at: string @@ -2438,18 +2450,28 @@ export interface DeliveryClosureWorkbenchSnapshot { loaded_source_count: number average_completion_percent: number high_risk_blocker_count: number - runtime_execution_authorized: false - remote_write_authorized: false - repo_creation_authorized: false - refs_sync_authorized: false - workflow_trigger_authorized: false - secret_values_collected: false + runtime_execution_authorized: boolean + remote_write_authorized: boolean + repo_creation_authorized: boolean + visibility_change_authorized: boolean + refs_sync_authorized: boolean + workflow_trigger_authorized: boolean + github_write_channel_ready: boolean + github_account_status: string + github_account_suspended: boolean + github_api_forbidden_count: number + github_controlled_apply_ready_count: number + github_blocked_preflight_target_count: number + github_operator_unblock_required: boolean + github_operator_unblock_status: string + secret_values_collected: boolean } source_statuses: Array<{ id: string loaded: boolean schema_version: string generated_at: string + missing_reason: string }> lanes: Array<{ id: 'release' | 'github' | 'gitea' | 'runtime' | 'backup' @@ -2459,11 +2481,21 @@ export interface DeliveryClosureWorkbenchSnapshot { blocker_count: number metric: | { kind: 'blocked_gate'; blocked: number; total: number } - | { kind: 'private_backup_verified'; verified: number; total: number } + | { + kind: 'private_backup_verified' + verified: number + total: number + controlled_apply_ready: number + blocked_preflight: number + write_channel_ready: boolean + github_account_status: string + github_account_suspended: boolean + } | { kind: 'workflow_count'; count: number } | { kind: 'surface_count'; total: number } | { kind: 'readiness_row_count'; rows: number } href: string + operator_unblock?: DeliveryOperatorUnblock next_action: string tone: 'ok' | 'warn' | 'danger' }> @@ -2475,15 +2507,17 @@ export interface DeliveryClosureWorkbenchSnapshot { }> 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 - secret_value_collection_allowed: false - backup_restore_execution_allowed: false - active_scan_allowed: false + runtime_write_allowed: boolean + remote_write_allowed: boolean + repo_creation_allowed: boolean + visibility_change_allowed: boolean + refs_sync_allowed: boolean + workflow_trigger_allowed: boolean + github_write_channel_ready: boolean + github_controlled_apply_allowed: boolean + secret_value_collection_allowed: boolean + backup_restore_execution_allowed: boolean + active_scan_allowed: boolean } }