Compare commits

..
42 changed files with 411 additions and 3925 deletions
-412
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+11 -46
View File
@@ -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
}
+7 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
-404
View File
@@ -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、离线全量验收或在线预发布。
+11 -28
View File
@@ -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)
+1 -1
View File
@@ -59,7 +59,7 @@ class FileSigningMaterialResolver:
raise RuntimeError("signing material reference is not a regular file")
if metadata.st_mode & 0o077:
raise RuntimeError("signing material file permissions must be 0600 or stricter")
value = self.path.read_bytes()
value = self.path.read_bytes().strip()
if len(value) < 32:
raise RuntimeError("signing material is too short")
return value
-183
View File
@@ -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
-4
View File
@@ -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 探针共同验证。
-272
View File
@@ -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 监督结论不存在未解决阻塞。
-235
View File
@@ -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
View File
@@ -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
View File
@@ -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`
- 对应代码、测试和部署资产
@@ -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
View File
@@ -1 +0,0 @@
export { default } from "../index.js";
-1
View File
@@ -1 +0,0 @@
export * from "../policy.js";
-1
View File
@@ -1 +0,0 @@
export * from "../provenance.js";
-1
View File
@@ -1 +0,0 @@
export * from "../writer.js";
-173
View File
@@ -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 };
-68
View File
@@ -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" }
}
}
}
-23
View File
@@ -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"
]
}
}
-73
View File
@@ -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 };
}
-141
View File
@@ -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"]);
});
-143
View File
@@ -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);
}
}
+28 -116
View File
@@ -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
+23 -84
View File
@@ -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
+1 -21
View File
@@ -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)
-271
View File
@@ -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())