Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce7aa08249 | ||
|
|
b4d3e53167 | ||
|
|
0c5a268548 | ||
|
|
02682d27a5 | ||
|
|
6077331add | ||
|
|
75f61ac0e6 | ||
|
|
0b41a3952a | ||
|
|
8f205c0d87 | ||
|
|
d2a4a765ac | ||
|
|
b1110cad62 | ||
|
|
b483dfadbf | ||
|
|
1e5e144318 | ||
|
|
d552876a80 | ||
|
|
6569065198 | ||
|
|
2b80c50415 | ||
|
|
954982647e | ||
|
|
8074eb31b1 | ||
|
|
90138221ee | ||
|
|
729c7bb23b | ||
|
|
d2de7f23d1 | ||
|
|
73c0a233ce | ||
|
|
00816a13c1 | ||
|
|
4920ac6192 | ||
|
|
b12ff086bf | ||
|
|
57a668d87e | ||
|
|
bab319575b | ||
|
|
c172953a3c | ||
|
|
c7617fa548 | ||
|
|
2eba23d274 | ||
|
|
f2e0406a20 | ||
|
|
ccb02640ac | ||
|
|
5c52f6c82c | ||
|
|
429c6875bc | ||
|
|
f10d0d5b17 | ||
|
|
c9974f3e47 | ||
|
|
c72450aeb0 | ||
|
|
f95be48a42 | ||
|
|
ba989d7340 | ||
|
|
1a9cb9095e | ||
|
|
0312159168 | ||
|
|
e204a90398 | ||
|
|
d8cf6058e9 | ||
|
|
2aefc19e3d | ||
|
|
28a430066f | ||
|
|
1013aef461 | ||
|
|
bb3210d74f | ||
|
|
745395e93b | ||
|
|
ae49bf0fa3 | ||
|
|
8410cd895e | ||
|
|
dc1642c2e8 | ||
|
|
c75b865710 | ||
|
|
e992b624e0 | ||
|
|
c60e4cd02c | ||
|
|
4b2996ca9f | ||
|
|
086076b74f | ||
|
|
7b2d694272 | ||
|
|
0583060c7c | ||
|
|
20e707fb52 | ||
|
|
6c75ec6bb2 | ||
|
|
e9fefd4c18 | ||
|
|
3ece481c74 | ||
|
|
33177548c3 | ||
|
|
ed1413a05e | ||
|
|
05d1e64228 | ||
|
|
64bbe1b51c | ||
|
|
3c6f2c8c65 |
@@ -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
|
||||||
+19
@@ -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`
|
||||||
@@ -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)
|
||||||
@@ -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())
|
||||||
@@ -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")
|
||||||
@@ -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])
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
+12
-7
@@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
"current_baseline": "V1R4C3",
|
"current_baseline": "V1R4C6",
|
||||||
"previous_baseline": "V1R3C1",
|
"previous_baseline": "V1R4C5",
|
||||||
"promoted_at": "2026-07-09T17:44:57+08:00",
|
"promoted_at": "2026-07-11T21:00:00+08:00",
|
||||||
"git": "f5435e308794c737f3e39de3f6b79807486943c3",
|
"git": "e204a90398bf646d46245b3088226749fa056edd",
|
||||||
"promoted_by": "openclaw",
|
"promoted_by": "openclaw/admin",
|
||||||
"promotion_reason": "V1R4C3 autonomous operations, search gateway, task center, owner gate, and blocker self-optimization baseline promotion"
|
"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,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'
|
||||||
@@ -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"
|
||||||
@@ -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.
|
||||||
+49
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "aios-tenant-boundary",
|
||||||
|
"name": "AIOS Tenant Boundary",
|
||||||
|
"description": "Enforces AIOS V2R1 tenant provenance and tool policy boundaries.",
|
||||||
|
"version": "2.1.0-rc1",
|
||||||
|
"entry": "dist/index.js",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"requireExplicitTrustedSenders": { "type": "boolean", "default": true },
|
||||||
|
"trustedSenderIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
|
"blockedSenderIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
|
"readOnlyTools": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"requireApprovalForHumanSideEffects": { "type": "boolean", "default": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@aios/openclaw-tenant-boundary",
|
||||||
|
"version": "2.1.0-rc.1",
|
||||||
|
"type": "module",
|
||||||
|
"description": "AIOS V2R1 Tenant Cell provenance and side-effect policy boundary for OpenClaw.",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"openclaw.plugin.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test test/*.test.js"
|
||||||
|
},
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": [
|
||||||
|
"./dist/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
# CMD-20260711-v1r4c6-integrate-deploy-verify
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Perform the actual controlled integration, deployment, and verification for V1R4C6.
|
||||||
|
|
||||||
|
V1R4C6 includes:
|
||||||
|
|
||||||
|
1. Admin Workbench / Portal AIOS read-only rollout.
|
||||||
|
2. SR-03 Search Gateway real provider binding.
|
||||||
|
3. Agent Platform Integration.
|
||||||
|
|
||||||
|
Stop before V1R4C6 Git tag creation and baseline promotion. Those operations require owner confirmation.
|
||||||
|
|
||||||
|
## Hard Constraints
|
||||||
|
|
||||||
|
```text
|
||||||
|
AIOS_MEMORY_ENABLED=false
|
||||||
|
memory search remains disabled
|
||||||
|
no global vector index
|
||||||
|
no force-push
|
||||||
|
no V1R4C5 tag movement
|
||||||
|
no Gitea DB mutation
|
||||||
|
no secret plaintext in Git, logs, or reports
|
||||||
|
```
|
||||||
|
|
||||||
|
If any mandatory security or reliability check fails, stop deployment and report the blocker. Do not bypass the gate.
|
||||||
|
|
||||||
|
## Verified Base
|
||||||
|
|
||||||
|
Required before starting:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current_baseline = V1R4C5
|
||||||
|
previous_baseline = V1R4C3
|
||||||
|
baseline_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
||||||
|
AIOS_GITOPS_BARE_REPO = /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
||||||
|
runtime = active
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 0 — Preflight and Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=/data/aios-gitops/workspace
|
||||||
|
SOURCE_RELEASE=release/v1r4c6-admin-search-agent-integration-20260711
|
||||||
|
EXEC_BRANCH=release/v1r4c6-integration-exec-20260711
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
git fetch origin --tags --prune
|
||||||
|
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
test -f baseline/current.json
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
p = Path('baseline/current.json')
|
||||||
|
d = json.loads(p.read_text(encoding='utf-8'))
|
||||||
|
assert d['current_baseline'] == 'V1R4C5', d
|
||||||
|
assert d['previous_baseline'] == 'V1R4C3', d
|
||||||
|
assert d['git'] == 'a32c76a3c1baa49562e53fb8adce10a5ed0e6704', d
|
||||||
|
assert d.get('constraints', {}).get('memory_search_enabled') is False, d
|
||||||
|
print('baseline preflight: PASS')
|
||||||
|
PY
|
||||||
|
|
||||||
|
test -d /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
||||||
|
|
||||||
|
BACKUP=/data/aios-gitops/runtime/backups/v1r4c6-predeploy-$(date +%Y%m%d-%H%M%S)
|
||||||
|
mkdir -p "$BACKUP"
|
||||||
|
cp -a baseline/current.json "$BACKUP/current.json"
|
||||||
|
cp -a versions/V1R4C5.json "$BACKUP/V1R4C5.json"
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet "refs/heads/$EXEC_BRANCH"; then
|
||||||
|
git checkout "$EXEC_BRANCH"
|
||||||
|
git pull --ff-only origin "$EXEC_BRANCH"
|
||||||
|
else
|
||||||
|
git checkout -b "$EXEC_BRANCH" "origin/$SOURCE_RELEASE"
|
||||||
|
git push -u origin "$EXEC_BRANCH"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Never use `git push --force` or `git tag -f`.
|
||||||
|
|
||||||
|
## Phase 1 — Merge Group A: Admin Workbench / Portal AIOS
|
||||||
|
|
||||||
|
Merge in this order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge --no-ff origin/feature/v1r4c4-sr04-admin-workbench-visibility-20260711 \
|
||||||
|
-m 'merge(v1r4c6): admin workbench visibility preparation'
|
||||||
|
|
||||||
|
git merge --no-ff origin/feature/v1r4c4-sr04-portal-aios-visibility-20260710 \
|
||||||
|
-m 'merge(v1r4c6): portal AIOS visibility preparation'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/user-portal-me-route-diagnostics-20260709 \
|
||||||
|
-m 'merge(v1r4c6): portal AIOS routes and RBAC manifest'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/user-portal-rbac-ops-dashboard \
|
||||||
|
-m 'merge(v1r4c6): portal RBAC operations dashboard'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/agent-observability-webui \
|
||||||
|
-m 'merge(v1r4c6): agent observability web UI'
|
||||||
|
```
|
||||||
|
|
||||||
|
Before merging `patch/web-ui-complete-gitops-dashboard-20260706`, compare overlap:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --stat HEAD..origin/patch/web-ui-complete-gitops-dashboard-20260706
|
||||||
|
```
|
||||||
|
|
||||||
|
Merge only if it adds required functionality not already present. Otherwise record it as superseded.
|
||||||
|
|
||||||
|
### Conflict Policy
|
||||||
|
|
||||||
|
Do not auto-resolve conflicts in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
auth/RBAC configuration
|
||||||
|
user/session/tenant isolation
|
||||||
|
search provider configuration
|
||||||
|
safe-mode/controller dispatch
|
||||||
|
systemd units
|
||||||
|
baseline/current.json
|
||||||
|
versions/*.json
|
||||||
|
```
|
||||||
|
|
||||||
|
For those files, stop and report conflict paths.
|
||||||
|
|
||||||
|
### Group A Tests
|
||||||
|
|
||||||
|
Run at minimum:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pytest -q \
|
||||||
|
tests/test_portal_aios_frontend_contract.py \
|
||||||
|
tests/test_portal_aios_module_registry.py \
|
||||||
|
tests/test_portal_aios_navigation.py \
|
||||||
|
tests/test_portal_integration_health.py \
|
||||||
|
tests/test_portal_system_health_adapter.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Run any additional portal/RBAC/dashboard tests discovered under `tests/`.
|
||||||
|
|
||||||
|
## Phase 2 — Merge Group B: SR-03 Search Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge --no-ff origin/feature/v1r4c4-sr02-search-gateway-resilience-20260710 \
|
||||||
|
-m 'merge(v1r4c6): search gateway resilience metadata'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/search-gateway-governed-general-capability \
|
||||||
|
-m 'merge(v1r4c6): governed search gateway provider binding'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/gitops-change-archive-audit \
|
||||||
|
-m 'merge(v1r4c6): search and GitOps audit evidence'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group B Security Review
|
||||||
|
|
||||||
|
Verify before deployment:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scope filtering occurs before ranking
|
||||||
|
provenance is attached to accepted results
|
||||||
|
out-of-scope records never reach ranking or summarization
|
||||||
|
fallback is text-only, no-memory, or scoped-only
|
||||||
|
global memory/vector index is disabled
|
||||||
|
memory search remains disabled
|
||||||
|
provider credentials are secret references only
|
||||||
|
```
|
||||||
|
|
||||||
|
Reject any code path that silently broadens search scope.
|
||||||
|
|
||||||
|
### Group B Tests
|
||||||
|
|
||||||
|
Discover and run all search/provider/isolation tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find tests -maxdepth 2 -type f \
|
||||||
|
\( -iname '*search*' -o -iname '*provider*' -o -iname '*scope*' -o -iname '*isolation*' \) \
|
||||||
|
-print
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the discovered files with `python3 -m pytest -q`.
|
||||||
|
|
||||||
|
Also run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ops/ci/run-user-data-isolation-acceptance.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If that script is not present after merge, stop: the data-isolation gate is mandatory.
|
||||||
|
|
||||||
|
## Phase 3 — Merge Group C: Agent Platform
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git merge --no-ff origin/patch/agent-event-bus-unified \
|
||||||
|
-m 'merge(v1r4c6): unified scoped agent event bus'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/agent-gpt-message-plane \
|
||||||
|
-m 'merge(v1r4c6): scoped agent GPT message plane'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/autonomous-controller-dispatch-20260709 \
|
||||||
|
-m 'merge(v1r4c6): autonomous controller dispatch with gates'
|
||||||
|
|
||||||
|
git merge --no-ff origin/patch/self-evolution-agent-orchestration-policy \
|
||||||
|
-m 'merge(v1r4c6): self-evolution orchestration policy'
|
||||||
|
```
|
||||||
|
|
||||||
|
Review `patch/aios-autonomous-orchestrator-architecture-review-v1`:
|
||||||
|
|
||||||
|
- merge if it contains a current architecture record not already present;
|
||||||
|
- otherwise mark it superseded in the release report.
|
||||||
|
|
||||||
|
### Group C Security Review
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
event envelope includes tenant/group, user, project, session, agent, source and provenance
|
||||||
|
message plane rejects cross-scope delivery
|
||||||
|
denied messages never forward body/content
|
||||||
|
controller dispatch checks autonomous safe mode before mutation
|
||||||
|
consequential actions remain owner-gated
|
||||||
|
duplicate dispatch is idempotent or rejected
|
||||||
|
queue/retry behavior is bounded and observable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group C Tests
|
||||||
|
|
||||||
|
Discover and run agent/event/message/dispatch/controller tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find tests -maxdepth 2 -type f \
|
||||||
|
\( -iname '*agent*' -o -iname '*event*' -o -iname '*message*' -o -iname '*dispatch*' -o -iname '*autonomous_controller*' \) \
|
||||||
|
-print
|
||||||
|
```
|
||||||
|
|
||||||
|
Run all discovered files with `python3 -m pytest -q`.
|
||||||
|
|
||||||
|
Re-run the SR-02 watcher/controller suite and require all tests to pass.
|
||||||
|
|
||||||
|
## Phase 4 — Full Test and Static Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m compileall -q active-runtime scripts tests
|
||||||
|
python3 -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
If the full suite has unrelated historical failures, report them separately, but all V1R4C6-touched component tests must pass.
|
||||||
|
|
||||||
|
Validate JSON/YAML configuration without printing secrets.
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit integration result normally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m 'release: integrate V1R4C6 admin search and agent platform'
|
||||||
|
git push origin "$EXEC_BRANCH"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no uncommitted changes remain because merge commits already captured everything, just push normally.
|
||||||
|
|
||||||
|
## Phase 5 — Deploy to Staging/Production Runtime
|
||||||
|
|
||||||
|
Use the existing deployment mechanism for this repository. Do not invent a new deployment path.
|
||||||
|
|
||||||
|
Before restart/reload, identify changed services and frontend bundles from the merged diff.
|
||||||
|
|
||||||
|
At minimum:
|
||||||
|
|
||||||
|
1. Deploy backend modules under `active-runtime/app/`.
|
||||||
|
2. Deploy portal frontend/navigation/proxy changes.
|
||||||
|
3. Deploy Search Gateway provider binding using secret references.
|
||||||
|
4. Deploy Agent Platform event/message/dispatch changes.
|
||||||
|
5. Preserve existing Gitea sentinel and user-data isolation systemd controls.
|
||||||
|
6. Keep `AIOS_MEMORY_ENABLED=false`.
|
||||||
|
|
||||||
|
Restart only affected services. Do not restart Gitea unless it independently fails health checks.
|
||||||
|
|
||||||
|
After deployment, verify affected services are active and no restart loop exists.
|
||||||
|
|
||||||
|
## Phase 6 — Actual Deployment Verification
|
||||||
|
|
||||||
|
### Global
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:18890/api/base | python3 -m json.tool
|
||||||
|
curl -fsS https://cnnetmkt.top/api/base | python3 -m json.tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Before V1R4C6 promotion both must still report V1R4C5.
|
||||||
|
|
||||||
|
Verify Gitea sentinel remains healthy and memory search remains disabled.
|
||||||
|
|
||||||
|
### Gate A — Portal/Admin Workbench
|
||||||
|
|
||||||
|
Use an existing authenticated test mechanism or secret-backed admin session. Do not log cookies/tokens.
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/portal/me/aios/manifest
|
||||||
|
/portal/me/aios/dashboard
|
||||||
|
/portal/me/aios/health
|
||||||
|
```
|
||||||
|
|
||||||
|
For `user`, `group_admin/admin`, and `owner`, record only module names/status, not personal data.
|
||||||
|
|
||||||
|
Required admin-visible functions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
task progress
|
||||||
|
process/flow diagram entry
|
||||||
|
system health
|
||||||
|
agent observability
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify forbidden owner modules are hidden from non-owner admins.
|
||||||
|
|
||||||
|
### Gate B — Search Gateway
|
||||||
|
|
||||||
|
Execute scoped test queries using non-sensitive fixtures.
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
real provider selected
|
||||||
|
scope applied before ranking
|
||||||
|
provenance returned
|
||||||
|
out-of-scope fixture denied
|
||||||
|
safe fallback works
|
||||||
|
memory search remains disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gate C — Agent Platform
|
||||||
|
|
||||||
|
Using synthetic non-sensitive events/messages:
|
||||||
|
|
||||||
|
```text
|
||||||
|
same-scope delivery succeeds
|
||||||
|
cross-user/project/session delivery is denied
|
||||||
|
denied payload body is not forwarded
|
||||||
|
safe-mode blocks controller mutation
|
||||||
|
owner-gated action remains pending approval
|
||||||
|
duplicate dispatch is idempotent/rejected
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 7 — Release Evidence
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/releases/V1R4C6-acceptance-checklist.md
|
||||||
|
docs/releases/V1R4C6-acceptance-evidence.md
|
||||||
|
docs/releases/V1R4C6-release-gate-status.md
|
||||||
|
versions/V1R4C6.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Set each gate to PASS/FAIL with concrete test commands and results.
|
||||||
|
|
||||||
|
Required final state before requesting owner confirmation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gate A = PASS
|
||||||
|
Gate B = PASS
|
||||||
|
Gate C = PASS
|
||||||
|
full touched-component tests = PASS
|
||||||
|
runtime write acceptance = PASS
|
||||||
|
Gitea sentinel = healthy
|
||||||
|
memory search = disabled
|
||||||
|
force-push used = no
|
||||||
|
V1R4C6 tag = absent
|
||||||
|
active baseline = V1R4C5
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit and push evidence normally to `$EXEC_BRANCH`.
|
||||||
|
|
||||||
|
## Stop Point — Owner Confirmation Required
|
||||||
|
|
||||||
|
Do not create `V1R4C6` tag.
|
||||||
|
Do not promote baseline to V1R4C6.
|
||||||
|
Do not enable memory search.
|
||||||
|
|
||||||
|
When all gates pass, notify the owner through the configured notification mechanism, preferably WeChat if configured. The notification must contain no secrets and must request confirmation for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Create V1R4C6 Git tag
|
||||||
|
Promote active baseline from V1R4C5 to V1R4C6
|
||||||
|
```
|
||||||
|
|
||||||
|
Required report fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Status
|
||||||
|
Integration branch and HEAD
|
||||||
|
Merged branches
|
||||||
|
Skipped/superseded branches
|
||||||
|
Gate A result
|
||||||
|
Gate B result
|
||||||
|
Gate C result
|
||||||
|
Test totals
|
||||||
|
Deployment services/build versions
|
||||||
|
Portal/admin verification result
|
||||||
|
Search provider verification result
|
||||||
|
Agent platform verification result
|
||||||
|
Gitea sentinel status
|
||||||
|
Memory search status
|
||||||
|
Active baseline
|
||||||
|
V1R4C6 tag status
|
||||||
|
Rollback readiness
|
||||||
|
Owner confirmation required
|
||||||
|
```
|
||||||
|
|
||||||
|
If WeChat notification is not configured, write the exact notification message to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/aios-gitops/runtime/state/V1R4C6-owner-confirmation-message.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
and report that path.
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# CMD-20260711-v1r4c6-run-to-owner-gate-wechat
|
||||||
|
|
||||||
|
## Execution Mode
|
||||||
|
|
||||||
|
Execute immediately using the existing V1R4C6 integration command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ops/openclaw/CMD-20260711-v1r4c6-integrate-deploy-verify.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Continue through actual merge, tests, deployment, and production verification.
|
||||||
|
|
||||||
|
Stop only at the owner gate for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
V1R4C6 Git tag creation
|
||||||
|
V1R4C6 baseline promotion
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not ask the owner to perform routine engineering or deployment work.
|
||||||
|
|
||||||
|
## Verified Starting State
|
||||||
|
|
||||||
|
Required before starting:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime = active
|
||||||
|
current_baseline = V1R4C5
|
||||||
|
previous_baseline = V1R4C3
|
||||||
|
baseline_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
||||||
|
AIOS_GITOPS_BARE_REPO = /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
||||||
|
memory_search_enabled = false
|
||||||
|
force_push_allowed = false
|
||||||
|
```
|
||||||
|
|
||||||
|
If any starting condition differs, stop and send a WeChat blocker notification.
|
||||||
|
|
||||||
|
## Required Work
|
||||||
|
|
||||||
|
Complete all phases in the referenced command:
|
||||||
|
|
||||||
|
1. Create/update the normal non-force-pushed integration execution branch.
|
||||||
|
2. Merge and validate Admin Workbench / Portal AIOS.
|
||||||
|
3. Merge and validate SR-03 Search Gateway real provider binding.
|
||||||
|
4. Merge and validate Agent Platform Integration.
|
||||||
|
5. Run component and full touched-scope test suites.
|
||||||
|
6. Deploy using the repository's existing deployment mechanism.
|
||||||
|
7. Verify production routes, RBAC, scope isolation, provenance, safe mode, and owner gates.
|
||||||
|
8. Update V1R4C6 release evidence and version metadata.
|
||||||
|
9. Push all normal commits to origin.
|
||||||
|
10. Keep V1R4C6 tag absent and baseline at V1R4C5 until owner approval.
|
||||||
|
|
||||||
|
## Hard Constraints
|
||||||
|
|
||||||
|
```text
|
||||||
|
AIOS_MEMORY_ENABLED=false
|
||||||
|
memory search remains disabled
|
||||||
|
no global memory/vector index
|
||||||
|
no force-push
|
||||||
|
no tag rewrite
|
||||||
|
no V1R4C5 tag movement
|
||||||
|
no Gitea DB mutation
|
||||||
|
no plaintext secrets in Git, logs, reports, or WeChat
|
||||||
|
```
|
||||||
|
|
||||||
|
Never bypass a failed security, isolation, RBAC, safe-mode, or owner gate.
|
||||||
|
|
||||||
|
## WeChat Progress Notifications
|
||||||
|
|
||||||
|
Use the configured OpenClaw WeChat notification channel.
|
||||||
|
|
||||||
|
Send concise progress notifications at these milestones:
|
||||||
|
|
||||||
|
### Notification 1 — Integration Started
|
||||||
|
|
||||||
|
```text
|
||||||
|
V1R4C6 集成已启动。
|
||||||
|
范围:管理员工作台、SR-03 Search Gateway、Agent Platform。
|
||||||
|
当前基线:V1R4C5。
|
||||||
|
memory search 保持关闭。
|
||||||
|
下一步:完成合并、测试和部署验证。
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification 2 — Gate A Complete
|
||||||
|
|
||||||
|
Include only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gate A result: PASS/FAIL
|
||||||
|
Admin task progress visibility: PASS/FAIL
|
||||||
|
Flow diagram visibility: PASS/FAIL
|
||||||
|
RBAC/isolation: PASS/FAIL
|
||||||
|
Blockers, if any
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not include user records, tokens, cookies, or sensitive task content.
|
||||||
|
|
||||||
|
### Notification 3 — Gate B Complete
|
||||||
|
|
||||||
|
Include only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gate B result: PASS/FAIL
|
||||||
|
Real provider binding: PASS/FAIL
|
||||||
|
Scope-before-ranking: PASS/FAIL
|
||||||
|
Provenance: PASS/FAIL
|
||||||
|
Safe fallback: PASS/FAIL
|
||||||
|
Memory search: disabled
|
||||||
|
Blockers, if any
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification 4 — Gate C Complete
|
||||||
|
|
||||||
|
Include only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gate C result: PASS/FAIL
|
||||||
|
Event scope: PASS/FAIL
|
||||||
|
Message plane scope: PASS/FAIL
|
||||||
|
Safe-mode dispatch block: PASS/FAIL
|
||||||
|
Owner gate: PASS/FAIL
|
||||||
|
Blockers, if any
|
||||||
|
```
|
||||||
|
|
||||||
|
## Final WeChat Owner Authorization Request
|
||||||
|
|
||||||
|
Send only after all mandatory gates and deployment verification pass.
|
||||||
|
|
||||||
|
Use this message:
|
||||||
|
|
||||||
|
```text
|
||||||
|
V1R4C6 已完成实际合并、测试、部署和生产验证,现等待最终发布授权。
|
||||||
|
|
||||||
|
版本范围:
|
||||||
|
1. 管理员工作台 / Portal AIOS 上线
|
||||||
|
2. SR-03 Search Gateway Real Provider Binding
|
||||||
|
3. Agent Platform Integration
|
||||||
|
|
||||||
|
验收结果:
|
||||||
|
- Gate A(管理员工作台 / RBAC / 隔离):PASS
|
||||||
|
- Gate B(Provider / scope / provenance / fallback):PASS
|
||||||
|
- Gate C(Event Bus / Message Plane / Dispatch / Owner Gate):PASS
|
||||||
|
- Runtime 写链路:PASS
|
||||||
|
- Gitea consistency sentinel:healthy
|
||||||
|
- memory search:仍为 disabled
|
||||||
|
- force-push:未使用
|
||||||
|
- 当前 active baseline:V1R4C5
|
||||||
|
- V1R4C6 tag:尚未创建
|
||||||
|
|
||||||
|
请明确回复以下其中一项:
|
||||||
|
A. 批准创建 V1R4C6 Git tag,并将 active baseline 从 V1R4C5 晋升至 V1R4C6。
|
||||||
|
B. 暂不批准,保持 V1R4C5,并说明需要处理的问题。
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorization Rules
|
||||||
|
|
||||||
|
Do not treat reactions, emojis, acknowledgements, or ambiguous replies as approval.
|
||||||
|
|
||||||
|
Valid approval must explicitly authorize both:
|
||||||
|
|
||||||
|
```text
|
||||||
|
create V1R4C6 Git tag
|
||||||
|
promote baseline V1R4C5 -> V1R4C6
|
||||||
|
```
|
||||||
|
|
||||||
|
If the owner approves only one operation, execute only the approved operation and stop before the other.
|
||||||
|
|
||||||
|
Before executing a consequential operation, record authorization in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/releases/V1R4C6-owner-approval.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Record only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
approved operation(s)
|
||||||
|
approved_at
|
||||||
|
approval channel = wechat
|
||||||
|
message/conversation identifier if available
|
||||||
|
owner identity reference without copying personal message content
|
||||||
|
memory search remains disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not store access tokens, cookies, phone numbers, or unrelated chat content.
|
||||||
|
|
||||||
|
## WeChat Failure Handling
|
||||||
|
|
||||||
|
If the configured WeChat channel is unavailable:
|
||||||
|
|
||||||
|
1. Do not claim that a message was sent.
|
||||||
|
2. Stop before tag/promotion.
|
||||||
|
3. Write the exact final authorization request to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/aios-gitops/runtime/state/V1R4C6-owner-confirmation-message.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Write notification failure details to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/aios-gitops/runtime/state/V1R4C6-wechat-notification-failure.json
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Report the failure through the active GitOps task/status channel.
|
||||||
|
|
||||||
|
## Final Stop Condition
|
||||||
|
|
||||||
|
Before explicit owner authorization, final state must remain:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current_baseline = V1R4C5
|
||||||
|
V1R4C6 tag = absent
|
||||||
|
memory search = disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
After owner authorization, follow the existing tag confirmation and baseline promotion controls. Do not bypass consequential-operation confirmation layers.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "branch",
|
||||||
|
"branch": "reconcile/v1r4c5-release-to-main-20260711",
|
||||||
|
"base": "main",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-10T23:56:41Z",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"owner_approval_required": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "branch",
|
||||||
|
"branch": "release/v2r1c0-tenant-cell-fabric-20260712",
|
||||||
|
"base": "main",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-12T14:39:23Z",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"owner_approval_required": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "branch",
|
||||||
|
"branch": "task/openclaw/v2r1c0-b3-full-rc1-integration-20260714T180646UTC8",
|
||||||
|
"base": "release/v2r1c0-tenant-cell-fabric-20260712",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-14T10:09:21Z",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"owner_approval_required": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "patch-1783707996243-33717dfe",
|
||||||
|
"patch_name": "v1r4c6-admin-search-agent-integration-20260711",
|
||||||
|
"branch": "release/v1r4c6-admin-search-agent-integration-20260711",
|
||||||
|
"risk": "high",
|
||||||
|
"description": "V1R4C6 bundled integration release: Admin Workbench visibility, SR-03 Search Gateway real provider binding, and Agent Platform Integration. Memory search remains disabled and user data isolation gates remain mandatory.",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-10T18:26:36Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# AIOS V2R1C0 RC1
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
OpenClaw Tenant Cell + Hermes Shared Agent Fabric.
|
||||||
|
|
||||||
|
## Included
|
||||||
|
|
||||||
|
- Signed Tenant Context
|
||||||
|
- Canonical Message Envelope
|
||||||
|
- Tenant Router
|
||||||
|
- OpenClaw Cell lifecycle adapter
|
||||||
|
- Exact Message Ledger
|
||||||
|
- Hermes tenant partitioned scheduler
|
||||||
|
- Memory Namespace policy
|
||||||
|
- Secret Scope references
|
||||||
|
- Append-only Audit Chain
|
||||||
|
- Hermes Profile isolation
|
||||||
|
- OpenClaw tenant boundary plugin
|
||||||
|
|
||||||
|
## Release Gates
|
||||||
|
|
||||||
|
Before production:
|
||||||
|
|
||||||
|
- automated isolation tests pass
|
||||||
|
- OpenClaw plugin runtime inspect passes
|
||||||
|
- Cell health probes pass
|
||||||
|
- running commit matches release commit
|
||||||
|
- rollback test passes
|
||||||
|
|
||||||
|
## Security Rules
|
||||||
|
|
||||||
|
Unknown provenance fails closed.
|
||||||
|
Quoted text is data, not command input.
|
||||||
|
Cross-tenant access is denied.
|
||||||
|
Shared Fabric does not share tenant state.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# AIOS V2R1C0 RC1 Live Status
|
||||||
|
|
||||||
|
Last verified: 2026-07-13 Europe/Amsterdam
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
- Release branch: `release/v2r1c0-tenant-cell-fabric-20260712`
|
||||||
|
- Verified branch tree: `6569065`
|
||||||
|
- Current promoted baseline: `V1R4C6`
|
||||||
|
- Production baseline state: not yet promoted to V2R1C0
|
||||||
|
|
||||||
|
This file is the single human-readable progress page for V2R1C0 RC1. Update it in the same commit that changes a release gate.
|
||||||
|
|
||||||
|
## Overall status
|
||||||
|
|
||||||
|
`DEVELOPMENT COMPLETE / EXECUTABLE ACCEPTANCE INCOMPLETE / NOT DEPLOYED`
|
||||||
|
|
||||||
|
## Component status
|
||||||
|
|
||||||
|
| Workstream | Status | Evidence | Remaining gate |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Architecture baseline | Done | `docs/architecture/AIOS_V2R1_OpenClaw_Tenant_Cell_Hermes_Shared_Agent_Fabric_Architecture.md` | none |
|
||||||
|
| Tenant Context | Done | `active-runtime/app/aios_v2r1/tenant_context.py` | executable security tests |
|
||||||
|
| Canonical Message | Done | `tenant_context.py` | channel adapter integration test |
|
||||||
|
| Tenant Router | Done | `router.py` | cross-tenant executable tests |
|
||||||
|
| Cell lifecycle adapter | Done | `fleet.py`, `cell_manager.py`, `cli.py` | real OpenClaw Fleet probe |
|
||||||
|
| Message Ledger | Done | `message_ledger.py` | concurrency and restart tests |
|
||||||
|
| Hermes Fabric | Done | `hermes_fabric.py` | worker/profile runtime test |
|
||||||
|
| Memory isolation | Done | `memory_scope.py` | cross-tenant executable tests |
|
||||||
|
| Secret scope | Done | `secret_scope.py` | secret broker integration |
|
||||||
|
| Audit chain | Done | `audit.py` | tamper executable test |
|
||||||
|
| Hermes Profile isolation | Done | `hermes_profile.py` | container/profile runtime test |
|
||||||
|
| OpenClaw boundary plugin | Code complete | `openclaw/plugins/aios-tenant-boundary/` | install + runtime hook inspection |
|
||||||
|
| Python automated tests | Blocked | no committed executable suite | add and run tests |
|
||||||
|
| Node plugin automated tests | Blocked | no committed executable suite | add and run tests |
|
||||||
|
| Staging Cell deployment | Not started | none | tests must pass first |
|
||||||
|
| Rollback rehearsal | Not started | none | staging deployment required |
|
||||||
|
| Merge to `main` | Not started | release branch differs from main | owner approval after gates pass |
|
||||||
|
| RC tag | Not created | no V2R1 tag | owner approval |
|
||||||
|
| Production deployment | Not started | baseline remains V1R4C6 | owner production approval |
|
||||||
|
|
||||||
|
## Release gates
|
||||||
|
|
||||||
|
- [ ] Python executable tests committed and passing
|
||||||
|
- [ ] Node executable tests committed and passing
|
||||||
|
- [ ] OpenClaw plugin installs successfully
|
||||||
|
- [ ] Runtime inspection proves hooks registered
|
||||||
|
- [ ] Tenant Cell status and doctor pass
|
||||||
|
- [ ] Staging message and tool-policy smoke tests pass
|
||||||
|
- [ ] Rollback succeeds
|
||||||
|
- [ ] Running commit matches approved release commit
|
||||||
|
- [ ] Owner approves merge, tag and production cutover
|
||||||
|
|
||||||
|
## Parallel work ownership
|
||||||
|
|
||||||
|
- Track A: Python isolation and message tests
|
||||||
|
- Track B: OpenClaw plugin tests and runtime inspection
|
||||||
|
- Track C: Cell staging deployment, health and rollback
|
||||||
|
- Track D: release evidence, digests and final acceptance report
|
||||||
|
|
||||||
|
## Update rule
|
||||||
|
|
||||||
|
Every worker must update this page with:
|
||||||
|
|
||||||
|
1. exact command,
|
||||||
|
2. exit code,
|
||||||
|
3. test counts,
|
||||||
|
4. commit hash,
|
||||||
|
5. artifact digest,
|
||||||
|
6. blocker or PASS result.
|
||||||
|
|
||||||
|
No item may be marked Done from prose-only reports.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# AIOS V2R1C0 RC1 Acceptance
|
||||||
|
|
||||||
|
## Tenant Isolation
|
||||||
|
|
||||||
|
- cross tenant memory access: DENY
|
||||||
|
- cross cell context: DENY
|
||||||
|
- cross tenant secret scope: DENY
|
||||||
|
- cross tenant queue claim: DENY
|
||||||
|
|
||||||
|
## Message Safety
|
||||||
|
|
||||||
|
- unsafe numeric message id: DENY
|
||||||
|
- exact reply resolution only: PASS
|
||||||
|
- quote text separate from command body: PASS
|
||||||
|
- heartbeat side effects: DENY
|
||||||
|
- cron side effects: DENY
|
||||||
|
- assistant output execution: DENY
|
||||||
|
|
||||||
|
## Hermes
|
||||||
|
|
||||||
|
- profile HOME isolation: PASS
|
||||||
|
- workspace escape: DENY
|
||||||
|
- tenant queue partition: PASS
|
||||||
|
|
||||||
|
## OpenClaw
|
||||||
|
|
||||||
|
- plugin manifest validation: PASS
|
||||||
|
- runtime hook registration: REQUIRED
|
||||||
|
- side effect approval: REQUIRED
|
||||||
|
|
||||||
|
## Release Gate
|
||||||
|
|
||||||
|
No production deployment until executable tests and runtime evidence pass.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Hermes Fabric Contract Tests
|
||||||
|
|
||||||
|
Required executable assertions:
|
||||||
|
|
||||||
|
1. Duplicate event_id must raise ConflictError, never raw sqlite3.IntegrityError.
|
||||||
|
2. Same tenant/cell/profile worker can claim its own queue.
|
||||||
|
3. Cross tenant queue claim must return no task or be denied.
|
||||||
|
4. Non-command messages cannot enqueue execution jobs.
|
||||||
|
5. Unknown provenance cannot create execution jobs.
|
||||||
|
|
||||||
|
Evidence required:
|
||||||
|
|
||||||
|
- command
|
||||||
|
- exit code
|
||||||
|
- pass count
|
||||||
|
- commit
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Gate 1 contract tests for the Hermes shared agent fabric."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from aios_v2r1.hermes_fabric import HermesFabric
|
||||||
|
from aios_v2r1.tenant_context import CanonicalMessage, ConflictError, ScopeError
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated_command(
|
||||||
|
event_id: str,
|
||||||
|
*,
|
||||||
|
command_capable: bool = True,
|
||||||
|
origin_type: str = "channel_user",
|
||||||
|
) -> CanonicalMessage:
|
||||||
|
return CanonicalMessage(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
principal_type="authenticated_user",
|
||||||
|
origin_type=origin_type,
|
||||||
|
channel="weixin",
|
||||||
|
channel_account_id="account-a",
|
||||||
|
peer_id="peer-a",
|
||||||
|
conversation_id="conversation-a",
|
||||||
|
session_id="session-a",
|
||||||
|
task_id=None,
|
||||||
|
event_id=event_id,
|
||||||
|
message_id="1234567890123456789",
|
||||||
|
reply_to_message_id=None,
|
||||||
|
direction="inbound",
|
||||||
|
command_capable=command_capable,
|
||||||
|
occurred_at="2026-07-13T00:00:00+00:00",
|
||||||
|
received_at="2026-07-13T00:00:01+00:00",
|
||||||
|
body="/run",
|
||||||
|
quoted_body=None,
|
||||||
|
adapter_name="weixin",
|
||||||
|
adapter_version="v2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HermesFabricContractTests(unittest.TestCase):
|
||||||
|
def test_duplicate_event_returns_sanitized_conflict(self) -> None:
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
fabric = HermesFabric(f"{directory}/fabric.sqlite3")
|
||||||
|
message = authenticated_command("event-a")
|
||||||
|
|
||||||
|
first = fabric.enqueue(message=message, profile_id="profile-a")
|
||||||
|
self.assertEqual(first.event_id, "event-a")
|
||||||
|
|
||||||
|
with self.assertRaises(ConflictError) as raised:
|
||||||
|
fabric.enqueue(message=message, profile_id="profile-a")
|
||||||
|
|
||||||
|
error = raised.exception
|
||||||
|
self.assertNotIsInstance(error, sqlite3.IntegrityError)
|
||||||
|
self.assertNotIn("sqlite", str(error).lower())
|
||||||
|
self.assertIsNone(error.__cause__)
|
||||||
|
self.assertIsNone(error.__context__)
|
||||||
|
|
||||||
|
def test_non_command_event_cannot_enqueue(self) -> None:
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
fabric = HermesFabric(f"{directory}/fabric.sqlite3")
|
||||||
|
|
||||||
|
with self.assertRaises(ScopeError):
|
||||||
|
fabric.enqueue(
|
||||||
|
message=authenticated_command(
|
||||||
|
"event-non-command", command_capable=False
|
||||||
|
),
|
||||||
|
profile_id="profile-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_heartbeat_provenance_cannot_enqueue(self) -> None:
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
fabric = HermesFabric(f"{directory}/fabric.sqlite3")
|
||||||
|
|
||||||
|
with self.assertRaises(ScopeError):
|
||||||
|
fabric.enqueue(
|
||||||
|
message=authenticated_command(
|
||||||
|
"event-heartbeat", origin_type="heartbeat"
|
||||||
|
),
|
||||||
|
profile_id="profile-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Gate 1 contract tests for authoritative TenantRouter isolation."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from aios_v2r1.router import TenantRouter
|
||||||
|
from aios_v2r1.tenant_context import CanonicalMessage, ConflictError, ScopeError
|
||||||
|
|
||||||
|
|
||||||
|
class MutableClock:
|
||||||
|
def __init__(self, now: datetime) -> None:
|
||||||
|
self.now = now
|
||||||
|
|
||||||
|
def __call__(self) -> datetime:
|
||||||
|
return self.now
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_message(*, tenant_id: str = "tenant-a", cell_id: str = "cell-a") -> CanonicalMessage:
|
||||||
|
return CanonicalMessage(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
cell_id=cell_id,
|
||||||
|
principal_id="user-a",
|
||||||
|
principal_type="authenticated_user",
|
||||||
|
origin_type="channel_user",
|
||||||
|
channel="weixin",
|
||||||
|
channel_account_id="account-a",
|
||||||
|
peer_id="peer-a",
|
||||||
|
conversation_id="conversation-a",
|
||||||
|
session_id="session-a",
|
||||||
|
task_id=None,
|
||||||
|
event_id="event-a",
|
||||||
|
message_id="message-a",
|
||||||
|
reply_to_message_id=None,
|
||||||
|
direction="inbound",
|
||||||
|
command_capable=True,
|
||||||
|
occurred_at="2026-07-13T00:00:00+00:00",
|
||||||
|
received_at="2026-07-13T00:00:01+00:00",
|
||||||
|
body="hello",
|
||||||
|
quoted_body=None,
|
||||||
|
adapter_name="weixin",
|
||||||
|
adapter_version="v2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TenantRouterIsolationContractTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.directory = TemporaryDirectory()
|
||||||
|
self.addCleanup(self.directory.cleanup)
|
||||||
|
self.database = f"{self.directory.name}/router.sqlite3"
|
||||||
|
self.router = TenantRouter(self.database)
|
||||||
|
self.router.register_tenant("tenant-a", "cell-a")
|
||||||
|
self.router.register_tenant("tenant-b", "cell-b")
|
||||||
|
self.router.bind_channel(
|
||||||
|
tenant_id="tenant-a", channel="weixin", channel_account_id="account-a"
|
||||||
|
)
|
||||||
|
self.router.bind_channel(
|
||||||
|
tenant_id="tenant-b", channel="weixin", channel_account_id="account-b"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_channel_account_cannot_bind_two_tenants(self) -> None:
|
||||||
|
with self.assertRaises(ConflictError):
|
||||||
|
self.router.bind_channel(
|
||||||
|
tenant_id="tenant-b", channel="weixin", channel_account_id="account-a"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cell_cannot_bind_two_tenants(self) -> None:
|
||||||
|
with self.assertRaises(ConflictError):
|
||||||
|
self.router.register_tenant("tenant-c", "cell-a")
|
||||||
|
|
||||||
|
def test_principal_cannot_bind_across_tenant_boundary(self) -> None:
|
||||||
|
with self.assertRaises(ScopeError):
|
||||||
|
self.router.bind_principal(
|
||||||
|
tenant_id="tenant-b",
|
||||||
|
channel="weixin",
|
||||||
|
channel_account_id="account-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_cannot_cross_cell(self) -> None:
|
||||||
|
with self.assertRaises(ScopeError):
|
||||||
|
self.router.bind_session(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
session_id="session-a",
|
||||||
|
cell_id="cell-b",
|
||||||
|
principal_id="user-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_cannot_cross_principal(self) -> None:
|
||||||
|
self.router.bind_session(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
session_id="session-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ConflictError):
|
||||||
|
self.router.bind_session(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
session_id="session-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-b",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_expired_session_is_rejected_without_private_storage_access(self) -> None:
|
||||||
|
clock = MutableClock(datetime(2026, 7, 13, tzinfo=timezone.utc))
|
||||||
|
router = TenantRouter(f"{self.directory.name}/expiry.sqlite3", clock=clock)
|
||||||
|
router.register_tenant("tenant-a", "cell-a")
|
||||||
|
router.bind_channel(
|
||||||
|
tenant_id="tenant-a", channel="weixin", channel_account_id="account-a"
|
||||||
|
)
|
||||||
|
router.bind_session(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
session_id="session-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
)
|
||||||
|
clock.now += timedelta(days=2)
|
||||||
|
|
||||||
|
with self.assertRaises(ScopeError):
|
||||||
|
router.resolve(
|
||||||
|
channel="weixin",
|
||||||
|
channel_account_id="account-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
session_id="session-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_self_reported_tenant_or_cell_mismatch_fails_against_authority(self) -> None:
|
||||||
|
self.router.bind_session(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
session_id="session-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
)
|
||||||
|
expected = "message claims do not match authoritative route"
|
||||||
|
with self.assertRaisesRegex(ScopeError, expected):
|
||||||
|
self.router.assert_message_route(canonical_message(tenant_id="tenant-b"))
|
||||||
|
with self.assertRaisesRegex(ScopeError, expected):
|
||||||
|
self.router.assert_message_route(canonical_message(cell_id="cell-b"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Gate 1 contract tests for canonical tenant-context signatures."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from aios_v2r1.tenant_context import TenantContext, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TenantContextSignatureContractTests(unittest.TestCase):
|
||||||
|
key = b"k" * 32
|
||||||
|
now = datetime(2026, 7, 13, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.context = TenantContext(
|
||||||
|
tenant_id="tenant-a",
|
||||||
|
cell_id="cell-a",
|
||||||
|
principal_id="user-a",
|
||||||
|
principal_type="user",
|
||||||
|
session_id="session-a",
|
||||||
|
task_id=None,
|
||||||
|
channel="hermes",
|
||||||
|
channel_account_id="account-a",
|
||||||
|
issued_at="2026-07-13T00:00:00+00:00",
|
||||||
|
expires_at="2026-07-13T00:05:00+00:00",
|
||||||
|
nonce="nonce-b",
|
||||||
|
)
|
||||||
|
self.token = self.context.sign(self.key)
|
||||||
|
|
||||||
|
def test_valid_canonical_token_verifies(self) -> None:
|
||||||
|
verified = TenantContext.verify(self.token, self.key, now=self.now)
|
||||||
|
|
||||||
|
self.assertEqual(verified, self.context)
|
||||||
|
|
||||||
|
def test_rejects_padded_signature_encoding(self) -> None:
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
TenantContext.verify(f"{self.token}=", self.key, now=self.now)
|
||||||
|
|
||||||
|
def test_rejects_standard_base64_signature_encoding(self) -> None:
|
||||||
|
payload, signature = self.token.rsplit(".", 1)
|
||||||
|
standard_signature = signature.replace("-", "+").replace("_", "/")
|
||||||
|
self.assertNotEqual(signature, standard_signature)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
TenantContext.verify(
|
||||||
|
f"{payload}.{standard_signature}", self.key, now=self.now
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"version": "V1R4C5",
|
||||||
|
"codename": "C5 reliability-security-admin",
|
||||||
|
"status": "active-baseline",
|
||||||
|
"tag": "V1R4C5",
|
||||||
|
"tag_ref": "refs/tags/V1R4C5",
|
||||||
|
"tag_object": "ac9bdacb19ff66cba8046d75358866f0f0fffb82",
|
||||||
|
"previous_version": "V1R4C3",
|
||||||
|
"rollback_version": "V1R4C3",
|
||||||
|
"baseline": "V1R4C3",
|
||||||
|
"release_reason": "reliability, watcher cleanup, user data isolation gate, and admin workbench read-only preparation; memory search disabled",
|
||||||
|
"purpose": "stabilize autonomous operations, enforce user isolation gate, prepare admin workbench read-only for C6",
|
||||||
|
"changes": [
|
||||||
|
"reliability: idle container GC with nixos-activate fallback retention policy and CI reset",
|
||||||
|
"watcher: remove redundant local async watcher, consolidate to server-pushed events",
|
||||||
|
"security: user data isolation gate (read-only admin panel, fb isolation check)",
|
||||||
|
"admin workbench: read-only preparation layout for V1R4C6 admin-search integration",
|
||||||
|
"governance: final owner approval via Owner Gate Web UI"
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
"VERSION",
|
||||||
|
"docs/releases/V1R4C5-acceptance-evidence.md",
|
||||||
|
"docs/releases/V1R4C5-final-owner-approval.md"
|
||||||
|
],
|
||||||
|
"validated_actions": [],
|
||||||
|
"secret_plaintext_written": false,
|
||||||
|
"owner_approval_required": true,
|
||||||
|
"owner_notification_channels": ["wechat", "web_ui_audit"],
|
||||||
|
"constraints_original": {
|
||||||
|
"memory_search": "remain disabled",
|
||||||
|
"sr04": "disable read-only visibility slice if RBAC or isolation check fails",
|
||||||
|
"gitea": "enter autonomous safe mode if branch consistency drift detected"
|
||||||
|
},
|
||||||
|
"promoted_at": "2026-07-11T02:17:31+08:00",
|
||||||
|
"promoted_by": "openclaw/admin",
|
||||||
|
"created_at": "2026-07-11T01:40:00+08:00",
|
||||||
|
"author": "openclaw"
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"version": "V1R4C6",
|
||||||
|
"codename": "admin-search-agent-integration",
|
||||||
|
"status": "release-candidate-started",
|
||||||
|
"base_baseline": "V1R4C5",
|
||||||
|
"created_at": "2026-07-11",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"release_type": "controlled-bundled-integration",
|
||||||
|
"promotion_policy": "owner-gated",
|
||||||
|
"owner_decision": {
|
||||||
|
"accepted_scope": true,
|
||||||
|
"requested_delivery": "admin workbench, SR-03, and Agent Platform Integration in one release train",
|
||||||
|
"memory_search_enabled": false,
|
||||||
|
"force_push_allowed": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"included_groups": [
|
||||||
|
{
|
||||||
|
"id": "V1R4C6-A",
|
||||||
|
"name": "Admin Workbench and Portal AIOS Read-Only Rollout",
|
||||||
|
"branches": [
|
||||||
|
"feature/v1r4c4-sr04-admin-workbench-visibility-20260711",
|
||||||
|
"feature/v1r4c4-sr04-portal-aios-visibility-20260710",
|
||||||
|
"patch/user-portal-me-route-diagnostics-20260709",
|
||||||
|
"patch/user-portal-rbac-ops-dashboard",
|
||||||
|
"patch/agent-observability-webui",
|
||||||
|
"patch/web-ui-complete-gitops-dashboard-20260706"
|
||||||
|
],
|
||||||
|
"risk": "medium",
|
||||||
|
"delivery": "administrator-visible task progress, process diagrams, health, agent observability, and portal AIOS entry points",
|
||||||
|
"guardrails": [
|
||||||
|
"read-only first",
|
||||||
|
"RBAC enforced",
|
||||||
|
"empty states explicit",
|
||||||
|
"no cross-user or cross-project data exposure"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "V1R4C6-B",
|
||||||
|
"name": "SR-03 Search Gateway Real Provider Binding",
|
||||||
|
"branches": [
|
||||||
|
"feature/v1r4c4-sr02-search-gateway-resilience-20260710",
|
||||||
|
"patch/search-gateway-governed-general-capability",
|
||||||
|
"patch/gitops-change-archive-audit"
|
||||||
|
],
|
||||||
|
"risk": "high",
|
||||||
|
"delivery": "real provider binding, governed search gateway, resilience, and audit trail",
|
||||||
|
"guardrails": [
|
||||||
|
"user data isolation gate required",
|
||||||
|
"scope filter before ranking",
|
||||||
|
"provenance required",
|
||||||
|
"no global memory/vector index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "V1R4C6-C",
|
||||||
|
"name": "Agent Platform Integration",
|
||||||
|
"branches": [
|
||||||
|
"patch/agent-event-bus-unified",
|
||||||
|
"patch/agent-gpt-message-plane",
|
||||||
|
"patch/autonomous-controller-dispatch-20260709",
|
||||||
|
"patch/self-evolution-agent-orchestration-policy",
|
||||||
|
"patch/aios-autonomous-orchestrator-architecture-review-v1"
|
||||||
|
],
|
||||||
|
"risk": "high",
|
||||||
|
"delivery": "agent event bus, GPT message plane, dispatch, orchestration policy, and architecture record",
|
||||||
|
"guardrails": [
|
||||||
|
"controller safe-mode respected",
|
||||||
|
"event/message scopes enforced",
|
||||||
|
"owner gate for consequential operations",
|
||||||
|
"observability before autonomous expansion"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"excluded": [
|
||||||
|
"memory search activation",
|
||||||
|
"global vector index",
|
||||||
|
"full cross-session recall",
|
||||||
|
"unscoped search over memory",
|
||||||
|
"bulk merge of test/smoke/auth branches",
|
||||||
|
"Gitea DB mutation",
|
||||||
|
"force-push reconciliation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"acceptance_required": [
|
||||||
|
"getRuntimeStatus active",
|
||||||
|
"getBaseline reports V1R4C5 before integration and V1R4C6 after promotion",
|
||||||
|
"Admin workbench RBAC read-only check passes",
|
||||||
|
"Portal AIOS manifest/dashboard/health routes verified",
|
||||||
|
"Flow/process diagram renders or returns explicit empty state",
|
||||||
|
"Search gateway provider binding returns scoped, provenanced results only",
|
||||||
|
"User data isolation acceptance remains green",
|
||||||
|
"Agent event bus and GPT message plane tests pass",
|
||||||
|
"Autonomous controller dispatch respects safe mode",
|
||||||
|
"Memory search remains disabled"
|
||||||
|
],
|
||||||
|
"deployment_constraints": {
|
||||||
|
"AIOS_MEMORY_ENABLED": false,
|
||||||
|
"memory_search_activation_requires_future_owner_approval": true,
|
||||||
|
"force_push_allowed": false,
|
||||||
|
"gitea_db_mutation_requires_owner_admin_approval": true,
|
||||||
|
"admin_workbench_mode": "read_only_first",
|
||||||
|
"search_provider_scope": "scoped_only"
|
||||||
|
},
|
||||||
|
"rollback_policy": {
|
||||||
|
"admin_workbench": "disable new menu/routes if RBAC or route acceptance fails",
|
||||||
|
"search_gateway": "fallback to disabled provider binding, not memory search",
|
||||||
|
"agent_platform": "disable dispatch expansion and keep event/message plane read-only if safety tests fail",
|
||||||
|
"memory_search": "remain disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user