Compare commits

..
Author SHA1 Message Date
openclaw 189b655d04 Add autonomous operation command center deployment command 2026-07-09 15:57:18 +08:00
openclaw 5f2b5f16ba Add command stall detector tests 2026-07-09 15:57:13 +08:00
openclaw e2d9de3a1f Add unified operations command center model 2026-07-09 15:57:11 +08:00
openclaw 83cbd64fb6 Add Command Watcher stall detector 2026-07-09 15:57:05 +08:00
openclaw 7852844338 Add autonomous health center deployment command 2026-07-09 15:53:00 +08:00
openclaw 46b9d3188d Add autonomous health adapter tests 2026-07-09 15:52:46 +08:00
openclaw c69916442c Add system health API 2026-07-09 15:52:39 +08:00
openclaw 781239bea7 Add autonomous controller health adapter 2026-07-09 15:52:27 +08:00
openclaw 193f6546b1 Fix Git permission watchdog service entrypoint 2026-07-09 15:52:15 +08:00
openclaw e063c6cb3c Add Git permission watchdog CLI entrypoint 2026-07-09 15:52:07 +08:00
openclaw 9f9f7be796 Add blocker health center deployment command 2026-07-09 15:45:12 +08:00
openclaw 77d0d7176d Add system health center tests 2026-07-09 15:45:01 +08:00
openclaw 0029980d74 Add unified system health center 2026-07-09 15:44:58 +08:00
openclaw 0ed265e3cb Add blocker to Task Center adapter 2026-07-09 15:44:50 +08:00
openclaw c7a1cf5b2a Add OpenClaw command for system blocker self optimization 2026-07-09 15:37:32 +08:00
openclaw d6a6491d2b Add system blocker optimization tests 2026-07-09 15:37:27 +08:00
openclaw e364432a1f Add recurring blocker optimization policy 2026-07-09 15:37:22 +08:00
openclaw 343b6f1ba6 Add system blocker memory and recurrence detection 2026-07-09 15:37:15 +08:00
openclaw e1d4bfc77b Add Git permission watchdog systemd timer 2026-07-09 15:34:15 +08:00
openclaw f7cf7249f1 Add Git permission watchdog systemd service 2026-07-09 15:34:12 +08:00
openclaw 13846c222f Add Git permission watchdog tests 2026-07-09 15:34:09 +08:00
openclaw 749afecf08 Add Git permission watchdog policy 2026-07-09 15:34:05 +08:00
openclaw b424de9d98 Add Git permission watchdog self-healing module 2026-07-09 15:34:00 +08:00
openclaw b761536f16 Dispatch Git permission self-healing mechanism 2026-07-09 15:31:43 +08:00
175 changed files with 1119 additions and 8557 deletions
@@ -1,38 +0,0 @@
status: PASS
integration_path: tests/integration/test_runtime_ingress_flow.py
tests: pytest tests/v2r1 = 29 passed in 0.98s; pytest tests/integration = 3 passed in 0.18s
runtime_flow: PASS - signed public ingress -> IngressAuthenticator -> RuntimeIdentityResolver -> RuntimeWiring.ingest -> ChannelAdapter -> MessageLedger.append -> SessionPool
commit: a6878d30b8690e55f1bf9819da5aac189dabf89e
gate1: BLOCKED
risk: high
# Result
## Changed Files
- `tests/integration/test_runtime_ingress_flow.py`
- `.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md`
## RED Evidence
The baseline integration entrypoint from `f11513e9d7326caa827e59f4f10f07681633dfc0` was inadequate: its successful flow replaced `MessageLedger.remember()` and its alleged duplicate conflict changed the message ID. The quality checks failed before this update because neither test proved the required runtime contract.
## Verification
- The success path sends a real signed public ingress request and uses a `sys.setprofile` observer against the real `MessageLedger.remember` code object. It preserves the production object and all behavior, then asserts zero legacy calls.
- The duplicate path sends two authenticated requests under the same principal, tenant, cell, channel, account, peer, direction, session, and `message_id`; only `event_id` and body differ. The second response must be `CONFLICT`, with exactly one ledger record and one session lease remaining.
## Commands
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/v2r1 -q
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/integration -q
```
## Gate 1
`BLOCKED`. This task makes the runtime integration entrypoint meaningful, but does not close the independent Gate 1 matrix, authenticated production OpenClaw registration, secret scanning, second clean-room review, or staging rollback rehearsal.
## Recommendation
Keep this isolated evidence commit for independent review. Do not merge, tag, or deploy until all Gate 1 requirements have independent evidence.
@@ -1,95 +0,0 @@
status: PASS
commit: 7b82ae11f685b77628308ba46b9cfdf5bcdf78d3
tests:
v2r1: "29 passed, exit code 0"
integration: "3 passed, exit code 0"
cleanup_scan:
status: PASS
findings: "No remaining conflict markers, unexplained worktree changes, unapproved temporary/cache/build artifacts, or scanner-detected secrets. Runtime SQLite was removed and its sidecars are ignored with exact root-anchored rules; tracked dist/ZIP artifacts are documented exceptions."
risk: MEDIUM
recommendation: "Push the controlled work branch, then request independent OpenClaw Gate Review; do not merge or release from this result alone."
# V2R1 Gate 1 Codex Cleanup Result
## Scope and Git Baseline
- Task: `V2R1-G1-CODEX-CLEANUP-20260713`
- Branch: `work/v2r1-g1-cleanup-remediation-20260713`
- Cleanup implementation commit: `7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`
- Integration commit: `a6878d30b8690e55f1bf9819da5aac189dabf89e`
- Locked integration result baseline: `d6ba7e8fa859407ec55acf49b5468bba5eac96da`
- `git merge-base --is-ancestor a6878d30b8690e55f1bf9819da5aac189dabf89e HEAD`: exit code `0`
- `git merge-base --is-ancestor d6ba7e8fa859407ec55acf49b5468bba5eac96da HEAD`: exit code `0`
- Tree before this result-only commit: `94146e05f700921b12ef023eb4d8be771bae4047`
The locked integration artifacts were not modified by cleanup:
- `tests/integration/test_runtime_ingress_flow.py`: blob `6b6732a4c73bbb7fb5273a05b897adce9da39a9a`
- `.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md`: blob `e2d6ce3b037339385831f51cb0db42c86b2ae827`
## Cleanup Changes Verified
Commit `7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`:
- removed the tracked runtime database `data/webops.sqlite3`;
- added exact root-anchored ignore rules for `/data/webops.sqlite3`, `/data/webops.sqlite3-wal`, `/data/webops.sqlite3-shm`, and `/data/webops.sqlite3-journal`;
- resolved tracked merge-conflict residue in `agents/registry.yaml` and `docs/architecture/AIOS_AGENT_AUDIT_PLANE.md`;
- left the locked integration test and prior Codex result blobs unchanged.
`git grep -n -E '^(<<<<<<<|=======|>>>>>>>)' -- . ':!source-baseline/*.zip'` returned no matches (exit code `1`, the expected no-match result).
The repository scan found no unapproved `__pycache__`, `*.pyc`, `*.pyo`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `*.tmp`, `*.swp`, backup-suffix, or `.DS_Store` artifacts after verification cleanup. The following tracked artifacts are expected baseline exceptions, not unexplained generated files:
- `openclaw/plugins/aios-tenant-boundary/dist/` (2 tracked plugin distribution files);
- `source-baseline/current_actual_deployment_source.zip`;
- `source-baseline/previous_current_actual_deployment_source_from_webops_delta.zip`.
Before creating this result file, `git status --porcelain=v1 --untracked-files=all` was empty with exit code `0`.
## Secret Scan
Scanner: Gitleaks `8.30.1`, official Linux x64 release asset.
- Asset: `/tmp/gitleaks_8.30.1_linux_x64.tar.gz.codex`
- SHA-256: `551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb`
- `/tmp/gitleaks-codex-8.30.1/gitleaks git --no-banner --no-color --redact --report-format json --report-path /tmp/v2r1-cleanup-audit-git.json --log-opts=7b82ae11f685b77628308ba46b9cfdf5bcdf78d3 /data/aios-gitops/worktrees/v2r1-g1-cleanup-remediation-20260713`: exit code `0`; Gitleaks reported `312 commits scanned`, approximately `1,384,427 bytes (1.38 MB)` scanned, and `no leaks found` (`0` findings in the JSON report).
- `git rev-list --count 7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`: exit code `0`, `327` reachable commits. This Git reachability count is recorded separately and is not the Gitleaks scanned-commit count.
- `/tmp/gitleaks-codex-8.30.1/gitleaks dir --no-banner --no-color --redact --report-format json --report-path /tmp/v2r1-cleanup-audit-tree.json /data/aios-gitops/worktrees/v2r1-g1-cleanup-remediation-20260713`: exit code `0`; Gitleaks reported approximately `1,214,737 bytes (1.21 MB)` scanned and `no leaks found` (`0` findings in the JSON report).
No secret content is reproduced in this report.
## Tests
Command:
```bash
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/v2r1
```
Output summary:
```text
collected 29 items
29 passed in 0.97s
exit code: 0
```
Command:
```bash
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/integration
```
Output summary:
```text
collected 3 items
3 passed in 0.18s
exit code: 0
```
Environment: Linux, Python `3.12.3`, pytest `9.1.1`, pluggy `1.6.0`.
## Disposition
The cleanup and requested test verification pass at the stated implementation commit. Risk remains `MEDIUM` until the controlled branch is pushed and independently fetched and reviewed by OpenClaw. This result does not authorize merge, tag creation, baseline update, staging, or production deployment.
-4
View File
@@ -11,7 +11,3 @@ runtime/tmp/
runtime/cache/
logs/
config/test_write_perms
/data/webops.sqlite3
/data/webops.sqlite3-wal
/data/webops.sqlite3-shm
/data/webops.sqlite3-journal
+1 -1
View File
@@ -1 +1 @@
V1R4C6
V1R4C2
-89
View File
@@ -1,89 +0,0 @@
"""Append-only audit chain for AIOS V2R1."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import Any
from .tenant_context import canonical_json, isoformat, require_ident
class AuditLog:
def __init__(self, database: str | Path) -> None:
self.database = str(database)
with sqlite3.connect(self.database) as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute(
"""
CREATE TABLE IF NOT EXISTS audit_events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id TEXT NOT NULL,
trace_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
previous_hash TEXT NOT NULL,
event_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
)
"""
)
def append(self, *, tenant_id: str, trace_id: str, event_type: str, payload: dict[str, Any]) -> str:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
trace_id = require_ident("trace_id", trace_id)
event_type = require_ident("event_type", event_type)
with sqlite3.connect(self.database) as db:
previous = db.execute(
"SELECT event_hash FROM audit_events ORDER BY seq DESC LIMIT 1"
).fetchone()
previous_hash = previous[0] if previous else "GENESIS"
body = {
"tenant_id": tenant_id,
"trace_id": trace_id,
"event_type": event_type,
"payload": payload,
"previous_hash": previous_hash,
"created_at": isoformat(),
}
digest = hashlib.sha256(
canonical_json(body)
).hexdigest()
db.execute(
"INSERT INTO audit_events VALUES(NULL,?,?,?,?,?,?,?)",
(
tenant_id,
trace_id,
event_type,
json.dumps(payload, ensure_ascii=False),
previous_hash,
digest,
body["created_at"],
),
)
return digest
def verify(self) -> bool:
previous = "GENESIS"
with sqlite3.connect(self.database) as db:
rows = db.execute(
"SELECT tenant_id,trace_id,event_type,payload,previous_hash,event_hash,created_at "
"FROM audit_events ORDER BY seq"
)
for row in rows:
body = {
"tenant_id": row[0],
"trace_id": row[1],
"event_type": row[2],
"payload": json.loads(row[3]),
"previous_hash": row[4],
"created_at": row[6],
}
if row[4] != previous:
return False
digest = hashlib.sha256(canonical_json(body)).hexdigest()
if digest != row[5]:
return False
previous = digest
return True
@@ -1,73 +0,0 @@
"""AIOS V2R1 Tenant Cell lifecycle CLI primitives."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class CellRecord:
tenant_id: str
cell_id: str
version: str
state: str
config_digest: str
created_at: str
class CellManager:
def __init__(self, registry: str | Path) -> None:
self.registry = Path(registry)
self.registry.parent.mkdir(parents=True, exist_ok=True)
if not self.registry.exists():
self.registry.write_text("{\"cells\":{}}\n", encoding="utf-8")
def _load(self) -> dict:
return json.loads(self.registry.read_text(encoding="utf-8"))
def _save(self, value: dict) -> None:
self.registry.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def create(self, tenant_id: str, cell_id: str, version: str, digest: str) -> CellRecord:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
cell_id = require_ident("cell_id", cell_id)
data = self._load()
if tenant_id in data["cells"]:
raise ValidationError("tenant already has a cell")
record = CellRecord(
tenant_id,
cell_id,
version,
"created",
digest,
datetime.now(timezone.utc).isoformat(),
)
data["cells"][tenant_id] = record.__dict__
self._save(data)
return record
def status(self, tenant_id: str) -> CellRecord:
data = self._load()
item = data["cells"].get(tenant_id)
if not item:
raise ValidationError("unknown tenant")
return CellRecord(**item)
def transition(self, tenant_id: str, state: str) -> CellRecord:
allowed = {"created", "starting", "healthy", "upgrading", "rollback", "failed"}
if state not in allowed:
raise ValidationError("invalid cell state")
data = self._load()
item = data["cells"].get(tenant_id)
if not item:
raise ValidationError("unknown tenant")
item["state"] = state
self._save(data)
return CellRecord(**item)
@@ -1,64 +0,0 @@
"""The only V2R1 ingress adapter: verified context to routed ledger record."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from threading import RLock
from typing import Callable, Mapping, Any
from .message_ledger import LedgerRecord, MessageLedger
from .router import Route, TenantRouter
from .session_pool import SessionLease, SessionPool
from .tenant_context import CanonicalMessage, TenantContext, utc_now
@dataclass(frozen=True)
class IngressResult:
record: LedgerRecord
route: Route
session: SessionLease
class ChannelAdapter:
"""Fail-closed runtime ingress composition.
The adapter intentionally accepts no tenant or Cell authority outside the
signed context and TenantRouter. A process lock keeps rollback handling
deterministic for a single runtime while SQLite protects cross-process
ledger writes.
"""
def __init__(
self,
*,
router: TenantRouter,
ledger: MessageLedger,
session_pool: SessionPool,
signing_key: bytes,
clock: Callable[[], datetime] = utc_now,
) -> None:
self._router = router
self._ledger = ledger
self._session_pool = session_pool
self._signing_key = signing_key
self._clock = clock
self._lock = RLock()
def ingest(self, *, token: str, message: Mapping[str, Any]) -> IngressResult:
with self._lock:
context = TenantContext.verify(token, self._signing_key, now=self._clock())
canonical = CanonicalMessage.from_mapping(message)
canonical.assert_context(context)
route = self._router.assert_context_route(context)
self._router.assert_message_route(canonical)
self._session_pool.assert_available(context, route)
existed = self._ledger.matches(canonical)
record = self._ledger.append(canonical)
try:
lease = self._session_pool.acquire(context, route)
except Exception:
if not existed:
self._ledger.remove(record)
raise
return IngressResult(record=record, route=route, session=lease)
-48
View File
@@ -1,48 +0,0 @@
"""aios-cell command implementation for V2R1 Cell lifecycle."""
from __future__ import annotations
import argparse
import json
import sys
from .cell_manager import CellManager
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="aios-cell")
parser.add_argument("registry")
sub = parser.add_subparsers(dest="command", required=True)
create = sub.add_parser("create")
create.add_argument("tenant_id")
create.add_argument("cell_id")
create.add_argument("version")
create.add_argument("digest")
status = sub.add_parser("status")
status.add_argument("tenant_id")
transition = sub.add_parser("transition")
transition.add_argument("tenant_id")
transition.add_argument("state")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
manager = CellManager(args.registry)
if args.command == "create":
result = manager.create(args.tenant_id, args.cell_id, args.version, args.digest)
elif args.command == "status":
result = manager.status(args.tenant_id)
elif args.command == "transition":
result = manager.transition(args.tenant_id, args.state)
else:
return 2
print(json.dumps(result.__dict__, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main())
-106
View File
@@ -1,106 +0,0 @@
"""OpenClaw Cell lifecycle adapter for AIOS V2R1."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
import json
from pathlib import Path
import subprocess
from typing import Any, Mapping
from .tenant_context import ValidationError, V2R1Error, require_ident
@dataclass(frozen=True)
class CellSpec:
tenant_id: str
image: str
runtime: str = "docker"
port: int | None = None
memory: str = "2g"
cpus: str = "2"
pids_limit: int = 512
network: str = "bridge"
env: tuple[str, ...] = field(default_factory=tuple)
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.runtime not in {"docker", "podman"}:
raise ValidationError("unsupported runtime")
if not self.image:
raise ValidationError("image required")
class OpenClawFleet:
def __init__(self, binary: str = "openclaw") -> None:
self.binary = binary
def _run(self, args: list[str]) -> dict[str, Any]:
proc = subprocess.run(
[self.binary, *args],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if proc.returncode:
raise V2R1Error(proc.stderr.strip() or "openclaw command failed")
data = json.loads(proc.stdout or "{}")
if isinstance(data, dict):
return data
return {"result": data}
def create(self, spec: CellSpec) -> dict[str, Any]:
return self._run([
"fleet", "create", spec.tenant_id,
"--image", spec.image,
"--runtime", spec.runtime,
"--json",
])
def status(self, tenant_id: str) -> dict[str, Any]:
require_ident("tenant_id", tenant_id, tenant=True)
return self._run(["fleet", "status", tenant_id, "--json"])
def doctor(self, tenant_id: str | None = None) -> dict[str, Any]:
args = ["fleet", "doctor"]
if tenant_id:
require_ident("tenant_id", tenant_id, tenant=True)
args.append(tenant_id)
args.append("--json")
return self._run(args)
def backup(self, tenant_id: str, output: str | Path) -> dict[str, Any]:
return self._run(["fleet", "backup", tenant_id, "--out", str(output), "--json"])
def restore(self, tenant_id: str, archive: str | Path) -> dict[str, Any]:
return self._run(["fleet", "restore", tenant_id, "--from", str(archive), "--json"])
def upgrade(self, tenant_id: str, image: str) -> dict[str, Any]:
return self._run(["fleet", "upgrade", tenant_id, "--image", image, "--json"])
def load_inventory(path: str | Path) -> list[CellSpec]:
raw = json.loads(Path(path).read_text(encoding="utf-8"))
if raw.get("schema_version") != "aios.cells.v1":
raise ValidationError("invalid inventory schema")
result: list[CellSpec] = []
seen: set[str] = set()
for item in raw.get("cells", []):
spec = CellSpec(
tenant_id=item["tenant_id"],
image=item["image"],
runtime=item.get("runtime", "docker"),
port=item.get("port"),
env=tuple(item.get("env", [])),
)
if spec.tenant_id in seen:
raise ValidationError("duplicate tenant")
seen.add(spec.tenant_id)
result.append(spec)
if not result:
raise ValidationError("no cells")
return result
def inventory_as_dict(items: list[CellSpec]) -> dict[str, Any]:
return {"schema_version": "aios.cells.v1", "cells": [asdict(x) for x in items]}
@@ -1,143 +0,0 @@
"""Tenant-partitioned Hermes shared agent fabric primitives."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from contextlib import closing
from pathlib import Path
import sqlite3
from typing import Any
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
ValidationError,
isoformat,
require_ident,
)
@dataclass(frozen=True)
class FabricJob:
job_id: str
tenant_id: str
cell_id: str
profile_id: str
session_id: str
event_id: str
status: str = "queued"
def __post_init__(self) -> None:
for name in (
"job_id", "cell_id", "profile_id", "session_id", "event_id"
):
require_ident(name, getattr(self, name))
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.status not in {"queued", "running", "completed", "failed", "cancelled"}:
raise ValidationError("invalid job status")
class HermesFabric:
"""Shared scheduler storage with mandatory tenant partition keys."""
def __init__(self, database: str | Path) -> None:
self.database = str(database)
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute(
"""
CREATE TABLE IF NOT EXISTS fabric_jobs (
job_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
profile_id TEXT NOT NULL,
session_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
db.execute(
"CREATE INDEX IF NOT EXISTS idx_fabric_tenant_queue "
"ON fabric_jobs(tenant_id, cell_id, status)"
)
def enqueue(
self,
*,
message: CanonicalMessage,
profile_id: str,
) -> FabricJob:
profile_id = require_ident("profile_id", profile_id)
if not message.command_capable:
raise ScopeError("non-command events cannot create execution jobs")
if not message.is_authenticated_human_command:
raise ScopeError("only authenticated human commands can create jobs")
job = FabricJob(
job_id=f"job-{message.event_id}",
tenant_id=message.tenant_id,
cell_id=message.cell_id,
profile_id=profile_id,
session_id=message.session_id,
event_id=message.event_id,
)
now = isoformat()
conflict: ConflictError | None = None
try:
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute(
"INSERT INTO fabric_jobs VALUES(?,?,?,?,?,?,?,?,?)",
(
job.job_id,
job.tenant_id,
job.cell_id,
job.profile_id,
job.session_id,
job.event_id,
job.status,
now,
now,
),
)
except sqlite3.IntegrityError:
conflict = ConflictError(
"fabric event already exists or violates queue uniqueness"
)
if conflict is not None:
raise conflict
return job
def claim(self, tenant_id: str, cell_id: str, profile_id: str) -> FabricJob | None:
require_ident("tenant_id", tenant_id, tenant=True)
require_ident("cell_id", cell_id)
require_ident("profile_id", profile_id)
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute(
"SELECT * FROM fabric_jobs WHERE tenant_id=? AND cell_id=? "
"AND profile_id=? AND status='queued' ORDER BY created_at LIMIT 1",
(tenant_id, cell_id, profile_id),
).fetchone()
if not row:
db.commit()
return None
db.execute(
"UPDATE fabric_jobs SET status='running', updated_at=? WHERE job_id=?",
(isoformat(), row[0]),
)
db.commit()
return FabricJob(*row[:7])
def health(self) -> dict[str, Any]:
with closing(sqlite3.connect(self.database)) as connection, connection as db:
count = db.execute("SELECT count(*) FROM fabric_jobs").fetchone()[0]
return {
"component": "hermes_fabric",
"jobs": count,
"checked_at": datetime.now(timezone.utc).isoformat(),
}
@@ -1,47 +0,0 @@
"""Hermes shared fabric profile isolation primitives."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import os
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class HermesProfile:
tenant_id: str
cell_id: str
profile_id: str
hermes_home: Path
workspace: Path
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in ("cell_id", "profile_id"):
require_ident(name, getattr(self, name))
if not self.hermes_home.is_absolute() or not self.workspace.is_absolute():
raise ValidationError("profile paths must be absolute")
@property
def environment(self) -> dict[str, str]:
return {
"HERMES_HOME": str(self.hermes_home),
"HOME": str(self.hermes_home),
"AIOS_TENANT_ID": self.tenant_id,
"AIOS_CELL_ID": self.cell_id,
"AIOS_PROFILE_ID": self.profile_id,
}
def prepare(self) -> None:
self.hermes_home.mkdir(parents=True, exist_ok=True)
self.workspace.mkdir(parents=True, exist_ok=True)
os.chmod(self.hermes_home, 0o700)
os.chmod(self.workspace, 0o700)
def assert_path_allowed(self, path: str | Path) -> None:
target = Path(path).resolve()
for root in (self.hermes_home.resolve(), self.workspace.resolve()):
if target == root or root in target.parents:
return
raise ValidationError("path escapes profile isolation boundary")
-158
View File
@@ -1,158 +0,0 @@
"""Fail-closed identity binding for authenticated external ingress.
External payload claims are not authority. An HMAC-authenticated envelope is
the sole factory for :class:`AuthenticatedPrincipal`; the resolver then checks
every claim against the authoritative ``TenantRouter`` before the runtime signs
an internal ``TenantContext``.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import hmac
import re
from typing import Any, Mapping
from .router import Route, TenantRouter
from .tenant_context import ScopeError, ValidationError, canonical_json, require_ident
_PROVENANCE = object()
_SIGNATURE_RE = re.compile(r"^[0-9a-f]{64}$")
_SCOPE_FIELDS = (
"principal_id", "tenant_id", "cell_id", "channel",
"channel_account_id", "session_id", "message",
)
class AuthenticationError(ValidationError):
"""Authentication or identity resolution failed without disclosing scope."""
@dataclass(frozen=True, init=False)
class AuthenticatedPrincipal:
"""A principal which only the verified envelope factory can create."""
principal_id: str
def __init__(self, principal_id: str, *, _provenance: object) -> None:
if _provenance is not _PROVENANCE:
raise TypeError("AuthenticatedPrincipal is created by authenticated ingress only")
object.__setattr__(self, "principal_id", require_ident("principal_id", principal_id))
@classmethod
def _verified(cls, principal_id: str) -> "AuthenticatedPrincipal":
return cls(principal_id, _provenance=_PROVENANCE)
@dataclass(frozen=True)
class IdentityClaims:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
session_id: str
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for field in ("cell_id", "channel", "channel_account_id", "session_id"):
require_ident(field, getattr(self, field))
@dataclass(frozen=True)
class AuthenticatedIngress:
principal: AuthenticatedPrincipal
claims: IdentityClaims
message: Mapping[str, Any]
@dataclass(frozen=True)
class RuntimeIdentity:
tenant_id: str
cell_id: str
principal_id: str
channel: str
channel_account_id: str
session_id: str
class IngressAuthenticator:
"""Verify a canonical HMAC envelope before any identity lookup occurs."""
def __init__(self, hmac_key: bytes) -> None:
if not isinstance(hmac_key, bytes) or len(hmac_key) < 32:
raise ValidationError("ingress HMAC key must be at least 32 bytes")
self._hmac_key = hmac_key
@staticmethod
def _signed_payload(request: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(request, Mapping):
raise ValidationError("ingress request must be an object")
if set(request) != {*_SCOPE_FIELDS, "signature"}:
raise ValidationError("ingress request fields are invalid")
values = {field: request[field] for field in _SCOPE_FIELDS}
if not isinstance(values["message"], Mapping):
raise ValidationError("ingress message must be an object")
return values
def authenticate(self, request: Mapping[str, Any]) -> AuthenticatedIngress:
values = self._signed_payload(request)
signature = request["signature"]
if not isinstance(signature, str) or not _SIGNATURE_RE.fullmatch(signature):
raise AuthenticationError("invalid ingress signature")
expected = hmac.new(
self._hmac_key, canonical_json(values), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise AuthenticationError("invalid ingress signature")
return AuthenticatedIngress(
principal=AuthenticatedPrincipal._verified(values["principal_id"]),
claims=IdentityClaims(
tenant_id=values["tenant_id"],
cell_id=values["cell_id"],
channel=values["channel"],
channel_account_id=values["channel_account_id"],
session_id=values["session_id"],
),
message=dict(values["message"]),
)
class RuntimeIdentityResolver:
"""Resolve authenticated ingress identity through ``TenantRouter`` only."""
def __init__(self, router: TenantRouter) -> None:
self._router = router
def resolve(
self, principal: AuthenticatedPrincipal, claims: IdentityClaims
) -> RuntimeIdentity:
if not isinstance(principal, AuthenticatedPrincipal):
raise AuthenticationError("untrusted principal type")
try:
route: Route = self._router.resolve(
channel=claims.channel,
channel_account_id=claims.channel_account_id,
principal_id=principal.principal_id,
session_id=claims.session_id,
)
except (ScopeError, ValidationError) as exc:
raise AuthenticationError("identity is not registered") from None
expected = (
claims.tenant_id, claims.cell_id, claims.channel,
claims.channel_account_id, principal.principal_id, claims.session_id,
)
actual = (
route.tenant_id, route.cell_id, route.channel,
route.channel_account_id, route.principal_id, route.session_id,
)
if actual != expected:
raise AuthenticationError("identity claims do not match authoritative route")
return RuntimeIdentity(
tenant_id=route.tenant_id,
cell_id=route.cell_id,
principal_id=route.principal_id,
channel=route.channel,
channel_account_id=route.channel_account_id,
session_id=route.session_id or "",
)
@@ -1,60 +0,0 @@
"""Tenant-scoped memory namespace and access policy for AIOS V2R1."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import PurePosixPath
from typing import Literal
from .tenant_context import ScopeError, ValidationError, require_ident
MemoryKind = Literal["session", "profile", "cell", "tenant", "shared_artifact"]
@dataclass(frozen=True)
class MemoryScope:
tenant_id: str
cell_id: str
profile_id: str
session_id: str | None
kind: MemoryKind
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in ("cell_id", "profile_id"):
require_ident(name, getattr(self, name))
if self.session_id is not None:
require_ident("session_id", self.session_id)
if self.kind not in {"session", "profile", "cell", "tenant", "shared_artifact"}:
raise ValidationError("invalid memory kind")
if self.kind == "session" and self.session_id is None:
raise ValidationError("session memory requires session_id")
@property
def namespace(self) -> str:
parts = ["tenant", self.tenant_id, "cell", self.cell_id]
if self.kind in {"profile", "session"}:
parts.extend(["profile", self.profile_id])
if self.kind == "session":
parts.extend(["session", str(self.session_id)])
parts.extend(["memory", self.kind])
return str(PurePosixPath(*parts))
def assert_access(self, requested: "MemoryScope", *, write: bool = False) -> None:
if self.tenant_id != requested.tenant_id:
raise ScopeError("cross-tenant memory access denied")
if self.cell_id != requested.cell_id:
raise ScopeError("cross-cell memory access denied")
if requested.kind == "shared_artifact":
if write:
raise ScopeError("shared artifacts are immutable")
return
if self.profile_id != requested.profile_id and requested.kind in {"profile", "session"}:
raise ScopeError("cross-profile memory access denied")
if requested.kind == "session" and self.session_id != requested.session_id:
raise ScopeError("cross-session memory access denied")
def qualify_memory_key(scope: MemoryScope, key: str) -> str:
key = require_ident("memory_key", key)
return f"{scope.namespace}/{key}"
@@ -1,224 +0,0 @@
"""Tenant-scoped append-only message ledger and exact quote resolver."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from contextlib import closing
import os
from pathlib import Path
import sqlite3
from typing import Iterable
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
ValidationError,
isoformat,
normalize_message_id,
parse_time,
utc_now,
)
@dataclass(frozen=True)
class LedgerRecord:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
peer_id: str
direction: str
message_id: str
event_id: str
body: str
occurred_at: str
expires_at: str
class MessageLedger:
"""Exact-ID ledger.
The resolver never uses time windows, prefixes, similarity, or nearest-message
fallbacks. Ambiguity and absence fail closed.
"""
def __init__(self, database: str | Path, *, ttl_days: int = 7) -> None:
if not 1 <= ttl_days <= 365:
raise ValidationError("ttl_days must be between 1 and 365")
self.database = Path(database)
self.ttl_days = ttl_days
self.database.parent.mkdir(parents=True, exist_ok=True)
self._initialize()
try:
os.chmod(self.database, 0o600)
except OSError:
pass
def _connect(self) -> sqlite3.Connection:
db = sqlite3.connect(str(self.database), timeout=30)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute("PRAGMA busy_timeout=30000")
return db
def _initialize(self) -> None:
with closing(self._connect()) as connection, connection as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS message_ledger (
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
peer_id TEXT NOT NULL,
direction TEXT NOT NULL CHECK(direction IN ('inbound','outbound','internal')),
message_id TEXT NOT NULL,
event_id TEXT NOT NULL,
body TEXT NOT NULL,
occurred_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(
tenant_id, cell_id, channel, channel_account_id,
peer_id, direction, message_id
),
UNIQUE(tenant_id, cell_id, event_id)
);
CREATE INDEX IF NOT EXISTS idx_message_reply_lookup ON message_ledger(
tenant_id, cell_id, channel, channel_account_id,
peer_id, message_id, expires_at
);
"""
)
def append(self, message: CanonicalMessage) -> LedgerRecord:
"""Append one exact message through the runtime write path.
This is intentionally the owner of validation, idempotency, and the
immediate SQLite transaction. Runtime callers must not depend on the
legacy ``remember`` compatibility name.
"""
message_id = normalize_message_id(message.message_id)
now = utc_now()
expires = isoformat(now + timedelta(days=self.ttl_days))
key = (
message.tenant_id,
message.cell_id,
message.channel,
message.channel_account_id,
message.peer_id,
message.direction,
message_id,
)
storage_error: ConflictError | None = None
try:
with closing(self._connect()) as connection, connection as db:
db.execute("BEGIN IMMEDIATE")
existing = db.execute(
"SELECT event_id, body, occurred_at, expires_at FROM message_ledger "
"WHERE tenant_id=? AND cell_id=? AND channel=? AND channel_account_id=? "
"AND peer_id=? AND direction=? AND message_id=?",
key,
).fetchone()
if existing:
if existing["body"] != message.body:
db.rollback()
raise ConflictError("message ID collision with different content")
db.commit()
return LedgerRecord(
*key, existing["event_id"], existing["body"],
existing["occurred_at"], existing["expires_at"],
)
db.execute(
"INSERT INTO message_ledger VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
(*key, message.event_id, message.body, message.occurred_at, expires, isoformat(now)),
)
db.commit()
except sqlite3.Error:
storage_error = ConflictError("ledger storage unavailable")
if storage_error is not None:
# Raise outside the sqlite exception handler so callers cannot inspect
# a driver exception through __cause__ or __context__.
raise storage_error
return LedgerRecord(*key, message.event_id, message.body, message.occurred_at, expires)
def remember(self, message: CanonicalMessage) -> LedgerRecord:
"""Backward-compatible API wrapper for callers outside the runtime path."""
return self.append(message)
def matches(self, message: CanonicalMessage) -> bool:
"""Whether this exact message is already durably represented."""
message_id = normalize_message_id(message.message_id)
with closing(self._connect()) as connection, connection as db:
row = db.execute(
"SELECT event_id, body FROM message_ledger WHERE tenant_id=? AND "
"cell_id=? AND channel=? AND channel_account_id=? AND peer_id=? "
"AND direction=? AND message_id=?",
(
message.tenant_id, message.cell_id, message.channel,
message.channel_account_id, message.peer_id, message.direction,
message_id,
),
).fetchone()
return bool(row and row["event_id"] == message.event_id and row["body"] == message.body)
def remove(self, record: LedgerRecord) -> None:
"""Compensate a newly appended record if a later ingress step fails."""
with closing(self._connect()) as connection, connection as db:
db.execute(
"DELETE FROM message_ledger WHERE tenant_id=? AND cell_id=? AND "
"channel=? AND channel_account_id=? AND peer_id=? AND direction=? "
"AND message_id=? AND event_id=?",
(
record.tenant_id, record.cell_id, record.channel,
record.channel_account_id, record.peer_id, record.direction,
record.message_id, record.event_id,
),
)
def resolve_quote(self, message: CanonicalMessage) -> str:
if message.quoted_body is not None:
return message.quoted_body
if message.reply_to_message_id is None:
raise ScopeError("quoted_text_unavailable")
reply_id = normalize_message_id(
message.reply_to_message_id,
field_name="reply_to_message_id",
)
with closing(self._connect()) as connection, connection as db:
rows = db.execute(
"SELECT direction, body, event_id FROM message_ledger "
"WHERE tenant_id=? AND cell_id=? AND channel=? AND channel_account_id=? "
"AND peer_id=? AND message_id=? AND expires_at>?",
(
message.tenant_id,
message.cell_id,
message.channel,
message.channel_account_id,
message.peer_id,
reply_id,
isoformat(),
),
).fetchall()
if len(rows) == 1:
return str(rows[0]["body"])
if len(rows) > 1:
raise ConflictError("ambiguous reply ID across message directions")
raise ScopeError("quoted_text_unavailable")
def purge_expired(self) -> int:
with closing(self._connect()) as connection, connection as db:
cursor = db.execute(
"DELETE FROM message_ledger WHERE expires_at<=?", (isoformat(),)
)
return cursor.rowcount
def integrity_check(self) -> str:
with closing(self._connect()) as connection, connection as db:
return str(db.execute("PRAGMA integrity_check").fetchone()[0])
def count(self) -> int:
with closing(self._connect()) as connection, connection as db:
return int(db.execute("SELECT count(*) FROM message_ledger").fetchone()[0])
-336
View File
@@ -1,336 +0,0 @@
"""Authoritative tenant registry and fail-closed message router."""
from __future__ import annotations
from contextlib import contextmanager
from contextlib import closing
from dataclasses import dataclass
from datetime import datetime, timedelta
import sqlite3
from pathlib import Path
from typing import Callable, Iterator
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
TenantContext,
ValidationError,
isoformat,
parse_time,
require_ident,
utc_now,
)
@dataclass(frozen=True)
class Route:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
principal_id: str
session_id: str | None
class TenantRouter:
"""SQLite-backed routing registry.
Channel accounts are authoritative tenant ownership boundaries. Principal and
session bindings may narrow a route, but can never move traffic to another
tenant or Cell.
"""
def __init__(
self, database: str | Path, *, clock: Callable[[], datetime] = utc_now
) -> None:
self.database = str(database)
self._clock = clock
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.database, timeout=30, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@contextmanager
def _transaction(self) -> Iterator[sqlite3.Connection]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
yield connection
connection.execute("COMMIT")
except Exception:
connection.execute("ROLLBACK")
raise
finally:
connection.close()
def _initialize(self) -> None:
with closing(self._connect()) as connection, connection as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS tenants (
tenant_id TEXT PRIMARY KEY,
cell_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK(status IN ('active','suspended','retired')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS channel_routes (
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(channel, channel_account_id),
FOREIGN KEY(tenant_id) REFERENCES tenants(tenant_id)
);
CREATE TABLE IF NOT EXISTS principal_routes (
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(channel, channel_account_id, principal_id),
FOREIGN KEY(channel, channel_account_id)
REFERENCES channel_routes(channel, channel_account_id)
);
CREATE TABLE IF NOT EXISTS session_bindings (
tenant_id TEXT NOT NULL,
session_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(tenant_id, session_id),
FOREIGN KEY(tenant_id) REFERENCES tenants(tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_session_expiry
ON session_bindings(expires_at);
"""
)
def register_tenant(self, tenant_id: str, cell_id: str) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
cell_id = require_ident("cell_id", cell_id)
now = isoformat(self._clock())
with self._transaction() as db:
row = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if row:
if row["cell_id"] != cell_id:
raise ConflictError("tenant already bound to another cell")
db.execute(
"UPDATE tenants SET status='active', updated_at=? WHERE tenant_id=?",
(now, tenant_id),
)
return
conflict = db.execute(
"SELECT tenant_id FROM tenants WHERE cell_id=?", (cell_id,)
).fetchone()
if conflict:
raise ConflictError("cell already belongs to another tenant")
db.execute(
"INSERT INTO tenants VALUES(?,?,?,?,?)",
(tenant_id, cell_id, "active", now, now),
)
def set_tenant_status(self, tenant_id: str, status: str) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
if status not in {"active", "suspended", "retired"}:
raise ValidationError("invalid tenant status")
with self._transaction() as db:
cursor = db.execute(
"UPDATE tenants SET status=?, updated_at=? WHERE tenant_id=?",
(status, isoformat(self._clock()), tenant_id),
)
if cursor.rowcount != 1:
raise ValidationError("unknown tenant")
def bind_channel(
self,
*,
tenant_id: str,
channel: str,
channel_account_id: str,
) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
channel = require_ident("channel", channel)
channel_account_id = require_ident("channel_account_id", channel_account_id)
with self._transaction() as db:
tenant = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if not tenant or tenant["status"] != "active":
raise ValidationError("tenant is not active")
existing = db.execute(
"SELECT tenant_id, cell_id FROM channel_routes "
"WHERE channel=? AND channel_account_id=?",
(channel, channel_account_id),
).fetchone()
if existing:
if existing["tenant_id"] != tenant_id or existing["cell_id"] != tenant["cell_id"]:
raise ConflictError("channel account already belongs to another tenant")
return
db.execute(
"INSERT INTO channel_routes VALUES(?,?,?,?,?)",
(channel, channel_account_id, tenant_id, tenant["cell_id"], isoformat(self._clock())),
)
def bind_principal(
self,
*,
tenant_id: str,
channel: str,
channel_account_id: str,
principal_id: str,
) -> None:
principal_id = require_ident("principal_id", principal_id)
route = self.resolve_channel(channel, channel_account_id)
if route.tenant_id != tenant_id:
raise ScopeError("principal cannot cross tenant boundary")
with self._transaction() as db:
existing = db.execute(
"SELECT tenant_id, cell_id FROM principal_routes WHERE "
"channel=? AND channel_account_id=? AND principal_id=?",
(channel, channel_account_id, principal_id),
).fetchone()
if existing:
if existing["tenant_id"] != tenant_id or existing["cell_id"] != route.cell_id:
raise ConflictError("principal route conflict")
return
db.execute(
"INSERT INTO principal_routes VALUES(?,?,?,?,?,?)",
(
channel,
channel_account_id,
principal_id,
tenant_id,
route.cell_id,
isoformat(self._clock()),
),
)
def bind_session(
self,
*,
tenant_id: str,
session_id: str,
cell_id: str,
principal_id: str,
ttl_seconds: int = 86400,
) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
session_id = require_ident("session_id", session_id)
cell_id = require_ident("cell_id", cell_id)
principal_id = require_ident("principal_id", principal_id)
if not 60 <= ttl_seconds <= 31 * 86400:
raise ValidationError("invalid session TTL")
now = self._clock()
expires = isoformat(now + timedelta(seconds=ttl_seconds))
with self._transaction() as db:
tenant = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if not tenant or tenant["status"] != "active" or tenant["cell_id"] != cell_id:
raise ScopeError("session cell does not match tenant")
existing = db.execute(
"SELECT cell_id, principal_id FROM session_bindings "
"WHERE tenant_id=? AND session_id=?",
(tenant_id, session_id),
).fetchone()
if existing and (
existing["cell_id"] != cell_id or existing["principal_id"] != principal_id
):
raise ConflictError("session is already bound to another principal or cell")
db.execute(
"INSERT INTO session_bindings VALUES(?,?,?,?,?,?) "
"ON CONFLICT(tenant_id,session_id) DO UPDATE SET expires_at=excluded.expires_at",
(tenant_id, session_id, cell_id, principal_id, expires, isoformat(now)),
)
def resolve_channel(self, channel: str, channel_account_id: str) -> Route:
channel = require_ident("channel", channel)
channel_account_id = require_ident("channel_account_id", channel_account_id)
with closing(self._connect()) as connection, connection as db:
row = db.execute(
"SELECT r.tenant_id, r.cell_id, t.status FROM channel_routes r "
"JOIN tenants t ON t.tenant_id=r.tenant_id "
"WHERE r.channel=? AND r.channel_account_id=?",
(channel, channel_account_id),
).fetchone()
if not row:
raise ScopeError("unknown channel account")
if row["status"] != "active":
raise ScopeError("tenant is not active")
return Route(row["tenant_id"], row["cell_id"], channel, channel_account_id, "", None)
def resolve(
self,
*,
channel: str,
channel_account_id: str,
principal_id: str,
session_id: str | None = None,
) -> Route:
principal_id = require_ident("principal_id", principal_id)
base = self.resolve_channel(channel, channel_account_id)
with closing(self._connect()) as connection, connection as db:
principal = db.execute(
"SELECT tenant_id, cell_id FROM principal_routes WHERE "
"channel=? AND channel_account_id=? AND principal_id=?",
(channel, channel_account_id, principal_id),
).fetchone()
if principal and (
principal["tenant_id"] != base.tenant_id or principal["cell_id"] != base.cell_id
):
raise ScopeError("principal route violates channel ownership")
if session_id is not None:
session_id = require_ident("session_id", session_id)
session = db.execute(
"SELECT cell_id, principal_id, expires_at FROM session_bindings "
"WHERE tenant_id=? AND session_id=?",
(base.tenant_id, session_id),
).fetchone()
if not session:
raise ScopeError("unknown session")
if parse_time(session["expires_at"]) <= self._clock():
raise ScopeError("session expired")
if session["cell_id"] != base.cell_id or session["principal_id"] != principal_id:
raise ScopeError("session scope mismatch")
return Route(
base.tenant_id,
base.cell_id,
channel,
channel_account_id,
principal_id,
session_id,
)
def assert_message_route(self, message: CanonicalMessage) -> Route:
route = self.resolve(
channel=message.channel,
channel_account_id=message.channel_account_id,
principal_id=message.principal_id,
session_id=message.session_id,
)
if message.tenant_id != route.tenant_id or message.cell_id != route.cell_id:
raise ScopeError("message claims do not match authoritative route")
return route
def assert_context_route(self, context: TenantContext) -> Route:
route = self.resolve(
channel=context.channel,
channel_account_id=context.channel_account_id,
principal_id=context.principal_id,
session_id=context.session_id,
)
if context.tenant_id != route.tenant_id or context.cell_id != route.cell_id:
raise ScopeError("context claims do not match authoritative route")
return route
-136
View File
@@ -1,136 +0,0 @@
"""V2R1 runtime composition root for authenticated channel ingress."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Mapping
from .channel_adapter import ChannelAdapter, IngressResult
from .identity import (
AuthenticationError,
IngressAuthenticator,
RuntimeIdentity,
RuntimeIdentityResolver,
)
from .message_ledger import MessageLedger
from .router import TenantRouter
from .session_pool import SessionPool
from .tenant_context import ConflictError, ScopeError, TenantContext, ValidationError, utc_now
@dataclass(frozen=True)
class RuntimeIngressResponse:
"""Sanitised boundary response; no exception text crosses external ingress."""
status: str
result: IngressResult | None = None
class RuntimeWiring:
"""Own the only external-to-runtime identity binding and message path.
The request accepted by :meth:`ingest` is an HMAC-authenticated envelope;
tenant, cell, channel, account, session, principal, and message claims are
signed together. The envelope is then resolved against ``TenantRouter``.
Only that resolved identity can mint the short-lived internal context passed
to ``ChannelAdapter``.
"""
def __init__(
self,
*,
router: TenantRouter,
ledger: MessageLedger,
session_pool: SessionPool,
channel_adapter: ChannelAdapter,
identity_resolver: RuntimeIdentityResolver,
ingress_authenticator: IngressAuthenticator,
internal_signing_key: bytes,
clock: Callable[[], datetime],
) -> None:
self.router = router
self.ledger = ledger
self.session_pool = session_pool
self.channel_adapter = channel_adapter
self.identity_resolver = identity_resolver
self._ingress_authenticator = ingress_authenticator
self._internal_signing_key = internal_signing_key
self._clock = clock
@classmethod
def create(
cls,
root: str | Path,
*,
signing_key: bytes,
ingress_hmac_key: bytes | None = None,
clock: Callable[[], datetime] = utc_now,
) -> "RuntimeWiring":
if not isinstance(signing_key, bytes) or len(signing_key) < 32:
raise ValidationError("internal signing key must be at least 32 bytes")
# Kept optional for one controlled migration release; the external
# request path still cannot supply a TenantContext token.
hmac_key = ingress_hmac_key if ingress_hmac_key is not None else signing_key
root_path = Path(root)
root_path.mkdir(parents=True, exist_ok=True)
router = TenantRouter(root_path / "router.sqlite3", clock=clock)
ledger = MessageLedger(root_path / "message_ledger.sqlite3")
session_pool = SessionPool(router)
adapter = ChannelAdapter(
router=router,
ledger=ledger,
session_pool=session_pool,
signing_key=signing_key,
clock=clock,
)
return cls(
router=router,
ledger=ledger,
session_pool=session_pool,
channel_adapter=adapter,
identity_resolver=RuntimeIdentityResolver(router),
ingress_authenticator=IngressAuthenticator(hmac_key),
internal_signing_key=signing_key,
clock=clock,
)
def _issue_internal_context(self, identity: RuntimeIdentity) -> TenantContext:
"""Mint internal scope only after all claims matched the router route."""
return TenantContext.issue(
lifetime_seconds=300,
now=self._clock(),
tenant_id=identity.tenant_id,
cell_id=identity.cell_id,
principal_id=identity.principal_id,
principal_type="authenticated_user",
session_id=identity.session_id,
task_id=None,
channel=identity.channel,
channel_account_id=identity.channel_account_id,
)
def ingest(self, *, request: Mapping[str, Any]) -> RuntimeIngressResponse:
"""Authenticate, resolve, issue internal context, then append and lease.
The only successful route is ``IngressAuthenticator`` ->
``RuntimeIdentityResolver`` -> ``ChannelAdapter`` ->
``MessageLedger.append`` + ``SessionPool``. The legacy caller-supplied
TenantContext token interface is intentionally not accepted.
"""
try:
ingress = self._ingress_authenticator.authenticate(request)
identity = self.identity_resolver.resolve(ingress.principal, ingress.claims)
context = self._issue_internal_context(identity)
result = self.channel_adapter.ingest(
token=context.sign(self._internal_signing_key), message=ingress.message
)
return RuntimeIngressResponse(status="ACCEPTED", result=result)
except (AuthenticationError, ScopeError):
return RuntimeIngressResponse(status="AUTH_FAILED")
except ValidationError:
return RuntimeIngressResponse(status="VALIDATION_FAILED")
except ConflictError:
return RuntimeIngressResponse(status="CONFLICT")
except Exception:
return RuntimeIngressResponse(status="INTERNAL_ERROR")
@@ -1,33 +0,0 @@
"""Tenant and Cell scoped secret reference model.
This module never stores secret values. It stores ownership and retrieval scope
metadata for integration with a real secret broker.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class SecretRef:
secret_id: str
tenant_id: str
cell_id: str
kind: Literal["channel", "model", "tool", "runtime"]
version: str
broker_path: str
def __post_init__(self) -> None:
for name in ("secret_id", "cell_id", "version"):
require_ident(name, getattr(self, name))
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.kind not in {"channel", "model", "tool", "runtime"}:
raise ValidationError("invalid secret kind")
if not self.broker_path.startswith("secret://"):
raise ValidationError("secret broker path required")
def can_be_used_by(self, *, tenant_id: str, cell_id: str) -> bool:
return self.tenant_id == tenant_id and self.cell_id == cell_id
@@ -1,59 +0,0 @@
"""Process-local session lease pool backed by TenantRouter authority."""
from __future__ import annotations
from dataclasses import dataclass
from threading import RLock
from .router import Route, TenantRouter
from .tenant_context import ScopeError, TenantContext
@dataclass(frozen=True)
class SessionLease:
tenant_id: str
cell_id: str
principal_id: str
session_id: str
class SessionPool:
"""Tracks active leases without creating a second session authority store."""
def __init__(self, router: TenantRouter) -> None:
self._router = router
self._leases: dict[tuple[str, str], SessionLease] = {}
self._lock = RLock()
@staticmethod
def _assert_route(context: TenantContext, route: Route) -> None:
if (
route.tenant_id != context.tenant_id
or route.cell_id != context.cell_id
or route.principal_id != context.principal_id
or route.session_id != context.session_id
):
raise ScopeError("session lease does not match authoritative route")
def assert_available(self, context: TenantContext, route: Route) -> None:
"""Re-resolve the context from the router before a ledger mutation."""
self._assert_route(context, route)
self._assert_route(context, self._router.assert_context_route(context))
def acquire(self, context: TenantContext, route: Route) -> SessionLease:
self._assert_route(context, route)
lease = SessionLease(
tenant_id=context.tenant_id,
cell_id=context.cell_id,
principal_id=context.principal_id,
session_id=context.session_id,
)
with self._lock:
return self._leases.setdefault((lease.tenant_id, lease.session_id), lease)
def release(self, lease: SessionLease) -> None:
with self._lock:
self._leases.pop((lease.tenant_id, lease.session_id), None)
def active_count(self) -> int:
with self._lock:
return len(self._leases)
@@ -1,316 +0,0 @@
"""Signed tenant context and canonical message contract for AIOS V2R1."""
from __future__ import annotations
import base64
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone
import hashlib
import hmac
import json
import re
import secrets
from typing import Any, Mapping
VERSION = "V2R1C0"
MAX_SAFE_INTEGER = 9_007_199_254_740_991
TENANT_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$")
IDENT_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._:@-]{0,126}[A-Za-z0-9])?$")
class V2R1Error(RuntimeError):
pass
class ValidationError(V2R1Error):
pass
class ScopeError(V2R1Error):
pass
class ConflictError(V2R1Error):
pass
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def isoformat(value: datetime | None = None) -> str:
return (value or utc_now()).astimezone(timezone.utc).isoformat()
def parse_time(value: str) -> datetime:
if not isinstance(value, str) or not value.strip():
raise ValidationError("timestamp must be a non-empty string")
text = value.strip()
if text.endswith("Z"):
text = text[:-1] + "+00:00"
try:
parsed = datetime.fromisoformat(text)
except ValueError as exc:
raise ValidationError(f"invalid timestamp: {value!r}") from exc
if parsed.tzinfo is None:
raise ValidationError("timestamp must include timezone")
return parsed.astimezone(timezone.utc)
def require_ident(name: str, value: Any, *, tenant: bool = False) -> str:
if not isinstance(value, str):
raise ValidationError(f"{name} must be a string")
value = value.strip()
pattern = TENANT_RE if tenant else IDENT_RE
if not pattern.fullmatch(value):
raise ValidationError(f"invalid {name}: {value!r}")
return value
def normalize_message_id(value: Any, *, field_name: str = "message_id") -> str:
if isinstance(value, bool):
raise ValidationError(f"{field_name} cannot be boolean")
if isinstance(value, str):
value = value.strip()
if not value or len(value) > 256 or any(ord(ch) < 32 for ch in value):
raise ValidationError(f"invalid {field_name}")
return value
if isinstance(value, int):
if abs(value) > MAX_SAFE_INTEGER:
raise ValidationError(f"{field_name} is an unsafe numeric ID")
return str(value)
raise ValidationError(f"{field_name} must be a lossless string or safe integer")
def canonical_json(value: Any) -> bytes:
return json.dumps(
value, ensure_ascii=False, separators=(",", ":"), sort_keys=True
).encode("utf-8")
def _b64(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _unb64(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
@dataclass(frozen=True)
class TenantContext:
tenant_id: str
cell_id: str
principal_id: str
principal_type: str
session_id: str
task_id: str | None
channel: str
channel_account_id: str
issued_at: str
expires_at: str
nonce: str
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in (
"cell_id", "principal_id", "principal_type", "session_id",
"channel", "channel_account_id", "nonce",
):
require_ident(name, getattr(self, name))
if self.task_id is not None:
require_ident("task_id", self.task_id)
issued = parse_time(self.issued_at)
expires = parse_time(self.expires_at)
if expires <= issued or expires - issued > timedelta(hours=1):
raise ValidationError("invalid tenant context lifetime")
@classmethod
def issue(cls, *, lifetime_seconds: int = 300, now: datetime | None = None, **scope: Any) -> "TenantContext":
if not 1 <= lifetime_seconds <= 3600:
raise ValidationError("lifetime_seconds must be between 1 and 3600")
current = (now or utc_now()).astimezone(timezone.utc)
return cls(
**scope,
issued_at=isoformat(current),
expires_at=isoformat(current + timedelta(seconds=lifetime_seconds)),
nonce=secrets.token_hex(16),
)
def sign(self, key: bytes) -> str:
if len(key) < 32:
raise ValidationError("signing key must be at least 32 bytes")
payload = _b64(canonical_json(asdict(self)))
signature = _b64(hmac.new(key, payload.encode("ascii"), hashlib.sha256).digest())
return f"{payload}.{signature}"
@classmethod
def verify(
cls,
token: str,
key: bytes,
*,
now: datetime | None = None,
expected_tenant_id: str | None = None,
expected_cell_id: str | None = None,
) -> "TenantContext":
if len(key) < 32:
raise ValidationError("signing key must be at least 32 bytes")
try:
payload, encoded_signature = token.split(".", 1)
actual = _unb64(encoded_signature)
if encoded_signature != _b64(actual):
raise ValidationError("non-canonical tenant context signature")
data = json.loads(_unb64(payload))
except Exception as exc:
raise ValidationError("malformed tenant context token") from exc
expected = hmac.new(key, payload.encode("ascii"), hashlib.sha256).digest()
if not hmac.compare_digest(actual, expected):
raise ValidationError("tenant context signature mismatch")
context = cls(**data)
current = (now or utc_now()).astimezone(timezone.utc)
if current < parse_time(context.issued_at) - timedelta(seconds=60):
raise ValidationError("tenant context issued in future")
if current >= parse_time(context.expires_at):
raise ValidationError("tenant context expired")
if expected_tenant_id and context.tenant_id != expected_tenant_id:
raise ScopeError("tenant mismatch")
if expected_cell_id and context.cell_id != expected_cell_id:
raise ScopeError("cell mismatch")
return context
@dataclass(frozen=True)
class CanonicalMessage:
tenant_id: str
cell_id: str
principal_id: str
principal_type: str
origin_type: str
channel: str
channel_account_id: str
peer_id: str
conversation_id: str
session_id: str
task_id: str | None
event_id: str
message_id: str
reply_to_message_id: str | None
direction: str
command_capable: bool
occurred_at: str
received_at: str
body: str
quoted_body: str | None
adapter_name: str
adapter_version: str
schema_version: str = "aios.message.v1"
def __post_init__(self) -> None:
if self.schema_version != "aios.message.v1":
raise ValidationError("unsupported message schema version")
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in (
"cell_id", "principal_id", "principal_type", "origin_type", "channel",
"channel_account_id", "peer_id", "conversation_id", "session_id",
"event_id", "adapter_name", "adapter_version",
):
require_ident(name, getattr(self, name))
if self.task_id is not None:
require_ident("task_id", self.task_id)
normalize_message_id(self.message_id)
if self.reply_to_message_id is not None:
normalize_message_id(self.reply_to_message_id, field_name="reply_to_message_id")
if self.direction not in {"inbound", "outbound", "internal"}:
raise ValidationError("invalid direction")
if not isinstance(self.command_capable, bool):
raise ValidationError("command_capable must be boolean")
parse_time(self.occurred_at)
parse_time(self.received_at)
if not isinstance(self.body, str):
raise ValidationError("body must be a string")
if self.quoted_body is not None and not isinstance(self.quoted_body, str):
raise ValidationError("quoted_body must be a string or null")
@classmethod
def from_mapping(cls, data: Mapping[str, Any]) -> "CanonicalMessage":
content = data.get("content") or {}
adapter = data.get("adapter") or {}
if not isinstance(content, Mapping) or not isinstance(adapter, Mapping):
raise ValidationError("content and adapter must be objects")
values = dict(data)
return cls(
schema_version=values.get("schema_version", "aios.message.v1"),
tenant_id=values["tenant_id"],
cell_id=values["cell_id"],
principal_id=values["principal_id"],
principal_type=values["principal_type"],
origin_type=values["origin_type"],
channel=values["channel"],
channel_account_id=values["channel_account_id"],
peer_id=values["peer_id"],
conversation_id=values["conversation_id"],
session_id=values["session_id"],
task_id=values.get("task_id"),
event_id=values["event_id"],
message_id=normalize_message_id(values["message_id"]),
reply_to_message_id=(
normalize_message_id(values["reply_to_message_id"], field_name="reply_to_message_id")
if values.get("reply_to_message_id") is not None else None
),
direction=values["direction"],
command_capable=values["command_capable"],
occurred_at=values["occurred_at"],
received_at=values["received_at"],
body=content.get("body", ""),
quoted_body=content.get("quoted_body"),
adapter_name=adapter["name"],
adapter_version=adapter["version"],
)
def assert_context(self, context: TenantContext) -> None:
fields = (
"tenant_id", "cell_id", "principal_id", "session_id", "task_id",
"channel", "channel_account_id",
)
mismatches = [name for name in fields if getattr(self, name) != getattr(context, name)]
if mismatches:
raise ScopeError("message/context mismatch: " + ", ".join(mismatches))
@property
def is_authenticated_human_command(self) -> bool:
return (
self.direction == "inbound"
and self.command_capable
and self.principal_type == "authenticated_user"
and self.origin_type == "channel_user"
)
@dataclass(frozen=True)
class PolicyDecision:
allowed: bool
approval_required: bool
code: str
reason: str
READ_ONLY_TOOLS = frozenset({
"read", "grep", "glob", "find", "ls", "web_search", "web_fetch",
"memory_search", "sessions_list", "sessions_history", "session_status",
})
def evaluate_tool_call(tool_name: str, message: CanonicalMessage) -> PolicyDecision:
normalized = tool_name.strip().lower() if isinstance(tool_name, str) else ""
if not normalized:
return PolicyDecision(False, False, "invalid_tool", "tool name missing")
if normalized in READ_ONLY_TOOLS:
return PolicyDecision(True, False, "read_only", "read-only operation")
if not message.is_authenticated_human_command:
return PolicyDecision(
False, False, "origin_denied",
"side effects require an authenticated current human command",
)
return PolicyDecision(
True, True, "approval_required",
"authenticated human side effect requires explicit approval",
)
@@ -1,120 +0,0 @@
"""Architect governance agent responsibilities.
This agent records requirement changes, maintains versioned human-readable
requirement/architecture/deployment/test documents, reviews architecture changes,
validates deployment docs against actual configuration, and produces version bump
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class RequirementChange:
id: str
source: str
actor: str
requirement: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class DocumentRefreshPlan:
requirement_doc: bool
architecture_doc: bool
deployment_doc: bool
test_case_doc: bool
reason: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureReviewDecision:
status: str
findings: list[str]
optimization_recommendations: list[str]
version_bump: str
docs_to_refresh: DocumentRefreshPlan
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": self.findings,
"optimization_recommendations": self.optimization_recommendations,
"version_bump": self.version_bump,
"docs_to_refresh": self.docs_to_refresh.to_dict(),
"created_at": self.created_at,
}
class ArchitectGovernanceAgent:
"""Independent architecture governance role."""
def record_requirement_change(self, source: str, actor: str, requirement: str) -> RequirementChange:
change_id = "REQ-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
return RequirementChange(change_id, source, actor, requirement)
def review_architecture_change(self, proposal: dict[str, Any]) -> ArchitectureReviewDecision:
impact = proposal.get("impact", "medium")
findings = [
"requirement_traceability_checked",
"architecture_consistency_checked",
"deployment_doc_alignment_checked",
"test_case_doc_required",
"rollback_and_owner_gate_checked",
]
recommendations = [
"refresh user requirements document",
"refresh architecture design document",
"refresh engineering deployment document",
"generate acceptance test case document",
]
version_bump = self.decide_version_bump(proposal)
return ArchitectureReviewDecision(
status="review_required" if impact in {"high", "architecture"} else "pass",
findings=findings,
optimization_recommendations=recommendations,
version_bump=version_bump,
docs_to_refresh=DocumentRefreshPlan(True, True, True, True, "architecture or requirement changed"),
)
def decide_version_bump(self, proposal: dict[str, Any]) -> str:
if proposal.get("breaks_architecture") or proposal.get("major_capability"):
return "V"
if proposal.get("new_release_capability") or proposal.get("architecture_change"):
return "R"
return "C"
def validate_deployment_doc(self, deployment_doc: dict[str, Any], actual_config: dict[str, Any]) -> dict[str, Any]:
expected = set(deployment_doc.get("services", []))
actual = set(actual_config.get("services", []))
missing = sorted(expected - actual)
extra = sorted(actual - expected)
return {
"status": "pass" if not missing else "attention_required",
"missing_services": missing,
"extra_services": extra,
"checked_at": utc_now(),
}
__all__ = [
"RequirementChange",
"DocumentRefreshPlan",
"ArchitectureReviewDecision",
"ArchitectGovernanceAgent",
]
@@ -1,32 +0,0 @@
"""API for Architect Governance Center."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_governance_dashboard import ArchitectGovernanceDashboard
dashboard = ArchitectGovernanceDashboard()
def get_architect_governance_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect", tags=["architect-governance"])
@router.get("/overview", operation_id="getArchitectGovernanceOverview")
def overview():
return get_architect_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_architect_governance_overview"]
@@ -1,153 +0,0 @@
"""Web UI dashboard model for Architect Governance Agent.
Shows requirement changes, architecture review findings, document refresh status,
deployment consistency, test case readiness, weekly research and V/R/C version
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectPanelMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectPanel:
key: str
title: str
description: str
metrics: list[ArchitectPanelMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"description": self.description,
"metrics": [m.to_dict() for m in self.metrics],
}
class ArchitectGovernanceDashboard:
def overview(
self,
requirement_changes: list[dict[str, Any]] | None = None,
architecture_reviews: list[dict[str, Any]] | None = None,
document_status: dict[str, Any] | None = None,
research_status: dict[str, Any] | None = None,
version_recommendations: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
requirement_changes = requirement_changes or []
architecture_reviews = architecture_reviews or []
document_status = document_status or {}
research_status = research_status or {}
version_recommendations = version_recommendations or []
panels = [
self.requirement_panel(requirement_changes),
self.architecture_panel(architecture_reviews),
self.document_panel(document_status),
self.research_panel(research_status),
self.version_panel(version_recommendations),
]
return {
"module": "architect-governance",
"route": "/admin/ai-evolution/architect",
"title": "架构师 Agent 治理中心",
"status": "attention_required" if any(self._panel_warn(panel) for panel in panels) else "healthy",
"generated_at": utc_now(),
"panels": [panel.to_dict() for panel in panels],
"requirement_changes": requirement_changes,
"architecture_reviews": architecture_reviews,
"version_recommendations": version_recommendations,
}
def requirement_panel(self, changes: list[dict[str, Any]]) -> ArchitectPanel:
return ArchitectPanel(
key="requirements",
title="需求变化记录",
description="记录来自 Custom GPT、Codex、OpenClaw、Hermes 和用户反馈的需求变化。",
metrics=[
ArchitectPanelMetric("total_requirement_changes", "需求变化数量", len(changes)),
ArchitectPanelMetric(
"undocumented_changes",
"待刷新到用户需求文档",
len([c for c in changes if not c.get("documented")]),
"warn" if any(not c.get("documented") for c in changes) else "ok",
),
],
)
def architecture_panel(self, reviews: list[dict[str, Any]]) -> ArchitectPanel:
pending = [r for r in reviews if r.get("status") not in {"pass", "not_required"}]
return ArchitectPanel(
key="architecture_review",
title="全量架构审视",
description="展示架构一致性、扩展性、安全边界、可运维性和回滚策略审查。",
metrics=[
ArchitectPanelMetric("review_count", "架构审查数量", len(reviews)),
ArchitectPanelMetric("pending_reviews", "待处理架构审查", len(pending), "warn" if pending else "ok"),
],
)
def document_panel(self, status: dict[str, Any]) -> ArchitectPanel:
required_docs = ["requirements", "architecture", "deployment", "test_cases"]
missing = [doc for doc in required_docs if status.get(doc) not in {"ready", "updated"}]
return ArchitectPanel(
key="documents",
title="文档版本化状态",
description="用户需求、架构设计、工程部署、测试例文档刷新状态。",
metrics=[
ArchitectPanelMetric("requirements_doc", "用户需求文档", status.get("requirements", "unknown"), "warn" if "requirements" in missing else "ok"),
ArchitectPanelMetric("architecture_doc", "架构设计文档", status.get("architecture", "unknown"), "warn" if "architecture" in missing else "ok"),
ArchitectPanelMetric("deployment_doc", "工程部署文档", status.get("deployment", "unknown"), "warn" if "deployment" in missing else "ok"),
ArchitectPanelMetric("test_case_doc", "测试例文档", status.get("test_cases", "unknown"), "warn" if "test_cases" in missing else "ok"),
],
)
def research_panel(self, status: dict[str, Any]) -> ArchitectPanel:
overdue = bool(status.get("overdue"))
return ArchitectPanel(
key="weekly_research",
title="每周外部技术情报",
description="联网搜索 Agent/模型版本、GitHub top 实现、潜在安全影响。",
metrics=[
ArchitectPanelMetric("last_research_at", "最近调研时间", status.get("last_research_at", "never"), "warn" if overdue else "ok"),
ArchitectPanelMetric("findings", "发现数量", status.get("findings", 0)),
ArchitectPanelMetric("optimization_candidates", "可借鉴优化项", status.get("optimization_candidates", 0)),
],
)
def version_panel(self, recommendations: list[dict[str, Any]]) -> ArchitectPanel:
latest = recommendations[-1] if recommendations else {}
return ArchitectPanel(
key="version_decision",
title="V/R/C 版本建议",
description="由架构师 Agent 判断变更应进入 C、R 或 V。",
metrics=[
ArchitectPanelMetric("recommendation_count", "版本建议数量", len(recommendations)),
ArchitectPanelMetric("latest_recommendation", "最近建议", latest.get("version_bump", "none")),
ArchitectPanelMetric("reason", "建议原因", latest.get("reason", "")),
],
)
def _panel_warn(self, panel: ArchitectPanel) -> bool:
return any(metric.status == "warn" for metric in panel.metrics)
__all__ = ["ArchitectPanelMetric", "ArchitectPanel", "ArchitectGovernanceDashboard"]
@@ -1,40 +0,0 @@
"""Web UI model for Architect weekly research."""
from .architect_weekly_research_agent import ArchitectWeeklyResearchAgent
class ArchitectResearchDashboard:
def __init__(self, agent=None):
self.agent = agent or ArchitectWeeklyResearchAgent()
def overview(self):
return {
"module": "architect-weekly-research",
"route": "/admin/ai-evolution/architect/research",
"title": "架构师 Agent 技术情报中心",
"panels": [
{
"title": "Research Targets",
"data": self.agent.plan(),
},
{
"title": "Evaluation",
"fields": [
"relevance",
"impact",
"recommendation",
],
},
{
"title": "Version Decision",
"fields": [
"C",
"R",
"V",
],
},
],
}
__all__ = ["ArchitectResearchDashboard"]
@@ -1,122 +0,0 @@
"""Architect research timeline dashboard.
Shows weekly external research findings as a traceable architecture-impact flow:
research target -> finding -> impact -> recommendation -> version decision ->
governance event.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTimelineStep:
id: str
title: str
step_type: str
status: str
source: str
timestamp: str
impact: str = "none"
version_bump: str = "none"
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectResearchTimeline:
ORDER = {
"research_planned": 10,
"finding_created": 20,
"impact_analyzed": 30,
"recommendation_created": 40,
"version_decision_created": 50,
"architecture_review_event_created": 60,
}
def build(self, findings: list[dict[str, Any]] | None = None, decisions: list[dict[str, Any]] | None = None) -> dict[str, Any]:
findings = findings or []
decisions = decisions or []
steps: list[ResearchTimelineStep] = [
ResearchTimelineStep(
id="research-plan",
title="每周技术情报调研计划",
step_type="research_planned",
status="ok",
source="architect-agent",
timestamp=utc_now(),
detail={"cadence": "weekly"},
)
]
for idx, finding in enumerate(findings):
steps.append(
ResearchTimelineStep(
id=finding.get("id", f"finding-{idx}"),
title=finding.get("title", "research finding"),
step_type="finding_created",
status="ok",
source=finding.get("source", "unknown"),
timestamp=finding.get("created_at", utc_now()),
impact=finding.get("impact", "none"),
detail=finding,
)
)
steps.append(
ResearchTimelineStep(
id=f"impact-{finding.get('id', idx)}",
title="架构影响分析",
step_type="impact_analyzed",
status="attention_required" if finding.get("impact") in {"architecture", "breaking", "new_capability"} else "ok",
source="architect-agent",
timestamp=utc_now(),
impact=finding.get("impact", "none"),
detail={"relevance": finding.get("relevance"), "recommendation": finding.get("recommendation")},
)
)
for idx, decision in enumerate(decisions):
steps.append(
ResearchTimelineStep(
id=decision.get("id", f"decision-{idx}"),
title="C/R/V 版本建议",
step_type="version_decision_created",
status=decision.get("status", "ok"),
source="architect-agent",
timestamp=decision.get("created_at", utc_now()),
version_bump=decision.get("version_bump", "none"),
detail=decision,
)
)
steps.sort(key=lambda s: (self.ORDER.get(s.step_type, 999), s.timestamp))
return {
"module": "architect-research-timeline",
"route": "/admin/ai-evolution/architect/research/timeline",
"title": "架构师技术情报时间线",
"status": self._status(steps),
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"summary": self._summary(steps),
}
def _status(self, steps: list[ResearchTimelineStep]) -> str:
if any(step.status == "attention_required" for step in steps):
return "attention_required"
return "healthy"
def _summary(self, steps: list[ResearchTimelineStep]) -> dict[str, Any]:
return {
"findings": len([s for s in steps if s.step_type == "finding_created"]),
"architecture_impacts": len([s for s in steps if s.impact in {"architecture", "breaking", "new_capability"}]),
"version_decisions": len([s for s in steps if s.step_type == "version_decision_created"]),
"recommended_bumps": sorted({s.version_bump for s in steps if s.version_bump != "none"}),
}
__all__ = ["ResearchTimelineStep", "ArchitectResearchTimeline"]
@@ -1,32 +0,0 @@
"""API for architect research timeline."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_research_timeline import ArchitectResearchTimeline
timeline = ArchitectResearchTimeline()
def get_architect_research_timeline():
return timeline.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect/research", tags=["architect-research"])
@router.get("/timeline", operation_id="getArchitectResearchTimeline")
def get_timeline():
return get_architect_research_timeline()
return router
router = build_router()
__all__ = ["router", "get_architect_research_timeline"]
@@ -1,113 +0,0 @@
"""Weekly external research loop for Architect Governance Agent.
The agent plans and normalizes weekly research tasks. Actual internet access is
provided by the runtime/search worker; this module defines the audited workflow,
findings, impact analysis, and V/R/C recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTarget:
id: str
topic: str
source_type: str
query: str
cadence: str = "weekly"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchFinding:
id: str
target_id: str
title: str
summary: str
source: str
relevance: str
impact: str
recommendation: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchDecision:
status: str
version_bump: str
reason: str
findings: list[ResearchFinding]
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"created_at": self.created_at,
}
class ArchitectWeeklyResearchAgent:
DEFAULT_TARGETS = (
ResearchTarget("agent-frameworks", "Agent framework versions", "web", "latest multi-agent orchestration framework releases"),
ResearchTarget("model-ecosystem", "Model ecosystem changes", "web", "latest LLM model releases tool calling agent features"),
ResearchTarget("github-top-agentops", "GitHub top AgentOps implementations", "github", "top repositories agent observability multi agent orchestration"),
ResearchTarget("security-agent-risks", "Agent security changes", "web", "latest AI agent security sandbox prompt injection tool use risks"),
)
def plan(self) -> dict[str, Any]:
return {
"status": "planned",
"cadence": "weekly",
"targets": [target.to_dict() for target in self.DEFAULT_TARGETS],
"created_at": utc_now(),
}
def normalize_finding(
self,
target_id: str,
title: str,
summary: str,
source: str,
relevance: str = "medium",
impact: str = "none",
recommendation: str = "monitor",
) -> ResearchFinding:
return ResearchFinding(
id=f"finding_{uuid4().hex[:12]}",
target_id=target_id,
title=title,
summary=summary,
source=source,
relevance=relevance,
impact=impact,
recommendation=recommendation,
)
def decide(self, findings: list[ResearchFinding]) -> ResearchDecision:
if any(f.impact == "breaking" for f in findings):
return ResearchDecision("action_required", "V", "breaking external ecosystem change", findings)
if any(f.impact in {"architecture", "new_capability"} for f in findings):
return ResearchDecision("optimization_recommended", "R", "external capability may improve architecture", findings)
if any(f.impact in {"bugfix", "minor"} for f in findings):
return ResearchDecision("minor_change_recommended", "C", "minor compatible improvement", findings)
return ResearchDecision("no_action", "none", "no relevant external change", findings)
__all__ = ["ResearchTarget", "ResearchFinding", "ResearchDecision", "ArchitectWeeklyResearchAgent"]
@@ -1,32 +0,0 @@
"""API for architecture change governance dashboard."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architecture_change_dashboard import ArchitectureChangeDashboard
dashboard = ArchitectureChangeDashboard()
def get_architecture_change_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architecture-changes", tags=["architecture-changes"])
@router.get("/overview", operation_id="getArchitectureChangeOverview")
def overview():
return get_architecture_change_overview()
return router
router = build_router()
__all__ = ["router", "get_architecture_change_overview"]
@@ -1,90 +0,0 @@
"""Architecture change classification.
Small compatible architecture changes may be handled automatically by the
Architect Agent with audit evidence. Large, risky, production-impacting or
breaking architecture changes require explicit Owner approval before
implementation.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
@dataclass(frozen=True)
class ArchitectureChangeClassification:
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeClassifier:
"""Classifies architecture changes into small/medium/large governance paths."""
LARGE_FLAGS = {
"breaking_change",
"breaks_architecture",
"production_architecture_update",
"security_boundary_change",
"cross_tenant_effect",
"cross_user_data_effect",
"data_migration",
"permission_model_change",
"runtime_infrastructure_change",
}
def classify(self, change: dict[str, Any]) -> ArchitectureChangeClassification:
risk = str(change.get("risk", "medium")).lower()
version_bump = str(change.get("version_bump", change.get("recommended_version_bump", "C"))).upper()
scope = str(change.get("scope", "component")).lower()
reasons: list[str] = []
for flag in sorted(self.LARGE_FLAGS):
if change.get(flag):
reasons.append(flag)
if risk in {"high", "critical"}:
reasons.append(f"risk={risk}")
if version_bump == "V":
reasons.append("version_bump=V")
if version_bump == "R" and scope in {"system", "platform", "production", "cross_agent", "cross_tenant"}:
reasons.append(f"version_bump=R scope={scope}")
if reasons:
return ArchitectureChangeClassification(
level="large",
owner_required=True,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=reasons,
required_gates=["architect_review", "owner_architecture_approval", "test_gate", "promotion_gate"],
)
if risk == "low" and version_bump in {"C", "NONE"} and scope in {"component", "local", "documentation", "test"}:
return ArchitectureChangeClassification(
level="small",
owner_required=False,
auto_implement_allowed=True,
version_bump=version_bump,
reasons=["low_risk_compatible_change"],
required_gates=["architect_review", "audit", "test_gate"],
)
return ArchitectureChangeClassification(
level="medium",
owner_required=False,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=["requires_architect_review_before_implementation"],
required_gates=["architect_review", "test_gate", "audit"],
)
__all__ = ["ArchitectureChangeClassification", "ArchitectureChangeClassifier"]
@@ -1,93 +0,0 @@
"""Architecture change governance dashboard.
Displays architecture change classification, owner approval requirements,
implementation readiness, version recommendation and audit evidence for Web UI.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .architecture_change_classifier import ArchitectureChangeClassifier
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureChangeView:
change_id: str
title: str
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
status: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
owner_decision: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeDashboard:
def __init__(self, classifier: ArchitectureChangeClassifier | None = None):
self.classifier = classifier or ArchitectureChangeClassifier()
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
views = [self._view(change) for change in changes]
return {
"module": "architecture-change-governance",
"route": "/admin/ai-evolution/architecture-changes",
"title": "架构变更治理中心",
"status": self._status(views),
"generated_at": utc_now(),
"summary": self._summary(views),
"changes": [view.to_dict() for view in views],
}
def _view(self, change: dict[str, Any]) -> ArchitectureChangeView:
classification = self.classifier.classify(change)
owner_decision = change.get("owner_decision")
if classification.owner_required and owner_decision != "approved":
status = "waiting_owner_approval"
elif classification.auto_implement_allowed:
status = "auto_implementation_allowed"
else:
status = "ready_after_required_gates"
return ArchitectureChangeView(
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
level=classification.level,
owner_required=classification.owner_required,
auto_implement_allowed=classification.auto_implement_allowed,
version_bump=classification.version_bump,
status=status,
reasons=classification.reasons,
required_gates=classification.required_gates,
owner_decision=owner_decision,
)
def _status(self, views: list[ArchitectureChangeView]) -> str:
if any(view.status == "waiting_owner_approval" for view in views):
return "attention_required"
return "healthy"
def _summary(self, views: list[ArchitectureChangeView]) -> dict[str, Any]:
return {
"total": len(views),
"small": len([v for v in views if v.level == "small"]),
"medium": len([v for v in views if v.level == "medium"]),
"large": len([v for v in views if v.level == "large"]),
"waiting_owner_approval": len([v for v in views if v.status == "waiting_owner_approval"]),
"auto_allowed": len([v for v in views if v.auto_implement_allowed]),
"version_bumps": sorted({v.version_bump for v in views if v.version_bump and v.version_bump != "NONE"}),
}
__all__ = ["ArchitectureChangeView", "ArchitectureChangeDashboard"]
@@ -1,89 +0,0 @@
"""Owner approval gate for architecture changes.
Architecture changes may be proposed, reviewed and optimized by agents, but they
must not be implemented until owner approval is recorded. This gate sits before
implementation and before promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureOwnerApprovalRequest:
request_id: str
change_id: str
title: str
reason: str
risk: str
architect_decision: str
status: str = "pending_owner_approval"
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureOwnerDecision:
request_id: str
change_id: str
decision: str
decided_by: str
reason: str = ""
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureOwnerGate:
"""Blocks architecture implementation until owner explicitly approves."""
def create_request(self, change: dict[str, Any], architect_decision: dict[str, Any]) -> ArchitectureOwnerApprovalRequest:
return ArchitectureOwnerApprovalRequest(
request_id=f"ARCH-OWNER-{change.get('change_id', change.get('id', 'unknown'))}",
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
reason=change.get("reason", "architecture impact requires owner approval"),
risk=change.get("risk", "medium"),
architect_decision=architect_decision.get("status", "reviewed"),
)
def can_implement(self, request: dict[str, Any], decision: dict[str, Any] | None = None) -> dict[str, Any]:
if not decision:
return {
"allowed": False,
"status": "waiting_owner_approval",
"reason": "architecture change requires owner approval before implementation",
"request": request,
}
if decision.get("decision") == "approved":
return {
"allowed": True,
"status": "owner_approved",
"reason": decision.get("reason", "approved by owner"),
"request": request,
"decision": decision,
}
return {
"allowed": False,
"status": "owner_rejected",
"reason": decision.get("reason", "rejected by owner"),
"request": request,
"decision": decision,
}
__all__ = [
"ArchitectureOwnerApprovalRequest",
"ArchitectureOwnerDecision",
"ArchitectureOwnerGate",
]
@@ -1,79 +0,0 @@
"""HTTP handler and display data for architecture owner gate."""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any
from .architecture_owner_gate import ArchitectureOwnerGate, ArchitectureOwnerApprovalRequest
gate = ArchitectureOwnerGate()
# In-memory store for pending approval requests
_pending_requests: dict[str, dict[str, Any]] = {}
_decisions: dict[str, dict[str, Any]] = {}
def create_owner_approval_request(change: dict[str, Any], architect_decision: dict[str, Any]) -> dict[str, Any]:
req = gate.create_request(change, architect_decision)
d = asdict(req)
_pending_requests[req.change_id] = d
return d
def approve_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
decision = asdict(
ArchitectureOwnerApprovalRequest.__bases__[1].__new__(type("_ArchitectureOwnerDecision", (), {})) # noqa
)
try:
from .architecture_owner_gate import ArchitectureOwnerDecision
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="approved",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
# remove from pending
_pending_requests.pop(change_id, None)
return {"allowed": True, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def reject_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
from .architecture_owner_gate import ArchitectureOwnerDecision
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
try:
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="rejected",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
_pending_requests.pop(change_id, None)
return {"allowed": False, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def get_owner_gate_overview() -> dict[str, Any]:
return {
"pending_requests": list(_pending_requests.values()),
"decisions": list(_decisions.values()),
"pending_count": len(_pending_requests),
"approved_count": sum(1 for d in _decisions.values() if d.get("decision") == "approved"),
"rejected_count": sum(1 for d in _decisions.values() if d.get("decision") == "rejected"),
}
@@ -1,40 +0,0 @@
"""Independent architecture optimization agent."""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureReview:
agent: str
decision: str
findings: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class ArchitectureReviewAgent:
"""Reviews architecture changes from system-wide optimal perspective."""
def review(self, proposal: dict) -> ArchitectureReview:
findings = [
"dependency_impact_checked",
"security_boundary_checked",
"scalability_checked",
"operability_checked",
"rollback_strategy_checked",
]
return ArchitectureReview(
"architecture-optimization-agent",
"pass" if proposal.get("architecture_change") else "not_required",
findings,
)
__all__ = ["ArchitectureReviewAgent", "ArchitectureReview"]
@@ -0,0 +1,38 @@
"""Autonomous Controller health adapter."""
from __future__ import annotations
from typing import Any
from .blocker_task_adapter import BlockerTaskAdapter
from .git_permission_watchdog import GitPermissionWatchdog
from .system_blocker_memory import SystemBlockerMemory
from .system_health_center import SystemHealthCenter
class AutonomousHealthAdapter:
def __init__(self):
self.git = GitPermissionWatchdog()
self.memory = SystemBlockerMemory()
self.task_adapter = BlockerTaskAdapter()
self.health = SystemHealthCenter()
def check(self, previous_blockers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
git_report = self.git.scan().to_dict()
blockers = list(previous_blockers or [])
if git_report.get("status") != "healthy":
blockers.append({
"blocker_id": "git-permission",
"category": "git_permission",
"title": "Git object permission drift",
"severity": "p0",
"evidence": [str(git_report)],
})
return self.health.build(git_report=git_report, blockers=blockers)
def optimize_task(self, blocker: dict[str, Any]) -> dict[str, Any]:
decision = self.memory.decide(blocker)
return self.task_adapter.to_task(blocker, decision.to_dict())
__all__ = ["AutonomousHealthAdapter"]
@@ -0,0 +1,64 @@
"""Convert recurring blockers into Task Center tasks.
When a blocker becomes recurrent or critical, it should not remain only as an
incident log. It becomes a visible task with owner/executor/progress/approval
state in the Task Center.
"""
from __future__ import annotations
from typing import Any
from datetime import datetime, timezone
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class BlockerTaskAdapter:
def to_task(self, blocker: dict[str, Any], decision: dict[str, Any]) -> dict[str, Any]:
priority = decision.get("priority", "P2")
status = "waiting_approval" if decision.get("owner_gate_required") else "pending"
if priority == "P0":
status = "running"
return {
"task_id": f"blocker-{blocker.get('fingerprint', blocker.get('blocker_id', 'unknown'))}",
"title": f"Resolve blocker: {blocker.get('title', 'unknown')}",
"assigned_by": "system-blocker-memory",
"executor": decision.get("assigned_to", "autonomous-controller"),
"source": "system_blocker",
"status": status,
"priority": priority,
"created_at": utc_now(),
"updated_at": utc_now(),
"progress": {
"percent": 20 if status == "running" else 0,
"current_step": decision.get("action", "record_and_monitor"),
"completed_steps": ["blocker_recorded", "decision_created"],
"remaining_steps": self._remaining_steps(decision),
"blocked_reason": blocker.get("title") if status == "waiting_approval" else None,
},
"notification": {
"required": True,
"sent": False,
"channel": "wechat",
},
"owner_gate_required": bool(decision.get("owner_gate_required")),
"owner_gate_status": "pending" if decision.get("owner_gate_required") else None,
"evidence": blocker.get("evidence", []),
"metadata": {
"blocker": blocker,
"decision": decision,
},
}
def _remaining_steps(self, decision: dict[str, Any]) -> list[str]:
steps = ["root_cause_analysis", "patch_or_repair", "test", "deploy", "verify", "close"]
if decision.get("architect_review_required"):
steps.insert(1, "architect_review")
if decision.get("owner_gate_required"):
steps.insert(2, "owner_approval")
return steps
__all__ = ["BlockerTaskAdapter"]
@@ -1,103 +0,0 @@
"""Change acceptance governance for AIOS.
Every feature change must provide matching tests. Every architecture change must
be reviewed by an independent architecture agent. User-facing changes must be
validated by an independent user acceptance agent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ChangeDescriptor:
change_id: str
change_type: str
title: str
files_changed: list[str]
tests: list[str] = field(default_factory=list)
user_facing: bool = False
architecture_impact: bool = False
risk: str = "low"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceFinding:
check: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceDecision:
status: str
findings: list[AcceptanceFinding]
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": [f.to_dict() for f in self.findings],
"decided_at": self.decided_at,
}
class ChangeAcceptanceGate:
def evaluate(self, change: ChangeDescriptor) -> AcceptanceDecision:
findings: list[AcceptanceFinding] = []
findings.append(self._tests_required(change))
findings.append(self._user_acceptance_required(change))
findings.append(self._architecture_review_required(change))
findings.append(self._risk_gate(change))
status = "pass" if all(f.passed() for f in findings) else "blocked"
return AcceptanceDecision(status, findings)
def _tests_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.tests:
return AcceptanceFinding("tests_present", "pass", "matching tests declared")
return AcceptanceFinding("tests_present", "fail", "every feature change requires tests")
def _user_acceptance_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.user_facing:
return AcceptanceFinding("user_acceptance", "pass", "not user-facing")
has_user_test = any("user" in t.lower() or "acceptance" in t.lower() for t in change.tests)
return AcceptanceFinding(
"user_acceptance",
"pass" if has_user_test else "fail",
"user-facing change requires user acceptance test",
)
def _architecture_review_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.architecture_impact:
return AcceptanceFinding("architecture_review", "pass", "no architecture impact")
has_arch_test = any("arch" in t.lower() or "architecture" in t.lower() for t in change.tests)
return AcceptanceFinding(
"architecture_review",
"pass" if has_arch_test else "fail",
"architecture change requires independent architect review artifact/test",
)
def _risk_gate(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.risk in {"low", "medium", "high"}:
return AcceptanceFinding("risk_classified", "pass", f"risk={change.risk}")
return AcceptanceFinding("risk_classified", "fail", "risk must be low/medium/high")
__all__ = ["ChangeDescriptor", "AcceptanceFinding", "AcceptanceDecision", "ChangeAcceptanceGate"]
@@ -1,34 +0,0 @@
"""API for AIOS change governance dashboard."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .change_governance_dashboard import ChangeGovernanceDashboard
dashboard = ChangeGovernanceDashboard()
def get_change_governance_overview(changes: list[dict[str, Any]] | None = None):
return dashboard.overview(changes)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["change-governance"])
@router.get("/overview", operation_id="getChangeGovernanceOverview")
def overview():
return get_change_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_change_governance_overview"]
@@ -1,93 +0,0 @@
"""Web UI dashboard model for AIOS change governance.
Shows test gates, user acceptance, architecture review, risk gates and release
blocking status for human operators and owner review.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GovernanceMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernancePanel:
key: str
title: str
metrics: list[GovernanceMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class ChangeGovernanceDashboard:
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
blocked = [c for c in changes if c.get("status") == "blocked"]
waiting_arch = [c for c in changes if c.get("architecture_review") == "required"]
waiting_user = [c for c in changes if c.get("user_acceptance") == "required"]
missing_tests = [c for c in changes if not c.get("tests")]
panels = [
GovernancePanel(
"test_gate",
"测试 Gate",
[
GovernanceMetric("total_changes", "变更总数", len(changes)),
GovernanceMetric("missing_tests", "缺少测试的变更", len(missing_tests), "warn" if missing_tests else "ok"),
],
),
GovernancePanel(
"user_acceptance",
"用户验收 Agent",
[
GovernanceMetric("waiting_user_acceptance", "等待用户验收", len(waiting_user), "warn" if waiting_user else "ok"),
],
),
GovernancePanel(
"architecture_review",
"架构师 Agent",
[
GovernanceMetric("waiting_architecture_review", "等待架构审查", len(waiting_arch), "warn" if waiting_arch else "ok"),
],
),
GovernancePanel(
"release_gate",
"发布 Gate",
[
GovernanceMetric("blocked_changes", "阻塞发布的变更", len(blocked), "warn" if blocked else "ok"),
],
),
]
return {
"module": "change-governance",
"route": "/admin/ai-evolution/governance",
"title": "AIOS 变更治理中心",
"status": "attention_required" if blocked or missing_tests or waiting_arch or waiting_user else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"changes": changes,
}
__all__ = ["GovernanceMetric", "GovernancePanel", "ChangeGovernanceDashboard"]
@@ -0,0 +1,90 @@
"""Command watcher stall detector.
Detects commands that remain in active/running states beyond the configured
threshold and emits blocker records so AIOS can self-optimize instead of waiting
for manual progress checks.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_ts(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except Exception:
return None
@dataclass(frozen=True)
class CommandStallRecord:
command_id: str
path: str
bucket: str
age_minutes: float
threshold_minutes: int
severity: str
detected_at: str = field(default_factory=utc_now)
def to_blocker(self) -> dict[str, Any]:
return {
"blocker_id": f"command-stall-{self.command_id}",
"category": "command_stuck",
"title": f"Command stalled: {self.command_id}",
"source": "command-stall-detector",
"severity": self.severity,
"status": "open",
"first_seen_at": self.detected_at,
"last_seen_at": self.detected_at,
"occurrence_count": 1,
"evidence": [self.path],
"metadata": self.to_dict(),
}
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class CommandStallDetector:
def __init__(self, root: str = "agents/commands", threshold_minutes: int = 15):
self.root = Path(root)
self.threshold_minutes = threshold_minutes
def detect(self) -> list[CommandStallRecord]:
records: list[CommandStallRecord] = []
for bucket in ("active", "running"):
bucket_path = self.root / bucket
if not bucket_path.exists():
continue
for path in bucket_path.glob("*.md"):
age = self._age_minutes(path)
if age >= self.threshold_minutes:
records.append(
CommandStallRecord(
command_id=path.stem,
path=str(path),
bucket=bucket,
age_minutes=round(age, 2),
threshold_minutes=self.threshold_minutes,
severity="p0" if bucket == "running" else "high",
)
)
return records
def _age_minutes(self, path: Path) -> float:
stat = path.stat()
age_seconds = datetime.now(timezone.utc).timestamp() - stat.st_mtime
return age_seconds / 60
__all__ = ["CommandStallRecord", "CommandStallDetector"]
@@ -0,0 +1,141 @@
"""Git object permission watchdog and self-healing.
Prevents AIOS autonomous operation from being blocked by recurring
`.git/objects` ownership drift. The repair is intentionally conservative and
limited to the workspace Git object database.
"""
from __future__ import annotations
import os
import pwd
import grp
import subprocess
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GitPermissionIssue:
path: str
issue: str
actual_uid: int | None = None
actual_gid: int | None = None
expected_user: str = "aiman"
expected_group: str = "gitops"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GitPermissionReport:
workspace: str
expected_user: str
expected_group: str
status: str
issues: list[GitPermissionIssue] = field(default_factory=list)
checked_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"workspace": self.workspace,
"expected_user": self.expected_user,
"expected_group": self.expected_group,
"status": self.status,
"issues": [issue.to_dict() for issue in self.issues],
"checked_at": self.checked_at,
}
class GitPermissionWatchdog:
def __init__(
self,
workspace: str = "/data/aios-gitops/workspace",
expected_user: str = "aiman",
expected_group: str = "gitops",
max_scan_depth: int = 2,
):
self.workspace = Path(workspace)
self.expected_user = expected_user
self.expected_group = expected_group
self.max_scan_depth = max_scan_depth
@property
def objects_dir(self) -> Path:
return self.workspace / ".git" / "objects"
def scan(self) -> GitPermissionReport:
issues: list[GitPermissionIssue] = []
if not self.objects_dir.exists():
issues.append(GitPermissionIssue(str(self.objects_dir), "missing_objects_dir"))
return GitPermissionReport(str(self.workspace), self.expected_user, self.expected_group, "blocked", issues)
expected_uid = pwd.getpwnam(self.expected_user).pw_uid
expected_gid = grp.getgrnam(self.expected_group).gr_gid
for path in self._walk_limited(self.objects_dir):
try:
stat = path.stat()
except FileNotFoundError:
continue
if stat.st_uid != expected_uid:
issues.append(GitPermissionIssue(str(path), "owner_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if stat.st_gid != expected_gid:
issues.append(GitPermissionIssue(str(path), "group_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if not os.access(path, os.W_OK):
issues.append(GitPermissionIssue(str(path), "not_writable", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if len(issues) >= 200:
break
return GitPermissionReport(
workspace=str(self.workspace),
expected_user=self.expected_user,
expected_group=self.expected_group,
status="healthy" if not issues else "drift_detected",
issues=issues,
)
def repair(self, dry_run: bool = False) -> dict[str, Any]:
report = self.scan()
if report.status == "healthy":
return {"status": "healthy", "report": report.to_dict(), "repaired": False}
if dry_run:
return {"status": "dry_run", "report": report.to_dict(), "repaired": False}
commands = [
["chown", "-R", f"{self.expected_user}:{self.expected_group}", str(self.objects_dir)],
["chmod", "-R", "u+rwX,g+rwX", str(self.objects_dir)],
]
results = []
for command in commands:
proc = subprocess.run(command, text=True, capture_output=True, check=False)
results.append({"cmd": command, "returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr})
if proc.returncode != 0:
return {"status": "repair_failed", "report": report.to_dict(), "results": results, "repaired": False}
post = self.scan()
return {"status": "repaired" if post.status == "healthy" else "repair_incomplete", "report": post.to_dict(), "results": results, "repaired": post.status == "healthy"}
def write_probe(self) -> dict[str, Any]:
probe = self.objects_dir / f".aios-write-probe-{datetime.now(timezone.utc).timestamp()}"
try:
probe.write_text("probe\n", encoding="utf-8")
probe.unlink(missing_ok=True)
return {"status": "ok", "path": str(probe)}
except Exception as exc: # pragma: no cover - depends on runtime perms
return {"status": "failed", "path": str(probe), "error": str(exc)}
def _walk_limited(self, root: Path):
yield root
base_depth = len(root.parts)
for path in root.rglob("*"):
if len(path.parts) - base_depth <= self.max_scan_depth:
yield path
__all__ = ["GitPermissionIssue", "GitPermissionReport", "GitPermissionWatchdog"]
@@ -0,0 +1,31 @@
"""CLI entrypoint for Git Permission Watchdog."""
from __future__ import annotations
import json
import os
from pathlib import Path
from .git_permission_watchdog import GitPermissionWatchdog
def main() -> int:
workspace = os.getenv("AIOS_GITOPS_WORKSPACE", "/data/aios-gitops/workspace")
expected_user = os.getenv("AIOS_GIT_EXPECTED_USER", "aiman")
expected_group = os.getenv("AIOS_GIT_EXPECTED_GROUP", "gitops")
audit_dir = Path(os.getenv("AIOS_GIT_PERMISSION_AUDIT_DIR", "runtime/git-permission-watchdog"))
audit_dir.mkdir(parents=True, exist_ok=True)
watchdog = GitPermissionWatchdog(workspace, expected_user, expected_group)
result = watchdog.repair(dry_run=os.getenv("AIOS_GIT_PERMISSION_DRY_RUN") == "1")
probe = watchdog.write_probe()
payload = {"repair": result, "write_probe": probe}
status = result.get("status")
out = audit_dir / "latest.json"
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False))
return 0 if status in {"healthy", "repaired"} and probe.get("status") == "ok" else 2
if __name__ == "__main__":
raise SystemExit(main())
-36
View File
@@ -1,36 +0,0 @@
"""Git repository location resolver with validation.
Prevents hard-coded repository names from silently pointing to stale paths.
"""
from __future__ import annotations
import os
from pathlib import Path
class GitRepoLocator:
def __init__(self, root: str | None = None):
self.root = Path(root or os.getenv("AIOS_GITOPS_ROOT", "/data/aios-gitops/runtime/gitea-bin/data/repositories"))
def candidates(self, owner: str = "aios", names: tuple[str, ...] = ("aios.git", "aios-v23.git")):
return [self.root / owner / name for name in names]
def locate(self, owner: str = "aios") -> dict:
found = [str(path) for path in self.candidates(owner) if path.exists()]
return {
"root": str(self.root),
"owner": owner,
"found": found,
"healthy": len(found) == 1,
"warning": "multiple repositories found" if len(found) > 1 else None,
}
def require(self, owner: str = "aios") -> Path:
result = self.locate(owner)
if not result["healthy"]:
raise RuntimeError(f"Git repo resolution failed: {result}")
return Path(result["found"][0])
__all__ = ["GitRepoLocator"]
@@ -1,148 +0,0 @@
"""Bridge governance agents into the unified Agent Event Bus.
This module emits normalized governance events for requirement changes,
architecture reviews, user acceptance, test gates and version decisions. It uses
duck typing so it can work with the unified AgentEventBus when available, while
remaining testable without a running bus.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class EventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class GovernanceEventEnvelope:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryGovernanceEventBus:
"""Small fallback bus for tests and offline governance runs."""
def __init__(self) -> None:
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = GovernanceEventEnvelope(
id=f"evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class GovernanceEventBridge:
"""Publishes governance lifecycle events to the Agent Event Bus."""
def __init__(self, bus: EventPublisher | None = None):
self.bus = bus or InMemoryGovernanceEventBus()
def requirement_changed(self, requirement: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="requirement_changed",
source=requirement.get("source", "unknown"),
target="architect-agent",
summary=requirement.get("requirement", "requirement changed"),
payload=requirement,
correlation_id=requirement.get("id"),
)
def architecture_review_started(self, proposal: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_started",
source="architect-agent",
target="architecture-review-agent",
summary=proposal.get("title", "architecture review started"),
payload=proposal,
correlation_id=proposal.get("change_id"),
)
def architecture_review_completed(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_completed",
source="architecture-review-agent",
target="change-governance",
summary=f"architecture review {decision.get('status', 'completed')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
def user_acceptance_completed(self, report: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="user_acceptance_completed",
source="user-acceptance-agent",
target="change-governance",
summary=f"user acceptance {report.get('decision', 'completed')}",
payload=report,
correlation_id=report.get("change_id"),
)
def test_gate_completed(self, result: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="test_gate_completed",
source="test-gate",
target="change-governance",
summary=f"test gate {result.get('status', 'completed')}",
payload=result,
correlation_id=result.get("change_id"),
)
def version_decision_created(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="version_decision_created",
source="architect-agent",
target="owner-gate",
summary=f"version bump recommendation: {decision.get('version_bump', 'unknown')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
__all__ = [
"GovernanceEventEnvelope",
"InMemoryGovernanceEventBus",
"GovernanceEventBridge",
]
@@ -1,32 +0,0 @@
"""API for governance timeline visualization."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .governance_timeline_dashboard import GovernanceTimelineDashboard
dashboard = GovernanceTimelineDashboard()
def get_governance_timeline(events=None):
return dashboard.build(events)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["governance-timeline"])
@router.get("/timeline", operation_id="getGovernanceTimeline")
def timeline():
return get_governance_timeline()
return router
router = build_router()
__all__ = ["router", "get_governance_timeline"]
@@ -1,106 +0,0 @@
"""Governance timeline dashboard.
Turns governance events into a human-readable flow for Web UI so operators can
see why a change is blocked or ready for promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
EVENT_ORDER = {
"requirement_changed": 10,
"architecture_review_started": 20,
"architecture_review_completed": 30,
"test_gate_completed": 40,
"user_acceptance_completed": 50,
"version_decision_created": 60,
"owner_gate_completed": 70,
"promotion_completed": 80,
}
@dataclass(frozen=True)
class GovernanceTimelineStep:
id: str
kind: str
title: str
actor: str
status: str
timestamp: str
correlation_id: str | None = None
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GovernanceTimelineDashboard:
def build(self, events: list[dict[str, Any]] | None = None) -> dict[str, Any]:
events = events or []
steps = [self._event_to_step(event) for event in events]
steps.sort(key=lambda step: (EVENT_ORDER.get(step.kind, 999), step.timestamp))
blockers = [step for step in steps if step.status in {"blocked", "fail", "attention_required"}]
return {
"module": "governance-timeline",
"route": "/admin/ai-evolution/governance/timeline",
"title": "治理事件时间线",
"status": "attention_required" if blockers else "healthy",
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"blockers": [step.to_dict() for step in blockers],
"flow": self._flow_summary(steps),
}
def _event_to_step(self, event: dict[str, Any]) -> GovernanceTimelineStep:
payload = event.get("payload", {})
kind = event.get("kind", "unknown")
return GovernanceTimelineStep(
id=event.get("id", f"event-{len(str(event))}"),
kind=kind,
title=self._title(kind, payload),
actor=event.get("source", payload.get("actor", "unknown")),
status=payload.get("status") or payload.get("decision") or event.get("status", "ok"),
timestamp=event.get("created_at") or payload.get("created_at") or utc_now(),
correlation_id=event.get("correlation_id") or payload.get("change_id") or payload.get("id"),
detail=payload,
)
def _title(self, kind: str, payload: dict[str, Any]) -> str:
titles = {
"requirement_changed": "需求变化",
"architecture_review_started": "架构审查开始",
"architecture_review_completed": "架构审查完成",
"test_gate_completed": "测试 Gate 完成",
"user_acceptance_completed": "用户验收完成",
"version_decision_created": "版本建议产生",
"owner_gate_completed": "Owner Gate 完成",
"promotion_completed": "Promotion 完成",
}
return payload.get("title") or payload.get("summary") or titles.get(kind, kind)
def _flow_summary(self, steps: list[GovernanceTimelineStep]) -> dict[str, Any]:
present = {step.kind for step in steps}
required = [
"requirement_changed",
"architecture_review_completed",
"test_gate_completed",
"user_acceptance_completed",
"version_decision_created",
]
missing = [kind for kind in required if kind not in present]
return {
"required_steps": required,
"missing_steps": missing,
"ready_for_owner_gate": not missing,
}
__all__ = ["GovernanceTimelineStep", "GovernanceTimelineDashboard"]
@@ -1,59 +0,0 @@
"""Independent acceptance agents.
Simulates independent roles: user representative and architecture reviewer.
They provide acceptance evidence, not implementation decisions.
"""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AcceptanceReport:
agent: str
role: str
decision: str
evidence: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class UserAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"user_requirement_match",
"user_workflow_validation",
"permission_visibility_check",
]
return AcceptanceReport(
"user-acceptance-agent",
"user_representative",
"pass" if change.get("user_facing") else "not_required",
checks,
)
class ArchitectureAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"architecture_consistency",
"scalability_review",
"security_boundary_review",
"operability_review",
]
return AcceptanceReport(
"architecture-review-agent",
"system_architect",
"pass" if change.get("architecture_impact") else "not_required",
checks,
)
__all__ = ["AcceptanceReport", "UserAcceptanceAgent", "ArchitectureAcceptanceAgent"]
@@ -0,0 +1,52 @@
"""Unified AIOS operations command center view model."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class OperationsCommandCenter:
def build(
self,
tasks: list[dict[str, Any]] | None = None,
health: dict[str, Any] | None = None,
blockers: list[dict[str, Any]] | None = None,
searches: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
tasks = tasks or []
blockers = blockers or []
searches = searches or []
return {
"module": "operations-command-center",
"route": "/admin/ai-evolution/operations",
"title": "AIOS 自治运营驾驶舱",
"generated_at": utc_now(),
"health": health or {},
"tasks": {
"active": len([t for t in tasks if t.get("status") in {"running", "pending", "waiting_approval"}]),
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
"completed": len([t for t in tasks if t.get("status") == "completed"]),
},
"blockers": {
"total": len(blockers),
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
"recurring": len([b for b in blockers if b.get("mechanism_gap")]),
},
"search": {
"recent": len(searches),
},
"loops": {
"task_loop": True,
"health_loop": True,
"optimization_loop": True,
"notification_loop": True,
},
}
__all__ = ["OperationsCommandCenter"]
@@ -58,8 +58,6 @@ pre { white-space: pre-wrap; word-break: break-word; }
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
</ul>
</div>
<div class="main">
@@ -92,14 +90,6 @@ pre { white-space: pre-wrap; word-break: break-word; }
</div>
<div id="panel-agents" class="panel" style="display:none;">
<div class="main-header"><h2>Recent Agent Executions</h2></div>
<div id="panel-arch-owner" class="panel" style="display:none;">
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
<div id="arch-owner-content">Loading...</div>
</div>
<div id="panel-arch-approve" class="panel" style="display:none;">
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
<div id="arch-approve-content">Loading...</div>
</div>
<div id="agents-list">Loading...</div>
</div>
</div>
@@ -107,159 +97,3 @@ pre { white-space: pre-wrap; word-break: break-word; }
<script>
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('content').style.display = '';
const hs = document.getElementById('header-status');
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
document.getElementById('base').textContent = data.baseline || '-';
document.getElementById('branch').textContent = data.main_branch || 'main';
document.getElementById('uptime').textContent = data.uptime || '-';
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
}
async function loadVersions() {
const el = document.getElementById('versions-list');
el.innerHTML = '<div class="loading">Loading versions...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/versions');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '<div class="grid">';
for (const v of data.versions || []) {
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
}
}
async function loadCommands() {
const el = document.getElementById('commands-list');
el.innerHTML = '<div class="loading">Loading commands...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
let cmds = data.commands || [];
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
html += '<div><div class="card"><div class="card-title">By Category</div>';
const cats = {};
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
html += '</div></div></div>';
for (const cmd of cmds.slice().reverse()) {
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
}
}
async function loadAgents() {
const el = document.getElementById('agents-list');
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/agents');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '';
for (const agent of data.agents || []) {
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
}
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
} catch(e) {
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
}
}
function showPanel(name) {
async function loadOwnerGate() {
const el = document.getElementById('arch-owner-content');
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '<div class="row">';
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
html += '</div>';
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
if (data.pending_requests && data.pending_requests.length) {
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
html += '</div>';
}
} else {
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
async function loadApprove() {
const el = document.getElementById('arch-approve-content');
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '';
if (data.pending_requests && data.pending_requests.length) {
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
html += '<div style="display:flex;gap:8px;">';
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
html += '</div></div>';
}
html += '</div>';
} else {
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
document.getElementById('panel-' + name).style.display = '';
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
if (name === 'overview') {
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
fetchData();
}
const loaders = {
'arch-owner': loadOwnerGate,
'arch-approve': loadApprove,
versions: loadVersions,
commands: loadCommands,
agents: loadAgents,
};
const fn = loaders[name];
if (fn) fn();
}
fetchData();
</script>
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
"""System blocker memory and recurring issue detection.
Records blocking issues, long-no-response incidents and repeated operational
failures. Repeated blockers are escalated from incident handling into mechanism
optimization work.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SystemBlockerRecord:
blocker_id: str
category: str
title: str
source: str
severity: str
status: str = "open"
first_seen_at: str = field(default_factory=utc_now)
last_seen_at: str = field(default_factory=utc_now)
occurrence_count: int = 1
evidence: list[str] = field(default_factory=list)
mechanism_gap: bool = False
recommended_owner: str = "autonomous-controller"
def fingerprint(self) -> str:
return f"{self.category}:{self.title}".lower().replace(" ", "_")
def to_dict(self) -> dict[str, Any]:
return asdict(self) | {"fingerprint": self.fingerprint()}
@dataclass(frozen=True)
class BlockerEscalationDecision:
status: str
priority: str
reason: str
action: str
assigned_to: str
create_task: bool = True
architect_review_required: bool = False
owner_gate_required: bool = False
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SystemBlockerMemory:
"""In-memory decision logic; production storage can wrap this with persistence."""
def record(self, existing: list[dict[str, Any]], incoming: SystemBlockerRecord) -> dict[str, Any]:
records = [dict(item) for item in existing]
fp = incoming.fingerprint()
for item in records:
if item.get("fingerprint") == fp or f"{item.get('category')}:{item.get('title')}".lower().replace(" ", "_") == fp:
item["occurrence_count"] = int(item.get("occurrence_count", 1)) + 1
item["last_seen_at"] = utc_now()
item["status"] = "recurrent"
evidence = list(item.get("evidence", [])) + incoming.evidence
item["evidence"] = list(dict.fromkeys(evidence))
item["mechanism_gap"] = self.is_mechanism_gap(item)
item["fingerprint"] = fp
return item
item = incoming.to_dict()
item["mechanism_gap"] = self.is_mechanism_gap(item)
records.append(item)
return item
def decide(self, blocker: dict[str, Any]) -> BlockerEscalationDecision:
count = int(blocker.get("occurrence_count", 1))
severity = blocker.get("severity", "medium")
category = blocker.get("category", "unknown")
if severity in {"critical", "p0"} or category in {"git_permission", "runtime_deadlock", "autonomous_controller_stall"}:
return BlockerEscalationDecision(
status="escalate_to_mechanism_fix",
priority="P0",
reason="blocking infrastructure issue affects autonomous operation",
action="create_self_healing_mechanism",
assigned_to="openclaw",
architect_review_required=True,
)
if count >= 3:
return BlockerEscalationDecision(
status="escalate_to_mechanism_fix",
priority="P1",
reason="recurring issue exceeded threshold",
action="create_root_cause_and_prevention_patch",
assigned_to="autonomous-controller",
architect_review_required=True,
)
if category == "long_no_response":
return BlockerEscalationDecision(
status="create_monitoring_task",
priority="P1",
reason="long no response requires watchdog and notification",
action="create_stall_watchdog_task",
assigned_to="openclaw",
)
return BlockerEscalationDecision(
status="track_incident",
priority="P2",
reason="single incident tracked for recurrence",
action="record_and_monitor",
assigned_to="autonomous-controller",
create_task=False,
)
def is_mechanism_gap(self, blocker: dict[str, Any]) -> bool:
return int(blocker.get("occurrence_count", 1)) >= 2 or blocker.get("severity") in {"critical", "p0"}
__all__ = ["SystemBlockerRecord", "BlockerEscalationDecision", "SystemBlockerMemory"]
+32
View File
@@ -0,0 +1,32 @@
"""System Health Center API."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .system_health_center import SystemHealthCenter
center = SystemHealthCenter()
def get_system_health_overview():
return center.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/system-health", tags=["system-health"])
@router.get("/overview", operation_id="getSystemHealthOverview")
def overview():
return get_system_health_overview()
return router
router = build_router()
__all__ = ["router", "get_system_health_overview"]
@@ -0,0 +1,54 @@
"""Unified AIOS system health center model."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class SystemHealthCenter:
def build(
self,
git_report: dict[str, Any] | None = None,
blockers: list[dict[str, Any]] | None = None,
tasks: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
blockers = blockers or []
tasks = tasks or []
return {
"module": "system-health-center",
"route": "/admin/ai-evolution/system-health",
"title": "AIOS 系统健康与自优化中心",
"generated_at": utc_now(),
"status": self._status(git_report, blockers),
"git": git_report or {},
"blockers": {
"total": len(blockers),
"recurrent": len([b for b in blockers if b.get("mechanism_gap")]),
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
},
"tasks": {
"optimization_tasks": len([t for t in tasks if t.get("source") == "system_blocker"]),
"pending": len([t for t in tasks if t.get("status") in {"pending", "running", "waiting_approval"}]),
},
"optimization_loop": {
"detect": True,
"remember": True,
"optimize": True,
"verify": True,
},
}
def _status(self, git_report, blockers):
if git_report and git_report.get("status") not in {"healthy", "ok"}:
return "attention_required"
if any(b.get("severity") in {"p0", "critical"} for b in blockers):
return "attention_required"
return "healthy"
__all__ = ["SystemHealthCenter"]
@@ -1,36 +0,0 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -1,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,7 +0,0 @@
{
"nid": "n-1783527185",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:13:05+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783529900",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:20+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783529914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783530514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:08:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783531114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:18:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783531714",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:28:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783532314",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:38:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783532914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:48:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783533514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:58:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783534114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T02:08:34+0800"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783527185",
"created": "2026-07-09T00:13:05+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"test/alias-write-verify",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/gitops-write-auth-smoke-20260706T115230Z",
"patch/gpt-actions-auth-live-20260708"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783529900",
"created": "2026-07-09T00:58:20+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"test/alias-test-file-branch",
"patch/codex-online-sync-acceptance-20260706T102819Z",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/actions-invalid-diag-smoke-20260706T140311Z",
"patch/V1R2C2-autonomous-recovery-loop"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783529914",
"created": "2026-07-09T00:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783530514",
"created": "2026-07-09T01:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783531114",
"created": "2026-07-09T01:18:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783531714",
"created": "2026-07-09T01:28:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783532314",
"created": "2026-07-09T01:38:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783532914",
"created": "2026-07-09T01:48:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783533514",
"created": "2026-07-09T01:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783534114",
"created": "2026-07-09T02:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
+29 -18
View File
@@ -1,7 +1,8 @@
<<<<<<< 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"
created_at: 2026-07-08T15:20:00+08:00
baseline: V1R3C1
commands:
@@ -11,22 +12,6 @@ commands:
completed_path: "agents/commands/completed/"
failed_path: "agents/commands/failed/"
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"
lifecycle:
new: active/
running: running/
success: completed/
failure: failed/
# Command lifecycle state machine:
# active/ → locked & moved to running/ → execute → success → completed/
# → failure → failed/
policies:
path: "agents/policies/"
source: "git"
@@ -53,4 +38,30 @@ audit:
agent_runs: "audit/agent-runs/"
version_history: "audit/version-history/"
command_history: "audit/command-history/"
interaction_history: "audit/interaction-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,12 +0,0 @@
{
"ts": "2026-07-09T02:08:34+0800",
"ok": false,
"checks": {
"gitea": false,
"api": true,
"branch_sync": false,
"stuck": true
},
"plans": 1,
"applied": 0
}
-11
View File
@@ -1,11 +0,0 @@
{
"ts": "2026-07-09T00:00:00+09:00",
"ok": true,
"checks": {
"git_object_permission": true,
"api": true,
"branch_sync": true,
"stale_runtime_state_cleaned": true
},
"note": "Updated during PR #2 cleanup before merge"
}
@@ -1,21 +0,0 @@
{
"ts": "2026-07-09T00:59:30+0800",
"action": "command_watcher_state_clean",
"description": "清除了 command watcher 目录中的 stale 重复副本",
"result": "clean",
"stale_removed": [
{
"name": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"removed_from": ["active/", "running/"],
"preserved_in": "completed/",
"archive_path": "agents/commands/archive/"
},
{
"name": "CMD-20260709-enable-autonomous-controller.md",
"removed_from": ["running/"],
"preserved_in": "active/",
"archive_path": "agents/commands/archive/"
}
],
"note": "已完成命令保留在 completed/,副本移动到 archive/;未完成命令仅保留 active/"
}
@@ -1,17 +0,0 @@
{
"ts": "2026-07-09T01:00:00+0800",
"action": "daily_wechat_cron_verification",
"description": "验证每日 08:00 WeChat 汇报 cron 配置并手动触发 dry-run",
"cron_name": "AIOS每日自治报告",
"cron_id": "987f8619-6ccc-4370-8720-87149479a222",
"schedule": "0 8 * * * Asia/Shanghai",
"delivery": {
"mode": "announce",
"channel": "openclaw-weixin",
"to": "o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat",
"accountId": "ed3da13663c0-im-bot"
},
"dry_run_triggered": true,
"dry_run_result": "enqueued",
"status": "verified"
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783527185",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:13:05+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 184.6
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 185.8
}
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783529900",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:20+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 229.8
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.0
}
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783529914",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:34+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 230.1
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.3
}
]
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783530514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783531114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:18:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783531714",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:28:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783532314",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:38:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783532914",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:48:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783533514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:58:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783534114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T02:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+1 -13
View File
@@ -1,13 +1 @@
{
"current_baseline": "V1R4C6",
"previous_baseline": "V1R4C5",
"promoted_at": "2026-07-11T21:00:00+08:00",
"git": "e204a90398bf646d46245b3088226749fa056edd",
"promoted_by": "openclaw/admin",
"promotion_reason": "V1R4C6 admin search agent integration, search gateway, agent platform, VERSION bump, merge tests deploy pipeline — not yet deployed to production",
"constraints": {
"memory_search_enabled": false,
"force_push_allowed": false,
"gitea_db_mutation_requires_owner_approval": true
}
}
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
@@ -1,33 +0,0 @@
architect_agent_governance:
enabled: true
responsibilities:
requirement_change_tracking: true
requirement_document_refresh: true
architecture_document_refresh: true
deployment_document_validation: true
test_case_document_generation: true
architecture_optimization_review: true
weekly_external_research: true
github_top_implementation_review: true
sources:
requirement_sources:
- custom_gpt
- codex
- openclaw
- hermes
- user_feedback
version_decision:
criteria:
C: bug_fix_or_small_compatible_change
R: architecture_or_capability_release
V: major_architecture_or_breaking_change
research_cycle:
frequency: weekly
targets:
- agent_framework_versions
- model_versions
- github_top_implementations
- security_changes
governance:
owner_gate_required: true
audit_required: true
-22
View File
@@ -1,22 +0,0 @@
architect_research:
enabled: true
schedule:
frequency: weekly
research_sources:
- agent_framework_releases
- model_ecosystem_updates
- github_top_implementations
- security_advisories
evaluation:
required:
- relevance
- architecture_impact
- security_impact
- migration_cost
output:
- optimization_recommendation
- version_recommendation
- architecture_review_event
governance:
owner_gate_required: true
audit_required: true
@@ -1,36 +0,0 @@
architecture_owner_gate:
enabled: true
principle: owner_approves_large_architecture_changes_only
classification:
small:
owner_required: false
architect_auto_implementation: true
requirements:
- architect_review
- test_gate
- audit
medium:
owner_required: false
architect_review_required: true
implementation_after_review: true
requirements:
- architect_review
- test_gate
- audit
large:
owner_required: true
architect_auto_implementation: false
requirements:
- architect_review
- owner_architecture_approval
- test_gate
- promotion_gate
mandatory_owner_cases:
- breaking_change
- V_version_change
- production_architecture_update
- security_boundary_change
- cross_tenant_change
- permission_model_change
audit:
required: true
-18
View File
@@ -1,18 +0,0 @@
change_governance:
enabled: true
feature_change:
require_tests: true
require_user_acceptance_if_user_facing: true
architecture_change:
require_architecture_review_agent: true
require_system_optimization_review: true
acceptance_agents:
user_agent: user-acceptance-agent
architect_agent: architecture-review-agent
merge_gate:
required:
- tests_passed
- acceptance_passed
- architecture_review_passed
audit:
enabled: true
@@ -0,0 +1,35 @@
git_permission_watchdog:
enabled: true
priority: P0
workspace: /data/aios-gitops/workspace
expected_identity:
user: aiman
group: gitops
checks:
- object_owner
- object_group
- object_write_permission
- git_write_probe
repair:
enabled: true
allowed_paths:
- .git/objects
forbidden:
- chmod_777
- system_wide_permission_change
runtime:
interval_minutes: 5
create_task_on_drift: true
send_notification_on_failure: true
audit:
enabled: true
record:
- detection_time
- affected_paths
- repair_action
- repair_result
-22
View File
@@ -1,22 +0,0 @@
governance_event_bus:
enabled: true
event_types:
- requirement_changed
- architecture_review_started
- architecture_review_completed
- user_acceptance_completed
- test_gate_completed
- version_decision_created
producers:
- architect-agent
- user-acceptance-agent
- test-gate-agent
- change-governance-agent
consumers:
- web-ui
- audit-plane
- owner-gate
- reporting-agent
audit:
required: true
retention: versioned
-21
View File
@@ -1,21 +0,0 @@
models:
default:
- id: gpt-5.5
provider: openai
enabled: true
fallback: false
- id: qwen-3.6-35b
provider: qwen
enabled: true
fallback: true
- id: deepseek-v4-flash
provider: deepseek
enabled: true
fallback: true
runtime_behavior:
unavailable_model:
action: warn_and_fallback
hardcoded_model_list: false
health_check_required: true
audit_model_selection: true
@@ -0,0 +1,37 @@
system_blocker_optimization:
enabled: true
purpose: convert_recurring_failures_into_mechanism_improvements
tracked_categories:
- git_permission
- runtime_stall
- command_stuck
- provider_failure
- deployment_failure
- approval_wait
- notification_failure
- agent_timeout
thresholds:
repeat_count_for_mechanism_review: 3
critical_issue_immediate_review: true
long_no_response_minutes: 15
actions:
first_occurrence:
- record_incident
- monitor
repeated_occurrence:
- create_optimization_task
- assign_autonomous_controller
- require_architect_review
critical_occurrence:
- create_p0_task
- notify_owner
- self_healing_attempt
integration:
task_center: true
audit_plane: true
event_bus: true
architect_agent: true
Binary file not shown.
@@ -0,0 +1,13 @@
[Unit]
Description=AIOS Git Permission Watchdog
After=network.target
[Service]
Type=oneshot
User=aiman
Group=gitops
WorkingDirectory=/data/aios-gitops/workspace
ExecStart=/usr/bin/python3 -m active_runtime.app.git_permission_watchdog_cli
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Run AIOS Git Permission Watchdog periodically
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
Unit=aios-git-permission-watchdog.service
[Install]
WantedBy=timers.target

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