Compare commits

...
Author SHA1 Message Date
openclaw ce7aa08249 task(openclaw): B3 integration RESULT 20260714T190300UTC8 2026-07-14 19:03:49 +08:00
openclaw b4d3e53167 feat(v2r1c0): integrate B3 identity module into full RC1 runtime 20260714T190200UTC8 2026-07-14 19:02:52 +08:00
openclaw 0c5a268548 task(openclaw): create continue integration task 20260714T184637UTC8 2026-07-14 19:00:22 +08:00
openclaw 02682d27a5 task(openclaw): integrate B3 into full V2R1C0 RC1 runtime 20260714T180646UTC8 2026-07-14 18:09:21 +08:00
openclaw 6077331add test(v2r1): make router isolation contracts authoritative 2026-07-13 02:28:32 +08:00
openclaw 75f61ac0e6 test(v2r1): cover tenant router isolation contracts 2026-07-13 02:18:04 +08:00
openclaw 0b41a3952a fix-v2r1-suppress-hermes-conflict-context 2026-07-13 02:10:23 +08:00
openclaw 8f205c0d87 test-v2r1-cover-hermes-command-provenance 2026-07-13 02:05:03 +08:00
openclaw d2a4a765ac test-v2r1-enforce-sanitized-hermes-replay-conflicts 2026-07-13 02:03:49 +08:00
openclaw b1110cad62 fix(v2r1): reject noncanonical tenant signatures 2026-07-13 01:52:00 +08:00
openclaw b483dfadbf test(v2r1): define Hermes contract acceptance evidence 2026-07-13 01:25:28 +08:00
openclaw 1e5e144318 fix(v2r1): convert duplicate Hermes events to ConflictError 2026-07-13 01:25:23 +08:00
openclaw d552876a80 release(v2r1): add single source live status page 2026-07-13 00:12:28 +08:00
openclaw 6569065198 ops(v2r1): add RC1 health check 2026-07-12 23:41:01 +08:00
openclaw 2b80c50415 test(v2r1): add RC1 acceptance gate manifest 2026-07-12 23:40:56 +08:00
openclaw 954982647e fix(v2r1): align plugin package entry with OpenClaw SDK contract 2026-07-12 23:40:52 +08:00
openclaw 8074eb31b1 ops(v2r1): add RC1 install preflight 2026-07-12 23:37:42 +08:00
openclaw 90138221ee fix(v2r1): add OpenClaw runtime extension package metadata 2026-07-12 23:37:37 +08:00
openclaw 729c7bb23b fix(v2r1): bind OpenClaw trust only to authoritative runId 2026-07-12 23:37:33 +08:00
openclaw d2de7f23d1 ops(v2r1): add RC1 validation gate 2026-07-12 23:32:51 +08:00
openclaw 73c0a233ce feat(v2r1): add aios-cell lifecycle CLI 2026-07-12 23:32:47 +08:00
openclaw 00816a13c1 feat(v2r1): register OpenClaw runtime provenance and tool hooks 2026-07-12 23:32:42 +08:00
openclaw 4920ac6192 release: define AIOS V2R1C0 RC1 manifest 2026-07-12 23:26:05 +08:00
openclaw b12ff086bf feat(v2r1): add OpenClaw tenant boundary plugin manifest 2026-07-12 23:26:00 +08:00
openclaw 57a668d87e feat(v2r1): add OpenClaw tenant boundary policy runtime 2026-07-12 23:25:57 +08:00
openclaw bab319575b feat(v2r1): add Tenant Cell manager lifecycle registry 2026-07-12 23:22:54 +08:00
openclaw c172953a3c feat(v2r1): add Hermes profile isolation primitives 2026-07-12 23:22:47 +08:00
openclaw c7617fa548 feat(v2r1): add tenant-scoped memory namespace policy 2026-07-12 23:22:41 +08:00
openclaw 2eba23d274 feat(v2r1): add tenant secret scope references 2026-07-12 23:17:56 +08:00
openclaw f2e0406a20 feat(v2r1): add append-only tenant audit chain 2026-07-12 23:17:50 +08:00
openclaw ccb02640ac feat(v2r1): add tenant-scoped exact message ledger 2026-07-12 23:17:43 +08:00
openclaw 5c52f6c82c feat(v2r1): add tenant partitioned Hermes fabric scheduler 2026-07-12 23:10:55 +08:00
openclaw 429c6875bc feat(v2r1): add fail-closed Tenant Router and registry 2026-07-12 23:10:47 +08:00
openclaw f10d0d5b17 feat(v2r1): add OpenClaw Tenant Cell lifecycle adapter 2026-07-12 23:04:34 +08:00
openclaw c9974f3e47 feat(v2r1): add signed tenant context and canonical message contract 2026-07-12 23:04:24 +08:00
openclaw c72450aeb0 docs: add V1R5 to V2R1 migration plan 2026-07-12 22:39:32 +08:00
openclaw f95be48a42 docs: add AIOS V2R1 architecture baseline 2026-07-12 22:39:28 +08:00
openclaw ba989d7340 Start AIOS V2R1C0 Tenant Cell and Hermes Shared Agent Fabric release train 2026-07-12 22:39:23 +08:00
gito 1a9cb9095e Merge pull request 'reconcile(v1r4c6): finalize baseline metadata — previous_baseline V1R4C5, git e204a90' (#9) from reconcile/v1r4c6-baseline-finalize-20260711 into main 2026-07-11 20:55:54 +08:00
openclaw 0312159168 feat(reconcile): write V1R4C6 baseline metadata — previous_baseline V1R4C5, git e204a90 2026-07-11 20:54:46 +08:00
openclaw e204a90398 docs: baseline V1R4C6 promotion 2026-07-11 20:47:44 +08:00
openclaw d8cf6058e9 docs: promote VERSION V1R4C3 -> V1R4C6 ahead of main promotion 2026-07-11 20:47:28 +08:00
openclaw 2aefc19e3d authorize OpenClaw to execute V1R4C6 merge tests deploy pipeline 2026-07-11 20:41:31 +08:00
gito 28a430066f Merge pull request 'V1R4C6: merge candidate → main (release control artifacts)' (#8) from candidate/v1r4c6-integration-main-20260711 into main 2026-07-11 19:30:58 +08:00
openclaw 1013aef461 Merge branch 'release/v1r4c6-admin-search-agent-integration-20260711' into candidate/v1r4c6-integration-main-20260711
Resolve merge conflicts per V1R4C6 release control merge:

- baseline/current.json: keep feature branch version (updates to V1R4C5,
  adds constraints block). This was the intended runtime state that main
  was missing after V1R4C5 backfill (PR #7). Feature branch correctly has
  the V1R4C5 state; main still had V1R4C3.

- versions/V1R4C5.json: keep main version (complete fields with codename,
  tag_ref, tag_object, constraints, promoted_at/by). Feature branch had an
  older copy lacking these fields.

- docs/releases/V1R4C6-acceptance-evidence.md: keep main version (blank
  'NOT RUN' status). Feature branch had draft content.

- docs/releases/V1R4C6-release-gate-status.md: keep main version (blank
  'NOT RUN' status). Feature branch had draft content.
2026-07-11 19:29:47 +08:00
gito bb3210d74f Merge pull request 'docs: update V1R4C6 metadata to NOT RUN after control artifacts merge' (#7) from reconcile/v1r4c6-gate-status-notrun-docs-20260711 into main 2026-07-11 17:07:03 +08:00
openclaw 745395e93b docs: update V1R4C6 gate status, acceptance checklist, and acceptance evidence to NOT RUN
All engineering gates remain pending. This updates the release documents
to reflect the current integration-pending status after PR #6 merged.
2026-07-11 17:06:31 +08:00
gito ae49bf0fa3 Merge pull request 'reconcile: merge V1R4C6 release candidate into main' (#6) from candidate/v1r4c6-merge-to-main-20260711 into main 2026-07-11 16:45:28 +08:00
openclaw 8410cd895e reconcile: merge V1R4C6 release candidate into main
V1R4C6 merge candidate (squashed).
Contains 10 files, ~1440 lines added across:
- docs/releases/ (6 files: checklist, evidence, blocker, resume, merge-matrix, gate-status)
- ops/openclaw/ (2 files: integrate-deploy-verify, run-to-owner-gate-wechat)
- patches/ (1 file: branch patch)
- versions/ (1 file: V1R4C6.json)

Task: admin workbench, SR-03 Search Gateway, Agent Platform Integration
Baseline: V1R4C5
V1R4C5 tag semantics:
- tag refs/tags/V1R4C5 target d457ac3: immutable release snapshot
- baseline git a32c76a: runtime baseline promotion commit
No tag creation, baseline promotion, or memory search activation in this merge.
2026-07-11 16:34:03 +08:00
gito dc1642c2e8 Merge pull request 'reconcile: V1R4C5 baseline metadata (current.json + V1R4C5.json)' (#5) from reconcile/v1r4c5-baseline-metadata-20260711 into main 2026-07-11 13:48:30 +08:00
openclaw c75b865710 reconcile: update baseline registry to V1R4C5 and create V1R4C5 version metadata
Root cause:
The V1R4C5 baseline promotion commit (a32c76a) was made on a feature branch
that was never merged to main. As a result, both baseline/current.json and
versions/V1R4C5.json on main still reflect V1R4C3 state. Runtime
getBaseline() continues to return V1R4C3 with commit f5435e3.

Changes:
- baseline/current.json: update current_baseline -> V1R4C5, git ->
  a32c76a3c1 (the actual promotion commit),
  add constraints from the original promotion
- versions/V1R4C5.json: create the missing version metadata file with status
  active-baseline, referencing V1R4C5 tag (object ac9bdac...)

Why this does not affect V1R4C5 release content:
- V1R4C5 release documents (docs/releases/*.md) are untouched
- V1R4C5 tag is unchanged and not moved
- No V1R4C6 functionality introduced
- No memory search or vector index enabled

Rollback method:
  git revert <this commit hash>
  # Or restore baseline/current.json from V1R4C3 content:
  # git checkout main~1 baseline/current.json
  # And delete versions/V1R4C5.json:
  # git rm versions/V1R4C5.json
  # Then commit the revert

Constraints honored:
  - No direct writes to main (using PR)
  - No force push
  - V1R4C5 tag not moved/rebuild
  - No V1R4C6 content
  - Memory search remains disabled
2026-07-11 13:46:13 +08:00
gito e992b624e0 V1R4C5 → main: backfill release docs (acceptance evidence + owner approval) 2026-07-11 13:37:58 +08:00
openclaw c60e4cd02c reconcile: backfill V1R4C5 release documents (acceptance evidence + final owner approval) for main merge
Source: V1R4C5 tag based on unique publication commit a32c76a
- docs/releases/V1R4C5-acceptance-evidence.md (from V1R4C5 tag)
- docs/releases/V1R4C5-final-owner-approval.md (from V1R4C5 tag)

Constraints honored:
- Not writing directly to main
- No V1R4C6 content introduced
- Memory search remains disabled per owner approval

Generated merge candidate branch: reconcile/v1r4c5-release-to-main-20260711
2026-07-11 13:36:18 +08:00
openclaw 4b2996ca9f release: resume V1R4C6 after scoped WeChat notification verification 2026-07-11 07:47:05 +08:00
openclaw 086076b74f ops: direct OpenClaw to complete V1R4C6 and request owner approval via WeChat 2026-07-11 03:13:32 +08:00
openclaw 7b2d694272 ops: queue actual V1R4C6 integration deployment and verification 2026-07-11 03:10:28 +08:00
openclaw 0583060c7c release: update V1R4C6 gate status 2026-07-11 03:07:13 +08:00
openclaw 20e707fb52 release: add V1R4C6 acceptance evidence 2026-07-11 03:05:52 +08:00
openclaw 6c75ec6bb2 release: add V1R4C6 acceptance checklist 2026-07-11 03:04:02 +08:00
openclaw e9fefd4c18 release: add V1R4C6 integration merge matrix 2026-07-11 03:01:59 +08:00
openclaw 3ece481c74 fix(runtime): point GitOps API to active aios.git bare repository 2026-07-11 02:56:49 +08:00
openclaw 33177548c3 fix(baseline): stabilize V1R4C5 git pointer 2026-07-11 02:55:05 +08:00
openclaw ed1413a05e V1R4C5 baseline promotion: V1R4C3 -> V1R4C5
promoted_by: openclaw

promotion_reason: V1R4C5 SR-01 Gitea branch sync fix and SR-02 Watcher state cleanup

memory_search: disabled
2026-07-11 02:46:00 +08:00
openclaw 05d1e64228 release: record V1R4C6 baseline visibility blocker 2026-07-11 02:46:00 +08:00
openclaw 64bbe1b51c Create V1R4C6 integration release patch 2026-07-11 02:46:00 +08:00
openclaw 3c6f2c8c65 release: start V1R4C6 admin search agent integration 2026-07-11 02:26:31 +08:00
openclaw f6d9bb1510 Promote baseline to V1R4C3
- baseline/current.json updated from V1R3C1 to V1R4C3
- Fixed test_single_provider_failure_is_not_terminal (provider_order override)
- All 38 tests passing

AIOS_V1R4C3_BASELINE_PROMOTED
AIOS_BASELINE_CURRENT_UPDATED
2026-07-09 17:45:09 +08:00
openclaw 8ca4db3e8c Close completed autonomous controller CMD
- Moved CMD-20260709-enable-autonomous-controller.md from active to completed
- All autonomous controller tasks verified complete

AIOS_AUTONOMOUS_CONTROLLER_CMD_CLOSED
AIOS_RUNNING_COMMAND_QUEUE_CLEAN
2026-07-09 17:38:04 +08:00
openclaw d194d3a4d7 Add Owner Gate Web UI panels with approve/reject functionality 2026-07-09 17:23:23 +08:00
openclaw f5435e3087 release: version bump to V1R4C3 2026-07-09 16:13:42 +08:00
gito 88087dc7ff Merge pull request 'V1R4C1 review fixes: arch change API, tests, owner gate, governance' (#3) from patch/v1r4c1-review-risk-fixes into main 2026-07-09 16:13:08 +08:00
openclaw 2f04b9f46c release: version bump to V1R4C2 2026-07-09 16:10:29 +08:00
openclaw d8a426cea1 Clean stale runtime state before PR #2 merge 2026-07-09 12:27:58 +08:00
openclaw 22d48bad4d v1r4c1: batch commit - arch change api + tests + owner gate 2026-07-09 12:12:02 +08:00
openclaw 99c50464d9 Add architecture change governance dashboard 2026-07-09 11:49:30 +08:00
openclaw d0141450fe Add OpenClaw command for tiered architecture owner gate 2026-07-09 11:47:58 +08:00
openclaw 84c67fa743 Add architecture change classification tests 2026-07-09 11:47:55 +08:00
openclaw d0536dbe14 Update owner gate to support small auto and large approval 2026-07-09 11:47:53 +08:00
openclaw 4177a22936 Add architecture change classifier for owner gate levels 2026-07-09 11:47:48 +08:00
openclaw 5c38b23810 Add OpenClaw command for architecture owner gate 2026-07-09 11:42:54 +08:00
openclaw aa0f721ce8 Add architecture owner gate tests 2026-07-09 11:42:51 +08:00
openclaw 2e94439f49 Add architecture owner gate policy 2026-07-09 11:42:48 +08:00
openclaw 961c45c825 Add owner approval gate for architecture changes 2026-07-09 11:42:43 +08:00
openclaw 20f1f9820d Add OpenClaw command for architect research timeline UI 2026-07-09 11:39:28 +08:00
openclaw a6f08472dd Add architect research timeline tests 2026-07-09 11:39:22 +08:00
openclaw 9f95029225 Add architect research timeline API 2026-07-09 11:39:13 +08:00
openclaw 4ad4651ffa Add architect research timeline dashboard model 2026-07-09 11:39:05 +08:00
openclaw c0d008fae7 Add OpenClaw command for architect weekly research 2026-07-09 11:33:25 +08:00
openclaw 1974327e32 Add architect research policy 2026-07-09 11:33:19 +08:00
openclaw 110817b60c Add Architect weekly research tests 2026-07-09 11:33:12 +08:00
openclaw 95070ea029 Add Architect research dashboard model 2026-07-09 11:33:09 +08:00
openclaw 44aea3a9a1 Add Architect weekly external research agent 2026-07-09 11:33:03 +08:00
openclaw ce42691be5 Add OpenClaw command for governance timeline UI 2026-07-09 11:31:36 +08:00
openclaw 6f823de67d Add governance timeline tests 2026-07-09 11:31:32 +08:00
openclaw 7443fc7f26 Add governance timeline API 2026-07-09 11:31:28 +08:00
openclaw e1d484655f Add governance timeline dashboard model 2026-07-09 11:31:24 +08:00
openclaw 362a41bff8 Add OpenClaw command for governance event loop 2026-07-09 11:26:22 +08:00
openclaw 62974e879e Add governance event bridge tests 2026-07-09 11:26:19 +08:00
openclaw cd713cd73a Add governance event bus policy 2026-07-09 11:26:16 +08:00
openclaw bd8afc3cc4 Bridge governance agents into unified event bus 2026-07-09 11:26:12 +08:00
openclaw 1688639d7d Add OpenClaw command for architect governance UI 2026-07-09 11:21:41 +08:00
openclaw 60d07f9b17 Add architect governance tests 2026-07-09 11:21:38 +08:00
openclaw 824ff5dc90 Add Architect Governance API 2026-07-09 11:21:35 +08:00
openclaw 3d27f2eb43 Add Architect Governance Web UI dashboard model 2026-07-09 11:21:30 +08:00
openclaw a168610826 Add OpenClaw command for architect governance loop 2026-07-09 11:20:25 +08:00
openclaw 5447476b08 Add architect output document template 2026-07-09 11:20:22 +08:00
openclaw e58d99ba57 Add architect agent governance policy 2026-07-09 11:20:18 +08:00
openclaw 51d6f7e0c2 Extend architect agent governance responsibilities 2026-07-09 11:20:13 +08:00
openclaw 40459af09e Add OpenClaw command for change governance UI 2026-07-09 11:11:17 +08:00
openclaw c7dc61597a Add change governance dashboard tests 2026-07-09 11:11:14 +08:00
openclaw 43d55ac3b1 Add change governance dashboard API 2026-07-09 11:11:11 +08:00
openclaw 736e981005 Add change governance Web UI dashboard model 2026-07-09 11:11:06 +08:00
openclaw b84d6be3ca Add OpenClaw command for change governance agents 2026-07-09 11:09:56 +08:00
openclaw eff4940e9c Add change governance policy for acceptance agents 2026-07-09 11:09:52 +08:00
openclaw 0d6f2ce68d Add independent architecture optimization agent 2026-07-09 11:09:49 +08:00
openclaw a654978813 Add independent user and architecture acceptance agents 2026-07-09 11:09:45 +08:00
openclaw a399e30a8e Add change acceptance governance gate 2026-07-09 11:09:40 +08:00
openclaw e0644c0297 Add OpenClaw validation command for V1R4C1 fixes 2026-07-09 11:06:46 +08:00
openclaw 37eed9df7d Add V1R4C1 risk fix tests 2026-07-09 11:06:42 +08:00
openclaw 22582b834f Move model list from hardcode to configurable registry 2026-07-09 11:06:38 +08:00
openclaw 8a0c5f0556 Add Git repository path resolver and validator 2026-07-09 11:06:33 +08:00
openclaw 62b10d89c2 Create V1R4C1 review risk fixes branch 2026-07-09 11:06:26 +08:00
142 changed files with 7670 additions and 4 deletions
@@ -0,0 +1,40 @@
# RESULT-V2R1C0-B3-FULL-RC1-INTEGRATION-20260714T190300UTC8
## status
SUCCESS
## integration_branch
task/openclaw/v2r1c0-b3-full-rc1-integration-20260714T180646UTC8
## integration_commit
b4d3e53
## included_files
- active-runtime/app/aios_v2r1/identity.py (B3, 241 lines)
- active-runtime/app/aios_v2r1/tenant_context.py
- active-runtime/app/aios_v2r1/router.py
- active-runtime/app/aios_v2r1/hermes_fabric.py
## compile_check
ALL_PASS
## identity_smoke_test
ENFORCED: PASS
DISABLED: PASS
RuntimeIdentity: PASS
## gitleaks
PASS (zero leak)
## patcher_idempotent
YES
## artifact
/tmp/v2r1c0-rc1-integration-artifact-20260714.tar.gz
SHA256: f796a76f375c11f9b55b85c195514a636922962ae79572fb3ed8fbc2b1f5796f
## production
V1R4C6 (unchanged)
## next_step
awaiting production deployment decision
@@ -0,0 +1,19 @@
# TASK-V2R1C0-B3-FULL-RC1-INTEGRATION-CONTINUE-20260714T184637UTC8
## 目标
继续 V2R1C0 B3 Full RC1 Integration,完成剩余步骤。
## 执行链
1. **[4/7] 检查当前 RC1 分支状态** — 确认 B3 代码是否已在
2. **[5/7] 完整 runtime 整合** — 合入 runtime 依赖文件
3. **[6/7] cherry-pick d2acafa** — 确保 B3 代码在 RC1 分支中
4. **[7/7] 完整门禁** — 执行测试、gitleaks、patcher 验证
5. **[8/7] artifact 构建**
6. **[9/7] staging 验证**
7. **[10/7] RESULT 文件**
8. **[11/7] production 准备**
## 分支
- 基础分支: `v2r1c0-b3-full-rc1-integration-20260714T180646UTC8`
- B3 执行分支: `work/openclaw/v2r1c0-b3-execution-20260714`
- B3 commit: `d2acafa`
+1 -1
View File
@@ -1 +1 @@
V1R4C2
V1R4C6
+89
View File
@@ -0,0 +1,89 @@
"""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
@@ -0,0 +1,73 @@
"""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)
+48
View File
@@ -0,0 +1,48 @@
"""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
@@ -0,0 +1,106 @@
"""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]}
@@ -0,0 +1,142 @@
"""Tenant-partitioned Hermes shared agent fabric primitives."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
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 sqlite3.connect(self.database) 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 sqlite3.connect(self.database) 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 sqlite3.connect(self.database) 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 sqlite3.connect(self.database) 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(),
}
@@ -0,0 +1,47 @@
"""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")
+241
View File
@@ -0,0 +1,241 @@
"""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``.
B3 development adds ``disabled/enforced`` mode selection and a
bridge-ingress / receipt-read dual credential for external-side splits.
"""
from __future__ import annotations
from dataclasses import dataclass
import enum
import hashlib
import hmac
import re
from typing import Any, Mapping, Sequence
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}$")
_HEX_RE = re.compile(r"^[0-9a-f]+$", re.IGNORECASE)
_SCOPE_FIELDS = (
"principal_id", "tenant_id", "cell_id", "channel",
"channel_account_id", "session_id", "message",
)
_SCOPE_FIELDS_RECEIPT = (
"principal_id", "tenant_id", "cell_id", "channel",
"channel_account_id", "session_id", "receipt", "message",
)
_ALLOWED_MESSAGE_FIELDS = frozenset({"agent_id", "type", "payload"})
class AuthenticationError(ValidationError):
pass
class IngressMode(enum.Enum):
DISABLED = "disabled"
ENFORCED = "enforced"
@dataclass(frozen=True, init=False)
class AuthenticatedPrincipal:
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 BridgeIngressClaims:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
session_id: str
receipt: 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))
if not isinstance(self.receipt, str) or not self.receipt:
raise ValidationError("bridge ingress receipt must be a non-empty string")
@dataclass(frozen=True)
class AuthenticatedIngress:
principal: AuthenticatedPrincipal
claims: IdentityClaims
message: Mapping[str, Any]
@dataclass(frozen=True)
class BridgeAuthenticatedIngress:
principal: AuthenticatedPrincipal
claims: BridgeIngressClaims
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:
def __init__(self, hmac_key: bytes, *, mode: IngressMode = IngressMode.ENFORCED) -> 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
self._mode = mode
@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
@staticmethod
def _bridge_signed_payload(request: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(request, Mapping):
raise ValidationError("bridge ingress request must be an object")
allowed = {*_SCOPE_FIELDS_RECEIPT, "signature"}
if set(request) != allowed:
raise ValidationError(
f"bridge ingress request fields are invalid; "
f"expected {sorted(allowed)}, got {sorted(request)}"
)
values = {field: request[field] for field in _SCOPE_FIELDS_RECEIPT}
if not isinstance(values["message"], Mapping):
raise ValidationError("bridge ingress message must be an object")
return values
def _verify(self, values: dict[str, Any], signature: str) -> None:
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")
def authenticate(self, request: Mapping[str, Any]) -> AuthenticatedIngress:
values = self._signed_payload(request)
signature = request["signature"]
if self._mode == IngressMode.ENFORCED:
self._verify(values, signature)
elif self._mode == IngressMode.DISABLED:
if not isinstance(signature, str) or not _SIGNATURE_RE.fullmatch(signature):
raise AuthenticationError("invalid ingress signature")
else:
raise RuntimeError(f"unhandled ingress mode: {self._mode}")
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"]),
)
def authenticate_bridge(self, request: Mapping[str, Any]) -> BridgeAuthenticatedIngress:
values = self._bridge_signed_payload(request)
signature = request["signature"]
if self._mode == IngressMode.ENFORCED:
self._verify(values, signature)
elif self._mode == IngressMode.DISABLED:
if not isinstance(signature, str) or not _SIGNATURE_RE.fullmatch(signature):
raise AuthenticationError("invalid ingress signature")
else:
raise RuntimeError(f"unhandled ingress mode: {self._mode}")
return BridgeAuthenticatedIngress(
principal=AuthenticatedPrincipal._verified(values["principal_id"]),
claims=BridgeIngressClaims(
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"],
receipt=values["receipt"],
),
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 "",
)
@@ -0,0 +1,60 @@
"""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}"
@@ -0,0 +1,172 @@
"""Tenant-scoped append-only message ledger and exact quote resolver."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
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 self._connect() 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 remember(self, message: CanonicalMessage) -> LedgerRecord:
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,
)
with self._connect() 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["event_id"] != message.event_id or 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()
return LedgerRecord(*key, message.event_id, message.body, message.occurred_at, expires)
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 self._connect() 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 self._connect() as db:
cursor = db.execute(
"DELETE FROM message_ledger WHERE expires_at<=?", (isoformat(),)
)
return cursor.rowcount
def integrity_check(self) -> str:
with self._connect() as db:
return str(db.execute("PRAGMA integrity_check").fetchone()[0])
def count(self) -> int:
with self._connect() as db:
return int(db.execute("SELECT count(*) FROM message_ledger").fetchone()[0])
+335
View File
@@ -0,0 +1,335 @@
"""Authoritative tenant registry and fail-closed message router."""
from __future__ import annotations
from contextlib import contextmanager
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 self._connect() 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 self._connect() 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 self._connect() 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
@@ -0,0 +1,33 @@
"""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
@@ -0,0 +1,316 @@
"""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",
)
@@ -0,0 +1,120 @@
"""Architect governance agent responsibilities.
This agent records requirement changes, maintains versioned human-readable
requirement/architecture/deployment/test documents, reviews architecture changes,
validates deployment docs against actual configuration, and produces version bump
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class RequirementChange:
id: str
source: str
actor: str
requirement: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class DocumentRefreshPlan:
requirement_doc: bool
architecture_doc: bool
deployment_doc: bool
test_case_doc: bool
reason: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureReviewDecision:
status: str
findings: list[str]
optimization_recommendations: list[str]
version_bump: str
docs_to_refresh: DocumentRefreshPlan
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": self.findings,
"optimization_recommendations": self.optimization_recommendations,
"version_bump": self.version_bump,
"docs_to_refresh": self.docs_to_refresh.to_dict(),
"created_at": self.created_at,
}
class ArchitectGovernanceAgent:
"""Independent architecture governance role."""
def record_requirement_change(self, source: str, actor: str, requirement: str) -> RequirementChange:
change_id = "REQ-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
return RequirementChange(change_id, source, actor, requirement)
def review_architecture_change(self, proposal: dict[str, Any]) -> ArchitectureReviewDecision:
impact = proposal.get("impact", "medium")
findings = [
"requirement_traceability_checked",
"architecture_consistency_checked",
"deployment_doc_alignment_checked",
"test_case_doc_required",
"rollback_and_owner_gate_checked",
]
recommendations = [
"refresh user requirements document",
"refresh architecture design document",
"refresh engineering deployment document",
"generate acceptance test case document",
]
version_bump = self.decide_version_bump(proposal)
return ArchitectureReviewDecision(
status="review_required" if impact in {"high", "architecture"} else "pass",
findings=findings,
optimization_recommendations=recommendations,
version_bump=version_bump,
docs_to_refresh=DocumentRefreshPlan(True, True, True, True, "architecture or requirement changed"),
)
def decide_version_bump(self, proposal: dict[str, Any]) -> str:
if proposal.get("breaks_architecture") or proposal.get("major_capability"):
return "V"
if proposal.get("new_release_capability") or proposal.get("architecture_change"):
return "R"
return "C"
def validate_deployment_doc(self, deployment_doc: dict[str, Any], actual_config: dict[str, Any]) -> dict[str, Any]:
expected = set(deployment_doc.get("services", []))
actual = set(actual_config.get("services", []))
missing = sorted(expected - actual)
extra = sorted(actual - expected)
return {
"status": "pass" if not missing else "attention_required",
"missing_services": missing,
"extra_services": extra,
"checked_at": utc_now(),
}
__all__ = [
"RequirementChange",
"DocumentRefreshPlan",
"ArchitectureReviewDecision",
"ArchitectGovernanceAgent",
]
@@ -0,0 +1,32 @@
"""API for Architect Governance Center."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_governance_dashboard import ArchitectGovernanceDashboard
dashboard = ArchitectGovernanceDashboard()
def get_architect_governance_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect", tags=["architect-governance"])
@router.get("/overview", operation_id="getArchitectGovernanceOverview")
def overview():
return get_architect_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_architect_governance_overview"]
@@ -0,0 +1,153 @@
"""Web UI dashboard model for Architect Governance Agent.
Shows requirement changes, architecture review findings, document refresh status,
deployment consistency, test case readiness, weekly research and V/R/C version
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectPanelMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectPanel:
key: str
title: str
description: str
metrics: list[ArchitectPanelMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"description": self.description,
"metrics": [m.to_dict() for m in self.metrics],
}
class ArchitectGovernanceDashboard:
def overview(
self,
requirement_changes: list[dict[str, Any]] | None = None,
architecture_reviews: list[dict[str, Any]] | None = None,
document_status: dict[str, Any] | None = None,
research_status: dict[str, Any] | None = None,
version_recommendations: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
requirement_changes = requirement_changes or []
architecture_reviews = architecture_reviews or []
document_status = document_status or {}
research_status = research_status or {}
version_recommendations = version_recommendations or []
panels = [
self.requirement_panel(requirement_changes),
self.architecture_panel(architecture_reviews),
self.document_panel(document_status),
self.research_panel(research_status),
self.version_panel(version_recommendations),
]
return {
"module": "architect-governance",
"route": "/admin/ai-evolution/architect",
"title": "架构师 Agent 治理中心",
"status": "attention_required" if any(self._panel_warn(panel) for panel in panels) else "healthy",
"generated_at": utc_now(),
"panels": [panel.to_dict() for panel in panels],
"requirement_changes": requirement_changes,
"architecture_reviews": architecture_reviews,
"version_recommendations": version_recommendations,
}
def requirement_panel(self, changes: list[dict[str, Any]]) -> ArchitectPanel:
return ArchitectPanel(
key="requirements",
title="需求变化记录",
description="记录来自 Custom GPT、Codex、OpenClaw、Hermes 和用户反馈的需求变化。",
metrics=[
ArchitectPanelMetric("total_requirement_changes", "需求变化数量", len(changes)),
ArchitectPanelMetric(
"undocumented_changes",
"待刷新到用户需求文档",
len([c for c in changes if not c.get("documented")]),
"warn" if any(not c.get("documented") for c in changes) else "ok",
),
],
)
def architecture_panel(self, reviews: list[dict[str, Any]]) -> ArchitectPanel:
pending = [r for r in reviews if r.get("status") not in {"pass", "not_required"}]
return ArchitectPanel(
key="architecture_review",
title="全量架构审视",
description="展示架构一致性、扩展性、安全边界、可运维性和回滚策略审查。",
metrics=[
ArchitectPanelMetric("review_count", "架构审查数量", len(reviews)),
ArchitectPanelMetric("pending_reviews", "待处理架构审查", len(pending), "warn" if pending else "ok"),
],
)
def document_panel(self, status: dict[str, Any]) -> ArchitectPanel:
required_docs = ["requirements", "architecture", "deployment", "test_cases"]
missing = [doc for doc in required_docs if status.get(doc) not in {"ready", "updated"}]
return ArchitectPanel(
key="documents",
title="文档版本化状态",
description="用户需求、架构设计、工程部署、测试例文档刷新状态。",
metrics=[
ArchitectPanelMetric("requirements_doc", "用户需求文档", status.get("requirements", "unknown"), "warn" if "requirements" in missing else "ok"),
ArchitectPanelMetric("architecture_doc", "架构设计文档", status.get("architecture", "unknown"), "warn" if "architecture" in missing else "ok"),
ArchitectPanelMetric("deployment_doc", "工程部署文档", status.get("deployment", "unknown"), "warn" if "deployment" in missing else "ok"),
ArchitectPanelMetric("test_case_doc", "测试例文档", status.get("test_cases", "unknown"), "warn" if "test_cases" in missing else "ok"),
],
)
def research_panel(self, status: dict[str, Any]) -> ArchitectPanel:
overdue = bool(status.get("overdue"))
return ArchitectPanel(
key="weekly_research",
title="每周外部技术情报",
description="联网搜索 Agent/模型版本、GitHub top 实现、潜在安全影响。",
metrics=[
ArchitectPanelMetric("last_research_at", "最近调研时间", status.get("last_research_at", "never"), "warn" if overdue else "ok"),
ArchitectPanelMetric("findings", "发现数量", status.get("findings", 0)),
ArchitectPanelMetric("optimization_candidates", "可借鉴优化项", status.get("optimization_candidates", 0)),
],
)
def version_panel(self, recommendations: list[dict[str, Any]]) -> ArchitectPanel:
latest = recommendations[-1] if recommendations else {}
return ArchitectPanel(
key="version_decision",
title="V/R/C 版本建议",
description="由架构师 Agent 判断变更应进入 C、R 或 V。",
metrics=[
ArchitectPanelMetric("recommendation_count", "版本建议数量", len(recommendations)),
ArchitectPanelMetric("latest_recommendation", "最近建议", latest.get("version_bump", "none")),
ArchitectPanelMetric("reason", "建议原因", latest.get("reason", "")),
],
)
def _panel_warn(self, panel: ArchitectPanel) -> bool:
return any(metric.status == "warn" for metric in panel.metrics)
__all__ = ["ArchitectPanelMetric", "ArchitectPanel", "ArchitectGovernanceDashboard"]
@@ -0,0 +1,40 @@
"""Web UI model for Architect weekly research."""
from .architect_weekly_research_agent import ArchitectWeeklyResearchAgent
class ArchitectResearchDashboard:
def __init__(self, agent=None):
self.agent = agent or ArchitectWeeklyResearchAgent()
def overview(self):
return {
"module": "architect-weekly-research",
"route": "/admin/ai-evolution/architect/research",
"title": "架构师 Agent 技术情报中心",
"panels": [
{
"title": "Research Targets",
"data": self.agent.plan(),
},
{
"title": "Evaluation",
"fields": [
"relevance",
"impact",
"recommendation",
],
},
{
"title": "Version Decision",
"fields": [
"C",
"R",
"V",
],
},
],
}
__all__ = ["ArchitectResearchDashboard"]
@@ -0,0 +1,122 @@
"""Architect research timeline dashboard.
Shows weekly external research findings as a traceable architecture-impact flow:
research target -> finding -> impact -> recommendation -> version decision ->
governance event.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTimelineStep:
id: str
title: str
step_type: str
status: str
source: str
timestamp: str
impact: str = "none"
version_bump: str = "none"
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectResearchTimeline:
ORDER = {
"research_planned": 10,
"finding_created": 20,
"impact_analyzed": 30,
"recommendation_created": 40,
"version_decision_created": 50,
"architecture_review_event_created": 60,
}
def build(self, findings: list[dict[str, Any]] | None = None, decisions: list[dict[str, Any]] | None = None) -> dict[str, Any]:
findings = findings or []
decisions = decisions or []
steps: list[ResearchTimelineStep] = [
ResearchTimelineStep(
id="research-plan",
title="每周技术情报调研计划",
step_type="research_planned",
status="ok",
source="architect-agent",
timestamp=utc_now(),
detail={"cadence": "weekly"},
)
]
for idx, finding in enumerate(findings):
steps.append(
ResearchTimelineStep(
id=finding.get("id", f"finding-{idx}"),
title=finding.get("title", "research finding"),
step_type="finding_created",
status="ok",
source=finding.get("source", "unknown"),
timestamp=finding.get("created_at", utc_now()),
impact=finding.get("impact", "none"),
detail=finding,
)
)
steps.append(
ResearchTimelineStep(
id=f"impact-{finding.get('id', idx)}",
title="架构影响分析",
step_type="impact_analyzed",
status="attention_required" if finding.get("impact") in {"architecture", "breaking", "new_capability"} else "ok",
source="architect-agent",
timestamp=utc_now(),
impact=finding.get("impact", "none"),
detail={"relevance": finding.get("relevance"), "recommendation": finding.get("recommendation")},
)
)
for idx, decision in enumerate(decisions):
steps.append(
ResearchTimelineStep(
id=decision.get("id", f"decision-{idx}"),
title="C/R/V 版本建议",
step_type="version_decision_created",
status=decision.get("status", "ok"),
source="architect-agent",
timestamp=decision.get("created_at", utc_now()),
version_bump=decision.get("version_bump", "none"),
detail=decision,
)
)
steps.sort(key=lambda s: (self.ORDER.get(s.step_type, 999), s.timestamp))
return {
"module": "architect-research-timeline",
"route": "/admin/ai-evolution/architect/research/timeline",
"title": "架构师技术情报时间线",
"status": self._status(steps),
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"summary": self._summary(steps),
}
def _status(self, steps: list[ResearchTimelineStep]) -> str:
if any(step.status == "attention_required" for step in steps):
return "attention_required"
return "healthy"
def _summary(self, steps: list[ResearchTimelineStep]) -> dict[str, Any]:
return {
"findings": len([s for s in steps if s.step_type == "finding_created"]),
"architecture_impacts": len([s for s in steps if s.impact in {"architecture", "breaking", "new_capability"}]),
"version_decisions": len([s for s in steps if s.step_type == "version_decision_created"]),
"recommended_bumps": sorted({s.version_bump for s in steps if s.version_bump != "none"}),
}
__all__ = ["ResearchTimelineStep", "ArchitectResearchTimeline"]
@@ -0,0 +1,32 @@
"""API for architect research timeline."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_research_timeline import ArchitectResearchTimeline
timeline = ArchitectResearchTimeline()
def get_architect_research_timeline():
return timeline.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect/research", tags=["architect-research"])
@router.get("/timeline", operation_id="getArchitectResearchTimeline")
def get_timeline():
return get_architect_research_timeline()
return router
router = build_router()
__all__ = ["router", "get_architect_research_timeline"]
@@ -0,0 +1,113 @@
"""Weekly external research loop for Architect Governance Agent.
The agent plans and normalizes weekly research tasks. Actual internet access is
provided by the runtime/search worker; this module defines the audited workflow,
findings, impact analysis, and V/R/C recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTarget:
id: str
topic: str
source_type: str
query: str
cadence: str = "weekly"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchFinding:
id: str
target_id: str
title: str
summary: str
source: str
relevance: str
impact: str
recommendation: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchDecision:
status: str
version_bump: str
reason: str
findings: list[ResearchFinding]
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"created_at": self.created_at,
}
class ArchitectWeeklyResearchAgent:
DEFAULT_TARGETS = (
ResearchTarget("agent-frameworks", "Agent framework versions", "web", "latest multi-agent orchestration framework releases"),
ResearchTarget("model-ecosystem", "Model ecosystem changes", "web", "latest LLM model releases tool calling agent features"),
ResearchTarget("github-top-agentops", "GitHub top AgentOps implementations", "github", "top repositories agent observability multi agent orchestration"),
ResearchTarget("security-agent-risks", "Agent security changes", "web", "latest AI agent security sandbox prompt injection tool use risks"),
)
def plan(self) -> dict[str, Any]:
return {
"status": "planned",
"cadence": "weekly",
"targets": [target.to_dict() for target in self.DEFAULT_TARGETS],
"created_at": utc_now(),
}
def normalize_finding(
self,
target_id: str,
title: str,
summary: str,
source: str,
relevance: str = "medium",
impact: str = "none",
recommendation: str = "monitor",
) -> ResearchFinding:
return ResearchFinding(
id=f"finding_{uuid4().hex[:12]}",
target_id=target_id,
title=title,
summary=summary,
source=source,
relevance=relevance,
impact=impact,
recommendation=recommendation,
)
def decide(self, findings: list[ResearchFinding]) -> ResearchDecision:
if any(f.impact == "breaking" for f in findings):
return ResearchDecision("action_required", "V", "breaking external ecosystem change", findings)
if any(f.impact in {"architecture", "new_capability"} for f in findings):
return ResearchDecision("optimization_recommended", "R", "external capability may improve architecture", findings)
if any(f.impact in {"bugfix", "minor"} for f in findings):
return ResearchDecision("minor_change_recommended", "C", "minor compatible improvement", findings)
return ResearchDecision("no_action", "none", "no relevant external change", findings)
__all__ = ["ResearchTarget", "ResearchFinding", "ResearchDecision", "ArchitectWeeklyResearchAgent"]
@@ -0,0 +1,32 @@
"""API for architecture change governance dashboard."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architecture_change_dashboard import ArchitectureChangeDashboard
dashboard = ArchitectureChangeDashboard()
def get_architecture_change_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architecture-changes", tags=["architecture-changes"])
@router.get("/overview", operation_id="getArchitectureChangeOverview")
def overview():
return get_architecture_change_overview()
return router
router = build_router()
__all__ = ["router", "get_architecture_change_overview"]
@@ -0,0 +1,90 @@
"""Architecture change classification.
Small compatible architecture changes may be handled automatically by the
Architect Agent with audit evidence. Large, risky, production-impacting or
breaking architecture changes require explicit Owner approval before
implementation.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
@dataclass(frozen=True)
class ArchitectureChangeClassification:
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeClassifier:
"""Classifies architecture changes into small/medium/large governance paths."""
LARGE_FLAGS = {
"breaking_change",
"breaks_architecture",
"production_architecture_update",
"security_boundary_change",
"cross_tenant_effect",
"cross_user_data_effect",
"data_migration",
"permission_model_change",
"runtime_infrastructure_change",
}
def classify(self, change: dict[str, Any]) -> ArchitectureChangeClassification:
risk = str(change.get("risk", "medium")).lower()
version_bump = str(change.get("version_bump", change.get("recommended_version_bump", "C"))).upper()
scope = str(change.get("scope", "component")).lower()
reasons: list[str] = []
for flag in sorted(self.LARGE_FLAGS):
if change.get(flag):
reasons.append(flag)
if risk in {"high", "critical"}:
reasons.append(f"risk={risk}")
if version_bump == "V":
reasons.append("version_bump=V")
if version_bump == "R" and scope in {"system", "platform", "production", "cross_agent", "cross_tenant"}:
reasons.append(f"version_bump=R scope={scope}")
if reasons:
return ArchitectureChangeClassification(
level="large",
owner_required=True,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=reasons,
required_gates=["architect_review", "owner_architecture_approval", "test_gate", "promotion_gate"],
)
if risk == "low" and version_bump in {"C", "NONE"} and scope in {"component", "local", "documentation", "test"}:
return ArchitectureChangeClassification(
level="small",
owner_required=False,
auto_implement_allowed=True,
version_bump=version_bump,
reasons=["low_risk_compatible_change"],
required_gates=["architect_review", "audit", "test_gate"],
)
return ArchitectureChangeClassification(
level="medium",
owner_required=False,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=["requires_architect_review_before_implementation"],
required_gates=["architect_review", "test_gate", "audit"],
)
__all__ = ["ArchitectureChangeClassification", "ArchitectureChangeClassifier"]
@@ -0,0 +1,93 @@
"""Architecture change governance dashboard.
Displays architecture change classification, owner approval requirements,
implementation readiness, version recommendation and audit evidence for Web UI.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .architecture_change_classifier import ArchitectureChangeClassifier
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureChangeView:
change_id: str
title: str
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
status: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
owner_decision: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeDashboard:
def __init__(self, classifier: ArchitectureChangeClassifier | None = None):
self.classifier = classifier or ArchitectureChangeClassifier()
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
views = [self._view(change) for change in changes]
return {
"module": "architecture-change-governance",
"route": "/admin/ai-evolution/architecture-changes",
"title": "架构变更治理中心",
"status": self._status(views),
"generated_at": utc_now(),
"summary": self._summary(views),
"changes": [view.to_dict() for view in views],
}
def _view(self, change: dict[str, Any]) -> ArchitectureChangeView:
classification = self.classifier.classify(change)
owner_decision = change.get("owner_decision")
if classification.owner_required and owner_decision != "approved":
status = "waiting_owner_approval"
elif classification.auto_implement_allowed:
status = "auto_implementation_allowed"
else:
status = "ready_after_required_gates"
return ArchitectureChangeView(
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
level=classification.level,
owner_required=classification.owner_required,
auto_implement_allowed=classification.auto_implement_allowed,
version_bump=classification.version_bump,
status=status,
reasons=classification.reasons,
required_gates=classification.required_gates,
owner_decision=owner_decision,
)
def _status(self, views: list[ArchitectureChangeView]) -> str:
if any(view.status == "waiting_owner_approval" for view in views):
return "attention_required"
return "healthy"
def _summary(self, views: list[ArchitectureChangeView]) -> dict[str, Any]:
return {
"total": len(views),
"small": len([v for v in views if v.level == "small"]),
"medium": len([v for v in views if v.level == "medium"]),
"large": len([v for v in views if v.level == "large"]),
"waiting_owner_approval": len([v for v in views if v.status == "waiting_owner_approval"]),
"auto_allowed": len([v for v in views if v.auto_implement_allowed]),
"version_bumps": sorted({v.version_bump for v in views if v.version_bump and v.version_bump != "NONE"}),
}
__all__ = ["ArchitectureChangeView", "ArchitectureChangeDashboard"]
@@ -0,0 +1,89 @@
"""Owner approval gate for architecture changes.
Architecture changes may be proposed, reviewed and optimized by agents, but they
must not be implemented until owner approval is recorded. This gate sits before
implementation and before promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureOwnerApprovalRequest:
request_id: str
change_id: str
title: str
reason: str
risk: str
architect_decision: str
status: str = "pending_owner_approval"
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureOwnerDecision:
request_id: str
change_id: str
decision: str
decided_by: str
reason: str = ""
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureOwnerGate:
"""Blocks architecture implementation until owner explicitly approves."""
def create_request(self, change: dict[str, Any], architect_decision: dict[str, Any]) -> ArchitectureOwnerApprovalRequest:
return ArchitectureOwnerApprovalRequest(
request_id=f"ARCH-OWNER-{change.get('change_id', change.get('id', 'unknown'))}",
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
reason=change.get("reason", "architecture impact requires owner approval"),
risk=change.get("risk", "medium"),
architect_decision=architect_decision.get("status", "reviewed"),
)
def can_implement(self, request: dict[str, Any], decision: dict[str, Any] | None = None) -> dict[str, Any]:
if not decision:
return {
"allowed": False,
"status": "waiting_owner_approval",
"reason": "architecture change requires owner approval before implementation",
"request": request,
}
if decision.get("decision") == "approved":
return {
"allowed": True,
"status": "owner_approved",
"reason": decision.get("reason", "approved by owner"),
"request": request,
"decision": decision,
}
return {
"allowed": False,
"status": "owner_rejected",
"reason": decision.get("reason", "rejected by owner"),
"request": request,
"decision": decision,
}
__all__ = [
"ArchitectureOwnerApprovalRequest",
"ArchitectureOwnerDecision",
"ArchitectureOwnerGate",
]
@@ -0,0 +1,79 @@
"""HTTP handler and display data for architecture owner gate."""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any
from .architecture_owner_gate import ArchitectureOwnerGate, ArchitectureOwnerApprovalRequest
gate = ArchitectureOwnerGate()
# In-memory store for pending approval requests
_pending_requests: dict[str, dict[str, Any]] = {}
_decisions: dict[str, dict[str, Any]] = {}
def create_owner_approval_request(change: dict[str, Any], architect_decision: dict[str, Any]) -> dict[str, Any]:
req = gate.create_request(change, architect_decision)
d = asdict(req)
_pending_requests[req.change_id] = d
return d
def approve_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
decision = asdict(
ArchitectureOwnerApprovalRequest.__bases__[1].__new__(type("_ArchitectureOwnerDecision", (), {})) # noqa
)
try:
from .architecture_owner_gate import ArchitectureOwnerDecision
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="approved",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
# remove from pending
_pending_requests.pop(change_id, None)
return {"allowed": True, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def reject_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
from .architecture_owner_gate import ArchitectureOwnerDecision
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
try:
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="rejected",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
_pending_requests.pop(change_id, None)
return {"allowed": False, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def get_owner_gate_overview() -> dict[str, Any]:
return {
"pending_requests": list(_pending_requests.values()),
"decisions": list(_decisions.values()),
"pending_count": len(_pending_requests),
"approved_count": sum(1 for d in _decisions.values() if d.get("decision") == "approved"),
"rejected_count": sum(1 for d in _decisions.values() if d.get("decision") == "rejected"),
}
@@ -0,0 +1,40 @@
"""Independent architecture optimization agent."""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureReview:
agent: str
decision: str
findings: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class ArchitectureReviewAgent:
"""Reviews architecture changes from system-wide optimal perspective."""
def review(self, proposal: dict) -> ArchitectureReview:
findings = [
"dependency_impact_checked",
"security_boundary_checked",
"scalability_checked",
"operability_checked",
"rollback_strategy_checked",
]
return ArchitectureReview(
"architecture-optimization-agent",
"pass" if proposal.get("architecture_change") else "not_required",
findings,
)
__all__ = ["ArchitectureReviewAgent", "ArchitectureReview"]
@@ -0,0 +1,103 @@
"""Change acceptance governance for AIOS.
Every feature change must provide matching tests. Every architecture change must
be reviewed by an independent architecture agent. User-facing changes must be
validated by an independent user acceptance agent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ChangeDescriptor:
change_id: str
change_type: str
title: str
files_changed: list[str]
tests: list[str] = field(default_factory=list)
user_facing: bool = False
architecture_impact: bool = False
risk: str = "low"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceFinding:
check: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceDecision:
status: str
findings: list[AcceptanceFinding]
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": [f.to_dict() for f in self.findings],
"decided_at": self.decided_at,
}
class ChangeAcceptanceGate:
def evaluate(self, change: ChangeDescriptor) -> AcceptanceDecision:
findings: list[AcceptanceFinding] = []
findings.append(self._tests_required(change))
findings.append(self._user_acceptance_required(change))
findings.append(self._architecture_review_required(change))
findings.append(self._risk_gate(change))
status = "pass" if all(f.passed() for f in findings) else "blocked"
return AcceptanceDecision(status, findings)
def _tests_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.tests:
return AcceptanceFinding("tests_present", "pass", "matching tests declared")
return AcceptanceFinding("tests_present", "fail", "every feature change requires tests")
def _user_acceptance_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.user_facing:
return AcceptanceFinding("user_acceptance", "pass", "not user-facing")
has_user_test = any("user" in t.lower() or "acceptance" in t.lower() for t in change.tests)
return AcceptanceFinding(
"user_acceptance",
"pass" if has_user_test else "fail",
"user-facing change requires user acceptance test",
)
def _architecture_review_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.architecture_impact:
return AcceptanceFinding("architecture_review", "pass", "no architecture impact")
has_arch_test = any("arch" in t.lower() or "architecture" in t.lower() for t in change.tests)
return AcceptanceFinding(
"architecture_review",
"pass" if has_arch_test else "fail",
"architecture change requires independent architect review artifact/test",
)
def _risk_gate(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.risk in {"low", "medium", "high"}:
return AcceptanceFinding("risk_classified", "pass", f"risk={change.risk}")
return AcceptanceFinding("risk_classified", "fail", "risk must be low/medium/high")
__all__ = ["ChangeDescriptor", "AcceptanceFinding", "AcceptanceDecision", "ChangeAcceptanceGate"]
@@ -0,0 +1,34 @@
"""API for AIOS change governance dashboard."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .change_governance_dashboard import ChangeGovernanceDashboard
dashboard = ChangeGovernanceDashboard()
def get_change_governance_overview(changes: list[dict[str, Any]] | None = None):
return dashboard.overview(changes)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["change-governance"])
@router.get("/overview", operation_id="getChangeGovernanceOverview")
def overview():
return get_change_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_change_governance_overview"]
@@ -0,0 +1,93 @@
"""Web UI dashboard model for AIOS change governance.
Shows test gates, user acceptance, architecture review, risk gates and release
blocking status for human operators and owner review.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GovernanceMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernancePanel:
key: str
title: str
metrics: list[GovernanceMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class ChangeGovernanceDashboard:
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
blocked = [c for c in changes if c.get("status") == "blocked"]
waiting_arch = [c for c in changes if c.get("architecture_review") == "required"]
waiting_user = [c for c in changes if c.get("user_acceptance") == "required"]
missing_tests = [c for c in changes if not c.get("tests")]
panels = [
GovernancePanel(
"test_gate",
"测试 Gate",
[
GovernanceMetric("total_changes", "变更总数", len(changes)),
GovernanceMetric("missing_tests", "缺少测试的变更", len(missing_tests), "warn" if missing_tests else "ok"),
],
),
GovernancePanel(
"user_acceptance",
"用户验收 Agent",
[
GovernanceMetric("waiting_user_acceptance", "等待用户验收", len(waiting_user), "warn" if waiting_user else "ok"),
],
),
GovernancePanel(
"architecture_review",
"架构师 Agent",
[
GovernanceMetric("waiting_architecture_review", "等待架构审查", len(waiting_arch), "warn" if waiting_arch else "ok"),
],
),
GovernancePanel(
"release_gate",
"发布 Gate",
[
GovernanceMetric("blocked_changes", "阻塞发布的变更", len(blocked), "warn" if blocked else "ok"),
],
),
]
return {
"module": "change-governance",
"route": "/admin/ai-evolution/governance",
"title": "AIOS 变更治理中心",
"status": "attention_required" if blocked or missing_tests or waiting_arch or waiting_user else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"changes": changes,
}
__all__ = ["GovernanceMetric", "GovernancePanel", "ChangeGovernanceDashboard"]
+36
View File
@@ -0,0 +1,36 @@
"""Git repository location resolver with validation.
Prevents hard-coded repository names from silently pointing to stale paths.
"""
from __future__ import annotations
import os
from pathlib import Path
class GitRepoLocator:
def __init__(self, root: str | None = None):
self.root = Path(root or os.getenv("AIOS_GITOPS_ROOT", "/data/aios-gitops/runtime/gitea-bin/data/repositories"))
def candidates(self, owner: str = "aios", names: tuple[str, ...] = ("aios.git", "aios-v23.git")):
return [self.root / owner / name for name in names]
def locate(self, owner: str = "aios") -> dict:
found = [str(path) for path in self.candidates(owner) if path.exists()]
return {
"root": str(self.root),
"owner": owner,
"found": found,
"healthy": len(found) == 1,
"warning": "multiple repositories found" if len(found) > 1 else None,
}
def require(self, owner: str = "aios") -> Path:
result = self.locate(owner)
if not result["healthy"]:
raise RuntimeError(f"Git repo resolution failed: {result}")
return Path(result["found"][0])
__all__ = ["GitRepoLocator"]
@@ -0,0 +1,148 @@
"""Bridge governance agents into the unified Agent Event Bus.
This module emits normalized governance events for requirement changes,
architecture reviews, user acceptance, test gates and version decisions. It uses
duck typing so it can work with the unified AgentEventBus when available, while
remaining testable without a running bus.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class EventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class GovernanceEventEnvelope:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryGovernanceEventBus:
"""Small fallback bus for tests and offline governance runs."""
def __init__(self) -> None:
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = GovernanceEventEnvelope(
id=f"evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class GovernanceEventBridge:
"""Publishes governance lifecycle events to the Agent Event Bus."""
def __init__(self, bus: EventPublisher | None = None):
self.bus = bus or InMemoryGovernanceEventBus()
def requirement_changed(self, requirement: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="requirement_changed",
source=requirement.get("source", "unknown"),
target="architect-agent",
summary=requirement.get("requirement", "requirement changed"),
payload=requirement,
correlation_id=requirement.get("id"),
)
def architecture_review_started(self, proposal: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_started",
source="architect-agent",
target="architecture-review-agent",
summary=proposal.get("title", "architecture review started"),
payload=proposal,
correlation_id=proposal.get("change_id"),
)
def architecture_review_completed(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_completed",
source="architecture-review-agent",
target="change-governance",
summary=f"architecture review {decision.get('status', 'completed')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
def user_acceptance_completed(self, report: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="user_acceptance_completed",
source="user-acceptance-agent",
target="change-governance",
summary=f"user acceptance {report.get('decision', 'completed')}",
payload=report,
correlation_id=report.get("change_id"),
)
def test_gate_completed(self, result: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="test_gate_completed",
source="test-gate",
target="change-governance",
summary=f"test gate {result.get('status', 'completed')}",
payload=result,
correlation_id=result.get("change_id"),
)
def version_decision_created(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="version_decision_created",
source="architect-agent",
target="owner-gate",
summary=f"version bump recommendation: {decision.get('version_bump', 'unknown')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
__all__ = [
"GovernanceEventEnvelope",
"InMemoryGovernanceEventBus",
"GovernanceEventBridge",
]
@@ -0,0 +1,32 @@
"""API for governance timeline visualization."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .governance_timeline_dashboard import GovernanceTimelineDashboard
dashboard = GovernanceTimelineDashboard()
def get_governance_timeline(events=None):
return dashboard.build(events)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["governance-timeline"])
@router.get("/timeline", operation_id="getGovernanceTimeline")
def timeline():
return get_governance_timeline()
return router
router = build_router()
__all__ = ["router", "get_governance_timeline"]
@@ -0,0 +1,106 @@
"""Governance timeline dashboard.
Turns governance events into a human-readable flow for Web UI so operators can
see why a change is blocked or ready for promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
EVENT_ORDER = {
"requirement_changed": 10,
"architecture_review_started": 20,
"architecture_review_completed": 30,
"test_gate_completed": 40,
"user_acceptance_completed": 50,
"version_decision_created": 60,
"owner_gate_completed": 70,
"promotion_completed": 80,
}
@dataclass(frozen=True)
class GovernanceTimelineStep:
id: str
kind: str
title: str
actor: str
status: str
timestamp: str
correlation_id: str | None = None
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GovernanceTimelineDashboard:
def build(self, events: list[dict[str, Any]] | None = None) -> dict[str, Any]:
events = events or []
steps = [self._event_to_step(event) for event in events]
steps.sort(key=lambda step: (EVENT_ORDER.get(step.kind, 999), step.timestamp))
blockers = [step for step in steps if step.status in {"blocked", "fail", "attention_required"}]
return {
"module": "governance-timeline",
"route": "/admin/ai-evolution/governance/timeline",
"title": "治理事件时间线",
"status": "attention_required" if blockers else "healthy",
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"blockers": [step.to_dict() for step in blockers],
"flow": self._flow_summary(steps),
}
def _event_to_step(self, event: dict[str, Any]) -> GovernanceTimelineStep:
payload = event.get("payload", {})
kind = event.get("kind", "unknown")
return GovernanceTimelineStep(
id=event.get("id", f"event-{len(str(event))}"),
kind=kind,
title=self._title(kind, payload),
actor=event.get("source", payload.get("actor", "unknown")),
status=payload.get("status") or payload.get("decision") or event.get("status", "ok"),
timestamp=event.get("created_at") or payload.get("created_at") or utc_now(),
correlation_id=event.get("correlation_id") or payload.get("change_id") or payload.get("id"),
detail=payload,
)
def _title(self, kind: str, payload: dict[str, Any]) -> str:
titles = {
"requirement_changed": "需求变化",
"architecture_review_started": "架构审查开始",
"architecture_review_completed": "架构审查完成",
"test_gate_completed": "测试 Gate 完成",
"user_acceptance_completed": "用户验收完成",
"version_decision_created": "版本建议产生",
"owner_gate_completed": "Owner Gate 完成",
"promotion_completed": "Promotion 完成",
}
return payload.get("title") or payload.get("summary") or titles.get(kind, kind)
def _flow_summary(self, steps: list[GovernanceTimelineStep]) -> dict[str, Any]:
present = {step.kind for step in steps}
required = [
"requirement_changed",
"architecture_review_completed",
"test_gate_completed",
"user_acceptance_completed",
"version_decision_created",
]
missing = [kind for kind in required if kind not in present]
return {
"required_steps": required,
"missing_steps": missing,
"ready_for_owner_gate": not missing,
}
__all__ = ["GovernanceTimelineStep", "GovernanceTimelineDashboard"]
@@ -0,0 +1,59 @@
"""Independent acceptance agents.
Simulates independent roles: user representative and architecture reviewer.
They provide acceptance evidence, not implementation decisions.
"""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AcceptanceReport:
agent: str
role: str
decision: str
evidence: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class UserAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"user_requirement_match",
"user_workflow_validation",
"permission_visibility_check",
]
return AcceptanceReport(
"user-acceptance-agent",
"user_representative",
"pass" if change.get("user_facing") else "not_required",
checks,
)
class ArchitectureAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"architecture_consistency",
"scalability_review",
"security_boundary_review",
"operability_review",
]
return AcceptanceReport(
"architecture-review-agent",
"system_architect",
"pass" if change.get("architecture_impact") else "not_required",
checks,
)
__all__ = ["AcceptanceReport", "UserAcceptanceAgent", "ArchitectureAcceptanceAgent"]
@@ -58,6 +58,8 @@ pre { white-space: pre-wrap; word-break: break-word; }
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
</ul>
</div>
<div class="main">
@@ -90,6 +92,14 @@ pre { white-space: pre-wrap; word-break: break-word; }
</div>
<div id="panel-agents" class="panel" style="display:none;">
<div class="main-header"><h2>Recent Agent Executions</h2></div>
<div id="panel-arch-owner" class="panel" style="display:none;">
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
<div id="arch-owner-content">Loading...</div>
</div>
<div id="panel-arch-approve" class="panel" style="display:none;">
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
<div id="arch-approve-content">Loading...</div>
</div>
<div id="agents-list">Loading...</div>
</div>
</div>
@@ -97,3 +107,159 @@ pre { white-space: pre-wrap; word-break: break-word; }
<script>
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('content').style.display = '';
const hs = document.getElementById('header-status');
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
document.getElementById('base').textContent = data.baseline || '-';
document.getElementById('branch').textContent = data.main_branch || 'main';
document.getElementById('uptime').textContent = data.uptime || '-';
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
}
async function loadVersions() {
const el = document.getElementById('versions-list');
el.innerHTML = '<div class="loading">Loading versions...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/versions');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '<div class="grid">';
for (const v of data.versions || []) {
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
}
}
async function loadCommands() {
const el = document.getElementById('commands-list');
el.innerHTML = '<div class="loading">Loading commands...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
let cmds = data.commands || [];
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
html += '<div><div class="card"><div class="card-title">By Category</div>';
const cats = {};
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
html += '</div></div></div>';
for (const cmd of cmds.slice().reverse()) {
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
}
}
async function loadAgents() {
const el = document.getElementById('agents-list');
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/agents');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '';
for (const agent of data.agents || []) {
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
}
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
} catch(e) {
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
}
}
function showPanel(name) {
async function loadOwnerGate() {
const el = document.getElementById('arch-owner-content');
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '<div class="row">';
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
html += '</div>';
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
if (data.pending_requests && data.pending_requests.length) {
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
html += '</div>';
}
} else {
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
async function loadApprove() {
const el = document.getElementById('arch-approve-content');
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '';
if (data.pending_requests && data.pending_requests.length) {
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
html += '<div style="display:flex;gap:8px;">';
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
html += '</div></div>';
}
html += '</div>';
} else {
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
document.getElementById('panel-' + name).style.display = '';
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
if (name === 'overview') {
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
fetchData();
}
const loaders = {
'arch-owner': loadOwnerGate,
'arch-approve': loadApprove,
versions: loadVersions,
commands: loadCommands,
agents: loadAgents,
};
const fn = loaders[name];
if (fn) fn();
}
fetchData();
</script>
</body>
</html>
@@ -0,0 +1,36 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -0,0 +1,35 @@
# CMD-20260709-enable-autonomous-controller
## Command ID
CMD-20260709-enable-autonomous-controller
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
Enable autonomous controller for AIOS GitOps:
- Auto-detect issues
- Auto-diagnose
- Auto-generate fixes
- Auto-test
- Auto-create patches
- Auto-request approval
## Preconditions
- Agent Command Plane initialized (CMD-BOOTSTRAP-001)
- Git Actions write permission fixed (CMD-20260708)
- Audit plane enabled (CMD-20260708)
- Notification service enabled (CMD-20260708)
## Completion Criteria
- Autonomous controller service running
- Can detect, diagnose, fix, and report
- Pipeline: detect → diagnose → fix → test → patch → approve → commit → notify
@@ -0,0 +1,7 @@
{
"nid": "n-1783527185",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:13:05+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783529900",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:20+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783529914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783530514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:08:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783531114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:18:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783531714",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:28:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783532314",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:38:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783532914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:48:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783533514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:58:34+0800"
}
@@ -0,0 +1,7 @@
{
"nid": "n-1783534114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T02:08:34+0800"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783527185",
"created": "2026-07-09T00:13:05+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"test/alias-write-verify",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/gitops-write-auth-smoke-20260706T115230Z",
"patch/gpt-actions-auth-live-20260708"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783529900",
"created": "2026-07-09T00:58:20+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"test/alias-test-file-branch",
"patch/codex-online-sync-acceptance-20260706T102819Z",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/actions-invalid-diag-smoke-20260706T140311Z",
"patch/V1R2C2-autonomous-recovery-loop"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783529914",
"created": "2026-07-09T00:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783530514",
"created": "2026-07-09T01:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783531114",
"created": "2026-07-09T01:18:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783531714",
"created": "2026-07-09T01:28:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783532314",
"created": "2026-07-09T01:38:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783532914",
"created": "2026-07-09T01:48:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783533514",
"created": "2026-07-09T01:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783534114",
"created": "2026-07-09T02:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,12 @@
{
"ts": "2026-07-09T02:08:34+0800",
"ok": false,
"checks": {
"gitea": false,
"api": true,
"branch_sync": false,
"stuck": true
},
"plans": 1,
"applied": 0
}
+11
View File
@@ -0,0 +1,11 @@
{
"ts": "2026-07-09T00:00:00+09:00",
"ok": true,
"checks": {
"git_object_permission": true,
"api": true,
"branch_sync": true,
"stale_runtime_state_cleaned": true
},
"note": "Updated during PR #2 cleanup before merge"
}
@@ -0,0 +1,21 @@
{
"ts": "2026-07-09T00:59:30+0800",
"action": "command_watcher_state_clean",
"description": "清除了 command watcher 目录中的 stale 重复副本",
"result": "clean",
"stale_removed": [
{
"name": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"removed_from": ["active/", "running/"],
"preserved_in": "completed/",
"archive_path": "agents/commands/archive/"
},
{
"name": "CMD-20260709-enable-autonomous-controller.md",
"removed_from": ["running/"],
"preserved_in": "active/",
"archive_path": "agents/commands/archive/"
}
],
"note": "已完成命令保留在 completed/,副本移动到 archive/;未完成命令仅保留 active/"
}
@@ -0,0 +1,17 @@
{
"ts": "2026-07-09T01:00:00+0800",
"action": "daily_wechat_cron_verification",
"description": "验证每日 08:00 WeChat 汇报 cron 配置并手动触发 dry-run",
"cron_name": "AIOS每日自治报告",
"cron_id": "987f8619-6ccc-4370-8720-87149479a222",
"schedule": "0 8 * * * Asia/Shanghai",
"delivery": {
"mode": "announce",
"channel": "openclaw-weixin",
"to": "o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat",
"accountId": "ed3da13663c0-im-bot"
},
"dry_run_triggered": true,
"dry_run_result": "enqueued",
"status": "verified"
}
+19
View File
@@ -0,0 +1,19 @@
{
"run_id": "c-1783527185",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:13:05+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 184.6
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 185.8
}
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"run_id": "c-1783529900",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:20+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 229.8
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.0
}
]
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"run_id": "c-1783529914",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:34+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 230.1
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.3
}
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783530514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783531114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:18:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783531714",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:28:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783532314",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:38:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783532914",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:48:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783533514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:58:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783534114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T02:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+13 -1
View File
@@ -1 +1,13 @@
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
{
"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
}
}
@@ -0,0 +1,33 @@
architect_agent_governance:
enabled: true
responsibilities:
requirement_change_tracking: true
requirement_document_refresh: true
architecture_document_refresh: true
deployment_document_validation: true
test_case_document_generation: true
architecture_optimization_review: true
weekly_external_research: true
github_top_implementation_review: true
sources:
requirement_sources:
- custom_gpt
- codex
- openclaw
- hermes
- user_feedback
version_decision:
criteria:
C: bug_fix_or_small_compatible_change
R: architecture_or_capability_release
V: major_architecture_or_breaking_change
research_cycle:
frequency: weekly
targets:
- agent_framework_versions
- model_versions
- github_top_implementations
- security_changes
governance:
owner_gate_required: true
audit_required: true
+22
View File
@@ -0,0 +1,22 @@
architect_research:
enabled: true
schedule:
frequency: weekly
research_sources:
- agent_framework_releases
- model_ecosystem_updates
- github_top_implementations
- security_advisories
evaluation:
required:
- relevance
- architecture_impact
- security_impact
- migration_cost
output:
- optimization_recommendation
- version_recommendation
- architecture_review_event
governance:
owner_gate_required: true
audit_required: true
@@ -0,0 +1,36 @@
architecture_owner_gate:
enabled: true
principle: owner_approves_large_architecture_changes_only
classification:
small:
owner_required: false
architect_auto_implementation: true
requirements:
- architect_review
- test_gate
- audit
medium:
owner_required: false
architect_review_required: true
implementation_after_review: true
requirements:
- architect_review
- test_gate
- audit
large:
owner_required: true
architect_auto_implementation: false
requirements:
- architect_review
- owner_architecture_approval
- test_gate
- promotion_gate
mandatory_owner_cases:
- breaking_change
- V_version_change
- production_architecture_update
- security_boundary_change
- cross_tenant_change
- permission_model_change
audit:
required: true
+18
View File
@@ -0,0 +1,18 @@
change_governance:
enabled: true
feature_change:
require_tests: true
require_user_acceptance_if_user_facing: true
architecture_change:
require_architecture_review_agent: true
require_system_optimization_review: true
acceptance_agents:
user_agent: user-acceptance-agent
architect_agent: architecture-review-agent
merge_gate:
required:
- tests_passed
- acceptance_passed
- architecture_review_passed
audit:
enabled: true
+22
View File
@@ -0,0 +1,22 @@
governance_event_bus:
enabled: true
event_types:
- requirement_changed
- architecture_review_started
- architecture_review_completed
- user_acceptance_completed
- test_gate_completed
- version_decision_created
producers:
- architect-agent
- user-acceptance-agent
- test-gate-agent
- change-governance-agent
consumers:
- web-ui
- audit-plane
- owner-gate
- reporting-agent
audit:
required: true
retention: versioned
+21
View File
@@ -0,0 +1,21 @@
models:
default:
- id: gpt-5.5
provider: openai
enabled: true
fallback: false
- id: qwen-3.6-35b
provider: qwen
enabled: true
fallback: true
- id: deepseek-v4-flash
provider: deepseek
enabled: true
fallback: true
runtime_behavior:
unavailable_model:
action: warn_and_fallback
hardcoded_model_list: false
health_check_required: true
audit_model_selection: true
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
printf 'AIOS V2R1C0 RC1 health check\n'
python3 - <<'PY'
from pathlib import Path
required = [
'active-runtime/app/aios_v2r1/tenant_context.py',
'active-runtime/app/aios_v2r1/router.py',
'active-runtime/app/aios_v2r1/hermes_fabric.py',
'openclaw/plugins/aios-tenant-boundary/openclaw.plugin.json',
]
missing=[p for p in required if not Path(p).exists()]
if missing:
raise SystemExit('missing: '+','.join(missing))
print('artifact presence PASS')
PY
echo 'runtime health requires OpenClaw environment probe'
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
EXPECTED_BRANCH="release/v2r1c0-tenant-cell-fabric-20260712"
if [[ "$(git -C "$ROOT" branch --show-current)" != "$EXPECTED_BRANCH" ]]; then
echo "wrong release branch"
exit 1
fi
COMMIT=$(git -C "$ROOT" rev-parse HEAD)
echo "Installing AIOS V2R1C0 RC1 commit=$COMMIT"
python3 - <<'PY'
import compileall
if not compileall.compile_dir('active-runtime/app/aios_v2r1', quiet=1):
raise SystemExit(1)
print('python compile PASS')
PY
if [[ -f openclaw/plugins/aios-tenant-boundary/package.json ]]; then
echo "plugin package PASS"
else
echo "plugin package missing"
exit 1
fi
echo "Installation preflight PASS"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
MODE=${1:---check}
case "$MODE" in
--check)
echo "V2R1C0 RC1 validation mode"
;;
--install)
echo "Install requires approved OpenClaw Cell target"
;;
--rollback)
echo "Rollback requires recorded Cell backup"
;;
*)
echo "Usage: $0 [--check|--install|--rollback]"
exit 2
;;
esac
python3 - <<'PY'
import compileall
raise SystemExit(0 if compileall.compile_dir('active-runtime/app/aios_v2r1', quiet=1) else 1)
PY
echo "python runtime syntax: PASS"
@@ -0,0 +1,26 @@
# AIOS V1R5 to V2R1 Migration Plan
## Goal
Move from shared Gateway assumptions to Tenant Cell isolation.
## Order
1. Establish Tenant Registry and signed Tenant Context.
2. Implement Tenant Router.
3. Implement OpenClaw Cell lifecycle management.
4. Introduce canonical message envelope.
5. Add provenance and execution policy enforcement.
6. Migrate Hermes into shared fabric with isolated profiles.
7. Migrate memory, secrets and audit storage.
## Do Not
- Do not modify Hermes first.
- Do not share production credentials across tenants.
- Do not treat workspace separation as a sandbox.
- Do not allow unknown message origins to execute tools.
## Acceptance
Each migration stage requires automated tests and runtime evidence before promotion.
@@ -0,0 +1,49 @@
# AIOS V2R1 OpenClaw Tenant Cell + Hermes Shared Agent Fabric Architecture
## Status
Release train: V2R1C0
Purpose: replace shared runtime trust assumptions with tenant-isolated Cell architecture.
## Principles
1. OpenClaw Gateway is a trust boundary.
2. Each production tenant receives an isolated Cell.
3. Hermes provides shared scheduling/fabric capability but not shared tenant state.
4. Tenant identity, origin, session and task scope are mandatory in every event.
5. Unknown provenance fails closed.
## Target Components
- Tenant Router
- OpenClaw Cell Manager
- Canonical Message Envelope
- Tenant Inbox/Outbox Ledger
- Secret Scope Manager
- Memory Namespace Manager
- Hermes Profile Worker Fabric
- Sandbox Execution Layer
- Audit Event Pipeline
## Migration Order
Phase 1: Tenant Router + Cell Manager
Phase 2: Canonical Message and Policy Layer
Phase 3: Hermes Shared Agent Fabric
Phase 4: Memory, Secret and Audit isolation
Phase 5: Production tenant migration
## Release Gate
A V2R1 production release requires:
- tenant isolation tests passed
- message provenance tests passed
- runtime commit matches release commit
- OpenClaw plugin runtime inspection passed
- rollback verified
+212
View File
@@ -0,0 +1,212 @@
# V1R4C5 Acceptance Evidence
Date: 2026-07-11
Status: release candidate evidence in progress
Current active baseline: V1R4C3
V1R4C5 has not been promoted yet.
## Owner Decision
Owner approved continued V1R4C5 work under the following constraints:
```text
允许无 force-push 的分支 reconcile
memory search 保持关闭
先完成 SR-01/SR-02/数据隔离验收
再进入 merge/deploy/tag owner gate
```
## Scope
V1R4C5 release candidate scope:
1. SR-01 Gitea reliability and branch consistency guardrails.
2. SR-02 watcher/stuck command cleanup.
3. User data isolation redline and enforcement gates.
4. SR-04 admin workbench read-only visibility preparation only.
Explicitly excluded from V1R4C5 activation:
- memory search activation;
- full SR-04 portal production rollout;
- bulk merge of old test/smoke/auth branches;
- force push reconciliation;
- Gitea DB mutation without owner/admin approval.
## Runtime Acceptance Evidence
### Runtime status
Observed:
```json
{
"service": "aios-gitops-model-api",
"status": "active",
"workspace_branch": "patch/v1r4c5-runtime-acceptance-clean-20260711-01"
}
```
### Baseline
Observed:
```json
{
"current_baseline": "V1R4C3",
"previous_baseline": "V1R3C1",
"promoted_by": "openclaw"
}
```
### Clean createBranch acceptance
Branch:
```text
test/v1r4c5-runtime-acceptance-clean-20260711-01
```
Result:
```text
status: ok
commit: 6ae7058
sync: synced
```
### Clean createPatch acceptance
Patch:
```text
patch/v1r4c5-runtime-acceptance-clean-20260711-01
```
Patch id:
```text
patch-1783706123789-f5ca8b69
```
Commit:
```text
ba0391e
```
Result:
```text
synced
```
## SR-01 Evidence
Known accepted evidence:
- Gitea branch consistency sentinel deployed.
- Sentinel reports:
```json
{
"status": "healthy",
"unhealthy_checks": []
}
```
- systemd timer runs every 5 minutes.
- service runs with:
```text
--emit-safe-mode --json
```
- broken symlink issue resolved by replacing the external reliability symlink with workspace-local files.
Required before final promotion:
- confirm latest sentinel report remains healthy after acceptance writes.
## SR-02 Evidence
Known accepted evidence:
```text
11 / 11 tests pass
```
Fix summary:
- stuck command detection reads parsed timestamps through `data.get('ts', 0)`;
- `.md` fixture mtime is aligned with stored stuck timestamp via `os.utime(path, (ts, ts))`;
- behavior is consistent across `.json` and `.md` state formats.
Required before final promotion:
- re-run SR-02 tests on the final integration candidate.
## User Data Isolation Evidence
Implemented controls:
- user data isolation redline;
- memory scope guard;
- provider gate;
- activation gate;
- audit service/timer;
- CI acceptance scripts;
- memory search activation check.
Required safety state:
```text
AIOS_MEMORY_ENABLED=false
```
Required before memory/search activation:
- isolation audit healthy;
- no denied records;
- no invalid records;
- provider filters by scope before ranking;
- provenance required;
- global index disabled;
- activation gate returns allowed;
- explicit owner approval.
V1R4C5 does not approve memory search activation.
## SR-04 Admin Workbench Evidence
Current user-visible gap is confirmed:
- administrator cannot yet see task progress;
- administrator cannot yet see flow/process diagrams;
- SR-04 remains activation-ready but gated.
V1R4C5 scope allows only read-only visibility preparation and acceptance design, not full production rollout.
Required before SR-04 deployment:
- SR-01 green;
- SR-02 green;
- user data isolation gate accepted;
- admin RBAC read-only check green.
## Remaining Required Gates Before Promotion
- [ ] Latest Gitea sentinel report remains healthy.
- [ ] SR-02 watcher tests pass on final candidate.
- [ ] User data isolation acceptance passes.
- [ ] Admin workbench read-only RBAC acceptance passes or remains explicitly deferred.
- [ ] No force-push was used.
- [ ] Memory search remains disabled.
- [ ] Owner approves merge/deploy/tag/promotion.
## Promotion Rule
V1R4C5 may not be promoted until all required gates are green or explicitly deferred with owner approval.
@@ -0,0 +1,54 @@
# V1R4C5 Final Owner Approval
Date: 2026-07-11
Status: approved for final promotion
## Owner Decision
Owner approved final V1R4C5 promotion.
Memory search must remain disabled.
```text
允许最终晋升
继续保持 memory search 关闭
```
## Promotion Scope
Approved for V1R4C5:
1. SR-01 Gitea reliability and branch consistency guardrails.
2. SR-02 watcher/stuck command cleanup.
3. User data isolation redline and enforcement gates.
4. SR-04 admin workbench read-only visibility preparation only.
## Explicit Non-Activation
The following are not approved by this promotion:
```text
memory search activation
full cross-session memory recall
global vector index
full SR-04 production portal rollout beyond read-only/admin visibility preparation
bulk merge of old test/smoke/auth branches
force-push based reconciliation
```
## Required Runtime State
Memory remains disabled:
```text
AIOS_MEMORY_ENABLED=false
```
If a provider or deployment attempts to enable memory/search, it must be blocked unless a future owner approval explicitly allows it after the user-data isolation activation gate returns allowed.
## Final Promotion Rule
V1R4C5 may proceed to tag/promotion under owner gate.
If any final gate fails, promotion must stop and memory/search must remain disabled.
+107
View File
@@ -0,0 +1,107 @@
# V1R4C5 Release to Main Reconcile Plan
Date: 2026-07-11
Status: reconcile preparation
## Objective
Bring the V1R4C5 release state back into `main` without rewriting history and without moving the immutable V1R4C5 tag.
## Source of Truth
Release baseline:
```text
V1R4C5
```
Promotion commit:
```text
a32c76a3c1baa49562e53fb8adce10a5ed0e6704
```
Tag:
```text
V1R4C5
```
Tag policy:
```text
immutable
```
## Reconcile Rules
Allowed:
- normal merge commits;
- fast-forward where possible;
- conflict resolution with review;
- updating main to contain released C5 state.
Forbidden:
- force-push;
- tag movement;
- rewriting release history;
- importing V1R4C6 feature code into this reconcile.
## Expected Included C5 Content
- SR-01 Gitea reliability guardrails.
- Gitea branch consistency sentinel.
- SR-02 watcher state cleanup.
- User Data Isolation gate.
- Reliability governance documents.
- Admin Workbench read-only preparation records.
## Expected Excluded Content
Do not include:
- V1R4C6 Admin Workbench implementation code.
- SR-03 Search Gateway implementation.
- Agent Platform integration.
- Memory search activation.
- Global vector index.
## Validation Before Main Merge
Required:
```text
getBaseline.current_baseline == V1R4C5
V1R4C5 tag unchanged
memory search disabled
Gitea sentinel healthy
SR-02 tests pass
main merge does not include V1R4C6 branches
```
## Post Merge Validation
Run:
```text
getRuntimeStatus
getBaseline
createBranch
createPatch
```
Expected:
```text
main contains V1R4C5 release state
active baseline remains V1R4C5
```
## Owner Gate
Normal merge to `main` may proceed after validation.
No tag creation or baseline promotion is required because V1R4C5 is already promoted.
@@ -0,0 +1,137 @@
# V1R4C6 Acceptance Checklist
Date: 2026-07-11
Base baseline: V1R4C5
Promotion blocked until every mandatory gate is green.
## Global Preflight
- [ ] `getRuntimeStatus.status == active`
- [ ] `getBaseline.current_baseline == V1R4C5`
- [ ] `getBaseline.git == a32c76a3c1baa49562e53fb8adce10a5ed0e6704`
- [ ] `AIOS_GITOPS_BARE_REPO` points to `/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git`
- [ ] Gitea consistency sentinel reports `healthy`
- [ ] `AIOS_MEMORY_ENABLED=false`
- [ ] no force-push used
## Gate A — Admin Workbench / Portal AIOS
### Route/API
- [ ] `/portal/me/aios/manifest` returns authenticated module manifest
- [ ] `/portal/me/aios/dashboard` returns admin-authorized dashboard data
- [ ] `/portal/me/aios/health` returns system health data
- [ ] failed/empty backend data produces explicit empty/error state
### UI
- [ ] admin sees task progress
- [ ] admin sees process/flow diagram entry
- [ ] admin sees system health
- [ ] admin sees authorized agent observability
- [ ] user does not see admin-only modules
- [ ] admin does not see owner-only modules without owner role
### Isolation
- [ ] no cross-user data
- [ ] no cross-project data
- [ ] no cross-session data
- [ ] no cross-tenant/group data
Gate A result: `PASS / FAIL / DEFER`
## Gate B — SR-03 Search Gateway
### Provider Binding
- [ ] real provider configured and reachable
- [ ] provider errors return safe non-memory fallback
- [ ] timeout/retry/circuit-breaker behavior verified
### Scope and Provenance
- [ ] scope filter occurs before ranking
- [ ] provenance returned for accepted results
- [ ] denied/out-of-scope content never reaches ranking or summary
- [ ] global memory/vector index disabled
- [ ] memory search remains disabled
### Audit
- [ ] search request scope is logged without leaking content
- [ ] provider/fallback decision is auditable
- [ ] denied scope mismatch is auditable
Gate B result: `PASS / FAIL / DEFER`
## Gate C — Agent Platform
### Event Bus
- [ ] event envelope includes tenant/group, user, project, session and agent scope
- [ ] cross-scope subscribers cannot receive events
- [ ] failed delivery is observable and retry-bounded
### GPT Message Plane
- [ ] message envelope includes scope and provenance
- [ ] messages cannot cross user/project/session boundaries
- [ ] denied messages do not forward content
### Controller Dispatch
- [ ] dispatch stops when autonomous safe mode is active
- [ ] consequential actions require owner gate
- [ ] duplicate dispatch is idempotent or rejected
- [ ] stuck state cleanup remains green
### Observability
- [ ] admin can see platform status without unrestricted content access
- [ ] failures and queue state are visible
Gate C result: `PASS / FAIL / DEFER`
## Combined Runtime Acceptance
Run after integration and deployment:
```text
getRuntimeStatus
getBaseline
createBranch
createPatch
```
Expected before promotion:
```text
runtime = active
baseline = V1R4C5
write branch = committed + synced
write patch = committed + synced
```
## Rollback Validation
- [ ] Group A can disable workbench routes/menu entries
- [ ] Group B can disable provider binding and use safe non-memory fallback
- [ ] Group C can disable expanded dispatch
- [ ] all rollback modes preserve `AIOS_MEMORY_ENABLED=false`
## Release Decision
V1R4C6 is tag-ready only when:
```text
Global preflight PASS
AND Gate A PASS
AND Gate B PASS
AND Gate C PASS
AND combined runtime acceptance PASS
```
Any failed mandatory gate blocks tag, deployment promotion, and baseline promotion.
@@ -0,0 +1,51 @@
# V1R4C6 Acceptance Evidence
Date: 2026-07-11
## Merge
- **PR #6** (merge commit `ae49bf0fa35b0f599a96a4d12644576447f39cec`): V1R4C6 release-candidate control artifacts merged to main.
- **Result**: PASS
- 10 files (1440 lines) added: release docs, patch records, ops commands, version metadata
- No V1R4C6 tag created
- No baseline promotion
- Memory search remains disabled
- Force-push remains disabled
## Verified State (post-merge)
| Check | Result |
|--------------------------|--------|
| main HEAD | ae49bf0fa35b0f599a96a4d12644576447f39cec |
| V1R4C5 tag | Still at d457ac396147043ed5d0a3fd8b44bcb10f48bceb (not moved) |
| V1R4C6 tag | absent |
| baseline/current.json | current_baseline = V1R4C5, git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704 |
| memory_search_enabled | false |
| force_push_allowed | false |
| versions/V1R4C5.json | exists |
| versions/V1R4C6.json | exists |
## Gates
| Gate | Status | Notes |
|------------------------------|---------|--------------------------------|
| control artifacts merge | PASS | PR #6 merged to main |
| Gate A — Admin Workbench | NOT RUN | Engineering integration pending|
| Gate B — SR-03 Search Gateway| NOT RUN | Engineering integration pending|
| Gate C — Agent Platform | NOT RUN | Engineering integration pending|
| tests | NOT RUN | No test suite executed |
| artifact digest | NOT CREATED | No build performed |
| deployment | NOT RUN | No deploy performed |
| production verification | NOT RUN | No production verification |
## Rollback
Revert command (creates new branch and PR, non-destructive):
```bash
git revert -m 1 ae49bf0fa35b0f599a96a4d12644576447f39cec
```
## Current Overall Status
**BLOCKED / INTEGRATION PENDING** — all engineering gates remain unexecuted.
@@ -0,0 +1,80 @@
# V1R4C6 Blocker: Baseline Visibility Discrepancy
Date: 2026-07-11
Status: blocking merge/deploy/tag
## Summary
OpenClaw/operator reported V1R4C5 baseline promotion completed successfully, but the ChatGPT GitOps action view still reports the active baseline as V1R4C3.
V1R4C6 must not proceed to merge/deploy/tag until this view is reconciled.
## Operator Reported State
Reported by operator:
```text
current_baseline = V1R4C5
previous_baseline = V1R4C3
promotion_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
promoted_by = openclaw/admin
memory_search_enabled = false
force_push_allowed = false
gitea_db_mutation_requires_owner_approval = true
```
## ChatGPT GitOps Action Observed State
Observed through `getBaseline`:
```text
current_baseline = V1R4C3
previous_baseline = V1R3C1
git = f5435e308794c737f3e39de3f6b79807486943c3
promoted_by = openclaw
```
Observed through `listVersions`:
```text
V1R4C5 tag exists
versions_dir does not list canonical V1R4C5.json
workspace_branch = release/v1r4c6-admin-search-agent-integration-20260711
```
## Risk
If V1R4C6 proceeds while different controllers disagree on the active baseline, the release may be built against the wrong base and post-deployment verification may be invalid.
## Required Reconciliation
Before continuing V1R4C6:
1. Confirm the GitOps API used by ChatGPT reads the same `baseline/current.json` updated by OpenClaw.
2. Confirm the API service has reloaded or refreshed baseline state.
3. Confirm canonical `versions/V1R4C5.json` is visible to the action runtime or intentionally explain why it is not required.
4. Re-run:
```text
getBaseline
listVersions
getRuntimeStatus
```
Required result:
```text
getBaseline.current_baseline = V1R4C5
listVersions.tags contains V1R4C5
memory search remains disabled
```
## Hard Stop
Do not create V1R4C6 tag.
Do not promote V1R4C6 baseline.
Do not deploy V1R4C6.
Do not enable memory search.
Proceed only after baseline visibility is reconciled.
@@ -0,0 +1,83 @@
# V1R4C6 Context Isolation Recovery and Resume Decision
Date: 2026-07-11
Status: approved to resume merge/deploy/verification; stop before tag/promotion
## Recovery Evidence
The unrelated OpenClaw/WeChat report was traced to notification/task isolation weaknesses rather than GitOps baseline corruption.
Actions completed:
1. The legacy `AIOS每日自治报告` cron was disabled and retained for forensic review.
2. The expired `2026-07-06-reminder` cron was archived and deleted after confirming no pending dependency.
3. Pending notification queue depth was verified as zero.
4. Unknown/unscoped queued message count was verified as zero.
5. A scoped V1R4C6 WeChat test notification was sent successfully.
The test notification included:
```text
task_id = V1R4C6-integration
release_id = V1R4C6
branch = release/v1r4c6-admin-search-agent-integration-20260711
baseline = V1R4C5
correlation_id = 70308f5e243ab81d
```
No recipient identifier, token, cookie, or unrelated message content is stored in this release record.
## Resume Decision
V1R4C6 may resume:
```text
merge
component tests
security/isolation tests
deployment
production verification
release evidence updates
```
V1R4C6 must still stop before:
```text
Git tag creation
baseline promotion
memory search activation
```
## Required Execution Context
Before every OpenClaw execution or WeChat notification, assert:
```text
task_id = V1R4C6-integration
release_id = V1R4C6
branch = release/v1r4c6-admin-search-agent-integration-20260711
baseline = V1R4C5
memory_search_enabled = false
```
Any mismatch re-enters safe mode and blocks execution/notification.
## Hard Constraints
```text
no force-push
no Gitea DB mutation
no V1R4C5 tag movement
no global memory/vector index
no cross-user/project/session notification or task data
```
## Owner Gate
After Gate A, Gate B, Gate C, deployment verification, runtime acceptance, and Gitea sentinel checks pass, OpenClaw must request explicit owner approval through the scoped WeChat channel for both:
1. Create V1R4C6 Git tag.
2. Promote active baseline from V1R4C5 to V1R4C6.
Ambiguous acknowledgements do not count as approval.
@@ -0,0 +1,136 @@
# V1R4C6 Integration Merge Matrix
Date: 2026-07-11
Status: integration-ready
Base baseline: V1R4C5
Base baseline git: `a32c76a3c1baa49562e53fb8adce10a5ed0e6704`
## Release Objective
Deliver three capability groups in one controlled release train:
1. Admin Workbench and Portal AIOS read-only rollout.
2. SR-03 Search Gateway real provider binding.
3. Agent Platform Integration.
This is a bundled release train, not an uncontrolled bulk merge. Each group has an independent acceptance gate and rollback switch.
## Group A — Admin Workbench and Portal AIOS
| Branch | Capability | Decision |
|---|---|---|
| `feature/v1r4c4-sr04-admin-workbench-visibility-20260711` | Admin workbench task progress and process visibility | integrate |
| `feature/v1r4c4-sr04-portal-aios-visibility-20260710` | Portal AIOS route/menu visibility | integrate |
| `patch/user-portal-me-route-diagnostics-20260709` | `/portal/me/aios/manifest`, dashboard and health route contract | integrate after route review |
| `patch/user-portal-rbac-ops-dashboard` | Admin/group-admin RBAC dashboard | integrate after RBAC review |
| `patch/agent-observability-webui` | Agent observability UI | integrate read-only |
| `patch/web-ui-complete-gitops-dashboard-20260706` | GitOps dashboard UI | integrate only if not superseded by the newer workbench implementation |
### Required user-visible result
Administrators can see:
- task progress;
- task/flow/process diagram entry;
- system health;
- authorized agent observability;
- explicit empty/error states.
### Security constraints
- Read-only first.
- RBAC is enforced server-side and client-side.
- No cross-user, cross-project, cross-session, or cross-tenant data exposure.
- Owner-only modules remain hidden from non-owner administrators.
## Group B — SR-03 Search Gateway Real Provider Binding
| Branch | Capability | Decision |
|---|---|---|
| `feature/v1r4c4-sr02-search-gateway-resilience-20260710` | Gateway resilience and safe fallback | integrate |
| `patch/search-gateway-governed-general-capability` | Governed provider binding and policy surface | integrate after scope review |
| `patch/gitops-change-archive-audit` | Change/archive audit evidence | integrate or retain as supporting audit module |
### Required result
- Real provider binding works.
- Query scope is enforced before ranking.
- Accepted records include provenance.
- Provider failure falls back to `text-only`, `no-memory`, or `scoped-only`.
- Global memory/vector indexes are not used.
### Security constraints
```text
AIOS_MEMORY_ENABLED=false
```
Search Gateway provider binding must not implicitly enable memory search.
## Group C — Agent Platform Integration
| Branch | Capability | Decision |
|---|---|---|
| `patch/agent-event-bus-unified` | Unified scoped agent event bus | integrate |
| `patch/agent-gpt-message-plane` | Scoped Agent/GPT message plane | integrate |
| `patch/autonomous-controller-dispatch-20260709` | Autonomous controller dispatch | integrate behind safe-mode gate |
| `patch/self-evolution-agent-orchestration-policy` | Agent orchestration and governance policy | integrate |
| `patch/aios-autonomous-orchestrator-architecture-review-v1` | Architecture record | integrate as documentation or mark superseded after review |
### Required result
- Agent event and message envelopes include user/project/session scope.
- Controller dispatch stops when autonomous safe mode is active.
- Consequential actions remain owner-gated.
- Observability is available before autonomous expansion.
## Explicitly Excluded
Do not merge or activate in V1R4C6:
- memory search activation;
- global vector index;
- cross-session/global recall;
- unscoped search or message delivery;
- old test/smoke/auth-only branches;
- old V1R2/V1R3 promotion requests;
- force-push reconciliation;
- Gitea database mutation.
## Merge and Deployment Order
```text
1. Verify V1R4C5 baseline and bare-repo configuration.
2. Integrate Group A read-only workbench/portal surface.
3. Run Group A RBAC and visibility acceptance.
4. Integrate Group B provider binding under isolation gate.
5. Run Group B scope/provenance/fallback acceptance.
6. Integrate Group C event/message/dispatch capabilities.
7. Run Group C scope/safe-mode/owner-gate acceptance.
8. Run combined runtime acceptance.
9. Prepare V1R4C6 tag and promotion request.
10. Stop for owner confirmation before tag/promotion.
```
## Combined Acceptance
Required:
```text
getRuntimeStatus -> active
getBaseline -> V1R4C5 before promotion
createBranch -> committed and synced
createPatch -> committed and synced
Gitea branch sentinel -> healthy
memory search -> disabled
```
## Rollback Switches
- Group A: disable new workbench routes/menu entries.
- Group B: disable real provider binding and use safe non-memory fallback.
- Group C: disable expanded dispatch and keep event/message plane observational.
- Memory search remains disabled in all rollback modes.
@@ -0,0 +1,51 @@
# V1R4C6 Release Acceptance Checklist
Date: 2026-07-11
## Gate A — Admin Workbench / Portal AIOS
| Check | Status | Notes |
|--------------------------------|---------|--------------------------------|
| Admin workbench RBAC read-only | NOT RUN | |
| Portal AIOS manifest route | NOT RUN | |
| Portal AIOS dashboard route | NOT RUN | |
| Flow/process diagram renders | NOT RUN | |
| Portal AIOS health route | NOT RUN | |
## Gate B — SR-03 Search Gateway
| Check | Status | Notes |
|------------------------------------|---------|--------------------------------|
| Provider binding correctness | NOT RUN | |
| Scope-before-ranking validation | NOT RUN | |
| Provenance correctness | NOT RUN | |
| Fallback to disabled provider | NOT RUN | |
## Gate C — Agent Platform
| Check | Status | Notes |
|------------------------------------|---------|--------------------------------|
| Event scope isolation | NOT RUN | |
| Message plane scope isolation | NOT RUN | |
| Dispatch safe-mode respects control | NOT RUN | |
| Owner gate for consequential ops | NOT RUN | |
## Final Deployment Gates
| Check | Status | Notes |
|---------------------------|-----------|--------------------------------|
| Tag V1R4C6 | NO | Requires owner approval |
| Baseline promotion | NO | Requires owner approval |
| Memory search enabled | NO | Requires future owner approval |
## Active Constraints
- AIOS_MEMORY_ENABLED: false
- Force-push: forbidden
- Gitea DB mutation: owner approval required
- Memory search: disabled (future owner approval required for enable)
## Artifact
- **PR #6 merge commit**: `ae49bf0fa35b0f599a96a4d12644576447f39cec`
- **Source baseline**: V1R4C5 (commit a32c76a3c1baa49562e53fb8adce10a5ed0e6704)
@@ -0,0 +1,92 @@
# V1R4C6 Release Gate Status
Date: 2026-07-11
Status: integration pending — release-candidate control artifacts merged, engineering gates not yet executed
## Completed
- V1R4C5 baseline verified as active base.
- GitOps API and public API return:
- current_baseline: V1R4C5
- previous_baseline: V1R4C3
- git: a32c76a3c1baa49562e53fb8adce10a5ed0e6704
- GitOps bare repository path corrected to active `aios.git`.
- V1R4C6 merge matrix created.
- V1R4C6 acceptance checklist created.
- V1R4C6 acceptance evidence created.
- PR #6: control artifacts merge — PASS
## Result Summary
| Item | Status |
|-------------------------|-----------|
| control artifacts merge | PASS |
| Gate A | NOT RUN |
| Gate B | NOT RUN |
| Gate C | NOT RUN |
| tests | NOT RUN |
| artifact | NOT CREATED |
| deployment | NOT RUN |
| production verification | NOT RUN |
| tag created | NO |
| baseline promoted | NO |
| memory search enabled | NO |
| **overall** | **BLOCKED / INTEGRATION PENDING** |
## Verified Artifact
- **PR #6 merge commit**: `ae49bf0fa35b0f599a96a4d12644576447f39cec`
- **Current baseline**: V1R4C5
- **V1R4C6 tag**: absent
- **Memory search**: disabled
- **Force-push**: disabled
## Remaining Gates (not yet executed)
### Gate A — Admin Workbench / Portal AIOS
- merge candidate review;
- RBAC acceptance;
- task progress visibility;
- flow diagram visibility;
- health/observability validation.
### Gate B — SR-03 Search Gateway
- provider binding validation;
- scope-before-ranking validation;
- provenance validation;
- fallback validation.
### Gate C — Agent Platform
- event scope validation;
- message plane scope validation;
- dispatch safe-mode validation;
- owner gate validation.
## Release Stop Point
After Gates A/B/C pass:
```text
prepare V1R4C6 tag
↓
owner confirmation required
↓
tag creation
↓
baseline promotion
```
## Hard Constraints
Remain:
```text
AIOS_MEMORY_ENABLED=false
memory search disabled
force-push forbidden
gitea DB mutation requires owner approval
```
+58
View File
@@ -0,0 +1,58 @@
# Architect Agent Output Template
## 1. Requirement Change Record
- Requirement ID:
- Source:
- Custom GPT
- Codex
- OpenClaw
- Hermes
- User feedback
- Time:
- Requirement:
## 2. Architecture Review
### Current architecture impact
### Risks
### Optimization recommendations
## 3. Document Refresh
Required:
- User Requirements Document
- Architecture Design Document
- Engineering Deployment Document
- Test Case Document
## 4. Deployment Consistency Check
Compare:
- Deployment document
- Runtime configuration
- Actual services
- Environment variables
## 5. Test Acceptance Plan
List:
- Unit tests
- Integration tests
- User acceptance tests
- Architecture validation tests
## 6. Version Decision
Choose:
- C: compatible change/fix
- R: release capability or architecture evolution
- V: major architecture/breaking change
Reason:
@@ -0,0 +1,152 @@
apiVersion: aios.gitops/v1
kind: ExecutionOrder
metadata:
name: openclaw-v1r4c6-merge-tests-deploy
date: 2026-07-11
owner_approval:
branch: request/openclaw-v1r4c6-merge-deploy-20260711
file: approvals/openclaw-v1r4c6-merge-deploy-owner-approval.yaml
commit: 4653f92
spec:
objective: >-
Integrate the approved V1R4C6 Admin Workbench, Search Gateway, and Agent Platform
changes into a clean candidate based on the latest main, execute all mandatory gates,
merge to main only after success, then deploy and verify production.
confirmation_policy: no_additional_confirmation_required
source:
primary_branch: release/v1r4c6-admin-search-agent-integration-20260711
supporting_domains:
- admin-workbench-portal-aios
- search-gateway-governance
- agent-platform-integration
target:
integration_branch: integration/v1r4c6-merge-deploy-20260711-01
final_branch: main
release_version: V1R4C6
execution:
- id: preflight
actions:
- fetch_all_refs_and_tags
- verify_workspace_clean
- reset_integration_candidate_to_latest_main
- verify_baseline_is_V1R4C5
- verify_memory_search_enabled_is_false
- verify_V1R4C5_tag_unchanged
- record_main_head_and_source_head
stop_conditions:
- dirty_workspace
- missing_source_branch
- baseline_not_V1R4C5
- protected_setting_changed
- id: semantic-diff-review
actions:
- compute_three_dot_diff_from_main_to_primary_branch
- classify_changes_by_admin_search_agent_domains
- detect_duplicate_control_artifacts_already_merged_by_PR_6
- preserve_PR_7_NOT_RUN_status_until_tests_execute
- reject_reversion_of_V1R4C5_baseline_metadata
- reject_enablement_of_memory_search
output:
- execution/evidence/v1r4c6-semantic-diff-report.md
- id: merge-candidate
actions:
- merge_primary_branch_into_integration_candidate_without_fast_forward
- resolve_text_conflicts_by_preserving_latest_main_control_metadata
- preserve_source_business_functionality_when_not_conflicting_with_safety_controls
- commit_resolved_candidate
forbidden:
- force_push
- direct_commit_to_main
- bypass_branch_protection
- bypass_owner_gate
- weaken_rbac_or_scope_isolation
- mark_unexecuted_tests_as_passed
- id: gate-a-admin
required_checks:
- admin_workbench_routes_and_assets_load
- portal_aios_manifest_dashboard_health_endpoints
- server_side_and_frontend_rbac
- empty_error_and_loading_states
- tenant_user_project_session_isolation
- id: gate-b-search
required_checks:
- real_provider_binding_or_explicit_safe_fallback
- timeout_limited_retry_and_safe_fallback
- scope_filter_before_ranking
- provenance_in_results
- provider_and_fallback_audit
- unauthorized_results_never_enter_ranking_or_summary
- memory_search_remains_disabled
- id: gate-c-agent
required_checks:
- unified_agent_event_bus
- scoped_agent_gpt_message_plane
- autonomous_controller_dispatch
- safe_mode_stops_dispatch_and_change
- high_risk_actions_remain_owner_gated
- duplicate_dispatch_is_idempotent_or_rejected
- queue_failure_retry_observability
- id: runtime-and-regression
actions:
- run_getRuntimeStatus
- run_getBaseline
- run_createBranch_smoke_test
- run_createPatch_smoke_test
- run_full_regression_suite
- run_deployment_preflight_health_checks
expected:
reads: "200 OK with service, status, workspace_branch"
writes: "200 OK with branch, commit, rev, sync_status"
- id: promote-main
conditions:
- gate_a_passed
- gate_b_passed
- gate_c_passed
- runtime_acceptance_passed
- regression_passed
- deployment_preflight_passed
actions:
- open_or_update_merge_request_from_integration_candidate_to_main
- merge_without_rewriting_history
- record_new_main_head
- update_versions_V1R4C6_json_to_release_ready_or_active_baseline_as_policy_requires
- update_baseline_current_json_only_after_successful_promotion
- create_V1R4C6_tag_according_to_repository_tag_semantics
- id: deploy-and-verify
actions:
- trigger_direct_deployment_from_new_main
- verify_service_health
- verify_admin_search_agent_smoke_paths
- verify_runtime_status_and_baseline_after_deploy
- verify_rollback_artifact_and_previous_baseline_are_available
- publish_production_verification_report
failure_policy:
mode: fail_closed
on_any_failure:
- stop_pipeline
- do_not_update_main
- do_not_create_or_move_release_tag
- do_not_deploy
- preserve_integration_branch_for_forensics
- commit_failure_evidence_and_exact_blocker
reporting:
required_files:
- execution/evidence/v1r4c6-semantic-diff-report.md
- execution/evidence/gate-a-admin-result.md
- execution/evidence/gate-b-search-result.md
- execution/evidence/gate-c-agent-result.md
- execution/evidence/runtime-acceptance-result.md
- execution/evidence/regression-result.md
- execution/evidence/deployment-result.md
- execution/evidence/production-verification-result.md
final_summary_fields:
- source_head
- integration_head
- main_head_before
- main_head_after
- merge_commit
- release_tag
- deployment_id
- gate_results
- production_health
- rollback_ready
+86
View File
@@ -0,0 +1,86 @@
import {
classifyRun,
decideToolCall,
runKey,
} from "./policy.js";
const activeRuns = new Map();
const MAX_RUN_AGE_MS = 60 * 60 * 1000;
function prune(now = Date.now()) {
for (const [key, value] of activeRuns.entries()) {
if (now - value.recordedAt > MAX_RUN_AGE_MS) activeRuns.delete(key);
}
}
export default {
id: "aios-tenant-boundary",
name: "AIOS Tenant Boundary",
description: "Fail-closed user-origin and side-effect policy for Tenant Cells.",
register(api) {
api.on(
"before_agent_run",
async (event, ctx) => {
prune();
const config = event.context?.pluginConfig ?? api.pluginConfig ?? {};
const trust = classifyRun(ctx, config);
const key = runKey(event, ctx);
if (key) {
activeRuns.set(key, { ...trust, recordedAt: Date.now() });
}
if (
config.blockUnknownAgentRuns === true &&
trust.trusted !== true &&
trust.reason !== "job_id_present"
) {
return {
outcome: "block",
reason: trust.reason,
message: "This message cannot be verified as coming from the active authenticated user.",
};
}
return { outcome: "pass" };
},
{ priority: 100 },
);
api.on(
"before_tool_call",
async (event, ctx) => {
prune();
const config = event.context?.pluginConfig ?? api.pluginConfig ?? {};
const key = runKey(event, ctx);
const trust = key ? activeRuns.get(key) : undefined;
const decision = decideToolCall(event.toolName, trust, config);
if (decision.action === "block") {
return {
block: true,
blockReason: `AIOS V2R1 origin policy denied tool call: ${decision.code}`,
};
}
if (decision.action === "approval") {
return {
requireApproval: {
title: `Approve ${String(event.toolName)}`,
description: "Authenticated user requested a side-effecting tool call in this Tenant Cell.",
severity: "warning",
timeoutMs: Number(config.approvalTimeoutMs ?? 60000),
allowedDecisions: ["allow-once", "deny"],
pluginId: "aios-tenant-boundary",
},
};
}
return undefined;
},
{ priority: 100 },
);
api.on("agent_end", async (event, ctx) => {
const key = runKey(event, ctx);
if (key) activeRuns.delete(key);
});
},
};
+71
View File
@@ -0,0 +1,71 @@
const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
"read", "grep", "glob", "find", "ls", "web_search", "web_fetch",
"memory_search", "sessions_list", "sessions_history", "session_status",
]);
function strings(value) {
return Array.isArray(value)
? value.filter((item) => typeof item === "string" && item.trim())
.map((item) => item.trim())
: [];
}
export function resolveSenderId(ctx = {}) {
const direct = typeof ctx.senderId === "string" ? ctx.senderId.trim() : "";
const nested = typeof ctx.channelContext?.sender?.id === "string"
? ctx.channelContext.sender.id.trim()
: "";
return direct || nested || undefined;
}
export function classifyRun(ctx = {}, config = {}) {
const senderId = resolveSenderId(ctx);
const trusted = new Set(strings(config.trustedSenderIds));
const blocked = new Set(strings(config.blockedSenderIds));
if (typeof ctx.jobId === "string" && ctx.jobId) {
return { trusted: false, origin: "automation", reason: "job_id_present" };
}
if (!senderId) {
return { trusted: false, origin: "system_or_unknown", reason: "sender_id_missing" };
}
if (blocked.has(senderId)) {
return { trusted: false, origin: "blocked_sender", reason: "sender_blocked", senderId };
}
if (config.requireExplicitTrustedSenders !== false && !trusted.has(senderId)) {
return { trusted: false, origin: "untrusted_sender", reason: "sender_not_allowlisted", senderId };
}
return {
trusted: true,
origin: "authenticated_channel_user",
reason: "sender_verified",
senderId,
};
}
export function decideToolCall(toolName, runTrust, config = {}) {
const name = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
const configured = strings(config.readOnlyTools).map((item) => item.toLowerCase());
const readOnly = new Set(configured.length ? configured : DEFAULT_READ_ONLY_TOOLS);
if (!name) {
return { action: "block", code: "tool_name_missing" };
}
if (readOnly.has(name)) {
return { action: "allow", code: "read_only" };
}
if (!runTrust || runTrust.trusted !== true) {
return {
action: "block",
code: runTrust?.reason || "run_provenance_missing",
};
}
if (config.requireApprovalForHumanSideEffects === false) {
return { action: "allow", code: "trusted_side_effect" };
}
return { action: "approval", code: "human_side_effect" };
}
export function runKey(event = {}, ctx = {}) {
const key = event.runId || ctx.runId;
return typeof key === "string" && key.trim() ? key.trim() : undefined;
}

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