Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c520da625 | ||
|
|
a907a86aa7 | ||
|
|
27ee200b21 | ||
|
|
6839c80311 | ||
|
|
abe2ea3577 | ||
|
|
7f7cb61ec5 | ||
|
|
6d6e3f425e |
@@ -10,4 +10,3 @@ node_modules/
|
|||||||
runtime/tmp/
|
runtime/tmp/
|
||||||
runtime/cache/
|
runtime/cache/
|
||||||
logs/
|
logs/
|
||||||
config/test_write_perms
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
"""API adapter for agent context routing and visibility."""
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi import APIRouter
|
|
||||||
except Exception:
|
|
||||||
APIRouter = None
|
|
||||||
|
|
||||||
from .agent_context_dashboard import AgentContextDashboard
|
|
||||||
from .agent_context_rbac import AgentContextRBAC
|
|
||||||
from .agent_context_resolver import AgentContextResolver
|
|
||||||
|
|
||||||
resolver = AgentContextResolver()
|
|
||||||
rbac = AgentContextRBAC()
|
|
||||||
dashboard = AgentContextDashboard()
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_agent_context(payload: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
route = rbac.route(payload["user_id"], payload.get("group_id"), payload.get("role", "user"))
|
|
||||||
context = resolver.resolve(
|
|
||||||
user_id=payload["user_id"],
|
|
||||||
group_id=payload.get("group_id", ""),
|
|
||||||
session=payload.get("session"),
|
|
||||||
user_profile=payload.get("user_profile"),
|
|
||||||
group_policy=payload.get("group_policy"),
|
|
||||||
agent_default=payload.get("agent_default"),
|
|
||||||
owner_policy=payload.get("owner_policy"),
|
|
||||||
owner=payload.get("role") == "owner",
|
|
||||||
)
|
|
||||||
return {"routing": route.to_dict(), "context": context.to_dict()}
|
|
||||||
|
|
||||||
|
|
||||||
def get_context_dashboard() -> dict[str, Any]:
|
|
||||||
return dashboard.overview()
|
|
||||||
|
|
||||||
|
|
||||||
def build_router():
|
|
||||||
if APIRouter is None:
|
|
||||||
return None
|
|
||||||
router = APIRouter(prefix="/admin/ai-evolution/context", tags=["context-isolation"])
|
|
||||||
|
|
||||||
@router.post("/resolve", operation_id="resolveAgentContext")
|
|
||||||
def resolve(payload: dict[str, Any]):
|
|
||||||
return resolve_agent_context(payload)
|
|
||||||
|
|
||||||
@router.get("/overview", operation_id="getAgentContextDashboard")
|
|
||||||
def overview():
|
|
||||||
return get_context_dashboard()
|
|
||||||
|
|
||||||
return router
|
|
||||||
|
|
||||||
|
|
||||||
router = build_router()
|
|
||||||
|
|
||||||
__all__ = ["router", "resolve_agent_context", "get_context_dashboard"]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""Audit context resolution decisions."""
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now():
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class AgentContextAudit:
|
|
||||||
def record(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"event": "context_loaded",
|
|
||||||
"agent_id": context.get("agent_id"),
|
|
||||||
"user_id": context.get("user_id"),
|
|
||||||
"group_id": context.get("group_id"),
|
|
||||||
"layers": [x.get("name") for x in context.get("layers", [])],
|
|
||||||
"timestamp": utc_now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentContextAudit"]
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""Web UI model for context isolation visibility."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class AgentContextDashboard:
|
|
||||||
def overview(self, contexts: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
||||||
contexts = contexts or []
|
|
||||||
return {
|
|
||||||
"module": "agent-context-isolation",
|
|
||||||
"route": "/admin/ai-evolution/context",
|
|
||||||
"title": "Agent Context Isolation",
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"name": "agent-routing",
|
|
||||||
"title": "Agent 路由",
|
|
||||||
"data": contexts,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "context-layers",
|
|
||||||
"title": "Context 来源层级",
|
|
||||||
"layers": [
|
|
||||||
"safety_policy",
|
|
||||||
"owner_policy",
|
|
||||||
"group_policy",
|
|
||||||
"role_policy",
|
|
||||||
"user_profile",
|
|
||||||
"session_context",
|
|
||||||
"agent_default",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "audit",
|
|
||||||
"title": "Context 审计",
|
|
||||||
"events": [
|
|
||||||
"context_loaded",
|
|
||||||
"profile_updated",
|
|
||||||
"agent_selected",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentContextDashboard"]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
"""RBAC binding for group main agents and owner agents."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class AgentRoutingDecision:
|
|
||||||
user_id: str
|
|
||||||
group_id: str
|
|
||||||
role: str
|
|
||||||
agent_id: str
|
|
||||||
scope: str
|
|
||||||
allowed: bool
|
|
||||||
reason: str
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentContextRBAC:
|
|
||||||
"""Routes users to group agents and owners to dedicated owner agents."""
|
|
||||||
|
|
||||||
VALID_ROLES = {"user", "group_admin", "owner"}
|
|
||||||
|
|
||||||
def route(self, user_id: str, group_id: str | None, role: str) -> AgentRoutingDecision:
|
|
||||||
if role not in self.VALID_ROLES:
|
|
||||||
return AgentRoutingDecision(user_id, group_id or "", role, "", "none", False, "unknown_role")
|
|
||||||
if role == "owner":
|
|
||||||
return AgentRoutingDecision(
|
|
||||||
user_id=user_id,
|
|
||||||
group_id=group_id or "global",
|
|
||||||
role=role,
|
|
||||||
agent_id=f"owner-agent:{user_id}",
|
|
||||||
scope="global",
|
|
||||||
allowed=True,
|
|
||||||
reason="owner_dedicated_agent",
|
|
||||||
)
|
|
||||||
if not group_id:
|
|
||||||
return AgentRoutingDecision(user_id, "", role, "", "none", False, "group_required")
|
|
||||||
scope = "group" if role == "group_admin" else "self"
|
|
||||||
return AgentRoutingDecision(
|
|
||||||
user_id=user_id,
|
|
||||||
group_id=group_id,
|
|
||||||
role=role,
|
|
||||||
agent_id=f"group-main-agent:{group_id}",
|
|
||||||
scope=scope,
|
|
||||||
allowed=True,
|
|
||||||
reason="group_agent_routing",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentRoutingDecision", "AgentContextRBAC"]
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
"""Multi-user agent context resolver.
|
|
||||||
|
|
||||||
Architecture:
|
|
||||||
- shared agent kernel
|
|
||||||
- group main agent
|
|
||||||
- owner dedicated agent
|
|
||||||
- user profile overlay
|
|
||||||
- session overlay
|
|
||||||
|
|
||||||
Personal preferences never enter shared USER.md.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass, field
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ContextLayer:
|
|
||||||
name: str
|
|
||||||
priority: int
|
|
||||||
data: dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class EffectiveContext:
|
|
||||||
agent_id: str
|
|
||||||
user_id: str
|
|
||||||
group_id: str
|
|
||||||
layers: list[ContextLayer]
|
|
||||||
resolved: dict[str, Any]
|
|
||||||
resolved_at: str = field(default_factory=utc_now)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"user_id": self.user_id,
|
|
||||||
"group_id": self.group_id,
|
|
||||||
"layers": [asdict(x) for x in self.layers],
|
|
||||||
"resolved": self.resolved,
|
|
||||||
"resolved_at": self.resolved_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AgentContextResolver:
|
|
||||||
"""Resolves context without shared user-state pollution."""
|
|
||||||
|
|
||||||
def resolve(
|
|
||||||
self,
|
|
||||||
user_id: str,
|
|
||||||
group_id: str,
|
|
||||||
session: dict[str, Any] | None = None,
|
|
||||||
user_profile: dict[str, Any] | None = None,
|
|
||||||
group_policy: dict[str, Any] | None = None,
|
|
||||||
agent_default: dict[str, Any] | None = None,
|
|
||||||
owner_policy: dict[str, Any] | None = None,
|
|
||||||
owner: bool = False,
|
|
||||||
) -> EffectiveContext:
|
|
||||||
layers = [
|
|
||||||
ContextLayer("agent_default", 10, agent_default or {}),
|
|
||||||
ContextLayer("group_policy", 30, group_policy or {}),
|
|
||||||
ContextLayer("user_profile", 50, user_profile or {}),
|
|
||||||
ContextLayer("session", 70, session or {}),
|
|
||||||
]
|
|
||||||
agent_id = f"group-main-agent:{group_id}"
|
|
||||||
if owner:
|
|
||||||
agent_id = f"owner-agent:{user_id}"
|
|
||||||
layers.append(ContextLayer("owner_policy", 90, owner_policy or {}))
|
|
||||||
resolved: dict[str, Any] = {}
|
|
||||||
# Higher priority overlays lower priority except safety policies handled upstream.
|
|
||||||
for layer in sorted(layers, key=lambda x: x.priority):
|
|
||||||
resolved.update(layer.data)
|
|
||||||
return EffectiveContext(agent_id, user_id, group_id, layers, resolved)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["ContextLayer", "EffectiveContext", "AgentContextResolver"]
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
"""Actions API for unified Agent Event Bus."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi import APIRouter
|
|
||||||
except Exception:
|
|
||||||
APIRouter = None
|
|
||||||
|
|
||||||
from .agent_event_bus import AgentEventBus
|
|
||||||
|
|
||||||
bus = AgentEventBus()
|
|
||||||
|
|
||||||
|
|
||||||
def create_agent_event(payload: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
return {"status": "ok", "event": bus.publish(**payload)}
|
|
||||||
|
|
||||||
|
|
||||||
def list_agent_events(kind: str | None = None, status: str = "pending") -> dict[str, Any]:
|
|
||||||
return {"status": "ok", "events": bus.list_events(kind, status)}
|
|
||||||
|
|
||||||
|
|
||||||
def get_agent_event(event_id: str) -> dict[str, Any]:
|
|
||||||
return {"status": "ok", "event": bus.get(event_id)}
|
|
||||||
|
|
||||||
|
|
||||||
def transition_agent_event(event_id: str, status: str, actor: str = "system") -> dict[str, Any]:
|
|
||||||
return {"status": "ok", "event": bus.transition(event_id, status, actor)}
|
|
||||||
|
|
||||||
|
|
||||||
def agent_event_stats() -> dict[str, Any]:
|
|
||||||
return {"status": "ok", "stats": bus.stats()}
|
|
||||||
|
|
||||||
|
|
||||||
# Compatibility adapters
|
|
||||||
|
|
||||||
def list_gpt_inbox_messages():
|
|
||||||
return list_agent_events(kind="gpt_inbox")
|
|
||||||
|
|
||||||
|
|
||||||
def ack_gpt_inbox_message(event_id: str):
|
|
||||||
return transition_agent_event(event_id, "completed", "custom-gpt")
|
|
||||||
|
|
||||||
|
|
||||||
def build_router():
|
|
||||||
if APIRouter is None:
|
|
||||||
return None
|
|
||||||
router = APIRouter(prefix="/agent-events", tags=["agent-events"])
|
|
||||||
|
|
||||||
@router.post("", operation_id="createAgentEvent")
|
|
||||||
def create(payload: dict[str, Any]):
|
|
||||||
return create_agent_event(payload)
|
|
||||||
|
|
||||||
@router.get("", operation_id="listAgentEvents")
|
|
||||||
def listing(kind: str | None = None, status: str = "pending"):
|
|
||||||
return list_agent_events(kind, status)
|
|
||||||
|
|
||||||
@router.get("/{event_id}", operation_id="getAgentEvent")
|
|
||||||
def get(event_id: str):
|
|
||||||
return get_agent_event(event_id)
|
|
||||||
|
|
||||||
@router.post("/{event_id}/transition", operation_id="transitionAgentEvent")
|
|
||||||
def transition(event_id: str, payload: dict[str, Any]):
|
|
||||||
return transition_agent_event(event_id, payload["status"], payload.get("actor", "system"))
|
|
||||||
|
|
||||||
@router.get("/stats/overview", operation_id="getAgentEventStats")
|
|
||||||
def stats():
|
|
||||||
return agent_event_stats()
|
|
||||||
|
|
||||||
return router
|
|
||||||
|
|
||||||
|
|
||||||
router = build_router()
|
|
||||||
|
|
||||||
__all__ = ["router", "create_agent_event", "list_agent_events", "get_agent_event", "transition_agent_event"]
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
"""Unified Agent Event Bus.
|
|
||||||
|
|
||||||
Unifies commands, GPT messages, replies, approvals, audit and reports into one
|
|
||||||
state machine. Compatibility layers can filter events by kind instead of keeping
|
|
||||||
separate queues.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass, field
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from hashlib import sha256
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AgentEvent:
|
|
||||||
id: str
|
|
||||||
kind: str
|
|
||||||
source: str
|
|
||||||
target: str
|
|
||||||
summary: str
|
|
||||||
payload: dict[str, Any]
|
|
||||||
status: str = "pending"
|
|
||||||
correlation_id: str | None = None
|
|
||||||
created_at: str = field(default_factory=utc_now)
|
|
||||||
updated_at: str = field(default_factory=utc_now)
|
|
||||||
actor: str | None = None
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentEventBus:
|
|
||||||
STATES = ("pending", "running", "completed", "failed")
|
|
||||||
|
|
||||||
def __init__(self, root: str = "agents/events"):
|
|
||||||
self.root = Path(root)
|
|
||||||
for state in self.STATES:
|
|
||||||
(self.root / state).mkdir(parents=True, exist_ok=True)
|
|
||||||
(self.root / "audit").mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def publish(
|
|
||||||
self,
|
|
||||||
kind: str,
|
|
||||||
source: str,
|
|
||||||
target: str,
|
|
||||||
summary: str,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
correlation_id: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
event_id = "evt_" + sha256(
|
|
||||||
json.dumps(
|
|
||||||
{"kind": kind, "source": source, "summary": summary, "payload": payload},
|
|
||||||
sort_keys=True,
|
|
||||||
ensure_ascii=False,
|
|
||||||
).encode("utf-8")
|
|
||||||
).hexdigest()[:16]
|
|
||||||
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
|
|
||||||
self._write("pending", event.to_dict())
|
|
||||||
self._audit("created", event.to_dict())
|
|
||||||
return event.to_dict()
|
|
||||||
|
|
||||||
def list_events(self, kind: str | None = None, status: str = "pending", limit: int = 100) -> list[dict[str, Any]]:
|
|
||||||
result = []
|
|
||||||
for path in sorted((self.root / status).glob("*.json")):
|
|
||||||
event = self._read(path)
|
|
||||||
if kind and event.get("kind") != kind:
|
|
||||||
continue
|
|
||||||
result.append(event)
|
|
||||||
return result[:limit]
|
|
||||||
|
|
||||||
def get(self, event_id: str) -> dict[str, Any] | None:
|
|
||||||
for state in self.STATES:
|
|
||||||
path = self.root / state / f"{event_id}.json"
|
|
||||||
if path.exists():
|
|
||||||
return self._read(path)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def transition(self, event_id: str, status: str, actor: str | None = None) -> dict[str, Any]:
|
|
||||||
event = self.get(event_id)
|
|
||||||
if not event:
|
|
||||||
return {"status": "not_found", "id": event_id}
|
|
||||||
if status not in self.STATES:
|
|
||||||
return {"status": "invalid_state"}
|
|
||||||
old = event.get("status", "pending")
|
|
||||||
old_path = self.root / old / f"{event_id}.json"
|
|
||||||
event["status"] = status
|
|
||||||
event["actor"] = actor
|
|
||||||
event["updated_at"] = utc_now()
|
|
||||||
self._write(status, event)
|
|
||||||
if old_path.exists() and old != status:
|
|
||||||
old_path.unlink()
|
|
||||||
self._audit("transition", event, {"from": old, "to": status})
|
|
||||||
return event
|
|
||||||
|
|
||||||
def stats(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"events": {state: len(list((self.root / state).glob("*.json"))) for state in self.STATES},
|
|
||||||
"audit_events": len(list((self.root / "audit").glob("*.json"))),
|
|
||||||
"status": "healthy",
|
|
||||||
}
|
|
||||||
|
|
||||||
def _write(self, state: str, data: dict[str, Any]):
|
|
||||||
(self.root / state / f"{data['id']}.json").write_text(
|
|
||||||
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _read(self, path: Path):
|
|
||||||
return json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
def _audit(self, action: str, event: dict[str, Any], extra: dict[str, Any] | None = None):
|
|
||||||
data = {"action": action, "event": event, "extra": extra or {}, "time": utc_now()}
|
|
||||||
(self.root / "audit" / f"{event['id']}-{action}.json").write_text(
|
|
||||||
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentEvent", "AgentEventBus"]
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"""Admin dashboard data model for the unified Agent Event Bus."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections import Counter
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .agent_event_bus import AgentEventBus
|
|
||||||
|
|
||||||
|
|
||||||
class AgentEventDashboard:
|
|
||||||
def __init__(self, bus: AgentEventBus | None = None):
|
|
||||||
self.bus = bus or AgentEventBus()
|
|
||||||
|
|
||||||
def overview(self) -> dict[str, Any]:
|
|
||||||
stats = self.bus.stats()
|
|
||||||
pending = self.bus.list_events(status="pending")
|
|
||||||
running = self.bus.list_events(status="running")
|
|
||||||
completed = self.bus.list_events(status="completed")
|
|
||||||
failed = self.bus.list_events(status="failed")
|
|
||||||
all_visible = pending + running + completed + failed
|
|
||||||
by_kind = Counter(event.get("kind", "unknown") for event in all_visible)
|
|
||||||
by_target = Counter(event.get("target", "unknown") for event in all_visible)
|
|
||||||
return {
|
|
||||||
"module": "agent-event-bus",
|
|
||||||
"title": "Unified Agent Event Bus",
|
|
||||||
"route": "/admin/ai-evolution/events",
|
|
||||||
"metrics": {
|
|
||||||
**stats,
|
|
||||||
"by_kind": dict(by_kind),
|
|
||||||
"by_target": dict(by_target),
|
|
||||||
"gpt_inbox_pending": by_kind.get("gpt_inbox", 0),
|
|
||||||
"openclaw_pending": len([e for e in pending if e.get("target") == "openclaw"]),
|
|
||||||
},
|
|
||||||
"panels": [
|
|
||||||
"event timeline",
|
|
||||||
"pending commands",
|
|
||||||
"GPT inbox",
|
|
||||||
"agent replies",
|
|
||||||
"failure reports",
|
|
||||||
"audit trail",
|
|
||||||
],
|
|
||||||
"audit": {
|
|
||||||
"enabled": True,
|
|
||||||
"location": "agents/events/audit",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentEventDashboard"]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""Bridge Agent Event Bus events into Agent Observability metrics."""
|
|
||||||
|
|
||||||
from .agent_observability_hooks import AgentObservabilityHooks
|
|
||||||
|
|
||||||
|
|
||||||
class AgentEventObservabilityBridge:
|
|
||||||
def __init__(self, hooks: AgentObservabilityHooks | None = None):
|
|
||||||
self.hooks = hooks or AgentObservabilityHooks()
|
|
||||||
|
|
||||||
def consume(self, event: dict):
|
|
||||||
kind = event.get("kind")
|
|
||||||
payload = event.get("payload", {})
|
|
||||||
if kind == "agent_started":
|
|
||||||
return self.hooks.start(
|
|
||||||
user_id=payload.get("user_id", "unknown"),
|
|
||||||
visible_scope=payload.get("scope", "self"),
|
|
||||||
entry_agent=payload.get("agent", "unknown"),
|
|
||||||
execution_id=event.get("id"),
|
|
||||||
)
|
|
||||||
if kind == "agent_delegated":
|
|
||||||
return self.hooks.delegate(event["execution_id"], payload.get("agent", "unknown"))
|
|
||||||
if kind == "token_consumed":
|
|
||||||
return self.hooks.tokens(
|
|
||||||
event["execution_id"],
|
|
||||||
payload.get("input_tokens", 0),
|
|
||||||
payload.get("output_tokens", 0),
|
|
||||||
)
|
|
||||||
if kind in {"agent_completed", "agent_failed"}:
|
|
||||||
return self.hooks.finish(
|
|
||||||
event["execution_id"],
|
|
||||||
payload.get("closed_by"),
|
|
||||||
"failed" if kind == "agent_failed" else "completed",
|
|
||||||
)
|
|
||||||
return {"status": "ignored", "kind": kind}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentEventObservabilityBridge"]
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""Agent orchestration observability model.
|
|
||||||
|
|
||||||
Tracks OpenClaw, Hermes and sub-agent execution chains. Token usage is optional
|
|
||||||
and depends on model adapter instrumentation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict, dataclass, field
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now():
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AgentExecutionRecord:
|
|
||||||
execution_id: str
|
|
||||||
user_id: str
|
|
||||||
visible_scope: str
|
|
||||||
entry_agent: str
|
|
||||||
path: list[str]
|
|
||||||
used_hermes: bool
|
|
||||||
closed_by: str
|
|
||||||
duration_ms: int = 0
|
|
||||||
input_tokens: int | None = None
|
|
||||||
output_tokens: int | None = None
|
|
||||||
total_tokens: int | None = None
|
|
||||||
status: str = "completed"
|
|
||||||
started_at: str = field(default_factory=utc_now)
|
|
||||||
ended_at: str = field(default_factory=utc_now)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentObservability:
|
|
||||||
def __init__(self):
|
|
||||||
self.records: list[AgentExecutionRecord] = []
|
|
||||||
|
|
||||||
def record(self, record: AgentExecutionRecord):
|
|
||||||
self.records.append(record)
|
|
||||||
return record.to_dict()
|
|
||||||
|
|
||||||
def summary(self, user_id: str | None = None) -> dict[str, Any]:
|
|
||||||
records = [r for r in self.records if not user_id or r.user_id == user_id]
|
|
||||||
hermes = [r for r in records if r.used_hermes]
|
|
||||||
direct = [r for r in records if not r.used_hermes]
|
|
||||||
return {
|
|
||||||
"total_executions": len(records),
|
|
||||||
"openclaw_independent_loops": len(direct),
|
|
||||||
"hermes_orchestrated_loops": len(hermes),
|
|
||||||
"average_duration_ms": (
|
|
||||||
sum(r.duration_ms for r in records) / len(records) if records else 0
|
|
||||||
),
|
|
||||||
"token_usage": {
|
|
||||||
"input": sum(r.input_tokens or 0 for r in records),
|
|
||||||
"output": sum(r.output_tokens or 0 for r in records),
|
|
||||||
"total": sum(r.total_tokens or 0 for r in records),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def hermes_tree(self, user_id: str | None = None):
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"execution_id": r.execution_id,
|
|
||||||
"entry": r.entry_agent,
|
|
||||||
"used_hermes": r.used_hermes,
|
|
||||||
"children": r.path,
|
|
||||||
"duration_ms": r.duration_ms,
|
|
||||||
"tokens": r.total_tokens,
|
|
||||||
}
|
|
||||||
for r in self.records
|
|
||||||
if (not user_id or r.user_id == user_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentExecutionRecord", "AgentObservability"]
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"""API for agent orchestration observability."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fastapi import APIRouter
|
|
||||||
except Exception:
|
|
||||||
APIRouter = None
|
|
||||||
|
|
||||||
from .agent_observability import AgentExecutionRecord
|
|
||||||
from .agent_observability_dashboard import AgentObservabilityDashboard
|
|
||||||
from .agent_observability_store import AgentObservabilityStore
|
|
||||||
|
|
||||||
store = AgentObservabilityStore()
|
|
||||||
dashboard = AgentObservabilityDashboard(store.load())
|
|
||||||
|
|
||||||
|
|
||||||
def record_agent_execution(payload: dict[str, Any]):
|
|
||||||
record = AgentExecutionRecord(**payload)
|
|
||||||
return {"status": "ok", "record": store.append(record)}
|
|
||||||
|
|
||||||
|
|
||||||
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
|
|
||||||
records = store.query(user_id, group_id, role)
|
|
||||||
return {
|
|
||||||
"status": "ok",
|
|
||||||
"records": records,
|
|
||||||
"summary": store.load().summary(user_id if role != "owner" else None),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_router():
|
|
||||||
if APIRouter is None:
|
|
||||||
return None
|
|
||||||
router = APIRouter(prefix="/admin/ai-evolution/agents", tags=["agent-observability"])
|
|
||||||
|
|
||||||
@router.post("/execution", operation_id="recordAgentExecution")
|
|
||||||
def record(payload: dict[str, Any]):
|
|
||||||
return record_agent_execution(payload)
|
|
||||||
|
|
||||||
@router.get("/overview", operation_id="getAgentObservabilityOverview")
|
|
||||||
def overview(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
|
|
||||||
return get_agent_observability(user_id, group_id, role)
|
|
||||||
|
|
||||||
return router
|
|
||||||
|
|
||||||
|
|
||||||
router = build_router()
|
|
||||||
|
|
||||||
__all__ = ["router", "record_agent_execution", "get_agent_observability"]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"""Web UI dashboard model for agent execution visibility."""
|
|
||||||
|
|
||||||
from .agent_observability import AgentObservability
|
|
||||||
|
|
||||||
|
|
||||||
class AgentObservabilityDashboard:
|
|
||||||
def __init__(self, observability: AgentObservability | None = None):
|
|
||||||
self.observability = observability or AgentObservability()
|
|
||||||
|
|
||||||
def overview(self, user_id: str | None = None):
|
|
||||||
return {
|
|
||||||
"route": "/admin/ai-evolution/agents",
|
|
||||||
"title": "Agent Orchestration Observability",
|
|
||||||
"summary": self.observability.summary(user_id),
|
|
||||||
"views": [
|
|
||||||
{
|
|
||||||
"name": "scheduler_flow",
|
|
||||||
"title": "OpenClaw/Hermes 调度流程",
|
|
||||||
"data": self.observability.hermes_tree(user_id),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "agent_metrics",
|
|
||||||
"title": "智能体指标",
|
|
||||||
"fields": [
|
|
||||||
"duration_ms",
|
|
||||||
"input_tokens",
|
|
||||||
"output_tokens",
|
|
||||||
"total_tokens",
|
|
||||||
"status",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "loop_distribution",
|
|
||||||
"title": "闭环分布",
|
|
||||||
"fields": [
|
|
||||||
"openclaw_independent_loops",
|
|
||||||
"hermes_orchestrated_loops",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentObservabilityDashboard"]
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
"""Runtime hooks for Agent Observability.
|
|
||||||
|
|
||||||
OpenClaw/Hermes/sub-agents can call these hooks at start/end/delegation points.
|
|
||||||
The hooks store sanitized metadata only: no prompts, no secrets, no private message
|
|
||||||
content.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from .agent_observability import AgentExecutionRecord
|
|
||||||
from .agent_observability_store import AgentObservabilityStore
|
|
||||||
|
|
||||||
|
|
||||||
def utc_now() -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_time(value: str) -> datetime:
|
|
||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AgentRunContext:
|
|
||||||
execution_id: str
|
|
||||||
user_id: str
|
|
||||||
visible_scope: str
|
|
||||||
entry_agent: str
|
|
||||||
path: list[str] = field(default_factory=list)
|
|
||||||
used_hermes: bool = False
|
|
||||||
started_at: str = field(default_factory=utc_now)
|
|
||||||
input_tokens: int = 0
|
|
||||||
output_tokens: int = 0
|
|
||||||
status: str = "running"
|
|
||||||
|
|
||||||
def delegate(self, agent_name: str) -> None:
|
|
||||||
if agent_name not in self.path:
|
|
||||||
self.path.append(agent_name)
|
|
||||||
if agent_name.lower() == "hermes":
|
|
||||||
self.used_hermes = True
|
|
||||||
|
|
||||||
def add_tokens(self, input_tokens: int = 0, output_tokens: int = 0) -> None:
|
|
||||||
self.input_tokens += max(input_tokens, 0)
|
|
||||||
self.output_tokens += max(output_tokens, 0)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentObservabilityHooks:
|
|
||||||
def __init__(self, store: AgentObservabilityStore | None = None):
|
|
||||||
self.store = store or AgentObservabilityStore()
|
|
||||||
self.active: dict[str, AgentRunContext] = {}
|
|
||||||
|
|
||||||
def start(
|
|
||||||
self,
|
|
||||||
user_id: str,
|
|
||||||
visible_scope: str,
|
|
||||||
entry_agent: str,
|
|
||||||
execution_id: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
run = AgentRunContext(
|
|
||||||
execution_id=execution_id or f"exec_{uuid4().hex[:12]}",
|
|
||||||
user_id=user_id,
|
|
||||||
visible_scope=visible_scope,
|
|
||||||
entry_agent=entry_agent,
|
|
||||||
path=[entry_agent],
|
|
||||||
used_hermes=entry_agent.lower() == "hermes",
|
|
||||||
)
|
|
||||||
self.active[run.execution_id] = run
|
|
||||||
return {"status": "started", "execution_id": run.execution_id, "run": run.__dict__}
|
|
||||||
|
|
||||||
def delegate(self, execution_id: str, agent_name: str) -> dict[str, Any]:
|
|
||||||
run = self.active[execution_id]
|
|
||||||
run.delegate(agent_name)
|
|
||||||
return {"status": "delegated", "execution_id": execution_id, "path": run.path}
|
|
||||||
|
|
||||||
def tokens(self, execution_id: str, input_tokens: int = 0, output_tokens: int = 0) -> dict[str, Any]:
|
|
||||||
run = self.active[execution_id]
|
|
||||||
run.add_tokens(input_tokens, output_tokens)
|
|
||||||
return {
|
|
||||||
"status": "tokens_recorded",
|
|
||||||
"execution_id": execution_id,
|
|
||||||
"input_tokens": run.input_tokens,
|
|
||||||
"output_tokens": run.output_tokens,
|
|
||||||
"total_tokens": run.input_tokens + run.output_tokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
def finish(self, execution_id: str, closed_by: str | None = None, status: str = "completed") -> dict[str, Any]:
|
|
||||||
run = self.active.pop(execution_id)
|
|
||||||
ended_at = utc_now()
|
|
||||||
duration_ms = int((parse_time(ended_at) - parse_time(run.started_at)).total_seconds() * 1000)
|
|
||||||
record = AgentExecutionRecord(
|
|
||||||
execution_id=run.execution_id,
|
|
||||||
user_id=run.user_id,
|
|
||||||
visible_scope=run.visible_scope,
|
|
||||||
entry_agent=run.entry_agent,
|
|
||||||
path=run.path,
|
|
||||||
used_hermes=run.used_hermes,
|
|
||||||
closed_by=closed_by or run.path[-1],
|
|
||||||
duration_ms=duration_ms,
|
|
||||||
input_tokens=run.input_tokens,
|
|
||||||
output_tokens=run.output_tokens,
|
|
||||||
total_tokens=run.input_tokens + run.output_tokens,
|
|
||||||
status=status,
|
|
||||||
started_at=run.started_at,
|
|
||||||
ended_at=ended_at,
|
|
||||||
)
|
|
||||||
return {"status": "stored", "record": self.store.append(record)}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentRunContext", "AgentObservabilityHooks"]
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"""Persistent store for agent observability records.
|
|
||||||
|
|
||||||
Stores sanitized execution metrics for OpenClaw/Hermes/sub-agents. No prompts,
|
|
||||||
secrets, or private message contents are persisted here; only metrics and routing
|
|
||||||
metadata are stored for Web UI statistics and audit.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .agent_observability import AgentExecutionRecord, AgentObservability
|
|
||||||
|
|
||||||
|
|
||||||
class AgentObservabilityStore:
|
|
||||||
def __init__(self, root: str = "runtime/agent-observability"):
|
|
||||||
self.root = Path(root)
|
|
||||||
self.records_dir = self.root / "records"
|
|
||||||
self.records_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def append(self, record: AgentExecutionRecord) -> dict[str, Any]:
|
|
||||||
path = self.records_dir / f"{record.execution_id}.json"
|
|
||||||
data = record.to_dict()
|
|
||||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
||||||
return data
|
|
||||||
|
|
||||||
def load(self) -> AgentObservability:
|
|
||||||
observability = AgentObservability()
|
|
||||||
for path in sorted(self.records_dir.glob("*.json")):
|
|
||||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
observability.records.append(AgentExecutionRecord(**raw))
|
|
||||||
return observability
|
|
||||||
|
|
||||||
def query(self, user_id: str | None = None, group_id: str | None = None, role: str = "user") -> list[dict[str, Any]]:
|
|
||||||
records = [record.to_dict() for record in self.load().records]
|
|
||||||
if role == "owner":
|
|
||||||
return records
|
|
||||||
if role == "group_admin" and group_id:
|
|
||||||
return [r for r in records if r.get("visible_scope") == group_id or r.get("user_id") == user_id]
|
|
||||||
return [r for r in records if r.get("user_id") == user_id]
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AgentObservabilityStore"]
|
|
||||||
Executable → Regular
+1
-99
@@ -200,11 +200,6 @@ class H(BaseHTTPRequestHandler):
|
|||||||
latest=monitor.latest()
|
latest=monitor.latest()
|
||||||
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
|
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
|
||||||
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
|
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
|
||||||
# Admin AI Evolution Dashboard
|
|
||||||
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
|
|
||||||
return self.ai_evolution_page()
|
|
||||||
if path in ('/admin/ai-evolution/api/data'):
|
|
||||||
return self.ai_evolution_data()
|
|
||||||
return self.sendj({'error':'not_found','path':path},404)
|
return self.sendj({'error':'not_found','path':path},404)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
monitor.record_failure('do_GET', str(e), {'path':path})
|
monitor.record_failure('do_GET', str(e), {'path':path})
|
||||||
@@ -295,102 +290,9 @@ class H(BaseHTTPRequestHandler):
|
|||||||
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
|
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
|
||||||
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
|
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
|
||||||
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
|
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
|
||||||
_EVOLUTION_HTML = '''<!DOCTYPE html>
|
|
||||||
<html lang=zh-CN>
|
|
||||||
<head>
|
|
||||||
<meta charset=UTF-8>
|
|
||||||
<meta name=viewport content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>AIOS Evolution Dashboard</title>
|
|
||||||
<script src=https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js></script>
|
|
||||||
<style>
|
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
|
||||||
body{background:#0d1117;color:#c9d1d9;font-size:14px;line-height:1.7;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif}
|
|
||||||
.c{max-width:1000px;margin:0 auto;padding:32px 24px}
|
|
||||||
h1{font-size:24px;color:#58a6ff;margin-bottom:2px}
|
|
||||||
.sub{color:#8b949e;font-size:13px;border-bottom:1px solid #21262d;padding-bottom:16px;margin-bottom:20px;display:flex;justify-content:space-between}
|
|
||||||
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px;margin-bottom:20px}
|
|
||||||
.s{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:18px;text-align:center}
|
|
||||||
.s .v{font-size:28px;font-weight:700;color:#f0f6fc}
|
|
||||||
.s .l{font-size:11px;color:#8b949e;margin-top:4px}
|
|
||||||
.bx{background:#161b22;border:1px solid #21262d;border-radius:8px;margin-bottom:14px}
|
|
||||||
.bh{border-bottom:1px solid #21262d;padding:10px 16px;font-weight:600;font-size:14px;display:flex;gap:6px;align-items:center}
|
|
||||||
.bd{padding:12px 16px}
|
|
||||||
.rw{display:grid;grid-template-columns:1fr 1fr 100px 60px;gap:8px;padding:8px 16px;border-bottom:1px solid #21262d;font-size:13px;align-items:center;cursor:pointer;transition:background .15s}
|
|
||||||
.rw:hover{background:#1c2128}.rw:last-child{border-bottom:none}
|
|
||||||
.tg{display:inline-block;padding:2px 10px;border-radius:99px;font-size:11px;font-weight:600}
|
|
||||||
.ok{background:#3fb95033;color:#3fb950}.fl{background:#da363333;color:#f85149}.if{background:#1f6feb33;color:#58a6ff}
|
|
||||||
pre{background:#0d1117;padding:12px;border-radius:6px;overflow:auto;font-size:12px;color:#8b949e;max-height:300px;margin-top:8px}select{padding:8px 10px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;font-size:13px;max-width:400px;width:100%;margin-bottom:4px;cursor:pointer}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id=app class=c>
|
|
||||||
<h1>AIOS Evolution Dashboard</h1>
|
|
||||||
<div class=sub><span>Baseline: {{baseline}}</span><span style=color:#8b949e>{{refreshMsg}}</span></div>
|
|
||||||
<div class=g><div class=s><div class=v>{{tot}}</div><div class=l>Commands</div></div><div class=s><div class=v style=color:#3fb950>{{sok}}</div><div class=l>Succeeded</div></div><div class=s><div class=v style=color:#f85149>{{sfail}}</div><div class=l>Failed</div></div><div class=s><div class=v>{{runs}}</div><div class=l>Executions</div></div></div>
|
|
||||||
<div class=bx><div class=bh>Version Timeline</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Version</span><span>Time</span><span>Result</span></div><div v-for=v in vs class=rw @click=selVer(v)><span style=color:#58a6ff>{{v.tag}}</span><span>{{v.time||'-'}}</span><span :class="'tg '+v.result">{{v.result||'-'}}</span></div></div></div>
|
|
||||||
<div class=bx><div class=bh>Commands</div><div class=bd><select v-model=cmd @change=loadCmd><option value>-- Select --</option><option v-for=c in cmds :value=c>{{c}}</option></select><pre v-if=cdet>{{cdet}}</pre></div></div>
|
|
||||||
<div class=bx><div class=bh>Recent Agent Runs</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Agent</span><span>Command</span><span>Count</span><span>Result</span></div><div v-for=r in aruns class=rw><span>{{r.agent||'-'}}</span><span style=overflow:hidden;white-space:nowrap;text-overflow:ellipsis>{{r.command}}</span><span>{{r.interaction_count||0}}</span><span :class="'tg '+r.final_result">{{r.final_result||'-'}}</span></div></div></div></div>
|
|
||||||
<script>
|
|
||||||
const{createApp,ref,onMounted}=Vue;createApp({setup(){const vs=ref([]),cmds=ref([]),ar=ref([]),cmd=ref(''),cdet=ref(null),tot=ref(0),sok=ref(0),sfail=ref(0),runs=ref(0),baseline=ref('V1R3C1'),refreshMsg=ref('')
|
|
||||||
async function ld(){try{const r=await fetch('/admin/ai-evolution/api/data'),d=await r.json();baseline.value=d.baseline||'V1R3C1';vs.value=d.versions||[];cmds.value=d.commands||[];ar.value=d.agent_runs||[];tot.value=d.stats?.total||0;sok.value=d.stats?.success||0;sfail.value=d.stats?.failed||0;runs.value=d.stats?.agent_runs||0;refreshMsg.value='Last updated: '+new Date().toLocaleString()}catch(e){}}
|
|
||||||
async function loadCmd(){if(!cmd.value){cdet.value=null;return}try{const r=await fetch('/admin/ai-evolution/api/data?command='+encodeURIComponent(cmd.value)),d=await r.json();cdet.value=JSON.stringify(d,null,2)}catch(e){cdet.value='Error'}}
|
|
||||||
function selVer(v){cmd.value=v.tag||v.id||v.version;loadCmd()}
|
|
||||||
onMounted(()=>{ld();setInterval(ld,30000)})
|
|
||||||
return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer}})mount('#app')
|
|
||||||
</script></body></html>'''
|
|
||||||
|
|
||||||
def ai_evolution_page(self):
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
self.send_header('Content-Length', str(len(self._EVOLUTION_HTML)))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(self._EVOLUTION_HTML.encode())
|
|
||||||
|
|
||||||
def ai_evolution_data(self):
|
|
||||||
q=urllib.parse.urlparse(self.path).query
|
|
||||||
params=urllib.parse.parse_qs(q)
|
|
||||||
cmd_filter=(params.get('command') or [None])[0]
|
|
||||||
bl=jload(WS/'baseline'/'current.json',{})
|
|
||||||
current_ver=bl.get('current_baseline','V1R3C1')
|
|
||||||
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
|
|
||||||
versions=[]
|
|
||||||
if (WS/'baseline'/'promotions').exists():
|
|
||||||
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
|
|
||||||
d=jload(p,{});v=d.get('version',p.stem)
|
|
||||||
versions.append({'tag':v,'time':d.get('promoted_at','-'),'result':'ok'})
|
|
||||||
for t in tags:
|
|
||||||
if not any(v['tag']==t for v in versions):
|
|
||||||
versions.append({'tag':t,'time':'-','result':'if'})
|
|
||||||
agent_runs=[];ard=WS/'audit'/'agent-runs'
|
|
||||||
if ard.exists():
|
|
||||||
for f in sorted(ard.glob('*.json'),reverse=True)[:50]:
|
|
||||||
d=jload(f,{});agent_runs.append(d) if isinstance(d,dict) else None
|
|
||||||
cmd_dirs=[WS/'agents/commands/active',WS/'agents/commands/running',WS/'agents/commands/completed',WS/'agents/commands/failed']
|
|
||||||
commands=[]
|
|
||||||
for d in cmd_dirs:
|
|
||||||
if d.exists():
|
|
||||||
for f in sorted(d.glob('*.md')):
|
|
||||||
commands.append(f.name)
|
|
||||||
command_detail=None
|
|
||||||
if cmd_filter:
|
|
||||||
for d in cmd_dirs:
|
|
||||||
if d.exists():
|
|
||||||
fp=d/cmd_filter
|
|
||||||
if fp.exists():
|
|
||||||
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
|
|
||||||
break
|
|
||||||
if not command_detail:
|
|
||||||
command_detail={'command':cmd_filter,'error':'not_found'}
|
|
||||||
return self.sendj(command_detail)
|
|
||||||
completed=list((WS/'agents/commands/completed').glob('*.md')) if (WS/'agents/commands/completed').exists() else []
|
|
||||||
failed=list((WS/'agents/commands/failed').glob('*.md')) if (WS/'agents/commands/failed').exists() else []
|
|
||||||
stats={'total':len(commands),'success':len(completed),'failed':len(failed),'agent_runs':len(agent_runs)}
|
|
||||||
data={'baseline':current_ver,'versions':versions[-30:],'agent_runs':agent_runs,'commands':sorted(set(commands)),'stats':stats}
|
|
||||||
self.sendj(data)
|
|
||||||
|
|
||||||
|
|
||||||
def api_spec():
|
def api_spec():
|
||||||
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
|
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>AI Evolution Dashboard</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0d1117; color: #c9d1d9; height: 100vh; overflow: hidden; }
|
|
||||||
.app { display: flex; height: 100vh; }
|
|
||||||
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
|
||||||
.sidebar h1 { font-size: 18px; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 12px; margin-bottom: 8px; }
|
|
||||||
.sidebar-nav { list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
|
||||||
.sidebar-nav li { padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; color: #8b949e; }
|
|
||||||
.sidebar-nav li:hover, .sidebar-nav li.active { background: #1f6feb22; color: #58a6ff; }
|
|
||||||
.sidebar-nav li i { margin-right: 8px; }
|
|
||||||
.main { flex: 1; padding: 24px; overflow-y: auto; }
|
|
||||||
.main-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
||||||
.main-header h2 { font-size: 20px; }
|
|
||||||
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
|
||||||
.status-badge.ok { background: #1b3a2d; color: #3fb950; border: 1px solid #3fb95044; }
|
|
||||||
.status-badge.degraded { background: #3a2e1b; color: #d29922; border: 1px solid #d2992244; }
|
|
||||||
.status-badge.fail { background: #3a1b1b; color: #f85149; border: 1px solid #f8514944; }
|
|
||||||
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
||||||
.card-title { font-size: 14px; color: #8b949e; margin-bottom: 8px; }
|
|
||||||
.card-value { font-size: 28px; font-weight: 700; color: #f0f6fc; }
|
|
||||||
.row { display: flex; gap: 16px; }
|
|
||||||
.col { flex: 1; }
|
|
||||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
|
||||||
pre { white-space: pre-wrap; word-break: break-word; }
|
|
||||||
.command-item { padding: 12px; margin-bottom: 8px; border-radius: 6px; background: #0d1117; border: 1px solid #21262d; }
|
|
||||||
.command-item .meta { display: flex; gap: 8px; align-items: center; font-size: 12px; color: #8b949e; }
|
|
||||||
.command-item .name { font-size: 14px; color: #58a6ff; font-weight: 600; }
|
|
||||||
.command-item .state { font-size: 11px; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; }
|
|
||||||
.state-active { background: #1b3a2d; color: #3fb950; }
|
|
||||||
.state-running { background: #3a2e1b; color: #d29922; }
|
|
||||||
.state-completed { background: #1b3a2d; color: #3fb950; }
|
|
||||||
.state-failed { background: #3a1b1b; color: #f85149; }
|
|
||||||
.loading { text-align: center; padding: 40px; color: #8b949e; }
|
|
||||||
.error { padding: 20px; background: #3a1b1b; border: 1px solid #f8514944; border-radius: 8px; color: #f85149; margin-bottom: 16px; }
|
|
||||||
.version-item { padding: 8px 12px; background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
||||||
.version-item .tag { color: #58a6ff; font-weight: 600; }
|
|
||||||
.version-item .name { color: #8b949e; font-size: 13px; }
|
|
||||||
.agent-item { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 8px; padding: 12px; }
|
|
||||||
.agent-item .agent-title { font-size: 14px; font-weight: 600; color: #c9d1d9; margin-bottom: 4px; }
|
|
||||||
.agent-item .agent-meta { font-size: 12px; color: #8b949e; }
|
|
||||||
.agent-run-item { padding: 8px 12px; margin-bottom: 4px; border-radius: 4px; background: #0d1117; border: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; }
|
|
||||||
.agent-run-item .run-id { color: #58a6ff; font-size: 13px; font-weight: 600; }
|
|
||||||
.agent-run-item .source { color: #8b949e; font-size: 12px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app" class="app">
|
|
||||||
<div class="sidebar">
|
|
||||||
<h1>🕹️ AI Evolution Dashboard</h1>
|
|
||||||
<ul class="sidebar-nav">
|
|
||||||
<li class="active" onclick="showPanel('overview')"><i>📊</i> Overview</li>
|
|
||||||
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
|
||||||
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
|
||||||
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
|
||||||
<div id="panel-overview" class="panel">
|
|
||||||
<div class="main-header">
|
|
||||||
<h2>System Overview</h2>
|
|
||||||
<div id="header-status" class="status-badge">Loading...</div>
|
|
||||||
</div>
|
|
||||||
<div id="loading" class="loading">Loading...</div>
|
|
||||||
<div id="error" class="error" style="display:none;"></div>
|
|
||||||
<div id="content">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div>
|
|
||||||
<div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div>
|
|
||||||
<div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="margin-top:16px">
|
|
||||||
<div class="card-title">Event Bus Status</div>
|
|
||||||
<pre id="bus-status">-</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="panel-versions" class="panel" style="display:none;">
|
|
||||||
<div class="main-header"><h2>Version Timeline</h2></div>
|
|
||||||
<div id="versions-list">Loading...</div>
|
|
||||||
</div>
|
|
||||||
<div id="panel-commands" class="panel" style="display:none;">
|
|
||||||
<div class="main-header"><h2>Command Lookup</h2></div>
|
|
||||||
<div id="commands-list">Loading...</div>
|
|
||||||
</div>
|
|
||||||
<div id="panel-agents" class="panel" style="display:none;">
|
|
||||||
<div class="main-header"><h2>Recent Agent Executions</h2></div>
|
|
||||||
<div id="agents-list">Loading...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
async function fetchData() {
|
|
||||||
const r = await fetch('/admin/ai-evolution/api/data');
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Agent Acknowledgement
|
|
||||||
Initialized by CMD-20260708-fix-git-actions-write-and-enable-audit-plane
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 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
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# AIOS Agent Policies
|
|
||||||
Policies for AIOS Git Agent execution.
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
# AIOS Agent Command Plane Registry
|
|
||||||
# Git is the single source of truth for all agent commands.
|
|
||||||
version: 1.0.0
|
|
||||||
created_at: 2026-07-08T15:20:00+08:00
|
|
||||||
baseline: V1R3C1
|
|
||||||
|
|
||||||
commands:
|
|
||||||
source: "git"
|
|
||||||
active_path: "agents/commands/active/"
|
|
||||||
running_path: "agents/commands/running/"
|
|
||||||
completed_path: "agents/commands/completed/"
|
|
||||||
failed_path: "agents/commands/failed/"
|
|
||||||
|
|
||||||
policies:
|
|
||||||
path: "agents/policies/"
|
|
||||||
source: "git"
|
|
||||||
|
|
||||||
state:
|
|
||||||
path: "agents/state/"
|
|
||||||
source: "git"
|
|
||||||
|
|
||||||
agents:
|
|
||||||
- id: openclaw
|
|
||||||
type: executor
|
|
||||||
role: primary
|
|
||||||
- id: hermes
|
|
||||||
type: reviewer
|
|
||||||
role: supporting
|
|
||||||
- id: codex
|
|
||||||
type: reviewer
|
|
||||||
role: review
|
|
||||||
|
|
||||||
# Audit plane configuration
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
path: "audit/"
|
|
||||||
agent_runs: "audit/agent-runs/"
|
|
||||||
version_history: "audit/version-history/"
|
|
||||||
command_history: "audit/command-history/"
|
|
||||||
=======
|
|
||||||
# AIOS Agent Command Registry
|
|
||||||
# Git is the single source of truth for all agent commands.
|
|
||||||
|
|
||||||
registry_version: 1.0.0
|
|
||||||
created_at: "2026-07-08T15:44:00+08:00"
|
|
||||||
baseline: V1R3C1
|
|
||||||
|
|
||||||
directories:
|
|
||||||
commands/active: "New commands pending execution — Git is the only entry point"
|
|
||||||
commands/running: "Commands currently being executed by an agent"
|
|
||||||
commands/completed: "Commands that finished successfully — moved here on success"
|
|
||||||
commands/failed: "Commands that finished with errors — moved here on failure"
|
|
||||||
|
|
||||||
state: agents/state/
|
|
||||||
policies: agents/policies/
|
|
||||||
|
|
||||||
lifecycle:
|
|
||||||
new: active/
|
|
||||||
running: running/
|
|
||||||
success: completed/
|
|
||||||
failure: failed/
|
|
||||||
|
|
||||||
# Command lifecycle state machine:
|
|
||||||
# active/ → locked & moved to running/ → execute → success → completed/
|
|
||||||
# → failure → failed/
|
|
||||||
>>>>>>> 4d18ee0 (feat(cmd): enable Agent Command Plane, audit plane, and Web UI dashboard)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"owner_approved": true, "version": "V1R4C0-test"}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
AIOS Agent Command Watcher
|
|
||||||
|
|
||||||
Scans agents/commands/active/ periodically.
|
|
||||||
Dispatches new .md commands to the AIOS agent runtime.
|
|
||||||
Supports: restart recovery, retry, dedup, state persistence.
|
|
||||||
|
|
||||||
Runs as a standalone HTTP service or polling loop.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
WORKSPACE = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace"))
|
|
||||||
ACTIVE_DIR = WORKSPACE / "agents/commands/active/"
|
|
||||||
RUNNING_DIR = WORKSPACE / "agents/commands/running/"
|
|
||||||
COMPLETED_DIR = WORKSPACE / "agents/commands/completed/"
|
|
||||||
FAILED_DIR = WORKSPACE / "agents/commands/failed/"
|
|
||||||
STATE_FILE = WORKSPACE / "agents/state/watcher-state.json"
|
|
||||||
AUDIT_DIR = WORKSPACE / "audit/agent-runs/"
|
|
||||||
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", "300"))
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s [watcher] %(levelname)s %(message)s",
|
|
||||||
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
||||||
)
|
|
||||||
log = logging.getLogger("command-watcher")
|
|
||||||
|
|
||||||
|
|
||||||
def compute_file_hash(path: Path) -> str:
|
|
||||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def load_state() -> dict:
|
|
||||||
if STATE_FILE.exists():
|
|
||||||
try:
|
|
||||||
return json.loads(STATE_FILE.read_text())
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
return {"processed": {}, "last_scan": None}
|
|
||||||
|
|
||||||
|
|
||||||
def save_state(state: dict):
|
|
||||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
def discover_new_commands(state: dict) -> list[Path]:
|
|
||||||
"""Return paths of commands not yet processed (by content hash)."""
|
|
||||||
if not ACTIVE_DIR.exists():
|
|
||||||
return []
|
|
||||||
new_cmds = []
|
|
||||||
for cmd_file in sorted(ACTIVE_DIR.iterdir()):
|
|
||||||
if not cmd_file.name.endswith(".md"):
|
|
||||||
continue
|
|
||||||
fhash = compute_file_hash(cmd_file)
|
|
||||||
prev = state["processed"].get(cmd_file.name)
|
|
||||||
if prev and prev["hash"] == fhash and prev.get("status") in ("completed", "running"):
|
|
||||||
continue
|
|
||||||
new_cmds.append(cmd_file)
|
|
||||||
return new_cmds
|
|
||||||
|
|
||||||
|
|
||||||
def write_audit_record(cmd_name: str, status: str, agent: str = "openclaw",
|
|
||||||
details: dict = None) -> dict:
|
|
||||||
record = {
|
|
||||||
"agent_run_id": f"{agent}-{int(time.time())}",
|
|
||||||
"command": cmd_name,
|
|
||||||
"agent": agent,
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"objective": f"Command: {cmd_name}",
|
|
||||||
"start_time": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"end_time": None,
|
|
||||||
"interaction_count": 0,
|
|
||||||
"tool_call_count": 0,
|
|
||||||
"changed_files": [],
|
|
||||||
"commit_sha": None,
|
|
||||||
"test_result": None,
|
|
||||||
"final_result": status,
|
|
||||||
"evidence_path": f"evidence/{cmd_name.replace('.md', '')}/",
|
|
||||||
**(details or {}),
|
|
||||||
}
|
|
||||||
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
audit_file = AUDIT_DIR / f"{record['agent_run_id']}.json"
|
|
||||||
audit_file.write_text(json.dumps(record, indent=2, ensure_ascii=False))
|
|
||||||
return record
|
|
||||||
|
|
||||||
|
|
||||||
def execute_command(cmd_path: Path) -> bool:
|
|
||||||
"""Execute a command via the agent runtime.
|
|
||||||
|
|
||||||
Move the command to running/ first, then trigger execution.
|
|
||||||
For now, this marks the command lifecycle. Actual execution
|
|
||||||
is handled by the main agent session.
|
|
||||||
"""
|
|
||||||
cmd_name = cmd_path.name
|
|
||||||
target = RUNNING_DIR / cmd_name
|
|
||||||
|
|
||||||
# Move to running
|
|
||||||
log.info(f"Moving {cmd_name} → running/")
|
|
||||||
cmd_path.rename(target)
|
|
||||||
|
|
||||||
# Write audit record
|
|
||||||
write_audit_record(cmd_name, "running", agent="openclaw")
|
|
||||||
|
|
||||||
try:
|
|
||||||
log.info(f"Executing {cmd_name} via agent runtime...")
|
|
||||||
# The actual execution happens in the main agent session.
|
|
||||||
# This watcher ensures lifecycle management.
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"Execution failed for {cmd_name}: {e}")
|
|
||||||
(RUNNING_DIR / cmd_name).rename(FAILED_DIR / cmd_name)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def scan_and_dispatch():
|
|
||||||
state = load_state()
|
|
||||||
state["last_scan"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
||||||
|
|
||||||
new_cmds = discover_new_commands(state)
|
|
||||||
|
|
||||||
if not new_cmds:
|
|
||||||
log.debug("No new commands found.")
|
|
||||||
save_state(state)
|
|
||||||
return
|
|
||||||
|
|
||||||
for cmd_path in new_cmds:
|
|
||||||
cmd_name = cmd_path.name
|
|
||||||
log.info(f"Discovered new command: {cmd_name}")
|
|
||||||
state["processed"][cmd_name] = {
|
|
||||||
"hash": compute_file_hash(cmd_path),
|
|
||||||
"status": "discovered",
|
|
||||||
"discovered_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
}
|
|
||||||
save_state(state)
|
|
||||||
|
|
||||||
# Execute (move to running)
|
|
||||||
execute_command(cmd_path)
|
|
||||||
|
|
||||||
state["processed"][cmd_name]["status"] = "running"
|
|
||||||
save_state(state)
|
|
||||||
|
|
||||||
log.info(f"Dispatched {len(new_cmds)} command(s)")
|
|
||||||
|
|
||||||
|
|
||||||
def run_forever():
|
|
||||||
log.info(f"Command Watcher started. Poll interval: {POLL_INTERVAL}s")
|
|
||||||
log.info(f"Active dir: {ACTIVE_DIR}")
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
scan_and_dispatch()
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(f"Scan error: {e}")
|
|
||||||
time.sleep(POLL_INTERVAL)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "--once":
|
|
||||||
scan_and_dispatch()
|
|
||||||
else:
|
|
||||||
run_forever()
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
AIOS Autonomous Controller — Full Pipeline v2.0
|
|
||||||
|
|
||||||
Pipeline: Detect -> Diagnose -> Fix Plan -> Patch Generate -> Owner Approval -> Apply -> Notify
|
|
||||||
|
|
||||||
Complete autonomous cycle with notification integration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json, os, hashlib, time, sys, logging, subprocess, requests
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
WORKSPACE = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace"))
|
|
||||||
PENDING_DIR = WORKSPACE / "agents" / "patches" / "pending/"
|
|
||||||
APPROVED_DIR = WORKSPACE / "agents" / "patches" / "approved/"
|
|
||||||
APPLIED_DIR = WORKSPACE / "agents" / "patches" / "applied/"
|
|
||||||
REJECTED_DIR = WORKSPACE / "agents" / "patches" / "rejected/"
|
|
||||||
AUDIT_DIR = WORKSPACE / "audit" / "controller-runs/"
|
|
||||||
STATE_FILE = WORKSPACE / "agents" / "state" / "controller-state.json"
|
|
||||||
HEALTH_FILE = WORKSPACE / "agents" / "state" / "health.json"
|
|
||||||
NOTIF_DIR = WORKSPACE / "agents" / "notifications/"
|
|
||||||
ACTIVE_DIR = WORKSPACE / "agents" / "commands" / "active/"
|
|
||||||
RUNNING_DIR = WORKSPACE / "agents" / "commands" / "running/"
|
|
||||||
COMPLETED_DIR = WORKSPACE / "agents" / "commands" / "completed/"
|
|
||||||
FAILED_DIR = WORKSPACE / "agents" / "commands" / "failed/"
|
|
||||||
POLL_INTERVAL = int(os.environ.get("CONTROLLER_POLL_INTERVAL_SECONDS", "600"))
|
|
||||||
GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000")
|
|
||||||
API_URL = os.environ.get("AIOS_API_URL", "http://127.0.0.1:18890")
|
|
||||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO,format="%(asctime)s [controller] %(levelname)s %(message)s",datefmt="%Y-%m-%dT%H:%M:%S%z")
|
|
||||||
log = logging.getLogger("autonomous-controller")
|
|
||||||
CONTROLLER_VERSION = "2.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
def load_state():
|
|
||||||
if STATE_FILE.exists():
|
|
||||||
try: return json.loads(STATE_FILE.read_text())
|
|
||||||
except: pass
|
|
||||||
return {"cv": CONTROLLER_VERSION, "checks": {}, "patches": {}, "notifs": [], "applied": []}
|
|
||||||
|
|
||||||
def save_state(s):
|
|
||||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
STATE_FILE.write_text(json.dumps(s, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
def audit(name, status, details):
|
|
||||||
r = {"run_id": f"c-{int(time.time())}", "check": name, "status": status,
|
|
||||||
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "details": details}
|
|
||||||
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
(AUDIT_DIR / f"{r['run_id']}.json").write_text(json.dumps(r, indent=2, ensure_ascii=False))
|
|
||||||
return r
|
|
||||||
|
|
||||||
def notify(title, body, urgency="info"):
|
|
||||||
n = {"nid": f"n-{int(time.time())}", "title": title, "body": body, "urgency": urgency,
|
|
||||||
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z")}
|
|
||||||
NOTIF_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
(NOTIF_DIR / f"{n['nid']}.json").write_text(json.dumps(n, indent=2, ensure_ascii=False))
|
|
||||||
log.info(f"NOTIF: {n['nid']} -> {title}")
|
|
||||||
return n
|
|
||||||
|
|
||||||
# === Health Checks ===
|
|
||||||
|
|
||||||
def h_gitea():
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{GITEA_URL}/api/v1/version", timeout=5)
|
|
||||||
return {"ok": r.status_code == 200, "data": r.json() if r.status_code == 200 else None, "err": None if r.status_code == 200 else f"HTTP {r.status_code}"}
|
|
||||||
except Exception as e: return {"ok": False, "err": str(e)}
|
|
||||||
|
|
||||||
def h_api():
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{API_URL}/admin/ai-evolution/api/data", timeout=5)
|
|
||||||
if r.status_code == 200:
|
|
||||||
d = r.json(); b = d.get("baseline","?"); v = len(d.get("versions",[]))
|
|
||||||
return {"ok": True, "baseline": b, "versions": v}
|
|
||||||
return {"ok": False, "err": f"HTTP {r.status_code}"}
|
|
||||||
except Exception as e: return {"ok": False, "err": str(e)}
|
|
||||||
|
|
||||||
def h_branch_sync():
|
|
||||||
bare = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git")
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["git","--git-dir",str(bare),"branch","-a"], capture_output=True, text=True, timeout=10)
|
|
||||||
if r.returncode != 0: return {"ok": False, "err": r.stderr[:200]}
|
|
||||||
bb = set(l.strip().replace("remotes/origin/","").replace("* ","") for l in r.stdout.strip().split("\n") if l.strip())
|
|
||||||
gb = []; pg = 1
|
|
||||||
while True:
|
|
||||||
r2 = requests.get(f"{GITEA_URL}/api/v1/repos/aios/aios-v23/branches", params={"page":pg,"limit":50},
|
|
||||||
headers={"Authorization":f"token {GITEA_TOKEN}"}, timeout=10)
|
|
||||||
if r2.status_code != 200: break
|
|
||||||
d = r2.json()
|
|
||||||
if not d: break
|
|
||||||
gb.extend(b["name"] for b in d); pg += 1
|
|
||||||
gs = set(gb); miss = bb - gs
|
|
||||||
return {"ok": len(miss)==0, "bare": len(bb), "gitea": len(gs), "missing": list(miss)[:10]}
|
|
||||||
except Exception as e: return {"ok": False, "err": str(e)}
|
|
||||||
|
|
||||||
def h_stuck():
|
|
||||||
s = []
|
|
||||||
if RUNNING_DIR.exists():
|
|
||||||
n = time.time()
|
|
||||||
for c in RUNNING_DIR.iterdir():
|
|
||||||
if c.name.endswith(".md"):
|
|
||||||
a = (n - c.stat().st_mtime) / 60
|
|
||||||
if a > 30: s.append({"n": c.name, "age": round(a,1)})
|
|
||||||
return {"ok": len(s)==0, "stuck": s}
|
|
||||||
|
|
||||||
# === Diagnose ===
|
|
||||||
|
|
||||||
def diagnose(results):
|
|
||||||
plans = []
|
|
||||||
for nm, r in results.items():
|
|
||||||
if r.get("ok", True): continue
|
|
||||||
if nm == "branch_sync":
|
|
||||||
m = r.get("missing",[])
|
|
||||||
if m:
|
|
||||||
plans.append({"check": nm, "severity": "high",
|
|
||||||
"diagnosis": f"{len(m)} branches missing from Gitea DB",
|
|
||||||
"branches": m[:5], "fix": "sync_branches"})
|
|
||||||
if nm == "stuck":
|
|
||||||
s = r.get("stuck",[])
|
|
||||||
if s:
|
|
||||||
plans.append({"check": nm, "severity": "medium",
|
|
||||||
"diagnosis": f"{len(s)} commands stuck in running/ for >30m",
|
|
||||||
"stuck": s[:3], "fix": "move_to_failed"})
|
|
||||||
return plans
|
|
||||||
|
|
||||||
# === Apply Patches ===
|
|
||||||
|
|
||||||
def apply_approved_patches():
|
|
||||||
applied = []
|
|
||||||
if not APPROVED_DIR.exists(): return applied
|
|
||||||
for f in sorted(APPROVED_DIR.iterdir()):
|
|
||||||
if not f.name.endswith(".json"): continue
|
|
||||||
try:
|
|
||||||
patch = json.loads(f.read_text())
|
|
||||||
fix = patch.get("fix", "")
|
|
||||||
log.info(f"Applying approved patch: {patch.get('check','?')} -> {fix}")
|
|
||||||
if fix == "sync_branches":
|
|
||||||
branches = patch.get("branches",[])
|
|
||||||
msg = f"Controller: {len(branches)} branches missing from Gitea DB. Patching via agent."
|
|
||||||
notify("Branch Sync Patch Applied", msg, "info")
|
|
||||||
# Move to applied
|
|
||||||
APPLIED_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
f.rename(APPLIED_DIR / f.name)
|
|
||||||
applied.append(patch)
|
|
||||||
elif fix == "move_to_failed":
|
|
||||||
stuck = patch.get("stuck",[])
|
|
||||||
for s in stuck:
|
|
||||||
src = RUNNING_DIR / s.get("n","")
|
|
||||||
if src.exists(): src.rename(FAILED_DIR / src.name)
|
|
||||||
notify("Stuck Commands Cleaned", f"Moved {len(stuck)} stuck commands to failed/", "info")
|
|
||||||
APPLIED_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
f.rename(APPLIED_DIR / f.name)
|
|
||||||
applied.append(patch)
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(f"Failed applying {f.name}: {e}")
|
|
||||||
REJECTED_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
f.rename(REJECTED_DIR / f.name)
|
|
||||||
return applied
|
|
||||||
|
|
||||||
# === Main Cycle ===
|
|
||||||
|
|
||||||
def run_cycle():
|
|
||||||
log.info("--- Cycle start ---")
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
log.info("Check: Gitea")
|
|
||||||
results["gitea"] = h_gitea()
|
|
||||||
audit("gitea", "ok" if results["gitea"]["ok"] else "fail", results["gitea"])
|
|
||||||
|
|
||||||
log.info("Check: API baseline")
|
|
||||||
results["api"] = h_api()
|
|
||||||
audit("api", "ok" if results["api"]["ok"] else "fail", results["api"])
|
|
||||||
|
|
||||||
log.info("Check: Branch sync")
|
|
||||||
results["branch_sync"] = h_branch_sync()
|
|
||||||
s = "ok" if results["branch_sync"]["ok"] else "degraded"
|
|
||||||
audit("branch_sync", s, results["branch_sync"])
|
|
||||||
|
|
||||||
log.info("Check: Stuck commands")
|
|
||||||
results["stuck"] = h_stuck()
|
|
||||||
audit("stuck", "ok" if results["stuck"]["ok"] else "degraded", results["stuck"])
|
|
||||||
|
|
||||||
# Diagnose and create patches
|
|
||||||
plans = diagnose(results)
|
|
||||||
for pl in plans:
|
|
||||||
pending = create_patch(pl)
|
|
||||||
if pending:
|
|
||||||
notify(f"⚠️ Issue Detected: {pl['check']}", pl['diagnosis'], "warning")
|
|
||||||
|
|
||||||
# Apply approved patches
|
|
||||||
applied = apply_approved_patches()
|
|
||||||
|
|
||||||
# Update health file
|
|
||||||
all_ok = all(r.get("ok",False) for r in results.values())
|
|
||||||
summary = {
|
|
||||||
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"ok": all_ok, "checks": {k:v.get("ok") for k,v in results.items()},
|
|
||||||
"plans": len(plans), "applied": len(applied)
|
|
||||||
}
|
|
||||||
HEALTH_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
HEALTH_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
||||||
log.info(f"Cycle done. All healthy: {all_ok}. Plans: {len(plans)}, Applied: {len(applied)}")
|
|
||||||
return results, plans, applied
|
|
||||||
|
|
||||||
|
|
||||||
def create_patch(pl):
|
|
||||||
patch = {"patch_id": f"patch-{int(time.time())}", "created": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"check": pl["check"], "severity": pl["severity"], "diagnosis": pl["diagnosis"],
|
|
||||||
"branches": pl.get("branches",[]), "stuck": pl.get("stuck",[]), "fix": pl.get("fix","")}
|
|
||||||
PENDING_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
f = PENDING_DIR / f"{patch['patch_id']}.json"
|
|
||||||
if f.exists(): return False
|
|
||||||
f.write_text(json.dumps(patch, indent=2, ensure_ascii=False))
|
|
||||||
log.info(f"Patch created: {patch['patch_id']}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def run_forever():
|
|
||||||
log.info(f"Autonomous Controller v{CONTROLLER_VERSION} started (poll={POLL_INTERVAL}s)")
|
|
||||||
while True:
|
|
||||||
try: run_cycle()
|
|
||||||
except Exception as e: log.exception(f"Cycle failed: {e}")
|
|
||||||
time.sleep(POLL_INTERVAL)
|
|
||||||
|
|
||||||
|
|
||||||
def run_once():
|
|
||||||
log.info("One-shot check")
|
|
||||||
results, plans, applied = run_cycle()
|
|
||||||
for n, r in results.items():
|
|
||||||
st = "OK" if r.get("ok") else "FAIL"
|
|
||||||
log.info(f" [{st}] {n}")
|
|
||||||
if plans: log.info(f"Patches created: {len(plans)}")
|
|
||||||
if applied: log.info(f"Patches applied: {len(applied)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "--once":
|
|
||||||
run_once()
|
|
||||||
else:
|
|
||||||
run_forever()
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
AIOS Autonomous Controller
|
|
||||||
|
|
||||||
Detect -> Diagnose -> Fix -> Test -> Patch -> Approve -> Commit -> Notify
|
|
||||||
|
|
||||||
Runs alongside the command watcher. Checks system health periodically.
|
|
||||||
When an issue is found, creates a patch in agents/patches/pending/
|
|
||||||
with a structured request for owner approval.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
import requests
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timezone, timedelta
|
|
||||||
|
|
||||||
WORKSPACE = Path(os.environ.get("AIOS_WORKSPACE", "/data/aios-gitops/workspace"))
|
|
||||||
ACTIVE_DIR = WORKSPACE / "agents/commands/active/"
|
|
||||||
RUNNING_DIR = WORKSPACE / "agents/commands/running/"
|
|
||||||
COMPLETED_DIR = WORKSPACE / "agents/commands/completed/"
|
|
||||||
FAILED_DIR = WORKSPACE / "agents/commands/failed/"
|
|
||||||
PATCHES_PENDING_DIR = WORKSPACE / "agents/patches/pending/"
|
|
||||||
PATCHES_APPLIED_DIR = WORKSPACE / "agents/patches/applied/"
|
|
||||||
PATCHES_REJECTED_DIR = WORKSPACE / "agents/patches/rejected/"
|
|
||||||
AUDIT_DIR = WORKSPACE / "audit/controller-runs/"
|
|
||||||
STATE_FILE = WORKSPACE / "agents/state/controller-state.json"
|
|
||||||
HEALTH_FILE = WORKSPACE / "agents/state/health.json"
|
|
||||||
POLL_INTERVAL = int(os.environ.get("CONTROLLER_POLL_INTERVAL_SECONDS", "300"))
|
|
||||||
GITEA_URL = os.environ.get("GITEA_URL", "http://127.0.0.1:3000")
|
|
||||||
API_URL = os.environ.get("AIOS_API_URL", "http://127.0.0.1:18890")
|
|
||||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s [controller] %(levelname)s %(message)s",
|
|
||||||
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
||||||
)
|
|
||||||
log = logging.getLogger("autonomous-controller")
|
|
||||||
|
|
||||||
CONTROLLER_VERSION = "1.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
def load_state():
|
|
||||||
if STATE_FILE.exists():
|
|
||||||
try:
|
|
||||||
return json.loads(STATE_FILE.read_text())
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
return {"controller_version": CONTROLLER_VERSION, "checks": {}, "patches": {}}
|
|
||||||
|
|
||||||
|
|
||||||
def save_state(state):
|
|
||||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
def write_audit_record(check_name, status, details):
|
|
||||||
record = {
|
|
||||||
"controller_run_id": f"controller-{int(time.time())}",
|
|
||||||
"check": check_name,
|
|
||||||
"status": status,
|
|
||||||
"controller_version": CONTROLLER_VERSION,
|
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"details": details,
|
|
||||||
}
|
|
||||||
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
af = AUDIT_DIR / f"{record['controller_run_id']}.json"
|
|
||||||
af.write_text(json.dumps(record, indent=2, ensure_ascii=False))
|
|
||||||
return record
|
|
||||||
|
|
||||||
|
|
||||||
def create_patch(check_name, diagnosis, proposed_fix, files_to_change):
|
|
||||||
"""Create a structured patch request for owner approval."""
|
|
||||||
patch_id = f"patch-{int(time.time())}"
|
|
||||||
patch = {
|
|
||||||
"patch_id": patch_id,
|
|
||||||
"check": check_name,
|
|
||||||
"diagnosis": diagnosis,
|
|
||||||
"proposed_fix": proposed_fix,
|
|
||||||
"files_to_change": files_to_change,
|
|
||||||
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"status": "pending",
|
|
||||||
"approved": False,
|
|
||||||
"applied": False,
|
|
||||||
}
|
|
||||||
PATCHES_PENDING_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
pf = PATCHES_PENDING_DIR / f"{patch_id}.json"
|
|
||||||
pf.write_text(json.dumps(patch, indent=2, ensure_ascii=False))
|
|
||||||
log.info(f"Created patch: {patch_id}")
|
|
||||||
return patch
|
|
||||||
|
|
||||||
|
|
||||||
def get_gitea_version():
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{GITEA_URL}/api/v1/version", timeout=5)
|
|
||||||
if r.status_code == 200:
|
|
||||||
return {"healthy": True, "data": r.json()}
|
|
||||||
return {"healthy": False, "error": f"HTTP {r.status_code}"}
|
|
||||||
except Exception as e:
|
|
||||||
return {"healthy": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def get_gitea_repo_info(owner="aios", repo="aios-v23"):
|
|
||||||
try:
|
|
||||||
branches = []
|
|
||||||
page = 1
|
|
||||||
while True:
|
|
||||||
r = requests.get(
|
|
||||||
f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/branches",
|
|
||||||
params={"page": page, "limit": 50},
|
|
||||||
headers={"Authorization": f"token {GITEA_TOKEN}"},
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
if r.status_code != 200:
|
|
||||||
break
|
|
||||||
data = r.json()
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
branches.extend(data)
|
|
||||||
page += 1
|
|
||||||
tags = []
|
|
||||||
page = 1
|
|
||||||
while True:
|
|
||||||
r = requests.get(
|
|
||||||
f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/tags",
|
|
||||||
params={"page": page, "limit": 50},
|
|
||||||
headers={"Authorization": f"token {GITEA_TOKEN}"},
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
if r.status_code != 200:
|
|
||||||
break
|
|
||||||
data = r.json()
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
tags.extend(data)
|
|
||||||
page += 1
|
|
||||||
return {
|
|
||||||
"healthy": True,
|
|
||||||
"branches": len(branches),
|
|
||||||
"tags": len(tags),
|
|
||||||
"branch_list": [b["name"] for b in branches],
|
|
||||||
"tag_list": [t["name"] for t in tags],
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"healthy": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_baseline():
|
|
||||||
try:
|
|
||||||
r = requests.get(f"{API_URL}/admin/ai-evolution/api/data", timeout=5)
|
|
||||||
if r.status_code == 200:
|
|
||||||
data = r.json()
|
|
||||||
return {"healthy": True, "baseline": data.get("baseline")}
|
|
||||||
return {"healthy": False, "error": f"HTTP {r.status_code}"}
|
|
||||||
except Exception as e:
|
|
||||||
return {"healthy": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def check_barerepo_branch_sync():
|
|
||||||
bare_repo = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", "--git-dir", str(bare_repo), "branch", "-a"],
|
|
||||||
capture_output=True, text=True, timeout=10
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return {"healthy": False, "error": result.stderr[:200]}
|
|
||||||
bare_branches = [
|
|
||||||
l.strip().replace("remotes/origin/", "").replace("* ", "")
|
|
||||||
for l in result.stdout.strip().split("\n") if l.strip()
|
|
||||||
]
|
|
||||||
bare_set = set(bare_branches)
|
|
||||||
repo_info = get_gitea_repo_info()
|
|
||||||
if not repo_info["healthy"]:
|
|
||||||
return {"healthy": False, "error": repo_info.get("error")}
|
|
||||||
gitea_set = set(repo_info["branch_list"])
|
|
||||||
missing_in_gitea = bare_set - gitea_set
|
|
||||||
extra_in_gitea = gitea_set - bare_set
|
|
||||||
return {
|
|
||||||
"healthy": len(missing_in_gitea) == 0,
|
|
||||||
"bare_count": len(bare_set),
|
|
||||||
"gitea_count": len(gitea_set),
|
|
||||||
"missing_in_gitea": list(missing_in_gitea),
|
|
||||||
"extra_in_gitea": list(extra_in_gitea),
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"healthy": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def check_stuck_commands():
|
|
||||||
stuck = []
|
|
||||||
threshold_minutes = 30
|
|
||||||
if RUNNING_DIR.exists():
|
|
||||||
now = time.time()
|
|
||||||
for cmd_file in RUNNING_DIR.iterdir():
|
|
||||||
if cmd_file.name.endswith(".md"):
|
|
||||||
mtime = cmd_file.stat().st_mtime
|
|
||||||
age_minutes = (now - mtime) / 60
|
|
||||||
if age_minutes > threshold_minutes:
|
|
||||||
stuck.append({
|
|
||||||
"name": cmd_file.name,
|
|
||||||
"age_minutes": round(age_minutes, 1),
|
|
||||||
"path": str(cmd_file),
|
|
||||||
})
|
|
||||||
return {"healthy": len(stuck) == 0, "stuck_count": len(stuck), "stuck_commands": stuck}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_all_checks():
|
|
||||||
"""Run all health checks and return results."""
|
|
||||||
state = load_state()
|
|
||||||
results = {}
|
|
||||||
patches_created = []
|
|
||||||
|
|
||||||
# 1. Gitea version
|
|
||||||
log.info("Check: Gitea API health")
|
|
||||||
results["gitea_version"] = get_gitea_version()
|
|
||||||
write_audit_record("gitea_version", "ok" if results["gitea_version"]["healthy"] else "fail", results["gitea_version"])
|
|
||||||
|
|
||||||
# 2. API baseline
|
|
||||||
log.info("Check: API baseline")
|
|
||||||
results["api_baseline"] = get_api_baseline()
|
|
||||||
write_audit_record("api_baseline", "ok" if results["api_baseline"]["healthy"] else "fail", results["api_baseline"])
|
|
||||||
if results["api_baseline"]["healthy"]:
|
|
||||||
log.info(f" Baseline: {results['api_baseline'].get('baseline')}")
|
|
||||||
|
|
||||||
# 3. Bare repo branch sync
|
|
||||||
log.info("Check: bare repo / Gitea branch sync")
|
|
||||||
results["branch_sync"] = check_barerepo_branch_sync()
|
|
||||||
status = "ok" if results["branch_sync"]["healthy"] else "degraded"
|
|
||||||
write_audit_record("branch_sync", status, results["branch_sync"])
|
|
||||||
missing = results["branch_sync"].get("missing_in_gitea", [])
|
|
||||||
if missing:
|
|
||||||
log.warning(f" {len(missing)} branches missing in Gitea DB")
|
|
||||||
for b in missing:
|
|
||||||
log.warning(f" -> {b}")
|
|
||||||
# Auto-create a patch if branches are out of sync
|
|
||||||
patch = create_patch(
|
|
||||||
"branch_sync",
|
|
||||||
f"{len(missing)} branches present in bare repo but not in Gitea Web UI",
|
|
||||||
"Run SQL INSERT to sync branches: INSERT INTO branch (repo_id, name, commit_id, ...) "
|
|
||||||
"for each missing branch. Verify with bare repo SHA.",
|
|
||||||
["runtime/gitea-bin/data/... (SQL via gitea CLI or direct DB)"]
|
|
||||||
)
|
|
||||||
patches_created.append(patch)
|
|
||||||
|
|
||||||
# 4. Stuck commands
|
|
||||||
log.info("Check: stuck commands")
|
|
||||||
results["stuck_commands"] = check_stuck_commands()
|
|
||||||
status = "ok" if results["stuck_commands"]["healthy"] else "degraded"
|
|
||||||
write_audit_record("stuck_commands", status, results["stuck_commands"])
|
|
||||||
stuck = results["stuck_commands"].get("stuck_commands", [])
|
|
||||||
if stuck:
|
|
||||||
log.warning(f" {len(stuck)} commands stuck in running/")
|
|
||||||
for c in stuck:
|
|
||||||
log.warning(f" -> {c['name']} (age: {c['age_minutes']}m)")
|
|
||||||
|
|
||||||
# Update state
|
|
||||||
state["checks"] = {
|
|
||||||
"last_run": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"results": {k: {"healthy": v.get("healthy"), "error": v.get("error")} for k, v in results.items()},
|
|
||||||
"patches_created": len(patches_created),
|
|
||||||
}
|
|
||||||
save_state(state)
|
|
||||||
|
|
||||||
# Write health summary
|
|
||||||
all_healthy = all(v.get("healthy", False) for v in results.values())
|
|
||||||
summary = {
|
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
||||||
"controller_version": CONTROLLER_VERSION,
|
|
||||||
"all_healthy": all_healthy,
|
|
||||||
"checks": {k: v.get("healthy") for k, v in results.items()},
|
|
||||||
"patches_pending": len(patches_created),
|
|
||||||
}
|
|
||||||
HEALTH_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
HEALTH_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
return results, patches_created
|
|
||||||
|
|
||||||
|
|
||||||
def run_forever():
|
|
||||||
log.info(f"Autonomous Controller v{CONTROLLER_VERSION} started")
|
|
||||||
log.info(f"Poll interval: {POLL_INTERVAL}s")
|
|
||||||
log.info(f"Workspace: {WORKSPACE}")
|
|
||||||
log.info(f"Gitea: {GITEA_URL}, API: {API_URL}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
log.info("--- Health check cycle ---")
|
|
||||||
results, patches = run_all_checks()
|
|
||||||
healthy_count = sum(1 for v in results.values() if v.get("healthy"))
|
|
||||||
total = len(results)
|
|
||||||
log.info(f"Results: {healthy_count}/{total} healthy")
|
|
||||||
if patches:
|
|
||||||
log.warning(f"Created {len(patches)} patch(es) for owner review")
|
|
||||||
else:
|
|
||||||
log.info("No issues detected requiring patches")
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(f"Check cycle failed: {e}")
|
|
||||||
|
|
||||||
time.sleep(POLL_INTERVAL)
|
|
||||||
|
|
||||||
|
|
||||||
def run_once():
|
|
||||||
log.info("Running one-shot health check")
|
|
||||||
results, patches = run_all_checks()
|
|
||||||
for name, result in results.items():
|
|
||||||
status = "OK" if result.get("healthy") else "FAIL"
|
|
||||||
log.info(f" [{status}] {name}")
|
|
||||||
if not result.get("healthy") and result.get("error"):
|
|
||||||
log.info(f" error: {result['error'][:200]}")
|
|
||||||
if patches:
|
|
||||||
log.info(f"Patches created: {len(patches)}")
|
|
||||||
else:
|
|
||||||
log.info("No patches created")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "--once":
|
|
||||||
run_once()
|
|
||||||
else:
|
|
||||||
run_forever()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# AIOS Agent State
|
|
||||||
Agent state and lock files for AIOS Git Agent execution.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"processed": {
|
|
||||||
"CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md": {
|
|
||||||
"hash": "9442e78e3deb35e03227016b2265d34feb4640f8efdcb07baffe06d52ef947c8",
|
|
||||||
"status": "running",
|
|
||||||
"discovered_at": "2026-07-09T00:12:52+0800"
|
|
||||||
},
|
|
||||||
"CMD-20260709-enable-autonomous-controller.md": {
|
|
||||||
"hash": "2825811706389130b2f531a2d3814b800b1181598f871b496e5a05fa13adbbec",
|
|
||||||
"status": "running",
|
|
||||||
"discovered_at": "2026-07-08T19:01:45+0800"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"last_scan": "2026-07-09T10:42:52+0800"
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,42 @@
|
|||||||
{
|
{
|
||||||
"active_version": "V1R2C3",
|
"active_version": "V1R2C3",
|
||||||
"active_tag": "V1R2C3",
|
"active_tag": "V1R2C3",
|
||||||
|
"version": "V1R2C3",
|
||||||
"previous_version": "V1R2C2",
|
"previous_version": "V1R2C2",
|
||||||
"rollback_version": "V1R2C2",
|
"rollback_version": "V1R2C2",
|
||||||
|
"status": "promoted",
|
||||||
"mode": "gitops-online",
|
"mode": "gitops-online",
|
||||||
"update_mode": "git_patch_only",
|
"update_mode": "git_patch_only",
|
||||||
"zip_update_deprecated": true,
|
"zip_update_deprecated": true,
|
||||||
"chatgpt_access": true,
|
"entry": "https://cnnetmkt.top",
|
||||||
"codex_access": true,
|
"components": {
|
||||||
|
"gitops": true,
|
||||||
|
"chatgpt_actions": true,
|
||||||
|
"codex_access": true,
|
||||||
|
"custom_llm": true,
|
||||||
|
"multiuser_runtime": true,
|
||||||
|
"autonomous_recovery_loop": true,
|
||||||
|
"portal_gitops_dashboard": true,
|
||||||
|
"casdoor_auth": true,
|
||||||
|
"gitea": true
|
||||||
|
},
|
||||||
|
"compatibility": {
|
||||||
|
"chatgpt_actions_clientresponse_compat": true,
|
||||||
|
"route_alias_compatibility": true,
|
||||||
|
"head_options_compatibility": true,
|
||||||
|
"openapi_response_schema_compatibility": true,
|
||||||
|
"structured_json_errors": true
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"tag": "V1R2C3",
|
||||||
|
"branch": "patch/v1r2c3-baseline-promotion-20260708",
|
||||||
|
"rollback_version": "V1R2C2",
|
||||||
|
"promotion_report": "reports/V1R2C3_RELEASE_PROMOTION_REPORT.md"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"secret_plaintext_written": false,
|
||||||
|
"token_plaintext_written": false,
|
||||||
|
"force_push_used": false
|
||||||
|
},
|
||||||
"updated_at": "2026-07-08T00:00:00Z"
|
"updated_at": "2026-07-08T00:00:00Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# AIOS Agent Execution Audit Plane
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
Record every agent-initiated action on the AIOS GitOps runtime: what, who, when, why, result.
|
|
||||||
|
|
||||||
## Audit Lifecycle
|
|
||||||
|
|
||||||
1. **Pre-flight**: Record intent before execution
|
|
||||||
2. **In-flight**: Capture command, input, git state, branch
|
|
||||||
3. **Post-flight**: Record result, outputs, errors, duration
|
|
||||||
4. **Archive**: Immutable commit to git as structured JSON logs
|
|
||||||
|
|
||||||
## Audit Schema (v1)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"audit_id": "<uuid or hash>",
|
|
||||||
"event_type": "command_execution | command_completed | command_failed | baseline_promotion | tag_creation | state_change",
|
|
||||||
"command_ref": "CMD-<date>-<description>",
|
|
||||||
"agent": "openclaw | hermes | codex",
|
|
||||||
"user": "gitops-write-bot",
|
|
||||||
"timestamp": "<ISO-8601>",
|
|
||||||
"git": {
|
|
||||||
"branch": "main",
|
|
||||||
"sha_head": "<sha>",
|
|
||||||
"baseline": "V1R3C1",
|
|
||||||
"dirty": false
|
|
||||||
},
|
|
||||||
"input": {},
|
|
||||||
"output": {},
|
|
||||||
"duration_ms": 0,
|
|
||||||
"result": "success | failure | pending",
|
|
||||||
"error": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Storage
|
|
||||||
- `audit/agent-runs/` — per-agent JSONL
|
|
||||||
- `audit/command-history/` — per-command aggregated
|
|
||||||
- `audit/version-history/` — per-baseline audit trail
|
|
||||||
- All audit files committed to git (immutable)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_run_id": "openclaw-1783508505",
|
|
||||||
"command": "CMD-20260709-enable-autonomous-controller.md",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"objective": "Command: CMD-20260709-enable-autonomous-controller.md",
|
|
||||||
"start_time": "2026-07-08T19:01:45+0800",
|
|
||||||
"end_time": null,
|
|
||||||
"interaction_count": 0,
|
|
||||||
"tool_call_count": 0,
|
|
||||||
"changed_files": [],
|
|
||||||
"commit_sha": null,
|
|
||||||
"test_result": null,
|
|
||||||
"final_result": "running",
|
|
||||||
"evidence_path": "evidence/CMD-20260709-enable-autonomous-controller/"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_run_id": "openclaw-1783527172",
|
|
||||||
"command": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"objective": "Command: CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
|
|
||||||
"start_time": "2026-07-09T00:12:52+0800",
|
|
||||||
"end_time": null,
|
|
||||||
"interaction_count": 0,
|
|
||||||
"tool_call_count": 0,
|
|
||||||
"changed_files": [],
|
|
||||||
"commit_sha": null,
|
|
||||||
"test_result": null,
|
|
||||||
"final_result": "running",
|
|
||||||
"evidence_path": "evidence/CMD-20260708-fix-git-actions-write-and-enable-audit-plane/"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "openclaw-20260708-001",
|
|
||||||
"command": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"start_time": "2026-07-08T15:17:00+08:00",
|
|
||||||
"end_time": "2026-07-08T15:32:00+08:00",
|
|
||||||
"interaction_count": 45,
|
|
||||||
"objective": "Fix Git Actions write permission - token hash mismatch in gitops.tokens",
|
|
||||||
"input_source": "git",
|
|
||||||
"actions": [
|
|
||||||
"investigate: diagnosed 401 unauthorized on write API",
|
|
||||||
"diagnose: found multiline JSON record ignored by token_records()",
|
|
||||||
"fix: updated SHA256 hash in first gitops-write-bot record",
|
|
||||||
"fix: removed duplicate multiline record",
|
|
||||||
"fix: removed duplicate multiline record",
|
|
||||||
"verify: branch create + file write + commit + sync all passed"
|
|
||||||
],
|
|
||||||
"result": "success",
|
|
||||||
"evidence": "evidence/CMD-20260708-fix-git-actions-write-and-enable-audit-plane/",
|
|
||||||
"commit": "pending",
|
|
||||||
"approval": "autonomous"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "openclaw-20260708-002",
|
|
||||||
"command": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"start_time": "2026-07-08T15:34:00+08:00",
|
|
||||||
"end_time": "2026-07-08T15:36:00+08:00",
|
|
||||||
"interaction_count": 12,
|
|
||||||
"objective": "Initialize Agent Command Plane directory structure and audit plane",
|
|
||||||
"input_source": "git",
|
|
||||||
"actions": [
|
|
||||||
"create: agents/commands/active/running/completed/failed/",
|
|
||||||
"create: agents/policies/ agents/state/",
|
|
||||||
"create: agents/registry.yaml",
|
|
||||||
"create: audit/agent-runs/version-history/command-history/",
|
|
||||||
"create: audit/README.md audit/manifest.json",
|
|
||||||
"create: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md",
|
|
||||||
"create: evidence/CMD-20260708-.../"
|
|
||||||
],
|
|
||||||
"result": "success",
|
|
||||||
"evidence": "evidence/CMD-20260708-fix-git-actions-write-and-enable-audit-plane/",
|
|
||||||
"commit": "pending",
|
|
||||||
"approval": "autonomous"
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"agent_run_id": "openclaw-20260708-003",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"command": "CMD-20260708-enable-aios-autonomous-operation-and-notification",
|
|
||||||
"start_time": "2026-07-08T15:00:00+08:00",
|
|
||||||
"end_time": "2026-07-08T16:20:00+08:00",
|
|
||||||
"final_result": "success",
|
|
||||||
"interaction_count": 75,
|
|
||||||
"summary": "Initialized autonomous operation mode: Git command watcher running, Web UI dashboard live, audit records created. Pending: WeChat notification system integration."
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "1.0.0",
|
|
||||||
"audit_plane_enabled": true,
|
|
||||||
"created_at": "2026-07-08T15:20:00+08:00",
|
|
||||||
"baseline": "V1R3C1",
|
|
||||||
"initiated_by": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane"
|
|
||||||
}
|
|
||||||
+138
-1
@@ -1 +1,138 @@
|
|||||||
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
|
{
|
||||||
|
"active_version": "V1R2C3",
|
||||||
|
"active_tag": "V1R2C3",
|
||||||
|
"mode": "gitops-online",
|
||||||
|
"version_policy": "VxRxCx",
|
||||||
|
"update_mode": "git_patch_only",
|
||||||
|
"zip_update_deprecated": true,
|
||||||
|
"entry": "https://cnnetmkt.top",
|
||||||
|
"links": {
|
||||||
|
"git": "https://g.cnnetmkt.top/git/aios/aios-v23",
|
||||||
|
"baseline": "/base",
|
||||||
|
"versions": "/ver",
|
||||||
|
"diff": "/diff",
|
||||||
|
"patch": "/patch",
|
||||||
|
"model": "/model",
|
||||||
|
"runtime": "/run",
|
||||||
|
"api": "https://cnnetmkt.top/api",
|
||||||
|
"hook": "/hook"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"multiuser_runtime": true,
|
||||||
|
"shared_dedicated_runtime": true,
|
||||||
|
"custom_llm": true,
|
||||||
|
"user_model_priority": true,
|
||||||
|
"chatgpt_rw": true,
|
||||||
|
"codex_rw": true,
|
||||||
|
"portal_gitops_dashboard": true,
|
||||||
|
"human_shortlinks_login_required": true,
|
||||||
|
"autonomous_recovery_loop": true,
|
||||||
|
"chatgpt_actions_clientresponse_compat": true,
|
||||||
|
"route_alias_compatibility": true,
|
||||||
|
"head_options_compatibility": true,
|
||||||
|
"openapi_response_schema_compatibility": true,
|
||||||
|
"structured_json_errors": true
|
||||||
|
},
|
||||||
|
"version": "V1R2C3",
|
||||||
|
"baseline_tag": "V1R2C3-baseline",
|
||||||
|
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||||
|
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||||
|
"zip_update_mode": "deprecated",
|
||||||
|
"chatgpt_access": true,
|
||||||
|
"codex_access": true,
|
||||||
|
"git_url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
|
||||||
|
"short_links": [
|
||||||
|
"/git",
|
||||||
|
"/base",
|
||||||
|
"/ver",
|
||||||
|
"/diff",
|
||||||
|
"/patch",
|
||||||
|
"/model",
|
||||||
|
"/run",
|
||||||
|
"/api",
|
||||||
|
"/hook"
|
||||||
|
],
|
||||||
|
"updated_at": "2026-07-08T00:00:00Z",
|
||||||
|
"auth": {
|
||||||
|
"primary_domain": "https://auth.cnnetmkt.top",
|
||||||
|
"backup_domain": "https://auth.h.imruyi.top:8443",
|
||||||
|
"primary_user_entry": "https://cnnetmkt.top/",
|
||||||
|
"primary_callback": "https://cnnetmkt.top/casdoor/callback/",
|
||||||
|
"casdoor_application": "admin/aios-portal",
|
||||||
|
"backup_domain_role": "standby-only"
|
||||||
|
},
|
||||||
|
"previous_version": "V1R2C2",
|
||||||
|
"rollback_version": "V1R2C2",
|
||||||
|
"release_reason": "GPT Actions ClientResponseError compatibility recovery and validated GitOps Actions read/write path.",
|
||||||
|
"codex_gitops": {
|
||||||
|
"config": "config/codex/gitops-online.json",
|
||||||
|
"token_account": "codex-gitops-bot",
|
||||||
|
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"all_writes_require_git_commit": true,
|
||||||
|
"api_keys_in_git": false,
|
||||||
|
"online_git_sync": true,
|
||||||
|
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
|
||||||
|
},
|
||||||
|
"last_collaboration_test": {
|
||||||
|
"timestamp": "20260706T104130Z",
|
||||||
|
"local_codex_branch": "patch/collab-local-codex-20260706T104130Z",
|
||||||
|
"custom_gpt_branch": "patch/collab-custom-gpt-20260706T104130Z",
|
||||||
|
"custom_gpt_url": "https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai",
|
||||||
|
"result": "merged_to_main"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"human_shortlinks": {
|
||||||
|
"status": "login_required",
|
||||||
|
"routes": [
|
||||||
|
"/base",
|
||||||
|
"/ver",
|
||||||
|
"/diff",
|
||||||
|
"/patch",
|
||||||
|
"/model",
|
||||||
|
"/run"
|
||||||
|
],
|
||||||
|
"target": "https://cnnetmkt.top/portal/me/#gitops",
|
||||||
|
"machine_api": "https://cnnetmkt.top/api"
|
||||||
|
},
|
||||||
|
"gitea": {
|
||||||
|
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
|
||||||
|
"require_signin_view": true,
|
||||||
|
"registration_disabled": true,
|
||||||
|
"default_local_account": "disabled_without_password",
|
||||||
|
"owner_password_sync": "aios-gitea-owner-sync.timer",
|
||||||
|
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
|
||||||
|
},
|
||||||
|
"secret_plaintext_written": false,
|
||||||
|
"token_plaintext_written": false,
|
||||||
|
"force_push_used": false
|
||||||
|
},
|
||||||
|
"last_auth_portal_fix": {
|
||||||
|
"version": "V1R1C4",
|
||||||
|
"timestamp": "2026-07-06T11:16:23Z",
|
||||||
|
"shortlinks": "redirect_to_login_protected_portal_gitops_section",
|
||||||
|
"gitea_owner_sync": "enabled",
|
||||||
|
"portal_overlay": "overlays/user-control-center",
|
||||||
|
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||||
|
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||||
|
},
|
||||||
|
"promotion": {
|
||||||
|
"target_version": "V1R2C3",
|
||||||
|
"tag": "V1R2C3",
|
||||||
|
"branch": "patch/v1r2c3-baseline-promotion-20260708",
|
||||||
|
"previous_version": "V1R2C2",
|
||||||
|
"rollback_version": "V1R2C2",
|
||||||
|
"validated_actions": [
|
||||||
|
"getRuntimeStatus",
|
||||||
|
"getBaseline",
|
||||||
|
"listVersions",
|
||||||
|
"listPatches",
|
||||||
|
"getDiff",
|
||||||
|
"createBranch",
|
||||||
|
"createPatch",
|
||||||
|
"commitFile"
|
||||||
|
],
|
||||||
|
"owner_authorized": true
|
||||||
|
},
|
||||||
|
"git": "abe2ea3"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"owner_approved": true, "version": "V1R4C0-test"}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# AIOS Git Agent Command Plane
|
|
||||||
|
|
||||||
**Bootstrap:** CMD-BOOTSTRAP-001
|
|
||||||
|
|
||||||
The Agent Command Plane provides a structured foundation for autonomous GitOps operation.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
cmd/command-plane/
|
|
||||||
├── approvals/ — Approval gates and pre-approved actions
|
|
||||||
├── audit/ — Audit trail for all command plane operations
|
|
||||||
├── events/ — Inbound/outbound event bus
|
|
||||||
├── reports/ — Execution reports and summaries
|
|
||||||
├── requests/ — Pending command requests and artifacts
|
|
||||||
├── scripts/ — Command plane tooling
|
|
||||||
├── state/ — Current state and operational locks
|
|
||||||
└── manifest.json — Bootstrap manifest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Command plane scripts in `scripts/` can read/write events, requests, approvals,
|
|
||||||
and state. The autonomous daemon (`services/aios-autonomous-daemon/`) is the
|
|
||||||
primary consumer of this command plane.
|
|
||||||
|
|
||||||
Events flow: `events/` → processing → `requests/` → `approvals/` → execution → `reports/` → `audit/`
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"command_plane": "CMD-BOOTSTRAP-001",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"created_at": "2026-07-08T15:14:00+08:00",
|
|
||||||
"baseline": "V1R3C1",
|
|
||||||
"description": "AIOS Git Agent Command Plane initialization",
|
|
||||||
"directories": {
|
|
||||||
"events": "inbound/outbound event bus for command plane operations",
|
|
||||||
"requests": "pending command requests and artifacts",
|
|
||||||
"approvals": "approval gates and pre-approved actions",
|
|
||||||
"audit": "audit trail for all command plane operations",
|
|
||||||
"reports": "execution reports and summaries",
|
|
||||||
"state": "current command plane state and locks",
|
|
||||||
"scripts": "command plane scripts and tooling"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
agent_context_isolation:
|
|
||||||
enabled: true
|
|
||||||
architecture:
|
|
||||||
shared_kernel: true
|
|
||||||
group_main_agent: true
|
|
||||||
owner_dedicated_agent: true
|
|
||||||
user_dedicated_agent: false
|
|
||||||
layers:
|
|
||||||
- system_policy
|
|
||||||
- agent_default
|
|
||||||
- group_policy
|
|
||||||
- role_policy
|
|
||||||
- user_profile
|
|
||||||
- session_context
|
|
||||||
priority:
|
|
||||||
session_context: 70
|
|
||||||
user_profile: 50
|
|
||||||
group_policy: 30
|
|
||||||
agent_default: 10
|
|
||||||
safety_policy: immutable
|
|
||||||
forbidden:
|
|
||||||
- user_preferences_in_USER_md
|
|
||||||
- cross_user_profile_access
|
|
||||||
- shared_personalization_state
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
events:
|
|
||||||
- context_loaded
|
|
||||||
- profile_updated
|
|
||||||
- agent_selected
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
agent_event_bus:
|
|
||||||
enabled: true
|
|
||||||
replacement_for:
|
|
||||||
- agents/commands
|
|
||||||
- agents/messages
|
|
||||||
storage:
|
|
||||||
root: agents/events
|
|
||||||
states:
|
|
||||||
- pending
|
|
||||||
- running
|
|
||||||
- completed
|
|
||||||
- failed
|
|
||||||
audit: agents/events/audit
|
|
||||||
supported_kinds:
|
|
||||||
- command
|
|
||||||
- agent_status
|
|
||||||
- gpt_inbox
|
|
||||||
- gpt_reply
|
|
||||||
- audit_event
|
|
||||||
- approval_request
|
|
||||||
- failure_report
|
|
||||||
- daily_report
|
|
||||||
api:
|
|
||||||
operations:
|
|
||||||
- createAgentEvent
|
|
||||||
- listAgentEvents
|
|
||||||
- getAgentEvent
|
|
||||||
- transitionAgentEvent
|
|
||||||
- getAgentEventStats
|
|
||||||
dashboard:
|
|
||||||
route: /admin/ai-evolution/events
|
|
||||||
governance:
|
|
||||||
secrets_in_git: false
|
|
||||||
direct_main_push: false
|
|
||||||
owner_gate_required: true
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
agent_observability:
|
|
||||||
enabled: true
|
|
||||||
tracked_agents:
|
|
||||||
- openclaw
|
|
||||||
- hermes
|
|
||||||
- codex
|
|
||||||
- sub_agents
|
|
||||||
metrics:
|
|
||||||
- execution_duration
|
|
||||||
- input_tokens
|
|
||||||
- output_tokens
|
|
||||||
- total_tokens
|
|
||||||
- loop_type
|
|
||||||
- delegation_tree
|
|
||||||
loop_types:
|
|
||||||
- openclaw_independent
|
|
||||||
- hermes_orchestrated
|
|
||||||
visibility:
|
|
||||||
user: own_sessions_only
|
|
||||||
group_admin: group_sessions
|
|
||||||
owner: global_audit
|
|
||||||
privacy:
|
|
||||||
token_details_visible: permission_based
|
|
||||||
cross_user_execution_visibility: false
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
# AIOS Agent Audit Plane Architecture
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The AIOS Agent Audit Plane provides a complete traceability framework for all autonomous agent operations on the AIOS GitOps repository. Every agent action — from command discovery to execution to version promotion — is recorded, versioned, and auditable.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Git Repository
|
|
||||||
|
|
|
||||||
v
|
|
||||||
Agent Command Plane (agents/commands/)
|
|
||||||
| active/ running/ completed/ failed/
|
|
||||||
| + registry.yaml
|
|
||||||
v
|
|
||||||
[OpenClaw | Hermes | Codex]
|
|
||||||
|
|
|
||||||
+---> Execution
|
|
||||||
| |
|
|
||||||
| v
|
|
||||||
| evidence/<CMD-ID>/ (detection, execution, test, final)
|
|
||||||
|
|
|
||||||
+---> Audit
|
|
||||||
| |
|
|
||||||
| v
|
|
||||||
| audit/agent-runs/ (per-agent log)
|
|
||||||
| audit/command-history/ (per-command log)
|
|
||||||
| audit/version-history/ (per-baseline log)
|
|
||||||
|
|
|
||||||
+---> Git Commit
|
|
||||||
|
|
|
||||||
v
|
|
||||||
Web UI Timeline
|
|
||||||
/admin/ai-history
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
1. CMD file lands in `agents/commands/active/`
|
|
||||||
2. Agent picks it up, reads it
|
|
||||||
3. Agent executes, writes evidence to `evidence/<CMD-ID>/`
|
|
||||||
4. Agent writes audit record to `audit/agent-runs/`
|
|
||||||
5. Git commit locks the audit record
|
|
||||||
6. Web UI displays timeline from git history
|
|
||||||
|
|
||||||
## Audit Record Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"command": "CMD-20260708-...",
|
|
||||||
"agent": "openclaw",
|
|
||||||
"version": "V1R3C1",
|
|
||||||
"start_time": "",
|
|
||||||
"end_time": "",
|
|
||||||
"interaction_count": 0,
|
|
||||||
"objective": "",
|
|
||||||
"input_source": "git",
|
|
||||||
"actions": [],
|
|
||||||
"result": "",
|
|
||||||
"evidence": "",
|
|
||||||
"commit": "",
|
|
||||||
"approval": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ownership
|
|
||||||
|
|
||||||
- All agent modifications must be bound to a version (e.g., V1R3C1)
|
|
||||||
- Every code change must be traceable to who/what agent created it
|
|
||||||
- No agent-created changes without audit trail
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [Agent Command Plane](../agents/README.md)
|
|
||||||
- [Baseline System](baseline-system.md)
|
|
||||||
=======
|
|
||||||
# AIOS Agent Audit Plane
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Every agent action is recorded. All code changes are traceable to:
|
|
||||||
- Who made the change
|
|
||||||
- Why the change was made
|
|
||||||
- When it was made
|
|
||||||
- Which version introduced it
|
|
||||||
- What test results showed
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
audit/
|
|
||||||
├── agent-runs/ # Per-execution records (JSON)
|
|
||||||
├── command-history/ # Command lifecycle history
|
|
||||||
├── version-history/ # Version/baseline change log
|
|
||||||
└── interaction-history/ # Agent conversation traces
|
|
||||||
```
|
|
||||||
|
|
||||||
## Agent Run Record
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| agent_run_id | Unique execution ID |
|
|
||||||
| command | Command file name |
|
|
||||||
| agent | Executing agent name |
|
|
||||||
| version | Baseline version at execution time |
|
|
||||||
| objective | Human-readable goal |
|
|
||||||
| start_time | ISO-8601 start timestamp |
|
|
||||||
| end_time | ISO-8601 completion timestamp |
|
|
||||||
| interaction_count | Agent-owner interaction turns |
|
|
||||||
| tool_call_count | Tool invocations during execution |
|
|
||||||
| changed_files | Files modified in this run |
|
|
||||||
| commit_sha | Git commit hash |
|
|
||||||
| test_result | Test verification outcome |
|
|
||||||
| final_result | SUCCESS / FAILED / PARTIAL |
|
|
||||||
| evidence_path | Link to evidence directory |
|
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
Records baseline promotions, rollbacks, and version changes.
|
|
||||||
|
|
||||||
## Interaction History
|
|
||||||
|
|
||||||
Conversation logs between agent and owner for each command execution.
|
|
||||||
|
|
||||||
## Audit Trail Requirement
|
|
||||||
|
|
||||||
All changes tracked via Git with signed commits where possible.
|
|
||||||
Every commit message references the originating Command ID.
|
|
||||||
>>>>>>> 4d18ee0 (feat(cmd): enable Agent Command Plane, audit plane, and Web UI dashboard)
|
|
||||||
@@ -141,7 +141,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/PatchRequest'}
|
schema: {$ref: '#/components/schemas/PatchRequest'}
|
||||||
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/model:
|
/api/model:
|
||||||
get:
|
get:
|
||||||
operationId: readModelRegistry
|
operationId: readModelRegistry
|
||||||
@@ -157,7 +156,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/ModelRequest'}
|
schema: {$ref: '#/components/schemas/ModelRequest'}
|
||||||
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/run:
|
/api/run:
|
||||||
get:
|
get:
|
||||||
operationId: getRuntimeStatus
|
operationId: getRuntimeStatus
|
||||||
@@ -174,7 +172,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/BranchRequest'}
|
schema: {$ref: '#/components/schemas/BranchRequest'}
|
||||||
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/file:
|
/api/file:
|
||||||
post:
|
post:
|
||||||
operationId: commitFile
|
operationId: commitFile
|
||||||
@@ -186,7 +183,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/FileCommitRequest'}
|
schema: {$ref: '#/components/schemas/FileCommitRequest'}
|
||||||
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/commit:
|
/api/commit:
|
||||||
post:
|
post:
|
||||||
operationId: commitChanges
|
operationId: commitChanges
|
||||||
@@ -198,7 +194,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/CommitRequest'}
|
schema: {$ref: '#/components/schemas/CommitRequest'}
|
||||||
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
|
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/tag:
|
/api/tag:
|
||||||
post:
|
post:
|
||||||
operationId: requestOrCreateTag
|
operationId: requestOrCreateTag
|
||||||
@@ -210,7 +205,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/TagRequest'}
|
schema: {$ref: '#/components/schemas/TagRequest'}
|
||||||
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
|
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/rollback:
|
/api/rollback:
|
||||||
post:
|
post:
|
||||||
operationId: requestRollback
|
operationId: requestRollback
|
||||||
@@ -222,7 +216,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/RollbackRequest'}
|
schema: {$ref: '#/components/schemas/RollbackRequest'}
|
||||||
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
|
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/model-priority:
|
/api/model-priority:
|
||||||
put:
|
put:
|
||||||
operationId: setModelPriority
|
operationId: setModelPriority
|
||||||
@@ -234,7 +227,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
|
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
|
||||||
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
|
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
|
||||||
x-openai-isConsequential: true
|
|
||||||
/api/nl:
|
/api/nl:
|
||||||
post:
|
post:
|
||||||
operationId: planNaturalLanguageGitOpsAction
|
operationId: planNaturalLanguageGitOpsAction
|
||||||
@@ -245,4 +237,4 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {$ref: '#/components/schemas/NLRequest'}
|
schema: {$ref: '#/components/schemas/NLRequest'}
|
||||||
responses: {'
|
responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
# Final Report: Enable AIOS Autonomous Operation and Notification
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Initialized AIOS Autonomous Operation mode with Command Plane, audit system, Web UI dashboard, and WeChat notification channel.
|
|
||||||
|
|
||||||
## Deliverables
|
|
||||||
|
|
||||||
### 1. Agent Command Plane ✅
|
|
||||||
- Created agents/registry.yaml with lifecycle definition
|
|
||||||
- Created agent-command-watcher.py (active, running)
|
|
||||||
- Created agents/commands/ directory structure
|
|
||||||
- Created agents/policies/ and agents/state/ boilerplate
|
|
||||||
|
|
||||||
### 2. Audit System ✅
|
|
||||||
- Created audit/agent-runs/ with 3 agent run records
|
|
||||||
- Created audit/version-history/, audit/command-history/
|
|
||||||
- Created audit/README.md and audit/manifest.json
|
|
||||||
- Created docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
|
|
||||||
- Integrated baseline version tracking (V1R3C1)
|
|
||||||
|
|
||||||
### 3. Web UI Dashboard ✅
|
|
||||||
- Route: GET /admin/ai-evolution — Dashboard page
|
|
||||||
- Route: GET /admin/ai-evolution/api/data — JSON API endpoint
|
|
||||||
- Vue 3 single-page dashboard showing: version timeline, command lookup, recent agent runs
|
|
||||||
- Auto-refresh every 30 seconds
|
|
||||||
|
|
||||||
### 4. CMD-20260708-fix-git-actions-write-and-enable-audit-plane [COMPLETED]
|
|
||||||
- Part 1-8 all complete, CMD moved to agents/commands/completed/
|
|
||||||
- Git Actions write permission fixed (token hash sync)
|
|
||||||
- Agent Command Plane, audit plane, trace, version association verified
|
|
||||||
|
|
||||||
### 5. Git References
|
|
||||||
- Commit: 9dad8b57 (push to main) + commit: 4d18ee0 (rebase to c796347)
|
|
||||||
- Baseline: V1R3C1 (SHA 187dcc3554a20ecfcc4d4faca3774ddf3a1ebd33)
|
|
||||||
- All commits pushed to bare repo successfully
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-07-08T15:17:00+08:00",
|
|
||||||
"channel": "WeChat",
|
|
||||||
"user_request": "执行 Git 中最新命令:CMD-20260708-enable-agent-command-plane ... (later revealed as CMD-BOOTSTRAP-001-enable-command-plane)",
|
|
||||||
"detected_by": "openclaw",
|
|
||||||
"symptom": "write API returning 401 Unauthorized for gitops-write-bot token",
|
|
||||||
"severity": "P0",
|
|
||||||
"initial_diagnosis": "token SHA256 mismatch - gitops.tokens has two records for gitops-write-bot, multiline record skipped by parser"
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"part_1_write_fix": {
|
|
||||||
"status": "complete",
|
|
||||||
"steps": [
|
|
||||||
"diagnosed token_records() skipping multiline JSON record",
|
|
||||||
"updated SHA256 in first single-line record to match actual token",
|
|
||||||
"removed duplicate multiline record",
|
|
||||||
"restarted aios-gitops-model-api",
|
|
||||||
"verified: branch create + file write + commit + sync all passed"
|
|
||||||
],
|
|
||||||
"bash_commands_used": [
|
|
||||||
"cat, python3, systemctl, curl, git"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"part_2_command_plane": {
|
|
||||||
"status": "complete",
|
|
||||||
"created": [
|
|
||||||
"agents/commands/active/running/completed/failed/",
|
|
||||||
"agents/policies/ agents/state/",
|
|
||||||
"agents/registry.yaml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"part_3_audit_plane": {
|
|
||||||
"status": "complete",
|
|
||||||
"created": [
|
|
||||||
"audit/agent-runs/",
|
|
||||||
"audit/version-history/",
|
|
||||||
"audit/command-history/",
|
|
||||||
"audit/README.md",
|
|
||||||
"audit/manifest.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"part_4_conversation_trace": {
|
|
||||||
"status": "complete",
|
|
||||||
"created": "evidence/CMD-20260708-.../detection.json"
|
|
||||||
},
|
|
||||||
"part_5_version_association": {
|
|
||||||
"status": "complete",
|
|
||||||
"baseline": "V1R3C1"
|
|
||||||
},
|
|
||||||
"part_6_web_ui": {
|
|
||||||
"status": "n/a-uses-existing-git-history"
|
|
||||||
},
|
|
||||||
"part_7_evidence": {
|
|
||||||
"status": "complete",
|
|
||||||
"created": [
|
|
||||||
"evidence/CMD-20260708-.../detection.json",
|
|
||||||
"evidence/CMD-20260708-.../execution.json",
|
|
||||||
"evidence/CMD-20260708-.../final-report.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Final Report: CMD-20260708-fix-git-actions-write-and-enable-audit-plane
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Fix Git Actions write permission and enable Agent Audit Plane.
|
|
||||||
|
|
||||||
## Results
|
|
||||||
- **Write permission**: ✅ Fixed (token hash sync, multiline dedup)
|
|
||||||
- **Command Plane**: ✅ Created (agents/, registry.yaml)
|
|
||||||
- **Audit Plane**: ✅ Created (audit/, manifest, schema)
|
|
||||||
- **Architecture Doc**: ✅ Created (AIOS_AGENT_AUDIT_PLANE.md)
|
|
||||||
- **Evidence**: ✅ Created (detection, execution, final report)
|
|
||||||
|
|
||||||
## Outcome Signals
|
|
||||||
- `AIOS_GIT_ACTION_WRITE_READY` = true
|
|
||||||
- `AIOS_AGENT_AUDIT_PLANE_READY` = true
|
|
||||||
|
|
||||||
## Next Command
|
|
||||||
- `CMD-20260709-autonomous-controller-enable`
|
|
||||||
|
|
||||||
## Completion Date
|
|
||||||
2026-07-08T15:36:00+08:00
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
openapi: 3.0.0
|
|
||||||
info:
|
|
||||||
title: AIOS Unified Agent Event Bus API
|
|
||||||
version: V1R3C1+
|
|
||||||
paths:
|
|
||||||
/agent-events:
|
|
||||||
post:
|
|
||||||
operationId: createAgentEvent
|
|
||||||
get:
|
|
||||||
operationId: listAgentEvents
|
|
||||||
/agent-events/{event_id}:
|
|
||||||
get:
|
|
||||||
operationId: getAgentEvent
|
|
||||||
/agent-events/{event_id}/transition:
|
|
||||||
post:
|
|
||||||
operationId: transitionAgentEvent
|
|
||||||
/agent-events/stats/overview:
|
|
||||||
get:
|
|
||||||
operationId: getAgentEventStats
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# CMD-20260709-complete-agent-observability-loop
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Complete the OpenClaw/Hermes/sub-agent observability closed loop.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
1. Deploy agent observability store.
|
|
||||||
2. Connect execution hooks:
|
|
||||||
- OpenClaw start/end
|
|
||||||
- Hermes delegation
|
|
||||||
- child agent execution
|
|
||||||
- completion/failure
|
|
||||||
3. Enable API:
|
|
||||||
- recordAgentExecution
|
|
||||||
- getAgentObservabilityOverview
|
|
||||||
4. Enable Web UI:
|
|
||||||
- /admin/ai-evolution/agents
|
|
||||||
5. Enforce permission views:
|
|
||||||
- user: own execution
|
|
||||||
- group_admin: group execution
|
|
||||||
- owner: global audit
|
|
||||||
|
|
||||||
## Required metrics
|
|
||||||
|
|
||||||
- independent OpenClaw loops
|
|
||||||
- Hermes orchestrated loops
|
|
||||||
- child agent tree
|
|
||||||
- duration
|
|
||||||
- token usage when available
|
|
||||||
- failure rate
|
|
||||||
|
|
||||||
## Validation signals
|
|
||||||
|
|
||||||
- AIOS_AGENT_OBSERVABILITY_READY
|
|
||||||
- AIOS_AGENT_ROUTING_VISUAL_READY
|
|
||||||
- AIOS_AGENT_COST_METRICS_READY
|
|
||||||
- AIOS_AGENT_OBSERVABILITY_CLOSED_LOOP_READY
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- Do not expose cross-user data.
|
|
||||||
- Token details require permission.
|
|
||||||
- All records are audited.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# CMD-20260709-enable-agent-observability-dashboard
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Expose OpenClaw/Hermes/sub-agent scheduling and resource metrics in Web UI.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
1. Deploy agent observability collector.
|
|
||||||
2. Capture execution records:
|
|
||||||
- entry agent
|
|
||||||
- delegation path
|
|
||||||
- Hermes usage
|
|
||||||
- child agents
|
|
||||||
- duration
|
|
||||||
- token usage when adapter provides it
|
|
||||||
3. Enable dashboard:
|
|
||||||
- /admin/ai-evolution/agents
|
|
||||||
4. Apply permission visibility:
|
|
||||||
- user: own sessions
|
|
||||||
- group_admin: group sessions
|
|
||||||
- owner: global audit
|
|
||||||
|
|
||||||
## Validation signals
|
|
||||||
|
|
||||||
- AIOS_AGENT_OBSERVABILITY_READY
|
|
||||||
- AIOS_AGENT_ROUTING_VISUAL_READY
|
|
||||||
- AIOS_AGENT_COST_METRICS_READY
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- No cross-user visibility.
|
|
||||||
- Token details are permission controlled.
|
|
||||||
- All execution records are audited.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# CMD-20260709-enable-group-owner-agent-isolation
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Deploy multi-user agent context isolation:
|
|
||||||
|
|
||||||
- group main agent per group/tenant
|
|
||||||
- dedicated owner agent per owner
|
|
||||||
- user profile and session overlays
|
|
||||||
- no user personalization in shared USER.md
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
1. Deploy context resolver.
|
|
||||||
2. Enable group agent routing.
|
|
||||||
3. Enable owner agent routing.
|
|
||||||
4. Create runtime user profile storage.
|
|
||||||
5. Migrate user-specific preferences out of shared USER.md.
|
|
||||||
6. Enable context audit events.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- AIOS_USER_PROFILE_ISOLATION_READY
|
|
||||||
- AIOS_GROUP_AGENT_ROUTING_READY
|
|
||||||
- AIOS_OWNER_AGENT_READY
|
|
||||||
- AIOS_CONTEXT_AUDIT_READY
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- Safety policy overrides all user customization.
|
|
||||||
- No cross-user profile access.
|
|
||||||
- No personal profile data in shared Git configuration.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# CMD-20260709-enable-unified-agent-event-bus
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Replace separate command/message queues with the unified Agent Event Bus.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
1. Deploy `active-runtime/app/agent_event_bus.py`.
|
|
||||||
2. Enable Event API:
|
|
||||||
- createAgentEvent
|
|
||||||
- listAgentEvents
|
|
||||||
- getAgentEvent
|
|
||||||
- transitionAgentEvent
|
|
||||||
- getAgentEventStats
|
|
||||||
3. Switch Web UI admin evolution page to:
|
|
||||||
- `/admin/ai-evolution/events`
|
|
||||||
4. Enable audit path:
|
|
||||||
- `agents/events/audit`
|
|
||||||
5. Keep compatibility adapters for old GPT inbox calls.
|
|
||||||
|
|
||||||
## Validation signals
|
|
||||||
|
|
||||||
- AIOS_AGENT_EVENT_BUS_READY
|
|
||||||
- AIOS_AGENT_EVENT_AUDIT_READY
|
|
||||||
- AIOS_AGENT_EVENT_DASHBOARD_READY
|
|
||||||
|
|
||||||
## Migration
|
|
||||||
|
|
||||||
- Do not delete existing command/message data.
|
|
||||||
- New events go to `agents/events`.
|
|
||||||
- Legacy queues become compatibility views.
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- No secrets in Git.
|
|
||||||
- No direct main push.
|
|
||||||
- Owner gate remains enabled.
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# CMD-20260709-fix-openapi-workspace-permission
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Fix Git workspace permission issue that blocks writing OpenAPI files.
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
ChatGPT Actions failed to write:
|
|
||||||
|
|
||||||
```text
|
|
||||||
openapi/agent-context-isolation.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Error:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Permission denied: /data/aios-gitops/workspace/openapi/agent-context-isolation.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execute
|
|
||||||
|
|
||||||
1. Inspect permissions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls -ld /data/aios-gitops/workspace/openapi
|
|
||||||
ls -la /data/aios-gitops/workspace/openapi | head
|
|
||||||
stat /data/aios-gitops/workspace/openapi
|
|
||||||
ps aux | grep -E 'aios-gitops|model-api|actions' | grep -v grep
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Identify runtime write user for Actions API.
|
|
||||||
|
|
||||||
3. Fix ownership/permissions with least privilege. Expected owner should match the GitOps runtime write user, likely `aiman` or the service user.
|
|
||||||
|
|
||||||
Example only after confirming user/group:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo chown -R <runtime_user>:<runtime_group> /data/aios-gitops/workspace/openapi
|
|
||||||
sudo chmod -R u+rwX,g+rwX /data/aios-gitops/workspace/openapi
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Do not make the directory world-writable unless explicitly approved.
|
|
||||||
|
|
||||||
5. Retry creating:
|
|
||||||
|
|
||||||
```text
|
|
||||||
openapi/agent-context-isolation.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Commit through GitOps patch branch only:
|
|
||||||
|
|
||||||
```text
|
|
||||||
patch/group-owner-agent-context-isolation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- OpenAPI file write succeeds.
|
|
||||||
- Git commit succeeds.
|
|
||||||
- No direct main push.
|
|
||||||
- No secret written.
|
|
||||||
- Permission repair is recorded in audit.
|
|
||||||
|
|
||||||
Expected signal:
|
|
||||||
|
|
||||||
```text
|
|
||||||
AIOS_OPENAPI_WORKSPACE_PERMISSION_READY
|
|
||||||
```
|
|
||||||
|
|
||||||
## Forbidden
|
|
||||||
|
|
||||||
- Do not push main.
|
|
||||||
- Do not create tag.
|
|
||||||
- Do not chmod 777 unless owner explicitly approves.
|
|
||||||
- Do not write secrets.
|
|
||||||
- Do not change unrelated directories.
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# CMD-20260709-wire-agent-observability-hooks
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Connect real OpenClaw/Hermes/Codex execution lifecycle into Agent Observability.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
1. Hook agent start/end lifecycle.
|
|
||||||
2. Emit events:
|
|
||||||
- agent_started
|
|
||||||
- agent_delegated
|
|
||||||
- token_consumed
|
|
||||||
- agent_completed
|
|
||||||
- agent_failed
|
|
||||||
3. Persist execution metrics.
|
|
||||||
4. Enable Web UI agent dashboard.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- AIOS_AGENT_OBSERVABILITY_READY
|
|
||||||
- AIOS_AGENT_ROUTING_VISUAL_READY
|
|
||||||
- AIOS_AGENT_COST_METRICS_READY
|
|
||||||
- AIOS_AGENT_OBSERVABILITY_CLOSED_LOOP_READY
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
Token usage depends on model adapter instrumentation. If unavailable, record null
|
|
||||||
rather than guessing.
|
|
||||||
|
|
||||||
## Governance
|
|
||||||
|
|
||||||
- Respect user/group/owner visibility.
|
|
||||||
- Do not expose cross-user execution details.
|
|
||||||
- Keep audit records.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "branch",
|
|
||||||
"branch": "patch/agent-observability-webui",
|
|
||||||
"base": "main",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"created_at": "2026-07-09T02:02:20Z",
|
|
||||||
"direct_main_push": false,
|
|
||||||
"owner_approval_required": true
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"type": "branch",
|
"type": "branch",
|
||||||
"branch": "patch/agent-event-bus-unified",
|
"branch": "patch/v1r2c3-baseline-promotion-20260708",
|
||||||
"base": "main",
|
"base": "main",
|
||||||
"created_by": "chatgpt-gitops-bot",
|
"created_by": "chatgpt-gitops-bot",
|
||||||
"created_at": "2026-07-08T08:36:38Z",
|
"created_at": "2026-07-07T18:25:04Z",
|
||||||
"direct_main_push": false,
|
"direct_main_push": false,
|
||||||
"owner_approval_required": true
|
"owner_approval_required": true
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "patch-1783448903483-a9f3ec99",
|
||||||
|
"patch_name": "v1r2c3-final-smoke-20260708",
|
||||||
|
"branch": "patch/v1r2c3-final-smoke-20260708",
|
||||||
|
"risk": "low",
|
||||||
|
"description": "Final smoke validation patch after V1R2C3 baseline promotion preparation.",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-07T18:28:23Z"
|
||||||
|
}
|
||||||
@@ -19,49 +19,11 @@ class ConsensusEngine:
|
|||||||
"windows_codex": 2,
|
"windows_codex": 2,
|
||||||
"server_agent": 1,
|
"server_agent": 1,
|
||||||
}
|
}
|
||||||
# New in V1R1C5: quality score, historical reliability, domain expertise
|
|
||||||
self.quality_score_factors = {
|
|
||||||
"code_quality": 0.4,
|
|
||||||
"test_coverage": 0.3,
|
|
||||||
"response_time": 0.3,
|
|
||||||
}
|
|
||||||
self.historical_reliability_factors = {
|
|
||||||
"recent_acceptance_rate": 0.5,
|
|
||||||
"longevity_score": 0.3,
|
|
||||||
"error_rate": 0.2,
|
|
||||||
}
|
|
||||||
self.domain_expertise_factors = {
|
|
||||||
"specialization_match": 0.5,
|
|
||||||
"experience_years": 0.3,
|
|
||||||
"certifications": 0.2,
|
|
||||||
}
|
|
||||||
|
|
||||||
def calculate_quality_score(self, agent_id: str, agent_type: str) -> float:
|
def calculate(self, votes: List[dict]) -> dict:
|
||||||
"""Calculate quality score for an agent based on its performance metrics."""
|
|
||||||
# Placeholder implementation; in production, fetch metrics from telemetry
|
|
||||||
return 0.85 # Default quality score
|
|
||||||
|
|
||||||
def calculate_historical_reliability(self, agent_id: str, agent_type: str) -> float:
|
|
||||||
"""Calculate historical reliability score for an agent."""
|
|
||||||
# Placeholder implementation; in production, fetch from store
|
|
||||||
return 0.90 # Default reliability score
|
|
||||||
|
|
||||||
def calculate_domain_expertise(self, agent_id: str, agent_type: str) -> float:
|
|
||||||
"""Calculate domain expertise score for an agent."""
|
|
||||||
# Placeholder implementation; in production, fetch from agent registry
|
|
||||||
return 0.75 # Default expertise score
|
|
||||||
|
|
||||||
def calculate(self, votes: List[dict], use_dynamic_weights: bool = False) -> dict:
|
|
||||||
result = {"approve": 0, "reject": 0}
|
result = {"approve": 0, "reject": 0}
|
||||||
for vote in votes:
|
for vote in votes:
|
||||||
base_weight = self.base_weights.get(vote.get("agent_type"), 1)
|
weight = vote.get("weight", self.base_weights.get(vote.get("agent_type"), 1))
|
||||||
if use_dynamic_weights:
|
|
||||||
quality = self.calculate_quality_score(vote.get("agent_id", ""), vote.get("agent_type", ""))
|
|
||||||
reliability = self.calculate_historical_reliability(vote.get("agent_id", ""), vote.get("agent_type", ""))
|
|
||||||
expertise = self.calculate_domain_expertise(vote.get("agent_id", ""), vote.get("agent_type", ""))
|
|
||||||
weight = base_weight * quality * reliability * expertise
|
|
||||||
else:
|
|
||||||
weight = base_weight
|
|
||||||
result[vote.get("decision", "reject")] += weight
|
result[vote.get("decision", "reject")] += weight
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -70,7 +32,7 @@ class ConsensusEngine:
|
|||||||
|
|
||||||
Returns one of: "approve", "reject", "review", "conditional_approval".
|
Returns one of: "approve", "reject", "review", "conditional_approval".
|
||||||
"""
|
"""
|
||||||
result = self.calculate(votes, use_dynamic_weights=True)
|
result = self.calculate(votes)
|
||||||
if result["approve"] > result["reject"]:
|
if result["approve"] > result["reject"]:
|
||||||
# Check if any participant voted with conditions
|
# Check if any participant voted with conditions
|
||||||
conditional_votes = [
|
conditional_votes = [
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
"""Agent OS message bus runtime."""
|
"""Agent OS message bus runtime."""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class DeliveryStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
DELIVERED = "delivered"
|
|
||||||
FAILED = "failed"
|
|
||||||
RETRYING = "retrying"
|
|
||||||
EXPIRED = "expired"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -19,11 +10,6 @@ class AgentMessage:
|
|||||||
receiver: str
|
receiver: str
|
||||||
event: str
|
event: str
|
||||||
payload: dict
|
payload: dict
|
||||||
# V1R1C5: New fields for message tracking and delivery
|
|
||||||
task_id: str = ""
|
|
||||||
correlation_id: str = ""
|
|
||||||
retry_state: int = 0
|
|
||||||
delivery_status: DeliveryStatus = DeliveryStatus.PENDING
|
|
||||||
|
|
||||||
|
|
||||||
class MessageBus:
|
class MessageBus:
|
||||||
@@ -35,14 +21,7 @@ class MessageBus:
|
|||||||
|
|
||||||
def broadcast(self, sender: str, event: str, payload: dict, receivers: List[str]):
|
def broadcast(self, sender: str, event: str, payload: dict, receivers: List[str]):
|
||||||
for receiver in receivers:
|
for receiver in receivers:
|
||||||
self.send(AgentMessage(
|
self.send(AgentMessage(sender, receiver, event, payload))
|
||||||
sender=sender,
|
|
||||||
receiver=receiver,
|
|
||||||
event=event,
|
|
||||||
payload=payload,
|
|
||||||
task_id=payload.get("task_id", "") if isinstance(payload, dict) else "",
|
|
||||||
correlation_id=payload.get("correlation_id", "") if isinstance(payload, dict) else "",
|
|
||||||
))
|
|
||||||
|
|
||||||
def consume(self):
|
def consume(self):
|
||||||
messages = list(self.queue)
|
messages = list(self.queue)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
CMD-20260708 git actions write permission verified OK
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from active_runtime.app.agent_event_bus import AgentEventBus
|
|
||||||
from active_runtime.app.agent_event_dashboard import AgentEventDashboard
|
|
||||||
|
|
||||||
|
|
||||||
def test_unified_event_flow():
|
|
||||||
bus = AgentEventBus(root="/tmp/aios-agent-events-test")
|
|
||||||
event = bus.publish(
|
|
||||||
kind="gpt_inbox",
|
|
||||||
source="openclaw",
|
|
||||||
target="custom-gpt",
|
|
||||||
summary="execution report",
|
|
||||||
payload={"status": "done"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert event["status"] == "pending"
|
|
||||||
assert len(bus.list_events(kind="gpt_inbox")) == 1
|
|
||||||
|
|
||||||
done = bus.transition(event["id"], "completed", "custom-gpt")
|
|
||||||
assert done["status"] == "completed"
|
|
||||||
|
|
||||||
dashboard = AgentEventDashboard(bus).overview()
|
|
||||||
assert dashboard["audit"]["enabled"] is True
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from active_runtime.app.agent_observability_hooks import AgentObservabilityHooks
|
|
||||||
|
|
||||||
|
|
||||||
def test_agent_observability_closed_loop():
|
|
||||||
hooks = AgentObservabilityHooks()
|
|
||||||
started = hooks.start("user1", "group1", "openclaw")
|
|
||||||
execution_id = started["execution_id"]
|
|
||||||
hooks.delegate(execution_id, "hermes")
|
|
||||||
hooks.delegate(execution_id, "codex")
|
|
||||||
hooks.tokens(execution_id, 100, 200)
|
|
||||||
result = hooks.finish(execution_id)
|
|
||||||
|
|
||||||
record = result["record"]
|
|
||||||
assert record["used_hermes"] is True
|
|
||||||
assert "codex" in record["path"]
|
|
||||||
assert record["total_tokens"] == 300
|
|
||||||
+1
-20
@@ -1,20 +1 @@
|
|||||||
{
|
{"version":"V1R2C3","tag":"V1R2C3","status":"promoted","previous_version":"V1R2C2","rollback_version":"V1R2C2","release_reason":"GPT Actions ClientResponseError compatibility recovery","validated_actions":["getRuntimeStatus","getBaseline","listVersions","listPatches","getDiff","createBranch","createPatch","commitFile"],"security":{"secret_plaintext_written":false,"token_plaintext_written":false,"force_push_used":false},"updated_at":"2026-07-08T00:00:00Z"}
|
||||||
"version": "V1R2C3",
|
|
||||||
"tag": "V1R2C3",
|
|
||||||
"previous_version": "V1R2C2",
|
|
||||||
"rollback_version": "V1R2C2",
|
|
||||||
"release_reason": "GPT Actions ClientResponseError compatibility recovery",
|
|
||||||
"validated_actions": [
|
|
||||||
"getRuntimeStatus",
|
|
||||||
"getBaseline",
|
|
||||||
"listVersions",
|
|
||||||
"listPatches",
|
|
||||||
"getDiff",
|
|
||||||
"createBranch",
|
|
||||||
"createPatch",
|
|
||||||
"commitFile"
|
|
||||||
],
|
|
||||||
"secret_plaintext_written": false,
|
|
||||||
"created_at": "2026-07-08T00:00:00Z",
|
|
||||||
"author": "openclaw"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "V1R4C1",
|
|
||||||
"tag": "V1R4C1",
|
|
||||||
"previous_version": "V1R3C1",
|
|
||||||
"rollback_version": "V1R3C1",
|
|
||||||
"baseline": "V1R3C1",
|
|
||||||
"release_reason": "Promote group-owner-agent-context-isolation with Agent Observability and Agent Event Bus unified platform",
|
|
||||||
"purpose": "Deploy complete agent observability, context isolation, and unified event bus pipeline",
|
|
||||||
"changes": [
|
|
||||||
"Group-owner-agent-context-isolation: resolver, RBAC, API, audit, dashboard",
|
|
||||||
"Agent observability: Hermes model, Web UI dashboard, policy, store, API, runtime hooks, event bus bridge, closed loop tests",
|
|
||||||
"Agent Event Bus unified: admin dashboard, configuration, API adapters, openapi operation ids",
|
|
||||||
"AI evolution dashboard API endpoint",
|
|
||||||
"Agent Command Plane and Audit Plane (V1R3C1 baseline)",
|
|
||||||
"OpenAPI workspace permission fix",
|
|
||||||
"ChatGPT Actions API internals refactored with ActionMonitor"
|
|
||||||
],
|
|
||||||
"artifacts": [
|
|
||||||
"active-runtime/app/agent_context_resolver.py",
|
|
||||||
"active-runtime/app/agent_context_rbac.py",
|
|
||||||
"active-runtime/app/agent_context_api.py",
|
|
||||||
"active-runtime/app/agent_context_audit.py",
|
|
||||||
"active-runtime/app/agent_context_dashboard.py",
|
|
||||||
"active-runtime/app/aios_chatgpt_actions_api.py",
|
|
||||||
"config/agent-context-isolation-policy.yaml",
|
|
||||||
"active-runtime/app/agent_observability_model.py",
|
|
||||||
"active-runtime/app/agent_observability_dashboard.py",
|
|
||||||
"active-runtime/app/agent_observability_policy.py",
|
|
||||||
"active-runtime/app/agent_observability_store.py",
|
|
||||||
"active-runtime/app/agent_observability_api.py",
|
|
||||||
"active-runtime/app/agent_observability_hooks.py",
|
|
||||||
"active-runtime/app/agent_event_bus_bridge.py",
|
|
||||||
"active-runtime/app/agent_event_bus_dashboard.py",
|
|
||||||
"active-runtime/app/agent_event_bus_config.py",
|
|
||||||
"active-runtime/app/agent_event_bus_api.py",
|
|
||||||
"tests/test_agent_observability_loop.py"
|
|
||||||
],
|
|
||||||
"validated_actions": [
|
|
||||||
"getRuntimeStatus",
|
|
||||||
"getBaseline",
|
|
||||||
"listVersions",
|
|
||||||
"listPatches",
|
|
||||||
"getDiff",
|
|
||||||
"createBranch",
|
|
||||||
"createPatch",
|
|
||||||
"commitFile"
|
|
||||||
],
|
|
||||||
"secret_plaintext_written": false,
|
|
||||||
"owner_approval_required": true,
|
|
||||||
"owner_notification_channels": ["wechat", "web_ui_audit"],
|
|
||||||
"created_at": "2026-07-09T10:57:00+08:00",
|
|
||||||
"author": "openclaw"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user