Compare commits

..
Author SHA1 Message Date
Codex bd45c0ad77 test(aios-v2): 固化回滚执行模式验收证据 2026-07-21 12:21:16 +08:00
Codex c81d4f40f3 merge(deploy): 修复回滚脚本执行模式 2026-07-21 12:20:52 +08:00
Codex ea2a466d96 fix(deploy): 保留回滚健康脚本执行模式 2026-07-21 12:20:52 +08:00
Codex 63b31165b7 test(aios-v2): 固化 Key ID 修复验收证据 2026-07-21 12:16:10 +08:00
Codex 07b5c2c9c2 merge(aios-v2): 修复 OpenClaw Key ID 传播 2026-07-21 12:15:48 +08:00
Codex d71b7903df fix(aios-v2): 传播 OpenClaw 签名 Key ID 2026-07-21 12:15:48 +08:00
Codex 79fd83cd61 test(aios-v2): 固化 Runtime HTTP 离线验收证据 2026-07-21 12:11:42 +08:00
Codex fedb6b08fb merge(openclaw): 接入签名 Runtime HTTP 合同 2026-07-21 12:05:52 +08:00
Codex 83f8f878c7 feat(openclaw): 增加签名 Runtime HTTP 合同 2026-07-21 12:05:29 +08:00
Codex 1352a664ab test(aios-v2): 固化 HTTP 修复后验收证据 2026-07-21 11:40:16 +08:00
Codex 57209760cd merge(bugfix): 修复真实 HTTP 身份头解析 2026-07-21 11:39:44 +08:00
Codex 850bb669b7 fix(aios-v2): 规范化 HTTP 身份头大小写 2026-07-21 11:39:43 +08:00
Codex 9c20bf1362 test(aios-v2): 固化 Gate 与离线验收证据 2026-07-21 11:29:56 +08:00
Codex 09fa4f9d73 docs(aios-v2): 补齐核心模型中文追踪 2026-07-21 11:25:34 +08:00
Codex 816be7bec5 docs(agent): 固化 provenance 清障与 Gate 启动证据 2026-07-21 11:23:27 +08:00
Codex 25c25abd3e chore(aios): provenance openclaw-v2r1-20260721T031523Z heartbeat.json 2026-07-21 11:18:57 +08:00
Codex 8edb96a0fa merge(security): 纳入 provenance service-user Git guard 2026-07-21 11:18:02 +08:00
Codex 695a846645 fix(openclaw): 限定服务用户 Git safe directory 2026-07-21 11:18:02 +08:00
Codex d8f01f745d merge(security): 修复 provenance plugin runtime 合同 2026-07-21 11:11:34 +08:00
Codex c75fa77898 fix(openclaw): 声明 provenance writer 工具合同 2026-07-21 11:11:33 +08:00
Codex 9206b883ab merge(security): 纳入 OpenClaw run provenance writer 2026-07-21 11:06:34 +08:00
Codex b199935db8 feat(openclaw): 增加 Owner 签名 provenance writer 2026-07-21 11:06:17 +08:00
AIOS-Owner f7307c82b2 task(agent): 固化四项清障与 provenance 阻塞证据 2026-07-20 18:17:49 +08:00
AIOS-Owner bf01a13f3a docs(aios-v2): 记录四项阻塞清除与 provenance 剩余门禁 2026-07-20 18:17:06 +08:00
AIOS-Owner 8a36f7832b fix(aios-v2): 对齐 Ed25519 Candidate 签名合同 2026-07-20 18:14:42 +08:00
AIOS-Owner f1aeefff20 task(agent): 记录 Candidate 合并与 Worktree 修复证据 2026-07-20 17:39:16 +08:00
AIOS-Owner f55dfd975e merge(candidate): 同步最新 AIOS V2R1 Candidate 2026-07-20 17:38:42 +08:00
Codex ba7669c3b4 docs(aios-v2): 更新监督状态与阻塞处置清单 2026-07-20 17:22:32 +08:00
Windows ChatGPT Owner Supervisor bd2784015d chore(evidence): refine AIOS V2 signing-material blocker 2026-07-20 17:05:09 +08:00
Windows ChatGPT Owner Supervisor 1aa71f233d chore(evidence): block OpenClaw deployment with verified owner findings 2026-07-20 17:02:18 +08:00
Codex 558dc932fa chore(evidence): Phase 4 - Secret reference configuration complete
- Created /etc/aios-v2/aios-v2.env with credential ref (not plaintext)
- Saved Gitea deploy token to protected file under /etc/openclaw/secrets/
- Credential rotation completed: new token with minimal scope (write:repository,read:repository)
- Env file references AIOS_OPENCLAW_URL, CREDENTIAL_REF, SIGNING_MATERIAL_FILE, SIGNING_KEY_ID
2026-07-20 16:54:12 +08:00
Codex 5fd5ae0a33 chore(evidence): add claim, heartbeat, progress, update status for task AIOS-V2R1-PRODUCTION-DEPLOY-20260720
Phase 1 completed: worktree created, claim written, branches synced to Gitea, OID verified.
Phase 2-3 completed: branch sync & Gitea OID verification all matched.
Next: Phase 4 - Configure Secret references.
2026-07-20 16:48:33 +08:00
Codex 81572b8231 security(openclaw): record restored Gitea credential without secret value 2026-07-20 15:37:29 +08:00
Codex 8c3d80fe53 task(openclaw): 下发执行器启动、Secret 引用与 Gitea 同步在线指令 2026-07-20 14:34:13 +08:00
Codex 99bcc03b40 task(openclaw): 初始化无人值守任务状态 2026-07-20 12:07:46 +08:00
Codex c6e7edcf54 task(openclaw): 写入预发布无人值守任务载荷 2026-07-20 12:07:36 +08:00
Codex 634f6b164f docs(candidate): 新增完整中文生产候选报告 2026-07-20 12:06:55 +08:00
Codex c9ce12d102 deploy(candidate): 新增中文全量验收入口 2026-07-20 12:05:38 +08:00
Codex db50d2fa5a tasks(aios-v2): 完整中文化 Codex 生产监督任务 2026-07-20 12:04:43 +08:00
Codex 63fd23a464 tasks(aios-v2): 完整中文化 OpenClaw 生产部署任务 2026-07-20 12:04:11 +08:00
Codex 2e03ed74f6 feat(governance): 新增文档与代码匹配门禁 2026-07-20 12:03:36 +08:00
Codex b86f51c9ab docs(aios-v2): 新增 ADR-006 候选持久化策略 2026-07-20 12:01:49 +08:00
Codex 09448a5d3d docs(aios-v2): 完整中文化 ADR-005 2026-07-20 12:01:05 +08:00
Codex 18c719b2c6 docs(aios-v2): 完整中文化 ADR-004 2026-07-20 12:00:39 +08:00
Codex b0a78fa9a8 docs(aios-v2): 完整中文化 ADR-003 2026-07-20 12:00:07 +08:00
Codex 0aca7dbcfc docs(aios-v2): 完整中文化 ADR-002 2026-07-20 11:59:34 +08:00
Codex 3f84d8991e docs(aios-v2): 完整中文化 ADR-001 2026-07-20 11:59:10 +08:00
Codex 5ac8f3ea6f docs(aios-v2): 完整中文化 ADR 索引 2026-07-20 11:58:39 +08:00
Codex bfb1c2fd52 docs(aios-v2): 新增完整中文文档治理规范 2026-07-20 11:57:51 +08:00
Codex cbfe1211c1 docs(aios-v2): 将文档索引完整转换为中文 2026-07-20 11:56:30 +08:00
Codex efd99e620c docs(aios-v2): add ADR-004 tenant isolation decision 2026-07-20 11:53:04 +08:00
Codex c464560a0f task(openclaw): launch AIOS V2R1 pre-production deployment 2026-07-20 11:49:09 +08:00
56 changed files with 4846 additions and 420 deletions
+412
View File
@@ -0,0 +1,412 @@
# OpenClaw 在线执行指令:启动执行器、配置 Secret 引用并确认 Gitea 同步
- 任务 ID:`AIOS-V2R1-PRODUCTION-DEPLOY-20260720`
- 执行器:OpenClaw
- 任务分支:`task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 语言:中文
- 执行模式:无人值守、证据驱动、默认拒绝
## 1. 任务目标
一次性完成以下工作:
1. 发现并启动环境中已注册的 OpenClaw 在线执行器;
2. 领取本任务并持续写入 claim、heartbeat、progress、blocker 和 result;
3. 将任务分支同步到最新 Candidate HEAD;
4. 确认 Candidate、OpenClaw Task、Codex Task 三个分支已同步到 Gitea,且本地与远端 OID 完全一致;
5. 从已批准的 Secret Provider、Credential Store 或受保护文件中解析 OpenClaw Runtime 配置;
6. 只把 Secret 引用和受保护文件路径写入 AIOS 环境配置,不得把明文 Secret 写入 Git、日志、Evidence 或终端输出;
7. 执行文档—代码检查、候选测试、全量验收和预发布健康检查;
8. 对低风险部署或 Runtime 缺陷创建 Patch 并修复;
9. 把所有修改和阻塞提交到系统,交由 Codex 统一审视。
## 2. 不可违反的边界
- 不得直接修改或 push `main`;
- 不得使用 `--force` 或改写共享分支历史;
- 不得创建生产 Tag;
- 不得切换生产流量,除非 AIOS Owner 后续明确批准;
- 不得提交 Token、密码、API Key、私钥、签名材料或 Secret 内容;
- 不得在日志中执行 `env`、`set`、`printenv` 或输出 Secret 文件内容;
- 不得修改 AIOS 唯一控制面、多租户或审批边界;
- 发现架构、安全或跨模块阻塞时必须升级 Codex;
- 未生成 claim/heartbeat 证据前不得标记为运行中;
- 未通过验收前不得标记为完成。
## 3. 第一阶段:执行器发现与领取
### 3.1 发现执行器入口
使用当前环境中已经注册和批准的 OpenClaw 执行入口。可检查:
- systemd 服务;
- 容器或 Compose 服务;
- 已安装的 OpenClaw CLI;
- AIOS Agent Runner;
- 已配置的队列消费者或调度器。
不得臆造不存在的命令、服务名或路径。
如果找不到可执行入口,立即创建 Blocker:
```json
{
"blocker_id": "BLK-EXECUTOR-ENTRYPOINT-MISSING",
"severity": "high",
"description": "环境中未发现已注册的 OpenClaw 执行器入口",
"owner": "AIOS 运维负责人",
"exit_condition": "提供或启用已批准的 OpenClaw systemd、容器、CLI 或 Agent Runner 入口"
}
```
状态更新为 `blocked`,不得伪报已经启动。
### 3.2 启动执行器
发现入口后:
1. 验证配置来源和运行用户;
2. 启动或恢复执行器;
3. 验证进程/容器处于运行状态;
4. 验证执行器能够读取本任务分支;
5. 立即写入 `.agent/claim.json`;
6. 更新 `.agent/status.json`:`queued -> claimed -> running`;
7. 写入首次 `.agent/heartbeat.json`;
8. 将证据提交到任务分支并同步 Gitea。
`claim.json` 至少包含:
```json
{
"schema": "aios.v2.agent-claim.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"executor": "openclaw",
"task_branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"candidate_branch": "integration/aios-v2r1-cto-production-candidate-20260720",
"candidate_commit": "<领取时实际 Candidate OID>",
"claimed_at": "<UTC ISO-8601>",
"executor_instance": "<非敏感实例标识>"
}
```
Heartbeat 间隔不得超过 300 秒。
## 4. 第二阶段:同步最新 Candidate
### 4.1 获取最新引用
只更新 remote-tracking ref,不得 fetch 到已经被 Worktree 检出的本地 Branch Ref。
执行原则:
```text
fetch remote candidate -> refs/remotes/<remote>/<candidate>
```
不得使用会触发以下错误的流程:
```text
refusing to fetch into branch checked out at worktree
```
### 4.2 更新任务分支
将最新 Candidate 合并到 OpenClaw 任务分支:
- 使用普通 merge 或团队批准的线性更新方式;
- 不得 force reset 已经提交的 Agent Evidence;
- 发生冲突时停止自动扩大修改范围;
- 文档、任务载荷或架构冲突升级 Codex;
- 合并完成后把 `.agent/task.json` 与 `.agent/status.json` 中的 `candidate_commit` 更新为实际 Candidate OID。
### 4.3 必须记录
在 `.agent/progress.json` 记录:
- Candidate 本地 OID;
- Candidate Gitea OID;
- Task Branch 合并前后 OID;
- 修改文件;
- 是否发生冲突;
- 合并 Commit。
## 5. 第三阶段:确认 Gitea 同步
需要验证以下分支:
```text
integration/aios-v2r1-cto-production-candidate-20260720
task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720
task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720
```
目标仓库:
```text
g.cnnetmkt.top/git/aios/aios
```
### 5.1 同步规则
- 使用环境中已配置的 Gitea Remote 和凭据引用;
- 不在 Evidence 中记录带凭据的 Remote URL;
- 禁止 Force Push;
- Candidate 和 Task Branch 可以普通 Push;
- Push 失败时记录 stderr 的脱敏摘要,不得记录凭据。
### 5.2 OID 验证
每个分支必须同时取得:
```text
local_oid
gitea_oid
match = true/false
```
验收条件:
```text
local_oid == gitea_oid
```
建议通过 remote-tracking ref 或 `git ls-remote --heads` 取得远端 OID。输出中不得泄露认证信息。
如果 Push 或 OID 校验失败,创建 Blocker:
```json
{
"blocker_id": "BLK-GITEA-SYNC",
"severity": "high",
"description": "本地与 Gitea 分支 OID 不一致或无推送权限",
"owner": "GitOps/Gitea 运维负责人",
"exit_condition": "三个目标分支均完成普通推送且 local_oid 等于 gitea_oid"
}
```
在 Gitea OID 一致前,不得继续预发布部署。
## 6. 第四阶段:配置 Secret 引用
### 6.1 需要解析的配置
至少确认:
```text
AIOS_OPENCLAW_URL
AIOS_OPENCLAW_CREDENTIAL_REF
AIOS_OPENCLAW_SIGNING_MATERIAL_FILE
AIOS_OPENCLAW_SIGNING_KEY_ID
```
其中:
- `AIOS_OPENCLAW_URL` 是经过批准的 Runtime Endpoint;
- `AIOS_OPENCLAW_CREDENTIAL_REF` 只保存 Secret Provider 引用;
- `AIOS_OPENCLAW_SIGNING_MATERIAL_FILE` 指向仓库外的受保护文件;
- `AIOS_OPENCLAW_SIGNING_KEY_ID` 是 Key 标识,不是 Key 内容。
### 6.2 Secret 来源优先级
按以下顺序查找:
1. 已批准的运行时 Secret Store;
2. 已批准的 Credential Provider;
3. systemd Credential;
4. 只读 Secret Volume;
5. 仓库外受保护文件。
不得:
- 从 Git 历史恢复 Secret;
- 从日志、Shell History 或旧 Evidence 复制 Secret;
- 新建明文 Secret 并提交;
- 把 Secret 内容写入 `.env` 模板或任务 JSON。
### 6.3 配置文件与权限
生产候选环境文件:
```text
/etc/aios-v2/aios-v2.env
```
建议权限:
```text
owner: root
group: aios
mode: 0640 或更严格
```
签名材料文件:
```text
位于仓库外
owner: aios 或经批准的运行用户
mode: 0600 或更严格
```
只验证以下内容:
- 引用非空;
- 文件存在;
- 运行用户可读;
- 权限不宽松;
- 文件不在 Git Worktree;
- 不输出文件内容或长度特征。
### 6.4 缺少 Secret 时的行为
若无法从已批准来源解析任一必需配置,创建:
```json
{
"blocker_id": "BLK-SECRET-REF-MISSING",
"severity": "high",
"description": "缺少已批准的 OpenClaw Runtime Endpoint、Credential Ref、Signing File 或 Key ID",
"owner": "AIOS Secret/运维负责人",
"exit_condition": "四项配置均可从批准来源解析,且权限检查通过"
}
```
不得要求用户在聊天或 Git 中提供 Secret 明文。
## 7. 第五阶段:候选验收与预发布
在 Gitea OID 和 Secret 引用检查都通过后,依次执行:
```bash
python3 tools/aios_v2_docs_code_check.py \
--base-ref integration/aios-v2r1-cto-production-candidate-20260720
python3 -m unittest discover \
-s tests/aios_v2_candidate \
-p 'test_*.py' \
-v
bash deploy/aios-v2/acceptance/run-all.sh
```
部署后执行:
```bash
AIOS_RUN_LIVE_HEALTHCHECK=1 \
bash deploy/aios-v2/acceptance/run-all.sh
bash deploy/aios-v2/healthcheck/check.sh
```
必须验证:
- AIOS HTTP Health;
- SQLite `quick_check=ok`;
- GitOps Controller detached + clean;
- OpenClaw execute/status/cancel/timeout/health;
- Hermes claim/heartbeat/complete/fail/lease recovery;
- 服务重启后 Tenant/Agent/Task/Audit/Idempotency 恢复;
- 回滚后健康检查通过;
- 日志与 Evidence 无 Secret。
## 8. Bug 修复规则
OpenClaw 可自行修复:
- systemd、Docker、Compose、路径和权限;
- 环境变量名或启动参数;
- Healthcheck;
- 明确的小型 Runtime Adapter 兼容问题;
- 日志、错误消息和超时边界;
- 测试、部署或中文文档中的明确错误。
每个修复必须:
1. 创建 `patch/bugfix/*`;
2. 记录复现步骤;
3. 添加或更新测试;
4. 修改代码;
5. 同步 `docs/aios-v2/` 和追踪矩阵;
6. 执行文档门禁和相关测试;
7. 提交 Git;
8. 更新 `.agent/blockers.json` 和 `.agent/progress.json`;
9. 提交 Codex 审查。
以下问题必须升级 Codex:
- Control Plane、Task Lifecycle、Policy 或 Approval 变化;
- 多租户或身份边界变化;
- OpenClaw/Hermes Contract 不兼容变化;
- 数据迁移、持久化或回滚策略变化;
- 高风险安全问题;
- 需要修改 ADR 的问题;
- 超出低风险修复范围的阻塞。
## 9. 证据文件
任务分支必须持续维护:
```text
.agent/claim.json
.agent/heartbeat.json
.agent/progress.json
.agent/blockers.json
.agent/result.json
.agent/status.json
```
`result.json` 至少包含:
```json
{
"schema": "aios.v2.agent-result.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"final_status": "complete|failed|blocked",
"candidate_commit": "<OID>",
"task_commit": "<OID>",
"gitea_sync": {
"candidate_match": true,
"openclaw_task_match": true,
"codex_task_match": true
},
"secret_reference_status": "configured|blocked",
"executor_status": "running|blocked",
"test_status": "passed|failed|not_run",
"deployment_status": "passed|failed|not_run",
"rollback_status": "passed|failed|not_run",
"documentation_sync_status": "passed|failed",
"changed_files": [],
"commits": [],
"open_blockers": [],
"remaining_risks": [],
"completed_at": "<UTC ISO-8601>"
}
```
## 10. 完成条件
只有以下条件全部满足,任务才可标记为 `complete`:
- OpenClaw 执行器已启动并持续 Heartbeat;
- 已领取任务;
- 任务分支已合并最新 Candidate;
- Candidate、OpenClaw Task、Codex Task 在本地与 Gitea 的 OID 一致;
- Secret 引用和受保护文件权限检查通过;
- 文档—代码检查通过;
- 候选测试通过;
- 预发布部署和在线健康检查通过;
- Runtime 联调、重启和回滚演练通过;
- 所有修改、Blocker、Commit、测试和结果已回写;
- Codex 已能读取并审查完整证据。
如果缺少外部权限、执行入口或已批准 Secret,任务必须标记为 `blocked`,并准确说明 Owner 与解除条件,不得伪报完成。
## 11. Owner Gate
完成本指令不等于自动批准生产发布。以下动作仍需 AIOS Owner 明确批准:
- 合并 `main`;
- 创建生产 Tag;
- 正式生产部署;
- 生产流量切换;
- 接受高风险剩余问题。
+20 -19
View File
@@ -1,37 +1,38 @@
{
"schema": "aios.v2.agent-blockers.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"updated_at": "2026-07-21T04:30:00Z",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"updated_at": "2026-07-21T03:23:27Z",
"evidence_writer": "windows-chatgpt-owner",
"blockers": [
{
"blocker_id": "BLK-OPENCLAW-PLAINTEXT-SECRETS",
"severity": "high",
"status": "resolved",
"root_cause": "OpenClaw 三处 managed plaintext。",
"fix": "迁移为单一 file Secret Provider 并轮换 Gateway Token。",
"verification": "最终 audit 四类计数均为 0,OneAPI 实际调用通过。",
"root_cause": "OpenClaw managed config、auth profile store 与 models.json 存在三处明文凭据。",
"fix": "使用 OpenClaw 原生 secrets apply 迁移为单一 file Secret Provider;Gateway Token 已轮换,OneAPI 使用单一 auth-profile SecretRef。",
"verification": "audit plaintext/unresolved/shadowed/legacy 全部为 0,实际 OneAPI agent 调用通过。",
"commit": "bf01a13f3aaf42a386232ed2f5409feb0f79fcf6",
"resolved_at": "2026-07-20T10:18:36Z"
"resolved_at": "2026-07-20T10:17:49Z"
},
{
"blocker_id": "BLK-AIOS-V2-SIGNING-MATERIAL-MISSING",
"severity": "high",
"status": "resolved",
"root_cause": "Ed25519 Key ID 与 HMAC 实现不一致且材料缺失。",
"fix": "真实 Ed25519 合同及受保护材料。",
"verification": "0600、sign/verify、篡改拒绝通过。",
"root_cause": "Key ID 标称 Ed25519,但代码实际使用 HMAC 且材料文件缺失。",
"fix": "签名合同改为真实 Ed25519,并由 Owner 在受保护边界生成运行材料。",
"verification": "aios:aios 0600;resolver sign/verify 与篡改拒绝通过。",
"commit": "8a36f7832bc105e4b9d32ffbe018abed2ff1b1ab",
"resolved_at": "2026-07-20T10:18:36Z"
"resolved_at": "2026-07-20T10:17:49Z"
},
{
"blocker_id": "BLK-CANDIDATE-NOT-MERGED",
"severity": "high",
"status": "resolved",
"root_cause": "OpenClaw 任务未包含 Candidate。",
"fix": "--no-ff 合并最新 Candidate。",
"verification": "Candidate 为 OpenClaw HEAD 祖先且 OID 同步。",
"root_cause": "OpenClaw 任务未包含最新 Candidate。",
"fix": "保护 Evidence 后以 --no-ff 合并 Candidate ba7669c。",
"verification": "Candidate 为任务分支祖先,普通 Push 后 local_oid 等于 gitea_oid。",
"commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"resolved_at": "2026-07-20T10:18:36Z"
"resolved_at": "2026-07-20T10:17:49Z"
},
{
"blocker_id": "BLK-OPENCLAW-RUN-PROVENANCE-MISSING",
@@ -41,17 +42,17 @@
"fix": "部署 aios-tenant-boundary rc2,增加 Ed25519 issuer/store、Run/Sender/OID/期限绑定、一次性 permit 与七路径专用 Writer。",
"verification": "Provenance openclaw-v2r1-20260721T031523Z 完成 heartbeat 写入、Commit、普通 Push 和审计;README.md 越权写入被拒绝;Node 测试 9/9。",
"commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb",
"resolved_at": "2026-07-21T03:23:44Z"
"resolved_at": "2026-07-21T03:23:27Z"
},
{
"blocker_id": "BLK-CONTROLLER-LEGACY-GITEA-CHECK",
"severity": "high",
"status": "resolved",
"root_cause": "硬编码旧仓库。",
"fix": "配置化 V2R1 OID 健康检查并使用只读 Credential。",
"verification": "12 项测试、detached clean、fully_synced。",
"root_cause": "Controller 硬编码旧 aios-v23 仓库与 API,无法区分本地和 Gitea OID。",
"fix": "配置化 V2R1 仓库/分支,使用只读 systemd Credential,部署到 detached clean 工作树。",
"verification": "12 项测试通过;gitea/branch_sync/oid_match 为 true,sync_status=fully_synced。",
"commit": "6188da26453505dacde9d6d298191437164c8d27",
"resolved_at": "2026-07-20T10:18:36Z"
"resolved_at": "2026-07-20T10:17:49Z"
}
]
}
+11 -8
View File
@@ -1,13 +1,16 @@
{
"schema": "aios.v2.agent-claim.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"executor": "codex",
"executor_instance": "windows-chatgpt-owner-supervisor",
"task_branch": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"executor": "openclaw",
"task_branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"candidate_branch": "integration/aios-v2r1-cto-production-candidate-20260720",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"claimed_at": "2026-07-20T08:51:25Z",
"owner_observation": "五项 Blocker、Gate A/B、离线验收、OpenClaw 在线合同、双服务重启与回滚均通过;Hermes 完整在线 Worker 生命周期仍缺服务端点证据,Codex 维持 STOP_PREPRODUCTION。",
"owner_observation_at": "2026-07-21T04:30:00Z",
"reviewed_openclaw_commit_before_evidence": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a"
"claimed_at": "2026-07-20T08:24:30Z",
"executor_instance": "openclaw-main-cn-network-mkt",
"owner_observation_at": "2026-07-21T03:23:27Z",
"owner_observation": "五项高风险 Blocker 已解除;Provenance Writer 真实写入、普通 Push、审计和越权拒绝通过。",
"evidence_writer": "windows-chatgpt-owner",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z"
}
+47 -8
View File
@@ -1,18 +1,57 @@
{
"schema": "aios.agent.credential-status.v1",
"task_id": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"last_updated": "2026-07-21T04:30:00Z",
"schema": "aios.v2.credential-status.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"credential_name": "openclaw-deploy-cli",
"credential_type": "gitea-api-token-reference",
"endpoints": [
"http://127.0.0.1:3000",
"https://g.cnnetmkt.top/git"
],
"reported_scope": "all",
"reported_status": "working",
"verification_source": "owner-reported",
"security_status": "secretref_resolved",
"reason": "令牌值曾在聊天中以明文出现,必须按已暴露凭据处理",
"required_actions": [
"已完成",
"新令牌已保存到受保护文件",
"令牌用于HTTPS Git认证"
],
"rotation_completed_at": "2026-07-20T08:48:00Z",
"new_token_created": "openclaw-deploy-task-commit",
"new_token_scope": "write:repository,read:repository",
"token_file_path": "/etc/openclaw/secrets/openclaw-gitea-deploy-token",
"token_file_owner": "root:root",
"token_file_mode": "0600",
"secret_provider_contract_path": "/etc/openclaw/SECRET_PROVIDER_CONTRACT.json",
"last_verified": "2026-07-21T03:23:27Z",
"verification_result": "HTTPS Git push successful with new token",
"env_file_reference": "AIOS_OPENCLAW_CREDENTIAL_REF=file:///etc/openclaw/secrets/openclaw-gitea-deploy-token",
"env_file_path": "/etc/aios-v2/aios-v2.env",
"env_file_mode": "0640 root:gitops",
"last_updated": "2026-07-21T03:23:27Z",
"old_credential_revoked": true,
"revoked": true,
"new_credential_name": "openclaw-deploy-cli-v2-20260720",
"rotated": true,
"new_account": "gitops-bot",
"scope": "write:repository",
"credential_ref": "/data/aios-gitops/runtime/secrets/openclaw-deploy-cli-v2-20260720",
"protected_file_owner": "root",
"protected_file_group": "gitops",
"protected_file_mode": "0640",
"verified_at": "2026-07-20T09:02:18Z",
"evidence_writer": "windows-chatgpt-owner",
"plaintext_findings_before": 3,
"plaintext_findings_after": 0,
"gateway_token_rotated": true,
"credentials_rotated": true,
"secret_values_recorded": false,
"signing_material_configured": true,
"signing_key_id": "openclaw-v2r1-candidate-ed25519",
"signing_permission_check": "passed",
"runtime_sign_verify_test": "passed",
"controller_credential_scope": "read:repository",
"run_provenance_status": "resolved",
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"openclaw_runtime_plugin_version": "2.1.0-rc.3",
"runtime_public_key_only_hot_path": true,
"secret_values_recorded_in_evidence": false
"provenance_public_key_configured": true,
"provenance_secret_values_recorded": false
}
+9 -7
View File
@@ -1,10 +1,12 @@
{
"current_step": "GATE_A_PENDING",
"evidence_writer": "aios_evidence_write",
"executor": "openclaw",
"heartbeat_at": "2026-07-21T03:23:27Z",
"heartbeat_source": "owner-signed-provenance-writer",
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"schema": "aios.v2.agent-heartbeat.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"executor": "codex",
"heartbeat_at": "2026-07-21T04:30:00Z",
"state": "reviewing",
"current_step": "FINAL_REVIEW_HERMES_ONLINE_GAP",
"reviewed_commit": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
"openclaw_evidence_commit": "0be3ba09e42b4f6db56461e3ca3a3fcc0a487f31"
"state": "running",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"valid_for_running": true
}
+45 -59
View File
@@ -1,55 +1,23 @@
{
"schema": "aios.v2.agent-progress.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"updated_at": "2026-07-21T04:30:00Z",
"state": "reviewing",
"candidate_oid": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"openclaw_task_oid_at_review": "0be3ba09e42b4f6db56461e3ca3a3fcc0a487f31",
"codex_task_oid_before_evidence": "be4726178aa8fa59321aafaabcff8a83653cc1b8",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"state": "running",
"status": "PARTIAL",
"updated_at": "2026-07-21T03:23:27Z",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
"gitea_sync": {
"candidate_match": true,
"openclaw_task_match": true,
"codex_task_match_before_this_evidence": true
"openclaw_task_match_before_owner_evidence": true,
"codex_task_match": true
},
"completed_checks": [
"worktree",
"candidate_merge",
"secrets_audit_zero",
"ed25519_sign_verify",
"controller_fully_synced",
"owner_signed_provenance_writer",
"unauthorized_path_denied",
"provenance_audit_chain",
"gate_a",
"gate_b_10_of_10",
"offline_acceptance_8_0_1",
"openclaw_online_contract",
"aios_openclaw_restart_recovery",
"controlled_rollback_and_forward"
],
"review_conclusion": "五项 Blocker、文档/代码 Gate、候选测试、离线验收、OpenClaw 在线合同、重启与回滚均通过;Hermes 完整在线 Worker 生命周期未形成服务端点证据,预发布仍停止。",
"check_time_window": {
"started_at": "2026-07-20T08:24:30Z",
"ended_at": "2026-07-20T09:03:08Z"
},
"operator": "windows-chatgpt-owner-supervisor",
"reviewer": "codex-supervisor + Spark architect/explorer/implementer/validator/docs_reporter",
"approver": "AIOS Owner gate recorded by Windows ChatGPT Owner Supervisor",
"evidence_artifacts": [
"task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720@1aa71f233d2c2a6ed7290d0a3ddcb04dcd84febe",
"task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720:.agent/",
"/data/aios-gitops/runtime/backups/gitea-db-before-openclaw-token-rotation-20260720T084251Z.sqlite3",
"/home/aiman/.ssh/known_hosts.aios-v2r1-backup-20260720T083647Z",
"/var/log/aios-openclaw-v2r1-evidence-repair-20260720.json"
],
"independent_reviews": {
"architect": "REJECT;只允许修复+复验,禁止生产放行。",
"explorer": "OpenClaw evidence OID 已重新闭环,但 Candidate 未合并、Secret 与预发布仍未确认。",
"implementer": "IMPLEMENTATION_SAFE_WITH_BLOCKERS;已实施动作无需立即回滚。",
"validator": "BLOCKED;Secret、环境、预发布门禁未通过。",
"docs_reporter": "已补齐时间窗、证据路径、operator/reviewer/approver、blocker 与 final_decision。"
},
"final_decision": "STOP_PREPRODUCTION",
"credential_rotation_status": "passed",
"executor_model_path": "passed",
"tests": "controller 12/12;provenance Node 9/9;真实签发/越权拒绝/heartbeat commit+push/审计通过;Gate A/B/离线 Acceptance 待执行",
"deployment": "aios-tenant-boundary rc2 已部署,runtime doctor 与 Gateway restart 通过",
"rollback": "两次验收脚本断言错误均实际触发备份回滚并恢复 rc1;修正后 rc2 部署成功",
"current_step": "GATE_A_PENDING",
"evidence_writer": "windows-chatgpt-owner",
"aios_v2_env_status": {
"exists": true,
"owner": "root",
@@ -57,17 +25,35 @@
"mode": "0640",
"four_fields_nonempty": true,
"credential_ref_target_exists": true,
"signing_material_target_exists": true
"signing_material_target_exists": false
},
"open_checks": [
"hermes_online_claim_start_heartbeat_complete_fail_cancel",
"hermes_online_lease_expiry_worker_crash_recovery",
"hermes_online_cell_profile_and_invalid_result_rejection"
],
"gate_a": "passed",
"gate_b": "passed_10_of_10",
"offline_acceptance": "passed_8_failed_0_warning_1",
"online_openclaw": "passed",
"restart_recovery": "passed",
"rollback_drill": "passed"
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z",
"worktree_repair": {
"status": "resolved",
"branch_unique_checkout": true,
"clean_before_merge": true,
"stale_lock_count": 0,
"prunable_count": 0,
"evidence_backup": "protected external backup created before merge",
"verified_at": "2026-07-20T09:39:16Z"
},
"release_recommendation": "STOP_PREPRODUCTION",
"gate_a_allowed": true,
"resolved_blockers": 5,
"open_blockers": 0,
"controller_patch_commit": "6188da26453505dacde9d6d298191437164c8d27",
"signing_contract_commit": "8a36f7832bc105e4b9d32ffbe018abed2ff1b1ab",
"secret_audit": {
"plaintext_before": 3,
"plaintext_after": 0,
"unresolved_after": 0,
"shadowed_after": 0,
"legacy_after": 0,
"gateway_token_rotated": true,
"secret_values_recorded": false
},
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"provenance_live_commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb"
}
+29 -17
View File
@@ -1,27 +1,39 @@
{
"schema": "aios.v2.agent-result.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"final_status": "PARTIAL",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"reviewed_commit": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
"gitea_sync": {
"candidate_match": true,
"openclaw_task_match": true,
"codex_task_match_before_this_evidence": true
"openclaw_task_match_before_owner_evidence": true,
"codex_task_match": true
},
"credential_rotation_status": "secretref_resolved",
"test_status": "Gate A 通过;Gate B 10/10;离线 8/0/1;OpenClaw 在线、重启恢复、回滚通过;Hermes 完整在线 Worker 生命周期未覆盖",
"deployment_status": "STOP_PREPRODUCTION",
"rollback_status": "Previous 2a2b702 与 Current b158041 双向演练通过且业务状态保留",
"documentation_sync_status": "updated",
"secret_reference_status": "resolved",
"executor_status": "running",
"test_status": "controller 12/12;provenance 9/9;live write/push/audit/deny passed;Gate A/B/offline pending",
"deployment_status": "provenance plugin rc2 active",
"rollback_status": "automatic plugin/config rollback exercised and passed",
"documentation_sync_status": "updated before Gate A",
"changed_files": [
".agent/claim.json",
".agent/heartbeat.json",
".agent/progress.json",
".agent/blockers.json",
".agent/result.json",
".agent/status.json",
".agent/credential-status.json"
],
"open_blockers": [],
"remaining_risks": [
"缺少 Owner-signed provenance issuer/store/writer 与越权拒绝证据"
],
"completed_at": "2026-07-21T03:23:27Z",
"evidence_writer": "windows-chatgpt-owner",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z",
"release_recommendation": "STOP_PREPRODUCTION",
"completed_at": "2026-07-21T04:30:00Z",
"openclaw_evidence_commit": "0be3ba09e42b4f6db56461e3ca3a3fcc0a487f31",
"independent_validation": "PASS_FOR_IMPLEMENTED_OPENCLAW_BOUNDARY",
"operator": "windows-chatgpt-owner-supervisor",
"reviewer": "codex-supervisor + Spark roles",
"approver": "AIOS Owner gate recorded",
"remaining_risk": "Hermes 完整在线 Worker 生命周期缺少可调用服务端点与实时证据",
"formal_online_acceptance": "partial"
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"provenance_live_commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb"
}
+13 -13
View File
@@ -1,18 +1,18 @@
{
"schema": "aios.v2.agent-status.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"executor": "codex",
"state": "reviewing",
"claimed_at": "2026-07-20T08:51:25Z",
"running_at": "2026-07-20T08:51:25Z",
"updated_at": "2026-07-21T04:30:00Z",
"last_heartbeat": "2026-07-21T04:30:00Z",
"current_step": "FINAL_REVIEW_HERMES_ONLINE_GAP",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"executor": "openclaw",
"state": "running",
"status": "running",
"claimed_at": "2026-07-20T08:24:30Z",
"updated_at": "2026-07-21T03:23:27Z",
"last_heartbeat": "2026-07-21T03:23:27Z",
"heartbeat_valid_for_running": true,
"current_step": "GATE_A_PENDING",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"reviewed_commit": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
"release_recommendation": "STOP_PREPRODUCTION",
"openclaw_evidence_commit": "0be3ba09e42b4f6db56461e3ca3a3fcc0a487f31",
"status": "reviewing"
,"formal_online_acceptance": "partial"
,"remaining_gap": "Hermes full online worker lifecycle"
"evidence_writer": "windows-chatgpt-owner",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z"
}
+18 -15
View File
@@ -1,31 +1,34 @@
{
"schema": "aios.v2.agent-task.v1",
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"display_name": "AIOS V2R1 Codex 架构监督、阻塞处理与发布审查任务",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"display_name": "AIOS V2R1 OpenClaw 预发布部署与小型缺陷修复任务",
"language": "zh-CN",
"executor": "codex",
"role": "architecture-supervisor",
"executor": "openclaw",
"role": "deployment-operator",
"state": "queued",
"candidate_branch": "integration/aios-v2r1-cto-production-candidate-20260720",
"candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"canonical_task": "tasks/codex-production-supervisor-task.yaml",
"supervises": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"canonical_task": "tasks/openclaw-production-deploy-task.yaml",
"supervisor_task": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"claim_required": true,
"heartbeat_interval_seconds": 300,
"evidence_directory": ".agent/evidence",
"instructions": [
"领取前读取候选分支完整中文文档、ADR、追踪矩阵和候选报告",
"领取前读取候选分支完整中文文档和生产候选报告",
"领取后写入 .agent/claim.json 并把状态改为 claimed",
"持续审阅 OpenClaw 的 claim、heartbeat、progress、blocker、commit 和 result",
"分析失败测试、架构偏移、多租户、安全和文档同步阻塞",
"需要修改时创建 Patch 分支、增加复现测试并同步文档",
"所有阻塞和审查结论必须提交到系统统一审视",
"禁止缺少证据时批准发布或削弱安全门禁"
"每五分钟或每个主要步骤写入 heartbeat 和 progress",
"所有部署修改、小型缺陷、阻塞、测试和结果必须提交到 Git",
"架构、控制面、多租户或安全问题必须升级 Codex",
"禁止提交明文密钥、直接修改 main 或绕过 GitOps"
],
"acceptance_commands": [
"python3 tools/aios_v2_docs_code_check.py --base-ref integration/aios-v2r1-cto-production-candidate-20260720",
"python3 -m unittest discover -s tests/aios_v2_candidate -p test_*.py -v",
"bash deploy/aios-v2/acceptance/run-all.sh"
"bash deploy/aios-v2/acceptance/run-all.sh",
"bash deploy/aios-v2/healthcheck/check.sh"
],
"created_at": "2026-07-20T00:00:00Z"
"created_at": "2026-07-20T00:00:00Z",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T03:26:12.120179Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "09fa4f9d73799a2e924153d198d8ebb09ff15cfd",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T03:40:03.455790Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "57209760cd159e564d7de1160090917dbbc7ce0a",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T04:11:34.291792Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "fedb6b08fbb8c825972f700318a81e902e89cda5",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T04:16:01.542048Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "07b5c2c9c28e7840b6261172a7d34aa7de7c48c9",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T04:21:06.025742Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "c81d4f40f3191037cb1a34f169e0a9ed7825bdbf",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T042105Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
+404
View File
@@ -0,0 +1,404 @@
# AIOS V2R1 生产候选报告
- 候选版本:`V2R1C0-candidate`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 报告语言:中文
- 架构负责人:AIOS V2 CTO
- 当前状态:代码与文档闭环已完成;精确 Commit 测试、真实 Runtime 联调和预发布验证待执行
## 1. 执行摘要
AIOS V2R1 Candidate 已完成以下核心建设:
- AIOS 唯一控制面;
- Tenant、Agent、Task、Policy、Approval 和 Audit 权威模型;
- SQLite WAL 候选持久化与重启恢复;
- Execution Orchestrator;
- OpenClaw 签名命令、Adapter、Transport、Health 和 Audit Callback;
- Hermes tenant/cell/profile 分区、Queue、Worker Lease、Heartbeat 和 Result;
- Agent Execution Evidence;
- Web Console;
- GitOps Status;
- systemd、Docker、Compose、Healthcheck 和 Rollback;
- 用户需求、目标架构、迁移策略、模块契约、追踪矩阵、文档治理和 ADR;
- 文档—代码匹配检查器;
- 全量验收入口;
- OpenClaw 预发布部署任务;
- Codex 架构监督和阻塞处理任务。
本报告不把“代码已提交”误报为“生产已验证”。以下状态仍需由无人值守任务执行并形成证据:
- 精确 Candidate Commit 的自动测试;
- 真实 OpenClaw Runtime 联调;
- Hermes 长任务、Worker 崩溃和 Lease Recovery 演练;
- systemd/Docker 预发布部署;
- 重启恢复与回滚演练;
- 本地仓库与 Gitea Candidate OID 一致验证;
- 身份代理、安全、性能和容量验证;
- Owner 对合并、Tag 和生产部署的批准。
## 2. 用户需求完成情况
| 需求 | 代码状态 | 测试状态 | 预发布状态 |
|---|---|---|---|
| AIOS 唯一控制面 | 已实现 | 已定义 | 待验证 |
| 复用 OpenClaw/Hermes/Agent Router | 已实现 | 已定义 | 待真实联调 |
| OpenClaw/Hermes 解耦 | 已实现 | 已定义 | 待旁路冻结验证 |
| 全面 Web 化 | 候选实现完成 | 冒烟测试待执行 | 待反向代理接入 |
| 多租户隔离 | 已实现 | Domain 负向测试已定义 | 待全链路安全验证 |
| 统一任务生命周期 | 已实现 | 已定义 | 待精确 Commit 执行 |
| 策略与审批 | 已实现 | 部分测试待补强 | 待身份代理验证 |
| 幂等与并发安全 | 已实现 | 基础测试已定义 | 待压力和并发验证 |
| 执行证据 | 已实现 | 已定义 | 待外部执行器持续回写 |
| GitOps 唯一流程 | 已实现并修复 Controller 冲突 | Runtime 部分验证 | 待双目标持续镜像证据 |
| 持久化与恢复 | 已实现 | 重启恢复测试已定义 | 待预发布演练 |
| 审计与合规 | 已实现候选 | 部分断言待补强 | 待不可变外部固化 |
| 密钥安全 | 引用模式已实现 | 基础扫描纳入验收 | 待 Secret Provider 联调 |
| 部署与回滚 | 资产完成 | 验收脚本已定义 | 待实际演练 |
| 文档与代码同步 | 已实现门禁 | 待精确 Commit 执行 | 待接入持续验收 |
## 3. 目标架构
```text
浏览器 / Web Console / Public API
|
v
AIOS 唯一控制面
Tenant | Agent | Task | Policy | Approval | Audit
|
v
Execution Orchestrator / Agent Router
/ \
v v
OpenClaw Adapter Hermes Boundary
| |
v v
OpenClaw Runtime Hermes Workers
\ /
v v
结果回写 + 审计 + 执行证据
```
权威边界:
- AIOS:租户、身份、Agent、任务、策略、审批、审计和执行证据;
- Agent Router:调度与路由;
- OpenClaw:签名命令执行、取消、状态、健康和回调;
- Hermes:队列、Worker、Lease、Heartbeat 和结果;
- Web:只调用 AIOS API;
- GitOps:所有代码、文档、部署和修复变更入口。
## 4. 实现清单
### 4.1 Control Plane
```text
active-runtime/app/aios_v2/control_plane.py
active-runtime/app/aios_v2/persistence.py
active-runtime/app/aios_v2/gateway.py
active-runtime/app/aios_v2/app.py
active-runtime/app/aios_v2/orchestrator.py
active-runtime/app/aios_v2/execution_tracking.py
active-runtime/app/aios_v2/gitops_status.py
active-runtime/app/aios_v2/console.py
```
### 4.2 OpenClaw
```text
active-runtime/app/aios_openclaw/contract.py
active-runtime/app/aios_openclaw/adapter.py
active-runtime/app/aios_openclaw/transport.py
active-runtime/app/aios_openclaw/health.py
active-runtime/app/aios_openclaw/callback.py
```
### 4.3 Hermes
```text
active-runtime/app/aios_hermes/contract.py
active-runtime/app/aios_hermes/adapter.py
active-runtime/app/aios_hermes/worker.py
```
### 4.4 测试与治理
```text
tests/aios_v2_candidate/test_candidate_flow.py
tools/aios_v2_docs_code_check.py
deploy/aios-v2/acceptance/run-all.sh
```
### 4.5 部署
```text
deploy/aios-v2/systemd/aios-v2.service
deploy/aios-v2/env/aios-v2.env.example
deploy/aios-v2/docker/Dockerfile
deploy/aios-v2/docker/compose.yaml
deploy/aios-v2/healthcheck/check.sh
deploy/aios-v2/rollback/rollback.sh
```
### 4.6 文档与任务
```text
docs/aios-v2/
tasks/openclaw-production-deploy-task.yaml
tasks/codex-production-supervisor-task.yaml
```
## 5. 多租户与安全设计
### 5.1 权威上下文
```text
tenant_id
principal_id
session_id
trace_id
task_id
policy_version
idempotency_key
```
### 5.2 OpenClaw
- HMAC-SHA256 规范化签名;
- 签名绑定 tenant/task/trace/action/payload/idempotency/time;
- Runtime Result 身份字段必须匹配;
- Secret 只通过受保护文件或引用解析。
### 5.3 Hermes
- tenant/cell/profile 分区;
- Worker Lease 与 Heartbeat;
- revision 乐观并发;
- Lease 过期自动回到 QUEUED;
- Worker 完成前停止 Heartbeat,避免 revision 竞态。
### 5.4 Web/API
- 可信代理注入 `X-AIOS-*`;
- 浏览器不选择租户;
- CSP、no-store、nosniff、DENY frame、no-referrer;
- 应用端口不得直接暴露公网。
### 5.5 GitOps
- Controller detached + clean;
- 禁止 fetch 到已检出本地分支;
- 所有修改进入分支、Commit、测试和统一审查;
- 明文 Secret 门禁;
- 文档变化与代码变化同步门禁。
## 6. 持久化策略
候选版:
- SQLite WAL;
- `synchronous=FULL`;
- `BEGIN IMMEDIATE`;
- 原子 Snapshot;
- schema version;
- 状态目录与代码版本分离;
- Execution Evidence 租户分区原子 JSON。
已知限制:
- 单进程候选优先;
- 尚无生产 RLS、多实例 HA、PITR 和容量证据;
- 后续必须通过新 ADR 迁移到生产数据层。
## 7. 测试与验收
### 7.1 文档—代码匹配
```bash
python3 tools/aios_v2_docs_code_check.py \
--base-ref integration/aios-v2r1-cto-production-candidate-20260720
```
### 7.2 候选测试
```bash
python3 -m unittest discover \
-s tests/aios_v2_candidate \
-p 'test_*.py' \
-v
```
覆盖:
- OpenClaw 完整执行;
- Hermes 完整执行;
- 跨租户拒绝;
- SQLite 重启恢复;
- Execution Evidence。
### 7.3 全量验收
```bash
bash deploy/aios-v2/acceptance/run-all.sh
```
可选在线健康检查:
```bash
AIOS_RUN_LIVE_HEALTHCHECK=1 \
bash deploy/aios-v2/acceptance/run-all.sh
```
### 7.4 健康检查
```bash
bash deploy/aios-v2/healthcheck/check.sh
```
检查:
- systemd;
- HTTP Health;
- 状态目录;
- SQLite quick_check;
- GitOps detached/clean;
- Evidence 文件。
## 8. 部署方式
### 8.1 systemd
```text
/etc/aios-v2/aios-v2.env
/opt/aios/current
/var/lib/aios-v2
```
运行用户:`aios`。
安全选项:NoNewPrivileges、ProtectSystem、ProtectHome、PrivateTmp、Capability 限制。
### 8.2 Docker/Compose
- 非 root UID/GID;
- Read-only root filesystem;
- Drop all capabilities;
- 独立 State Volume;
- HTTP Healthcheck;
- Secret 文件只读挂载。
## 9. 回滚
```bash
bash deploy/aios-v2/rollback/rollback.sh <previous-release>
```
原则:
- 回滚前自动备份状态;
- 默认只回滚代码;
- 状态恢复需显式 `AIOS_V2_RESTORE_STATE=1`;
- 回滚后必须执行健康检查;
- 目标版本失败时自动恢复 previous release。
## 10. 无人值守任务
### OpenClaw
任务:`tasks/openclaw-production-deploy-task.yaml`
职责:
- 预发布部署;
- 健康检查;
- Runtime 联调;
- 小型部署与 Runtime Bug 修复;
- 证据回写。
### Codex
任务:`tasks/codex-production-supervisor-task.yaml`
职责:
- 架构监督;
- 阻塞分析和跨模块修复;
- 文档、测试和安全审查;
- 发布建议。
所有修改和阻塞必须进入 Git 和 Execution Evidence,最终统一审视。
## 11. 当前已知风险
| 风险 | 级别 | 当前处理 |
|---|---|---|
| 精确 Candidate Commit 尚未执行全部测试 | 高 | OpenClaw 执行,Codex 监督 |
| 真实 OpenClaw Runtime 尚未联调 | 高 | 预发布任务必须完成 |
| 候选 SQLite 不支持生产多实例 HA | 高 | Candidate 单实例;后续 ADR 迁移 |
| 身份代理 Header 防伪尚未环境验证 | 高 | 预发布安全检查 |
| Web Console 为候选只读 MVP | 中 | 生产前补充 E2E 与安全验证 |
| 部分策略负向、并发和审计测试待补强 | 中 | Codex 登记 blocker 并补充 |
| 本地/Gitea 自动双写状态仍需持续证明 | 中 | 验收记录 OID |
| 性能、容量和 SLO 尚无实测 | 中 | 预发布基准测试 |
| 外部不可变审计固化尚未实现 | 中 | 生产数据层阶段完成 |
## 12. 发布 Gate
### 允许进入预发布
条件:
- Candidate 分支可见;
- 文档完整;
- 任务合同已生成;
- 部署资产存在;
- 无明文 Secret。
### 允许进入合并审查
条件:
- 全量验收通过;
- OpenClaw/Hermes 联调通过;
- 重启和回滚通过;
- 严重/高风险 blocker 已解决;
- Codex 输出正向监督结论;
- 本地/Gitea OID 一致。
### 允许生产发布
条件:
- Owner 明确批准;
- Merge 与 Tag 通过受控流程;
- 生产配置、Secret 和反向代理准备完成;
- 发布窗口、观察指标、回滚负责人明确。
## 13. 当前结论
**代码和正式中文文档已经达到候选闭环状态,但尚不能宣称生产已验证。**
下一阶段由 OpenClaw 执行预发布部署和低风险 Bug 修复,由 Codex 监督架构、文档、安全、测试和阻塞。所有过程证据统一进入系统,满足 Gate 后再请求 Owner 批准。
## 14. 更新规则
以下变化必须更新本报告:
- Candidate Commit;
- 测试结果;
- 部署和回滚结果;
- Blocker 和风险;
- OpenClaw/Codex 任务状态;
- 文档—代码匹配状态;
- Gitea OID;
- 发布建议和 Owner 决策。
## 15. Candidate 命令签名算法
Key ID `openclaw-v2r1-candidate-ed25519` 对应真实 Ed25519 私钥,不得再把同名材料作为 HMAC key 使用。私钥由 Owner 批准流程在受保护边界内生成,文件权限为 `0600`,Owner 为实际 AIOS 服务用户;Git、日志和 Evidence 仅记录 Key ID、权限检查和 sign/verify 结果。
## 16. 2026-07-20 阻塞清除快照
五项高风险 Blocker 均已形成运行证据。`BLK-OPENCLAW-RUN-PROVENANCE-MISSING` 通过 Git 中的 `aios-tenant-boundary` rc2 插件解除:Ed25519 Owner 签发记录持久保存,授权绑定 Run、Sender、Task、Claim OID、Candidate OID 与任务分支;专用 `aios_evidence_write` 只允许七个 `.agent` JSON 和固定 Git 操作,通用 Shell/写工具保持拒绝。
Provenance `openclaw-v2r1-20260721T031523Z` 已完成 `heartbeat.json` 更新、Commit、当前任务分支普通 Push 和追加式审计;`README.md` 越权写入被拒绝。由此允许启动 Gate A、Gate B 和离线验收,但在三项验收及后续在线联调完成前仍保持 `STOP_PREPRODUCTION`。
+2 -2
View File
@@ -33,7 +33,7 @@ class CommandConflict(AdapterError):
class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ...
def cancel(self, command_id: str, *, tenant_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ...
@@ -203,7 +203,7 @@ class OpenClawAdapter:
CommandState.REJECTED,
}:
return current
if not self.transport.cancel(command_id):
if not self.transport.cancel(command_id, tenant_id=tenant_id):
raise AdapterError("runtime rejected cancellation")
cancelled = replace(
current,
+28 -11
View File
@@ -1,8 +1,7 @@
"""Versioned AIOS to OpenClaw execution contracts."""
from __future__ import annotations
import hashlib
import hmac
import base64
import json
import uuid
from dataclasses import dataclass, field, replace
@@ -10,6 +9,10 @@ from datetime import datetime, timezone
from enum import Enum
from typing import Any, Mapping
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
CONTRACT_VERSION = "aios.openclaw.command.v1"
DEFAULT_CLOCK_SKEW_SECONDS = 300
@@ -179,11 +182,26 @@ def canonical_command_bytes(command: OpenClawCommand) -> bytes:
).encode("utf-8")
def _ed25519_private_key(key_material: bytes | str) -> Ed25519PrivateKey:
material = key_material.encode("utf-8") if isinstance(key_material, str) else bytes(key_material)
if material.startswith(b"-----BEGIN"):
key = serialization.load_pem_private_key(material, password=None)
if not isinstance(key, Ed25519PrivateKey):
raise ValueError("signing material is not an Ed25519 private key")
return key
if len(material) != 32:
raise ValueError("Ed25519 raw private signing material must be exactly 32 bytes")
return Ed25519PrivateKey.from_private_bytes(material)
def _decode_signature(value: str) -> bytes:
encoded = value.encode("ascii")
return base64.urlsafe_b64decode(encoded + b"=" * (-len(encoded) % 4))
def sign_command(command: OpenClawCommand, key_material: bytes | str) -> OpenClawCommand:
key = key_material.encode("utf-8") if isinstance(key_material, str) else bytes(key_material)
if not key:
raise ValueError("signing material is required")
signature = hmac.new(key, canonical_command_bytes(command), hashlib.sha256).hexdigest()
key = _ed25519_private_key(key_material)
signature = base64.urlsafe_b64encode(key.sign(canonical_command_bytes(command))).decode("ascii").rstrip("=")
return replace(command, signature=signature)
@@ -196,11 +214,10 @@ def verify_command_signature(
) -> bool:
if not command.signature:
return False
key = key_material.encode("utf-8") if isinstance(key_material, str) else bytes(key_material)
if not key:
return False
expected = hmac.new(key, canonical_command_bytes(command), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, command.signature):
try:
key = _ed25519_private_key(key_material)
key.public_key().verify(_decode_signature(command.signature), canonical_command_bytes(command))
except (InvalidSignature, ValueError, TypeError, UnicodeError):
return False
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
created = _parse_timestamp(command.created_at)
+12 -4
View File
@@ -19,7 +19,7 @@ from .contract import (
class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ...
def cancel(self, command_id: str, *, tenant_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ...
@@ -50,11 +50,16 @@ class InProcessRuntimeTransport:
self._lock = threading.RLock()
self._running: set[str] = set()
self._cancelled: set[str] = set()
self._command_tenants: dict[str, str] = {}
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult:
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
with self._lock:
known_tenant = self._command_tenants.get(command.command_id)
if known_tenant is not None and known_tenant != command.tenant_id:
raise TransportError("command id belongs to another tenant")
self._command_tenants[command.command_id] = command.tenant_id
if command.command_id in self._cancelled:
return OpenClawResult(
command_id=command.command_id,
@@ -90,8 +95,10 @@ class InProcessRuntimeTransport:
with self._lock:
self._running.discard(command.command_id)
def cancel(self, command_id: str) -> bool:
def cancel(self, command_id: str, *, tenant_id: str) -> bool:
with self._lock:
if self._command_tenants.get(command_id) != tenant_id:
return False
self._cancelled.add(command_id)
return command_id in self._running
@@ -190,10 +197,11 @@ class HTTPRuntimeTransport:
attempts=int(value.get("attempts", 1)),
)
def cancel(self, command_id: str) -> bool:
def cancel(self, command_id: str, *, tenant_id: str) -> bool:
value = self._request(
"POST",
f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel",
f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel"
f"?tenant_id={urllib.parse.quote(tenant_id, safe='')}",
payload={},
timeout_seconds=10.0,
)
+3 -1
View File
@@ -59,7 +59,7 @@ class FileSigningMaterialResolver:
raise RuntimeError("signing material reference is not a regular file")
if metadata.st_mode & 0o077:
raise RuntimeError("signing material file permissions must be 0600 or stricter")
value = self.path.read_bytes().strip()
value = self.path.read_bytes()
if len(value) < 32:
raise RuntimeError("signing material is too short")
return value
@@ -321,6 +321,7 @@ def build_application() -> CandidateApplication:
openclaw_url = os.getenv("AIOS_OPENCLAW_URL", "").strip()
material_file = os.getenv("AIOS_OPENCLAW_SIGNING_MATERIAL_FILE", "").strip()
openclaw_key_id = os.getenv("AIOS_OPENCLAW_SIGNING_KEY_ID", "default").strip() or "default"
openclaw = None
resolver = None
if openclaw_url and material_file:
@@ -341,6 +342,7 @@ def build_application() -> CandidateApplication:
openclaw=openclaw,
hermes=hermes,
openclaw_signing_material_resolver=resolver,
openclaw_key_id=openclaw_key_id,
)
return CandidateApplication(gateway, orchestrator, tracker, hermes, openclaw)
+18 -7
View File
@@ -79,20 +79,31 @@ class GatewayApplication:
@staticmethod
def context_from_headers(headers: Mapping[str, str]) -> RequestContext:
authenticated = str(headers.get(AUTHENTICATED_HEADER, "")).strip().lower()
# HTTP field names are case-insensitive. BaseHTTPRequestHandler commonly
# presents them as title-cased (for example ``X-Aios-Authenticated``),
# so copying the message into a plain dict must not make identity lookup
# case-sensitive.
normalized_headers = {
str(key).strip().lower(): value for key, value in headers.items()
}
def header(name: str, default: str = "") -> str:
return str(normalized_headers.get(name.lower(), default))
authenticated = header(AUTHENTICATED_HEADER).strip().lower()
if authenticated not in {"1", "true", "yes"}:
raise Unauthorized("trusted authentication assertion is required")
roles = [
role.strip()
for role in str(headers.get(ROLES_HEADER, "")).split(",")
for role in header(ROLES_HEADER).split(",")
if role.strip()
]
return RequestContext.create(
tenant_id=str(headers.get(TENANT_HEADER, "")),
principal_id=str(headers.get(PRINCIPAL_HEADER, "")),
trace_id=str(headers.get(TRACE_HEADER, "")).strip() or None,
session_id=str(headers.get(SESSION_HEADER, "")).strip() or None,
policy_version=str(headers.get(POLICY_HEADER, "v1")).strip() or "v1",
tenant_id=header(TENANT_HEADER),
principal_id=header(PRINCIPAL_HEADER),
trace_id=header(TRACE_HEADER).strip() or None,
session_id=header(SESSION_HEADER).strip() or None,
policy_version=header(POLICY_HEADER, "v1").strip() or "v1",
roles=roles,
)
+12 -1
View File
@@ -7,6 +7,7 @@ execution runtimes and never own tenant policy or task lifecycle.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Mapping
from aios_hermes import HermesAdapter, HermesJob, HermesResult
@@ -129,6 +130,15 @@ class ExecutionOrchestrator:
task.tenant_id,
self.openclaw_key_id,
)
requested_timeout = (
self.openclaw.default_timeout_seconds
if timeout_seconds is None
else float(timeout_seconds)
)
effective_timeout = min(requested_timeout, 300.0)
expires_at = (
datetime.now(timezone.utc) + timedelta(seconds=effective_timeout)
).isoformat().replace("+00:00", "Z")
command = OpenClawCommand.create(
tenant_id=task.tenant_id,
task_id=task.task_id,
@@ -136,10 +146,11 @@ class ExecutionOrchestrator:
action=task.action,
payload=task.payload,
idempotency_key=task.idempotency_key or task.task_id,
expires_at=expires_at,
key_id=self.openclaw_key_id,
)
command = sign_command(command, key_material)
result = self.openclaw.execute(command, timeout_seconds=timeout_seconds)
result = self.openclaw.execute(command, timeout_seconds=effective_timeout)
self.reconcile_openclaw(context, result)
return DispatchRecord(
tenant_id=task.tenant_id,
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
set -Eeuo pipefail
REPO_ROOT="${AIOS_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
BASE_REF="${AIOS_DOCS_BASE_REF:-}"
RUN_LIVE_HEALTHCHECK="${AIOS_RUN_LIVE_HEALTHCHECK:-0}"
RESULT_DIR="${AIOS_ACCEPTANCE_RESULT_DIR:-$REPO_ROOT/.aios-evidence/acceptance}"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
RUN_DIR="$RESULT_DIR/$STAMP"
SUMMARY="$RUN_DIR/summary.json"
install -d -m 0750 "$RUN_DIR"
pass=0
failed=0
warnings=0
steps=()
record() {
local name="$1" status="$2" code="$3" log="$4"
steps+=("$name|$status|$code|$log")
case "$status" in
通过) pass=$((pass + 1)) ;;
失败) failed=$((failed + 1)) ;;
警告) warnings=$((warnings + 1)) ;;
esac
printf '%s:%s(退出码 %s)\n' "$status" "$name" "$code"
}
run_step() {
local name="$1"
shift
local safe_name
safe_name="$(printf '%s' "$name" | tr ' /:' '___')"
local log="$RUN_DIR/${safe_name}.log"
set +e
(cd "$REPO_ROOT" && "$@") >"$log" 2>&1
local code=$?
set -e
if [[ "$code" -eq 0 ]]; then
record "$name" "通过" "$code" "$log"
else
record "$name" "失败" "$code" "$log"
fi
}
run_optional_step() {
local name="$1"
shift
local safe_name
safe_name="$(printf '%s' "$name" | tr ' /:' '___')"
local log="$RUN_DIR/${safe_name}.log"
set +e
(cd "$REPO_ROOT" && "$@") >"$log" 2>&1
local code=$?
set -e
if [[ "$code" -eq 0 ]]; then
record "$name" "通过" "$code" "$log"
else
record "$name" "警告" "$code" "$log"
fi
}
printf 'AIOS V2R1 Candidate 全量验收开始\n'
printf '仓库:%s\n' "$REPO_ROOT"
printf '结果目录:%s\n' "$RUN_DIR"
[[ -d "$REPO_ROOT" ]] || { echo "错误:仓库不存在" >&2; exit 2; }
DOCS_ARGS=(python3 tools/aios_v2_docs_code_check.py --repo "$REPO_ROOT")
if [[ -n "$BASE_REF" ]]; then
DOCS_ARGS+=(--base-ref "$BASE_REF")
fi
run_step "文档与代码匹配" "${DOCS_ARGS[@]}"
run_step "Python 语法编译" python3 -m compileall -q \
active-runtime/app/aios_v2 \
active-runtime/app/aios_openclaw \
active-runtime/app/aios_hermes
run_step "候选单元与集成测试" python3 -m unittest discover \
-s tests/aios_v2_candidate \
-p 'test_*.py' \
-v
run_step "Shell 脚本语法" bash -c '
set -Eeuo pipefail
while IFS= read -r -d "" path; do
bash -n "$path"
done < <(find deploy/aios-v2 -type f -name "*.sh" -print0)
'
run_step "文档 Git 冲突标记" bash -c '
set -Eeuo pipefail
if grep -RInE "^(<<<<<<<|=======|>>>>>>>)" docs/aios-v2; then
exit 1
fi
'
run_step "明文密钥基础扫描" python3 -c '
from pathlib import Path
import re
import sys
roots = [Path("active-runtime/app"), Path("deploy/aios-v2"), Path("docs/aios-v2"), Path("tasks")]
patterns = [
re.compile(r"(?i)(api[_-]?key|password|private[_-]?key|access[_-]?token)\s*[:=]\s*[\"\x27][^\"\x27\n]{12,}[\"\x27]"),
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
]
violations = []
for root in roots:
if not root.exists():
continue
for path in root.rglob("*"):
if not path.is_file() or path.suffix in {".pyc", ".db", ".sqlite3"}:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
if any(pattern.search(text) for pattern in patterns):
violations.append(str(path))
if violations:
print("疑似明文密钥文件:", *violations, sep="\n", file=sys.stderr)
raise SystemExit(1)
'
if command -v systemd-analyze >/dev/null 2>&1; then
run_optional_step "systemd 单元验证" systemd-analyze verify \
deploy/aios-v2/systemd/aios-v2.service
else
record "systemd 单元验证" "警告" "127" "未安装 systemd-analyze"
fi
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
run_optional_step "Docker Compose 配置验证" docker compose \
-f deploy/aios-v2/docker/compose.yaml config
else
record "Docker Compose 配置验证" "警告" "127" "未安装 Docker Compose"
fi
if [[ "$RUN_LIVE_HEALTHCHECK" == "1" ]]; then
run_step "在线健康检查" bash deploy/aios-v2/healthcheck/check.sh
else
record "在线健康检查" "警告" "0" "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
fi
branch="$(git -C "$REPO_ROOT" branch --show-current 2>/dev/null || true)"
commit="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
dirty="$(git -C "$REPO_ROOT" status --porcelain=v1 2>/dev/null || true)"
python3 - "$SUMMARY" "$branch" "$commit" "$dirty" "$pass" "$failed" "$warnings" "${steps[@]}" <<'PY'
import json
import pathlib
import sys
from datetime import datetime, timezone
summary_path = pathlib.Path(sys.argv[1])
branch, commit, dirty = sys.argv[2:5]
passed, failed, warnings = map(int, sys.argv[5:8])
steps = []
for item in sys.argv[8:]:
name, status, code, log = item.split("|", 3)
steps.append({"name": name, "status": status, "exit_code": int(code), "log": log})
payload = {
"schema": "aios.v2.acceptance.v1",
"generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"branch": branch or None,
"commit": commit or None,
"workspace_dirty": bool(dirty),
"passed": passed,
"failed": failed,
"warnings": warnings,
"steps": steps,
"final_status": "passed" if failed == 0 else "failed",
}
summary_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
printf '验收结果:通过 %s,失败 %s,警告 %s\n' "$pass" "$failed" "$warnings"
printf '证据:%s\n' "$SUMMARY"
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
View File
View File
+27
View File
@@ -419,6 +419,8 @@ X-AIOS-Policy-Version
- 代理必须删除客户端同名头后重新注入。
- 应用不得直接暴露公网端口。
- HTTP 字段名按协议不区分大小写;应用把 Header 集合转换为普通映射后,必须先统一字段名大小写再解析身份。`X-AIOS-*`、`X-Aios-*` 与全小写形式必须得到相同的 `RequestContext`,但字段值本身不得被改写。
- 真实监听器回归测试必须覆盖 `BaseHTTPRequestHandler` 的 title-case 行为,避免纯函数测试使用固定大写键而漏掉在线 401。
### 11.2 核心 API
@@ -488,6 +490,8 @@ POST /api/v2/hermes/results/reconcile
- 默认只回滚代码。
- 状态恢复需显式授权。
- 回滚后必须执行 healthcheck。
- Release Archive 必须保留 `rollback.sh` 与 `healthcheck/check.sh` 的 executable bit;
验收测试必须阻止非执行模式进入候选包。
## 14. 契约变更规则
@@ -500,3 +504,26 @@ POST /api/v2/hermes/results/reconcile
- Evidence/Audit 格式。
不兼容变化必须新增 ADR 和版本号,不得静默修改。
## 15. OpenClaw Runtime HTTP 合同
AIOS 只通过 OpenClaw loopback Gateway 的签名 Runtime 路由联调:
- `GET /v1/health`
- `POST /v1/execute`
- `GET /v1/commands/<command_id>?tenant_id=<tenant_id>`
- `POST /v1/commands/<command_id>/cancel?tenant_id=<tenant_id>`
`execute` 必须携带 `aios.openclaw.command.v1` Ed25519 签名合同和不超过 300 秒的
`expires_at`。Runtime 同时核对仓库外 CredentialRef;签名错误、过期、身份字段篡改、
跨租户查询/取消以及幂等冲突均失败关闭。取消接口的 `tenant_id` 是 Transport 合同的
组成部分,不允许仅凭 `command_id` 控制另一租户的命令。
Candidate 组合层必须把 `AIOS_OPENCLAW_SIGNING_KEY_ID` 原样传入 Orchestrator,
不得在已有批准 Key ID 时回落为 `default`。
Orchestrator 对调用方请求的更长超时执行 300 秒上限收敛,使签名有效期与 Runtime
硬上限保持一致。
当前 Runtime action allowlist 仅包含无副作用的 `status` 和正式验收专用
`acceptance.delay`。其成功只代表 OpenClaw 插件 Runtime 已执行对应合同动作,不代表
模型推理或任意 Agent 工具执行。命令身份、幂等键与终态追加持久化;Gateway 重启时
未完成的 `RUNNING` 记录收敛为 `FAILED/restart_interrupted`,不得误报为真实超时。
+18
View File
@@ -70,6 +70,20 @@
| ExecutionEvidence | §9 | `execution_tracking.py` | evidence persistence test |
| GitOpsStatus | §10 | `gitops_status.py` | healthcheck detached/clean检查 |
### 4.1 核心控制面模型说明
`RequestContext` 是每次控制面请求的不可缺失上下文。它承载租户、调用主体、追踪标识和请求来源,所有读取、写入、审批与调度都必须先用该上下文完成租户边界和身份检查。适配器不得自行补造租户,也不得把缺失上下文解释为默认管理权限。上下文校验失败时,控制面必须拒绝请求并写入不含密钥值的审计记录。
`TaskState` 是权威任务生命周期状态,不是前端展示标签。任务从创建、批准、调度、运行到完成、失败或取消,只能沿合同允许的方向迁移;OpenClaw 与 Hermes 回调只能请求迁移,最终状态由唯一控制面持久化。重复回调依靠幂等键收敛,跨租户回调、旧租约回调和非法逆向迁移必须被拒绝。
`TenantRecord`、`AgentRecord` 与 `TaskRecord` 共同定义所有权:租户拥有 Agent 和 Task,Agent 只能处理其租户与授权 Cell/Profile 范围内的任务。任何查询若缺少租户过滤条件,都不能作为正式实现。`AuditEvent` 记录主体、动作、对象、结果和追踪标识,但不得保存 Token、密码、私钥或签名材料。
`OpenClawCommand` 与 `OpenClawResult` 描述短执行路径;前者必须绑定任务、租户、追踪标识、命令期限和签名合同,后者必须回传相同身份域并接受幂等校验。`RuntimeHealth` 只报告可验证运行事实,不能把本地 Bare Repo 更新误报为 Gitea 完全同步。
`HermesJob`、`HermesResult` 与 `HermesHealth` 描述长任务路径。Job 的领取、心跳和 Lease 续期必须持久化;Result 只有在任务、租户、执行器和租约全部匹配时才能改变权威状态。Worker 崩溃后可重新领取过期 Lease,但不得重复产生业务副作用。
`ExecutionEvidence` 保存 Claim、Heartbeat、Result 与必要的追踪关系,用于重启恢复和审计闭环。Evidence 的写入必须限定任务分支与固定路径;本轮增加的 Owner-signed Provenance Writer 只允许七个 `.agent` JSON,并以普通 Commit 与 Push 固化,不授予通用 Shell 或任意仓库写权限。
## 5. 部署追踪
| 能力 | 文件 | 验收命令 |
@@ -111,3 +125,7 @@
## 8. 更新要求
任何代码、API、部署或测试路径变化,必须同步更新本矩阵。验收脚本会检查本文档引用的关键路径存在,并检查架构/功能代码变化是否同时包含 `docs/aios-v2/` 变化。
### OpenClaw 命令签名材料
`openclaw-v2r1-candidate-ed25519` 使用 Ed25519 私钥对 canonical command bytes 签名,签名以 URL-safe Base64 编码。运行材料只允许是 32 字节 raw private key 或 Ed25519 PEM;`FileSigningMaterialResolver` 不得对 raw bytes 执行 `strip()`。签名、篡改拒绝、过期拒绝由候选测试与运行时 sign/verify 探针共同验证。
+272
View File
@@ -0,0 +1,272 @@
# AIOS V2 文档治理规范
- 文档版本:`V2R1C0-candidate`
- 适用范围:`docs/aios-v2/`、架构决策记录、生产候选报告、在线任务合同、部署与验收说明
- 状态:已采纳
## 1. 目的
本规范用于保证 AIOS V2 的需求、架构、代码、测试、部署和运行事实始终保持一致,避免出现以下问题:
- 代码已经变化,但架构文档仍描述旧边界;
- API、字段或状态机变化后,测试与追踪矩阵未同步;
- OpenClaw、Hermes 或 Agent Router 的控制职责发生漂移;
- 候选分支存在实现,但版本无法解释为什么这样设计;
- 部署、回滚或安全策略发生变化,却没有形成可审计文档;
- 归档包只包含摘要或占位符,无法作为正式交付材料。
## 2. 文档语言要求
所有面向用户、产品、架构、运维、审计和发布的正式文档统一使用中文。
以下内容保留原始英文技术标识:
- 类名、函数名、字段名、环境变量;
- API 路径、HTTP 方法和 Header;
- Git 分支、Commit、Tag;
- OpenClaw、Hermes、Agent Router、GitOps 等专有名称;
- systemd、Docker、SQLite、HMAC、HTTP、JSON 等标准术语。
禁止在同一文档中无必要地切换中英文叙述。
## 3. 文档完整性要求
每份正式文档至少包含:
1. 文档目的;
2. 适用范围;
3. 当前版本和状态;
4. 完整正文;
5. 与代码、测试或部署的映射;
6. 验收规则;
7. 已知限制;
8. 后续演进方向;
9. 更新触发条件。
正式归档包禁止使用以下内容代替正文:
- “后续补充”;
- “待完善”;
- 只有标题没有说明;
- 只包含目录摘要;
- 与候选分支代码不一致的旧文档。
确实尚未验证的项目,应明确标记为“已实现但尚未验证”,并说明验证命令、责任方和进入下一状态的条件。
## 4. 文档分类与维护责任
| 文档 | 主要内容 | 触发更新的变化 |
|---|---|---|
| `README.md` | 文档索引、版本状态、实现地图 | 新增模块、目录、版本或交付物 |
| `01-USER-REQUIREMENTS.md` | 用户目标、功能需求、非功能需求、验收场景 | 用户行为、产品范围、验收标准变化 |
| `02-TARGET-ARCHITECTURE.md` | 分层、边界、数据流、安全、租户、部署架构 | 控制权、Runtime 边界、数据流或安全模型变化 |
| `03-MIGRATION-AND-DELIVERY.md` | 迁移阶段、GitOps、发布门禁、回滚、Bug 修复 | 迁移路线、发布流程、门禁或回滚方式变化 |
| `04-MODULE-CONTRACTS.md` | 数据模型、API、状态机、错误语义、Runtime Contract | 字段、接口、状态机、Header、错误行为变化 |
| `05-TRACEABILITY-MATRIX.md` | 需求到代码、测试、部署、验收的映射 | 代码、测试、部署路径或状态变化 |
| `adr/*` | 跨模块、长期或不可逆架构决策 | 关键决策新增、替换或废弃 |
| `AIOS-V2R1-PRODUCTION-CANDIDATE.md` | 候选版本总览、测试、部署、风险和发布结论 | Candidate SHA、测试证据、部署或风险变化 |
| `tasks/*` | OpenClaw、Codex 等在线执行任务 | 职责、边界、阻塞、验收或执行流程变化 |
## 5. 代码变化与文档同步规则
以下代码或配置路径发生变化时,必须同步更新 `docs/aios-v2/`:
### 5.1 Control Plane
触发路径:
```text
active-runtime/app/aios_v2/control_plane.py
active-runtime/app/aios_v2/persistence.py
active-runtime/app/aios_v2/gateway.py
active-runtime/app/aios_v2/app.py
active-runtime/app/aios_v2/orchestrator.py
active-runtime/app/aios_v2/execution_tracking.py
active-runtime/app/aios_v2/gitops_status.py
```
至少检查:
- 用户需求;
- 目标架构;
- 模块契约;
- 追踪矩阵;
- 必要时新增或更新 ADR。
### 5.2 OpenClaw
触发路径:
```text
active-runtime/app/aios_openclaw/
```
至少检查:
- OpenClawCommand、OpenClawResult、RuntimeHealth;
- 签名、幂等、取消、超时和审计边界;
- ADR-002;
- 真实 Runtime 联调要求。
### 5.3 Hermes
触发路径:
```text
active-runtime/app/aios_hermes/
```
至少检查:
- HermesJob、HermesResult、HermesHealth;
- tenant/cell/profile 分区;
- Worker Lease、Heartbeat、Recovery;
- ADR-002、ADR-004。
### 5.4 部署与运维
触发路径:
```text
deploy/aios-v2/
```
至少检查:
- 迁移与交付策略;
- 部署契约;
- 健康检查;
- 回滚;
- 候选报告。
### 5.5 API、Web 与用户行为
任何 API 路径、Header、请求/响应、页面或用户操作变化,必须同步:
- `01-USER-REQUIREMENTS.md`;
- `02-TARGET-ARCHITECTURE.md`;
- `04-MODULE-CONTRACTS.md`;
- `05-TRACEABILITY-MATRIX.md`。
## 6. ADR 治理
必须新增或更新 ADR 的场景:
- AIOS、OpenClaw、Hermes、Agent Router 的控制边界变化;
- 多租户隔离模型变化;
- 持久化、消息、审计、密钥或部署出现跨模块变化;
- 不兼容 API、字段或状态机变化;
- GitOps、文档门禁或发布策略变化;
- 替换核心基础设施或引入新的权威事实源。
ADR 状态:
- `已提出`:尚未正式采纳;
- `已采纳`:当前有效;
- `已替代`:被新 ADR 替代;
- `已废弃`:保留历史但禁止新实现采用。
任何 ADR 替代必须说明:
- 被替代的 ADR;
- 迁移影响;
- 兼容性和回滚方案;
- 受影响代码、测试和文档。
## 7. 文档—代码匹配门禁
验收必须执行:
```text
python3 tools/aios_v2_docs_code_check.py
```
门禁至少检查:
1. 必需文档存在且非空;
2. 文档语言以中文为主;
3. 文档引用的关键代码、测试和部署路径存在;
4. 文档列出的核心 API 能在代码中找到;
5. 文档列出的核心模型和字段能在代码中找到;
6. Traceability Matrix 覆盖所有 `UR-*` 需求;
7. ADR 索引引用的文件存在;
8. 架构或功能代码变化时,当前交付范围包含 `docs/aios-v2/` 变化;
9. 文档中不存在未解决 Git 冲突标记;
10. 正式文档中不存在摘要占位符或空章节。
门禁失败时:
- Candidate 不得标记为 `Test-verified`;
- OpenClaw 不得继续生产部署;
- Codex 必须登记 blocker 并给出修复建议;
- 修复必须进入 Git 分支并统一审视。
## 8. 在线任务文档要求
OpenClaw 和 Codex 任务必须使用中文说明,并保留可执行字段名。
每个任务至少包含:
- 任务目标;
- 责任范围;
- 禁止事项;
- 输入和输出;
- 分支;
- 执行状态;
- 验收命令;
- claim、heartbeat、progress、blocker、commit、test_status、result 证据要求;
- 阻塞升级路径。
## 9. 归档包要求
正式归档包必须包含完整正文:
```text
docs/aios-v2/
tasks/openclaw-production-deploy-task.yaml
tasks/codex-production-supervisor-task.yaml
deploy/aios-v2/
AIOS-V2R1-PRODUCTION-CANDIDATE.md
```
归档包必须同时生成清单:
- 文件路径;
- 文件大小;
- SHA-256;
- Candidate branch;
- Candidate commit;
- 生成时间。
禁止只打包摘要副本。
## 10. 审查职责
### OpenClaw
- 部署和运行修复时同步相关部署、运维和已知问题文档;
- 不得修改架构边界;
- 发现架构影响时升级给 Codex。
### Codex
- 监督需求、架构、契约、追踪和 ADR 一致性;
- 解决阻塞;
- 拒绝缺少文档或证据的补丁;
- 输出统一发布建议。
### AIOS Owner
- 审批合并、Tag 和生产部署;
- 对重大架构决策和风险接受做最终确认。
## 11. 验收状态
文档完成状态只有在以下条件全部满足时才标记为“完整”:
- 所有必需文档和 ADR 已提交;
- 文档—代码匹配检查通过;
- 归档包包含仓库完整正文;
- 任务合同为中文完整版本;
- Candidate commit、清单和 SHA-256 已记录;
- Codex 监督结论不存在未解决阻塞。
+218
View File
@@ -0,0 +1,218 @@
# AIOS V2R1 监督状态与阻塞处置清单
- 文档版本:`V2R1C0-candidate`
- 快照时间:`2026-07-20 17:03:53 CST`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 候选 OID:`634f6b164f5b20372ab9ec8144bbf76df97de335`
- 当前决策:`STOP_PREPRODUCTION`
- 当前状态:`BLOCKED`
## 1. 执行摘要
AIOS V2R1 的核心代码、完整中文文档、任务合同、部署资产和 GitOps 证据机制已经形成。OpenClaw 与 Codex 已经领取任务并向 Gitea 提交监督证据,三个关键分支的本地 OID 与 Gitea OID 已一致。
当前仍不允许进入预发布,原因不是主体功能缺失,而是安全、签名材料、Candidate 合并、执行来源证明和 Controller 健康检查五项高风险门禁未通过。
当前禁止:
- 进入 Gate A–D;
- 执行在线 Runtime 联调;
- 执行重启恢复和回滚演练;
- 合并 `main`;
- 创建 Tag;
- 正式部署或切换流量。
## 2. 权威仓库与执行状态
| 项目 | 当前值 |
|---|---|
| 权威 Bare Repo | `/data/aios-gitops/repo.git` |
| OpenClaw Worktree | 独立且 clean |
| Codex Worktree | 独立且 clean |
| Gitea Remote | `https://g.cnnetmkt.top/git/aios/aios.git` |
| Candidate OID | `634f6b164f5b20372ab9ec8144bbf76df97de335` |
| OpenClaw Task OID | `bd2784015d40c328d158cd52ebd8683b6e56b3ad` |
| Codex Task OID | `e28245f411dd85ff9384ed595dacc53aab8a91ba` |
| 三分支 Gitea 同步 | `local_oid == gitea_oid` |
| OpenClaw 状态 | `blocked` |
| OpenClaw Heartbeat | 有记录,但 `valid_for_running=false` |
| Codex 状态 | `blocked` |
| Codex Heartbeat | 新鲜 |
| Codex 发布建议 | `STOP_PREPRODUCTION` |
## 3. 已完成的安全与运行修复
### 3.1 Gitea 凭据轮换
已完成:
- 旧 `owner/openclaw-deploy-cli`、`scope=all` 凭据 live count 为 `0`;
- 新凭据:`gitops-bot/openclaw-deploy-cli-v2-20260720`;
- 新凭据作用域:`write:repository`;
- 凭据文件权限:`root:gitops 0640`;
- 令牌值未进入 Git、日志、Evidence 或报告。
### 3.2 Gitea 与执行服务
已完成:
- 验证 Gitea SSH 指纹;
- 备份并修复 `known_hosts`;
- 恢复 `aios-agent-command-watcher`;
- 恢复 `aios-autonomous-controller`;
- 两项服务均为 `active`;
- OpenClaw/One API 真实模型调用返回 HTTP 200;
- OpenClaw/Codex claim、heartbeat、progress、blockers、result、status 已提交并同步 Gitea。
## 4. 当前开放阻塞
### 4.1 BLK-OPENCLAW-PLAINTEXT-SECRETS
- 严重级别:高
- 状态:开放
- 现象:OpenClaw 受管配置仍发现 3 处 `PLAINTEXT_FOUND`。
- 风险:明文 Secret 可能进入配置、日志、命令参数或执行证据。
- 解除条件:
1. 三处明文内容全部迁移为 SecretRef、systemd Credential、只读 Secret Volume 或仓库外受保护文件;
2. 旧值完成撤销和轮换;
3. Secret Scanner 复验通过;
4. Git 历史、日志和 Evidence 不包含明文。
### 4.2 BLK-AIOS-V2-SIGNING-MATERIAL-MISSING
- 严重级别:高
- 状态:开放
- 现象:`/etc/aios-v2/aios-v2.env` 已存在且权限合规,但 `/etc/aios-v2/openclaw-signing-material.bin` 不存在。
- 风险:无法验证 OpenClaw 签名命令、Key ID 与真实 Runtime 身份链。
- 外部受信输入:批准的签名材料。
- 解除条件:
1. 签名材料由批准的 Secret/Key 交付链安装到仓库外;
2. 权限为 `0600` 或更严格;
3. 运行用户可读;
4. Key ID 与材料匹配;
5. execute/status 路径完成真实签名验证;
6. 不输出材料内容、Hash 或长度特征。
### 4.3 BLK-CANDIDATE-NOT-MERGED
- 严重级别:高
- 状态:开放
- 现象:OpenClaw Task 尚未包含当前 Candidate,`merge-base --is-ancestor` 未通过。
- 风险:后续测试和部署不能证明当前 Candidate 可用。
- 解除条件:
1. 保护已有 `.agent/` Evidence;
2. 普通 Merge 当前 Candidate 到 OpenClaw Task;
3. 解决冲突并由 Codex 审查;
4. 更新所有 Evidence 中的 `candidate_commit`;
5. 本地与 Gitea OID 再次一致。
### 4.4 BLK-OPENCLAW-RUN-PROVENANCE-MISSING
- 严重级别:高
- 状态:已解决
- 根因:旧插件只有进程内 `runId -> sender` Map,没有 Owner-signed Schema、持久 Store 和限定 Git Writer。
- 修复:部署 `aios-tenant-boundary` rc2,增加 Ed25519 issuer/store、Run/Sender/OID/期限绑定、一次性 permit 与专用 `aios_evidence_write`。
- 验证:Provenance `openclaw-v2r1-20260721T031523Z` 完成 Heartbeat 写入、Commit、普通 Push 和审计;`README.md` 越权请求被拒绝;Node 测试 9/9。
### 4.5 BLK-CONTROLLER-LEGACY-GITEA-CHECK
- 严重级别:高
- 状态:已解决
- 现象:Controller 健康检查仍硬编码旧 `aios-v23`,并返回 `gitea=false`、`branch_sync=false`。
- 风险:健康状态与当前 AIOS V2R1 仓库不一致,可能错误阻止或错误放行发布。
- 解除条件:
1. 移除旧仓库硬编码;
2. 从配置读取权威仓库、Remote 和目标分支;
3. 健康检查分别报告 `local_sync`、`gitea_sync` 和 `oid_match`;
4. 不再把本地 Bare Repo 更新当作完全同步;
5. 增加回归测试;
6. 更新中文文档和追踪矩阵。
## 5. 模型审查策略修正
`gpt-5.3-codex-spark` 是子 Agent 审查的优先推荐模型,不是强制依赖。
执行规则:
1. Spark 可用时优先使用;
2. Spark 不可用时,使用模型注册表中已批准、已启用且优先级最高的合适模型;
3. 必须记录实际 `model_id`、模型名称和审查结果;
4. 禁止冒充使用不存在的模型;
5. 只有完全没有可用审查模型时,模型能力才构成阻塞。
当前五个 Spark 角色的独立结论一致为“禁止生产放行”,该结论作为已有审查证据保留;后续复验可使用任何满足上述规则的批准模型。
## 6. Gate 状态
| Gate | 当前状态 | 说明 |
|---|---|---|
| Gate A:文档与设计 | 允许启动 | 五项高风险 Blocker 已解除,等待正式执行 |
| Gate B:代码与测试 | 等待 Gate A | 等待正式执行候选测试 |
| Gate C:预发布 | 禁止 | 等待 Gate A、Gate B、离线全量验收和在线联调 |
| Gate D:发布审查 | `STOP_PREPRODUCTION` | Codex 明确停止预发布 |
| Gate E:生产发布 | 禁止 | 未满足任何生产 Gate |
## 7. 阻塞解除顺序
必须按以下顺序处理,避免重复测试和错误放行:
```text
1. 清除 3 处明文 Secret
↓
2. 安装批准的签名材料
↓
3. 签发最小范围 Run Provenance
↓
4. 修复 Controller 旧仓库健康检查
↓
5. Merge 当前 Candidate 到 OpenClaw Task
↓
6. 校验 local_oid == gitea_oid
↓
7. 执行 Gate A:文档—代码检查
↓
8. 执行 Gate B:单元与集成测试
↓
9. 执行离线全量验收
↓
10. 执行在线 Runtime 联调
↓
11. 执行重启恢复与回滚
↓
12. Codex 重新审查并给出发布建议
```
## 8. 外部受信输入
五项高风险 Blocker 当前均不再需要新的外部受信输入。签名材料继续留在受保护边界,Provenance 仍须按 Run 由 Owner 限时签发,不得转化为长期通用权限。
## 9. 监督结论
当前状态保持:
```text
PARTIAL
STOP_PREPRODUCTION
```
三分支已同步 Gitea,五项高风险 Blocker 已解除,允许执行 Gate A、Gate B 和离线全量验收;在三项正式结果与后续在线验收形成证据前,不进入预发布。
任何后续状态更新必须同步:
- OpenClaw `.agent/status.json`、`blockers.json`、`progress.json`、`result.json`;
- Codex 对应监督 Evidence;
- 本文档;
- `05-TRACEABILITY-MATRIX.md`;
- `AIOS-V2R1-PRODUCTION-CANDIDATE.md`;
- 如涉及架构或发布语义,更新 ADR。
## 10. 2026-07-20 至 2026-07-21 Owner 修复实况
- Worktree:OpenClaw 与 Codex 独立、目标分支唯一检出、无 lock、无 prunable;现有 Evidence 已先备份。
- Candidate:`ba7669c3b49f98cf3500af6ea966ba0db472ff3b` 已普通 Push 到 Gitea,并以 `--no-ff` 合入 OpenClaw 任务分支。
- Secrets:OpenClaw 原生 audit 从 3 项明文降为 `plaintext=0`、`unresolved=0`、`shadowed=0`、`legacy=0`;Gateway Token 已轮换,OneAPI 实际 agent 调用通过。
- Signing:命令合同已从名实不符的 HMAC 改为真实 Ed25519;`openclaw-v2r1-candidate-ed25519` 材料由 Owner 在受保护边界生成,运行用户权限检查、sign/verify 和篡改拒绝通过。
- Controller:修复分支 `patch/bugfix/controller-gitea-health-v2r1-20260720` 的 OID `6188da26453505dacde9d6d298191437164c8d27` 已部署;12 项测试通过,active worktree detached + clean,`gitea=true`、`branch_sync=true`、`oid_match=true`、`sync_status=fully_synced`。
- Provenance:插件 rc2 已声明原生工具合同并通过 runtime doctor;Owner-signed Store、一次性 permit、七路径 Writer、普通 Push、审计与越权拒绝均通过。首次部署检查脚本曾两次因读取错误 JSON 层级触发自动回滚,旧插件与配置均成功恢复,修正断言后部署成功。
因此当前结论仍为 `STOP_PREPRODUCTION`,但 Gate A 已允许启动;后续结论必须基于 Gate A、Gate B、离线 Acceptance、在线 Runtime、重启恢复和正式回滚演练的最新证据。
+92 -61
View File
@@ -1,76 +1,82 @@
# AIOS V2 Architecture and Delivery Index
# AIOS V2 架构与交付文档索引
- Version line: `V2R1C0-candidate`
- Candidate branch: `integration/aios-v2r1-cto-production-candidate-20260720`
- Architecture owner: AIOS V2 CTO
- Status: implementation-complete candidate; runtime and production verification pending
- 版本线:`V2R1C0-candidate`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 架构负责人:AIOS V2 CTO
- 当前状态:候选代码闭环已完成,运行时、预发布和生产验证尚待执行
## 1. Purpose
## 1. 文档目的
This directory is the single source of truth for AIOS V2 requirements, target architecture, module boundaries, migration strategy, release gates and architectural decisions.
本目录是 AIOS V2 用户需求、目标架构、模块边界、迁移策略、发布门禁和架构决策的唯一事实源。
The design is based on the following non-negotiable principles:
所有后续架构、功能、接口、数据模型、安全策略、部署方式和验收流程变化,都必须在同一交付流程中同步更新本目录。
1. Do not rewrite the existing platform.
2. Reuse OpenClaw, Hermes and Agent Router capabilities.
3. Decouple OpenClaw and Hermes from control-plane ownership.
4. AIOS is the only control plane.
5. All user-facing and operational capabilities are Web/API accessible.
6. Every module enforces multi-tenant isolation.
7. Every change flows through Git branches, commits, tests, review and controlled promotion.
AIOS V2 遵循以下不可协商原则:
## 2. Document map
1. 不推倒重写现有平台。
2. 优先复用 OpenClaw、Hermes 和 Agent Router 能力。
3. OpenClaw 与 Hermes 从控制面职责中逐步解耦。
4. AIOS 是唯一控制面。
5. 所有面向用户和运维的能力必须通过 Web 或 API 访问。
6. 所有模块必须强制多租户隔离。
7. 所有修改必须通过 Git 分支、提交、测试、审查和受控提升完成。
8. 代码变化必须同步更新需求、架构、契约、追踪矩阵或 ADR。
| Document | Purpose |
## 2. 文档地图
| 文档 | 用途 |
|---|---|
| [01-USER-REQUIREMENTS.md](01-USER-REQUIREMENTS.md) | Product goals, user requirements, non-functional requirements and acceptance criteria |
| [02-TARGET-ARCHITECTURE.md](02-TARGET-ARCHITECTURE.md) | Target logical architecture, control boundaries, data flow, security and tenancy model |
| [03-MIGRATION-AND-DELIVERY.md](03-MIGRATION-AND-DELIVERY.md) | Incremental migration plan, reuse strategy, GitOps workflow and release gates |
| [04-MODULE-CONTRACTS.md](04-MODULE-CONTRACTS.md) | Tenant context, task lifecycle, OpenClaw, Hermes, audit and execution-evidence contracts |
| [05-TRACEABILITY-MATRIX.md](05-TRACEABILITY-MATRIX.md) | Requirement-to-design-to-code-to-test mapping |
| [adr/README.md](adr/README.md) | Architecture decision records |
| [01-USER-REQUIREMENTS.md](01-USER-REQUIREMENTS.md) | 产品目标、用户角色、功能需求、非功能需求和验收标准 |
| [02-TARGET-ARCHITECTURE.md](02-TARGET-ARCHITECTURE.md) | 目标逻辑架构、控制边界、数据流、安全模型和多租户模型 |
| [03-MIGRATION-AND-DELIVERY.md](03-MIGRATION-AND-DELIVERY.md) | 增量迁移、能力复用、GitOps 流程、发布门禁和回滚策略 |
| [04-MODULE-CONTRACTS.md](04-MODULE-CONTRACTS.md) | 租户上下文、任务生命周期、OpenClaw、Hermes、审计和执行证据契约 |
| [05-TRACEABILITY-MATRIX.md](05-TRACEABILITY-MATRIX.md) | 需求、设计、代码、测试、部署和验收之间的追踪关系 |
| [06-DOCUMENT-GOVERNANCE.md](06-DOCUMENT-GOVERNANCE.md) | 文档语言、完整性、同步门禁、版本与审查要求 |
| [adr/README.md](adr/README.md) | 架构决策记录索引与治理规则 |
## 3. Current implementation map
## 3. 当前实现映射
| Architecture capability | Candidate implementation |
| 架构能力 | 候选实现 |
|---|---|
| Tenant, agent, task, policy and audit control plane | `active-runtime/app/aios_v2/control_plane.py` |
| Durable control-plane state | `active-runtime/app/aios_v2/persistence.py` |
| Web/API gateway | `active-runtime/app/aios_v2/gateway.py` |
| Unified application entrypoint | `active-runtime/app/aios_v2/app.py` |
| Runtime dispatch and reconciliation | `active-runtime/app/aios_v2/orchestrator.py` |
| Agent execution evidence | `active-runtime/app/aios_v2/execution_tracking.py` |
| GitOps runtime visibility | `active-runtime/app/aios_v2/gitops_status.py` |
| Tenant、Agent、Task、Policy 和 Audit 控制面 | `active-runtime/app/aios_v2/control_plane.py` |
| 控制面持久化 | `active-runtime/app/aios_v2/persistence.py` |
| Web/API Gateway | `active-runtime/app/aios_v2/gateway.py` |
| 统一应用入口 | `active-runtime/app/aios_v2/app.py` |
| Runtime 调度与结果回写 | `active-runtime/app/aios_v2/orchestrator.py` |
| Agent 执行证据 | `active-runtime/app/aios_v2/execution_tracking.py` |
| GitOps 状态可视化 | `active-runtime/app/aios_v2/gitops_status.py` |
| Web Console | `active-runtime/app/aios_v2/console.py` |
| OpenClaw execution boundary | `active-runtime/app/aios_openclaw/` |
| Hermes runtime boundary | `active-runtime/app/aios_hermes/` |
| Candidate tests | `tests/aios_v2_candidate/` |
| Deployment, health and rollback | `deploy/aios-v2/` |
| OpenClaw 执行边界 | `active-runtime/app/aios_openclaw/` |
| Hermes Runtime 边界 | `active-runtime/app/aios_hermes/` |
| 候选测试 | `tests/aios_v2_candidate/` |
| 部署、健康检查与回滚 | `deploy/aios-v2/` |
| OpenClaw 生产部署任务 | `tasks/openclaw-production-deploy-task.yaml` |
| Codex 生产监督任务 | `tasks/codex-production-supervisor-task.yaml` |
## 4. Status vocabulary
## 4. 状态术语
The project separates code availability from release readiness:
项目必须严格区分“代码存在”和“可生产发布”:
- **Designed**: documented boundary and acceptance criteria exist.
- **Implemented**: code is committed on the candidate branch.
- **Test-defined**: automated tests exist in Git.
- **Test-verified**: tests were executed for the exact commit and evidence was recorded.
- **Pre-production verified**: deployed in an isolated environment with health, restart and rollback evidence.
- **Production released**: owner-approved merge/tag/deployment completed.
- **已设计**:文档中存在明确边界和验收标准。
- **已实现**:代码已经提交到候选分支。
- **已定义测试**:Git 中存在自动化测试代码。
- **已验证测试**:已对精确 commit 执行测试并保存证据。
- **已验证预发布**:已在隔离环境完成部署、健康检查、重启和回滚演练。
- **已生产发布**:已取得 Owner 批准并完成合并、Tag 和生产部署。
A branch or file existing does not, by itself, mean a capability is production-ready.
分支、文件、测试脚本或部署脚本存在,并不自动代表能力已通过生产验收。
## 5. Design summary
## 5. 设计摘要
```text
Browser / Web Console / Public API
浏览器 / Web Console / Public API
|
v
AIOS Control Plane
AIOS 唯一控制面
Tenant | Agent | Task | Policy | Audit
|
v
Execution Orchestrator / Router
Execution Orchestrator / Agent Router
/ \
v v
OpenClaw Adapter Hermes Boundary
@@ -79,20 +85,45 @@ Browser / Web Console / Public API
OpenClaw Runtime Hermes Workers
\
v
Result Reconciliation + Evidence
结果回写 + 审计 + 执行证据
```
The control plane owns authoritative identity, policy, approval, task lifecycle and audit state. OpenClaw and Hermes receive only tenant-scoped, approved execution work and return runtime results.
控制面拥有权威身份、策略、审批、任务生命周期和审计状态。OpenClaw 与 Hermes 只接收带租户上下文、已经批准的执行工作,并返回运行结果。
## 6. Release rule
## 6. 发布规则
No candidate may be promoted until:
候选版本在满足以下条件前不得提升:
- requirement-to-code traceability is complete;
- multi-tenant negative tests pass;
- controller workspace is detached and clean;
- local Git and Gitea branch OIDs match;
- persistence restart recovery passes;
- healthcheck and rollback pass in pre-production;
- no plaintext secret enters Git;
- owner approval is recorded for merge, tag and production deployment.
- 需求到代码的追踪关系完整;
- 文档—代码匹配检查通过;
- 多租户负向测试通过;
- GitOps Controller worktree 保持 detached 且 clean;
- 本地 Git 与 Gitea 目标分支 OID 一致;
- 持久化重启恢复测试通过;
- 预发布健康检查和回滚演练通过;
- Git 中不存在明文密钥;
- OpenClaw 与 Hermes 真实 Runtime 联调通过;
- Owner 对合并、Tag 和生产部署做出明确批准。
## 7. 文档完整性要求
每份正式文档必须包含:
- 文档目的与适用范围;
- 当前版本和状态;
- 关键设计或需求正文;
- 代码、测试或部署映射;
- 验收与更新规则;
- 已知限制和后续演进方向。
禁止在正式归档包中以摘要、占位符或“后续补充”代替正文。
## 8. 中文输出要求
所有面向用户、产品、架构、运维和审计的正式文档统一使用中文。以下技术标识保留原始英文,避免与代码不一致:
- 类名、函数名、字段名;
- API 路径和 HTTP Header;
- Git 分支、Commit、Tag;
- OpenClaw、Hermes、Agent Router、GitOps 等专有名称;
- systemd、Docker、SQLite、HMAC、HTTP 等标准技术术语。
@@ -1,39 +1,108 @@
# ADR-001: AIOS 作为唯一控制面
# ADR-001:AIOS 作为唯一控制面
- Status: Accepted
- Date: 2026-07-20
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Control Plane、Agent Router、OpenClaw、Hermes、Web Console、GitOps、审计
## Context
## 1. 背景
现有系统包含多个执行和调度能力,若每个 Runtime 保存自己的任务、租户和策略状态,会形成多个事实源。
现有系统已经包含多个执行、调度和治理能力。如果 OpenClaw、Hermes、Agent Router 或 Web 层分别保存自己的租户、策略、审批和任务状态,就会形成多个权威事实源,带来以下问题:
## Decision
- 同一任务在不同模块中状态不一致;
- 审计链不完整;
- 多租户隔离难以统一验证;
- Web、Runtime 和运维入口可能绕过策略;
- Bug 修复和发布无法判断应该修改哪个事实源。
## 2. 决策
AIOS Control Plane 是唯一权威控制面,负责:
- Tenant
- Agent
- Task
- Policy
- Approval
- Audit
- Execution Evidence
- Tenant Registry;
- Identity 与角色上下文;
- Agent Registry;
- Task Lifecycle;
- Policy Decision;
- Approval;
- Audit Event;
- Runtime Inventory;
- Execution Evidence;
- GitOps 与发布状态可视化。
OpenClaw、Hermes、Agent Router 只能执行或路由,不拥有第二套控制状态。
OpenClaw、Hermes 和 Agent Router 只能执行、路由或调度已经由 AIOS 批准的工作,不得拥有第二套租户、策略、审批或任务生命周期。
## Consequences
## 3. 目标边界
Positive:
```text
Web / API
|
v
AIOS Control Plane
|
v
Execution Orchestrator / Agent Router
|
+--> OpenClaw Adapter --> OpenClaw Runtime
|
+--> Hermes Boundary --> Hermes Workers
```
- 状态唯一;
- 审计完整;
- Web/API 统一。
## 4. 备选方案
Negative:
### 方案 A:保持多个控制面
- Runtime 接入需要 Adapter。
- 迁移需要阶段性边界收敛。
拒绝原因:状态会持续分裂,无法形成统一审计和租户隔离。
## Compliance
### 方案 B:由 OpenClaw 或 Hermes 成为主控制面
任何新 Runtime 必须通过 AIOS Orchestrator 接入。
拒绝原因:二者主要价值是执行与 Runtime,不适合作为统一产品、租户、审批和发布控制面。
### 方案 C:推倒重写全部能力
拒绝原因:会丢失现有执行能力、运行经验和迁移可回滚性。
## 5. 正面影响
- 任务、策略和租户状态唯一;
- Web/API、OpenClaw、Hermes 和 GitOps 使用同一权威模型;
- 审计、执行证据和发布判断可以统一;
- 新 Runtime 可以通过稳定 Adapter 接入;
- 多租户和权限模型可集中验证。
## 6. 负面影响与风险
- 所有旧执行入口需要迁移到 Orchestrator;
- Runtime 需要稳定 Contract;
- Control Plane 可靠性要求提高;
- 在迁移完成前可能短期存在旧旁路,需要明确冻结和监控。
## 7. 迁移与回滚
迁移顺序:
1. 建立 AIOS Control Plane 权威模型;
2. 引入 Orchestrator;
3. OpenClaw/Hermes 通过 Adapter 接入;
4. 将 Web 和自动任务切换到 AIOS API;
5. 关闭旧控制旁路。
回滚只允许回滚 Runtime 或应用版本,不允许恢复第二套权威控制状态。
## 8. 合规检查
任何新功能必须回答:
- 权威状态是否存放在 AIOS?
- Runtime 是否只接收已批准工作?
- Web 是否只调用 AIOS API?
- 是否新增了第二套状态机或租户事实源?
若最后一项为“是”,变更必须拒绝或新增替代 ADR。
## 9. 实现与测试映射
- 实现:`active-runtime/app/aios_v2/control_plane.py`
- 编排:`active-runtime/app/aios_v2/orchestrator.py`
- 应用入口:`active-runtime/app/aios_v2/app.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
- 追踪:`docs/aios-v2/05-TRACEABILITY-MATRIX.md`
@@ -1,46 +1,108 @@
# ADR-002: OpenClaw/Hermes 与控制面解耦
# ADR-002:OpenClaw 与 Hermes 从控制面职责中解耦
- Status: Accepted
- Date: 2026-07-20
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:OpenClaw、Hermes、Execution Orchestrator、Agent Router、Audit、Web Console
## Context
## 1. 背景
OpenClaw 和 Hermes 已具备执行能力,但历史上可能承担部分控制职责。
OpenClaw 和 Hermes 已经具备成熟的执行能力,但历史实现可能同时承担部分租户、策略、任务状态或运维控制职责。如果继续扩展这种模式,AIOS 将无法成为唯一控制面,也无法统一 Web、审计和多租户隔离。
## Decision
## 2. 决策
引入边界层:
OpenClaw 与 Hermes 通过稳定的 Adapter/Boundary 接入 AIOS:
```text
AIOS Control Plane
|
v
Adapter / Boundary
Execution Orchestrator
|
v
Runtime
+--> OpenClaw Adapter --> OpenClaw Runtime
|
+--> Hermes Boundary --> Hermes Workers
```
OpenClaw:
OpenClaw 只负责:
- 执行签名命令;
- 返回结果;
- 提供健康状态。
- 验证签名命令;
- 执行;
- 查询状态;
- 取消;
- 健康检查;
- 返回结果和生命周期事件。
Hermes:
Hermes 只负责:
- 队列;
- Worker;
- Lease;
- Heartbeat;
- 执行结果。
- tenant/cell/profile 分区队列;
- Worker claim;
- Lease 与 Heartbeat;
- 执行;
- 完成、失败、取消和结果回传。
## Consequences
二者均不得:
- Runtime 可独立演进。
- 控制逻辑不会分裂。
- 需要维护稳定 Contract。
- 创建或批准 AIOS Task;
- 修改租户或策略;
- 维护第二套任务生命周期;
- 绕过 AIOS Audit;
- 向浏览器提供直接控制入口。
## Migration
## 3. 备选方案
先接 Adapter,再迁移旧调用入口,最后关闭控制旁路。
### 方案 A:让 OpenClaw 继续拥有控制职责
拒绝原因:会产生第二控制面,并使 Web、审批和审计持续分裂。
### 方案 B:让 Hermes 统一控制和执行
拒绝原因:Hermes 的核心价值是 Worker Runtime 与队列,不应承担产品、租户和策略治理。
### 方案 C:直接替换 OpenClaw/Hermes
拒绝原因:不符合能力复用原则,会扩大迁移风险和开发量。
## 4. 正面影响
- AIOS 保持唯一权威状态;
- OpenClaw/Hermes 可以独立升级;
- 新 Runtime 可使用相同接入模式;
- Web 和审计不再依赖 Runtime 内部实现;
- 可逐步关闭旧旁路,而不需要一次性重写执行层。
## 5. 负面影响与风险
- 需要维护稳定的命令、结果和健康 Contract;
- 旧调用路径可能在迁移期并存;
- Runtime 联调和错误语义需要统一;
- Adapter 本身成为重要可靠性边界。
## 6. 迁移步骤
1. 定义 OpenClawCommand/OpenClawResult/RuntimeHealth;
2. 定义 HermesJob/HermesResult/HermesHealth;
3. 引入 AIOS Orchestrator;
4. 将新任务先通过 Adapter 执行;
5. 迁移 Web、Codex 和自动任务入口;
6. 冻结旧 Runtime 控制接口;
7. 通过网络策略或代码门禁关闭旁路。
## 7. 回滚原则
允许回滚 Runtime 或 Adapter 版本,但不允许回滚到 Runtime 重新拥有 Tenant、Policy、Approval 或 Task Lifecycle 的模式。
## 8. 合规检查
- OpenClaw 输入是否为 AIOS 生成的签名命令?
- Hermes Job 是否包含 tenant/cell/profile?
- Runtime 是否只返回结果,不直接写 AIOS Store?
- Web 是否只调用 AIOS API?
- Runtime Bug 修复是否同步更新 Contract、测试和文档?
## 9. 实现与测试映射
- OpenClaw:`active-runtime/app/aios_openclaw/`
- Hermes:`active-runtime/app/aios_hermes/`
- Orchestrator:`active-runtime/app/aios_v2/orchestrator.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
- 模块契约:`docs/aios-v2/04-MODULE-CONTRACTS.md`
@@ -1,44 +1,121 @@
# ADR-003: 所有变更必须通过 GitOps
# ADR-003:所有变更必须通过 GitOps
- Status: Accepted
- Date: 2026-07-20
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:代码、配置、文档、测试、部署、回滚、生产修复
## Context
## 1. 背景
直接在生产目录、长期 worktree 或主分支上修改代码,会造成不可追踪、不可回滚和分支冲突。
直接在生产目录、长期 worktree、主分支或运行容器中修改代码,会造成以下问题:
## Decision
- 变更不可追踪;
- 无法稳定回滚;
- 本地仓库与 Gitea 不一致;
- Worktree 和 Branch Ref 冲突;
- 紧急修复无法统一审视;
- 文档、测试和部署资产无法同步。
所有代码、配置、文档、测试、部署和回滚资产必须通过以下流程:
## 2. 决策
所有代码、配置、文档、测试、部署和回滚资产必须经过以下流程:
```text
non-main branch -> commit -> local sync -> Gitea sync -> test -> review -> controlled promotion
非 main 分支
-> Commit
-> 本地仓库同步
-> Gitea 镜像与 OID 校验
-> 自动测试
-> Codex 监督审查
-> 候选集成
-> Owner 批准
-> Merge / Tag / Deploy
```
Controller worktree 必须长期保持 detached 且 clean。每个写任务使用独立工作区或受控写入流程;相同分支写入必须串行。
GitOps Controller worktree 必须长期保持 detached 且 clean。相同分支写入必须串行,任务工作区必须可回收。
## Required evidence
## 3. 必需证据
每个任务必须记录:
- branch
- commit
- changed files
- test command and result
- blocker and resolution
- local/Gitea sync status
- rollback note
- 任务 ID;
- 执行器;
- 分支;
- Commit;
- 修改文件;
- claim;
- heartbeat;
- progress;
- blocker;
- 测试命令、exit code 和结果;
- 本地/Gitea 同步状态;
- 回滚说明;
- 最终结果和监督结论。
## Prohibited
## 4. 禁止事项
- 直接 push `main`
- 未审批创建生产 Tag
- 在生产目录手工修改后不回写 Git
- fetch 到已被 worktree 检出的本地 branch ref
- 在 Git 中提交明文密钥
- 直接 push `main`;
- 未审批创建生产 Tag;
- 未记录任务直接修改生产;
- 在生产目录手工修复后不回写 Git;
- fetch 到已被 worktree 检出的本地 branch ref;
- 以新分支名反复规避 Worktree 生命周期缺陷;
- 在 Git 中提交明文凭据;
- 缺少文档和测试时标记为完成。
## Consequences
## 5. 备选方案
优点:可审计、可回滚、可并行治理。
### 方案 A:允许紧急生产手工修改
成本:紧急修复也必须形成 patch branch 和证据。
拒绝原因:即使紧急修复也必须形成 Patch、Commit 和证据,否则无法统一审视和回滚。
### 方案 B:只同步本地 Bare Repo
拒绝原因:Web Gitea 无法看到真实任务进度,团队无法审查。
### 方案 C:Controller 长期检出任务分支
拒绝原因:会触发 `refusing to fetch into branch checked out at worktree`,并破坏并发任务。
## 6. 正面影响
- 所有变更可审计;
- 本地和 Web 远端一致;
- 紧急修复也可回滚;
- OpenClaw/Codex 进度可统一查询;
- 文档与代码可通过门禁同步。
## 7. 负面影响与风险
- 生产修复增加提交与证据步骤;
- GitOps Runtime 成为关键基础设施;
- 需要处理分支锁、任务恢复和镜像失败;
- 自动执行器必须遵循统一任务格式。
## 8. 运行规则
- Controller:detached + clean;
- Fetch:更新 remote-tracking ref,不写已检出本地 branch;
- Write:独立任务工作区或受控写入;
- Lock:同一分支串行;
- Mirror:本地成功和 Gitea 成功分别记录;
- Failure:状态标记为 degraded/blocked,不得伪报 synced。
## 9. 紧急 Bug 修复流程
1. 创建 `patch/bugfix/*`;
2. 登记 blocker 和影响;
3. 添加复现测试;
4. 修复代码;
5. 同步文档;
6. 执行验收;
7. Codex 审核;
8. Owner 批准发布;
9. 生产观察并记录结果。
## 10. 实现与验收映射
- GitOps 状态:`active-runtime/app/aios_v2/gitops_status.py`
- 健康检查:`deploy/aios-v2/healthcheck/check.sh`
- 回滚:`deploy/aios-v2/rollback/rollback.sh`
- 任务:`tasks/openclaw-production-deploy-task.yaml`、`tasks/codex-production-supervisor-task.yaml`
- 文档门禁:`tools/aios_v2_docs_code_check.py`
@@ -0,0 +1,150 @@
# ADR-004:多租户隔离采用默认拒绝模型
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Web/API、Control Plane、OpenClaw、Hermes、Persistence、Audit、Execution Evidence、Logs、Metrics、Secret
## 1. 背景
AIOS V2 同时承载多个租户的用户请求、控制面状态、OpenClaw 命令、Hermes 队列、执行证据、审计和 GitOps 状态。任何一层缺失租户边界,都可能造成数据泄露、跨租户任务执行或错误回写。
## 2. 决策
多租户隔离采用:
```text
显式租户上下文
+
默认拒绝
+
全链路传播
+
负向测试
```
所有资源、命令、Job、证据和审计记录都必须显式绑定 `tenant_id`。缺少上下文、上下文不匹配或无法确认所有权时,系统必须 fail closed。
## 3. 权威上下文
控制请求至少包含:
- `tenant_id`;
- `principal_id`;
- `trace_id`;
- `policy_version`;
- 可选 `session_id`。
执行请求增加:
- `task_id`;
- `command_id` 或 `job_id`;
- `idempotency_key`。
Hermes 额外包含:
- `cell_id`;
- `profile_id`;
- `worker_id`。
## 4. 各层执行规则
### Web/API
- 可信代理必须删除客户端同名 `X-AIOS-*` Header 后重新注入;
- 浏览器不得自行选择或覆盖租户;
- 未认证身份断言必须拒绝。
### Control Plane
- Tenant、Agent、Task、Audit 全部带不可为空的 `tenant_id`;
- 跨租户读取返回拒绝或“未找到”;
- 不泄露其他租户资源存在性。
### OpenClaw
- 签名覆盖 tenant/task/trace/action/payload/idempotency/time;
- Result 身份字段必须与命令完全一致;
- 跨租户 status/cancel 返回“未找到”。
### Hermes
- Job 按 tenant/cell/profile 分区;
- Worker 只能 claim 自己的分区;
- Worker ID 不得在未授权分区之间迁移;
- Result 必须匹配 tenant/task/trace。
### Persistence
- 候选版使用显式租户字段;
- 生产数据库必须提供 RLS 或物理隔离策略;
- 备份、恢复和迁移必须保持租户边界。
### Audit 与 Evidence
- 仅按当前租户查询;
- 不记录明文 Secret;
- 统一包含 tenant/task/trace。
### Cache、Memory、Artifact、Logs、Metrics
- Key 或标签必须包含租户命名空间;
- 查询和导出必须受租户权限限制;
- 不允许使用无租户前缀的全局缓存保存业务数据。
## 5. 备选方案
### 方案 A:仅在 API 层检查租户
拒绝原因:后台任务、Runtime、缓存和结果回写仍可能跨租户。
### 方案 B:仅依赖数据库隔离
拒绝原因:命令、队列、审计和文件系统仍需要显式边界。
### 方案 C:发生冲突时自动推断租户
拒绝原因:推断可能产生跨租户副作用,必须默认拒绝。
## 6. 正面影响
- 安全模型一致;
- Runtime 无法绕过租户边界;
- 负向测试可自动化;
- 审计和问题定位更明确。
## 7. 负面影响与风险
- 所有接口和模型都需要传播租户上下文;
- 日志、指标和缓存设计更复杂;
- 候选 SQLite 只有逻辑隔离,生产仍需更强存储证据;
- 身份代理配置错误可能破坏入口隔离。
## 8. 测试要求
至少覆盖:
- Tenant B 读取 Tenant A Task;
- Tenant B 查询或取消 Tenant A OpenClawCommand;
- Worker 跨 tenant/cell/profile claim;
- Runtime 返回错误 tenant/task/trace;
- Evidence 和 Audit 跨租户查询;
- 缺少 Tenant Context 的请求;
- 客户端伪造身份 Header。
## 9. 合规检查
任何新模块没有以下内容时不得进入 Candidate:
- 显式 `tenant_id`;
- 所有权检查;
- 跨租户负向测试;
- 审计映射;
- 文档与追踪矩阵更新。
## 10. 实现与测试映射
- Control Plane:`active-runtime/app/aios_v2/control_plane.py`
- OpenClaw:`active-runtime/app/aios_openclaw/`
- Hermes:`active-runtime/app/aios_hermes/`
- Evidence:`active-runtime/app/aios_v2/execution_tracking.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
@@ -1,43 +1,116 @@
# ADR-005: 文档与代码同步门禁
# ADR-005:文档与代码同步门禁
- Status: Accepted
- Date: 2026-07-20
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:需求、架构、代码、测试、部署、在线任务、发布和归档
## Context
## 1. 背景
架构、功能和部署变化如果不同步文档,会导致版本无法审计,后续 Agent 无法判断真实设计。
架构、功能和部署变化如果不同步文档,会导致版本无法审计,后续 Agent 无法判断真实设计,发布人员也无法确认代码是否符合用户需求。历史上还出现过文档目录不存在、文档含冲突标记、分支名与代码内容不一致等问题。
## Decision
## 2. 决策
所有影响以下内容的变更必须同步 `docs/aios-v2/`:
所有影响以下内容的变更必须在同一交付流程中同步 `docs/aios-v2/`:
- 用户行为;
- API;
- 数据模型;
- 状态机;
- Runtime Boundary;
- 安全策略;
- 部署流程;
- 测试门禁。
- 用户行为和功能范围;
- API、Header、请求和响应;
- 数据模型、字段和状态机;
- Control Plane、Agent Router、OpenClaw、Hermes 的边界;
- 多租户和安全策略;
- 持久化、审计和执行证据;
- 部署、健康检查和回滚;
- 测试和发布门禁。
## Gate
正式文档统一使用中文,技术标识保留代码中的原始英文名称。
验收必须检查:
## 3. 门禁要求
1. 文档引用代码路径存在;
2. API 文档有实现映射;
3. Contract 字段有模型映射;
4. Traceability Matrix 已更新;
5. ADR 与架构变化一致。
验收必须执行:
## Consequences
```text
python3 tools/aios_v2_docs_code_check.py
```
优点:
检查至少包括:
- 架构长期可维护;
- Agent 执行有明确上下文;
- Bug 修复有历史依据。
1. 必需文档和 ADR 存在且非空;
2. 文档以中文叙述为主;
3. 文档引用的代码、测试和部署路径存在;
4. 核心 API 能在代码中找到;
5. 核心模型和字段能在代码中找到;
6. Traceability Matrix 覆盖所有 `UR-*`;
7. ADR 索引引用的文件存在;
8. 文档中不存在 Git 冲突标记;
9. 不存在摘要占位符或空章节;
10. 架构/功能代码变化时,同一交付范围包含文档变化。
成本:
## 4. 状态影响
- 小功能也需要更新对应文档。
门禁失败时:
- Candidate 不得标记为 `Test-verified`;
- OpenClaw 不得继续生产部署;
- Codex 必须登记 blocker;
- 修复必须进入 Git 分支;
- Owner 不得批准 Tag 或生产部署。
## 5. 备选方案
### 方案 A:依赖人工记忆更新文档
拒绝原因:长期必然产生文档漂移。
### 方案 B:发布前集中补文档
拒绝原因:无法确定每次代码变化对应的真实设计,也会增加错误归因。
### 方案 C:只检查文件存在
拒绝原因:文件存在不代表内容完整、路径有效或与代码一致。
## 6. 正面影响
- 架构和代码长期一致;
- Agent 有明确执行上下文;
- Bug 修复可以追踪需求和决策;
- 归档包可以作为正式交付材料;
- 发布风险和未验证项更透明。
## 7. 负面影响与风险
- 小功能也需要更新对应文档;
- 检查器需要随代码结构维护;
- 自动检查不能替代架构审查;
- 文档完整性会增加提交内容,但这是可审计交付的必要成本。
## 8. 文档更新映射
- 用户需求变化:`01-USER-REQUIREMENTS.md`;
- 架构边界变化:`02-TARGET-ARCHITECTURE.md` 和 ADR;
- 迁移/发布变化:`03-MIGRATION-AND-DELIVERY.md`;
- Contract 变化:`04-MODULE-CONTRACTS.md`;
- 路径和状态变化:`05-TRACEABILITY-MATRIX.md`;
- 文档规则变化:`06-DOCUMENT-GOVERNANCE.md`。
## 9. 在线任务要求
OpenClaw 和 Codex 的任何修改或阻塞必须回写:
- claim;
- heartbeat;
- progress;
- blocker;
- branch;
- commit;
- test_status;
- result;
- 文档同步状态。
OpenClaw 发现架构影响时必须停止扩大修复范围,并升级给 Codex。
## 10. 实现与验收映射
- 检查器:`tools/aios_v2_docs_code_check.py`
- 验收入口:`deploy/aios-v2/acceptance/run-all.sh`
- 治理规范:`docs/aios-v2/06-DOCUMENT-GOVERNANCE.md`
- 追踪矩阵:`docs/aios-v2/05-TRACEABILITY-MATRIX.md`
@@ -0,0 +1,142 @@
# ADR-006:候选持久化采用可替换 SQLite Snapshot
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Control Plane、Persistence、Deployment、Backup、Recovery、HA 演进
## 1. 背景
AIOS V2 候选版本需要在服务重启后恢复 Tenant、Agent、Task、Audit 和 Idempotency 状态,同时又必须尽快进入预发布部署与 Bug 修复阶段。
候选阶段如果立即引入完整分布式数据库、消息系统和 HA,会显著扩大开发范围,推迟控制面、Runtime Boundary 和多租户模型的验证。
## 2. 决策
候选版本使用可替换的 SQLite WAL 原子 Snapshot:
- `SQLiteSnapshotRepository` 保存版本化 Control Plane Snapshot;
- `DurableControlPlaneService` 在成功变更后持久化;
- 使用 WAL、`synchronous=FULL` 和 `BEGIN IMMEDIATE`;
- 状态目录独立于代码版本;
- 上层 Domain、API、Orchestrator 和 Runtime Contract 不依赖 SQLite 实现。
执行证据使用租户分区 JSON 文件,由 `ExecutionTracker` 原子写入。
## 3. 持久化范围
候选 Snapshot 包含:
- Tenant;
- Agent;
- Task;
- Audit;
- Idempotency。
独立 Evidence 目录包含:
- claim;
- heartbeat;
- progress;
- blocker;
- commit;
- test_status;
- result。
## 4. 选择理由
- 无额外外部依赖;
- 易于快速部署和回滚;
- 支持事务、WAL 和完整性检查;
- 可以验证上层控制面和 Runtime Contract;
- 便于在预发布阶段复现和修复 Bug;
- 可通过 Repository 边界替换为生产数据库。
## 5. 备选方案
### 方案 A:候选阶段直接使用 PostgreSQL 集群
暂不采用原因:会增加部署、迁移、HA、Secret、网络和容量范围,延迟核心架构验证。
### 方案 B:继续使用纯内存状态
拒绝原因:服务重启会丢失权威状态,无法进入预发布。
### 方案 C:每个模块各自使用 SQLite
拒绝原因:会形成多个事实源和跨模块事务问题。
### 方案 D:将完整状态写入 Git
拒绝原因:运行状态不应成为 Git 内容,可能暴露敏感业务数据并造成提交风暴。
## 6. 正面影响
- 候选版具备重启恢复能力;
- 状态与代码版本解耦;
- 回滚默认只切换代码,不覆盖新状态;
- 可快速进入预发布测试;
- 生产后端替换不改变上层 Contract。
## 7. 负面影响与风险
- SQLite 主要适用于单进程候选部署;
- 尚未提供多实例写入和分布式锁;
- 租户隔离是逻辑隔离,不是 RLS 或物理隔离;
- 大规模任务、审计和并发容量尚未验证;
- Evidence 文件数量增长需要归档策略;
- Snapshot 粒度可能导致状态增长后的写放大。
## 8. 生产迁移要求
生产数据层替换必须满足:
1. 保持 Control Plane Domain/API Contract 不变;
2. 提供 schema migration;
3. 提供租户 RLS 或物理隔离;
4. 提供备份、恢复、PITR 和灾难恢复;
5. 提供 HA、多实例并发和故障转移;
6. 提供容量、延迟和锁等待测试;
7. 保持 Audit 与 Evidence 可追踪;
8. 提供从 SQLite Candidate 到生产后端的数据迁移工具;
9. 通过新的 ADR 说明生产持久化决策。
## 9. 回滚策略
- 回滚前备份 `/var/lib/aios-v2`;
- 默认只回滚代码;
- 状态恢复必须由 Owner 显式授权;
- 恢复前必须验证 Snapshot schema 兼容性;
- 回滚后执行 SQLite `PRAGMA quick_check` 和 AIOS Healthcheck。
## 10. 验收要求
候选版至少通过:
- 创建 Tenant、Agent、Task;
- 服务重启;
- 读取恢复后的 Task;
- Idempotency 仍有效;
- Audit 仍存在;
- SQLite `quick_check=ok`;
- 状态目录权限正确;
- 回滚演练不丢失新增状态。
## 11. 实现与测试映射
- Repository:`active-runtime/app/aios_v2/persistence.py`
- 应用入口:`active-runtime/app/aios_v2/app.py`
- Evidence:`active-runtime/app/aios_v2/execution_tracking.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
- 健康检查:`deploy/aios-v2/healthcheck/check.sh`
- 回滚:`deploy/aios-v2/rollback/rollback.sh`
## 12. 更新触发条件
以下变化必须更新或替代本 ADR:
- 引入生产数据库;
- 支持多实例写入;
- 修改 Snapshot 格式;
- 改变 Evidence 存储;
- 增加 RLS、HA、分片或事件溯源;
- 修改状态回滚策略。
+52 -24
View File
@@ -1,39 +1,67 @@
# AIOS V2 Architecture Decision Records
# AIOS V2 架构决策记录索引
本目录记录影响多个模块、长期演进、兼容性、安全或交付流程的关键架构决策。
- 文档版本:`V2R1C0-candidate`
- 适用范围:影响多个模块、长期演进、兼容性、安全、数据、部署或交付流程的关键架构决策
## ADR 列表
## 1. 架构决策记录列表
| ADR | 决策 |
|---|---|
| [ADR-001](ADR-001-single-control-plane.md) | AIOS 作为唯一控制面 |
| [ADR-002](ADR-002-openclaw-hermes-decoupling.md) | OpenClaw/Hermes 与控制面解耦 |
| [ADR-003](ADR-003-gitops-only-change-flow.md) | 所有变更必须通过 GitOps |
| [ADR-004](ADR-004-multi-tenant-isolation.md) | 多租户隔离默认拒绝 |
| [ADR-005](ADR-005-document-code-sync-gate.md) | 文档与代码同步门禁 |
| [ADR-006](ADR-006-candidate-persistence-strategy.md) | 候选持久化采用可替换 SQLite Snapshot |
| 编号 | 决策 | 状态 |
|---|---|---|
| [ADR-001](ADR-001-single-control-plane.md) | AIOS 作为唯一控制面 | 已采纳 |
| [ADR-002](ADR-002-openclaw-hermes-decoupling.md) | OpenClaw 与 Hermes 从控制面职责中解耦 | 已采纳 |
| [ADR-003](ADR-003-gitops-only-change-flow.md) | 所有变更必须通过 GitOps | 已采纳 |
| [ADR-004](ADR-004-multi-tenant-isolation.md) | 多租户隔离采用默认拒绝模型 | 已采纳 |
| [ADR-005](ADR-005-document-code-sync-gate.md) | 文档与代码同步门禁 | 已采纳 |
| [ADR-006](ADR-006-candidate-persistence-strategy.md) | 候选持久化采用可替换 SQLite Snapshot | 已采纳 |
## ADR 状态
## 2. 状态定义
- `Proposed`:提出但未采纳。
- `Accepted`:当前有效决策。
- `Superseded`:被新 ADR 替代。
- `Deprecated`:保留历史但禁止新实现采用。
- **已提出**:决策已提出,但尚未被正式采纳。
- **已采纳**:当前有效,所有新实现必须遵守。
- **已替代**:已被新的 ADR 替代,保留用于历史追踪。
- **已废弃**:保留历史,但禁止新实现继续采用。
## 何时必须新增或更新 ADR
## 3. 必须新增或更新 ADR 的场景
以下变化必须新增或更新 ADR:
1. 控制面与 Runtime 权限边界变化;
1. AIOS、OpenClaw、Hermes、Agent Router 的控制边界变化;
2. 多租户隔离模型变化;
3. API、数据模型或状态机出现不兼容变化;
4. 持久化、消息、密钥、审计或部署方案发生跨模块变化;
5. GitOps、发布、回滚或文档门禁变化;
6. 替换 OpenClaw、Hermes、Agent Router 或核心基础设施。
6. 替换 OpenClaw、Hermes、Agent Router 或核心基础设施;
7. 引入新的权威事实源、任务生命周期或租户状态;
8. 生产安全模型、身份代理或 Secret 管理方式变化。
ADR 变化必须同步更新:
## 4. ADR 内容要求
- `02-TARGET-ARCHITECTURE.md`
- `04-MODULE-CONTRACTS.md`
- `05-TRACEABILITY-MATRIX.md`
- 对应代码、测试和部署资产
每份 ADR 必须包含:
- 背景;
- 决策;
- 备选方案;
- 采用理由;
- 正面影响;
- 负面影响和风险;
- 迁移与回滚;
- 合规检查;
- 受影响的代码、测试和文档。
## 5. ADR 变更联动
ADR 新增、替代或废弃时,必须同步检查并更新:
- `02-TARGET-ARCHITECTURE.md`;
- `03-MIGRATION-AND-DELIVERY.md`;
- `04-MODULE-CONTRACTS.md`;
- `05-TRACEABILITY-MATRIX.md`;
- 相关代码、测试和部署资产;
- `AIOS-V2R1-PRODUCTION-CANDIDATE.md` 中的风险与发布结论。
## 6. 审查规则
- OpenClaw 不得自行采纳改变控制面边界的 ADR;发现需求后必须升级给 Codex。
- Codex 负责审查 ADR 与代码、测试和文档是否一致。
- 涉及合并、Tag、生产部署或重大风险接受的 ADR,必须由 AIOS Owner 最终批准。
- ADR 未完成审查前,相关变更不得标记为 `Test-verified` 或 `Production released`。
@@ -1,9 +1,9 @@
{
"type": "branch",
"branch": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"base": "integration/aios-v2r1-cto-production-candidate-20260720",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-20T03:49:13Z",
"created_at": "2026-07-20T03:49:09Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,51 @@
# AIOS OpenClaw Run Provenance Writer
本插件为 AIOS V2R1 提供 Owner 签名的运行授权、持久 Provenance Store 与专用 Evidence Writer。它不授予通用 Shell 或任意文件写权限。
## AIOS Runtime HTTP 合同
插件在 OpenClaw loopback Gateway 内注册 `/v1/health`、`/v1/execute` 和
`/v1/commands/<id>`。执行请求必须同时通过本机 CredentialRef 比对与 Ed25519
签名验证;热路径只读取公钥,不读取 Candidate 私密签名材料。
Runtime 仅允许无副作用的 `status` 与在线验收专用 `acceptance.delay`。它不把
返回成功冒充为模型推理或任意 Agent 执行。命令身份、幂等键和终态以追加式
JSONL 持久化;Gateway 重启时遗留的 `RUNNING` 命令会收敛为
`FAILED/restart_interrupted`,避免把重启误报成真实超时。
取消只对当前进程内仍在执行的命令生效,超时由签名命令的 `expires_at` 与
30 秒硬上限共同约束。未知 action、错误签名、过期命令、跨 tenant 状态查询、
冲突的 `command_id` 或幂等键均失败关闭。
## 安全边界
- 签名合同:`aios.run-provenance/v1`,Ed25519,授权最长 15 分钟。
- 授权绑定:`run_id`、`sender_id`、任务、执行器实例、当前 Claim OID、Candidate OID 与任务分支。
- 路径:仅允许七个 `.agent/*.json` Evidence 文件。
- 操作:仅允许 `create/update`,随后执行固定 argv 的 `git add`、`git commit` 和当前任务分支普通 push。
- 通用 `exec/shell/bash/write/edit/apply_patch/file_write` 始终拒绝。
- `toolCallId` permit 只能消费一次;参数摘要变化、过期、签名篡改、Run/Sender 不匹配均拒绝。
- Writer 拒绝 symlink、路径逃逸、脏 Worktree、错误分支、错误 HEAD、Candidate 非祖先及暂存范围扩大。
- Gateway 以服务用户调用 Git 时,仅对配置中的精确 `writerRepoRoot` 注入命令级 `safe.directory`,不写全局 Git 配置。
- 禁止 reset、rebase、amend、Tag 和 force push。提交后 push 失败时保留可审计的 local-ahead 状态,不伪造回滚。
## 组件
- `issuer.mjs`:在受保护边界读取现有 Ed25519 材料,生成公开验签材料并原子写入一次性签发记录。
- `provenance.js`:严格 Schema、canonical JSON、Ed25519 sign/verify 和安全 Store 读取。
- `index.js`:OpenClaw hook、Owner/Run/Sender 验证与一次性 permit。
- `writer.js`:限定路径原子写入、固定 Git 操作和追加式审计。
## 部署与回滚
部署必须先备份运行插件和 OpenClaw 配置,再安装 Git 中已测试的完整目录,配置绝对路径、Key ID、任务分支和审计路径,执行 `openclaw plugins doctor` 后才重启 Gateway。回滚只恢复备份插件与配置并重启,不修改任务分支历史或业务状态。
## 验证
执行:
```bash
node --test test/*.test.js
```
验收还必须包含受保护签发、Heartbeat 实际 commit/push、审计链验证,以及 `README.md` 等未授权路径的拒绝测试。
+1
View File
@@ -0,0 +1 @@
export { default } from "../index.js";
+1
View File
@@ -0,0 +1 @@
export * from "../policy.js";
+1
View File
@@ -0,0 +1 @@
export * from "../provenance.js";
+1
View File
@@ -0,0 +1 @@
export * from "../writer.js";
+178
View File
@@ -0,0 +1,178 @@
import crypto from "node:crypto";
import path from "node:path";
import { canonicalize, loadStoredRecord, readPublicKey, validatePayload, verifyRecord } from "./provenance.js";
import { classifyRun, decideToolCall, runKey, validateWriterParams } from "./policy.js";
import { executeEvidenceWrite } from "./writer.js";
import { createRuntimeApi } from "./runtime.js";
const activeRuns = new Map();
const permits = new Map();
const MAX_RUN_AGE_MS = 60 * 60 * 1000;
const DEFAULT_PERMIT_TTL_MS = 2 * 60 * 1000;
function pluginConfig(event, api) {
return event?.context?.pluginConfig ?? api.pluginConfig ?? {};
}
function prune(now = Date.now()) {
for (const [key, value] of activeRuns) if (now - value.recordedAt > MAX_RUN_AGE_MS) activeRuns.delete(key);
for (const [key, value] of permits) if (now - value.recordedAt > value.ttlMs) permits.delete(key);
}
function toolCallId(event) {
return typeof event?.toolCallId === "string" && event.toolCallId.trim() ? event.toolCallId.trim() : undefined;
}
function eventParams(event) {
return event?.params ?? event?.parameters;
}
function requireAbsoluteConfig(config, key) {
const value = config[key];
if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`config_${key}_missing`);
return value;
}
export function authorizeEvidenceCall({ event, trust, config, now = Date.now() }) {
const callId = toolCallId(event);
if (!callId) return { ok: false, code: "tool_call_id_missing" };
if (!trust?.trusted) return { ok: false, code: trust?.reason || "run_provenance_missing" };
const params = eventParams(event);
if (!params || typeof params !== "object" || Array.isArray(params)) return { ok: false, code: "tool_params_missing" };
let record;
try { record = loadStoredRecord(requireAbsoluteConfig(config, "provenanceStoreDir"), params.provenance_id); }
catch (error) {
const code = error instanceof Error && ["unsafe_provenance_file", "invalid_provenance_location", "provenance_record_too_large"].includes(error.message)
? error.message : "provenance_store_rejected";
return { ok: false, code };
}
const validation = validatePayload(record.payload, now);
if (!validation.ok) return validation;
let publicKey;
try { publicKey = readPublicKey(requireAbsoluteConfig(config, "provenancePublicKeyPath")); }
catch { return { ok: false, code: "public_key_rejected" }; }
const signature = verifyRecord(record, publicKey, config.provenanceKeyId);
if (!signature.ok) return signature;
const payload = record.payload;
const runId = runKey(event, {});
if (!runId || payload.run_id !== runId) return { ok: false, code: "run_id_mismatch" };
if (payload.sender_id !== trust.senderId) return { ok: false, code: "sender_id_mismatch" };
if (config.writerTaskBranch && payload.task_branch !== config.writerTaskBranch) return { ok: false, code: "task_branch_mismatch" };
if (config.writerTaskId && payload.task_id !== config.writerTaskId) return { ok: false, code: "task_id_mismatch" };
if (config.provenanceIssuer && payload.issued_by !== config.provenanceIssuer) return { ok: false, code: "issuer_mismatch" };
if (config.executor && payload.executor !== config.executor) return { ok: false, code: "executor_mismatch" };
if (config.executorInstance && payload.executor_instance !== config.executorInstance) return { ok: false, code: "executor_instance_mismatch" };
const paramsValidation = validateWriterParams(params, payload);
if (!paramsValidation.ok) return paramsValidation;
const digest = crypto.createHash("sha256").update(canonicalize(params)).digest("hex");
if (permits.has(callId)) return { ok: false, code: "permit_replay" };
const ttlMs = Number.isSafeInteger(config.permitTtlMs) && config.permitTtlMs > 0
? Math.min(config.permitTtlMs, DEFAULT_PERMIT_TTL_MS)
: DEFAULT_PERMIT_TTL_MS;
permits.set(callId, { payload, paramsDigest: digest, recordedAt: now, ttlMs });
return { ok: true };
}
export function consumePermit(toolCallIdValue, params, now = Date.now()) {
const permit = permits.get(toolCallIdValue);
permits.delete(toolCallIdValue);
if (!permit) throw new Error("permit_missing");
if (now - permit.recordedAt > permit.ttlMs) throw new Error("permit_expired");
const digest = crypto.createHash("sha256").update(canonicalize(params)).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(permit.paramsDigest))) throw new Error("permit_params_mismatch");
return permit;
}
function toolResult(details) {
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
}
function registerWriterTool(api) {
api.registerTool((toolContext) => ({
name: "aios_evidence_write",
label: "AIOS Evidence Writer",
description: "Write one Owner-authorized JSON evidence file and normally push the current task branch.",
parameters: {
type: "object",
additionalProperties: false,
required: ["provenance_id", "path", "operation", "document"],
properties: {
provenance_id: { type: "string" },
path: { type: "string" },
operation: { type: "string", enum: ["create", "update"] },
document: { type: "object" },
},
},
executionMode: "sequential",
async execute(callId, params) {
const permit = consumePermit(callId, params);
const config = toolContext?.getRuntimeConfig?.()?.plugins?.entries?.["aios-tenant-boundary"]?.config
?? api.pluginConfig ?? {};
const result = await executeEvidenceWrite({
repoRoot: requireAbsoluteConfig(config, "writerRepoRoot"),
evidencePath: params.path,
operation: params.operation,
document: params.document,
payload: permit.payload,
pushRemote: config.writerPushRemote ?? "gitea-http",
askPassPath: config.writerAskPassPath ?? "",
auditLogPath: requireAbsoluteConfig(config, "writerAuditLogPath"),
gitBinary: config.gitBinary ?? "/usr/bin/git",
});
return toolResult(result);
},
}), { name: "aios_evidence_write" });
}
export default {
id: "aios-tenant-boundary",
name: "AIOS Tenant Boundary",
description: "Fail-closed Owner provenance and dedicated evidence-writer boundary.",
register(api) {
const runtimeApi = createRuntimeApi(api);
api.registerHttpRoute({ path: "/v1/health", auth: "plugin", match: "exact", handler: runtimeApi.handleHealth });
api.registerHttpRoute({ path: "/v1/execute", auth: "plugin", match: "exact", handler: runtimeApi.handleExecute });
api.registerHttpRoute({ path: "/v1/commands/", auth: "plugin", match: "prefix", handler: runtimeApi.commandRequest });
registerWriterTool(api);
api.on("before_agent_run", async (event, ctx) => {
prune();
const config = pluginConfig(event, api);
const trust = classifyRun(event, ctx, config);
const key = runKey(event, ctx);
if (key) activeRuns.set(key, { ...trust, recordedAt: Date.now() });
if (config.blockUnknownAgentRuns === true && !trust.trusted && trust.reason !== "job_id_present") {
return { outcome: "block", reason: trust.reason, message: "Authenticated Owner provenance is required." };
}
return { outcome: "pass" };
}, { priority: 100 });
api.on("before_tool_call", async (event, ctx) => {
prune();
const config = pluginConfig(event, api);
const trust = activeRuns.get(runKey(event, ctx));
const decision = decideToolCall(event.toolName, trust, config);
if (decision.action === "block") return { block: true, blockReason: `AIOS V2R1 policy denied: ${decision.code}` };
if (decision.action === "verify_provenance") {
const authorized = authorizeEvidenceCall({ event, trust, config });
if (!authorized.ok) return { block: true, blockReason: `AIOS V2R1 provenance denied: ${authorized.code}` };
return undefined;
}
if (decision.action === "approval") {
return { requireApproval: {
title: `Approve ${String(event.toolName)}`,
description: "Authenticated Owner requested a non-evidence side effect.",
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);
});
},
};
export const __state = { activeRuns, permits };
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
import { canonicalize, privateKeyFromMaterial, signRecord, validatePayload } from "./provenance.js";
function parseArgs(argv) {
const result = {};
for (let index = 0; index < argv.length; index += 2) {
const key = argv[index];
const value = argv[index + 1];
if (!key?.startsWith("--") || value === undefined) throw new Error("invalid_arguments");
result[key.slice(2)] = value;
}
return result;
}
function atomicWrite(filePath, contents, mode) {
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o750 });
const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
fs.writeFileSync(temporary, contents, { mode, flag: "wx" });
fs.renameSync(temporary, filePath);
fs.chmodSync(filePath, mode);
}
function requireProtectedPrivateKey(privateKeyPath) {
const stat = fs.lstatSync(privateKeyPath);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("unsafe_private_key");
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) throw new Error("unsafe_private_key_mode");
}
export function issueFromFiles({ payloadPath, privateKeyPath, publicKeyPath, storeDir, keyId, now = Date.now() }) {
for (const value of [payloadPath, privateKeyPath, publicKeyPath, storeDir, keyId]) {
if (typeof value !== "string" || !value) throw new Error("missing_argument");
}
const payload = JSON.parse(fs.readFileSync(payloadPath, "utf8"));
const validated = validatePayload(payload, now);
if (!validated.ok) throw new Error(validated.code);
requireProtectedPrivateKey(privateKeyPath);
const recordPath = path.join(storeDir, `${payload.provenance_id}.json`);
if (fs.existsSync(recordPath)) throw new Error("provenance_id_already_issued");
const privateMaterial = fs.readFileSync(privateKeyPath);
const privateKey = privateKeyFromMaterial(privateMaterial);
const publicPem = crypto.createPublicKey(privateKey).export({ format: "pem", type: "spki" });
const record = signRecord(payload, privateMaterial, keyId);
atomicWrite(publicKeyPath, publicPem, 0o644);
atomicWrite(recordPath, `${canonicalize(record)}\n`, 0o640);
return payload.provenance_id;
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
try {
const args = parseArgs(process.argv.slice(2));
const provenanceId = issueFromFiles({
payloadPath: args.payload,
privateKeyPath: args["private-key"],
publicKeyPath: args["public-key"],
storeDir: args["store-dir"],
keyId: args["key-id"],
});
process.stdout.write(`PROVENANCE_ISSUED=${provenanceId}\n`);
} catch (error) {
process.stderr.write(`PROVENANCE_ISSUE_FAILED=${error instanceof Error ? error.message : "unknown"}\n`);
process.exitCode = 1;
}
}
@@ -0,0 +1,44 @@
{
"id": "aios-tenant-boundary",
"name": "AIOS Tenant Boundary",
"description": "Enforces Owner-signed run provenance, a dedicated evidence writer, and the signed AIOS runtime contract.",
"version": "2.1.0-rc3",
"entry": "dist/index.js",
"contracts": {
"tools": ["aios_evidence_write"]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"requireExplicitTrustedSenders": { "type": "boolean", "default": true },
"requireSenderIsOwner": { "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 },
"blockUnknownAgentRuns": { "type": "boolean", "default": true },
"approvalTimeoutMs": { "type": "integer", "minimum": 1000, "maximum": 300000 },
"permitTtlMs": { "type": "integer", "minimum": 1000, "maximum": 120000 },
"provenanceStoreDir": { "type": "string" },
"provenancePublicKeyPath": { "type": "string" },
"provenanceKeyId": { "type": "string" },
"provenanceIssuer": { "type": "string" },
"executor": { "type": "string" },
"executorInstance": { "type": "string" },
"writerRepoRoot": { "type": "string" },
"writerTaskId": { "type": "string" },
"writerTaskBranch": { "type": "string" },
"writerPushRemote": { "type": "string" },
"writerAskPassPath": { "type": "string" },
"writerAuditLogPath": { "type": "string" },
"gitBinary": { "type": "string" },
"runtimeStatePath": { "type": "string" },
"runtimeSigningPublicKeyPath": { "type": "string" },
"runtimeSigningKeyId": { "type": "string" },
"runtimeCredentialRef": { "type": "string" },
"runtimeMaxActive": { "type": "integer", "minimum": 1, "maximum": 16 },
"runtimeMaxDelayMs": { "type": "integer", "minimum": 100, "maximum": 30000 }
}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@aios/openclaw-tenant-boundary",
"version": "2.1.0-rc.3",
"type": "module",
"description": "AIOS V2R1 Tenant Cell provenance and side-effect policy boundary for OpenClaw.",
"files": [
"index.js",
"policy.js",
"provenance.js",
"writer.js",
"runtime.js",
"issuer.mjs",
"dist",
"openclaw.plugin.json"
],
"scripts": {
"test": "node --test test/*.test.js"
},
"openclaw": {
"extensions": [
"./dist/index.js"
]
}
}
+73
View File
@@ -0,0 +1,73 @@
import { ALLOWED_OPERATIONS, ALLOWED_PATHS } from "./provenance.js";
const DEFAULT_READ_ONLY_TOOLS = Object.freeze([
"read", "grep", "glob", "find", "ls", "web_search", "web_fetch",
"memory_search", "sessions_list", "sessions_history", "session_status",
]);
const GENERIC_MUTATION_TOOLS = new Set([
"exec", "shell", "bash", "write", "edit", "apply_patch", "file_write",
"computer", "process", "terminal",
]);
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(event = {}, ctx = {}, config = {}) {
const senderId = resolveSenderId({ ...ctx, senderId: event.senderId ?? ctx.senderId });
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 };
}
if (config.requireSenderIsOwner !== false && event.senderIsOwner !== true) {
return { trusted: false, origin: "non_owner", reason: "sender_not_owner", senderId };
}
return { trusted: true, origin: "authenticated_owner", reason: "owner_verified", senderId };
}
export function runKey(event = {}, ctx = {}) {
const key = event.runId || ctx.runId;
return typeof key === "string" && key.trim() ? key.trim() : undefined;
}
export function decideToolCall(toolName, runTrust, config = {}) {
const name = typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
if (!name) return { action: "block", code: "tool_name_missing" };
if (GENERIC_MUTATION_TOOLS.has(name)) return { action: "block", code: "dedicated_writer_required" };
const configured = strings(config.readOnlyTools).map((item) => item.toLowerCase());
const readOnly = new Set(configured.length ? configured : DEFAULT_READ_ONLY_TOOLS);
if (readOnly.has(name)) return { action: "allow", code: "read_only" };
if (!runTrust?.trusted) return { action: "block", code: runTrust?.reason || "run_provenance_missing" };
if (name === "aios_evidence_write") return { action: "verify_provenance", code: "dedicated_writer" };
if (config.requireApprovalForHumanSideEffects === false) return { action: "allow", code: "trusted_side_effect" };
return { action: "approval", code: "human_side_effect" };
}
export function validateWriterParams(params = {}, payload) {
const keys = Object.keys(params).sort();
const expected = ["document", "operation", "path", "provenance_id"];
if (keys.length !== expected.length || expected.some((key) => !keys.includes(key))) return { ok: false, code: "tool_params_schema_mismatch" };
if (params.provenance_id !== payload.provenance_id) return { ok: false, code: "provenance_id_mismatch" };
if (!ALLOWED_PATHS.includes(params.path)) return { ok: false, code: "path_not_allowed" };
if (params.operation !== "create" && params.operation !== "update") return { ok: false, code: "operation_not_allowed" };
if (!payload.allowed_paths.includes(params.path)) return { ok: false, code: "path_not_authorized" };
for (const operation of [params.operation, "git add", "git commit", "normal push current task branch"]) {
if (!payload.allowed_operations.includes(operation)) return { ok: false, code: "operation_not_authorized" };
}
let serialized;
try { serialized = JSON.stringify(params.document); } catch { return { ok: false, code: "document_not_json" }; }
if (serialized === undefined || Buffer.byteLength(serialized) > 256 * 1024) return { ok: false, code: "document_invalid_or_too_large" };
return { ok: true };
}
+141
View File
@@ -0,0 +1,141 @@
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
export const ALLOWED_PATHS = Object.freeze([
".agent/claim.json",
".agent/heartbeat.json",
".agent/progress.json",
".agent/status.json",
".agent/blockers.json",
".agent/result.json",
".agent/credential-status.json",
]);
export const ALLOWED_OPERATIONS = Object.freeze([
"create",
"update",
"git add",
"git commit",
"normal push current task branch",
]);
const PAYLOAD_FIELDS = Object.freeze([
"schema_version", "provenance_id", "run_id", "sender_id", "task_id",
"executor", "executor_instance", "claim_oid", "candidate_oid", "task_branch",
"allowed_paths", "allowed_operations", "issued_by", "issued_at", "expires_at",
]);
const OID_RE = /^[0-9a-f]{40}$/;
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const MAX_LIFETIME_MS = 15 * 60 * 1000;
function normalized(value) {
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(normalized);
const result = {};
for (const key of Object.keys(value).sort()) {
if (value[key] !== undefined) result[key] = normalized(value[key]);
}
return result;
}
export function canonicalize(value) {
return JSON.stringify(normalized(value));
}
function exactStringSet(actual, expected) {
if (!Array.isArray(actual) || actual.some((v) => typeof v !== "string")) return false;
if (actual.length !== expected.length || new Set(actual).size !== expected.length) return false;
const set = new Set(actual);
return expected.every((value) => set.has(value));
}
function failure(code) {
return { ok: false, code };
}
export function validatePayload(payload, now = Date.now()) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return failure("payload_not_object");
const keys = Object.keys(payload).sort();
if (keys.length !== PAYLOAD_FIELDS.length || PAYLOAD_FIELDS.some((key) => !keys.includes(key))) {
return failure("payload_schema_mismatch");
}
if (payload.schema_version !== "aios.run-provenance/v1") return failure("schema_version_mismatch");
for (const key of ["run_id", "sender_id", "task_id", "executor", "executor_instance", "task_branch", "issued_by"]) {
if (typeof payload[key] !== "string" || !payload[key].trim()) return failure(`invalid_${key}`);
}
if (!ID_RE.test(payload.provenance_id)) return failure("invalid_provenance_id");
if (!OID_RE.test(payload.claim_oid) || !OID_RE.test(payload.candidate_oid)) return failure("invalid_oid");
if (!exactStringSet(payload.allowed_paths, ALLOWED_PATHS)) return failure("allowed_paths_mismatch");
if (!exactStringSet(payload.allowed_operations, ALLOWED_OPERATIONS)) return failure("allowed_operations_mismatch");
if (!Number.isSafeInteger(payload.issued_at) || !Number.isSafeInteger(payload.expires_at)) return failure("invalid_time");
if (payload.expires_at <= payload.issued_at || payload.expires_at - payload.issued_at > MAX_LIFETIME_MS) {
return failure("invalid_lifetime");
}
if (now < payload.issued_at) return failure("not_yet_valid");
if (now >= payload.expires_at) return failure("expired");
return { ok: true, payload };
}
export function privateKeyFromMaterial(material) {
if (!Buffer.isBuffer(material)) material = Buffer.from(material);
if (material.length === 32) {
const pkcs8Prefix = Buffer.from("302e020100300506032b657004220420", "hex");
return crypto.createPrivateKey({ key: Buffer.concat([pkcs8Prefix, material]), format: "der", type: "pkcs8" });
}
if (material.subarray(0, 5).toString() === "-----") return crypto.createPrivateKey(material);
return crypto.createPrivateKey({ key: material, format: "der", type: "pkcs8" });
}
export function signRecord(payload, privateMaterial, keyId) {
const signature = crypto.sign(null, Buffer.from(canonicalize(payload)), privateKeyFromMaterial(privateMaterial));
return {
payload,
signature: { algorithm: "Ed25519", key_id: keyId, value: signature.toString("base64url") },
};
}
export function verifyRecord(record, publicKeyPem, expectedKeyId) {
if (!record || typeof record !== "object" || Array.isArray(record)) return failure("record_not_object");
const signature = record.signature;
if (!signature || signature.algorithm !== "Ed25519" || signature.key_id !== expectedKeyId || typeof signature.value !== "string") {
return failure("signature_contract_mismatch");
}
try {
const ok = crypto.verify(
null,
Buffer.from(canonicalize(record.payload)),
crypto.createPublicKey(publicKeyPem),
Buffer.from(signature.value, "base64url"),
);
return ok ? { ok: true } : failure("signature_invalid");
} catch {
return failure("signature_invalid");
}
}
function safeRegularFile(filePath) {
const stat = fs.lstatSync(filePath);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("unsafe_provenance_file");
if (process.platform !== "win32" && (stat.mode & 0o022) !== 0) throw new Error("unsafe_provenance_file");
}
export function loadStoredRecord(storeDir, provenanceId) {
if (!path.isAbsolute(storeDir) || !ID_RE.test(provenanceId)) throw new Error("invalid_provenance_location");
const storeReal = fs.realpathSync(storeDir);
const target = path.join(storeReal, `${provenanceId}.json`);
safeRegularFile(target);
const targetReal = fs.realpathSync(target);
if (path.dirname(targetReal) !== storeReal || path.basename(targetReal) !== `${provenanceId}.json`) {
throw new Error("unsafe_provenance_file");
}
const raw = fs.readFileSync(target, "utf8");
if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("provenance_record_too_large");
return JSON.parse(raw);
}
export function readPublicKey(publicKeyPath) {
if (!path.isAbsolute(publicKeyPath)) throw new Error("invalid_public_key_path");
safeRegularFile(publicKeyPath);
return fs.readFileSync(publicKeyPath, "utf8");
}
+280
View File
@@ -0,0 +1,280 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const CONTRACT_VERSION = "aios.openclaw.command.v1";
const MAX_BODY_BYTES = 64 * 1024;
const CLOCK_SKEW_MS = 300_000;
const COMMAND_FIELDS = [
"command_id", "tenant_id", "task_id", "trace_id", "action", "payload",
"idempotency_key", "created_at", "expires_at", "contract_version", "key_id",
];
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function sorted(value) {
if (Array.isArray(value)) return value.map(sorted);
if (!isObject(value)) return value;
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])]));
}
function canonical(value) {
return JSON.stringify(sorted(value));
}
function nowIso() {
return new Date().toISOString();
}
function sendJson(res, status, body) {
const data = Buffer.from(JSON.stringify(body));
res.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": String(data.length),
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
});
res.end(data);
return true;
}
async function readJson(req) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > MAX_BODY_BYTES) throw new Error("body_too_large");
chunks.push(chunk);
}
let value;
try { value = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); }
catch { throw new Error("invalid_json"); }
if (!isObject(value)) throw new Error("invalid_json_object");
return value;
}
function requireAbsolute(config, name) {
const value = config[name];
if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`config_${name}_missing`);
return value;
}
function appendRecord(statePath, record) {
const parent = path.dirname(statePath);
fs.mkdirSync(parent, { recursive: true, mode: 0o750 });
const fd = fs.openSync(statePath, "a", 0o640);
try {
fs.writeSync(fd, `${JSON.stringify(record)}\n`, null, "utf8");
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
fs.chmodSync(statePath, 0o640);
}
function validateCommand(value, expectedKeyId) {
const allowed = new Set([...COMMAND_FIELDS, "signature"]);
if (Object.keys(value).some((key) => !allowed.has(key))) throw new Error("unknown_command_field");
for (const name of COMMAND_FIELDS.filter((name) => name !== "expires_at" && name !== "payload")) {
if (typeof value[name] !== "string" || !value[name].trim()) throw new Error(`invalid_${name}`);
}
if (!isObject(value.payload)) throw new Error("invalid_payload");
if (typeof value.expires_at !== "string" || !value.expires_at) throw new Error("expires_at_required");
if (typeof value.signature !== "string" || !value.signature) throw new Error("signature_missing");
if (value.contract_version !== CONTRACT_VERSION) throw new Error("contract_version_rejected");
if (value.key_id !== expectedKeyId) throw new Error("key_id_rejected");
const created = Date.parse(value.created_at);
const expires = Date.parse(value.expires_at);
if (!Number.isFinite(created) || !Number.isFinite(expires)) throw new Error("timestamp_invalid");
if (created - Date.now() > CLOCK_SKEW_MS) throw new Error("created_at_in_future");
if (expires <= created || expires - created > 300_000) throw new Error("expiry_window_rejected");
if (Date.now() - expires > CLOCK_SKEW_MS) throw new Error("command_expired");
const unsigned = Object.fromEntries(COMMAND_FIELDS.map((name) => [name, value[name]]));
return { unsigned, expires };
}
function verifySignature(command, publicKey, expectedKeyId) {
const { unsigned, expires } = validateCommand(command, expectedKeyId);
let signature;
try { signature = Buffer.from(command.signature, "base64url"); }
catch { throw new Error("signature_invalid"); }
if (!crypto.verify(null, Buffer.from(canonical(unsigned)), publicKey, signature)) throw new Error("signature_invalid");
return { digest: crypto.createHash("sha256").update(canonical(unsigned)).digest("hex"), expires };
}
function resultFor(command, state, { output = null, error = null, startedAt, finishedAt = nowIso() } = {}) {
return {
command_id: command.command_id, tenant_id: command.tenant_id,
task_id: command.task_id, trace_id: command.trace_id, state, output, error,
started_at: startedAt ?? nowIso(), finished_at: finishedAt,
runtime_id: "openclaw-aios-runtime-v1", attempts: 1,
};
}
function loadState(statePath) {
const commands = new Map();
const idempotency = new Map();
if (!fs.existsSync(statePath)) return { commands, idempotency };
for (const line of fs.readFileSync(statePath, "utf8").split("\n").filter(Boolean)) {
let record;
try { record = JSON.parse(line); } catch { throw new Error("runtime_state_corrupt"); }
if (!isObject(record) || typeof record.command_id !== "string") throw new Error("runtime_state_corrupt");
commands.set(record.command_id, record);
if (record.tenant_id && record.idempotency_key) idempotency.set(`${record.tenant_id}\0${record.idempotency_key}`, record.command_id);
}
return { commands, idempotency };
}
export function createRuntimeApi(api) {
const config = api.pluginConfig ?? {};
const statePath = requireAbsolute(config, "runtimeStatePath");
const publicKeyPath = requireAbsolute(config, "runtimeSigningPublicKeyPath");
const expectedKeyId = String(config.runtimeSigningKeyId || "");
const credentialRef = String(config.runtimeCredentialRef || "");
if (!expectedKeyId) throw new Error("config_runtimeSigningKeyId_missing");
if (!credentialRef) throw new Error("config_runtimeCredentialRef_missing");
const publicKey = crypto.createPublicKey(fs.readFileSync(publicKeyPath));
const maxActive = Math.max(1, Math.min(Number(config.runtimeMaxActive || 4), 16));
const maxDelayMs = Math.max(100, Math.min(Number(config.runtimeMaxDelayMs || 30_000), 30_000));
const state = loadState(statePath);
const active = new Map();
for (const record of state.commands.values()) {
if (record.result?.state === "RUNNING") {
const reconciled = { ...record, result: { ...record.result, state: "FAILED", finished_at: nowIso(), error: { code: "restart_interrupted", message: "execution outcome is unknown after runtime restart" } } };
appendRecord(statePath, reconciled);
state.commands.set(record.command_id, reconciled);
}
}
function requireCredential(req) {
const supplied = String(req.headers["x-aios-credential-ref"] || "");
const left = Buffer.from(supplied);
const right = Buffer.from(credentialRef);
if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) throw new Error("credential_ref_rejected");
}
function persist(command, digest, result) {
const record = {
command_id: command.command_id, tenant_id: command.tenant_id,
task_id: command.task_id, trace_id: command.trace_id,
idempotency_key: command.idempotency_key, digest, result, recorded_at: nowIso(),
};
appendRecord(statePath, record);
state.commands.set(command.command_id, record);
state.idempotency.set(`${command.tenant_id}\0${command.idempotency_key}`, command.command_id);
return record;
}
async function execute(command, digest, expires) {
const startedAt = nowIso();
persist(command, digest, resultFor(command, "RUNNING", { startedAt, finishedAt: null }));
if (command.action === "status") {
const completed = resultFor(command, "COMPLETED", { startedAt, output: { status: "ok", runtime: "openclaw-plugin", side_effects: false } });
persist(command, digest, completed);
return completed;
}
if (command.action !== "acceptance.delay") throw new Error("unsupported_action");
const delayMs = Number(command.payload.delay_ms);
if (!Number.isInteger(delayMs) || delayMs < 0 || delayMs > maxDelayMs) {
const failed = resultFor(command, "FAILED", { startedAt, error: { code: "invalid_delay", message: "delay_ms is outside the allowed range" } });
persist(command, digest, failed);
return failed;
}
const controller = new AbortController();
active.set(command.command_id, controller);
let timedOut = false;
const deadlineMs = Math.max(0, Math.min(maxDelayMs, expires - Date.now()));
const deadline = setTimeout(() => { timedOut = true; controller.abort(); }, deadlineMs);
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, delayMs);
controller.signal.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("aborted")); }, { once: true });
});
const completed = resultFor(command, "COMPLETED", { startedAt, output: { delayed_ms: delayMs, side_effects: false } });
persist(command, digest, completed);
return completed;
} catch {
const terminal = resultFor(command, timedOut ? "TIMED_OUT" : "CANCELLED", {
startedAt,
error: timedOut ? { code: "runtime_timeout", message: "runtime deadline exceeded" } : { code: "cancelled", message: "cancelled by AIOS" },
});
persist(command, digest, terminal);
return terminal;
} finally {
clearTimeout(deadline);
active.delete(command.command_id);
}
}
async function handleExecute(req, res) {
if (req.method !== "POST") return sendJson(res, 405, { error: "method_not_allowed" });
try {
requireCredential(req);
const command = await readJson(req);
const { digest, expires } = verifySignature(command, publicKey, expectedKeyId);
const prior = state.commands.get(command.command_id);
if (prior) {
if (prior.digest !== digest) return sendJson(res, 409, { error: "command_id_conflict" });
return sendJson(res, 200, prior.result);
}
const idemId = state.idempotency.get(`${command.tenant_id}\0${command.idempotency_key}`);
if (idemId) {
const idem = state.commands.get(idemId);
if (!idem || idem.digest !== digest) return sendJson(res, 409, { error: "idempotency_conflict" });
return sendJson(res, 200, idem.result);
}
if (!new Set(["status", "acceptance.delay"]).has(command.action)) return sendJson(res, 422, { error: "unsupported_action" });
if (active.size >= maxActive) return sendJson(res, 429, { error: "runtime_capacity_exceeded" });
return sendJson(res, 200, await execute(command, digest, expires));
} catch (error) {
const code = error instanceof Error ? error.message : "request_rejected";
const status = code === "body_too_large" ? 413 : code.startsWith("config_") || code === "runtime_state_corrupt" ? 503 : 401;
return sendJson(res, status, { error: code });
}
}
function commandRequest(req, res) {
try {
requireCredential(req);
const url = new URL(req.url, "http://localhost");
const match = url.pathname.match(/^\/v1\/commands\/([^/]+)(?:\/(status|cancel))?$/);
if (!match) return false;
const commandId = decodeURIComponent(match[1]);
const operation = match[2] || "status";
const record = state.commands.get(commandId);
if (!record) return sendJson(res, 404, { error: "command_not_found" });
const tenantId = String(url.searchParams.get("tenant_id") || "");
if (!tenantId || tenantId !== record.tenant_id) return sendJson(res, 404, { error: "command_not_found" });
if (operation === "status" && req.method === "GET") {
return sendJson(res, 200, record.result);
}
if (operation === "cancel" && req.method === "POST") {
const controller = active.get(commandId);
if (controller) controller.abort();
return sendJson(res, 200, { accepted: Boolean(controller), command_id: commandId });
}
return sendJson(res, 405, { error: "method_not_allowed" });
} catch (error) {
return sendJson(res, 401, { error: error instanceof Error ? error.message : "request_rejected" });
}
}
return {
handleHealth(req, res) {
if (req.method !== "GET") return sendJson(res, 405, { error: "method_not_allowed" });
return sendJson(res, 200, {
runtime: "openclaw", runtime_id: "openclaw-aios-runtime-v1", status: "healthy",
version: "2.1.0-rc.3", heartbeat_at: nowIso(), active_tasks: active.size,
workspace_isolated: true, credential_ref_configured: true,
details: { transport: "openclaw_plugin_http", allowed_actions: ["status", "acceptance.delay"] },
});
},
handleExecute,
commandRequest,
};
}
export const __test = { canonical, validateCommand, verifySignature, loadState };
@@ -0,0 +1,189 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import plugin, { authorizeEvidenceCall, consumePermit, __state } from "../index.js";
import { decideToolCall, validateWriterParams } from "../policy.js";
import {
ALLOWED_OPERATIONS, ALLOWED_PATHS, canonicalize, signRecord, validatePayload, verifyRecord,
} from "../provenance.js";
import { buildPushArgs, executeEvidenceWrite } from "../writer.js";
import { issueFromFiles } from "../issuer.mjs";
function fixture(now = Date.now()) {
return {
schema_version: "aios.run-provenance/v1",
provenance_id: "prov-test-1",
run_id: "run-test-1",
sender_id: "owner-1",
task_id: "task/openclaw/test",
executor: "openclaw",
executor_instance: "gateway-test",
claim_oid: "a".repeat(40),
candidate_oid: "b".repeat(40),
task_branch: "task/openclaw/test",
allowed_paths: [...ALLOWED_PATHS],
allowed_operations: [...ALLOWED_OPERATIONS],
issued_by: "AIOS_OWNER",
issued_at: now - 1000,
expires_at: now + 60_000,
};
}
function keyFixture() {
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
return {
privateDer: privateKey.export({ format: "der", type: "pkcs8" }),
publicPem: publicKey.export({ format: "pem", type: "spki" }),
};
}
test("Ed25519 record verifies and tampering is rejected", () => {
const payload = fixture();
const keys = keyFixture();
const record = signRecord(payload, keys.privateDer, "key-1");
assert.equal(verifyRecord(record, keys.publicPem, "key-1").ok, true);
record.payload.task_id = "tampered";
assert.equal(verifyRecord(record, keys.publicPem, "key-1").ok, false);
});
test("payload rejects expiry and set drift", () => {
const now = Date.now();
const expired = fixture(now);
expired.issued_at = now - 2000;
expired.expires_at = now - 1000;
assert.equal(validatePayload(expired, now).code, "expired");
const drift = fixture(now);
drift.allowed_paths = drift.allowed_paths.slice(1);
assert.equal(validatePayload(drift, now).code, "allowed_paths_mismatch");
});
test("issuer accepts protected raw 32-byte material and refuses duplicate ids", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-issuer-"));
const keys = keyFixture();
const payload = fixture();
const payloadPath = path.join(root, "payload.json");
const privateKeyPath = path.join(root, "private.bin");
const publicKeyPath = path.join(root, "public.pem");
const storeDir = path.join(root, "store");
fs.writeFileSync(payloadPath, canonicalize(payload));
fs.writeFileSync(privateKeyPath, keys.privateDer.subarray(-32), { mode: 0o600 });
if (process.platform !== "win32") fs.chmodSync(privateKeyPath, 0o600);
assert.equal(issueFromFiles({ payloadPath, privateKeyPath, publicKeyPath, storeDir, keyId: "key-1" }), payload.provenance_id);
const record = JSON.parse(fs.readFileSync(path.join(storeDir, `${payload.provenance_id}.json`), "utf8"));
assert.equal(verifyRecord(record, fs.readFileSync(publicKeyPath, "utf8"), "key-1").ok, true);
assert.throws(() => issueFromFiles({ payloadPath, privateKeyPath, publicKeyPath, storeDir, keyId: "key-1" }), /already_issued/);
});
test("generic mutation tools stay blocked", () => {
for (const name of ["exec", "shell", "bash", "write", "edit", "apply_patch", "file_write"]) {
assert.deepEqual(decideToolCall(name, { trusted: true }, {}), { action: "block", code: "dedicated_writer_required" });
}
});
test("plugin registers the native writer factory and required hook names", () => {
const hooks = [];
const routes = [];
let factory;
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-runtime-register-"));
const keys = keyFixture();
const publicKeyPath = path.join(root, "public.pem");
fs.writeFileSync(publicKeyPath, keys.publicPem);
const api = {
pluginConfig: {
runtimeStatePath: path.join(root, "state.jsonl"),
runtimeSigningPublicKeyPath: publicKeyPath,
runtimeSigningKeyId: "key-1",
runtimeCredentialRef: "secretref:test",
},
registerTool(value, options) { factory = value; assert.equal(options.name, "aios_evidence_write"); },
registerHttpRoute(value) { routes.push(value); },
on(name) { hooks.push(name); },
};
plugin.register(api);
const tool = factory({});
assert.equal(tool.name, "aios_evidence_write");
assert.equal(tool.label, "AIOS Evidence Writer");
assert.equal(tool.executionMode, "sequential");
assert.deepEqual(routes.map((route) => route.path), ["/v1/health", "/v1/execute", "/v1/commands/"]);
assert.deepEqual(hooks.sort(), ["agent_end", "before_agent_run", "before_tool_call"]);
});
test("manifest explicitly declares the dedicated tool contract", () => {
const manifest = JSON.parse(fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf8"));
assert.deepEqual(manifest.contracts.tools, ["aios_evidence_write"]);
});
test("writer params reject unauthorized paths and operations", () => {
const payload = fixture();
assert.equal(validateWriterParams({ provenance_id: payload.provenance_id, path: "README.md", operation: "update", document: {} }, payload).code, "path_not_allowed");
assert.equal(validateWriterParams({ provenance_id: payload.provenance_id, path: ".agent/status.json", operation: "exec", document: {} }, payload).code, "operation_not_allowed");
});
test("authorization binds signature, run, sender, params, and one-use permit", () => {
__state.permits.clear();
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-prov-"));
const store = path.join(root, "store");
fs.mkdirSync(store);
const keys = keyFixture();
const payload = fixture();
const record = signRecord(payload, keys.privateDer, "key-1");
fs.writeFileSync(path.join(store, `${payload.provenance_id}.json`), canonicalize(record), { mode: 0o600 });
const publicKeyPath = path.join(root, "public.pem");
fs.writeFileSync(publicKeyPath, keys.publicPem, { mode: 0o600 });
const params = { provenance_id: payload.provenance_id, path: ".agent/status.json", operation: "update", document: { state: "ok" } };
const event = { toolName: "aios_evidence_write", toolCallId: "call-1", runId: payload.run_id, params };
const config = {
provenanceStoreDir: store, provenancePublicKeyPath: publicKeyPath, provenanceKeyId: "key-1",
writerTaskBranch: payload.task_branch, writerTaskId: payload.task_id, provenanceIssuer: payload.issued_by,
executor: payload.executor, executorInstance: payload.executor_instance,
};
const authorized = authorizeEvidenceCall({ event, trust: { trusted: true, senderId: payload.sender_id }, config });
assert.equal(authorized.ok, true, authorized.code);
assert.equal(consumePermit("call-1", params).payload.run_id, payload.run_id);
assert.throws(() => consumePermit("call-1", params), /permit_missing/);
const wrongRun = { ...event, toolCallId: "call-2", runId: "wrong" };
assert.equal(authorizeEvidenceCall({ event: wrongRun, trust: { trusted: true, senderId: payload.sender_id }, config }).code, "run_id_mismatch");
const wrongSender = { trusted: true, senderId: "wrong" };
assert.equal(authorizeEvidenceCall({ event: { ...event, toolCallId: "call-3" }, trust: wrongSender, config }).code, "sender_id_mismatch");
});
function git(cwd, args) {
const result = spawnSync("git", args, { cwd, encoding: "utf8", shell: false });
if (result.status !== 0) throw new Error(result.stderr || "git failed");
return (result.stdout || "").trim();
}
test("writer commits only one evidence path and explicitly pushes current branch", async (t) => {
if (spawnSync("git", ["--version"], { shell: false }).status !== 0) return t.skip("git unavailable");
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-writer-"));
const repo = path.join(root, "repo");
const bare = path.join(root, "remote.git");
fs.mkdirSync(repo);
git(root, ["init", "--bare", bare]);
git(repo, ["init", "-b", "task/openclaw/test"]);
git(repo, ["config", "user.name", "AIOS Test"]);
git(repo, ["config", "user.email", "aios-test@example.invalid"]);
fs.mkdirSync(path.join(repo, ".agent"));
fs.writeFileSync(path.join(repo, ".agent", "status.json"), "{\"state\":\"before\"}\n");
git(repo, ["add", ".agent/status.json"]);
git(repo, ["commit", "-m", "base"]);
const claim = git(repo, ["rev-parse", "HEAD"]);
git(repo, ["remote", "add", "origin", bare]);
git(repo, ["push", "origin", "refs/heads/task/openclaw/test:refs/heads/task/openclaw/test"]);
const payload = fixture();
payload.claim_oid = claim;
payload.candidate_oid = claim;
const result = await executeEvidenceWrite({
repoRoot: repo, evidencePath: ".agent/status.json", operation: "update",
document: { state: "after" }, payload, pushRemote: "origin",
auditLogPath: path.join(root, "audit", "writer.jsonl"), gitBinary: "git",
});
assert.equal(result.outcome, "pushed");
assert.equal(git(root, ["--git-dir", bare, "rev-parse", "refs/heads/task/openclaw/test"]), result.commit_oid);
assert.deepEqual(buildPushArgs("origin", payload.task_branch), ["push", "origin", "refs/heads/task/openclaw/test:refs/heads/task/openclaw/test"]);
});
@@ -0,0 +1,132 @@
import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Readable } from "node:stream";
import { createRuntimeApi, __test } from "../runtime.js";
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-runtime-"));
const keys = crypto.generateKeyPairSync("ed25519");
const publicKeyPath = path.join(root, "public.pem");
fs.writeFileSync(publicKeyPath, keys.publicKey.export({ format: "pem", type: "spki" }));
const api = createRuntimeApi({ pluginConfig: {
runtimeStatePath: path.join(root, "state.jsonl"),
runtimeSigningPublicKeyPath: publicKeyPath,
runtimeSigningKeyId: "runtime-key-1",
runtimeCredentialRef: "secretref:runtime-test",
runtimeMaxDelayMs: 3000,
} });
return { root, keys, api };
}
function signed(keys, overrides = {}) {
const command = {
command_id: `cmd-${crypto.randomUUID()}`,
tenant_id: "tenant-a",
task_id: "task-a",
trace_id: "trace-a",
action: "status",
payload: { side_effects: false },
idempotency_key: `idem-${crypto.randomUUID()}`,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 60_000).toISOString(),
contract_version: "aios.openclaw.command.v1",
key_id: "runtime-key-1",
...overrides,
};
command.signature = crypto.sign(null, Buffer.from(__test.canonical(command)), keys.privateKey).toString("base64url");
return command;
}
function request(method, url, body = null) {
const req = Readable.from(body === null ? [] : [Buffer.from(JSON.stringify(body))]);
req.method = method;
req.url = url;
req.headers = { "x-aios-credential-ref": "secretref:runtime-test" };
return req;
}
function response() {
let resolve;
const done = new Promise((value) => { resolve = value; });
return {
status: 0,
headers: {},
body: null,
done,
writeHead(status, headers) { this.status = status; this.headers = headers; },
end(data) { this.body = JSON.parse(Buffer.from(data).toString("utf8")); resolve(this); },
};
}
async function call(handler, req) {
const res = response();
await handler(req, res);
return res.done;
}
test("signed status executes, persists, and replays idempotently", async () => {
const { keys, api, root } = fixture();
const command = signed(keys);
const first = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.equal(first.status, 200);
assert.equal(first.body.state, "COMPLETED");
assert.equal(first.body.output.runtime, "openclaw-plugin");
const second = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.deepEqual(second.body, first.body);
assert.ok(fs.statSync(path.join(root, "state.jsonl")).size > 0);
});
test("tamper, expiry, and conflicting identity are rejected", async () => {
const { keys, api } = fixture();
const tampered = signed(keys);
tampered.tenant_id = "tenant-b";
assert.equal((await call(api.handleExecute, request("POST", "/v1/execute", tampered))).status, 401);
const expired = signed(keys, {
created_at: new Date(Date.now() - 700_000).toISOString(),
expires_at: new Date(Date.now() - 600_000).toISOString(),
});
assert.equal((await call(api.handleExecute, request("POST", "/v1/execute", expired))).status, 401);
});
test("status is tenant-scoped and unknown actions fail closed", async () => {
const { keys, api } = fixture();
const rejected = signed(keys, { action: "arbitrary.exec" });
const denied = await call(api.handleExecute, request("POST", "/v1/execute", rejected));
assert.equal(denied.status, 422);
assert.equal(denied.body.error, "unsupported_action");
const command = signed(keys);
await call(api.handleExecute, request("POST", "/v1/execute", command));
const wrong = await call(api.commandRequest, request("GET", `/v1/commands/${command.command_id}?tenant_id=tenant-b`));
assert.equal(wrong.status, 404);
const correct = await call(api.commandRequest, request("GET", `/v1/commands/${command.command_id}?tenant_id=tenant-a`));
assert.equal(correct.body.command_id, command.command_id);
});
test("active delay supports deterministic cancellation", async () => {
const { keys, api } = fixture();
const command = signed(keys, { action: "acceptance.delay", payload: { delay_ms: 2000 } });
const executeResponse = response();
const execution = api.handleExecute(request("POST", "/v1/execute", command), executeResponse);
await new Promise((resolve) => setTimeout(resolve, 30));
const cancelled = await call(api.commandRequest, request("POST", `/v1/commands/${command.command_id}/cancel?tenant_id=tenant-a`));
assert.equal(cancelled.body.accepted, true);
await execution;
const terminal = await executeResponse.done;
assert.equal(terminal.body.state, "CANCELLED");
});
test("signed deadline produces TIMED_OUT", async () => {
const { keys, api } = fixture();
const command = signed(keys, {
action: "acceptance.delay",
payload: { delay_ms: 1000 },
expires_at: new Date(Date.now() + 40).toISOString(),
});
const result = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.equal(result.body.state, "TIMED_OUT");
});
+143
View File
@@ -0,0 +1,143 @@
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import { ALLOWED_PATHS, canonicalize } from "./provenance.js";
function safeFsync(fd) {
try { fs.fsyncSync(fd); }
catch (error) {
if (process.platform !== "win32" || error?.code !== "EPERM") throw error;
}
}
function runGit(args, { cwd, gitBinary = "/usr/bin/git", env = {} }) {
const result = spawnSync(gitBinary, ["-c", `safe.directory=${cwd}`, ...args], {
cwd,
shell: false,
encoding: "utf8",
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env },
});
if (result.error || result.status !== 0) throw new Error(`git_${args[0]}_failed`);
return result.stdout || "";
}
function cleanBranch(branch) {
const value = typeof branch === "string" ? branch.replace(/^refs\/heads\//, "") : "";
if (!value || value.startsWith("-") || value.includes("..") || value.endsWith(".") || value.includes("@{")) {
throw new Error("invalid_branch");
}
for (const forbidden of ["~", "^", ":", "?", "*", "[", "\\", " ", "\t", "\n"]) {
if (value.includes(forbidden)) throw new Error("invalid_branch");
}
return value;
}
export function buildPushArgs(remote, branch) {
const clean = cleanBranch(branch);
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(remote)) throw new Error("invalid_remote");
return ["push", remote, `refs/heads/${clean}:refs/heads/${clean}`];
}
function ensureSafeTarget(repoRoot, evidencePath) {
if (!ALLOWED_PATHS.includes(evidencePath)) throw new Error("path_not_allowed");
const root = fs.realpathSync(repoRoot);
const target = path.resolve(root, evidencePath);
if (!target.startsWith(`${root}${path.sep}`)) throw new Error("path_escape");
let cursor = root;
for (const segment of path.relative(root, path.dirname(target)).split(path.sep).filter(Boolean)) {
cursor = path.join(cursor, segment);
if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink()) throw new Error("symlink_path");
}
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error("symlink_path");
return { root, target };
}
function atomicWrite(target, document, previousMode) {
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o770 });
const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${crypto.randomUUID()}.tmp`);
const mode = previousMode ?? 0o660;
fs.writeFileSync(temporary, `${canonicalize(document)}\n`, { flag: "wx", mode });
const handle = fs.openSync(temporary, "r");
try { safeFsync(handle); } finally { fs.closeSync(handle); }
fs.renameSync(temporary, target);
fs.chmodSync(target, mode);
}
function appendAudit(auditLogPath, entry) {
if (!path.isAbsolute(auditLogPath)) throw new Error("invalid_audit_log_path");
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true, mode: 0o750 });
const fd = fs.openSync(auditLogPath, "a", 0o640);
try {
fs.writeSync(fd, `${canonicalize(entry)}\n`);
safeFsync(fd);
} finally { fs.closeSync(fd); }
}
function gitEnvironment(askPassPath) {
if (!askPassPath) return {};
const stat = fs.lstatSync(askPassPath);
if (!path.isAbsolute(askPassPath) || !stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o022) !== 0) {
throw new Error("unsafe_askpass");
}
return { GIT_ASKPASS: askPassPath, GIT_ASKPASS_REQUIRE: "force", DISPLAY: "aios" };
}
function restoreBeforeCommit(target, existed, backup, mode, root, relative, gitOptions) {
try { runGit(["restore", "--staged", "--", relative], gitOptions); } catch { /* best effort */ }
if (existed) {
fs.writeFileSync(target, backup, { mode });
} else if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
}
export async function executeEvidenceWrite({
repoRoot, evidencePath, operation, document, payload, pushRemote = "gitea-http",
askPassPath = "", auditLogPath, gitBinary = "/usr/bin/git",
}) {
const { root, target } = ensureSafeTarget(repoRoot, evidencePath);
const relative = path.relative(root, target).replace(/\\/g, "/");
const gitOptions = { cwd: root, gitBinary, env: gitEnvironment(askPassPath) };
const branch = cleanBranch(payload.task_branch);
if (runGit(["symbolic-ref", "--short", "HEAD"], gitOptions).trim() !== branch) throw new Error("branch_mismatch");
if (runGit(["rev-parse", "HEAD"], gitOptions).trim() !== payload.claim_oid) throw new Error("head_mismatch");
runGit(["merge-base", "--is-ancestor", payload.candidate_oid, payload.claim_oid], gitOptions);
if (runGit(["status", "--porcelain", "--untracked-files=all"], gitOptions).trim()) throw new Error("worktree_not_clean");
const existed = fs.existsSync(target);
if (operation === "create" && existed) throw new Error("path_exists_for_create");
if (operation === "update" && !existed) throw new Error("path_missing_for_update");
const backup = existed ? fs.readFileSync(target) : null;
const priorMode = existed ? fs.statSync(target).mode & 0o777 : 0o660;
let committed = false;
let commitOid = null;
try {
atomicWrite(target, document, priorMode);
runGit(["add", "--", relative], gitOptions);
const staged = runGit(["diff", "--cached", "--name-only", "-z"], gitOptions).split("\0").filter(Boolean);
if (staged.length !== 1 || staged[0] !== relative) throw new Error("staged_scope_mismatch");
runGit(["commit", "-m", `chore(aios): provenance ${payload.provenance_id} ${path.basename(relative)}`], gitOptions);
committed = true;
commitOid = runGit(["rev-parse", "HEAD"], gitOptions).trim();
runGit(buildPushArgs(pushRemote, branch), gitOptions);
appendAudit(auditLogPath, {
schema_version: "aios.writer-audit/v1", provenance_id: payload.provenance_id,
task_id: payload.task_id, run_id: payload.run_id, path: relative, operation,
branch, commit_oid: commitOid, outcome: "pushed", recorded_at: Date.now(),
});
return { path: relative, operation, branch, commit_oid: commitOid, outcome: "pushed" };
} catch (error) {
const code = error instanceof Error ? error.message : "writer_failed";
if (!committed) restoreBeforeCommit(target, existed, backup, priorMode, root, relative, gitOptions);
try {
appendAudit(auditLogPath, {
schema_version: "aios.writer-audit/v1", provenance_id: payload.provenance_id,
task_id: payload.task_id, run_id: payload.run_id, path: relative, operation,
branch, commit_oid: commitOid, outcome: committed ? "committed_push_blocked" : "rejected",
error_code: code, recorded_at: Date.now(),
});
} catch { /* the original failure remains authoritative */ }
throw new Error(code);
}
}
+116 -28
View File
@@ -2,38 +2,126 @@ apiVersion: aios.v2/v1
kind: AgentTask
metadata:
name: codex-production-supervisor-task
displayName: AIOS V2R1 Codex 架构监督、阻塞处理与发布审查任务
owner: AIOS-Control-Plane
language: zh-CN
spec:
executor: codex
role: architecture-supervisor
candidateBranch: integration/aios-v2r1-cto-production-candidate-20260720
taskBranch: task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720
initialState: queued
objective: >-
Supervise AIOS V2R1 production candidate deployment, resolve blocking issues,
review patches, and maintain architecture/document consistency.
监督 AIOS V2R1 Candidate 的预发布部署和缺陷修复,持续检查唯一控制面、
多租户、安全、文档、测试和回滚边界;分析并解决 OpenClaw 无法独立处理的阻塞,
最终输出统一的发布就绪结论。
inputs:
- docs/aios-v2/
- docs/aios-v2/adr/
- AIOS-V2R1-PRODUCTION-CANDIDATE.md
- tasks/openclaw-production-deploy-task.yaml
- deploy/aios-v2/
- active-runtime/app/
- tests/aios_v2_candidate/
- tools/aios_v2_docs_code_check.py
responsibilities:
- review OpenClaw deployment evidence
- review Hermes runtime issues
- analyze failed tests and blockers
- approve or reject patch direction
- verify docs/code/traceability consistency
- require ADR updates for architecture changes
- maintain final release readiness assessment
- 领取任务并写入 claim 证据
- 定期写入 heartbeat、progress 和监督结论
- 审阅 OpenClaw 的部署、健康检查、联调和回滚证据
- 分析失败测试、运行时异常和阻塞根因
- 审查所有 patch 的架构边界、租户隔离和 Secret 安全
- 解决需要跨模块修改的阻塞问题
- 对控制面、数据模型、状态机或 Runtime Contract 变化要求新增或更新 ADR
- 执行文档与代码匹配检查
- 验证需求、架构、契约、代码、测试、部署和验收追踪完整
- 拒绝缺少复现、测试、文档或回滚说明的修改
- 将所有阻塞、审查结论、修复 Commit 和剩余风险提交到系统
- 输出是否允许进入合并、Tag 和生产部署 Gate 的最终建议
prohibited:
- bypass GitOps process
- directly apply unreviewed production changes
- weaken tenant isolation or security gates
- approve missing evidence
required_checks:
- requirement traceability
- architecture boundary compliance
- test evidence
- secret safety
- rollback readiness
- document synchronization
evidence_required:
- claim
- heartbeat
- progress
- blocker
- review_result
- commit
- final_recommendation
- 绕过 GitOps 直接应用生产修改
- 直接 push main 或创建生产 Tag
- 在没有证据时批准发布
- 为快速通过测试而削弱租户隔离、安全门禁或文档门禁
- 忽略 OpenClaw 提交的 blocker
- 未经 AIOS Owner 批准接受重大剩余风险
requiredChecks:
architecture:
- AIOS 仍是唯一控制面
- Agent Router 只承担调度
- OpenClaw 与 Hermes 不拥有权威 Task Lifecycle
- Web 不直接访问 Runtime
tenancy:
- 所有新资源和接口包含 tenant_id
- 跨租户负向测试存在且通过
- Runtime Result tenant/task/trace 校验存在
security:
- Git 中无明文 Secret
- Secret 仅通过引用或受保护文件解析
- 身份代理防伪边界明确
quality:
- 文档—代码匹配检查通过
- 测试命令、exit code 和 pass/fail 有证据
- 重启、健康检查和回滚演练有证据
- 本地与 Gitea Candidate OID 一致
documentation:
- 用户需求已同步
- 目标架构已同步
- 模块契约已同步
- 追踪矩阵已同步
- ADR 已同步
- 候选报告已同步
blockerWorkflow:
states: [new, analyzing, fix_proposed, fix_in_progress, verification, resolved, accepted_risk]
requiredFields:
- blocker_id
- severity
- affected_requirement
- affected_module
- reproduction
- root_cause
- proposed_fix
- owner
- patch_branch
- verification
- documentation_updates
- exit_condition
evidenceRequired:
claim:
fields: [task_id, executor, branch, candidate_commit, claimed_at]
heartbeat:
intervalSeconds: 300
fields: [status, current_step, reviewed_commits, open_blockers, heartbeat_at]
review:
fields:
- patch_branch
- patch_commit
- architecture_result
- tenant_result
- security_result
- documentation_result
- test_result
- decision
result:
fields:
- final_status
- candidate_commit
- reviewed_commits
- resolved_blockers
- open_blockers
- accepted_risks
- documentation_sync_status
- test_evidence_status
- preproduction_status
- rollback_status
- local_gitea_oid_status
- final_recommendation
acceptanceCommands:
- python3 tools/aios_v2_docs_code_check.py --base-ref integration/aios-v2r1-cto-production-candidate-20260720
- python3 -m unittest discover -s tests/aios_v2_candidate -p 'test_*.py' -v
- bash deploy/aios-v2/acceptance/run-all.sh
completionCriteria:
- OpenClaw 部署与修复结果已完整审阅
- 所有严重和高风险 blocker 已解决或由 Owner 明确接受
- 文档、代码、测试和部署一致
- Candidate 精确 Commit 的测试和预发布证据完整
- 输出明确的进入下一 Gate、继续修复或停止发布建议
+84 -23
View File
@@ -2,34 +2,95 @@ apiVersion: aios.v2/v1
kind: AgentTask
metadata:
name: openclaw-production-deploy-task
displayName: AIOS V2R1 OpenClaw 预发布部署与小型缺陷修复任务
owner: AIOS-Control-Plane
language: zh-CN
spec:
executor: openclaw
role: deployment-operator
candidateBranch: integration/aios-v2r1-cto-production-candidate-20260720
taskBranch: task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720
initialState: queued
objective: >-
Deploy AIOS V2R1 candidate runtime, execute health checks, integrate OpenClaw
runtime boundary, and fix low-risk runtime/deployment defects.
在隔离预发布环境部署 AIOS V2R1 Candidate,执行健康检查、运行时联调、
重启与回滚演练;对部署、配置、脚本和 Runtime 适配中的低风险缺陷进行小范围修复。
inputs:
- docs/aios-v2/
- AIOS-V2R1-PRODUCTION-CANDIDATE.md
- deploy/aios-v2/
- active-runtime/app/aios_v2/
- active-runtime/app/aios_openclaw/
- active-runtime/app/aios_hermes/
- tests/aios_v2_candidate/
responsibilities:
- install systemd or Docker candidate deployment
- validate environment configuration
- execute healthcheck and smoke tests
- verify OpenClaw runtime connectivity
- fix small deployment/runtime defects through patch branches
- update ExecutionEvidence after every step
- 领取任务并写入 claim 证据
- 每个主要步骤写入 heartbeat 和 progress
- 安装或验证 systemd 部署
- 构建或验证 Docker/Compose 部署
- 配置非敏感环境变量和 Secret 引用
- 执行文档与代码匹配检查
- 执行候选测试和健康检查
- 验证 SQLite 状态目录和重启恢复
- 联调真实 OpenClaw Runtime 的 execute、status、cancel、timeout 和 health
- 验证 Hermes Worker claim、heartbeat、complete、fail 和 lease recovery
- 执行代码回滚和健康检查演练
- 对低风险部署或 Runtime 缺陷创建 patch 分支并修复
- 所有修改同步更新 docs/aios-v2/ 和追踪矩阵
- 所有阻塞提交到 Execution Evidence,交由 Codex 监督任务统一审视
prohibited:
- modify AIOS control-plane architecture
- bypass GitOps workflow
- commit secrets
- directly edit production without a recorded change
evidence_required:
- claim
- heartbeat
- progress
- blocker
- commit
- test_status
- result
- 修改 AIOS 唯一控制面原则
- 修改 Tenant、Policy、Approval 或 Task Lifecycle 权威边界
- 绕过 GitOps 直接修改生产
- 直接 push main 或创建生产 Tag
- 提交 Token、密码、私钥、API Key 或签名材料
- 在没有复现测试和文档更新时扩大修复范围
- 对架构问题自行做不可逆决策
lowRiskFixScope:
- systemd 单元和文件权限
- Dockerfile、Compose 和健康检查
- 环境变量、路径和启动参数
- 日志、错误消息和超时边界
- Runtime Adapter 的小型兼容性缺陷
- 测试、部署或文档中的明确错误
escalation:
- architecture change -> Codex supervisor
- security issue -> AIOS owner
- blocked deployment -> Codex supervisor
architectureChange: Codex 监督任务
controlPlaneChange: Codex 监督任务
tenantIsolationIssue: Codex 监督任务和 AIOS Owner
secretOrSecurityIssue: 立即停止并升级 AIOS Owner
productionApproval: AIOS Owner
evidenceRequired:
claim:
fields: [task_id, executor, branch, candidate_commit, claimed_at]
heartbeat:
intervalSeconds: 300
fields: [status, current_step, progress, last_commit, heartbeat_at]
blocker:
fields: [blocker_id, severity, description, reproduction, owner, exit_condition]
result:
fields:
- final_status
- branch
- commit
- changed_files
- test_commands
- test_exit_codes
- passed
- failed
- healthcheck
- deployment_status
- rollback_status
- documentation_sync_status
- remaining_risks
acceptanceCommands:
- python3 tools/aios_v2_docs_code_check.py --base-ref integration/aios-v2r1-cto-production-candidate-20260720
- python3 -m unittest discover -s tests/aios_v2_candidate -p 'test_*.py' -v
- bash deploy/aios-v2/acceptance/run-all.sh
- bash deploy/aios-v2/healthcheck/check.sh
completionCriteria:
- 文档—代码检查通过
- 候选自动测试通过
- systemd 或 Docker 预发布部署成功
- OpenClaw 与 Hermes 真实联调完成
- 重启恢复和回滚演练完成
- 所有缺陷、阻塞、提交和测试证据已回写
- Codex 输出可进入下一 Gate 的监督结论
+88 -5
View File
@@ -1,8 +1,12 @@
from __future__ import annotations
import sys
import os
import stat
import tempfile
import unittest
from dataclasses import replace
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -16,6 +20,7 @@ from aios_openclaw import (
InProcessRuntimeTransport,
OpenClawAdapter,
)
from aios_openclaw.contract import OpenClawCommand, sign_command, verify_command_signature
from aios_v2 import (
ControlPlaneService,
ExecutionEvidence,
@@ -26,9 +31,11 @@ from aios_v2 import (
TenantIsolationError,
)
from aios_v2.persistence import DurableControlPlaneService, SQLiteSnapshotRepository
from aios_v2.gateway import GatewayApplication
from aios_v2.app import build_application
SIGNING_MATERIAL = b"candidate-test-signing-material-0001"
SIGNING_MATERIAL = bytes(range(32))
def context(
@@ -64,6 +71,73 @@ def bootstrap(service: ControlPlaneService, tenant_id: str = "tenant-a"):
class CandidateFlowTests(unittest.TestCase):
def test_rollback_entrypoints_are_executable_in_release_archive(self) -> None:
for relative in (
"deploy/aios-v2/rollback/rollback.sh",
"deploy/aios-v2/healthcheck/check.sh",
):
mode = (REPO_ROOT / relative).stat().st_mode
self.assertTrue(mode & stat.S_IXUSR, relative)
def test_build_application_propagates_openclaw_signing_key_id(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
material_path = Path(temp_dir) / "signing-material.bin"
material_path.write_bytes(SIGNING_MATERIAL)
values = {
"AIOS_V2_STATE_DIR": temp_dir,
"AIOS_OPENCLAW_URL": "http://127.0.0.1:18789",
"AIOS_OPENCLAW_SIGNING_MATERIAL_FILE": str(material_path),
"AIOS_OPENCLAW_SIGNING_KEY_ID": "openclaw-v2r1-candidate-ed25519",
"AIOS_OPENCLAW_CREDENTIAL_REF": "secretref:test",
}
previous = {key: os.environ.get(key) for key in values}
try:
os.environ.update(values)
application = build_application()
finally:
for key, value in previous.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
self.assertEqual(
application.orchestrator.openclaw_key_id,
"openclaw-v2r1-candidate-ed25519",
)
def test_http_identity_headers_are_case_insensitive(self) -> None:
request_context = GatewayApplication.context_from_headers(
{
"X-Aios-Authenticated": "true",
"x-aios-tenant-id": "tenant-a",
"X-AIOS-PRINCIPAL-ID": "operator-a",
"X-Aios-Roles": "operator,auditor",
"x-aios-trace-id": "trace-http-header-case",
}
)
self.assertEqual(request_context.tenant_id, "tenant-a")
self.assertEqual(request_context.principal_id, "operator-a")
self.assertEqual(request_context.trace_id, "trace-http-header-case")
self.assertEqual(request_context.roles, frozenset({"operator", "auditor"}))
def test_ed25519_command_signature_accepts_valid_and_rejects_tampering(self) -> None:
command = OpenClawCommand(
command_id="cmd-signature-test",
tenant_id="tenant-a",
task_id="task-signature-test",
trace_id="trace-signature-test",
action="echo",
payload={"message": "hello"},
idempotency_key="signature-test-1",
key_id="openclaw-v2r1-candidate-ed25519",
)
signed = sign_command(command, SIGNING_MATERIAL)
self.assertTrue(verify_command_signature(signed, SIGNING_MATERIAL))
self.assertFalse(
verify_command_signature(replace(signed, payload={"message": "tampered"}), SIGNING_MATERIAL)
)
self.assertFalse(verify_command_signature(signed, bytes(reversed(SIGNING_MATERIAL))))
def test_openclaw_full_flow_reconciles_completed_task(self) -> None:
service = ControlPlaneService()
_, operator = bootstrap(service)
@@ -88,12 +162,16 @@ class CandidateFlowTests(unittest.TestCase):
expected_revision=task.revision,
)
transport = InProcessRuntimeTransport(
lambda command: {
captured_commands = []
def execute(command):
captured_commands.append(command)
return {
"state": CommandState.COMPLETED.value,
"output": {"echo": command.payload["message"]},
}
)
transport = InProcessRuntimeTransport(execute)
resolver = lambda tenant_id, key_id: SIGNING_MATERIAL
adapter = OpenClawAdapter(transport, resolver)
orchestrator = ExecutionOrchestrator(
@@ -102,13 +180,18 @@ class CandidateFlowTests(unittest.TestCase):
openclaw_signing_material_resolver=resolver,
)
dispatch = orchestrator.dispatch(operator, task_id=approved.task_id)
dispatch = orchestrator.dispatch(
operator, task_id=approved.task_id, timeout_seconds=600
)
completed = service.get_task(operator, task.task_id)
self.assertEqual(dispatch.runtime, "openclaw")
self.assertEqual(dispatch.state, CommandState.COMPLETED.value)
self.assertEqual(completed.state, TaskState.COMPLETED)
self.assertEqual(completed.result, {"echo": "hello"})
created = datetime.fromisoformat(captured_commands[0].created_at.replace("Z", "+00:00"))
expires = datetime.fromisoformat(captured_commands[0].expires_at.replace("Z", "+00:00"))
self.assertLessEqual((expires - created).total_seconds(), 300)
def test_hermes_full_flow_reconciles_worker_result(self) -> None:
service = ControlPlaneService()
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""AIOS V2 文档—代码匹配门禁。
仅使用 Python 标准库,可由本地、Gitea Actions、Codex 监督任务或发布验收脚本调用。
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
@dataclass(frozen=True)
class CheckResult:
level: str
message: str
REQUIRED_DOCUMENTS = (
"docs/aios-v2/README.md",
"docs/aios-v2/01-USER-REQUIREMENTS.md",
"docs/aios-v2/02-TARGET-ARCHITECTURE.md",
"docs/aios-v2/03-MIGRATION-AND-DELIVERY.md",
"docs/aios-v2/04-MODULE-CONTRACTS.md",
"docs/aios-v2/05-TRACEABILITY-MATRIX.md",
"docs/aios-v2/06-DOCUMENT-GOVERNANCE.md",
"docs/aios-v2/adr/README.md",
"docs/aios-v2/adr/ADR-001-single-control-plane.md",
"docs/aios-v2/adr/ADR-002-openclaw-hermes-decoupling.md",
"docs/aios-v2/adr/ADR-003-gitops-only-change-flow.md",
"docs/aios-v2/adr/ADR-004-multi-tenant-isolation.md",
"docs/aios-v2/adr/ADR-005-document-code-sync-gate.md",
"docs/aios-v2/adr/ADR-006-candidate-persistence-strategy.md",
)
REQUIRED_CODE_PATHS = (
"active-runtime/app/aios_v2/control_plane.py",
"active-runtime/app/aios_v2/persistence.py",
"active-runtime/app/aios_v2/gateway.py",
"active-runtime/app/aios_v2/app.py",
"active-runtime/app/aios_v2/orchestrator.py",
"active-runtime/app/aios_v2/execution_tracking.py",
"active-runtime/app/aios_v2/gitops_status.py",
"active-runtime/app/aios_v2/console.py",
"active-runtime/app/aios_openclaw/contract.py",
"active-runtime/app/aios_openclaw/adapter.py",
"active-runtime/app/aios_openclaw/transport.py",
"active-runtime/app/aios_openclaw/health.py",
"active-runtime/app/aios_openclaw/callback.py",
"active-runtime/app/aios_hermes/contract.py",
"active-runtime/app/aios_hermes/adapter.py",
"active-runtime/app/aios_hermes/worker.py",
"tests/aios_v2_candidate/test_candidate_flow.py",
"deploy/aios-v2/systemd/aios-v2.service",
"deploy/aios-v2/docker/Dockerfile",
"deploy/aios-v2/docker/compose.yaml",
"deploy/aios-v2/healthcheck/check.sh",
"deploy/aios-v2/rollback/rollback.sh",
"tasks/openclaw-production-deploy-task.yaml",
"tasks/codex-production-supervisor-task.yaml",
)
REQUIRED_APIS = (
"/api/v2/health",
"/api/v2/tenants",
"/api/v2/agents",
"/api/v2/tasks",
"/api/v2/audit",
"/api/v2/runtime",
"/api/v2/executions",
"/api/v2/gitops",
"/api/v2/hermes/results/reconcile",
)
REQUIRED_MODELS = (
"RequestContext",
"TenantRecord",
"AgentRecord",
"TaskRecord",
"TaskState",
"AuditEvent",
"OpenClawCommand",
"OpenClawResult",
"RuntimeHealth",
"HermesJob",
"HermesResult",
"HermesHealth",
"ExecutionEvidence",
)
CODE_CHANGE_PREFIXES = (
"active-runtime/app/aios_v2/",
"active-runtime/app/aios_openclaw/",
"active-runtime/app/aios_hermes/",
"deploy/aios-v2/",
"tests/aios_v2_candidate/",
"tasks/",
)
CONFLICT_MARKERS = ("<<<<<<<", "=======", ">>>>>>>")
def run_git(repo: Path, *args: str) -> tuple[int, str]:
process = subprocess.run(
["git", "-C", str(repo), *args],
text=True,
capture_output=True,
timeout=20,
)
return process.returncode, process.stdout.strip()
def chinese_ratio(text: str) -> float:
chinese = len(re.findall(r"[\u3400-\u9fff]", text))
latin = len(re.findall(r"[A-Za-z]", text))
denominator = chinese + latin
return chinese / denominator if denominator else 0.0
def all_text(paths: Iterable[Path]) -> str:
return "\n".join(path.read_text(encoding="utf-8") for path in paths)
def check_required_files(repo: Path, results: list[CheckResult]) -> list[Path]:
document_paths: list[Path] = []
for relative in REQUIRED_DOCUMENTS:
path = repo / relative
if not path.is_file():
results.append(CheckResult("错误", f"缺少必需文档:{relative}"))
continue
size = path.stat().st_size
if size < 500:
results.append(CheckResult("错误", f"文档正文过短:{relative}({size} 字节)"))
else:
document_paths.append(path)
for relative in REQUIRED_CODE_PATHS:
if not (repo / relative).is_file():
results.append(CheckResult("错误", f"文档引用的实现路径不存在:{relative}"))
return document_paths
def check_document_quality(paths: list[Path], results: list[CheckResult]) -> None:
for path in paths:
text = path.read_text(encoding="utf-8")
relative = path.as_posix()
if any(marker in text for marker in CONFLICT_MARKERS):
results.append(CheckResult("错误", f"文档存在未解决 Git 冲突标记:{relative}"))
ratio = chinese_ratio(text)
if ratio < 0.18:
results.append(
CheckResult(
"错误",
f"文档中文正文比例过低:{relative}({ratio:.1%})",
)
)
headings = re.findall(r"^#{1,6}\s+.+$", text, flags=re.MULTILINE)
if len(headings) < 3:
results.append(CheckResult("错误", f"文档章节不完整:{relative}"))
if re.search(r"(?mi)^\s*(TODO|TBD)\s*$", text):
results.append(CheckResult("错误", f"文档包含未完成占位符:{relative}"))
def check_requirements(repo: Path, results: list[CheckResult]) -> None:
requirements = (repo / "docs/aios-v2/01-USER-REQUIREMENTS.md").read_text(
encoding="utf-8"
)
matrix = (repo / "docs/aios-v2/05-TRACEABILITY-MATRIX.md").read_text(
encoding="utf-8"
)
for number in range(1, 16):
requirement = f"UR-{number:03d}"
if requirement not in requirements:
results.append(CheckResult("错误", f"需求文档缺少:{requirement}"))
if requirement not in matrix:
results.append(CheckResult("错误", f"追踪矩阵缺少:{requirement}"))
def check_api_and_models(repo: Path, docs_text: str, results: list[CheckResult]) -> None:
code_paths = [repo / path for path in REQUIRED_CODE_PATHS if path.endswith(".py")]
code_text = all_text(path for path in code_paths if path.is_file())
for api in REQUIRED_APIS:
if api not in docs_text:
results.append(CheckResult("错误", f"正式文档未覆盖核心 API:{api}"))
if api not in code_text:
results.append(CheckResult("错误", f"代码中未找到核心 API:{api}"))
for model in REQUIRED_MODELS:
if model not in docs_text:
results.append(CheckResult("错误", f"正式文档未覆盖核心模型:{model}"))
if model not in code_text:
results.append(CheckResult("错误", f"代码中未找到核心模型:{model}"))
def check_adr_links(repo: Path, results: list[CheckResult]) -> None:
index = (repo / "docs/aios-v2/adr/README.md").read_text(encoding="utf-8")
linked = re.findall(r"\((ADR-[^)]+\.md)\)", index)
if len(set(linked)) < 6:
results.append(CheckResult("错误", "ADR 索引未覆盖六项必需决策"))
for filename in linked:
if not (repo / "docs/aios-v2/adr" / filename).is_file():
results.append(CheckResult("错误", f"ADR 索引引用不存在:{filename}"))
def check_changed_docs(
repo: Path,
base_ref: str | None,
results: list[CheckResult],
) -> None:
if not base_ref:
results.append(CheckResult("警告", "未提供基准分支,跳过变更级文档同步检查"))
return
code, output = run_git(repo, "diff", "--name-only", f"{base_ref}...HEAD")
if code != 0:
results.append(CheckResult("警告", f"无法读取与 {base_ref} 的变更,跳过变更级检查"))
return
changed = [line.strip() for line in output.splitlines() if line.strip()]
code_changed = any(path.startswith(CODE_CHANGE_PREFIXES) for path in changed)
docs_changed = any(path.startswith("docs/aios-v2/") for path in changed)
if code_changed and not docs_changed:
results.append(
CheckResult(
"错误",
"检测到架构、功能、测试、任务或部署变化,但本交付范围没有 docs/aios-v2/ 变化",
)
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="AIOS V2 文档—代码匹配检查")
parser.add_argument("--repo", default=".", help="仓库根目录")
parser.add_argument(
"--base-ref",
default=None,
help="用于检查代码变化是否同步文档的基准引用",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
repo = Path(args.repo).resolve()
if not (repo / ".git").exists() and run_git(repo, "rev-parse", "--is-inside-work-tree")[0] != 0:
print(f"错误:不是 Git 仓库:{repo}", file=sys.stderr)
return 2
results: list[CheckResult] = []
document_paths = check_required_files(repo, results)
check_document_quality(document_paths, results)
check_requirements(repo, results)
docs_text = all_text(document_paths)
check_api_and_models(repo, docs_text, results)
check_adr_links(repo, results)
check_changed_docs(repo, args.base_ref, results)
errors = [item for item in results if item.level == "错误"]
warnings = [item for item in results if item.level == "警告"]
for item in results:
stream = sys.stderr if item.level == "错误" else sys.stdout
print(f"{item.level}:{item.message}", file=stream)
print(
f"文档—代码匹配检查完成:错误 {len(errors)},警告 {len(warnings)},正式文档 {len(document_paths)}。"
)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())