From ba5a10c96e4d176c1d1d41e2404b131656293694 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:56:17 +0800 Subject: [PATCH] feat(api): expose github controlled execution preflight --- apps/api/src/api/v1/agents.py | 35 ++ .../services/delivery_closure_workbench.py | 40 +- ...hub_target_private_backup_evidence_gate.py | 353 ++++++++++++++++++ .../test_delivery_closure_workbench_api.py | 12 +- ...hub_target_private_backup_evidence_gate.py | 100 +++++ ...target_private_backup_evidence_gate_api.py | 44 +++ docs/LOGBOOK.md | 42 +++ ...ntrolled-execution-preflight.snapshot.json | 202 ++++++++++ 8 files changed, 823 insertions(+), 5 deletions(-) create mode 100644 docs/security/github-target-controlled-execution-preflight.snapshot.json diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 3d557bb33..9b6b4bc1e 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -338,6 +338,7 @@ from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) from src.services.github_target_private_backup_evidence_gate import ( + load_latest_github_target_controlled_execution_preflight, load_latest_github_target_private_backup_evidence_gate, preflight_github_target_owner_response_submission, validate_github_target_safe_credential_evidence_refs, @@ -992,6 +993,40 @@ async def get_github_target_private_backup_evidence_gate() -> dict[str, Any]: ) from exc +@router.get( + "/github-target-controlled-execution-preflight", + response_model=dict[str, Any], + summary="取得 GitHub target controlled execution preflight", + description=( + "讀取已提交的 GitHub target controlled execution preflight,明確區分 owner authorization " + "與實際 GitHub create/sync write channel、canonical source、dry-run verifier 是否 ready。" + "此端點不呼叫 GitHub live API、不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、" + "不收 private clone URL credential 或任何 secret value。" + ), +) +async def get_github_target_controlled_execution_preflight() -> dict[str, Any]: + """回傳 GitHub target controlled execution preflight 只讀彙總。""" + try: + payload = await asyncio.to_thread( + load_latest_github_target_controlled_execution_preflight + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error( + "github_target_controlled_execution_preflight_invalid", + error=str(exc), + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="GitHub target controlled execution preflight 無效", + ) from exc + + @router.post( "/github-target-owner-response-intake-preflight", response_model=dict[str, Any], diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index a30de2f9e..ef4ee3253 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -53,6 +53,7 @@ def build_delivery_closure_workbench( status_summary = _dict(status_cleanup.get("summary")) github_summary = _dict(github.get("summary")) github_boundaries = _dict(github.get("operation_boundaries")) + github_preflight = _dict(github.get("controlled_execution_preflight")) gitea_status = _dict(gitea.get("program_status")) gitea_rollups = _dict(gitea.get("rollups")) runtime_status = _dict(runtime.get("program_status")) @@ -62,6 +63,13 @@ 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")) + github_preflight_blockers = _int( + github_preflight.get("blocked_preflight_target_count") + ) + github_lane_blockers = max( + _int(github_summary.get("blocked_target_count")), + github_preflight_blockers, + ) runtime_action_required = set( _strings(runtime_rollups.get("action_required_surface_ids")) ) @@ -90,16 +98,28 @@ def build_delivery_closure_workbench( "completion_percent": _percent( (github_verified / github_required) * 100 if github_required else 0 ), - "status": str(github.get("status") or "unknown"), - "blocker_count": _int(github_summary.get("blocked_target_count")), + "status": str( + github_preflight.get("status") or github.get("status") or "unknown" + ), + "blocker_count": github_lane_blockers, "metric": { "kind": "private_backup_verified", "verified": github_verified, "total": github_required, + "controlled_apply_ready": _int( + github_preflight.get("controlled_apply_ready_count") + ), + "blocked_preflight": github_preflight_blockers, + "write_channel_ready": github_preflight.get( + "github_write_channel_ready" + ) + is True, }, "href": "/governance?tab=automation-inventory", "next_action": str( - github.get("next_action") or _first_target_action(github.get("targets")) + _first_target_action(github_preflight.get("targets")) + or github.get("next_action") + or _first_target_action(github.get("targets")) ), }, { @@ -207,6 +227,14 @@ def build_delivery_closure_workbench( "workflow_trigger_authorized" ) is True, + "github_write_channel_ready": github_preflight.get( + "github_write_channel_ready" + ) + is True, + "github_controlled_apply_ready_count": _int( + github_preflight.get("controlled_apply_ready_count") + ), + "github_blocked_preflight_target_count": github_preflight_blockers, "secret_values_collected": False, }, "source_statuses": source_statuses, @@ -228,6 +256,12 @@ def build_delivery_closure_workbench( "workflow_trigger_allowed" ) is True, + "github_write_channel_ready": github_preflight.get( + "github_write_channel_ready" + ) + is True, + "github_controlled_apply_allowed": github_preflight.get("preflight_ready") + is True, "secret_value_collection_allowed": False, "backup_restore_execution_allowed": False, "active_scan_allowed": False, 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 7fd07a457..d0b82a62b 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 @@ -26,9 +26,15 @@ _MISSING_SOURCE_READINESS_FILE = "github-target-missing-source-readiness.snapsho _EXECUTION_AUTHORIZATION_FILE = ( "github-target-owner-execution-authorization.snapshot.json" ) +_CONTROLLED_EXECUTION_PREFLIGHT_FILE = ( + "github-target-controlled-execution-preflight.snapshot.json" +) _EXECUTION_AUTHORIZATION_SCHEMA_VERSION = ( "github_target_owner_execution_authorization_v1" ) +_CONTROLLED_EXECUTION_PREFLIGHT_SCHEMA_VERSION = ( + "github_target_controlled_execution_preflight_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 = ( @@ -143,6 +149,9 @@ def load_latest_github_target_private_backup_evidence_gate( execution_authorization = _load_optional_snapshot( directory / _EXECUTION_AUTHORIZATION_FILE ) + controlled_execution_preflight = _load_optional_snapshot( + directory / _CONTROLLED_EXECUTION_PREFLIGHT_FILE + ) _require_source_contracts( decision=decision, @@ -152,6 +161,7 @@ def load_latest_github_target_private_backup_evidence_gate( connector_readback=connector_readback, missing_source_readiness=missing_source_readiness, execution_authorization=execution_authorization, + controlled_execution_preflight=controlled_execution_preflight, ) return build_github_target_private_backup_evidence_gate( decision=decision, @@ -161,9 +171,19 @@ def load_latest_github_target_private_backup_evidence_gate( connector_readback=connector_readback, missing_source_readiness=missing_source_readiness, execution_authorization=execution_authorization, + controlled_execution_preflight=controlled_execution_preflight, ) +def load_latest_github_target_controlled_execution_preflight( + security_dir: Path | None = None, +) -> dict[str, Any]: + """Load the GitHub controlled execution preflight readback.""" + return load_latest_github_target_private_backup_evidence_gate(security_dir)[ + "controlled_execution_preflight" + ] + + def preflight_github_target_owner_response_submission( submission: dict[str, Any], security_dir: Path | None = None, @@ -469,6 +489,7 @@ def build_github_target_private_backup_evidence_gate( connector_readback: dict[str, Any] | None = None, missing_source_readiness: dict[str, Any] | None = None, execution_authorization: dict[str, Any] | None = None, + controlled_execution_preflight: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build the read-only gate response from source-control snapshots.""" connector_payload = _dict(connector_readback) @@ -476,6 +497,7 @@ def build_github_target_private_backup_evidence_gate( missing_payload = _dict(missing_source_readiness) missing_summary = _dict(missing_payload.get("summary")) authorization_payload = _dict(execution_authorization) + execution_preflight_payload = _dict(controlled_execution_preflight) authorization_summary = _execution_authorization_summary(authorization_payload) execution_authorized = ( authorization_summary["authorization_present"] @@ -510,6 +532,11 @@ def build_github_target_private_backup_evidence_gate( for row in _list(authorization_payload.get("authorized_targets")) if row.get("github_repo") } + execution_preflight_by_repo = { + str(row.get("github_repo")): _dict(row) + for row in _list(execution_preflight_payload.get("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 = ( @@ -541,6 +568,9 @@ def build_github_target_private_backup_evidence_gate( execution_authorization=authorization_by_repo.get( str(row.get("github_repo")), {} ), + controlled_execution_preflight=execution_preflight_by_repo.get( + str(row.get("github_repo")), {} + ), global_execution_authorized=execution_authorized, ) for row in decisions @@ -566,6 +596,11 @@ def build_github_target_private_backup_evidence_gate( blocked_targets = [ row for row in approval_required_targets if not row["execution_ready"] ] + controlled_preflight = _controlled_execution_preflight_readiness( + payload=execution_preflight_payload, + targets=targets, + authorization_summary=authorization_summary, + ) private_backup_verified_count = sum( 1 for row in approval_required_targets if row["private_backup_verified"] @@ -597,6 +632,7 @@ def build_github_target_private_backup_evidence_gate( "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}", + "github_controlled_execution_preflight": f"docs/security/{_CONTROLLED_EXECUTION_PREFLIGHT_FILE}", }, "summary": { "target_decision_count": len(targets), @@ -739,6 +775,27 @@ def build_github_target_private_backup_evidence_gate( "github_missing_target_refs_sync_authorized_count": authorization_summary[ "github_missing_target_refs_sync_authorized_count" ], + "github_controlled_execution_preflight_ready": controlled_preflight[ + "preflight_ready" + ], + "github_write_channel_ready": controlled_preflight[ + "github_write_channel_ready" + ], + "github_create_repo_channel_ready": controlled_preflight[ + "github_create_repo_channel_ready" + ], + "github_refs_sync_channel_ready": controlled_preflight[ + "github_refs_sync_channel_ready" + ], + "github_missing_target_controlled_apply_ready_count": controlled_preflight[ + "controlled_apply_ready_count" + ], + "github_missing_target_blocked_preflight_count": controlled_preflight[ + "blocked_preflight_target_count" + ], + "github_missing_target_github_404_count": controlled_preflight[ + "github_connector_missing_target_404_count" + ], "execution_ready_count": sum( 1 for row in approval_required_targets if row["execution_ready"] ), @@ -768,6 +825,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, "targets": targets, "acceptance_requirements": _acceptance_requirements(owner_response), "rejection_rules": _rejection_rules(owner_response), @@ -786,6 +844,10 @@ def build_github_target_private_backup_evidence_gate( "workflow_trigger_allowed": authorization_summary[ "workflow_trigger_authorized" ], + "github_write_channel_ready": controlled_preflight[ + "github_write_channel_ready" + ], + "controlled_apply_allowed": controlled_preflight["preflight_ready"], "github_primary_switch_allowed": False, "secret_value_collection_allowed": False, "private_clone_url_collection_allowed": False, @@ -1004,6 +1066,7 @@ def _build_target( missing_source_readiness: dict[str, Any], owner_response_template: dict[str, Any], execution_authorization: dict[str, Any], + controlled_execution_preflight: dict[str, Any], global_execution_authorized: bool, ) -> dict[str, Any]: github_repo = str(decision.get("github_repo") or "") @@ -1049,6 +1112,10 @@ def _build_target( and refs_sync_authorized and workflow_trigger_authorized ) + controlled_apply_ready = ( + execution_ready + and controlled_execution_preflight.get("controlled_apply_ready") is True + ) blockers = _target_blockers( visibility_status, approval_required, private_visibility_verified ) @@ -1150,6 +1217,34 @@ def _build_target( "owner_execution_authorization_source": execution_authorization.get( "source_disposition" ), + "controlled_execution_preflight_status": controlled_execution_preflight.get( + "source_resolution_status" + ), + "controlled_execution_target_selector": controlled_execution_preflight.get( + "target_selector" + ), + "controlled_execution_github_readback_status": controlled_execution_preflight.get( + "github_readback_status" + ), + "source_preflight_ready": controlled_execution_preflight.get( + "source_preflight_ready" + ), + "canonical_source_ready": controlled_execution_preflight.get( + "canonical_source_ready" + ), + "github_collision_preflight_ready": controlled_execution_preflight.get( + "github_collision_preflight_ready" + ), + "create_private_repo_apply_ready": controlled_execution_preflight.get( + "create_private_repo_apply_ready" + ), + "refs_sync_apply_ready": controlled_execution_preflight.get( + "refs_sync_apply_ready" + ), + "controlled_apply_ready": controlled_apply_ready, + "controlled_apply_blockers": _strings( + controlled_execution_preflight.get("blockers") + ), "controlled_preflight_required": bool( owner_execution_authorized and execution_authorization.get("controlled_preflight_required") is True @@ -1246,6 +1341,7 @@ def _require_source_contracts( connector_readback: dict[str, Any], missing_source_readiness: dict[str, Any], execution_authorization: dict[str, Any], + controlled_execution_preflight: dict[str, Any], ) -> None: _require_schema(decision, "github_target_decision_v1", _DECISION_FILE) _require_schema( @@ -1275,6 +1371,12 @@ def _require_source_contracts( _EXECUTION_AUTHORIZATION_SCHEMA_VERSION, _EXECUTION_AUTHORIZATION_FILE, ) + if controlled_execution_preflight: + _require_schema( + controlled_execution_preflight, + _CONTROLLED_EXECUTION_PREFLIGHT_SCHEMA_VERSION, + _CONTROLLED_EXECUTION_PREFLIGHT_FILE, + ) _require_decision_consistency(decision, _DECISION_FILE) _require_probe_consistency(probe, _PROBE_FILE) _require_approval_package_consistency(approval_package, _APPROVAL_PACKAGE_FILE) @@ -1293,6 +1395,12 @@ def _require_source_contracts( missing_source_readiness=missing_source_readiness, label=_EXECUTION_AUTHORIZATION_FILE, ) + if controlled_execution_preflight: + _require_controlled_execution_preflight_consistency( + preflight=controlled_execution_preflight, + missing_source_readiness=missing_source_readiness, + label=_CONTROLLED_EXECUTION_PREFLIGHT_FILE, + ) _require_owner_response_boundaries(owner_response, _OWNER_RESPONSE_FILE) @@ -1592,6 +1700,119 @@ def _require_execution_authorization_consistency( raise ValueError(f"{label}: missing target refs authorization count drift") +def _require_controlled_execution_preflight_consistency( + *, + preflight: dict[str, Any], + missing_source_readiness: dict[str, Any], + label: str, +) -> None: + summary = _dict(preflight.get("summary")) + boundaries = _dict(preflight.get("operation_boundaries")) + targets = [_dict(row) for row in _list(preflight.get("targets"))] + missing_targets = { + str(row.get("github_repo")) + for row in _list(missing_source_readiness.get("targets")) + if _dict(row).get("github_repo") + } + preflight_targets = { + str(row.get("github_repo")) for row in targets if row.get("github_repo") + } + if preflight_targets != missing_targets: + raise ValueError(f"{label}: preflight targets must match missing targets") + if _int(summary.get("authorized_missing_target_count")) != len(targets): + raise ValueError(f"{label}: authorized missing target count must match targets") + + github_404_count = sum( + 1 for row in targets if row.get("github_readback_status") == "api_404_not_found" + ) + source_ready_count = sum( + 1 for row in targets if row.get("source_preflight_ready") is True + ) + create_ready_count = sum( + 1 for row in targets if row.get("create_private_repo_apply_ready") is True + ) + refs_ready_count = sum( + 1 for row in targets if row.get("refs_sync_apply_ready") is True + ) + blocked_count = sum( + 1 for row in targets if row.get("controlled_apply_ready") is not True + ) + if ( + _int(summary.get("github_connector_missing_target_404_count")) + != github_404_count + ): + raise ValueError(f"{label}: GitHub 404 count must match targets") + if _int(summary.get("source_preflight_ready_count")) != source_ready_count: + raise ValueError(f"{label}: source preflight ready count must match targets") + if _int(summary.get("create_private_repo_apply_ready_count")) != create_ready_count: + raise ValueError(f"{label}: create apply ready count must match targets") + if _int(summary.get("refs_sync_apply_ready_count")) != refs_ready_count: + raise ValueError(f"{label}: refs sync ready count must match targets") + if _int(summary.get("blocked_preflight_target_count")) != blocked_count: + raise ValueError(f"{label}: blocked preflight count must match targets") + + false_summary_flags = { + "local_gh_auth_ready", + "github_connector_repo_creation_tool_available", + "github_create_repo_channel_ready", + "github_refs_sync_channel_ready", + "write_performed", + "repo_creation_performed", + "visibility_change_performed", + "refs_sync_performed", + "workflow_trigger_performed", + "secret_values_collected", + "private_clone_urls_collected", + } + enabled = sorted( + flag for flag in false_summary_flags if summary.get(flag) is not False + ) + false_boundary_flags = { + "github_create_repo_channel_ready", + "github_refs_sync_channel_ready", + "controlled_apply_allowed", + "repo_creation_allowed", + "visibility_change_allowed", + "refs_sync_allowed", + "workflow_trigger_allowed", + "force_push_allowed", + "delete_refs_allowed", + "public_visibility_allowed", + "github_primary_switch_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + "raw_payload_storage_allowed", + } + enabled.extend( + sorted( + flag for flag in false_boundary_flags if boundaries.get(flag) is not False + ) + ) + if enabled: + raise ValueError( + f"{label}: preflight must not claim write readiness: {enabled}" + ) + + required_forbidden = { + "secret_value", + "token_value", + "private_clone_url_credential", + "repo_archive", + "git_object_pack", + "force_push", + "delete_refs", + "github_primary_switch", + "public_visibility", + } + missing_forbidden = sorted( + required_forbidden - set(_strings(preflight.get("still_forbidden"))) + ) + if missing_forbidden: + raise ValueError( + f"{label}: still_forbidden missing critical boundaries: {missing_forbidden}" + ) + + 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") @@ -1826,6 +2047,138 @@ def _execution_authorization_summary( } +def _controlled_execution_preflight_readiness( + *, + payload: dict[str, Any], + targets: list[dict[str, Any]], + authorization_summary: dict[str, Any], +) -> dict[str, Any]: + summary = _dict(payload.get("summary")) + boundaries = _dict(payload.get("operation_boundaries")) + preflight_targets = [ + _controlled_execution_target_summary(row) + for row in _list(payload.get("targets")) + ] + blocked_count = _int(summary.get("blocked_preflight_target_count")) + controlled_apply_ready_count = sum( + 1 for row in preflight_targets if row["controlled_apply_ready"] + ) + 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 + preflight_ready = ( + bool(preflight_targets) + and authorization_summary["authorization_present"] is True + and authorization_summary["repo_creation_authorized"] is True + and authorization_summary["refs_sync_authorized"] is True + and write_channel_ready + and blocked_count == 0 + ) + missing_target_repos = { + str(row.get("github_repo")) + for row in targets + if row.get("missing_target_source_resolution_status") + } + evaluated_repos = { + str(row.get("github_repo")) + for row in preflight_targets + if row.get("github_repo") + } + return { + "schema_version": str( + payload.get("schema_version") + or "missing_github_target_controlled_execution_preflight_v1" + ), + "generated_at": str(payload.get("generated_at") or ""), + "status": str( + payload.get("status") + or "blocked_github_controlled_execution_preflight_missing" + ), + "mode": str( + payload.get("mode") or "controlled_apply_preflight_missing_no_github_write" + ), + "authorization_source": str(payload.get("authorization_source") or ""), + "preflight_ready": preflight_ready, + "authorization_ready": authorization_summary["authorization_present"] is True + and authorization_summary["repo_creation_authorized"] is True + and authorization_summary["refs_sync_authorized"] is True, + "github_write_channel_ready": write_channel_ready, + "github_create_repo_channel_ready": create_channel_ready, + "github_refs_sync_channel_ready": refs_channel_ready, + "github_connector_repo_creation_tool_available": summary.get( + "github_connector_repo_creation_tool_available" + ) + is True, + "github_connector_refs_write_tool_available": summary.get( + "github_connector_refs_write_tool_available" + ) + is True, + "local_gh_auth_ready": summary.get("local_gh_auth_ready") is True, + "source_preflight_ready_count": _int( + summary.get("source_preflight_ready_count") + ), + "controlled_apply_ready_count": controlled_apply_ready_count, + "create_private_repo_apply_ready_count": _int( + summary.get("create_private_repo_apply_ready_count") + ), + "refs_sync_apply_ready_count": _int(summary.get("refs_sync_apply_ready_count")), + "blocked_preflight_target_count": blocked_count, + "github_connector_missing_target_404_count": _int( + summary.get("github_connector_missing_target_404_count") + ), + "missing_target_count": len(missing_target_repos), + "evaluated_missing_target_count": len(evaluated_repos & missing_target_repos), + "tool_channel_readback": _dict(payload.get("tool_channel_readback")), + "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")), + "operation_boundaries": { + "read_only_api_allowed": boundaries.get("read_only_api_allowed") is True, + "github_api_write_allowed_by_authorization": boundaries.get( + "github_api_write_allowed_by_authorization" + ) + is True, + "github_create_repo_channel_ready": create_channel_ready, + "github_refs_sync_channel_ready": refs_channel_ready, + "controlled_apply_allowed": preflight_ready, + "repo_creation_allowed": preflight_ready, + "visibility_change_allowed": preflight_ready, + "refs_sync_allowed": preflight_ready, + "workflow_trigger_allowed": preflight_ready, + "force_push_allowed": False, + "delete_refs_allowed": False, + "public_visibility_allowed": False, + "github_primary_switch_allowed": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + "raw_payload_storage_allowed": False, + }, + "targets": preflight_targets, + "still_forbidden": _strings(payload.get("still_forbidden")), + } + + +def _controlled_execution_target_summary(value: Any) -> dict[str, Any]: + row = _dict(value) + return { + "github_repo": str(row.get("github_repo") or ""), + "github_readback_status": str(row.get("github_readback_status") or ""), + "target_selector": str(row.get("target_selector") or ""), + "source_resolution_status": str(row.get("source_resolution_status") or ""), + "source_candidate_type": str(row.get("source_candidate_type") or ""), + "source_preflight_ready": row.get("source_preflight_ready") is True, + "canonical_source_ready": row.get("canonical_source_ready") is True, + "github_collision_preflight_ready": row.get("github_collision_preflight_ready") + is True, + "create_private_repo_apply_ready": row.get("create_private_repo_apply_ready") + is True, + "refs_sync_apply_ready": row.get("refs_sync_apply_ready") is True, + "controlled_apply_ready": row.get("controlled_apply_ready") is True, + "blockers": _strings(row.get("blockers")), + "next_action": str(row.get("next_action") or ""), + } + + 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 cfafb0a76..27f4c9600 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -24,6 +24,9 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): 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"]["github_write_channel_ready"] is False + assert data["summary"]["github_controlled_apply_ready_count"] == 0 + assert data["summary"]["github_blocked_preflight_target_count"] == 5 assert data["summary"]["secret_values_collected"] is False assert data["summary"]["average_completion_percent"] >= 0 assert data["summary"]["high_risk_blocker_count"] > 0 @@ -42,13 +45,16 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): == "github_target_private_backup_evidence_gate_v1" ) assert sources["github_private_backup"]["missing_reason"] == "" - assert lanes["github"]["blocker_count"] == 0 + assert lanes["github"]["blocker_count"] == 5 assert ( lanes["github"]["status"] - == "owner_authorized_controlled_execution_preflight_ready" + == "blocked_github_write_channel_and_source_preflight_required" ) assert lanes["github"]["metric"]["verified"] == 4 assert lanes["github"]["metric"]["total"] == 9 + assert lanes["github"]["metric"]["controlled_apply_ready"] == 0 + assert lanes["github"]["metric"]["blocked_preflight"] == 5 + assert lanes["github"]["metric"]["write_channel_ready"] is False 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()) @@ -60,6 +66,8 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert boundaries["visibility_change_allowed"] is True assert boundaries["refs_sync_allowed"] is True assert boundaries["workflow_trigger_allowed"] is True + assert boundaries["github_write_channel_ready"] is False + assert boundaries["github_controlled_apply_allowed"] is False 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 b0feaeada..405b65ead 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 @@ -7,6 +7,7 @@ from pathlib import Path import pytest from src.services.github_target_private_backup_evidence_gate import ( + load_latest_github_target_controlled_execution_preflight, load_latest_github_target_private_backup_evidence_gate, preflight_github_target_owner_response_submission, validate_github_target_safe_credential_evidence_refs, @@ -61,6 +62,15 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot == 5 ) assert snapshot["summary"]["github_missing_target_refs_sync_authorized_count"] == 5 + assert snapshot["summary"]["github_controlled_execution_preflight_ready"] is False + assert snapshot["summary"]["github_write_channel_ready"] is False + assert snapshot["summary"]["github_create_repo_channel_ready"] is False + assert snapshot["summary"]["github_refs_sync_channel_ready"] is False + assert ( + snapshot["summary"]["github_missing_target_controlled_apply_ready_count"] == 0 + ) + assert snapshot["summary"]["github_missing_target_blocked_preflight_count"] == 5 + assert snapshot["summary"]["github_missing_target_github_404_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 @@ -125,6 +135,8 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot 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"]["github_write_channel_ready"] is False + assert snapshot["operation_boundaries"]["controlled_apply_allowed"] is False assert snapshot["operation_boundaries"]["secret_value_collection_allowed"] is False assert ( snapshot["operation_boundaries"]["private_clone_url_collection_allowed"] @@ -210,6 +222,38 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot assert first_redaction_example["stored_raw_payload_allowed"] is False assert first_redaction_example["execution_authorized"] is False + controlled_preflight = snapshot["controlled_execution_preflight"] + assert ( + controlled_preflight["schema_version"] + == "github_target_controlled_execution_preflight_v1" + ) + assert controlled_preflight["status"] == ( + "blocked_github_write_channel_and_source_preflight_required" + ) + 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["github_create_repo_channel_ready"] is False + assert controlled_preflight["github_refs_sync_channel_ready"] is False + assert controlled_preflight["github_connector_missing_target_404_count"] == 5 + assert controlled_preflight["blocked_preflight_target_count"] == 5 + assert controlled_preflight["controlled_apply_ready_count"] == 0 + assert ( + controlled_preflight["operation_boundaries"]["controlled_apply_allowed"] + is False + ) + assert ( + controlled_preflight["operation_boundaries"]["repo_creation_allowed"] is False + ) + assert controlled_preflight["operation_boundaries"]["refs_sync_allowed"] is False + assert controlled_preflight["tool_channel_readback"]["gh_cli_write_ready"] is False + assert ( + controlled_preflight["tool_channel_readback"][ + "github_connector_create_repo_ready" + ] + is False + ) + targets = {target["github_repo"]: target for target in snapshot["targets"]} assert targets["owenhytsai/awoooi"]["visibility_evidence_status"] == ( "verified_private_by_github_connector_readback" @@ -270,6 +314,13 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot 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"]["controlled_apply_ready"] is False + assert targets["owenhytsai/ewoooc"]["source_preflight_ready"] is False + assert targets["owenhytsai/ewoooc"]["canonical_source_ready"] is False + assert ( + "github_create_repo_channel_unavailable" + in targets["owenhytsai/ewoooc"]["controlled_apply_blockers"] + ) assert targets["owenhytsai/ewoooc"]["private_backup_verified"] is False assert ( targets["owenhytsai/ewoooc"]["owner_response_template_id"] @@ -303,6 +354,39 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot ) +def test_load_github_target_controlled_execution_preflight_from_committed_snapshot(): + preflight = load_latest_github_target_controlled_execution_preflight() + + assert ( + preflight["schema_version"] == "github_target_controlled_execution_preflight_v1" + ) + assert ( + preflight["status"] + == "blocked_github_write_channel_and_source_preflight_required" + ) + assert preflight["authorization_ready"] is True + assert preflight["preflight_ready"] is False + assert preflight["github_write_channel_ready"] is False + assert preflight["github_create_repo_channel_ready"] is False + assert preflight["github_refs_sync_channel_ready"] is False + assert preflight["source_preflight_ready_count"] == 0 + assert preflight["controlled_apply_ready_count"] == 0 + assert preflight["blocked_preflight_target_count"] == 5 + assert preflight["github_connector_missing_target_404_count"] == 5 + assert preflight["operation_boundaries"]["controlled_apply_allowed"] is False + assert preflight["operation_boundaries"]["secret_value_collection_allowed"] is False + assert "private_clone_url_credential" in preflight["still_forbidden"] + target_by_repo = {target["github_repo"]: target for target in preflight["targets"]} + assert ( + target_by_repo["owenhytsai/bitan-pharmacy"]["controlled_apply_ready"] is False + ) + assert ( + "local_worktree_has_tracked_changes" + in target_by_repo["owenhytsai/bitan-pharmacy"]["blockers"] + ) + assert target_by_repo["owenhytsai/VibeWork"]["refs_sync_apply_ready"] is False + + def test_github_target_private_backup_gate_rejects_runtime_authorization(tmp_path): _copy_security_snapshots(tmp_path) owner_response_path = ( @@ -411,6 +495,21 @@ def test_github_target_private_backup_gate_rejects_execution_authorization_secre load_latest_github_target_private_backup_evidence_gate(tmp_path) +def test_github_target_private_backup_gate_rejects_false_preflight_write_ready( + tmp_path, +): + _copy_security_snapshots(tmp_path) + preflight_path = ( + tmp_path / "github-target-controlled-execution-preflight.snapshot.json" + ) + preflight = json.loads(preflight_path.read_text(encoding="utf-8")) + preflight["summary"]["github_create_repo_channel_ready"] = True + preflight_path.write_text(json.dumps(preflight), encoding="utf-8") + + with pytest.raises(ValueError, match="preflight must not claim write readiness"): + 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() @@ -570,6 +669,7 @@ def _copy_security_snapshots(tmp_path: Path) -> None: "github-target-connector-readback.snapshot.json", "github-target-missing-source-readiness.snapshot.json", "github-target-owner-execution-authorization.snapshot.json", + "github-target-controlled-execution-preflight.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 02625ef00..6fdbd5e1e 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 @@ -31,6 +31,13 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g == 5 ) assert data["summary"]["github_missing_target_refs_sync_authorized_count"] == 5 + assert data["summary"]["github_controlled_execution_preflight_ready"] is False + assert data["summary"]["github_write_channel_ready"] is False + assert data["summary"]["github_create_repo_channel_ready"] is False + assert data["summary"]["github_refs_sync_channel_ready"] is False + assert data["summary"]["github_missing_target_controlled_apply_ready_count"] == 0 + assert data["summary"]["github_missing_target_blocked_preflight_count"] == 5 + assert data["summary"]["github_missing_target_github_404_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 @@ -68,6 +75,8 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g 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"]["github_write_channel_ready"] is False + assert data["operation_boundaries"]["controlled_apply_allowed"] is False assert data["operation_boundaries"]["private_clone_url_collection_allowed"] is False intake = data["owner_response_intake_readiness"] assert ( @@ -113,6 +122,41 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g data["targets"][0]["safe_credential_private_clone_url_collection_allowed"] is False ) + controlled_preflight = data["controlled_execution_preflight"] + 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["blocked_preflight_target_count"] == 5 + assert "192.168.0." not in response.text + + +def test_github_target_controlled_execution_preflight_endpoint_returns_write_gap(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/github-target-controlled-execution-preflight") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "github_target_controlled_execution_preflight_v1" + assert ( + data["status"] == "blocked_github_write_channel_and_source_preflight_required" + ) + assert data["authorization_ready"] is True + assert data["preflight_ready"] is False + assert data["github_write_channel_ready"] is False + assert data["github_create_repo_channel_ready"] is False + assert data["github_refs_sync_channel_ready"] is False + assert data["source_preflight_ready_count"] == 0 + assert data["controlled_apply_ready_count"] == 0 + assert data["blocked_preflight_target_count"] == 5 + assert data["github_connector_missing_target_404_count"] == 5 + 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["targets"][0]["controlled_apply_ready"] is False + assert "github_create_repo_channel_unavailable" in data["targets"][0]["blockers"] assert "192.168.0." not in response.text diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 2a2cc90a2..940413625 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -48136,3 +48136,45 @@ production browser smoke: **下一個 P0**: - commit feature,正常 push 到 Gitea;確認 main CD idle/success 後 normal push `HEAD:main`,再做 production readback,目標為 `safe_credential_reviewer_validation_ready=true`、required fields `7`、committed gate reviewer validation passed `0`、`safe_credential_accepted_evidence_count=0`,同時 create/private ready `0`、refs sync ready `0`、execution ready `0` 維持不變。 + +## 2026-06-28 — 08:45 GitHub controlled execution preflight 本地完成 + +**時間與來源**: +- 2026-06-28 08:44-08:55 Asia/Taipei。 +- 來源:feature branch `codex/github-redacted-evidence-validator-20260627`、`gitea/main=54eea881e`、GitHub connector readback 與本地 `gh auth status`。 + +**完成內容**: +- 新增 `docs/security/github-target-controlled-execution-preflight.snapshot.json`,把「統帥全面授權已開」與「實際 GitHub create / refs sync 執行器是否 ready」拆成兩層。 +- `GET /api/v1/agents/github-target-private-backup-evidence-gate` 新增 controlled execution preflight summary:`github_write_channel_ready=false`、`github_create_repo_channel_ready=false`、`github_refs_sync_channel_ready=false`、`github_missing_target_controlled_apply_ready_count=0`、`github_missing_target_blocked_preflight_count=5`、`github_missing_target_github_404_count=5`。 +- 新增 `GET /api/v1/agents/github-target-controlled-execution-preflight`,直接回傳 5 個 missing target 的 target selector、source preflight、GitHub 404 readback、write channel gap、rollback plan 與 post-apply verifier。 +- Delivery Workbench GitHub lane 改用 preflight 狀態:authorization 已開但 write channel / source preflight 未 ready 時,GitHub lane 顯示 `blocked_github_write_channel_and_source_preflight_required`、`blocker_count=5`,避免把 `execution_ready_count=9` 誤讀成「可立刻盲推所有 repo」。 +- GitHub connector live readback:`owenhytsai/awoooi` 可見且 private/admin/push;`owenhytsai/ewoooc`、`bitan-pharmacy`、`tsenyang-website`、`VibeWork`、`agent-bounty-protocol` 均回 `404 Not Found`。此結果只證明 connector 目前不可見 target,不代表可跳過 source preflight 或私有 collision guard。 +- 本地 `gh auth status`:token invalid;目前沒有可用 `gh` create repo / push refs channel。GitHub connector目前可讀 repo,也有 blob/tree/commit/ref 工具,但未提供 create repository 工具。 + +**本地驗證結果**: +- `python3 -m json.tool docs/security/github-target-controlled-execution-preflight.snapshot.json`:通過。 +- `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`:通過。 +- `python3.11 -m ruff check 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 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`:通過。 +- `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`:`20 passed`。 +- `git diff --check`:通過。 +- 本地 readback snippet:gate `execution_ready_count=9`、`github_write_channel_ready=False`、`github_missing_target_controlled_apply_ready_count=0`;preflight `authorization_ready=True`、`blocked_preflight_target_count=5`;Delivery Workbench GitHub lane `status=blocked_github_write_channel_and_source_preflight_required`、`blocker_count=5`。 + +**為什麼還沒有全部推上 GitHub**: +- 不是舊 owner / read-only gate 阻擋;`github_target_owner_execution_authorization_v1` 已把 repo creation、private visibility、refs sync、workflow trigger authorization 打開。 +- 目前實際缺口是 GitHub write channel 與 source preflight:本機 `gh` token invalid、connector未暴露 create repository 工具、5 個 missing target 的 canonical source 仍為 `source_preflight_ready=false`。 +- `not_found_or_private` / connector 404 不能直接當成「一定不存在且可建立」;仍需 collision preflight、source-of-truth diff、no-force refs dry-run、post-sync readback。 + +**仍維持 0 / false**: +- `github_write_channel_ready=false`、`github_create_repo_channel_ready=false`、`github_refs_sync_channel_ready=false`。 +- `github_missing_target_controlled_apply_ready_count=0`、`source_preflight_ready_count=0`、`blocked_preflight_target_count=5`。 +- `repo_creation_performed=false`、`visibility_change_performed=false`、`refs_sync_performed=false`、`workflow_trigger_performed=false`。 +- `secret_value_collection_allowed=false`、`private_clone_url_collection_allowed=false`、`force_push_allowed=false`、`delete_refs_allowed=false`、`public_visibility_allowed=false`。 + +**做過的命令類型**: +- 寫入:repo service / API route / snapshot / tests / LOGBOOK。 +- 只讀:GitHub connector `_get_repo`、`gh auth status`、git fetch / merge、治理 snapshot、本地 readback snippet。 +- 未做:沒有 GitHub repo creation、visibility change、refs sync、workflow trigger、private clone URL collection、secret value collection;沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有 force push。 + +**下一個 P0**: +- commit feature,正常 push 到 Gitea;若 main CD idle/success,normal push `HEAD:main`,部署後 production readback 目標:`github_write_channel_ready=false`、`github_missing_target_controlled_apply_ready_count=0`、`blocked_preflight_target_count=5`,並確認 Workbench GitHub lane 顯示 preflight blocker。 +- 後續真正 controlled apply 需要補 GitHub create repo channel 或可用 refs sync channel,並逐 target 產生 source-of-truth diff / no-force dry-run;仍不讀 secret、不收 private clone URL、不 force push。 diff --git a/docs/security/github-target-controlled-execution-preflight.snapshot.json b/docs/security/github-target-controlled-execution-preflight.snapshot.json new file mode 100644 index 000000000..265978522 --- /dev/null +++ b/docs/security/github-target-controlled-execution-preflight.snapshot.json @@ -0,0 +1,202 @@ +{ + "schema_version": "github_target_controlled_execution_preflight_v1", + "generated_at": "2026-06-28T08:44:54+08:00", + "status": "blocked_github_write_channel_and_source_preflight_required", + "mode": "controlled_apply_preflight_no_secret_no_repo_write", + "authorization_source": "chat_authorization_2026-06-28_full_hard_gate_open", + "summary": { + "owner_execution_authorization_received_count": 1, + "authorized_missing_target_count": 5, + "github_connector_get_repo_checked_count": 6, + "github_connector_existing_private_admin_count": 1, + "github_connector_missing_target_404_count": 5, + "local_gh_auth_ready": false, + "github_connector_repo_creation_tool_available": false, + "github_connector_refs_write_tool_available": true, + "github_create_repo_channel_ready": false, + "github_refs_sync_channel_ready": false, + "source_preflight_ready_count": 0, + "create_private_repo_apply_ready_count": 0, + "refs_sync_apply_ready_count": 0, + "blocked_preflight_target_count": 5, + "write_performed": false, + "repo_creation_performed": false, + "visibility_change_performed": false, + "refs_sync_performed": false, + "workflow_trigger_performed": false, + "secret_values_collected": false, + "private_clone_urls_collected": false + }, + "tool_channel_readback": { + "gh_cli_auth_status": "invalid_token_in_keyring", + "gh_cli_write_ready": false, + "github_connector_read_repo_ready": true, + "github_connector_create_repo_ready": false, + "github_connector_refs_write_ready": true, + "github_connector_verified_private_admin_repo": "owenhytsai/awoooi", + "github_connector_missing_target_status": "api_404_not_found", + "credential_collection_attempted": false, + "secret_value_collection_allowed": false + }, + "required_preflight_checks": [ + "confirm_target_owner_scope_is_owenhytsai", + "verify_github_repo_collision_with_authenticated_readback", + "select_canonical_source_without_copying_secret_values", + "verify_source_worktree_clean_or_pick_remote_source", + "build_source_of_truth_ref_diff", + "run_no_force_refs_sync_dry_run", + "confirm_private_visibility_post_create", + "run_post_sync_refs_readback" + ], + "rollback_plan": { + "repo_creation": "new_private_repo_can_be_left_empty_or_archived_after_no_ref_sync; repo_delete_is_not_authorized_here", + "refs_sync": "normal_push_only; rollback_requires new forward commit or branch restore, never force push or ref delete", + "visibility": "private_only; public_visibility_is_forbidden", + "workflow_trigger": "post_sync_verification_only_after_refs_readback" + }, + "post_apply_verifiers": [ + "github_get_repo_visibility_private", + "github_default_branch_readback", + "github_refs_compare_against_canonical_source", + "awoooi_github_private_backup_evidence_gate_readback", + "delivery_closure_workbench_readback" + ], + "operation_boundaries": { + "read_only_api_allowed": true, + "github_api_write_allowed_by_authorization": true, + "github_create_repo_channel_ready": false, + "github_refs_sync_channel_ready": false, + "controlled_apply_allowed": false, + "repo_creation_allowed": false, + "visibility_change_allowed": false, + "refs_sync_allowed": false, + "workflow_trigger_allowed": false, + "force_push_allowed": false, + "delete_refs_allowed": false, + "public_visibility_allowed": false, + "github_primary_switch_allowed": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false, + "raw_payload_storage_allowed": false + }, + "targets": [ + { + "github_repo": "owenhytsai/ewoooc", + "github_readback_status": "api_404_not_found", + "target_selector": "github_owner=owenhytsai repo=ewoooc source_candidate=wooo/ewoooc", + "source_resolution_status": "blocked_canonical_source_ambiguous", + "source_candidate_type": "gitea_repo_exists_with_momo_lineage_conflict", + "source_preflight_ready": false, + "canonical_source_ready": false, + "github_collision_preflight_ready": true, + "create_private_repo_apply_ready": false, + "refs_sync_apply_ready": false, + "controlled_apply_ready": false, + "blockers": [ + "canonical_source_ambiguous", + "momo_lineage_conflict_requires_source_truth_diff", + "github_create_repo_channel_unavailable", + "github_refs_sync_channel_unavailable" + ], + "next_action": "produce ewoooc versus momo source-of-truth diff, then rerun create/sync dry-run" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "github_readback_status": "api_404_not_found", + "target_selector": "github_owner=owenhytsai repo=bitan-pharmacy source_candidate=local_internal_remote_snapshot", + "source_resolution_status": "blocked_local_worktree_dirty", + "source_candidate_type": "internal_remote_snapshot_candidate", + "source_preflight_ready": false, + "canonical_source_ready": false, + "github_collision_preflight_ready": true, + "create_private_repo_apply_ready": false, + "refs_sync_apply_ready": false, + "controlled_apply_ready": false, + "blockers": [ + "local_worktree_has_tracked_changes", + "canonical_remote_source_not_committed_as_gitea_repo", + "github_create_repo_channel_unavailable", + "github_refs_sync_channel_unavailable" + ], + "next_action": "choose clean local or internal remote source, run source diff, then rerun no-force refs dry-run" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "github_readback_status": "api_404_not_found", + "target_selector": "github_owner=owenhytsai repo=tsenyang-website source_candidate=local_internal_remote_snapshot", + "source_resolution_status": "blocked_local_worktree_dirty", + "source_candidate_type": "internal_remote_snapshot_candidate", + "source_preflight_ready": false, + "canonical_source_ready": false, + "github_collision_preflight_ready": true, + "create_private_repo_apply_ready": false, + "refs_sync_apply_ready": false, + "controlled_apply_ready": false, + "blockers": [ + "local_worktree_has_tracked_and_untracked_changes", + "canonical_remote_source_not_committed_as_gitea_repo", + "github_create_repo_channel_unavailable", + "github_refs_sync_channel_unavailable" + ], + "next_action": "select clean canonical source for tsenyang-website, then rerun no-force refs dry-run" + }, + { + "github_repo": "owenhytsai/VibeWork", + "github_readback_status": "api_404_not_found", + "target_selector": "github_owner=owenhytsai repo=VibeWork source_candidate=wooo/vibework", + "source_resolution_status": "blocked_product_boundary_and_local_divergence", + "source_candidate_type": "gitea_repo_exists_local_worktree_diverged", + "source_preflight_ready": false, + "canonical_source_ready": false, + "github_collision_preflight_ready": true, + "create_private_repo_apply_ready": false, + "refs_sync_apply_ready": false, + "controlled_apply_ready": false, + "blockers": [ + "product_boundary_requires_source_selector", + "local_worktree_ahead_behind_with_changes", + "github_create_repo_channel_unavailable", + "github_refs_sync_channel_unavailable" + ], + "next_action": "select Gitea vibework or local VibeWork as canonical source, then run refs diff dry-run" + }, + { + "github_repo": "owenhytsai/agent-bounty-protocol", + "github_readback_status": "api_404_not_found", + "target_selector": "github_owner=owenhytsai repo=agent-bounty-protocol source_candidate=wooo/agent-bounty-protocol", + "source_resolution_status": "blocked_high_risk_runtime_surface", + "source_candidate_type": "gitea_repo_exists_high_risk_runtime_surface", + "source_preflight_ready": false, + "canonical_source_ready": false, + "github_collision_preflight_ready": true, + "create_private_repo_apply_ready": false, + "refs_sync_apply_ready": false, + "controlled_apply_ready": false, + "blockers": [ + "large_dirty_scan_not_completed", + "runtime_surface_source_selector_required", + "github_create_repo_channel_unavailable", + "github_refs_sync_channel_unavailable" + ], + "next_action": "finish bounded dirty/source scan without secrets, then run canonical refs dry-run" + } + ], + "still_forbidden": [ + "secret_value", + "token_value", + "private_key", + "cookie_or_session", + "authorization_header", + "private_clone_url_credential", + "repo_archive", + "git_object_pack", + "force_push", + "delete_refs", + "tag_rewrite", + "repo_delete", + "github_primary_switch", + "public_visibility", + "raw_runtime_secret_volume", + "unrelated_history_merge" + ] +}