diff --git a/apps/api/src/services/ai_agent_log_controlled_writeback_executor_readback.py b/apps/api/src/services/ai_agent_log_controlled_writeback_executor_readback.py index 5ec0d5c39..27009e096 100644 --- a/apps/api/src/services/ai_agent_log_controlled_writeback_executor_readback.py +++ b/apps/api/src/services/ai_agent_log_controlled_writeback_executor_readback.py @@ -106,6 +106,10 @@ def load_latest_ai_agent_log_controlled_writeback_executor_readback() -> dict[st "current_blocker_local_recovery_package_count": sum( 1 for item in current_blocker_queue if item["controlled_recovery_package"] ), + "current_blocker_harbor_recovery_receipt_input_count": sum( + item["harbor_recovery_receipt_input_count"] + for item in current_blocker_queue + ), "runtime_dispatch_performed": False, }, "active_blockers": active_blockers, @@ -331,6 +335,13 @@ def _current_blocker_queue_item(recovery: dict[str, Any]) -> dict[str, Any]: recovery.get("controlled_recovery_package") or "" ), "post_apply_verifier": str(recovery.get("post_apply_verifier") or ""), + "harbor_recovery_receipt_endpoint": ( + "/api/v1/agents/harbor-registry-controlled-recovery-receipt" + ), + "harbor_recovery_receipt_inputs": _harbor_recovery_receipt_inputs(), + "harbor_recovery_receipt_input_count": len( + _harbor_recovery_receipt_inputs() + ), "controlled_local_console_execution_plan": _list_of_dicts( recovery.get("controlled_local_console_execution_plan") ), @@ -355,6 +366,61 @@ def _current_blocker_queue_item(recovery: dict[str, Any]) -> dict[str, Any]: } +def _harbor_recovery_receipt_inputs() -> list[dict[str, Any]]: + metadata_boundary = { + "metadata_only": True, + "raw_output_allowed": False, + "secret_value_allowed": False, + } + return [ + { + "input_id": "gitea_actions_queue_readback", + "source": "read-public-gitea-actions-queue.py --json", + "required_when": "before_and_after_110_controlled_recovery", + "purpose": ( + "classify cd run status, harbor repair waiting state, " + "awoooi-host no-matching runner, and stale job payloads" + ), + **metadata_boundary, + }, + { + "input_id": "ssh_local_repair_output", + "source": "recover-110-control-path-and-harbor-local.sh --check/apply", + "required_when": "after_diagnose_ssh_publickey_phase", + "purpose": "prove 110 ssh control-channel metadata and permissions", + **metadata_boundary, + }, + { + "input_id": "watchdog_check_output", + "source": "harbor-watchdog.sh --check", + "required_when": "before_harbor_repair_once_phase", + "purpose": "prove check-mode harbor local registry /v2/ state", + **metadata_boundary, + }, + { + "input_id": "watchdog_repair_output", + "source": "harbor-watchdog.sh --repair-once", + "required_when": "only_if_check_mode_proves_v2_unhealthy", + "purpose": "prove bounded repair-once result without daemon restart", + **metadata_boundary, + }, + { + "input_id": "public_registry_v2_http_status", + "source": "public registry /v2/ verifier", + "required_when": "post_apply_verifier", + "purpose": "prove external registry route is 200/401 before CD retry", + **metadata_boundary, + }, + { + "input_id": "internal_registry_v2_http_status", + "source": "internal 110 registry /v2/ verifier", + "required_when": "post_apply_verifier", + "purpose": "prove local registry route is 200/401 before CD retry", + **metadata_boundary, + }, + ] + + def _list_of_dicts(value: Any) -> list[dict[str, Any]]: if not isinstance(value, list): return [] diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 2df1e0748..52ab35641 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -533,6 +533,15 @@ def apply_ai_loop_current_blocker_execution_queue( post_recovery_readback_commands = _strings( first_item.get("post_recovery_readback_commands") ) + harbor_recovery_receipt_inputs = [ + _dict(item) + for item in _list(first_item.get("harbor_recovery_receipt_inputs")) + ] + harbor_recovery_receipt_input_ids = [ + str(item.get("input_id") or "") + for item in harbor_recovery_receipt_inputs + if item.get("input_id") + ] forbidden_runtime_actions = _strings(first_item.get("forbidden_runtime_actions")) external_blocker = str(first_item.get("external_control_path_blocker") or "") runtime_write_gate = str(first_item.get("runtime_write_gate") or "") @@ -559,6 +568,12 @@ def apply_ai_loop_current_blocker_execution_queue( state["ai_loop_current_blocker_post_recovery_readback_command_count"] = len( post_recovery_readback_commands ) + state["ai_loop_current_blocker_harbor_recovery_receipt_input_count"] = len( + harbor_recovery_receipt_inputs + ) + state["ai_loop_current_blocker_harbor_recovery_receipt_input_ids"] = ( + harbor_recovery_receipt_input_ids + ) state["ai_loop_current_blocker_forbidden_runtime_action_count"] = len( forbidden_runtime_actions ) @@ -610,6 +625,18 @@ def apply_ai_loop_current_blocker_execution_queue( evidence["ai_loop_current_blocker_post_recovery_readback_commands"] = ( post_recovery_readback_commands ) + evidence["ai_loop_current_blocker_harbor_recovery_receipt_endpoint"] = str( + first_item.get("harbor_recovery_receipt_endpoint") or "" + ) + evidence["ai_loop_current_blocker_harbor_recovery_receipt_input_count"] = len( + harbor_recovery_receipt_inputs + ) + evidence["ai_loop_current_blocker_harbor_recovery_receipt_input_ids"] = ( + harbor_recovery_receipt_input_ids + ) + evidence["ai_loop_current_blocker_harbor_recovery_receipt_inputs"] = ( + harbor_recovery_receipt_inputs + ) evidence["ai_loop_current_blocker_forbidden_runtime_actions"] = ( forbidden_runtime_actions ) @@ -651,7 +678,8 @@ def apply_ai_loop_current_blocker_execution_queue( "P0-006-AI-LOOP-CURRENT-BLOCKER-EXECUTION-QUEUE: execute the " f"{blocker_id} queue item through 110 control-path readback, " f"{len(local_console_plan)} ordered local-console phases, post-recovery " - "readback commands, and metadata-only KM/RAG/MCP/PlayBook writeback." + "readback commands, Harbor receipt inputs, and metadata-only " + "KM/RAG/MCP/PlayBook writeback." ), ( "P0-006-HARBOR-REGISTRY-CONTROLLED-RECOVERY-PREFLIGHT: after the " @@ -684,6 +712,12 @@ def apply_ai_loop_current_blocker_execution_queue( summary["ai_loop_current_blocker_post_recovery_readback_command_count"] = len( post_recovery_readback_commands ) + summary["ai_loop_current_blocker_harbor_recovery_receipt_input_count"] = len( + harbor_recovery_receipt_inputs + ) + summary["ai_loop_current_blocker_harbor_recovery_receipt_input_ids"] = ( + harbor_recovery_receipt_input_ids + ) summary["ai_loop_current_blocker_forbidden_runtime_action_count"] = len( forbidden_runtime_actions ) diff --git a/apps/api/tests/test_ai_agent_log_controlled_writeback_executor_readback_api.py b/apps/api/tests/test_ai_agent_log_controlled_writeback_executor_readback_api.py index e41a66772..65262736d 100644 --- a/apps/api/tests/test_ai_agent_log_controlled_writeback_executor_readback_api.py +++ b/apps/api/tests/test_ai_agent_log_controlled_writeback_executor_readback_api.py @@ -66,6 +66,10 @@ def _assert_executor_readback(payload: dict, *, public_endpoint: bool = False): assert payload["rollups"]["current_blocker_execution_queue_count"] == 1 assert payload["rollups"]["current_blocker_control_path_blocked_count"] == 1 assert payload["rollups"]["current_blocker_local_recovery_package_count"] == 1 + assert ( + payload["rollups"]["current_blocker_harbor_recovery_receipt_input_count"] + == 6 + ) assert payload["rollups"]["runtime_dispatch_performed"] is False batches = {batch["target"]: batch for batch in payload["execution_batches"]} @@ -120,6 +124,27 @@ def _assert_executor_readback(payload: dict, *, public_endpoint: bool = False): assert current_queue[0]["post_apply_verifier"] == ( "check-awoooi-110-controlled-cd-lane-readiness.sh" ) + assert current_queue[0]["harbor_recovery_receipt_endpoint"] == ( + "/api/v1/agents/harbor-registry-controlled-recovery-receipt" + ) + assert current_queue[0]["harbor_recovery_receipt_input_count"] == 6 + assert [ + item["input_id"] + for item in current_queue[0]["harbor_recovery_receipt_inputs"] + ] == [ + "gitea_actions_queue_readback", + "ssh_local_repair_output", + "watchdog_check_output", + "watchdog_repair_output", + "public_registry_v2_http_status", + "internal_registry_v2_http_status", + ] + assert all( + item["metadata_only"] is True + and item["raw_output_allowed"] is False + and item["secret_value_allowed"] is False + for item in current_queue[0]["harbor_recovery_receipt_inputs"] + ) assert [phase["phase_id"] for phase in current_queue[0][ "controlled_local_console_execution_plan" ]] == [ diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 5b8fdd3c2..9338bb6fd 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -318,6 +318,24 @@ def test_awoooi_priority_work_order_readback_overlays_ai_loop_current_blocker_qu "curl -k https://registry.wooo.work/v2/", "curl http://192.168.0.110:5000/v2/", ] + assert evidence["ai_loop_current_blocker_harbor_recovery_receipt_endpoint"] == ( + "/api/v1/agents/harbor-registry-controlled-recovery-receipt" + ) + assert evidence["ai_loop_current_blocker_harbor_recovery_receipt_input_count"] == 6 + assert evidence["ai_loop_current_blocker_harbor_recovery_receipt_input_ids"] == [ + "gitea_actions_queue_readback", + "ssh_local_repair_output", + "watchdog_check_output", + "watchdog_repair_output", + "public_registry_v2_http_status", + "internal_registry_v2_http_status", + ] + assert all( + item["metadata_only"] is True + and item["raw_output_allowed"] is False + and item["secret_value_allowed"] is False + for item in evidence["ai_loop_current_blocker_harbor_recovery_receipt_inputs"] + ) assert "workflow_dispatch_from_this_endpoint" in evidence[ "ai_loop_current_blocker_forbidden_runtime_actions" ] @@ -331,6 +349,15 @@ def test_awoooi_priority_work_order_readback_overlays_ai_loop_current_blocker_qu assert payload["summary"][ "ai_loop_current_blocker_post_recovery_readback_command_count" ] == 3 + assert ( + payload["summary"][ + "ai_loop_current_blocker_harbor_recovery_receipt_input_count" + ] + == 6 + ) + assert payload["summary"][ + "ai_loop_current_blocker_harbor_recovery_receipt_input_ids" + ][0] == "gitea_actions_queue_readback" assert payload["summary"][ "ai_loop_current_blocker_forbidden_runtime_action_count" ] == 6 @@ -338,6 +365,7 @@ def test_awoooi_priority_work_order_readback_overlays_ai_loop_current_blocker_qu "P0-006-AI-LOOP-CURRENT-BLOCKER-EXECUTION-QUEUE" ) assert "5 ordered local-console phases" in payload["next_execution_order"][0] + assert "Harbor receipt inputs" in payload["next_execution_order"][0] def test_awoooi_priority_work_order_readback_overlays_live_stockplatform_drift(): diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 1222e06ef..e432d15b1 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -50902,3 +50902,21 @@ production browser smoke: **下一步**: - 跑 focused suite / guards 後 commit / push;runtime 仍卡 Harbor / 110 時,P0 下一步仍是恢復 `awoooi-host` controlled lane 或在 110 local console / root shell 依 phase 執行 check-mode / bounded repair,再提交 queue + registry + deploy marker receipt。 + +## 2026-06-30 — 23:12 AI Loop executor Harbor receipt input contract + +**完成內容**: +- `agent-log-controlled-writeback-executor-readback` 的 `current_blocker_execution_queue` 新增 `harbor_recovery_receipt_endpoint` 與 6 個 `harbor_recovery_receipt_inputs`,把下一步 receipt 需要的 `gitea_actions_queue_readback`、`ssh_local_repair_output`、`watchdog_check_output`、`watchdog_repair_output`、`public_registry_v2_http_status`、`internal_registry_v2_http_status` 明確列給 AI Agent。 +- 每個 receipt input 固定 `metadata_only=true`、`raw_output_allowed=false`、`secret_value_allowed=false`,避免把 queue / SSH / watchdog / verifier 回讀變成 raw log 或 secret 收集。 +- `awoooi-priority-work-order-readback` 同步投影 receipt endpoint、input ids、input count 與 redaction policy,讓主工作順序、AI Loop executor、Harbor receipt validator 對齊同一批 receipt 節點。 + +**本地驗證結果**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost:5432/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_ai_agent_log_controlled_writeback_executor_readback_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/tests/test_harbor_registry_controlled_recovery_receipt.py -q`:`16 passed`。 +- `ruff check`、`py_compile`:通過。 + +**live truth**: +- 23:10 live queue:CD `#4111` Running / `b72628914 feat(agent): add harbor queue receipt readback`,classifier `harbor_registry_public_route_unavailable_pending_retry`,Harbor repair `#4112` Waiting,no matching label `awoooi-host`,Harbor retry skip reason `not_110_host`。 +- registry / Harbor health 仍 502;本輪仍未做 runtime write、workflow dispatch、host restart、Docker / K3s / Nginx reload、secret / runner token 讀取。 + +**下一步**: +- commit / push;若 live runtime 仍卡,下一個主線動作仍是 110 local console / root shell 依 receipt input contract 提交 non-secret queue + SSH + watchdog + registry verifier receipt,或恢復 `awoooi-host` controlled lane 後讓 `harbor-110-local-repair` 接手。