Compare commits
80
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5450e05205 | ||
|
|
18b20942fd | ||
|
|
1ca8fe3228 | ||
|
|
c1ed0ec449 | ||
|
|
06e45d089f | ||
|
|
3af3cca6ad | ||
|
|
809449a392 | ||
|
|
715107df1f | ||
|
|
09e8987355 | ||
|
|
302000e27d | ||
|
|
17cd30ec6d | ||
|
|
21a8a22ca8 | ||
|
|
cf7c69de18 | ||
|
|
643c456351 | ||
|
|
ce88fccae9 | ||
|
|
463850eb1b | ||
|
|
04c3aca7da | ||
|
|
75a2e7cd1f | ||
|
|
b0655da8b3 | ||
|
|
7fdcc52b56 | ||
|
|
f0a33f3adc | ||
|
|
33b7027a55 | ||
|
|
e2a546f077 | ||
|
|
e990780c88 | ||
|
|
f6d9bb1510 | ||
|
|
8ca4db3e8c | ||
|
|
d194d3a4d7 | ||
|
|
f5435e3087 | ||
|
|
88087dc7ff | ||
|
|
2f04b9f46c | ||
|
|
d8a426cea1 | ||
|
|
22d48bad4d | ||
|
|
99c50464d9 | ||
|
|
d0141450fe | ||
|
|
84c67fa743 | ||
|
|
d0536dbe14 | ||
|
|
4177a22936 | ||
|
|
5c38b23810 | ||
|
|
aa0f721ce8 | ||
|
|
2e94439f49 | ||
|
|
961c45c825 | ||
|
|
20f1f9820d | ||
|
|
a6f08472dd | ||
|
|
9f95029225 | ||
|
|
4ad4651ffa | ||
|
|
c0d008fae7 | ||
|
|
1974327e32 | ||
|
|
110817b60c | ||
|
|
95070ea029 | ||
|
|
44aea3a9a1 | ||
|
|
ce42691be5 | ||
|
|
6f823de67d | ||
|
|
7443fc7f26 | ||
|
|
e1d484655f | ||
|
|
362a41bff8 | ||
|
|
62974e879e | ||
|
|
cd713cd73a | ||
|
|
bd8afc3cc4 | ||
|
|
1688639d7d | ||
|
|
60d07f9b17 | ||
|
|
824ff5dc90 | ||
|
|
3d27f2eb43 | ||
|
|
a168610826 | ||
|
|
5447476b08 | ||
|
|
e58d99ba57 | ||
|
|
51d6f7e0c2 | ||
|
|
40459af09e | ||
|
|
c7dc61597a | ||
|
|
43d55ac3b1 | ||
|
|
736e981005 | ||
|
|
b84d6be3ca | ||
|
|
eff4940e9c | ||
|
|
0d6f2ce68d | ||
|
|
a654978813 | ||
|
|
a399e30a8e | ||
|
|
e0644c0297 | ||
|
|
37eed9df7d | ||
|
|
22582b834f | ||
|
|
8a0c5f0556 | ||
|
|
62b10d89c2 |
@@ -0,0 +1,45 @@
|
|||||||
|
portal_aios_acceptance:
|
||||||
|
version: V1R4C3
|
||||||
|
status: pending_deployment_validation
|
||||||
|
|
||||||
|
login:
|
||||||
|
- casdoor_auth_success
|
||||||
|
- portal_session_created
|
||||||
|
- portal_me_loaded
|
||||||
|
|
||||||
|
roles:
|
||||||
|
user:
|
||||||
|
checks:
|
||||||
|
- task_center_visible
|
||||||
|
- owner_modules_hidden
|
||||||
|
group_admin:
|
||||||
|
checks:
|
||||||
|
- task_center_visible
|
||||||
|
- operations_visible
|
||||||
|
- system_health_visible
|
||||||
|
- search_visible
|
||||||
|
- agents_visible
|
||||||
|
owner:
|
||||||
|
checks:
|
||||||
|
- all_aios_modules_visible
|
||||||
|
- governance_visible
|
||||||
|
- architecture_visible
|
||||||
|
- owner_gate_visible
|
||||||
|
|
||||||
|
api:
|
||||||
|
- manifest_returns_modules
|
||||||
|
- dashboard_returns_navigation
|
||||||
|
- rbac_filters_modules
|
||||||
|
|
||||||
|
ui:
|
||||||
|
- menu_rendered
|
||||||
|
- cards_rendered
|
||||||
|
- quick_actions_rendered
|
||||||
|
- forbidden_routes_hidden
|
||||||
|
|
||||||
|
success_signals:
|
||||||
|
- AIOS_PORTAL_ROUTE_READY
|
||||||
|
- AIOS_WEB_UI_MODULES_VISIBLE
|
||||||
|
- AIOS_RBAC_VISIBILITY_READY
|
||||||
|
- AIOS_PORTAL_AUTH_INTEGRATION_READY
|
||||||
|
- AIOS_PORTAL_USER_EXPERIENCE_READY
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""Autonomous goal completion supervisor.
|
||||||
|
|
||||||
|
Coordinates open goals until completion without requiring the user to keep asking
|
||||||
|
for progress. It breaks goals into tasks, routes work to the correct agent,
|
||||||
|
tracks blockers, retries safe failures, requests approvals only when policy
|
||||||
|
requires, and sends completion notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 CompletionGoal:
|
||||||
|
goal_id: str
|
||||||
|
title: str
|
||||||
|
owner: str
|
||||||
|
status: str = "open"
|
||||||
|
priority: str = "P1"
|
||||||
|
target_signals: list[str] = field(default_factory=list)
|
||||||
|
tasks: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
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(frozen=True)
|
||||||
|
class CompletionDecision:
|
||||||
|
status: str
|
||||||
|
action: str
|
||||||
|
assigned_to: str
|
||||||
|
reason: str
|
||||||
|
requires_user: bool = False
|
||||||
|
requires_owner_gate: bool = False
|
||||||
|
notification_required: bool = True
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class AutonomousGoalCompletionSupervisor:
|
||||||
|
"""Completes goals through AIOS autonomous loops."""
|
||||||
|
|
||||||
|
HARD_OWNER_GATE_REASONS = {
|
||||||
|
"secret_required",
|
||||||
|
"paid_provider_required",
|
||||||
|
"production_baseline_promotion",
|
||||||
|
"large_architecture_change",
|
||||||
|
"cross_tenant_data_change",
|
||||||
|
"private_data_source_enablement",
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_goal(self, title: str, owner: str = "owner", priority: str = "P1", target_signals: list[str] | None = None) -> CompletionGoal:
|
||||||
|
return CompletionGoal(
|
||||||
|
goal_id=f"goal_{uuid4().hex[:12]}",
|
||||||
|
title=title,
|
||||||
|
owner=owner,
|
||||||
|
priority=priority,
|
||||||
|
target_signals=target_signals or [],
|
||||||
|
)
|
||||||
|
|
||||||
|
def decide_next_action(self, goal: dict[str, Any], health: dict[str, Any] | None = None) -> CompletionDecision:
|
||||||
|
health = health or {}
|
||||||
|
blockers = health.get("blockers", []) if isinstance(health.get("blockers"), list) else []
|
||||||
|
owner_reason = self._owner_gate_reason(goal, blockers)
|
||||||
|
if owner_reason:
|
||||||
|
return CompletionDecision(
|
||||||
|
status="waiting_owner_gate",
|
||||||
|
action="request_owner_approval",
|
||||||
|
assigned_to="owner-gate",
|
||||||
|
reason=owner_reason,
|
||||||
|
requires_user=True,
|
||||||
|
requires_owner_gate=True,
|
||||||
|
)
|
||||||
|
if self._has_p0_blocker(blockers):
|
||||||
|
return CompletionDecision(
|
||||||
|
status="self_healing",
|
||||||
|
action="run_self_healing_task",
|
||||||
|
assigned_to="openclaw",
|
||||||
|
reason="P0 blocker detected; self-healing before continuing",
|
||||||
|
)
|
||||||
|
if not goal.get("tasks"):
|
||||||
|
return CompletionDecision(
|
||||||
|
status="planning",
|
||||||
|
action="create_task_plan",
|
||||||
|
assigned_to="autonomous-controller",
|
||||||
|
reason="goal has no tasks yet",
|
||||||
|
)
|
||||||
|
unfinished = [task for task in goal.get("tasks", []) if task.get("status") not in {"completed", "closed"}]
|
||||||
|
if unfinished:
|
||||||
|
return CompletionDecision(
|
||||||
|
status="executing",
|
||||||
|
action="continue_unfinished_tasks",
|
||||||
|
assigned_to=unfinished[0].get("executor", "openclaw"),
|
||||||
|
reason="unfinished tasks remain",
|
||||||
|
)
|
||||||
|
return CompletionDecision(
|
||||||
|
status="completed",
|
||||||
|
action="send_completion_notification",
|
||||||
|
assigned_to="notification-agent",
|
||||||
|
reason="all tasks completed",
|
||||||
|
)
|
||||||
|
|
||||||
|
def completion_summary(self, goal: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
tasks = goal.get("tasks", [])
|
||||||
|
completed = [task for task in tasks if task.get("status") in {"completed", "closed"}]
|
||||||
|
blocked = [task for task in tasks if task.get("status") in {"blocked", "waiting_approval"}]
|
||||||
|
return {
|
||||||
|
"goal_id": goal.get("goal_id"),
|
||||||
|
"title": goal.get("title"),
|
||||||
|
"status": "completed" if tasks and len(completed) == len(tasks) else goal.get("status", "open"),
|
||||||
|
"total_tasks": len(tasks),
|
||||||
|
"completed_tasks": len(completed),
|
||||||
|
"blocked_tasks": len(blocked),
|
||||||
|
"completion_rate": round(len(completed) / len(tasks) * 100, 2) if tasks else 0,
|
||||||
|
"updated_at": utc_now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _owner_gate_reason(self, goal: dict[str, Any], blockers: list[dict[str, Any]]) -> str | None:
|
||||||
|
for reason in goal.get("owner_gate_reasons", []):
|
||||||
|
if reason in self.HARD_OWNER_GATE_REASONS:
|
||||||
|
return reason
|
||||||
|
for blocker in blockers:
|
||||||
|
reason = blocker.get("owner_gate_reason")
|
||||||
|
if reason in self.HARD_OWNER_GATE_REASONS:
|
||||||
|
return reason
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _has_p0_blocker(self, blockers: list[dict[str, Any]]) -> bool:
|
||||||
|
return any(blocker.get("severity") in {"p0", "critical"} for blocker in blockers)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["CompletionGoal", "CompletionDecision", "AutonomousGoalCompletionSupervisor"]
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Autonomous Controller health adapter."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .blocker_task_adapter import BlockerTaskAdapter
|
|
||||||
from .git_permission_watchdog import GitPermissionWatchdog
|
|
||||||
from .system_blocker_memory import SystemBlockerMemory
|
|
||||||
from .system_health_center import SystemHealthCenter
|
|
||||||
|
|
||||||
|
|
||||||
class AutonomousHealthAdapter:
|
|
||||||
def __init__(self):
|
|
||||||
self.git = GitPermissionWatchdog()
|
|
||||||
self.memory = SystemBlockerMemory()
|
|
||||||
self.task_adapter = BlockerTaskAdapter()
|
|
||||||
self.health = SystemHealthCenter()
|
|
||||||
|
|
||||||
def check(self, previous_blockers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
||||||
git_report = self.git.scan().to_dict()
|
|
||||||
blockers = list(previous_blockers or [])
|
|
||||||
if git_report.get("status") != "healthy":
|
|
||||||
blockers.append({
|
|
||||||
"blocker_id": "git-permission",
|
|
||||||
"category": "git_permission",
|
|
||||||
"title": "Git object permission drift",
|
|
||||||
"severity": "p0",
|
|
||||||
"evidence": [str(git_report)],
|
|
||||||
})
|
|
||||||
return self.health.build(git_report=git_report, blockers=blockers)
|
|
||||||
|
|
||||||
def optimize_task(self, blocker: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
decision = self.memory.decide(blocker)
|
|
||||||
return self.task_adapter.to_task(blocker, decision.to_dict())
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AutonomousHealthAdapter"]
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
"""Convert recurring blockers into Task Center tasks.
|
|
||||||
|
|
||||||
When a blocker becomes recurrent or critical, it should not remain only as an
|
|
||||||
incident log. It becomes a visible task with owner/executor/progress/approval
|
|
||||||
state in the Task Center.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class BlockerTaskAdapter:
|
|
||||||
def to_task(self, blocker: dict[str, Any], decision: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
priority = decision.get("priority", "P2")
|
|
||||||
status = "waiting_approval" if decision.get("owner_gate_required") else "pending"
|
|
||||||
if priority == "P0":
|
|
||||||
status = "running"
|
|
||||||
return {
|
|
||||||
"task_id": f"blocker-{blocker.get('fingerprint', blocker.get('blocker_id', 'unknown'))}",
|
|
||||||
"title": f"Resolve blocker: {blocker.get('title', 'unknown')}",
|
|
||||||
"assigned_by": "system-blocker-memory",
|
|
||||||
"executor": decision.get("assigned_to", "autonomous-controller"),
|
|
||||||
"source": "system_blocker",
|
|
||||||
"status": status,
|
|
||||||
"priority": priority,
|
|
||||||
"created_at": utc_now(),
|
|
||||||
"updated_at": utc_now(),
|
|
||||||
"progress": {
|
|
||||||
"percent": 20 if status == "running" else 0,
|
|
||||||
"current_step": decision.get("action", "record_and_monitor"),
|
|
||||||
"completed_steps": ["blocker_recorded", "decision_created"],
|
|
||||||
"remaining_steps": self._remaining_steps(decision),
|
|
||||||
"blocked_reason": blocker.get("title") if status == "waiting_approval" else None,
|
|
||||||
},
|
|
||||||
"notification": {
|
|
||||||
"required": True,
|
|
||||||
"sent": False,
|
|
||||||
"channel": "wechat",
|
|
||||||
},
|
|
||||||
"owner_gate_required": bool(decision.get("owner_gate_required")),
|
|
||||||
"owner_gate_status": "pending" if decision.get("owner_gate_required") else None,
|
|
||||||
"evidence": blocker.get("evidence", []),
|
|
||||||
"metadata": {
|
|
||||||
"blocker": blocker,
|
|
||||||
"decision": decision,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _remaining_steps(self, decision: dict[str, Any]) -> list[str]:
|
|
||||||
steps = ["root_cause_analysis", "patch_or_repair", "test", "deploy", "verify", "close"]
|
|
||||||
if decision.get("architect_review_required"):
|
|
||||||
steps.insert(1, "architect_review")
|
|
||||||
if decision.get("owner_gate_required"):
|
|
||||||
steps.insert(2, "owner_approval")
|
|
||||||
return steps
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BlockerTaskAdapter"]
|
|
||||||
@@ -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"]
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
"""Command watcher stall detector.
|
|
||||||
|
|
||||||
Detects commands that remain in active/running states beyond the configured
|
|
||||||
threshold and emits blocker records so AIOS can self-optimize instead of waiting
|
|
||||||
for manual progress checks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ts(value: str | None) -> datetime | None:
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CommandStallRecord:
|
|
||||||
command_id: str
|
|
||||||
path: str
|
|
||||||
bucket: str
|
|
||||||
age_minutes: float
|
|
||||||
threshold_minutes: int
|
|
||||||
severity: str
|
|
||||||
detected_at: str = field(default_factory=utc_now)
|
|
||||||
|
|
||||||
def to_blocker(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"blocker_id": f"command-stall-{self.command_id}",
|
|
||||||
"category": "command_stuck",
|
|
||||||
"title": f"Command stalled: {self.command_id}",
|
|
||||||
"source": "command-stall-detector",
|
|
||||||
"severity": self.severity,
|
|
||||||
"status": "open",
|
|
||||||
"first_seen_at": self.detected_at,
|
|
||||||
"last_seen_at": self.detected_at,
|
|
||||||
"occurrence_count": 1,
|
|
||||||
"evidence": [self.path],
|
|
||||||
"metadata": self.to_dict(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandStallDetector:
|
|
||||||
def __init__(self, root: str = "agents/commands", threshold_minutes: int = 15):
|
|
||||||
self.root = Path(root)
|
|
||||||
self.threshold_minutes = threshold_minutes
|
|
||||||
|
|
||||||
def detect(self) -> list[CommandStallRecord]:
|
|
||||||
records: list[CommandStallRecord] = []
|
|
||||||
for bucket in ("active", "running"):
|
|
||||||
bucket_path = self.root / bucket
|
|
||||||
if not bucket_path.exists():
|
|
||||||
continue
|
|
||||||
for path in bucket_path.glob("*.md"):
|
|
||||||
age = self._age_minutes(path)
|
|
||||||
if age >= self.threshold_minutes:
|
|
||||||
records.append(
|
|
||||||
CommandStallRecord(
|
|
||||||
command_id=path.stem,
|
|
||||||
path=str(path),
|
|
||||||
bucket=bucket,
|
|
||||||
age_minutes=round(age, 2),
|
|
||||||
threshold_minutes=self.threshold_minutes,
|
|
||||||
severity="p0" if bucket == "running" else "high",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return records
|
|
||||||
|
|
||||||
def _age_minutes(self, path: Path) -> float:
|
|
||||||
stat = path.stat()
|
|
||||||
age_seconds = datetime.now(timezone.utc).timestamp() - stat.st_mtime
|
|
||||||
return age_seconds / 60
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["CommandStallRecord", "CommandStallDetector"]
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""Git object permission watchdog and self-healing.
|
|
||||||
|
|
||||||
Prevents AIOS autonomous operation from being blocked by recurring
|
|
||||||
`.git/objects` ownership drift. The repair is intentionally conservative and
|
|
||||||
limited to the workspace Git object database.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import pwd
|
|
||||||
import grp
|
|
||||||
import subprocess
|
|
||||||
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 GitPermissionIssue:
|
|
||||||
path: str
|
|
||||||
issue: str
|
|
||||||
actual_uid: int | None = None
|
|
||||||
actual_gid: int | None = None
|
|
||||||
expected_user: str = "aiman"
|
|
||||||
expected_group: str = "gitops"
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class GitPermissionReport:
|
|
||||||
workspace: str
|
|
||||||
expected_user: str
|
|
||||||
expected_group: str
|
|
||||||
status: str
|
|
||||||
issues: list[GitPermissionIssue] = field(default_factory=list)
|
|
||||||
checked_at: str = field(default_factory=utc_now)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"workspace": self.workspace,
|
|
||||||
"expected_user": self.expected_user,
|
|
||||||
"expected_group": self.expected_group,
|
|
||||||
"status": self.status,
|
|
||||||
"issues": [issue.to_dict() for issue in self.issues],
|
|
||||||
"checked_at": self.checked_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GitPermissionWatchdog:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
workspace: str = "/data/aios-gitops/workspace",
|
|
||||||
expected_user: str = "aiman",
|
|
||||||
expected_group: str = "gitops",
|
|
||||||
max_scan_depth: int = 2,
|
|
||||||
):
|
|
||||||
self.workspace = Path(workspace)
|
|
||||||
self.expected_user = expected_user
|
|
||||||
self.expected_group = expected_group
|
|
||||||
self.max_scan_depth = max_scan_depth
|
|
||||||
|
|
||||||
@property
|
|
||||||
def objects_dir(self) -> Path:
|
|
||||||
return self.workspace / ".git" / "objects"
|
|
||||||
|
|
||||||
def scan(self) -> GitPermissionReport:
|
|
||||||
issues: list[GitPermissionIssue] = []
|
|
||||||
if not self.objects_dir.exists():
|
|
||||||
issues.append(GitPermissionIssue(str(self.objects_dir), "missing_objects_dir"))
|
|
||||||
return GitPermissionReport(str(self.workspace), self.expected_user, self.expected_group, "blocked", issues)
|
|
||||||
|
|
||||||
expected_uid = pwd.getpwnam(self.expected_user).pw_uid
|
|
||||||
expected_gid = grp.getgrnam(self.expected_group).gr_gid
|
|
||||||
|
|
||||||
for path in self._walk_limited(self.objects_dir):
|
|
||||||
try:
|
|
||||||
stat = path.stat()
|
|
||||||
except FileNotFoundError:
|
|
||||||
continue
|
|
||||||
if stat.st_uid != expected_uid:
|
|
||||||
issues.append(GitPermissionIssue(str(path), "owner_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
|
|
||||||
if stat.st_gid != expected_gid:
|
|
||||||
issues.append(GitPermissionIssue(str(path), "group_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
|
|
||||||
if not os.access(path, os.W_OK):
|
|
||||||
issues.append(GitPermissionIssue(str(path), "not_writable", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
|
|
||||||
if len(issues) >= 200:
|
|
||||||
break
|
|
||||||
|
|
||||||
return GitPermissionReport(
|
|
||||||
workspace=str(self.workspace),
|
|
||||||
expected_user=self.expected_user,
|
|
||||||
expected_group=self.expected_group,
|
|
||||||
status="healthy" if not issues else "drift_detected",
|
|
||||||
issues=issues,
|
|
||||||
)
|
|
||||||
|
|
||||||
def repair(self, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
report = self.scan()
|
|
||||||
if report.status == "healthy":
|
|
||||||
return {"status": "healthy", "report": report.to_dict(), "repaired": False}
|
|
||||||
if dry_run:
|
|
||||||
return {"status": "dry_run", "report": report.to_dict(), "repaired": False}
|
|
||||||
commands = [
|
|
||||||
["chown", "-R", f"{self.expected_user}:{self.expected_group}", str(self.objects_dir)],
|
|
||||||
["chmod", "-R", "u+rwX,g+rwX", str(self.objects_dir)],
|
|
||||||
]
|
|
||||||
results = []
|
|
||||||
for command in commands:
|
|
||||||
proc = subprocess.run(command, text=True, capture_output=True, check=False)
|
|
||||||
results.append({"cmd": command, "returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr})
|
|
||||||
if proc.returncode != 0:
|
|
||||||
return {"status": "repair_failed", "report": report.to_dict(), "results": results, "repaired": False}
|
|
||||||
post = self.scan()
|
|
||||||
return {"status": "repaired" if post.status == "healthy" else "repair_incomplete", "report": post.to_dict(), "results": results, "repaired": post.status == "healthy"}
|
|
||||||
|
|
||||||
def write_probe(self) -> dict[str, Any]:
|
|
||||||
probe = self.objects_dir / f".aios-write-probe-{datetime.now(timezone.utc).timestamp()}"
|
|
||||||
try:
|
|
||||||
probe.write_text("probe\n", encoding="utf-8")
|
|
||||||
probe.unlink(missing_ok=True)
|
|
||||||
return {"status": "ok", "path": str(probe)}
|
|
||||||
except Exception as exc: # pragma: no cover - depends on runtime perms
|
|
||||||
return {"status": "failed", "path": str(probe), "error": str(exc)}
|
|
||||||
|
|
||||||
def _walk_limited(self, root: Path):
|
|
||||||
yield root
|
|
||||||
base_depth = len(root.parts)
|
|
||||||
for path in root.rglob("*"):
|
|
||||||
if len(path.parts) - base_depth <= self.max_scan_depth:
|
|
||||||
yield path
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["GitPermissionIssue", "GitPermissionReport", "GitPermissionWatchdog"]
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""CLI entrypoint for Git Permission Watchdog."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .git_permission_watchdog import GitPermissionWatchdog
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
workspace = os.getenv("AIOS_GITOPS_WORKSPACE", "/data/aios-gitops/workspace")
|
|
||||||
expected_user = os.getenv("AIOS_GIT_EXPECTED_USER", "aiman")
|
|
||||||
expected_group = os.getenv("AIOS_GIT_EXPECTED_GROUP", "gitops")
|
|
||||||
audit_dir = Path(os.getenv("AIOS_GIT_PERMISSION_AUDIT_DIR", "runtime/git-permission-watchdog"))
|
|
||||||
audit_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
watchdog = GitPermissionWatchdog(workspace, expected_user, expected_group)
|
|
||||||
result = watchdog.repair(dry_run=os.getenv("AIOS_GIT_PERMISSION_DRY_RUN") == "1")
|
|
||||||
probe = watchdog.write_probe()
|
|
||||||
payload = {"repair": result, "write_probe": probe}
|
|
||||||
status = result.get("status")
|
|
||||||
out = audit_dir / "latest.json"
|
|
||||||
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
print(json.dumps(payload, ensure_ascii=False))
|
|
||||||
return 0 if status in {"healthy", "repaired"} and probe.get("status") == "ok" else 2
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"""Unified AIOS operations command center view model."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class OperationsCommandCenter:
|
|
||||||
def build(
|
|
||||||
self,
|
|
||||||
tasks: list[dict[str, Any]] | None = None,
|
|
||||||
health: dict[str, Any] | None = None,
|
|
||||||
blockers: list[dict[str, Any]] | None = None,
|
|
||||||
searches: list[dict[str, Any]] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
tasks = tasks or []
|
|
||||||
blockers = blockers or []
|
|
||||||
searches = searches or []
|
|
||||||
return {
|
|
||||||
"module": "operations-command-center",
|
|
||||||
"route": "/admin/ai-evolution/operations",
|
|
||||||
"title": "AIOS 自治运营驾驶舱",
|
|
||||||
"generated_at": utc_now(),
|
|
||||||
"health": health or {},
|
|
||||||
"tasks": {
|
|
||||||
"active": len([t for t in tasks if t.get("status") in {"running", "pending", "waiting_approval"}]),
|
|
||||||
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
|
|
||||||
"completed": len([t for t in tasks if t.get("status") == "completed"]),
|
|
||||||
},
|
|
||||||
"blockers": {
|
|
||||||
"total": len(blockers),
|
|
||||||
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
|
|
||||||
"recurring": len([b for b in blockers if b.get("mechanism_gap")]),
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"recent": len(searches),
|
|
||||||
},
|
|
||||||
"loops": {
|
|
||||||
"task_loop": True,
|
|
||||||
"health_loop": True,
|
|
||||||
"optimization_loop": True,
|
|
||||||
"notification_loop": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["OperationsCommandCenter"]
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Portal AIOS dashboard aggregation API."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fastapi import APIRouter
|
||||||
|
except Exception:
|
||||||
|
APIRouter = None
|
||||||
|
|
||||||
|
from .portal_aios_module_registry import PortalAIOSModuleRegistry
|
||||||
|
from .portal_aios_navigation import PortalAIOSNavigationBuilder
|
||||||
|
|
||||||
|
|
||||||
|
registry = PortalAIOSModuleRegistry()
|
||||||
|
navigation = PortalAIOSNavigationBuilder(registry)
|
||||||
|
|
||||||
|
|
||||||
|
def get_portal_aios_dashboard(role: str = "user"):
|
||||||
|
return {
|
||||||
|
"entry": "/portal/me/",
|
||||||
|
"role": role,
|
||||||
|
"modules": registry.route_manifest(role),
|
||||||
|
"navigation": navigation.build(role),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_router():
|
||||||
|
if APIRouter is None:
|
||||||
|
return None
|
||||||
|
router = APIRouter(prefix="/portal/me/aios", tags=["portal-aios-dashboard"])
|
||||||
|
|
||||||
|
@router.get("/dashboard", operation_id="getPortalAIOSDashboard")
|
||||||
|
def dashboard(role: str = "user"):
|
||||||
|
return get_portal_aios_dashboard(role)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
router = build_router()
|
||||||
|
|
||||||
|
__all__ = ["router", "get_portal_aios_dashboard"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Portal AIOS module manifest API."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fastapi import APIRouter
|
||||||
|
except Exception:
|
||||||
|
APIRouter = None
|
||||||
|
|
||||||
|
from .portal_aios_module_registry import PortalAIOSModuleRegistry
|
||||||
|
|
||||||
|
|
||||||
|
registry = PortalAIOSModuleRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
def get_portal_manifest(role: str = "owner"):
|
||||||
|
return registry.route_manifest(role)
|
||||||
|
|
||||||
|
|
||||||
|
def build_router():
|
||||||
|
if APIRouter is None:
|
||||||
|
return None
|
||||||
|
router = APIRouter(prefix="/portal/me/aios", tags=["portal-aios"])
|
||||||
|
|
||||||
|
@router.get("/manifest", operation_id="getPortalAIOSManifest")
|
||||||
|
def manifest(role: str = "owner"):
|
||||||
|
return get_portal_manifest(role)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
router = build_router()
|
||||||
|
|
||||||
|
__all__ = ["router", "get_portal_manifest"]
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"""AIOS Portal module registry.
|
||||||
|
|
||||||
|
Defines the product-level modules that should be visible under
|
||||||
|
https://user.cnnetmkt.top/portal/me/ after authentication. This bridges backend
|
||||||
|
AIOS capabilities into portal routes, menu entries and RBAC visibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROLE_ORDER = {
|
||||||
|
"user": 1,
|
||||||
|
"group_admin": 5,
|
||||||
|
"owner": 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortalAIOSModule:
|
||||||
|
key: str
|
||||||
|
title: str
|
||||||
|
route: str
|
||||||
|
api_routes: list[str]
|
||||||
|
min_role: str = "user"
|
||||||
|
description: str = ""
|
||||||
|
children: list["PortalAIOSModule"] = field(default_factory=list)
|
||||||
|
|
||||||
|
def visible_to(self, role: str) -> bool:
|
||||||
|
return ROLE_ORDER.get(role, 0) >= ROLE_ORDER.get(self.min_role, 0)
|
||||||
|
|
||||||
|
def to_dict(self, role: str = "owner") -> dict[str, Any] | None:
|
||||||
|
if not self.visible_to(role):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"key": self.key,
|
||||||
|
"title": self.title,
|
||||||
|
"route": self.route,
|
||||||
|
"api_routes": self.api_routes,
|
||||||
|
"min_role": self.min_role,
|
||||||
|
"description": self.description,
|
||||||
|
"children": [child for child in [c.to_dict(role) for c in self.children] if child],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PortalAIOSModuleRegistry:
|
||||||
|
def modules(self) -> list[PortalAIOSModule]:
|
||||||
|
return [
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="tasks",
|
||||||
|
title="任务中心",
|
||||||
|
route="/admin/ai-evolution/tasks",
|
||||||
|
api_routes=["/admin/ai-evolution/tasks/overview"],
|
||||||
|
min_role="user",
|
||||||
|
description="查看任务来源、执行者、进度、阻塞、审批和通知状态。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="operations",
|
||||||
|
title="自治运营驾驶舱",
|
||||||
|
route="/admin/ai-evolution/operations",
|
||||||
|
api_routes=["/admin/ai-evolution/operations/overview"],
|
||||||
|
min_role="group_admin",
|
||||||
|
description="统一查看任务、健康、阻塞、自愈和搜索运行状态。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="system_health",
|
||||||
|
title="系统健康",
|
||||||
|
route="/admin/ai-evolution/system-health",
|
||||||
|
api_routes=["/admin/ai-evolution/system-health/overview"],
|
||||||
|
min_role="group_admin",
|
||||||
|
description="查看 P0 问题、重复阻塞、自愈记录和系统健康。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="search",
|
||||||
|
title="搜索治理",
|
||||||
|
route="/admin/ai-evolution/search",
|
||||||
|
api_routes=[
|
||||||
|
"/admin/ai-evolution/search/status",
|
||||||
|
"/admin/ai-evolution/search/overview",
|
||||||
|
"/admin/ai-evolution/search/query",
|
||||||
|
],
|
||||||
|
min_role="group_admin",
|
||||||
|
description="查看联网搜索 Provider、Profile、Fallback、证据和审计。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="agents",
|
||||||
|
title="Agent 观察",
|
||||||
|
route="/admin/ai-evolution/agents",
|
||||||
|
api_routes=["/admin/ai-evolution/agents/overview"],
|
||||||
|
min_role="group_admin",
|
||||||
|
description="查看 OpenClaw、Hermes、Codex 等 Agent 执行链路。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="architecture_changes",
|
||||||
|
title="架构变更",
|
||||||
|
route="/admin/ai-evolution/architecture-changes",
|
||||||
|
api_routes=["/admin/ai-evolution/architecture-changes/overview"],
|
||||||
|
min_role="owner",
|
||||||
|
description="查看架构变化分类、Owner Gate 和 C/R/V 建议。",
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="architect",
|
||||||
|
title="架构治理",
|
||||||
|
route="/admin/ai-evolution/architect",
|
||||||
|
api_routes=["/admin/ai-evolution/architect/overview"],
|
||||||
|
min_role="owner",
|
||||||
|
description="查看需求变化、架构审查、文档刷新和版本建议。",
|
||||||
|
children=[
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="architect_research",
|
||||||
|
title="技术情报",
|
||||||
|
route="/admin/ai-evolution/architect/research",
|
||||||
|
api_routes=["/admin/ai-evolution/architect/research/timeline"],
|
||||||
|
min_role="owner",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
PortalAIOSModule(
|
||||||
|
key="governance",
|
||||||
|
title="变更治理",
|
||||||
|
route="/admin/ai-evolution/governance",
|
||||||
|
api_routes=[
|
||||||
|
"/admin/ai-evolution/governance/overview",
|
||||||
|
"/admin/ai-evolution/governance/timeline",
|
||||||
|
],
|
||||||
|
min_role="owner",
|
||||||
|
description="查看测试 Gate、用户验收、架构审查和发布决策链。",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def visible_modules(self, role: str) -> list[dict[str, Any]]:
|
||||||
|
return [module for module in [m.to_dict(role) for m in self.modules()] if module]
|
||||||
|
|
||||||
|
def route_manifest(self, role: str = "owner") -> dict[str, Any]:
|
||||||
|
modules = self.visible_modules(role)
|
||||||
|
return {
|
||||||
|
"portal_entry": "/portal/me/",
|
||||||
|
"role": role,
|
||||||
|
"modules": modules,
|
||||||
|
"routes": [module["route"] for module in modules],
|
||||||
|
"api_routes": [api for module in modules for api in module.get("api_routes", [])],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["PortalAIOSModule", "PortalAIOSModuleRegistry"]
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Portal navigation builder for AIOS modules.
|
||||||
|
|
||||||
|
Builds role-aware navigation entries consumed by the portal frontend under
|
||||||
|
/portal/me/. The frontend can render these entries as cards, sidebar items or
|
||||||
|
quick actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .portal_aios_module_registry import PortalAIOSModuleRegistry
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortalNavigationItem:
|
||||||
|
key: str
|
||||||
|
label: str
|
||||||
|
href: str
|
||||||
|
icon: str
|
||||||
|
description: str
|
||||||
|
badge: str | None = None
|
||||||
|
order: int = 100
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class PortalAIOSNavigationBuilder:
|
||||||
|
ICONS = {
|
||||||
|
"tasks": "list-checks",
|
||||||
|
"operations": "activity",
|
||||||
|
"system_health": "heart-pulse",
|
||||||
|
"search": "search",
|
||||||
|
"agents": "bot",
|
||||||
|
"architecture_changes": "git-branch",
|
||||||
|
"architect": "network",
|
||||||
|
"governance": "shield-check",
|
||||||
|
}
|
||||||
|
|
||||||
|
ORDER = {
|
||||||
|
"tasks": 10,
|
||||||
|
"operations": 20,
|
||||||
|
"system_health": 30,
|
||||||
|
"search": 40,
|
||||||
|
"agents": 50,
|
||||||
|
"architecture_changes": 60,
|
||||||
|
"architect": 70,
|
||||||
|
"governance": 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, registry: PortalAIOSModuleRegistry | None = None):
|
||||||
|
self.registry = registry or PortalAIOSModuleRegistry()
|
||||||
|
|
||||||
|
def build(self, role: str = "user") -> dict[str, Any]:
|
||||||
|
manifest = self.registry.route_manifest(role)
|
||||||
|
items = [self._item(module) for module in manifest["modules"]]
|
||||||
|
items.sort(key=lambda item: item.order)
|
||||||
|
return {
|
||||||
|
"portal_entry": "/portal/me/",
|
||||||
|
"role": role,
|
||||||
|
"section": "AIOS",
|
||||||
|
"items": [item.to_dict() for item in items],
|
||||||
|
"quick_actions": self.quick_actions(role),
|
||||||
|
}
|
||||||
|
|
||||||
|
def quick_actions(self, role: str) -> list[dict[str, Any]]:
|
||||||
|
actions = [
|
||||||
|
{"key": "open_tasks", "label": "查看任务", "href": "/admin/ai-evolution/tasks"},
|
||||||
|
]
|
||||||
|
if role in {"group_admin", "owner"}:
|
||||||
|
actions.append({"key": "open_operations", "label": "自治运营", "href": "/admin/ai-evolution/operations"})
|
||||||
|
actions.append({"key": "open_health", "label": "系统健康", "href": "/admin/ai-evolution/system-health"})
|
||||||
|
if role == "owner":
|
||||||
|
actions.append({"key": "open_owner_gate", "label": "审批治理", "href": "/admin/ai-evolution/governance"})
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def _item(self, module: dict[str, Any]) -> PortalNavigationItem:
|
||||||
|
key = module["key"]
|
||||||
|
return PortalNavigationItem(
|
||||||
|
key=key,
|
||||||
|
label=module["title"],
|
||||||
|
href=module["route"],
|
||||||
|
icon=self.ICONS.get(key, "box"),
|
||||||
|
description=module.get("description", ""),
|
||||||
|
badge=self._badge(key),
|
||||||
|
order=self.ORDER.get(key, 100),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _badge(self, key: str) -> str | None:
|
||||||
|
if key in {"tasks", "system_health"}:
|
||||||
|
return "live"
|
||||||
|
if key in {"architecture_changes", "governance"}:
|
||||||
|
return "owner"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["PortalNavigationItem", "PortalAIOSNavigationBuilder"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Portal integration health API."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fastapi import APIRouter
|
||||||
|
except Exception:
|
||||||
|
APIRouter = None
|
||||||
|
|
||||||
|
from .portal_integration_health import PortalIntegrationHealth
|
||||||
|
|
||||||
|
|
||||||
|
health = PortalIntegrationHealth()
|
||||||
|
|
||||||
|
|
||||||
|
def get_portal_integration_health():
|
||||||
|
return health.check()
|
||||||
|
|
||||||
|
|
||||||
|
def build_router():
|
||||||
|
if APIRouter is None:
|
||||||
|
return None
|
||||||
|
router = APIRouter(prefix="/portal/me/aios", tags=["portal-health"])
|
||||||
|
|
||||||
|
@router.get("/health", operation_id="getPortalIntegrationHealth")
|
||||||
|
def health_check():
|
||||||
|
return get_portal_integration_health()
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
router = build_router()
|
||||||
|
|
||||||
|
__all__ = ["router", "get_portal_integration_health"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Portal integration health checks."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
class PortalIntegrationHealth:
|
||||||
|
def check(self, manifest: dict[str, Any] | None = None, frontend: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
manifest = manifest or {}
|
||||||
|
frontend = frontend or {}
|
||||||
|
routes = manifest.get("routes", [])
|
||||||
|
return {
|
||||||
|
"module": "portal-integration-health",
|
||||||
|
"status": "healthy" if routes else "attention_required",
|
||||||
|
"checked_at": utc_now(),
|
||||||
|
"checks": {
|
||||||
|
"manifest_available": bool(manifest),
|
||||||
|
"routes_available": len(routes) > 0,
|
||||||
|
"frontend_registered": frontend.get("registered", False),
|
||||||
|
"rbac_enabled": True,
|
||||||
|
},
|
||||||
|
"missing": self._missing(manifest, frontend),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _missing(self, manifest: dict[str, Any], frontend: dict[str, Any]) -> list[str]:
|
||||||
|
missing = []
|
||||||
|
if not manifest:
|
||||||
|
missing.append("portal_manifest")
|
||||||
|
if not frontend.get("registered", False):
|
||||||
|
missing.append("frontend_navigation_registration")
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["PortalIntegrationHealth"]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Portal integration adapter for System Health Center.
|
||||||
|
|
||||||
|
Adds portal visibility state to the AIOS health model so missing user-facing
|
||||||
|
routes are treated as operational issues, not merely frontend tasks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .portal_aios_module_registry import PortalAIOSModuleRegistry
|
||||||
|
from .portal_integration_health import PortalIntegrationHealth
|
||||||
|
|
||||||
|
|
||||||
|
class PortalSystemHealthAdapter:
|
||||||
|
def __init__(self):
|
||||||
|
self.registry = PortalAIOSModuleRegistry()
|
||||||
|
self.health = PortalIntegrationHealth()
|
||||||
|
|
||||||
|
def build(self, role: str = "owner", frontend: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
manifest = self.registry.route_manifest(role)
|
||||||
|
health = self.health.check(manifest, frontend or {})
|
||||||
|
return {
|
||||||
|
"component": "portal-aios-integration",
|
||||||
|
"role": role,
|
||||||
|
"status": health["status"],
|
||||||
|
"manifest_routes": manifest.get("routes", []),
|
||||||
|
"api_routes": manifest.get("api_routes", []),
|
||||||
|
"missing": health.get("missing", []),
|
||||||
|
"checks": health.get("checks", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["PortalSystemHealthAdapter"]
|
||||||
@@ -58,6 +58,8 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
|||||||
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
||||||
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
||||||
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
@@ -90,6 +92,14 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
|||||||
</div>
|
</div>
|
||||||
<div id="panel-agents" class="panel" style="display:none;">
|
<div id="panel-agents" class="panel" style="display:none;">
|
||||||
<div class="main-header"><h2>Recent Agent Executions</h2></div>
|
<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 id="agents-list">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,3 +107,159 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
|||||||
<script>
|
<script>
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const r = await fetch('/admin/ai-evolution/api/data');
|
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>
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
"""System blocker memory and recurring issue detection.
|
|
||||||
|
|
||||||
Records blocking issues, long-no-response incidents and repeated operational
|
|
||||||
failures. Repeated blockers are escalated from incident handling into mechanism
|
|
||||||
optimization work.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 SystemBlockerRecord:
|
|
||||||
blocker_id: str
|
|
||||||
category: str
|
|
||||||
title: str
|
|
||||||
source: str
|
|
||||||
severity: str
|
|
||||||
status: str = "open"
|
|
||||||
first_seen_at: str = field(default_factory=utc_now)
|
|
||||||
last_seen_at: str = field(default_factory=utc_now)
|
|
||||||
occurrence_count: int = 1
|
|
||||||
evidence: list[str] = field(default_factory=list)
|
|
||||||
mechanism_gap: bool = False
|
|
||||||
recommended_owner: str = "autonomous-controller"
|
|
||||||
|
|
||||||
def fingerprint(self) -> str:
|
|
||||||
return f"{self.category}:{self.title}".lower().replace(" ", "_")
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self) | {"fingerprint": self.fingerprint()}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BlockerEscalationDecision:
|
|
||||||
status: str
|
|
||||||
priority: str
|
|
||||||
reason: str
|
|
||||||
action: str
|
|
||||||
assigned_to: str
|
|
||||||
create_task: bool = True
|
|
||||||
architect_review_required: bool = False
|
|
||||||
owner_gate_required: bool = False
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class SystemBlockerMemory:
|
|
||||||
"""In-memory decision logic; production storage can wrap this with persistence."""
|
|
||||||
|
|
||||||
def record(self, existing: list[dict[str, Any]], incoming: SystemBlockerRecord) -> dict[str, Any]:
|
|
||||||
records = [dict(item) for item in existing]
|
|
||||||
fp = incoming.fingerprint()
|
|
||||||
for item in records:
|
|
||||||
if item.get("fingerprint") == fp or f"{item.get('category')}:{item.get('title')}".lower().replace(" ", "_") == fp:
|
|
||||||
item["occurrence_count"] = int(item.get("occurrence_count", 1)) + 1
|
|
||||||
item["last_seen_at"] = utc_now()
|
|
||||||
item["status"] = "recurrent"
|
|
||||||
evidence = list(item.get("evidence", [])) + incoming.evidence
|
|
||||||
item["evidence"] = list(dict.fromkeys(evidence))
|
|
||||||
item["mechanism_gap"] = self.is_mechanism_gap(item)
|
|
||||||
item["fingerprint"] = fp
|
|
||||||
return item
|
|
||||||
item = incoming.to_dict()
|
|
||||||
item["mechanism_gap"] = self.is_mechanism_gap(item)
|
|
||||||
records.append(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def decide(self, blocker: dict[str, Any]) -> BlockerEscalationDecision:
|
|
||||||
count = int(blocker.get("occurrence_count", 1))
|
|
||||||
severity = blocker.get("severity", "medium")
|
|
||||||
category = blocker.get("category", "unknown")
|
|
||||||
if severity in {"critical", "p0"} or category in {"git_permission", "runtime_deadlock", "autonomous_controller_stall"}:
|
|
||||||
return BlockerEscalationDecision(
|
|
||||||
status="escalate_to_mechanism_fix",
|
|
||||||
priority="P0",
|
|
||||||
reason="blocking infrastructure issue affects autonomous operation",
|
|
||||||
action="create_self_healing_mechanism",
|
|
||||||
assigned_to="openclaw",
|
|
||||||
architect_review_required=True,
|
|
||||||
)
|
|
||||||
if count >= 3:
|
|
||||||
return BlockerEscalationDecision(
|
|
||||||
status="escalate_to_mechanism_fix",
|
|
||||||
priority="P1",
|
|
||||||
reason="recurring issue exceeded threshold",
|
|
||||||
action="create_root_cause_and_prevention_patch",
|
|
||||||
assigned_to="autonomous-controller",
|
|
||||||
architect_review_required=True,
|
|
||||||
)
|
|
||||||
if category == "long_no_response":
|
|
||||||
return BlockerEscalationDecision(
|
|
||||||
status="create_monitoring_task",
|
|
||||||
priority="P1",
|
|
||||||
reason="long no response requires watchdog and notification",
|
|
||||||
action="create_stall_watchdog_task",
|
|
||||||
assigned_to="openclaw",
|
|
||||||
)
|
|
||||||
return BlockerEscalationDecision(
|
|
||||||
status="track_incident",
|
|
||||||
priority="P2",
|
|
||||||
reason="single incident tracked for recurrence",
|
|
||||||
action="record_and_monitor",
|
|
||||||
assigned_to="autonomous-controller",
|
|
||||||
create_task=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_mechanism_gap(self, blocker: dict[str, Any]) -> bool:
|
|
||||||
return int(blocker.get("occurrence_count", 1)) >= 2 or blocker.get("severity") in {"critical", "p0"}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["SystemBlockerRecord", "BlockerEscalationDecision", "SystemBlockerMemory"]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""System Health Center API."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi import APIRouter
|
|
||||||
except Exception:
|
|
||||||
APIRouter = None
|
|
||||||
|
|
||||||
from .system_health_center import SystemHealthCenter
|
|
||||||
|
|
||||||
|
|
||||||
center = SystemHealthCenter()
|
|
||||||
|
|
||||||
|
|
||||||
def get_system_health_overview():
|
|
||||||
return center.build()
|
|
||||||
|
|
||||||
|
|
||||||
def build_router():
|
|
||||||
if APIRouter is None:
|
|
||||||
return None
|
|
||||||
router = APIRouter(prefix="/admin/ai-evolution/system-health", tags=["system-health"])
|
|
||||||
|
|
||||||
@router.get("/overview", operation_id="getSystemHealthOverview")
|
|
||||||
def overview():
|
|
||||||
return get_system_health_overview()
|
|
||||||
|
|
||||||
return router
|
|
||||||
|
|
||||||
|
|
||||||
router = build_router()
|
|
||||||
|
|
||||||
__all__ = ["router", "get_system_health_overview"]
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""Unified AIOS system health center model."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class SystemHealthCenter:
|
|
||||||
def build(
|
|
||||||
self,
|
|
||||||
git_report: dict[str, Any] | None = None,
|
|
||||||
blockers: list[dict[str, Any]] | None = None,
|
|
||||||
tasks: list[dict[str, Any]] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
blockers = blockers or []
|
|
||||||
tasks = tasks or []
|
|
||||||
return {
|
|
||||||
"module": "system-health-center",
|
|
||||||
"route": "/admin/ai-evolution/system-health",
|
|
||||||
"title": "AIOS 系统健康与自优化中心",
|
|
||||||
"generated_at": utc_now(),
|
|
||||||
"status": self._status(git_report, blockers),
|
|
||||||
"git": git_report or {},
|
|
||||||
"blockers": {
|
|
||||||
"total": len(blockers),
|
|
||||||
"recurrent": len([b for b in blockers if b.get("mechanism_gap")]),
|
|
||||||
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
|
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"optimization_tasks": len([t for t in tasks if t.get("source") == "system_blocker"]),
|
|
||||||
"pending": len([t for t in tasks if t.get("status") in {"pending", "running", "waiting_approval"}]),
|
|
||||||
},
|
|
||||||
"optimization_loop": {
|
|
||||||
"detect": True,
|
|
||||||
"remember": True,
|
|
||||||
"optimize": True,
|
|
||||||
"verify": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _status(self, git_report, blockers):
|
|
||||||
if git_report and git_report.get("status") not in {"healthy", "ok"}:
|
|
||||||
return "attention_required"
|
|
||||||
if any(b.get("severity") in {"p0", "critical"} for b in blockers):
|
|
||||||
return "attention_required"
|
|
||||||
return "healthy"
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["SystemHealthCenter"]
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"patch_id": "patch-1783532314",
|
||||||
|
"created": "2026-07-09T01:38: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-1783532914",
|
||||||
|
"created": "2026-07-09T01:48: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-1783533514",
|
||||||
|
"created": "2026-07-09T01: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-1783534114",
|
||||||
|
"created": "2026-07-09T02: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,12 @@
|
|||||||
|
{
|
||||||
|
"ts": "2026-07-09T02:08:34+0800",
|
||||||
|
"ok": false,
|
||||||
|
"checks": {
|
||||||
|
"gitea": false,
|
||||||
|
"api": true,
|
||||||
|
"branch_sync": false,
|
||||||
|
"stuck": true
|
||||||
|
},
|
||||||
|
"plans": 1,
|
||||||
|
"applied": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"ts": "2026-07-09T00:00:00+09:00",
|
||||||
|
"ok": true,
|
||||||
|
"checks": {
|
||||||
|
"git_object_permission": true,
|
||||||
|
"api": true,
|
||||||
|
"branch_sync": true,
|
||||||
|
"stale_runtime_state_cleaned": true
|
||||||
|
},
|
||||||
|
"note": "Updated during PR #2 cleanup before merge"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"ts": "2026-07-09T00:59:30+0800",
|
||||||
|
"action": "command_watcher_state_clean",
|
||||||
|
"description": "清除了 command watcher 目录中的 stale 重复副本",
|
||||||
|
"result": "clean",
|
||||||
|
"stale_removed": [
|
||||||
|
{
|
||||||
|
"name": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
||||||
|
"removed_from": ["active/", "running/"],
|
||||||
|
"preserved_in": "completed/",
|
||||||
|
"archive_path": "agents/commands/archive/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CMD-20260709-enable-autonomous-controller.md",
|
||||||
|
"removed_from": ["running/"],
|
||||||
|
"preserved_in": "active/",
|
||||||
|
"archive_path": "agents/commands/archive/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"note": "已完成命令保留在 completed/,副本移动到 archive/;未完成命令仅保留 active/"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"ts": "2026-07-09T01:00:00+0800",
|
||||||
|
"action": "daily_wechat_cron_verification",
|
||||||
|
"description": "验证每日 08:00 WeChat 汇报 cron 配置并手动触发 dry-run",
|
||||||
|
"cron_name": "AIOS每日自治报告",
|
||||||
|
"cron_id": "987f8619-6ccc-4370-8720-87149479a222",
|
||||||
|
"schedule": "0 8 * * * Asia/Shanghai",
|
||||||
|
"delivery": {
|
||||||
|
"mode": "announce",
|
||||||
|
"channel": "openclaw-weixin",
|
||||||
|
"to": "o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat",
|
||||||
|
"accountId": "ed3da13663c0-im-bot"
|
||||||
|
},
|
||||||
|
"dry_run_triggered": true,
|
||||||
|
"dry_run_result": "enqueued",
|
||||||
|
"status": "verified"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783527185",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "degraded",
|
||||||
|
"ts": "2026-07-09T00:13:05+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": false,
|
||||||
|
"stuck": [
|
||||||
|
{
|
||||||
|
"n": "CMD-20260709-enable-autonomous-controller.md",
|
||||||
|
"age": 184.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
||||||
|
"age": 185.8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783529900",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "degraded",
|
||||||
|
"ts": "2026-07-09T00:58:20+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": false,
|
||||||
|
"stuck": [
|
||||||
|
{
|
||||||
|
"n": "CMD-20260709-enable-autonomous-controller.md",
|
||||||
|
"age": 229.8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
||||||
|
"age": 231.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783529914",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "degraded",
|
||||||
|
"ts": "2026-07-09T00:58:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": false,
|
||||||
|
"stuck": [
|
||||||
|
{
|
||||||
|
"n": "CMD-20260709-enable-autonomous-controller.md",
|
||||||
|
"age": 230.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
||||||
|
"age": 231.3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783530514",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:08:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783531114",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:18:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783531714",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:28:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783532314",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:38:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783532914",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:48:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783533514",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T01:58:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"run_id": "c-1783534114",
|
||||||
|
"check": "stuck",
|
||||||
|
"status": "ok",
|
||||||
|
"ts": "2026-07-09T02:08:34+0800",
|
||||||
|
"details": {
|
||||||
|
"ok": true,
|
||||||
|
"stuck": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,8 @@
|
|||||||
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
|
{
|
||||||
|
"current_baseline": "V1R4C3",
|
||||||
|
"previous_baseline": "V1R3C1",
|
||||||
|
"promoted_at": "2026-07-09T17:44:57+08:00",
|
||||||
|
"git": "f5435e308794c737f3e39de3f6b79807486943c3",
|
||||||
|
"promoted_by": "openclaw",
|
||||||
|
"promotion_reason": "V1R4C3 autonomous operations, search gateway, task center, owner gate, and blocker self-optimization baseline promotion"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
architect_agent_governance:
|
||||||
|
enabled: true
|
||||||
|
responsibilities:
|
||||||
|
requirement_change_tracking: true
|
||||||
|
requirement_document_refresh: true
|
||||||
|
architecture_document_refresh: true
|
||||||
|
deployment_document_validation: true
|
||||||
|
test_case_document_generation: true
|
||||||
|
architecture_optimization_review: true
|
||||||
|
weekly_external_research: true
|
||||||
|
github_top_implementation_review: true
|
||||||
|
sources:
|
||||||
|
requirement_sources:
|
||||||
|
- custom_gpt
|
||||||
|
- codex
|
||||||
|
- openclaw
|
||||||
|
- hermes
|
||||||
|
- user_feedback
|
||||||
|
version_decision:
|
||||||
|
criteria:
|
||||||
|
C: bug_fix_or_small_compatible_change
|
||||||
|
R: architecture_or_capability_release
|
||||||
|
V: major_architecture_or_breaking_change
|
||||||
|
research_cycle:
|
||||||
|
frequency: weekly
|
||||||
|
targets:
|
||||||
|
- agent_framework_versions
|
||||||
|
- model_versions
|
||||||
|
- github_top_implementations
|
||||||
|
- security_changes
|
||||||
|
governance:
|
||||||
|
owner_gate_required: true
|
||||||
|
audit_required: true
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
architect_research:
|
||||||
|
enabled: true
|
||||||
|
schedule:
|
||||||
|
frequency: weekly
|
||||||
|
research_sources:
|
||||||
|
- agent_framework_releases
|
||||||
|
- model_ecosystem_updates
|
||||||
|
- github_top_implementations
|
||||||
|
- security_advisories
|
||||||
|
evaluation:
|
||||||
|
required:
|
||||||
|
- relevance
|
||||||
|
- architecture_impact
|
||||||
|
- security_impact
|
||||||
|
- migration_cost
|
||||||
|
output:
|
||||||
|
- optimization_recommendation
|
||||||
|
- version_recommendation
|
||||||
|
- architecture_review_event
|
||||||
|
governance:
|
||||||
|
owner_gate_required: true
|
||||||
|
audit_required: true
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
architecture_owner_gate:
|
||||||
|
enabled: true
|
||||||
|
principle: owner_approves_large_architecture_changes_only
|
||||||
|
classification:
|
||||||
|
small:
|
||||||
|
owner_required: false
|
||||||
|
architect_auto_implementation: true
|
||||||
|
requirements:
|
||||||
|
- architect_review
|
||||||
|
- test_gate
|
||||||
|
- audit
|
||||||
|
medium:
|
||||||
|
owner_required: false
|
||||||
|
architect_review_required: true
|
||||||
|
implementation_after_review: true
|
||||||
|
requirements:
|
||||||
|
- architect_review
|
||||||
|
- test_gate
|
||||||
|
- audit
|
||||||
|
large:
|
||||||
|
owner_required: true
|
||||||
|
architect_auto_implementation: false
|
||||||
|
requirements:
|
||||||
|
- architect_review
|
||||||
|
- owner_architecture_approval
|
||||||
|
- test_gate
|
||||||
|
- promotion_gate
|
||||||
|
mandatory_owner_cases:
|
||||||
|
- breaking_change
|
||||||
|
- V_version_change
|
||||||
|
- production_architecture_update
|
||||||
|
- security_boundary_change
|
||||||
|
- cross_tenant_change
|
||||||
|
- permission_model_change
|
||||||
|
audit:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
autonomous_goal_completion:
|
||||||
|
enabled: true
|
||||||
|
purpose: complete_goals_without_manual_progress_queries
|
||||||
|
|
||||||
|
autonomous_actions:
|
||||||
|
- create_task_plan
|
||||||
|
- assign_agent
|
||||||
|
- monitor_progress
|
||||||
|
- retry_safe_failure
|
||||||
|
- trigger_self_healing
|
||||||
|
- update_task_center
|
||||||
|
- send_completion_notification
|
||||||
|
|
||||||
|
require_owner_gate:
|
||||||
|
- secret_required
|
||||||
|
- paid_provider_required
|
||||||
|
- production_baseline_promotion
|
||||||
|
- large_architecture_change
|
||||||
|
- cross_tenant_data_change
|
||||||
|
- private_data_source_enablement
|
||||||
|
|
||||||
|
integrations:
|
||||||
|
task_center: true
|
||||||
|
system_health: true
|
||||||
|
blocker_memory: true
|
||||||
|
architect_agent: true
|
||||||
|
notification_agent: true
|
||||||
|
|
||||||
|
reporting:
|
||||||
|
notify_on:
|
||||||
|
- blocked
|
||||||
|
- owner_gate_required
|
||||||
|
- completed
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
change_governance:
|
||||||
|
enabled: true
|
||||||
|
feature_change:
|
||||||
|
require_tests: true
|
||||||
|
require_user_acceptance_if_user_facing: true
|
||||||
|
architecture_change:
|
||||||
|
require_architecture_review_agent: true
|
||||||
|
require_system_optimization_review: true
|
||||||
|
acceptance_agents:
|
||||||
|
user_agent: user-acceptance-agent
|
||||||
|
architect_agent: architecture-review-agent
|
||||||
|
merge_gate:
|
||||||
|
required:
|
||||||
|
- tests_passed
|
||||||
|
- acceptance_passed
|
||||||
|
- architecture_review_passed
|
||||||
|
audit:
|
||||||
|
enabled: true
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
git_permission_watchdog:
|
|
||||||
enabled: true
|
|
||||||
priority: P0
|
|
||||||
workspace: /data/aios-gitops/workspace
|
|
||||||
|
|
||||||
expected_identity:
|
|
||||||
user: aiman
|
|
||||||
group: gitops
|
|
||||||
|
|
||||||
checks:
|
|
||||||
- object_owner
|
|
||||||
- object_group
|
|
||||||
- object_write_permission
|
|
||||||
- git_write_probe
|
|
||||||
|
|
||||||
repair:
|
|
||||||
enabled: true
|
|
||||||
allowed_paths:
|
|
||||||
- .git/objects
|
|
||||||
forbidden:
|
|
||||||
- chmod_777
|
|
||||||
- system_wide_permission_change
|
|
||||||
|
|
||||||
runtime:
|
|
||||||
interval_minutes: 5
|
|
||||||
create_task_on_drift: true
|
|
||||||
send_notification_on_failure: true
|
|
||||||
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
record:
|
|
||||||
- detection_time
|
|
||||||
- affected_paths
|
|
||||||
- repair_action
|
|
||||||
- repair_result
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
governance_event_bus:
|
||||||
|
enabled: true
|
||||||
|
event_types:
|
||||||
|
- requirement_changed
|
||||||
|
- architecture_review_started
|
||||||
|
- architecture_review_completed
|
||||||
|
- user_acceptance_completed
|
||||||
|
- test_gate_completed
|
||||||
|
- version_decision_created
|
||||||
|
producers:
|
||||||
|
- architect-agent
|
||||||
|
- user-acceptance-agent
|
||||||
|
- test-gate-agent
|
||||||
|
- change-governance-agent
|
||||||
|
consumers:
|
||||||
|
- web-ui
|
||||||
|
- audit-plane
|
||||||
|
- owner-gate
|
||||||
|
- reporting-agent
|
||||||
|
audit:
|
||||||
|
required: true
|
||||||
|
retention: versioned
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
models:
|
||||||
|
default:
|
||||||
|
- id: gpt-5.5
|
||||||
|
provider: openai
|
||||||
|
enabled: true
|
||||||
|
fallback: false
|
||||||
|
- id: qwen-3.6-35b
|
||||||
|
provider: qwen
|
||||||
|
enabled: true
|
||||||
|
fallback: true
|
||||||
|
- id: deepseek-v4-flash
|
||||||
|
provider: deepseek
|
||||||
|
enabled: true
|
||||||
|
fallback: true
|
||||||
|
|
||||||
|
runtime_behavior:
|
||||||
|
unavailable_model:
|
||||||
|
action: warn_and_fallback
|
||||||
|
hardcoded_model_list: false
|
||||||
|
health_check_required: true
|
||||||
|
audit_model_selection: true
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
portal_aios_frontend_contract:
|
||||||
|
enabled: true
|
||||||
|
entry: /portal/me/
|
||||||
|
|
||||||
|
backend_manifest:
|
||||||
|
dashboard: /portal/me/aios/dashboard
|
||||||
|
manifest: /portal/me/aios/manifest
|
||||||
|
|
||||||
|
render_modes:
|
||||||
|
- cards
|
||||||
|
- sidebar
|
||||||
|
- quick_actions
|
||||||
|
|
||||||
|
role_visibility:
|
||||||
|
user:
|
||||||
|
required_modules:
|
||||||
|
- tasks
|
||||||
|
forbidden_modules:
|
||||||
|
- architecture_changes
|
||||||
|
- architect
|
||||||
|
- governance
|
||||||
|
group_admin:
|
||||||
|
required_modules:
|
||||||
|
- tasks
|
||||||
|
- operations
|
||||||
|
- system_health
|
||||||
|
- search
|
||||||
|
- agents
|
||||||
|
forbidden_modules:
|
||||||
|
- architecture_changes
|
||||||
|
- architect
|
||||||
|
- governance
|
||||||
|
owner:
|
||||||
|
required_modules:
|
||||||
|
- tasks
|
||||||
|
- operations
|
||||||
|
- system_health
|
||||||
|
- search
|
||||||
|
- agents
|
||||||
|
- architecture_changes
|
||||||
|
- architect
|
||||||
|
- governance
|
||||||
|
|
||||||
|
api_proxy_required:
|
||||||
|
- /portal/me/aios/dashboard
|
||||||
|
- /portal/me/aios/manifest
|
||||||
|
- /admin/ai-evolution/tasks/overview
|
||||||
|
- /admin/ai-evolution/operations/overview
|
||||||
|
- /admin/ai-evolution/system-health/overview
|
||||||
|
- /admin/ai-evolution/search/overview
|
||||||
|
- /admin/ai-evolution/search/status
|
||||||
|
- /admin/ai-evolution/agents/overview
|
||||||
|
- /admin/ai-evolution/architecture-changes/overview
|
||||||
|
- /admin/ai-evolution/governance/overview
|
||||||
|
- /admin/ai-evolution/governance/timeline
|
||||||
|
|
||||||
|
acceptance:
|
||||||
|
login_required: true
|
||||||
|
casdoor_callback_required: true
|
||||||
|
no_blank_portal_after_login: true
|
||||||
|
show_empty_state_when_api_unavailable: true
|
||||||
|
hide_forbidden_modules: true
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
system_blocker_optimization:
|
|
||||||
enabled: true
|
|
||||||
purpose: convert_recurring_failures_into_mechanism_improvements
|
|
||||||
|
|
||||||
tracked_categories:
|
|
||||||
- git_permission
|
|
||||||
- runtime_stall
|
|
||||||
- command_stuck
|
|
||||||
- provider_failure
|
|
||||||
- deployment_failure
|
|
||||||
- approval_wait
|
|
||||||
- notification_failure
|
|
||||||
- agent_timeout
|
|
||||||
|
|
||||||
thresholds:
|
|
||||||
repeat_count_for_mechanism_review: 3
|
|
||||||
critical_issue_immediate_review: true
|
|
||||||
long_no_response_minutes: 15
|
|
||||||
|
|
||||||
actions:
|
|
||||||
first_occurrence:
|
|
||||||
- record_incident
|
|
||||||
- monitor
|
|
||||||
repeated_occurrence:
|
|
||||||
- create_optimization_task
|
|
||||||
- assign_autonomous_controller
|
|
||||||
- require_architect_review
|
|
||||||
critical_occurrence:
|
|
||||||
- create_p0_task
|
|
||||||
- notify_owner
|
|
||||||
- self_healing_attempt
|
|
||||||
|
|
||||||
integration:
|
|
||||||
task_center: true
|
|
||||||
audit_plane: true
|
|
||||||
event_bus: true
|
|
||||||
architect_agent: true
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=AIOS Git Permission Watchdog
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=aiman
|
|
||||||
Group=gitops
|
|
||||||
WorkingDirectory=/data/aios-gitops/workspace
|
|
||||||
ExecStart=/usr/bin/python3 -m active_runtime.app.git_permission_watchdog_cli
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Run AIOS Git Permission Watchdog periodically
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=5min
|
|
||||||
OnUnitActiveSec=5min
|
|
||||||
Unit=aios-git-permission-watchdog.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
# Architect Agent Output Template
|
||||||
|
|
||||||
|
## 1. Requirement Change Record
|
||||||
|
|
||||||
|
- Requirement ID:
|
||||||
|
- Source:
|
||||||
|
- Custom GPT
|
||||||
|
- Codex
|
||||||
|
- OpenClaw
|
||||||
|
- Hermes
|
||||||
|
- User feedback
|
||||||
|
- Time:
|
||||||
|
- Requirement:
|
||||||
|
|
||||||
|
## 2. Architecture Review
|
||||||
|
|
||||||
|
### Current architecture impact
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
### Optimization recommendations
|
||||||
|
|
||||||
|
## 3. Document Refresh
|
||||||
|
|
||||||
|
Required:
|
||||||
|
|
||||||
|
- User Requirements Document
|
||||||
|
- Architecture Design Document
|
||||||
|
- Engineering Deployment Document
|
||||||
|
- Test Case Document
|
||||||
|
|
||||||
|
## 4. Deployment Consistency Check
|
||||||
|
|
||||||
|
Compare:
|
||||||
|
|
||||||
|
- Deployment document
|
||||||
|
- Runtime configuration
|
||||||
|
- Actual services
|
||||||
|
- Environment variables
|
||||||
|
|
||||||
|
## 5. Test Acceptance Plan
|
||||||
|
|
||||||
|
List:
|
||||||
|
|
||||||
|
- Unit tests
|
||||||
|
- Integration tests
|
||||||
|
- User acceptance tests
|
||||||
|
- Architecture validation tests
|
||||||
|
|
||||||
|
## 6. Version Decision
|
||||||
|
|
||||||
|
Choose:
|
||||||
|
|
||||||
|
- C: compatible change/fix
|
||||||
|
- R: release capability or architecture evolution
|
||||||
|
- V: major architecture/breaking change
|
||||||
|
|
||||||
|
Reason:
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# CMD-20260709-deploy-portal-aios-dashboard
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Deploy AIOS modules into authenticated user portal.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
Enable:
|
||||||
|
|
||||||
|
- Portal AIOS Module Registry
|
||||||
|
- Portal Navigation Builder
|
||||||
|
- Portal Dashboard API
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/portal/me/aios/manifest
|
||||||
|
/portal/me/aios/dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Integrate portal `/portal/me/`:
|
||||||
|
|
||||||
|
1. Load AIOS dashboard manifest after authentication.
|
||||||
|
2. Render AIOS navigation items.
|
||||||
|
3. Respect role visibility.
|
||||||
|
4. Link routes to deployed AIOS pages.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
Validate:
|
||||||
|
|
||||||
|
User:
|
||||||
|
- Task Center only authorized modules
|
||||||
|
|
||||||
|
Group Admin:
|
||||||
|
- Operations
|
||||||
|
- System Health
|
||||||
|
- Search
|
||||||
|
- Agents
|
||||||
|
|
||||||
|
Owner:
|
||||||
|
- All governance modules
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_PORTAL_DASHBOARD_READY
|
||||||
|
- AIOS_PORTAL_NAVIGATION_READY
|
||||||
|
- AIOS_ROLE_VISIBILITY_READY
|
||||||
|
- AIOS_PORTAL_USER_EXPERIENCE_READY
|
||||||
|
|
||||||
|
## Report
|
||||||
|
|
||||||
|
Return:
|
||||||
|
|
||||||
|
- portal URL
|
||||||
|
- frontend version
|
||||||
|
- backend routes
|
||||||
|
- role visibility test result
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# CMD-20260709-enable-architect-governance-loop
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Enable architect agent as a mandatory governance role.
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
1. Record every requirement change from:
|
||||||
|
- Custom GPT
|
||||||
|
- Codex
|
||||||
|
- OpenClaw
|
||||||
|
- Hermes
|
||||||
|
- user feedback
|
||||||
|
|
||||||
|
2. Refresh versioned documents:
|
||||||
|
- user requirements
|
||||||
|
- architecture design
|
||||||
|
- engineering deployment
|
||||||
|
- test cases
|
||||||
|
|
||||||
|
3. Review every architecture change:
|
||||||
|
- full architecture consistency
|
||||||
|
- optimization opportunities
|
||||||
|
- security boundaries
|
||||||
|
- scalability
|
||||||
|
- rollback
|
||||||
|
|
||||||
|
4. Weekly research:
|
||||||
|
- agent framework versions
|
||||||
|
- model ecosystem changes
|
||||||
|
- GitHub top implementations
|
||||||
|
- security changes
|
||||||
|
|
||||||
|
5. Decide version bump:
|
||||||
|
- C compatible fix/change
|
||||||
|
- R release capability or architecture evolution
|
||||||
|
- V major architecture/breaking change
|
||||||
|
|
||||||
|
## Validation signals
|
||||||
|
|
||||||
|
- AIOS_ARCHITECT_GOVERNANCE_READY
|
||||||
|
- AIOS_REQUIREMENT_VERSIONING_READY
|
||||||
|
- AIOS_ARCHITECTURE_REVIEW_LOOP_READY
|
||||||
|
- AIOS_WEEKLY_TECH_RESEARCH_READY
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
All outputs must be audited and stored through GitOps.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# CMD-20260709-enable-architect-governance-ui
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Deploy Architect Governance Center Web UI.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. Register API:
|
||||||
|
- getArchitectGovernanceOverview
|
||||||
|
2. Enable page:
|
||||||
|
- /admin/ai-evolution/architect
|
||||||
|
3. Display:
|
||||||
|
- requirement changes
|
||||||
|
- architecture reviews
|
||||||
|
- document refresh status
|
||||||
|
- deployment consistency checks
|
||||||
|
- weekly research findings
|
||||||
|
- V/R/C recommendations
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_ARCHITECT_GOVERNANCE_UI_READY
|
||||||
|
- AIOS_REQUIREMENT_TRACEABILITY_VISIBLE
|
||||||
|
- AIOS_VERSION_DECISION_VISIBLE
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
All architecture decisions require audit evidence.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# CMD-20260709-enable-architect-research-timeline-ui
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Expose weekly architect research lifecycle in Web UI.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. Enable API:
|
||||||
|
- getArchitectResearchTimeline
|
||||||
|
2. Add page:
|
||||||
|
- /admin/ai-evolution/architect/research/timeline
|
||||||
|
3. Display:
|
||||||
|
- research plan
|
||||||
|
- findings
|
||||||
|
- architecture impact
|
||||||
|
- recommendation
|
||||||
|
- C/R/V decision
|
||||||
|
- governance event link
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_ARCHITECT_RESEARCH_TIMELINE_READY
|
||||||
|
- AIOS_TECH_FINDING_TRACE_READY
|
||||||
|
- AIOS_VERSION_RECOMMENDATION_TRACE_READY
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
No technology adoption without architecture review.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# CMD-20260709-enable-architect-weekly-research
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Enable weekly proactive architecture intelligence loop.
|
||||||
|
|
||||||
|
## Execute
|
||||||
|
|
||||||
|
1. Schedule weekly research.
|
||||||
|
2. Search/evaluate:
|
||||||
|
- agent framework versions
|
||||||
|
- model ecosystem changes
|
||||||
|
- GitHub top implementations
|
||||||
|
- security changes
|
||||||
|
3. Generate findings.
|
||||||
|
4. Evaluate architecture impact.
|
||||||
|
5. Create recommendation:
|
||||||
|
- no action
|
||||||
|
- C change
|
||||||
|
- R release
|
||||||
|
- V major architecture change
|
||||||
|
6. Emit architecture review event.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_ARCHITECT_WEEKLY_RESEARCH_READY
|
||||||
|
- AIOS_TECH_INTELLIGENCE_READY
|
||||||
|
- AIOS_VERSION_RECOMMENDATION_READY
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
No external technology adoption without architecture review and owner gate.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# CMD-20260709-enable-architecture-owner-gate
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Require Owner approval before implementing architecture changes.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Requirement change
|
||||||
|
-> Architect review
|
||||||
|
-> Owner architecture approval
|
||||||
|
-> Implementation
|
||||||
|
-> Test Gate
|
||||||
|
-> Promotion
|
||||||
|
|
||||||
|
## Block rules
|
||||||
|
|
||||||
|
Architecture changes cannot enter implementation when:
|
||||||
|
|
||||||
|
- owner approval missing
|
||||||
|
- owner rejected
|
||||||
|
- audit evidence missing
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_ARCHITECTURE_OWNER_GATE_READY
|
||||||
|
- AIOS_ARCHITECTURE_IMPLEMENTATION_PROTECTED
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
Owner approval must be auditable with request, architect decision and owner decision.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# CMD-20260709-enable-autonomous-goal-completion
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Enable AIOS to complete normal development and deployment goals without repeated owner progress queries.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
Components:
|
||||||
|
|
||||||
|
- Autonomous Goal Completion Supervisor
|
||||||
|
- Task Center
|
||||||
|
- System Health Center
|
||||||
|
- Blocker Memory
|
||||||
|
- Architect Agent
|
||||||
|
- Notification Agent
|
||||||
|
|
||||||
|
## Autonomous Operations
|
||||||
|
|
||||||
|
Allowed:
|
||||||
|
|
||||||
|
- create task plans
|
||||||
|
- assign agents
|
||||||
|
- monitor execution
|
||||||
|
- retry safe failures
|
||||||
|
- trigger self healing
|
||||||
|
- update progress
|
||||||
|
- send completion reports
|
||||||
|
|
||||||
|
## Owner Gate Only
|
||||||
|
|
||||||
|
Require approval for:
|
||||||
|
|
||||||
|
- secrets
|
||||||
|
- paid providers
|
||||||
|
- production baseline promotion
|
||||||
|
- large architecture changes
|
||||||
|
- private data sources
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_AUTONOMOUS_GOAL_COMPLETION_READY
|
||||||
|
- AIOS_NO_PROGRESS_QUERY_REQUIRED_READY
|
||||||
|
- AIOS_OWNER_GATE_BOUNDARY_READY
|
||||||
|
- AIOS_AUTOMATED_REPORTING_READY
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# CMD-20260709-enable-autonomous-operation-command-center
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Create unified AIOS autonomous operations command center.
|
|
||||||
|
|
||||||
## Integrate
|
|
||||||
|
|
||||||
- Autonomous Controller
|
|
||||||
- System Health Center
|
|
||||||
- Task Center
|
|
||||||
- Blocker Memory
|
|
||||||
- Search Gateway monitoring
|
|
||||||
- Notification loop
|
|
||||||
|
|
||||||
## Page
|
|
||||||
|
|
||||||
/admin/ai-evolution/operations
|
|
||||||
|
|
||||||
## Display
|
|
||||||
|
|
||||||
- active tasks
|
|
||||||
- blocked tasks
|
|
||||||
- P0 issues
|
|
||||||
- recurring problems
|
|
||||||
- self-healing actions
|
|
||||||
- search health
|
|
||||||
- notification status
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- AIOS_OPERATIONS_COMMAND_CENTER_READY
|
|
||||||
- AIOS_AUTONOMOUS_LOOP_VISIBLE
|
|
||||||
- AIOS_BLOCKER_OPTIMIZATION_VISIBLE
|
|
||||||
- AIOS_TASK_HEALTH_UNIFIED_READY
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user