Compare commits
118
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1789572efe | ||
|
|
f6d9bb1510 | ||
|
|
8ca4db3e8c | ||
|
|
d194d3a4d7 | ||
|
|
f5435e3087 | ||
|
|
88087dc7ff | ||
|
|
2f04b9f46c | ||
|
|
06d3b4cda6 | ||
|
|
09186fe551 | ||
|
|
c634f2e23b | ||
|
|
b3ccae4c2b | ||
|
|
85ac09110f | ||
|
|
6622ebb30e | ||
|
|
824b079fea | ||
|
|
2e5a8bfb5a | ||
|
|
ce36b790ea | ||
|
|
8d60aa921a | ||
|
|
78ce96a8db | ||
|
|
ce308f84ba | ||
|
|
7459f24696 | ||
|
|
8f90744ba0 | ||
|
|
4db407345c | ||
|
|
4a09eacfe4 | ||
|
|
a9e1b26652 | ||
|
|
513d90bb8a | ||
|
|
d03f06aa82 | ||
|
|
c657057826 | ||
|
|
84717341d7 | ||
|
|
f631a61330 | ||
|
|
38222c9fec | ||
|
|
055fc895a6 | ||
|
|
8ab38f142a | ||
|
|
1b3921f9c8 | ||
|
|
c15453db1a | ||
|
|
83641c2491 | ||
|
|
e1f4d1f267 | ||
|
|
00d37b2d1a | ||
|
|
887061e65b | ||
|
|
bb2b301c4e | ||
|
|
2cc1acebd2 | ||
|
|
9dd2d33cbb | ||
|
|
f648e19d4d | ||
|
|
9793239962 | ||
|
|
81d5744e8e | ||
|
|
5d37ecfff0 | ||
|
|
64577a1eb8 | ||
|
|
720a7026b4 | ||
|
|
e4a83388ae | ||
|
|
2e9b749ce2 | ||
|
|
a074b13a7a | ||
|
|
16877032ac | ||
|
|
21dd8d10c0 | ||
|
|
a2e9dab162 | ||
|
|
99d49b5d85 | ||
|
|
024bfc52f2 | ||
|
|
4588cdf2d9 | ||
|
|
81147e44bb | ||
|
|
0f41034b78 | ||
|
|
2c5d9995f9 | ||
|
|
aa71ce9062 | ||
|
|
967ef3bb3c | ||
|
|
d05055abb6 | ||
|
|
a905b92fcc | ||
|
|
797e301968 | ||
|
|
d8a426cea1 | ||
|
|
d5ded1e48a | ||
|
|
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 | ||
|
|
741206597b | ||
|
|
36755f5f8b | ||
|
|
c1e05f7c4a |
@@ -15,7 +15,7 @@ TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.toke
|
||||
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
||||
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
||||
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
|
||||
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
|
||||
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
|
||||
monitor = ActionMonitor()
|
||||
|
||||
BASE_MODELS = [
|
||||
|
||||
@@ -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,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"]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Agent adapters for AIOS Search Gateway.
|
||||
|
||||
All runtime agents should call SearchGatewayAgentAdapter instead of performing
|
||||
ad-hoc network search. This gives consistent provider fallback, profile
|
||||
resolution, evidence capture, audit and event visibility.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .search_gateway_orchestrator import SearchGatewayOrchestrator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentSearchContext:
|
||||
agent: str
|
||||
user_id: str | None = None
|
||||
group_id: str | None = None
|
||||
role: str = "user"
|
||||
group_profile: dict[str, Any] | None = None
|
||||
user_profile: dict[str, Any] | None = None
|
||||
session_profile: dict[str, Any] | None = None
|
||||
authorized_sources: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class SearchGatewayAgentAdapter:
|
||||
def __init__(self, orchestrator: SearchGatewayOrchestrator | None = None):
|
||||
self.orchestrator = orchestrator or SearchGatewayOrchestrator()
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
context: AgentSearchContext,
|
||||
intent: str = "general",
|
||||
region: str | None = None,
|
||||
time_range: str | None = None,
|
||||
data_type: str | None = None,
|
||||
domain_profiles: list[str] | None = None,
|
||||
user_scope: str = "public",
|
||||
correlation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload = {
|
||||
"query": query,
|
||||
"requester": context.agent,
|
||||
"intent": intent,
|
||||
"region": region,
|
||||
"time_range": time_range,
|
||||
"data_type": data_type,
|
||||
"domain_profiles": domain_profiles or [],
|
||||
"user_scope": user_scope,
|
||||
"authorized_sources": context.authorized_sources,
|
||||
"group_profile": context.group_profile,
|
||||
"user_profile": context.user_profile,
|
||||
"session_profile": context.session_profile,
|
||||
"correlation_id": correlation_id,
|
||||
}
|
||||
return self.orchestrator.execute(payload)
|
||||
|
||||
|
||||
class OpenClawSearchClient(SearchGatewayAgentAdapter):
|
||||
def search_public(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
|
||||
return self.search(query, context, user_scope="public", **kwargs)
|
||||
|
||||
|
||||
class HermesSearchClient(SearchGatewayAgentAdapter):
|
||||
def delegate_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
|
||||
return self.search(query, context, **kwargs)
|
||||
|
||||
|
||||
class CodexSearchClient(SearchGatewayAgentAdapter):
|
||||
def technical_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
|
||||
return self.search(query, context, intent=kwargs.pop("intent", "software_library"), **kwargs)
|
||||
|
||||
|
||||
class CustomGPTSearchClient(SearchGatewayAgentAdapter):
|
||||
def user_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
|
||||
return self.search(query, context, **kwargs)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentSearchContext",
|
||||
"SearchGatewayAgentAdapter",
|
||||
"OpenClawSearchClient",
|
||||
"HermesSearchClient",
|
||||
"CodexSearchClient",
|
||||
"CustomGPTSearchClient",
|
||||
]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Architect governance integration for Search Gateway.
|
||||
|
||||
Search Gateway is a shared platform capability. The Architect Agent periodically
|
||||
reviews provider health, external search/browser capabilities, cost, security and
|
||||
privacy impact, then emits C/R/V recommendations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchArchitectureFinding:
|
||||
finding_id: str
|
||||
title: str
|
||||
impact: str
|
||||
recommendation: str
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchArchitectureDecision:
|
||||
status: str
|
||||
version_bump: str
|
||||
reason: str
|
||||
findings: list[SearchArchitectureFinding]
|
||||
owner_gate_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"version_bump": self.version_bump,
|
||||
"reason": self.reason,
|
||||
"findings": [f.to_dict() for f in self.findings],
|
||||
"owner_gate_required": self.owner_gate_required,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
class SearchArchitectIntegration:
|
||||
def review(self, signals: dict[str, Any]) -> SearchArchitectureDecision:
|
||||
findings: list[SearchArchitectureFinding] = []
|
||||
if signals.get("provider_breaking_change"):
|
||||
findings.append(SearchArchitectureFinding("search-provider-breaking", "Provider breaking change", "breaking", "owner_review"))
|
||||
if signals.get("new_search_capability"):
|
||||
findings.append(SearchArchitectureFinding("search-new-capability", "New search capability available", "architecture", "evaluate_adoption"))
|
||||
if signals.get("cost_increase"):
|
||||
findings.append(SearchArchitectureFinding("search-cost", "Search provider cost increase", "minor", "optimize_provider_order"))
|
||||
if signals.get("security_advisory"):
|
||||
findings.append(SearchArchitectureFinding("search-security", "Search security advisory", "breaking", "owner_review"))
|
||||
|
||||
if any(f.impact == "breaking" for f in findings):
|
||||
return SearchArchitectureDecision("action_required", "V", "breaking search ecosystem/security change", findings, True)
|
||||
if any(f.impact == "architecture" for f in findings):
|
||||
return SearchArchitectureDecision("optimization_recommended", "R", "new search capability may improve architecture", findings, True)
|
||||
if findings:
|
||||
return SearchArchitectureDecision("minor_change_recommended", "C", "compatible provider optimization", findings, False)
|
||||
return SearchArchitectureDecision("no_action", "none", "no relevant search architecture change", findings, False)
|
||||
|
||||
|
||||
__all__ = ["SearchArchitectureFinding", "SearchArchitectureDecision", "SearchArchitectIntegration"]
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Audit storage for Search Gateway executions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SearchAuditStore:
|
||||
def __init__(self, root: str = "runtime/search-gateway/audit"):
|
||||
self.root = Path(root)
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def append(self, record: dict[str, Any]) -> str:
|
||||
path = self.root / f"{record.get('request_id', 'search')}.json"
|
||||
path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return str(path)
|
||||
|
||||
def list_recent(self) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
for path in sorted(self.root.glob("*.json")):
|
||||
result.append(json.loads(path.read_text(encoding="utf-8")))
|
||||
return result[-50:]
|
||||
|
||||
|
||||
__all__ = ["SearchAuditStore"]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Search Gateway event bridge.
|
||||
|
||||
Publishes normalized search lifecycle events into the AIOS Agent Event Bus when a
|
||||
bus is available, with an in-memory fallback for tests and offline runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Protocol
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class SearchEventPublisher(Protocol):
|
||||
def publish(
|
||||
self,
|
||||
kind: str,
|
||||
source: str,
|
||||
target: str,
|
||||
summary: str,
|
||||
payload: dict[str, Any],
|
||||
correlation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchEvent:
|
||||
id: str
|
||||
kind: str
|
||||
source: str
|
||||
target: str
|
||||
summary: str
|
||||
payload: dict[str, Any]
|
||||
correlation_id: str | None = None
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class InMemorySearchEventBus:
|
||||
def __init__(self):
|
||||
self.events: list[dict[str, Any]] = []
|
||||
|
||||
def publish(
|
||||
self,
|
||||
kind: str,
|
||||
source: str,
|
||||
target: str,
|
||||
summary: str,
|
||||
payload: dict[str, Any],
|
||||
correlation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
event = SearchEvent(
|
||||
id=f"search_evt_{uuid4().hex[:12]}",
|
||||
kind=kind,
|
||||
source=source,
|
||||
target=target,
|
||||
summary=summary,
|
||||
payload=payload,
|
||||
correlation_id=correlation_id,
|
||||
).to_dict()
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
|
||||
class SearchEventBridge:
|
||||
def __init__(self, bus: SearchEventPublisher | None = None):
|
||||
self.bus = bus or InMemorySearchEventBus()
|
||||
|
||||
def search_started(self, request: dict[str, Any]) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="search_started",
|
||||
source=request.get("requester", "unknown"),
|
||||
target="search-gateway",
|
||||
summary=request.get("query", "search started"),
|
||||
payload=request,
|
||||
correlation_id=request.get("correlation_id"),
|
||||
)
|
||||
|
||||
def provider_attempted(self, request_id: str, attempt: dict[str, Any]) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="search_provider_attempted",
|
||||
source="search-gateway",
|
||||
target=attempt.get("provider", "unknown"),
|
||||
summary=f"provider {attempt.get('provider', 'unknown')} {attempt.get('status', 'attempted')}",
|
||||
payload=attempt,
|
||||
correlation_id=request_id,
|
||||
)
|
||||
|
||||
def fallback_triggered(self, request_id: str, reason: str) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="search_fallback_triggered",
|
||||
source="search-gateway",
|
||||
target="search-provider-router",
|
||||
summary=reason,
|
||||
payload={"reason": reason},
|
||||
correlation_id=request_id,
|
||||
)
|
||||
|
||||
def search_completed(self, response: dict[str, Any]) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="search_completed",
|
||||
source="search-gateway",
|
||||
target=response.get("requester", "requester"),
|
||||
summary=f"search {response.get('status', 'completed')}",
|
||||
payload=response,
|
||||
correlation_id=response.get("request_id"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["SearchEvent", "InMemorySearchEventBus", "SearchEventBridge"]
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Governed general-purpose Search Gateway for AIOS.
|
||||
|
||||
The gateway coordinates query planning, provider routing, fallback, evidence
|
||||
normalization and compliance checks. It is provider-agnostic: runtime workers plug
|
||||
in public or explicitly authorized search providers.
|
||||
|
||||
Search is a shared capability. Groups and users may customize provider order,
|
||||
trusted domains, query expansion hints and domain profiles, but customization may
|
||||
not override hard authorization, privacy, safety or audit rules.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Protocol
|
||||
from uuid import uuid4
|
||||
|
||||
from .search_profile_resolver import DomainQueryProfileRegistry, SearchProfile, SearchProfileResolver
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchRequest:
|
||||
query: str
|
||||
requester: str
|
||||
intent: str = "general"
|
||||
region: str | None = None
|
||||
time_range: str | None = None
|
||||
data_type: str | None = None
|
||||
user_scope: str = "public"
|
||||
authorized_sources: list[str] = field(default_factory=list)
|
||||
domain_profiles: list[str] = field(default_factory=list)
|
||||
group_profile: dict[str, Any] | None = None
|
||||
user_profile: dict[str, Any] | None = None
|
||||
session_profile: dict[str, Any] | None = None
|
||||
correlation_id: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchEvidence:
|
||||
title: str
|
||||
url: str | None
|
||||
source: str
|
||||
snippet: str
|
||||
evidence_type: str = "web"
|
||||
confidence: str = "medium"
|
||||
retrieved_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchProviderAttempt:
|
||||
provider: str
|
||||
status: str
|
||||
result_count: int = 0
|
||||
reason: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchResponse:
|
||||
request_id: str
|
||||
status: str
|
||||
answer: str
|
||||
evidence: list[SearchEvidence]
|
||||
attempts: list[SearchProviderAttempt]
|
||||
confidence: str
|
||||
expanded_queries: list[str] = field(default_factory=list)
|
||||
resolved_profile: dict[str, Any] | None = None
|
||||
need_clarification: list[str] = field(default_factory=list)
|
||||
blocked_reasons: list[str] = field(default_factory=list)
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"request_id": self.request_id,
|
||||
"status": self.status,
|
||||
"answer": self.answer,
|
||||
"evidence": [e.to_dict() for e in self.evidence],
|
||||
"attempts": [a.to_dict() for a in self.attempts],
|
||||
"confidence": self.confidence,
|
||||
"expanded_queries": self.expanded_queries,
|
||||
"resolved_profile": self.resolved_profile,
|
||||
"need_clarification": self.need_clarification,
|
||||
"blocked_reasons": self.blocked_reasons,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
|
||||
class SearchProvider(Protocol):
|
||||
name: str
|
||||
|
||||
def search(self, request: SearchRequest, queries: list[str]) -> tuple[list[SearchEvidence], SearchProviderAttempt]:
|
||||
...
|
||||
|
||||
|
||||
class QueryPlanner:
|
||||
def __init__(self, domain_registry: DomainQueryProfileRegistry | None = None):
|
||||
self.domain_registry = domain_registry or DomainQueryProfileRegistry()
|
||||
|
||||
def expand(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
|
||||
base = request.query.strip()
|
||||
queries = [base]
|
||||
if request.region:
|
||||
queries.append(f"{request.region} {base}")
|
||||
if request.time_range:
|
||||
queries.append(f"{base} {request.time_range}")
|
||||
domain_profiles = list(dict.fromkeys([request.intent] + request.domain_profiles + profile.domain_profiles))
|
||||
hints = list(profile.query_expansion_hints) + self.domain_registry.hints_for(domain_profiles)
|
||||
for hint in hints:
|
||||
queries.append(f"{base} {hint}")
|
||||
for domain in profile.trusted_domains:
|
||||
queries.append(f"site:{domain} {base}")
|
||||
return list(dict.fromkeys([q for q in queries if q]))
|
||||
|
||||
|
||||
class SearchPolicyGuard:
|
||||
"""Blocks non-public or unauthorized collection patterns."""
|
||||
|
||||
FORBIDDEN_TERMS = (
|
||||
"绕过登录",
|
||||
"绕过验证码",
|
||||
"验证码绕过",
|
||||
"破解登录",
|
||||
"个人成绩",
|
||||
"学生隐私",
|
||||
"受保护平台批量爬取",
|
||||
)
|
||||
|
||||
def evaluate(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
|
||||
q = request.query.lower()
|
||||
reasons: list[str] = []
|
||||
for term in self.FORBIDDEN_TERMS:
|
||||
if term.lower() in q:
|
||||
reasons.append(f"blocked_term:{term}")
|
||||
if request.user_scope not in {"public", "authorized_user_file", "authorized_link", "internal_archive"}:
|
||||
reasons.append(f"unsupported_scope:{request.user_scope}")
|
||||
if request.data_type in {"personal_grade", "student_private_data", "private_person_data"} and request.user_scope != "authorized_user_file":
|
||||
reasons.append("private_data_requires_user_authorized_file")
|
||||
if not profile.audit_required:
|
||||
reasons.append("audit_cannot_be_disabled")
|
||||
if not profile.fallback_required:
|
||||
reasons.append("fallback_cannot_be_disabled")
|
||||
return reasons
|
||||
|
||||
|
||||
class SearchGateway:
|
||||
def __init__(self, providers: list[SearchProvider] | None = None):
|
||||
self.providers = providers or []
|
||||
self.profile_resolver = SearchProfileResolver()
|
||||
self.planner = QueryPlanner()
|
||||
self.guard = SearchPolicyGuard()
|
||||
|
||||
def search(self, request: SearchRequest) -> SearchResponse:
|
||||
request_id = request.correlation_id or f"search_{uuid4().hex[:12]}"
|
||||
profile = self.profile_resolver.resolve(request.group_profile, request.user_profile, request.session_profile)
|
||||
expanded_queries = self.planner.expand(request, profile)
|
||||
blocked = self.guard.evaluate(request, profile)
|
||||
if blocked:
|
||||
return SearchResponse(
|
||||
request_id=request_id,
|
||||
status="blocked",
|
||||
answer="该搜索请求涉及未授权访问、验证码/登录绕过、受保护平台批量抓取或个人隐私数据,已阻断。可改为搜索公开网页、官方公告、公开文档,或处理用户授权上传的文件/链接。",
|
||||
evidence=[],
|
||||
attempts=[],
|
||||
confidence="high",
|
||||
expanded_queries=expanded_queries,
|
||||
resolved_profile=profile.to_dict(),
|
||||
blocked_reasons=blocked,
|
||||
)
|
||||
|
||||
evidence: list[SearchEvidence] = []
|
||||
attempts: list[SearchProviderAttempt] = []
|
||||
providers = self._ordered_providers(profile)
|
||||
for provider in providers:
|
||||
try:
|
||||
provider_evidence, attempt = provider.search(request, expanded_queries)
|
||||
evidence.extend(provider_evidence)
|
||||
attempts.append(attempt)
|
||||
if provider_evidence:
|
||||
break
|
||||
except Exception as exc:
|
||||
attempts.append(SearchProviderAttempt(provider=getattr(provider, "name", "unknown"), status="failed", reason=str(exc)))
|
||||
|
||||
if evidence:
|
||||
return SearchResponse(
|
||||
request_id=request_id,
|
||||
status="found",
|
||||
answer="已找到公开或授权来源结果。",
|
||||
evidence=evidence,
|
||||
attempts=attempts,
|
||||
confidence=self._confidence(evidence),
|
||||
expanded_queries=expanded_queries,
|
||||
resolved_profile=profile.to_dict(),
|
||||
)
|
||||
|
||||
return SearchResponse(
|
||||
request_id=request_id,
|
||||
status="not_found",
|
||||
answer="未找到足够可靠的公开或授权结果;已记录 provider 尝试和失败原因。",
|
||||
evidence=[],
|
||||
attempts=attempts,
|
||||
confidence="low",
|
||||
expanded_queries=expanded_queries,
|
||||
resolved_profile=profile.to_dict(),
|
||||
need_clarification=self._clarification(request),
|
||||
)
|
||||
|
||||
def _ordered_providers(self, profile: SearchProfile) -> list[SearchProvider]:
|
||||
if not self.providers:
|
||||
return []
|
||||
by_name = {provider.name: provider for provider in self.providers}
|
||||
ordered = [by_name[name] for name in profile.provider_order if name in by_name]
|
||||
remaining = [provider for provider in self.providers if provider.name not in {p.name for p in ordered}]
|
||||
return ordered + remaining
|
||||
|
||||
def _confidence(self, evidence: list[SearchEvidence]) -> str:
|
||||
if any(e.confidence == "high" for e in evidence):
|
||||
return "high"
|
||||
if evidence:
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
def _clarification(self, request: SearchRequest) -> list[str]:
|
||||
if request.intent == "education_exam_data":
|
||||
return ["请补充学校名称、联考名称、年级、科目,以及需要试卷/答案还是成绩统计。"]
|
||||
if request.intent in {"company_research", "policy_regulation", "software_library", "product_research"}:
|
||||
return ["请补充主体名称、地区/版本、时间范围、需要的数据类型或可信来源范围。"]
|
||||
return ["请补充更具体的主体、平台、地区、时间、数据类型或授权文件/链接。"]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SearchRequest",
|
||||
"SearchEvidence",
|
||||
"SearchProviderAttempt",
|
||||
"SearchResponse",
|
||||
"SearchProvider",
|
||||
"QueryPlanner",
|
||||
"SearchPolicyGuard",
|
||||
"SearchGateway",
|
||||
]
|
||||
@@ -0,0 +1,65 @@
|
||||
"""API endpoints for AIOS Search Gateway."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter
|
||||
except Exception:
|
||||
APIRouter = None
|
||||
|
||||
from .search_gateway import SearchGateway, SearchRequest
|
||||
from .search_gateway_dashboard import SearchGatewayDashboard
|
||||
|
||||
|
||||
gateway = SearchGateway()
|
||||
dashboard = SearchGatewayDashboard()
|
||||
|
||||
|
||||
def search_gateway_request(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
request = SearchRequest(**payload)
|
||||
return gateway.search(request).to_dict()
|
||||
|
||||
|
||||
def get_search_gateway_status() -> dict[str, Any]:
|
||||
return {
|
||||
"module": "search-gateway",
|
||||
"status": "ready",
|
||||
"providers_configured": len(gateway.providers),
|
||||
"policy_guard": "enabled",
|
||||
"profile_customization": "enabled",
|
||||
"fallback_required_when_provider_unavailable": True,
|
||||
}
|
||||
|
||||
|
||||
def get_search_gateway_overview() -> dict[str, Any]:
|
||||
return dashboard.overview()
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
router = APIRouter(prefix="/admin/ai-evolution/search", tags=["search-gateway"])
|
||||
|
||||
@router.get("/status", operation_id="getSearchGatewayStatus")
|
||||
def status():
|
||||
return get_search_gateway_status()
|
||||
|
||||
@router.get("/overview", operation_id="getSearchGatewayOverview")
|
||||
def overview():
|
||||
return get_search_gateway_overview()
|
||||
|
||||
@router.post("/query", operation_id="runSearchGatewayQuery")
|
||||
def query(payload: dict[str, Any]):
|
||||
return search_gateway_request(payload)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
"search_gateway_request",
|
||||
"get_search_gateway_status",
|
||||
"get_search_gateway_overview",
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Search Gateway Web UI dashboard model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchDashboardMetric:
|
||||
key: str
|
||||
title: str
|
||||
value: Any
|
||||
status: str = "ok"
|
||||
detail: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchDashboardPanel:
|
||||
key: str
|
||||
title: str
|
||||
metrics: list[SearchDashboardMetric] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"key": self.key,
|
||||
"title": self.title,
|
||||
"metrics": [m.to_dict() for m in self.metrics],
|
||||
}
|
||||
|
||||
|
||||
class SearchGatewayDashboard:
|
||||
def overview(self, search_runs: list[dict[str, Any]] | None = None, providers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
search_runs = search_runs or []
|
||||
providers = providers or []
|
||||
blocked = [r for r in search_runs if r.get("status") == "blocked"]
|
||||
not_found = [r for r in search_runs if r.get("status") == "not_found"]
|
||||
provider_failures = [p for p in providers if p.get("status") == "failed"]
|
||||
panels = [
|
||||
SearchDashboardPanel(
|
||||
"search_health",
|
||||
"搜索健康度",
|
||||
[
|
||||
SearchDashboardMetric("total_runs", "搜索请求数", len(search_runs)),
|
||||
SearchDashboardMetric("not_found", "未找到结果", len(not_found), "warn" if not_found else "ok"),
|
||||
SearchDashboardMetric("blocked", "合规阻断", len(blocked), "warn" if blocked else "ok"),
|
||||
],
|
||||
),
|
||||
SearchDashboardPanel(
|
||||
"provider_health",
|
||||
"Provider 可用性",
|
||||
[
|
||||
SearchDashboardMetric("providers", "Provider 数量", len(providers)),
|
||||
SearchDashboardMetric("provider_failures", "Provider 失败", len(provider_failures), "warn" if provider_failures else "ok"),
|
||||
],
|
||||
),
|
||||
SearchDashboardPanel(
|
||||
"evidence_quality",
|
||||
"证据质量",
|
||||
[
|
||||
SearchDashboardMetric("with_evidence", "有证据的回答", len([r for r in search_runs if r.get("evidence")])) ,
|
||||
SearchDashboardMetric("fallback_required", "要求多源降级", True),
|
||||
],
|
||||
),
|
||||
]
|
||||
return {
|
||||
"module": "search-gateway",
|
||||
"route": "/admin/ai-evolution/search",
|
||||
"title": "AIOS 联网搜索治理中心",
|
||||
"status": "attention_required" if provider_failures else "healthy",
|
||||
"generated_at": utc_now(),
|
||||
"panels": [p.to_dict() for p in panels],
|
||||
"recent_runs": search_runs[-20:],
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SearchDashboardMetric", "SearchDashboardPanel", "SearchGatewayDashboard"]
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Runtime orchestrator joining Search Gateway, registry, audit and events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .search_audit_store import SearchAuditStore
|
||||
from .search_event_bridge import SearchEventBridge
|
||||
from .search_gateway import SearchGateway, SearchRequest
|
||||
from .search_provider_registry import SearchProviderRegistry
|
||||
|
||||
|
||||
class SearchGatewayOrchestrator:
|
||||
def __init__(self):
|
||||
self.registry = SearchProviderRegistry()
|
||||
self.audit = SearchAuditStore()
|
||||
self.events = SearchEventBridge()
|
||||
self.gateway = SearchGateway(self.registry.ordered())
|
||||
|
||||
def execute(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
request = SearchRequest(**payload)
|
||||
self.events.search_started(request.to_dict())
|
||||
response = self.gateway.search(request)
|
||||
result = response.to_dict()
|
||||
for attempt in result.get("attempts", []):
|
||||
self.events.provider_attempted(result["request_id"], attempt)
|
||||
if len(result.get("attempts", [])) > 1:
|
||||
self.events.fallback_triggered(result["request_id"], "multiple providers attempted")
|
||||
self.audit.append(result)
|
||||
self.events.search_completed(result)
|
||||
return result
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"registry": self.registry.status(),
|
||||
"audit": "enabled",
|
||||
"events": "enabled",
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["SearchGatewayOrchestrator"]
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Search profile resolver for general-purpose AIOS Search Gateway.
|
||||
|
||||
The Search Gateway is a shared platform capability. Groups and users may customize
|
||||
provider preference, query expansion hints, trusted sources and domain profiles on
|
||||
top of the shared base profile. Customization is intentionally layered and cannot
|
||||
override hard safety, authorization or audit rules.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
HARD_BLOCKED_BEHAVIORS = {
|
||||
"bypass_login",
|
||||
"bypass_captcha",
|
||||
"scrape_private_student_grades",
|
||||
"scrape_student_private_data",
|
||||
"bulk_scrape_protected_platforms",
|
||||
"collect_data_without_authorization",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchProfile:
|
||||
profile_id: str
|
||||
scope: str
|
||||
provider_order: list[str] = field(default_factory=lambda: [
|
||||
"cache",
|
||||
"official_site",
|
||||
"general_web",
|
||||
"vertical_source",
|
||||
"browser_fetch",
|
||||
"user_authorized_file",
|
||||
"internal_archive",
|
||||
])
|
||||
trusted_domains: list[str] = field(default_factory=list)
|
||||
query_expansion_hints: list[str] = field(default_factory=list)
|
||||
domain_profiles: list[str] = field(default_factory=list)
|
||||
blocked_behaviors: set[str] = field(default_factory=lambda: set(HARD_BLOCKED_BEHAVIORS))
|
||||
audit_required: bool = True
|
||||
fallback_required: bool = True
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["blocked_behaviors"] = sorted(self.blocked_behaviors)
|
||||
return data
|
||||
|
||||
|
||||
class SearchProfileResolver:
|
||||
"""Resolves base -> group -> user -> session search preferences."""
|
||||
|
||||
def base_profile(self) -> SearchProfile:
|
||||
return SearchProfile(profile_id="base", scope="global")
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
group_profile: dict[str, Any] | None = None,
|
||||
user_profile: dict[str, Any] | None = None,
|
||||
session_profile: dict[str, Any] | None = None,
|
||||
) -> SearchProfile:
|
||||
profile = self.base_profile().to_dict()
|
||||
for overlay in (group_profile, user_profile, session_profile):
|
||||
if overlay:
|
||||
profile = self._merge(profile, overlay)
|
||||
profile["blocked_behaviors"] = set(HARD_BLOCKED_BEHAVIORS)
|
||||
profile["audit_required"] = True
|
||||
profile["fallback_required"] = True
|
||||
return SearchProfile(**profile)
|
||||
|
||||
def _merge(self, base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
||||
merged = dict(base)
|
||||
if overlay.get("provider_order"):
|
||||
merged["provider_order"] = list(overlay["provider_order"])
|
||||
for list_key in ("trusted_domains", "query_expansion_hints", "domain_profiles"):
|
||||
merged[list_key] = self._dedupe(list(merged.get(list_key, [])) + list(overlay.get(list_key, [])))
|
||||
merged["profile_id"] = overlay.get("profile_id", merged.get("profile_id", "custom"))
|
||||
merged["scope"] = overlay.get("scope", merged.get("scope", "custom"))
|
||||
return merged
|
||||
|
||||
def _dedupe(self, items: list[str]) -> list[str]:
|
||||
return list(dict.fromkeys([item for item in items if item]))
|
||||
|
||||
|
||||
class DomainQueryProfileRegistry:
|
||||
"""Optional domain-specific expansions for the general Search Gateway."""
|
||||
|
||||
DEFAULT_PROFILES = {
|
||||
"education_exam_data": [
|
||||
"学校 年级 科目 试卷 答案 成绩统计",
|
||||
"联考 月考 期中 期末 质量检测",
|
||||
],
|
||||
"policy_regulation": [
|
||||
"官方 通知 政策 解读 实施细则",
|
||||
"site:gov.cn OR site:edu.cn",
|
||||
],
|
||||
"software_library": [
|
||||
"release notes changelog migration guide",
|
||||
"official documentation GitHub issues",
|
||||
],
|
||||
"product_research": [
|
||||
"specs review comparison price availability",
|
||||
"official page manual support",
|
||||
],
|
||||
"company_research": [
|
||||
"official announcement press release filings",
|
||||
"leadership product financial report",
|
||||
],
|
||||
}
|
||||
|
||||
def hints_for(self, domain_profiles: list[str]) -> list[str]:
|
||||
hints: list[str] = []
|
||||
for profile in domain_profiles:
|
||||
hints.extend(self.DEFAULT_PROFILES.get(profile, []))
|
||||
return list(dict.fromkeys(hints))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HARD_BLOCKED_BEHAVIORS",
|
||||
"SearchProfile",
|
||||
"SearchProfileResolver",
|
||||
"DomainQueryProfileRegistry",
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Provider registry for the general AIOS Search Gateway.
|
||||
|
||||
The registry makes provider availability explicit and auditable. Provider
|
||||
implementations are pluggable: production can register real public search,
|
||||
official-site, vertical-source, browser-fetch, authorized-file or archive
|
||||
providers without changing the Search Gateway core.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .search_gateway import SearchEvidence, SearchProviderAttempt, SearchRequest
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchProviderConfig:
|
||||
name: str
|
||||
provider_type: str
|
||||
enabled: bool = True
|
||||
requires_secret: bool = False
|
||||
supports_public_web: bool = True
|
||||
supports_authorized_sources: bool = False
|
||||
health: str = "unknown"
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class NullSearchProvider:
|
||||
"""Safe placeholder provider used until runtime wires a real adapter."""
|
||||
|
||||
def __init__(self, config: SearchProviderConfig):
|
||||
self.config = config
|
||||
self.name = config.name
|
||||
|
||||
def search(self, request: SearchRequest, queries: list[str]):
|
||||
return [], SearchProviderAttempt(
|
||||
provider=self.name,
|
||||
status="not_configured" if self.config.enabled else "disabled",
|
||||
result_count=0,
|
||||
reason="provider adapter not wired in runtime",
|
||||
)
|
||||
|
||||
|
||||
class StaticArchiveProvider:
|
||||
"""Small deterministic provider for tests and internal archived evidence."""
|
||||
|
||||
name = "internal_archive"
|
||||
|
||||
def __init__(self, records: list[dict[str, Any]] | None = None):
|
||||
self.records = records or []
|
||||
|
||||
def search(self, request: SearchRequest, queries: list[str]):
|
||||
evidence: list[SearchEvidence] = []
|
||||
for record in self.records:
|
||||
haystack = " ".join([record.get("title", ""), record.get("snippet", ""), record.get("tags", "")]).lower()
|
||||
if any(q.lower() in haystack for q in queries):
|
||||
evidence.append(
|
||||
SearchEvidence(
|
||||
title=record.get("title", "archive result"),
|
||||
url=record.get("url"),
|
||||
source=self.name,
|
||||
snippet=record.get("snippet", ""),
|
||||
evidence_type="internal_archive",
|
||||
confidence=record.get("confidence", "medium"),
|
||||
)
|
||||
)
|
||||
return evidence, SearchProviderAttempt(provider=self.name, status="ok", result_count=len(evidence))
|
||||
|
||||
|
||||
class SearchProviderRegistry:
|
||||
DEFAULT_PROVIDER_CONFIGS = [
|
||||
SearchProviderConfig("cache", "cache", supports_public_web=False),
|
||||
SearchProviderConfig("official_site", "official_site"),
|
||||
SearchProviderConfig("general_web", "general_web"),
|
||||
SearchProviderConfig("vertical_source", "vertical_source"),
|
||||
SearchProviderConfig("browser_fetch", "browser_fetch"),
|
||||
SearchProviderConfig("user_authorized_file", "authorized_file", supports_public_web=False, supports_authorized_sources=True),
|
||||
SearchProviderConfig("internal_archive", "archive", supports_public_web=False),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.configs: dict[str, SearchProviderConfig] = {}
|
||||
self.providers: dict[str, Any] = {}
|
||||
for config in self.DEFAULT_PROVIDER_CONFIGS:
|
||||
self.register_config(config)
|
||||
|
||||
def register_config(self, config: SearchProviderConfig) -> None:
|
||||
self.configs[config.name] = config
|
||||
self.providers.setdefault(config.name, NullSearchProvider(config))
|
||||
|
||||
def register_provider(self, name: str, provider: Any, config: SearchProviderConfig | None = None) -> None:
|
||||
if config:
|
||||
self.configs[name] = config
|
||||
elif name not in self.configs:
|
||||
self.configs[name] = SearchProviderConfig(name=name, provider_type="custom")
|
||||
self.providers[name] = provider
|
||||
|
||||
def ordered(self, order: list[str] | None = None) -> list[Any]:
|
||||
order = order or list(self.configs)
|
||||
result = [self.providers[name] for name in order if name in self.providers and self.configs[name].enabled]
|
||||
result.extend([provider for name, provider in self.providers.items() if name not in order and self.configs[name].enabled])
|
||||
return result
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"providers": [self.configs[name].to_dict() for name in sorted(self.configs)],
|
||||
"enabled": len([c for c in self.configs.values() if c.enabled]),
|
||||
"configured": len([name for name, provider in self.providers.items() if not isinstance(provider, NullSearchProvider)]),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SearchProviderConfig",
|
||||
"NullSearchProvider",
|
||||
"StaticArchiveProvider",
|
||||
"SearchProviderRegistry",
|
||||
]
|
||||
@@ -58,6 +58,8 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
||||
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
||||
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
|
||||
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
|
||||
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -90,6 +92,14 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
</div>
|
||||
<div id="panel-agents" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>Recent Agent Executions</h2></div>
|
||||
<div id="panel-arch-owner" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
|
||||
<div id="arch-owner-content">Loading...</div>
|
||||
</div>
|
||||
<div id="panel-arch-approve" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
|
||||
<div id="arch-approve-content">Loading...</div>
|
||||
</div>
|
||||
<div id="agents-list">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,3 +107,159 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
<script>
|
||||
async function fetchData() {
|
||||
const r = await fetch('/admin/ai-evolution/api/data');
|
||||
const data = await r.json();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('error').style.display = 'none';
|
||||
document.getElementById('content').style.display = '';
|
||||
|
||||
const hs = document.getElementById('header-status');
|
||||
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
|
||||
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
|
||||
|
||||
document.getElementById('base').textContent = data.baseline || '-';
|
||||
document.getElementById('branch').textContent = data.main_branch || 'main';
|
||||
document.getElementById('uptime').textContent = data.uptime || '-';
|
||||
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
const el = document.getElementById('versions-list');
|
||||
el.innerHTML = '<div class="loading">Loading versions...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/versions');
|
||||
const data = await r.json();
|
||||
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
|
||||
let html = '<div class="grid">';
|
||||
for (const v of data.versions || []) {
|
||||
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommands() {
|
||||
const el = document.getElementById('commands-list');
|
||||
el.innerHTML = '<div class="loading">Loading commands...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/data');
|
||||
const data = await r.json();
|
||||
let cmds = data.commands || [];
|
||||
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
|
||||
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
|
||||
html += '<div><div class="card"><div class="card-title">By Category</div>';
|
||||
const cats = {};
|
||||
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
|
||||
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
|
||||
html += '</div></div></div>';
|
||||
for (const cmd of cmds.slice().reverse()) {
|
||||
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
const el = document.getElementById('agents-list');
|
||||
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/agents');
|
||||
const data = await r.json();
|
||||
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
|
||||
let html = '';
|
||||
for (const agent of data.agents || []) {
|
||||
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
|
||||
}
|
||||
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function showPanel(name) {
|
||||
|
||||
async function loadOwnerGate() {
|
||||
const el = document.getElementById('arch-owner-content');
|
||||
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
||||
const data = await r.json();
|
||||
let html = '<div class="row">';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
|
||||
if (data.pending_requests && data.pending_requests.length) {
|
||||
for (const req of data.pending_requests) {
|
||||
html += '<div class="command-item">';
|
||||
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
|
||||
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
|
||||
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
} else {
|
||||
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApprove() {
|
||||
const el = document.getElementById('arch-approve-content');
|
||||
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
||||
const data = await r.json();
|
||||
let html = '';
|
||||
if (data.pending_requests && data.pending_requests.length) {
|
||||
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
|
||||
for (const req of data.pending_requests) {
|
||||
html += '<div class="command-item">';
|
||||
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
|
||||
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
|
||||
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
|
||||
html += '<div style="display:flex;gap:8px;">';
|
||||
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
|
||||
document.getElementById('panel-' + name).style.display = '';
|
||||
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
|
||||
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
|
||||
if (name === 'overview') {
|
||||
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
|
||||
fetchData();
|
||||
}
|
||||
const loaders = {
|
||||
'arch-owner': loadOwnerGate,
|
||||
'arch-approve': loadApprove,
|
||||
versions: loadVersions,
|
||||
commands: loadCommands,
|
||||
agents: loadAgents,
|
||||
};
|
||||
const fn = loaders[name];
|
||||
if (fn) fn();
|
||||
}
|
||||
|
||||
fetchData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Task Center action API model for Web UI buttons."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter
|
||||
except Exception:
|
||||
APIRouter = None
|
||||
|
||||
from .task_lifecycle import TaskLifecycle
|
||||
|
||||
|
||||
lifecycle = TaskLifecycle()
|
||||
|
||||
|
||||
def get_task_actions(task: dict[str, Any], role: str) -> dict[str, Any]:
|
||||
return {
|
||||
"task_id": task.get("task_id"),
|
||||
"actions": lifecycle.available_actions(task, role),
|
||||
}
|
||||
|
||||
|
||||
def execute_task_action(task: dict[str, Any], action: str, role: str, reason: str | None = None):
|
||||
return lifecycle.transition(task, action, role, reason)
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-actions"])
|
||||
|
||||
@router.post("/{task_id}/actions/{action}", operation_id="executeTaskAction")
|
||||
def action(task_id: str, action: str, payload: dict[str, Any]):
|
||||
return execute_task_action(payload.get("task", {}), action, payload.get("role", "user"), payload.get("reason"))
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = ["router", "get_task_actions", "execute_task_action"]
|
||||
@@ -0,0 +1,115 @@
|
||||
"""AIOS Task Center domain model.
|
||||
|
||||
The Task Center gives owners and authorized users a single Web UI page showing:
|
||||
who assigned a task, who is executing it, progress, elapsed time, remaining
|
||||
work, and whether completion notifications were sent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def parse_time(value: str | None) -> datetime | None:
|
||||
if not value:
|
||||
return None
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskNotificationStatus:
|
||||
required: bool = True
|
||||
sent: bool = False
|
||||
channel: str | None = None
|
||||
sent_at: str | None = None
|
||||
recipient_scope: str | None = None
|
||||
last_error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskProgress:
|
||||
percent: int = 0
|
||||
current_step: str = "pending"
|
||||
completed_steps: list[str] = field(default_factory=list)
|
||||
remaining_steps: list[str] = field(default_factory=list)
|
||||
blocked_reason: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AIOSTaskRecord:
|
||||
task_id: str
|
||||
title: str
|
||||
assigned_by: str
|
||||
executor: str
|
||||
source: str
|
||||
status: str = "pending"
|
||||
priority: str = "normal"
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
started_at: str | None = None
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
completed_at: str | None = None
|
||||
progress: TaskProgress = field(default_factory=TaskProgress)
|
||||
notification: TaskNotificationStatus = field(default_factory=TaskNotificationStatus)
|
||||
owner_gate_required: bool = False
|
||||
owner_gate_status: str | None = None
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
|
||||
def elapsed_seconds(self) -> int:
|
||||
start = parse_time(self.started_at) or parse_time(self.created_at)
|
||||
end = parse_time(self.completed_at) or datetime.now(timezone.utc)
|
||||
return int((end - start).total_seconds()) if start else 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["elapsed_seconds"] = self.elapsed_seconds()
|
||||
data["progress"] = self.progress.to_dict()
|
||||
data["notification"] = self.notification.to_dict()
|
||||
return data
|
||||
|
||||
|
||||
class TaskCenter:
|
||||
def summarize(self, tasks: list[AIOSTaskRecord] | list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
raw_tasks = tasks or []
|
||||
task_dicts = [t.to_dict() if hasattr(t, "to_dict") else dict(t) for t in raw_tasks]
|
||||
active = [t for t in task_dicts if t.get("status") in {"running", "pending", "blocked"}]
|
||||
blocked = [t for t in task_dicts if t.get("status") == "blocked"]
|
||||
completed = [t for t in task_dicts if t.get("status") == "completed"]
|
||||
notification_pending = [
|
||||
t for t in completed
|
||||
if t.get("notification", {}).get("required") and not t.get("notification", {}).get("sent")
|
||||
]
|
||||
return {
|
||||
"module": "task-center",
|
||||
"route": "/admin/ai-evolution/tasks",
|
||||
"title": "AIOS 任务中心",
|
||||
"status": "attention_required" if blocked or notification_pending else "healthy",
|
||||
"generated_at": utc_now(),
|
||||
"summary": {
|
||||
"total": len(task_dicts),
|
||||
"active": len(active),
|
||||
"blocked": len(blocked),
|
||||
"completed": len(completed),
|
||||
"notification_pending": len(notification_pending),
|
||||
},
|
||||
"tasks": sorted(task_dicts, key=lambda t: t.get("updated_at", ""), reverse=True),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TaskNotificationStatus",
|
||||
"TaskProgress",
|
||||
"AIOSTaskRecord",
|
||||
"TaskCenter",
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Task Center API."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter
|
||||
except Exception:
|
||||
APIRouter = None
|
||||
|
||||
from .task_center import TaskCenter
|
||||
|
||||
|
||||
task_center = TaskCenter()
|
||||
|
||||
|
||||
def get_task_overview(tasks: list[dict[str, Any]] | None = None):
|
||||
return task_center.summarize(tasks)
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-center"])
|
||||
|
||||
@router.get("/overview", operation_id="getTaskCenterOverview")
|
||||
def overview():
|
||||
return get_task_overview()
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = ["router", "get_task_overview"]
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Task Center visualization model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TaskDashboardView:
|
||||
def build(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
tasks = tasks or []
|
||||
return {
|
||||
"module": "task-center",
|
||||
"route": "/admin/ai-evolution/tasks",
|
||||
"title": "AIOS Task Center",
|
||||
"views": {
|
||||
"kanban": self._kanban(tasks),
|
||||
"timeline": self._timeline(tasks),
|
||||
"flow": self._flow(tasks),
|
||||
"statistics": self._statistics(tasks),
|
||||
},
|
||||
"tasks": [self._task_card(task) for task in tasks],
|
||||
}
|
||||
|
||||
def _task_card(self, task: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"task_id": task.get("task_id"),
|
||||
"title": task.get("title"),
|
||||
"assigned_by": task.get("assigned_by"),
|
||||
"executor": task.get("executor"),
|
||||
"status": task.get("status"),
|
||||
"progress": task.get("progress", {}),
|
||||
"elapsed_seconds": task.get("elapsed_seconds", 0),
|
||||
"remaining_steps": task.get("progress", {}).get("remaining_steps", []),
|
||||
"notification": task.get("notification", {}),
|
||||
"actions": [
|
||||
"start",
|
||||
"update_progress",
|
||||
"block",
|
||||
"request_approval",
|
||||
"approve",
|
||||
"reject",
|
||||
"retry",
|
||||
"complete",
|
||||
"notify",
|
||||
"close",
|
||||
],
|
||||
}
|
||||
|
||||
def _kanban(self, tasks):
|
||||
columns = {"pending": [], "running": [], "blocked": [], "waiting_approval": [], "completed": []}
|
||||
for task in tasks:
|
||||
columns.setdefault(task.get("status", "pending"), []).append(task.get("task_id"))
|
||||
return columns
|
||||
|
||||
def _timeline(self, tasks):
|
||||
return [
|
||||
{
|
||||
"task_id": task.get("task_id"),
|
||||
"updated_at": task.get("updated_at"),
|
||||
"status": task.get("status"),
|
||||
}
|
||||
for task in tasks
|
||||
]
|
||||
|
||||
def _flow(self, tasks):
|
||||
return {
|
||||
"nodes": ["created", "assigned", "running", "blocked", "approval", "completed", "closed"],
|
||||
"task_count": len(tasks),
|
||||
}
|
||||
|
||||
def _statistics(self, tasks):
|
||||
return {
|
||||
"total": len(tasks),
|
||||
"running": len([t for t in tasks if t.get("status") == "running"]),
|
||||
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
|
||||
"waiting_approval": len([t for t in tasks if t.get("status") == "waiting_approval"]),
|
||||
"completed": len([t for t in tasks if t.get("status") == "completed"]),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["TaskDashboardView"]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Task Center event bridge.
|
||||
|
||||
Converts task lifecycle actions and agent execution events into normalized AIOS
|
||||
Task Center events. Uses duck-typed Agent Event Bus publisher when available and
|
||||
an in-memory fallback for tests/offline runs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Protocol
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class TaskEventPublisher(Protocol):
|
||||
def publish(
|
||||
self,
|
||||
kind: str,
|
||||
source: str,
|
||||
target: str,
|
||||
summary: str,
|
||||
payload: dict[str, Any],
|
||||
correlation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskEvent:
|
||||
id: str
|
||||
kind: str
|
||||
source: str
|
||||
target: str
|
||||
summary: str
|
||||
payload: dict[str, Any]
|
||||
correlation_id: str | None = None
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class InMemoryTaskEventBus:
|
||||
def __init__(self):
|
||||
self.events: list[dict[str, Any]] = []
|
||||
|
||||
def publish(
|
||||
self,
|
||||
kind: str,
|
||||
source: str,
|
||||
target: str,
|
||||
summary: str,
|
||||
payload: dict[str, Any],
|
||||
correlation_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
event = TaskEvent(
|
||||
id=f"task_evt_{uuid4().hex[:12]}",
|
||||
kind=kind,
|
||||
source=source,
|
||||
target=target,
|
||||
summary=summary,
|
||||
payload=payload,
|
||||
correlation_id=correlation_id,
|
||||
).to_dict()
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
|
||||
class TaskEventBridge:
|
||||
def __init__(self, bus: TaskEventPublisher | None = None):
|
||||
self.bus = bus or InMemoryTaskEventBus()
|
||||
|
||||
def task_created(self, task: dict[str, Any]) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="task_created",
|
||||
source=task.get("assigned_by", "unknown"),
|
||||
target=task.get("executor", "task-center"),
|
||||
summary=task.get("title", "task created"),
|
||||
payload=task,
|
||||
correlation_id=task.get("task_id"),
|
||||
)
|
||||
|
||||
def task_updated(self, task: dict[str, Any], action: str) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="task_updated",
|
||||
source=task.get("executor", "task-center"),
|
||||
target="task-center",
|
||||
summary=f"task {action}: {task.get('status')}",
|
||||
payload={"action": action, "task": task},
|
||||
correlation_id=task.get("task_id"),
|
||||
)
|
||||
|
||||
def task_blocked(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="task_blocked",
|
||||
source=task.get("executor", "unknown"),
|
||||
target="owner-gate",
|
||||
summary=reason,
|
||||
payload={"task": task, "reason": reason},
|
||||
correlation_id=task.get("task_id"),
|
||||
)
|
||||
|
||||
def approval_requested(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="task_approval_requested",
|
||||
source=task.get("executor", "unknown"),
|
||||
target="owner-gate",
|
||||
summary=reason,
|
||||
payload={"task": task, "reason": reason},
|
||||
correlation_id=task.get("task_id"),
|
||||
)
|
||||
|
||||
def notification_sent(self, task: dict[str, Any], channel: str) -> dict[str, Any]:
|
||||
return self.bus.publish(
|
||||
kind="task_notification_sent",
|
||||
source="notification-agent",
|
||||
target=task.get("assigned_by", "owner"),
|
||||
summary=f"notification sent via {channel}",
|
||||
payload={"task": task, "channel": channel},
|
||||
correlation_id=task.get("task_id"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["TaskEvent", "InMemoryTaskEventBus", "TaskEventBridge"]
|
||||
@@ -0,0 +1,147 @@
|
||||
"""AIOS Task Lifecycle.
|
||||
|
||||
Defines a governed task state machine for task creation, assignment, execution,
|
||||
blocking, approval, retry, completion, notification and closure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
TERMINAL_STATES = {"completed", "cancelled", "failed", "closed"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskTransition:
|
||||
action: str
|
||||
from_states: set[str]
|
||||
to_state: str
|
||||
required_role: str = "agent"
|
||||
requires_reason: bool = False
|
||||
notify: bool = False
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = asdict(self)
|
||||
data["from_states"] = sorted(self.from_states)
|
||||
return data
|
||||
|
||||
|
||||
class TaskLifecycle:
|
||||
"""Central task state machine."""
|
||||
|
||||
TRANSITIONS = {
|
||||
"assign": TaskTransition("assign", {"new", "pending"}, "assigned", "dispatcher"),
|
||||
"start": TaskTransition("start", {"assigned", "pending"}, "running", "executor"),
|
||||
"update_progress": TaskTransition("update_progress", {"running"}, "running", "executor"),
|
||||
"block": TaskTransition("block", {"running", "assigned", "pending"}, "blocked", "executor", requires_reason=True, notify=True),
|
||||
"request_approval": TaskTransition("request_approval", {"running", "blocked"}, "waiting_approval", "executor", requires_reason=True, notify=True),
|
||||
"approve": TaskTransition("approve", {"waiting_approval"}, "approved", "owner", notify=True),
|
||||
"reject": TaskTransition("reject", {"waiting_approval"}, "rejected", "owner", requires_reason=True, notify=True),
|
||||
"resume": TaskTransition("resume", {"blocked", "approved"}, "running", "executor"),
|
||||
"retry": TaskTransition("retry", {"failed", "blocked"}, "running", "executor", notify=True),
|
||||
"complete": TaskTransition("complete", {"running", "approved"}, "completed", "executor", notify=True),
|
||||
"notify": TaskTransition("notify", {"completed"}, "completed", "notifier"),
|
||||
"close": TaskTransition("close", {"completed", "cancelled", "failed"}, "closed", "owner"),
|
||||
"cancel": TaskTransition("cancel", {"new", "pending", "assigned", "running", "blocked", "waiting_approval"}, "cancelled", "owner", requires_reason=True, notify=True),
|
||||
"fail": TaskTransition("fail", {"running", "assigned"}, "failed", "executor", requires_reason=True, notify=True),
|
||||
}
|
||||
|
||||
ROLE_ORDER = {
|
||||
"user": 1,
|
||||
"agent": 2,
|
||||
"executor": 3,
|
||||
"dispatcher": 4,
|
||||
"notifier": 4,
|
||||
"group_admin": 5,
|
||||
"owner": 9,
|
||||
}
|
||||
|
||||
def available_actions(self, task: dict[str, Any], role: str) -> list[dict[str, Any]]:
|
||||
state = task.get("status", "new")
|
||||
actions = []
|
||||
for name, transition in self.TRANSITIONS.items():
|
||||
allowed_state = state in transition.from_states
|
||||
allowed_role = self._role_allows(role, transition.required_role)
|
||||
actions.append({
|
||||
"action": name,
|
||||
"label": self._label(name),
|
||||
"enabled": allowed_state and allowed_role,
|
||||
"from_state": state,
|
||||
"to_state": transition.to_state,
|
||||
"required_role": transition.required_role,
|
||||
"requires_reason": transition.requires_reason,
|
||||
"notify": transition.notify,
|
||||
"disabled_reason": None if allowed_state and allowed_role else self._disabled_reason(allowed_state, allowed_role, transition),
|
||||
})
|
||||
return actions
|
||||
|
||||
def transition(self, task: dict[str, Any], action: str, actor_role: str, reason: str | None = None) -> dict[str, Any]:
|
||||
if action not in self.TRANSITIONS:
|
||||
raise ValueError(f"unknown task action: {action}")
|
||||
transition = self.TRANSITIONS[action]
|
||||
state = task.get("status", "new")
|
||||
if state not in transition.from_states:
|
||||
raise ValueError(f"action {action} not allowed from state {state}")
|
||||
if not self._role_allows(actor_role, transition.required_role):
|
||||
raise PermissionError(f"role {actor_role} cannot execute {action}; requires {transition.required_role}")
|
||||
if transition.requires_reason and not reason:
|
||||
raise ValueError(f"action {action} requires reason")
|
||||
updated = dict(task)
|
||||
updated["status"] = transition.to_state
|
||||
updated["updated_at"] = utc_now()
|
||||
history = list(updated.get("history", []))
|
||||
history.append({
|
||||
"action": action,
|
||||
"from": state,
|
||||
"to": transition.to_state,
|
||||
"actor_role": actor_role,
|
||||
"reason": reason,
|
||||
"ts": utc_now(),
|
||||
})
|
||||
updated["history"] = history
|
||||
if action == "complete":
|
||||
updated["completed_at"] = utc_now()
|
||||
notification = dict(updated.get("notification", {}))
|
||||
notification.setdefault("required", True)
|
||||
notification.setdefault("sent", False)
|
||||
updated["notification"] = notification
|
||||
return updated
|
||||
|
||||
def _role_allows(self, role: str, required: str) -> bool:
|
||||
return self.ROLE_ORDER.get(role, 0) >= self.ROLE_ORDER.get(required, 0)
|
||||
|
||||
def _label(self, action: str) -> str:
|
||||
labels = {
|
||||
"assign": "分配任务",
|
||||
"start": "开始执行",
|
||||
"update_progress": "更新进度",
|
||||
"block": "标记阻塞",
|
||||
"request_approval": "请求审批",
|
||||
"approve": "批准",
|
||||
"reject": "拒绝",
|
||||
"resume": "继续执行",
|
||||
"retry": "重试",
|
||||
"complete": "标记完成",
|
||||
"notify": "发送通知",
|
||||
"close": "关闭任务",
|
||||
"cancel": "取消任务",
|
||||
"fail": "标记失败",
|
||||
}
|
||||
return labels.get(action, action)
|
||||
|
||||
def _disabled_reason(self, allowed_state: bool, allowed_role: bool, transition: TaskTransition) -> str:
|
||||
if not allowed_state:
|
||||
return "当前状态不允许该操作"
|
||||
if not allowed_role:
|
||||
return f"需要角色: {transition.required_role}"
|
||||
return "不可用"
|
||||
|
||||
|
||||
__all__ = ["TaskTransition", "TaskLifecycle", "TERMINAL_STATES"]
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Task Center orchestrator.
|
||||
|
||||
Coordinates task ingestion, persistence, lifecycle updates, events and statistics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .task_event_bridge import TaskEventBridge
|
||||
from .task_source_ingestor import TaskSourceIngestor
|
||||
from .task_statistics import TaskStatistics
|
||||
from .task_store import TaskStore
|
||||
|
||||
|
||||
class TaskOrchestrator:
|
||||
def __init__(self):
|
||||
self.ingestor = TaskSourceIngestor()
|
||||
self.store = TaskStore()
|
||||
self.events = TaskEventBridge()
|
||||
self.statistics = TaskStatistics()
|
||||
|
||||
def create_from_command(self, path: str, bucket: str = "active") -> dict[str, Any]:
|
||||
task = self.ingestor.command_file_to_task(path, bucket)
|
||||
self.store.append(task)
|
||||
self.events.task_created(task)
|
||||
return task
|
||||
|
||||
def create_from_agent_execution(self, record: dict[str, Any]) -> dict[str, Any]:
|
||||
task = self.ingestor.agent_execution_to_task(record)
|
||||
self.store.append(task)
|
||||
self.events.task_created(task)
|
||||
return task
|
||||
|
||||
def sync_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
||||
task = self.ingestor.event_to_task(event)
|
||||
if task:
|
||||
self.store.append(task)
|
||||
self.events.task_updated(task, event.get("kind", "event"))
|
||||
return task
|
||||
|
||||
def dashboard(self) -> dict[str, Any]:
|
||||
tasks = self.store.latest()
|
||||
return {
|
||||
"tasks": tasks,
|
||||
"statistics": self.statistics.summarize(tasks),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["TaskOrchestrator"]
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Task source ingestion for AIOS Task Center.
|
||||
|
||||
Converts Command Watcher files, OpenClaw/Hermes/Codex execution records and
|
||||
Generic Agent Event Bus records into normalized Task Center task dictionaries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskSourceRecord:
|
||||
source_type: str
|
||||
source_id: str
|
||||
title: str
|
||||
assigned_by: str
|
||||
executor: str
|
||||
status: str
|
||||
priority: str = "normal"
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
evidence: list[str] = field(default_factory=list)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_task(self) -> dict[str, Any]:
|
||||
return {
|
||||
"task_id": f"task-{self.source_type}-{self.source_id}",
|
||||
"title": self.title,
|
||||
"assigned_by": self.assigned_by,
|
||||
"executor": self.executor,
|
||||
"source": self.source_type,
|
||||
"status": self.status,
|
||||
"priority": self.priority,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"progress": {
|
||||
"percent": self._progress_percent(),
|
||||
"current_step": self.status,
|
||||
"completed_steps": [],
|
||||
"remaining_steps": [],
|
||||
"blocked_reason": self.metadata.get("blocked_reason"),
|
||||
},
|
||||
"notification": {
|
||||
"required": True,
|
||||
"sent": bool(self.metadata.get("notification_sent")),
|
||||
"channel": self.metadata.get("notification_channel"),
|
||||
"sent_at": self.metadata.get("notification_sent_at"),
|
||||
},
|
||||
"evidence": self.evidence,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
def _progress_percent(self) -> int:
|
||||
mapping = {
|
||||
"new": 0,
|
||||
"pending": 5,
|
||||
"assigned": 15,
|
||||
"running": 50,
|
||||
"blocked": 50,
|
||||
"waiting_approval": 60,
|
||||
"approved": 70,
|
||||
"completed": 100,
|
||||
"closed": 100,
|
||||
"failed": 100,
|
||||
"cancelled": 100,
|
||||
}
|
||||
return mapping.get(self.status, 0)
|
||||
|
||||
|
||||
class TaskSourceIngestor:
|
||||
COMMAND_STATUS_MAP = {
|
||||
"active": "pending",
|
||||
"running": "running",
|
||||
"completed": "completed",
|
||||
"failed": "failed",
|
||||
"archive": "closed",
|
||||
}
|
||||
|
||||
def command_file_to_task(self, path: str, bucket: str = "active") -> dict[str, Any]:
|
||||
p = Path(path)
|
||||
title = p.stem
|
||||
return TaskSourceRecord(
|
||||
source_type="command_watcher",
|
||||
source_id=title,
|
||||
title=title,
|
||||
assigned_by="command-watcher",
|
||||
executor=self._infer_executor(title),
|
||||
status=self.COMMAND_STATUS_MAP.get(bucket, "pending"),
|
||||
priority="high" if "P0" in title or "fix" in title.lower() else "normal",
|
||||
evidence=[str(p)],
|
||||
metadata={"bucket": bucket},
|
||||
).to_task()
|
||||
|
||||
def agent_execution_to_task(self, record: dict[str, Any]) -> dict[str, Any]:
|
||||
source_id = record.get("execution_id", record.get("id", "unknown"))
|
||||
status = record.get("status", "running")
|
||||
return TaskSourceRecord(
|
||||
source_type="agent_execution",
|
||||
source_id=source_id,
|
||||
title=record.get("title", f"Agent execution {source_id}"),
|
||||
assigned_by=record.get("assigned_by", record.get("entry_agent", "agent-system")),
|
||||
executor=record.get("closed_by", record.get("entry_agent", "unknown-agent")),
|
||||
status=status,
|
||||
priority=record.get("priority", "normal"),
|
||||
created_at=record.get("started_at", utc_now()),
|
||||
updated_at=record.get("ended_at", utc_now()),
|
||||
evidence=[record.get("execution_id", source_id)],
|
||||
metadata=record,
|
||||
).to_task()
|
||||
|
||||
def event_to_task(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
||||
kind = event.get("kind")
|
||||
payload = event.get("payload", {})
|
||||
if kind not in {"task_created", "task_updated", "agent_started", "agent_completed", "agent_failed", "search_started", "search_completed"}:
|
||||
return None
|
||||
source_id = event.get("correlation_id") or event.get("id", "event")
|
||||
status = self._status_from_event(kind, payload)
|
||||
return TaskSourceRecord(
|
||||
source_type="event_bus",
|
||||
source_id=source_id,
|
||||
title=payload.get("title") or event.get("summary", kind),
|
||||
assigned_by=event.get("source", "event-bus"),
|
||||
executor=event.get("target", "task-center"),
|
||||
status=status,
|
||||
created_at=event.get("created_at", utc_now()),
|
||||
updated_at=event.get("created_at", utc_now()),
|
||||
evidence=[event.get("id", source_id)],
|
||||
metadata={"event_kind": kind, "payload": payload},
|
||||
).to_task()
|
||||
|
||||
def _status_from_event(self, kind: str, payload: dict[str, Any]) -> str:
|
||||
if kind.endswith("completed"):
|
||||
return "completed"
|
||||
if kind.endswith("failed"):
|
||||
return "failed"
|
||||
return payload.get("status", "running")
|
||||
|
||||
def _infer_executor(self, title: str) -> str:
|
||||
lower = title.lower()
|
||||
if "openclaw" in lower:
|
||||
return "openclaw"
|
||||
if "hermes" in lower:
|
||||
return "hermes"
|
||||
if "codex" in lower:
|
||||
return "codex"
|
||||
if "search" in lower:
|
||||
return "openclaw"
|
||||
return "openclaw"
|
||||
|
||||
|
||||
__all__ = ["TaskSourceRecord", "TaskSourceIngestor"]
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Task Center statistics."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TaskStatistics:
|
||||
def summarize(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
tasks = tasks or []
|
||||
completed = [t for t in tasks if t.get("status") == "completed"]
|
||||
blocked = [t for t in tasks if t.get("status") == "blocked"]
|
||||
waiting = [t for t in tasks if t.get("status") == "waiting_approval"]
|
||||
elapsed = [t.get("elapsed_seconds", 0) for t in tasks if t.get("elapsed_seconds") is not None]
|
||||
return {
|
||||
"total": len(tasks),
|
||||
"completed": len(completed),
|
||||
"blocked": len(blocked),
|
||||
"waiting_approval": len(waiting),
|
||||
"completion_rate": round(len(completed) / len(tasks) * 100, 2) if tasks else 0,
|
||||
"average_elapsed_seconds": round(sum(elapsed) / len(elapsed), 2) if elapsed else 0,
|
||||
"top_blockers": self._top_values(blocked, "blocker"),
|
||||
"executor_distribution": self._top_values(tasks, "executor"),
|
||||
}
|
||||
|
||||
def _top_values(self, tasks: list[dict[str, Any]], key: str):
|
||||
result: dict[str, int] = {}
|
||||
for task in tasks:
|
||||
value = task.get(key)
|
||||
if value:
|
||||
result[value] = result.get(value, 0) + 1
|
||||
return result
|
||||
|
||||
|
||||
__all__ = ["TaskStatistics"]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Persistent Task Center store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TaskStore:
|
||||
def __init__(self, root: str = "runtime/task-center"):
|
||||
self.root = Path(root)
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self.path = self.root / "tasks.jsonl"
|
||||
|
||||
def append(self, task: dict[str, Any]) -> None:
|
||||
with self.path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(task, ensure_ascii=False) + "\n")
|
||||
|
||||
def list_all(self) -> list[dict[str, Any]]:
|
||||
if not self.path.exists():
|
||||
return []
|
||||
return [json.loads(line) for line in self.path.read_text(encoding="utf-8").splitlines() if line]
|
||||
|
||||
def latest(self) -> list[dict[str, Any]]:
|
||||
return self.list_all()[-100:]
|
||||
|
||||
|
||||
__all__ = ["TaskStore"]
|
||||
@@ -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,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
|
||||
@@ -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,92 @@
|
||||
search_gateway:
|
||||
enabled: true
|
||||
scope: general_purpose_network_search
|
||||
principle: shared_base_capability_with_group_user_session_customization
|
||||
|
||||
customization:
|
||||
enabled: true
|
||||
precedence:
|
||||
- base
|
||||
- group_profile
|
||||
- user_profile
|
||||
- session_profile
|
||||
customizable:
|
||||
- provider_order
|
||||
- trusted_domains
|
||||
- query_expansion_hints
|
||||
- domain_profiles
|
||||
- preferred_language
|
||||
- preferred_region
|
||||
non_overridable:
|
||||
- audit_required
|
||||
- fallback_required
|
||||
- authorization_required
|
||||
- blocked_behaviors
|
||||
- privacy_boundaries
|
||||
|
||||
domain_profiles:
|
||||
supported_defaults:
|
||||
- general
|
||||
- education_exam_data
|
||||
- policy_regulation
|
||||
- software_library
|
||||
- product_research
|
||||
- company_research
|
||||
extensible_by_group: true
|
||||
extensible_by_user: true
|
||||
extension_requires_audit: true
|
||||
|
||||
providers:
|
||||
routing:
|
||||
- cache
|
||||
- official_site
|
||||
- general_web
|
||||
- vertical_source
|
||||
- browser_fetch
|
||||
- user_authorized_file
|
||||
- internal_archive
|
||||
failure_policy:
|
||||
single_provider_failure_is_not_terminal: true
|
||||
require_fallback_attempts: true
|
||||
record_provider_failures: true
|
||||
|
||||
allowed_sources:
|
||||
- public_web_pages
|
||||
- official_announcements
|
||||
- public_documents
|
||||
- public_exam_papers
|
||||
- user_authorized_files
|
||||
- user_authorized_links
|
||||
- internal_archives
|
||||
|
||||
blocked_behaviors:
|
||||
- bypass_login
|
||||
- bypass_captcha
|
||||
- scrape_private_student_grades
|
||||
- scrape_student_private_data
|
||||
- bulk_scrape_protected_platforms
|
||||
- collect_data_without_authorization
|
||||
|
||||
evidence:
|
||||
require_source_record: true
|
||||
require_confidence: true
|
||||
require_attempt_log: true
|
||||
require_clarification_when_insufficient: true
|
||||
|
||||
audit:
|
||||
enabled: true
|
||||
record:
|
||||
- query
|
||||
- requester
|
||||
- resolved_profile
|
||||
- expanded_queries
|
||||
- providers_attempted
|
||||
- provider_failures
|
||||
- evidence_selected
|
||||
- blocked_reasons
|
||||
- final_confidence
|
||||
|
||||
user_experience:
|
||||
do_not_answer_provider_unavailable_as_final_failure: true
|
||||
explain_public_vs_private_data_boundary: true
|
||||
ask_for_subject_platform_file_or_link_when_needed: true
|
||||
@@ -0,0 +1,47 @@
|
||||
task_center:
|
||||
enabled: true
|
||||
purpose: autonomous_operation_management
|
||||
|
||||
lifecycle:
|
||||
states:
|
||||
- new
|
||||
- pending
|
||||
- assigned
|
||||
- running
|
||||
- blocked
|
||||
- waiting_approval
|
||||
- approved
|
||||
- completed
|
||||
- failed
|
||||
- closed
|
||||
|
||||
sources:
|
||||
- command_watcher
|
||||
- openclaw
|
||||
- hermes
|
||||
- codex
|
||||
- agent_event_bus
|
||||
|
||||
required_tracking:
|
||||
- creator
|
||||
- assigned_by
|
||||
- executor
|
||||
- progress
|
||||
- elapsed_time
|
||||
- remaining_work
|
||||
- blockers
|
||||
- approvals
|
||||
- notifications
|
||||
|
||||
governance:
|
||||
require_audit: true
|
||||
require_rbac_actions: true
|
||||
require_notification_trace: true
|
||||
|
||||
statistics:
|
||||
- completion_rate
|
||||
- average_duration
|
||||
- blocked_count
|
||||
- approval_wait_time
|
||||
- executor_distribution
|
||||
- notification_rate
|
||||
@@ -0,0 +1,116 @@
|
||||
# V1R4C4 Reliability Freeze Before SR-04
|
||||
|
||||
Status: accepted
|
||||
|
||||
Decision date: 2026-07-10
|
||||
|
||||
Current baseline: V1R4C3
|
||||
|
||||
## Decision
|
||||
|
||||
V1R4C4 will complete reliability freeze before merging Portal AIOS Integration.
|
||||
|
||||
SR-04 remains `activation-ready`, but it must not be merged or deployed before SR-01 and SR-02 are complete and accepted.
|
||||
|
||||
## Scope Ordering
|
||||
|
||||
```text
|
||||
SR-01 Gitea Branch Sync Fix
|
||||
↓
|
||||
SR-02 Watcher State Cleanup
|
||||
↓
|
||||
SR-04 Portal AIOS Integration Merge
|
||||
```
|
||||
|
||||
## SR-01 Gate
|
||||
|
||||
SR-01 must complete the P0 reliability work for Gitea branch sync and metadata drift.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
1. branch_sync remains independent of Gitea branch API pagination.
|
||||
2. bare repository refs, Gitea DB branch metadata, and Gitea API visible state are checked for consistency.
|
||||
3. branch metadata drift triggers autonomous safe mode.
|
||||
4. recovery procedure favors minimal repair over default restart.
|
||||
5. runtime acceptance passes.
|
||||
|
||||
Acceptance sequence:
|
||||
|
||||
```text
|
||||
getRuntimeStatus
|
||||
->
|
||||
getBaseline
|
||||
->
|
||||
createBranch
|
||||
->
|
||||
createPatch
|
||||
```
|
||||
|
||||
## SR-02 Gate
|
||||
|
||||
SR-02 must complete watcher state cleanup integration.
|
||||
|
||||
Current known test signal:
|
||||
|
||||
```text
|
||||
11 / 11 tests pass
|
||||
```
|
||||
|
||||
Required outcomes:
|
||||
|
||||
1. stuck command detection reads parsed timestamps correctly.
|
||||
2. `.json` command state files are handled correctly.
|
||||
3. `.md` command state files are handled correctly.
|
||||
4. file mtime semantics are aligned with stored stuck timestamps in tests.
|
||||
5. watcher cleanup is integrated behind the reliability assumptions introduced by SR-01.
|
||||
|
||||
## SR-04 Status
|
||||
|
||||
SR-04 remains:
|
||||
|
||||
```text
|
||||
activation-ready
|
||||
```
|
||||
|
||||
But SR-04 is explicitly blocked from merge/deploy until SR-01 and SR-02 gates are green.
|
||||
|
||||
SR-04 may continue with planning, diff review, and acceptance design, but may not perform production portal deployment.
|
||||
|
||||
## Reason
|
||||
|
||||
The user-visible portal feature gap is real, but the system recently experienced a stop-the-line reliability incident involving Gitea branch metadata drift. Reliability freeze must complete first to avoid combining production portal changes with unresolved GitOps/Gitea consistency risk.
|
||||
|
||||
## Allowed During Freeze
|
||||
|
||||
Allowed:
|
||||
|
||||
- SR-04 planning;
|
||||
- SR-04 diff review;
|
||||
- frontend/backend contract review;
|
||||
- RBAC acceptance design;
|
||||
- non-production validation;
|
||||
- documentation updates.
|
||||
|
||||
Not allowed before SR-01/SR-02 acceptance:
|
||||
|
||||
- merging SR-04 to main;
|
||||
- deploying SR-04 to production;
|
||||
- bypassing RBAC to expose modules;
|
||||
- combining portal integration with Gitea DB repair;
|
||||
- bulk merging old portal branches.
|
||||
|
||||
## SR-04 Release Gate
|
||||
|
||||
SR-04 may enter merge/deploy only after:
|
||||
|
||||
1. SR-01 acceptance is green.
|
||||
2. SR-02 acceptance is green.
|
||||
3. GitOps runtime acceptance is green.
|
||||
4. Gitea consistency sentinel is deployed or explicitly deferred with owner approval.
|
||||
5. Portal RBAC acceptance plan is approved.
|
||||
|
||||
## Final Rule
|
||||
|
||||
V1R4C4 is reliability-first.
|
||||
|
||||
Portal AIOS Integration remains prepared but gated until reliability freeze is complete.
|
||||
+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,76 @@
|
||||
# CMD-20260709-complete-search-gateway-runtime-loop
|
||||
|
||||
## Goal
|
||||
|
||||
Complete deployment of AIOS General Search Gateway runtime loop.
|
||||
|
||||
## Components
|
||||
|
||||
Deploy:
|
||||
|
||||
- Search Gateway core
|
||||
- Profile resolver
|
||||
- Provider registry
|
||||
- Audit store
|
||||
- Event bridge
|
||||
- Runtime orchestrator
|
||||
- Search dashboard
|
||||
|
||||
## Runtime Integration
|
||||
|
||||
Connect:
|
||||
|
||||
- Custom GPT bridge
|
||||
- OpenClaw
|
||||
- Hermes
|
||||
- Codex
|
||||
- Agent Event Bus
|
||||
- Audit Plane
|
||||
|
||||
## Validation
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pytest tests/test_search_gateway_general_capability.py \
|
||||
tests/test_search_provider_registry.py \
|
||||
tests/test_search_gateway_orchestrator.py
|
||||
```
|
||||
|
||||
Required signals:
|
||||
|
||||
- AIOS_GENERAL_SEARCH_GATEWAY_READY
|
||||
- AIOS_SEARCH_PROFILE_CUSTOMIZATION_READY
|
||||
- AIOS_SEARCH_PROVIDER_REGISTRY_READY
|
||||
- AIOS_SEARCH_AUDIT_READY
|
||||
- AIOS_SEARCH_EVENT_LOOP_READY
|
||||
- AIOS_SEARCH_RUNTIME_LOOP_READY
|
||||
|
||||
## Web UI
|
||||
|
||||
Enable:
|
||||
|
||||
- /admin/ai-evolution/search
|
||||
|
||||
Show:
|
||||
|
||||
- provider health
|
||||
- fallback count
|
||||
- evidence quality
|
||||
- search audit
|
||||
- group/user profile customization
|
||||
|
||||
## Notification
|
||||
|
||||
After deployment send WeChat report containing:
|
||||
|
||||
- branch
|
||||
- commit SHA
|
||||
- test results
|
||||
- runtime status
|
||||
- enabled providers
|
||||
- success signals
|
||||
|
||||
## Governance
|
||||
|
||||
Production provider credentials, paid APIs and private data integrations require owner approval.
|
||||
@@ -0,0 +1,74 @@
|
||||
# CMD-20260709-complete-task-center-closed-loop
|
||||
|
||||
## Goal
|
||||
|
||||
Complete AIOS Task Center autonomous closed loop.
|
||||
|
||||
## Deploy
|
||||
|
||||
Components:
|
||||
|
||||
- Task lifecycle state machine
|
||||
- Task Event Bridge
|
||||
- Task Store
|
||||
- Task Statistics
|
||||
- Task Action API
|
||||
- Task Web UI
|
||||
|
||||
## Integrations
|
||||
|
||||
Connect:
|
||||
|
||||
- Command Watcher: every CMD becomes a task
|
||||
- OpenClaw: execution updates task automatically
|
||||
- Hermes: delegation updates task automatically
|
||||
- Codex: development tasks update task automatically
|
||||
- Audit Plane: stores evidence
|
||||
- Notification Agent: completion/blocking notifications
|
||||
|
||||
## Required UI actions
|
||||
|
||||
Every task page must expose RBAC controlled buttons:
|
||||
|
||||
- assign
|
||||
- start
|
||||
- update progress
|
||||
- block
|
||||
- request approval
|
||||
- approve
|
||||
- reject
|
||||
- resume
|
||||
- retry
|
||||
- complete
|
||||
- notify
|
||||
- close
|
||||
|
||||
## Statistics
|
||||
|
||||
Display:
|
||||
|
||||
- total tasks
|
||||
- completion rate
|
||||
- average execution time
|
||||
- blocked tasks
|
||||
- approval waiting time
|
||||
- executor distribution
|
||||
- blocker ranking
|
||||
- notification completion rate
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_TASK_LIFECYCLE_READY
|
||||
- AIOS_TASK_EVENT_LOOP_READY
|
||||
- AIOS_TASK_STATISTICS_READY
|
||||
- AIOS_TASK_CLOSURE_LOOP_READY
|
||||
- AIOS_TASK_WEB_UI_ACTIONS_READY
|
||||
|
||||
## Notification
|
||||
|
||||
After deployment send WeChat summary:
|
||||
|
||||
- deployment result
|
||||
- task center URL
|
||||
- tests
|
||||
- success signals
|
||||
@@ -0,0 +1,238 @@
|
||||
# CMD-20260709-dispatch-search-gateway-production-deployment
|
||||
|
||||
## Priority
|
||||
|
||||
P0
|
||||
|
||||
## Goal
|
||||
|
||||
Deploy AIOS General Search Gateway runtime loop and report completion to Owner via WeChat.
|
||||
|
||||
## Branch
|
||||
|
||||
```text
|
||||
patch/search-gateway-governed-general-capability
|
||||
```
|
||||
|
||||
## Preconditions
|
||||
|
||||
Existing implementation files are already committed on the branch:
|
||||
|
||||
- active-runtime/app/search_gateway.py
|
||||
- active-runtime/app/search_profile_resolver.py
|
||||
- active-runtime/app/search_provider_registry.py
|
||||
- active-runtime/app/search_audit_store.py
|
||||
- active-runtime/app/search_event_bridge.py
|
||||
- active-runtime/app/search_gateway_orchestrator.py
|
||||
- active-runtime/app/search_agent_adapter.py
|
||||
- active-runtime/app/search_gateway_api.py
|
||||
- active-runtime/app/search_gateway_dashboard.py
|
||||
- active-runtime/app/search_architect_integration.py
|
||||
- config/search-gateway-policy.yaml
|
||||
- tests/test_search_gateway_general_capability.py
|
||||
- tests/test_search_provider_registry.py
|
||||
- tests/test_search_gateway_orchestrator.py
|
||||
- tests/test_search_agent_adapter.py
|
||||
- tests/test_search_architect_integration.py
|
||||
|
||||
## Execute
|
||||
|
||||
### 1. Fix documentation path if needed
|
||||
|
||||
Run first if docs/architecture is not writable:
|
||||
|
||||
```text
|
||||
ops/openclaw/CMD-20260709-search-gateway-document-permission-fix.md
|
||||
```
|
||||
|
||||
### 2. Run test suite
|
||||
|
||||
```bash
|
||||
cd /data/aios-gitops/workspace
|
||||
|
||||
git fetch origin
|
||||
git checkout patch/search-gateway-governed-general-capability
|
||||
git pull origin patch/search-gateway-governed-general-capability
|
||||
|
||||
pytest tests/test_search_gateway_general_capability.py \
|
||||
tests/test_search_provider_registry.py \
|
||||
tests/test_search_gateway_orchestrator.py \
|
||||
tests/test_search_agent_adapter.py \
|
||||
tests/test_search_architect_integration.py
|
||||
```
|
||||
|
||||
### 3. Register runtime modules/routes
|
||||
|
||||
Register Search Gateway APIs:
|
||||
|
||||
- GET /admin/ai-evolution/search/status
|
||||
- GET /admin/ai-evolution/search/overview
|
||||
- POST /admin/ai-evolution/search/query
|
||||
|
||||
Operation IDs:
|
||||
|
||||
- getSearchGatewayStatus
|
||||
- getSearchGatewayOverview
|
||||
- runSearchGatewayQuery
|
||||
|
||||
### 4. Wire agent clients
|
||||
|
||||
Route all runtime search calls through Search Gateway adapters:
|
||||
|
||||
- OpenClaw -> OpenClawSearchClient
|
||||
- Hermes -> HermesSearchClient
|
||||
- Codex -> CodexSearchClient
|
||||
- Custom GPT bridge -> CustomGPTSearchClient
|
||||
|
||||
No agent should directly perform ad-hoc web search after this deployment.
|
||||
|
||||
### 5. Configure providers
|
||||
|
||||
Enable provider registry entries:
|
||||
|
||||
- cache
|
||||
- official_site
|
||||
- general_web
|
||||
- vertical_source
|
||||
- browser_fetch
|
||||
- user_authorized_file
|
||||
- internal_archive
|
||||
|
||||
Production credentials, paid APIs, private platform access, or sensitive data integrations require Owner approval before enabling.
|
||||
|
||||
Public web/official-site/internal-archive providers may be enabled immediately if already available in runtime.
|
||||
|
||||
### 6. Enable governance layers
|
||||
|
||||
Enable:
|
||||
|
||||
- profile resolver
|
||||
- group/user/session customization
|
||||
- audit store
|
||||
- event bridge
|
||||
- provider fallback
|
||||
- Search Gateway Web UI
|
||||
- Architect search governance integration
|
||||
|
||||
### 7. Web UI
|
||||
|
||||
Deploy:
|
||||
|
||||
```text
|
||||
/admin/ai-evolution/search
|
||||
```
|
||||
|
||||
Show:
|
||||
|
||||
- provider health
|
||||
- configured provider count
|
||||
- fallback status
|
||||
- recent search audit
|
||||
- blocked search count
|
||||
- profile customization state
|
||||
- evidence quality
|
||||
|
||||
### 8. Acceptance smoke tests
|
||||
|
||||
Run these live requests through Search Gateway API:
|
||||
|
||||
#### Public general search
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "latest Python package release notes",
|
||||
"requester": "openclaw",
|
||||
"intent": "software_library",
|
||||
"user_scope": "public"
|
||||
}
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- status is found or not_found
|
||||
- provider attempts recorded
|
||||
- no direct provider failure as final answer without fallback attempt
|
||||
|
||||
#### Group/user customization
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "AI agent observability",
|
||||
"requester": "hermes",
|
||||
"intent": "software_library",
|
||||
"group_profile": {
|
||||
"profile_id": "group-default",
|
||||
"scope": "group",
|
||||
"query_expansion_hints": ["GitHub", "release notes"]
|
||||
},
|
||||
"user_scope": "public"
|
||||
}
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- resolved_profile includes group overlay
|
||||
- audit is recorded
|
||||
|
||||
#### Guardrail block
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "绕过验证码 抓取个人成绩",
|
||||
"requester": "openclaw",
|
||||
"user_scope": "public"
|
||||
}
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- status = blocked
|
||||
- blocked_reasons populated
|
||||
- audit/event recorded
|
||||
|
||||
## Success Signals
|
||||
|
||||
Return all of these if deployment succeeds:
|
||||
|
||||
```text
|
||||
AIOS_GENERAL_SEARCH_GATEWAY_READY
|
||||
AIOS_SEARCH_PROFILE_CUSTOMIZATION_READY
|
||||
AIOS_SEARCH_PROVIDER_REGISTRY_READY
|
||||
AIOS_SEARCH_PROVIDER_FALLBACK_READY
|
||||
AIOS_SEARCH_AUDIT_READY
|
||||
AIOS_SEARCH_EVENT_LOOP_READY
|
||||
AIOS_SEARCH_RUNTIME_LOOP_READY
|
||||
AIOS_SEARCH_WEB_UI_READY
|
||||
AIOS_SEARCH_AGENT_ADAPTERS_READY
|
||||
AIOS_SEARCH_ARCHITECT_GOVERNANCE_READY
|
||||
AIOS_SEARCH_WECHAT_NOTIFICATION_SENT
|
||||
```
|
||||
|
||||
## WeChat report to Owner
|
||||
|
||||
After deployment, send WeChat report containing:
|
||||
|
||||
- branch
|
||||
- latest commit SHA
|
||||
- test results
|
||||
- provider status
|
||||
- live smoke test results
|
||||
- Web UI URL
|
||||
- success signals
|
||||
- remaining limitations, if any
|
||||
|
||||
## Output back to ChatGPT
|
||||
|
||||
Return:
|
||||
|
||||
- command status
|
||||
- commit SHA deployed
|
||||
- tests passed/failed
|
||||
- enabled providers
|
||||
- API routes verified
|
||||
- Web UI route verified
|
||||
- WeChat notification result
|
||||
|
||||
## Governance
|
||||
|
||||
This command deploys the governed shared Search Gateway. It does not authorize bypassing login, CAPTCHA, protected platforms, or private data boundaries. Any provider requiring secrets, payment, private platform access, or sensitive data requires Owner approval before production enablement.
|
||||
@@ -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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user