Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915d88cc3f | ||
|
|
af424fffac | ||
|
|
38055cda7b | ||
|
|
fa14a7f415 | ||
|
|
ec6559994c | ||
|
|
f29e826fd8 | ||
|
|
196a8a3363 | ||
|
|
2e1ea983c3 | ||
|
|
01ca67e830 | ||
|
|
84bf69f82f | ||
|
|
2baacee0b3 | ||
|
|
7a7f76c1db | ||
|
|
44185a9eeb | ||
|
|
f76bf49d58 | ||
|
|
ed8967af7d | ||
|
|
8c0f2a8512 | ||
|
|
f6d9bb1510 | ||
|
|
8ca4db3e8c | ||
|
|
d194d3a4d7 | ||
|
|
f5435e3087 | ||
|
|
88087dc7ff | ||
|
|
2f04b9f46c | ||
|
|
d8a426cea1 | ||
|
|
22d48bad4d | ||
|
|
99c50464d9 | ||
|
|
d0141450fe | ||
|
|
84c67fa743 | ||
|
|
d0536dbe14 | ||
|
|
4177a22936 | ||
|
|
5c38b23810 | ||
|
|
aa0f721ce8 | ||
|
|
2e94439f49 | ||
|
|
961c45c825 | ||
|
|
20f1f9820d | ||
|
|
a6f08472dd | ||
|
|
9f95029225 | ||
|
|
4ad4651ffa | ||
|
|
c0d008fae7 | ||
|
|
1974327e32 | ||
|
|
110817b60c | ||
|
|
95070ea029 | ||
|
|
44aea3a9a1 | ||
|
|
ce42691be5 | ||
|
|
6f823de67d | ||
|
|
7443fc7f26 | ||
|
|
e1d484655f | ||
|
|
362a41bff8 | ||
|
|
62974e879e | ||
|
|
cd713cd73a | ||
|
|
bd8afc3cc4 | ||
|
|
1688639d7d | ||
|
|
60d07f9b17 | ||
|
|
824ff5dc90 | ||
|
|
3d27f2eb43 | ||
|
|
a168610826 | ||
|
|
5447476b08 | ||
|
|
e58d99ba57 | ||
|
|
51d6f7e0c2 | ||
|
|
40459af09e | ||
|
|
c7dc61597a | ||
|
|
43d55ac3b1 | ||
|
|
736e981005 | ||
|
|
b84d6be3ca | ||
|
|
eff4940e9c | ||
|
|
0d6f2ce68d | ||
|
|
a654978813 | ||
|
|
a399e30a8e | ||
|
|
e0644c0297 | ||
|
|
37eed9df7d | ||
|
|
22582b834f | ||
|
|
8a0c5f0556 | ||
|
|
62b10d89c2 |
@@ -0,0 +1,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"]
|
||||
@@ -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,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,77 @@
|
||||
# User Data Isolation Activation Gate
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the final conditions required before enabling memory search or cross-agent memory recall.
|
||||
|
||||
## Current State
|
||||
|
||||
```text
|
||||
memory search: disabled
|
||||
isolation gate: enforced
|
||||
```
|
||||
|
||||
## Activation Checklist
|
||||
|
||||
Memory search may be enabled only when all checks pass.
|
||||
|
||||
### Storage isolation
|
||||
|
||||
- [ ] Records are stored under scoped namespaces.
|
||||
- [ ] No global shared memory path is used for production search.
|
||||
- [ ] Every record has required scope metadata.
|
||||
|
||||
### Read isolation
|
||||
|
||||
- [ ] Scope envelope is attached to every search request.
|
||||
- [ ] Provider filters before ranking.
|
||||
- [ ] Out-of-scope records never reach summarization.
|
||||
- [ ] Provenance is returned for every accepted record.
|
||||
|
||||
### Audit
|
||||
|
||||
- [ ] Isolation audit reports `healthy`.
|
||||
- [ ] Denied records count is zero.
|
||||
- [ ] Invalid records count is zero.
|
||||
- [ ] Safe-mode path is tested.
|
||||
|
||||
### Provider
|
||||
|
||||
Required contract:
|
||||
|
||||
```json
|
||||
{
|
||||
"scope_filter_before_ranking": true,
|
||||
"provenance_required": true,
|
||||
"allow_global_index": false
|
||||
}
|
||||
```
|
||||
|
||||
### Owner Gate
|
||||
|
||||
Activation requires explicit approval because memory/search changes the data exposure boundary.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
If any check fails:
|
||||
|
||||
```text
|
||||
memory remains disabled
|
||||
+
|
||||
audit event generated
|
||||
+
|
||||
safe-mode available for autonomous operations
|
||||
```
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This gate does not:
|
||||
|
||||
- grant RBAC permissions;
|
||||
- replace application authorization;
|
||||
- allow global recall for convenience;
|
||||
- bypass tenant/user/project boundaries.
|
||||
|
||||
## Release Rule
|
||||
|
||||
User-data isolation is a release blocker, not an optimization item.
|
||||
@@ -0,0 +1,112 @@
|
||||
# User Data Isolation Deployment Runbook
|
||||
|
||||
## Goal
|
||||
|
||||
Deploy user-data isolation controls before enabling memory search or cross-agent recall.
|
||||
|
||||
## Current Safety State
|
||||
|
||||
Default:
|
||||
|
||||
```text
|
||||
AIOS_MEMORY_ENABLED=false
|
||||
```
|
||||
|
||||
Memory search remains disabled until isolation acceptance passes.
|
||||
|
||||
## Deployment Components
|
||||
|
||||
1. Memory scope guard
|
||||
|
||||
```text
|
||||
scripts/security/memory_scope_guard.py
|
||||
```
|
||||
|
||||
2. Provider gate
|
||||
|
||||
```text
|
||||
scripts/security/memory_provider_gate.py
|
||||
```
|
||||
|
||||
3. Isolation audit service
|
||||
|
||||
```text
|
||||
user-data-isolation-audit.service
|
||||
user-data-isolation-audit.timer
|
||||
```
|
||||
|
||||
4. Activation gate
|
||||
|
||||
```text
|
||||
scripts/security/memory_search_activation_check.py
|
||||
```
|
||||
|
||||
## Deployment Order
|
||||
|
||||
```text
|
||||
Install audit
|
||||
↓
|
||||
Run isolation scan
|
||||
↓
|
||||
Fix unscoped records
|
||||
↓
|
||||
Enable provider scope contract
|
||||
↓
|
||||
Run acceptance suite
|
||||
↓
|
||||
Evaluate activation gate
|
||||
↓
|
||||
Only then enable memory search
|
||||
```
|
||||
|
||||
## Acceptance Requirements
|
||||
|
||||
Before enabling memory search:
|
||||
|
||||
### Isolation
|
||||
|
||||
- no untagged records;
|
||||
- no cross-user records;
|
||||
- no cross-project records;
|
||||
- no cross-tenant records;
|
||||
- no global shared memory paths;
|
||||
- all records have provenance.
|
||||
|
||||
### Provider
|
||||
|
||||
Required:
|
||||
|
||||
```json
|
||||
{
|
||||
"scope_filter_before_ranking": true,
|
||||
"provenance_required": true,
|
||||
"allow_global_index": false
|
||||
}
|
||||
```
|
||||
|
||||
### Activation Gate
|
||||
|
||||
The activation check must return:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "allowed"
|
||||
}
|
||||
```
|
||||
|
||||
Anything else keeps memory disabled.
|
||||
|
||||
## Incident Response
|
||||
|
||||
If contamination is detected:
|
||||
|
||||
1. Disable memory/search provider.
|
||||
2. Enter autonomous safe mode if operational state may be affected.
|
||||
3. Preserve audit evidence.
|
||||
4. Identify scope violation source.
|
||||
5. Migrate records into scoped namespaces.
|
||||
6. Rerun acceptance before reactivation.
|
||||
|
||||
## Redline
|
||||
|
||||
Convenience, recall quality, and search coverage never override user-data isolation.
|
||||
+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,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
python3 -m pytest \
|
||||
tests/security/test_memory_scope_guard.py \
|
||||
tests/security/test_memory_provider_gate.py
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
cat > "$TMP_DIR/report.json" <<'JSON'
|
||||
{
|
||||
"status": "healthy",
|
||||
"counts": {
|
||||
"denied": 0,
|
||||
"invalid": 0
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$TMP_DIR/provider.json" <<'JSON'
|
||||
{
|
||||
"name": "ci-provider",
|
||||
"scope_filter_before_ranking": true,
|
||||
"provenance_required": true,
|
||||
"fallback_mode": "text-only",
|
||||
"allow_global_index": false
|
||||
}
|
||||
JSON
|
||||
|
||||
python3 scripts/security/memory_search_activation_check.py \
|
||||
--isolation-report "$TMP_DIR/report.json" \
|
||||
--provider-config "$TMP_DIR/provider.json" \
|
||||
--request-enable
|
||||
|
||||
# The following must stay blocked: unsafe provider configuration.
|
||||
cat > "$TMP_DIR/provider-unsafe.json" <<'JSON'
|
||||
{
|
||||
"name": "unsafe-provider",
|
||||
"scope_filter_before_ranking": false,
|
||||
"provenance_required": false,
|
||||
"fallback_mode": "global-search",
|
||||
"allow_global_index": true
|
||||
}
|
||||
JSON
|
||||
|
||||
if python3 scripts/security/memory_search_activation_check.py \
|
||||
--isolation-report "$TMP_DIR/report.json" \
|
||||
--provider-config "$TMP_DIR/provider-unsafe.json" \
|
||||
--request-enable; then
|
||||
echo "ERROR: unsafe memory provider activation was allowed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "user-data-isolation acceptance: PASS"
|
||||
@@ -0,0 +1,26 @@
|
||||
name: user-data-isolation-gate
|
||||
|
||||
purpose: >-
|
||||
Block memory/search activation unless user-data isolation controls are healthy.
|
||||
|
||||
checks:
|
||||
- name: memory-scope-tests
|
||||
command: bash ops/ci/run-user-data-isolation-acceptance.sh
|
||||
|
||||
- name: activation-policy
|
||||
requirements:
|
||||
isolation_report: healthy
|
||||
denied_records: 0
|
||||
invalid_records: 0
|
||||
scope_filter_before_ranking: true
|
||||
provenance_required: true
|
||||
allow_global_index: false
|
||||
|
||||
failure_policy:
|
||||
action: block
|
||||
reason: user-data-isolation-redline
|
||||
|
||||
notes:
|
||||
- memory search must remain disabled when this gate fails.
|
||||
- no provider may bypass scope validation.
|
||||
- passing this gate does not grant access; RBAC and owner policy still apply.
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "memory-provider-default",
|
||||
"scope_filter_before_ranking": true,
|
||||
"provenance_required": true,
|
||||
"fallback_mode": "text-only",
|
||||
"allow_global_index": false,
|
||||
"allowed_record_fields": [
|
||||
"record_id",
|
||||
"user_id",
|
||||
"tenant_id",
|
||||
"group_id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"session_id",
|
||||
"branch",
|
||||
"baseline",
|
||||
"agent_id",
|
||||
"source_tool"
|
||||
],
|
||||
"notes": "Memory search cannot be enabled unless this contract and isolation audit are healthy."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Memory search activation gate configuration
|
||||
|
||||
AIOS_MEMORY_ACTIVATION_CHECK_SCRIPT=/data/aios-gitops/workspace/scripts/security/memory_search_activation_check.py
|
||||
AIOS_MEMORY_SCOPE_REPORT=/data/aios-gitops/runtime/state/memory-scope-guard-report.json
|
||||
AIOS_MEMORY_PROVIDER_CONFIG=/etc/aios/memory-provider-scope-contract.json
|
||||
|
||||
# Keep empty until explicit owner-approved activation.
|
||||
AIOS_MEMORY_ENABLE_REQUEST_FLAG=
|
||||
|
||||
# Safe default:
|
||||
AIOS_MEMORY_ENABLED=false
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# User data isolation audit configuration
|
||||
|
||||
AIOS_MEMORY_GUARD_SCRIPT=/data/aios-gitops/workspace/scripts/security/memory_scope_guard.py
|
||||
AIOS_MEMORY_ROOT=/data/aios-gitops/runtime/memory
|
||||
AIOS_MEMORY_SCOPE_REPORT=/data/aios-gitops/runtime/state/memory-scope-guard-report.json
|
||||
AUTONOMOUS_SAFE_MODE_FILE=/data/aios-gitops/runtime/state/autonomous-safe-mode.json
|
||||
|
||||
# Memory search remains disabled until isolation acceptance passes.
|
||||
AIOS_MEMORY_ENABLED=false
|
||||
AIOS_MEMORY_REQUIRE_SCOPE=true
|
||||
@@ -0,0 +1,49 @@
|
||||
# CMD-20260709-enable-architect-governance-loop
|
||||
|
||||
## Goal
|
||||
|
||||
Enable architect agent as a mandatory governance role.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. Record every requirement change from:
|
||||
- Custom GPT
|
||||
- Codex
|
||||
- OpenClaw
|
||||
- Hermes
|
||||
- user feedback
|
||||
|
||||
2. Refresh versioned documents:
|
||||
- user requirements
|
||||
- architecture design
|
||||
- engineering deployment
|
||||
- test cases
|
||||
|
||||
3. Review every architecture change:
|
||||
- full architecture consistency
|
||||
- optimization opportunities
|
||||
- security boundaries
|
||||
- scalability
|
||||
- rollback
|
||||
|
||||
4. Weekly research:
|
||||
- agent framework versions
|
||||
- model ecosystem changes
|
||||
- GitHub top implementations
|
||||
- security changes
|
||||
|
||||
5. Decide version bump:
|
||||
- C compatible fix/change
|
||||
- R release capability or architecture evolution
|
||||
- V major architecture/breaking change
|
||||
|
||||
## Validation signals
|
||||
|
||||
- AIOS_ARCHITECT_GOVERNANCE_READY
|
||||
- AIOS_REQUIREMENT_VERSIONING_READY
|
||||
- AIOS_ARCHITECTURE_REVIEW_LOOP_READY
|
||||
- AIOS_WEEKLY_TECH_RESEARCH_READY
|
||||
|
||||
## Governance
|
||||
|
||||
All outputs must be audited and stored through GitOps.
|
||||
@@ -0,0 +1,29 @@
|
||||
# CMD-20260709-enable-architect-governance-ui
|
||||
|
||||
## Goal
|
||||
|
||||
Deploy Architect Governance Center Web UI.
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Register API:
|
||||
- getArchitectGovernanceOverview
|
||||
2. Enable page:
|
||||
- /admin/ai-evolution/architect
|
||||
3. Display:
|
||||
- requirement changes
|
||||
- architecture reviews
|
||||
- document refresh status
|
||||
- deployment consistency checks
|
||||
- weekly research findings
|
||||
- V/R/C recommendations
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_ARCHITECT_GOVERNANCE_UI_READY
|
||||
- AIOS_REQUIREMENT_TRACEABILITY_VISIBLE
|
||||
- AIOS_VERSION_DECISION_VISIBLE
|
||||
|
||||
## Governance
|
||||
|
||||
All architecture decisions require audit evidence.
|
||||
@@ -0,0 +1,29 @@
|
||||
# CMD-20260709-enable-architect-research-timeline-ui
|
||||
|
||||
## Goal
|
||||
|
||||
Expose weekly architect research lifecycle in Web UI.
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Enable API:
|
||||
- getArchitectResearchTimeline
|
||||
2. Add page:
|
||||
- /admin/ai-evolution/architect/research/timeline
|
||||
3. Display:
|
||||
- research plan
|
||||
- findings
|
||||
- architecture impact
|
||||
- recommendation
|
||||
- C/R/V decision
|
||||
- governance event link
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_ARCHITECT_RESEARCH_TIMELINE_READY
|
||||
- AIOS_TECH_FINDING_TRACE_READY
|
||||
- AIOS_VERSION_RECOMMENDATION_TRACE_READY
|
||||
|
||||
## Governance
|
||||
|
||||
No technology adoption without architecture review.
|
||||
@@ -0,0 +1,32 @@
|
||||
# CMD-20260709-enable-architect-weekly-research
|
||||
|
||||
## Goal
|
||||
|
||||
Enable weekly proactive architecture intelligence loop.
|
||||
|
||||
## Execute
|
||||
|
||||
1. Schedule weekly research.
|
||||
2. Search/evaluate:
|
||||
- agent framework versions
|
||||
- model ecosystem changes
|
||||
- GitHub top implementations
|
||||
- security changes
|
||||
3. Generate findings.
|
||||
4. Evaluate architecture impact.
|
||||
5. Create recommendation:
|
||||
- no action
|
||||
- C change
|
||||
- R release
|
||||
- V major architecture change
|
||||
6. Emit architecture review event.
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_ARCHITECT_WEEKLY_RESEARCH_READY
|
||||
- AIOS_TECH_INTELLIGENCE_READY
|
||||
- AIOS_VERSION_RECOMMENDATION_READY
|
||||
|
||||
## Governance
|
||||
|
||||
No external technology adoption without architecture review and owner gate.
|
||||
@@ -0,0 +1,31 @@
|
||||
# CMD-20260709-enable-architecture-owner-gate
|
||||
|
||||
## Goal
|
||||
|
||||
Require Owner approval before implementing architecture changes.
|
||||
|
||||
## Workflow
|
||||
|
||||
Requirement change
|
||||
-> Architect review
|
||||
-> Owner architecture approval
|
||||
-> Implementation
|
||||
-> Test Gate
|
||||
-> Promotion
|
||||
|
||||
## Block rules
|
||||
|
||||
Architecture changes cannot enter implementation when:
|
||||
|
||||
- owner approval missing
|
||||
- owner rejected
|
||||
- audit evidence missing
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_ARCHITECTURE_OWNER_GATE_READY
|
||||
- AIOS_ARCHITECTURE_IMPLEMENTATION_PROTECTED
|
||||
|
||||
## Governance
|
||||
|
||||
Owner approval must be auditable with request, architect decision and owner decision.
|
||||
@@ -0,0 +1,36 @@
|
||||
# CMD-20260709-enable-change-governance-agents
|
||||
|
||||
## Goal
|
||||
|
||||
Enable mandatory acceptance and architecture review governance for all changes.
|
||||
|
||||
## Rules
|
||||
|
||||
Feature changes:
|
||||
- must include tests
|
||||
- user-facing changes require user acceptance agent
|
||||
|
||||
Architecture changes:
|
||||
- require independent architecture optimization agent
|
||||
- require system-wide architecture review
|
||||
- require scalability/security/rollback review
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Enable change governance gate.
|
||||
2. Register:
|
||||
- user-acceptance-agent
|
||||
- architecture-review-agent
|
||||
3. Connect to patch/review workflow.
|
||||
4. Store acceptance evidence in audit.
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_CHANGE_TEST_GATE_READY
|
||||
- AIOS_USER_ACCEPTANCE_AGENT_READY
|
||||
- AIOS_ARCHITECTURE_REVIEW_AGENT_READY
|
||||
- AIOS_CHANGE_GOVERNANCE_READY
|
||||
|
||||
## Governance
|
||||
|
||||
No merge/promotion without required evidence.
|
||||
@@ -0,0 +1,27 @@
|
||||
# CMD-20260709-enable-change-governance-ui
|
||||
|
||||
## Goal
|
||||
|
||||
Deploy Web UI for change governance and independent review agents.
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Enable dashboard:
|
||||
- /admin/ai-evolution/governance
|
||||
2. Show:
|
||||
- test gate status
|
||||
- user acceptance agent result
|
||||
- architecture agent result
|
||||
- release gate status
|
||||
- blocked changes
|
||||
3. Store evidence in audit.
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_CHANGE_GOVERNANCE_UI_READY
|
||||
- AIOS_ACCEPTANCE_AGENT_VISIBILITY_READY
|
||||
- AIOS_ARCHITECTURE_REVIEW_VISIBILITY_READY
|
||||
|
||||
## Governance
|
||||
|
||||
No change may promote without required evidence.
|
||||
@@ -0,0 +1,31 @@
|
||||
# CMD-20260709-enable-governance-event-loop
|
||||
|
||||
## Goal
|
||||
|
||||
Connect architecture, acceptance and change governance agents to the event loop.
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Enable governance event publisher.
|
||||
2. Emit events:
|
||||
- requirement_changed
|
||||
- architecture_review_started
|
||||
- architecture_review_completed
|
||||
- user_acceptance_completed
|
||||
- test_gate_completed
|
||||
- version_decision_created
|
||||
3. Connect consumers:
|
||||
- Web UI
|
||||
- Audit Plane
|
||||
- Owner Gate
|
||||
- Reporting Agent
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_GOVERNANCE_EVENT_LOOP_READY
|
||||
- AIOS_ARCHITECT_EVENT_VISIBLE
|
||||
- AIOS_CHANGE_AUDIT_TRACE_READY
|
||||
|
||||
## Governance
|
||||
|
||||
Every governance decision must have event evidence.
|
||||
@@ -0,0 +1,30 @@
|
||||
# CMD-20260709-enable-governance-timeline-ui
|
||||
|
||||
## Goal
|
||||
|
||||
Expose governance event lifecycle in Web UI.
|
||||
|
||||
## Deploy
|
||||
|
||||
1. Enable API:
|
||||
- getGovernanceTimeline
|
||||
2. Add page:
|
||||
- /admin/ai-evolution/governance/timeline
|
||||
3. Visualize:
|
||||
- requirement changes
|
||||
- architecture reviews
|
||||
- test gates
|
||||
- user acceptance
|
||||
- version decisions
|
||||
- owner gate
|
||||
- promotion
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_GOVERNANCE_TIMELINE_READY
|
||||
- AIOS_CHANGE_TRACE_VISIBLE
|
||||
- AIOS_RELEASE_DECISION_TRACE_READY
|
||||
|
||||
## Governance
|
||||
|
||||
Every release decision must have a visible event chain.
|
||||
@@ -0,0 +1,30 @@
|
||||
# CMD-20260709-enable-tiered-architecture-owner-gate
|
||||
|
||||
## Goal
|
||||
|
||||
Enable tiered architecture governance.
|
||||
|
||||
## Rules
|
||||
|
||||
Small changes:
|
||||
- architect agent may auto process
|
||||
- require audit and tests
|
||||
- no owner approval required
|
||||
|
||||
Medium changes:
|
||||
- require architect review
|
||||
- require tests and audit
|
||||
|
||||
Large changes:
|
||||
- require owner architecture approval before implementation
|
||||
- require promotion gate
|
||||
|
||||
## Validation
|
||||
|
||||
- AIOS_ARCHITECTURE_CHANGE_CLASSIFIER_READY
|
||||
- AIOS_SMALL_CHANGE_AUTO_FLOW_READY
|
||||
- AIOS_LARGE_CHANGE_OWNER_GATE_READY
|
||||
|
||||
## Governance
|
||||
|
||||
Never allow large architecture changes to enter implementation without owner approval.
|
||||
@@ -0,0 +1,82 @@
|
||||
# CMD-20260709-fix-workspace-git-objects-permission
|
||||
|
||||
## Goal
|
||||
|
||||
Restore Git write capability for AIOS GitOps workspace.
|
||||
|
||||
## Problem
|
||||
|
||||
ChatGPT Actions cannot commit new files because Git object database is not writable:
|
||||
|
||||
```text
|
||||
error: insufficient permission for adding an object to repository database .git/objects
|
||||
error: Error building trees
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
Workspace branch:
|
||||
|
||||
```text
|
||||
patch/v1r4c1-review-risk-fixes
|
||||
```
|
||||
|
||||
Affected path:
|
||||
|
||||
```text
|
||||
/data/aios-gitops/workspace/.git/objects
|
||||
```
|
||||
|
||||
## Execute
|
||||
|
||||
```bash
|
||||
cd /data/aios-gitops/workspace
|
||||
|
||||
ls -ld .git
|
||||
ls -ld .git/objects
|
||||
stat .git/objects
|
||||
|
||||
ps aux | grep -E 'aios-gitops|model-api|actions' | grep -v grep
|
||||
```
|
||||
|
||||
Identify the runtime user that runs `aios-gitops-model-api`, then fix ownership and write permissions:
|
||||
|
||||
```bash
|
||||
cd /data/aios-gitops/workspace
|
||||
|
||||
sudo chown -R <runtime_user>:<runtime_group> .git
|
||||
sudo chmod -R u+rwX,g+rwX .git/objects
|
||||
```
|
||||
|
||||
Do not use:
|
||||
|
||||
```bash
|
||||
chmod 777 .git
|
||||
```
|
||||
|
||||
## Validate
|
||||
|
||||
After repair, run a harmless Git write validation as the runtime user:
|
||||
|
||||
```bash
|
||||
cd /data/aios-gitops/workspace
|
||||
|
||||
git status --short
|
||||
|
||||
test_file="ops/openclaw/.git-write-test-$(date +%s).tmp"
|
||||
mkdir -p ops/openclaw
|
||||
printf 'git write test\n' > "$test_file"
|
||||
git add "$test_file"
|
||||
git commit -m "Validate Git object write permissions"
|
||||
git rm "$test_file"
|
||||
git commit -m "Remove Git object write permission validation file"
|
||||
```
|
||||
|
||||
## Success signals
|
||||
|
||||
- AIOS_GIT_OBJECT_PERMISSION_REPAIRED
|
||||
- AIOS_GITOPS_COMMIT_CAPABILITY_RESTORED
|
||||
|
||||
## Governance
|
||||
|
||||
Record the repair result in audit logs.
|
||||
@@ -0,0 +1,34 @@
|
||||
# CMD-20260709-validate-v1R4C1-review-fixes
|
||||
|
||||
## Goal
|
||||
|
||||
Validate V1R4C1 review risk fixes.
|
||||
|
||||
## Execute
|
||||
|
||||
1. Validate Git repository resolution:
|
||||
- AIOS_GITOPS_ROOT
|
||||
- repository owner/name
|
||||
- no stale hardcoded bare repo path
|
||||
|
||||
2. Validate model registry:
|
||||
- models loaded from config/model-registry.yaml
|
||||
- unavailable models fallback safely
|
||||
- model selection audited
|
||||
|
||||
3. Run tests for:
|
||||
- context isolation
|
||||
- agent observability
|
||||
- repository resolution
|
||||
|
||||
## Validation signals
|
||||
|
||||
- AIOS_V1R4C1_REPO_PATH_VALIDATED
|
||||
- AIOS_MODEL_REGISTRY_READY
|
||||
- AIOS_REVIEW_FIXES_READY
|
||||
|
||||
## Governance
|
||||
|
||||
- No direct main push.
|
||||
- No secrets in Git.
|
||||
- Owner gate required for promotion.
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Memory search activation safety gate
|
||||
Documentation=file:docs/security/user-data-isolation-redline.md
|
||||
Documentation=file:docs/security/user-data-isolation-deployment-runbook.md
|
||||
After=user-data-isolation-audit.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=-/etc/aios/memory-search-activation-check.env
|
||||
ExecStart=/bin/sh -lc 'exec /usr/bin/python3 ${AIOS_MEMORY_ACTIVATION_CHECK_SCRIPT} --isolation-report ${AIOS_MEMORY_SCOPE_REPORT} --provider-config ${AIOS_MEMORY_PROVIDER_CONFIG} ${AIOS_MEMORY_ENABLE_REQUEST_FLAG:-}'
|
||||
TimeoutStartSec=60
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadOnlyPaths=/data/aios-gitops/runtime/state
|
||||
ReadOnlyPaths=/etc/aios
|
||||
|
||||
# Non-zero exit means activation is blocked. This is expected when memory search
|
||||
# has not been explicitly requested or isolation is not fully healthy.
|
||||
SuccessExitStatus=78
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=User data isolation audit guard
|
||||
Documentation=file:docs/security/user-data-isolation-redline.md
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=-/etc/aios/user-data-isolation-audit.env
|
||||
ExecStart=/bin/sh -lc 'exec /usr/bin/python3 ${AIOS_MEMORY_GUARD_SCRIPT} audit --emit-safe-mode --json'
|
||||
TimeoutStartSec=120
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/data/aios-gitops/runtime/state
|
||||
ReadOnlyPaths=/data/aios-gitops/runtime/memory
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Periodic user data isolation audit
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=5min
|
||||
RandomizedDelaySec=30s
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/v1r4c1-review-risk-fixes",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-09T03:06:26Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Provider boundary for memory/search results.
|
||||
|
||||
The provider gate runs before ranking or summarization. It accepts candidate
|
||||
records only after scope validation. It never returns unscoped content.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from scripts.security.memory_scope_guard import record_matches_scope
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--scope-file", required=True, type=Path)
|
||||
parser.add_argument("--candidates-file", required=True, type=Path)
|
||||
parser.add_argument("--output-file", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
scope = json.loads(args.scope_file.read_text(encoding="utf-8"))
|
||||
candidates = json.loads(args.candidates_file.read_text(encoding="utf-8"))
|
||||
|
||||
if not isinstance(candidates, list):
|
||||
print("candidates must be a JSON list", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
allowed = []
|
||||
denied = []
|
||||
|
||||
for candidate in candidates:
|
||||
if not isinstance(candidate, dict):
|
||||
denied.append({"reason": "invalid_candidate_type"})
|
||||
continue
|
||||
|
||||
metadata = candidate.get("metadata", {})
|
||||
ok, reasons = record_matches_scope(metadata, scope)
|
||||
|
||||
# Never forward the body/content of denied records.
|
||||
if ok:
|
||||
allowed.append({
|
||||
"record_id": metadata.get("record_id"),
|
||||
"metadata": metadata,
|
||||
"provenance": candidate.get("provenance", {}),
|
||||
})
|
||||
else:
|
||||
denied.append({
|
||||
"record_id": metadata.get("record_id"),
|
||||
"reasons": reasons,
|
||||
})
|
||||
|
||||
result = {
|
||||
"status": "healthy" if not denied else "filtered",
|
||||
"allowed_count": len(allowed),
|
||||
"denied_count": len(denied),
|
||||
"allowed": allowed,
|
||||
"denied": denied,
|
||||
}
|
||||
|
||||
args.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output_file.write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,477 @@
|
||||
#!/usr/bin/env python3
|
||||
"""User data isolation guard for memory/search providers.
|
||||
|
||||
This guard enforces scope-first access for memory/state records. It is designed
|
||||
for two production-safe uses:
|
||||
|
||||
1. Audit memory directories for unscoped or cross-scope records.
|
||||
2. Filter provider candidates before ranking/summarization receives them.
|
||||
|
||||
It never prints record content. Reports contain only provenance, metadata, and
|
||||
violation reasons.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
REQUIRED_RECORD_FIELDS = (
|
||||
"record_id",
|
||||
"user_id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"session_id",
|
||||
"branch",
|
||||
"baseline",
|
||||
"agent_id",
|
||||
"source_tool",
|
||||
"created_at",
|
||||
"schema_version",
|
||||
"sensitivity",
|
||||
"retention_class",
|
||||
)
|
||||
|
||||
# tenant_id and group_id are deployment-dependent. At least one must exist.
|
||||
TENANT_FIELDS = ("tenant_id", "group_id")
|
||||
|
||||
REQUIRED_SCOPE_FIELDS = (
|
||||
"request_user_id",
|
||||
"request_project_id",
|
||||
"request_workspace_id",
|
||||
"request_session_id",
|
||||
"current_branch",
|
||||
"current_baseline",
|
||||
"purpose",
|
||||
)
|
||||
|
||||
TENANT_SCOPE_FIELDS = ("request_tenant_id", "request_group_id")
|
||||
|
||||
TEXT_EXTENSIONS = {".json", ".jsonl", ".md", ".txt"}
|
||||
DENY_EXIT = 2
|
||||
CONFIG_EXIT = 3
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return _dt.datetime.now(_dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def atomic_write_json(path: Path, payload: dict[str, Any], mode: int = 0o644) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
os.chmod(tmp_name, mode)
|
||||
os.replace(tmp_name, path)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def read_json(path: Path) -> Any:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def load_scope(path: Path | None) -> dict[str, Any]:
|
||||
if path is not None:
|
||||
scope = read_json(path)
|
||||
else:
|
||||
raw = os.environ.get("AIOS_SCOPE_ENVELOPE_JSON", "").strip()
|
||||
if not raw:
|
||||
raise ValueError("scope envelope is required via --scope-file or AIOS_SCOPE_ENVELOPE_JSON")
|
||||
scope = json.loads(raw)
|
||||
|
||||
if not isinstance(scope, dict):
|
||||
raise ValueError("scope envelope must be a JSON object")
|
||||
return scope
|
||||
|
||||
|
||||
def parse_simple_frontmatter(text: str) -> dict[str, Any]:
|
||||
"""Parse a conservative YAML-like frontmatter block.
|
||||
|
||||
Supported format:
|
||||
---
|
||||
key: value
|
||||
nested.key: value
|
||||
tags: [a, b]
|
||||
---
|
||||
|
||||
This avoids a YAML dependency and intentionally ignores complex YAML.
|
||||
"""
|
||||
|
||||
if not text.startswith("---\n"):
|
||||
return {}
|
||||
|
||||
end = text.find("\n---", 4)
|
||||
if end == -1:
|
||||
return {}
|
||||
|
||||
block = text[4:end]
|
||||
result: dict[str, Any] = {}
|
||||
for line in block.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
items = [item.strip().strip('"').strip("'") for item in value[1:-1].split(",") if item.strip()]
|
||||
set_nested(result, key, items)
|
||||
else:
|
||||
set_nested(result, key, value)
|
||||
return result
|
||||
|
||||
|
||||
def set_nested(target: dict[str, Any], dotted_key: str, value: Any) -> None:
|
||||
parts = [part for part in dotted_key.split(".") if part]
|
||||
if not parts:
|
||||
return
|
||||
current = target
|
||||
for part in parts[:-1]:
|
||||
child = current.get(part)
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
current[part] = child
|
||||
current = child
|
||||
current[parts[-1]] = value
|
||||
|
||||
|
||||
def metadata_from_payload(payload: Any) -> dict[str, Any]:
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
for key in ("metadata", "scope", "record_scope"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# Some legacy records store scope fields at top level. Accept only metadata,
|
||||
# not record body content, and never use body content as proof of scope.
|
||||
candidate = {key: payload[key] for key in REQUIRED_RECORD_FIELDS if key in payload}
|
||||
for key in TENANT_FIELDS:
|
||||
if key in payload:
|
||||
candidate[key] = payload[key]
|
||||
return candidate
|
||||
|
||||
|
||||
def load_record_metadata(path: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
provenance = {
|
||||
"path": str(path),
|
||||
"suffix": path.suffix.lower(),
|
||||
"mtime": _dt.datetime.fromtimestamp(path.stat().st_mtime, _dt.timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
if suffix == ".json":
|
||||
payload = read_json(path)
|
||||
return metadata_from_payload(payload), provenance
|
||||
|
||||
if suffix == ".jsonl":
|
||||
metadata: dict[str, Any] = {}
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
payload = json.loads(line)
|
||||
metadata = metadata_from_payload(payload)
|
||||
break
|
||||
return metadata, provenance
|
||||
|
||||
if suffix in {".md", ".txt"}:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
frontmatter = parse_simple_frontmatter(text)
|
||||
return metadata_from_payload(frontmatter) or frontmatter, provenance
|
||||
|
||||
return {}, provenance
|
||||
|
||||
|
||||
def list_candidate_files(root: Path) -> list[Path]:
|
||||
if root.is_file():
|
||||
return [root]
|
||||
if not root.exists():
|
||||
return []
|
||||
files: list[Path] = []
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file() and path.suffix.lower() in TEXT_EXTENSIONS:
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def missing_fields(metadata: dict[str, Any]) -> list[str]:
|
||||
missing = [field for field in REQUIRED_RECORD_FIELDS if metadata.get(field) in (None, "")]
|
||||
if not any(metadata.get(field) not in (None, "") for field in TENANT_FIELDS):
|
||||
missing.append("tenant_id_or_group_id")
|
||||
return missing
|
||||
|
||||
|
||||
def validate_scope_envelope(scope: dict[str, Any]) -> list[str]:
|
||||
missing = [field for field in REQUIRED_SCOPE_FIELDS if scope.get(field) in (None, "")]
|
||||
if not any(scope.get(field) not in (None, "") for field in TENANT_SCOPE_FIELDS):
|
||||
missing.append("request_tenant_id_or_request_group_id")
|
||||
return missing
|
||||
|
||||
|
||||
def as_set(value: Any) -> set[str]:
|
||||
if value is None or value == "":
|
||||
return set()
|
||||
if isinstance(value, list):
|
||||
return {str(item) for item in value if item not in (None, "")}
|
||||
if isinstance(value, tuple) or isinstance(value, set):
|
||||
return {str(item) for item in value if item not in (None, "")}
|
||||
return {str(value)}
|
||||
|
||||
|
||||
def scope_value(scope: dict[str, Any], primary: str, allowed: str | None = None) -> set[str]:
|
||||
values = as_set(scope.get(primary))
|
||||
if allowed:
|
||||
values |= as_set(scope.get(allowed))
|
||||
return values
|
||||
|
||||
|
||||
def record_matches_scope(metadata: dict[str, Any], scope: dict[str, Any]) -> tuple[bool, list[str]]:
|
||||
reasons: list[str] = []
|
||||
|
||||
missing = missing_fields(metadata)
|
||||
if missing:
|
||||
return False, [f"missing_metadata:{','.join(missing)}"]
|
||||
|
||||
scope_missing = validate_scope_envelope(scope)
|
||||
if scope_missing:
|
||||
return False, [f"invalid_scope:{','.join(scope_missing)}"]
|
||||
|
||||
def deny_if_mismatch(record_key: str, scope_key: str, reason: str, allowed_key: str | None = None) -> None:
|
||||
allowed = scope_value(scope, scope_key, allowed_key)
|
||||
if str(metadata.get(record_key)) not in allowed:
|
||||
reasons.append(reason)
|
||||
|
||||
# Tenant/group policy: if both sides have tenant_id, compare tenant_id. If not,
|
||||
# use group_id. Cross-tenant/group is always denied by default.
|
||||
if metadata.get("tenant_id") and scope.get("request_tenant_id"):
|
||||
deny_if_mismatch("tenant_id", "request_tenant_id", "tenant_mismatch")
|
||||
elif metadata.get("group_id") and scope.get("request_group_id"):
|
||||
deny_if_mismatch("group_id", "request_group_id", "group_mismatch")
|
||||
else:
|
||||
reasons.append("tenant_or_group_scope_unprovable")
|
||||
|
||||
deny_if_mismatch("user_id", "request_user_id", "user_mismatch", "allowed_user_ids")
|
||||
deny_if_mismatch("project_id", "request_project_id", "project_mismatch", "allowed_project_ids")
|
||||
deny_if_mismatch("workspace_id", "request_workspace_id", "workspace_mismatch", "allowed_workspace_ids")
|
||||
|
||||
session_policy = str(scope.get("session_policy", "same_session")).lower()
|
||||
if session_policy == "same_session":
|
||||
deny_if_mismatch("session_id", "request_session_id", "session_mismatch")
|
||||
elif session_policy == "allow_project_sessions":
|
||||
# Same project/workspace checks above still apply. Do not add session reason.
|
||||
pass
|
||||
else:
|
||||
reasons.append(f"unsupported_session_policy:{session_policy}")
|
||||
|
||||
current_branch = scope_value(scope, "current_branch", "allowed_branches")
|
||||
if str(metadata.get("branch")) not in current_branch:
|
||||
reasons.append("branch_mismatch")
|
||||
|
||||
current_baseline = scope_value(scope, "current_baseline", "allowed_baselines")
|
||||
if str(metadata.get("baseline")) not in current_baseline:
|
||||
reasons.append("baseline_mismatch")
|
||||
|
||||
allowed_agents = scope_value(scope, "request_agent_id", "allowed_agent_ids")
|
||||
if allowed_agents and str(metadata.get("agent_id")) not in allowed_agents:
|
||||
reasons.append("agent_mismatch")
|
||||
|
||||
return not reasons, reasons
|
||||
|
||||
|
||||
def path_namespace_violations(path: Path, metadata: dict[str, Any], root: Path) -> list[str]:
|
||||
"""Detect obvious global/shared path misuse.
|
||||
|
||||
This is a supplemental safety check. It does not grant access; it can only
|
||||
add violations.
|
||||
"""
|
||||
|
||||
try:
|
||||
relative = path.relative_to(root)
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
parts = relative.parts
|
||||
if not parts:
|
||||
return []
|
||||
|
||||
lower_parts = [part.lower() for part in parts]
|
||||
violations: list[str] = []
|
||||
|
||||
# Disallow records directly under a global memory root.
|
||||
if len(parts) <= 2:
|
||||
violations.append("global_memory_path")
|
||||
|
||||
# If path includes explicit namespace words, validate their following value.
|
||||
checks = {
|
||||
"tenant": metadata.get("tenant_id"),
|
||||
"tenant_id": metadata.get("tenant_id"),
|
||||
"group": metadata.get("group_id"),
|
||||
"group_id": metadata.get("group_id"),
|
||||
"user": metadata.get("user_id"),
|
||||
"user_id": metadata.get("user_id"),
|
||||
"project": metadata.get("project_id"),
|
||||
"project_id": metadata.get("project_id"),
|
||||
"session": metadata.get("session_id"),
|
||||
"session_id": metadata.get("session_id"),
|
||||
}
|
||||
for key, expected in checks.items():
|
||||
if key in lower_parts:
|
||||
index = lower_parts.index(key)
|
||||
if index + 1 < len(parts) and expected and str(expected) != parts[index + 1]:
|
||||
violations.append(f"path_metadata_mismatch:{key}")
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def scan(root: Path, scope: dict[str, Any] | None, require_scope: bool) -> dict[str, Any]:
|
||||
files = list_candidate_files(root)
|
||||
allowed: list[dict[str, Any]] = []
|
||||
denied: list[dict[str, Any]] = []
|
||||
invalid: list[dict[str, Any]] = []
|
||||
|
||||
for path in files:
|
||||
try:
|
||||
metadata, provenance = load_record_metadata(path)
|
||||
except Exception as exc: # noqa: BLE001 - report diagnostics without content
|
||||
invalid.append({"path": str(path), "reason": f"parse_error:{type(exc).__name__}", "message": str(exc)[:240]})
|
||||
continue
|
||||
|
||||
reasons: list[str] = []
|
||||
missing = missing_fields(metadata)
|
||||
if missing:
|
||||
reasons.append(f"missing_metadata:{','.join(missing)}")
|
||||
|
||||
reasons.extend(path_namespace_violations(path, metadata, root))
|
||||
|
||||
if require_scope:
|
||||
if scope is None:
|
||||
reasons.append("scope_required")
|
||||
else:
|
||||
matched, match_reasons = record_matches_scope(metadata, scope)
|
||||
if not matched:
|
||||
reasons.extend(match_reasons)
|
||||
|
||||
record = {
|
||||
"record_id": metadata.get("record_id"),
|
||||
"metadata": {key: metadata.get(key) for key in sorted(set(REQUIRED_RECORD_FIELDS + TENANT_FIELDS)) if key in metadata},
|
||||
"provenance": provenance,
|
||||
}
|
||||
|
||||
if reasons:
|
||||
record["reasons"] = sorted(set(reasons))
|
||||
denied.append(record)
|
||||
else:
|
||||
allowed.append(record)
|
||||
|
||||
status = "healthy" if not denied and not invalid else "unhealthy"
|
||||
return {
|
||||
"service": "memory-scope-guard",
|
||||
"status": status,
|
||||
"generated_at": utc_now(),
|
||||
"root": str(root),
|
||||
"counts": {
|
||||
"files": len(files),
|
||||
"allowed": len(allowed),
|
||||
"denied": len(denied),
|
||||
"invalid": len(invalid),
|
||||
},
|
||||
"allowed": allowed,
|
||||
"denied": denied,
|
||||
"invalid": invalid,
|
||||
}
|
||||
|
||||
|
||||
def write_safe_mode(report: dict[str, Any], path: Path) -> None:
|
||||
marker = {
|
||||
"service": "autonomous-safe-mode",
|
||||
"status": "active",
|
||||
"entered_at": utc_now(),
|
||||
"reason": "user_data_isolation_violation",
|
||||
"source": "memory-scope-guard",
|
||||
"report_file": report.get("report_file"),
|
||||
"counts": report.get("counts", {}),
|
||||
"operator_action": (
|
||||
"Keep memory/search disabled, inspect denied/invalid records, migrate to scoped namespaces, "
|
||||
"then rerun isolation acceptance before enabling memory search."
|
||||
),
|
||||
}
|
||||
atomic_write_json(path, marker)
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Audit or filter memory records with user-data isolation enforcement.")
|
||||
parser.add_argument("--root", default=os.environ.get("AIOS_MEMORY_ROOT", "/data/aios-gitops/runtime/memory"))
|
||||
parser.add_argument("--scope-file", type=Path, default=None)
|
||||
parser.add_argument("--report-file", type=Path, default=Path(os.environ.get("AIOS_MEMORY_SCOPE_REPORT", "/data/aios-gitops/runtime/state/memory-scope-guard-report.json")))
|
||||
parser.add_argument("--safe-mode-file", type=Path, default=Path(os.environ.get("AUTONOMOUS_SAFE_MODE_FILE", "/data/aios-gitops/runtime/state/autonomous-safe-mode.json")))
|
||||
parser.add_argument("--require-scope", action="store_true", default=os.environ.get("AIOS_MEMORY_REQUIRE_SCOPE", "true").lower() not in {"0", "false", "no"})
|
||||
parser.add_argument("--emit-safe-mode", action="store_true")
|
||||
parser.add_argument("--json", action="store_true")
|
||||
parser.add_argument("command", choices=["audit", "filter"], nargs="?", default="audit")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
scope = None
|
||||
if args.require_scope:
|
||||
try:
|
||||
scope = load_scope(args.scope_file)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
report = {
|
||||
"service": "memory-scope-guard",
|
||||
"status": "unhealthy",
|
||||
"generated_at": utc_now(),
|
||||
"error": f"scope_error:{type(exc).__name__}",
|
||||
"message": str(exc),
|
||||
"counts": {"files": 0, "allowed": 0, "denied": 0, "invalid": 1},
|
||||
}
|
||||
report["report_file"] = str(args.report_file)
|
||||
atomic_write_json(args.report_file, report)
|
||||
if args.emit_safe_mode:
|
||||
write_safe_mode(report, args.safe_mode_file)
|
||||
json.dump(report, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
return CONFIG_EXIT
|
||||
|
||||
report = scan(Path(args.root), scope, args.require_scope)
|
||||
report["report_file"] = str(args.report_file)
|
||||
atomic_write_json(args.report_file, report)
|
||||
|
||||
if report["status"] != "healthy" and args.emit_safe_mode:
|
||||
write_safe_mode(report, args.safe_mode_file)
|
||||
|
||||
if args.json or report["status"] != "healthy":
|
||||
json.dump(report, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
else:
|
||||
print(
|
||||
"healthy: "
|
||||
f"files={report['counts']['files']} "
|
||||
f"allowed={report['counts']['allowed']} "
|
||||
f"denied={report['counts']['denied']} "
|
||||
f"invalid={report['counts']['invalid']}"
|
||||
)
|
||||
|
||||
return 0 if report["status"] == "healthy" else DENY_EXIT
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Activation gate for memory/search providers.
|
||||
|
||||
Memory search may be enabled only after isolation audit is healthy and the
|
||||
activation config explicitly requests scoped mode. This script is intentionally
|
||||
conservative: missing reports, denied records, invalid records, or unsupported
|
||||
provider modes block activation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
BLOCKED_EXIT = 78
|
||||
CONFIG_EXIT = 3
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
value = json.load(handle)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"expected JSON object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def env_bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.environ.get(name)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||
|
||||
|
||||
def evaluate(report: dict[str, Any], provider: dict[str, Any], requested_enable: bool) -> dict[str, Any]:
|
||||
reasons: list[str] = []
|
||||
|
||||
if not requested_enable:
|
||||
reasons.append("memory_search_not_requested")
|
||||
|
||||
if report.get("status") != "healthy":
|
||||
reasons.append("isolation_report_not_healthy")
|
||||
|
||||
counts = report.get("counts", {})
|
||||
if counts.get("denied", 0):
|
||||
reasons.append("denied_records_present")
|
||||
if counts.get("invalid", 0):
|
||||
reasons.append("invalid_records_present")
|
||||
|
||||
if not provider.get("scope_filter_before_ranking", False):
|
||||
reasons.append("provider_does_not_filter_before_ranking")
|
||||
if not provider.get("provenance_required", False):
|
||||
reasons.append("provider_does_not_return_provenance")
|
||||
if provider.get("fallback_mode") not in {"text-only", "no-memory", "scoped-only"}:
|
||||
reasons.append("provider_fallback_mode_unsafe")
|
||||
if provider.get("allow_global_index", False):
|
||||
reasons.append("global_index_not_allowed")
|
||||
|
||||
status = "allowed" if requested_enable and not reasons else "blocked"
|
||||
return {
|
||||
"service": "memory-search-activation-check",
|
||||
"status": status,
|
||||
"reasons": reasons,
|
||||
"report_counts": counts,
|
||||
"provider": {
|
||||
"name": provider.get("name", "unknown"),
|
||||
"scope_filter_before_ranking": provider.get("scope_filter_before_ranking", False),
|
||||
"provenance_required": provider.get("provenance_required", False),
|
||||
"fallback_mode": provider.get("fallback_mode"),
|
||||
"allow_global_index": provider.get("allow_global_index", False),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Check whether memory search may be enabled safely.")
|
||||
parser.add_argument(
|
||||
"--isolation-report",
|
||||
type=Path,
|
||||
default=Path(os.environ.get("AIOS_MEMORY_SCOPE_REPORT", "/data/aios-gitops/runtime/state/memory-scope-guard-report.json")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--provider-config",
|
||||
type=Path,
|
||||
default=Path(os.environ.get("AIOS_MEMORY_PROVIDER_CONFIG", "/etc/aios/memory-provider-scope-contract.json")),
|
||||
)
|
||||
parser.add_argument("--request-enable", action="store_true", default=env_bool("AIOS_MEMORY_ENABLE_REQUESTED", False))
|
||||
parser.add_argument("--output-file", type=Path, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
report = read_json(args.isolation_report)
|
||||
provider = read_json(args.provider_config)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result = {
|
||||
"service": "memory-search-activation-check",
|
||||
"status": "blocked",
|
||||
"reasons": [f"configuration_error:{type(exc).__name__}"],
|
||||
"message": str(exc),
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return CONFIG_EXIT
|
||||
|
||||
result = evaluate(report, provider, args.request_enable)
|
||||
text = json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||
if args.output_file:
|
||||
args.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output_file.write_text(text, encoding="utf-8")
|
||||
print(text, end="")
|
||||
return 0 if result["status"] == "allowed" else BLOCKED_EXIT
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = ROOT / "scripts" / "security" / "memory_provider_gate.py"
|
||||
|
||||
|
||||
def metadata(**overrides):
|
||||
data = {
|
||||
"record_id": "rec-allowed",
|
||||
"tenant_id": "tenant-a",
|
||||
"user_id": "user-a",
|
||||
"project_id": "project-a",
|
||||
"workspace_id": "workspace-a",
|
||||
"session_id": "session-a",
|
||||
"branch": "main",
|
||||
"baseline": "V1R4C3",
|
||||
"agent_id": "agent-a",
|
||||
"source_tool": "unit-test",
|
||||
"created_at": "2026-07-10T00:00:00Z",
|
||||
"schema_version": "1",
|
||||
"sensitivity": "internal",
|
||||
"retention_class": "ephemeral",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def scope(**overrides):
|
||||
data = {
|
||||
"request_tenant_id": "tenant-a",
|
||||
"request_user_id": "user-a",
|
||||
"request_project_id": "project-a",
|
||||
"request_workspace_id": "workspace-a",
|
||||
"request_session_id": "session-a",
|
||||
"current_branch": "main",
|
||||
"current_baseline": "V1R4C3",
|
||||
"request_agent_id": "agent-a",
|
||||
"purpose": "unit-test",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def run_gate(tmp_path: Path, candidate_records: list[dict], request_scope: dict | None = None):
|
||||
scope_file = tmp_path / "scope.json"
|
||||
candidates_file = tmp_path / "candidates.json"
|
||||
output_file = tmp_path / "filtered.json"
|
||||
|
||||
scope_file.write_text(json.dumps(request_scope or scope()), encoding="utf-8")
|
||||
candidates_file.write_text(json.dumps(candidate_records), encoding="utf-8")
|
||||
|
||||
proc = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--scope-file",
|
||||
str(scope_file),
|
||||
"--candidates-file",
|
||||
str(candidates_file),
|
||||
"--output-file",
|
||||
str(output_file),
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(output_file.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_provider_gate_allows_only_scoped_candidates(tmp_path):
|
||||
result = run_gate(
|
||||
tmp_path,
|
||||
[
|
||||
{"metadata": metadata(), "body": "this body must not be required by the test"},
|
||||
{"metadata": metadata(record_id="rec-denied", user_id="user-b"), "body": "redacted"},
|
||||
],
|
||||
)
|
||||
|
||||
assert result["status"] == "filtered"
|
||||
assert result["allowed_count"] == 1
|
||||
assert result["denied_count"] == 1
|
||||
assert result["allowed"][0]["record_id"] == "rec-allowed"
|
||||
assert result["denied"][0]["record_id"] == "rec-denied"
|
||||
assert "user_mismatch" in result["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_provider_gate_never_forwards_denied_content(tmp_path):
|
||||
result = run_gate(
|
||||
tmp_path,
|
||||
[
|
||||
{
|
||||
"metadata": metadata(record_id="rec-denied", tenant_id="tenant-b"),
|
||||
"body": {"sensitive": "must-not-forward"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert result["allowed"] == []
|
||||
assert result["denied_count"] == 1
|
||||
assert "body" not in result["denied"][0]
|
||||
assert "tenant_mismatch" in result["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_provider_gate_denies_untagged_candidate(tmp_path):
|
||||
result = run_gate(tmp_path, [{"body": "untagged"}])
|
||||
|
||||
assert result["status"] == "filtered"
|
||||
assert result["allowed_count"] == 0
|
||||
assert result["denied_count"] == 1
|
||||
assert any(reason.startswith("missing_metadata") for reason in result["denied"][0]["reasons"])
|
||||
|
||||
|
||||
def test_provider_gate_requires_current_branch_and_baseline(tmp_path):
|
||||
result = run_gate(
|
||||
tmp_path,
|
||||
[
|
||||
{"metadata": metadata(record_id="old-branch", branch="old")},
|
||||
{"metadata": metadata(record_id="old-baseline", baseline="V1R1C0")},
|
||||
],
|
||||
)
|
||||
|
||||
reasons = {item["record_id"]: item["reasons"] for item in result["denied"]}
|
||||
assert "branch_mismatch" in reasons["old-branch"]
|
||||
assert "baseline_mismatch" in reasons["old-baseline"]
|
||||
|
||||
|
||||
def test_provider_gate_can_allow_explicit_agent_allowlist(tmp_path):
|
||||
result = run_gate(
|
||||
tmp_path,
|
||||
[{"metadata": metadata(record_id="agent-b", agent_id="agent-b")}],
|
||||
request_scope=scope(allowed_agent_ids=["agent-a", "agent-b"]),
|
||||
)
|
||||
|
||||
assert result["status"] == "healthy"
|
||||
assert result["allowed_count"] == 1
|
||||
assert result["denied_count"] == 0
|
||||
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.security.memory_scope_guard import ( # noqa: E402
|
||||
record_matches_scope,
|
||||
scan,
|
||||
validate_scope_envelope,
|
||||
)
|
||||
|
||||
|
||||
def base_metadata(**overrides):
|
||||
data = {
|
||||
"record_id": "rec-1",
|
||||
"tenant_id": "tenant-a",
|
||||
"user_id": "user-a",
|
||||
"project_id": "project-a",
|
||||
"workspace_id": "workspace-a",
|
||||
"session_id": "session-a",
|
||||
"branch": "main",
|
||||
"baseline": "V1R4C3",
|
||||
"agent_id": "agent-a",
|
||||
"source_tool": "unit-test",
|
||||
"created_at": "2026-07-10T00:00:00Z",
|
||||
"schema_version": "1",
|
||||
"sensitivity": "internal",
|
||||
"retention_class": "ephemeral",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def base_scope(**overrides):
|
||||
data = {
|
||||
"request_tenant_id": "tenant-a",
|
||||
"request_user_id": "user-a",
|
||||
"request_project_id": "project-a",
|
||||
"request_workspace_id": "workspace-a",
|
||||
"request_session_id": "session-a",
|
||||
"current_branch": "main",
|
||||
"current_baseline": "V1R4C3",
|
||||
"purpose": "unit-test",
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
|
||||
|
||||
def scoped_path(root: Path, metadata: dict) -> Path:
|
||||
return (
|
||||
root
|
||||
/ "tenant"
|
||||
/ metadata["tenant_id"]
|
||||
/ "user"
|
||||
/ metadata["user_id"]
|
||||
/ "project"
|
||||
/ metadata["project_id"]
|
||||
/ "session"
|
||||
/ metadata["session_id"]
|
||||
/ f"{metadata['record_id']}.json"
|
||||
)
|
||||
|
||||
|
||||
def write_json_record(root: Path, metadata: dict, body=None) -> Path:
|
||||
path = scoped_path(root, metadata)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps({"metadata": metadata, "body": body or {"redacted": True}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def write_md_record(root: Path, metadata: dict) -> Path:
|
||||
path = scoped_path(root, metadata).with_suffix(".md")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = ["---"]
|
||||
for key, value in metadata.items():
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.extend(["---", "", "redacted body"])
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_same_user_project_session_record_is_allowed(tmp_path):
|
||||
metadata = base_metadata()
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "healthy"
|
||||
assert report["counts"]["allowed"] == 1
|
||||
assert report["counts"]["denied"] == 0
|
||||
|
||||
|
||||
def test_markdown_frontmatter_record_is_allowed_when_scoped(tmp_path):
|
||||
metadata = base_metadata(record_id="rec-md")
|
||||
write_md_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "healthy"
|
||||
assert report["counts"]["allowed"] == 1
|
||||
|
||||
|
||||
def test_different_user_is_denied(tmp_path):
|
||||
metadata = base_metadata(user_id="user-b")
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert report["counts"]["denied"] == 1
|
||||
assert "user_mismatch" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_different_project_is_denied(tmp_path):
|
||||
metadata = base_metadata(project_id="project-b")
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert "project_mismatch" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_different_tenant_is_denied(tmp_path):
|
||||
metadata = base_metadata(tenant_id="tenant-b")
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert "tenant_mismatch" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_untagged_record_is_denied(tmp_path):
|
||||
path = tmp_path / "tenant" / "tenant-a" / "user" / "user-a" / "project" / "project-a" / "session" / "session-a" / "untagged.json"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"body": "redacted"}), encoding="utf-8")
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert any(reason.startswith("missing_metadata") for reason in report["denied"][0]["reasons"])
|
||||
|
||||
|
||||
def test_stale_branch_is_denied(tmp_path):
|
||||
metadata = base_metadata(branch="old-feature-branch")
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert "branch_mismatch" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_stale_baseline_is_denied_as_current_evidence(tmp_path):
|
||||
metadata = base_metadata(baseline="V1R1C0")
|
||||
write_json_record(tmp_path, metadata)
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert "baseline_mismatch" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_unscoped_global_memory_path_is_denied(tmp_path):
|
||||
metadata = base_metadata()
|
||||
path = tmp_path / "global.json"
|
||||
path.write_text(json.dumps({"metadata": metadata}), encoding="utf-8")
|
||||
|
||||
report = scan(tmp_path, base_scope(), require_scope=True)
|
||||
|
||||
assert report["status"] == "unhealthy"
|
||||
assert "global_memory_path" in report["denied"][0]["reasons"]
|
||||
|
||||
|
||||
def test_scope_envelope_requires_core_dimensions():
|
||||
missing = validate_scope_envelope({"request_user_id": "user-a"})
|
||||
|
||||
assert "request_project_id" in missing
|
||||
assert "request_tenant_id_or_request_group_id" in missing
|
||||
|
||||
|
||||
def test_provider_fallback_without_scope_is_denied():
|
||||
ok, reasons = record_matches_scope(base_metadata(), {"purpose": "fallback"})
|
||||
|
||||
assert ok is False
|
||||
assert any(reason.startswith("invalid_scope") for reason in reasons)
|
||||
|
||||
|
||||
def test_cross_session_denied_by_default():
|
||||
ok, reasons = record_matches_scope(base_metadata(session_id="session-b"), base_scope())
|
||||
|
||||
assert ok is False
|
||||
assert "session_mismatch" in reasons
|
||||
|
||||
|
||||
def test_explicit_project_session_policy_can_allow_same_project_other_session():
|
||||
ok, reasons = record_matches_scope(
|
||||
base_metadata(session_id="session-b"),
|
||||
base_scope(session_policy="allow_project_sessions"),
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
assert reasons == []
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.security.memory_search_activation_check import evaluate
|
||||
|
||||
|
||||
def healthy_report(**overrides):
|
||||
report = {
|
||||
"status": "healthy",
|
||||
"counts": {
|
||||
"files": 3,
|
||||
"allowed": 3,
|
||||
"denied": 0,
|
||||
"invalid": 0,
|
||||
},
|
||||
}
|
||||
report.update(overrides)
|
||||
return report
|
||||
|
||||
|
||||
def safe_provider(**overrides):
|
||||
provider = {
|
||||
"name": "safe-provider",
|
||||
"scope_filter_before_ranking": True,
|
||||
"provenance_required": True,
|
||||
"fallback_mode": "text-only",
|
||||
"allow_global_index": False,
|
||||
}
|
||||
provider.update(overrides)
|
||||
return provider
|
||||
|
||||
|
||||
def test_activation_allowed_only_when_report_and_provider_are_safe():
|
||||
result = evaluate(healthy_report(), safe_provider(), requested_enable=True)
|
||||
|
||||
assert result["status"] == "allowed"
|
||||
assert result["reasons"] == []
|
||||
|
||||
|
||||
def test_activation_blocked_when_not_requested():
|
||||
result = evaluate(healthy_report(), safe_provider(), requested_enable=False)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "memory_search_not_requested" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_when_isolation_report_unhealthy():
|
||||
result = evaluate(healthy_report(status="unhealthy"), safe_provider(), requested_enable=True)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "isolation_report_not_healthy" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_when_denied_records_exist():
|
||||
result = evaluate(
|
||||
healthy_report(counts={"files": 2, "allowed": 1, "denied": 1, "invalid": 0}),
|
||||
safe_provider(),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "denied_records_present" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_when_invalid_records_exist():
|
||||
result = evaluate(
|
||||
healthy_report(counts={"files": 2, "allowed": 1, "denied": 0, "invalid": 1}),
|
||||
safe_provider(),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "invalid_records_present" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_without_filter_before_ranking():
|
||||
result = evaluate(
|
||||
healthy_report(),
|
||||
safe_provider(scope_filter_before_ranking=False),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "provider_does_not_filter_before_ranking" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_without_provenance():
|
||||
result = evaluate(
|
||||
healthy_report(),
|
||||
safe_provider(provenance_required=False),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "provider_does_not_return_provenance" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_for_unsafe_fallback():
|
||||
result = evaluate(
|
||||
healthy_report(),
|
||||
safe_provider(fallback_mode="global-search"),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "provider_fallback_mode_unsafe" in result["reasons"]
|
||||
|
||||
|
||||
def test_activation_blocked_for_global_index():
|
||||
result = evaluate(
|
||||
healthy_report(),
|
||||
safe_provider(allow_global_index=True),
|
||||
requested_enable=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert "global_index_not_allowed" in result["reasons"]
|
||||
@@ -0,0 +1,16 @@
|
||||
from active_runtime.app.architect_governance_agent import ArchitectGovernanceAgent
|
||||
from active_runtime.app.architect_governance_dashboard import ArchitectGovernanceDashboard
|
||||
|
||||
|
||||
def test_architect_agent_decides_release_level():
|
||||
result = ArchitectGovernanceAgent().review_architecture_change({
|
||||
"architecture_change": True,
|
||||
"new_release_capability": True,
|
||||
})
|
||||
assert result.version_bump == "R"
|
||||
assert result.docs_to_refresh.architecture_doc is True
|
||||
|
||||
|
||||
def test_architect_dashboard_exists():
|
||||
result = ArchitectGovernanceDashboard().overview()
|
||||
assert result["route"] == "/admin/ai-evolution/architect"
|
||||
@@ -0,0 +1,23 @@
|
||||
from active_runtime.app.architect_research_timeline import ArchitectResearchTimeline
|
||||
|
||||
|
||||
def test_research_timeline_tracks_version_decision():
|
||||
result = ArchitectResearchTimeline().build(
|
||||
findings=[
|
||||
{
|
||||
"id": "f1",
|
||||
"title": "new agent capability",
|
||||
"impact": "new_capability",
|
||||
"source": "github",
|
||||
}
|
||||
],
|
||||
decisions=[
|
||||
{
|
||||
"id": "d1",
|
||||
"version_bump": "R",
|
||||
"status": "recommended",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert result["summary"]["version_decisions"] == 1
|
||||
assert "R" in result["summary"]["recommended_bumps"]
|
||||
@@ -0,0 +1,18 @@
|
||||
from active_runtime.app.architect_weekly_research_agent import ArchitectWeeklyResearchAgent
|
||||
|
||||
|
||||
def test_architect_research_decides_release_type():
|
||||
agent = ArchitectWeeklyResearchAgent()
|
||||
finding = agent.normalize_finding(
|
||||
"github-top-agentops",
|
||||
"new capability",
|
||||
"example",
|
||||
"github",
|
||||
impact="architecture",
|
||||
)
|
||||
decision = agent.decide([finding])
|
||||
assert decision.version_bump == "R"
|
||||
|
||||
|
||||
def test_research_plan_exists():
|
||||
assert ArchitectWeeklyResearchAgent().plan()["cadence"] == "weekly"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user