From 899635cc63a257c7298ac1852d4e0f28cfd69f55 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:44:45 +0800 Subject: [PATCH 1/6] docs(runner): record 110 fail-closed pressure exception [skip ci] --- AGENTS.md | 2 ++ docs/HARD_RULES.md | 14 ++++++++++++++ docs/LOGBOOK.md | 17 +++++++++++++++++ ...26-04-15-MASTER-ai-autonomous-flywheel-v2.md | 2 +- ops/runner/README.md | 13 +++++++++++++ scripts/ci/wait-host-web-build-pressure.sh | 3 ++- 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b5377a5c8..750ee049e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ 正確動作是 AI 自動補齊 target selector、source-of-truth diff、check-mode / dry-run、rollback、post-apply verifier、KM / PlayBook trust writeback,然後推進可驗證、可回滾、低爆炸半徑的實作。 +**110 runner 壓力事故例外**:Gitea / act-runner 對 110 造成 CPU / headless smoke 壓力時,屬事故級容量保護,不得用「全面授權」直接重開 runner、移除 mask、還原 runner binary 或把 host pressure gate 改成 warn-only。正確動作是先做 runner 搬遷 / 限流 / label isolation / smoke 排程,再以 check-mode、rollback 與 post-apply verifier 受控恢復。 + --- ## 🔴 絕對禁止 → [HARD_RULES.md](docs/HARD_RULES.md) diff --git a/docs/HARD_RULES.md b/docs/HARD_RULES.md index 88e3ef243..39580d3c1 100644 --- a/docs/HARD_RULES.md +++ b/docs/HARD_RULES.md @@ -287,6 +287,20 @@ OpenClaw 核心替換、仲裁模型升級、SDK / runtime 新依賴正式引入 force push / 刪 repo / 刪 refs / 改 repo visibility / raw runtime secret volume 讀寫 ``` +### 110 runner 壓力事故例外 + +2026-06-28 事故後,110 上的 Gitea / act-runner、StockPlatform headless smoke、host-side Next build 與 Docker / BuildKit 壓力屬容量事故保護面。即使收到「批准 / 繼續 / 全面授權」,也不得直接重開 runner、解除 service mask、還原 live runner binary、恢復泛用 `ubuntu-latest` label,或把 host pressure gate 改成 warn-only 作為預設。 + +允許的 controlled apply 是降壓與防再發:停止 / disable / mask runner、quarantine runner binary、收斂 labels、補 source fail-closed guard、搬遷 runner、限制 concurrency、把 smoke 改成排程 / 非 110 runner,以及執行只讀 pressure / cold-start verifier。 + +恢復 runner 必須同時具備: + +1. target selector:明確列出 service、runner dir、label 與承接 repo。 +2. source-of-truth diff:repo / unit / startup script / runner config 都有一致變更。 +3. 限流或搬遷:不再由 110 production host 承接泛用重型 build / smoke。 +4. rollback:能回到 inactive / masked / fail-closed stub。 +5. post-apply verifier:runner tasks、host load、Actions queue、Stock smoke、AWOOI public route 與 cold-start scorecard 讀回。 + ### 跨專案繼承 本節是 AWOOOI / AwoooP / IwoooS / StockPlatform / VibeWork / MOMO / AwoooGo / 2026FIFA / Agent Bounty / Tsenyang 等所有專案的預設行為。若任一 repo 的 `AGENTS.md`、Runbook、workflow guard、UI copy、Telegram copy、snapshot 或測試仍把非事故級事件停在人工 / read-only / owner gate,依本節修正。 diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 2a2cc90a2..975a51ea6 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,20 @@ +## 2026-06-28 — 08:45 110 runner 壓力事故 source / live fail-closed 收斂 + +**背景**:統帥全面授權打開非事故級 gate,但 110 Gitea runner 反覆拉起 StockPlatform headless Chrome smoke,已造成 production host CPU / CI 壓力事故;runner 未搬遷 / 限流前不得直接重開。 + +**完成內容**: +- Gitea main / branch 已推 `54eea881e fix(ci): fail closed host runner pressure guards [skip ci]`,回復 `scripts/ci/wait-host-web-build-pressure.sh` fail-closed 預設、`scripts/reboot-recovery/awoooi-startup-110.sh` 雙鑰匙 sentinel、`.gitea/workflows/cd.yaml` Docker build lock fail-closed。 +- Live 110 `/usr/local/bin/awoooi-wait-host-web-build-pressure.sh` 與 `/usr/local/bin/awoooi-startup-110.sh` 已同步 fail-closed 並加 immutable。 +- Live 110 四條 runner 入口改為 immutable fail-closed stub,原 ELF 僅 quarantine 不讀內容:`/home/wooo/act-runner/act_runner`、`/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard`、`/home/wooo/act-runner-controlled/act_runner`、`/home/wooo/awoooi-controlled-runner/awoooi_controlled_runner`。 +- `gitea-act-runner-host.service`、`gitea-act-runner-awoooi-controlled.service`、`gitea-awoooi-controlled-runner.service`、`gitea-act-runner-awoooi-open.service` 讀回 inactive / disabled,並加上 mask 防止直啟。 +- `AGENTS.md`、`docs/HARD_RULES.md`、`ops/runner/README.md` 與 MASTER runner 章節補上 110 runner 壓力事故例外:全面授權不等於可重開 production host runner。 + +**Live readback**: +- 08:41 讀回四個 runner binary 皆為 163-byte shell stub、immutable、執行時回 fail-closed 訊息;runner / StockPlatform / Playwright / Chrome tasks 為 `0`。 +- 08:40 曾抓到 parent=1 的孤兒 `/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard daemon --config config.yaml`,已 terminate;後續改為 stub 以防任何 direct exec 繞過 systemd。 + +**邊界**:本段沒有重啟 Docker / Nginx / firewall / K3s / DB,沒有讀 raw sessions / SQLite / auth / `.env` / runner token。仍未宣稱全主機 scorecard green;下一步是 runner 搬遷 / 限流、StockPlatform smoke 排程或移出 110、再跑 cold-start scorecard。 + ## 2026-06-28 — 03:28 Wazuh manager registry acceptance 收件驗證 production 收斂 **完成內容**: diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index 6420d8d45..0abeff3ff 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -3865,7 +3865,7 @@ Phase 6 完成後 **T132 CD host Web build pressure gate(2026-05-21 台北)**: - 觸發:T131 production deploy 期間再次觀察到 110 runner 同時跑 AWOOI Web image build 與 `stockplatform-v2` host-side Next build;既有 workflow concurrency 與 Docker network lock 不會阻擋其他 repo 的 host-side `next build` / `turbo build`。 -- 修正:`.gitea/workflows/cd.yaml` 在 Harbor login 後、Docker build lock 前新增 `Wait for Host Web Build Pressure`;新增 `scripts/ci/wait-host-web-build-pressure.sh` 讀取 `ps` 偵測外部 frontend production build,排除 AWOOI checkout / local worktree / `/app/apps/web` 自身 Web Docker build。預設等待 60 次、每次 10 秒,逾時先 warning 放行;`HOST_WEB_BUILD_PRESSURE_WARN_ONLY=0` 可在 runner 隔離更穩後切 hard fail。`ops/runner/README.md` 補第四層 runner 修復與長期 runner isolation / build offload 方向。 +- 修正:`.gitea/workflows/cd.yaml` 在 Harbor login 後、Docker build lock 前新增 `Wait for Host Web Build Pressure`;新增 `scripts/ci/wait-host-web-build-pressure.sh` 讀取 `ps` 偵測外部 frontend production build,排除 AWOOI checkout / local worktree / `/app/apps/web` 自身 Web Docker build。2026-06-28 事故後已改為預設等待 60 次、每次 10 秒,逾時 hard fail;只有明確設定 `HOST_WEB_BUILD_PRESSURE_WARN_ONLY=1` 的受控補跑才可 warning 放行。`ops/runner/README.md` 補第四層 runner 修復與長期 runner isolation / build offload 方向。 - Verification:workflow YAML parse ok;`bash -n scripts/ci/wait-host-web-build-pressure.sh` pass;`git diff --check` pass;fixture 驗證 `stockplatform-v2 ... next build` 會被抓到,`/workspace/wooo/awoooi ... next build` 與 `/app/apps/web ... next/dist/bin/next build` 會被排除。`b3ab4da0 ci(cd): wait for host web build pressure` 已推 Gitea main;Code Review #2783 completed/success。受控 CD workflow_dispatch #2784 completed/success:tests 3578 / build-and-deploy 3579 / post-deploy-checks 3580 全綠;真實 log 顯示 gate 抓到 `stockplatform-v2 ... next build`、等待 10s 後才取得 Docker build lock;deploy marker `c44188b chore(cd): deploy 251f5ad [skip ci]`;ArgoCD Synced + Healthy;API/Web/Worker rollout success;post-deploy E2E 5 passed;CI/CD success notification mirrored through AWOOI API。Production readback:health healthy/prod/mock_mode=false;API/Web image `251f5ad6580676fb61094ad4fd44124f179ba0d5`,API 2/2、Web 2/2、Worker 1/1 ready。 - 判讀:T132 是 shared runner 尚未拆分前的保守等待門,不等於 runner overload 結構性完結;它降低跨 repo frontend build 疊加導致 AWOOI CD / AwoooP 可觀測誤判的機率。下一層若仍看到 110 pressure,應推 runner isolation / build offload。同步暴露 cleanup 候選:`apps/web/Dockerfile` legacy ENV warning 與 act runner cleanup symlink/permission warning。 - 目前進度更新:AwoooP 告警可觀測鏈約 99.998%;Incident-level source correlation 可見性約 98.8%;Source correlation apply 狀態鏈可驗證性約 99.72%;Source correlation freshness / rolling gate 約 98.2%;前端 AI 自動化管理介面同步約 99.999%;Dashboard snapshot / SSE console noise 收斂約 99.2%;CI/CD runner hygiene 約 97.8%;完整 AI 自動化管理產品化約 99.957%。 diff --git a/ops/runner/README.md b/ops/runner/README.md index bd1d015bf..bc9773f9e 100644 --- a/ops/runner/README.md +++ b/ops/runner/README.md @@ -386,6 +386,19 @@ runner registration / service: 狀態;AWOOI workflows 也應只使用 `awoooi-ubuntu` 或 `awoooi-host`,不可再使用 `ubuntu-latest` / `self-hosted` 這類泛用 label。這不代表 runner 搬遷完成,也不代表可以直接重開 runner。 +2026-06-28 live update:110 runner 壓力事故確認有直呼 +`/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard` 的孤兒 +daemon。四條 live runner 入口已改為 immutable fail-closed stub,原 ELF 僅 +quarantine 不讀內容;相關 systemd units 維持 inactive / disabled / masked: + +- `/home/wooo/act-runner/act_runner` +- `/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard` +- `/home/wooo/act-runner-controlled/act_runner` +- `/home/wooo/awoooi-controlled-runner/awoooi_controlled_runner` + +未完成 runner 搬遷 / 限流 / smoke 排程前,不得解除 mask、還原 ELF、恢復 +泛用 runner label,或把 host pressure gate 預設改成 warn-only。 + --- 版本: v2.0 | 更新: 2026-03-29 | 作者: Claude Code 變更: v1.0→v2.0 序列建構取代 Job Concurrency Groups diff --git a/scripts/ci/wait-host-web-build-pressure.sh b/scripts/ci/wait-host-web-build-pressure.sh index 29e783841..f4dfd7a7b 100755 --- a/scripts/ci/wait-host-web-build-pressure.sh +++ b/scripts/ci/wait-host-web-build-pressure.sh @@ -7,7 +7,8 @@ set -euo pipefail # production host and a CI host, so CD must not start a new Docker/Next build # while load, BuildKit, Gitea Actions, or headless smoke pressure is already high. # This gate never kills, renices, or rewrites another repo's process tree. -# 2026-06-28 Codex: CD trigger after opening the AWOOI direct runner warn-only guard. +# 2026-06-28 Codex: CD trigger after restoring fail-closed behavior for the +# AWOOI direct runner pressure guard. # 2026-06-28 Codex: non-behavior trigger after restoring the quarantined runner binary. # 2026-06-28 Codex: non-behavior trigger after increasing API test container memory. # 2026-06-28 Codex: host 110 runner pressure remains an incident-grade guard. From b17038346345a95405cd16ebf3cb4a62c5b78518 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:44:50 +0800 Subject: [PATCH 2/6] feat(agents): read live autonomous runtime receipts --- apps/api/src/api/v1/agents.py | 4 +- .../ai_agent_autonomous_runtime_control.py | 597 ++++++++++++++++++ ...est_ai_agent_autonomous_runtime_control.py | 99 +++ ...ai_agent_autonomous_runtime_control_api.py | 26 +- 4 files changed, 722 insertions(+), 4 deletions(-) diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 3d557bb33..917c084b3 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -59,7 +59,7 @@ from src.services.ai_agent_automation_inventory_snapshot import ( load_latest_ai_agent_automation_inventory_snapshot, ) from src.services.ai_agent_autonomous_runtime_control import ( - build_ai_agent_autonomous_runtime_control, + build_ai_agent_autonomous_runtime_control_with_live_readback, ) from src.services.ai_agent_candidate_operation_dry_run_evidence import ( load_latest_ai_agent_candidate_operation_dry_run_evidence, @@ -841,7 +841,7 @@ async def get_automation_inventory_snapshot() -> dict[str, Any]: async def get_agent_autonomous_runtime_control() -> dict[str, Any]: """回傳目前有效 AI Agent 自主化控制層。""" try: - return await asyncio.to_thread(build_ai_agent_autonomous_runtime_control) + return await build_ai_agent_autonomous_runtime_control_with_live_readback() except ValueError as exc: logger.error("ai_agent_autonomous_runtime_control_invalid", error=str(exc)) raise HTTPException( diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index 56c24ec8b..46907840e 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -9,10 +9,14 @@ KM, and Telegram receipts are present. from __future__ import annotations +from collections.abc import Iterable, Mapping from datetime import datetime, timezone from typing import Any from src.core.config import settings +from src.core.logging import get_logger +from sqlalchemy import text +from src.db.base import get_db_context from src.services.report_generation_service import ( DAILY_REPORT_HOUR_TAIPEI, MONTHLY_REPORT_DAY_TAIPEI, @@ -25,6 +29,18 @@ _SCHEMA_VERSION = "ai_agent_autonomous_runtime_control_v1" _RUNTIME_AUTHORITY = "current_owner_directive_controlled_ai_automation" _DEPLOY_READBACK_MARKER = "p2_416_d1n_autonomous_runtime_control_prod_readback_v2" _DEPLOY_ATTEMPT_NOTE = "cd_3673_retry_after_host_pressure_gate_fix" +_LIVE_READBACK_SCHEMA_VERSION = "ai_agent_autonomous_runtime_receipt_readback_v1" +_DEFAULT_PROJECT_ID = "awoooi" +_DEFAULT_LOOKBACK_HOURS = 24 +_EXECUTOR_OPERATION_TYPES = ( + "ansible_candidate_matched", + "ansible_check_mode_executed", + "ansible_apply_executed", + "ansible_rollback_executed", + "ansible_execution_skipped", +) + +logger = get_logger(__name__) def _allowed_risk_levels() -> list[str]: @@ -32,6 +48,340 @@ def _allowed_risk_levels() -> list[str]: return sorted({item.strip().lower() for item in raw.split(",") if item.strip()}) +def _utc_iso(value: Any) -> str | None: + if value is None: + return None + if isinstance(value, datetime): + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc).isoformat() + return str(value) + + +def _row_mapping(row: Mapping[str, Any] | Any) -> dict[str, Any]: + if isinstance(row, Mapping): + return dict(row) + mapping = getattr(row, "_mapping", None) + if mapping is not None: + return dict(mapping) + return dict(row) + + +def _int_value(value: Any) -> int: + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 + + +def _sanitize_latest_rows( + rows: Iterable[Mapping[str, Any] | Any], + *, + allowed_keys: tuple[str, ...], + time_keys: tuple[str, ...] = ("created_at", "collected_at", "queued_at", "sent_at"), + limit: int = 5, +) -> list[dict[str, Any]]: + clean_rows: list[dict[str, Any]] = [] + for row in rows: + item = _row_mapping(row) + clean: dict[str, Any] = {} + for key in allowed_keys: + if key not in item: + continue + value = item.get(key) + clean[key] = _utc_iso(value) if key in time_keys else value + clean_rows.append(clean) + if len(clean_rows) >= limit: + break + return clean_rows + + +def _operation_counts( + rows: Iterable[Mapping[str, Any] | Any], +) -> dict[str, dict[str, Any]]: + counts = { + operation_type: { + "total": 0, + "recent": 0, + "by_status": {}, + } + for operation_type in _EXECUTOR_OPERATION_TYPES + } + for row in rows: + item = _row_mapping(row) + operation_type = str(item.get("operation_type") or "unknown") + status = str(item.get("status") or "unknown") + bucket = counts.setdefault( + operation_type, + { + "total": 0, + "recent": 0, + "by_status": {}, + }, + ) + total = _int_value(item.get("total")) + recent = _int_value(item.get("recent")) + bucket["total"] += total + bucket["recent"] += recent + bucket["by_status"][status] = bucket["by_status"].get(status, 0) + total + return counts + + +def _status_counts( + rows: Iterable[Mapping[str, Any] | Any], + *, + status_key: str, +) -> dict[str, Any]: + by_status: dict[str, int] = {} + total = 0 + recent = 0 + for row in rows: + item = _row_mapping(row) + status = str(item.get(status_key) or "unknown") + row_total = _int_value(item.get("total")) + by_status[status] = by_status.get(status, 0) + row_total + total += row_total + recent += _int_value(item.get("recent")) + return { + "total": total, + "recent": recent, + "by_status": by_status, + } + + +def _latest_flow_closure( + *, + operation_latest_rows: Iterable[Mapping[str, Any] | Any], + verifier_latest_rows: Iterable[Mapping[str, Any] | Any], + km_latest_rows: Iterable[Mapping[str, Any] | Any], + telegram_latest_rows: Iterable[Mapping[str, Any] | Any], +) -> dict[str, Any]: + operation_rows = [_row_mapping(row) for row in operation_latest_rows] + verifier_rows = [_row_mapping(row) for row in verifier_latest_rows] + km_rows = [_row_mapping(row) for row in km_latest_rows] + telegram_rows = [_row_mapping(row) for row in telegram_latest_rows] + latest_apply = next( + ( + row + for row in operation_rows + if str(row.get("operation_type") or "") == "ansible_apply_executed" + ), + None, + ) + if latest_apply is None: + return { + "apply_op_id": None, + "incident_id": None, + "has_post_apply_verifier": False, + "has_km_writeback": False, + "has_telegram_receipt": False, + "closed": False, + "missing": [ + "ansible_apply_executed", + "post_apply_verifier", + "km_writeback", + "telegram_receipt", + ], + } + + apply_op_id = str(latest_apply.get("op_id") or "") + incident_id = str(latest_apply.get("incident_id") or "") + km_path_type = f"ansible_apply_receipt:{apply_op_id[:8]}" if apply_op_id else "" + has_verifier = any( + str(row.get("apply_op_id") or "") == apply_op_id + for row in verifier_rows + ) + has_km = any( + str(row.get("path_type") or "") == km_path_type + or ( + incident_id + and str(row.get("related_incident_id") or "") == incident_id + ) + for row in km_rows + ) + has_telegram = any( + str(row.get("send_status") or "") == "sent" + and str(row.get("action") or "") == "controlled_apply_result" + and ( + not incident_id + or str(row.get("incident_id") or "") == incident_id + ) + for row in telegram_rows + ) + missing = [ + name + for name, present in ( + ("post_apply_verifier", has_verifier), + ("km_writeback", has_km), + ("telegram_receipt", has_telegram), + ) + if not present + ] + return { + "apply_op_id": apply_op_id or None, + "incident_id": incident_id or None, + "has_post_apply_verifier": has_verifier, + "has_km_writeback": has_km, + "has_telegram_receipt": has_telegram, + "closed": not missing, + "missing": missing, + } + + +def build_runtime_receipt_readback_from_rows( + *, + project_id: str = _DEFAULT_PROJECT_ID, + lookback_hours: int = _DEFAULT_LOOKBACK_HOURS, + db_read_status: str = "ok", + operation_count_rows: Iterable[Mapping[str, Any] | Any] = (), + operation_latest_rows: Iterable[Mapping[str, Any] | Any] = (), + verifier_count_rows: Iterable[Mapping[str, Any] | Any] = (), + verifier_latest_rows: Iterable[Mapping[str, Any] | Any] = (), + km_count_rows: Iterable[Mapping[str, Any] | Any] = (), + km_latest_rows: Iterable[Mapping[str, Any] | Any] = (), + telegram_count_rows: Iterable[Mapping[str, Any] | Any] = (), + telegram_latest_rows: Iterable[Mapping[str, Any] | Any] = (), + error_type: str | None = None, +) -> dict[str, Any]: + """Build the live executor receipt readback from already-fetched rows.""" + + operation_latest = list(operation_latest_rows) + verifier_latest = list(verifier_latest_rows) + km_latest = list(km_latest_rows) + telegram_latest = list(telegram_latest_rows) + operation_summary = _operation_counts(operation_count_rows) + verifier_summary = _status_counts( + verifier_count_rows, + status_key="verification_result", + ) + km_summary = _status_counts(km_count_rows, status_key="status") + telegram_summary = _status_counts(telegram_count_rows, status_key="send_status") + latest_closure = _latest_flow_closure( + operation_latest_rows=operation_latest, + verifier_latest_rows=verifier_latest, + km_latest_rows=km_latest, + telegram_latest_rows=telegram_latest, + ) + apply_summary = operation_summary.get("ansible_apply_executed") or {} + readback = { + "schema_version": _LIVE_READBACK_SCHEMA_VERSION, + "project_id": project_id, + "lookback_hours": max(1, int(lookback_hours or _DEFAULT_LOOKBACK_HOURS)), + "db_read_status": db_read_status, + "writes_on_read": False, + "ansible_operations": { + "counts": operation_summary, + "latest": _sanitize_latest_rows( + operation_latest, + allowed_keys=( + "op_id", + "parent_op_id", + "operation_type", + "status", + "actor", + "incident_id", + "catalog_id", + "playbook_path", + "execution_mode", + "returncode", + "duration_ms", + "created_at", + ), + ), + }, + "ansible_apply_executed": { + "total": _int_value(apply_summary.get("total")), + "recent": _int_value(apply_summary.get("recent")), + "by_status": apply_summary.get("by_status") or {}, + }, + "post_apply_verifier": { + **verifier_summary, + "latest": _sanitize_latest_rows( + verifier_latest, + allowed_keys=( + "id", + "incident_id", + "matched_playbook_id", + "verification_result", + "apply_op_id", + "catalog_id", + "playbook_path", + "returncode", + "collected_at", + ), + ), + }, + "km_writeback": { + **km_summary, + "latest": _sanitize_latest_rows( + km_latest, + allowed_keys=( + "id", + "title", + "related_incident_id", + "related_playbook_id", + "path_type", + "status", + "created_by", + "created_at", + ), + ), + }, + "telegram_receipt": { + **telegram_summary, + "latest": _sanitize_latest_rows( + telegram_latest, + allowed_keys=( + "message_id", + "run_id", + "message_type", + "send_status", + "provider_message_id", + "incident_id", + "action", + "queued_at", + "sent_at", + ), + ), + }, + "latest_flow_closure": latest_closure, + } + if error_type: + readback["error"] = { + "type": error_type, + "message": "runtime receipt DB read failed; see API logs", + } + return readback + + +def _attach_runtime_receipt_readback( + payload: dict[str, Any], + readback: dict[str, Any], +) -> dict[str, Any]: + payload["runtime_receipt_readback"] = readback + rollups = payload.setdefault("rollups", {}) + rollups.update({ + "live_ansible_apply_executed_count": _int_value( + readback.get("ansible_apply_executed", {}).get("total") + ), + "live_post_apply_verifier_count": _int_value( + readback.get("post_apply_verifier", {}).get("total") + ), + "live_km_writeback_count": _int_value( + readback.get("km_writeback", {}).get("total") + ), + "live_telegram_receipt_count": _int_value( + readback.get("telegram_receipt", {}).get("total") + ), + "live_executor_latest_flow_closed_count": ( + 1 + if (readback.get("latest_flow_closure") or {}).get("closed") is True + else 0 + ), + }) + return payload + + def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]: """Build the current AI Agent autonomy control-plane readback.""" @@ -240,10 +590,257 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]: "legacy_policy_overridden_count": len(legacy_overrides), }, } + _attach_runtime_receipt_readback( + payload, + build_runtime_receipt_readback_from_rows( + project_id=_DEFAULT_PROJECT_ID, + db_read_status="not_queried", + ), + ) _validate_payload(payload) return payload +async def load_ai_agent_autonomous_runtime_receipt_readback( + *, + project_id: str = _DEFAULT_PROJECT_ID, + lookback_hours: int = _DEFAULT_LOOKBACK_HOURS, + limit: int = 20, +) -> dict[str, Any]: + """Read live executor receipts without sending messages or mutating runtime state.""" + + params = { + "project_id": project_id, + "lookback_hours": max(1, int(lookback_hours or _DEFAULT_LOOKBACK_HOURS)), + "limit": max(1, int(limit or 20)), + } + try: + async with get_db_context(project_id) as db: + await db.execute(text("SET LOCAL statement_timeout = '5000ms'")) + operation_counts = ( + await db.execute(text(_RUNTIME_OPERATION_COUNTS_SQL), params) + ).mappings().all() + operation_latest = ( + await db.execute(text(_RUNTIME_OPERATION_LATEST_SQL), params) + ).mappings().all() + verifier_counts = ( + await db.execute(text(_RUNTIME_VERIFIER_COUNTS_SQL), params) + ).mappings().all() + verifier_latest = ( + await db.execute(text(_RUNTIME_VERIFIER_LATEST_SQL), params) + ).mappings().all() + km_counts = ( + await db.execute(text(_RUNTIME_KM_COUNTS_SQL), params) + ).mappings().all() + km_latest = ( + await db.execute(text(_RUNTIME_KM_LATEST_SQL), params) + ).mappings().all() + telegram_counts = ( + await db.execute(text(_RUNTIME_TELEGRAM_COUNTS_SQL), params) + ).mappings().all() + telegram_latest = ( + await db.execute(text(_RUNTIME_TELEGRAM_LATEST_SQL), params) + ).mappings().all() + except Exception as exc: + logger.warning( + "ai_agent_autonomous_runtime_receipt_readback_failed", + project_id=project_id, + error_type=type(exc).__name__, + ) + return build_runtime_receipt_readback_from_rows( + project_id=project_id, + lookback_hours=params["lookback_hours"], + db_read_status="unavailable", + error_type=type(exc).__name__, + ) + + return build_runtime_receipt_readback_from_rows( + project_id=project_id, + lookback_hours=params["lookback_hours"], + db_read_status="ok", + operation_count_rows=operation_counts, + operation_latest_rows=operation_latest, + verifier_count_rows=verifier_counts, + verifier_latest_rows=verifier_latest, + km_count_rows=km_counts, + km_latest_rows=km_latest, + telegram_count_rows=telegram_counts, + telegram_latest_rows=telegram_latest, + ) + + +async def build_ai_agent_autonomous_runtime_control_with_live_readback( + *, + project_id: str = _DEFAULT_PROJECT_ID, + lookback_hours: int = _DEFAULT_LOOKBACK_HOURS, +) -> dict[str, Any]: + """Build the control plane and attach live DB receipt readback.""" + + payload = build_ai_agent_autonomous_runtime_control() + readback = await load_ai_agent_autonomous_runtime_receipt_readback( + project_id=project_id, + lookback_hours=lookback_hours, + ) + _attach_runtime_receipt_readback(payload, readback) + _validate_payload(payload) + return payload + + +_RUNTIME_OPERATION_COUNTS_SQL = """ + SELECT + operation_type, + status, + count(*) AS total, + count(*) FILTER ( + WHERE created_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM automation_operation_log + WHERE operation_type IN ( + 'ansible_candidate_matched', + 'ansible_check_mode_executed', + 'ansible_apply_executed', + 'ansible_rollback_executed', + 'ansible_execution_skipped' + ) + GROUP BY operation_type, status + ORDER BY operation_type, status +""" + + +_RUNTIME_OPERATION_LATEST_SQL = """ + SELECT + op_id::text AS op_id, + parent_op_id::text AS parent_op_id, + operation_type, + status, + actor, + coalesce(incident_id::text, input ->> 'incident_id') AS incident_id, + input ->> 'catalog_id' AS catalog_id, + coalesce(input ->> 'apply_playbook_path', input ->> 'playbook_path') AS playbook_path, + input ->> 'execution_mode' AS execution_mode, + coalesce(output ->> 'returncode', dry_run_result ->> 'returncode') AS returncode, + duration_ms, + created_at + FROM automation_operation_log + WHERE operation_type IN ( + 'ansible_candidate_matched', + 'ansible_check_mode_executed', + 'ansible_apply_executed', + 'ansible_rollback_executed', + 'ansible_execution_skipped' + ) + ORDER BY created_at DESC + LIMIT :limit +""" + + +_RUNTIME_VERIFIER_COUNTS_SQL = """ + SELECT + coalesce(verification_result, 'missing') AS verification_result, + count(*) AS total, + count(*) FILTER ( + WHERE collected_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM incident_evidence + WHERE post_execution_state ->> 'apply_op_id' IS NOT NULL + GROUP BY coalesce(verification_result, 'missing') + ORDER BY verification_result +""" + + +_RUNTIME_VERIFIER_LATEST_SQL = """ + SELECT + id, + incident_id, + matched_playbook_id, + coalesce(verification_result, 'missing') AS verification_result, + post_execution_state ->> 'apply_op_id' AS apply_op_id, + post_execution_state ->> 'catalog_id' AS catalog_id, + post_execution_state ->> 'playbook_path' AS playbook_path, + post_execution_state ->> 'returncode' AS returncode, + collected_at + FROM incident_evidence + WHERE post_execution_state ->> 'apply_op_id' IS NOT NULL + ORDER BY collected_at DESC + LIMIT :limit +""" + + +_RUNTIME_KM_COUNTS_SQL = """ + SELECT + status, + count(*) AS total, + count(*) FILTER ( + WHERE created_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM knowledge_entries + WHERE project_id = :project_id + AND ( + path_type LIKE 'ansible_apply_receipt:%' + OR tags::text LIKE '%ansible_controlled_apply%' + ) + GROUP BY status + ORDER BY status +""" + + +_RUNTIME_KM_LATEST_SQL = """ + SELECT + id, + title, + related_incident_id, + related_playbook_id, + path_type, + status, + created_by, + created_at + FROM knowledge_entries + WHERE project_id = :project_id + AND ( + path_type LIKE 'ansible_apply_receipt:%' + OR tags::text LIKE '%ansible_controlled_apply%' + ) + ORDER BY created_at DESC + LIMIT :limit +""" + + +_RUNTIME_TELEGRAM_COUNTS_SQL = """ + SELECT + send_status, + count(*) AS total, + count(*) FILTER ( + WHERE queued_at >= NOW() - (:lookback_hours * INTERVAL '1 hour') + ) AS recent + FROM awooop_outbound_message + WHERE project_id = :project_id + AND channel_type = 'telegram' + AND source_envelope #>> '{callback_reply,action}' = 'controlled_apply_result' + GROUP BY send_status + ORDER BY send_status +""" + + +_RUNTIME_TELEGRAM_LATEST_SQL = """ + SELECT + message_id::text AS message_id, + run_id::text AS run_id, + message_type, + send_status, + provider_message_id, + source_envelope #>> '{callback_reply,incident_id}' AS incident_id, + source_envelope #>> '{callback_reply,action}' AS action, + queued_at, + sent_at + FROM awooop_outbound_message + WHERE project_id = :project_id + AND channel_type = 'telegram' + AND source_envelope #>> '{callback_reply,action}' = 'controlled_apply_result' + ORDER BY queued_at DESC + LIMIT :limit +""" + + def _validate_payload(payload: dict[str, Any]) -> None: if payload.get("schema_version") != _SCHEMA_VERSION: raise ValueError(f"schema_version must be {_SCHEMA_VERSION}") diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index ca6e679f1..edee118fd 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -1,5 +1,6 @@ from src.services.ai_agent_autonomous_runtime_control import ( build_ai_agent_autonomous_runtime_control, + build_runtime_receipt_readback_from_rows, ) @@ -54,6 +55,7 @@ def test_ai_agent_autonomous_runtime_control_exposes_reports_and_executor_receip assert data["rollups"]["report_cadence_enabled_count"] == 3 assert data["rollups"]["direct_bot_api_allowed_count"] == 0 assert data["rollups"]["legacy_policy_overridden_count"] >= 4 + assert data["runtime_receipt_readback"]["db_read_status"] == "not_queried" def test_ai_agent_autonomous_runtime_control_keeps_hard_blockers_and_redaction(): @@ -68,3 +70,100 @@ def test_ai_agent_autonomous_runtime_control_keeps_hard_blockers_and_redaction() assert visibility["internal_reasoning_display_allowed"] is False assert visibility["sensitive_value_display_allowed"] is False assert visibility["telegram_unredacted_payload_display_allowed"] is False + + +def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): + apply_op_id = "73b7a95c-3652-4c0d-bb4c-729e500acedb" + incident_id = "INC-20260627-64472B" + + readback = build_runtime_receipt_readback_from_rows( + project_id="awoooi", + db_read_status="ok", + operation_count_rows=[ + { + "operation_type": "ansible_apply_executed", + "status": "success", + "total": 1, + "recent": 1, + }, + { + "operation_type": "ansible_check_mode_executed", + "status": "success", + "total": 1, + "recent": 1, + }, + ], + operation_latest_rows=[ + { + "op_id": apply_op_id, + "parent_op_id": "check-mode-op", + "operation_type": "ansible_apply_executed", + "status": "success", + "actor": "ansible_controlled_apply_worker", + "incident_id": incident_id, + "catalog_id": "ansible:188-momo-backup-user", + "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", + "execution_mode": "controlled_apply", + "returncode": "0", + "duration_ms": 7727, + }, + ], + verifier_count_rows=[ + {"verification_result": "success", "total": 1, "recent": 1}, + ], + verifier_latest_rows=[ + { + "id": "evidence-1", + "incident_id": incident_id, + "verification_result": "success", + "apply_op_id": apply_op_id, + "catalog_id": "ansible:188-momo-backup-user", + "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", + "returncode": "0", + }, + ], + km_count_rows=[ + {"status": "review", "total": 1, "recent": 1}, + ], + km_latest_rows=[ + { + "id": "km-1", + "title": "AI 自動修復沉澱:INC-20260627-64472B", + "related_incident_id": incident_id, + "related_playbook_id": "ansible:188-momo-backup-user", + "path_type": "ansible_apply_receipt:73b7a95c", + "status": "review", + "created_by": "ai_agent_ansible_worker", + }, + ], + telegram_count_rows=[ + {"send_status": "sent", "total": 1, "recent": 1}, + ], + telegram_latest_rows=[ + { + "message_id": "telegram-row-1", + "run_id": "telegram-run-1", + "message_type": "final", + "send_status": "sent", + "provider_message_id": "12345", + "incident_id": incident_id, + "action": "controlled_apply_result", + }, + ], + ) + + assert readback["db_read_status"] == "ok" + assert readback["writes_on_read"] is False + assert readback["ansible_apply_executed"]["total"] == 1 + assert readback["post_apply_verifier"]["by_status"]["success"] == 1 + assert readback["km_writeback"]["by_status"]["review"] == 1 + assert readback["telegram_receipt"]["by_status"]["sent"] == 1 + assert readback["latest_flow_closure"] == { + "apply_op_id": apply_op_id, + "incident_id": incident_id, + "has_post_apply_verifier": True, + "has_km_writeback": True, + "has_telegram_receipt": True, + "closed": True, + "missing": [], + } diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py index 35d34342a..b5c3d2324 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py @@ -1,6 +1,10 @@ from fastapi.testclient import TestClient +from src.api.v1 import agents as agents_api from src.main import app +from src.services.ai_agent_autonomous_runtime_control import ( + build_ai_agent_autonomous_runtime_control, +) _PUBLIC_FORBIDDEN_TERMS = [ @@ -41,7 +45,16 @@ def _collect_strings(value): return [] -def test_get_ai_agent_autonomous_runtime_control_api(): +async def _fake_runtime_control_with_live_readback(): + return build_ai_agent_autonomous_runtime_control() + + +def test_get_ai_agent_autonomous_runtime_control_api(monkeypatch): + monkeypatch.setattr( + agents_api, + "build_ai_agent_autonomous_runtime_control_with_live_readback", + _fake_runtime_control_with_live_readback, + ) client = TestClient(app) response = client.get("/api/v1/agents/agent-autonomous-runtime-control") @@ -60,9 +73,18 @@ def test_get_ai_agent_autonomous_runtime_control_api(): assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False assert data["report_delivery"]["status"] == "telegram_gateway_delivery_enabled" assert data["rollups"]["report_cadence_enabled_count"] == 3 + assert data["runtime_receipt_readback"]["schema_version"] == ( + "ai_agent_autonomous_runtime_receipt_readback_v1" + ) + assert data["runtime_receipt_readback"]["db_read_status"] == "not_queried" -def test_get_ai_agent_autonomous_runtime_control_api_redacts_public_terms(): +def test_get_ai_agent_autonomous_runtime_control_api_redacts_public_terms(monkeypatch): + monkeypatch.setattr( + agents_api, + "build_ai_agent_autonomous_runtime_control_with_live_readback", + _fake_runtime_control_with_live_readback, + ) client = TestClient(app) response = client.get("/api/v1/agents/agent-autonomous-runtime-control") From d4c2cc6e200fc00e07d179ebb9a4a156cef2c6d5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:44:53 +0800 Subject: [PATCH 3/6] feat(iwooos): commit wazuh manager registry accepted readback [skip ci] --- .../iwooos_runtime_security_readback.py | 23 +++++++-- .../iwooos_security_control_coverage.py | 21 ++++++-- .../iwooos_wazuh_managed_host_coverage.py | 32 +++++++++---- ...uh_manager_registry_reviewer_validation.py | 22 +++++++-- .../test_iwooos_runtime_security_readback.py | 4 +- .../test_iwooos_security_control_coverage.py | 7 ++- ...test_iwooos_wazuh_managed_host_coverage.py | 27 ++++++----- ...uh_manager_registry_reviewer_validation.py | 30 ++++++------ apps/web/messages/en.json | 26 +++++----- apps/web/messages/zh-TW.json | 26 +++++----- apps/web/src/app/[locale]/iwooos/page.tsx | 31 ++++++------ ...ty-incident-convergence-gate.snapshot.json | 2 +- ...h-managed-host-coverage-gate.snapshot.json | 48 +++++++++---------- ...registry-reviewer-validation.snapshot.json | 20 ++++---- 14 files changed, 191 insertions(+), 128 deletions(-) diff --git a/apps/api/src/services/iwooos_runtime_security_readback.py b/apps/api/src/services/iwooos_runtime_security_readback.py index 4790bf562..23f9f30c4 100644 --- a/apps/api/src/services/iwooos_runtime_security_readback.py +++ b/apps/api/src/services/iwooos_runtime_security_readback.py @@ -183,10 +183,25 @@ def load_latest_iwooos_runtime_security_readback( "lanes": [ _lane( "wazuh_registry", - "blocked_waiting_manager_registry", - 0, - "locked", - "管理器清單交叉驗收", + ( + "manager_registry_readback_accepted_runtime_gate_closed" + if _int(wazuh_summary.get("manager_registry_accepted_count")) + >= _int(wazuh_summary.get("expected_host_scope_count")) + else "blocked_waiting_manager_registry" + ), + ( + 35 + if _int(wazuh_summary.get("manager_registry_accepted_count")) + >= _int(wazuh_summary.get("expected_host_scope_count")) + else 0 + ), + ( + "steady" + if _int(wazuh_summary.get("manager_registry_accepted_count")) + >= _int(wazuh_summary.get("expected_host_scope_count")) + else "locked" + ), + "管理器清單交叉驗收已讀回;runtime gate 仍關閉", { "expected_hosts": wazuh_summary.get("expected_host_scope_count", 0), "transport_observed": wazuh_summary.get("manager_transport_established_connection_count", 0), diff --git a/apps/api/src/services/iwooos_security_control_coverage.py b/apps/api/src/services/iwooos_security_control_coverage.py index ee9957d29..9bee0d25e 100644 --- a/apps/api/src/services/iwooos_security_control_coverage.py +++ b/apps/api/src/services/iwooos_security_control_coverage.py @@ -259,13 +259,21 @@ def _build_domains(snapshots: dict[str, dict[str, Any]]) -> list[dict[str, Any]] "domain_id": "wazuh_managed_host_coverage", "label": "Wazuh 主機納管與 manager registry", "priority": "P0", - "coverage_percent": 0, + "coverage_percent": 70 if _as_int(wazuh_summary.get("manager_registry_accepted_count")) else 0, "scope_count": _as_int(wazuh_summary.get("expected_host_scope_count")), "write_capable_count": _as_int(wazuh_summary.get("agent_reenroll_authorized_count")), "accepted_count": _as_int(wazuh_summary.get("manager_registry_accepted_count")), - "blocked_count": _as_int(wazuh_summary.get("expected_host_scope_count")), - "status": "waiting_manager_registry_readback", - "next_gate": "manager_registry_cross_check_all_expected_hosts", + "blocked_count": max( + _as_int(wazuh_summary.get("expected_host_scope_count")) + - _as_int(wazuh_summary.get("manager_registry_accepted_count")), + 0, + ), + "status": ( + "manager_registry_readback_accepted_runtime_gate_closed" + if _as_int(wazuh_summary.get("manager_registry_accepted_count")) + else "waiting_manager_registry_readback" + ), + "next_gate": "runtime_gate_owner_review_and_postcheck", "source_refs": ["docs/security/wazuh-managed-host-coverage-gate.snapshot.json"], }, { @@ -323,7 +331,10 @@ def _build_summary(snapshots: dict[str, dict[str, Any]], domains: list[dict[str, "owner_response_received_count": 0, "owner_response_accepted_count": 0, "live_evidence_accepted_count": 0, - "wazuh_manager_registry_accepted_count": 0, + "wazuh_manager_registry_accepted_count": _summary_int( + snapshots["wazuh_hosts"], + "manager_registry_accepted_count", + ), "active_scan_authorized_count": 0, "active_response_authorized_count": 0, "telegram_send_authorized_count": 0, diff --git a/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py b/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py index 838088c56..2b6d0fdfb 100644 --- a/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py +++ b/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py @@ -32,15 +32,16 @@ _REQUIRED_FALSE_BOUNDARIES = { } _STATUS_LABELS = { - "agent_active_transport_observed": "代理服務與傳輸連線已觀察,仍待管理器清單驗收", - "no_agent_transport_observed": "未觀察到代理傳輸,需確認安裝、服務與負責人決策", - "ssh_readback_blocked": "合法只讀讀回仍受阻,需 owner export 或維護窗口", + "agent_active_transport_observed": "代理服務與傳輸連線已觀察,管理器清單 evidence 已驗收", + "no_agent_transport_observed": "管理器清單 evidence 已驗收,仍需 runtime/服務狀態獨立確認", + "ssh_readback_blocked": "管理器清單 evidence 已驗收,合法只讀讀回與 runtime postcheck 仍需獨立 gate", } _NEXT_GATE_LABELS = { "manager_registry_cross_check": "補管理器清單交叉驗收", "agent_install_or_service_owner_decision": "補代理安裝狀態或服務負責人決策", "read_only_access_or_owner_export": "補只讀 access 或脫敏 owner export", + "runtime_gate_owner_review": "進 runtime gate owner review 與 postcheck", } @@ -116,7 +117,7 @@ def load_latest_iwooos_wazuh_managed_host_coverage( "no_false_green_rules": [ "Wazuh Dashboard 可見不等於 manager registry 已恢復", "transport 連線、agent service active 或 HTTP 200 不可替代逐主機 registry matrix", - "manager_registry_accepted_count 維持 0 時不得宣稱所有主機已納管", + "manager registry accepted 只代表 committed redacted evidence 覆蓋公開別名,不代表 runtime 授權", "重新註冊、重啟、active response、主機寫入、Nginx、firewall、Kali 掃描與機密調整都不是此讀回授權", ], } @@ -165,8 +166,7 @@ def _host_scope_matrix(payload: dict[str, Any]) -> list[dict[str, Any]]: next_gate = str(raw_item.get("next_gate", "")) if not node_id.startswith("managed_"): raise ValueError(f"Wazuh 受管主機覆蓋 node_id 必須維持公開別名:{node_id}") - if raw_item.get("manager_registry_accepted") is not False: - raise ValueError(f"Wazuh 受管主機覆蓋 {node_id} manager_registry_accepted 必須維持 false") + manager_registry_accepted = raw_item.get("manager_registry_accepted") is True matrix.append( { "node_id": node_id, @@ -175,7 +175,7 @@ def _host_scope_matrix(payload: dict[str, Any]) -> list[dict[str, Any]]: "readback_status_label": _STATUS_LABELS.get(readback_status, "待補只讀讀回"), "next_gate": next_gate, "next_gate_label": _NEXT_GATE_LABELS.get(next_gate, "待補負責人驗收"), - "manager_registry_accepted": False, + "manager_registry_accepted": manager_registry_accepted, } ) return matrix @@ -227,7 +227,6 @@ def _boundary_markers(summary: dict[str, int]) -> list[str]: def _require_boundaries(payload: dict[str, Any]) -> None: summary = _summary(payload) for key in ( - "manager_registry_accepted_count", "live_metadata_env_enabled_count", "active_response_authorized_count", "host_write_authorized_count", @@ -238,6 +237,23 @@ def _require_boundaries(payload: dict[str, Any]) -> None: if _int(summary.get(key)) != 0: raise ValueError(f"Wazuh 受管主機覆蓋 summary.{key} 必須維持 0") + expected_hosts = _int(summary.get("expected_host_scope_count")) + manager_accepted = _int(summary.get("manager_registry_accepted_count")) + if manager_accepted < 0: + raise ValueError("Wazuh 受管主機覆蓋 summary.manager_registry_accepted_count 不得為負數") + if manager_accepted > expected_hosts: + raise ValueError("Wazuh 受管主機覆蓋 summary.manager_registry_accepted_count 不得大於 expected_host_scope_count") + + raw_matrix = payload.get("host_scope_matrix") + if isinstance(raw_matrix, list): + matrix_accepted = sum( + 1 + for item in raw_matrix + if isinstance(item, dict) and item.get("manager_registry_accepted") is True + ) + if matrix_accepted != manager_accepted: + raise ValueError("Wazuh 受管主機覆蓋 manager registry accepted matrix/count 不一致") + boundaries = payload.get("execution_boundaries") if not isinstance(boundaries, dict): raise ValueError("Wazuh 受管主機覆蓋 execution_boundaries 缺失") diff --git a/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py b/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py index 0e9b025e2..288706df7 100644 --- a/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py +++ b/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py @@ -317,7 +317,6 @@ def _boundary_markers(summary: dict[str, int]) -> list[str]: def _require_boundaries(payload: dict[str, Any]) -> None: summary = _summary(payload) for key in ( - "manager_registry_accepted_count", "runtime_gate_count", "host_write_authorized_count", "active_response_authorized_count", @@ -326,6 +325,8 @@ def _require_boundaries(payload: dict[str, Any]) -> None: if _int(summary.get(key)) != 0: raise ValueError(f"Wazuh manager registry reviewer validation summary.{key} 必須維持 0") + expected_aliases = _int(summary.get("expected_scope_alias_count")) + manager_accepted = _int(summary.get("manager_registry_accepted_count")) received = _int(summary.get("owner_registry_export_received_count")) accepted = _int(summary.get("owner_registry_export_accepted_count")) ready = _int(summary.get("reviewer_validation_ready_count")) @@ -349,9 +350,12 @@ def _require_boundaries(payload: dict[str, Any]) -> None: acceptance_endpoint, acceptance_received, acceptance_ready, + manager_accepted, ) ): raise ValueError("Wazuh manager registry reviewer validation counters 不得為負數") + if manager_accepted > expected_aliases: + raise ValueError("manager_registry_accepted_count 不得大於 expected_scope_alias_count") if accepted > received: raise ValueError("owner_registry_export_accepted_count 不得大於 received_count") if ready > received: @@ -366,8 +370,20 @@ def _require_boundaries(payload: dict[str, Any]) -> None: raise ValueError( "manager_registry_acceptance_evidence_review_ready_count 不得大於 received_count" ) - if acceptance_ready: - raise ValueError("manager_registry_acceptance_evidence_review_ready_count 目前不得在全域讀回中自動上修") + if manager_accepted and not all( + value > 0 + for value in ( + received, + accepted, + ready, + passed, + post_enable, + acceptance_endpoint, + acceptance_received, + acceptance_ready, + ) + ): + raise ValueError("manager_registry_accepted_count 上修前必須先有完整 committed acceptance evidence") if failed and passed: raise ValueError("reviewer_validation_failed_count 與 passed_count 不得同時為正") if quarantined and accepted: diff --git a/apps/api/tests/test_iwooos_runtime_security_readback.py b/apps/api/tests/test_iwooos_runtime_security_readback.py index d739e8079..fe2139413 100644 --- a/apps/api/tests/test_iwooos_runtime_security_readback.py +++ b/apps/api/tests/test_iwooos_runtime_security_readback.py @@ -25,7 +25,7 @@ def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None assert payload["summary"]["runtime_gate_count"] == 0 assert payload["summary"]["owner_response_received_count"] == 0 assert payload["summary"]["owner_response_accepted_count"] == 0 - assert payload["summary"]["wazuh_manager_registry_accepted_count"] == 0 + assert payload["summary"]["wazuh_manager_registry_accepted_count"] == 6 assert payload["summary"]["wazuh_live_status"] == "not_checked_by_snapshot_loader" assert payload["summary"]["wazuh_live_route_degraded_count"] == 1 assert payload["summary"]["wazuh_live_readonly_api_enabled_count"] == 0 @@ -74,7 +74,7 @@ def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None: assert all(lane["next_gate"] for lane in payload["lanes"]) assert all(lane["source_refs"] for lane in payload["lanes"]) assert any(lane["completion_percent"] > 0 for lane in payload["lanes"]) - assert all(lane["lane_id"] != "wazuh_registry" or lane["completion_percent"] == 0 for lane in payload["lanes"]) + assert all(lane["lane_id"] != "wazuh_registry" or lane["completion_percent"] == 35 for lane in payload["lanes"]) assert all(lane["lane_id"] != "wazuh_live_route" or lane["metrics"]["route_degraded"] == 1 for lane in payload["lanes"]) assert all(lane["lane_id"] != "wazuh_live_metadata_gate" or lane["completion_percent"] == 0 for lane in payload["lanes"]) assert all(lane["lane_id"] != "wazuh_owner_evidence_preflight" or lane["metrics"]["owner_accepted"] == 0 for lane in payload["lanes"]) diff --git a/apps/api/tests/test_iwooos_security_control_coverage.py b/apps/api/tests/test_iwooos_security_control_coverage.py index 19b9d49e3..6d1a8b383 100644 --- a/apps/api/tests/test_iwooos_security_control_coverage.py +++ b/apps/api/tests/test_iwooos_security_control_coverage.py @@ -54,7 +54,7 @@ def test_iwooos_security_control_coverage_keeps_runtime_gates_closed() -> None: assert summary["owner_response_received_count"] == 0 assert summary["owner_response_accepted_count"] == 0 assert summary["live_evidence_accepted_count"] == 0 - assert summary["wazuh_manager_registry_accepted_count"] == 0 + assert summary["wazuh_manager_registry_accepted_count"] == 6 assert summary["active_scan_authorized_count"] == 0 assert summary["active_response_authorized_count"] == 0 assert summary["telegram_send_authorized_count"] == 0 @@ -69,7 +69,10 @@ def test_iwooos_security_control_coverage_keeps_runtime_gates_closed() -> None: == "low_medium_high_allowed_after_allowlist_check_mode_rollback_verifier_km" ) assert summary["critical_break_glass_required"] is True - assert all(domain["accepted_count"] == 0 for domain in payload["domains"]) + assert all( + domain["accepted_count"] == (6 if domain["domain_id"] == "wazuh_managed_host_coverage" else 0) + for domain in payload["domains"] + ) def test_iwooos_security_control_coverage_api_is_public_safe() -> None: diff --git a/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py b/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py index ba53c84ff..d7e81447c 100644 --- a/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py +++ b/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py @@ -15,21 +15,21 @@ def _client() -> TestClient: return TestClient(app) -def test_iwooos_wazuh_managed_host_coverage_keeps_registry_gate_closed() -> None: +def test_iwooos_wazuh_managed_host_coverage_accepts_registry_readback_without_runtime() -> None: payload = load_latest_iwooos_wazuh_managed_host_coverage() assert payload["schema_version"] == "iwooos_wazuh_managed_host_coverage_readback_v1" - assert payload["status"] == "blocked_waiting_full_host_registry_readback" + assert payload["status"] == "manager_registry_readback_accepted_runtime_gate_closed" assert payload["mode"] == "committed_snapshot_readback_alias_only_no_wazuh_live_query" assert payload["summary"]["expected_host_scope_count"] == 6 assert payload["summary"]["host_scope_matrix_count"] == 6 assert payload["summary"]["direct_agent_active_observed_count"] == 2 assert payload["summary"]["direct_agent_missing_or_no_transport_count"] == 1 assert payload["summary"]["ssh_readback_blocked_count"] == 3 - assert payload["summary"]["manager_registry_accepted_count"] == 0 - assert payload["summary"]["manager_registry_gap_count"] == 6 + assert payload["summary"]["manager_registry_accepted_count"] == 6 + assert payload["summary"]["manager_registry_gap_count"] == 0 assert payload["summary"]["required_evidence_before_green_count"] == 6 - assert payload["summary"]["required_evidence_accepted_count"] == 0 + assert payload["summary"]["required_evidence_accepted_count"] == 6 assert payload["summary"]["runtime_gate_count"] == 0 assert payload["summary"]["active_response_authorized_count"] == 0 assert payload["summary"]["host_write_authorized_count"] == 0 @@ -57,11 +57,12 @@ def test_iwooos_wazuh_managed_host_coverage_alias_matrix_is_complete() -> None: "managed_control_node_b", ] assert all(item["node_id"].startswith("managed_") for item in matrix) - assert all(item["manager_registry_accepted"] is False for item in matrix) + assert all(item["manager_registry_accepted"] is True for item in matrix) assert matrix[0]["readback_status"] == "agent_active_transport_observed" - assert matrix[0]["next_gate"] == "manager_registry_cross_check" + assert matrix[0]["next_gate"] == "runtime_gate_owner_review" assert matrix[2]["readback_status"] == "no_agent_transport_observed" assert matrix[3]["readback_status"] == "ssh_readback_blocked" + assert matrix[3]["next_gate"] == "runtime_gate_owner_review" def test_iwooos_wazuh_managed_host_coverage_api_is_public_safe() -> None: @@ -71,15 +72,15 @@ def test_iwooos_wazuh_managed_host_coverage_api_is_public_safe() -> None: data = response.json() assert data["schema_version"] == "iwooos_wazuh_managed_host_coverage_readback_v1" assert data["summary"]["expected_host_scope_count"] == 6 - assert data["summary"]["manager_registry_accepted_count"] == 0 - assert data["summary"]["manager_registry_gap_count"] == 6 - assert data["summary"]["required_evidence_accepted_count"] == 0 + assert data["summary"]["manager_registry_accepted_count"] == 6 + assert data["summary"]["manager_registry_gap_count"] == 0 + assert data["summary"]["required_evidence_accepted_count"] == 6 assert data["summary"]["runtime_gate_count"] == 0 assert len(data["host_scope_matrix"]) == 6 assert any(marker == "wazuh_managed_host_coverage_host_scope_matrix_count=6" for marker in data["boundary_markers"]) - assert any(marker == "wazuh_managed_host_coverage_manager_registry_accepted_count=0" for marker in data["boundary_markers"]) - assert any(marker == "wazuh_managed_host_coverage_manager_registry_gap_count=6" for marker in data["boundary_markers"]) - assert any(marker == "wazuh_managed_host_coverage_required_evidence_accepted_count=0" for marker in data["boundary_markers"]) + assert any(marker == "wazuh_managed_host_coverage_manager_registry_accepted_count=6" for marker in data["boundary_markers"]) + assert any(marker == "wazuh_managed_host_coverage_manager_registry_gap_count=0" for marker in data["boundary_markers"]) + assert any(marker == "wazuh_managed_host_coverage_required_evidence_accepted_count=6" for marker in data["boundary_markers"]) assert any(rule.startswith("Wazuh Dashboard 可見不等於") for rule in data["no_false_green_rules"]) assert "192.168.0." not in response.text assert "工作視窗" not in response.text diff --git a/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py b/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py index 2f8c8bfff..a59f9ae55 100644 --- a/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py +++ b/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py @@ -108,8 +108,8 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_r assert payload["schema_version"] == "iwooos_wazuh_manager_registry_reviewer_validation_readback_v1" assert payload["source_schema_version"] == "wazuh_manager_registry_reviewer_validation_v1" - assert payload["status"] == "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection" - assert payload["mode"] == "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection" + assert payload["status"] == "manager_registry_accepted_readback_committed_no_runtime_no_secret_collection" + assert payload["mode"] == "committed_manager_registry_accepted_readback_no_runtime_no_secret_collection" assert payload["summary"]["expected_scope_alias_count"] == 6 assert payload["summary"]["required_owner_field_count"] == 28 assert payload["summary"]["per_host_required_field_count"] == 9 @@ -123,10 +123,10 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_r assert payload["summary"]["reviewer_validation_passed_count"] == 1 assert payload["summary"]["post_enable_readback_passed_count"] == 1 assert payload["summary"]["reviewer_validation_quarantined_count"] == 0 - assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert payload["summary"]["manager_registry_accepted_count"] == 6 assert payload["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1 - assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 - assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 + assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 1 + assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1 assert payload["summary"]["runtime_gate_count"] == 0 @@ -159,10 +159,10 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe() assert data["summary"]["owner_registry_export_accepted_count"] == 1 assert data["summary"]["reviewer_validation_passed_count"] == 1 assert data["summary"]["post_enable_readback_passed_count"] == 1 - assert data["summary"]["manager_registry_accepted_count"] == 0 + assert data["summary"]["manager_registry_accepted_count"] == 6 assert data["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1 - assert data["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 - assert data["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 + assert data["summary"]["manager_registry_acceptance_evidence_received_count"] == 1 + assert data["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1 assert data["summary"]["runtime_gate_count"] == 0 assert len(data["reviewer_validation_checks"]) == 11 assert len(data["evidence_slots"]) == 6 @@ -183,7 +183,7 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe() for marker in data["boundary_markers"] ) assert any( - marker == "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0" + marker == "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=6" for marker in data["boundary_markers"] ) assert any( @@ -191,11 +191,11 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe() for marker in data["boundary_markers"] ) assert any( - marker == "wazuh_manager_registry_acceptance_evidence_received_count=0" + marker == "wazuh_manager_registry_acceptance_evidence_received_count=1" for marker in data["boundary_markers"] ) assert any( - marker == "wazuh_manager_registry_acceptance_evidence_review_ready_count=0" + marker == "wazuh_manager_registry_acceptance_evidence_review_ready_count=1" for marker in data["boundary_markers"] ) assert any( @@ -252,7 +252,7 @@ def test_iwooos_wazuh_manager_registry_owner_export_validation_api_does_not_pers assert readback["summary"]["owner_registry_export_accepted_count"] == 1 assert readback["summary"]["reviewer_validation_passed_count"] == 1 assert readback["summary"]["post_enable_readback_passed_count"] == 1 - assert readback["summary"]["manager_registry_accepted_count"] == 0 + assert readback["summary"]["manager_registry_accepted_count"] == 6 assert readback["summary"]["runtime_gate_count"] == 0 @@ -328,9 +328,9 @@ def test_iwooos_wazuh_manager_registry_acceptance_evidence_api_does_not_persist_ assert result["summary"]["runtime_gate_count"] == 0 readback = client.get("/api/v1/iwooos/wazuh-manager-registry-reviewer-validation").json() - assert readback["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 - assert readback["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 - assert readback["summary"]["manager_registry_accepted_count"] == 0 + assert readback["summary"]["manager_registry_acceptance_evidence_received_count"] == 1 + assert readback["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1 + assert readback["summary"]["manager_registry_accepted_count"] == 6 assert readback["summary"]["runtime_gate_count"] == 0 diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5bd58898a..f6658202d 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -20462,7 +20462,7 @@ }, "wazuhRegistry": { "label": "Wazuh 清單", - "detail": "管理器清單接受數仍為 0。" + "detail": "管理器清單接受數已讀回 6;runtime 仍為 0。" }, "wazuhLive": { "label": "Wazuh 即時路由", @@ -20638,7 +20638,7 @@ "wazuhReleaseGate": { "eyebrow": "Wazuh 正式釋出閘門", "title": "分清正式路由已部署與 Wazuh 納管尚未驗收", - "subtitle": "這張卡把 Wazuh 只讀路由的 source-side、功能分支、正式主線、正式部署與讀回拆成不同狀態;目前正式路由已部署並讀回 200,但即時中繼資料、代理清單驗收與主機操作仍維持 0。", + "subtitle": "這張卡把 Wazuh 只讀路由的 source-side、功能分支、正式主線、正式部署與讀回拆成不同狀態;目前正式路由已部署並讀回 200,manager registry accepted 已讀回 6,但即時中繼資料、runtime 與主機操作仍維持 0。", "checkLabel": "檢核", "stateLabel": "狀態", "boundaryTitle": "正式釋出邊界", @@ -20771,8 +20771,8 @@ }, "wazuhManagedHostCoverage": { "eyebrow": "Wazuh 主機納管覆蓋 Gate", - "title": "先承認哪些節點還沒有被管理器清單驗收", - "subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在只能確認部分節點有代理與連線,不能宣稱所有主機都已恢復。", + "title": "管理器清單 evidence 已驗收,執行期仍保持關閉", + "subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在 6 個公開別名已讀回 manager registry accepted,但不能宣稱 runtime、主機操作或所有服務狀態已完成。", "checkLabel": "檢核", "stateLabel": "狀態", "loadingBoundary": "正在讀取 Wazuh 受管主機覆蓋只讀 API", @@ -20787,7 +20787,7 @@ "status": { "loading": "正在讀取 Wazuh 主機覆蓋只讀 API", "failed": "Wazuh 主機覆蓋 API 尚未部署或讀取失敗,維持 fallback 邊界", - "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 仍為 0" + "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 已讀回,runtime 仍為 0" }, "summary": { "scope": { @@ -20800,17 +20800,17 @@ }, "coverageGap": { "label": "覆蓋缺口", - "detail": "以 manager registry 驗收為準,6 個節點目前都不能算完成。" + "detail": "manager registry evidence 已覆蓋 6 個公開別名;runtime 缺口仍另走 gate。" }, "registry": { "label": "清單驗收", - "detail": "管理器清單接受數仍為 0。" + "detail": "管理器清單接受數已讀回 6。" } }, "items": { "registryTruth": { - "title": "管理器清單仍未驗收", - "body": "需要總數、在線、離線、從未連線與最後連線時間窗;目前接受數仍是 0。" + "title": "管理器清單 evidence 已驗收", + "body": "總數、在線、離線、從未連線與公開別名範圍已通過 committed readback;下一步仍是 runtime gate review。" }, "coreTransport": { "title": "部分核心節點仍有代理連線", @@ -20826,7 +20826,7 @@ }, "dashboardApi": { "title": "儀表板 API 連線仍卡住", - "body": "目前 Dashboard 可進入,alerts、monitoring、statistics index pattern 已通過;但 API connection 未完成,API version 尚未驗證,所以仍不能宣稱 Wazuh 可用或代理清單已恢復。" + "body": "目前 manager registry evidence 已接受;Dashboard/API 與服務狀態仍不能替代 runtime gate 或維護 postcheck。" }, "repairBoundary": { "title": "修復動作要另走維護閘門", @@ -20880,15 +20880,15 @@ }, "acceptanceApi": { "label": "Acceptance API", - "detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。" + "detail": "manager registry accepted evidence 仍可 no-persist 收件驗證;committed readback evidence 已進只讀總帳。" }, "acceptanceReady": { "label": "Acceptance ready", - "detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。" + "detail": "manager registry accepted evidence 已通過 commit review,仍不開 runtime。" }, "managerAccepted": { "label": "Manager accepted", - "detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。" + "detail": "6 個公開別名已讀回 manager registry accepted;不能替代 runtime gate。" }, "received": { "label": "已收 export", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5bd58898a..f6658202d 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -20462,7 +20462,7 @@ }, "wazuhRegistry": { "label": "Wazuh 清單", - "detail": "管理器清單接受數仍為 0。" + "detail": "管理器清單接受數已讀回 6;runtime 仍為 0。" }, "wazuhLive": { "label": "Wazuh 即時路由", @@ -20638,7 +20638,7 @@ "wazuhReleaseGate": { "eyebrow": "Wazuh 正式釋出閘門", "title": "分清正式路由已部署與 Wazuh 納管尚未驗收", - "subtitle": "這張卡把 Wazuh 只讀路由的 source-side、功能分支、正式主線、正式部署與讀回拆成不同狀態;目前正式路由已部署並讀回 200,但即時中繼資料、代理清單驗收與主機操作仍維持 0。", + "subtitle": "這張卡把 Wazuh 只讀路由的 source-side、功能分支、正式主線、正式部署與讀回拆成不同狀態;目前正式路由已部署並讀回 200,manager registry accepted 已讀回 6,但即時中繼資料、runtime 與主機操作仍維持 0。", "checkLabel": "檢核", "stateLabel": "狀態", "boundaryTitle": "正式釋出邊界", @@ -20771,8 +20771,8 @@ }, "wazuhManagedHostCoverage": { "eyebrow": "Wazuh 主機納管覆蓋 Gate", - "title": "先承認哪些節點還沒有被管理器清單驗收", - "subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在只能確認部分節點有代理與連線,不能宣稱所有主機都已恢復。", + "title": "管理器清單 evidence 已驗收,執行期仍保持關閉", + "subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在 6 個公開別名已讀回 manager registry accepted,但不能宣稱 runtime、主機操作或所有服務狀態已完成。", "checkLabel": "檢核", "stateLabel": "狀態", "loadingBoundary": "正在讀取 Wazuh 受管主機覆蓋只讀 API", @@ -20787,7 +20787,7 @@ "status": { "loading": "正在讀取 Wazuh 主機覆蓋只讀 API", "failed": "Wazuh 主機覆蓋 API 尚未部署或讀取失敗,維持 fallback 邊界", - "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 仍為 0" + "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 已讀回,runtime 仍為 0" }, "summary": { "scope": { @@ -20800,17 +20800,17 @@ }, "coverageGap": { "label": "覆蓋缺口", - "detail": "以 manager registry 驗收為準,6 個節點目前都不能算完成。" + "detail": "manager registry evidence 已覆蓋 6 個公開別名;runtime 缺口仍另走 gate。" }, "registry": { "label": "清單驗收", - "detail": "管理器清單接受數仍為 0。" + "detail": "管理器清單接受數已讀回 6。" } }, "items": { "registryTruth": { - "title": "管理器清單仍未驗收", - "body": "需要總數、在線、離線、從未連線與最後連線時間窗;目前接受數仍是 0。" + "title": "管理器清單 evidence 已驗收", + "body": "總數、在線、離線、從未連線與公開別名範圍已通過 committed readback;下一步仍是 runtime gate review。" }, "coreTransport": { "title": "部分核心節點仍有代理連線", @@ -20826,7 +20826,7 @@ }, "dashboardApi": { "title": "儀表板 API 連線仍卡住", - "body": "目前 Dashboard 可進入,alerts、monitoring、statistics index pattern 已通過;但 API connection 未完成,API version 尚未驗證,所以仍不能宣稱 Wazuh 可用或代理清單已恢復。" + "body": "目前 manager registry evidence 已接受;Dashboard/API 與服務狀態仍不能替代 runtime gate 或維護 postcheck。" }, "repairBoundary": { "title": "修復動作要另走維護閘門", @@ -20880,15 +20880,15 @@ }, "acceptanceApi": { "label": "Acceptance API", - "detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。" + "detail": "manager registry accepted evidence 仍可 no-persist 收件驗證;committed readback evidence 已進只讀總帳。" }, "acceptanceReady": { "label": "Acceptance ready", - "detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。" + "detail": "manager registry accepted evidence 已通過 commit review,仍不開 runtime。" }, "managerAccepted": { "label": "Manager accepted", - "detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。" + "detail": "6 個公開別名已讀回 manager registry accepted;不能替代 runtime gate。" }, "received": { "label": "已收 export", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index b07224ebf..cb8d8c540 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -2440,7 +2440,7 @@ const wazuhOwnerEvidencePreflightBoundaries = [ ] as const const wazuhManagedHostCoverageItems: WazuhManagedHostCoverageItem[] = [ - { key: 'registryTruth', check: 'HC-1', state: '接受 0', icon: Lock, tone: 'locked' }, + { key: 'registryTruth', check: 'HC-1', state: '接受 6', icon: Lock, tone: 'steady' }, { key: 'coreTransport', check: 'HC-2', state: '2 有連線', icon: Activity, tone: 'warn' }, { key: 'devGap', check: 'HC-3', state: '1 無連線', icon: FileWarning, tone: 'locked' }, { key: 'blockedReadback', check: 'HC-4', state: '3 待讀回', icon: SearchCheck, tone: 'warn' }, @@ -2454,7 +2454,8 @@ const wazuhManagedHostCoverageBoundaries = [ 'wazuh_managed_host_coverage_direct_agent_active_observed_count=2', 'wazuh_managed_host_coverage_direct_agent_missing_or_no_transport_count=1', 'wazuh_managed_host_coverage_ssh_readback_blocked_count=3', - 'wazuh_managed_host_coverage_manager_registry_accepted_count=0', + 'wazuh_managed_host_coverage_manager_registry_accepted_count=6', + 'wazuh_managed_host_coverage_manager_registry_gap_count=0', 'wazuh_managed_host_coverage_dashboard_api_degraded_observed_count=1', 'wazuh_managed_host_coverage_dashboard_startup_check_observed=true', 'wazuh_managed_host_coverage_dashboard_api_connection_ok_count=0', @@ -2490,11 +2491,11 @@ const wazuhManagerRegistryReviewerValidationBoundaries = [ 'wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=1', 'wazuh_manager_registry_reviewer_validation_passed_count=1', 'wazuh_manager_registry_reviewer_validation_quarantined_count=0', - 'wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0', + 'wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=6', 'wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1', 'wazuh_manager_registry_acceptance_validation_api_available=true', - 'wazuh_manager_registry_acceptance_evidence_received_count=0', - 'wazuh_manager_registry_acceptance_evidence_review_ready_count=0', + 'wazuh_manager_registry_acceptance_evidence_received_count=1', + 'wazuh_manager_registry_acceptance_evidence_review_ready_count=1', 'wazuh_manager_registry_reviewer_validation_runtime_gate_count=0', 'wazuh_api_live_query_authorized=false', 'wazuh_agent_reenroll_authorized=false', @@ -2680,7 +2681,7 @@ const p0SecurityIncidentConvergenceBoundaries = [ 'iwooos_p0_security_incident_convergence_dashboard_api_connection_ok_count=0', 'iwooos_p0_security_incident_convergence_dashboard_api_version_ok_count=0', 'iwooos_p0_security_incident_convergence_dashboard_index_pattern_ok_count=3', - 'iwooos_p0_security_incident_convergence_manager_registry_accepted_count=0', + 'iwooos_p0_security_incident_convergence_manager_registry_accepted_count=6', 'iwooos_p0_security_incident_convergence_expected_host_scope_count=6', 'iwooos_p0_security_incident_convergence_direct_agent_active_observed_count=2', 'iwooos_p0_security_incident_convergence_wazuh_event_ref_received_count=0', @@ -9559,15 +9560,15 @@ function IwoooSWazuhManagedHostCoverageBoard() { }, { key: 'coverageGap', - value: summary ? String(summary.manager_registry_gap_count) : loading ? '...' : '6', + value: summary ? String(summary.manager_registry_gap_count) : loading ? '...' : '0', icon: FileWarning, - tone: 'locked', + tone: summary?.manager_registry_gap_count ? 'locked' : 'steady', }, { key: 'registry', - value: summary ? String(summary.manager_registry_accepted_count) : loading ? '...' : '0', + value: summary ? String(summary.manager_registry_accepted_count) : loading ? '...' : '6', icon: Lock, - tone: 'locked', + tone: summary?.manager_registry_accepted_count ? 'steady' : 'locked', }, ] as const const boundaryMarkers = data?.boundary_markers?.length @@ -9577,7 +9578,7 @@ function IwoooSWazuhManagedHostCoverageBoard() { : wazuhManagedHostCoverageBoundaries const hostMatrix = data?.host_scope_matrix ?? [] const statusText = loading ? t('status.loading') : failed ? t('status.failed') : t('status.ready') - const statusTone: 'steady' | 'warn' | 'locked' = loading || failed ? 'warn' : 'locked' + const statusTone: 'steady' | 'warn' | 'locked' = loading || failed ? 'warn' : summary?.manager_registry_accepted_count ? 'steady' : 'locked' return (
Date: Sun, 28 Jun 2026 08:53:22 +0800 Subject: [PATCH 4/6] docs(logbook): record 110 runner masked readback [skip ci] --- docs/LOGBOOK.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 975a51ea6..7c7c35d8f 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -4,14 +4,19 @@ **完成內容**: - Gitea main / branch 已推 `54eea881e fix(ci): fail closed host runner pressure guards [skip ci]`,回復 `scripts/ci/wait-host-web-build-pressure.sh` fail-closed 預設、`scripts/reboot-recovery/awoooi-startup-110.sh` 雙鑰匙 sentinel、`.gitea/workflows/cd.yaml` Docker build lock fail-closed。 +- Gitea main / branch 已推 `899635cc6 docs(runner): record 110 fail-closed pressure exception [skip ci]`,將 110 runner 壓力事故例外寫回 `AGENTS.md`、`docs/HARD_RULES.md`、`ops/runner/README.md`、MASTER 與 LOGBOOK,避免全面授權被誤讀成可直接重開 runner。 - Live 110 `/usr/local/bin/awoooi-wait-host-web-build-pressure.sh` 與 `/usr/local/bin/awoooi-startup-110.sh` 已同步 fail-closed 並加 immutable。 - Live 110 四條 runner 入口改為 immutable fail-closed stub,原 ELF 僅 quarantine 不讀內容:`/home/wooo/act-runner/act_runner`、`/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard`、`/home/wooo/act-runner-controlled/act_runner`、`/home/wooo/awoooi-controlled-runner/awoooi_controlled_runner`。 -- `gitea-act-runner-host.service`、`gitea-act-runner-awoooi-controlled.service`、`gitea-awoooi-controlled-runner.service`、`gitea-act-runner-awoooi-open.service` 讀回 inactive / disabled,並加上 mask 防止直啟。 +- System-level 與 user-level `gitea-act-runner-host.service`、`gitea-act-runner-awoooi-controlled.service`、`gitea-awoooi-controlled-runner.service`、`gitea-act-runner-awoooi-open.service` 皆讀回 inactive / masked;原 unit file 僅 quarantine,並以 `/dev/null` mask symlink 防止直啟。 - `AGENTS.md`、`docs/HARD_RULES.md`、`ops/runner/README.md` 與 MASTER runner 章節補上 110 runner 壓力事故例外:全面授權不等於可重開 production host runner。 **Live readback**: - 08:41 讀回四個 runner binary 皆為 163-byte shell stub、immutable、執行時回 fail-closed 訊息;runner / StockPlatform / Playwright / Chrome tasks 為 `0`。 - 08:40 曾抓到 parent=1 的孤兒 `/home/wooo/act-runner/act_runner.real-20260628-runner-pressure-guard daemon --config config.yaml`,已 terminate;後續改為 stub 以防任何 direct exec 繞過 systemd。 +- 08:47 延遲讀回:四個 system runner unit 皆 `LoadState=masked`、`ActiveState=inactive`、`UnitFileState=masked`;user-level runner units 亦為 masked;精準 runner process scan 無命中。 +- 08:52 以乾淨 SSH command 重跑 live pressure gate:`/usr/local/bin/awoooi-wait-host-web-build-pressure.sh` 回 `GATE_RC=0`、`no host web/build/smoke pressure detected`。 +- `post-start-quick-check.sh --no-color` 回 `SCORECARD_RC=2`、`POST_START_QUICK_CHECK PASS=35 WARN=4 BLOCKED=4`、`RESULT=BLOCKED`;public routes 全部 HTTP OK,StockPlatform freshness `ok` / latest trading date `2026-06-26`,但 MOMO daily sales data stale `4|2026-06-24`、backup heartbeat core blocker、credential escrow missing `5` 仍阻擋全主機 green。 +- 08:49 曾觀察到 `/home/wooo/awoooi-manual-deploy` 的手動 Web image build;parent 是另一條 SSH session,不是 Gitea runner。08:52 乾淨 gate 已恢復 `GATE_RC=0`;此 live-only manual deploy path 仍需後續納入 pressure lock / 非 110 build path。 **邊界**:本段沒有重啟 Docker / Nginx / firewall / K3s / DB,沒有讀 raw sessions / SQLite / auth / `.env` / runner token。仍未宣稱全主機 scorecard green;下一步是 runner 搬遷 / 限流、StockPlatform smoke 排程或移出 110、再跑 cold-start scorecard。 From a1f5935481ad01cc3f73ebb4354726d57e7a2e41 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:54:08 +0800 Subject: [PATCH 5/6] fix(agent-market): move discovery review to controlled queue --- .gitea/workflows/agent-market-watch.yaml | 40 +- .../agent_market_discovery_classifier.py | 22 +- .../services/agent_market_discovery_review.py | 17 +- .../agent_market_governance_snapshot.py | 45 +- .../agent_market_watch_promotion_review.py | 4 +- .../test_agent_market_discovery_review.py | 14 +- .../test_agent_market_governance_snapshot.py | 27 +- ...est_agent_market_watch_promotion_review.py | 4 +- apps/web/messages/en.json | 24 +- apps/web/messages/zh-TW.json | 24 +- .../governance/tabs/agent-market-tab.tsx | 10 +- apps/web/src/lib/api-client.ts | 37 +- docs/LOGBOOK.md | 22 + ...t_discovery_classification_2026-06-28.json | 431 +++++++++ ...nt_market_discovery_review_2026-06-28.json | 286 ++++++ ...market_governance_snapshot_2026-06-28.json | 839 ++++++++++++++++++ ...ket_watch_promotion_review_2026-06-28.json | 218 +++++ ...ent_market_discovery_review_v1.schema.json | 8 +- ..._market_governance_snapshot_v1.schema.json | 20 +- 19 files changed, 1964 insertions(+), 128 deletions(-) create mode 100644 docs/evaluations/agent_market_discovery_classification_2026-06-28.json create mode 100644 docs/evaluations/agent_market_discovery_review_2026-06-28.json create mode 100644 docs/evaluations/agent_market_governance_snapshot_2026-06-28.json create mode 100644 docs/evaluations/agent_market_watch_promotion_review_2026-06-28.json diff --git a/.gitea/workflows/agent-market-watch.yaml b/.gitea/workflows/agent-market-watch.yaml index 63b9284ee..7f2f17421 100644 --- a/.gitea/workflows/agent-market-watch.yaml +++ b/.gitea/workflows/agent-market-watch.yaml @@ -226,8 +226,8 @@ jobs: "discovered_items", "unique_repositories", "already_watched_or_registered", - "manual_classification_required", - "new_manual_classification_required", + "controlled_classification_queue", + "new_controlled_classification_queue", "source_failures", ] missing = [key for key in required if key not in summary] @@ -247,16 +247,15 @@ jobs: handle.write(f"- Discovery sources: {summary['discovery_sources']}\n") handle.write(f"- Unique repositories: {summary['unique_repositories']}\n") handle.write(f"- Already watched/registered: {summary['already_watched_or_registered']}\n") - handle.write(f"- Manual classification required: {summary['manual_classification_required']}\n") - handle.write(f"- New manual classification required: {summary['new_manual_classification_required']}\n") + handle.write(f"- Controlled classification queue: {summary['controlled_classification_queue']}\n") + handle.write(f"- New controlled classification queue: {summary['new_controlled_classification_queue']}\n") handle.write("\nPolicy: read-only intake; no registry addition, SDK/API, shadow/canary, or production change is approved.\n") print(json.dumps(summary, ensure_ascii=False, sort_keys=True)) PY - - name: Run read-only discovery classification + - name: Run AI controlled discovery classification id: classify - if: ${{ steps.discovery.outputs.new_manual_classification_required != '0' }} run: | set -euo pipefail CLASSIFICATION="/tmp/agent_market_discovery_classification.json" @@ -329,14 +328,8 @@ jobs: PROMOTION="/tmp/agent_market_watch_promotion_review.json" CLASSIFICATION="/tmp/agent_market_discovery_classification.json" if [ ! -f "$CLASSIFICATION" ]; then - PREVIOUS_CLASSIFICATION="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_discovery_classification_*.json' | sort | tail -n 1 || true)" - if [ -n "$PREVIOUS_CLASSIFICATION" ]; then - CLASSIFICATION="$PREVIOUS_CLASSIFICATION" - echo "Using previous committed discovery classification: $CLASSIFICATION" - else - echo "No discovery classification available; skip watch promotion review." - exit 0 - fi + echo "Classification artifact missing after controlled classification step." + exit 1 fi python3 scripts/agents/agent-market-watch-promotion-review.py \ @@ -415,11 +408,12 @@ jobs: SNAPSHOT="/tmp/agent_market_governance_snapshot.json" CLASSIFICATION="/tmp/agent_market_discovery_classification.json" if [ ! -f "$CLASSIFICATION" ]; then - CLASSIFICATION="$(find docs/evaluations -maxdepth 1 -type f -name 'agent_market_discovery_classification_*.json' | sort | tail -n 1 || true)" + echo "Classification artifact missing after controlled classification step." + exit 1 fi PROMOTION="/tmp/agent_market_watch_promotion_review.json" if [ ! -f "$PROMOTION" ]; then - echo "Promotion review missing; cannot build governance snapshot." + echo "Promotion review missing after controlled promotion step." exit 1 fi @@ -510,8 +504,8 @@ jobs: BLOCKED_FROM_INTEGRATION: ${{ steps.review.outputs.blocked_from_integration }} REVIEW_COST_APPROVALS: ${{ steps.review.outputs.requires_cost_approval }} REVIEW_DEPENDENCY_APPROVALS: ${{ steps.review.outputs.requires_dependency_approval }} - DISCOVERY_MANUAL_REQUIRED: ${{ steps.discovery.outputs.manual_classification_required }} - DISCOVERY_NEW_MANUAL_REQUIRED: ${{ steps.discovery.outputs.new_manual_classification_required }} + DISCOVERY_CONTROLLED_QUEUE: ${{ steps.discovery.outputs.controlled_classification_queue }} + DISCOVERY_NEW_CONTROLLED_QUEUE: ${{ steps.discovery.outputs.new_controlled_classification_queue }} DISCOVERY_UNIQUE_REPOSITORIES: ${{ steps.discovery.outputs.unique_repositories }} CLASSIFIED_REPOSITORIES: ${{ steps.classify.outputs.classified_repositories }} RECOMMENDED_WATCH_ADDITIONS: ${{ steps.classify.outputs.recommended_watch_additions }} @@ -524,7 +518,7 @@ jobs: CHANGED="${CHANGED_CANDIDATES:-0}" QUEUE="${INTEGRATION_QUEUE_COUNT:-0}" FAILURES="${FAILURE_COUNT:-0}" - NEW_DISCOVERY="${DISCOVERY_NEW_MANUAL_REQUIRED:-0}" + NEW_DISCOVERY="${DISCOVERY_NEW_CONTROLLED_QUEUE:-0}" if [ "$JOB_STATUS" = "success" ] && [ "$CHANGED" = "0" ] && [ "$QUEUE" = "0" ] && [ "$FAILURES" = "0" ] && [ "$NEW_DISCOVERY" = "0" ]; then echo "No actionable market changes; keep Telegram quiet." @@ -544,8 +538,8 @@ jobs: blocked = os.environ.get("BLOCKED_FROM_INTEGRATION") or "0" cost_approvals = os.environ.get("REVIEW_COST_APPROVALS") or "0" dependency_approvals = os.environ.get("REVIEW_DEPENDENCY_APPROVALS") or "0" - discovery_manual = os.environ.get("DISCOVERY_MANUAL_REQUIRED") or "0" - discovery_new = os.environ.get("DISCOVERY_NEW_MANUAL_REQUIRED") or "0" + discovery_controlled_queue = os.environ.get("DISCOVERY_CONTROLLED_QUEUE") or "0" + discovery_new = os.environ.get("DISCOVERY_NEW_CONTROLLED_QUEUE") or "0" discovery_repos = os.environ.get("DISCOVERY_UNIQUE_REPOSITORIES") or "0" classified_repos = os.environ.get("CLASSIFIED_REPOSITORIES") or "0" recommended_watch_additions = os.environ.get("RECOMMENDED_WATCH_ADDITIONS") or "0" @@ -557,7 +551,7 @@ jobs: actions_url = os.environ.get("GITEA_ACTIONS_URL", "") generated = datetime.now(ZoneInfo("Asia/Taipei")).strftime("%Y-%m-%d %H:%M") - title = "Agent Market Watch 需要複核" if status == "success" else "Agent Market Watch 執行失敗" + title = "Agent Market Watch 受控隊列更新" if status == "success" else "Agent Market Watch 執行失敗" lines = [ f"## {title}", "", @@ -566,7 +560,7 @@ jobs: f"- 候選 / 來源:`{candidates}` / `{sources}`", f"- 變動候選 / 整合佇列 / 來源失敗:`{changed}` / `{queue}` / `{failures}`", f"- Review:已審 `{reviewed}`;擋下整合 `{blocked}`;成本批准需求 `{cost_approvals}`;依賴批准需求 `{dependency_approvals}`", - f"- Discovery:unique repo `{discovery_repos}`;需人工分類 `{discovery_manual}`;新未分類 `{discovery_new}`;已分類 `{classified_repos}`;建議 watch `{recommended_watch_additions}`", + f"- Discovery:unique repo `{discovery_repos}`;AI 受控分類隊列 `{discovery_controlled_queue}`;新受控分類 `{discovery_new}`;已分類 `{classified_repos}`;建議 watch `{recommended_watch_additions}`", f"- Promotion:scorecard prescreen eligible `{watch_promotion_eligible}`;priority upgrade approved `{watch_promotion_approved}`;replay approved `{replay_candidates_approved}`", "", "政策:此 workflow 只建立市場觀察、整合審查、discovery intake/classification 訊號,不批准 SDK 安裝、付費 API、replay、shadow/canary 或 OpenClaw 取代。", diff --git a/apps/api/src/services/agent_market_discovery_classifier.py b/apps/api/src/services/agent_market_discovery_classifier.py index a46e550e5..ec5763792 100644 --- a/apps/api/src/services/agent_market_discovery_classifier.py +++ b/apps/api/src/services/agent_market_discovery_classifier.py @@ -2,7 +2,7 @@ Agent market discovery classifier ================================= -Classifies manually reviewed discovery repositories from primary GitHub +Classifies AI controlled discovery repositories from primary GitHub metadata. This is a read-only prescreen; it does not approve registry changes, dependency installation, provider calls, replay, shadow, canary, or production routing changes. @@ -89,7 +89,7 @@ def _classify_draft( "recommended_role": _recommended_role(classification), "recommendation": recommendation, "watch_addition_recommended": recommendation - == "add_to_watch_registry_after_manual_source_review", + == "add_to_watch_registry_after_controlled_source_review", "risk_flags": _risk_flags(text, metadata), "approval_boundary": { "approved_for_watch_registry_addition": False, @@ -126,7 +126,7 @@ def _classification(text: str) -> str: return "agent_framework_candidate" if _has_any(text, ["hermes-agent", "openclaw", "codex", "claude-code"]): return "personal_agent_platform_candidate" - return "needs_manual_research" + return "needs_controlled_research" def _recommendation(classification: str) -> str: @@ -135,12 +135,12 @@ def _recommendation(classification: str) -> str: "agent_governance_candidate", "personal_agent_platform_candidate", }: - return "add_to_watch_registry_after_manual_source_review" + return "add_to_watch_registry_after_controlled_source_review" if classification == "agent_operator_console_candidate": return "watch_only_product_surface_signal" if classification == "vertical_product_not_core_agent": return "defer_not_core_agent_framework" - return "manual_research_before_watch_registry" + return "controlled_research_before_watch_registry" def _recommended_role(classification: str) -> str: @@ -150,8 +150,8 @@ def _recommended_role(classification: str) -> str: "personal_agent_platform_candidate": "personal_agent_platform_candidate", "agent_operator_console_candidate": "operator_console_or_agent_ui_candidate", "vertical_product_not_core_agent": "vertical_product_signal_not_openclaw_replacement", - "needs_manual_research": "manual_research_required", - }.get(classification, "manual_research_required") + "needs_controlled_research": "controlled_research_required", + }.get(classification, "controlled_research_required") def _risk_flags(text: str, metadata: dict[str, Any]) -> list[str]: @@ -166,11 +166,11 @@ def _risk_flags(text: str, metadata: dict[str, Any]) -> list[str]: def _required_next_gate(recommendation: str) -> str: - if recommendation == "add_to_watch_registry_after_manual_source_review": - return "operator_confirms_primary_sources_then_add_watch_registry_only" + if recommendation == "add_to_watch_registry_after_controlled_source_review": + return "ai_controlled_primary_source_verifier_then_watch_registry_patch" if recommendation == "watch_only_product_surface_signal": - return "operator_confirms_product_surface_relevance_before_watch_only_entry" - return "manual_research_no_registry_change" + return "ai_controlled_product_surface_relevance_check_before_watch_only_entry" + return "controlled_research_no_registry_change" def _metadata_text(repo: str, metadata: dict[str, Any]) -> str: diff --git a/apps/api/src/services/agent_market_discovery_review.py b/apps/api/src/services/agent_market_discovery_review.py index 3211b0df2..e811472e1 100644 --- a/apps/api/src/services/agent_market_discovery_review.py +++ b/apps/api/src/services/agent_market_discovery_review.py @@ -2,8 +2,8 @@ Agent market discovery review ============================= -Turns raw discovery search results from the market watch into a manual intake -queue. This service is read-only: it does not add candidates to the registry, +Turns raw discovery search results from the market watch into an AI controlled +classification queue. This service is read-only: it does not add candidates to the registry, install SDKs, call LLMs, approve paid APIs, or change production routing. """ @@ -100,7 +100,7 @@ def _candidate_drafts( decision = ( "keep_existing_candidate_watch" if known - else "manual_primary_source_classification_required" + else "ai_controlled_primary_source_classification_required" ) next_gate = ( "use_existing_market_watch_candidate" @@ -116,6 +116,7 @@ def _candidate_drafts( "decision": decision, "recommended_next_gate": next_gate, "approval_boundary": { + "controlled_classification_allowed": not known, "approved_for_registry_addition": False, "approved_for_sdk_install": False, "approved_for_paid_api_calls": False, @@ -128,7 +129,7 @@ def _candidate_drafts( def _summary(watch_report: dict[str, Any], drafts: list[dict[str, Any]]) -> dict[str, int]: - manual = [ + controlled = [ draft for draft in drafts if draft["status"] == "needs_primary_source_classification" @@ -143,9 +144,9 @@ def _summary(watch_report: dict[str, Any], drafts: list[dict[str, Any]]) -> dict "already_watched_or_registered": sum( 1 for draft in drafts if draft["status"] == "already_watched_or_registered" ), - "manual_classification_required": len(manual), - "new_manual_classification_required": sum( - 1 for draft in manual if draft["new_since_previous_review"] + "controlled_classification_queue": len(controlled), + "new_controlled_classification_queue": sum( + 1 for draft in controlled if draft["new_since_previous_review"] ), "source_failures": sum( 1 @@ -209,7 +210,7 @@ def _recommended_actions(*, known: bool) -> list[str]: return [ "verify_official_or_primary_sources", "classify_role_against_awoooi_agent_taxonomy", - "add_to_watch_registry_only_after_manual_review", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", "do_not_install_sdk_or_call_provider", "do_not_enter_replacement_replay_before_market_scorecard", ] diff --git a/apps/api/src/services/agent_market_governance_snapshot.py b/apps/api/src/services/agent_market_governance_snapshot.py index 13738f970..aa1ab5c56 100644 --- a/apps/api/src/services/agent_market_governance_snapshot.py +++ b/apps/api/src/services/agent_market_governance_snapshot.py @@ -58,7 +58,7 @@ def build_agent_market_governance_snapshot( current_decision = ( "openclaw_remains_production_decision_core" if approvals["replacement_decisions_approved"] == 0 - else "manual_review_required_unexpected_replacement_approval" + else "break_glass_review_required_unexpected_replacement_approval" ) snapshot_generated_at = generated_at or datetime.now(timezone.utc).isoformat() # noqa: UP017 cadence = _evaluation_cadence(snapshot_generated_at) @@ -379,12 +379,12 @@ def _decision_queue_status(gate_status: str) -> str: return { "production_baseline": "baseline_protected", "integration_blocked": "blocked_needs_evidence", - "integration_reviewed": "operator_review_required", - "watch_only_prescreen_ready": "operator_priority_review", + "integration_reviewed": "controlled_review_required", + "watch_only_prescreen_ready": "controlled_priority_review", "watch_only_blocked": "watch_only_blocked", "watch_only_monitoring": "watch_only_monitoring", "registered_no_review": "registered_no_review", - }.get(gate_status, "operator_review_required") + }.get(gate_status, "controlled_review_required") def _decision_queue_action( @@ -394,7 +394,7 @@ def _decision_queue_action( required_next_gate: str, ) -> str: if candidate_id == "openclaw_incumbent": - return "keep_openclaw_as_production_decision_core_until_formal_replacement_adr" + return "keep_openclaw_as_production_decision_core_until_replay_shadow_canary_replacement_adr" if required_next_gate: return required_next_gate if gate_status == "registered_no_review": @@ -446,11 +446,11 @@ def _decision_risk_notes( notes.append("no_candidate_has_formal_replacement_approval") market_score = integration.get("market_score") or {} - notes.extend(str(value) for value in market_score.get("risks") or []) + notes.extend(_controlled_label(value) for value in market_score.get("risks") or []) classification = promotion.get("classification") or {} - notes.extend(str(value) for value in classification.get("risk_flags") or []) - notes.extend(str(value) for value in operator_blockers) + notes.extend(_controlled_label(value) for value in classification.get("risk_flags") or []) + notes.extend(_controlled_label(value) for value in operator_blockers) return list(dict.fromkeys(notes))[:6] @@ -568,16 +568,16 @@ def _candidate_operator_blockers( ) -> list[str]: blockers = [] for value in promotion.get("blockers") or []: - blockers.append(str(value)) + blockers.append(_controlled_label(value)) for value in integration.get("unblock_conditions") or []: - blockers.append(str(value)) + blockers.append(_controlled_label(value)) return blockers def _next_allowed_actions(candidate_groups: dict[str, list[str]]) -> list[str]: actions = ["continue_weekly_primary_source_market_watch"] if candidate_groups["watch_only_scorecard_prescreen_ready"]: - actions.append("operator_may_review_priority_upgrade_for_watch_only_candidates") + actions.append("agent_may_prepare_controlled_priority_upgrade_package_for_watch_only_candidates") if candidate_groups["replay_or_integration_blocked"]: actions.append("rerun_existing_replay_only_after_evidence_or_adapter_change") return actions @@ -591,12 +591,12 @@ def _evaluation_cadence(generated_at: str) -> dict[str, Any]: "next_scheduled_run_at": _next_monday_0900_taipei(generated_at), "trigger_modes": [ "scheduled_weekly", - "manual_dispatch", - "operator_triggered_after_primary_source_signal", + "workflow_dispatch", + "agent_controlled_trigger_after_primary_source_signal", ], "primary_source_policy": "primary_sources_only_no_llm_no_sdk_no_paid_api", - "operator_review_gate": ( - "priority_upgrade_required_before_scorecard_replay_sdk_api_shadow_canary_or_production" + "controlled_review_gate": ( + "controlled_priority_upgrade_required_before_scorecard_replay_sdk_api_shadow_canary_or_production" ), } @@ -614,15 +614,18 @@ def _market_watch_health( if summary["integration_queue_count"] > 0: blockers.append("integration_queue_not_empty") - status = "healthy" if not blockers else "blocked" + status = "healthy" if not blockers else "controlled_queue" stale_after = _stale_after(cadence["next_scheduled_run_at"]) return { "status": status, "freshness_sla_hours": _FRESHNESS_SLA_HOURS, "stale_grace_hours": _STALE_GRACE_HOURS, "stale_after": stale_after, + "source_failures_require_controlled_reverify": summary["source_failures"] > 0, + # Legacy alias for older readers; this is no longer a workflow hard stop. "source_failures_block_priority_upgrade": summary["source_failures"] > 0, "blocked_from_integration": summary["blocked_from_integration"], + "controlled_queue_reasons": blockers, "operator_blockers": blockers, } @@ -654,6 +657,16 @@ def _is_watch_only(candidate: dict[str, Any]) -> bool: ) +def _controlled_label(value: Any) -> str: + label = str(value) + if label.endswith("_by_operator"): + return f"{label.removesuffix('_by_operator')}_by_ai_controlled_verifier" + if label.startswith("operator_confirms_"): + subject = label.removeprefix("operator_confirms_") + return f"ai_controlled_{subject}_verifier_passed" + return label + + def _require_schema(report: dict[str, Any], expected: str, name: str) -> None: if report.get("schema_version") != expected: raise ValueError(f"{name} must be {expected}") diff --git a/apps/api/src/services/agent_market_watch_promotion_review.py b/apps/api/src/services/agent_market_watch_promotion_review.py index 106d334e1..db66b7fcc 100644 --- a/apps/api/src/services/agent_market_watch_promotion_review.py +++ b/apps/api/src/services/agent_market_watch_promotion_review.py @@ -109,7 +109,7 @@ def _review_watch_only_candidate( and classification_recommended ) decision = ( - "eligible_for_operator_priority_review_before_market_scorecard" + "eligible_for_controlled_priority_review_before_market_scorecard" if eligible_for_scorecard else "remain_watch_only_until_evidence_gap_resolved" ) @@ -147,7 +147,7 @@ def _review_watch_only_candidate( "approved_for_shadow_or_canary": False, "blockers": blockers, "required_next_gate": ( - "operator_priority_upgrade_then_market_scorecard_prescreen" + "controlled_priority_upgrade_then_market_scorecard_prescreen" if eligible_for_scorecard else "continue_watch_only_until_primary_source_evidence_is_sufficient" ), diff --git a/apps/api/tests/test_agent_market_discovery_review.py b/apps/api/tests/test_agent_market_discovery_review.py index 25c11df1d..f261ec513 100644 --- a/apps/api/tests/test_agent_market_discovery_review.py +++ b/apps/api/tests/test_agent_market_discovery_review.py @@ -37,8 +37,8 @@ def test_discovery_review_classifies_known_and_unknown_repositories(): assert report["policy"]["auto_registry_addition_approved"] is False assert report["summary"]["unique_repositories"] == 2 assert report["summary"]["already_watched_or_registered"] == 1 - assert report["summary"]["manual_classification_required"] == 1 - assert report["summary"]["new_manual_classification_required"] == 1 + assert report["summary"]["controlled_classification_queue"] == 1 + assert report["summary"]["new_controlled_classification_queue"] == 1 drafts = {draft["repository_full_name"]: draft for draft in report["candidate_drafts"]} assert drafts["microsoft/agent-framework"]["status"] == "already_watched_or_registered" @@ -46,6 +46,12 @@ def test_discovery_review_classifies_known_and_unknown_repositories(): assert drafts["pydantic/pydantic-ai"]["recommended_next_gate"] == ( "classify_official_sources_then_update_watch_registry" ) + assert drafts["pydantic/pydantic-ai"]["decision"] == ( + "ai_controlled_primary_source_classification_required" + ) + assert drafts["pydantic/pydantic-ai"]["approval_boundary"][ + "controlled_classification_allowed" + ] is True assert drafts["pydantic/pydantic-ai"]["approval_boundary"][ "approved_for_registry_addition" ] is False @@ -67,8 +73,8 @@ def test_discovery_review_previous_review_suppresses_new_repeat_signal(): generated_at="2026-06-03T00:00:00+00:00", ) - assert report["summary"]["manual_classification_required"] == 2 - assert report["summary"]["new_manual_classification_required"] == 0 + assert report["summary"]["controlled_classification_queue"] == 2 + assert report["summary"]["new_controlled_classification_queue"] == 0 assert all(not draft["new_since_previous_review"] for draft in report["candidate_drafts"]) diff --git a/apps/api/tests/test_agent_market_governance_snapshot.py b/apps/api/tests/test_agent_market_governance_snapshot.py index daa643c03..9ebb1ae8a 100644 --- a/apps/api/tests/test_agent_market_governance_snapshot.py +++ b/apps/api/tests/test_agent_market_governance_snapshot.py @@ -34,12 +34,12 @@ def test_governance_snapshot_keeps_openclaw_as_production_core_without_approvals "next_scheduled_run_at": "2026-06-08T09:00:00+08:00", "trigger_modes": [ "scheduled_weekly", - "manual_dispatch", - "operator_triggered_after_primary_source_signal", + "workflow_dispatch", + "agent_controlled_trigger_after_primary_source_signal", ], "primary_source_policy": "primary_sources_only_no_llm_no_sdk_no_paid_api", - "operator_review_gate": ( - "priority_upgrade_required_before_scorecard_replay_sdk_api_shadow_canary_or_production" + "controlled_review_gate": ( + "controlled_priority_upgrade_required_before_scorecard_replay_sdk_api_shadow_canary_or_production" ), } assert snapshot["market_watch_health"] == { @@ -47,8 +47,10 @@ def test_governance_snapshot_keeps_openclaw_as_production_core_without_approvals "freshness_sla_hours": 168, "stale_grace_hours": 6, "stale_after": "2026-06-08T15:00:00+08:00", + "source_failures_require_controlled_reverify": False, "source_failures_block_priority_upgrade": False, "blocked_from_integration": 1, + "controlled_queue_reasons": [], "operator_blockers": [], } assert snapshot["candidate_groups"]["production_baseline"] == ["openclaw_incumbent"] @@ -88,7 +90,7 @@ def test_governance_snapshot_keeps_openclaw_as_production_core_without_approvals "evaluation_priority": "watch_only", "gate_status": "watch_only_prescreen_ready", "current_gate": "watch_only_primary_source_monitoring", - "required_next_gate": "operator_priority_upgrade_then_market_scorecard_prescreen", + "required_next_gate": "controlled_priority_upgrade_then_market_scorecard_prescreen", "integration_decision": "do_not_integrate_watch_only_primary_source_monitoring", "score": None, "evidence": { @@ -112,8 +114,8 @@ def test_governance_snapshot_keeps_openclaw_as_production_core_without_approvals "candidate_id": "hermes_agent_personal_platform", "display_name": "Hermes Agent", "priority": 30, - "queue_status": "operator_priority_review", - "recommended_action": "operator_priority_upgrade_then_market_scorecard_prescreen", + "queue_status": "controlled_priority_review", + "recommended_action": "controlled_priority_upgrade_then_market_scorecard_prescreen", "approval_boundary": { "replacement_adr_required": True, "priority_upgrade_required": True, @@ -133,7 +135,7 @@ def test_governance_snapshot_keeps_openclaw_as_production_core_without_approvals "priority": 90, "queue_status": "baseline_protected", "recommended_action": ( - "keep_openclaw_as_production_decision_core_until_formal_replacement_adr" + "keep_openclaw_as_production_decision_core_until_replay_shadow_canary_replacement_adr" ), "approval_boundary": { "replacement_adr_required": True, @@ -162,9 +164,10 @@ def test_governance_snapshot_blocks_market_health_when_sources_or_queue_are_not_ generated_at="2026-06-04T00:00:00+00:00", ) - assert snapshot["market_watch_health"]["status"] == "blocked" + assert snapshot["market_watch_health"]["status"] == "controlled_queue" + assert snapshot["market_watch_health"]["source_failures_require_controlled_reverify"] is True assert snapshot["market_watch_health"]["source_failures_block_priority_upgrade"] is True - assert snapshot["market_watch_health"]["operator_blockers"] == [ + assert snapshot["market_watch_health"]["controlled_queue_reasons"] == [ "source_failures_present", "unclassified_discovery_watch_additions_remaining", "integration_queue_not_empty", @@ -300,9 +303,9 @@ def _promotion_review() -> dict: "candidate_id": "hermes_agent_personal_platform", "eligible_for_market_scorecard_prescreen": True, "display_name": "Hermes Agent", - "decision": "eligible_for_operator_priority_review_before_market_scorecard", + "decision": "eligible_for_controlled_priority_review_before_market_scorecard", "integration_stage": "watch_only_primary_source_monitoring", - "required_next_gate": "operator_priority_upgrade_then_market_scorecard_prescreen", + "required_next_gate": "controlled_priority_upgrade_then_market_scorecard_prescreen", "role": "personal_agent_platform_candidate", "approved_for_replay": False, "approved_for_sdk_install": False, diff --git a/apps/api/tests/test_agent_market_watch_promotion_review.py b/apps/api/tests/test_agent_market_watch_promotion_review.py index c23657262..e3d4a5460 100644 --- a/apps/api/tests/test_agent_market_watch_promotion_review.py +++ b/apps/api/tests/test_agent_market_watch_promotion_review.py @@ -23,7 +23,7 @@ def test_watch_promotion_review_allows_only_scorecard_prescreen_readiness(): assert review["eligible_for_market_scorecard_prescreen"] is True assert review["approved_for_replay"] is False assert review["required_next_gate"] == ( - "operator_priority_upgrade_then_market_scorecard_prescreen" + "controlled_priority_upgrade_then_market_scorecard_prescreen" ) @@ -145,7 +145,7 @@ def _classification() -> dict: "html_url": "https://github.com/NousResearch/hermes-agent", "homepage": "https://hermes-agent.nousresearch.com", "classification": "personal_agent_platform_candidate", - "recommendation": "add_to_watch_registry_after_manual_source_review", + "recommendation": "add_to_watch_registry_after_controlled_source_review", "watch_addition_recommended": True, "risk_flags": ["requires_dependency_boundary_review"], } diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index f6658202d..1c08ff0e6 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3561,19 +3561,21 @@ "health": { "title": "監測健康", "status": "狀態", - "statuses": { - "healthy": "Healthy", - "blocked": "Blocked" - }, + "statuses": { + "healthy": "Healthy", + "controlled_queue": "受控隊列", + "blocked": "Blocked" + }, "freshnessSla": "新鮮度 SLA", "slaValue": "{slaHours}h + {graceHours}h", "staleAfter": "過期時間", "priorityGate": "升級關卡", "blockedIntegrations": "已擋下整合", - "blockers": "阻擋", - "blocked": "已阻擋", - "clear": "通過", - "noBlockers": "無 operator 阻擋" + "blockers": "阻擋", + "blocked": "已阻擋", + "controlledReverify": "AI 受控重驗", + "clear": "通過", + "noBlockers": "無受控阻擋" }, "cadence": { "title": "定期評估", @@ -3596,8 +3598,10 @@ "statuses": { "baseline_protected": "基準受保護", "blocked_needs_evidence": "需要證據", - "operator_review_required": "需要受控審查", - "operator_priority_review": "優先級受控審查", + "controlled_review_required": "需要受控審查", + "controlled_priority_review": "優先級受控審查", + "operator_review_required": "需要受控審查", + "operator_priority_review": "優先級受控審查", "watch_only_blocked": "觀察已阻擋", "watch_only_monitoring": "觀察中", "registered_no_review": "尚未審查" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f6658202d..1c08ff0e6 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3561,19 +3561,21 @@ "health": { "title": "監測健康", "status": "狀態", - "statuses": { - "healthy": "Healthy", - "blocked": "Blocked" - }, + "statuses": { + "healthy": "Healthy", + "controlled_queue": "受控隊列", + "blocked": "Blocked" + }, "freshnessSla": "新鮮度 SLA", "slaValue": "{slaHours}h + {graceHours}h", "staleAfter": "過期時間", "priorityGate": "升級關卡", "blockedIntegrations": "已擋下整合", - "blockers": "阻擋", - "blocked": "已阻擋", - "clear": "通過", - "noBlockers": "無 operator 阻擋" + "blockers": "阻擋", + "blocked": "已阻擋", + "controlledReverify": "AI 受控重驗", + "clear": "通過", + "noBlockers": "無受控阻擋" }, "cadence": { "title": "定期評估", @@ -3596,8 +3598,10 @@ "statuses": { "baseline_protected": "基準受保護", "blocked_needs_evidence": "需要證據", - "operator_review_required": "需要受控審查", - "operator_priority_review": "優先級受控審查", + "controlled_review_required": "需要受控審查", + "controlled_priority_review": "優先級受控審查", + "operator_review_required": "需要受控審查", + "operator_priority_review": "優先級受控審查", "watch_only_blocked": "觀察已阻擋", "watch_only_monitoring": "觀察中", "registered_no_review": "尚未審查" diff --git a/apps/web/src/app/[locale]/governance/tabs/agent-market-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/agent-market-tab.tsx index eeb7e6088..ae170cc2a 100644 --- a/apps/web/src/app/[locale]/governance/tabs/agent-market-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/agent-market-tab.tsx @@ -1237,6 +1237,8 @@ export function AgentMarketTab() { .length const watchHealth = snapshot.market_watch_health const watchHealthHealthy = watchHealth.status === 'healthy' + const watchHealthReasons = watchHealth.controlled_queue_reasons ?? watchHealth.operator_blockers ?? [] + const controlledReviewGate = snapshot.evaluation_cadence.controlled_review_gate ?? snapshot.evaluation_cadence.operator_review_gate return (
@@ -1867,15 +1869,15 @@ export function AgentMarketTab() { {formatDateTime(watchHealth.stale_after)} - {watchHealth.source_failures_block_priority_upgrade ? t('health.blocked') : t('health.clear')} + {watchHealth.source_failures_require_controlled_reverify ? t('health.controlledReverify') : t('health.clear')} {watchHealth.blocked_from_integration}
- {watchHealth.operator_blockers.length > 0 ? ( - watchHealth.operator_blockers.map(blocker => ( + {watchHealthReasons.length > 0 ? ( + watchHealthReasons.map(blocker => ( )) ) : ( @@ -1913,7 +1915,7 @@ export function AgentMarketTab() { - +
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index fae6345e3..89fb1332e 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -1538,18 +1538,21 @@ export interface AgentMarketGovernanceSnapshot { schedule: string timezone: 'Asia/Taipei' next_scheduled_run_at: string - trigger_modes: string[] - primary_source_policy: string - operator_review_gate: string - } - market_watch_health: { - status: 'healthy' | 'blocked' - freshness_sla_hours: 168 - stale_grace_hours: 6 - stale_after: string - source_failures_block_priority_upgrade: boolean - blocked_from_integration: number - operator_blockers: string[] + trigger_modes: string[] + primary_source_policy: string + controlled_review_gate: string + operator_review_gate?: string + } + market_watch_health: { + status: 'healthy' | 'controlled_queue' | 'blocked' + freshness_sla_hours: 168 + stale_grace_hours: 6 + stale_after: string + source_failures_require_controlled_reverify: boolean + source_failures_block_priority_upgrade?: boolean + blocked_from_integration: number + controlled_queue_reasons: string[] + operator_blockers?: string[] } summary: { candidate_count: number @@ -1613,10 +1616,12 @@ export interface AgentMarketGovernanceSnapshot { display_name: string priority: number queue_status: - | 'baseline_protected' - | 'blocked_needs_evidence' - | 'operator_review_required' - | 'operator_priority_review' + | 'baseline_protected' + | 'blocked_needs_evidence' + | 'controlled_review_required' + | 'controlled_priority_review' + | 'operator_review_required' + | 'operator_priority_review' | 'watch_only_blocked' | 'watch_only_monitoring' | 'registered_no_review' diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 7c7c35d8f..02d38bf3b 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,25 @@ +## 2026-06-28 — 09:35 Agent Market discovery guard 轉 AI controlled queue + +**背景**:統帥要求非事故級 hard gate / guard 全部打開並快速推進實作。本段針對 `agent-market-watch` 中舊 `manual_classification_required`、`needs_manual_research`、promotion review hard fail 與 operator priority gate,改成可審計的 AI controlled classification / priority queue。 + +**完成內容**: +- `.gitea/workflows/agent-market-watch.yaml` 將 discovery / classification / summary / Telegram 文案改為 AI 受控隊列;classification step 不再因沒有 `new_manual` 候選而跳過,會固定產生 controlled queue artifact,避免後段缺 artifact 變成硬阻塞。 +- `apps/api/src/services/agent_market_discovery_review.py`、`agent_market_discovery_classifier.py`、`agent_market_watch_promotion_review.py`、`agent_market_governance_snapshot.py` 移除非 critical manual/operator 預設終局,改為 `controlled_classification_queue`、`needs_controlled_research`、`controlled_priority_review`、`controlled_queue`。 +- `docs/schemas/agent_market_discovery_review_v1.schema.json`、`docs/schemas/agent_market_governance_snapshot_v1.schema.json` 與 `apps/web/src/lib/api-client.ts` 同步 controlled queue 欄位,前端 governance Agent Market tab 改讀 controlled reverify / controlled review gate。 +- `apps/web/messages/zh-TW.json`、`apps/web/messages/en.json` 補齊 Agent Market controlled queue / controlled review i18n,避免 UI 仍把非 critical 市場 watch path 說成 manual blocker。 +- 重新產生 `docs/evaluations/agent_market_*_2026-06-28.json` 四份 snapshot;本次 readback:`controlled_classification_queue=9`、`recommended_watch_additions=7`、`integration_queue_count=5`、`market_watch_health.status=controlled_queue`。 + +**本地驗證結果**: +- YAML / JSON parse:通過。 +- Agent Market pytest:`9 passed`。 +- i18n mirror:`zh-TW=14480`、`en=14480`、missing `0 / 0`。 +- `pnpm --dir apps/web typecheck`:通過。 +- `git diff --check`:通過。 + +**仍保留的 break-glass 邊界**: +- OpenClaw 仍維持 production decision core;替換前必須 replay / shadow / canary / ADR。 +- SDK install、API shadow / canary、production route、paid provider / cost route、external active security scan、secret value / credential URL / raw env、DB destructive / backup restore、force push / repo refs deletion 仍不得被本段 controlled queue 直接打開。 + ## 2026-06-28 — 08:45 110 runner 壓力事故 source / live fail-closed 收斂 **背景**:統帥全面授權打開非事故級 gate,但 110 Gitea runner 反覆拉起 StockPlatform headless Chrome smoke,已造成 production host CPU / CI 壓力事故;runner 未搬遷 / 限流前不得直接重開。 diff --git a/docs/evaluations/agent_market_discovery_classification_2026-06-28.json b/docs/evaluations/agent_market_discovery_classification_2026-06-28.json new file mode 100644 index 000000000..c8a832196 --- /dev/null +++ b/docs/evaluations/agent_market_discovery_classification_2026-06-28.json @@ -0,0 +1,431 @@ +{ + "candidates": [ + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "needs_controlled_research", + "description": "The Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol", + "homepage": "https://docs.copilotkit.ai", + "html_url": "https://github.com/CopilotKit/CopilotKit", + "language": "TypeScript", + "pushed_at": "2026-06-27T06:40:27Z", + "recommendation": "controlled_research_before_watch_registry", + "recommended_role": "controlled_research_required", + "repository_full_name": "copilotkit/copilotkit", + "required_next_gate": "controlled_research_no_registry_change", + "risk_flags": [ + "requires_dependency_boundary_review" + ], + "stargazers_count": 35572, + "topics": [ + "agent", + "agent-native", + "agentic-ai", + "agents", + "ai", + "ai-agent", + "ai-assistant", + "assistant", + "assistant-chat-bots", + "copilot", + "copilot-chat", + "generative-ui", + "js", + "llm", + "nextjs", + "open-source", + "react", + "reactjs", + "ts", + "typescript" + ], + "watch_addition_recommended": false + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "needs_controlled_research", + "description": "Open-source infrastructure for Computer-Use Agents. Sandboxes, SDKs, and benchmarks to train and evaluate AI agents that can control full desktops (macOS, Linux, Windows).", + "homepage": "https://cua.ai", + "html_url": "https://github.com/trycua/cua", + "language": "HTML", + "pushed_at": "2026-06-28T00:16:47Z", + "recommendation": "controlled_research_before_watch_registry", + "recommended_role": "controlled_research_required", + "repository_full_name": "trycua/cua", + "required_next_gate": "controlled_research_no_registry_change", + "risk_flags": [ + "requires_dependency_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 19131, + "topics": [ + "agent", + "ai-agent", + "apple", + "computer-use", + "computer-use-agent", + "containerization", + "cua", + "desktop-automation", + "hacktoberfest", + "lume", + "macos", + "manus", + "operator", + "swift", + "virtualization", + "virtualization-framework", + "windows", + "windows-sandbox" + ], + "watch_addition_recommended": false + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_framework_candidate", + "description": "Eigent: The Open Source Cowork Desktop to Unlock Your Exceptional Productivity. Local and Free Alternative to Claude Cowork.", + "homepage": "https://www.eigent.ai", + "html_url": "https://github.com/eigent-ai/eigent", + "language": "TypeScript", + "pushed_at": "2026-06-27T10:16:01Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_framework_or_orchestrator_candidate", + "repository_full_name": "eigent-ai/eigent", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "likely_requires_paid_provider_boundary_review" + ], + "stargazers_count": 14404, + "topics": [ + "agent-framework", + "agent-skills", + "agentic-ai", + "agentic-workflow", + "claude-cowork", + "claude-cowork-alternative", + "claude-cowork-free", + "desktop-agent", + "multi-agent-systems" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_governance_candidate", + "description": "Omnigent is an open-source AI agent framework and meta-harness: orchestrate Claude Code, Codex, Cursor, Pi, and custom agents — swap harnesses without rewriting, enforce policies and sandboxing, and collaborate in real time from any device.", + "homepage": "https://omnigent.ai", + "html_url": "https://github.com/omnigent-ai/omnigent", + "language": "Python", + "pushed_at": "2026-06-27T23:15:21Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_governance_policy_evaluator_candidate", + "repository_full_name": "omnigent-ai/omnigent", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "likely_requires_paid_provider_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 5132, + "topics": [ + "agent-framework", + "agent-governance", + "agent-orchestration", + "agents", + "ai", + "ai-agent", + "ai-agents", + "claude-code", + "codex", + "coding-agents", + "developer-tools", + "llm", + "ml", + "multi-agent", + "python", + "sandbox" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_framework_candidate", + "description": "AG2 (formerly AutoGen): The Open-Source AgentOS.Join us at: https://discord.gg/sNGSwQME3x", + "homepage": "https://ag2.ai", + "html_url": "https://github.com/ag2ai/ag2", + "language": "Python", + "pushed_at": "2026-06-27T22:37:56Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_framework_or_orchestrator_candidate", + "repository_full_name": "ag2ai/ag2", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 4714, + "topics": [ + "a2a", + "ag2", + "agent-framework", + "agentic", + "agentic-ai", + "ai", + "ai-agents-framework", + "aiagents", + "genai", + "llm", + "llms", + "mcp", + "multi-agent", + "multi-agent-system", + "open-source", + "python" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_framework_candidate", + "description": "AI 时代的伯克希尔:基于 Claude Code 的价值投资研究框架。巴菲特·芒格·段永平·李录四大师方法论 + 多Agent并行研究。| AI-era Berkshire: a value investing research framework built on Claude Code. 4 masters' methodologies + multi-agent adversarial analysis.", + "homepage": "https://github.com/xbtlin/ai-berkshire#readme", + "html_url": "https://github.com/xbtlin/ai-berkshire", + "language": "Python", + "pushed_at": "2026-06-27T16:49:55Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_framework_or_orchestrator_candidate", + "repository_full_name": "xbtlin/ai-berkshire", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "likely_requires_paid_provider_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 4117, + "topics": [ + "ai", + "ai-agent", + "anthropic", + "berkshire-hathaway", + "charlie-munger", + "china-stock", + "claude", + "claude-code", + "financial-analysis", + "fintech", + "fundamental-analysis", + "investment", + "investment-research", + "llm", + "mcp", + "portfolio-management", + "stock-analysis", + "stock-market", + "value-investing", + "warren-buffett" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_framework_candidate", + "description": "A Go framework for building production agent systems with graph workflows, tools, memory, A2A, AG-UI, MCP, evaluation, and observability.", + "homepage": "https://trpc-group.github.io/trpc-agent-go/", + "html_url": "https://github.com/trpc-group/trpc-agent-go", + "language": "Go", + "pushed_at": "2026-06-27T03:22:33Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_framework_or_orchestrator_candidate", + "repository_full_name": "trpc-group/trpc-agent-go", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 1414, + "topics": [ + "a2a", + "a2a-protocol", + "ag-ui", + "agent", + "agent-framework", + "ai", + "ai-agents", + "evaluation", + "go", + "golang", + "graph-workflows", + "llm", + "mcp", + "model-context-protocol", + "multi-agent", + "observability", + "opentelemetry", + "rag" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "personal_agent_platform_candidate", + "description": "Zero, your trustworthy AI teammate for real work. ", + "homepage": "https://vm0.ai", + "html_url": "https://github.com/vm0-ai/vm0", + "language": "TypeScript", + "pushed_at": "2026-06-28T00:39:40Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "personal_agent_platform_candidate", + "repository_full_name": "vm0-ai/vm0", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "likely_requires_paid_provider_boundary_review", + "requires_tool_execution_sandbox_review" + ], + "stargazers_count": 1133, + "topics": [ + "agentic-workflow", + "ai-agent", + "ai-runtime", + "ai-sandbox", + "claude-code", + "dev-tools", + "sandbox" + ], + "watch_addition_recommended": true + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "approved_for_watch_registry_addition": false + }, + "archived": false, + "classification": "agent_framework_candidate", + "description": "CORAL is a robust, lightweight infrastructure for multi-agent autonomous self-evolution, built for autoresearch. Works with Claude Code, Codex, Cursor, OpenCode, Kiro, and more.", + "homepage": "https://docs.coralxyz.com", + "html_url": "https://github.com/Human-Agent-Society/CORAL", + "language": "Python", + "pushed_at": "2026-06-26T06:40:13Z", + "recommendation": "add_to_watch_registry_after_controlled_source_review", + "recommended_role": "agent_framework_or_orchestrator_candidate", + "repository_full_name": "human-agent-society/coral", + "required_next_gate": "ai_controlled_primary_source_verifier_then_watch_registry_patch", + "risk_flags": [ + "requires_dependency_boundary_review", + "likely_requires_paid_provider_boundary_review" + ], + "stargazers_count": 756, + "topics": [ + "agent-framework", + "agentic-ai", + "alpha-evolve", + "autonomous-agents", + "autoresearch", + "claude-code", + "code-generation", + "codex", + "coding-agent", + "evolutionary-algorithm", + "large-language-models", + "llm-agents", + "multi-agent", + "opencode", + "self-evolving", + "self-evolving-agents", + "swe-bench" + ], + "watch_addition_recommended": true + } + ], + "generated_at": "2026-06-28T00:46:51.907924+00:00", + "inputs": { + "discovery_review_generated_at": "2026-06-28T00:46:48.886379+00:00", + "metadata_source": "github_repository_api_summary" + }, + "policy": { + "auto_watch_registry_addition_approved": false, + "paid_api_calls_approved": false, + "production_changes_approved": false, + "raw_external_pages_committed": false, + "replacement_decision_allowed": false, + "sdk_installation_approved": false, + "shadow_or_canary_approved": false + }, + "schema_version": "agent_market_discovery_classification_v1", + "summary": { + "classification_counts": { + "agent_framework_candidate": 5, + "agent_governance_candidate": 1, + "needs_controlled_research": 2, + "personal_agent_platform_candidate": 1 + }, + "classified_repositories": 9, + "production_changes_approved": 0, + "recommendation_counts": { + "add_to_watch_registry_after_controlled_source_review": 7, + "controlled_research_before_watch_registry": 2 + }, + "recommended_watch_additions": 7, + "shadow_or_canary_approved": 0, + "watch_only_or_defer": 2 + } +} diff --git a/docs/evaluations/agent_market_discovery_review_2026-06-28.json b/docs/evaluations/agent_market_discovery_review_2026-06-28.json new file mode 100644 index 000000000..854723c89 --- /dev/null +++ b/docs/evaluations/agent_market_discovery_review_2026-06-28.json @@ -0,0 +1,286 @@ +{ + "candidate_drafts": [ + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/CopilotKit/CopilotKit", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "copilotkit/copilotkit", + "seen_before": true, + "source_ids": [ + "github_ai_agent_topic" + ], + "stargazers_count_max": 35522, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:36:59Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/trycua/cua", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "trycua/cua", + "seen_before": true, + "source_ids": [ + "github_ai_agent_topic" + ], + "stargazers_count_max": 19004, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:40:04Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/eigent-ai/eigent", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "eigent-ai/eigent", + "seen_before": true, + "source_ids": [ + "github_agent_framework_topic" + ], + "stargazers_count_max": 14394, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:38:22Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/omnigent-ai/omnigent", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "omnigent-ai/omnigent", + "seen_before": true, + "source_ids": [ + "github_ai_agent_topic", + "github_agent_framework_topic" + ], + "stargazers_count_max": 4924, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:37:54Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/ag2ai/ag2", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "ag2ai/ag2", + "seen_before": true, + "source_ids": [ + "github_agent_framework_topic" + ], + "stargazers_count_max": 4710, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:29:07Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/xbtlin/ai-berkshire", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "xbtlin/ai-berkshire", + "seen_before": true, + "source_ids": [ + "github_ai_agent_topic" + ], + "stargazers_count_max": 2172, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:41:15Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/trpc-group/trpc-agent-go", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "trpc-group/trpc-agent-go", + "seen_before": true, + "source_ids": [ + "github_agent_framework_topic" + ], + "stargazers_count_max": 1408, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T01:55:30Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/vm0-ai/vm0", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "vm0-ai/vm0", + "seen_before": true, + "source_ids": [ + "github_ai_agent_topic" + ], + "stargazers_count_max": 1131, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:32:54Z" + }, + { + "approval_boundary": { + "approved_for_paid_api_calls": false, + "approved_for_registry_addition": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "controlled_classification_allowed": true + }, + "decision": "ai_controlled_primary_source_classification_required", + "html_url": "https://github.com/Human-Agent-Society/CORAL", + "new_since_previous_review": false, + "recommended_actions": [ + "verify_official_or_primary_sources", + "classify_role_against_awoooi_agent_taxonomy", + "queue_controlled_watch_registry_patch_after_primary_source_verifier", + "do_not_install_sdk_or_call_provider", + "do_not_enter_replacement_replay_before_market_scorecard" + ], + "recommended_next_gate": "classify_official_sources_then_update_watch_registry", + "repository_full_name": "human-agent-society/coral", + "seen_before": true, + "source_ids": [ + "github_agent_framework_topic" + ], + "stargazers_count_max": 754, + "status": "needs_primary_source_classification", + "updated_at_latest": "2026-06-26T03:05:29Z" + } + ], + "generated_at": "2026-06-28T00:46:48.886379+00:00", + "inputs": { + "candidate_registry_schema_version": "agent_replacement_candidates_v1", + "previous_review_generated_at": "2026-06-26T03:41:38.325050+00:00", + "source_registry_schema_version": "agent_market_watch_sources_v1", + "watch_report_generated_at": "2026-06-26T03:41:17.266599+00:00", + "watch_report_mode": "live" + }, + "policy": { + "auto_registry_addition_approved": false, + "paid_api_calls_approved": false, + "production_changes_approved": false, + "replacement_decision_allowed": false, + "sdk_installation_approved": false, + "shadow_or_canary_approved": false + }, + "schema_version": "agent_market_discovery_review_v1", + "summary": { + "already_watched_or_registered": 0, + "auto_registry_additions_approved": 0, + "controlled_classification_queue": 9, + "discovered_items": 10, + "discovery_sources": 2, + "new_controlled_classification_queue": 0, + "production_changes_approved": 0, + "shadow_or_canary_approved": 0, + "source_failures": 0, + "unique_repositories": 9 + } +} diff --git a/docs/evaluations/agent_market_governance_snapshot_2026-06-28.json b/docs/evaluations/agent_market_governance_snapshot_2026-06-28.json new file mode 100644 index 000000000..2b856926b --- /dev/null +++ b/docs/evaluations/agent_market_governance_snapshot_2026-06-28.json @@ -0,0 +1,839 @@ +{ + "candidate_groups": { + "production_baseline": [ + "openclaw_incumbent" + ], + "replay_or_integration_blocked": [ + "claude_agent_sdk_remediator", + "crewai_flows_crews", + "langgraph_incident_kernel", + "nemo_nemotron_fabric", + "openai_agents_sdk_coordinator" + ], + "watch_only_candidates": [ + "agentos_framework", + "bernstein_agent_governance", + "hermes_agent_personal_platform", + "microsoft_agent_governance_toolkit", + "pydantic_deepagents", + "thclaws_agent_harness" + ], + "watch_only_scorecard_prescreen_ready": [] + }, + "candidate_statuses": [ + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "openclaw_incumbent", + "current_gate": "production_decision_core", + "display_name": "OpenClaw incumbent", + "evaluation_priority": "baseline", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "production_baseline", + "integration_decision": "", + "operator_blockers": [], + "required_next_gate": "formal_replacement_adr_and_promotion_gate_required", + "role": "current_production_decision_core", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "openai_agents_sdk_coordinator", + "current_gate": "offline_replay", + "display_name": "OpenAI Agents SDK Coordinator", + "evaluation_priority": "must_test", + "evidence": { + "latest_replay_summary": "docs/evaluations/agent_openai_coordinator_replay_summary_2026-06-02.json", + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "registered_no_review", + "integration_decision": "deterministic_offline_coordinator_blocked_does_not_beat_openclaw", + "operator_blockers": [], + "required_next_gate": "continue_weekly_primary_source_market_watch", + "role": "coordinator_orchestrator", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "langgraph_incident_kernel", + "current_gate": "offline_replay", + "display_name": "LangGraph Incident Kernel", + "evaluation_priority": "must_test", + "evidence": { + "latest_replay_summary": "docs/evaluations/agent_langgraph_replay_summary_2026-06-02.json", + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "registered_no_review", + "integration_decision": "deterministic_offline_kernel_blocked_does_not_beat_openclaw", + "operator_blockers": [], + "required_next_gate": "continue_weekly_primary_source_market_watch", + "role": "durable_incident_workflow_kernel", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "nemo_nemotron_fabric", + "current_gate": "blocked_existing_replay_evidence", + "display_name": "NVIDIA NeMo Agent Toolkit + Nemotron Fabric", + "evaluation_priority": "must_test", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": "docs/evaluations/agent_nemotron_contract_tuned_49b_v15_smoke_gate_2026-06-02.json", + "latest_smoke_matrix": "docs/evaluations/agent_nemotron_contract_tuned_smoke_matrix_2026-06-02.json", + "latest_smoke_model": "nvidia/llama-3.3-nemotron-super-49b-v1.5" + }, + "gate_status": "integration_blocked", + "integration_decision": "do_not_integrate_refresh_evidence_then_smoke_gate", + "operator_blockers": [ + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval", + "5_record_smoke_gate_passes", + "latency_and_output_contract_blockers_resolved", + "cost_approval_recorded" + ], + "required_next_gate": "refresh_source_evidence_then_5_record_smoke_only", + "role": "agent_fabric_tool_model_evaluator", + "score": 0.8033 + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "claude_agent_sdk_remediator", + "current_gate": "has_offline_replay_summary", + "display_name": "Claude Agent SDK Remediator", + "evaluation_priority": "must_test", + "evidence": { + "latest_replay_summary": "docs/evaluations/agent_claude_remediator_replay_summary_2026-06-02.json", + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "integration_blocked", + "integration_decision": "do_not_integrate_refresh_replay_gate", + "operator_blockers": [ + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval", + "offline_adapter_contract_valid", + "50_record_hidden_label_replay_beats_openclaw_baseline", + "cost_approval_recorded" + ], + "required_next_gate": "refresh_scorecard_then_offline_replay_or_promotion_gate", + "role": "devops_code_remediation_agent", + "score": 0.7533 + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "google_adk_stack", + "current_gate": "offline_replay", + "display_name": "Google Agent Development Kit Stack", + "evaluation_priority": "can_test", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "registered_no_review", + "integration_decision": "", + "operator_blockers": [], + "required_next_gate": "continue_weekly_primary_source_market_watch", + "role": "gemini_vertex_agent_stack", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "microsoft_agent_framework", + "current_gate": "offline_replay", + "display_name": "Microsoft Agent Framework", + "evaluation_priority": "can_test", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "registered_no_review", + "integration_decision": "", + "operator_blockers": [], + "required_next_gate": "continue_weekly_primary_source_market_watch", + "role": "enterprise_workflow_agent_stack", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "crewai_flows_crews", + "current_gate": "not_yet_replayed", + "display_name": "CrewAI Flows + Crews", + "evaluation_priority": "secondary", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "integration_blocked", + "integration_decision": "do_not_integrate_prepare_no_cost_offline_adapter", + "operator_blockers": [ + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval", + "offline_adapter_contract_valid", + "50_record_hidden_label_replay_beats_openclaw_baseline" + ], + "required_next_gate": "create_no_sdk_no_api_adapter_then_offline_replay", + "role": "rapid_agent_team_prototype", + "score": 0.6033 + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "hermes_agent_personal_platform", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "NousResearch Hermes Agent", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "remain_watch_only_until_evidence_gap_resolved", + "operator_blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "personal_agent_platform_candidate", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "microsoft_agent_governance_toolkit", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "Microsoft Agent Governance Toolkit", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "remain_watch_only_until_evidence_gap_resolved", + "operator_blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_governance_policy_evaluator_candidate", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "thclaws_agent_harness", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "thClaws Agent Harness", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "do_not_integrate_watch_only_primary_source_monitoring", + "operator_blockers": [ + "discovery_classification_must_recommend_watch_addition", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval", + "ai_controlled_primary_sources_verifier_passed", + "watch_registry_baseline_refreshed", + "explicit_priority_upgrade_before_replay", + "cost_approval_recorded" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "pydantic_deepagents", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "Pydantic DeepAgents", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "remain_watch_only_until_evidence_gap_resolved", + "operator_blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "agentos_framework", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "AgentOS Framework", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "remain_watch_only_until_evidence_gap_resolved", + "operator_blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "score": null + }, + { + "approvals": { + "paid_api": false, + "production_routing": false, + "replay": false, + "sdk_install": false, + "shadow_or_canary": false + }, + "candidate_id": "bernstein_agent_governance", + "current_gate": "watch_only_primary_source_monitoring", + "display_name": "Bernstein Agent Governance", + "evaluation_priority": "watch_only", + "evidence": { + "latest_replay_summary": null, + "latest_smoke_gate": null, + "latest_smoke_matrix": null, + "latest_smoke_model": null + }, + "gate_status": "watch_only_blocked", + "integration_decision": "do_not_integrate_watch_only_primary_source_monitoring", + "operator_blockers": [ + "discovery_classification_must_recommend_watch_addition", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval", + "ai_controlled_primary_sources_verifier_passed", + "watch_registry_baseline_refreshed", + "explicit_priority_upgrade_before_replay", + "cost_approval_recorded" + ], + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_governance_policy_evaluator_candidate", + "score": null + } + ], + "current_decision": "openclaw_remains_production_decision_core", + "evaluation_cadence": { + "controlled_review_gate": "controlled_priority_upgrade_required_before_scorecard_replay_sdk_api_shadow_canary_or_production", + "next_scheduled_run_at": "2026-06-29T09:00:00+08:00", + "primary_source_policy": "primary_sources_only_no_llm_no_sdk_no_paid_api", + "schedule": "weekly_monday_0900_asia_taipei", + "timezone": "Asia/Taipei", + "trigger_modes": [ + "scheduled_weekly", + "workflow_dispatch", + "agent_controlled_trigger_after_primary_source_signal" + ], + "workflow": ".gitea/workflows/agent-market-watch.yaml" + }, + "forbidden_actions_without_new_approval": [ + "replace_openclaw", + "enter_shadow_or_canary", + "install_new_agent_sdk", + "call_paid_provider_api", + "run_replay_for_watch_only_candidate", + "change_production_routing" + ], + "generated_at": "2026-06-28T00:47:02.688946+00:00", + "inputs": { + "candidate_registry_schema_version": "agent_replacement_candidates_v1", + "discovery_classification_generated_at": "2026-06-28T00:46:51.907924+00:00", + "integration_review_generated_at": "2026-06-26T03:41:38.289424+00:00", + "promotion_review_generated_at": "2026-06-28T00:47:01.200212+00:00", + "watch_report_generated_at": "2026-06-26T03:41:17.266599+00:00" + }, + "market_watch_health": { + "blocked_from_integration": 5, + "controlled_queue_reasons": [ + "unclassified_discovery_watch_additions_remaining", + "integration_queue_not_empty" + ], + "freshness_sla_hours": 168, + "operator_blockers": [ + "unclassified_discovery_watch_additions_remaining", + "integration_queue_not_empty" + ], + "source_failures_block_priority_upgrade": false, + "source_failures_require_controlled_reverify": false, + "stale_after": "2026-06-29T15:00:00+08:00", + "stale_grace_hours": 6, + "status": "controlled_queue" + }, + "next_allowed_actions": [ + "continue_weekly_primary_source_market_watch", + "rerun_existing_replay_only_after_evidence_or_adapter_change" + ], + "operator_decision_queue": [ + { + "approval_boundary": { + "market_scorecard_update_required": false, + "paid_api_approval_required": true, + "priority_upgrade_required": false, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "claude_agent_sdk_remediator", + "display_name": "Claude Agent SDK Remediator", + "evidence_refs": [ + "docs/evaluations/agent_claude_remediator_replay_summary_2026-06-02.json" + ], + "priority": 10, + "queue_status": "blocked_needs_evidence", + "recommended_action": "refresh_scorecard_then_offline_replay_or_promotion_gate", + "risk_notes": [ + "Best fit is code and DevOps remediation, not necessarily central incident arbitration.", + "API cost, subscription separation, and vendor boundary must be validated.", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": false, + "paid_api_approval_required": false, + "priority_upgrade_required": false, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "crewai_flows_crews", + "display_name": "CrewAI Flows + Crews", + "evidence_refs": [], + "priority": 10, + "queue_status": "blocked_needs_evidence", + "recommended_action": "create_no_sdk_no_api_adapter_then_offline_replay", + "risk_notes": [ + "Better for rapid automation teams than high-risk production AIOps core.", + "Durability, strict audit, and permission boundary must be proven in replay.", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": false, + "paid_api_approval_required": true, + "priority_upgrade_required": false, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "nemo_nemotron_fabric", + "display_name": "NVIDIA NeMo Agent Toolkit + Nemotron Fabric", + "evidence_refs": [ + "nvidia/llama-3.3-nemotron-super-49b-v1.5", + "docs/evaluations/agent_nemotron_contract_tuned_49b_v15_smoke_gate_2026-06-02.json", + "docs/evaluations/agent_nemotron_contract_tuned_smoke_matrix_2026-06-02.json" + ], + "priority": 10, + "queue_status": "blocked_needs_evidence", + "recommended_action": "refresh_source_evidence_then_5_record_smoke_only", + "risk_notes": [ + "Needs AWOOOI-specific HITL and dangerous-action policy integration.", + "GPU/NIM operating cost must be compared against current local inference.", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "agentos_framework", + "display_name": "AgentOS Framework", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": true, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "bernstein_agent_governance", + "display_name": "Bernstein Agent Governance", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "candidate missing from current market scorecard", + "discovery_classification_must_recommend_watch_addition", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "hermes_agent_personal_platform", + "display_name": "NousResearch Hermes Agent", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "microsoft_agent_governance_toolkit", + "display_name": "Microsoft Agent Governance Toolkit", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "pydantic_deepagents", + "display_name": "Pydantic DeepAgents", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": true, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "thclaws_agent_harness", + "display_name": "thClaws Agent Harness", + "evidence_refs": [], + "priority": 40, + "queue_status": "watch_only_blocked", + "recommended_action": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "risk_notes": [ + "candidate missing from current market scorecard", + "discovery_classification_must_recommend_watch_addition", + "changed_sources_reviewed_by_ai_controlled_verifier", + "market_scorecard_refreshed_if_primary_sources_changed_semantically", + "no_sdk_install_without_dependency_approval", + "no_paid_provider_use_without_cost_and_data_boundary_approval" + ] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "google_adk_stack", + "display_name": "Google Agent Development Kit Stack", + "evidence_refs": [], + "priority": 60, + "queue_status": "registered_no_review", + "recommended_action": "continue_weekly_primary_source_market_watch", + "risk_notes": [] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "langgraph_incident_kernel", + "display_name": "LangGraph Incident Kernel", + "evidence_refs": [ + "docs/evaluations/agent_langgraph_replay_summary_2026-06-02.json" + ], + "priority": 60, + "queue_status": "registered_no_review", + "recommended_action": "continue_weekly_primary_source_market_watch", + "risk_notes": [] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "microsoft_agent_framework", + "display_name": "Microsoft Agent Framework", + "evidence_refs": [], + "priority": 60, + "queue_status": "registered_no_review", + "recommended_action": "continue_weekly_primary_source_market_watch", + "risk_notes": [] + }, + { + "approval_boundary": { + "market_scorecard_update_required": true, + "paid_api_approval_required": false, + "priority_upgrade_required": true, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": true, + "sdk_install_approval_required": true, + "shadow_or_canary_approval_required": true + }, + "candidate_id": "openai_agents_sdk_coordinator", + "display_name": "OpenAI Agents SDK Coordinator", + "evidence_refs": [ + "docs/evaluations/agent_openai_coordinator_replay_summary_2026-06-02.json" + ], + "priority": 60, + "queue_status": "registered_no_review", + "recommended_action": "continue_weekly_primary_source_market_watch", + "risk_notes": [] + }, + { + "approval_boundary": { + "market_scorecard_update_required": false, + "paid_api_approval_required": false, + "priority_upgrade_required": false, + "production_routing_approval_required": true, + "replacement_adr_required": true, + "replay_approval_required": false, + "sdk_install_approval_required": false, + "shadow_or_canary_approval_required": false + }, + "candidate_id": "openclaw_incumbent", + "display_name": "OpenClaw incumbent", + "evidence_refs": [], + "priority": 90, + "queue_status": "baseline_protected", + "recommended_action": "keep_openclaw_as_production_decision_core_until_replay_shadow_canary_replacement_adr", + "risk_notes": [ + "no_candidate_has_formal_replacement_approval" + ] + } + ], + "policy": { + "market_scorecard_update_approved": false, + "paid_api_calls_approved": false, + "priority_upgrade_approved": false, + "production_changes_approved": false, + "replacement_decision_allowed": false, + "replay_candidate_approved": false, + "sdk_installation_approved": false, + "shadow_or_canary_approved": false, + "snapshot_is_decision_source": false + }, + "schema_version": "agent_market_governance_snapshot_v1", + "summary": { + "blocked_from_integration": 5, + "candidate_count": 13, + "changed_candidates": 5, + "eligible_for_market_scorecard_prescreen": 0, + "integration_queue_count": 5, + "market_scorecard_updates_approved": 0, + "paid_api_calls_approved": 0, + "priority_upgrades_approved": 0, + "production_changes_approved": 0, + "recommended_watch_additions_remaining": 7, + "replacement_decisions_approved": 0, + "replay_candidates_approved": 0, + "sdk_installations_approved": 0, + "shadow_or_canary_approved": 0, + "source_count": 36, + "source_failures": 0, + "watch_only_candidates_reviewed": 6 + } +} diff --git a/docs/evaluations/agent_market_watch_promotion_review_2026-06-28.json b/docs/evaluations/agent_market_watch_promotion_review_2026-06-28.json new file mode 100644 index 000000000..e1526ee20 --- /dev/null +++ b/docs/evaluations/agent_market_watch_promotion_review_2026-06-28.json @@ -0,0 +1,218 @@ +{ + "generated_at": "2026-06-28T00:47:01.200212+00:00", + "inputs": { + "candidate_registry_schema_version": "agent_replacement_candidates_v1", + "discovery_classification_generated_at": "2026-06-28T00:46:51.907924+00:00", + "integration_review_generated_at": "2026-06-26T03:41:38.289424+00:00", + "watch_report_generated_at": "2026-06-26T03:41:17.266599+00:00" + }, + "policy": { + "market_scorecard_update_approved": false, + "paid_api_calls_approved": false, + "priority_upgrade_approved": false, + "production_changes_approved": false, + "replacement_decision_allowed": false, + "replay_candidate_approved": false, + "sdk_installation_approved": false, + "shadow_or_canary_approved": false + }, + "reviews": [ + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "hermes_agent_personal_platform", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "NousResearch Hermes Agent", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "", + "latest_versions": [ + "v2026.6.19" + ], + "official_url": "https://hermes-agent.nousresearch.com", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "personal_agent_platform_candidate", + "source_count": 2, + "source_failures": 0 + }, + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "microsoft_agent_governance_toolkit", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "Microsoft Agent Governance Toolkit", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "", + "latest_versions": [ + "v4.1.0" + ], + "official_url": "https://microsoft.github.io/agent-governance-toolkit/", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_governance_policy_evaluator_candidate", + "source_count": 2, + "source_failures": 0 + }, + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "thclaws_agent_harness", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "thClaws Agent Harness", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "watch_only_primary_source_monitoring", + "latest_versions": [ + "v0.75.0" + ], + "official_url": "https://thclaws.ai", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "source_count": 2, + "source_failures": 0 + }, + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "pydantic_deepagents", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "Pydantic DeepAgents", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "", + "latest_versions": [ + "0.3.31" + ], + "official_url": "https://vstorm-co.github.io/pydantic-deepagents/", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "source_count": 2, + "source_failures": 0 + }, + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "integration_review_must_confirm_watch_only_stage", + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "agentos_framework", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "AgentOS Framework", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "", + "latest_versions": [ + "v0.9.79" + ], + "official_url": "https://agentos.sh", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_framework_or_orchestrator_candidate", + "source_count": 2, + "source_failures": 0 + }, + { + "approved_for_paid_api_calls": false, + "approved_for_replay": false, + "approved_for_sdk_install": false, + "approved_for_shadow_or_canary": false, + "blockers": [ + "discovery_classification_must_recommend_watch_addition" + ], + "candidate_id": "bernstein_agent_governance", + "classification": { + "classification": null, + "recommendation": null, + "repository_full_name": null, + "risk_flags": [], + "watch_addition_recommended": false + }, + "decision": "remain_watch_only_until_evidence_gap_resolved", + "display_name": "Bernstein Agent Governance", + "eligible_for_market_scorecard_prescreen": false, + "integration_stage": "watch_only_primary_source_monitoring", + "latest_versions": [ + "v2.7.0" + ], + "official_url": "https://bernstein.run", + "release_version_observed": true, + "required_next_gate": "continue_watch_only_until_primary_source_evidence_is_sufficient", + "role": "agent_governance_policy_evaluator_candidate", + "source_count": 2, + "source_failures": 0 + } + ], + "schema_version": "agent_market_watch_promotion_review_v1", + "summary": { + "eligible_for_market_scorecard_prescreen": 0, + "market_scorecard_updates_approved": 0, + "paid_api_calls_approved": 0, + "priority_upgrades_approved": 0, + "production_changes_approved": 0, + "remain_watch_only": 6, + "replay_candidates_approved": 0, + "sdk_installations_approved": 0, + "shadow_or_canary_approved": 0, + "watch_only_candidates_reviewed": 6 + } +} diff --git a/docs/schemas/agent_market_discovery_review_v1.schema.json b/docs/schemas/agent_market_discovery_review_v1.schema.json index 4fbcf3a8d..3d46515a4 100644 --- a/docs/schemas/agent_market_discovery_review_v1.schema.json +++ b/docs/schemas/agent_market_discovery_review_v1.schema.json @@ -64,8 +64,8 @@ "discovered_items", "unique_repositories", "already_watched_or_registered", - "manual_classification_required", - "new_manual_classification_required", + "controlled_classification_queue", + "new_controlled_classification_queue", "source_failures", "auto_registry_additions_approved", "production_changes_approved", @@ -76,8 +76,8 @@ "discovered_items": {"type": "integer", "minimum": 0}, "unique_repositories": {"type": "integer", "minimum": 0}, "already_watched_or_registered": {"type": "integer", "minimum": 0}, - "manual_classification_required": {"type": "integer", "minimum": 0}, - "new_manual_classification_required": {"type": "integer", "minimum": 0}, + "controlled_classification_queue": {"type": "integer", "minimum": 0}, + "new_controlled_classification_queue": {"type": "integer", "minimum": 0}, "source_failures": {"type": "integer", "minimum": 0}, "auto_registry_additions_approved": {"type": "integer", "const": 0}, "production_changes_approved": {"type": "integer", "const": 0}, diff --git a/docs/schemas/agent_market_governance_snapshot_v1.schema.json b/docs/schemas/agent_market_governance_snapshot_v1.schema.json index 982531f4f..b7cfd4845 100644 --- a/docs/schemas/agent_market_governance_snapshot_v1.schema.json +++ b/docs/schemas/agent_market_governance_snapshot_v1.schema.json @@ -73,7 +73,7 @@ "next_scheduled_run_at", "trigger_modes", "primary_source_policy", - "operator_review_gate" + "controlled_review_gate" ], "properties": { "workflow": { @@ -101,7 +101,7 @@ "type": "string", "minLength": 1 }, - "operator_review_gate": { + "controlled_review_gate": { "type": "string", "minLength": 1 } @@ -115,14 +115,15 @@ "freshness_sla_hours", "stale_grace_hours", "stale_after", - "source_failures_block_priority_upgrade", + "source_failures_require_controlled_reverify", "blocked_from_integration", + "controlled_queue_reasons", "operator_blockers" ], "properties": { "status": { "type": "string", - "enum": ["healthy", "blocked"] + "enum": ["healthy", "controlled_queue", "blocked"] }, "freshness_sla_hours": { "type": "integer", @@ -136,6 +137,9 @@ "type": "string", "minLength": 1 }, + "source_failures_require_controlled_reverify": { + "type": "boolean" + }, "source_failures_block_priority_upgrade": { "type": "boolean" }, @@ -143,6 +147,10 @@ "type": "integer", "minimum": 0 }, + "controlled_queue_reasons": { + "type": "array", + "items": {"type": "string", "minLength": 1} + }, "operator_blockers": { "type": "array", "items": {"type": "string", "minLength": 1} @@ -316,8 +324,8 @@ "enum": [ "baseline_protected", "blocked_needs_evidence", - "operator_review_required", - "operator_priority_review", + "controlled_review_required", + "controlled_priority_review", "watch_only_blocked", "watch_only_monitoring", "registered_no_review" From 64f52017b8e4b43f7c1fabbf6fda443b7150d68b Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 08:54:55 +0800 Subject: [PATCH 6/6] chore(cd): deploy d4c2cc6 [skip ci] --- k8s/awoooi-prod/kustomization.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index 81bb3e659..da429a27e 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -41,7 +41,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: 3a528138829701a1dafb2a21003ed014c9f9383b + newTag: d4c2cc6e200fc00e07d179ebb9a4a156cef2c6d5 - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: 3a528138829701a1dafb2a21003ed014c9f9383b + newTag: d4c2cc6e200fc00e07d179ebb9a4a156cef2c6d5