Compare commits

..
211 changed files with 167 additions and 12004 deletions
-1
View File
@@ -10,4 +10,3 @@ node_modules/
runtime/tmp/
runtime/cache/
logs/
config/test_write_perms
+1 -1
View File
@@ -1 +1 @@
V1R4C2
V1R1C4
@@ -1,90 +0,0 @@
version: V1R4C2
capability: general_search_gateway
status: pending_runtime_validation
scope:
- general Search Gateway
- provider registry
- group user session profile customization
- audit store
- event bridge
- agent adapters
- task center integration
- web ui
required_tests:
unit:
- tests/test_search_gateway_general_capability.py
- tests/test_search_provider_registry.py
- tests/test_search_gateway_orchestrator.py
- tests/test_search_agent_adapter.py
- tests/test_search_architect_integration.py
task_center:
- tests/test_task_center.py
- tests/test_task_lifecycle.py
- tests/test_task_orchestrator.py
- tests/test_task_event_and_statistics.py
- tests/test_task_dashboard_view.py
- tests/test_task_policy_acceptance.py
acceptance_cases:
- id: SG-001
name: public general search request
expected:
- provider_attempts_recorded
- evidence_or_clarification_returned
- audit_record_created
- id: SG-002
name: single provider failure fallback
expected:
- first_provider_failure_not_terminal
- fallback_attempt_recorded
- id: SG-003
name: group user session customization
expected:
- profile_overlay_resolved
- hard_guardrails_not_overridden
- id: SG-004
name: agent adapter integration
expected:
- openclaw_uses_search_gateway
- hermes_uses_search_gateway
- codex_uses_search_gateway
- custom_gpt_bridge_uses_search_gateway
- id: SG-005
name: task center visibility
expected:
- search_deployment_task_visible
- executor_visible
- progress_visible
- notification_state_visible
- id: SG-006
name: blocked unauthorized request
expected:
- status_blocked
- blocked_reasons_recorded
- audit_record_created
owner_gate:
required_for:
- paid_provider
- provider_secret
- private_data_source
- production_baseline_promotion
not_required_for:
- local_code_tests
- public_provider_framework
- task_center_dashboard_model
success_signals:
- AIOS_GENERAL_SEARCH_GATEWAY_READY
- AIOS_SEARCH_PROFILE_CUSTOMIZATION_READY
- AIOS_SEARCH_PROVIDER_REGISTRY_READY
- AIOS_SEARCH_PROVIDER_FALLBACK_READY
- AIOS_SEARCH_AUDIT_READY
- AIOS_SEARCH_EVENT_LOOP_READY
- AIOS_SEARCH_RUNTIME_LOOP_READY
- AIOS_SEARCH_WEB_UI_READY
- AIOS_SEARCH_AGENT_ADAPTERS_READY
- AIOS_TASK_OPERATION_CENTER_READY
- AIOS_SEARCH_WECHAT_NOTIFICATION_SENT
-56
View File
@@ -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"]
-23
View File
@@ -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"]
-55
View File
@@ -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"]
-77
View File
@@ -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"]
-125
View File
@@ -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"]
-80
View File
@@ -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"]
+13 -133
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
import hashlib, hmac, json, os, re, subprocess, time, urllib.parse, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
from orchestrator.action_monitor import ActionMonitor
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
SECRETS = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
@@ -15,9 +12,7 @@ TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.toke
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
monitor = ActionMonitor()
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display_name':'DeepSeek V4 Flash','scope':'global'},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display_name':'Qwen 3.6 35B','scope':'global'},
@@ -185,30 +180,19 @@ class H(BaseHTTPRequestHandler):
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in ('/api/base','/api/baseline','/base'):
if path in ('/api/base','/base'):
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
if path in ('/api/ver','/ver'):
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
if path in ('/api/patch','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
if path in ('/api/run','/api/runtime-status','/run'):
if path in ('/api/run','/run'):
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
if path in ('/api/action-monitor','/action-monitor'):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
# Admin AI Evolution Dashboard
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
return self.ai_evolution_page()
if path in ('/admin/ai-evolution/api/data'):
return self.ai_evolution_data()
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_GET', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
except Exception as e: return self.sendj({'error':str(e)},500)
def do_PUT(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
if path == '/api/model-priority': return self.model_priority(data)
@@ -216,19 +200,17 @@ class H(BaseHTTPRequestHandler):
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
if path in ('/api/file','/api/commit-file'): return self.file(data)
if path == '/api/branch': return self.branch(data)
if path == '/api/file': return self.file(data)
if path == '/api/commit': return self.commit(data)
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
if path == '/api/patch': return self.patch(data)
if path == '/api/tag': return self.tag(data)
if path == '/api/rollback': return self.rollback(data)
if path == '/api/model': return self.model(data)
if path == '/api/model-priority': return self.model_priority(data)
if path == '/api/nl': return self.nl(data)
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_POST', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
except Exception as e: return self.sendj({'error':str(e)},500)
def branch(self,data):
a=self.need('branch:create');
if not a: return
@@ -252,9 +234,6 @@ class H(BaseHTTPRequestHandler):
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
@@ -275,16 +254,10 @@ class H(BaseHTTPRequestHandler):
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model','hint':'This operation registers a new model. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
r=write_json(f'config/model_registry/chatgpt/{safe(mid)}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): register model {name}'); audit(a['account'],'model',mid,doc); self.sendj({'status':'model_registered','branch':b,'model_id':mid,'secret_ref':secret_ref,'commit':c})
def model_priority(self,data):
a=self.need('model:priority');
if not a: return
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model-priority','hint':'This operation updates model priority. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
user=safe(data.get('username') or a['account']); b=checkout(data.get('branch') or f'patch/model-priority-{user}', 'main', True); doc={'username':user,'items':data.get('items') or data.get('priority') or [],'updated_by':a['account'],'updated_at':now()}; r=write_json(f'config/model_priority/{user}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): update model priority {user}'); audit(a['account'],'model-priority',user,doc); self.sendj({'status':'updated','branch':b,'priority':doc,'commit':c})
def nl(self,data):
a=self.need('nl:plan');
@@ -295,102 +268,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 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
_EVOLUTION_HTML = '''<!DOCTYPE html>
<html lang=zh-CN>
<head>
<meta charset=UTF-8>
<meta name=viewport content="width=device-width,initial-scale=1.0">
<title>AIOS Evolution Dashboard</title>
<script src=https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d1117;color:#c9d1d9;font-size:14px;line-height:1.7;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif}
.c{max-width:1000px;margin:0 auto;padding:32px 24px}
h1{font-size:24px;color:#58a6ff;margin-bottom:2px}
.sub{color:#8b949e;font-size:13px;border-bottom:1px solid #21262d;padding-bottom:16px;margin-bottom:20px;display:flex;justify-content:space-between}
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px;margin-bottom:20px}
.s{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:18px;text-align:center}
.s .v{font-size:28px;font-weight:700;color:#f0f6fc}
.s .l{font-size:11px;color:#8b949e;margin-top:4px}
.bx{background:#161b22;border:1px solid #21262d;border-radius:8px;margin-bottom:14px}
.bh{border-bottom:1px solid #21262d;padding:10px 16px;font-weight:600;font-size:14px;display:flex;gap:6px;align-items:center}
.bd{padding:12px 16px}
.rw{display:grid;grid-template-columns:1fr 1fr 100px 60px;gap:8px;padding:8px 16px;border-bottom:1px solid #21262d;font-size:13px;align-items:center;cursor:pointer;transition:background .15s}
.rw:hover{background:#1c2128}.rw:last-child{border-bottom:none}
.tg{display:inline-block;padding:2px 10px;border-radius:99px;font-size:11px;font-weight:600}
.ok{background:#3fb95033;color:#3fb950}.fl{background:#da363333;color:#f85149}.if{background:#1f6feb33;color:#58a6ff}
pre{background:#0d1117;padding:12px;border-radius:6px;overflow:auto;font-size:12px;color:#8b949e;max-height:300px;margin-top:8px}select{padding:8px 10px;border:1px solid #30363d;border-radius:6px;background:#21262d;color:#c9d1d9;font-size:13px;max-width:400px;width:100%;margin-bottom:4px;cursor:pointer}
</style>
</head>
<body>
<div id=app class=c>
<h1>AIOS Evolution Dashboard</h1>
<div class=sub><span>Baseline: {{baseline}}</span><span style=color:#8b949e>{{refreshMsg}}</span></div>
<div class=g><div class=s><div class=v>{{tot}}</div><div class=l>Commands</div></div><div class=s><div class=v style=color:#3fb950>{{sok}}</div><div class=l>Succeeded</div></div><div class=s><div class=v style=color:#f85149>{{sfail}}</div><div class=l>Failed</div></div><div class=s><div class=v>{{runs}}</div><div class=l>Executions</div></div></div>
<div class=bx><div class=bh>Version Timeline</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Version</span><span>Time</span><span>Result</span></div><div v-for=v in vs class=rw @click=selVer(v)><span style=color:#58a6ff>{{v.tag}}</span><span>{{v.time||'-'}}</span><span :class="'tg '+v.result">{{v.result||'-'}}</span></div></div></div>
<div class=bx><div class=bh>Commands</div><div class=bd><select v-model=cmd @change=loadCmd><option value>-- Select --</option><option v-for=c in cmds :value=c>{{c}}</option></select><pre v-if=cdet>{{cdet}}</pre></div></div>
<div class=bx><div class=bh>Recent Agent Runs</div><div class=bd><div class=rw style=font-weight:600;border-bottom:2px solid #30363d;cursor:default><span>Agent</span><span>Command</span><span>Count</span><span>Result</span></div><div v-for=r in aruns class=rw><span>{{r.agent||'-'}}</span><span style=overflow:hidden;white-space:nowrap;text-overflow:ellipsis>{{r.command}}</span><span>{{r.interaction_count||0}}</span><span :class="'tg '+r.final_result">{{r.final_result||'-'}}</span></div></div></div></div>
<script>
const{createApp,ref,onMounted}=Vue;createApp({setup(){const vs=ref([]),cmds=ref([]),ar=ref([]),cmd=ref(''),cdet=ref(null),tot=ref(0),sok=ref(0),sfail=ref(0),runs=ref(0),baseline=ref('V1R3C1'),refreshMsg=ref('')
async function ld(){try{const r=await fetch('/admin/ai-evolution/api/data'),d=await r.json();baseline.value=d.baseline||'V1R3C1';vs.value=d.versions||[];cmds.value=d.commands||[];ar.value=d.agent_runs||[];tot.value=d.stats?.total||0;sok.value=d.stats?.success||0;sfail.value=d.stats?.failed||0;runs.value=d.stats?.agent_runs||0;refreshMsg.value='Last updated: '+new Date().toLocaleString()}catch(e){}}
async function loadCmd(){if(!cmd.value){cdet.value=null;return}try{const r=await fetch('/admin/ai-evolution/api/data?command='+encodeURIComponent(cmd.value)),d=await r.json();cdet.value=JSON.stringify(d,null,2)}catch(e){cdet.value='Error'}}
function selVer(v){cmd.value=v.tag||v.id||v.version;loadCmd()}
onMounted(()=>{ld();setInterval(ld,30000)})
return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer}})mount('#app')
</script></body></html>'''
def ai_evolution_page(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(self._EVOLUTION_HTML)))
self.end_headers()
self.wfile.write(self._EVOLUTION_HTML.encode())
def ai_evolution_data(self):
q=urllib.parse.urlparse(self.path).query
params=urllib.parse.parse_qs(q)
cmd_filter=(params.get('command') or [None])[0]
bl=jload(WS/'baseline'/'current.json',{})
current_ver=bl.get('current_baseline','V1R3C1')
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
versions=[]
if (WS/'baseline'/'promotions').exists():
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
d=jload(p,{});v=d.get('version',p.stem)
versions.append({'tag':v,'time':d.get('promoted_at','-'),'result':'ok'})
for t in tags:
if not any(v['tag']==t for v in versions):
versions.append({'tag':t,'time':'-','result':'if'})
agent_runs=[];ard=WS/'audit'/'agent-runs'
if ard.exists():
for f in sorted(ard.glob('*.json'),reverse=True)[:50]:
d=jload(f,{});agent_runs.append(d) if isinstance(d,dict) else None
cmd_dirs=[WS/'agents/commands/active',WS/'agents/commands/running',WS/'agents/commands/completed',WS/'agents/commands/failed']
commands=[]
for d in cmd_dirs:
if d.exists():
for f in sorted(d.glob('*.md')):
commands.append(f.name)
command_detail=None
if cmd_filter:
for d in cmd_dirs:
if d.exists():
fp=d/cmd_filter
if fp.exists():
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not command_detail:
command_detail={'command':cmd_filter,'error':'not_found'}
return self.sendj(command_detail)
completed=list((WS/'agents/commands/completed').glob('*.md')) if (WS/'agents/commands/completed').exists() else []
failed=list((WS/'agents/commands/failed').glob('*.md')) if (WS/'agents/commands/failed').exists() else []
stats={'total':len(commands),'success':len(completed),'failed':len(failed),'agent_runs':len(agent_runs)}
data={'baseline':current_ver,'versions':versions[-30:],'agent_runs':agent_runs,'commands':sorted(set(commands)),'stats':stats}
self.sendj(data)
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','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():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
@@ -1,92 +0,0 @@
"""Agent adapters for AIOS Search Gateway.
All runtime agents should call SearchGatewayAgentAdapter instead of performing
ad-hoc network search. This gives consistent provider fallback, profile
resolution, evidence capture, audit and event visibility.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from .search_gateway_orchestrator import SearchGatewayOrchestrator
@dataclass(frozen=True)
class AgentSearchContext:
agent: str
user_id: str | None = None
group_id: str | None = None
role: str = "user"
group_profile: dict[str, Any] | None = None
user_profile: dict[str, Any] | None = None
session_profile: dict[str, Any] | None = None
authorized_sources: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SearchGatewayAgentAdapter:
def __init__(self, orchestrator: SearchGatewayOrchestrator | None = None):
self.orchestrator = orchestrator or SearchGatewayOrchestrator()
def search(
self,
query: str,
context: AgentSearchContext,
intent: str = "general",
region: str | None = None,
time_range: str | None = None,
data_type: str | None = None,
domain_profiles: list[str] | None = None,
user_scope: str = "public",
correlation_id: str | None = None,
) -> dict[str, Any]:
payload = {
"query": query,
"requester": context.agent,
"intent": intent,
"region": region,
"time_range": time_range,
"data_type": data_type,
"domain_profiles": domain_profiles or [],
"user_scope": user_scope,
"authorized_sources": context.authorized_sources,
"group_profile": context.group_profile,
"user_profile": context.user_profile,
"session_profile": context.session_profile,
"correlation_id": correlation_id,
}
return self.orchestrator.execute(payload)
class OpenClawSearchClient(SearchGatewayAgentAdapter):
def search_public(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, user_scope="public", **kwargs)
class HermesSearchClient(SearchGatewayAgentAdapter):
def delegate_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, **kwargs)
class CodexSearchClient(SearchGatewayAgentAdapter):
def technical_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, intent=kwargs.pop("intent", "software_library"), **kwargs)
class CustomGPTSearchClient(SearchGatewayAgentAdapter):
def user_search(self, query: str, context: AgentSearchContext, **kwargs) -> dict[str, Any]:
return self.search(query, context, **kwargs)
__all__ = [
"AgentSearchContext",
"SearchGatewayAgentAdapter",
"OpenClawSearchClient",
"HermesSearchClient",
"CodexSearchClient",
"CustomGPTSearchClient",
]
@@ -1,73 +0,0 @@
"""Architect governance integration for Search Gateway.
Search Gateway is a shared platform capability. The Architect Agent periodically
reviews provider health, external search/browser capabilities, cost, security and
privacy impact, then emits C/R/V recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchArchitectureFinding:
finding_id: str
title: str
impact: str
recommendation: str
evidence: list[str] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchArchitectureDecision:
status: str
version_bump: str
reason: str
findings: list[SearchArchitectureFinding]
owner_gate_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"owner_gate_required": self.owner_gate_required,
"created_at": self.created_at,
}
class SearchArchitectIntegration:
def review(self, signals: dict[str, Any]) -> SearchArchitectureDecision:
findings: list[SearchArchitectureFinding] = []
if signals.get("provider_breaking_change"):
findings.append(SearchArchitectureFinding("search-provider-breaking", "Provider breaking change", "breaking", "owner_review"))
if signals.get("new_search_capability"):
findings.append(SearchArchitectureFinding("search-new-capability", "New search capability available", "architecture", "evaluate_adoption"))
if signals.get("cost_increase"):
findings.append(SearchArchitectureFinding("search-cost", "Search provider cost increase", "minor", "optimize_provider_order"))
if signals.get("security_advisory"):
findings.append(SearchArchitectureFinding("search-security", "Search security advisory", "breaking", "owner_review"))
if any(f.impact == "breaking" for f in findings):
return SearchArchitectureDecision("action_required", "V", "breaking search ecosystem/security change", findings, True)
if any(f.impact == "architecture" for f in findings):
return SearchArchitectureDecision("optimization_recommended", "R", "new search capability may improve architecture", findings, True)
if findings:
return SearchArchitectureDecision("minor_change_recommended", "C", "compatible provider optimization", findings, False)
return SearchArchitectureDecision("no_action", "none", "no relevant search architecture change", findings, False)
__all__ = ["SearchArchitectureFinding", "SearchArchitectureDecision", "SearchArchitectIntegration"]
-27
View File
@@ -1,27 +0,0 @@
"""Audit storage for Search Gateway executions."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
class SearchAuditStore:
def __init__(self, root: str = "runtime/search-gateway/audit"):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def append(self, record: dict[str, Any]) -> str:
path = self.root / f"{record.get('request_id', 'search')}.json"
path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
return str(path)
def list_recent(self) -> list[dict[str, Any]]:
result = []
for path in sorted(self.root.glob("*.json")):
result.append(json.loads(path.read_text(encoding="utf-8")))
return result[-50:]
__all__ = ["SearchAuditStore"]
-118
View File
@@ -1,118 +0,0 @@
"""Search Gateway event bridge.
Publishes normalized search lifecycle events into the AIOS Agent Event Bus when a
bus is available, with an in-memory fallback for tests and offline runs.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class SearchEventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class SearchEvent:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemorySearchEventBus:
def __init__(self):
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = SearchEvent(
id=f"search_evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class SearchEventBridge:
def __init__(self, bus: SearchEventPublisher | None = None):
self.bus = bus or InMemorySearchEventBus()
def search_started(self, request: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_started",
source=request.get("requester", "unknown"),
target="search-gateway",
summary=request.get("query", "search started"),
payload=request,
correlation_id=request.get("correlation_id"),
)
def provider_attempted(self, request_id: str, attempt: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_provider_attempted",
source="search-gateway",
target=attempt.get("provider", "unknown"),
summary=f"provider {attempt.get('provider', 'unknown')} {attempt.get('status', 'attempted')}",
payload=attempt,
correlation_id=request_id,
)
def fallback_triggered(self, request_id: str, reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="search_fallback_triggered",
source="search-gateway",
target="search-provider-router",
summary=reason,
payload={"reason": reason},
correlation_id=request_id,
)
def search_completed(self, response: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="search_completed",
source="search-gateway",
target=response.get("requester", "requester"),
summary=f"search {response.get('status', 'completed')}",
payload=response,
correlation_id=response.get("request_id"),
)
__all__ = ["SearchEvent", "InMemorySearchEventBus", "SearchEventBridge"]
-252
View File
@@ -1,252 +0,0 @@
"""Governed general-purpose Search Gateway for AIOS.
The gateway coordinates query planning, provider routing, fallback, evidence
normalization and compliance checks. It is provider-agnostic: runtime workers plug
in public or explicitly authorized search providers.
Search is a shared capability. Groups and users may customize provider order,
trusted domains, query expansion hints and domain profiles, but customization may
not override hard authorization, privacy, safety or audit rules.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
from .search_profile_resolver import DomainQueryProfileRegistry, SearchProfile, SearchProfileResolver
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchRequest:
query: str
requester: str
intent: str = "general"
region: str | None = None
time_range: str | None = None
data_type: str | None = None
user_scope: str = "public"
authorized_sources: list[str] = field(default_factory=list)
domain_profiles: list[str] = field(default_factory=list)
group_profile: dict[str, Any] | None = None
user_profile: dict[str, Any] | None = None
session_profile: dict[str, Any] | None = None
correlation_id: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchEvidence:
title: str
url: str | None
source: str
snippet: str
evidence_type: str = "web"
confidence: str = "medium"
retrieved_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchProviderAttempt:
provider: str
status: str
result_count: int = 0
reason: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchResponse:
request_id: str
status: str
answer: str
evidence: list[SearchEvidence]
attempts: list[SearchProviderAttempt]
confidence: str
expanded_queries: list[str] = field(default_factory=list)
resolved_profile: dict[str, Any] | None = None
need_clarification: list[str] = field(default_factory=list)
blocked_reasons: list[str] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"request_id": self.request_id,
"status": self.status,
"answer": self.answer,
"evidence": [e.to_dict() for e in self.evidence],
"attempts": [a.to_dict() for a in self.attempts],
"confidence": self.confidence,
"expanded_queries": self.expanded_queries,
"resolved_profile": self.resolved_profile,
"need_clarification": self.need_clarification,
"blocked_reasons": self.blocked_reasons,
"created_at": self.created_at,
}
class SearchProvider(Protocol):
name: str
def search(self, request: SearchRequest, queries: list[str]) -> tuple[list[SearchEvidence], SearchProviderAttempt]:
...
class QueryPlanner:
def __init__(self, domain_registry: DomainQueryProfileRegistry | None = None):
self.domain_registry = domain_registry or DomainQueryProfileRegistry()
def expand(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
base = request.query.strip()
queries = [base]
if request.region:
queries.append(f"{request.region} {base}")
if request.time_range:
queries.append(f"{base} {request.time_range}")
domain_profiles = list(dict.fromkeys([request.intent] + request.domain_profiles + profile.domain_profiles))
hints = list(profile.query_expansion_hints) + self.domain_registry.hints_for(domain_profiles)
for hint in hints:
queries.append(f"{base} {hint}")
for domain in profile.trusted_domains:
queries.append(f"site:{domain} {base}")
return list(dict.fromkeys([q for q in queries if q]))
class SearchPolicyGuard:
"""Blocks non-public or unauthorized collection patterns."""
FORBIDDEN_TERMS = (
"绕过登录",
"绕过验证码",
"验证码绕过",
"破解登录",
"个人成绩",
"学生隐私",
"受保护平台批量爬取",
)
def evaluate(self, request: SearchRequest, profile: SearchProfile) -> list[str]:
q = request.query.lower()
reasons: list[str] = []
for term in self.FORBIDDEN_TERMS:
if term.lower() in q:
reasons.append(f"blocked_term:{term}")
if request.user_scope not in {"public", "authorized_user_file", "authorized_link", "internal_archive"}:
reasons.append(f"unsupported_scope:{request.user_scope}")
if request.data_type in {"personal_grade", "student_private_data", "private_person_data"} and request.user_scope != "authorized_user_file":
reasons.append("private_data_requires_user_authorized_file")
if not profile.audit_required:
reasons.append("audit_cannot_be_disabled")
if not profile.fallback_required:
reasons.append("fallback_cannot_be_disabled")
return reasons
class SearchGateway:
def __init__(self, providers: list[SearchProvider] | None = None):
self.providers = providers or []
self.profile_resolver = SearchProfileResolver()
self.planner = QueryPlanner()
self.guard = SearchPolicyGuard()
def search(self, request: SearchRequest) -> SearchResponse:
request_id = request.correlation_id or f"search_{uuid4().hex[:12]}"
profile = self.profile_resolver.resolve(request.group_profile, request.user_profile, request.session_profile)
expanded_queries = self.planner.expand(request, profile)
blocked = self.guard.evaluate(request, profile)
if blocked:
return SearchResponse(
request_id=request_id,
status="blocked",
answer="该搜索请求涉及未授权访问、验证码/登录绕过、受保护平台批量抓取或个人隐私数据,已阻断。可改为搜索公开网页、官方公告、公开文档,或处理用户授权上传的文件/链接。",
evidence=[],
attempts=[],
confidence="high",
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
blocked_reasons=blocked,
)
evidence: list[SearchEvidence] = []
attempts: list[SearchProviderAttempt] = []
providers = self._ordered_providers(profile)
for provider in providers:
try:
provider_evidence, attempt = provider.search(request, expanded_queries)
evidence.extend(provider_evidence)
attempts.append(attempt)
if provider_evidence:
break
except Exception as exc:
attempts.append(SearchProviderAttempt(provider=getattr(provider, "name", "unknown"), status="failed", reason=str(exc)))
if evidence:
return SearchResponse(
request_id=request_id,
status="found",
answer="已找到公开或授权来源结果。",
evidence=evidence,
attempts=attempts,
confidence=self._confidence(evidence),
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
)
return SearchResponse(
request_id=request_id,
status="not_found",
answer="未找到足够可靠的公开或授权结果;已记录 provider 尝试和失败原因。",
evidence=[],
attempts=attempts,
confidence="low",
expanded_queries=expanded_queries,
resolved_profile=profile.to_dict(),
need_clarification=self._clarification(request),
)
def _ordered_providers(self, profile: SearchProfile) -> list[SearchProvider]:
if not self.providers:
return []
by_name = {provider.name: provider for provider in self.providers}
ordered = [by_name[name] for name in profile.provider_order if name in by_name]
remaining = [provider for provider in self.providers if provider.name not in {p.name for p in ordered}]
return ordered + remaining
def _confidence(self, evidence: list[SearchEvidence]) -> str:
if any(e.confidence == "high" for e in evidence):
return "high"
if evidence:
return "medium"
return "low"
def _clarification(self, request: SearchRequest) -> list[str]:
if request.intent == "education_exam_data":
return ["请补充学校名称、联考名称、年级、科目,以及需要试卷/答案还是成绩统计。"]
if request.intent in {"company_research", "policy_regulation", "software_library", "product_research"}:
return ["请补充主体名称、地区/版本、时间范围、需要的数据类型或可信来源范围。"]
return ["请补充更具体的主体、平台、地区、时间、数据类型或授权文件/链接。"]
__all__ = [
"SearchRequest",
"SearchEvidence",
"SearchProviderAttempt",
"SearchResponse",
"SearchProvider",
"QueryPlanner",
"SearchPolicyGuard",
"SearchGateway",
]
-65
View File
@@ -1,65 +0,0 @@
"""API endpoints for AIOS Search Gateway."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .search_gateway import SearchGateway, SearchRequest
from .search_gateway_dashboard import SearchGatewayDashboard
gateway = SearchGateway()
dashboard = SearchGatewayDashboard()
def search_gateway_request(payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
return gateway.search(request).to_dict()
def get_search_gateway_status() -> dict[str, Any]:
return {
"module": "search-gateway",
"status": "ready",
"providers_configured": len(gateway.providers),
"policy_guard": "enabled",
"profile_customization": "enabled",
"fallback_required_when_provider_unavailable": True,
}
def get_search_gateway_overview() -> dict[str, Any]:
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/search", tags=["search-gateway"])
@router.get("/status", operation_id="getSearchGatewayStatus")
def status():
return get_search_gateway_status()
@router.get("/overview", operation_id="getSearchGatewayOverview")
def overview():
return get_search_gateway_overview()
@router.post("/query", operation_id="runSearchGatewayQuery")
def query(payload: dict[str, Any]):
return search_gateway_request(payload)
return router
router = build_router()
__all__ = [
"router",
"search_gateway_request",
"get_search_gateway_status",
"get_search_gateway_overview",
]
@@ -1,85 +0,0 @@
"""Search Gateway Web UI dashboard model."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SearchDashboardMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class SearchDashboardPanel:
key: str
title: str
metrics: list[SearchDashboardMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class SearchGatewayDashboard:
def overview(self, search_runs: list[dict[str, Any]] | None = None, providers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
search_runs = search_runs or []
providers = providers or []
blocked = [r for r in search_runs if r.get("status") == "blocked"]
not_found = [r for r in search_runs if r.get("status") == "not_found"]
provider_failures = [p for p in providers if p.get("status") == "failed"]
panels = [
SearchDashboardPanel(
"search_health",
"搜索健康度",
[
SearchDashboardMetric("total_runs", "搜索请求数", len(search_runs)),
SearchDashboardMetric("not_found", "未找到结果", len(not_found), "warn" if not_found else "ok"),
SearchDashboardMetric("blocked", "合规阻断", len(blocked), "warn" if blocked else "ok"),
],
),
SearchDashboardPanel(
"provider_health",
"Provider 可用性",
[
SearchDashboardMetric("providers", "Provider 数量", len(providers)),
SearchDashboardMetric("provider_failures", "Provider 失败", len(provider_failures), "warn" if provider_failures else "ok"),
],
),
SearchDashboardPanel(
"evidence_quality",
"证据质量",
[
SearchDashboardMetric("with_evidence", "有证据的回答", len([r for r in search_runs if r.get("evidence")])) ,
SearchDashboardMetric("fallback_required", "要求多源降级", True),
],
),
]
return {
"module": "search-gateway",
"route": "/admin/ai-evolution/search",
"title": "AIOS 联网搜索治理中心",
"status": "attention_required" if provider_failures else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"recent_runs": search_runs[-20:],
}
__all__ = ["SearchDashboardMetric", "SearchDashboardPanel", "SearchGatewayDashboard"]
@@ -1,41 +0,0 @@
"""Runtime orchestrator joining Search Gateway, registry, audit and events."""
from __future__ import annotations
from typing import Any
from .search_audit_store import SearchAuditStore
from .search_event_bridge import SearchEventBridge
from .search_gateway import SearchGateway, SearchRequest
from .search_provider_registry import SearchProviderRegistry
class SearchGatewayOrchestrator:
def __init__(self):
self.registry = SearchProviderRegistry()
self.audit = SearchAuditStore()
self.events = SearchEventBridge()
self.gateway = SearchGateway(self.registry.ordered())
def execute(self, payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
self.events.search_started(request.to_dict())
response = self.gateway.search(request)
result = response.to_dict()
for attempt in result.get("attempts", []):
self.events.provider_attempted(result["request_id"], attempt)
if len(result.get("attempts", [])) > 1:
self.events.fallback_triggered(result["request_id"], "multiple providers attempted")
self.audit.append(result)
self.events.search_completed(result)
return result
def status(self) -> dict[str, Any]:
return {
"registry": self.registry.status(),
"audit": "enabled",
"events": "enabled",
}
__all__ = ["SearchGatewayOrchestrator"]
@@ -1,124 +0,0 @@
"""Search profile resolver for general-purpose AIOS Search Gateway.
The Search Gateway is a shared platform capability. Groups and users may customize
provider preference, query expansion hints, trusted sources and domain profiles on
top of the shared base profile. Customization is intentionally layered and cannot
override hard safety, authorization or audit rules.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
HARD_BLOCKED_BEHAVIORS = {
"bypass_login",
"bypass_captcha",
"scrape_private_student_grades",
"scrape_student_private_data",
"bulk_scrape_protected_platforms",
"collect_data_without_authorization",
}
@dataclass(frozen=True)
class SearchProfile:
profile_id: str
scope: str
provider_order: list[str] = field(default_factory=lambda: [
"cache",
"official_site",
"general_web",
"vertical_source",
"browser_fetch",
"user_authorized_file",
"internal_archive",
])
trusted_domains: list[str] = field(default_factory=list)
query_expansion_hints: list[str] = field(default_factory=list)
domain_profiles: list[str] = field(default_factory=list)
blocked_behaviors: set[str] = field(default_factory=lambda: set(HARD_BLOCKED_BEHAVIORS))
audit_required: bool = True
fallback_required: bool = True
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["blocked_behaviors"] = sorted(self.blocked_behaviors)
return data
class SearchProfileResolver:
"""Resolves base -> group -> user -> session search preferences."""
def base_profile(self) -> SearchProfile:
return SearchProfile(profile_id="base", scope="global")
def resolve(
self,
group_profile: dict[str, Any] | None = None,
user_profile: dict[str, Any] | None = None,
session_profile: dict[str, Any] | None = None,
) -> SearchProfile:
profile = self.base_profile().to_dict()
for overlay in (group_profile, user_profile, session_profile):
if overlay:
profile = self._merge(profile, overlay)
profile["blocked_behaviors"] = set(HARD_BLOCKED_BEHAVIORS)
profile["audit_required"] = True
profile["fallback_required"] = True
return SearchProfile(**profile)
def _merge(self, base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
merged = dict(base)
if overlay.get("provider_order"):
merged["provider_order"] = list(overlay["provider_order"])
for list_key in ("trusted_domains", "query_expansion_hints", "domain_profiles"):
merged[list_key] = self._dedupe(list(merged.get(list_key, [])) + list(overlay.get(list_key, [])))
merged["profile_id"] = overlay.get("profile_id", merged.get("profile_id", "custom"))
merged["scope"] = overlay.get("scope", merged.get("scope", "custom"))
return merged
def _dedupe(self, items: list[str]) -> list[str]:
return list(dict.fromkeys([item for item in items if item]))
class DomainQueryProfileRegistry:
"""Optional domain-specific expansions for the general Search Gateway."""
DEFAULT_PROFILES = {
"education_exam_data": [
"学校 年级 科目 试卷 答案 成绩统计",
"联考 月考 期中 期末 质量检测",
],
"policy_regulation": [
"官方 通知 政策 解读 实施细则",
"site:gov.cn OR site:edu.cn",
],
"software_library": [
"release notes changelog migration guide",
"official documentation GitHub issues",
],
"product_research": [
"specs review comparison price availability",
"official page manual support",
],
"company_research": [
"official announcement press release filings",
"leadership product financial report",
],
}
def hints_for(self, domain_profiles: list[str]) -> list[str]:
hints: list[str] = []
for profile in domain_profiles:
hints.extend(self.DEFAULT_PROFILES.get(profile, []))
return list(dict.fromkeys(hints))
__all__ = [
"HARD_BLOCKED_BEHAVIORS",
"SearchProfile",
"SearchProfileResolver",
"DomainQueryProfileRegistry",
]
@@ -1,121 +0,0 @@
"""Provider registry for the general AIOS Search Gateway.
The registry makes provider availability explicit and auditable. Provider
implementations are pluggable: production can register real public search,
official-site, vertical-source, browser-fetch, authorized-file or archive
providers without changing the Search Gateway core.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from .search_gateway import SearchEvidence, SearchProviderAttempt, SearchRequest
@dataclass(frozen=True)
class SearchProviderConfig:
name: str
provider_type: str
enabled: bool = True
requires_secret: bool = False
supports_public_web: bool = True
supports_authorized_sources: bool = False
health: str = "unknown"
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class NullSearchProvider:
"""Safe placeholder provider used until runtime wires a real adapter."""
def __init__(self, config: SearchProviderConfig):
self.config = config
self.name = config.name
def search(self, request: SearchRequest, queries: list[str]):
return [], SearchProviderAttempt(
provider=self.name,
status="not_configured" if self.config.enabled else "disabled",
result_count=0,
reason="provider adapter not wired in runtime",
)
class StaticArchiveProvider:
"""Small deterministic provider for tests and internal archived evidence."""
name = "internal_archive"
def __init__(self, records: list[dict[str, Any]] | None = None):
self.records = records or []
def search(self, request: SearchRequest, queries: list[str]):
evidence: list[SearchEvidence] = []
for record in self.records:
haystack = " ".join([record.get("title", ""), record.get("snippet", ""), record.get("tags", "")]).lower()
if any(q.lower() in haystack for q in queries):
evidence.append(
SearchEvidence(
title=record.get("title", "archive result"),
url=record.get("url"),
source=self.name,
snippet=record.get("snippet", ""),
evidence_type="internal_archive",
confidence=record.get("confidence", "medium"),
)
)
return evidence, SearchProviderAttempt(provider=self.name, status="ok", result_count=len(evidence))
class SearchProviderRegistry:
DEFAULT_PROVIDER_CONFIGS = [
SearchProviderConfig("cache", "cache", supports_public_web=False),
SearchProviderConfig("official_site", "official_site"),
SearchProviderConfig("general_web", "general_web"),
SearchProviderConfig("vertical_source", "vertical_source"),
SearchProviderConfig("browser_fetch", "browser_fetch"),
SearchProviderConfig("user_authorized_file", "authorized_file", supports_public_web=False, supports_authorized_sources=True),
SearchProviderConfig("internal_archive", "archive", supports_public_web=False),
]
def __init__(self):
self.configs: dict[str, SearchProviderConfig] = {}
self.providers: dict[str, Any] = {}
for config in self.DEFAULT_PROVIDER_CONFIGS:
self.register_config(config)
def register_config(self, config: SearchProviderConfig) -> None:
self.configs[config.name] = config
self.providers.setdefault(config.name, NullSearchProvider(config))
def register_provider(self, name: str, provider: Any, config: SearchProviderConfig | None = None) -> None:
if config:
self.configs[name] = config
elif name not in self.configs:
self.configs[name] = SearchProviderConfig(name=name, provider_type="custom")
self.providers[name] = provider
def ordered(self, order: list[str] | None = None) -> list[Any]:
order = order or list(self.configs)
result = [self.providers[name] for name in order if name in self.providers and self.configs[name].enabled]
result.extend([provider for name, provider in self.providers.items() if name not in order and self.configs[name].enabled])
return result
def status(self) -> dict[str, Any]:
return {
"providers": [self.configs[name].to_dict() for name in sorted(self.configs)],
"enabled": len([c for c in self.configs.values() if c.enabled]),
"configured": len([name for name, provider in self.providers.items() if not isinstance(provider, NullSearchProvider)]),
}
__all__ = [
"SearchProviderConfig",
"NullSearchProvider",
"StaticArchiveProvider",
"SearchProviderRegistry",
]
@@ -1,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');
-41
View File
@@ -1,41 +0,0 @@
"""Task Center action API model for Web UI buttons."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .task_lifecycle import TaskLifecycle
lifecycle = TaskLifecycle()
def get_task_actions(task: dict[str, Any], role: str) -> dict[str, Any]:
return {
"task_id": task.get("task_id"),
"actions": lifecycle.available_actions(task, role),
}
def execute_task_action(task: dict[str, Any], action: str, role: str, reason: str | None = None):
return lifecycle.transition(task, action, role, reason)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-actions"])
@router.post("/{task_id}/actions/{action}", operation_id="executeTaskAction")
def action(task_id: str, action: str, payload: dict[str, Any]):
return execute_task_action(payload.get("task", {}), action, payload.get("role", "user"), payload.get("reason"))
return router
router = build_router()
__all__ = ["router", "get_task_actions", "execute_task_action"]
-115
View File
@@ -1,115 +0,0 @@
"""AIOS Task Center domain model.
The Task Center gives owners and authorized users a single Web UI page showing:
who assigned a task, who is executing it, progress, elapsed time, remaining
work, and whether completion notifications were sent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_time(value: str | None) -> datetime | None:
if not value:
return None
return datetime.fromisoformat(value.replace("Z", "+00:00"))
@dataclass(frozen=True)
class TaskNotificationStatus:
required: bool = True
sent: bool = False
channel: str | None = None
sent_at: str | None = None
recipient_scope: str | None = None
last_error: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class TaskProgress:
percent: int = 0
current_step: str = "pending"
completed_steps: list[str] = field(default_factory=list)
remaining_steps: list[str] = field(default_factory=list)
blocked_reason: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AIOSTaskRecord:
task_id: str
title: str
assigned_by: str
executor: str
source: str
status: str = "pending"
priority: str = "normal"
created_at: str = field(default_factory=utc_now)
started_at: str | None = None
updated_at: str = field(default_factory=utc_now)
completed_at: str | None = None
progress: TaskProgress = field(default_factory=TaskProgress)
notification: TaskNotificationStatus = field(default_factory=TaskNotificationStatus)
owner_gate_required: bool = False
owner_gate_status: str | None = None
evidence: list[str] = field(default_factory=list)
def elapsed_seconds(self) -> int:
start = parse_time(self.started_at) or parse_time(self.created_at)
end = parse_time(self.completed_at) or datetime.now(timezone.utc)
return int((end - start).total_seconds()) if start else 0
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["elapsed_seconds"] = self.elapsed_seconds()
data["progress"] = self.progress.to_dict()
data["notification"] = self.notification.to_dict()
return data
class TaskCenter:
def summarize(self, tasks: list[AIOSTaskRecord] | list[dict[str, Any]] | None = None) -> dict[str, Any]:
raw_tasks = tasks or []
task_dicts = [t.to_dict() if hasattr(t, "to_dict") else dict(t) for t in raw_tasks]
active = [t for t in task_dicts if t.get("status") in {"running", "pending", "blocked"}]
blocked = [t for t in task_dicts if t.get("status") == "blocked"]
completed = [t for t in task_dicts if t.get("status") == "completed"]
notification_pending = [
t for t in completed
if t.get("notification", {}).get("required") and not t.get("notification", {}).get("sent")
]
return {
"module": "task-center",
"route": "/admin/ai-evolution/tasks",
"title": "AIOS 任务中心",
"status": "attention_required" if blocked or notification_pending else "healthy",
"generated_at": utc_now(),
"summary": {
"total": len(task_dicts),
"active": len(active),
"blocked": len(blocked),
"completed": len(completed),
"notification_pending": len(notification_pending),
},
"tasks": sorted(task_dicts, key=lambda t: t.get("updated_at", ""), reverse=True),
}
__all__ = [
"TaskNotificationStatus",
"TaskProgress",
"AIOSTaskRecord",
"TaskCenter",
]
-34
View File
@@ -1,34 +0,0 @@
"""Task Center API."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .task_center import TaskCenter
task_center = TaskCenter()
def get_task_overview(tasks: list[dict[str, Any]] | None = None):
return task_center.summarize(tasks)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-center"])
@router.get("/overview", operation_id="getTaskCenterOverview")
def overview():
return get_task_overview()
return router
router = build_router()
__all__ = ["router", "get_task_overview"]
-81
View File
@@ -1,81 +0,0 @@
"""Task Center visualization model."""
from __future__ import annotations
from typing import Any
class TaskDashboardView:
def build(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
tasks = tasks or []
return {
"module": "task-center",
"route": "/admin/ai-evolution/tasks",
"title": "AIOS Task Center",
"views": {
"kanban": self._kanban(tasks),
"timeline": self._timeline(tasks),
"flow": self._flow(tasks),
"statistics": self._statistics(tasks),
},
"tasks": [self._task_card(task) for task in tasks],
}
def _task_card(self, task: dict[str, Any]) -> dict[str, Any]:
return {
"task_id": task.get("task_id"),
"title": task.get("title"),
"assigned_by": task.get("assigned_by"),
"executor": task.get("executor"),
"status": task.get("status"),
"progress": task.get("progress", {}),
"elapsed_seconds": task.get("elapsed_seconds", 0),
"remaining_steps": task.get("progress", {}).get("remaining_steps", []),
"notification": task.get("notification", {}),
"actions": [
"start",
"update_progress",
"block",
"request_approval",
"approve",
"reject",
"retry",
"complete",
"notify",
"close",
],
}
def _kanban(self, tasks):
columns = {"pending": [], "running": [], "blocked": [], "waiting_approval": [], "completed": []}
for task in tasks:
columns.setdefault(task.get("status", "pending"), []).append(task.get("task_id"))
return columns
def _timeline(self, tasks):
return [
{
"task_id": task.get("task_id"),
"updated_at": task.get("updated_at"),
"status": task.get("status"),
}
for task in tasks
]
def _flow(self, tasks):
return {
"nodes": ["created", "assigned", "running", "blocked", "approval", "completed", "closed"],
"task_count": len(tasks),
}
def _statistics(self, tasks):
return {
"total": len(tasks),
"running": len([t for t in tasks if t.get("status") == "running"]),
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
"waiting_approval": len([t for t in tasks if t.get("status") == "waiting_approval"]),
"completed": len([t for t in tasks if t.get("status") == "completed"]),
}
__all__ = ["TaskDashboardView"]
-129
View File
@@ -1,129 +0,0 @@
"""Task Center event bridge.
Converts task lifecycle actions and agent execution events into normalized AIOS
Task Center events. Uses duck-typed Agent Event Bus publisher when available and
an in-memory fallback for tests/offline runs.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class TaskEventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class TaskEvent:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryTaskEventBus:
def __init__(self):
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = TaskEvent(
id=f"task_evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class TaskEventBridge:
def __init__(self, bus: TaskEventPublisher | None = None):
self.bus = bus or InMemoryTaskEventBus()
def task_created(self, task: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="task_created",
source=task.get("assigned_by", "unknown"),
target=task.get("executor", "task-center"),
summary=task.get("title", "task created"),
payload=task,
correlation_id=task.get("task_id"),
)
def task_updated(self, task: dict[str, Any], action: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_updated",
source=task.get("executor", "task-center"),
target="task-center",
summary=f"task {action}: {task.get('status')}",
payload={"action": action, "task": task},
correlation_id=task.get("task_id"),
)
def task_blocked(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_blocked",
source=task.get("executor", "unknown"),
target="owner-gate",
summary=reason,
payload={"task": task, "reason": reason},
correlation_id=task.get("task_id"),
)
def approval_requested(self, task: dict[str, Any], reason: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_approval_requested",
source=task.get("executor", "unknown"),
target="owner-gate",
summary=reason,
payload={"task": task, "reason": reason},
correlation_id=task.get("task_id"),
)
def notification_sent(self, task: dict[str, Any], channel: str) -> dict[str, Any]:
return self.bus.publish(
kind="task_notification_sent",
source="notification-agent",
target=task.get("assigned_by", "owner"),
summary=f"notification sent via {channel}",
payload={"task": task, "channel": channel},
correlation_id=task.get("task_id"),
)
__all__ = ["TaskEvent", "InMemoryTaskEventBus", "TaskEventBridge"]
-147
View File
@@ -1,147 +0,0 @@
"""AIOS Task Lifecycle.
Defines a governed task state machine for task creation, assignment, execution,
blocking, approval, retry, completion, notification and closure.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
TERMINAL_STATES = {"completed", "cancelled", "failed", "closed"}
@dataclass(frozen=True)
class TaskTransition:
action: str
from_states: set[str]
to_state: str
required_role: str = "agent"
requires_reason: bool = False
notify: bool = False
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["from_states"] = sorted(self.from_states)
return data
class TaskLifecycle:
"""Central task state machine."""
TRANSITIONS = {
"assign": TaskTransition("assign", {"new", "pending"}, "assigned", "dispatcher"),
"start": TaskTransition("start", {"assigned", "pending"}, "running", "executor"),
"update_progress": TaskTransition("update_progress", {"running"}, "running", "executor"),
"block": TaskTransition("block", {"running", "assigned", "pending"}, "blocked", "executor", requires_reason=True, notify=True),
"request_approval": TaskTransition("request_approval", {"running", "blocked"}, "waiting_approval", "executor", requires_reason=True, notify=True),
"approve": TaskTransition("approve", {"waiting_approval"}, "approved", "owner", notify=True),
"reject": TaskTransition("reject", {"waiting_approval"}, "rejected", "owner", requires_reason=True, notify=True),
"resume": TaskTransition("resume", {"blocked", "approved"}, "running", "executor"),
"retry": TaskTransition("retry", {"failed", "blocked"}, "running", "executor", notify=True),
"complete": TaskTransition("complete", {"running", "approved"}, "completed", "executor", notify=True),
"notify": TaskTransition("notify", {"completed"}, "completed", "notifier"),
"close": TaskTransition("close", {"completed", "cancelled", "failed"}, "closed", "owner"),
"cancel": TaskTransition("cancel", {"new", "pending", "assigned", "running", "blocked", "waiting_approval"}, "cancelled", "owner", requires_reason=True, notify=True),
"fail": TaskTransition("fail", {"running", "assigned"}, "failed", "executor", requires_reason=True, notify=True),
}
ROLE_ORDER = {
"user": 1,
"agent": 2,
"executor": 3,
"dispatcher": 4,
"notifier": 4,
"group_admin": 5,
"owner": 9,
}
def available_actions(self, task: dict[str, Any], role: str) -> list[dict[str, Any]]:
state = task.get("status", "new")
actions = []
for name, transition in self.TRANSITIONS.items():
allowed_state = state in transition.from_states
allowed_role = self._role_allows(role, transition.required_role)
actions.append({
"action": name,
"label": self._label(name),
"enabled": allowed_state and allowed_role,
"from_state": state,
"to_state": transition.to_state,
"required_role": transition.required_role,
"requires_reason": transition.requires_reason,
"notify": transition.notify,
"disabled_reason": None if allowed_state and allowed_role else self._disabled_reason(allowed_state, allowed_role, transition),
})
return actions
def transition(self, task: dict[str, Any], action: str, actor_role: str, reason: str | None = None) -> dict[str, Any]:
if action not in self.TRANSITIONS:
raise ValueError(f"unknown task action: {action}")
transition = self.TRANSITIONS[action]
state = task.get("status", "new")
if state not in transition.from_states:
raise ValueError(f"action {action} not allowed from state {state}")
if not self._role_allows(actor_role, transition.required_role):
raise PermissionError(f"role {actor_role} cannot execute {action}; requires {transition.required_role}")
if transition.requires_reason and not reason:
raise ValueError(f"action {action} requires reason")
updated = dict(task)
updated["status"] = transition.to_state
updated["updated_at"] = utc_now()
history = list(updated.get("history", []))
history.append({
"action": action,
"from": state,
"to": transition.to_state,
"actor_role": actor_role,
"reason": reason,
"ts": utc_now(),
})
updated["history"] = history
if action == "complete":
updated["completed_at"] = utc_now()
notification = dict(updated.get("notification", {}))
notification.setdefault("required", True)
notification.setdefault("sent", False)
updated["notification"] = notification
return updated
def _role_allows(self, role: str, required: str) -> bool:
return self.ROLE_ORDER.get(role, 0) >= self.ROLE_ORDER.get(required, 0)
def _label(self, action: str) -> str:
labels = {
"assign": "分配任务",
"start": "开始执行",
"update_progress": "更新进度",
"block": "标记阻塞",
"request_approval": "请求审批",
"approve": "批准",
"reject": "拒绝",
"resume": "继续执行",
"retry": "重试",
"complete": "标记完成",
"notify": "发送通知",
"close": "关闭任务",
"cancel": "取消任务",
"fail": "标记失败",
}
return labels.get(action, action)
def _disabled_reason(self, allowed_state: bool, allowed_role: bool, transition: TaskTransition) -> str:
if not allowed_state:
return "当前状态不允许该操作"
if not allowed_role:
return f"需要角色: {transition.required_role}"
return "不可用"
__all__ = ["TaskTransition", "TaskLifecycle", "TERMINAL_STATES"]
-50
View File
@@ -1,50 +0,0 @@
"""Task Center orchestrator.
Coordinates task ingestion, persistence, lifecycle updates, events and statistics.
"""
from __future__ import annotations
from typing import Any
from .task_event_bridge import TaskEventBridge
from .task_source_ingestor import TaskSourceIngestor
from .task_statistics import TaskStatistics
from .task_store import TaskStore
class TaskOrchestrator:
def __init__(self):
self.ingestor = TaskSourceIngestor()
self.store = TaskStore()
self.events = TaskEventBridge()
self.statistics = TaskStatistics()
def create_from_command(self, path: str, bucket: str = "active") -> dict[str, Any]:
task = self.ingestor.command_file_to_task(path, bucket)
self.store.append(task)
self.events.task_created(task)
return task
def create_from_agent_execution(self, record: dict[str, Any]) -> dict[str, Any]:
task = self.ingestor.agent_execution_to_task(record)
self.store.append(task)
self.events.task_created(task)
return task
def sync_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
task = self.ingestor.event_to_task(event)
if task:
self.store.append(task)
self.events.task_updated(task, event.get("kind", "event"))
return task
def dashboard(self) -> dict[str, Any]:
tasks = self.store.latest()
return {
"tasks": tasks,
"statistics": self.statistics.summarize(tasks),
}
__all__ = ["TaskOrchestrator"]
-159
View File
@@ -1,159 +0,0 @@
"""Task source ingestion for AIOS Task Center.
Converts Command Watcher files, OpenClaw/Hermes/Codex execution records and
Generic Agent Event Bus records into normalized Task Center task dictionaries.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class TaskSourceRecord:
source_type: str
source_id: str
title: str
assigned_by: str
executor: str
status: str
priority: str = "normal"
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
evidence: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_task(self) -> dict[str, Any]:
return {
"task_id": f"task-{self.source_type}-{self.source_id}",
"title": self.title,
"assigned_by": self.assigned_by,
"executor": self.executor,
"source": self.source_type,
"status": self.status,
"priority": self.priority,
"created_at": self.created_at,
"updated_at": self.updated_at,
"progress": {
"percent": self._progress_percent(),
"current_step": self.status,
"completed_steps": [],
"remaining_steps": [],
"blocked_reason": self.metadata.get("blocked_reason"),
},
"notification": {
"required": True,
"sent": bool(self.metadata.get("notification_sent")),
"channel": self.metadata.get("notification_channel"),
"sent_at": self.metadata.get("notification_sent_at"),
},
"evidence": self.evidence,
"metadata": self.metadata,
}
def _progress_percent(self) -> int:
mapping = {
"new": 0,
"pending": 5,
"assigned": 15,
"running": 50,
"blocked": 50,
"waiting_approval": 60,
"approved": 70,
"completed": 100,
"closed": 100,
"failed": 100,
"cancelled": 100,
}
return mapping.get(self.status, 0)
class TaskSourceIngestor:
COMMAND_STATUS_MAP = {
"active": "pending",
"running": "running",
"completed": "completed",
"failed": "failed",
"archive": "closed",
}
def command_file_to_task(self, path: str, bucket: str = "active") -> dict[str, Any]:
p = Path(path)
title = p.stem
return TaskSourceRecord(
source_type="command_watcher",
source_id=title,
title=title,
assigned_by="command-watcher",
executor=self._infer_executor(title),
status=self.COMMAND_STATUS_MAP.get(bucket, "pending"),
priority="high" if "P0" in title or "fix" in title.lower() else "normal",
evidence=[str(p)],
metadata={"bucket": bucket},
).to_task()
def agent_execution_to_task(self, record: dict[str, Any]) -> dict[str, Any]:
source_id = record.get("execution_id", record.get("id", "unknown"))
status = record.get("status", "running")
return TaskSourceRecord(
source_type="agent_execution",
source_id=source_id,
title=record.get("title", f"Agent execution {source_id}"),
assigned_by=record.get("assigned_by", record.get("entry_agent", "agent-system")),
executor=record.get("closed_by", record.get("entry_agent", "unknown-agent")),
status=status,
priority=record.get("priority", "normal"),
created_at=record.get("started_at", utc_now()),
updated_at=record.get("ended_at", utc_now()),
evidence=[record.get("execution_id", source_id)],
metadata=record,
).to_task()
def event_to_task(self, event: dict[str, Any]) -> dict[str, Any] | None:
kind = event.get("kind")
payload = event.get("payload", {})
if kind not in {"task_created", "task_updated", "agent_started", "agent_completed", "agent_failed", "search_started", "search_completed"}:
return None
source_id = event.get("correlation_id") or event.get("id", "event")
status = self._status_from_event(kind, payload)
return TaskSourceRecord(
source_type="event_bus",
source_id=source_id,
title=payload.get("title") or event.get("summary", kind),
assigned_by=event.get("source", "event-bus"),
executor=event.get("target", "task-center"),
status=status,
created_at=event.get("created_at", utc_now()),
updated_at=event.get("created_at", utc_now()),
evidence=[event.get("id", source_id)],
metadata={"event_kind": kind, "payload": payload},
).to_task()
def _status_from_event(self, kind: str, payload: dict[str, Any]) -> str:
if kind.endswith("completed"):
return "completed"
if kind.endswith("failed"):
return "failed"
return payload.get("status", "running")
def _infer_executor(self, title: str) -> str:
lower = title.lower()
if "openclaw" in lower:
return "openclaw"
if "hermes" in lower:
return "hermes"
if "codex" in lower:
return "codex"
if "search" in lower:
return "openclaw"
return "openclaw"
__all__ = ["TaskSourceRecord", "TaskSourceIngestor"]
-33
View File
@@ -1,33 +0,0 @@
"""Task Center statistics."""
from typing import Any
class TaskStatistics:
def summarize(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
tasks = tasks or []
completed = [t for t in tasks if t.get("status") == "completed"]
blocked = [t for t in tasks if t.get("status") == "blocked"]
waiting = [t for t in tasks if t.get("status") == "waiting_approval"]
elapsed = [t.get("elapsed_seconds", 0) for t in tasks if t.get("elapsed_seconds") is not None]
return {
"total": len(tasks),
"completed": len(completed),
"blocked": len(blocked),
"waiting_approval": len(waiting),
"completion_rate": round(len(completed) / len(tasks) * 100, 2) if tasks else 0,
"average_elapsed_seconds": round(sum(elapsed) / len(elapsed), 2) if elapsed else 0,
"top_blockers": self._top_values(blocked, "blocker"),
"executor_distribution": self._top_values(tasks, "executor"),
}
def _top_values(self, tasks: list[dict[str, Any]], key: str):
result: dict[str, int] = {}
for task in tasks:
value = task.get(key)
if value:
result[value] = result.get(value, 0) + 1
return result
__all__ = ["TaskStatistics"]
-29
View File
@@ -1,29 +0,0 @@
"""Persistent Task Center store."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
class TaskStore:
def __init__(self, root: str = "runtime/task-center"):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
self.path = self.root / "tasks.jsonl"
def append(self, task: dict[str, Any]) -> None:
with self.path.open("a", encoding="utf-8") as f:
f.write(json.dumps(task, ensure_ascii=False) + "\n")
def list_all(self) -> list[dict[str, Any]]:
if not self.path.exists():
return []
return [json.loads(line) for line in self.path.read_text(encoding="utf-8").splitlines() if line]
def latest(self) -> list[dict[str, Any]]:
return self.list_all()[-100:]
__all__ = ["TaskStore"]
-29
View File
@@ -1,29 +0,0 @@
# Agent Instruction Center
Version-controlled execution instructions for AIOS agents.
## Supported agents
- codex
- openclaw
- hermes
- windows_codex_app
- custom_gpt_review
## Workflow
1. Create versioned instruction file.
2. Target agent executes instruction.
3. Result is recorded with GitOps audit information.
## Naming
agent-instructions/<agent>/<instruction-name>-v<version>.md
## Rules
- GitOps managed only.
- Patch-first workflow.
- No secrets, tokens, passwords or credentials.
- Production changes require approval.
- Main branch changes require approval.
-34
View File
@@ -1,34 +0,0 @@
# Agent Execution Instruction
Instruction ID:
Version:
Target Agent:
## Objective
## Context
## Execution Steps
## Constraints
- GitOps only
- Patch-first
- No secrets
- No force push
- Production changes require approval
## Validation
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
-1
View File
@@ -1 +0,0 @@
# Placeholder for versioned Codex instructions.
@@ -1,65 +0,0 @@
# Codex Fix Patches Artifact Lifecycle Instruction
Instruction ID: fix-patches-artifact-lifecycle-v1
Version: v1
Target Agent: codex
## Objective
Manage the lifecycle of artifacts in the `patches/` directory — specifically, clean up stale or committed artifacts (`.branch.json`, `nl/*.json`, standalone `.json` patch files) that are no longer needed.
## Background
The ChatGPT Actions API writes several types of artifacts into `patches/`:
1. **Branch metadata** (`patches/patch_*.branch.json`) — created when the `branch()` action writes ahead. These are summary records of created branches.
2. **Patch files** (`patches/*.json`, top level) — created when the `patch()` action saves a JSON patch proposal. Example: `patches/self-evolution-agent-orchestration-policy.json`.
3. **Natural language plan files** (`patches/nl/nl-*.json`) — created when `nl()` action records a natural-language plan. These are transient planning artifacts.
None of these files represent durable long-term state. Once committed, they are snapshots of ephemeral decisions. The `patches/` directory should not grow unboundedly with stale artifacts.
## Preconditions
- GitOps authentication healthy.
- Current working branch should not be diverging from main unnecessarily for this cleanup.
- Run from the workspace root.
## Steps
### Phase 1: Assess Current State
1. List all files under `patches/`.
2. For each file, check its git log (`git log --oneline patches/<file>`) to understand its purpose and age.
3. Identify which files are:
- Stale `.branch.json` files describing branches that have already been merged.
- Stale `nl/` plan files that were experimental, incomplete, or superseded.
- Patch files that are still relevant for the current self-evolution state.
### Phase 2: Clean Up Stale Artifacts
For each stale artifact file:
1. Remove it with `git rm`.
2. Create a single commit message describing the batch cleanup, e.g.:
`chore(patches): remove stale artifact files [nl/*.json|branch.json]`
### Phase 3: Housekeeping (Optional)
If the file `self-evolution-agent-orchestration-policy.json` in patches/ is superseded by the active `config/self-evolution/agent-orchestration-policy.json`, evaluate whether it should also be removed or kept as reference. Document this decision.
## Constraints
- No force push.
- No direct production publish.
- No secret output.
- Preserve audit trail.
- Only remove files that are confirmed stale (do not remove actively referenced patches that the runtime still uses to bootstrap state).
## Return Format
Status:
Workspace:
Branch:
Files Removed:
Files Kept:
Commit:
Issues:
@@ -1,66 +0,0 @@
# Codex Execution Instruction
Instruction ID: fix-reports-permission-v1
Version: v1
Target Agent: codex
## Objective
Fix the GitOps workspace reports directory permission issue so the Agent OS acceptance report can be committed.
## Context
Current blocked path:
/data/aios-gitops/workspace/reports/
Error:
Permission denied
## Execution Steps
1. Enter workspace:
cd /data/aios-gitops/workspace
2. Check current user and permissions:
whoami
id
ls -ld reports
3. Create directory if missing:
mkdir -p reports
4. Fix ownership and write permission for this directory only:
chown -R <gitops-user>:<gitops-group> reports
chmod -R u+rwX reports
5. Verify write access:
touch reports/.write-test
rm reports/.write-test
6. Commit pending acceptance report changes after verification.
## Constraints
- GitOps only.
- No force push.
- No chmod 777.
- Do not modify unrelated directories.
- Do not write secrets.
- Production changes require approval.
## Validation
Return:
Status:
Branch:
Commit:
Rev:
Sync Status:
Permission Result:
Issues:
@@ -1,31 +0,0 @@
# Codex System Recovery Instruction
Version: v1
Target Agent: codex
## Objective
Recover AIOS system issues requiring deep diagnosis or administrator assistance.
## Execution
1. Inspect runtime status.
2. Inspect GitOps state.
3. Identify root cause.
4. Propose safe patch.
5. Apply only approved GitOps changes.
## Escalation
Use when server agents cannot resolve issues.
## Constraints
- GitOps only.
- No secrets.
- No force push.
- Production changes require approval.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
@@ -1,48 +0,0 @@
# Codex Main Merge Verification Instruction
Instruction ID: v1r1c5-main-merge-verify-v1
Version: v1
Target Agent: codex
## Objective
Verify whether V1R1C5 patch branch has been merged into main.
## Preconditions
Ensure GitOps authentication is healthy.
If Gitea account password state blocks access, resolve account activation through approved admin procedure before continuing.
## Verification Steps
1. Check repository status.
2. Fetch remote branches.
3. Compare:
source:
patch/self-evolution-agent-orchestration-policy
target:
main
4. Confirm merge commit.
5. Confirm main HEAD.
6. Confirm no divergence.
## Constraints
- No force push.
- No direct production publish.
- No secret output.
- Preserve audit trail.
## Return Format
Status:
Source Branch:
Target Branch:
Merge Commit:
Rev:
Sync Status:
Conflicts:
Main HEAD:
Issues:
@@ -1,59 +0,0 @@
# Codex V1R2C2 Branch Sync Validation Instruction
Instruction ID: v1r2c2-branch-sync-validation-v1
Version: v1
Target Agent: codex
## Objective
Validate synchronization state of all git branches between local, origin (Gitea), and gitops remote. Identify divergent branches that need reconciliation.
## Preconditions
- GitOps authentication healthy.
- Run from workspace root (`/data/aios-gitops/workspace`).
## Validation Scope
1. **main branch sync**: local main == origin/main == gitops/main
2. **patch branch sync**: for each local branch, compare HEAD against origin and gitops remotes
3. **Current branch**: verify the active working branch is on the correct base
4. **Untracked files**: check for files that should be committed vs ignored
## Steps
1. Fetch all remotes.
2. Capture local branch HEAD hashes.
3. Capture remote origin branch HEAD hashes.
4. Capture remote gitops branch HEAD hashes.
5. For each branch, report:
- Fully synced: local == origin == gitops
- Divergent: local != origin or local != gitops
- Missing remote: branch exists locally but not in remote
6. Check if current branch is based on main or is diverging.
7. List untracked files that may need governance.
## Return Format
```
Status: <synced|divergence_found>
Workspace: /data/aios-gitops/workspace
Main Sync: <yes|no>
Main HEAD: <sha>
Origin/main HEAD: <sha>
Gitops/main HEAD: <sha>
Divergent Branches:
- <branch name>: <local sha> vs <remote sha> [origin|gitops] (<reason>)
Missing Remote Branches:
- <branch name>: <local sha>
Untracked File Count: <count>
Issues:
- <issue description>
```
## Constraints
- No force push.
- No destructive actions.
- No secret output.
- Report only; do not auto-resolve discrepancies.
@@ -1 +0,0 @@
# Placeholder for versioned custom GPT review instructions.
@@ -1,31 +0,0 @@
# Custom GPT Solution Review Instruction
Version: v1
Target Agent: custom_gpt_review
## Objective
Review difficult architecture decisions and implementation proposals from server agents.
## Role
Act as reviewer and advisor for OpenClaw, Hermes and other AIOS agents.
## Execution
1. Receive complete problem context.
2. Review baseline, branch and proposed changes.
3. Analyze alternative solutions.
4. Recommend implementation approach.
5. Identify risks and validation requirements.
## Constraints
- Advisory role only.
- No direct production changes.
- No secrets.
- GitOps workflow required.
- Release requires approval.
## Return
Status:
Recommendation:
Risk:
Validation:
Next Step:
-1
View File
@@ -1 +0,0 @@
# Placeholder for versioned Hermes instructions.
@@ -1,32 +0,0 @@
# Hermes Architecture Review Instruction
Version: v1
Target Agent: hermes
## Objective
Review AIOS architecture, policies and implementation plans.
## Execution
1. Review baseline and branch diff.
2. Analyze architecture impact.
3. Review OpenClaw execution proposals.
4. Validate acceptance criteria.
5. Provide improvement recommendations.
## Escalation
For unresolved architecture choices, prepare complete context for external review.
## Constraints
- GitOps only.
- No secrets.
- No direct production changes.
- Approval required for release.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
Recommendations:
-1
View File
@@ -1 +0,0 @@
# Placeholder for versioned OpenClaw instructions.
-24
View File
@@ -1,24 +0,0 @@
# OpenClaw Agent Instructions Index
## Available Instructions
### v1r1c5-hermes-coordination-v1
Instruction file:
agent-instructions/openclaw/v1r1c5-hermes-coordination-v1.md
Purpose:
Coordinate Hermes architecture review through OpenClaw controlled communication channel.
Invocation:
Execute openclaw/v1r1c5-hermes-coordination-v1 instruction
Requirements:
- Read the referenced markdown file from the GitOps workspace.
- Do not treat the identifier as a local shell command.
- Do not search external skill marketplaces unless the file is missing.
- Follow GitOps workflow rules defined in the instruction.
@@ -1,32 +0,0 @@
# OpenClaw Runtime Maintenance Instruction
Version: v1
Target Agent: openclaw
## Objective
Maintain server-side AIOS runtime and perform safe optimization tasks.
## Execution
1. Check runtime health.
2. Review logs and GitOps status.
3. Identify optimization opportunities.
4. Create patch proposals.
5. Execute approved changes.
## Escalation
When solution selection or implementation is uncertain, prepare full context and escalate for review.
## Constraints
- GitOps only.
- Patch-first workflow.
- No secrets.
- No force push.
- Production release requires approval.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
@@ -1,51 +0,0 @@
# OpenClaw Execution Instruction: Hermes Coordination
Instruction ID: v1r1c5-hermes-coordination-v1
Version: v1
Target Agent: openclaw
## Objective
Coordinate Hermes architecture review through OpenClaw-controlled communication channel.
## Architecture Rule
Hermes is not directly connected to external control channels.
All Hermes requests and responses must be routed through OpenClaw personal WeChat channel.
## Workflow
1. Receive AIOS task or review request.
2. Send required context to Hermes through OpenClaw WeChat.
3. Collect Hermes architecture analysis.
4. Record review result in GitOps reports.
5. Continue execution or escalate when required.
## Context Package
Include:
- Baseline version
- Branch
- Diff summary
- Problem description
- Proposed solution
- Validation requirements
## Constraints
- GitOps only.
- No secrets.
- No direct production changes.
- Maintain audit records.
- Use escalation flow for unresolved decisions.
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Hermes Result:
Issues:
@@ -1,119 +0,0 @@
# OpenClaw Execution Instruction
Instruction ID: v1r1c5-hermes-review-improvement-v1
Version: v1
Target Agent: openclaw
## Objective
Implement V1R1C5 improvements based on Hermes Conditional Approval review.
## Context
Baseline:
V1R1C4
Branch:
patch/self-evolution-agent-orchestration-policy
Proposal:
V1R1C5-agent-os-evolution-proposal
## Required Changes
1. Add Agent Resource Governance:
Create:
config/self-evolution/agent-resource-manager.json
Support:
- resource limits
- parallel task limits
- agent priority
- runtime isolation
- overload handling
2. Enhance Consensus Engine:
Add dynamic weighting:
Final Weight = Base Weight * Quality Score * Historical Reliability * Domain Expertise
Base weights:
- custom_gpt: 2
- windows_codex: 2
- server_agent: 1
Add risk gates:
- Production Release
- V-Level Change
- Security Policy Change
Require:
Consensus Approval + Owner Approval
3. Enhance Message Bus:
Update runtime/agent-os/message_bus.py
Add:
- task_id
- correlation_id
- retry_state
- delivery_status
Support:
- message tracking
- retry
- recovery
- audit
4. Add Agent Health Score:
Create:
runtime/agent-os/agent_health_score.py
Calculate from:
- quality score
- acceptance rate
- historical reliability
Use for:
- scheduler selection
- consensus weighting
## Validation
Run:
python3 -m compileall runtime/agent-os
Run:
runtime/agent-os/integration_test.py
Validate:
- scheduler
- consensus
- message bus
- escalation
- recovery
## Constraints
- GitOps only
- Patch-first
- No secrets
- No force push
- No direct main write
- Production changes require approval
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Changed Files:
Tests:
Hermes Review Items Completed:
Remaining Issues:
@@ -1,61 +0,0 @@
# OpenClaw Hermes Routing Correction Instruction
Instruction ID: v1r1c5-hermes-routing-correction-v1
Version: v1
Target Agent: openclaw
## Objective
Correct Hermes coordination routing. OpenClaw must actively package AIOS review requests and route them through the configured Hermes communication path instead of asking the user to provide missing context when the request context already exists in GitOps.
## Current Architecture
OpenClaw is the coordination entry point.
Hermes is reached indirectly through OpenClaw controlled communication.
## Hermes Routing Targets
Authorized Hermes communication Chat IDs:
- o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat
- o9cq80x9RoA6ljoFZDxg71mS0gDA@im.wechat
## Execution Flow
1. Read current AIOS GitOps context.
2. Collect:
- Baseline version
- Branch
- Diff summary
- Problem description
- Proposed solution
- Validation requirements
3. Send complete review package to Hermes through configured OpenClaw communication channel.
4. Receive Hermes analysis.
5. Record review result in GitOps reports.
## Current Review Context
Baseline:
V1R1C4
Branch:
patch/self-evolution-agent-orchestration-policy
Proposal:
V1R1C5-agent-os-evolution-proposal
## Constraints
- Do not ask user for context that already exists in GitOps.
- Do not treat instruction ID as external skill package.
- GitOps audit required.
- No secrets.
- No direct production changes.
## Return
Status:
Hermes Routing:
Review Context:
Result:
Issues:
@@ -1,52 +0,0 @@
# OpenClaw Owner Approval Gate Instruction
Instruction ID: v1r1c5-owner-approval-gate-v1
Version: v1
Target Agent: openclaw
## Objective
Manage owner approval workflow for high-risk AIOS operations.
## Approval Required For
- V-level version changes.
- Production release.
- Tag creation.
- Runtime changes with production impact.
- Administrator authorization actions.
- Unresolved multi-agent decisions.
## Workflow
1. Collect approval context.
2. Include baseline version, branch, diff summary and risk assessment.
3. Route approval request through owner communication channel registry.
4. Wait for approval decision.
5. Record decision in GitOps audit.
## Approval Context
Must include:
- Request reason.
- Affected components.
- Validation results.
- Rollback considerations.
- Risk level.
## Constraints
- No direct production action without approval.
- No secrets in messages or reports.
- Use configured owner communication channels only.
- Maintain audit records.
## Return Format
Status:
Approval Status:
Channel Used:
Decision:
Audit Record:
Issues:
@@ -1,43 +0,0 @@
# OpenClaw Owner Notification Routing Instruction
Instruction ID: v1r1c5-owner-notification-routing-v1
Version: v1
Target Agent: openclaw
## Objective
Implement owner notification routing through configured owner communication channels.
## Routing Rules
1. OpenClaw must first inspect the owner-bound communication channels already configured in the system.
2. Select the primary owner communication channel according to system priority configuration.
3. If the primary channel is unavailable, unhealthy, or delivery fails, automatically fallback to the configured backup communication channel.
4. Record routing decision and delivery status in GitOps audit records.
## Notification Scenarios
Notify owner for:
- High risk architecture decisions.
- Agent consensus deadlock.
- Production-impacting approval requests.
- Required administrator authorization.
- Release approval gates.
## Constraints
- Do not hardcode personal communication identifiers in instruction files.
- Use configured channel registry only.
- Do not expose private contact information in logs.
- Maintain audit trail.
- GitOps workflow required.
## Return Format
Status:
Primary Channel Status:
Fallback Channel Status:
Delivery Result:
Audit Record:
Issues:
@@ -1 +0,0 @@
# Placeholder for versioned Windows Codex App instructions.
@@ -1,47 +0,0 @@
# Windows Codex App Execution Instruction
Instruction ID: v1r1c5-recovery-and-admin-assistance-v1
Version: v1
Target Agent: windows_codex_app
## Objective
Participate in V1R1C5 Agent OS evolution as recovery, administrator assistance and exceptional execution support agent.
## Responsibilities
1. Assist when server OpenClaw agents have runtime failures.
2. Assist when Hermes review cannot be completed through OpenClaw.
3. Handle cases requiring administrator authorization workflow.
4. Participate in multi-agent voting when decision conflicts occur.
5. Validate recovery results after server agent restoration.
## Execution Rules
- Work from online GitOps versions.
- Use patch-first workflow.
- Do not bypass approval gates.
- Do not write secrets.
- Do not force push.
- Do not directly publish production versions.
## Recovery Flow
Server agent failure:
OpenClaw/Hermes issue
-> Windows Codex diagnosis
-> propose fix
-> GitOps patch
-> validation
-> approval
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Recovery Result:
Issues:
-2
View File
@@ -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
@@ -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
-2
View File
@@ -1,2 +0,0 @@
# AIOS Agent Policies
Policies for AIOS Git Agent execution.
-67
View File
@@ -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"}
-170
View File
@@ -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()
-240
View File
@@ -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()
-328
View File
@@ -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()
-2
View File
@@ -1,2 +0,0 @@
# AIOS Agent State
Agent state and lock files for AIOS Git Agent execution.
-15
View File
@@ -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,27 +0,0 @@
{
"approval_id": "owner-approve-V1R1C5-migration-20260707",
"created_at": "2026-07-07T11:46:17+08:00",
"approved_by_role": "owner",
"approved_via": "openclaw-weixin",
"approved_at": "2026-07-07T11:46:17+08:00",
"target_branch": "patch/self-evolution-agent-orchestration-policy",
"target_base": "main",
"target_version": "V1R1C5",
"rollback_target": "V1R1C4",
"allowed_actions": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline"
],
"risk_acceptance": "Owner approved V1R1C5 Agent OS evolution migration to main. Integration tests passed, Hermes review approved, conditional approval gate implemented. Rollback target V1R1C4.",
"expires_at": "2026-07-14T23:59:59Z",
"constraints": {
"direct_main_push": false,
"force_push": false,
"secrets_in_git": false,
"destructive_operations": false,
"validation_required": true,
"owner_notification_required": true,
"web_ui_audit_required": true
}
}
@@ -1,25 +0,0 @@
{
"approval_id": "owner-preapprove-V1R1C5-main-entry-20260707",
"created_at": "2026-07-07T00:00:00Z",
"approved_by_role": "owner",
"target_branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"target_base": "main",
"target_version": "V1R1C5",
"rollback_target": "V1R1C4",
"allowed_actions": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline"
],
"risk_acceptance": "Owner approved promotion of autonomous GitOps platform control plane to main, subject to validation, OpenClaw supervision, no secret leakage, no direct force push, and rollback target V1R1C4.",
"expires_at": "2026-07-14T23:59:59Z",
"constraints": {
"direct_main_push": false,
"force_push": false,
"secrets_in_git": false,
"destructive_operations": false,
"validation_required": true,
"owner_notification_required": true,
"web_ui_audit_required": true
}
}
+20 -10
View File
@@ -1,12 +1,22 @@
{
"active_version": "V1R2C3",
"active_tag": "V1R2C3",
"previous_version": "V1R2C2",
"rollback_version": "V1R2C2",
"mode": "gitops-online",
"update_mode": "git_patch_only",
"zip_update_deprecated": true,
"chatgpt_access": true,
"codex_access": true,
"updated_at": "2026-07-08T00:00:00Z"
"version": "V1R1C0",
"architecture_version": "V1",
"release_version": "R1",
"change_version": "C0",
"components": [
"webops",
"owner-admin-user",
"npm",
"casdoor",
"openclaw",
"hermes",
"one-api",
"gitops-control",
"baseline-registry",
"custom-llm-registry",
"runtime-selector"
],
"entry": "https://cnnetmkt.top",
"proxy": "npm",
"runtime_sync": "openclaw"
}
-41
View File
@@ -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)
-16
View File
@@ -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/"
}
-16
View File
@@ -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."
}
-7
View File
@@ -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"
}
+108 -1
View File
@@ -1 +1,108 @@
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
{
"active_version": "V1R1C4",
"active_tag": "V1R1C4",
"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
},
"version": "V1R1C4",
"baseline_tag": "V1R1C0-baseline-20260706",
"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-06T11:16:23Z",
"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": "V1R1C3",
"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"
}
},
"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"
}
}
@@ -1 +0,0 @@
{"owner_approved": true, "version": "V1R4C0-test"}
-27
View File
@@ -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/`
-16
View File
@@ -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
-35
View File
@@ -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
-26
View File
@@ -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
-92
View File
@@ -1,92 +0,0 @@
search_gateway:
enabled: true
scope: general_purpose_network_search
principle: shared_base_capability_with_group_user_session_customization
customization:
enabled: true
precedence:
- base
- group_profile
- user_profile
- session_profile
customizable:
- provider_order
- trusted_domains
- query_expansion_hints
- domain_profiles
- preferred_language
- preferred_region
non_overridable:
- audit_required
- fallback_required
- authorization_required
- blocked_behaviors
- privacy_boundaries
domain_profiles:
supported_defaults:
- general
- education_exam_data
- policy_regulation
- software_library
- product_research
- company_research
extensible_by_group: true
extensible_by_user: true
extension_requires_audit: true
providers:
routing:
- cache
- official_site
- general_web
- vertical_source
- browser_fetch
- user_authorized_file
- internal_archive
failure_policy:
single_provider_failure_is_not_terminal: true
require_fallback_attempts: true
record_provider_failures: true
allowed_sources:
- public_web_pages
- official_announcements
- public_documents
- public_exam_papers
- user_authorized_files
- user_authorized_links
- internal_archives
blocked_behaviors:
- bypass_login
- bypass_captcha
- scrape_private_student_grades
- scrape_student_private_data
- bulk_scrape_protected_platforms
- collect_data_without_authorization
evidence:
require_source_record: true
require_confidence: true
require_attempt_log: true
require_clarification_when_insufficient: true
audit:
enabled: true
record:
- query
- requester
- resolved_profile
- expanded_queries
- providers_attempted
- provider_failures
- evidence_selected
- blocked_reasons
- final_confidence
user_experience:
do_not_answer_provider_unavailable_as_final_failure: true
explain_public_vs_private_data_boundary: true
ask_for_subject_platform_file_or_link_when_needed: true
@@ -1 +0,0 @@
{"schema":"acceptance-pipeline-v1","status":"proposal","pipeline":{"stages":[{"name":"code_review","agent_role":"review_agent"},{"name":"automated_test","agent_role":"test_agent"},{"name":"security_check","agent_role":"security_agent"},{"name":"performance_check","agent_role":"performance_agent"},{"name":"regression_check","agent_role":"regression_agent"},{"name":"release_readiness","agent_role":"release_readiness_agent"}],"parallel_validation":true,"failure_blocks_release":true},"outputs":{"quality_score":true,"risk_report":true,"approval_request":true},"safety":{"production_release_requires_owner":true,"credentials_scan":true,"audit_log":true}}
@@ -1 +0,0 @@
{"schema":"agent-governance-v1","status":"proposal","governance":{"identity":true,"reputation":true,"capability_discovery":true,"memory_service":true,"shared_knowledge":true,"skill_packages":true,"peer_review":true,"acceptance_pipeline":["optimization","implementation","acceptance","security","release_readiness"],"dynamic_voting":{"enabled":true,"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"quality_score_modifier":true},"audit":{"full_trace":true,"decision_log":true}},"release":{"patch_only":true,"owner_approval_required":true}}
@@ -1,63 +0,0 @@
{
"policy_id": "self-evolution-agent-orchestration-policy",
"version": 1,
"status": "proposal",
"scope": "self-evolution-execution-orchestration",
"execution_subjects": {
"primary": [
"server.openclaw_agent",
"server.hermes_agent"
],
"collaborators": [
"windows.codex_app",
"custom_gpt.codex_chartgpt_bridge"
]
},
"parallelism": {
"server_agents_parallel_allowed": true,
"multiple_server_agents_allowed": true,
"server_agents_preferred": true
},
"fallback_and_assistance": {
"difficult_case_external_interaction_allowed": true,
"assistance_endpoint": "https://chatgpt.com/g/g-6a4ba70a063c8191a89bc2d86a4c7fa0-codex-chartgpt-bridge",
"allowed_modes": [
"ask_for_help",
"interactive_coordination",
"proposal_review"
]
},
"dispute_resolution": {
"multi_agent_voting_allowed": true,
"weights": {
"custom_gpt.codex_chartgpt_bridge": 2,
"windows.codex_app": 2,
"server.openclaw_agent": 1,
"server.hermes_agent": 1,
"server.additional_agent_default": 1
},
"tie_breaking": "require_owner_or_admin_review"
},
"safety": {
"credentials_in_git_allowed": false,
"force_push_allowed": false,
"destructive_bulk_delete_allowed": false,
"v_level_change_requires_owner_approval": true,
"production_switch_requires_owner_approval": true,
"tag_creation_requires_owner_approval": true,
"v_level_tag_or_production_switch_requires_owner_approval": true,
"direct_main_push_allowed": false,
"direct_main_merge_allowed": false
},
"gitops": {
"audit_required": true,
"writes_require_commit": true,
"default_target_branch": "patch/self-evolution-agent-orchestration-policy",
"direct_main_push_default": false,
"patch_branch_only": true,
"main_merge_requires_owner_admin_approval": true
},
"created_by": "codex-chartgpt-bridge",
"created_at": "2026-07-07T00:00:00Z",
"branch": "patch/self-evolution-agent-orchestration-policy"
}
@@ -1 +0,0 @@
{"schema":"agent-os-v1","status":"proposal","agents":{"openclaw":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true},"hermes":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true}},"coordination":{"voting":true,"weighted_voting":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"owner_review_on_tie":true},"safety":{"gitops_only":true,"patch_first":true,"no_credentials":true,"no_direct_main":true}}
@@ -1 +0,0 @@
{"schema":"agent-platform-v1","status":"proposal","modules":[{"name":"memory-manager","enabled":true},{"name":"skill-registry","enabled":true},{"name":"agent-registry","enabled":true},{"name":"task-scheduler","enabled":true},{"name":"consensus-engine","enabled":true},{"name":"continuous-self-evolution","enabled":true}],"release_policy":{"patch_first":true,"owner_approval":true}}
@@ -1 +0,0 @@
{"schema":"agent-protocol-v1","status":"proposal","agent_identity":{"id":"string","name":"string","family":"openclaw|hermes|custom","version":"string","capabilities":[],"skills":[],"health":"unknown|healthy|degraded|offline"},"task_protocol":{"task_id":"string","requester":"agent_id","executor":"agent_id","objective":"string","priority":"low|normal|high|critical","status":"queued|running|completed|failed|review","result":"object"},"memory_protocol":{"memory_id":"string","type":"experience|knowledge|decision|reflection","scope":"agent|shared","audit":true},"voting_protocol":{"enabled":true,"proposal":"string","votes":[],"weighted":true,"tie":"owner_review"},"security":{"credentials":false,"direct_main_write":false,"production_action_approval":true}}
@@ -1,43 +0,0 @@
{
"$schema": "schema/self-evolution.schema.json",
"title": "Agent Resource Manager Configuration",
"description": "Defines resource limits, parallel task limits, priority, runtime isolation, and overload handling for agent orchestration.",
"version": "1.0.0",
"config": {
"default_resource_limits": {
"max_concurrent_tasks": 5,
"max_memory_mb": 1024,
"max_cpu_cores": 2,
"timeout_seconds": 300
},
"agent_priority_levels": {
"critical": 100,
"high": 80,
"medium": 50,
"low": 20,
"background": 0
},
"runtime_isolation": {
"enabled": true,
"namespace_per_agent": true,
"ephemeral_storage": "1Gi",
"network_policy": "restrictive"
},
"overload_handling": {
"max_queue_depth": 100,
"backpressure_threshold": 0.9,
"cooldown_seconds": 30,
"scaling_strategy": "queue"
},
"agent_pools": {
"server_agents": {
"max_instances": 3,
"priority": "high"
},
"external_agents": {
"max_instances": 5,
"priority": "medium"
}
}
}
}
@@ -1 +0,0 @@
{"schema":"agent-runtime-components-v1","status":"proposal","components":{"agent-runtime-manager":{"enabled":true,"responsibilities":["lifecycle_management","health_check","recovery","resource_tracking"]},"agent-registry":{"enabled":true,"responsibilities":["identity","capability_discovery","status_tracking","version_tracking"]},"message-bus":{"enabled":true,"responsibilities":["task_routing","event_delivery","agent_coordination"]},"memory-service":{"enabled":true,"responsibilities":["long_term_memory","working_memory","knowledge_retrieval","audit"]},"skill-loader":{"enabled":true,"responsibilities":["skill_installation","skill_validation","skill_versioning"]},"agent-evaluator":{"enabled":true,"responsibilities":["quality_score","acceptance_check","performance_feedback"]}},"security":{"credential_storage":false,"gitops_audit":true,"patch_only":true}}
@@ -1 +0,0 @@
{"schema":"consensus-engine-v1","status":"proposal","consensus":{"enabled":true,"modes":["weighted_vote","review_board","owner_escalation"],"participants":["openclaw_agent","hermes_agent","windows_codex","custom_gpt"],"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"dynamic_adjustment":{"enabled":true,"factors":["quality_score","acceptance_rate","historical_reliability"]},"conflict_detection":true,"tie_breaking":"owner_admin_review"},"decision_log":{"required":true,"audit":true},"safety":{"production_decision_requires_approval":true}}
@@ -1 +0,0 @@
{"schema":"continuous-evolution-engine-v1","status":"proposal","pipeline":{"monitor":true,"detect":true,"plan":true,"schedule":true,"execute":true,"learn":true},"workflow":{"detect_sources":["runtime_metrics","logs","acceptance_results","agent_feedback","user_requests"],"proposal_generation":true,"patch_generation":true,"validation_required":true,"consensus_required":true,"approval_gate":true,"release_feedback":true},"agents":{"optimizer":"system_optimization_agent","planner":"planning_agent","executor":"patch_execution_agent","validator":"acceptance_validation_agent","reviewer":"consensus_engine"},"learning":{"store_experience":true,"update_quality_score":true,"reflection_required":true},"safety":{"gitops_only":true,"patch_first":true,"credentials":false,"production_requires_approval":true}}

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