fix(api): expose controlled runtime promotion summaries
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m40s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m40s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -186,12 +186,18 @@ def _build_no_action_manual_handoff_payload(approval) -> dict:
|
|||||||
)
|
)
|
||||||
promotion_summary = _safe_str(metadata.get("repair_candidate_promotion_summary"))
|
promotion_summary = _safe_str(metadata.get("repair_candidate_promotion_summary"))
|
||||||
if not promotion_summary and promotion_contract:
|
if not promotion_summary and promotion_contract:
|
||||||
|
runtime_state = (
|
||||||
|
"controlled"
|
||||||
|
if promotion_contract.get("runtime_execution_authorized") is True
|
||||||
|
or promotion_contract.get("runtime_write_allowed") is True
|
||||||
|
else "false"
|
||||||
|
)
|
||||||
promotion_summary = (
|
promotion_summary = (
|
||||||
f"route={promotion_contract.get('route_id') or '--'}; "
|
f"route={promotion_contract.get('route_id') or '--'}; "
|
||||||
f"promotion={promotion_contract.get('ready_count') or 0}/"
|
f"promotion={promotion_contract.get('ready_count') or 0}/"
|
||||||
f"{promotion_contract.get('total_count') or 0}; "
|
f"{promotion_contract.get('total_count') or 0}; "
|
||||||
f"blocked={promotion_contract.get('blocked_count') or 0}; "
|
f"blocked={promotion_contract.get('blocked_count') or 0}; "
|
||||||
f"runtime=false"
|
f"runtime={runtime_state}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5973,9 +5973,15 @@ def _repair_candidate_promotion_summary(contract: Mapping[str, Any]) -> str:
|
|||||||
total = _safe_int(contract.get("total_count"))
|
total = _safe_int(contract.get("total_count"))
|
||||||
blocked = _safe_int(contract.get("blocked_count"))
|
blocked = _safe_int(contract.get("blocked_count"))
|
||||||
status_value = str(contract.get("status") or "unknown")
|
status_value = str(contract.get("status") or "unknown")
|
||||||
|
runtime_state = (
|
||||||
|
"controlled"
|
||||||
|
if contract.get("runtime_execution_authorized") is True
|
||||||
|
or contract.get("runtime_write_allowed") is True
|
||||||
|
else "false"
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
f"route={route}; promotion={ready}/{total}; "
|
f"route={route}; promotion={ready}/{total}; "
|
||||||
f"blocked={blocked}; status={status_value}; runtime=false"
|
f"blocked={blocked}; status={status_value}; runtime={runtime_state}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -867,9 +867,15 @@ class RepairCandidateService:
|
|||||||
total = int(contract.get("total_count") or 0)
|
total = int(contract.get("total_count") or 0)
|
||||||
blocked = int(contract.get("blocked_count") or 0)
|
blocked = int(contract.get("blocked_count") or 0)
|
||||||
status = str(contract.get("status") or "unknown")
|
status = str(contract.get("status") or "unknown")
|
||||||
|
runtime_state = (
|
||||||
|
"controlled"
|
||||||
|
if contract.get("runtime_execution_authorized") is True
|
||||||
|
or contract.get("runtime_write_allowed") is True
|
||||||
|
else "false"
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
f"route={route}; promotion={ready}/{total}; "
|
f"route={route}; promotion={ready}/{total}; "
|
||||||
f"blocked={blocked}; status={status}; runtime=false"
|
f"blocked={blocked}; status={status}; runtime={runtime_state}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _draft_summary_for_operator(self, template: dict[str, Any]) -> str:
|
def _draft_summary_for_operator(self, template: dict[str, Any]) -> str:
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ from src.services.platform_operator_service import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_repair_candidate_promotion_summary_marks_controlled_runtime() -> None:
|
||||||
|
summary = platform_operator_service._repair_candidate_promotion_summary({
|
||||||
|
"route_id": "ansible:188-ai-web",
|
||||||
|
"ready_count": 11,
|
||||||
|
"total_count": 11,
|
||||||
|
"blocked_count": 0,
|
||||||
|
"status": "controlled_apply_auto_authorized",
|
||||||
|
"runtime_execution_authorized": True,
|
||||||
|
"runtime_write_allowed": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert summary == (
|
||||||
|
"route=ansible:188-ai-web; promotion=11/11; "
|
||||||
|
"blocked=0; status=controlled_apply_auto_authorized; runtime=controlled"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_outbound_timeline_title_labels_runbook_review() -> None:
|
def test_outbound_timeline_title_labels_runbook_review() -> None:
|
||||||
title = _outbound_timeline_title(
|
title = _outbound_timeline_title(
|
||||||
"telegram",
|
"telegram",
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ async def test_candidate_blocked_observe_only_prompts_repair_playbook_draft() ->
|
|||||||
assert promotion_contract["runtime_write_allowed"] is False
|
assert promotion_contract["runtime_write_allowed"] is False
|
||||||
assert result.metadata["repair_candidate_promotion_contract"] == promotion_contract
|
assert result.metadata["repair_candidate_promotion_contract"] == promotion_contract
|
||||||
assert "promotion=6/11" in result.metadata["repair_candidate_promotion_summary"]
|
assert "promotion=6/11" in result.metadata["repair_candidate_promotion_summary"]
|
||||||
|
assert "runtime=false" in result.metadata["repair_candidate_promotion_summary"]
|
||||||
work_item = draft_package["awooop_work_item"]
|
work_item = draft_package["awooop_work_item"]
|
||||||
assert work_item["status"] == "owner_review_ready"
|
assert work_item["status"] == "owner_review_ready"
|
||||||
assert work_item["next_action"] == "owner_review_repair_candidate_draft"
|
assert work_item["next_action"] == "owner_review_repair_candidate_draft"
|
||||||
@@ -375,6 +376,25 @@ async def test_candidate_blocked_observe_only_prompts_repair_playbook_draft() ->
|
|||||||
assert work_item["coverage_gap"]["next_owner_lane"] == "promote_diagnostic_to_repair_playbook"
|
assert work_item["coverage_gap"]["next_owner_lane"] == "promote_diagnostic_to_repair_playbook"
|
||||||
|
|
||||||
|
|
||||||
|
def test_promotion_summary_marks_controlled_runtime_when_apply_gate_passes() -> None:
|
||||||
|
service = RepairCandidateService()
|
||||||
|
|
||||||
|
summary = service._promotion_summary_for_operator({
|
||||||
|
"route_id": "ansible:188-ai-web",
|
||||||
|
"ready_count": 11,
|
||||||
|
"total_count": 11,
|
||||||
|
"blocked_count": 0,
|
||||||
|
"status": "controlled_apply_auto_authorized",
|
||||||
|
"runtime_execution_authorized": True,
|
||||||
|
"runtime_write_allowed": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert summary == (
|
||||||
|
"route=ansible:188-ai-web; promotion=11/11; "
|
||||||
|
"blocked=0; status=controlled_apply_auto_authorized; runtime=controlled"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_postgres_slow_query_gap_prefills_database_owner_review_not_restart() -> None:
|
async def test_postgres_slow_query_gap_prefills_database_owner_review_not_restart() -> None:
|
||||||
incident = _incident()
|
incident = _incident()
|
||||||
|
|||||||
@@ -43,6 +43,30 @@
|
|||||||
|
|
||||||
**下一個 P0**:補 Wazuh live metadata owner gate、server-side secret source metadata、readonly account scope、manager health ref、TLS validation ref、post-enable readback,之後才可進 manager registry 全量交叉驗收;仍不得直接寫 secret、重啟 Wazuh、重新註冊 agent、改 Nginx / firewall、SSH 主機或啟用 active response。
|
**下一個 P0**:補 Wazuh live metadata owner gate、server-side secret source metadata、readonly account scope、manager health ref、TLS validation ref、post-enable readback,之後才可進 manager registry 全量交叉驗收;仍不得直接寫 secret、重啟 Wazuh、重新註冊 agent、改 Nginx / firewall、SSH 主機或啟用 active response。
|
||||||
|
|
||||||
|
## 2026-06-26|D1H 修復候選 promotion summary:受控執行不再誤顯示 runtime=false
|
||||||
|
|
||||||
|
**背景**:使用者持續指出 Telegram / AwoooP 告警看起來幾乎都停在人工處理。D1F 已把低 / 中 / 高風險受控自動化契約更新為 controlled apply,但程式內仍有三個摘要路徑把所有 `repair_candidate_promotion_contract` 固定輸出成 `runtime=false`,導致即使合約已帶有 `runtime_execution_authorized=true` 或 `runtime_write_allowed=true`,Telegram / AwoooP operator summary 仍會被誤讀為完全沒有自動化。
|
||||||
|
|
||||||
|
**完成內容**:
|
||||||
|
- `platform_operator_service` 的 `_repair_candidate_promotion_summary` 改為讀取 promotion contract 的 `runtime_execution_authorized` / `runtime_write_allowed`。
|
||||||
|
- `repair_candidate_service` 的 `_promotion_summary_for_operator` 同步採用同一判讀。
|
||||||
|
- Telegram fallback handoff payload 在缺少既有 summary 時,也依 promotion contract 產生 `runtime=controlled` 或 `runtime=false`。
|
||||||
|
- blocked / owner review 草案仍維持 `runtime=false`;只有已通過受控執行條件的合約才顯示 `runtime=controlled`,避免把未過安全門的事件誤標成可執行。
|
||||||
|
|
||||||
|
**驗證**:
|
||||||
|
- `apps/api/venv/bin/python -m pytest apps/api/tests/test_repair_candidate_service.py apps/api/tests/test_awooop_operator_timeline_labels.py -q`:`77 passed`。
|
||||||
|
- `apps/api/venv/bin/python -m py_compile apps/api/src/services/platform_operator_service.py apps/api/src/services/repair_candidate_service.py apps/api/src/api/v1/telegram.py`:通過。
|
||||||
|
- `git diff --check`:通過。
|
||||||
|
- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。
|
||||||
|
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
|
||||||
|
- 本機 Telegram 目標測試因 `apps/api/venv` 缺少 `opentelemetry`,在 collection 階段停止;需以 Gitea CD 完整測試環境作最終驗證。
|
||||||
|
|
||||||
|
**完成度 / 邊界**:
|
||||||
|
- Promotion summary runtime truthfulness:`100%`。
|
||||||
|
- Telegram / AwoooP 告警自動化可追蹤性:`99% -> 99.5%`。
|
||||||
|
- 真正 AI 自動化 runtime 閉環:仍需新 incident / 重診驗證 controlled apply worker、post-apply verifier、KM / PlayBook trust writeback。
|
||||||
|
- 本段沒有開啟 runtime gate、沒有執行 Ansible apply、沒有 SSH、沒有 service restart、沒有 Telegram live send、沒有 secret read、沒有 provider switch。
|
||||||
|
|
||||||
## 2026-06-26|D1F AI Agent 受控自動化契約:低 / 中 / 高風險不再停在人工審核
|
## 2026-06-26|D1F AI Agent 受控自動化契約:低 / 中 / 高風險不再停在人工審核
|
||||||
|
|
||||||
**背景**:使用者明確修正方向:低、中、高風險都必須由 AI Agent 走受控自動化處理,高風險不再預設等待人工審核;只有 critical / secret / destructive / paid / force-push 等 break-glass 邊界需保留。盤點後確認部分報表、Schema、API 型別與 AI 技術雷達日週月報仍殘留 `high risk owner review`、`current_execution_enabled=false` 或「高風險必須人工」語意。
|
**背景**:使用者明確修正方向:低、中、高風險都必須由 AI Agent 走受控自動化處理,高風險不再預設等待人工審核;只有 critical / secret / destructive / paid / force-push 等 break-glass 邊界需保留。盤點後確認部分報表、Schema、API 型別與 AI 技術雷達日週月報仍殘留 `high risk owner review`、`current_execution_enabled=false` 或「高風險必須人工」語意。
|
||||||
|
|||||||
Reference in New Issue
Block a user