Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b0db21a85 | ||
|
|
be4726178a | ||
|
|
0834b272f0 | ||
|
|
e28245f411 | ||
|
|
8a9d033e9c | ||
|
|
e567699283 | ||
|
|
c935aac3b8 | ||
|
|
75f0148878 | ||
|
|
91afbfaf76 |
@@ -1,412 +0,0 @@
|
||||
# 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;
|
||||
- 正式生产部署;
|
||||
- 生产流量切换;
|
||||
- 接受高风险剩余问题。
|
||||
+24
-25
@@ -1,58 +1,57 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-blockers.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"updated_at": "2026-07-20T10:17:49Z",
|
||||
"evidence_writer": "windows-chatgpt-owner",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"updated_at": "2026-07-21T04:30:00Z",
|
||||
"blockers": [
|
||||
{
|
||||
"blocker_id": "BLK-OPENCLAW-PLAINTEXT-SECRETS",
|
||||
"severity": "high",
|
||||
"status": "resolved",
|
||||
"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 调用通过。",
|
||||
"root_cause": "OpenClaw 三处 managed plaintext。",
|
||||
"fix": "迁移为单一 file Secret Provider 并轮换 Gateway Token。",
|
||||
"verification": "最终 audit 四类计数均为 0,OneAPI 实际调用通过。",
|
||||
"commit": "bf01a13f3aaf42a386232ed2f5409feb0f79fcf6",
|
||||
"resolved_at": "2026-07-20T10:17:49Z"
|
||||
"resolved_at": "2026-07-20T10:18:36Z"
|
||||
},
|
||||
{
|
||||
"blocker_id": "BLK-AIOS-V2-SIGNING-MATERIAL-MISSING",
|
||||
"severity": "high",
|
||||
"status": "resolved",
|
||||
"root_cause": "Key ID 标称 Ed25519,但代码实际使用 HMAC 且材料文件缺失。",
|
||||
"fix": "签名合同改为真实 Ed25519,并由 Owner 在受保护边界生成运行材料。",
|
||||
"verification": "aios:aios 0600;resolver sign/verify 与篡改拒绝通过。",
|
||||
"root_cause": "Ed25519 Key ID 与 HMAC 实现不一致且材料缺失。",
|
||||
"fix": "真实 Ed25519 合同及受保护材料。",
|
||||
"verification": "0600、sign/verify、篡改拒绝通过。",
|
||||
"commit": "8a36f7832bc105e4b9d32ffbe018abed2ff1b1ab",
|
||||
"resolved_at": "2026-07-20T10:17:49Z"
|
||||
"resolved_at": "2026-07-20T10:18:36Z"
|
||||
},
|
||||
{
|
||||
"blocker_id": "BLK-CANDIDATE-NOT-MERGED",
|
||||
"severity": "high",
|
||||
"status": "resolved",
|
||||
"root_cause": "OpenClaw 任务未包含最新 Candidate。",
|
||||
"fix": "保护 Evidence 后以 --no-ff 合并 Candidate ba7669c。",
|
||||
"verification": "Candidate 为任务分支祖先,普通 Push 后 local_oid 等于 gitea_oid。",
|
||||
"root_cause": "OpenClaw 任务未包含 Candidate。",
|
||||
"fix": "--no-ff 合并最新 Candidate。",
|
||||
"verification": "Candidate 为 OpenClaw HEAD 祖先且 OID 同步。",
|
||||
"commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
|
||||
"resolved_at": "2026-07-20T10:17:49Z"
|
||||
"resolved_at": "2026-07-20T10:18:36Z"
|
||||
},
|
||||
{
|
||||
"blocker_id": "BLK-OPENCLAW-RUN-PROVENANCE-MISSING",
|
||||
"severity": "high",
|
||||
"status": "blocked",
|
||||
"root_cause": "现有插件只有进程内 runId/sender trust Map,没有 Owner-signed task provenance schema、持久 Store 或受限 Evidence 写工具。",
|
||||
"fix": "禁止用 run-key fallback 绕过;等待经审查的 issuer/store/writer 或外部同等受信系统。",
|
||||
"verification": "当前无法完成 7 路径写测试与越权拒绝审计。",
|
||||
"commit": "bf01a13f3aaf42a386232ed2f5409feb0f79fcf6",
|
||||
"resolved_at": null
|
||||
"status": "resolved",
|
||||
"root_cause": "旧插件只有进程内 runId/sender Map,缺少 Owner-signed Schema、持久 Store 和限定 Git Writer。",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"blocker_id": "BLK-CONTROLLER-LEGACY-GITEA-CHECK",
|
||||
"severity": "high",
|
||||
"status": "resolved",
|
||||
"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。",
|
||||
"root_cause": "硬编码旧仓库。",
|
||||
"fix": "配置化 V2R1 OID 健康检查并使用只读 Credential。",
|
||||
"verification": "12 项测试、detached clean、fully_synced。",
|
||||
"commit": "6188da26453505dacde9d6d298191437164c8d27",
|
||||
"resolved_at": "2026-07-20T10:17:49Z"
|
||||
"resolved_at": "2026-07-20T10:18:36Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+8
-11
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-claim.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"executor": "openclaw",
|
||||
"task_branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"executor": "codex",
|
||||
"executor_instance": "windows-chatgpt-owner-supervisor",
|
||||
"task_branch": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"candidate_branch": "integration/aios-v2r1-cto-production-candidate-20260720",
|
||||
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
|
||||
"claimed_at": "2026-07-20T08:24:30Z",
|
||||
"executor_instance": "openclaw-main-cn-network-mkt",
|
||||
"owner_observation_at": "2026-07-20T10:17:49Z",
|
||||
"owner_observation": "四项高风险阻塞已清除;provenance 仍缺可信签发/强制执行链。",
|
||||
"evidence_writer": "windows-chatgpt-owner",
|
||||
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
|
||||
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
|
||||
"candidate_updated_at": "2026-07-20T09:39:16Z"
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,53 +1,18 @@
|
||||
{
|
||||
"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-20T10:17:49Z",
|
||||
"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-20T10:17:49Z",
|
||||
"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",
|
||||
"schema": "aios.agent.credential-status.v1",
|
||||
"task_id": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"last_updated": "2026-07-21T04:30:00Z",
|
||||
"plaintext_findings_before": 3,
|
||||
"plaintext_findings_after": 0,
|
||||
"credentials_rotated": true,
|
||||
"gateway_token_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"
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-heartbeat.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"executor": "openclaw",
|
||||
"state": "blocked",
|
||||
"heartbeat_at": "2026-07-20T10:17:49Z",
|
||||
"heartbeat_source": "windows-chatgpt-owner",
|
||||
"valid_for_running": false,
|
||||
"current_step": "BLOCKED_PROVENANCE_ISSUER_STORE_WRITER_REQUIRED",
|
||||
"evidence_writer": "windows-chatgpt-owner"
|
||||
"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"
|
||||
}
|
||||
|
||||
+59
-43
@@ -1,23 +1,55 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-progress.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"state": "blocked",
|
||||
"status": "PARTIAL",
|
||||
"updated_at": "2026-07-20T10:17:49Z",
|
||||
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
|
||||
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
|
||||
"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",
|
||||
"gitea_sync": {
|
||||
"candidate_match": true,
|
||||
"openclaw_task_match_before_owner_evidence": true,
|
||||
"codex_task_match": true
|
||||
"openclaw_task_match": true,
|
||||
"codex_task_match_before_this_evidence": true
|
||||
},
|
||||
"credential_rotation_status": "passed",
|
||||
"executor_model_path": "passed",
|
||||
"tests": "not_run",
|
||||
"deployment": "not_run",
|
||||
"rollback": "not_run",
|
||||
"current_step": "四项 blocker resolved;provenance blocked",
|
||||
"evidence_writer": "windows-chatgpt-owner",
|
||||
"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",
|
||||
"aios_v2_env_status": {
|
||||
"exists": true,
|
||||
"owner": "root",
|
||||
@@ -25,33 +57,17 @@
|
||||
"mode": "0640",
|
||||
"four_fields_nonempty": true,
|
||||
"credential_ref_target_exists": true,
|
||||
"signing_material_target_exists": false
|
||||
"signing_material_target_exists": true
|
||||
},
|
||||
"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": false,
|
||||
"resolved_blockers": 4,
|
||||
"open_blockers": 1,
|
||||
"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
|
||||
}
|
||||
"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"
|
||||
}
|
||||
|
||||
+18
-30
@@ -1,39 +1,27 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-result.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"final_status": "BLOCKED",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"final_status": "PARTIAL",
|
||||
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
|
||||
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
|
||||
"reviewed_commit": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
|
||||
"gitea_sync": {
|
||||
"candidate_match": true,
|
||||
"openclaw_task_match_before_owner_evidence": true,
|
||||
"codex_task_match": true
|
||||
"openclaw_task_match": true,
|
||||
"codex_task_match_before_this_evidence": true
|
||||
},
|
||||
"secret_reference_status": "resolved",
|
||||
"executor_status": "blocked",
|
||||
"test_status": "targeted controller 12/12; Ed25519 candidate flow 6/6; Gate A/B 未执行",
|
||||
"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": "not_run",
|
||||
"rollback_status": "Previous 2a2b702 与 Current b158041 双向演练通过且业务状态保留",
|
||||
"documentation_sync_status": "updated",
|
||||
"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": [
|
||||
"BLK-OPENCLAW-RUN-PROVENANCE-MISSING"
|
||||
],
|
||||
"remaining_risks": [
|
||||
"缺少 Owner-signed provenance issuer/store/writer 与越权拒绝证据"
|
||||
],
|
||||
"completed_at": "2026-07-20T10:17:49Z",
|
||||
"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"
|
||||
"open_blockers": [],
|
||||
"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"
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-status.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"executor": "openclaw",
|
||||
"state": "blocked",
|
||||
"status": "BLOCKED_PROVENANCE_REQUIRED",
|
||||
"claimed_at": "2026-07-20T08:24:30Z",
|
||||
"updated_at": "2026-07-20T10:17:49Z",
|
||||
"last_heartbeat": "2026-07-20T10:17:49Z",
|
||||
"heartbeat_valid_for_running": false,
|
||||
"current_step": "等待受信 provenance issuer/store/writer",
|
||||
"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",
|
||||
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
|
||||
"reviewed_commit": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
|
||||
"release_recommendation": "STOP_PREPRODUCTION",
|
||||
"evidence_writer": "windows-chatgpt-owner",
|
||||
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
|
||||
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
|
||||
"candidate_updated_at": "2026-07-20T09:39:16Z"
|
||||
"openclaw_evidence_commit": "0be3ba09e42b4f6db56461e3ca3a3fcc0a487f31",
|
||||
"status": "reviewing"
|
||||
,"formal_online_acceptance": "partial"
|
||||
,"remaining_gap": "Hermes full online worker lifecycle"
|
||||
}
|
||||
|
||||
+15
-18
@@ -1,34 +1,31 @@
|
||||
{
|
||||
"schema": "aios.v2.agent-task.v1",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"display_name": "AIOS V2R1 OpenClaw 预发布部署与小型缺陷修复任务",
|
||||
"task_id": "AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"display_name": "AIOS V2R1 Codex 架构监督、阻塞处理与发布审查任务",
|
||||
"language": "zh-CN",
|
||||
"executor": "openclaw",
|
||||
"role": "deployment-operator",
|
||||
"executor": "codex",
|
||||
"role": "architecture-supervisor",
|
||||
"state": "queued",
|
||||
"candidate_branch": "integration/aios-v2r1-cto-production-candidate-20260720",
|
||||
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
|
||||
"canonical_task": "tasks/openclaw-production-deploy-task.yaml",
|
||||
"supervisor_task": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
|
||||
"canonical_task": "tasks/codex-production-supervisor-task.yaml",
|
||||
"supervises": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"claim_required": true,
|
||||
"heartbeat_interval_seconds": 300,
|
||||
"evidence_directory": ".agent/evidence",
|
||||
"instructions": [
|
||||
"领取前读取候选分支完整中文文档和生产候选报告",
|
||||
"领取前读取候选分支完整中文文档、ADR、追踪矩阵和候选报告",
|
||||
"领取后写入 .agent/claim.json 并把状态改为 claimed",
|
||||
"每五分钟或每个主要步骤写入 heartbeat 和 progress",
|
||||
"所有部署修改、小型缺陷、阻塞、测试和结果必须提交到 Git",
|
||||
"架构、控制面、多租户或安全问题必须升级 Codex",
|
||||
"禁止提交明文密钥、直接修改 main 或绕过 GitOps"
|
||||
"持续审阅 OpenClaw 的 claim、heartbeat、progress、blocker、commit 和 result",
|
||||
"分析失败测试、架构偏移、多租户、安全和文档同步阻塞",
|
||||
"需要修改时创建 Patch 分支、增加复现测试并同步文档",
|
||||
"所有阻塞和审查结论必须提交到系统统一审视",
|
||||
"禁止缺少证据时批准发布或削弱安全门禁"
|
||||
],
|
||||
"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/healthcheck/check.sh"
|
||||
"bash deploy/aios-v2/acceptance/run-all.sh"
|
||||
],
|
||||
"created_at": "2026-07-20T00:00:00Z",
|
||||
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
|
||||
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
|
||||
"candidate_updated_at": "2026-07-20T09:39:16Z"
|
||||
"created_at": "2026-07-20T00:00:00Z"
|
||||
}
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
# 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 阻塞清除快照
|
||||
|
||||
Candidate 合并、OpenClaw 明文 Secret、Ed25519 Signing Material 和 Controller legacy Gitea health 四项已形成运行证据。`BLK-OPENCLAW-RUN-PROVENANCE-MISSING` 尚未解决:当前运行插件不能表达或强制 `task_id/executor/claim_oid/candidate_oid/task_branch/allowed_paths/allowed_operations/issued_by/issued_at/expires_at` 的 Owner-signed 授权合同。
|
||||
|
||||
在 provenance 专用签发、验证、受限写入、普通 commit/push 和越权拒绝测试完成前,保持 `STOP_PREPRODUCTION`,不得执行 Gate A、Gate B、离线全量验收或在线预发布。
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Versioned AIOS to OpenClaw execution contracts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import dataclass, field, replace
|
||||
@@ -9,10 +10,6 @@ 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
|
||||
|
||||
@@ -182,26 +179,11 @@ 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 = _ed25519_private_key(key_material)
|
||||
signature = base64.urlsafe_b64encode(key.sign(canonical_command_bytes(command))).decode("ascii").rstrip("=")
|
||||
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()
|
||||
return replace(command, signature=signature)
|
||||
|
||||
|
||||
@@ -214,10 +196,11 @@ def verify_command_signature(
|
||||
) -> bool:
|
||||
if not command.signature:
|
||||
return False
|
||||
try:
|
||||
key = _ed25519_private_key(key_material)
|
||||
key.public_key().verify(_decode_signature(command.signature), canonical_command_bytes(command))
|
||||
except (InvalidSignature, ValueError, TypeError, UnicodeError):
|
||||
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):
|
||||
return False
|
||||
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||||
created = _parse_timestamp(command.created_at)
|
||||
|
||||
@@ -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()
|
||||
value = self.path.read_bytes().strip()
|
||||
if len(value) < 32:
|
||||
raise RuntimeError("signing material is too short")
|
||||
return value
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/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
|
||||
@@ -111,7 +111,3 @@
|
||||
## 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 探针共同验证。
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
# 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 监督结论不存在未解决阻塞。
|
||||
@@ -1,235 +0,0 @@
|
||||
# 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
|
||||
|
||||
- 严重级别:高
|
||||
- 状态:开放
|
||||
- 现象:OpenClaw 写工具被 `run_provenance_missing` 策略拒绝。
|
||||
- 风险:无法证明写操作来自已领取、已授权、可审计的执行会话。
|
||||
- 外部受信输入:允许七个 `.agent` JSON 写入的 OpenClaw Provenance 授权。
|
||||
- 解除条件:
|
||||
1. 创建带任务 ID、执行器实例、Claim OID、Candidate OID 和授权主体的 Run Provenance;
|
||||
2. Provenance 与七个 `.agent` JSON 写入范围绑定;
|
||||
3. 权限仅覆盖当前任务分支和 Evidence 路径;
|
||||
4. 由 Owner 或批准策略签发;
|
||||
5. 写入、Commit、Push 和审计链验证通过。
|
||||
|
||||
### 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:文档与设计 | 未运行 | Candidate 未合并到任务,阻塞未解除 |
|
||||
| Gate B:代码与测试 | 未运行 | 不能基于不完整任务分支测试 |
|
||||
| Gate C:预发布 | 禁止 | Secret、签名、Provenance、Controller Health 未通过 |
|
||||
| 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. 外部受信输入
|
||||
|
||||
目前仅有两项确实需要外部受信输入:
|
||||
|
||||
1. **批准的签名材料**
|
||||
- 不得通过聊天、Git 或普通日志传递;
|
||||
- 必须通过批准的 Secret/Key 交付链安装。
|
||||
|
||||
2. **OpenClaw Run Provenance 授权**
|
||||
- 仅允许当前任务所需的七个 `.agent` JSON 写入;
|
||||
- 必须绑定任务、执行器实例、Claim、Candidate 和分支;
|
||||
- 不得授予通用生产写权限。
|
||||
|
||||
其余阻塞均可由 OpenClaw、Codex、Windows ChatGPT Owner 和 GitOps 运维流程处理。
|
||||
|
||||
## 9. 监督结论
|
||||
|
||||
当前状态保持:
|
||||
|
||||
```text
|
||||
BLOCKED
|
||||
STOP_PREPRODUCTION
|
||||
```
|
||||
|
||||
虽然三分支已经同步 Gitea、凭据已经最小权限轮换、执行服务已经恢复,但当前尚无足够证据允许执行 Gate A–D 或进入预发布。
|
||||
|
||||
任何后续状态更新必须同步:
|
||||
|
||||
- OpenClaw `.agent/status.json`、`blockers.json`、`progress.json`、`result.json`;
|
||||
- Codex 对应监督 Evidence;
|
||||
- 本文档;
|
||||
- `05-TRACEABILITY-MATRIX.md`;
|
||||
- `AIOS-V2R1-PRODUCTION-CANDIDATE.md`;
|
||||
- 如涉及架构或发布语义,更新 ADR。
|
||||
|
||||
## 9. 2026-07-20 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:仍为高风险阻塞。现有插件只有进程内 `runId -> sender trust` Map,没有 Owner-signed task provenance schema、持久 Store 或能够强制 7 路径/5 操作边界的专用写工具。禁止以 run-key fallback 冒充签发。
|
||||
|
||||
因此当前结论仍为 `STOP_PREPRODUCTION`,Gate A 不得启动。解除最后阻塞需要经审查的 provenance issuer/store/writer 实现,或外部受信系统提供同等合同。
|
||||
+61
-92
@@ -1,82 +1,76 @@
|
||||
# AIOS V2 架构与交付文档索引
|
||||
# AIOS V2 Architecture and Delivery Index
|
||||
|
||||
- 版本线:`V2R1C0-candidate`
|
||||
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
|
||||
- 架构负责人:AIOS V2 CTO
|
||||
- 当前状态:候选代码闭环已完成,运行时、预发布和生产验证尚待执行
|
||||
- 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
|
||||
|
||||
## 1. 文档目的
|
||||
## 1. Purpose
|
||||
|
||||
本目录是 AIOS V2 用户需求、目标架构、模块边界、迁移策略、发布门禁和架构决策的唯一事实源。
|
||||
This directory is the single source of truth for AIOS V2 requirements, target architecture, module boundaries, migration strategy, release gates and architectural decisions.
|
||||
|
||||
所有后续架构、功能、接口、数据模型、安全策略、部署方式和验收流程变化,都必须在同一交付流程中同步更新本目录。
|
||||
The design is based on the following non-negotiable principles:
|
||||
|
||||
AIOS V2 遵循以下不可协商原则:
|
||||
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.
|
||||
|
||||
1. 不推倒重写现有平台。
|
||||
2. 优先复用 OpenClaw、Hermes 和 Agent Router 能力。
|
||||
3. OpenClaw 与 Hermes 从控制面职责中逐步解耦。
|
||||
4. AIOS 是唯一控制面。
|
||||
5. 所有面向用户和运维的能力必须通过 Web 或 API 访问。
|
||||
6. 所有模块必须强制多租户隔离。
|
||||
7. 所有修改必须通过 Git 分支、提交、测试、审查和受控提升完成。
|
||||
8. 代码变化必须同步更新需求、架构、契约、追踪矩阵或 ADR。
|
||||
## 2. Document map
|
||||
|
||||
## 2. 文档地图
|
||||
|
||||
| 文档 | 用途 |
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [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) | 架构决策记录索引与治理规则 |
|
||||
| [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 |
|
||||
|
||||
## 3. 当前实现映射
|
||||
## 3. Current implementation map
|
||||
|
||||
| 架构能力 | 候选实现 |
|
||||
| Architecture capability | Candidate implementation |
|
||||
|---|---|
|
||||
| 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` |
|
||||
| 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` |
|
||||
| Web Console | `active-runtime/app/aios_v2/console.py` |
|
||||
| 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` |
|
||||
| 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/` |
|
||||
|
||||
## 4. 状态术语
|
||||
## 4. Status vocabulary
|
||||
|
||||
项目必须严格区分“代码存在”和“可生产发布”:
|
||||
The project separates code availability from release readiness:
|
||||
|
||||
- **已设计**:文档中存在明确边界和验收标准。
|
||||
- **已实现**:代码已经提交到候选分支。
|
||||
- **已定义测试**:Git 中存在自动化测试代码。
|
||||
- **已验证测试**:已对精确 commit 执行测试并保存证据。
|
||||
- **已验证预发布**:已在隔离环境完成部署、健康检查、重启和回滚演练。
|
||||
- **已生产发布**:已取得 Owner 批准并完成合并、Tag 和生产部署。
|
||||
- **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.
|
||||
|
||||
分支、文件、测试脚本或部署脚本存在,并不自动代表能力已通过生产验收。
|
||||
A branch or file existing does not, by itself, mean a capability is production-ready.
|
||||
|
||||
## 5. 设计摘要
|
||||
## 5. Design summary
|
||||
|
||||
```text
|
||||
浏览器 / Web Console / Public API
|
||||
Browser / Web Console / Public API
|
||||
|
|
||||
v
|
||||
AIOS 唯一控制面
|
||||
AIOS Control Plane
|
||||
Tenant | Agent | Task | Policy | Audit
|
||||
|
|
||||
v
|
||||
Execution Orchestrator / Agent Router
|
||||
Execution Orchestrator / Router
|
||||
/ \
|
||||
v v
|
||||
OpenClaw Adapter Hermes Boundary
|
||||
@@ -85,45 +79,20 @@ AIOS V2 遵循以下不可协商原则:
|
||||
OpenClaw Runtime Hermes Workers
|
||||
\
|
||||
v
|
||||
结果回写 + 审计 + 执行证据
|
||||
Result Reconciliation + Evidence
|
||||
```
|
||||
|
||||
控制面拥有权威身份、策略、审批、任务生命周期和审计状态。OpenClaw 与 Hermes 只接收带租户上下文、已经批准的执行工作,并返回运行结果。
|
||||
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.
|
||||
|
||||
## 6. 发布规则
|
||||
## 6. Release rule
|
||||
|
||||
候选版本在满足以下条件前不得提升:
|
||||
No candidate may be promoted until:
|
||||
|
||||
- 需求到代码的追踪关系完整;
|
||||
- 文档—代码匹配检查通过;
|
||||
- 多租户负向测试通过;
|
||||
- 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 等标准技术术语。
|
||||
- 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.
|
||||
|
||||
@@ -1,108 +1,39 @@
|
||||
# ADR-001:AIOS 作为唯一控制面
|
||||
# ADR-001: AIOS 作为唯一控制面
|
||||
|
||||
- 状态:已采纳
|
||||
- 日期:2026-07-20
|
||||
- 影响范围:Control Plane、Agent Router、OpenClaw、Hermes、Web Console、GitOps、审计
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-20
|
||||
|
||||
## 1. 背景
|
||||
## Context
|
||||
|
||||
现有系统已经包含多个执行、调度和治理能力。如果 OpenClaw、Hermes、Agent Router 或 Web 层分别保存自己的租户、策略、审批和任务状态,就会形成多个权威事实源,带来以下问题:
|
||||
现有系统包含多个执行和调度能力,若每个 Runtime 保存自己的任务、租户和策略状态,会形成多个事实源。
|
||||
|
||||
- 同一任务在不同模块中状态不一致;
|
||||
- 审计链不完整;
|
||||
- 多租户隔离难以统一验证;
|
||||
- Web、Runtime 和运维入口可能绕过策略;
|
||||
- Bug 修复和发布无法判断应该修改哪个事实源。
|
||||
|
||||
## 2. 决策
|
||||
## Decision
|
||||
|
||||
AIOS Control Plane 是唯一权威控制面,负责:
|
||||
|
||||
- Tenant Registry;
|
||||
- Identity 与角色上下文;
|
||||
- Agent Registry;
|
||||
- Task Lifecycle;
|
||||
- Policy Decision;
|
||||
- Approval;
|
||||
- Audit Event;
|
||||
- Runtime Inventory;
|
||||
- Execution Evidence;
|
||||
- GitOps 与发布状态可视化。
|
||||
- Tenant
|
||||
- Agent
|
||||
- Task
|
||||
- Policy
|
||||
- Approval
|
||||
- Audit
|
||||
- Execution Evidence
|
||||
|
||||
OpenClaw、Hermes 和 Agent Router 只能执行、路由或调度已经由 AIOS 批准的工作,不得拥有第二套租户、策略、审批或任务生命周期。
|
||||
OpenClaw、Hermes、Agent Router 只能执行或路由,不拥有第二套控制状态。
|
||||
|
||||
## 3. 目标边界
|
||||
## Consequences
|
||||
|
||||
```text
|
||||
Web / API
|
||||
|
|
||||
v
|
||||
AIOS Control Plane
|
||||
|
|
||||
v
|
||||
Execution Orchestrator / Agent Router
|
||||
|
|
||||
+--> OpenClaw Adapter --> OpenClaw Runtime
|
||||
|
|
||||
+--> Hermes Boundary --> Hermes Workers
|
||||
```
|
||||
Positive:
|
||||
|
||||
## 4. 备选方案
|
||||
- 状态唯一;
|
||||
- 审计完整;
|
||||
- Web/API 统一。
|
||||
|
||||
### 方案 A:保持多个控制面
|
||||
Negative:
|
||||
|
||||
拒绝原因:状态会持续分裂,无法形成统一审计和租户隔离。
|
||||
- Runtime 接入需要 Adapter。
|
||||
- 迁移需要阶段性边界收敛。
|
||||
|
||||
### 方案 B:由 OpenClaw 或 Hermes 成为主控制面
|
||||
## Compliance
|
||||
|
||||
拒绝原因:二者主要价值是执行与 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`
|
||||
任何新 Runtime 必须通过 AIOS Orchestrator 接入。
|
||||
|
||||
@@ -1,108 +1,46 @@
|
||||
# ADR-002:OpenClaw 与 Hermes 从控制面职责中解耦
|
||||
# ADR-002: OpenClaw/Hermes 与控制面解耦
|
||||
|
||||
- 状态:已采纳
|
||||
- 日期:2026-07-20
|
||||
- 影响范围:OpenClaw、Hermes、Execution Orchestrator、Agent Router、Audit、Web Console
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-20
|
||||
|
||||
## 1. 背景
|
||||
## Context
|
||||
|
||||
OpenClaw 和 Hermes 已经具备成熟的执行能力,但历史实现可能同时承担部分租户、策略、任务状态或运维控制职责。如果继续扩展这种模式,AIOS 将无法成为唯一控制面,也无法统一 Web、审计和多租户隔离。
|
||||
OpenClaw 和 Hermes 已具备执行能力,但历史上可能承担部分控制职责。
|
||||
|
||||
## 2. 决策
|
||||
## Decision
|
||||
|
||||
OpenClaw 与 Hermes 通过稳定的 Adapter/Boundary 接入 AIOS:
|
||||
引入边界层:
|
||||
|
||||
```text
|
||||
AIOS Control Plane
|
||||
|
|
||||
v
|
||||
Execution Orchestrator
|
||||
Adapter / Boundary
|
||||
|
|
||||
+--> OpenClaw Adapter --> OpenClaw Runtime
|
||||
|
|
||||
+--> Hermes Boundary --> Hermes Workers
|
||||
v
|
||||
Runtime
|
||||
```
|
||||
|
||||
OpenClaw 只负责:
|
||||
OpenClaw:
|
||||
|
||||
- 验证签名命令;
|
||||
- 执行;
|
||||
- 查询状态;
|
||||
- 取消;
|
||||
- 健康检查;
|
||||
- 返回结果和生命周期事件。
|
||||
- 执行签名命令;
|
||||
- 返回结果;
|
||||
- 提供健康状态。
|
||||
|
||||
Hermes 只负责:
|
||||
Hermes:
|
||||
|
||||
- tenant/cell/profile 分区队列;
|
||||
- Worker claim;
|
||||
- Lease 与 Heartbeat;
|
||||
- 执行;
|
||||
- 完成、失败、取消和结果回传。
|
||||
- 队列;
|
||||
- Worker;
|
||||
- Lease;
|
||||
- Heartbeat;
|
||||
- 执行结果。
|
||||
|
||||
二者均不得:
|
||||
## Consequences
|
||||
|
||||
- 创建或批准 AIOS Task;
|
||||
- 修改租户或策略;
|
||||
- 维护第二套任务生命周期;
|
||||
- 绕过 AIOS Audit;
|
||||
- 向浏览器提供直接控制入口。
|
||||
- Runtime 可独立演进。
|
||||
- 控制逻辑不会分裂。
|
||||
- 需要维护稳定 Contract。
|
||||
|
||||
## 3. 备选方案
|
||||
## Migration
|
||||
|
||||
### 方案 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`
|
||||
先接 Adapter,再迁移旧调用入口,最后关闭控制旁路。
|
||||
|
||||
@@ -1,121 +1,44 @@
|
||||
# ADR-003:所有变更必须通过 GitOps
|
||||
# ADR-003: 所有变更必须通过 GitOps
|
||||
|
||||
- 状态:已采纳
|
||||
- 日期:2026-07-20
|
||||
- 影响范围:代码、配置、文档、测试、部署、回滚、生产修复
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-20
|
||||
|
||||
## 1. 背景
|
||||
## Context
|
||||
|
||||
直接在生产目录、长期 worktree、主分支或运行容器中修改代码,会造成以下问题:
|
||||
直接在生产目录、长期 worktree 或主分支上修改代码,会造成不可追踪、不可回滚和分支冲突。
|
||||
|
||||
- 变更不可追踪;
|
||||
- 无法稳定回滚;
|
||||
- 本地仓库与 Gitea 不一致;
|
||||
- Worktree 和 Branch Ref 冲突;
|
||||
- 紧急修复无法统一审视;
|
||||
- 文档、测试和部署资产无法同步。
|
||||
## Decision
|
||||
|
||||
## 2. 决策
|
||||
|
||||
所有代码、配置、文档、测试、部署和回滚资产必须经过以下流程:
|
||||
所有代码、配置、文档、测试、部署和回滚资产必须通过以下流程:
|
||||
|
||||
```text
|
||||
非 main 分支
|
||||
-> Commit
|
||||
-> 本地仓库同步
|
||||
-> Gitea 镜像与 OID 校验
|
||||
-> 自动测试
|
||||
-> Codex 监督审查
|
||||
-> 候选集成
|
||||
-> Owner 批准
|
||||
-> Merge / Tag / Deploy
|
||||
non-main branch -> commit -> local sync -> Gitea sync -> test -> review -> controlled promotion
|
||||
```
|
||||
|
||||
GitOps Controller worktree 必须长期保持 detached 且 clean。相同分支写入必须串行,任务工作区必须可回收。
|
||||
Controller worktree 必须长期保持 detached 且 clean。每个写任务使用独立工作区或受控写入流程;相同分支写入必须串行。
|
||||
|
||||
## 3. 必需证据
|
||||
## Required evidence
|
||||
|
||||
每个任务必须记录:
|
||||
|
||||
- 任务 ID;
|
||||
- 执行器;
|
||||
- 分支;
|
||||
- Commit;
|
||||
- 修改文件;
|
||||
- claim;
|
||||
- heartbeat;
|
||||
- progress;
|
||||
- blocker;
|
||||
- 测试命令、exit code 和结果;
|
||||
- 本地/Gitea 同步状态;
|
||||
- 回滚说明;
|
||||
- 最终结果和监督结论。
|
||||
- branch
|
||||
- commit
|
||||
- changed files
|
||||
- test command and result
|
||||
- blocker and resolution
|
||||
- local/Gitea sync status
|
||||
- rollback note
|
||||
|
||||
## 4. 禁止事项
|
||||
## Prohibited
|
||||
|
||||
- 直接 push `main`;
|
||||
- 未审批创建生产 Tag;
|
||||
- 未记录任务直接修改生产;
|
||||
- 在生产目录手工修复后不回写 Git;
|
||||
- fetch 到已被 worktree 检出的本地 branch ref;
|
||||
- 以新分支名反复规避 Worktree 生命周期缺陷;
|
||||
- 在 Git 中提交明文凭据;
|
||||
- 缺少文档和测试时标记为完成。
|
||||
- 直接 push `main`
|
||||
- 未审批创建生产 Tag
|
||||
- 在生产目录手工修改后不回写 Git
|
||||
- fetch 到已被 worktree 检出的本地 branch ref
|
||||
- 在 Git 中提交明文密钥
|
||||
|
||||
## 5. 备选方案
|
||||
## Consequences
|
||||
|
||||
### 方案 A:允许紧急生产手工修改
|
||||
优点:可审计、可回滚、可并行治理。
|
||||
|
||||
拒绝原因:即使紧急修复也必须形成 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`
|
||||
成本:紧急修复也必须形成 patch branch 和证据。
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
# 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,116 +1,43 @@
|
||||
# ADR-005:文档与代码同步门禁
|
||||
# ADR-005: 文档与代码同步门禁
|
||||
|
||||
- 状态:已采纳
|
||||
- 日期:2026-07-20
|
||||
- 影响范围:需求、架构、代码、测试、部署、在线任务、发布和归档
|
||||
- Status: Accepted
|
||||
- Date: 2026-07-20
|
||||
|
||||
## 1. 背景
|
||||
## Context
|
||||
|
||||
架构、功能和部署变化如果不同步文档,会导致版本无法审计,后续 Agent 无法判断真实设计,发布人员也无法确认代码是否符合用户需求。历史上还出现过文档目录不存在、文档含冲突标记、分支名与代码内容不一致等问题。
|
||||
架构、功能和部署变化如果不同步文档,会导致版本无法审计,后续 Agent 无法判断真实设计。
|
||||
|
||||
## 2. 决策
|
||||
## Decision
|
||||
|
||||
所有影响以下内容的变更必须在同一交付流程中同步 `docs/aios-v2/`:
|
||||
所有影响以下内容的变更必须同步 `docs/aios-v2/`:
|
||||
|
||||
- 用户行为和功能范围;
|
||||
- API、Header、请求和响应;
|
||||
- 数据模型、字段和状态机;
|
||||
- Control Plane、Agent Router、OpenClaw、Hermes 的边界;
|
||||
- 多租户和安全策略;
|
||||
- 持久化、审计和执行证据;
|
||||
- 部署、健康检查和回滚;
|
||||
- 测试和发布门禁。
|
||||
- 用户行为;
|
||||
- API;
|
||||
- 数据模型;
|
||||
- 状态机;
|
||||
- Runtime Boundary;
|
||||
- 安全策略;
|
||||
- 部署流程;
|
||||
- 测试门禁。
|
||||
|
||||
正式文档统一使用中文,技术标识保留代码中的原始英文名称。
|
||||
## Gate
|
||||
|
||||
## 3. 门禁要求
|
||||
验收必须检查:
|
||||
|
||||
验收必须执行:
|
||||
1. 文档引用代码路径存在;
|
||||
2. API 文档有实现映射;
|
||||
3. Contract 字段有模型映射;
|
||||
4. Traceability Matrix 已更新;
|
||||
5. ADR 与架构变化一致。
|
||||
|
||||
```text
|
||||
python3 tools/aios_v2_docs_code_check.py
|
||||
```
|
||||
## Consequences
|
||||
|
||||
检查至少包括:
|
||||
优点:
|
||||
|
||||
1. 必需文档和 ADR 存在且非空;
|
||||
2. 文档以中文叙述为主;
|
||||
3. 文档引用的代码、测试和部署路径存在;
|
||||
4. 核心 API 能在代码中找到;
|
||||
5. 核心模型和字段能在代码中找到;
|
||||
6. Traceability Matrix 覆盖所有 `UR-*`;
|
||||
7. ADR 索引引用的文件存在;
|
||||
8. 文档中不存在 Git 冲突标记;
|
||||
9. 不存在摘要占位符或空章节;
|
||||
10. 架构/功能代码变化时,同一交付范围包含文档变化。
|
||||
- 架构长期可维护;
|
||||
- Agent 执行有明确上下文;
|
||||
- Bug 修复有历史依据。
|
||||
|
||||
## 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`
|
||||
- 小功能也需要更新对应文档。
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
# 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、分片或事件溯源;
|
||||
- 修改状态回滚策略。
|
||||
+24
-52
@@ -1,67 +1,39 @@
|
||||
# AIOS V2 架构决策记录索引
|
||||
# AIOS V2 Architecture Decision Records
|
||||
|
||||
- 文档版本:`V2R1C0-candidate`
|
||||
- 适用范围:影响多个模块、长期演进、兼容性、安全、数据、部署或交付流程的关键架构决策
|
||||
本目录记录影响多个模块、长期演进、兼容性、安全或交付流程的关键架构决策。
|
||||
|
||||
## 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 | 决策 |
|
||||
|---|---|
|
||||
| [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 |
|
||||
|
||||
## 2. 状态定义
|
||||
## ADR 状态
|
||||
|
||||
- **已提出**:决策已提出,但尚未被正式采纳。
|
||||
- **已采纳**:当前有效,所有新实现必须遵守。
|
||||
- **已替代**:已被新的 ADR 替代,保留用于历史追踪。
|
||||
- **已废弃**:保留历史,但禁止新实现继续采用。
|
||||
- `Proposed`:提出但未采纳。
|
||||
- `Accepted`:当前有效决策。
|
||||
- `Superseded`:被新 ADR 替代。
|
||||
- `Deprecated`:保留历史但禁止新实现采用。
|
||||
|
||||
## 3. 必须新增或更新 ADR 的场景
|
||||
## 何时必须新增或更新 ADR
|
||||
|
||||
以下变化必须新增或更新 ADR:
|
||||
|
||||
1. AIOS、OpenClaw、Hermes、Agent Router 的控制边界变化;
|
||||
1. 控制面与 Runtime 权限边界变化;
|
||||
2. 多租户隔离模型变化;
|
||||
3. API、数据模型或状态机出现不兼容变化;
|
||||
4. 持久化、消息、密钥、审计或部署方案发生跨模块变化;
|
||||
5. GitOps、发布、回滚或文档门禁变化;
|
||||
6. 替换 OpenClaw、Hermes、Agent Router 或核心基础设施;
|
||||
7. 引入新的权威事实源、任务生命周期或租户状态;
|
||||
8. 生产安全模型、身份代理或 Secret 管理方式变化。
|
||||
6. 替换 OpenClaw、Hermes、Agent Router 或核心基础设施。
|
||||
|
||||
## 4. ADR 内容要求
|
||||
ADR 变化必须同步更新:
|
||||
|
||||
每份 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`。
|
||||
- `02-TARGET-ARCHITECTURE.md`
|
||||
- `04-MODULE-CONTRACTS.md`
|
||||
- `05-TRACEABILITY-MATRIX.md`
|
||||
- 对应代码、测试和部署资产
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
|
||||
"branch": "task/codex/AIOS-V2R1-PRODUCTION-SUPERVISOR-20260720",
|
||||
"base": "integration/aios-v2r1-cto-production-candidate-20260720",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-20T03:49:09Z",
|
||||
"created_at": "2026-07-20T03:49:13Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# AIOS OpenClaw Run Provenance Writer
|
||||
|
||||
本插件为 AIOS V2R1 提供 Owner 签名的运行授权、持久 Provenance Store 与专用 Evidence Writer。它不授予通用 Shell 或任意文件写权限。
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 签名合同:`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
@@ -1 +0,0 @@
|
||||
export { default } from "../index.js";
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from "../policy.js";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "../provenance.js";
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from "../writer.js";
|
||||
@@ -1,173 +0,0 @@
|
||||
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";
|
||||
|
||||
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) {
|
||||
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 };
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/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;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"id": "aios-tenant-boundary",
|
||||
"name": "AIOS Tenant Boundary",
|
||||
"description": "Enforces Owner-signed run provenance and a dedicated evidence writer.",
|
||||
"version": "2.1.0-rc2",
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "@aios/openclaw-tenant-boundary",
|
||||
"version": "2.1.0-rc.2",
|
||||
"type": "module",
|
||||
"description": "AIOS V2R1 Tenant Cell provenance and side-effect policy boundary for OpenClaw.",
|
||||
"files": [
|
||||
"index.js",
|
||||
"policy.js",
|
||||
"provenance.js",
|
||||
"writer.js",
|
||||
"issuer.mjs",
|
||||
"dist",
|
||||
"openclaw.plugin.json"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node --test test/*.test.js"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./dist/index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
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");
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
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 = [];
|
||||
let factory;
|
||||
const api = {
|
||||
pluginConfig: {},
|
||||
registerTool(value, options) { factory = value; assert.equal(options.name, "aios_evidence_write"); },
|
||||
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(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"]);
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,126 +2,38 @@ 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: >-
|
||||
监督 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
|
||||
Supervise AIOS V2R1 production candidate deployment, resolve blocking issues,
|
||||
review patches, and maintain architecture/document consistency.
|
||||
responsibilities:
|
||||
- 领取任务并写入 claim 证据
|
||||
- 定期写入 heartbeat、progress 和监督结论
|
||||
- 审阅 OpenClaw 的部署、健康检查、联调和回滚证据
|
||||
- 分析失败测试、运行时异常和阻塞根因
|
||||
- 审查所有 patch 的架构边界、租户隔离和 Secret 安全
|
||||
- 解决需要跨模块修改的阻塞问题
|
||||
- 对控制面、数据模型、状态机或 Runtime Contract 变化要求新增或更新 ADR
|
||||
- 执行文档与代码匹配检查
|
||||
- 验证需求、架构、契约、代码、测试、部署和验收追踪完整
|
||||
- 拒绝缺少复现、测试、文档或回滚说明的修改
|
||||
- 将所有阻塞、审查结论、修复 Commit 和剩余风险提交到系统
|
||||
- 输出是否允许进入合并、Tag 和生产部署 Gate 的最终建议
|
||||
- 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
|
||||
prohibited:
|
||||
- 绕过 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、继续修复或停止发布建议
|
||||
- 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
|
||||
|
||||
@@ -2,95 +2,34 @@ 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: >-
|
||||
在隔离预发布环境部署 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/
|
||||
Deploy AIOS V2R1 candidate runtime, execute health checks, integrate OpenClaw
|
||||
runtime boundary, and fix low-risk runtime/deployment defects.
|
||||
responsibilities:
|
||||
- 领取任务并写入 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 监督任务统一审视
|
||||
- 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
|
||||
prohibited:
|
||||
- 修改 AIOS 唯一控制面原则
|
||||
- 修改 Tenant、Policy、Approval 或 Task Lifecycle 权威边界
|
||||
- 绕过 GitOps 直接修改生产
|
||||
- 直接 push main 或创建生产 Tag
|
||||
- 提交 Token、密码、私钥、API Key 或签名材料
|
||||
- 在没有复现测试和文档更新时扩大修复范围
|
||||
- 对架构问题自行做不可逆决策
|
||||
lowRiskFixScope:
|
||||
- systemd 单元和文件权限
|
||||
- Dockerfile、Compose 和健康检查
|
||||
- 环境变量、路径和启动参数
|
||||
- 日志、错误消息和超时边界
|
||||
- Runtime Adapter 的小型兼容性缺陷
|
||||
- 测试、部署或文档中的明确错误
|
||||
- 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
|
||||
escalation:
|
||||
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 的监督结论
|
||||
- architecture change -> Codex supervisor
|
||||
- security issue -> AIOS owner
|
||||
- blocked deployment -> Codex supervisor
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -17,7 +16,6 @@ from aios_openclaw import (
|
||||
InProcessRuntimeTransport,
|
||||
OpenClawAdapter,
|
||||
)
|
||||
from aios_openclaw.contract import OpenClawCommand, sign_command, verify_command_signature
|
||||
from aios_v2 import (
|
||||
ControlPlaneService,
|
||||
ExecutionEvidence,
|
||||
@@ -30,7 +28,7 @@ from aios_v2 import (
|
||||
from aios_v2.persistence import DurableControlPlaneService, SQLiteSnapshotRepository
|
||||
|
||||
|
||||
SIGNING_MATERIAL = bytes(range(32))
|
||||
SIGNING_MATERIAL = b"candidate-test-signing-material-0001"
|
||||
|
||||
|
||||
def context(
|
||||
@@ -66,24 +64,6 @@ def bootstrap(service: ControlPlaneService, tenant_id: str = "tenant-a"):
|
||||
|
||||
|
||||
class CandidateFlowTests(unittest.TestCase):
|
||||
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)
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user