Compare commits

..
Author SHA1 Message Date
Steel Dan (AIOS) 8160c2b2f6 docs(weixin): record quoted text reference fix 2026-07-12 01:00:14 +08:00
Steel Dan (AIOS) 706c0b0096 fix(weixin): ignore unconfirmed text references 2026-07-12 00:22:36 +08:00
Steel Dan (AIOS) 2c08a9d0cf docs(weixin): update confirmed image reference recovery 2026-07-11 17:54:49 +08:00
Steel Dan (AIOS) 3d9782fc34 fix(weixin): require confirmed image reference context 2026-07-11 17:36:00 +08:00
Steel Dan (AIOS) 4a9c5138af docs(weixin): record text cache isolation recovery 2026-07-11 16:46:58 +08:00
Steel Dan (AIOS) 2de29c59bb fix(weixin): add process-only runtime patch mode 2026-07-11 15:23:36 +08:00
Steel Dan (AIOS) 66d637fc03 fix(weixin): isolate text from stale image cache 2026-07-11 14:24:06 +08:00
Steel Dan (AIOS) d19b307fff Apply safe-mode gate, h_branch_sync, sentinel wiring, create_patch fix
- safe_mode: check_safe_mode() raises if instability thresholds exceeded
- h_branch_sync: use git ls-remote + SQLite cache, no Gitea API dependency
- check_sentinel_drift: detect missing/inconsistent gitea-branch-consistency sentinel
- create_patch: return dict instead of bool for caller visibility
- tests: adapt test_autonomous_controller to new return signatures
- add: test_evolution_engine.py for core engine cycles and regression
2026-07-10 13:04:16 +08:00
Steel Dan (AIOS) 59565ebc25 feat(sr01): add consistency sentinel, safe-mode gate, and test suite
- Integrate gitea_branch_consistency_sentinel.py (bare repo refs ↔ Gitea SQLite ↔ API)
- Integrate autonomous_safe_mode.py for controller mutation gate
- Update autonomous_controller.py with safe-mode checks before writes
- Add tests/test_autonomous_controller.py (11 tests covering h_branch_sync, h_stuck, apply_approved_patches, diagnostics)
2026-07-10 09:19:28 +08:00
Steel Dan (AIOS) fd7abd84ba Merge remote-tracking branch 'origin/feature/gitea-reliability-guardrails-20260710' into feature/v1r4c4-sr01-gitea-branch-sync-fix-20260710 2026-07-10 09:18:57 +08:00
openclaw 12d09b1dea proposal: start V1R4C4 SR-01 Gitea branch sync fix 2026-07-10 01:42:12 +08:00
openclaw 6905629268 ops: add Gitea consistency sentinel timer unit 2026-07-10 01:27:26 +08:00
openclaw 44de4b9e7d feat: add autonomous safe mode mutation gate 2026-07-10 01:27:22 +08:00
openclaw dbc482e0c1 feat: add Gitea branch consistency sentinel 2026-07-10 01:27:18 +08:00
openclaw f6d9bb1510 Promote baseline to V1R4C3
- baseline/current.json updated from V1R3C1 to V1R4C3
- Fixed test_single_provider_failure_is_not_terminal (provider_order override)
- All 38 tests passing

AIOS_V1R4C3_BASELINE_PROMOTED
AIOS_BASELINE_CURRENT_UPDATED
2026-07-09 17:45:09 +08:00
openclaw 8ca4db3e8c Close completed autonomous controller CMD
- Moved CMD-20260709-enable-autonomous-controller.md from active to completed
- All autonomous controller tasks verified complete

AIOS_AUTONOMOUS_CONTROLLER_CMD_CLOSED
AIOS_RUNNING_COMMAND_QUEUE_CLEAN
2026-07-09 17:38:04 +08:00
openclaw d194d3a4d7 Add Owner Gate Web UI panels with approve/reject functionality 2026-07-09 17:23:23 +08:00
openclaw f5435e3087 release: version bump to V1R4C3 2026-07-09 16:13:42 +08:00
gito 88087dc7ff Merge pull request 'V1R4C1 review fixes: arch change API, tests, owner gate, governance' (#3) from patch/v1r4c1-review-risk-fixes into main 2026-07-09 16:13:08 +08:00
openclaw 2f04b9f46c release: version bump to V1R4C2 2026-07-09 16:10:29 +08:00
openclaw 06d3b4cda6 Add Task Center V1R4C2 proposal 2026-07-09 14:55:22 +08:00
openclaw 09186fe551 Add Task Center policy acceptance tests 2026-07-09 14:55:16 +08:00
openclaw c634f2e23b Add Task Center governance policy 2026-07-09 14:55:11 +08:00
openclaw b3ccae4c2b Add Task Center visual dashboard deployment note 2026-07-09 14:55:08 +08:00
openclaw 85ac09110f Add Task Dashboard view tests 2026-07-09 14:52:01 +08:00
openclaw 6622ebb30e Add Task Center visualization model 2026-07-09 14:51:58 +08:00
openclaw 824b079fea Add Task Center source sync deployment command 2026-07-09 14:51:51 +08:00
openclaw 2e5a8bfb5a Add Task orchestrator tests 2026-07-09 14:48:48 +08:00
openclaw ce36b790ea Add Task Center orchestrator 2026-07-09 14:48:44 +08:00
openclaw 8d60aa921a Add Task Center source ingestor 2026-07-09 14:48:33 +08:00
openclaw 78ce96a8db Add OpenClaw command for complete Task Center closed loop 2026-07-09 14:45:26 +08:00
openclaw ce308f84ba Add Task Center event and statistics tests 2026-07-09 14:45:22 +08:00
openclaw 7459f24696 Add Task Center statistics 2026-07-09 14:45:18 +08:00
openclaw 8f90744ba0 Add Task Center persistent store 2026-07-09 14:45:14 +08:00
openclaw 4db407345c Add Task Center event bridge 2026-07-09 14:45:09 +08:00
openclaw 4a09eacfe4 Add OpenClaw command for Task Center governance upgrade 2026-07-09 14:39:12 +08:00
openclaw a9e1b26652 Add task lifecycle tests 2026-07-09 14:39:08 +08:00
openclaw 513d90bb8a Add Task Center action API 2026-07-09 14:39:05 +08:00
openclaw d03f06aa82 Add governed task lifecycle state machine 2026-07-09 14:39:00 +08:00
openclaw c657057826 Add OpenClaw command for Task Center Web UI 2026-07-09 14:36:42 +08:00
openclaw 84717341d7 Add Task Center tests 2026-07-09 14:36:37 +08:00
openclaw f631a61330 Add Task Center API 2026-07-09 14:36:32 +08:00
openclaw 38222c9fec Add AIOS Task Center domain model 2026-07-09 14:36:24 +08:00
openclaw 055fc895a6 Dispatch Search Gateway production deployment to OpenClaw 2026-07-09 14:26:10 +08:00
openclaw 8ab38f142a Add OpenClaw command for architecture doc permission fix 2026-07-09 14:08:02 +08:00
openclaw 1b3921f9c8 Add Search Architect integration tests 2026-07-09 14:07:52 +08:00
openclaw c15453db1a Add Search Gateway Architect integration 2026-07-09 14:07:46 +08:00
openclaw 83641c2491 Add V1R4C2 Search Gateway capability proposal 2026-07-09 14:04:02 +08:00
openclaw e1f4d1f267 Add Search agent adapter tests 2026-07-09 14:03:59 +08:00
openclaw 00d37b2d1a Add Search Gateway agent adapters 2026-07-09 14:03:53 +08:00
openclaw 887061e65b Add complete Search Gateway runtime deployment command 2026-07-09 13:27:49 +08:00
openclaw bb2b301c4e Add Search Gateway orchestrator tests 2026-07-09 13:27:45 +08:00
openclaw 2cc1acebd2 Add Search Gateway runtime orchestrator 2026-07-09 13:27:42 +08:00
openclaw 9dd2d33cbb Add Search Gateway event bridge 2026-07-09 13:27:37 +08:00
openclaw f648e19d4d Add Search provider registry tests 2026-07-09 13:26:37 +08:00
openclaw 9793239962 Add Search Gateway audit store 2026-07-09 13:26:34 +08:00
openclaw 81d5744e8e Add Search Gateway provider registry 2026-07-09 13:26:28 +08:00
openclaw 5d37ecfff0 Add OpenClaw deployment command for general Search Gateway 2026-07-09 12:48:23 +08:00
openclaw 64577a1eb8 Expose Search Gateway overview API 2026-07-09 12:48:14 +08:00
openclaw 720a7026b4 Generalize Search Gateway policy for group and user customization 2026-07-09 12:48:06 +08:00
openclaw e4a83388ae Add general Search Gateway tests 2026-07-09 12:47:54 +08:00
openclaw 2e9b749ce2 Generalize Search Gateway with profile-based query planning 2026-07-09 12:47:46 +08:00
openclaw a074b13a7a Add general Search Gateway profile resolver 2026-07-09 12:47:23 +08:00
openclaw 16877032ac Add OpenClaw deployment command for Search Gateway 2026-07-09 12:45:14 +08:00
openclaw 21dd8d10c0 Add Search Gateway dashboard tests 2026-07-09 12:45:05 +08:00
openclaw a2e9dab162 Add Search Gateway overview API 2026-07-09 12:45:01 +08:00
openclaw 99d49b5d85 Add Search Gateway fallback and compliance tests 2026-07-09 12:44:55 +08:00
openclaw 024bfc52f2 Add Search Gateway dashboard model 2026-07-09 12:44:45 +08:00
openclaw 4588cdf2d9 Add Search Gateway governance policy 2026-07-09 12:44:35 +08:00
openclaw 81147e44bb Add OpenClaw deployment command for Search Gateway 2026-07-09 12:41:12 +08:00
openclaw 0f41034b78 Add Search Gateway tests 2026-07-09 12:41:04 +08:00
openclaw 2c5d9995f9 Add Search Gateway governance policy 2026-07-09 12:40:55 +08:00
openclaw aa71ce9062 Add Search Gateway dashboard model 2026-07-09 12:40:49 +08:00
openclaw 967ef3bb3c Add Search Gateway API 2026-07-09 12:40:41 +08:00
openclaw d05055abb6 Add governed Search Gateway core 2026-07-09 12:40:35 +08:00
openclaw a905b92fcc Create governed Search Gateway capability branch 2026-07-09 12:40:14 +08:00
gito 797e301968 Merge pull request 'chore: merge agent-observability-webui patch branch' (#2) from patch/agent-observability-webui into main 2026-07-09 12:32:29 +08:00
openclaw d8a426cea1 Clean stale runtime state before PR #2 merge 2026-07-09 12:27:58 +08:00
openclaw d5ded1e48a release: version bump to V1R4C2 2026-07-09 12:27:04 +08:00
openclaw 22d48bad4d v1r4c1: batch commit - arch change api + tests + owner gate 2026-07-09 12:12:02 +08:00
openclaw 99c50464d9 Add architecture change governance dashboard 2026-07-09 11:49:30 +08:00
openclaw d0141450fe Add OpenClaw command for tiered architecture owner gate 2026-07-09 11:47:58 +08:00
openclaw 84c67fa743 Add architecture change classification tests 2026-07-09 11:47:55 +08:00
openclaw d0536dbe14 Update owner gate to support small auto and large approval 2026-07-09 11:47:53 +08:00
openclaw 4177a22936 Add architecture change classifier for owner gate levels 2026-07-09 11:47:48 +08:00
openclaw 5c38b23810 Add OpenClaw command for architecture owner gate 2026-07-09 11:42:54 +08:00
openclaw aa0f721ce8 Add architecture owner gate tests 2026-07-09 11:42:51 +08:00
openclaw 2e94439f49 Add architecture owner gate policy 2026-07-09 11:42:48 +08:00
openclaw 961c45c825 Add owner approval gate for architecture changes 2026-07-09 11:42:43 +08:00
openclaw 20f1f9820d Add OpenClaw command for architect research timeline UI 2026-07-09 11:39:28 +08:00
openclaw a6f08472dd Add architect research timeline tests 2026-07-09 11:39:22 +08:00
openclaw 9f95029225 Add architect research timeline API 2026-07-09 11:39:13 +08:00
openclaw 4ad4651ffa Add architect research timeline dashboard model 2026-07-09 11:39:05 +08:00
openclaw c0d008fae7 Add OpenClaw command for architect weekly research 2026-07-09 11:33:25 +08:00
openclaw 1974327e32 Add architect research policy 2026-07-09 11:33:19 +08:00
openclaw 110817b60c Add Architect weekly research tests 2026-07-09 11:33:12 +08:00
openclaw 95070ea029 Add Architect research dashboard model 2026-07-09 11:33:09 +08:00
openclaw 44aea3a9a1 Add Architect weekly external research agent 2026-07-09 11:33:03 +08:00
openclaw ce42691be5 Add OpenClaw command for governance timeline UI 2026-07-09 11:31:36 +08:00
openclaw 6f823de67d Add governance timeline tests 2026-07-09 11:31:32 +08:00
openclaw 7443fc7f26 Add governance timeline API 2026-07-09 11:31:28 +08:00
openclaw e1d484655f Add governance timeline dashboard model 2026-07-09 11:31:24 +08:00
openclaw 362a41bff8 Add OpenClaw command for governance event loop 2026-07-09 11:26:22 +08:00
openclaw 62974e879e Add governance event bridge tests 2026-07-09 11:26:19 +08:00
openclaw cd713cd73a Add governance event bus policy 2026-07-09 11:26:16 +08:00
openclaw bd8afc3cc4 Bridge governance agents into unified event bus 2026-07-09 11:26:12 +08:00
openclaw 1688639d7d Add OpenClaw command for architect governance UI 2026-07-09 11:21:41 +08:00
openclaw 60d07f9b17 Add architect governance tests 2026-07-09 11:21:38 +08:00
openclaw 824ff5dc90 Add Architect Governance API 2026-07-09 11:21:35 +08:00
openclaw 3d27f2eb43 Add Architect Governance Web UI dashboard model 2026-07-09 11:21:30 +08:00
openclaw a168610826 Add OpenClaw command for architect governance loop 2026-07-09 11:20:25 +08:00
openclaw 5447476b08 Add architect output document template 2026-07-09 11:20:22 +08:00
openclaw e58d99ba57 Add architect agent governance policy 2026-07-09 11:20:18 +08:00
openclaw 51d6f7e0c2 Extend architect agent governance responsibilities 2026-07-09 11:20:13 +08:00
openclaw 40459af09e Add OpenClaw command for change governance UI 2026-07-09 11:11:17 +08:00
openclaw c7dc61597a Add change governance dashboard tests 2026-07-09 11:11:14 +08:00
openclaw 43d55ac3b1 Add change governance dashboard API 2026-07-09 11:11:11 +08:00
openclaw 736e981005 Add change governance Web UI dashboard model 2026-07-09 11:11:06 +08:00
openclaw b84d6be3ca Add OpenClaw command for change governance agents 2026-07-09 11:09:56 +08:00
openclaw eff4940e9c Add change governance policy for acceptance agents 2026-07-09 11:09:52 +08:00
openclaw 0d6f2ce68d Add independent architecture optimization agent 2026-07-09 11:09:49 +08:00
openclaw a654978813 Add independent user and architecture acceptance agents 2026-07-09 11:09:45 +08:00
openclaw a399e30a8e Add change acceptance governance gate 2026-07-09 11:09:40 +08:00
openclaw e0644c0297 Add OpenClaw validation command for V1R4C1 fixes 2026-07-09 11:06:46 +08:00
openclaw 37eed9df7d Add V1R4C1 risk fix tests 2026-07-09 11:06:42 +08:00
openclaw 22582b834f Move model list from hardcode to configurable registry 2026-07-09 11:06:38 +08:00
openclaw 8a0c5f0556 Add Git repository path resolver and validator 2026-07-09 11:06:33 +08:00
openclaw 62b10d89c2 Create V1R4C1 review risk fixes branch 2026-07-09 11:06:26 +08:00
openclaw 741206597b Merge branch 'patch/agent-observability-webui' 2026-07-09 11:03:37 +08:00
openclaw 36755f5f8b fix: update BARE_REPO path from aios-v23.git to aios.git 2026-07-09 11:03:29 +08:00
openclaw fe36cc9241 feat(version): define V1R4C1 release version
- Group-owner-agent-context-isolation
- Agent Observability complete loop
- Agent Event Bus unified platform
- Agent Command Plane and Audit Plane
- OpenAPI workspace permission fix
2026-07-09 10:59:35 +08:00
gito c1e05f7c4a feat: merge group-owner-agent-context-isolation patch
feat: deploy group-owner-agent-context-isolation
2026-07-09 10:52:27 +08:00
openclaw eb4a3adb3f feat: deploy group-owner-agent-context-isolation patch
- Add agent context isolation API, RBAC, resolver, audit, dashboard modules
- Deploy context isolation policy config
- Fix OpenAPI workspace permissions (chown aiman:gitops)
- Update ChatGPT actions API with new internals
- Add CMD files for group/owner isolation and permission fix
- Archive completed command files
2026-07-09 10:45:21 +08:00
openclaw f1ebd2cf99 Add OpenClaw hook wiring command for agent observability 2026-07-09 10:16:30 +08:00
openclaw cb3b5f1e01 Add closed loop agent observability tests 2026-07-09 10:16:27 +08:00
openclaw 6fc538bf50 Add Agent Event Bus observability bridge 2026-07-09 10:16:23 +08:00
openclaw 665c33f69a Add runtime hooks for agent observability 2026-07-09 10:16:17 +08:00
openclaw 880591fc00 Add OpenClaw command for complete agent observability loop 2026-07-09 10:14:28 +08:00
openclaw 68060fbcba Add agent observability API 2026-07-09 10:14:24 +08:00
openclaw db1ebc29e0 Add persistent agent observability store 2026-07-09 10:14:18 +08:00
openclaw 93b8709c36 Add OpenClaw command for agent observability dashboard 2026-07-09 10:02:45 +08:00
openclaw 7900c4df2e Add agent observability policy 2026-07-09 10:02:40 +08:00
openclaw 8b6fb1c06e Add agent observability Web UI dashboard model 2026-07-09 10:02:35 +08:00
openclaw de8b996dea Add OpenClaw Hermes agent observability model 2026-07-09 10:02:29 +08:00
openclaw eb8d229f28 Create agent observability web UI patch branch 2026-07-09 10:02:20 +08:00
openclaw c93f99da41 feat: add AI evolution dashboard API endpoint
- Serve HTML dashboard at /admin/ai-evolution/
- API endpoint /admin/ai-evolution/api/data returning baseline, versions, commands, agent_runs
- Background agent runner base class for autonomous file operations
- Chat API sync service with real-time message polling and task execution
- Add admin route to aios_chatgpt_actions_api.py, fix missing start_time variable
2026-07-09 00:24:23 +08:00
openclaw c2e782e6fa fix: add admin/ai-evolution route to API, fix missing start_time
- Inserted /admin/ai-evolution endpoint returning baseline, versions, commands, agent_runs
- Added helper functions: baselines(), commands(), agent_runs()
- Added missing start_time module-level variable
- This enables the Web UI dashboard for monitoring AIOS GitOps state
2026-07-09 00:07:16 +08:00
openclaw 7fd4a4aae3 Merge: resolve conflict in aios_chatgpt_actions_api.py, remove patches conflict 2026-07-08 21:07:54 +08:00
openclaw e4bcf47f79 feat(cmd): enable Agent Command Plane, audit plane, and Web UI dashboard
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
CMD-BOOTSTRAP-001-enable-command-plane

- Add admin AI evolution dashboard (Web UI)
- Add Agent Command Plane structure (agents/, cmd/)
- Add audit plane with agent-run records
- Add architecture documentation
- Fix gitops-write-bot token authentication

Baseline: V1R3C1
2026-07-08 21:07:54 +08:00
openclaw 00cc1d9b6d feat(cmd): add final report for autonomous operation enablement
- Add evidence/final-report.md documenting all deliverables
- Web UI dashboard confirmed running at /admin/ai-evolution

Baseline: V1R3C1
2026-07-08 17:24:21 +08:00
openclaw da67ec9714 feat(cmd): enable autonomous operation and notification system
CMD-20260708-enable-aios-autonomous-operation-and-notification

- Add autonomous controller daemon (agents/scripts/autonomous_controller.py)
- Add agent-run audit record (openclaw-20260708-003)
- Register ai-evolution Web UI routes in API service
- Update baseline/promotions/ with V1R4C0-test owner approval
- Update version-history/ with V1R4C0-test promotion record

Components:
  - agents/scripts/autonomous_controller.py - health checks, auto-patching, state mgmt
  - active-runtime/app/aios_chatgpt_actions_api.py - admin/ai-evolution routes
  - audit/agent-runs/ - agent execution audit trail

Baseline: V1R3C1
2026-07-08 17:11:56 +08:00
openclaw 2e93ed904a Add OpenClaw deployment command for unified Agent Event Bus 2026-07-08 16:51:02 +08:00
openclaw b901fa3a5a Add unified Agent Event Bus tests 2026-07-08 16:50:24 +08:00
openclaw b9778f1f69 Add unified Agent Event Bus OpenAPI operation ids 2026-07-08 16:50:07 +08:00
openclaw 41c4c4f003 Add unified Agent Event Bus configuration 2026-07-08 16:49:57 +08:00
openclaw d119c2d87c Add unified Agent Event Bus admin dashboard model 2026-07-08 16:49:31 +08:00
openclaw 0a143c6b98 Add OpenClaw deploy command for unified Agent Event Bus 2026-07-08 16:37:04 +08:00
openclaw 7ef76b739f Add unified Agent Event Bus configuration 2026-07-08 16:37:00 +08:00
openclaw a46878f9b8 Add unified Agent Event Bus admin dashboard 2026-07-08 16:36:56 +08:00
openclaw f73c7946d0 Add unified Agent Event Bus Actions API with compatibility adapters 2026-07-08 16:36:53 +08:00
openclaw ecd7ec40dd Implement unified Agent Event Bus 2026-07-08 16:36:47 +08:00
openclaw 82bf1bd118 Create unified agent event bus implementation branch 2026-07-08 16:36:38 +08:00
openclaw ac89ba2576 chore: create next command CMD-20260709-enable-autonomous-controller 2026-07-08 16:19:17 +08:00
openclaw c9f1c28616 chore: add evidence for CMD-20260708-enable-aios-autonomous-operation-and-notification 2026-07-08 16:15:16 +08:00
openclaw c796347655 chore(chatgpt): create patch/chatgpt-br-1783496324783-496c38aa 2026-07-08 16:14:39 +08:00
openclaw 9dad8b57a7 feat(cmd): implement Agent Command Plane and Audit Plane (V1R3C1)
CMD-20260708-fix-git-actions-write-and-enable-audit-plane

- Fix: gitops.tokens gitops-write-bot SHA256 hash sync
- Add: agents/ command registry + directory structure
- Add: audit/ full execution audit plane
- Add: evidence/ CMD report files
- Add: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md

Closes CMD-20260708. Next: CMD-20260709-autonomous-controller-enable
2026-07-08 15:38:18 +08:00
aios ff40a89987 chore(chatgpt): update test/cmd-permission-check.txt 2026-07-08 15:33:52 +08:00
aios 8fd86d6100 chore(baseline): promote baseline to V1R3C1
V1R3C1 is the self-loop autonomous engine corrective release.
V1R3C0 remains infrastructure fix only.
Previous baseline: V1R2C3
2026-07-08 14:36:16 +08:00
245 changed files with 13128 additions and 3389 deletions
+1
View File
@@ -10,3 +10,4 @@ node_modules/
runtime/tmp/
runtime/cache/
logs/
config/test_write_perms
+1 -1
View File
@@ -1 +1 @@
V1R1C4
V1R4C3
-93
View File
@@ -1,93 +0,0 @@
"""Adaptive policy engine for self-loop optimization.
Optimizes retry, cooldown, and low-risk automation choices from failure history.
It never disables owner gates or security controls.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any
from .failure_memory import FailureMemory
@dataclass(frozen=True)
class AdaptiveDecision:
retry_limit: int
cooldown_seconds: int
automation_level: str
risk_level: str
owner_approval_required: bool
reasons: tuple[str, ...]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class AdaptivePolicy:
def __init__(self, default_retry_limit: int = 3, default_cooldown_seconds: int = 30):
self.default_retry_limit = default_retry_limit
self.default_cooldown_seconds = default_cooldown_seconds
def evaluate(
self,
memory: FailureMemory,
runtime: dict[str, Any],
baseline: dict[str, Any],
goal: str,
) -> AdaptiveDecision:
reasons: list[str] = []
retry_limit = self.default_retry_limit
cooldown = self.default_cooldown_seconds
risk = "low"
automation = "safe"
top = memory.top_failures(1)
if top:
score = float(top[0].get("recurrence_score", 0))
if score >= 0.7:
retry_limit = 1
cooldown = 300
risk = "high"
automation = "analysis_only"
reasons.append("recurrent failure detected; reduce automatic retries")
elif score >= 0.4:
cooldown = 120
reasons.append("failure recurrence increasing; slow down loop")
if runtime.get("status") != "active":
risk = "high"
automation = "analysis_only"
reasons.append("runtime not healthy")
if baseline.get("codex_gitops", {}).get("direct_main_push"):
risk = "high"
automation = "analysis_only"
reasons.append("direct main push policy drift")
owner_gate = True
reasons.append("owner gate preserved")
if not reasons:
reasons.append(f"optimize policy for goal: {goal}")
return AdaptiveDecision(
retry_limit=retry_limit,
cooldown_seconds=cooldown,
automation_level=automation,
risk_level=risk,
owner_approval_required=owner_gate,
reasons=tuple(reasons),
)
def apply_to_scheduler(self, decision: AdaptiveDecision) -> dict[str, Any]:
return {
"max_attempts": decision.retry_limit,
"cooldown_seconds": decision.cooldown_seconds,
"automation_level": decision.automation_level,
"owner_approval_required": decision.owner_approval_required,
}
__all__ = ["AdaptiveDecision", "AdaptivePolicy"]
+56
View File
@@ -0,0 +1,56 @@
"""API adapter for agent context routing and visibility."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .agent_context_dashboard import AgentContextDashboard
from .agent_context_rbac import AgentContextRBAC
from .agent_context_resolver import AgentContextResolver
resolver = AgentContextResolver()
rbac = AgentContextRBAC()
dashboard = AgentContextDashboard()
def resolve_agent_context(payload: dict[str, Any]) -> dict[str, Any]:
route = rbac.route(payload["user_id"], payload.get("group_id"), payload.get("role", "user"))
context = resolver.resolve(
user_id=payload["user_id"],
group_id=payload.get("group_id", ""),
session=payload.get("session"),
user_profile=payload.get("user_profile"),
group_policy=payload.get("group_policy"),
agent_default=payload.get("agent_default"),
owner_policy=payload.get("owner_policy"),
owner=payload.get("role") == "owner",
)
return {"routing": route.to_dict(), "context": context.to_dict()}
def get_context_dashboard() -> dict[str, Any]:
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/context", tags=["context-isolation"])
@router.post("/resolve", operation_id="resolveAgentContext")
def resolve(payload: dict[str, Any]):
return resolve_agent_context(payload)
@router.get("/overview", operation_id="getAgentContextDashboard")
def overview():
return get_context_dashboard()
return router
router = build_router()
__all__ = ["router", "resolve_agent_context", "get_context_dashboard"]
+23
View File
@@ -0,0 +1,23 @@
"""Audit context resolution decisions."""
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
class AgentContextAudit:
def record(self, context: dict[str, Any]) -> dict[str, Any]:
return {
"event": "context_loaded",
"agent_id": context.get("agent_id"),
"user_id": context.get("user_id"),
"group_id": context.get("group_id"),
"layers": [x.get("name") for x in context.get("layers", [])],
"timestamp": utc_now(),
}
__all__ = ["AgentContextAudit"]
@@ -0,0 +1,47 @@
"""Web UI model for context isolation visibility."""
from __future__ import annotations
from typing import Any
class AgentContextDashboard:
def overview(self, contexts: list[dict[str, Any]] | None = None) -> dict[str, Any]:
contexts = contexts or []
return {
"module": "agent-context-isolation",
"route": "/admin/ai-evolution/context",
"title": "Agent Context Isolation",
"panels": [
{
"name": "agent-routing",
"title": "Agent 路由",
"data": contexts,
},
{
"name": "context-layers",
"title": "Context 来源层级",
"layers": [
"safety_policy",
"owner_policy",
"group_policy",
"role_policy",
"user_profile",
"session_context",
"agent_default",
],
},
{
"name": "audit",
"title": "Context 审计",
"events": [
"context_loaded",
"profile_updated",
"agent_selected",
],
},
],
}
__all__ = ["AgentContextDashboard"]
+55
View File
@@ -0,0 +1,55 @@
"""RBAC binding for group main agents and owner agents."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any
@dataclass(frozen=True)
class AgentRoutingDecision:
user_id: str
group_id: str
role: str
agent_id: str
scope: str
allowed: bool
reason: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class AgentContextRBAC:
"""Routes users to group agents and owners to dedicated owner agents."""
VALID_ROLES = {"user", "group_admin", "owner"}
def route(self, user_id: str, group_id: str | None, role: str) -> AgentRoutingDecision:
if role not in self.VALID_ROLES:
return AgentRoutingDecision(user_id, group_id or "", role, "", "none", False, "unknown_role")
if role == "owner":
return AgentRoutingDecision(
user_id=user_id,
group_id=group_id or "global",
role=role,
agent_id=f"owner-agent:{user_id}",
scope="global",
allowed=True,
reason="owner_dedicated_agent",
)
if not group_id:
return AgentRoutingDecision(user_id, "", role, "", "none", False, "group_required")
scope = "group" if role == "group_admin" else "self"
return AgentRoutingDecision(
user_id=user_id,
group_id=group_id,
role=role,
agent_id=f"group-main-agent:{group_id}",
scope=scope,
allowed=True,
reason="group_agent_routing",
)
__all__ = ["AgentRoutingDecision", "AgentContextRBAC"]
@@ -0,0 +1,82 @@
"""Multi-user agent context resolver.
Architecture:
- shared agent kernel
- group main agent
- owner dedicated agent
- user profile overlay
- session overlay
Personal preferences never enter shared USER.md.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ContextLayer:
name: str
priority: int
data: dict[str, Any]
@dataclass
class EffectiveContext:
agent_id: str
user_id: str
group_id: str
layers: list[ContextLayer]
resolved: dict[str, Any]
resolved_at: str = field(default_factory=utc_now)
def to_dict(self):
return {
"agent_id": self.agent_id,
"user_id": self.user_id,
"group_id": self.group_id,
"layers": [asdict(x) for x in self.layers],
"resolved": self.resolved,
"resolved_at": self.resolved_at,
}
class AgentContextResolver:
"""Resolves context without shared user-state pollution."""
def resolve(
self,
user_id: str,
group_id: str,
session: dict[str, Any] | None = None,
user_profile: dict[str, Any] | None = None,
group_policy: dict[str, Any] | None = None,
agent_default: dict[str, Any] | None = None,
owner_policy: dict[str, Any] | None = None,
owner: bool = False,
) -> EffectiveContext:
layers = [
ContextLayer("agent_default", 10, agent_default or {}),
ContextLayer("group_policy", 30, group_policy or {}),
ContextLayer("user_profile", 50, user_profile or {}),
ContextLayer("session", 70, session or {}),
]
agent_id = f"group-main-agent:{group_id}"
if owner:
agent_id = f"owner-agent:{user_id}"
layers.append(ContextLayer("owner_policy", 90, owner_policy or {}))
resolved: dict[str, Any] = {}
# Higher priority overlays lower priority except safety policies handled upstream.
for layer in sorted(layers, key=lambda x: x.priority):
resolved.update(layer.data)
return EffectiveContext(agent_id, user_id, group_id, layers, resolved)
__all__ = ["ContextLayer", "EffectiveContext", "AgentContextResolver"]
+77
View File
@@ -0,0 +1,77 @@
"""Actions API for unified Agent Event Bus."""
from __future__ import annotations
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .agent_event_bus import AgentEventBus
bus = AgentEventBus()
def create_agent_event(payload: dict[str, Any]) -> dict[str, Any]:
return {"status": "ok", "event": bus.publish(**payload)}
def list_agent_events(kind: str | None = None, status: str = "pending") -> dict[str, Any]:
return {"status": "ok", "events": bus.list_events(kind, status)}
def get_agent_event(event_id: str) -> dict[str, Any]:
return {"status": "ok", "event": bus.get(event_id)}
def transition_agent_event(event_id: str, status: str, actor: str = "system") -> dict[str, Any]:
return {"status": "ok", "event": bus.transition(event_id, status, actor)}
def agent_event_stats() -> dict[str, Any]:
return {"status": "ok", "stats": bus.stats()}
# Compatibility adapters
def list_gpt_inbox_messages():
return list_agent_events(kind="gpt_inbox")
def ack_gpt_inbox_message(event_id: str):
return transition_agent_event(event_id, "completed", "custom-gpt")
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/agent-events", tags=["agent-events"])
@router.post("", operation_id="createAgentEvent")
def create(payload: dict[str, Any]):
return create_agent_event(payload)
@router.get("", operation_id="listAgentEvents")
def listing(kind: str | None = None, status: str = "pending"):
return list_agent_events(kind, status)
@router.get("/{event_id}", operation_id="getAgentEvent")
def get(event_id: str):
return get_agent_event(event_id)
@router.post("/{event_id}/transition", operation_id="transitionAgentEvent")
def transition(event_id: str, payload: dict[str, Any]):
return transition_agent_event(event_id, payload["status"], payload.get("actor", "system"))
@router.get("/stats/overview", operation_id="getAgentEventStats")
def stats():
return agent_event_stats()
return router
router = build_router()
__all__ = ["router", "create_agent_event", "list_agent_events", "get_agent_event", "transition_agent_event"]
+125
View File
@@ -0,0 +1,125 @@
"""Unified Agent Event Bus.
Unifies commands, GPT messages, replies, approvals, audit and reports into one
state machine. Compatibility layers can filter events by kind instead of keeping
separate queues.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
import json
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass
class AgentEvent:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
status: str = "pending"
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
actor: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class AgentEventBus:
STATES = ("pending", "running", "completed", "failed")
def __init__(self, root: str = "agents/events"):
self.root = Path(root)
for state in self.STATES:
(self.root / state).mkdir(parents=True, exist_ok=True)
(self.root / "audit").mkdir(parents=True, exist_ok=True)
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event_id = "evt_" + sha256(
json.dumps(
{"kind": kind, "source": source, "summary": summary, "payload": payload},
sort_keys=True,
ensure_ascii=False,
).encode("utf-8")
).hexdigest()[:16]
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
self._write("pending", event.to_dict())
self._audit("created", event.to_dict())
return event.to_dict()
def list_events(self, kind: str | None = None, status: str = "pending", limit: int = 100) -> list[dict[str, Any]]:
result = []
for path in sorted((self.root / status).glob("*.json")):
event = self._read(path)
if kind and event.get("kind") != kind:
continue
result.append(event)
return result[:limit]
def get(self, event_id: str) -> dict[str, Any] | None:
for state in self.STATES:
path = self.root / state / f"{event_id}.json"
if path.exists():
return self._read(path)
return None
def transition(self, event_id: str, status: str, actor: str | None = None) -> dict[str, Any]:
event = self.get(event_id)
if not event:
return {"status": "not_found", "id": event_id}
if status not in self.STATES:
return {"status": "invalid_state"}
old = event.get("status", "pending")
old_path = self.root / old / f"{event_id}.json"
event["status"] = status
event["actor"] = actor
event["updated_at"] = utc_now()
self._write(status, event)
if old_path.exists() and old != status:
old_path.unlink()
self._audit("transition", event, {"from": old, "to": status})
return event
def stats(self) -> dict[str, Any]:
return {
"events": {state: len(list((self.root / state).glob("*.json"))) for state in self.STATES},
"audit_events": len(list((self.root / "audit").glob("*.json"))),
"status": "healthy",
}
def _write(self, state: str, data: dict[str, Any]):
(self.root / state / f"{data['id']}.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
)
def _read(self, path: Path):
return json.loads(path.read_text(encoding="utf-8"))
def _audit(self, action: str, event: dict[str, Any], extra: dict[str, Any] | None = None):
data = {"action": action, "event": event, "extra": extra or {}, "time": utc_now()}
(self.root / "audit" / f"{event['id']}-{action}.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
)
__all__ = ["AgentEvent", "AgentEventBus"]
@@ -0,0 +1,50 @@
"""Admin dashboard data model for the unified Agent Event Bus."""
from __future__ import annotations
from collections import Counter
from typing import Any
from .agent_event_bus import AgentEventBus
class AgentEventDashboard:
def __init__(self, bus: AgentEventBus | None = None):
self.bus = bus or AgentEventBus()
def overview(self) -> dict[str, Any]:
stats = self.bus.stats()
pending = self.bus.list_events(status="pending")
running = self.bus.list_events(status="running")
completed = self.bus.list_events(status="completed")
failed = self.bus.list_events(status="failed")
all_visible = pending + running + completed + failed
by_kind = Counter(event.get("kind", "unknown") for event in all_visible)
by_target = Counter(event.get("target", "unknown") for event in all_visible)
return {
"module": "agent-event-bus",
"title": "Unified Agent Event Bus",
"route": "/admin/ai-evolution/events",
"metrics": {
**stats,
"by_kind": dict(by_kind),
"by_target": dict(by_target),
"gpt_inbox_pending": by_kind.get("gpt_inbox", 0),
"openclaw_pending": len([e for e in pending if e.get("target") == "openclaw"]),
},
"panels": [
"event timeline",
"pending commands",
"GPT inbox",
"agent replies",
"failure reports",
"audit trail",
],
"audit": {
"enabled": True,
"location": "agents/events/audit",
},
}
__all__ = ["AgentEventDashboard"]
@@ -0,0 +1,37 @@
"""Bridge Agent Event Bus events into Agent Observability metrics."""
from .agent_observability_hooks import AgentObservabilityHooks
class AgentEventObservabilityBridge:
def __init__(self, hooks: AgentObservabilityHooks | None = None):
self.hooks = hooks or AgentObservabilityHooks()
def consume(self, event: dict):
kind = event.get("kind")
payload = event.get("payload", {})
if kind == "agent_started":
return self.hooks.start(
user_id=payload.get("user_id", "unknown"),
visible_scope=payload.get("scope", "self"),
entry_agent=payload.get("agent", "unknown"),
execution_id=event.get("id"),
)
if kind == "agent_delegated":
return self.hooks.delegate(event["execution_id"], payload.get("agent", "unknown"))
if kind == "token_consumed":
return self.hooks.tokens(
event["execution_id"],
payload.get("input_tokens", 0),
payload.get("output_tokens", 0),
)
if kind in {"agent_completed", "agent_failed"}:
return self.hooks.finish(
event["execution_id"],
payload.get("closed_by"),
"failed" if kind == "agent_failed" else "completed",
)
return {"status": "ignored", "kind": kind}
__all__ = ["AgentEventObservabilityBridge"]
+80
View File
@@ -0,0 +1,80 @@
"""Agent orchestration observability model.
Tracks OpenClaw, Hermes and sub-agent execution chains. Token usage is optional
and depends on model adapter instrumentation.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass
class AgentExecutionRecord:
execution_id: str
user_id: str
visible_scope: str
entry_agent: str
path: list[str]
used_hermes: bool
closed_by: str
duration_ms: int = 0
input_tokens: int | None = None
output_tokens: int | None = None
total_tokens: int | None = None
status: str = "completed"
started_at: str = field(default_factory=utc_now)
ended_at: str = field(default_factory=utc_now)
def to_dict(self):
return asdict(self)
class AgentObservability:
def __init__(self):
self.records: list[AgentExecutionRecord] = []
def record(self, record: AgentExecutionRecord):
self.records.append(record)
return record.to_dict()
def summary(self, user_id: str | None = None) -> dict[str, Any]:
records = [r for r in self.records if not user_id or r.user_id == user_id]
hermes = [r for r in records if r.used_hermes]
direct = [r for r in records if not r.used_hermes]
return {
"total_executions": len(records),
"openclaw_independent_loops": len(direct),
"hermes_orchestrated_loops": len(hermes),
"average_duration_ms": (
sum(r.duration_ms for r in records) / len(records) if records else 0
),
"token_usage": {
"input": sum(r.input_tokens or 0 for r in records),
"output": sum(r.output_tokens or 0 for r in records),
"total": sum(r.total_tokens or 0 for r in records),
},
}
def hermes_tree(self, user_id: str | None = None):
return [
{
"execution_id": r.execution_id,
"entry": r.entry_agent,
"used_hermes": r.used_hermes,
"children": r.path,
"duration_ms": r.duration_ms,
"tokens": r.total_tokens,
}
for r in self.records
if (not user_id or r.user_id == user_id)
]
__all__ = ["AgentExecutionRecord", "AgentObservability"]
@@ -0,0 +1,52 @@
"""API for agent orchestration observability."""
from __future__ import annotations
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .agent_observability import AgentExecutionRecord
from .agent_observability_dashboard import AgentObservabilityDashboard
from .agent_observability_store import AgentObservabilityStore
store = AgentObservabilityStore()
dashboard = AgentObservabilityDashboard(store.load())
def record_agent_execution(payload: dict[str, Any]):
record = AgentExecutionRecord(**payload)
return {"status": "ok", "record": store.append(record)}
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
records = store.query(user_id, group_id, role)
return {
"status": "ok",
"records": records,
"summary": store.load().summary(user_id if role != "owner" else None),
}
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/agents", tags=["agent-observability"])
@router.post("/execution", operation_id="recordAgentExecution")
def record(payload: dict[str, Any]):
return record_agent_execution(payload)
@router.get("/overview", operation_id="getAgentObservabilityOverview")
def overview(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
return get_agent_observability(user_id, group_id, role)
return router
router = build_router()
__all__ = ["router", "record_agent_execution", "get_agent_observability"]
@@ -0,0 +1,44 @@
"""Web UI dashboard model for agent execution visibility."""
from .agent_observability import AgentObservability
class AgentObservabilityDashboard:
def __init__(self, observability: AgentObservability | None = None):
self.observability = observability or AgentObservability()
def overview(self, user_id: str | None = None):
return {
"route": "/admin/ai-evolution/agents",
"title": "Agent Orchestration Observability",
"summary": self.observability.summary(user_id),
"views": [
{
"name": "scheduler_flow",
"title": "OpenClaw/Hermes 调度流程",
"data": self.observability.hermes_tree(user_id),
},
{
"name": "agent_metrics",
"title": "智能体指标",
"fields": [
"duration_ms",
"input_tokens",
"output_tokens",
"total_tokens",
"status",
],
},
{
"name": "loop_distribution",
"title": "闭环分布",
"fields": [
"openclaw_independent_loops",
"hermes_orchestrated_loops",
],
},
],
}
__all__ = ["AgentObservabilityDashboard"]
@@ -0,0 +1,113 @@
"""Runtime hooks for Agent Observability.
OpenClaw/Hermes/sub-agents can call these hooks at start/end/delegation points.
The hooks store sanitized metadata only: no prompts, no secrets, no private message
content.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from .agent_observability import AgentExecutionRecord
from .agent_observability_store import AgentObservabilityStore
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_time(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
@dataclass
class AgentRunContext:
execution_id: str
user_id: str
visible_scope: str
entry_agent: str
path: list[str] = field(default_factory=list)
used_hermes: bool = False
started_at: str = field(default_factory=utc_now)
input_tokens: int = 0
output_tokens: int = 0
status: str = "running"
def delegate(self, agent_name: str) -> None:
if agent_name not in self.path:
self.path.append(agent_name)
if agent_name.lower() == "hermes":
self.used_hermes = True
def add_tokens(self, input_tokens: int = 0, output_tokens: int = 0) -> None:
self.input_tokens += max(input_tokens, 0)
self.output_tokens += max(output_tokens, 0)
class AgentObservabilityHooks:
def __init__(self, store: AgentObservabilityStore | None = None):
self.store = store or AgentObservabilityStore()
self.active: dict[str, AgentRunContext] = {}
def start(
self,
user_id: str,
visible_scope: str,
entry_agent: str,
execution_id: str | None = None,
) -> dict[str, Any]:
run = AgentRunContext(
execution_id=execution_id or f"exec_{uuid4().hex[:12]}",
user_id=user_id,
visible_scope=visible_scope,
entry_agent=entry_agent,
path=[entry_agent],
used_hermes=entry_agent.lower() == "hermes",
)
self.active[run.execution_id] = run
return {"status": "started", "execution_id": run.execution_id, "run": run.__dict__}
def delegate(self, execution_id: str, agent_name: str) -> dict[str, Any]:
run = self.active[execution_id]
run.delegate(agent_name)
return {"status": "delegated", "execution_id": execution_id, "path": run.path}
def tokens(self, execution_id: str, input_tokens: int = 0, output_tokens: int = 0) -> dict[str, Any]:
run = self.active[execution_id]
run.add_tokens(input_tokens, output_tokens)
return {
"status": "tokens_recorded",
"execution_id": execution_id,
"input_tokens": run.input_tokens,
"output_tokens": run.output_tokens,
"total_tokens": run.input_tokens + run.output_tokens,
}
def finish(self, execution_id: str, closed_by: str | None = None, status: str = "completed") -> dict[str, Any]:
run = self.active.pop(execution_id)
ended_at = utc_now()
duration_ms = int((parse_time(ended_at) - parse_time(run.started_at)).total_seconds() * 1000)
record = AgentExecutionRecord(
execution_id=run.execution_id,
user_id=run.user_id,
visible_scope=run.visible_scope,
entry_agent=run.entry_agent,
path=run.path,
used_hermes=run.used_hermes,
closed_by=closed_by or run.path[-1],
duration_ms=duration_ms,
input_tokens=run.input_tokens,
output_tokens=run.output_tokens,
total_tokens=run.input_tokens + run.output_tokens,
status=status,
started_at=run.started_at,
ended_at=ended_at,
)
return {"status": "stored", "record": self.store.append(record)}
__all__ = ["AgentRunContext", "AgentObservabilityHooks"]
@@ -0,0 +1,45 @@
"""Persistent store for agent observability records.
Stores sanitized execution metrics for OpenClaw/Hermes/sub-agents. No prompts,
secrets, or private message contents are persisted here; only metrics and routing
metadata are stored for Web UI statistics and audit.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from .agent_observability import AgentExecutionRecord, AgentObservability
class AgentObservabilityStore:
def __init__(self, root: str = "runtime/agent-observability"):
self.root = Path(root)
self.records_dir = self.root / "records"
self.records_dir.mkdir(parents=True, exist_ok=True)
def append(self, record: AgentExecutionRecord) -> dict[str, Any]:
path = self.records_dir / f"{record.execution_id}.json"
data = record.to_dict()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return data
def load(self) -> AgentObservability:
observability = AgentObservability()
for path in sorted(self.records_dir.glob("*.json")):
raw = json.loads(path.read_text(encoding="utf-8"))
observability.records.append(AgentExecutionRecord(**raw))
return observability
def query(self, user_id: str | None = None, group_id: str | None = None, role: str = "user") -> list[dict[str, Any]]:
records = [record.to_dict() for record in self.load().records]
if role == "owner":
return records
if role == "group_admin" and group_id:
return [r for r in records if r.get("visible_scope") == group_id or r.get("user_id") == user_id]
return [r for r in records if r.get("user_id") == user_id]
__all__ = ["AgentObservabilityStore"]
+100 -2
View File
@@ -15,7 +15,7 @@ TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.toke
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
monitor = ActionMonitor()
BASE_MODELS = [
@@ -200,6 +200,11 @@ class H(BaseHTTPRequestHandler):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
# Admin AI Evolution Dashboard
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
return self.ai_evolution_page()
if path in ('/admin/ai-evolution/api/data'):
return self.ai_evolution_data()
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_GET', str(e), {'path':path})
@@ -290,9 +295,102 @@ class H(BaseHTTPRequestHandler):
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
_EVOLUTION_HTML = '''<!DOCTYPE html>
<html lang=zh-CN>
<head>
<meta charset=UTF-8>
<meta name=viewport content="width=device-width,initial-scale=1.0">
<title>AIOS Evolution Dashboard</title>
<script src=https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-size:14px;line-height:1.7;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif}
.c{max-width:1000px;margin:0 auto;padding:32px 24px}
h1{font-size:24px;color:#58a6ff;margin-bottom:2px}
.sub{color:#8b949e;font-size:13px;border-bottom:1px solid #21262d;padding-bottom:16px;margin-bottom:20px;display:flex;justify-content:space-between}
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px;margin-bottom:20px}
.s{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:18px;text-align:center}
.s .v{font-size:28px;font-weight:700;color:#f0f6fc}
.s .l{font-size:11px;color:#8b949e;margin-top:4px}
.bx{background:#161b22;border:1px solid #21262d;border-radius:8px;margin-bottom:14px}
.bh{border-bottom:1px solid #21262d;padding:10px 16px;font-weight:600;font-size:14px;display:flex;gap:6px;align-items:center}
.bd{padding:12px 16px}
.rw{display:grid;grid-template-columns:1fr 1fr 100px 60px;gap:8px;padding:8px 16px;border-bottom:1px solid #21262d;font-size:13px;align-items:center;cursor:pointer;transition:background .15s}
.rw:hover{background:#1c2128}.rw:last-child{border-bottom:none}
.tg{display:inline-block;padding:2px 10px;border-radius:99px;font-size:11px;font-weight:600}
.ok{background:#3fb95033;color:#3fb950}.fl{background:#da363333;color:#f85149}.if{background:#1f6feb33;color:#58a6ff}
pre{background:#0d1117;padding:12px;border-radius:6px;overflow:auto;font-size:12px;color:#8b949e;max-height:300px;margin-top:8px}select{padding:8px 10px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;font-size:13px;max-width:400px;width:100%;margin-bottom:4px;cursor:pointer}
</style>
</head>
<body>
<div id=app class=c>
<h1>AIOS Evolution Dashboard</h1>
<div class=sub><span>Baseline: {{baseline}}</span><span style=color:#8b949e>{{refreshMsg}}</span></div>
<div class=g><div class=s><div class=v>{{tot}}</div><div class=l>Commands</div></div><div class=s><div class=v style=color:#3fb950>{{sok}}</div><div class=l>Succeeded</div></div><div class=s><div class=v style=color:#f85149>{{sfail}}</div><div class=l>Failed</div></div><div class=s><div class=v>{{runs}}</div><div class=l>Executions</div></div></div>
<div class=bx><div class=bh>Version Timeline</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Version</span><span>Time</span><span>Result</span></div><div v-for=v in vs class=rw @click=selVer(v)><span style=color:#58a6ff>{{v.tag}}</span><span>{{v.time||'-'}}</span><span :class="'tg '+v.result">{{v.result||'-'}}</span></div></div></div>
<div class=bx><div class=bh>Commands</div><div class=bd><select v-model=cmd @change=loadCmd><option value>-- Select --</option><option v-for=c in cmds :value=c>{{c}}</option></select><pre v-if=cdet>{{cdet}}</pre></div></div>
<div class=bx><div class=bh>Recent Agent Runs</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Agent</span><span>Command</span><span>Count</span><span>Result</span></div><div v-for=r in aruns class=rw><span>{{r.agent||'-'}}</span><span style=overflow:hidden;white-space:nowrap;text-overflow:ellipsis>{{r.command}}</span><span>{{r.interaction_count||0}}</span><span :class="'tg '+r.final_result">{{r.final_result||'-'}}</span></div></div></div></div>
<script>
const{createApp,ref,onMounted}=Vue;createApp({setup(){const vs=ref([]),cmds=ref([]),ar=ref([]),cmd=ref(''),cdet=ref(null),tot=ref(0),sok=ref(0),sfail=ref(0),runs=ref(0),baseline=ref('V1R3C1'),refreshMsg=ref('')
async function ld(){try{const r=await fetch('/admin/ai-evolution/api/data'),d=await r.json();baseline.value=d.baseline||'V1R3C1';vs.value=d.versions||[];cmds.value=d.commands||[];ar.value=d.agent_runs||[];tot.value=d.stats?.total||0;sok.value=d.stats?.success||0;sfail.value=d.stats?.failed||0;runs.value=d.stats?.agent_runs||0;refreshMsg.value='Last updated: '+new Date().toLocaleString()}catch(e){}}
async function loadCmd(){if(!cmd.value){cdet.value=null;return}try{const r=await fetch('/admin/ai-evolution/api/data?command='+encodeURIComponent(cmd.value)),d=await r.json();cdet.value=JSON.stringify(d,null,2)}catch(e){cdet.value='Error'}}
function selVer(v){cmd.value=v.tag||v.id||v.version;loadCmd()}
onMounted(()=>{ld();setInterval(ld,30000)})
return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer}})mount('#app')
</script></body></html>'''
def ai_evolution_page(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(self._EVOLUTION_HTML)))
self.end_headers()
self.wfile.write(self._EVOLUTION_HTML.encode())
def ai_evolution_data(self):
q=urllib.parse.urlparse(self.path).query
params=urllib.parse.parse_qs(q)
cmd_filter=(params.get('command') or [None])[0]
bl=jload(WS/'baseline'/'current.json',{})
current_ver=bl.get('current_baseline','V1R3C1')
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
versions=[]
if (WS/'baseline'/'promotions').exists():
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
d=jload(p,{});v=d.get('version',p.stem)
versions.append({'tag':v,'time':d.get('promoted_at','-'),'result':'ok'})
for t in tags:
if not any(v['tag']==t for v in versions):
versions.append({'tag':t,'time':'-','result':'if'})
agent_runs=[];ard=WS/'audit'/'agent-runs'
if ard.exists():
for f in sorted(ard.glob('*.json'),reverse=True)[:50]:
d=jload(f,{});agent_runs.append(d) if isinstance(d,dict) else None
cmd_dirs=[WS/'agents/commands/active',WS/'agents/commands/running',WS/'agents/commands/completed',WS/'agents/commands/failed']
commands=[]
for d in cmd_dirs:
if d.exists():
for f in sorted(d.glob('*.md')):
commands.append(f.name)
command_detail=None
if cmd_filter:
for d in cmd_dirs:
if d.exists():
fp=d/cmd_filter
if fp.exists():
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not command_detail:
command_detail={'command':cmd_filter,'error':'not_found'}
return self.sendj(command_detail)
completed=list((WS/'agents/commands/completed').glob('*.md')) if (WS/'agents/commands/completed').exists() else []
failed=list((WS/'agents/commands/failed').glob('*.md')) if (WS/'agents/commands/failed').exists() else []
stats={'total':len(commands),'success':len(completed),'failed':len(failed),'agent_runs':len(agent_runs)}
data={'baseline':current_ver,'versions':versions[-30:],'agent_runs':agent_runs,'commands':sorted(set(commands)),'stats':stats}
self.sendj(data)
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
@@ -0,0 +1,120 @@
"""Architect governance agent responsibilities.
This agent records requirement changes, maintains versioned human-readable
requirement/architecture/deployment/test documents, reviews architecture changes,
validates deployment docs against actual configuration, and produces version bump
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class RequirementChange:
id: str
source: str
actor: str
requirement: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class DocumentRefreshPlan:
requirement_doc: bool
architecture_doc: bool
deployment_doc: bool
test_case_doc: bool
reason: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureReviewDecision:
status: str
findings: list[str]
optimization_recommendations: list[str]
version_bump: str
docs_to_refresh: DocumentRefreshPlan
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": self.findings,
"optimization_recommendations": self.optimization_recommendations,
"version_bump": self.version_bump,
"docs_to_refresh": self.docs_to_refresh.to_dict(),
"created_at": self.created_at,
}
class ArchitectGovernanceAgent:
"""Independent architecture governance role."""
def record_requirement_change(self, source: str, actor: str, requirement: str) -> RequirementChange:
change_id = "REQ-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
return RequirementChange(change_id, source, actor, requirement)
def review_architecture_change(self, proposal: dict[str, Any]) -> ArchitectureReviewDecision:
impact = proposal.get("impact", "medium")
findings = [
"requirement_traceability_checked",
"architecture_consistency_checked",
"deployment_doc_alignment_checked",
"test_case_doc_required",
"rollback_and_owner_gate_checked",
]
recommendations = [
"refresh user requirements document",
"refresh architecture design document",
"refresh engineering deployment document",
"generate acceptance test case document",
]
version_bump = self.decide_version_bump(proposal)
return ArchitectureReviewDecision(
status="review_required" if impact in {"high", "architecture"} else "pass",
findings=findings,
optimization_recommendations=recommendations,
version_bump=version_bump,
docs_to_refresh=DocumentRefreshPlan(True, True, True, True, "architecture or requirement changed"),
)
def decide_version_bump(self, proposal: dict[str, Any]) -> str:
if proposal.get("breaks_architecture") or proposal.get("major_capability"):
return "V"
if proposal.get("new_release_capability") or proposal.get("architecture_change"):
return "R"
return "C"
def validate_deployment_doc(self, deployment_doc: dict[str, Any], actual_config: dict[str, Any]) -> dict[str, Any]:
expected = set(deployment_doc.get("services", []))
actual = set(actual_config.get("services", []))
missing = sorted(expected - actual)
extra = sorted(actual - expected)
return {
"status": "pass" if not missing else "attention_required",
"missing_services": missing,
"extra_services": extra,
"checked_at": utc_now(),
}
__all__ = [
"RequirementChange",
"DocumentRefreshPlan",
"ArchitectureReviewDecision",
"ArchitectGovernanceAgent",
]
@@ -0,0 +1,32 @@
"""API for Architect Governance Center."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_governance_dashboard import ArchitectGovernanceDashboard
dashboard = ArchitectGovernanceDashboard()
def get_architect_governance_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect", tags=["architect-governance"])
@router.get("/overview", operation_id="getArchitectGovernanceOverview")
def overview():
return get_architect_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_architect_governance_overview"]
@@ -0,0 +1,153 @@
"""Web UI dashboard model for Architect Governance Agent.
Shows requirement changes, architecture review findings, document refresh status,
deployment consistency, test case readiness, weekly research and V/R/C version
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectPanelMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectPanel:
key: str
title: str
description: str
metrics: list[ArchitectPanelMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"description": self.description,
"metrics": [m.to_dict() for m in self.metrics],
}
class ArchitectGovernanceDashboard:
def overview(
self,
requirement_changes: list[dict[str, Any]] | None = None,
architecture_reviews: list[dict[str, Any]] | None = None,
document_status: dict[str, Any] | None = None,
research_status: dict[str, Any] | None = None,
version_recommendations: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
requirement_changes = requirement_changes or []
architecture_reviews = architecture_reviews or []
document_status = document_status or {}
research_status = research_status or {}
version_recommendations = version_recommendations or []
panels = [
self.requirement_panel(requirement_changes),
self.architecture_panel(architecture_reviews),
self.document_panel(document_status),
self.research_panel(research_status),
self.version_panel(version_recommendations),
]
return {
"module": "architect-governance",
"route": "/admin/ai-evolution/architect",
"title": "架构师 Agent 治理中心",
"status": "attention_required" if any(self._panel_warn(panel) for panel in panels) else "healthy",
"generated_at": utc_now(),
"panels": [panel.to_dict() for panel in panels],
"requirement_changes": requirement_changes,
"architecture_reviews": architecture_reviews,
"version_recommendations": version_recommendations,
}
def requirement_panel(self, changes: list[dict[str, Any]]) -> ArchitectPanel:
return ArchitectPanel(
key="requirements",
title="需求变化记录",
description="记录来自 Custom GPT、Codex、OpenClaw、Hermes 和用户反馈的需求变化。",
metrics=[
ArchitectPanelMetric("total_requirement_changes", "需求变化数量", len(changes)),
ArchitectPanelMetric(
"undocumented_changes",
"待刷新到用户需求文档",
len([c for c in changes if not c.get("documented")]),
"warn" if any(not c.get("documented") for c in changes) else "ok",
),
],
)
def architecture_panel(self, reviews: list[dict[str, Any]]) -> ArchitectPanel:
pending = [r for r in reviews if r.get("status") not in {"pass", "not_required"}]
return ArchitectPanel(
key="architecture_review",
title="全量架构审视",
description="展示架构一致性、扩展性、安全边界、可运维性和回滚策略审查。",
metrics=[
ArchitectPanelMetric("review_count", "架构审查数量", len(reviews)),
ArchitectPanelMetric("pending_reviews", "待处理架构审查", len(pending), "warn" if pending else "ok"),
],
)
def document_panel(self, status: dict[str, Any]) -> ArchitectPanel:
required_docs = ["requirements", "architecture", "deployment", "test_cases"]
missing = [doc for doc in required_docs if status.get(doc) not in {"ready", "updated"}]
return ArchitectPanel(
key="documents",
title="文档版本化状态",
description="用户需求、架构设计、工程部署、测试例文档刷新状态。",
metrics=[
ArchitectPanelMetric("requirements_doc", "用户需求文档", status.get("requirements", "unknown"), "warn" if "requirements" in missing else "ok"),
ArchitectPanelMetric("architecture_doc", "架构设计文档", status.get("architecture", "unknown"), "warn" if "architecture" in missing else "ok"),
ArchitectPanelMetric("deployment_doc", "工程部署文档", status.get("deployment", "unknown"), "warn" if "deployment" in missing else "ok"),
ArchitectPanelMetric("test_case_doc", "测试例文档", status.get("test_cases", "unknown"), "warn" if "test_cases" in missing else "ok"),
],
)
def research_panel(self, status: dict[str, Any]) -> ArchitectPanel:
overdue = bool(status.get("overdue"))
return ArchitectPanel(
key="weekly_research",
title="每周外部技术情报",
description="联网搜索 Agent/模型版本、GitHub top 实现、潜在安全影响。",
metrics=[
ArchitectPanelMetric("last_research_at", "最近调研时间", status.get("last_research_at", "never"), "warn" if overdue else "ok"),
ArchitectPanelMetric("findings", "发现数量", status.get("findings", 0)),
ArchitectPanelMetric("optimization_candidates", "可借鉴优化项", status.get("optimization_candidates", 0)),
],
)
def version_panel(self, recommendations: list[dict[str, Any]]) -> ArchitectPanel:
latest = recommendations[-1] if recommendations else {}
return ArchitectPanel(
key="version_decision",
title="V/R/C 版本建议",
description="由架构师 Agent 判断变更应进入 C、R 或 V。",
metrics=[
ArchitectPanelMetric("recommendation_count", "版本建议数量", len(recommendations)),
ArchitectPanelMetric("latest_recommendation", "最近建议", latest.get("version_bump", "none")),
ArchitectPanelMetric("reason", "建议原因", latest.get("reason", "")),
],
)
def _panel_warn(self, panel: ArchitectPanel) -> bool:
return any(metric.status == "warn" for metric in panel.metrics)
__all__ = ["ArchitectPanelMetric", "ArchitectPanel", "ArchitectGovernanceDashboard"]
@@ -0,0 +1,40 @@
"""Web UI model for Architect weekly research."""
from .architect_weekly_research_agent import ArchitectWeeklyResearchAgent
class ArchitectResearchDashboard:
def __init__(self, agent=None):
self.agent = agent or ArchitectWeeklyResearchAgent()
def overview(self):
return {
"module": "architect-weekly-research",
"route": "/admin/ai-evolution/architect/research",
"title": "架构师 Agent 技术情报中心",
"panels": [
{
"title": "Research Targets",
"data": self.agent.plan(),
},
{
"title": "Evaluation",
"fields": [
"relevance",
"impact",
"recommendation",
],
},
{
"title": "Version Decision",
"fields": [
"C",
"R",
"V",
],
},
],
}
__all__ = ["ArchitectResearchDashboard"]
@@ -0,0 +1,122 @@
"""Architect research timeline dashboard.
Shows weekly external research findings as a traceable architecture-impact flow:
research target -> finding -> impact -> recommendation -> version decision ->
governance event.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTimelineStep:
id: str
title: str
step_type: str
status: str
source: str
timestamp: str
impact: str = "none"
version_bump: str = "none"
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectResearchTimeline:
ORDER = {
"research_planned": 10,
"finding_created": 20,
"impact_analyzed": 30,
"recommendation_created": 40,
"version_decision_created": 50,
"architecture_review_event_created": 60,
}
def build(self, findings: list[dict[str, Any]] | None = None, decisions: list[dict[str, Any]] | None = None) -> dict[str, Any]:
findings = findings or []
decisions = decisions or []
steps: list[ResearchTimelineStep] = [
ResearchTimelineStep(
id="research-plan",
title="每周技术情报调研计划",
step_type="research_planned",
status="ok",
source="architect-agent",
timestamp=utc_now(),
detail={"cadence": "weekly"},
)
]
for idx, finding in enumerate(findings):
steps.append(
ResearchTimelineStep(
id=finding.get("id", f"finding-{idx}"),
title=finding.get("title", "research finding"),
step_type="finding_created",
status="ok",
source=finding.get("source", "unknown"),
timestamp=finding.get("created_at", utc_now()),
impact=finding.get("impact", "none"),
detail=finding,
)
)
steps.append(
ResearchTimelineStep(
id=f"impact-{finding.get('id', idx)}",
title="架构影响分析",
step_type="impact_analyzed",
status="attention_required" if finding.get("impact") in {"architecture", "breaking", "new_capability"} else "ok",
source="architect-agent",
timestamp=utc_now(),
impact=finding.get("impact", "none"),
detail={"relevance": finding.get("relevance"), "recommendation": finding.get("recommendation")},
)
)
for idx, decision in enumerate(decisions):
steps.append(
ResearchTimelineStep(
id=decision.get("id", f"decision-{idx}"),
title="C/R/V 版本建议",
step_type="version_decision_created",
status=decision.get("status", "ok"),
source="architect-agent",
timestamp=decision.get("created_at", utc_now()),
version_bump=decision.get("version_bump", "none"),
detail=decision,
)
)
steps.sort(key=lambda s: (self.ORDER.get(s.step_type, 999), s.timestamp))
return {
"module": "architect-research-timeline",
"route": "/admin/ai-evolution/architect/research/timeline",
"title": "架构师技术情报时间线",
"status": self._status(steps),
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"summary": self._summary(steps),
}
def _status(self, steps: list[ResearchTimelineStep]) -> str:
if any(step.status == "attention_required" for step in steps):
return "attention_required"
return "healthy"
def _summary(self, steps: list[ResearchTimelineStep]) -> dict[str, Any]:
return {
"findings": len([s for s in steps if s.step_type == "finding_created"]),
"architecture_impacts": len([s for s in steps if s.impact in {"architecture", "breaking", "new_capability"}]),
"version_decisions": len([s for s in steps if s.step_type == "version_decision_created"]),
"recommended_bumps": sorted({s.version_bump for s in steps if s.version_bump != "none"}),
}
__all__ = ["ResearchTimelineStep", "ArchitectResearchTimeline"]
@@ -0,0 +1,32 @@
"""API for architect research timeline."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_research_timeline import ArchitectResearchTimeline
timeline = ArchitectResearchTimeline()
def get_architect_research_timeline():
return timeline.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect/research", tags=["architect-research"])
@router.get("/timeline", operation_id="getArchitectResearchTimeline")
def get_timeline():
return get_architect_research_timeline()
return router
router = build_router()
__all__ = ["router", "get_architect_research_timeline"]
@@ -0,0 +1,113 @@
"""Weekly external research loop for Architect Governance Agent.
The agent plans and normalizes weekly research tasks. Actual internet access is
provided by the runtime/search worker; this module defines the audited workflow,
findings, impact analysis, and V/R/C recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTarget:
id: str
topic: str
source_type: str
query: str
cadence: str = "weekly"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchFinding:
id: str
target_id: str
title: str
summary: str
source: str
relevance: str
impact: str
recommendation: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchDecision:
status: str
version_bump: str
reason: str
findings: list[ResearchFinding]
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"created_at": self.created_at,
}
class ArchitectWeeklyResearchAgent:
DEFAULT_TARGETS = (
ResearchTarget("agent-frameworks", "Agent framework versions", "web", "latest multi-agent orchestration framework releases"),
ResearchTarget("model-ecosystem", "Model ecosystem changes", "web", "latest LLM model releases tool calling agent features"),
ResearchTarget("github-top-agentops", "GitHub top AgentOps implementations", "github", "top repositories agent observability multi agent orchestration"),
ResearchTarget("security-agent-risks", "Agent security changes", "web", "latest AI agent security sandbox prompt injection tool use risks"),
)
def plan(self) -> dict[str, Any]:
return {
"status": "planned",
"cadence": "weekly",
"targets": [target.to_dict() for target in self.DEFAULT_TARGETS],
"created_at": utc_now(),
}
def normalize_finding(
self,
target_id: str,
title: str,
summary: str,
source: str,
relevance: str = "medium",
impact: str = "none",
recommendation: str = "monitor",
) -> ResearchFinding:
return ResearchFinding(
id=f"finding_{uuid4().hex[:12]}",
target_id=target_id,
title=title,
summary=summary,
source=source,
relevance=relevance,
impact=impact,
recommendation=recommendation,
)
def decide(self, findings: list[ResearchFinding]) -> ResearchDecision:
if any(f.impact == "breaking" for f in findings):
return ResearchDecision("action_required", "V", "breaking external ecosystem change", findings)
if any(f.impact in {"architecture", "new_capability"} for f in findings):
return ResearchDecision("optimization_recommended", "R", "external capability may improve architecture", findings)
if any(f.impact in {"bugfix", "minor"} for f in findings):
return ResearchDecision("minor_change_recommended", "C", "minor compatible improvement", findings)
return ResearchDecision("no_action", "none", "no relevant external change", findings)
__all__ = ["ResearchTarget", "ResearchFinding", "ResearchDecision", "ArchitectWeeklyResearchAgent"]
@@ -0,0 +1,32 @@
"""API for architecture change governance dashboard."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architecture_change_dashboard import ArchitectureChangeDashboard
dashboard = ArchitectureChangeDashboard()
def get_architecture_change_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architecture-changes", tags=["architecture-changes"])
@router.get("/overview", operation_id="getArchitectureChangeOverview")
def overview():
return get_architecture_change_overview()
return router
router = build_router()
__all__ = ["router", "get_architecture_change_overview"]
@@ -0,0 +1,90 @@
"""Architecture change classification.
Small compatible architecture changes may be handled automatically by the
Architect Agent with audit evidence. Large, risky, production-impacting or
breaking architecture changes require explicit Owner approval before
implementation.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
@dataclass(frozen=True)
class ArchitectureChangeClassification:
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeClassifier:
"""Classifies architecture changes into small/medium/large governance paths."""
LARGE_FLAGS = {
"breaking_change",
"breaks_architecture",
"production_architecture_update",
"security_boundary_change",
"cross_tenant_effect",
"cross_user_data_effect",
"data_migration",
"permission_model_change",
"runtime_infrastructure_change",
}
def classify(self, change: dict[str, Any]) -> ArchitectureChangeClassification:
risk = str(change.get("risk", "medium")).lower()
version_bump = str(change.get("version_bump", change.get("recommended_version_bump", "C"))).upper()
scope = str(change.get("scope", "component")).lower()
reasons: list[str] = []
for flag in sorted(self.LARGE_FLAGS):
if change.get(flag):
reasons.append(flag)
if risk in {"high", "critical"}:
reasons.append(f"risk={risk}")
if version_bump == "V":
reasons.append("version_bump=V")
if version_bump == "R" and scope in {"system", "platform", "production", "cross_agent", "cross_tenant"}:
reasons.append(f"version_bump=R scope={scope}")
if reasons:
return ArchitectureChangeClassification(
level="large",
owner_required=True,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=reasons,
required_gates=["architect_review", "owner_architecture_approval", "test_gate", "promotion_gate"],
)
if risk == "low" and version_bump in {"C", "NONE"} and scope in {"component", "local", "documentation", "test"}:
return ArchitectureChangeClassification(
level="small",
owner_required=False,
auto_implement_allowed=True,
version_bump=version_bump,
reasons=["low_risk_compatible_change"],
required_gates=["architect_review", "audit", "test_gate"],
)
return ArchitectureChangeClassification(
level="medium",
owner_required=False,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=["requires_architect_review_before_implementation"],
required_gates=["architect_review", "test_gate", "audit"],
)
__all__ = ["ArchitectureChangeClassification", "ArchitectureChangeClassifier"]
@@ -0,0 +1,93 @@
"""Architecture change governance dashboard.
Displays architecture change classification, owner approval requirements,
implementation readiness, version recommendation and audit evidence for Web UI.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .architecture_change_classifier import ArchitectureChangeClassifier
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureChangeView:
change_id: str
title: str
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
status: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
owner_decision: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeDashboard:
def __init__(self, classifier: ArchitectureChangeClassifier | None = None):
self.classifier = classifier or ArchitectureChangeClassifier()
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
views = [self._view(change) for change in changes]
return {
"module": "architecture-change-governance",
"route": "/admin/ai-evolution/architecture-changes",
"title": "架构变更治理中心",
"status": self._status(views),
"generated_at": utc_now(),
"summary": self._summary(views),
"changes": [view.to_dict() for view in views],
}
def _view(self, change: dict[str, Any]) -> ArchitectureChangeView:
classification = self.classifier.classify(change)
owner_decision = change.get("owner_decision")
if classification.owner_required and owner_decision != "approved":
status = "waiting_owner_approval"
elif classification.auto_implement_allowed:
status = "auto_implementation_allowed"
else:
status = "ready_after_required_gates"
return ArchitectureChangeView(
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
level=classification.level,
owner_required=classification.owner_required,
auto_implement_allowed=classification.auto_implement_allowed,
version_bump=classification.version_bump,
status=status,
reasons=classification.reasons,
required_gates=classification.required_gates,
owner_decision=owner_decision,
)
def _status(self, views: list[ArchitectureChangeView]) -> str:
if any(view.status == "waiting_owner_approval" for view in views):
return "attention_required"
return "healthy"
def _summary(self, views: list[ArchitectureChangeView]) -> dict[str, Any]:
return {
"total": len(views),
"small": len([v for v in views if v.level == "small"]),
"medium": len([v for v in views if v.level == "medium"]),
"large": len([v for v in views if v.level == "large"]),
"waiting_owner_approval": len([v for v in views if v.status == "waiting_owner_approval"]),
"auto_allowed": len([v for v in views if v.auto_implement_allowed]),
"version_bumps": sorted({v.version_bump for v in views if v.version_bump and v.version_bump != "NONE"}),
}
__all__ = ["ArchitectureChangeView", "ArchitectureChangeDashboard"]
@@ -0,0 +1,89 @@
"""Owner approval gate for architecture changes.
Architecture changes may be proposed, reviewed and optimized by agents, but they
must not be implemented until owner approval is recorded. This gate sits before
implementation and before promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureOwnerApprovalRequest:
request_id: str
change_id: str
title: str
reason: str
risk: str
architect_decision: str
status: str = "pending_owner_approval"
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureOwnerDecision:
request_id: str
change_id: str
decision: str
decided_by: str
reason: str = ""
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureOwnerGate:
"""Blocks architecture implementation until owner explicitly approves."""
def create_request(self, change: dict[str, Any], architect_decision: dict[str, Any]) -> ArchitectureOwnerApprovalRequest:
return ArchitectureOwnerApprovalRequest(
request_id=f"ARCH-OWNER-{change.get('change_id', change.get('id', 'unknown'))}",
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
reason=change.get("reason", "architecture impact requires owner approval"),
risk=change.get("risk", "medium"),
architect_decision=architect_decision.get("status", "reviewed"),
)
def can_implement(self, request: dict[str, Any], decision: dict[str, Any] | None = None) -> dict[str, Any]:
if not decision:
return {
"allowed": False,
"status": "waiting_owner_approval",
"reason": "architecture change requires owner approval before implementation",
"request": request,
}
if decision.get("decision") == "approved":
return {
"allowed": True,
"status": "owner_approved",
"reason": decision.get("reason", "approved by owner"),
"request": request,
"decision": decision,
}
return {
"allowed": False,
"status": "owner_rejected",
"reason": decision.get("reason", "rejected by owner"),
"request": request,
"decision": decision,
}
__all__ = [
"ArchitectureOwnerApprovalRequest",
"ArchitectureOwnerDecision",
"ArchitectureOwnerGate",
]
@@ -0,0 +1,79 @@
"""HTTP handler and display data for architecture owner gate."""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any
from .architecture_owner_gate import ArchitectureOwnerGate, ArchitectureOwnerApprovalRequest
gate = ArchitectureOwnerGate()
# In-memory store for pending approval requests
_pending_requests: dict[str, dict[str, Any]] = {}
_decisions: dict[str, dict[str, Any]] = {}
def create_owner_approval_request(change: dict[str, Any], architect_decision: dict[str, Any]) -> dict[str, Any]:
req = gate.create_request(change, architect_decision)
d = asdict(req)
_pending_requests[req.change_id] = d
return d
def approve_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
decision = asdict(
ArchitectureOwnerApprovalRequest.__bases__[1].__new__(type("_ArchitectureOwnerDecision", (), {})) # noqa
)
try:
from .architecture_owner_gate import ArchitectureOwnerDecision
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="approved",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
# remove from pending
_pending_requests.pop(change_id, None)
return {"allowed": True, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def reject_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
from .architecture_owner_gate import ArchitectureOwnerDecision
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
try:
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="rejected",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
_pending_requests.pop(change_id, None)
return {"allowed": False, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def get_owner_gate_overview() -> dict[str, Any]:
return {
"pending_requests": list(_pending_requests.values()),
"decisions": list(_decisions.values()),
"pending_count": len(_pending_requests),
"approved_count": sum(1 for d in _decisions.values() if d.get("decision") == "approved"),
"rejected_count": sum(1 for d in _decisions.values() if d.get("decision") == "rejected"),
}
@@ -0,0 +1,40 @@
"""Independent architecture optimization agent."""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureReview:
agent: str
decision: str
findings: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class ArchitectureReviewAgent:
"""Reviews architecture changes from system-wide optimal perspective."""
def review(self, proposal: dict) -> ArchitectureReview:
findings = [
"dependency_impact_checked",
"security_boundary_checked",
"scalability_checked",
"operability_checked",
"rollback_strategy_checked",
]
return ArchitectureReview(
"architecture-optimization-agent",
"pass" if proposal.get("architecture_change") else "not_required",
findings,
)
__all__ = ["ArchitectureReviewAgent", "ArchitectureReview"]
-108
View File
@@ -1,108 +0,0 @@
"""Autonomous self-loop orchestration primitives.
Goal: detect gaps, classify risk, plan remediation, run checks, and emit an
approval-aware report without storing secrets or bypassing owner governance.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SelfLoopSignal:
name: str
status: str
severity: str = "info"
detail: str = ""
def ok(self) -> bool:
return self.status in {"ok", "done", "pass"}
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class RemediationStep:
id: str
title: str
action: str
risk: str = "low"
requires_owner_approval: bool = False
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class SelfLoopReport:
id: str
status: str
signals: list[SelfLoopSignal] = field(default_factory=list)
remediation: list[RemediationStep] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"status": self.status,
"signals": [s.to_dict() for s in self.signals],
"remediation": [r.to_dict() for r in self.remediation],
"created_at": self.created_at,
}
class AutonomousSelfLoop:
def __init__(self, owner_approval_required: bool = True):
self.owner_approval_required = owner_approval_required
def probe(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[SelfLoopSignal]:
return [
SelfLoopSignal("runtime_active", "ok" if runtime.get("status") == "active" else "fail", "critical"),
SelfLoopSignal("git_patch_only", "ok" if baseline.get("update_mode") == "git_patch_only" else "fail", "high"),
SelfLoopSignal("no_direct_main_push", "ok" if not baseline.get("codex_gitops", {}).get("direct_main_push", True) else "fail", "high"),
SelfLoopSignal("autonomous_recovery_loop", "ok" if baseline.get("features", {}).get("autonomous_recovery_loop") else "fail", "high"),
SelfLoopSignal("portal_gitops_dashboard", "ok" if baseline.get("features", {}).get("portal_gitops_dashboard") else "fail", "medium"),
]
def plan(self, signals: list[SelfLoopSignal]) -> list[RemediationStep]:
steps: list[RemediationStep] = []
failed = {s.name for s in signals if not s.ok()}
if "runtime_active" in failed:
steps.append(RemediationStep("restart-runtime", "恢复 GitOps Runtime", "request-runtime-recovery", "high", True))
if "git_patch_only" in failed:
steps.append(RemediationStep("enforce-git-patch", "恢复 git_patch_only 更新模式", "commit-policy-patch", "high", True))
if "no_direct_main_push" in failed:
steps.append(RemediationStep("disable-direct-main", "禁止直接推送 main", "commit-security-policy", "high", True))
if "autonomous_recovery_loop" in failed:
steps.append(RemediationStep("enable-recovery-loop", "启用自动恢复闭环", "commit-runtime-config", "medium", True))
if "portal_gitops_dashboard" in failed:
steps.append(RemediationStep("enable-portal-dashboard", "启用 Portal GitOps 驾驶舱", "commit-portal-config", "medium", True))
if not steps:
steps.append(RemediationStep("continue-implementation", "继续补齐 Router/UI/OpenAPI/验收", "create-low-risk-patch", "low", False))
return steps
def evaluate(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> SelfLoopReport:
signals = self.probe(runtime, baseline)
remediation = self.plan(signals)
status = "done" if all(s.ok() for s in signals) and not any(r.requires_owner_approval for r in remediation) else "partial"
return SelfLoopReport("self-loop-report", status, signals, remediation)
def acceptance(self, report: SelfLoopReport) -> dict[str, Any]:
approval_blocked = any(step.requires_owner_approval for step in report.remediation)
return {
"status": report.status,
"can_unattended_continue": report.status == "done" and not approval_blocked,
"owner_approval_required": approval_blocked or self.owner_approval_required,
"report": report.to_dict(),
}
__all__ = ["SelfLoopSignal", "RemediationStep", "SelfLoopReport", "AutonomousSelfLoop"]
@@ -0,0 +1,103 @@
"""Change acceptance governance for AIOS.
Every feature change must provide matching tests. Every architecture change must
be reviewed by an independent architecture agent. User-facing changes must be
validated by an independent user acceptance agent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ChangeDescriptor:
change_id: str
change_type: str
title: str
files_changed: list[str]
tests: list[str] = field(default_factory=list)
user_facing: bool = False
architecture_impact: bool = False
risk: str = "low"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceFinding:
check: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceDecision:
status: str
findings: list[AcceptanceFinding]
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": [f.to_dict() for f in self.findings],
"decided_at": self.decided_at,
}
class ChangeAcceptanceGate:
def evaluate(self, change: ChangeDescriptor) -> AcceptanceDecision:
findings: list[AcceptanceFinding] = []
findings.append(self._tests_required(change))
findings.append(self._user_acceptance_required(change))
findings.append(self._architecture_review_required(change))
findings.append(self._risk_gate(change))
status = "pass" if all(f.passed() for f in findings) else "blocked"
return AcceptanceDecision(status, findings)
def _tests_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.tests:
return AcceptanceFinding("tests_present", "pass", "matching tests declared")
return AcceptanceFinding("tests_present", "fail", "every feature change requires tests")
def _user_acceptance_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.user_facing:
return AcceptanceFinding("user_acceptance", "pass", "not user-facing")
has_user_test = any("user" in t.lower() or "acceptance" in t.lower() for t in change.tests)
return AcceptanceFinding(
"user_acceptance",
"pass" if has_user_test else "fail",
"user-facing change requires user acceptance test",
)
def _architecture_review_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.architecture_impact:
return AcceptanceFinding("architecture_review", "pass", "no architecture impact")
has_arch_test = any("arch" in t.lower() or "architecture" in t.lower() for t in change.tests)
return AcceptanceFinding(
"architecture_review",
"pass" if has_arch_test else "fail",
"architecture change requires independent architect review artifact/test",
)
def _risk_gate(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.risk in {"low", "medium", "high"}:
return AcceptanceFinding("risk_classified", "pass", f"risk={change.risk}")
return AcceptanceFinding("risk_classified", "fail", "risk must be low/medium/high")
__all__ = ["ChangeDescriptor", "AcceptanceFinding", "AcceptanceDecision", "ChangeAcceptanceGate"]
@@ -0,0 +1,34 @@
"""API for AIOS change governance dashboard."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .change_governance_dashboard import ChangeGovernanceDashboard
dashboard = ChangeGovernanceDashboard()
def get_change_governance_overview(changes: list[dict[str, Any]] | None = None):
return dashboard.overview(changes)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["change-governance"])
@router.get("/overview", operation_id="getChangeGovernanceOverview")
def overview():
return get_change_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_change_governance_overview"]
@@ -0,0 +1,93 @@
"""Web UI dashboard model for AIOS change governance.
Shows test gates, user acceptance, architecture review, risk gates and release
blocking status for human operators and owner review.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GovernanceMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernancePanel:
key: str
title: str
metrics: list[GovernanceMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class ChangeGovernanceDashboard:
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
blocked = [c for c in changes if c.get("status") == "blocked"]
waiting_arch = [c for c in changes if c.get("architecture_review") == "required"]
waiting_user = [c for c in changes if c.get("user_acceptance") == "required"]
missing_tests = [c for c in changes if not c.get("tests")]
panels = [
GovernancePanel(
"test_gate",
"测试 Gate",
[
GovernanceMetric("total_changes", "变更总数", len(changes)),
GovernanceMetric("missing_tests", "缺少测试的变更", len(missing_tests), "warn" if missing_tests else "ok"),
],
),
GovernancePanel(
"user_acceptance",
"用户验收 Agent",
[
GovernanceMetric("waiting_user_acceptance", "等待用户验收", len(waiting_user), "warn" if waiting_user else "ok"),
],
),
GovernancePanel(
"architecture_review",
"架构师 Agent",
[
GovernanceMetric("waiting_architecture_review", "等待架构审查", len(waiting_arch), "warn" if waiting_arch else "ok"),
],
),
GovernancePanel(
"release_gate",
"发布 Gate",
[
GovernanceMetric("blocked_changes", "阻塞发布的变更", len(blocked), "warn" if blocked else "ok"),
],
),
]
return {
"module": "change-governance",
"route": "/admin/ai-evolution/governance",
"title": "AIOS 变更治理中心",
"status": "attention_required" if blocked or missing_tests or waiting_arch or waiting_user else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"changes": changes,
}
__all__ = ["GovernanceMetric", "GovernancePanel", "ChangeGovernanceDashboard"]
-192
View File
@@ -1,192 +0,0 @@
"""Failure memory for autonomous self-loop learning.
The memory keeps sanitized failure fingerprints, recurrence counters, and
remediation hints. It is intentionally in-memory and serializable; production
adapters can persist the exported dict to SQL/object storage without changing the
learning contract. Secrets and raw tokens must never be stored here.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from hashlib import sha256
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def _clean(value: str | None) -> str:
if not value:
return "unknown"
cleaned = value.replace("\n", " ").replace("\r", " ").strip().lower()
secret_markers = ("token", "password", "secret", "api_key", "private key")
for marker in secret_markers:
cleaned = cleaned.replace(marker, "redacted")
return cleaned[:500]
@dataclass(frozen=True)
class FailureSignature:
component: str
reason: str
fingerprint: str
severity: str = "medium"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class FailureRecord:
signature: FailureSignature
count: int = 0
first_seen: str = field(default_factory=utc_now)
last_seen: str = field(default_factory=utc_now)
last_job_id: str | None = None
examples: list[str] = field(default_factory=list)
remediation_hints: list[str] = field(default_factory=list)
resolved_count: int = 0
def to_dict(self) -> dict[str, Any]:
return {
"signature": self.signature.to_dict(),
"count": self.count,
"first_seen": self.first_seen,
"last_seen": self.last_seen,
"last_job_id": self.last_job_id,
"examples": list(self.examples),
"remediation_hints": list(self.remediation_hints),
"resolved_count": self.resolved_count,
"recurrence_score": self.recurrence_score(),
}
def recurrence_score(self) -> float:
if self.count <= 1:
return 0.0
unresolved = max(self.count - self.resolved_count, 0)
return min(1.0, unresolved / max(self.count, 1))
class FailureMemory:
def __init__(self) -> None:
self.records: dict[str, FailureRecord] = {}
def fingerprint(self, component: str, reason: str, detail: str | None = None) -> str:
base = "::".join([_clean(component), _clean(reason), _clean(detail)])
return sha256(base.encode("utf-8")).hexdigest()[:16]
def signature(self, component: str, reason: str, detail: str | None = None, severity: str = "medium") -> FailureSignature:
return FailureSignature(
component=_clean(component),
reason=_clean(reason),
fingerprint=self.fingerprint(component, reason, detail),
severity=severity,
)
def record_failure(
self,
component: str,
reason: str,
detail: str | None = None,
severity: str = "medium",
job_id: str | None = None,
remediation_hint: str | None = None,
) -> FailureRecord:
sig = self.signature(component, reason, detail, severity)
record = self.records.get(sig.fingerprint)
if record is None:
record = FailureRecord(signature=sig)
self.records[sig.fingerprint] = record
record.count += 1
record.last_seen = utc_now()
record.last_job_id = job_id
if detail:
example = _clean(detail)
if example not in record.examples:
record.examples.append(example)
record.examples = record.examples[-5:]
if remediation_hint and remediation_hint not in record.remediation_hints:
record.remediation_hints.append(remediation_hint)
record.remediation_hints = record.remediation_hints[-5:]
return record
def record_from_job(self, job: dict[str, Any]) -> FailureRecord | None:
status = job.get("status")
if status in {"completed", "waiting_owner"}:
return None
reason = job.get("last_error") or status or "unknown_failure"
return self.record_failure(
component="self_loop_job",
reason=reason,
detail=str(job.get("result") or job.get("last_error") or ""),
severity="high" if status == "failed" else "medium",
job_id=job.get("id"),
remediation_hint="inspect acceptance report and regenerate patch plan",
)
def mark_resolved(self, fingerprint: str) -> bool:
record = self.records.get(fingerprint)
if not record:
return False
record.resolved_count += 1
record.last_seen = utc_now()
return True
def recurrence_score(self, fingerprint: str) -> float:
record = self.records.get(fingerprint)
return record.recurrence_score() if record else 0.0
def top_failures(self, limit: int = 5) -> list[dict[str, Any]]:
ranked = sorted(
self.records.values(),
key=lambda record: (record.recurrence_score(), record.count, record.last_seen),
reverse=True,
)
return [record.to_dict() for record in ranked[:limit]]
def suggest_remediation(self, fingerprint: str) -> list[str]:
record = self.records.get(fingerprint)
if not record:
return []
if record.remediation_hints:
return list(record.remediation_hints)
reason = record.signature.reason
if "runtime" in reason:
return ["run runtime health probe", "request owner-gated runtime recovery if probe fails"]
if "acceptance" in reason or "test" in reason:
return ["rerun acceptance pipeline", "generate targeted low-risk patch"]
if "permission" in reason or "forbidden" in reason:
return ["verify GitOps branch policy", "request owner review for permission drift"]
return ["classify failure", "generate remediation plan", "keep owner gate for non-low-risk changes"]
def export(self) -> dict[str, Any]:
return {
"records": {fingerprint: record.to_dict() for fingerprint, record in self.records.items()},
"top_failures": self.top_failures(),
}
def load(self, data: dict[str, Any]) -> None:
self.records.clear()
for fingerprint, raw in data.get("records", {}).items():
sig_raw = raw.get("signature", {})
signature = FailureSignature(
component=sig_raw.get("component", "unknown"),
reason=sig_raw.get("reason", "unknown"),
fingerprint=sig_raw.get("fingerprint", fingerprint),
severity=sig_raw.get("severity", "medium"),
)
record = FailureRecord(signature=signature)
record.count = int(raw.get("count", 0))
record.first_seen = raw.get("first_seen", utc_now())
record.last_seen = raw.get("last_seen", utc_now())
record.last_job_id = raw.get("last_job_id")
record.examples = list(raw.get("examples", []))[-5:]
record.remediation_hints = list(raw.get("remediation_hints", []))[-5:]
record.resolved_count = int(raw.get("resolved_count", 0))
self.records[fingerprint] = record
__all__ = ["FailureSignature", "FailureRecord", "FailureMemory"]
@@ -1,98 +0,0 @@
"""Persistence adapters for FailureMemory.
Stores sanitized failure memory snapshots. The adapter only persists exported
FailureMemory dictionaries; it does not store secrets or raw runtime credentials.
"""
from __future__ import annotations
import json
import sqlite3
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .failure_memory import FailureMemory
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class MemoryStoreStatus:
status: str
path: str
records: int
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class JsonFailureMemoryStore:
def __init__(self, path: str | Path):
self.path = Path(path)
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
self.path.parent.mkdir(parents=True, exist_ok=True)
data = memory.export()
self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return MemoryStoreStatus("saved", str(self.path), len(data.get("records", {})))
def load(self) -> FailureMemory:
memory = FailureMemory()
if not self.path.exists():
return memory
memory.load(json.loads(self.path.read_text(encoding="utf-8")))
return memory
class SQLiteFailureMemoryStore:
def __init__(self, path: str | Path):
self.path = Path(path)
def _connect(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self.path))
conn.execute(
"""
CREATE TABLE IF NOT EXISTS self_loop_failure_memory (
fingerprint TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
return conn
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
data = memory.export().get("records", {})
with self._connect() as conn:
for fingerprint, payload in data.items():
conn.execute(
"""
INSERT INTO self_loop_failure_memory (fingerprint, payload_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(fingerprint) DO UPDATE SET
payload_json = excluded.payload_json,
updated_at = excluded.updated_at
""",
(fingerprint, json.dumps(payload, ensure_ascii=False), utc_now()),
)
return MemoryStoreStatus("saved", str(self.path), len(data))
def load(self) -> FailureMemory:
memory = FailureMemory()
if not self.path.exists():
return memory
records: dict[str, Any] = {}
with self._connect() as conn:
for fingerprint, payload_json in conn.execute("SELECT fingerprint, payload_json FROM self_loop_failure_memory"):
records[fingerprint] = json.loads(payload_json)
memory.load({"records": records})
return memory
__all__ = ["MemoryStoreStatus", "JsonFailureMemoryStore", "SQLiteFailureMemoryStore"]
+36
View File
@@ -0,0 +1,36 @@
"""Git repository location resolver with validation.
Prevents hard-coded repository names from silently pointing to stale paths.
"""
from __future__ import annotations
import os
from pathlib import Path
class GitRepoLocator:
def __init__(self, root: str | None = None):
self.root = Path(root or os.getenv("AIOS_GITOPS_ROOT", "/data/aios-gitops/runtime/gitea-bin/data/repositories"))
def candidates(self, owner: str = "aios", names: tuple[str, ...] = ("aios.git", "aios-v23.git")):
return [self.root / owner / name for name in names]
def locate(self, owner: str = "aios") -> dict:
found = [str(path) for path in self.candidates(owner) if path.exists()]
return {
"root": str(self.root),
"owner": owner,
"found": found,
"healthy": len(found) == 1,
"warning": "multiple repositories found" if len(found) > 1 else None,
}
def require(self, owner: str = "aios") -> Path:
result = self.locate(owner)
if not result["healthy"]:
raise RuntimeError(f"Git repo resolution failed: {result}")
return Path(result["found"][0])
__all__ = ["GitRepoLocator"]
@@ -1,59 +0,0 @@
"""GitOps commit planning adapter for self-loop patches.
Creates commit intents and validates governance. Actual repository writes remain
through the controlled GitOps Actions layer.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_patch_executor import PatchBundle
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class CommitIntent:
branch: str
message: str
files: tuple[str, ...]
direct_main_push: bool
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GitOpsCommitExecutor:
"""Produces safe commit intents instead of bypassing GitOps controls."""
def prepare(self, bundle: PatchBundle) -> CommitIntent:
if bundle.direct_main_push:
raise ValueError("direct main push is forbidden")
if not bundle.branch.startswith("patch/"):
raise ValueError("only patch branches are allowed")
return CommitIntent(
branch=bundle.branch,
message=f"Apply self-loop patch: {bundle.goal}",
files=tuple(file.path for file in bundle.files),
direct_main_push=False,
owner_approval_required=bundle.owner_approval_required,
)
def commit_result(self, intent: CommitIntent, revision: str | None = None) -> dict[str, Any]:
return {
"status": "ready" if not intent.owner_approval_required else "waiting_owner",
"branch": intent.branch,
"revision": revision,
"commit": intent.to_dict(),
"sync_required": True,
}
__all__ = ["CommitIntent", "GitOpsCommitExecutor"]
@@ -0,0 +1,148 @@
"""Bridge governance agents into the unified Agent Event Bus.
This module emits normalized governance events for requirement changes,
architecture reviews, user acceptance, test gates and version decisions. It uses
duck typing so it can work with the unified AgentEventBus when available, while
remaining testable without a running bus.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class EventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class GovernanceEventEnvelope:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryGovernanceEventBus:
"""Small fallback bus for tests and offline governance runs."""
def __init__(self) -> None:
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = GovernanceEventEnvelope(
id=f"evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class GovernanceEventBridge:
"""Publishes governance lifecycle events to the Agent Event Bus."""
def __init__(self, bus: EventPublisher | None = None):
self.bus = bus or InMemoryGovernanceEventBus()
def requirement_changed(self, requirement: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="requirement_changed",
source=requirement.get("source", "unknown"),
target="architect-agent",
summary=requirement.get("requirement", "requirement changed"),
payload=requirement,
correlation_id=requirement.get("id"),
)
def architecture_review_started(self, proposal: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_started",
source="architect-agent",
target="architecture-review-agent",
summary=proposal.get("title", "architecture review started"),
payload=proposal,
correlation_id=proposal.get("change_id"),
)
def architecture_review_completed(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_completed",
source="architecture-review-agent",
target="change-governance",
summary=f"architecture review {decision.get('status', 'completed')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
def user_acceptance_completed(self, report: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="user_acceptance_completed",
source="user-acceptance-agent",
target="change-governance",
summary=f"user acceptance {report.get('decision', 'completed')}",
payload=report,
correlation_id=report.get("change_id"),
)
def test_gate_completed(self, result: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="test_gate_completed",
source="test-gate",
target="change-governance",
summary=f"test gate {result.get('status', 'completed')}",
payload=result,
correlation_id=result.get("change_id"),
)
def version_decision_created(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="version_decision_created",
source="architect-agent",
target="owner-gate",
summary=f"version bump recommendation: {decision.get('version_bump', 'unknown')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
__all__ = [
"GovernanceEventEnvelope",
"InMemoryGovernanceEventBus",
"GovernanceEventBridge",
]
@@ -0,0 +1,32 @@
"""API for governance timeline visualization."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .governance_timeline_dashboard import GovernanceTimelineDashboard
dashboard = GovernanceTimelineDashboard()
def get_governance_timeline(events=None):
return dashboard.build(events)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["governance-timeline"])
@router.get("/timeline", operation_id="getGovernanceTimeline")
def timeline():
return get_governance_timeline()
return router
router = build_router()
__all__ = ["router", "get_governance_timeline"]
@@ -0,0 +1,106 @@
"""Governance timeline dashboard.
Turns governance events into a human-readable flow for Web UI so operators can
see why a change is blocked or ready for promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
EVENT_ORDER = {
"requirement_changed": 10,
"architecture_review_started": 20,
"architecture_review_completed": 30,
"test_gate_completed": 40,
"user_acceptance_completed": 50,
"version_decision_created": 60,
"owner_gate_completed": 70,
"promotion_completed": 80,
}
@dataclass(frozen=True)
class GovernanceTimelineStep:
id: str
kind: str
title: str
actor: str
status: str
timestamp: str
correlation_id: str | None = None
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GovernanceTimelineDashboard:
def build(self, events: list[dict[str, Any]] | None = None) -> dict[str, Any]:
events = events or []
steps = [self._event_to_step(event) for event in events]
steps.sort(key=lambda step: (EVENT_ORDER.get(step.kind, 999), step.timestamp))
blockers = [step for step in steps if step.status in {"blocked", "fail", "attention_required"}]
return {
"module": "governance-timeline",
"route": "/admin/ai-evolution/governance/timeline",
"title": "治理事件时间线",
"status": "attention_required" if blockers else "healthy",
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"blockers": [step.to_dict() for step in blockers],
"flow": self._flow_summary(steps),
}
def _event_to_step(self, event: dict[str, Any]) -> GovernanceTimelineStep:
payload = event.get("payload", {})
kind = event.get("kind", "unknown")
return GovernanceTimelineStep(
id=event.get("id", f"event-{len(str(event))}"),
kind=kind,
title=self._title(kind, payload),
actor=event.get("source", payload.get("actor", "unknown")),
status=payload.get("status") or payload.get("decision") or event.get("status", "ok"),
timestamp=event.get("created_at") or payload.get("created_at") or utc_now(),
correlation_id=event.get("correlation_id") or payload.get("change_id") or payload.get("id"),
detail=payload,
)
def _title(self, kind: str, payload: dict[str, Any]) -> str:
titles = {
"requirement_changed": "需求变化",
"architecture_review_started": "架构审查开始",
"architecture_review_completed": "架构审查完成",
"test_gate_completed": "测试 Gate 完成",
"user_acceptance_completed": "用户验收完成",
"version_decision_created": "版本建议产生",
"owner_gate_completed": "Owner Gate 完成",
"promotion_completed": "Promotion 完成",
}
return payload.get("title") or payload.get("summary") or titles.get(kind, kind)
def _flow_summary(self, steps: list[GovernanceTimelineStep]) -> dict[str, Any]:
present = {step.kind for step in steps}
required = [
"requirement_changed",
"architecture_review_completed",
"test_gate_completed",
"user_acceptance_completed",
"version_decision_created",
]
missing = [kind for kind in required if kind not in present]
return {
"required_steps": required,
"missing_steps": missing,
"ready_for_owner_gate": not missing,
}
__all__ = ["GovernanceTimelineStep", "GovernanceTimelineDashboard"]
@@ -0,0 +1,59 @@
"""Independent acceptance agents.
Simulates independent roles: user representative and architecture reviewer.
They provide acceptance evidence, not implementation decisions.
"""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AcceptanceReport:
agent: str
role: str
decision: str
evidence: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class UserAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"user_requirement_match",
"user_workflow_validation",
"permission_visibility_check",
]
return AcceptanceReport(
"user-acceptance-agent",
"user_representative",
"pass" if change.get("user_facing") else "not_required",
checks,
)
class ArchitectureAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"architecture_consistency",
"scalability_review",
"security_boundary_review",
"operability_review",
]
return AcceptanceReport(
"architecture-review-agent",
"system_architect",
"pass" if change.get("architecture_impact") else "not_required",
checks,
)
__all__ = ["AcceptanceReport", "UserAcceptanceAgent", "ArchitectureAcceptanceAgent"]
-33
View File
@@ -1,33 +0,0 @@
"""Portal operations API domain layer.
No secrets. Designed for user/group_admin/owner portal integration.
"""
ROLES = ("user", "group_admin", "owner")
class PortalOpsAPI:
def dashboard(self, role):
if role not in ROLES:
raise ValueError("invalid role")
return {
"role": role,
"modules": self.modules(role),
"status": "ok"
}
def modules(self, role):
common = ["profile", "alerts", "approvals", "audit"]
if role == "user":
return common + ["tasks", "usage"]
if role == "group_admin":
return common + ["group_users", "group_quota"]
return common + ["system", "gitops", "runtime", "security", "models"]
def approval(self, request_id, action):
return {"request_id": request_id, "action": action, "state": "recorded"}
def audit_event(self, actor, action):
return {"actor": actor, "action": action, "recorded": True}
def alert(self, alert_id, level):
return {"id": alert_id, "level": level, "state": "open"}
-493
View File
@@ -1,493 +0,0 @@
"""Three-role portal RBAC, workflow, alert, audit, and owner cockpit services.
This module is intentionally framework-neutral so it can be used by FastAPI routes,
unit tests, CLI checks, and future Hermes/OpenClaw agents without importing runtime
secrets or web framework globals.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Iterable
from uuid import uuid4
ROLE_USER = "user"
ROLE_GROUP_ADMIN = "group_admin"
ROLE_OWNER = "owner"
ROLES = (ROLE_USER, ROLE_GROUP_ADMIN, ROLE_OWNER)
SCOPE_SELF = "self"
SCOPE_GROUP = "group"
SCOPE_GLOBAL = "global"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class PortalContext:
user_id: str
role: str = ROLE_USER
group_id: str | None = None
display_name: str | None = None
def natural_scope(self) -> str:
if self.role == ROLE_OWNER:
return SCOPE_GLOBAL
if self.role == ROLE_GROUP_ADMIN:
return SCOPE_GROUP
return SCOPE_SELF
@dataclass(frozen=True)
class PortalResource:
scope: str
owner_user_id: str | None = None
group_id: str | None = None
resource_type: str = "portal"
resource_id: str | None = None
@dataclass(frozen=True)
class RBACDecision:
allowed: bool
reason: str
role: str
scope: str
action: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class ApprovalRequest:
id: str
request_type: str
requester_id: str
scope: str
group_id: str | None = None
target: str | None = None
payload: dict[str, Any] = field(default_factory=dict)
state: str = "pending"
decided_by: str | None = None
decision_reason: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class AlertRecord:
id: str
level: str
message: str
scope: str
group_id: str | None = None
owner_user_id: str | None = None
source: str = "portal"
state: str = "open"
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class AuditEvent:
id: str
actor_id: str
action: str
scope: str
group_id: str | None = None
resource_type: str = "portal"
resource_id: str | None = None
outcome: str = "success"
detail: dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class PortalModule:
key: str
title: str
route: str
permission: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
ROLE_PERMISSIONS: dict[str, set[str]] = {
ROLE_USER: {
"portal.dashboard.read:self",
"portal.profile.read:self",
"portal.tasks.read:self",
"portal.usage.read:self",
"portal.alerts.read:self",
"portal.approvals.create:self",
"portal.approvals.read:self",
"portal.audit.read:self",
},
ROLE_GROUP_ADMIN: {
"portal.dashboard.read:self",
"portal.dashboard.read:group",
"portal.profile.read:self",
"portal.tasks.read:self",
"portal.usage.read:self",
"portal.alerts.read:self",
"portal.alerts.read:group",
"portal.approvals.create:self",
"portal.approvals.read:self",
"portal.approvals.read:group",
"portal.approvals.decide:group",
"portal.audit.read:self",
"portal.audit.read:group",
"portal.group.users.read:group",
"portal.group.quota.read:group",
"portal.group.quota.write:group",
},
ROLE_OWNER: {
"portal.*:*",
},
}
ROLE_MODULES: dict[str, tuple[PortalModule, ...]] = {
ROLE_USER: (
PortalModule("profile", "个人中心", "/portal/user/profile", "portal.profile.read:self"),
PortalModule("tasks", "任务", "/portal/user/tasks", "portal.tasks.read:self"),
PortalModule("usage", "用量", "/portal/user/usage", "portal.usage.read:self"),
PortalModule("alerts", "告警", "/portal/user/alerts", "portal.alerts.read:self"),
PortalModule("approvals", "审批", "/portal/user/approvals", "portal.approvals.read:self"),
PortalModule("audit", "审计", "/portal/user/audit", "portal.audit.read:self"),
),
ROLE_GROUP_ADMIN: (
PortalModule("group_users", "组用户", "/portal/group_admin/users", "portal.group.users.read:group"),
PortalModule("group_quota", "组配额", "/portal/group_admin/quota", "portal.group.quota.read:group"),
PortalModule("alerts", "组告警", "/portal/group_admin/alerts", "portal.alerts.read:group"),
PortalModule("approvals", "组审批", "/portal/group_admin/approvals", "portal.approvals.read:group"),
PortalModule("audit", "组审计", "/portal/group_admin/audit", "portal.audit.read:group"),
),
ROLE_OWNER: (
PortalModule("system", "系统驾驶舱", "/portal/owner/system", "portal.system.read:global"),
PortalModule("gitops", "GitOps", "/portal/owner/gitops", "portal.gitops.read:global"),
PortalModule("runtime", "运行时", "/portal/owner/runtime", "portal.runtime.read:global"),
PortalModule("security", "安全", "/portal/owner/security", "portal.security.read:global"),
PortalModule("models", "模型", "/portal/owner/models", "portal.models.read:global"),
PortalModule("approvals", "全局审批", "/portal/owner/approvals", "portal.approvals.read:global"),
PortalModule("alerts", "全局告警", "/portal/owner/alerts", "portal.alerts.read:global"),
PortalModule("audit", "全局审计", "/portal/owner/audit", "portal.audit.read:global"),
),
}
class PortalRBACService:
"""Enforces user=self, group_admin=group, owner=global access."""
def __init__(self, permissions: dict[str, set[str]] | None = None):
self.permissions = permissions or ROLE_PERMISSIONS
def authorize(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> RBACDecision:
if ctx.role not in ROLES:
return RBACDecision(False, "unknown_role", ctx.role, SCOPE_SELF, action)
target = resource or PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
if ctx.role == ROLE_OWNER:
return RBACDecision(True, "owner_global_allow", ctx.role, target.scope, action)
if target.scope == SCOPE_GLOBAL:
return RBACDecision(False, "global_scope_requires_owner", ctx.role, target.scope, action)
if ctx.role == ROLE_USER:
if target.scope != SCOPE_SELF or target.owner_user_id not in (None, ctx.user_id):
return RBACDecision(False, "user_limited_to_self", ctx.role, target.scope, action)
return self._permission_decision(ctx.role, action, SCOPE_SELF)
if ctx.role == ROLE_GROUP_ADMIN:
if target.scope == SCOPE_SELF and target.owner_user_id in (None, ctx.user_id):
return self._permission_decision(ctx.role, action, SCOPE_SELF)
if target.scope == SCOPE_GROUP and ctx.group_id and target.group_id == ctx.group_id:
return self._permission_decision(ctx.role, action, SCOPE_GROUP)
return RBACDecision(False, "group_admin_limited_to_own_group", ctx.role, target.scope, action)
return RBACDecision(False, "deny_by_default", ctx.role, target.scope, action)
def visible_modules(self, ctx: PortalContext) -> list[dict[str, Any]]:
modules = ROLE_MODULES.get(ctx.role, ())
visible: list[dict[str, Any]] = []
for module in modules:
action, scope = module.permission.split(":", 1)
resource = PortalResource(scope=scope, owner_user_id=ctx.user_id, group_id=ctx.group_id)
if self.authorize(ctx, action, resource).allowed:
visible.append(module.to_dict())
return visible
def _permission_decision(self, role: str, action: str, scope: str) -> RBACDecision:
permissions = self.permissions.get(role, set())
if "portal.*:*" in permissions or f"{action}:{scope}" in permissions:
return RBACDecision(True, "permission_match", role, scope, action)
return RBACDecision(False, "permission_missing", role, scope, action)
class PortalRepository:
"""Small deterministic repository used for domain tests and route integration.
Production deployments can replace this repository with a SQL-backed adapter while
preserving the service contract.
"""
def __init__(self) -> None:
self.approvals: dict[str, ApprovalRequest] = {}
self.alerts: dict[str, AlertRecord] = {}
self.audit_events: list[AuditEvent] = []
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> ApprovalRequest:
approval = ApprovalRequest(
id=f"apr_{uuid4().hex[:12]}",
request_type=request_type,
requester_id=ctx.user_id,
scope=ctx.natural_scope(),
group_id=ctx.group_id,
target=target,
payload=payload or {},
)
self.approvals[approval.id] = approval
self.append_audit(ctx, "portal.approvals.create", approval.scope, approval.group_id, "approval", approval.id)
return approval
def decide_approval(self, ctx: PortalContext, approval_id: str, decision: str, reason: str | None = None) -> ApprovalRequest:
if decision not in {"approved", "rejected"}:
raise ValueError("decision must be approved or rejected")
approval = self.approvals[approval_id]
approval.state = decision
approval.decided_by = ctx.user_id
approval.decision_reason = reason
approval.updated_at = utc_now()
self.append_audit(ctx, f"portal.approvals.{decision}", approval.scope, approval.group_id, "approval", approval.id)
return approval
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> AlertRecord:
if level not in {"info", "warning", "high", "critical"}:
raise ValueError("invalid alert level")
alert_scope = scope or ctx.natural_scope()
alert = AlertRecord(
id=f"alt_{uuid4().hex[:12]}",
level=level,
message=message,
scope=alert_scope,
group_id=ctx.group_id if alert_scope == SCOPE_GROUP else None,
owner_user_id=ctx.user_id if alert_scope == SCOPE_SELF else None,
source=source,
)
self.alerts[alert.id] = alert
self.append_audit(ctx, "portal.alerts.create", alert.scope, alert.group_id, "alert", alert.id)
return alert
def resolve_alert(self, ctx: PortalContext, alert_id: str) -> AlertRecord:
alert = self.alerts[alert_id]
alert.state = "resolved"
alert.updated_at = utc_now()
self.append_audit(ctx, "portal.alerts.resolve", alert.scope, alert.group_id, "alert", alert.id)
return alert
def append_audit(
self,
ctx: PortalContext,
action: str,
scope: str | None = None,
group_id: str | None = None,
resource_type: str = "portal",
resource_id: str | None = None,
outcome: str = "success",
detail: dict[str, Any] | None = None,
) -> AuditEvent:
event = AuditEvent(
id=f"aud_{uuid4().hex[:12]}",
actor_id=ctx.user_id,
action=action,
scope=scope or ctx.natural_scope(),
group_id=group_id or ctx.group_id,
resource_type=resource_type,
resource_id=resource_id,
outcome=outcome,
detail=detail or {},
)
self.audit_events.append(event)
return event
def list_approvals(self, ctx: PortalContext) -> list[ApprovalRequest]:
return [a for a in self.approvals.values() if self._visible(ctx, a.scope, a.requester_id, a.group_id)]
def list_alerts(self, ctx: PortalContext) -> list[AlertRecord]:
return [a for a in self.alerts.values() if self._visible(ctx, a.scope, a.owner_user_id, a.group_id)]
def list_audit(self, ctx: PortalContext) -> list[AuditEvent]:
return [a for a in self.audit_events if self._visible(ctx, a.scope, a.actor_id, a.group_id)]
def _visible(self, ctx: PortalContext, scope: str, owner_user_id: str | None, group_id: str | None) -> bool:
if ctx.role == ROLE_OWNER:
return True
if scope == SCOPE_SELF:
return owner_user_id in (None, ctx.user_id)
if ctx.role == ROLE_GROUP_ADMIN and scope == SCOPE_GROUP:
return bool(ctx.group_id and group_id == ctx.group_id)
return False
class PortalDashboardService:
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
self.repo = repo or PortalRepository()
self.rbac = rbac or PortalRBACService()
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
resource = PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
decision = self.rbac.authorize(ctx, "portal.dashboard.read", resource)
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict(), "modules": []}
return {
"status": "ok",
"role": ctx.role,
"scope": ctx.natural_scope(),
"user_id": ctx.user_id,
"group_id": ctx.group_id,
"modules": self.rbac.visible_modules(ctx),
"counters": {
"approvals": len(self.repo.list_approvals(ctx)),
"alerts": len([a for a in self.repo.list_alerts(ctx) if a.state == "open"]),
"audit_events": len(self.repo.list_audit(ctx)),
},
}
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
decision = self.rbac.authorize(ctx, "portal.system.read", PortalResource(scope=SCOPE_GLOBAL))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
runtime = runtime or {}
return {
"status": "ok",
"scope": SCOPE_GLOBAL,
"runtime": {
"service": runtime.get("service", "unknown"),
"status": runtime.get("status", "unknown"),
"workspace_branch": runtime.get("workspace_branch"),
},
"gitops": {
"mode": runtime.get("mode", "git_patch_only"),
"sync_status": runtime.get("sync_status", "unknown"),
},
"security": {
"human_shortlinks": "login_required",
"secrets_in_git": False,
"direct_main_push": False,
},
"models": {
"custom_llm": bool(runtime.get("custom_llm", True)),
"user_priority": bool(runtime.get("user_model_priority", True)),
},
"ops": {
"open_alerts": len([a for a in self.repo.alerts.values() if a.state == "open"]),
"pending_approvals": len([a for a in self.repo.approvals.values() if a.state == "pending"]),
"audit_events": len(self.repo.audit_events),
},
}
class PortalOpsService:
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
self.repo = repo or PortalRepository()
self.rbac = rbac or PortalRBACService()
self.dashboard_service = PortalDashboardService(self.repo, self.rbac)
def check_rbac(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> dict[str, Any]:
return self.rbac.authorize(ctx, action, resource).to_dict()
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
return self.dashboard_service.dashboard(ctx)
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
return self.dashboard_service.owner_cockpit(ctx, runtime)
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> dict[str, Any]:
decision = self.rbac.authorize(ctx, "portal.approvals.create", PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
approval = self.repo.create_approval(ctx, request_type, payload, target)
return {"status": "ok", "approval": approval.to_dict()}
def decide_approval(self, ctx: PortalContext, approval_id: str, decision_value: str, reason: str | None = None) -> dict[str, Any]:
approval = self.repo.approvals[approval_id]
decision = self.rbac.authorize(ctx, "portal.approvals.decide", PortalResource(scope=approval.scope, group_id=approval.group_id, owner_user_id=approval.requester_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
updated = self.repo.decide_approval(ctx, approval_id, decision_value, reason)
return {"status": "ok", "approval": updated.to_dict()}
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> dict[str, Any]:
requested_scope = scope or ctx.natural_scope()
decision = self.rbac.authorize(ctx, "portal.alerts.read", PortalResource(scope=requested_scope, group_id=ctx.group_id, owner_user_id=ctx.user_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
alert = self.repo.create_alert(ctx, level, message, requested_scope, source)
return {"status": "ok", "alert": alert.to_dict()}
def list_alerts(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "alerts": [a.to_dict() for a in self.repo.list_alerts(ctx)]}
def list_approvals(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "approvals": [a.to_dict() for a in self.repo.list_approvals(ctx)]}
def list_audit(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "audit": [a.to_dict() for a in self.repo.list_audit(ctx)]}
def run_self_loop(self) -> dict[str, Any]:
owner = PortalContext("owner", ROLE_OWNER)
group_admin = PortalContext("admin", ROLE_GROUP_ADMIN, "g1")
user = PortalContext("user", ROLE_USER, "g1")
self.create_alert(owner, "info", "self-loop-started", SCOPE_GLOBAL, "self_loop")
approval = self.create_approval(user, "model_priority_change", {"model": "default"}, "model_priority")
approval_id = approval["approval"]["id"]
decision = self.decide_approval(group_admin, approval_id, "approved", "self-loop group approval")
return {
"status": "ok",
"checks": {
"user_dashboard": self.dashboard(user)["status"] == "ok",
"group_admin_dashboard": self.dashboard(group_admin)["status"] == "ok",
"owner_cockpit": self.owner_cockpit(owner)["status"] == "ok",
"approval_decision": decision["status"] == "ok",
"audit_written": len(self.repo.audit_events) >= 3,
},
}
def build_context(user_id: str, role: str = ROLE_USER, group_id: str | None = None, display_name: str | None = None) -> PortalContext:
return PortalContext(user_id=user_id, role=role, group_id=group_id, display_name=display_name)
__all__ = [
"ROLE_USER",
"ROLE_GROUP_ADMIN",
"ROLE_OWNER",
"ROLES",
"SCOPE_SELF",
"SCOPE_GROUP",
"SCOPE_GLOBAL",
"PortalContext",
"PortalResource",
"RBACDecision",
"ApprovalRequest",
"AlertRecord",
"AuditEvent",
"PortalRBACService",
"PortalRepository",
"PortalDashboardService",
"PortalOpsService",
"build_context",
]
-239
View File
@@ -1,239 +0,0 @@
"""HTTP routes for the three-role user portal.
The router is FastAPI-compatible but keeps handlers as plain functions so tests and
agent self-checks can exercise the contract without booting an ASGI server.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover - optional runtime dependency
from fastapi import APIRouter, Header, HTTPException
except Exception: # pragma: no cover - test/runtime fallback
APIRouter = None
Header = None
HTTPException = RuntimeError
from .portal_rbac_ops import (
ROLE_GROUP_ADMIN,
ROLE_OWNER,
ROLE_USER,
SCOPE_GLOBAL,
PortalOpsService,
PortalResource,
build_context,
)
portal_ops_service = PortalOpsService()
def context_from_headers(
user_id: str | None = None,
role: str | None = None,
group_id: str | None = None,
display_name: str | None = None,
):
return build_context(
user_id=user_id or "anonymous",
role=role or ROLE_USER,
group_id=group_id,
display_name=display_name,
)
def get_portal_dashboard(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.dashboard(ctx)
def get_owner_system_cockpit(user_id: str = "owner", runtime: dict[str, Any] | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=ROLE_OWNER)
return portal_ops_service.owner_cockpit(ctx, runtime)
def check_portal_rbac(
role: str,
action: str,
user_id: str = "anonymous",
group_id: str | None = None,
scope: str | None = None,
owner_user_id: str | None = None,
resource_group_id: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
resource = PortalResource(
scope=scope or ctx.natural_scope(),
owner_user_id=owner_user_id or user_id,
group_id=resource_group_id or group_id,
)
return portal_ops_service.check_rbac(ctx, action, resource)
def create_portal_approval(
role: str,
request_type: str,
user_id: str = "anonymous",
group_id: str | None = None,
payload: dict[str, Any] | None = None,
target: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.create_approval(ctx, request_type, payload, target)
def decide_portal_approval(
approval_id: str,
decision: str,
role: str,
user_id: str,
group_id: str | None = None,
reason: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.decide_approval(ctx, approval_id, decision, reason)
def list_portal_alerts(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_alerts(ctx)
def create_portal_alert(
role: str,
level: str,
message: str,
user_id: str = "anonymous",
group_id: str | None = None,
scope: str | None = None,
source: str = "portal",
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.create_alert(ctx, level, message, scope, source)
def list_portal_approvals(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_approvals(ctx)
def list_portal_audit_events(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_audit(ctx)
def run_portal_self_loop() -> dict[str, Any]:
return portal_ops_service.run_self_loop()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/portal", tags=["portal"])
@router.get("/dashboard", operation_id="getPortalDashboard")
def dashboard(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return get_portal_dashboard(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.post("/rbac/check", operation_id="checkPortalRBAC")
def rbac_check(payload: dict[str, Any]):
return check_portal_rbac(
role=payload.get("role", ROLE_USER),
action=payload["action"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
scope=payload.get("scope"),
owner_user_id=payload.get("owner_user_id"),
resource_group_id=payload.get("resource_group_id"),
)
@router.post("/approvals", operation_id="createPortalApproval")
def create_approval(payload: dict[str, Any]):
return create_portal_approval(
role=payload.get("role", ROLE_USER),
request_type=payload["request_type"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
payload=payload.get("payload"),
target=payload.get("target"),
)
@router.post("/approvals/{approval_id}/decision", operation_id="decidePortalApproval")
def decide_approval(approval_id: str, payload: dict[str, Any]):
return decide_portal_approval(
approval_id=approval_id,
decision=payload["decision"],
role=payload.get("role", ROLE_GROUP_ADMIN),
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
reason=payload.get("reason"),
)
@router.get("/approvals", operation_id="listPortalApprovals")
def approvals(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_approvals(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.get("/alerts", operation_id="listPortalAlerts")
def alerts(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_alerts(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.post("/alerts", operation_id="createPortalAlert")
def create_alert(payload: dict[str, Any]):
return create_portal_alert(
role=payload.get("role", ROLE_USER),
level=payload["level"],
message=payload["message"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
scope=payload.get("scope"),
source=payload.get("source", "portal"),
)
@router.get("/audit", operation_id="listPortalAuditEvents")
def audit(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_audit_events(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.get("/owner/system", operation_id="getOwnerSystemCockpit")
def owner_system(x_user_id: str | None = Header(default=None)):
return get_owner_system_cockpit(x_user_id or "owner")
@router.post("/self-loop", operation_id="runPortalSelfLoop")
def self_loop():
return run_portal_self_loop()
return router
router = build_router()
__all__ = [
"router",
"build_router",
"context_from_headers",
"get_portal_dashboard",
"get_owner_system_cockpit",
"check_portal_rbac",
"create_portal_approval",
"decide_portal_approval",
"list_portal_alerts",
"list_portal_approvals",
"list_portal_audit_events",
"run_portal_self_loop",
]
@@ -1,69 +0,0 @@
"""Production acceptance gate for self-loop promotion.
The gate verifies runtime, GitOps policy, portal readiness, and security rules
before any promotion request can proceed.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GateCheck:
name: str
passed: bool
detail: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ProductionGateResult:
status: str
checks: tuple[GateCheck, ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"checks": [c.to_dict() for c in self.checks],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
class ProductionAcceptanceGate:
def evaluate(
self,
runtime: dict[str, Any],
baseline: dict[str, Any],
acceptance: dict[str, Any],
owner_approval_required: bool = True,
) -> ProductionGateResult:
checks = (
GateCheck("runtime_active", runtime.get("status") == "active", "runtime status"),
GateCheck("git_patch_only", baseline.get("update_mode") == "git_patch_only", "patch-only policy"),
GateCheck("no_direct_main_push", baseline.get("codex_gitops", {}).get("direct_main_push") is False, "main push guard"),
GateCheck("acceptance_passed", acceptance.get("status") in {"pass", "waiting_owner_gate"}, "acceptance pipeline"),
GateCheck("no_secrets", baseline.get("codex_gitops", {}).get("api_keys_in_git") is False, "secret policy"),
)
passed = all(check.passed for check in checks)
if not passed:
status = "blocked"
elif owner_approval_required:
status = "waiting_owner_approval"
else:
status = "ready_for_promotion"
return ProductionGateResult(status, checks, owner_approval_required)
__all__ = ["GateCheck", "ProductionGateResult", "ProductionAcceptanceGate"]
@@ -0,0 +1,92 @@
"""Agent adapters for AIOS Search Gateway.
All runtime agents should call SearchGatewayAgentAdapter instead of performing
ad-hoc network search. This gives consistent provider fallback, profile
resolution, evidence capture, audit and event visibility.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from .search_gateway_orchestrator import SearchGatewayOrchestrator
@dataclass(frozen=True)
class AgentSearchContext:
agent: str
user_id: str | None = None
group_id: str | None = None
role: str = "user"
group_profile: dict[str, Any] | None = None
user_profile: dict[str, Any] | None = None
session_profile: dict[str, Any] | None = None
authorized_sources: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SearchGatewayAgentAdapter:
def __init__(self, orchestrator: SearchGatewayOrchestrator | None = None):
self.orchestrator = orchestrator or SearchGatewayOrchestrator()
def search(
self,
query: str,
context: AgentSearchContext,
intent: str = "general",
region: str | None = None,
time_range: str | None = None,
data_type: str | None = None,
domain_profiles: list[str] | None = None,
user_scope: str = "public",
correlation_id: str | None = None,
) -> dict[str, Any]:
payload = {
"query": query,
"requester": context.agent,
"intent": intent,
"region": region,
"time_range": time_range,
"data_type": data_type,
"domain_profiles": domain_profiles or [],
"user_scope": user_scope,
"authorized_sources": context.authorized_sources,
"group_profile": context.group_profile,
"user_profile": context.user_profile,
"session_profile": context.session_profile,
"correlation_id": correlation_id,
}
return self.orchestrator.execute(payload)
class OpenClawSearchClient(SearchGatewayAgentAdapter):
def search_public(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, user_scope="public", **kwargs)
class HermesSearchClient(SearchGatewayAgentAdapter):
def delegate_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, **kwargs)
class CodexSearchClient(SearchGatewayAgentAdapter):
def technical_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, intent=kwargs.pop("intent", "software_library"), **kwargs)
class CustomGPTSearchClient(SearchGatewayAgentAdapter):
def user_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, **kwargs)
__all__ = [
"AgentSearchContext",
"SearchGatewayAgentAdapter",
"OpenClawSearchClient",
"HermesSearchClient",
"CodexSearchClient",
"CustomGPTSearchClient",
]
@@ -0,0 +1,73 @@
"""Architect governance integration for Search Gateway.
Search Gateway is a shared platform capability. The Architect Agent periodically
reviews provider health, external search/browser capabilities, cost, security and
privacy impact, then emits C/R/V recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchArchitectureFinding:
finding_id: str
title: str
impact: str
recommendation: str
evidence: list[str] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchArchitectureDecision:
status: str
version_bump: str
reason: str
findings: list[SearchArchitectureFinding]
owner_gate_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"owner_gate_required": self.owner_gate_required,
"created_at": self.created_at,
}
class SearchArchitectIntegration:
def review(self, signals: dict[str, Any]) -> SearchArchitectureDecision:
findings: list[SearchArchitectureFinding] = []
if signals.get("provider_breaking_change"):
findings.append(SearchArchitectureFinding("search-provider-breaking", "Provider breaking change", "breaking", "owner_review"))
if signals.get("new_search_capability"):
findings.append(SearchArchitectureFinding("search-new-capability", "New search capability available", "architecture", "evaluate_adoption"))
if signals.get("cost_increase"):
findings.append(SearchArchitectureFinding("search-cost", "Search provider cost increase", "minor", "optimize_provider_order"))
if signals.get("security_advisory"):
findings.append(SearchArchitectureFinding("search-security", "Search security advisory", "breaking", "owner_review"))
if any(f.impact == "breaking" for f in findings):
return SearchArchitectureDecision("action_required", "V", "breaking search ecosystem/security change", findings, True)
if any(f.impact == "architecture" for f in findings):
return SearchArchitectureDecision("optimization_recommended", "R", "new search capability may improve architecture", findings, True)
if findings:
return SearchArchitectureDecision("minor_change_recommended", "C", "compatible provider optimization", findings, False)
return SearchArchitectureDecision("no_action", "none", "no relevant search architecture change", findings, False)
__all__ = ["SearchArchitectureFinding", "SearchArchitectureDecision", "SearchArchitectIntegration"]
+27
View File
@@ -0,0 +1,27 @@
"""Audit storage for Search Gateway executions."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
class SearchAuditStore:
def __init__(self, root: str = "runtime/search-gateway/audit"):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def append(self, record: dict[str, Any]) -> str:
path = self.root / f"{record.get('request_id', 'search')}.json"
path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
return str(path)
def list_recent(self) -> list[dict[str, Any]]:
result = []
for path in sorted(self.root.glob("*.json")):
result.append(json.loads(path.read_text(encoding="utf-8")))
return result[-50:]
__all__ = ["SearchAuditStore"]
+118
View File
@@ -0,0 +1,118 @@
"""Search Gateway event bridge.
Publishes normalized search lifecycle events into the AIOS Agent Event Bus when a
bus is available, with an in-memory fallback for tests and offline runs.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class SearchEventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class SearchEvent:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemorySearchEventBus:
def __init__(self):
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = SearchEvent(
id=f"search_evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class SearchEventBridge:
def __init__(self, bus: SearchEventPublisher | None = None):
self.bus = bus or InMemorySearchEventBus()
def search_started(self, request: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_started",
source=request.get("requester", "unknown"),
target="search-gateway",
summary=request.get("query", "search started"),
payload=request,
correlation_id=request.get("correlation_id"),
)
def provider_attempted(self, request_id: str, attempt: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_provider_attempted",
source="search-gateway",
target=attempt.get("provider", "unknown"),
summary=f"provider {attempt.get('provider', 'unknown')} {attempt.get('status', 'attempted')}",
payload=attempt,
correlation_id=request_id,
)
def fallback_triggered(self, request_id: str, reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="search_fallback_triggered",
source="search-gateway",
target="search-provider-router",
summary=reason,
payload={"reason": reason},
correlation_id=request_id,
)
def search_completed(self, response: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_completed",
source="search-gateway",
target=response.get("requester", "requester"),
summary=f"search {response.get('status', 'completed')}",
payload=response,
correlation_id=response.get("request_id"),
)
__all__ = ["SearchEvent", "InMemorySearchEventBus", "SearchEventBridge"]
+252
View File
@@ -0,0 +1,252 @@
"""Governed general-purpose Search Gateway for AIOS.
The gateway coordinates query planning, provider routing, fallback, evidence
normalization and compliance checks. It is provider-agnostic: runtime workers plug
in public or explicitly authorized search providers.
Search is a shared capability. Groups and users may customize provider order,
trusted domains, query expansion hints and domain profiles, but customization may
not override hard authorization, privacy, safety or audit rules.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
from .search_profile_resolver import DomainQueryProfileRegistry, SearchProfile, SearchProfileResolver
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchRequest:
query: str
requester: str
intent: str = "general"
region: str | None = None
time_range: str | None = None
data_type: str | None = None
user_scope: str = "public"
authorized_sources: list[str] = field(default_factory=list)
domain_profiles: list[str] = field(default_factory=list)
group_profile: dict[str, Any] | None = None
user_profile: dict[str, Any] | None = None
session_profile: dict[str, Any] | None = None
correlation_id: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchEvidence:
title: str
url: str | None
source: str
snippet: str
evidence_type: str = "web"
confidence: str = "medium"
retrieved_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchProviderAttempt:
provider: str
status: str
result_count: int = 0
reason: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchResponse:
request_id: str
status: str
answer: str
evidence: list[SearchEvidence]
attempts: list[SearchProviderAttempt]
confidence: str
expanded_queries: list[str] = field(default_factory=list)
resolved_profile: dict[str, Any] | None = None
need_clarification: list[str] = field(default_factory=list)
blocked_reasons: list[str] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"request_id": self.request_id,
"status": self.status,
"answer": self.answer,
"evidence": [e.to_dict() for e in self.evidence],
"attempts": [a.to_dict() for a in self.attempts],
"confidence": self.confidence,
"expanded_queries": self.expanded_queries,
"resolved_profile": self.resolved_profile,
"need_clarification": self.need_clarification,
"blocked_reasons": self.blocked_reasons,
"created_at": self.created_at,
}
class SearchProvider(Protocol):
name: str
def search(self, request: SearchRequest, queries: list[str]) -> tuple[list[SearchEvidence], SearchProviderAttempt]:
...
class QueryPlanner:
def __init__(self, domain_registry: DomainQueryProfileRegistry | None = None):
self.domain_registry = domain_registry or DomainQueryProfileRegistry()
def expand(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
base = request.query.strip()
queries = [base]
if request.region:
queries.append(f"{request.region} {base}")
if request.time_range:
queries.append(f"{base} {request.time_range}")
domain_profiles = list(dict.fromkeys([request.intent] + request.domain_profiles + profile.domain_profiles))
hints = list(profile.query_expansion_hints) + self.domain_registry.hints_for(domain_profiles)
for hint in hints:
queries.append(f"{base} {hint}")
for domain in profile.trusted_domains:
queries.append(f"site:{domain} {base}")
return list(dict.fromkeys([q for q in queries if q]))
class SearchPolicyGuard:
"""Blocks non-public or unauthorized collection patterns."""
FORBIDDEN_TERMS = (
"绕过登录",
"绕过验证码",
"验证码绕过",
"破解登录",
"个人成绩",
"学生隐私",
"受保护平台批量爬取",
)
def evaluate(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
q = request.query.lower()
reasons: list[str] = []
for term in self.FORBIDDEN_TERMS:
if term.lower() in q:
reasons.append(f"blocked_term:{term}")
if request.user_scope not in {"public", "authorized_user_file", "authorized_link", "internal_archive"}:
reasons.append(f"unsupported_scope:{request.user_scope}")
if request.data_type in {"personal_grade", "student_private_data", "private_person_data"} and request.user_scope != "authorized_user_file":
reasons.append("private_data_requires_user_authorized_file")
if not profile.audit_required:
reasons.append("audit_cannot_be_disabled")
if not profile.fallback_required:
reasons.append("fallback_cannot_be_disabled")
return reasons
class SearchGateway:
def __init__(self, providers: list[SearchProvider] | None = None):
self.providers = providers or []
self.profile_resolver = SearchProfileResolver()
self.planner = QueryPlanner()
self.guard = SearchPolicyGuard()
def search(self, request: SearchRequest) -> SearchResponse:
request_id = request.correlation_id or f"search_{uuid4().hex[:12]}"
profile = self.profile_resolver.resolve(request.group_profile, request.user_profile, request.session_profile)
expanded_queries = self.planner.expand(request, profile)
blocked = self.guard.evaluate(request, profile)
if blocked:
return SearchResponse(
request_id=request_id,
status="blocked",
answer="该搜索请求涉及未授权访问、验证码/登录绕过、受保护平台批量抓取或个人隐私数据,已阻断。可改为搜索公开网页、官方公告、公开文档,或处理用户授权上传的文件/链接。",
evidence=[],
attempts=[],
confidence="high",
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
blocked_reasons=blocked,
)
evidence: list[SearchEvidence] = []
attempts: list[SearchProviderAttempt] = []
providers = self._ordered_providers(profile)
for provider in providers:
try:
provider_evidence, attempt = provider.search(request, expanded_queries)
evidence.extend(provider_evidence)
attempts.append(attempt)
if provider_evidence:
break
except Exception as exc:
attempts.append(SearchProviderAttempt(provider=getattr(provider, "name", "unknown"), status="failed", reason=str(exc)))
if evidence:
return SearchResponse(
request_id=request_id,
status="found",
answer="已找到公开或授权来源结果。",
evidence=evidence,
attempts=attempts,
confidence=self._confidence(evidence),
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
)
return SearchResponse(
request_id=request_id,
status="not_found",
answer="未找到足够可靠的公开或授权结果;已记录 provider 尝试和失败原因。",
evidence=[],
attempts=attempts,
confidence="low",
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
need_clarification=self._clarification(request),
)
def _ordered_providers(self, profile: SearchProfile) -> list[SearchProvider]:
if not self.providers:
return []
by_name = {provider.name: provider for provider in self.providers}
ordered = [by_name[name] for name in profile.provider_order if name in by_name]
remaining = [provider for provider in self.providers if provider.name not in {p.name for p in ordered}]
return ordered + remaining
def _confidence(self, evidence: list[SearchEvidence]) -> str:
if any(e.confidence == "high" for e in evidence):
return "high"
if evidence:
return "medium"
return "low"
def _clarification(self, request: SearchRequest) -> list[str]:
if request.intent == "education_exam_data":
return ["请补充学校名称、联考名称、年级、科目,以及需要试卷/答案还是成绩统计。"]
if request.intent in {"company_research", "policy_regulation", "software_library", "product_research"}:
return ["请补充主体名称、地区/版本、时间范围、需要的数据类型或可信来源范围。"]
return ["请补充更具体的主体、平台、地区、时间、数据类型或授权文件/链接。"]
__all__ = [
"SearchRequest",
"SearchEvidence",
"SearchProviderAttempt",
"SearchResponse",
"SearchProvider",
"QueryPlanner",
"SearchPolicyGuard",
"SearchGateway",
]
+65
View File
@@ -0,0 +1,65 @@
"""API endpoints for AIOS Search Gateway."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .search_gateway import SearchGateway, SearchRequest
from .search_gateway_dashboard import SearchGatewayDashboard
gateway = SearchGateway()
dashboard = SearchGatewayDashboard()
def search_gateway_request(payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
return gateway.search(request).to_dict()
def get_search_gateway_status() -> dict[str, Any]:
return {
"module": "search-gateway",
"status": "ready",
"providers_configured": len(gateway.providers),
"policy_guard": "enabled",
"profile_customization": "enabled",
"fallback_required_when_provider_unavailable": True,
}
def get_search_gateway_overview() -> dict[str, Any]:
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/search", tags=["search-gateway"])
@router.get("/status", operation_id="getSearchGatewayStatus")
def status():
return get_search_gateway_status()
@router.get("/overview", operation_id="getSearchGatewayOverview")
def overview():
return get_search_gateway_overview()
@router.post("/query", operation_id="runSearchGatewayQuery")
def query(payload: dict[str, Any]):
return search_gateway_request(payload)
return router
router = build_router()
__all__ = [
"router",
"search_gateway_request",
"get_search_gateway_status",
"get_search_gateway_overview",
]
@@ -0,0 +1,85 @@
"""Search Gateway Web UI dashboard model."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchDashboardMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchDashboardPanel:
key: str
title: str
metrics: list[SearchDashboardMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class SearchGatewayDashboard:
def overview(self, search_runs: list[dict[str, Any]] | None = None, providers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
search_runs = search_runs or []
providers = providers or []
blocked = [r for r in search_runs if r.get("status") == "blocked"]
not_found = [r for r in search_runs if r.get("status") == "not_found"]
provider_failures = [p for p in providers if p.get("status") == "failed"]
panels = [
SearchDashboardPanel(
"search_health",
"搜索健康度",
[
SearchDashboardMetric("total_runs", "搜索请求数", len(search_runs)),
SearchDashboardMetric("not_found", "未找到结果", len(not_found), "warn" if not_found else "ok"),
SearchDashboardMetric("blocked", "合规阻断", len(blocked), "warn" if blocked else "ok"),
],
),
SearchDashboardPanel(
"provider_health",
"Provider 可用性",
[
SearchDashboardMetric("providers", "Provider 数量", len(providers)),
SearchDashboardMetric("provider_failures", "Provider 失败", len(provider_failures), "warn" if provider_failures else "ok"),
],
),
SearchDashboardPanel(
"evidence_quality",
"证据质量",
[
SearchDashboardMetric("with_evidence", "有证据的回答", len([r for r in search_runs if r.get("evidence")])) ,
SearchDashboardMetric("fallback_required", "要求多源降级", True),
],
),
]
return {
"module": "search-gateway",
"route": "/admin/ai-evolution/search",
"title": "AIOS 联网搜索治理中心",
"status": "attention_required" if provider_failures else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"recent_runs": search_runs[-20:],
}
__all__ = ["SearchDashboardMetric", "SearchDashboardPanel", "SearchGatewayDashboard"]
@@ -0,0 +1,41 @@
"""Runtime orchestrator joining Search Gateway, registry, audit and events."""
from __future__ import annotations
from typing import Any
from .search_audit_store import SearchAuditStore
from .search_event_bridge import SearchEventBridge
from .search_gateway import SearchGateway, SearchRequest
from .search_provider_registry import SearchProviderRegistry
class SearchGatewayOrchestrator:
def __init__(self):
self.registry = SearchProviderRegistry()
self.audit = SearchAuditStore()
self.events = SearchEventBridge()
self.gateway = SearchGateway(self.registry.ordered())
def execute(self, payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
self.events.search_started(request.to_dict())
response = self.gateway.search(request)
result = response.to_dict()
for attempt in result.get("attempts", []):
self.events.provider_attempted(result["request_id"], attempt)
if len(result.get("attempts", [])) > 1:
self.events.fallback_triggered(result["request_id"], "multiple providers attempted")
self.audit.append(result)
self.events.search_completed(result)
return result
def status(self) -> dict[str, Any]:
return {
"registry": self.registry.status(),
"audit": "enabled",
"events": "enabled",
}
__all__ = ["SearchGatewayOrchestrator"]
@@ -0,0 +1,124 @@
"""Search profile resolver for general-purpose AIOS Search Gateway.
The Search Gateway is a shared platform capability. Groups and users may customize
provider preference, query expansion hints, trusted sources and domain profiles on
top of the shared base profile. Customization is intentionally layered and cannot
override hard safety, authorization or audit rules.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
HARD_BLOCKED_BEHAVIORS = {
"bypass_login",
"bypass_captcha",
"scrape_private_student_grades",
"scrape_student_private_data",
"bulk_scrape_protected_platforms",
"collect_data_without_authorization",
}
@dataclass(frozen=True)
class SearchProfile:
profile_id: str
scope: str
provider_order: list[str] = field(default_factory=lambda: [
"cache",
"official_site",
"general_web",
"vertical_source",
"browser_fetch",
"user_authorized_file",
"internal_archive",
])
trusted_domains: list[str] = field(default_factory=list)
query_expansion_hints: list[str] = field(default_factory=list)
domain_profiles: list[str] = field(default_factory=list)
blocked_behaviors: set[str] = field(default_factory=lambda: set(HARD_BLOCKED_BEHAVIORS))
audit_required: bool = True
fallback_required: bool = True
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["blocked_behaviors"] = sorted(self.blocked_behaviors)
return data
class SearchProfileResolver:
"""Resolves base -> group -> user -> session search preferences."""
def base_profile(self) -> SearchProfile:
return SearchProfile(profile_id="base", scope="global")
def resolve(
self,
group_profile: dict[str, Any] | None = None,
user_profile: dict[str, Any] | None = None,
session_profile: dict[str, Any] | None = None,
) -> SearchProfile:
profile = self.base_profile().to_dict()
for overlay in (group_profile, user_profile, session_profile):
if overlay:
profile = self._merge(profile, overlay)
profile["blocked_behaviors"] = set(HARD_BLOCKED_BEHAVIORS)
profile["audit_required"] = True
profile["fallback_required"] = True
return SearchProfile(**profile)
def _merge(self, base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
merged = dict(base)
if overlay.get("provider_order"):
merged["provider_order"] = list(overlay["provider_order"])
for list_key in ("trusted_domains", "query_expansion_hints", "domain_profiles"):
merged[list_key] = self._dedupe(list(merged.get(list_key, [])) + list(overlay.get(list_key, [])))
merged["profile_id"] = overlay.get("profile_id", merged.get("profile_id", "custom"))
merged["scope"] = overlay.get("scope", merged.get("scope", "custom"))
return merged
def _dedupe(self, items: list[str]) -> list[str]:
return list(dict.fromkeys([item for item in items if item]))
class DomainQueryProfileRegistry:
"""Optional domain-specific expansions for the general Search Gateway."""
DEFAULT_PROFILES = {
"education_exam_data": [
"学校 年级 科目 试卷 答案 成绩统计",
"联考 月考 期中 期末 质量检测",
],
"policy_regulation": [
"官方 通知 政策 解读 实施细则",
"site:gov.cn OR site:edu.cn",
],
"software_library": [
"release notes changelog migration guide",
"official documentation GitHub issues",
],
"product_research": [
"specs review comparison price availability",
"official page manual support",
],
"company_research": [
"official announcement press release filings",
"leadership product financial report",
],
}
def hints_for(self, domain_profiles: list[str]) -> list[str]:
hints: list[str] = []
for profile in domain_profiles:
hints.extend(self.DEFAULT_PROFILES.get(profile, []))
return list(dict.fromkeys(hints))
__all__ = [
"HARD_BLOCKED_BEHAVIORS",
"SearchProfile",
"SearchProfileResolver",
"DomainQueryProfileRegistry",
]
@@ -0,0 +1,121 @@
"""Provider registry for the general AIOS Search Gateway.
The registry makes provider availability explicit and auditable. Provider
implementations are pluggable: production can register real public search,
official-site, vertical-source, browser-fetch, authorized-file or archive
providers without changing the Search Gateway core.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from .search_gateway import SearchEvidence, SearchProviderAttempt, SearchRequest
@dataclass(frozen=True)
class SearchProviderConfig:
name: str
provider_type: str
enabled: bool = True
requires_secret: bool = False
supports_public_web: bool = True
supports_authorized_sources: bool = False
health: str = "unknown"
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class NullSearchProvider:
"""Safe placeholder provider used until runtime wires a real adapter."""
def __init__(self, config: SearchProviderConfig):
self.config = config
self.name = config.name
def search(self, request: SearchRequest, queries: list[str]):
return [], SearchProviderAttempt(
provider=self.name,
status="not_configured" if self.config.enabled else "disabled",
result_count=0,
reason="provider adapter not wired in runtime",
)
class StaticArchiveProvider:
"""Small deterministic provider for tests and internal archived evidence."""
name = "internal_archive"
def __init__(self, records: list[dict[str, Any]] | None = None):
self.records = records or []
def search(self, request: SearchRequest, queries: list[str]):
evidence: list[SearchEvidence] = []
for record in self.records:
haystack = " ".join([record.get("title", ""), record.get("snippet", ""), record.get("tags", "")]).lower()
if any(q.lower() in haystack for q in queries):
evidence.append(
SearchEvidence(
title=record.get("title", "archive result"),
url=record.get("url"),
source=self.name,
snippet=record.get("snippet", ""),
evidence_type="internal_archive",
confidence=record.get("confidence", "medium"),
)
)
return evidence, SearchProviderAttempt(provider=self.name, status="ok", result_count=len(evidence))
class SearchProviderRegistry:
DEFAULT_PROVIDER_CONFIGS = [
SearchProviderConfig("cache", "cache", supports_public_web=False),
SearchProviderConfig("official_site", "official_site"),
SearchProviderConfig("general_web", "general_web"),
SearchProviderConfig("vertical_source", "vertical_source"),
SearchProviderConfig("browser_fetch", "browser_fetch"),
SearchProviderConfig("user_authorized_file", "authorized_file", supports_public_web=False, supports_authorized_sources=True),
SearchProviderConfig("internal_archive", "archive", supports_public_web=False),
]
def __init__(self):
self.configs: dict[str, SearchProviderConfig] = {}
self.providers: dict[str, Any] = {}
for config in self.DEFAULT_PROVIDER_CONFIGS:
self.register_config(config)
def register_config(self, config: SearchProviderConfig) -> None:
self.configs[config.name] = config
self.providers.setdefault(config.name, NullSearchProvider(config))
def register_provider(self, name: str, provider: Any, config: SearchProviderConfig | None = None) -> None:
if config:
self.configs[name] = config
elif name not in self.configs:
self.configs[name] = SearchProviderConfig(name=name, provider_type="custom")
self.providers[name] = provider
def ordered(self, order: list[str] | None = None) -> list[Any]:
order = order or list(self.configs)
result = [self.providers[name] for name in order if name in self.providers and self.configs[name].enabled]
result.extend([provider for name, provider in self.providers.items() if name not in order and self.configs[name].enabled])
return result
def status(self) -> dict[str, Any]:
return {
"providers": [self.configs[name].to_dict() for name in sorted(self.configs)],
"enabled": len([c for c in self.configs.values() if c.enabled]),
"configured": len([name for name, provider in self.providers.items() if not isinstance(provider, NullSearchProvider)]),
}
__all__ = [
"SearchProviderConfig",
"NullSearchProvider",
"StaticArchiveProvider",
"SearchProviderRegistry",
]
@@ -1,59 +0,0 @@
"""Self-evolution reporting for release and owner review."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class EvolutionReport:
status: str
improvements: list[str]
risks: list[str]
metrics: dict[str, Any]
recommendations: list[str]
created_at: str = utc_now()
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfEvolutionReporter:
def generate(
self,
supervisor: dict[str, Any],
failure_memory: dict[str, Any],
adaptive_policy: dict[str, Any],
) -> EvolutionReport:
failures = failure_memory.get("top_failures", [])
risks = []
if failures:
risks.append("recurring failures require owner-visible review")
if adaptive_policy.get("automation_level") == "analysis_only":
risks.append("automation reduced due to recurrence")
return EvolutionReport(
status="ready_for_review",
improvements=[
"self-loop supervisor enabled",
"failure memory accumulated",
"adaptive policy active",
],
risks=risks,
metrics={
"failure_records": len(failure_memory.get("records", {})),
"supervisor_cycles": supervisor.get("state", {}).get("cycles", 0),
},
recommendations=[
"continue low-risk autonomous patches",
"review owner-gated promotion requests",
],
)
__all__ = ["EvolutionReport", "SelfEvolutionReporter"]
@@ -1,89 +0,0 @@
"""Event hooks that trigger autonomous self-loop jobs."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .adaptive_policy import AdaptivePolicy
from .failure_memory import FailureMemory
from .self_loop_scheduler import SelfLoopScheduler
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SelfLoopEvent:
source: str
event_type: str
goal: str
runtime: dict[str, Any] = field(default_factory=dict)
baseline: dict[str, Any] = field(default_factory=dict)
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopEventHook:
SUPPORTED_EVENTS = {
"runtime_unhealthy",
"gitops_drift",
"test_failed",
"portal_gap_detected",
"acceptance_failed",
}
def __init__(self, scheduler: SelfLoopScheduler | None = None, memory: FailureMemory | None = None, policy: AdaptivePolicy | None = None):
self.scheduler = scheduler or SelfLoopScheduler()
self.memory = memory or FailureMemory()
self.policy = policy or AdaptivePolicy()
def parse(self, payload: dict[str, Any]) -> SelfLoopEvent:
return SelfLoopEvent(
source=str(payload.get("source", "unknown")),
event_type=str(payload.get("event_type", "unknown")),
goal=str(payload.get("goal", "investigate self-loop event")),
runtime=dict(payload.get("runtime", {})),
baseline=dict(payload.get("baseline", {})),
detail=str(payload.get("detail", "")),
)
def handle(self, payload: dict[str, Any]) -> dict[str, Any]:
event = self.parse(payload)
if event.event_type not in self.SUPPORTED_EVENTS:
return {
"status": "ignored",
"reason": "unsupported_event",
"event": event.to_dict(),
}
if event.event_type in {"test_failed", "acceptance_failed", "runtime_unhealthy"}:
self.memory.record_failure(
component=event.source,
reason=event.event_type,
detail=event.detail,
severity="high",
remediation_hint="run self-loop diagnosis and generate patch plan",
)
decision = self.policy.evaluate(self.memory, event.runtime, event.baseline, event.goal)
options = self.policy.apply_to_scheduler(decision)
job = self.scheduler.enqueue(
goal=event.goal,
runtime=event.runtime,
baseline=event.baseline,
max_attempts=options["max_attempts"],
)
return {
"status": "queued",
"event": event.to_dict(),
"policy": decision.to_dict(),
"job": job.to_dict(),
}
__all__ = ["SelfLoopEvent", "SelfLoopEventHook"]
-222
View File
@@ -1,222 +0,0 @@
"""Autonomous self-loop executor.
This layer turns probe/plan reports into safe execution outcomes. It does not
bypass owner governance: high-risk or restricted actions are blocked at the owner
approval gate and emitted as explicit pending work.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Callable
from .autonomous_self_loop import AutonomousSelfLoop, RemediationStep, SelfLoopReport
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ExecutionResult:
step_id: str
action: str
status: str
reason: str
risk: str
requires_owner_approval: bool
output: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class TestResult:
name: str
status: str
detail: str = ""
def passed(self) -> bool:
return self.status == "pass"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class ExecutionReport:
status: str
report_id: str
actions: list[ExecutionResult]
tests: list[TestResult]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"report_id": self.report_id,
"actions": [a.to_dict() for a in self.actions],
"tests": [t.to_dict() for t in self.tests],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
class SelfLoopTestRunner:
"""Deterministic self-loop checks that avoid shell execution and secrets."""
def run(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[TestResult]:
return [
self._runtime_active(runtime),
self._patch_only_policy(baseline),
self._owner_gate_policy(baseline),
self._portal_domain_smoke(),
]
def _runtime_active(self, runtime: dict[str, Any]) -> TestResult:
if runtime.get("status") == "active":
return TestResult("runtime_active", "pass", "runtime reports active")
return TestResult("runtime_active", "fail", "runtime is not active")
def _patch_only_policy(self, baseline: dict[str, Any]) -> TestResult:
if baseline.get("update_mode") == "git_patch_only":
return TestResult("git_patch_only", "pass", "patch-only updates enforced")
return TestResult("git_patch_only", "fail", "update mode is not git_patch_only")
def _owner_gate_policy(self, baseline: dict[str, Any]) -> TestResult:
codex_gitops = baseline.get("codex_gitops", {})
if codex_gitops.get("direct_main_push") is False:
return TestResult("owner_gate", "pass", "direct main push disabled")
return TestResult("owner_gate", "fail", "direct main push must remain disabled")
def _portal_domain_smoke(self) -> TestResult:
try:
from .portal_rbac_ops import ROLE_OWNER, PortalOpsService, build_context
service = PortalOpsService()
result = service.owner_cockpit(build_context("owner", ROLE_OWNER))
if result.get("status") == "ok":
return TestResult("portal_domain_smoke", "pass", "owner cockpit domain service responds")
return TestResult("portal_domain_smoke", "fail", "owner cockpit returned non-ok")
except Exception as exc: # pragma: no cover - defensive smoke output
return TestResult("portal_domain_smoke", "fail", f"portal smoke failed: {type(exc).__name__}")
class SelfLoopAgentExecutor:
"""Executes safe self-loop steps and gates restricted ones."""
SAFE_ACTIONS = {
"health_probe",
"test_execution",
"failure_classification",
"patch_plan_generation",
"report_generation",
"create-low-risk-patch",
}
RESTRICTED_ACTIONS = {
"request-runtime-recovery",
"commit-policy-patch",
"commit-security-policy",
"commit-runtime-config",
"commit-portal-config",
"production-promotion",
"permission-escalation",
"secret-rotation",
}
def __init__(self, loop: AutonomousSelfLoop | None = None, test_runner: SelfLoopTestRunner | None = None):
self.loop = loop or AutonomousSelfLoop(owner_approval_required=True)
self.test_runner = test_runner or SelfLoopTestRunner()
self.handlers: dict[str, Callable[[RemediationStep], dict[str, Any]]] = {
"create-low-risk-patch": self._create_low_risk_patch_manifest,
"patch_plan_generation": self._patch_plan_manifest,
"report_generation": self._report_manifest,
}
def execute_step(self, step: RemediationStep) -> ExecutionResult:
if step.requires_owner_approval or step.action in self.RESTRICTED_ACTIONS:
return ExecutionResult(
step.id,
step.action,
"blocked_owner_gate",
"restricted action requires owner approval",
step.risk,
True,
{"title": step.title},
)
if step.risk not in {"low"}:
return ExecutionResult(
step.id,
step.action,
"blocked_risk_gate",
"only low-risk autonomous actions are allowed",
step.risk,
True,
{"title": step.title},
)
handler = self.handlers.get(step.action, self._safe_noop_manifest)
return ExecutionResult(
step.id,
step.action,
"executed",
"safe autonomous action executed",
step.risk,
False,
handler(step),
)
def execute_report(self, report: SelfLoopReport, runtime: dict[str, Any], baseline: dict[str, Any]) -> ExecutionReport:
actions = [self.execute_step(step) for step in report.remediation]
tests = self.test_runner.run(runtime, baseline)
owner_approval_required = self.loop.owner_approval_required or any(a.requires_owner_approval for a in actions)
failed_tests = [t for t in tests if not t.passed()]
if failed_tests:
status = "failed_tests"
elif owner_approval_required:
status = "waiting_owner_gate"
else:
status = "completed"
return ExecutionReport(status, report.id, actions, tests, owner_approval_required)
def run_once(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> dict[str, Any]:
report = self.loop.evaluate(runtime, baseline)
execution = self.execute_report(report, runtime, baseline)
acceptance = self.loop.acceptance(report)
owner_approval_required = execution.owner_approval_required or acceptance["owner_approval_required"]
return {
"status": execution.status,
"can_unattended_continue": execution.status == "completed" and not owner_approval_required,
"owner_approval_required": owner_approval_required,
"plan": report.to_dict(),
"execution": execution.to_dict(),
}
def _create_low_risk_patch_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {
"patch_intent": step.title,
"branch_policy": "patch-only",
"direct_main_push": False,
"secrets_in_git": False,
}
def _patch_plan_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"plan": step.title, "action": step.action}
def _report_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"report": step.title, "generated": True}
def _safe_noop_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"noop": True, "title": step.title}
__all__ = [
"ExecutionResult",
"TestResult",
"ExecutionReport",
"SelfLoopTestRunner",
"SelfLoopAgentExecutor",
]
@@ -1,58 +0,0 @@
"""Adapter manifest for controlled GitOps Actions execution.
The adapter produces an action plan consumed by the GitOps bridge. It does not
embed credentials and does not perform direct repository writes.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_patch_executor import PatchBundle
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GitOpsActionPlan:
action: str
branch: str
operations: tuple[dict[str, Any], ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopGitOpsAdapter:
def build_action_plan(self, bundle: PatchBundle) -> GitOpsActionPlan:
operations = tuple(
{
"operation": "commitFile",
"path": patch_file.path,
"checksum": patch_file.checksum(),
}
for patch_file in bundle.files
)
return GitOpsActionPlan(
action="gitops_patch_apply",
branch=bundle.branch,
operations=operations,
owner_approval_required=bundle.owner_approval_required,
)
def validate_action_plan(self, plan: GitOpsActionPlan) -> dict[str, Any]:
return {
"valid": plan.branch.startswith("patch/") and plan.action == "gitops_patch_apply",
"branch": plan.branch,
"owner_approval_required": plan.owner_approval_required,
"operations": len(plan.operations),
}
__all__ = ["GitOpsActionPlan", "SelfLoopGitOpsAdapter"]
@@ -1,142 +0,0 @@
"""Multi-agent self-loop orchestration.
Coordinates planning, implementation proposals, and validation feedback from
Hermes/OpenClaw/CodexBridge-style agents without storing credentials or bypassing
GitOps governance.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_executor import SelfLoopAgentExecutor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AgentProfile:
name: str
role: str
weight: int
capabilities: tuple[str, ...]
@dataclass(frozen=True)
class AgentTask:
task_id: str
goal: str
task_type: str
risk: str = "low"
payload: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class AgentResponse:
agent: str
status: str
proposal: dict[str, Any]
confidence: float
@dataclass(frozen=True)
class AgentVote:
agent: str
approve: bool
weight: int
reason: str
@dataclass
class PatchManifest:
branch_policy: str
files: list[str]
tests: list[str]
approvals_required: bool
generated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopAgentOrchestrator:
AGENTS = (
AgentProfile("Hermes", "planner", 2, ("analysis", "planning", "risk")),
AgentProfile("OpenClaw", "implementation", 2, ("code", "patch", "test")),
AgentProfile("CodexBridge", "validation", 2, ("gitops", "validation", "acceptance")),
)
def __init__(self, executor: SelfLoopAgentExecutor | None = None):
self.executor = executor or SelfLoopAgentExecutor()
def dispatch(self, task: AgentTask) -> list[AgentResponse]:
responses = []
for agent in self.AGENTS:
if agent.role == "planner":
proposal = {"risk": task.risk, "plan": "classify issue and produce remediation steps"}
elif agent.role == "implementation":
proposal = {"patch": True, "branch_policy": "patch-only", "goal": task.goal}
else:
proposal = {"validation": ["tests", "gitops_policy", "acceptance_report"]}
responses.append(AgentResponse(agent.name, "ready", proposal, 0.9))
return responses
def vote(self, responses: list[AgentResponse]) -> dict[str, Any]:
votes = []
total = 0
approved = 0
for response in responses:
profile = next(a for a in self.AGENTS if a.name == response.agent)
approve = response.status == "ready"
votes.append(AgentVote(response.agent, approve, profile.weight, "ready" if approve else "rejected"))
total += profile.weight
approved += profile.weight if approve else 0
return {
"approved": approved >= total / 2,
"approval_weight": approved,
"total_weight": total,
"votes": [asdict(v) for v in votes],
}
def create_patch_manifest(self, task: AgentTask, requires_approval: bool) -> PatchManifest:
return PatchManifest(
branch_policy="patch-only",
files=[
"active-runtime/app",
"openapi",
"tests",
],
tests=[
"self_loop_executor",
"portal_self_loop",
"acceptance_pipeline",
],
approvals_required=requires_approval or task.risk != "low",
)
def run_cycle(self, runtime: dict[str, Any], baseline: dict[str, Any], goal: str) -> dict[str, Any]:
task = AgentTask("self-loop-cycle", goal, "remediation")
executor_result = self.executor.run_once(runtime, baseline)
responses = self.dispatch(task)
decision = self.vote(responses)
manifest = self.create_patch_manifest(task, executor_result["owner_approval_required"])
return {
"status": "ready_for_patch" if decision["approved"] else "blocked_agent_vote",
"executor": executor_result,
"agents": [asdict(r) for r in responses],
"vote": decision,
"patch_manifest": manifest.to_dict(),
}
__all__ = [
"AgentProfile",
"AgentTask",
"AgentResponse",
"SelfLoopAgentOrchestrator",
"PatchManifest",
]
@@ -1,284 +0,0 @@
"""Self-loop patch executor and acceptance pipeline.
This module converts multi-agent self-loop decisions into GitOps-safe patch
bundles, validates policy, runs deterministic acceptance checks, and prepares a
promotion request. It intentionally does not push main, rotate secrets, or bypass
owner approval.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from hashlib import sha256
from typing import Any
from .self_loop_orchestrator import SelfLoopAgentOrchestrator
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def stable_id(*parts: str) -> str:
digest = sha256("::".join(parts).encode("utf-8")).hexdigest()[:12]
return f"slp_{digest}"
@dataclass(frozen=True)
class PatchFile:
path: str
content: str
purpose: str
def checksum(self) -> str:
return sha256(self.content.encode("utf-8")).hexdigest()
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["sha256"] = self.checksum()
return data
@dataclass(frozen=True)
class PatchBundle:
id: str
branch: str
risk: str
goal: str
files: tuple[PatchFile, ...]
owner_approval_required: bool
direct_main_push: bool = False
secrets_in_git: bool = False
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"branch": self.branch,
"risk": self.risk,
"goal": self.goal,
"files": [f.to_dict() for f in self.files],
"owner_approval_required": self.owner_approval_required,
"direct_main_push": self.direct_main_push,
"secrets_in_git": self.secrets_in_git,
"created_at": self.created_at,
}
@dataclass(frozen=True)
class AcceptanceCheck:
name: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceReport:
status: str
patch_id: str
checks: tuple[AcceptanceCheck, ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"patch_id": self.patch_id,
"checks": [c.to_dict() for c in self.checks],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
@dataclass(frozen=True)
class PromotionRequest:
status: str
patch_id: str
branch: str
reason: str
owner_approval_required: bool
acceptance: AcceptanceReport
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"patch_id": self.patch_id,
"branch": self.branch,
"reason": self.reason,
"owner_approval_required": self.owner_approval_required,
"acceptance": self.acceptance.to_dict(),
}
class PatchPolicyValidator:
"""Validates GitOps safety constraints before any patch is promoted."""
FORBIDDEN_PATH_PREFIXES = (
".git/",
"secrets/",
"private/",
)
SECRET_MARKERS = (
"BEGIN PRIVATE KEY",
"api_key=",
"password=",
"token=",
"secret=",
)
def validate(self, bundle: PatchBundle) -> tuple[AcceptanceCheck, ...]:
checks: list[AcceptanceCheck] = []
checks.append(self._patch_only(bundle))
checks.append(self._no_direct_main_push(bundle))
checks.append(self._no_secret_files(bundle))
checks.append(self._file_paths_safe(bundle))
checks.append(self._content_safe(bundle))
return tuple(checks)
def _patch_only(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.branch.startswith("patch/"):
return AcceptanceCheck("patch_branch_policy", "pass", "branch is patch-scoped")
return AcceptanceCheck("patch_branch_policy", "fail", "branch must start with patch/")
def _no_direct_main_push(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.direct_main_push is False:
return AcceptanceCheck("no_direct_main_push", "pass", "direct main push disabled")
return AcceptanceCheck("no_direct_main_push", "fail", "direct main push is not allowed")
def _no_secret_files(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.secrets_in_git is False:
return AcceptanceCheck("no_secrets_in_git", "pass", "bundle declares no secrets")
return AcceptanceCheck("no_secrets_in_git", "fail", "secrets must not be stored in Git")
def _file_paths_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
unsafe = [f.path for f in bundle.files if f.path.startswith(self.FORBIDDEN_PATH_PREFIXES)]
if not unsafe:
return AcceptanceCheck("safe_file_paths", "pass", "all file paths are allowed")
return AcceptanceCheck("safe_file_paths", "fail", f"unsafe paths: {unsafe}")
def _content_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
flagged: list[str] = []
for patch_file in bundle.files:
content_lower = patch_file.content.lower()
for marker in self.SECRET_MARKERS:
if marker.lower() in content_lower:
flagged.append(patch_file.path)
break
if not flagged:
return AcceptanceCheck("safe_file_content", "pass", "no secret markers detected")
return AcceptanceCheck("safe_file_content", "fail", f"secret-like markers detected: {flagged}")
class AcceptancePipeline:
def __init__(self, validator: PatchPolicyValidator | None = None):
self.validator = validator or PatchPolicyValidator()
def run(self, bundle: PatchBundle, executor_result: dict[str, Any]) -> AcceptanceReport:
checks = list(self.validator.validate(bundle))
checks.extend(self._executor_checks(executor_result))
status = "pass" if all(c.passed() for c in checks) else "fail"
if status == "pass" and bundle.owner_approval_required:
status = "waiting_owner_gate"
return AcceptanceReport(status, bundle.id, tuple(checks), bundle.owner_approval_required)
def _executor_checks(self, executor_result: dict[str, Any]) -> tuple[AcceptanceCheck, ...]:
execution = executor_result.get("execution", {})
tests = execution.get("tests", [])
checks: list[AcceptanceCheck] = []
for test in tests:
checks.append(AcceptanceCheck(
name=f"executor_{test.get('name', 'unknown')}",
status="pass" if test.get("status") == "pass" else "fail",
detail=test.get("detail", ""),
))
if not checks:
checks.append(AcceptanceCheck("executor_tests_present", "fail", "no executor tests were reported"))
return tuple(checks)
class SelfLoopPatchExecutor:
def __init__(self, orchestrator: SelfLoopAgentOrchestrator | None = None, pipeline: AcceptancePipeline | None = None):
self.orchestrator = orchestrator or SelfLoopAgentOrchestrator()
self.pipeline = pipeline or AcceptancePipeline()
def build_patch_bundle(self, cycle: dict[str, Any], goal: str, branch: str = "patch/autonomous-self-loop") -> PatchBundle:
executor = cycle.get("executor", {})
owner_approval_required = bool(executor.get("owner_approval_required"))
manifest = cycle.get("patch_manifest", {})
risk = "medium" if owner_approval_required else "low"
patch_id = stable_id(branch, goal, cycle.get("status", "unknown"))
files = (
PatchFile(
path=f"patches/{patch_id}.self-loop.json",
purpose="self-loop patch manifest",
content=self._manifest_content(patch_id, goal, cycle, manifest),
),
)
return PatchBundle(
id=patch_id,
branch=branch,
risk=risk,
goal=goal,
files=files,
owner_approval_required=owner_approval_required or bool(manifest.get("approvals_required")),
)
def run_cycle_to_promotion(
self,
runtime: dict[str, Any],
baseline: dict[str, Any],
goal: str,
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
cycle = self.orchestrator.run_cycle(runtime, baseline, goal)
bundle = self.build_patch_bundle(cycle, goal, branch)
acceptance = self.pipeline.run(bundle, cycle.get("executor", {}))
promotion = self.prepare_promotion(bundle, acceptance)
return {
"status": promotion.status,
"cycle": cycle,
"patch_bundle": bundle.to_dict(),
"acceptance": acceptance.to_dict(),
"promotion": promotion.to_dict(),
}
def prepare_promotion(self, bundle: PatchBundle, acceptance: AcceptanceReport) -> PromotionRequest:
if acceptance.status == "fail":
return PromotionRequest("blocked_failed_acceptance", bundle.id, bundle.branch, "acceptance failed", True, acceptance)
if acceptance.owner_approval_required:
return PromotionRequest("waiting_owner_approval", bundle.id, bundle.branch, "owner approval required", True, acceptance)
return PromotionRequest("ready_for_auto_merge", bundle.id, bundle.branch, "low-risk patch passed acceptance", False, acceptance)
def _manifest_content(self, patch_id: str, goal: str, cycle: dict[str, Any], manifest: dict[str, Any]) -> str:
lines = [
"{",
f' "id": "{patch_id}",',
f' "goal": "{goal}",',
f' "status": "{cycle.get("status", "unknown")}",',
f' "branch_policy": "{manifest.get("branch_policy", "patch-only")}",',
' "direct_main_push": false,',
' "secrets_in_git": false,',
f' "owner_approval_required": {str(bool(manifest.get("approvals_required"))).lower()}',
"}",
]
return "\n".join(lines)
__all__ = [
"PatchFile",
"PatchBundle",
"AcceptanceCheck",
"AcceptanceReport",
"PromotionRequest",
"PatchPolicyValidator",
"AcceptancePipeline",
"SelfLoopPatchExecutor",
]
-140
View File
@@ -1,140 +0,0 @@
"""Runtime API wiring for autonomous self-loop jobs.
This module exposes plain functions and an optional FastAPI router for portal,
webhook, and worker integrations. It keeps all writes behind patch-only GitOps
plans and never stores secrets.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover - optional runtime dependency
from fastapi import APIRouter, Header
except Exception: # pragma: no cover - fallback for unit tests
APIRouter = None
Header = None
from .production_acceptance_gate import ProductionAcceptanceGate
from .self_loop_scheduler import SelfLoopScheduler, TriggerType
scheduler = SelfLoopScheduler()
production_gate = ProductionAcceptanceGate()
def enqueue_self_loop_job(
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
trigger: str = "event",
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
job = scheduler.enqueue(goal, runtime, baseline, TriggerType(trigger), branch)
return {"status": "queued", "job": job.to_dict()}
def run_self_loop_job(job_id: str) -> dict[str, Any]:
return {"status": "ok", "job": scheduler.run_job(job_id)}
def tick_self_loop_jobs(limit: int = 1) -> dict[str, Any]:
return {"status": "ok", "jobs": scheduler.tick(limit), "snapshot": scheduler.snapshot().to_dict()}
def list_self_loop_jobs() -> dict[str, Any]:
return {"status": "ok", "jobs": [job.to_dict() for job in scheduler.jobs.values()]}
def get_self_loop_job(job_id: str) -> dict[str, Any]:
job = scheduler.jobs.get(job_id)
if not job:
return {"status": "not_found", "job_id": job_id}
return {"status": "ok", "job": job.to_dict()}
def run_self_loop_once(
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
job = scheduler.enqueue(goal, runtime, baseline, TriggerType.MANUAL, branch, max_attempts=1)
return run_self_loop_job(job.id)
def evaluate_self_loop_production_gate(
runtime: dict[str, Any],
baseline: dict[str, Any],
acceptance: dict[str, Any],
owner_approval_required: bool = True,
) -> dict[str, Any]:
result = production_gate.evaluate(runtime, baseline, acceptance, owner_approval_required)
return {"status": "ok", "gate": result.to_dict()}
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/self-loop", tags=["self-loop"])
@router.post("/jobs", operation_id="enqueueSelfLoopJob")
def enqueue_job(payload: dict[str, Any]):
return enqueue_self_loop_job(
goal=payload["goal"],
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
trigger=payload.get("trigger", "event"),
branch=payload.get("branch", "patch/autonomous-self-loop"),
)
@router.get("/jobs", operation_id="listSelfLoopJobs")
def jobs():
return list_self_loop_jobs()
@router.get("/jobs/{job_id}", operation_id="getSelfLoopJob")
def get_job(job_id: str):
return get_self_loop_job(job_id)
@router.post("/jobs/{job_id}/run", operation_id="runSelfLoopJob")
def run_job(job_id: str):
return run_self_loop_job(job_id)
@router.post("/tick", operation_id="tickSelfLoopJobs")
def tick(payload: dict[str, Any] | None = None):
return tick_self_loop_jobs((payload or {}).get("limit", 1))
@router.post("/run-once", operation_id="runSelfLoopOnce")
def run_once(payload: dict[str, Any]):
return run_self_loop_once(
goal=payload["goal"],
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
branch=payload.get("branch", "patch/autonomous-self-loop"),
)
@router.post("/production-gate", operation_id="evaluateSelfLoopProductionGate")
def production_gate_route(payload: dict[str, Any]):
return evaluate_self_loop_production_gate(
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
acceptance=payload.get("acceptance", {}),
owner_approval_required=payload.get("owner_approval_required", True),
)
return router
router = build_router()
__all__ = [
"router",
"build_router",
"enqueue_self_loop_job",
"run_self_loop_job",
"tick_self_loop_jobs",
"list_self_loop_jobs",
"get_self_loop_job",
"run_self_loop_once",
"evaluate_self_loop_production_gate",
]
-163
View File
@@ -1,163 +0,0 @@
"""Autonomous self-loop scheduler.
Schedules event-triggered and interval-style self-loop cycles. The scheduler is
framework-neutral and deterministic: production code can invoke tick() from a
cron, worker, or webhook handler without giving the module direct access to
secrets, shell commands, or main-branch writes.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any
from uuid import uuid4
from .self_loop_patch_executor import SelfLoopPatchExecutor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class TriggerType(str, Enum):
INTERVAL = "interval"
EVENT = "event"
MANUAL = "manual"
class JobStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
WAITING_OWNER = "waiting_owner"
FAILED = "failed"
RETRY_PENDING = "retry_pending"
@dataclass
class SelfLoopJob:
id: str
goal: str
trigger: TriggerType
runtime: dict[str, Any]
baseline: dict[str, Any]
branch: str = "patch/autonomous-self-loop"
status: JobStatus = JobStatus.PENDING
attempts: int = 0
max_attempts: int = 3
last_error: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
result: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["trigger"] = self.trigger.value
data["status"] = self.status.value
return data
@dataclass(frozen=True)
class SchedulerSnapshot:
pending: int
running: int
completed: int
waiting_owner: int
failed: int
retry_pending: int
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopScheduler:
"""Small in-process scheduler used by runtime workers and tests."""
def __init__(self, executor: SelfLoopPatchExecutor | None = None):
self.executor = executor or SelfLoopPatchExecutor()
self.jobs: dict[str, SelfLoopJob] = {}
def enqueue(
self,
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
trigger: TriggerType | str = TriggerType.EVENT,
branch: str = "patch/autonomous-self-loop",
max_attempts: int = 3,
) -> SelfLoopJob:
trigger_value = trigger if isinstance(trigger, TriggerType) else TriggerType(trigger)
job = SelfLoopJob(
id=f"job_{uuid4().hex[:12]}",
goal=goal,
trigger=trigger_value,
runtime=runtime,
baseline=baseline,
branch=branch,
max_attempts=max_attempts,
)
self.jobs[job.id] = job
return job
def tick(self, limit: int = 1) -> list[dict[str, Any]]:
runnable = [j for j in self.jobs.values() if j.status in {JobStatus.PENDING, JobStatus.RETRY_PENDING}]
results: list[dict[str, Any]] = []
for job in runnable[:limit]:
results.append(self.run_job(job.id))
return results
def run_job(self, job_id: str) -> dict[str, Any]:
job = self.jobs[job_id]
job.status = JobStatus.RUNNING
job.attempts += 1
job.updated_at = utc_now()
try:
result = self.executor.run_cycle_to_promotion(job.runtime, job.baseline, job.goal, job.branch)
job.result = result
promotion_status = result.get("promotion", {}).get("status")
if promotion_status == "waiting_owner_approval":
job.status = JobStatus.WAITING_OWNER
elif promotion_status == "ready_for_auto_merge":
job.status = JobStatus.COMPLETED
elif job.attempts < job.max_attempts:
job.status = JobStatus.RETRY_PENDING
job.last_error = promotion_status or "promotion_not_ready"
else:
job.status = JobStatus.FAILED
job.last_error = promotion_status or "promotion_failed"
except Exception as exc: # pragma: no cover - defensive runtime guard
job.last_error = f"{type(exc).__name__}: {exc}"
job.status = JobStatus.RETRY_PENDING if job.attempts < job.max_attempts else JobStatus.FAILED
job.updated_at = utc_now()
return job.to_dict()
def retry_failed(self) -> list[str]:
retry_ids: list[str] = []
for job in self.jobs.values():
if job.status == JobStatus.FAILED and job.attempts < job.max_attempts:
job.status = JobStatus.RETRY_PENDING
job.updated_at = utc_now()
retry_ids.append(job.id)
return retry_ids
def snapshot(self) -> SchedulerSnapshot:
statuses = [job.status for job in self.jobs.values()]
return SchedulerSnapshot(
pending=statuses.count(JobStatus.PENDING),
running=statuses.count(JobStatus.RUNNING),
completed=statuses.count(JobStatus.COMPLETED),
waiting_owner=statuses.count(JobStatus.WAITING_OWNER),
failed=statuses.count(JobStatus.FAILED),
retry_pending=statuses.count(JobStatus.RETRY_PENDING),
)
__all__ = [
"TriggerType",
"JobStatus",
"SelfLoopJob",
"SchedulerSnapshot",
"SelfLoopScheduler",
]
@@ -1,77 +0,0 @@
"""Long-running supervisor model for autonomous self-loop runtime."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_event_hooks import SelfLoopEventHook
from .failure_memory import FailureMemory
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass
class SupervisorState:
running: bool = False
cycles: int = 0
last_cycle: str | None = None
last_status: str = "idle"
heartbeat: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopSupervisor:
def __init__(self, hook: SelfLoopEventHook | None = None, memory: FailureMemory | None = None):
self.memory = memory or FailureMemory()
self.hook = hook or SelfLoopEventHook(memory=self.memory)
self.state = SupervisorState()
def start(self) -> dict[str, Any]:
self.state.running = True
self.state.last_status = "running"
self.state.heartbeat = utc_now()
return self.status()
def stop(self) -> dict[str, Any]:
self.state.running = False
self.state.last_status = "stopped"
self.state.heartbeat = utc_now()
return self.status()
def heartbeat(self) -> dict[str, Any]:
self.state.heartbeat = utc_now()
return self.status()
def handle_event(self, payload: dict[str, Any]) -> dict[str, Any]:
if not self.state.running:
self.start()
self.state.cycles += 1
self.state.last_cycle = utc_now()
result = self.hook.handle(payload)
self.state.last_status = result.get("status", "unknown")
self.state.heartbeat = utc_now()
return result
def tick(self, events: list[dict[str, Any]]) -> dict[str, Any]:
results = [self.handle_event(event) for event in events]
return {
"status": "ok",
"supervisor": self.status(),
"events": results,
"failure_memory": self.memory.export(),
}
def status(self) -> dict[str, Any]:
return {
"state": self.state.to_dict(),
"failure_memory_size": len(self.memory.records),
}
__all__ = ["SupervisorState", "SelfLoopSupervisor"]
-70
View File
@@ -1,70 +0,0 @@
"""Resident worker model for self-loop supervision.
Provides a daemon-friendly lifecycle without forcing an infinite loop inside the
application. A process manager can call start/run_cycle/stop and persist state.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_supervisor import SelfLoopSupervisor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass
class WorkerState:
running: bool = False
cycles: int = 0
last_error: str | None = None
started_at: str | None = None
heartbeat: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopWorker:
def __init__(self, supervisor: SelfLoopSupervisor | None = None):
self.supervisor = supervisor or SelfLoopSupervisor()
self.state = WorkerState()
def start(self) -> dict[str, Any]:
self.state.running = True
self.state.started_at = utc_now()
self.state.heartbeat = utc_now()
self.supervisor.start()
return self.status()
def run_cycle(self, events: list[dict[str, Any]]) -> dict[str, Any]:
if not self.state.running:
self.start()
try:
result = self.supervisor.tick(events)
self.state.cycles += 1
self.state.heartbeat = utc_now()
return result
except Exception as exc:
self.state.last_error = f"{type(exc).__name__}: {exc}"
self.state.heartbeat = utc_now()
raise
def stop(self) -> dict[str, Any]:
self.state.running = False
self.supervisor.stop()
self.state.heartbeat = utc_now()
return self.status()
def status(self) -> dict[str, Any]:
return {
"worker": self.state.to_dict(),
"supervisor": self.supervisor.status(),
}
__all__ = ["WorkerState", "SelfLoopWorker"]
@@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Evolution Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0d1117; color: #c9d1d9; height: 100vh; overflow: hidden; }
.app { display: flex; height: 100vh; }
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.sidebar h1 { font-size: 18px; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 12px; margin-bottom: 8px; }
.sidebar-nav { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.sidebar-nav li { padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; color: #8b949e; }
.sidebar-nav li:hover, .sidebar-nav li.active { background: #1f6feb22; color: #58a6ff; }
.sidebar-nav li i { margin-right: 8px; }
.main { flex: 1; padding: 24px; overflow-y: auto; }
.main-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.main-header h2 { font-size: 20px; }
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
.status-badge.ok { background: #1b3a2d; color: #3fb950; border: 1px solid #3fb95044; }
.status-badge.degraded { background: #3a2e1b; color: #d29922; border: 1px solid #d2992244; }
.status-badge.fail { background: #3a1b1b; color: #f85149; border: 1px solid #f8514944; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.card-title { font-size: 14px; color: #8b949e; margin-bottom: 8px; }
.card-value { font-size: 28px; font-weight: 700; color: #f0f6fc; }
.row { display: flex; gap: 16px; }
.col { flex: 1; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
pre { white-space: pre-wrap; word-break: break-word; }
.command-item { padding: 12px; margin-bottom: 8px; border-radius: 6px; background: #0d1117; border: 1px solid #21262d; }
.command-item .meta { display: flex; gap: 8px; align-items: center; font-size: 12px; color: #8b949e; }
.command-item .name { font-size: 14px; color: #58a6ff; font-weight: 600; }
.command-item .state { font-size: 11px; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; }
.state-active { background: #1b3a2d; color: #3fb950; }
.state-running { background: #3a2e1b; color: #d29922; }
.state-completed { background: #1b3a2d; color: #3fb950; }
.state-failed { background: #3a1b1b; color: #f85149; }
.loading { text-align: center; padding: 40px; color: #8b949e; }
.error { padding: 20px; background: #3a1b1b; border: 1px solid #f8514944; border-radius: 8px; color: #f85149; margin-bottom: 16px; }
.version-item { padding: 8px 12px; background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 4px; display: flex; justify-content: space-between; }
.version-item .tag { color: #58a6ff; font-weight: 600; }
.version-item .name { color: #8b949e; font-size: 13px; }
.agent-item { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 8px; padding: 12px; }
.agent-item .agent-title { font-size: 14px; font-weight: 600; color: #c9d1d9; margin-bottom: 4px; }
.agent-item .agent-meta { font-size: 12px; color: #8b949e; }
.agent-run-item { padding: 8px 12px; margin-bottom: 4px; border-radius: 4px; background: #0d1117; border: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; }
.agent-run-item .run-id { color: #58a6ff; font-size: 13px; font-weight: 600; }
.agent-run-item .source { color: #8b949e; font-size: 12px; }
</style>
</head>
<body>
<div id="app" class="app">
<div class="sidebar">
<h1>🕹️ AI Evolution Dashboard</h1>
<ul class="sidebar-nav">
<li class="active" onclick="showPanel('overview')"><i>📊</i> Overview</li>
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
</ul>
</div>
<div class="main">
<div id="panel-overview" class="panel">
<div class="main-header">
<h2>System Overview</h2>
<div id="header-status" class="status-badge">Loading...</div>
</div>
<div id="loading" class="loading">Loading...</div>
<div id="error" class="error" style="display:none;"></div>
<div id="content">
<div class="row">
<div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div>
<div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div>
<div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title">Event Bus Status</div>
<pre id="bus-status">-</pre>
</div>
</div>
</div>
<div id="panel-versions" class="panel" style="display:none;">
<div class="main-header"><h2>Version Timeline</h2></div>
<div id="versions-list">Loading...</div>
</div>
<div id="panel-commands" class="panel" style="display:none;">
<div class="main-header"><h2>Command Lookup</h2></div>
<div id="commands-list">Loading...</div>
</div>
<div id="panel-agents" class="panel" style="display:none;">
<div class="main-header"><h2>Recent Agent Executions</h2></div>
<div id="panel-arch-owner" class="panel" style="display:none;">
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
<div id="arch-owner-content">Loading...</div>
</div>
<div id="panel-arch-approve" class="panel" style="display:none;">
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
<div id="arch-approve-content">Loading...</div>
</div>
<div id="agents-list">Loading...</div>
</div>
</div>
</div>
<script>
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('content').style.display = '';
const hs = document.getElementById('header-status');
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
document.getElementById('base').textContent = data.baseline || '-';
document.getElementById('branch').textContent = data.main_branch || 'main';
document.getElementById('uptime').textContent = data.uptime || '-';
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
}
async function loadVersions() {
const el = document.getElementById('versions-list');
el.innerHTML = '<div class="loading">Loading versions...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/versions');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '<div class="grid">';
for (const v of data.versions || []) {
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
}
}
async function loadCommands() {
const el = document.getElementById('commands-list');
el.innerHTML = '<div class="loading">Loading commands...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
let cmds = data.commands || [];
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
html += '<div><div class="card"><div class="card-title">By Category</div>';
const cats = {};
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
html += '</div></div></div>';
for (const cmd of cmds.slice().reverse()) {
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
}
}
async function loadAgents() {
const el = document.getElementById('agents-list');
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/agents');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '';
for (const agent of data.agents || []) {
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
}
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
} catch(e) {
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
}
}
function showPanel(name) {
async function loadOwnerGate() {
const el = document.getElementById('arch-owner-content');
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '<div class="row">';
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
html += '</div>';
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
if (data.pending_requests && data.pending_requests.length) {
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
html += '</div>';
}
} else {
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
async function loadApprove() {
const el = document.getElementById('arch-approve-content');
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '';
if (data.pending_requests && data.pending_requests.length) {
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
html += '<div style="display:flex;gap:8px;">';
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
html += '</div></div>';
}
html += '</div>';
} else {
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
document.getElementById('panel-' + name).style.display = '';
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
if (name === 'overview') {
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
fetchData();
}
const loaders = {
'arch-owner': loadOwnerGate,
'arch-approve': loadApprove,
versions: loadVersions,
commands: loadCommands,
agents: loadAgents,
};
const fn = loaders[name];
if (fn) fn();
}
fetchData();
</script>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
"""Task Center action API model for Web UI buttons."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .task_lifecycle import TaskLifecycle
lifecycle = TaskLifecycle()
def get_task_actions(task: dict[str, Any], role: str) -> dict[str, Any]:
return {
"task_id": task.get("task_id"),
"actions": lifecycle.available_actions(task, role),
}
def execute_task_action(task: dict[str, Any], action: str, role: str, reason: str | None = None):
return lifecycle.transition(task, action, role, reason)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-actions"])
@router.post("/{task_id}/actions/{action}", operation_id="executeTaskAction")
def action(task_id: str, action: str, payload: dict[str, Any]):
return execute_task_action(payload.get("task", {}), action, payload.get("role", "user"), payload.get("reason"))
return router
router = build_router()
__all__ = ["router", "get_task_actions", "execute_task_action"]
+115
View File
@@ -0,0 +1,115 @@
"""AIOS Task Center domain model.
The Task Center gives owners and authorized users a single Web UI page showing:
who assigned a task, who is executing it, progress, elapsed time, remaining
work, and whether completion notifications were sent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_time(value: str | None) -> datetime | None:
if not value:
return None
return datetime.fromisoformat(value.replace("Z", "+00:00"))
@dataclass(frozen=True)
class TaskNotificationStatus:
required: bool = True
sent: bool = False
channel: str | None = None
sent_at: str | None = None
recipient_scope: str | None = None
last_error: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class TaskProgress:
percent: int = 0
current_step: str = "pending"
completed_steps: list[str] = field(default_factory=list)
remaining_steps: list[str] = field(default_factory=list)
blocked_reason: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AIOSTaskRecord:
task_id: str
title: str
assigned_by: str
executor: str
source: str
status: str = "pending"
priority: str = "normal"
created_at: str = field(default_factory=utc_now)
started_at: str | None = None
updated_at: str = field(default_factory=utc_now)
completed_at: str | None = None
progress: TaskProgress = field(default_factory=TaskProgress)
notification: TaskNotificationStatus = field(default_factory=TaskNotificationStatus)
owner_gate_required: bool = False
owner_gate_status: str | None = None
evidence: list[str] = field(default_factory=list)
def elapsed_seconds(self) -> int:
start = parse_time(self.started_at) or parse_time(self.created_at)
end = parse_time(self.completed_at) or datetime.now(timezone.utc)
return int((end - start).total_seconds()) if start else 0
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["elapsed_seconds"] = self.elapsed_seconds()
data["progress"] = self.progress.to_dict()
data["notification"] = self.notification.to_dict()
return data
class TaskCenter:
def summarize(self, tasks: list[AIOSTaskRecord] | list[dict[str, Any]] | None = None) -> dict[str, Any]:
raw_tasks = tasks or []
task_dicts = [t.to_dict() if hasattr(t, "to_dict") else dict(t) for t in raw_tasks]
active = [t for t in task_dicts if t.get("status") in {"running", "pending", "blocked"}]
blocked = [t for t in task_dicts if t.get("status") == "blocked"]
completed = [t for t in task_dicts if t.get("status") == "completed"]
notification_pending = [
t for t in completed
if t.get("notification", {}).get("required") and not t.get("notification", {}).get("sent")
]
return {
"module": "task-center",
"route": "/admin/ai-evolution/tasks",
"title": "AIOS 任务中心",
"status": "attention_required" if blocked or notification_pending else "healthy",
"generated_at": utc_now(),
"summary": {
"total": len(task_dicts),
"active": len(active),
"blocked": len(blocked),
"completed": len(completed),
"notification_pending": len(notification_pending),
},
"tasks": sorted(task_dicts, key=lambda t: t.get("updated_at", ""), reverse=True),
}
__all__ = [
"TaskNotificationStatus",
"TaskProgress",
"AIOSTaskRecord",
"TaskCenter",
]
+34
View File
@@ -0,0 +1,34 @@
"""Task Center API."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .task_center import TaskCenter
task_center = TaskCenter()
def get_task_overview(tasks: list[dict[str, Any]] | None = None):
return task_center.summarize(tasks)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-center"])
@router.get("/overview", operation_id="getTaskCenterOverview")
def overview():
return get_task_overview()
return router
router = build_router()
__all__ = ["router", "get_task_overview"]
+81
View File
@@ -0,0 +1,81 @@
"""Task Center visualization model."""
from __future__ import annotations
from typing import Any
class TaskDashboardView:
def build(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
tasks = tasks or []
return {
"module": "task-center",
"route": "/admin/ai-evolution/tasks",
"title": "AIOS Task Center",
"views": {
"kanban": self._kanban(tasks),
"timeline": self._timeline(tasks),
"flow": self._flow(tasks),
"statistics": self._statistics(tasks),
},
"tasks": [self._task_card(task) for task in tasks],
}
def _task_card(self, task: dict[str, Any]) -> dict[str, Any]:
return {
"task_id": task.get("task_id"),
"title": task.get("title"),
"assigned_by": task.get("assigned_by"),
"executor": task.get("executor"),
"status": task.get("status"),
"progress": task.get("progress", {}),
"elapsed_seconds": task.get("elapsed_seconds", 0),
"remaining_steps": task.get("progress", {}).get("remaining_steps", []),
"notification": task.get("notification", {}),
"actions": [
"start",
"update_progress",
"block",
"request_approval",
"approve",
"reject",
"retry",
"complete",
"notify",
"close",
],
}
def _kanban(self, tasks):
columns = {"pending": [], "running": [], "blocked": [], "waiting_approval": [], "completed": []}
for task in tasks:
columns.setdefault(task.get("status", "pending"), []).append(task.get("task_id"))
return columns
def _timeline(self, tasks):
return [
{
"task_id": task.get("task_id"),
"updated_at": task.get("updated_at"),
"status": task.get("status"),
}
for task in tasks
]
def _flow(self, tasks):
return {
"nodes": ["created", "assigned", "running", "blocked", "approval", "completed", "closed"],
"task_count": len(tasks),
}
def _statistics(self, tasks):
return {
"total": len(tasks),
"running": len([t for t in tasks if t.get("status") == "running"]),
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
"waiting_approval": len([t for t in tasks if t.get("status") == "waiting_approval"]),
"completed": len([t for t in tasks if t.get("status") == "completed"]),
}
__all__ = ["TaskDashboardView"]
+129
View File
@@ -0,0 +1,129 @@
"""Task Center event bridge.
Converts task lifecycle actions and agent execution events into normalized AIOS
Task Center events. Uses duck-typed Agent Event Bus publisher when available and
an in-memory fallback for tests/offline runs.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class TaskEventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class TaskEvent:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryTaskEventBus:
def __init__(self):
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = TaskEvent(
id=f"task_evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class TaskEventBridge:
def __init__(self, bus: TaskEventPublisher | None = None):
self.bus = bus or InMemoryTaskEventBus()
def task_created(self, task: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="task_created",
source=task.get("assigned_by", "unknown"),
target=task.get("executor", "task-center"),
summary=task.get("title", "task created"),
payload=task,
correlation_id=task.get("task_id"),
)
def task_updated(self, task: dict[str, Any], action: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_updated",
source=task.get("executor", "task-center"),
target="task-center",
summary=f"task {action}: {task.get('status')}",
payload={"action": action, "task": task},
correlation_id=task.get("task_id"),
)
def task_blocked(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_blocked",
source=task.get("executor", "unknown"),
target="owner-gate",
summary=reason,
payload={"task": task, "reason": reason},
correlation_id=task.get("task_id"),
)
def approval_requested(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_approval_requested",
source=task.get("executor", "unknown"),
target="owner-gate",
summary=reason,
payload={"task": task, "reason": reason},
correlation_id=task.get("task_id"),
)
def notification_sent(self, task: dict[str, Any], channel: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_notification_sent",
source="notification-agent",
target=task.get("assigned_by", "owner"),
summary=f"notification sent via {channel}",
payload={"task": task, "channel": channel},
correlation_id=task.get("task_id"),
)
__all__ = ["TaskEvent", "InMemoryTaskEventBus", "TaskEventBridge"]
+147
View File
@@ -0,0 +1,147 @@
"""AIOS Task Lifecycle.
Defines a governed task state machine for task creation, assignment, execution,
blocking, approval, retry, completion, notification and closure.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
TERMINAL_STATES = {"completed", "cancelled", "failed", "closed"}
@dataclass(frozen=True)
class TaskTransition:
action: str
from_states: set[str]
to_state: str
required_role: str = "agent"
requires_reason: bool = False
notify: bool = False
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["from_states"] = sorted(self.from_states)
return data
class TaskLifecycle:
"""Central task state machine."""
TRANSITIONS = {
"assign": TaskTransition("assign", {"new", "pending"}, "assigned", "dispatcher"),
"start": TaskTransition("start", {"assigned", "pending"}, "running", "executor"),
"update_progress": TaskTransition("update_progress", {"running"}, "running", "executor"),
"block": TaskTransition("block", {"running", "assigned", "pending"}, "blocked", "executor", requires_reason=True, notify=True),
"request_approval": TaskTransition("request_approval", {"running", "blocked"}, "waiting_approval", "executor", requires_reason=True, notify=True),
"approve": TaskTransition("approve", {"waiting_approval"}, "approved", "owner", notify=True),
"reject": TaskTransition("reject", {"waiting_approval"}, "rejected", "owner", requires_reason=True, notify=True),
"resume": TaskTransition("resume", {"blocked", "approved"}, "running", "executor"),
"retry": TaskTransition("retry", {"failed", "blocked"}, "running", "executor", notify=True),
"complete": TaskTransition("complete", {"running", "approved"}, "completed", "executor", notify=True),
"notify": TaskTransition("notify", {"completed"}, "completed", "notifier"),
"close": TaskTransition("close", {"completed", "cancelled", "failed"}, "closed", "owner"),
"cancel": TaskTransition("cancel", {"new", "pending", "assigned", "running", "blocked", "waiting_approval"}, "cancelled", "owner", requires_reason=True, notify=True),
"fail": TaskTransition("fail", {"running", "assigned"}, "failed", "executor", requires_reason=True, notify=True),
}
ROLE_ORDER = {
"user": 1,
"agent": 2,
"executor": 3,
"dispatcher": 4,
"notifier": 4,
"group_admin": 5,
"owner": 9,
}
def available_actions(self, task: dict[str, Any], role: str) -> list[dict[str, Any]]:
state = task.get("status", "new")
actions = []
for name, transition in self.TRANSITIONS.items():
allowed_state = state in transition.from_states
allowed_role = self._role_allows(role, transition.required_role)
actions.append({
"action": name,
"label": self._label(name),
"enabled": allowed_state and allowed_role,
"from_state": state,
"to_state": transition.to_state,
"required_role": transition.required_role,
"requires_reason": transition.requires_reason,
"notify": transition.notify,
"disabled_reason": None if allowed_state and allowed_role else self._disabled_reason(allowed_state, allowed_role, transition),
})
return actions
def transition(self, task: dict[str, Any], action: str, actor_role: str, reason: str | None = None) -> dict[str, Any]:
if action not in self.TRANSITIONS:
raise ValueError(f"unknown task action: {action}")
transition = self.TRANSITIONS[action]
state = task.get("status", "new")
if state not in transition.from_states:
raise ValueError(f"action {action} not allowed from state {state}")
if not self._role_allows(actor_role, transition.required_role):
raise PermissionError(f"role {actor_role} cannot execute {action}; requires {transition.required_role}")
if transition.requires_reason and not reason:
raise ValueError(f"action {action} requires reason")
updated = dict(task)
updated["status"] = transition.to_state
updated["updated_at"] = utc_now()
history = list(updated.get("history", []))
history.append({
"action": action,
"from": state,
"to": transition.to_state,
"actor_role": actor_role,
"reason": reason,
"ts": utc_now(),
})
updated["history"] = history
if action == "complete":
updated["completed_at"] = utc_now()
notification = dict(updated.get("notification", {}))
notification.setdefault("required", True)
notification.setdefault("sent", False)
updated["notification"] = notification
return updated
def _role_allows(self, role: str, required: str) -> bool:
return self.ROLE_ORDER.get(role, 0) >= self.ROLE_ORDER.get(required, 0)
def _label(self, action: str) -> str:
labels = {
"assign": "分配任务",
"start": "开始执行",
"update_progress": "更新进度",
"block": "标记阻塞",
"request_approval": "请求审批",
"approve": "批准",
"reject": "拒绝",
"resume": "继续执行",
"retry": "重试",
"complete": "标记完成",
"notify": "发送通知",
"close": "关闭任务",
"cancel": "取消任务",
"fail": "标记失败",
}
return labels.get(action, action)
def _disabled_reason(self, allowed_state: bool, allowed_role: bool, transition: TaskTransition) -> str:
if not allowed_state:
return "当前状态不允许该操作"
if not allowed_role:
return f"需要角色: {transition.required_role}"
return "不可用"
__all__ = ["TaskTransition", "TaskLifecycle", "TERMINAL_STATES"]
+50
View File
@@ -0,0 +1,50 @@
"""Task Center orchestrator.
Coordinates task ingestion, persistence, lifecycle updates, events and statistics.
"""
from __future__ import annotations
from typing import Any
from .task_event_bridge import TaskEventBridge
from .task_source_ingestor import TaskSourceIngestor
from .task_statistics import TaskStatistics
from .task_store import TaskStore
class TaskOrchestrator:
def __init__(self):
self.ingestor = TaskSourceIngestor()
self.store = TaskStore()
self.events = TaskEventBridge()
self.statistics = TaskStatistics()
def create_from_command(self, path: str, bucket: str = "active") -> dict[str, Any]:
task = self.ingestor.command_file_to_task(path, bucket)
self.store.append(task)
self.events.task_created(task)
return task
def create_from_agent_execution(self, record: dict[str, Any]) -> dict[str, Any]:
task = self.ingestor.agent_execution_to_task(record)
self.store.append(task)
self.events.task_created(task)
return task
def sync_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
task = self.ingestor.event_to_task(event)
if task:
self.store.append(task)
self.events.task_updated(task, event.get("kind", "event"))
return task
def dashboard(self) -> dict[str, Any]:
tasks = self.store.latest()
return {
"tasks": tasks,
"statistics": self.statistics.summarize(tasks),
}
__all__ = ["TaskOrchestrator"]
+159
View File
@@ -0,0 +1,159 @@
"""Task source ingestion for AIOS Task Center.
Converts Command Watcher files, OpenClaw/Hermes/Codex execution records and
Generic Agent Event Bus records into normalized Task Center task dictionaries.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class TaskSourceRecord:
source_type: str
source_id: str
title: str
assigned_by: str
executor: str
status: str
priority: str = "normal"
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
evidence: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_task(self) -> dict[str, Any]:
return {
"task_id": f"task-{self.source_type}-{self.source_id}",
"title": self.title,
"assigned_by": self.assigned_by,
"executor": self.executor,
"source": self.source_type,
"status": self.status,
"priority": self.priority,
"created_at": self.created_at,
"updated_at": self.updated_at,
"progress": {
"percent": self._progress_percent(),
"current_step": self.status,
"completed_steps": [],
"remaining_steps": [],
"blocked_reason": self.metadata.get("blocked_reason"),
},
"notification": {
"required": True,
"sent": bool(self.metadata.get("notification_sent")),
"channel": self.metadata.get("notification_channel"),
"sent_at": self.metadata.get("notification_sent_at"),
},
"evidence": self.evidence,
"metadata": self.metadata,
}
def _progress_percent(self) -> int:
mapping = {
"new": 0,
"pending": 5,
"assigned": 15,
"running": 50,
"blocked": 50,
"waiting_approval": 60,
"approved": 70,
"completed": 100,
"closed": 100,
"failed": 100,
"cancelled": 100,
}
return mapping.get(self.status, 0)
class TaskSourceIngestor:
COMMAND_STATUS_MAP = {
"active": "pending",
"running": "running",
"completed": "completed",
"failed": "failed",
"archive": "closed",
}
def command_file_to_task(self, path: str, bucket: str = "active") -> dict[str, Any]:
p = Path(path)
title = p.stem
return TaskSourceRecord(
source_type="command_watcher",
source_id=title,
title=title,
assigned_by="command-watcher",
executor=self._infer_executor(title),
status=self.COMMAND_STATUS_MAP.get(bucket, "pending"),
priority="high" if "P0" in title or "fix" in title.lower() else "normal",
evidence=[str(p)],
metadata={"bucket": bucket},
).to_task()
def agent_execution_to_task(self, record: dict[str, Any]) -> dict[str, Any]:
source_id = record.get("execution_id", record.get("id", "unknown"))
status = record.get("status", "running")
return TaskSourceRecord(
source_type="agent_execution",
source_id=source_id,
title=record.get("title", f"Agent execution {source_id}"),
assigned_by=record.get("assigned_by", record.get("entry_agent", "agent-system")),
executor=record.get("closed_by", record.get("entry_agent", "unknown-agent")),
status=status,
priority=record.get("priority", "normal"),
created_at=record.get("started_at", utc_now()),
updated_at=record.get("ended_at", utc_now()),
evidence=[record.get("execution_id", source_id)],
metadata=record,
).to_task()
def event_to_task(self, event: dict[str, Any]) -> dict[str, Any] | None:
kind = event.get("kind")
payload = event.get("payload", {})
if kind not in {"task_created", "task_updated", "agent_started", "agent_completed", "agent_failed", "search_started", "search_completed"}:
return None
source_id = event.get("correlation_id") or event.get("id", "event")
status = self._status_from_event(kind, payload)
return TaskSourceRecord(
source_type="event_bus",
source_id=source_id,
title=payload.get("title") or event.get("summary", kind),
assigned_by=event.get("source", "event-bus"),
executor=event.get("target", "task-center"),
status=status,
created_at=event.get("created_at", utc_now()),
updated_at=event.get("created_at", utc_now()),
evidence=[event.get("id", source_id)],
metadata={"event_kind": kind, "payload": payload},
).to_task()
def _status_from_event(self, kind: str, payload: dict[str, Any]) -> str:
if kind.endswith("completed"):
return "completed"
if kind.endswith("failed"):
return "failed"
return payload.get("status", "running")
def _infer_executor(self, title: str) -> str:
lower = title.lower()
if "openclaw" in lower:
return "openclaw"
if "hermes" in lower:
return "hermes"
if "codex" in lower:
return "codex"
if "search" in lower:
return "openclaw"
return "openclaw"
__all__ = ["TaskSourceRecord", "TaskSourceIngestor"]
+33
View File
@@ -0,0 +1,33 @@
"""Task Center statistics."""
from typing import Any
class TaskStatistics:
def summarize(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
tasks = tasks or []
completed = [t for t in tasks if t.get("status") == "completed"]
blocked = [t for t in tasks if t.get("status") == "blocked"]
waiting = [t for t in tasks if t.get("status") == "waiting_approval"]
elapsed = [t.get("elapsed_seconds", 0) for t in tasks if t.get("elapsed_seconds") is not None]
return {
"total": len(tasks),
"completed": len(completed),
"blocked": len(blocked),
"waiting_approval": len(waiting),
"completion_rate": round(len(completed) / len(tasks) * 100, 2) if tasks else 0,
"average_elapsed_seconds": round(sum(elapsed) / len(elapsed), 2) if elapsed else 0,
"top_blockers": self._top_values(blocked, "blocker"),
"executor_distribution": self._top_values(tasks, "executor"),
}
def _top_values(self, tasks: list[dict[str, Any]], key: str):
result: dict[str, int] = {}
for task in tasks:
value = task.get(key)
if value:
result[value] = result.get(value, 0) + 1
return result
__all__ = ["TaskStatistics"]
+29
View File
@@ -0,0 +1,29 @@
"""Persistent Task Center store."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
class TaskStore:
def __init__(self, root: str = "runtime/task-center"):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
self.path = self.root / "tasks.jsonl"
def append(self, task: dict[str, Any]) -> None:
with self.path.open("a", encoding="utf-8") as f:
f.write(json.dumps(task, ensure_ascii=False) + "\n")
def list_all(self) -> list[dict[str, Any]]:
if not self.path.exists():
return []
return [json.loads(line) for line in self.path.read_text(encoding="utf-8").splitlines() if line]
def latest(self) -> list[dict[str, Any]]:
return self.list_all()[-100:]
__all__ = ["TaskStore"]
@@ -1,43 +0,0 @@
CREATE TABLE IF NOT EXISTS portal_roles (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
role TEXT NOT NULL,
group_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_approvals (
id TEXT PRIMARY KEY,
request_type TEXT NOT NULL,
requester_id TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
scope TEXT NOT NULL,
group_id TEXT,
payload_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_alerts (
id TEXT PRIMARY KEY,
level TEXT NOT NULL,
message TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'open',
scope TEXT NOT NULL,
group_id TEXT,
owner_user_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_audit_events (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
action TEXT NOT NULL,
scope TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
outcome TEXT NOT NULL DEFAULT 'success',
detail_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+2
View File
@@ -0,0 +1,2 @@
# Agent Acknowledgement
Initialized by CMD-20260708-fix-git-actions-write-and-enable-audit-plane
@@ -0,0 +1,36 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -0,0 +1,35 @@
# CMD-20260709-enable-autonomous-controller
## Command ID
CMD-20260709-enable-autonomous-controller
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
Enable autonomous controller for AIOS GitOps:
- Auto-detect issues
- Auto-diagnose
- Auto-generate fixes
- Auto-test
- Auto-create patches
- Auto-request approval
## Preconditions
- Agent Command Plane initialized (CMD-BOOTSTRAP-001)
- Git Actions write permission fixed (CMD-20260708)
- Audit plane enabled (CMD-20260708)
- Notification service enabled (CMD-20260708)
## Completion Criteria
- Autonomous controller service running
- Can detect, diagnose, fix, and report
- Pipeline: detect → diagnose → fix → test → patch → approve → commit → notify
@@ -0,0 +1,36 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -0,0 +1,35 @@
# CMD-20260709-enable-autonomous-controller
## Command ID
CMD-20260709-enable-autonomous-controller
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
Enable autonomous controller for AIOS GitOps:
- Auto-detect issues
- Auto-diagnose
- Auto-generate fixes
- Auto-test
- Auto-create patches
- Auto-request approval
## Preconditions
- Agent Command Plane initialized (CMD-BOOTSTRAP-001)
- Git Actions write permission fixed (CMD-20260708)
- Audit plane enabled (CMD-20260708)
- Notification service enabled (CMD-20260708)
## Completion Criteria
- Autonomous controller service running
- Can detect, diagnose, fix, and report
- Pipeline: detect → diagnose → fix → test → patch → approve → commit → notify
@@ -0,0 +1,7 @@
{
"nid": "n-1783527185",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:13:05+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783529900",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:20+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783529914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783530514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:08:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783531114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:18:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783531714",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:28:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783532314",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:38:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783532914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:48:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783533514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:58:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783534114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T02:08:34+0800"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783527185",
"created": "2026-07-09T00:13:05+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"test/alias-write-verify",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/gitops-write-auth-smoke-20260706T115230Z",
"patch/gpt-actions-auth-live-20260708"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783529900",
"created": "2026-07-09T00:58:20+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"test/alias-test-file-branch",
"patch/codex-online-sync-acceptance-20260706T102819Z",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/actions-invalid-diag-smoke-20260706T140311Z",
"patch/V1R2C2-autonomous-recovery-loop"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783529914",
"created": "2026-07-09T00:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783530514",
"created": "2026-07-09T01:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783531114",
"created": "2026-07-09T01:18:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783531714",
"created": "2026-07-09T01:28:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}

Some files were not shown because too many files have changed in this diff Show More