Compare commits

..
Author SHA1 Message Date
Codex 63b31165b7 test(aios-v2): 固化 Key ID 修复验收证据 2026-07-21 12:16:10 +08:00
Codex 07b5c2c9c2 merge(aios-v2): 修复 OpenClaw Key ID 传播 2026-07-21 12:15:48 +08:00
Codex d71b7903df fix(aios-v2): 传播 OpenClaw 签名 Key ID 2026-07-21 12:15:48 +08:00
Codex 79fd83cd61 test(aios-v2): 固化 Runtime HTTP 离线验收证据 2026-07-21 12:11:42 +08:00
Codex fedb6b08fb merge(openclaw): 接入签名 Runtime HTTP 合同 2026-07-21 12:05:52 +08:00
Codex 83f8f878c7 feat(openclaw): 增加签名 Runtime HTTP 合同 2026-07-21 12:05:29 +08:00
Codex 1352a664ab test(aios-v2): 固化 HTTP 修复后验收证据 2026-07-21 11:40:16 +08:00
Codex 57209760cd merge(bugfix): 修复真实 HTTP 身份头解析 2026-07-21 11:39:44 +08:00
Codex 850bb669b7 fix(aios-v2): 规范化 HTTP 身份头大小写 2026-07-21 11:39:43 +08:00
Codex 9c20bf1362 test(aios-v2): 固化 Gate 与离线验收证据 2026-07-21 11:29:56 +08:00
Codex 09fa4f9d73 docs(aios-v2): 补齐核心模型中文追踪 2026-07-21 11:25:34 +08:00
Codex 816be7bec5 docs(agent): 固化 provenance 清障与 Gate 启动证据 2026-07-21 11:23:27 +08:00
Codex 25c25abd3e chore(aios): provenance openclaw-v2r1-20260721T031523Z heartbeat.json 2026-07-21 11:18:57 +08:00
Codex 8edb96a0fa merge(security): 纳入 provenance service-user Git guard 2026-07-21 11:18:02 +08:00
Codex d8f01f745d merge(security): 修复 provenance plugin runtime 合同 2026-07-21 11:11:34 +08:00
Codex 9206b883ab merge(security): 纳入 OpenClaw run provenance writer 2026-07-21 11:06:34 +08:00
28 changed files with 937 additions and 103 deletions
+7 -7
View File
@@ -1,7 +1,7 @@
{ {
"schema": "aios.v2.agent-blockers.v1", "schema": "aios.v2.agent-blockers.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720", "task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"updated_at": "2026-07-20T10:17:49Z", "updated_at": "2026-07-21T03:23:27Z",
"evidence_writer": "windows-chatgpt-owner", "evidence_writer": "windows-chatgpt-owner",
"blockers": [ "blockers": [
{ {
@@ -37,12 +37,12 @@
{ {
"blocker_id": "BLK-OPENCLAW-RUN-PROVENANCE-MISSING", "blocker_id": "BLK-OPENCLAW-RUN-PROVENANCE-MISSING",
"severity": "high", "severity": "high",
"status": "blocked", "status": "resolved",
"root_cause": "现有插件只有进程内 runId/sender trust Map,没有 Owner-signed task provenance schema、持久 Store 或受限 Evidence 写工具。", "root_cause": "旧插件只有进程内 runId/sender Map,缺少 Owner-signed Schema、持久 Store 和限定 Git Writer。",
"fix": "禁止用 run-key fallback 绕过;等待经审查的 issuer/store/writer 或外部同等受信系统。", "fix": "部署 aios-tenant-boundary rc2,增加 Ed25519 issuer/store、Run/Sender/OID/期限绑定、一次性 permit 与七路径专用 Writer。",
"verification": "当前无法完成 7 路径写测试与越权拒绝审计。", "verification": "Provenance openclaw-v2r1-20260721T031523Z 完成 heartbeat 写入、Commit、普通 Push 和审计;README.md 越权写入被拒绝;Node 测试 9/9。",
"commit": "bf01a13f3aaf42a386232ed2f5409feb0f79fcf6", "commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb",
"resolved_at": null "resolved_at": "2026-07-21T03:23:27Z"
}, },
{ {
"blocker_id": "BLK-CONTROLLER-LEGACY-GITEA-CHECK", "blocker_id": "BLK-CONTROLLER-LEGACY-GITEA-CHECK",
+2 -2
View File
@@ -7,8 +7,8 @@
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b", "candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"claimed_at": "2026-07-20T08:24:30Z", "claimed_at": "2026-07-20T08:24:30Z",
"executor_instance": "openclaw-main-cn-network-mkt", "executor_instance": "openclaw-main-cn-network-mkt",
"owner_observation_at": "2026-07-20T10:17:49Z", "owner_observation_at": "2026-07-21T03:23:27Z",
"owner_observation": "四项高风险阻塞已清除;provenance 仍缺可信签发/强制执行链。", "owner_observation": "五项高风险 Blocker 已解除;Provenance Writer 真实写入、普通 Push、审计和越权拒绝通过。",
"evidence_writer": "windows-chatgpt-owner", "evidence_writer": "windows-chatgpt-owner",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335", "previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7", "candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
+7 -3
View File
@@ -24,12 +24,12 @@
"token_file_owner": "root:root", "token_file_owner": "root:root",
"token_file_mode": "0600", "token_file_mode": "0600",
"secret_provider_contract_path": "/etc/openclaw/SECRET_PROVIDER_CONTRACT.json", "secret_provider_contract_path": "/etc/openclaw/SECRET_PROVIDER_CONTRACT.json",
"last_verified": "2026-07-20T10:17:49Z", "last_verified": "2026-07-21T03:23:27Z",
"verification_result": "HTTPS Git push successful with new token", "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_reference": "AIOS_OPENCLAW_CREDENTIAL_REF=file:///etc/openclaw/secrets/openclaw-gitea-deploy-token",
"env_file_path": "/etc/aios-v2/aios-v2.env", "env_file_path": "/etc/aios-v2/aios-v2.env",
"env_file_mode": "0640 root:gitops", "env_file_mode": "0640 root:gitops",
"last_updated": "2026-07-20T10:17:49Z", "last_updated": "2026-07-21T03:23:27Z",
"old_credential_revoked": true, "old_credential_revoked": true,
"revoked": true, "revoked": true,
"new_credential_name": "openclaw-deploy-cli-v2-20260720", "new_credential_name": "openclaw-deploy-cli-v2-20260720",
@@ -49,5 +49,9 @@
"signing_material_configured": true, "signing_material_configured": true,
"signing_key_id": "openclaw-v2r1-candidate-ed25519", "signing_key_id": "openclaw-v2r1-candidate-ed25519",
"signing_permission_check": "passed", "signing_permission_check": "passed",
"runtime_sign_verify_test": "passed" "runtime_sign_verify_test": "passed",
"run_provenance_status": "resolved",
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"provenance_public_key_configured": true,
"provenance_secret_values_recorded": false
} }
+9 -8
View File
@@ -1,11 +1,12 @@
{ {
"schema": "aios.v2.agent-heartbeat.v1", "current_step": "GATE_A_PENDING",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720", "evidence_writer": "aios_evidence_write",
"executor": "openclaw", "executor": "openclaw",
"state": "blocked", "heartbeat_at": "2026-07-21T03:23:27Z",
"heartbeat_at": "2026-07-20T10:17:49Z", "heartbeat_source": "owner-signed-provenance-writer",
"heartbeat_source": "windows-chatgpt-owner", "provenance_id": "openclaw-v2r1-20260721T031523Z",
"valid_for_running": false, "schema": "aios.v2.agent-heartbeat.v1",
"current_step": "BLOCKED_PROVENANCE_ISSUER_STORE_WRITER_REQUIRED", "state": "running",
"evidence_writer": "windows-chatgpt-owner" "task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"valid_for_running": true
} }
+12 -10
View File
@@ -1,9 +1,9 @@
{ {
"schema": "aios.v2.agent-progress.v1", "schema": "aios.v2.agent-progress.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720", "task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"state": "blocked", "state": "running",
"status": "PARTIAL", "status": "PARTIAL",
"updated_at": "2026-07-20T10:17:49Z", "updated_at": "2026-07-21T03:23:27Z",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b", "candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc", "task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
"gitea_sync": { "gitea_sync": {
@@ -13,10 +13,10 @@
}, },
"credential_rotation_status": "passed", "credential_rotation_status": "passed",
"executor_model_path": "passed", "executor_model_path": "passed",
"tests": "not_run", "tests": "controller 12/12;provenance Node 9/9;真实签发/越权拒绝/heartbeat commit+push/审计通过;Gate A/B/离线 Acceptance 待执行",
"deployment": "not_run", "deployment": "aios-tenant-boundary rc2 已部署,runtime doctor 与 Gateway restart 通过",
"rollback": "not_run", "rollback": "两次验收脚本断言错误均实际触发备份回滚并恢复 rc1;修正后 rc2 部署成功",
"current_step": "四项 blocker resolved;provenance blocked", "current_step": "GATE_A_PENDING",
"evidence_writer": "windows-chatgpt-owner", "evidence_writer": "windows-chatgpt-owner",
"aios_v2_env_status": { "aios_v2_env_status": {
"exists": true, "exists": true,
@@ -40,9 +40,9 @@
"verified_at": "2026-07-20T09:39:16Z" "verified_at": "2026-07-20T09:39:16Z"
}, },
"release_recommendation": "STOP_PREPRODUCTION", "release_recommendation": "STOP_PREPRODUCTION",
"gate_a_allowed": false, "gate_a_allowed": true,
"resolved_blockers": 4, "resolved_blockers": 5,
"open_blockers": 1, "open_blockers": 0,
"controller_patch_commit": "6188da26453505dacde9d6d298191437164c8d27", "controller_patch_commit": "6188da26453505dacde9d6d298191437164c8d27",
"signing_contract_commit": "8a36f7832bc105e4b9d32ffbe018abed2ff1b1ab", "signing_contract_commit": "8a36f7832bc105e4b9d32ffbe018abed2ff1b1ab",
"secret_audit": { "secret_audit": {
@@ -53,5 +53,7 @@
"legacy_after": 0, "legacy_after": 0,
"gateway_token_rotated": true, "gateway_token_rotated": true,
"secret_values_recorded": false "secret_values_recorded": false
} },
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"provenance_live_commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb"
} }
+11 -11
View File
@@ -1,7 +1,7 @@
{ {
"schema": "aios.v2.agent-result.v1", "schema": "aios.v2.agent-result.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720", "task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"final_status": "BLOCKED", "final_status": "PARTIAL",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b", "candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc", "task_commit_before_owner_evidence": "558dc932fa200831045e0f2edebbc12d775232bc",
"gitea_sync": { "gitea_sync": {
@@ -10,11 +10,11 @@
"codex_task_match": true "codex_task_match": true
}, },
"secret_reference_status": "resolved", "secret_reference_status": "resolved",
"executor_status": "blocked", "executor_status": "running",
"test_status": "targeted controller 12/12; Ed25519 candidate flow 6/6; Gate A/B 未执行", "test_status": "controller 12/12;provenance 9/9;live write/push/audit/deny passed;Gate A/B/offline pending",
"deployment_status": "STOP_PREPRODUCTION", "deployment_status": "provenance plugin rc2 active",
"rollback_status": "not_run", "rollback_status": "automatic plugin/config rollback exercised and passed",
"documentation_sync_status": "updated", "documentation_sync_status": "updated before Gate A",
"changed_files": [ "changed_files": [
".agent/claim.json", ".agent/claim.json",
".agent/heartbeat.json", ".agent/heartbeat.json",
@@ -24,16 +24,16 @@
".agent/status.json", ".agent/status.json",
".agent/credential-status.json" ".agent/credential-status.json"
], ],
"open_blockers": [ "open_blockers": [],
"BLK-OPENCLAW-RUN-PROVENANCE-MISSING"
],
"remaining_risks": [ "remaining_risks": [
"缺少 Owner-signed provenance issuer/store/writer 与越权拒绝证据" "缺少 Owner-signed provenance issuer/store/writer 与越权拒绝证据"
], ],
"completed_at": "2026-07-20T10:17:49Z", "completed_at": "2026-07-21T03:23:27Z",
"evidence_writer": "windows-chatgpt-owner", "evidence_writer": "windows-chatgpt-owner",
"previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335", "previous_candidate_commit": "634f6b164f5b20372ab9ec8144bbf76df97de335",
"candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7", "candidate_merge_commit": "f55dfd975e9d716b690c7693742e7cf2beaa55e7",
"candidate_updated_at": "2026-07-20T09:39:16Z", "candidate_updated_at": "2026-07-20T09:39:16Z",
"release_recommendation": "STOP_PREPRODUCTION" "release_recommendation": "STOP_PREPRODUCTION",
"provenance_id": "openclaw-v2r1-20260721T031523Z",
"provenance_live_commit": "25c25abd3ed19091fe078d4d7a271c57f23e87fb"
} }
+6 -6
View File
@@ -2,13 +2,13 @@
"schema": "aios.v2.agent-status.v1", "schema": "aios.v2.agent-status.v1",
"task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720", "task_id": "AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"executor": "openclaw", "executor": "openclaw",
"state": "blocked", "state": "running",
"status": "BLOCKED_PROVENANCE_REQUIRED", "status": "running",
"claimed_at": "2026-07-20T08:24:30Z", "claimed_at": "2026-07-20T08:24:30Z",
"updated_at": "2026-07-20T10:17:49Z", "updated_at": "2026-07-21T03:23:27Z",
"last_heartbeat": "2026-07-20T10:17:49Z", "last_heartbeat": "2026-07-21T03:23:27Z",
"heartbeat_valid_for_running": false, "heartbeat_valid_for_running": true,
"current_step": "等待受信 provenance issuer/store/writer", "current_step": "GATE_A_PENDING",
"candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b", "candidate_commit": "ba7669c3b49f98cf3500af6ea966ba0db472ff3b",
"release_recommendation": "STOP_PREPRODUCTION", "release_recommendation": "STOP_PREPRODUCTION",
"evidence_writer": "windows-chatgpt-owner", "evidence_writer": "windows-chatgpt-owner",
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T03:26:12.120179Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "09fa4f9d73799a2e924153d198d8ebb09ff15cfd",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T032611Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T03:40:03.455790Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "57209760cd159e564d7de1160090917dbbc7ce0a",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T034002Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T04:11:34.291792Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "fedb6b08fbb8c825972f700318a81e902e89cda5",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041133Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
@@ -0,0 +1,67 @@
{
"schema": "aios.v2.acceptance.v1",
"generated_at": "2026-07-21T04:16:01.542048Z",
"branch": "task/openclaw/AIOS-V2R1-PRODUCTION-DEPLOY-20260720",
"commit": "07b5c2c9c28e7840b6261172a7d34aa7de7c48c9",
"workspace_dirty": false,
"passed": 8,
"failed": 0,
"warnings": 1,
"steps": [
{
"name": "文档与代码匹配",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/文档与代码匹配.log"
},
{
"name": "Python 语法编译",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Python_语法编译.log"
},
{
"name": "候选单元与集成测试",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/候选单元与集成测试.log"
},
{
"name": "Shell 脚本语法",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Shell_脚本语法.log"
},
{
"name": "文档 Git 冲突标记",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/文档_Git_冲突标记.log"
},
{
"name": "明文密钥基础扫描",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/明文密钥基础扫描.log"
},
{
"name": "systemd 单元验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/systemd_单元验证.log"
},
{
"name": "Docker Compose 配置验证",
"status": "通过",
"exit_code": 0,
"log": "/data/aios-gitops/worktrees/aios-v2r1-deploy-20260720/.aios-evidence/acceptance/20260721T041601Z/Docker_Compose_配置验证.log"
},
{
"name": "在线健康检查",
"status": "警告",
"exit_code": 0,
"log": "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
}
],
"final_status": "passed"
}
+2 -2
View File
@@ -399,6 +399,6 @@ Key ID `openclaw-v2r1-candidate-ed25519` 对应真实 Ed25519 私钥,不得再
## 16. 2026-07-20 阻塞清除快照 ## 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 授权合同。 五项高风险 Blocker 均已形成运行证据。`BLK-OPENCLAW-RUN-PROVENANCE-MISSING` 通过 Git 中的 `aios-tenant-boundary` rc2 插件解除:Ed25519 Owner 签发记录持久保存,授权绑定 Run、Sender、Task、Claim OID、Candidate OID 与任务分支;专用 `aios_evidence_write` 只允许七个 `.agent` JSON 和固定 Git 操作,通用 Shell/写工具保持拒绝。
在 provenance 专用签发、验证、受限写入、普通 commit/push 和越权拒绝测试完成前,保持 `STOP_PREPRODUCTION`,不得执行 Gate A、Gate B、离线全量验收或在线预发布。 Provenance `openclaw-v2r1-20260721T031523Z` 已完成 `heartbeat.json` 更新、Commit、当前任务分支普通 Push 和追加式审计;`README.md` 越权写入被拒绝。由此允许启动 Gate A、Gate B 和离线验收,但在三项验收及后续在线联调完成前仍保持 `STOP_PREPRODUCTION`。
+2 -2
View File
@@ -33,7 +33,7 @@ class CommandConflict(AdapterError):
class RuntimeTransport(Protocol): class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ... def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ... def cancel(self, command_id: str, *, tenant_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ... def health(self) -> RuntimeHealth: ...
@@ -203,7 +203,7 @@ class OpenClawAdapter:
CommandState.REJECTED, CommandState.REJECTED,
}: }:
return current return current
if not self.transport.cancel(command_id): if not self.transport.cancel(command_id, tenant_id=tenant_id):
raise AdapterError("runtime rejected cancellation") raise AdapterError("runtime rejected cancellation")
cancelled = replace( cancelled = replace(
current, current,
+12 -4
View File
@@ -19,7 +19,7 @@ from .contract import (
class RuntimeTransport(Protocol): class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ... def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ... def cancel(self, command_id: str, *, tenant_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ... def health(self) -> RuntimeHealth: ...
@@ -50,11 +50,16 @@ class InProcessRuntimeTransport:
self._lock = threading.RLock() self._lock = threading.RLock()
self._running: set[str] = set() self._running: set[str] = set()
self._cancelled: set[str] = set() self._cancelled: set[str] = set()
self._command_tenants: dict[str, str] = {}
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult:
if timeout_seconds <= 0: if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive") raise ValueError("timeout_seconds must be positive")
with self._lock: with self._lock:
known_tenant = self._command_tenants.get(command.command_id)
if known_tenant is not None and known_tenant != command.tenant_id:
raise TransportError("command id belongs to another tenant")
self._command_tenants[command.command_id] = command.tenant_id
if command.command_id in self._cancelled: if command.command_id in self._cancelled:
return OpenClawResult( return OpenClawResult(
command_id=command.command_id, command_id=command.command_id,
@@ -90,8 +95,10 @@ class InProcessRuntimeTransport:
with self._lock: with self._lock:
self._running.discard(command.command_id) self._running.discard(command.command_id)
def cancel(self, command_id: str) -> bool: def cancel(self, command_id: str, *, tenant_id: str) -> bool:
with self._lock: with self._lock:
if self._command_tenants.get(command_id) != tenant_id:
return False
self._cancelled.add(command_id) self._cancelled.add(command_id)
return command_id in self._running return command_id in self._running
@@ -190,10 +197,11 @@ class HTTPRuntimeTransport:
attempts=int(value.get("attempts", 1)), attempts=int(value.get("attempts", 1)),
) )
def cancel(self, command_id: str) -> bool: def cancel(self, command_id: str, *, tenant_id: str) -> bool:
value = self._request( value = self._request(
"POST", "POST",
f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel", f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel"
f"?tenant_id={urllib.parse.quote(tenant_id, safe='')}",
payload={}, payload={},
timeout_seconds=10.0, timeout_seconds=10.0,
) )
+2
View File
@@ -321,6 +321,7 @@ def build_application() -> CandidateApplication:
openclaw_url = os.getenv("AIOS_OPENCLAW_URL", "").strip() openclaw_url = os.getenv("AIOS_OPENCLAW_URL", "").strip()
material_file = os.getenv("AIOS_OPENCLAW_SIGNING_MATERIAL_FILE", "").strip() material_file = os.getenv("AIOS_OPENCLAW_SIGNING_MATERIAL_FILE", "").strip()
openclaw_key_id = os.getenv("AIOS_OPENCLAW_SIGNING_KEY_ID", "default").strip() or "default"
openclaw = None openclaw = None
resolver = None resolver = None
if openclaw_url and material_file: if openclaw_url and material_file:
@@ -341,6 +342,7 @@ def build_application() -> CandidateApplication:
openclaw=openclaw, openclaw=openclaw,
hermes=hermes, hermes=hermes,
openclaw_signing_material_resolver=resolver, openclaw_signing_material_resolver=resolver,
openclaw_key_id=openclaw_key_id,
) )
return CandidateApplication(gateway, orchestrator, tracker, hermes, openclaw) return CandidateApplication(gateway, orchestrator, tracker, hermes, openclaw)
+18 -7
View File
@@ -79,20 +79,31 @@ class GatewayApplication:
@staticmethod @staticmethod
def context_from_headers(headers: Mapping[str, str]) -> RequestContext: def context_from_headers(headers: Mapping[str, str]) -> RequestContext:
authenticated = str(headers.get(AUTHENTICATED_HEADER, "")).strip().lower() # HTTP field names are case-insensitive. BaseHTTPRequestHandler commonly
# presents them as title-cased (for example ``X-Aios-Authenticated``),
# so copying the message into a plain dict must not make identity lookup
# case-sensitive.
normalized_headers = {
str(key).strip().lower(): value for key, value in headers.items()
}
def header(name: str, default: str = "") -> str:
return str(normalized_headers.get(name.lower(), default))
authenticated = header(AUTHENTICATED_HEADER).strip().lower()
if authenticated not in {"1", "true", "yes"}: if authenticated not in {"1", "true", "yes"}:
raise Unauthorized("trusted authentication assertion is required") raise Unauthorized("trusted authentication assertion is required")
roles = [ roles = [
role.strip() role.strip()
for role in str(headers.get(ROLES_HEADER, "")).split(",") for role in header(ROLES_HEADER).split(",")
if role.strip() if role.strip()
] ]
return RequestContext.create( return RequestContext.create(
tenant_id=str(headers.get(TENANT_HEADER, "")), tenant_id=header(TENANT_HEADER),
principal_id=str(headers.get(PRINCIPAL_HEADER, "")), principal_id=header(PRINCIPAL_HEADER),
trace_id=str(headers.get(TRACE_HEADER, "")).strip() or None, trace_id=header(TRACE_HEADER).strip() or None,
session_id=str(headers.get(SESSION_HEADER, "")).strip() or None, session_id=header(SESSION_HEADER).strip() or None,
policy_version=str(headers.get(POLICY_HEADER, "v1")).strip() or "v1", policy_version=header(POLICY_HEADER, "v1").strip() or "v1",
roles=roles, roles=roles,
) )
+12 -1
View File
@@ -7,6 +7,7 @@ execution runtimes and never own tenant policy or task lifecycle.
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Mapping from typing import Any, Mapping
from aios_hermes import HermesAdapter, HermesJob, HermesResult from aios_hermes import HermesAdapter, HermesJob, HermesResult
@@ -129,6 +130,15 @@ class ExecutionOrchestrator:
task.tenant_id, task.tenant_id,
self.openclaw_key_id, self.openclaw_key_id,
) )
requested_timeout = (
self.openclaw.default_timeout_seconds
if timeout_seconds is None
else float(timeout_seconds)
)
effective_timeout = min(requested_timeout, 300.0)
expires_at = (
datetime.now(timezone.utc) + timedelta(seconds=effective_timeout)
).isoformat().replace("+00:00", "Z")
command = OpenClawCommand.create( command = OpenClawCommand.create(
tenant_id=task.tenant_id, tenant_id=task.tenant_id,
task_id=task.task_id, task_id=task.task_id,
@@ -136,10 +146,11 @@ class ExecutionOrchestrator:
action=task.action, action=task.action,
payload=task.payload, payload=task.payload,
idempotency_key=task.idempotency_key or task.task_id, idempotency_key=task.idempotency_key or task.task_id,
expires_at=expires_at,
key_id=self.openclaw_key_id, key_id=self.openclaw_key_id,
) )
command = sign_command(command, key_material) command = sign_command(command, key_material)
result = self.openclaw.execute(command, timeout_seconds=timeout_seconds) result = self.openclaw.execute(command, timeout_seconds=effective_timeout)
self.reconcile_openclaw(context, result) self.reconcile_openclaw(context, result)
return DispatchRecord( return DispatchRecord(
tenant_id=task.tenant_id, tenant_id=task.tenant_id,
+25
View File
@@ -419,6 +419,8 @@ X-AIOS-Policy-Version
- 代理必须删除客户端同名头后重新注入。 - 代理必须删除客户端同名头后重新注入。
- 应用不得直接暴露公网端口。 - 应用不得直接暴露公网端口。
- HTTP 字段名按协议不区分大小写;应用把 Header 集合转换为普通映射后,必须先统一字段名大小写再解析身份。`X-AIOS-*`、`X-Aios-*` 与全小写形式必须得到相同的 `RequestContext`,但字段值本身不得被改写。
- 真实监听器回归测试必须覆盖 `BaseHTTPRequestHandler` 的 title-case 行为,避免纯函数测试使用固定大写键而漏掉在线 401。
### 11.2 核心 API ### 11.2 核心 API
@@ -500,3 +502,26 @@ POST /api/v2/hermes/results/reconcile
- Evidence/Audit 格式。 - Evidence/Audit 格式。
不兼容变化必须新增 ADR 和版本号,不得静默修改。 不兼容变化必须新增 ADR 和版本号,不得静默修改。
## 15. OpenClaw Runtime HTTP 合同
AIOS 只通过 OpenClaw loopback Gateway 的签名 Runtime 路由联调:
- `GET /v1/health`
- `POST /v1/execute`
- `GET /v1/commands/<command_id>?tenant_id=<tenant_id>`
- `POST /v1/commands/<command_id>/cancel?tenant_id=<tenant_id>`
`execute` 必须携带 `aios.openclaw.command.v1` Ed25519 签名合同和不超过 300 秒的
`expires_at`。Runtime 同时核对仓库外 CredentialRef;签名错误、过期、身份字段篡改、
跨租户查询/取消以及幂等冲突均失败关闭。取消接口的 `tenant_id` 是 Transport 合同的
组成部分,不允许仅凭 `command_id` 控制另一租户的命令。
Candidate 组合层必须把 `AIOS_OPENCLAW_SIGNING_KEY_ID` 原样传入 Orchestrator,
不得在已有批准 Key ID 时回落为 `default`。
Orchestrator 对调用方请求的更长超时执行 300 秒上限收敛,使签名有效期与 Runtime
硬上限保持一致。
当前 Runtime action allowlist 仅包含无副作用的 `status` 和正式验收专用
`acceptance.delay`。其成功只代表 OpenClaw 插件 Runtime 已执行对应合同动作,不代表
模型推理或任意 Agent 工具执行。命令身份、幂等键与终态追加持久化;Gateway 重启时
未完成的 `RUNNING` 记录收敛为 `FAILED/restart_interrupted`,不得误报为真实超时。
+14
View File
@@ -70,6 +70,20 @@
| ExecutionEvidence | §9 | `execution_tracking.py` | evidence persistence test | | ExecutionEvidence | §9 | `execution_tracking.py` | evidence persistence test |
| GitOpsStatus | §10 | `gitops_status.py` | healthcheck detached/clean检查 | | GitOpsStatus | §10 | `gitops_status.py` | healthcheck detached/clean检查 |
### 4.1 核心控制面模型说明
`RequestContext` 是每次控制面请求的不可缺失上下文。它承载租户、调用主体、追踪标识和请求来源,所有读取、写入、审批与调度都必须先用该上下文完成租户边界和身份检查。适配器不得自行补造租户,也不得把缺失上下文解释为默认管理权限。上下文校验失败时,控制面必须拒绝请求并写入不含密钥值的审计记录。
`TaskState` 是权威任务生命周期状态,不是前端展示标签。任务从创建、批准、调度、运行到完成、失败或取消,只能沿合同允许的方向迁移;OpenClaw 与 Hermes 回调只能请求迁移,最终状态由唯一控制面持久化。重复回调依靠幂等键收敛,跨租户回调、旧租约回调和非法逆向迁移必须被拒绝。
`TenantRecord`、`AgentRecord` 与 `TaskRecord` 共同定义所有权:租户拥有 Agent 和 Task,Agent 只能处理其租户与授权 Cell/Profile 范围内的任务。任何查询若缺少租户过滤条件,都不能作为正式实现。`AuditEvent` 记录主体、动作、对象、结果和追踪标识,但不得保存 Token、密码、私钥或签名材料。
`OpenClawCommand` 与 `OpenClawResult` 描述短执行路径;前者必须绑定任务、租户、追踪标识、命令期限和签名合同,后者必须回传相同身份域并接受幂等校验。`RuntimeHealth` 只报告可验证运行事实,不能把本地 Bare Repo 更新误报为 Gitea 完全同步。
`HermesJob`、`HermesResult` 与 `HermesHealth` 描述长任务路径。Job 的领取、心跳和 Lease 续期必须持久化;Result 只有在任务、租户、执行器和租约全部匹配时才能改变权威状态。Worker 崩溃后可重新领取过期 Lease,但不得重复产生业务副作用。
`ExecutionEvidence` 保存 Claim、Heartbeat、Result 与必要的追踪关系,用于重启恢复和审计闭环。Evidence 的写入必须限定任务分支与固定路径;本轮增加的 Owner-signed Provenance Writer 只允许七个 `.agent` JSON,并以普通 Commit 与 Push 固化,不授予通用 Shell 或任意仓库写权限。
## 5. 部署追踪 ## 5. 部署追踪
| 能力 | 文件 | 验收命令 | | 能力 | 文件 | 验收命令 |
+14 -31
View File
@@ -109,21 +109,15 @@ AIOS V2R1 的核心代码、完整中文文档、任务合同、部署资产和
### 4.4 BLK-OPENCLAW-RUN-PROVENANCE-MISSING ### 4.4 BLK-OPENCLAW-RUN-PROVENANCE-MISSING
- 严重级别:高 - 严重级别:高
- 状态:开放 - 状态:已解决
- 现象:OpenClaw 写工具被 `run_provenance_missing` 策略拒绝。 - 根因:旧插件只有进程内 `runId -> sender` Map,没有 Owner-signed Schema、持久 Store 和限定 Git Writer。
- 风险:无法证明写操作来自已领取、已授权、可审计的执行会话。 - 修复:部署 `aios-tenant-boundary` rc2,增加 Ed25519 issuer/store、Run/Sender/OID/期限绑定、一次性 permit 与专用 `aios_evidence_write`。
- 外部受信输入:允许七个 `.agent` JSON 写入的 OpenClaw Provenance 授权。 - 验证:Provenance `openclaw-v2r1-20260721T031523Z` 完成 Heartbeat 写入、Commit、普通 Push 和审计;`README.md` 越权请求被拒绝;Node 测试 9/9。
- 解除条件:
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 ### 4.5 BLK-CONTROLLER-LEGACY-GITEA-CHECK
- 严重级别:高 - 严重级别:高
- 状态:开放 - 状态:已解决
- 现象:Controller 健康检查仍硬编码旧 `aios-v23`,并返回 `gitea=false`、`branch_sync=false`。 - 现象:Controller 健康检查仍硬编码旧 `aios-v23`,并返回 `gitea=false`、`branch_sync=false`。
- 风险:健康状态与当前 AIOS V2R1 仓库不一致,可能错误阻止或错误放行发布。 - 风险:健康状态与当前 AIOS V2R1 仓库不一致,可能错误阻止或错误放行发布。
- 解除条件: - 解除条件:
@@ -152,9 +146,9 @@ AIOS V2R1 的核心代码、完整中文文档、任务合同、部署资产和
| Gate | 当前状态 | 说明 | | Gate | 当前状态 | 说明 |
|---|---|---| |---|---|---|
| Gate A:文档与设计 | 未运行 | Candidate 未合并到任务,阻塞未解除 | | Gate A:文档与设计 | 允许启动 | 五项高风险 Blocker 已解除,等待正式执行 |
| Gate B:代码与测试 | 未运行 | 不能基于不完整任务分支测试 | | Gate B:代码与测试 | 等待 Gate A | 等待正式执行候选测试 |
| Gate C:预发布 | 禁止 | Secret、签名、Provenance、Controller Health 未通过 | | Gate C:预发布 | 禁止 | 等待 Gate A、Gate B、离线全量验收和在线联调 |
| Gate D:发布审查 | `STOP_PREPRODUCTION` | Codex 明确停止预发布 | | Gate D:发布审查 | `STOP_PREPRODUCTION` | Codex 明确停止预发布 |
| Gate E:生产发布 | 禁止 | 未满足任何生产 Gate | | Gate E:生产发布 | 禁止 | 未满足任何生产 Gate |
@@ -190,29 +184,18 @@ AIOS V2R1 的核心代码、完整中文文档、任务合同、部署资产和
## 8. 外部受信输入 ## 8. 外部受信输入
目前仅有两项确实需要外部受信输入: 五项高风险 Blocker 当前均不再需要新的外部受信输入。签名材料继续留在受保护边界,Provenance 仍须按 Run 由 Owner 限时签发,不得转化为长期通用权限。
1. **批准的签名材料**
- 不得通过聊天、Git 或普通日志传递;
- 必须通过批准的 Secret/Key 交付链安装。
2. **OpenClaw Run Provenance 授权**
- 仅允许当前任务所需的七个 `.agent` JSON 写入;
- 必须绑定任务、执行器实例、Claim、Candidate 和分支;
- 不得授予通用生产写权限。
其余阻塞均可由 OpenClaw、Codex、Windows ChatGPT Owner 和 GitOps 运维流程处理。
## 9. 监督结论 ## 9. 监督结论
当前状态保持: 当前状态保持:
```text ```text
BLOCKED PARTIAL
STOP_PREPRODUCTION STOP_PREPRODUCTION
``` ```
虽然三分支已经同步 Gitea、凭据已经最小权限轮换、执行服务已经恢复,但当前尚无足够证据允许执行 Gate A–D 或进入预发布。 三分支已同步 Gitea,五项高风险 Blocker 已解除,允许执行 Gate A、Gate B 和离线全量验收;在三项正式结果与后续在线验收形成证据前,不进入预发布。
任何后续状态更新必须同步: 任何后续状态更新必须同步:
@@ -223,13 +206,13 @@ STOP_PREPRODUCTION
- `AIOS-V2R1-PRODUCTION-CANDIDATE.md`; - `AIOS-V2R1-PRODUCTION-CANDIDATE.md`;
- 如涉及架构或发布语义,更新 ADR。 - 如涉及架构或发布语义,更新 ADR。
## 9. 2026-07-20 Owner 修复实况 ## 10. 2026-07-20 至 2026-07-21 Owner 修复实况
- Worktree:OpenClaw 与 Codex 独立、目标分支唯一检出、无 lock、无 prunable;现有 Evidence 已先备份。 - Worktree:OpenClaw 与 Codex 独立、目标分支唯一检出、无 lock、无 prunable;现有 Evidence 已先备份。
- Candidate:`ba7669c3b49f98cf3500af6ea966ba0db472ff3b` 已普通 Push 到 Gitea,并以 `--no-ff` 合入 OpenClaw 任务分支。 - Candidate:`ba7669c3b49f98cf3500af6ea966ba0db472ff3b` 已普通 Push 到 Gitea,并以 `--no-ff` 合入 OpenClaw 任务分支。
- Secrets:OpenClaw 原生 audit 从 3 项明文降为 `plaintext=0`、`unresolved=0`、`shadowed=0`、`legacy=0`;Gateway Token 已轮换,OneAPI 实际 agent 调用通过。 - 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 和篡改拒绝通过。 - 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`。 - 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 冒充签发。 - Provenance:插件 rc2 已声明原生工具合同并通过 runtime doctor;Owner-signed Store、一次性 permit、七路径 Writer、普通 Push、审计与越权拒绝均通过。首次部署检查脚本曾两次因读取错误 JSON 层级触发自动回滚,旧插件与配置均成功恢复,修正断言后部署成功。
因此当前结论仍为 `STOP_PREPRODUCTION`,Gate A 不得启动。解除最后阻塞需要经审查的 provenance issuer/store/writer 实现,或外部受信系统提供同等合同。 因此当前结论仍为 `STOP_PREPRODUCTION`,但 Gate A 已允许启动;后续结论必须基于 Gate A、Gate B、离线 Acceptance、在线 Runtime、重启恢复和正式回滚演练的最新证据。
@@ -2,6 +2,21 @@
本插件为 AIOS V2R1 提供 Owner 签名的运行授权、持久 Provenance Store 与专用 Evidence Writer。它不授予通用 Shell 或任意文件写权限。 本插件为 AIOS V2R1 提供 Owner 签名的运行授权、持久 Provenance Store 与专用 Evidence Writer。它不授予通用 Shell 或任意文件写权限。
## AIOS Runtime HTTP 合同
插件在 OpenClaw loopback Gateway 内注册 `/v1/health`、`/v1/execute` 和
`/v1/commands/<id>`。执行请求必须同时通过本机 CredentialRef 比对与 Ed25519
签名验证;热路径只读取公钥,不读取 Candidate 私密签名材料。
Runtime 仅允许无副作用的 `status` 与在线验收专用 `acceptance.delay`。它不把
返回成功冒充为模型推理或任意 Agent 执行。命令身份、幂等键和终态以追加式
JSONL 持久化;Gateway 重启时遗留的 `RUNNING` 命令会收敛为
`FAILED/restart_interrupted`,避免把重启误报成真实超时。
取消只对当前进程内仍在执行的命令生效,超时由签名命令的 `expires_at` 与
30 秒硬上限共同约束。未知 action、错误签名、过期命令、跨 tenant 状态查询、
冲突的 `command_id` 或幂等键均失败关闭。
## 安全边界 ## 安全边界
- 签名合同:`aios.run-provenance/v1`,Ed25519,授权最长 15 分钟。 - 签名合同:`aios.run-provenance/v1`,Ed25519,授权最长 15 分钟。
+5
View File
@@ -3,6 +3,7 @@ import path from "node:path";
import { canonicalize, loadStoredRecord, readPublicKey, validatePayload, verifyRecord } from "./provenance.js"; import { canonicalize, loadStoredRecord, readPublicKey, validatePayload, verifyRecord } from "./provenance.js";
import { classifyRun, decideToolCall, runKey, validateWriterParams } from "./policy.js"; import { classifyRun, decideToolCall, runKey, validateWriterParams } from "./policy.js";
import { executeEvidenceWrite } from "./writer.js"; import { executeEvidenceWrite } from "./writer.js";
import { createRuntimeApi } from "./runtime.js";
const activeRuns = new Map(); const activeRuns = new Map();
const permits = new Map(); const permits = new Map();
@@ -128,6 +129,10 @@ export default {
name: "AIOS Tenant Boundary", name: "AIOS Tenant Boundary",
description: "Fail-closed Owner provenance and dedicated evidence-writer boundary.", description: "Fail-closed Owner provenance and dedicated evidence-writer boundary.",
register(api) { register(api) {
const runtimeApi = createRuntimeApi(api);
api.registerHttpRoute({ path: "/v1/health", auth: "plugin", match: "exact", handler: runtimeApi.handleHealth });
api.registerHttpRoute({ path: "/v1/execute", auth: "plugin", match: "exact", handler: runtimeApi.handleExecute });
api.registerHttpRoute({ path: "/v1/commands/", auth: "plugin", match: "prefix", handler: runtimeApi.commandRequest });
registerWriterTool(api); registerWriterTool(api);
api.on("before_agent_run", async (event, ctx) => { api.on("before_agent_run", async (event, ctx) => {
prune(); prune();
@@ -1,8 +1,8 @@
{ {
"id": "aios-tenant-boundary", "id": "aios-tenant-boundary",
"name": "AIOS Tenant Boundary", "name": "AIOS Tenant Boundary",
"description": "Enforces Owner-signed run provenance and a dedicated evidence writer.", "description": "Enforces Owner-signed run provenance, a dedicated evidence writer, and the signed AIOS runtime contract.",
"version": "2.1.0-rc2", "version": "2.1.0-rc3",
"entry": "dist/index.js", "entry": "dist/index.js",
"contracts": { "contracts": {
"tools": ["aios_evidence_write"] "tools": ["aios_evidence_write"]
@@ -32,7 +32,13 @@
"writerPushRemote": { "type": "string" }, "writerPushRemote": { "type": "string" },
"writerAskPassPath": { "type": "string" }, "writerAskPassPath": { "type": "string" },
"writerAuditLogPath": { "type": "string" }, "writerAuditLogPath": { "type": "string" },
"gitBinary": { "type": "string" } "gitBinary": { "type": "string" },
"runtimeStatePath": { "type": "string" },
"runtimeSigningPublicKeyPath": { "type": "string" },
"runtimeSigningKeyId": { "type": "string" },
"runtimeCredentialRef": { "type": "string" },
"runtimeMaxActive": { "type": "integer", "minimum": 1, "maximum": 16 },
"runtimeMaxDelayMs": { "type": "integer", "minimum": 100, "maximum": 30000 }
} }
} }
} }
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@aios/openclaw-tenant-boundary", "name": "@aios/openclaw-tenant-boundary",
"version": "2.1.0-rc.2", "version": "2.1.0-rc.3",
"type": "module", "type": "module",
"description": "AIOS V2R1 Tenant Cell provenance and side-effect policy boundary for OpenClaw.", "description": "AIOS V2R1 Tenant Cell provenance and side-effect policy boundary for OpenClaw.",
"files": [ "files": [
@@ -8,6 +8,7 @@
"policy.js", "policy.js",
"provenance.js", "provenance.js",
"writer.js", "writer.js",
"runtime.js",
"issuer.mjs", "issuer.mjs",
"dist", "dist",
"openclaw.plugin.json" "openclaw.plugin.json"
+280
View File
@@ -0,0 +1,280 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const CONTRACT_VERSION = "aios.openclaw.command.v1";
const MAX_BODY_BYTES = 64 * 1024;
const CLOCK_SKEW_MS = 300_000;
const COMMAND_FIELDS = [
"command_id", "tenant_id", "task_id", "trace_id", "action", "payload",
"idempotency_key", "created_at", "expires_at", "contract_version", "key_id",
];
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function sorted(value) {
if (Array.isArray(value)) return value.map(sorted);
if (!isObject(value)) return value;
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])]));
}
function canonical(value) {
return JSON.stringify(sorted(value));
}
function nowIso() {
return new Date().toISOString();
}
function sendJson(res, status, body) {
const data = Buffer.from(JSON.stringify(body));
res.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": String(data.length),
"Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff",
});
res.end(data);
return true;
}
async function readJson(req) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > MAX_BODY_BYTES) throw new Error("body_too_large");
chunks.push(chunk);
}
let value;
try { value = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); }
catch { throw new Error("invalid_json"); }
if (!isObject(value)) throw new Error("invalid_json_object");
return value;
}
function requireAbsolute(config, name) {
const value = config[name];
if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`config_${name}_missing`);
return value;
}
function appendRecord(statePath, record) {
const parent = path.dirname(statePath);
fs.mkdirSync(parent, { recursive: true, mode: 0o750 });
const fd = fs.openSync(statePath, "a", 0o640);
try {
fs.writeSync(fd, `${JSON.stringify(record)}\n`, null, "utf8");
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
fs.chmodSync(statePath, 0o640);
}
function validateCommand(value, expectedKeyId) {
const allowed = new Set([...COMMAND_FIELDS, "signature"]);
if (Object.keys(value).some((key) => !allowed.has(key))) throw new Error("unknown_command_field");
for (const name of COMMAND_FIELDS.filter((name) => name !== "expires_at" && name !== "payload")) {
if (typeof value[name] !== "string" || !value[name].trim()) throw new Error(`invalid_${name}`);
}
if (!isObject(value.payload)) throw new Error("invalid_payload");
if (typeof value.expires_at !== "string" || !value.expires_at) throw new Error("expires_at_required");
if (typeof value.signature !== "string" || !value.signature) throw new Error("signature_missing");
if (value.contract_version !== CONTRACT_VERSION) throw new Error("contract_version_rejected");
if (value.key_id !== expectedKeyId) throw new Error("key_id_rejected");
const created = Date.parse(value.created_at);
const expires = Date.parse(value.expires_at);
if (!Number.isFinite(created) || !Number.isFinite(expires)) throw new Error("timestamp_invalid");
if (created - Date.now() > CLOCK_SKEW_MS) throw new Error("created_at_in_future");
if (expires <= created || expires - created > 300_000) throw new Error("expiry_window_rejected");
if (Date.now() - expires > CLOCK_SKEW_MS) throw new Error("command_expired");
const unsigned = Object.fromEntries(COMMAND_FIELDS.map((name) => [name, value[name]]));
return { unsigned, expires };
}
function verifySignature(command, publicKey, expectedKeyId) {
const { unsigned, expires } = validateCommand(command, expectedKeyId);
let signature;
try { signature = Buffer.from(command.signature, "base64url"); }
catch { throw new Error("signature_invalid"); }
if (!crypto.verify(null, Buffer.from(canonical(unsigned)), publicKey, signature)) throw new Error("signature_invalid");
return { digest: crypto.createHash("sha256").update(canonical(unsigned)).digest("hex"), expires };
}
function resultFor(command, state, { output = null, error = null, startedAt, finishedAt = nowIso() } = {}) {
return {
command_id: command.command_id, tenant_id: command.tenant_id,
task_id: command.task_id, trace_id: command.trace_id, state, output, error,
started_at: startedAt ?? nowIso(), finished_at: finishedAt,
runtime_id: "openclaw-aios-runtime-v1", attempts: 1,
};
}
function loadState(statePath) {
const commands = new Map();
const idempotency = new Map();
if (!fs.existsSync(statePath)) return { commands, idempotency };
for (const line of fs.readFileSync(statePath, "utf8").split("\n").filter(Boolean)) {
let record;
try { record = JSON.parse(line); } catch { throw new Error("runtime_state_corrupt"); }
if (!isObject(record) || typeof record.command_id !== "string") throw new Error("runtime_state_corrupt");
commands.set(record.command_id, record);
if (record.tenant_id && record.idempotency_key) idempotency.set(`${record.tenant_id}\0${record.idempotency_key}`, record.command_id);
}
return { commands, idempotency };
}
export function createRuntimeApi(api) {
const config = api.pluginConfig ?? {};
const statePath = requireAbsolute(config, "runtimeStatePath");
const publicKeyPath = requireAbsolute(config, "runtimeSigningPublicKeyPath");
const expectedKeyId = String(config.runtimeSigningKeyId || "");
const credentialRef = String(config.runtimeCredentialRef || "");
if (!expectedKeyId) throw new Error("config_runtimeSigningKeyId_missing");
if (!credentialRef) throw new Error("config_runtimeCredentialRef_missing");
const publicKey = crypto.createPublicKey(fs.readFileSync(publicKeyPath));
const maxActive = Math.max(1, Math.min(Number(config.runtimeMaxActive || 4), 16));
const maxDelayMs = Math.max(100, Math.min(Number(config.runtimeMaxDelayMs || 30_000), 30_000));
const state = loadState(statePath);
const active = new Map();
for (const record of state.commands.values()) {
if (record.result?.state === "RUNNING") {
const reconciled = { ...record, result: { ...record.result, state: "FAILED", finished_at: nowIso(), error: { code: "restart_interrupted", message: "execution outcome is unknown after runtime restart" } } };
appendRecord(statePath, reconciled);
state.commands.set(record.command_id, reconciled);
}
}
function requireCredential(req) {
const supplied = String(req.headers["x-aios-credential-ref"] || "");
const left = Buffer.from(supplied);
const right = Buffer.from(credentialRef);
if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) throw new Error("credential_ref_rejected");
}
function persist(command, digest, result) {
const record = {
command_id: command.command_id, tenant_id: command.tenant_id,
task_id: command.task_id, trace_id: command.trace_id,
idempotency_key: command.idempotency_key, digest, result, recorded_at: nowIso(),
};
appendRecord(statePath, record);
state.commands.set(command.command_id, record);
state.idempotency.set(`${command.tenant_id}\0${command.idempotency_key}`, command.command_id);
return record;
}
async function execute(command, digest, expires) {
const startedAt = nowIso();
persist(command, digest, resultFor(command, "RUNNING", { startedAt, finishedAt: null }));
if (command.action === "status") {
const completed = resultFor(command, "COMPLETED", { startedAt, output: { status: "ok", runtime: "openclaw-plugin", side_effects: false } });
persist(command, digest, completed);
return completed;
}
if (command.action !== "acceptance.delay") throw new Error("unsupported_action");
const delayMs = Number(command.payload.delay_ms);
if (!Number.isInteger(delayMs) || delayMs < 0 || delayMs > maxDelayMs) {
const failed = resultFor(command, "FAILED", { startedAt, error: { code: "invalid_delay", message: "delay_ms is outside the allowed range" } });
persist(command, digest, failed);
return failed;
}
const controller = new AbortController();
active.set(command.command_id, controller);
let timedOut = false;
const deadlineMs = Math.max(0, Math.min(maxDelayMs, expires - Date.now()));
const deadline = setTimeout(() => { timedOut = true; controller.abort(); }, deadlineMs);
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, delayMs);
controller.signal.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("aborted")); }, { once: true });
});
const completed = resultFor(command, "COMPLETED", { startedAt, output: { delayed_ms: delayMs, side_effects: false } });
persist(command, digest, completed);
return completed;
} catch {
const terminal = resultFor(command, timedOut ? "TIMED_OUT" : "CANCELLED", {
startedAt,
error: timedOut ? { code: "runtime_timeout", message: "runtime deadline exceeded" } : { code: "cancelled", message: "cancelled by AIOS" },
});
persist(command, digest, terminal);
return terminal;
} finally {
clearTimeout(deadline);
active.delete(command.command_id);
}
}
async function handleExecute(req, res) {
if (req.method !== "POST") return sendJson(res, 405, { error: "method_not_allowed" });
try {
requireCredential(req);
const command = await readJson(req);
const { digest, expires } = verifySignature(command, publicKey, expectedKeyId);
const prior = state.commands.get(command.command_id);
if (prior) {
if (prior.digest !== digest) return sendJson(res, 409, { error: "command_id_conflict" });
return sendJson(res, 200, prior.result);
}
const idemId = state.idempotency.get(`${command.tenant_id}\0${command.idempotency_key}`);
if (idemId) {
const idem = state.commands.get(idemId);
if (!idem || idem.digest !== digest) return sendJson(res, 409, { error: "idempotency_conflict" });
return sendJson(res, 200, idem.result);
}
if (!new Set(["status", "acceptance.delay"]).has(command.action)) return sendJson(res, 422, { error: "unsupported_action" });
if (active.size >= maxActive) return sendJson(res, 429, { error: "runtime_capacity_exceeded" });
return sendJson(res, 200, await execute(command, digest, expires));
} catch (error) {
const code = error instanceof Error ? error.message : "request_rejected";
const status = code === "body_too_large" ? 413 : code.startsWith("config_") || code === "runtime_state_corrupt" ? 503 : 401;
return sendJson(res, status, { error: code });
}
}
function commandRequest(req, res) {
try {
requireCredential(req);
const url = new URL(req.url, "http://localhost");
const match = url.pathname.match(/^\/v1\/commands\/([^/]+)(?:\/(status|cancel))?$/);
if (!match) return false;
const commandId = decodeURIComponent(match[1]);
const operation = match[2] || "status";
const record = state.commands.get(commandId);
if (!record) return sendJson(res, 404, { error: "command_not_found" });
const tenantId = String(url.searchParams.get("tenant_id") || "");
if (!tenantId || tenantId !== record.tenant_id) return sendJson(res, 404, { error: "command_not_found" });
if (operation === "status" && req.method === "GET") {
return sendJson(res, 200, record.result);
}
if (operation === "cancel" && req.method === "POST") {
const controller = active.get(commandId);
if (controller) controller.abort();
return sendJson(res, 200, { accepted: Boolean(controller), command_id: commandId });
}
return sendJson(res, 405, { error: "method_not_allowed" });
} catch (error) {
return sendJson(res, 401, { error: error instanceof Error ? error.message : "request_rejected" });
}
}
return {
handleHealth(req, res) {
if (req.method !== "GET") return sendJson(res, 405, { error: "method_not_allowed" });
return sendJson(res, 200, {
runtime: "openclaw", runtime_id: "openclaw-aios-runtime-v1", status: "healthy",
version: "2.1.0-rc.3", heartbeat_at: nowIso(), active_tasks: active.size,
workspace_isolated: true, credential_ref_configured: true,
details: { transport: "openclaw_plugin_http", allowed_actions: ["status", "acceptance.delay"] },
});
},
handleExecute,
commandRequest,
};
}
export const __test = { canonical, validateCommand, verifySignature, loadState };
@@ -87,10 +87,21 @@ test("generic mutation tools stay blocked", () => {
test("plugin registers the native writer factory and required hook names", () => { test("plugin registers the native writer factory and required hook names", () => {
const hooks = []; const hooks = [];
const routes = [];
let factory; let factory;
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-runtime-register-"));
const keys = keyFixture();
const publicKeyPath = path.join(root, "public.pem");
fs.writeFileSync(publicKeyPath, keys.publicPem);
const api = { const api = {
pluginConfig: {}, pluginConfig: {
runtimeStatePath: path.join(root, "state.jsonl"),
runtimeSigningPublicKeyPath: publicKeyPath,
runtimeSigningKeyId: "key-1",
runtimeCredentialRef: "secretref:test",
},
registerTool(value, options) { factory = value; assert.equal(options.name, "aios_evidence_write"); }, registerTool(value, options) { factory = value; assert.equal(options.name, "aios_evidence_write"); },
registerHttpRoute(value) { routes.push(value); },
on(name) { hooks.push(name); }, on(name) { hooks.push(name); },
}; };
plugin.register(api); plugin.register(api);
@@ -98,6 +109,7 @@ test("plugin registers the native writer factory and required hook names", () =>
assert.equal(tool.name, "aios_evidence_write"); assert.equal(tool.name, "aios_evidence_write");
assert.equal(tool.label, "AIOS Evidence Writer"); assert.equal(tool.label, "AIOS Evidence Writer");
assert.equal(tool.executionMode, "sequential"); assert.equal(tool.executionMode, "sequential");
assert.deepEqual(routes.map((route) => route.path), ["/v1/health", "/v1/execute", "/v1/commands/"]);
assert.deepEqual(hooks.sort(), ["agent_end", "before_agent_run", "before_tool_call"]); assert.deepEqual(hooks.sort(), ["agent_end", "before_agent_run", "before_tool_call"]);
}); });
@@ -0,0 +1,132 @@
import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Readable } from "node:stream";
import { createRuntimeApi, __test } from "../runtime.js";
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "aios-runtime-"));
const keys = crypto.generateKeyPairSync("ed25519");
const publicKeyPath = path.join(root, "public.pem");
fs.writeFileSync(publicKeyPath, keys.publicKey.export({ format: "pem", type: "spki" }));
const api = createRuntimeApi({ pluginConfig: {
runtimeStatePath: path.join(root, "state.jsonl"),
runtimeSigningPublicKeyPath: publicKeyPath,
runtimeSigningKeyId: "runtime-key-1",
runtimeCredentialRef: "secretref:runtime-test",
runtimeMaxDelayMs: 3000,
} });
return { root, keys, api };
}
function signed(keys, overrides = {}) {
const command = {
command_id: `cmd-${crypto.randomUUID()}`,
tenant_id: "tenant-a",
task_id: "task-a",
trace_id: "trace-a",
action: "status",
payload: { side_effects: false },
idempotency_key: `idem-${crypto.randomUUID()}`,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 60_000).toISOString(),
contract_version: "aios.openclaw.command.v1",
key_id: "runtime-key-1",
...overrides,
};
command.signature = crypto.sign(null, Buffer.from(__test.canonical(command)), keys.privateKey).toString("base64url");
return command;
}
function request(method, url, body = null) {
const req = Readable.from(body === null ? [] : [Buffer.from(JSON.stringify(body))]);
req.method = method;
req.url = url;
req.headers = { "x-aios-credential-ref": "secretref:runtime-test" };
return req;
}
function response() {
let resolve;
const done = new Promise((value) => { resolve = value; });
return {
status: 0,
headers: {},
body: null,
done,
writeHead(status, headers) { this.status = status; this.headers = headers; },
end(data) { this.body = JSON.parse(Buffer.from(data).toString("utf8")); resolve(this); },
};
}
async function call(handler, req) {
const res = response();
await handler(req, res);
return res.done;
}
test("signed status executes, persists, and replays idempotently", async () => {
const { keys, api, root } = fixture();
const command = signed(keys);
const first = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.equal(first.status, 200);
assert.equal(first.body.state, "COMPLETED");
assert.equal(first.body.output.runtime, "openclaw-plugin");
const second = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.deepEqual(second.body, first.body);
assert.ok(fs.statSync(path.join(root, "state.jsonl")).size > 0);
});
test("tamper, expiry, and conflicting identity are rejected", async () => {
const { keys, api } = fixture();
const tampered = signed(keys);
tampered.tenant_id = "tenant-b";
assert.equal((await call(api.handleExecute, request("POST", "/v1/execute", tampered))).status, 401);
const expired = signed(keys, {
created_at: new Date(Date.now() - 700_000).toISOString(),
expires_at: new Date(Date.now() - 600_000).toISOString(),
});
assert.equal((await call(api.handleExecute, request("POST", "/v1/execute", expired))).status, 401);
});
test("status is tenant-scoped and unknown actions fail closed", async () => {
const { keys, api } = fixture();
const rejected = signed(keys, { action: "arbitrary.exec" });
const denied = await call(api.handleExecute, request("POST", "/v1/execute", rejected));
assert.equal(denied.status, 422);
assert.equal(denied.body.error, "unsupported_action");
const command = signed(keys);
await call(api.handleExecute, request("POST", "/v1/execute", command));
const wrong = await call(api.commandRequest, request("GET", `/v1/commands/${command.command_id}?tenant_id=tenant-b`));
assert.equal(wrong.status, 404);
const correct = await call(api.commandRequest, request("GET", `/v1/commands/${command.command_id}?tenant_id=tenant-a`));
assert.equal(correct.body.command_id, command.command_id);
});
test("active delay supports deterministic cancellation", async () => {
const { keys, api } = fixture();
const command = signed(keys, { action: "acceptance.delay", payload: { delay_ms: 2000 } });
const executeResponse = response();
const execution = api.handleExecute(request("POST", "/v1/execute", command), executeResponse);
await new Promise((resolve) => setTimeout(resolve, 30));
const cancelled = await call(api.commandRequest, request("POST", `/v1/commands/${command.command_id}/cancel?tenant_id=tenant-a`));
assert.equal(cancelled.body.accepted, true);
await execution;
const terminal = await executeResponse.done;
assert.equal(terminal.body.state, "CANCELLED");
});
test("signed deadline produces TIMED_OUT", async () => {
const { keys, api } = fixture();
const command = signed(keys, {
action: "acceptance.delay",
payload: { delay_ms: 1000 },
expires_at: new Date(Date.now() + 40).toISOString(),
});
const result = await call(api.handleExecute, request("POST", "/v1/execute", command));
assert.equal(result.body.state, "TIMED_OUT");
});
+58 -4
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
import os
import tempfile import tempfile
import unittest import unittest
from dataclasses import replace from dataclasses import replace
from datetime import datetime
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2] REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -28,6 +30,8 @@ from aios_v2 import (
TenantIsolationError, TenantIsolationError,
) )
from aios_v2.persistence import DurableControlPlaneService, SQLiteSnapshotRepository from aios_v2.persistence import DurableControlPlaneService, SQLiteSnapshotRepository
from aios_v2.gateway import GatewayApplication
from aios_v2.app import build_application
SIGNING_MATERIAL = bytes(range(32)) SIGNING_MATERIAL = bytes(range(32))
@@ -66,6 +70,47 @@ def bootstrap(service: ControlPlaneService, tenant_id: str = "tenant-a"):
class CandidateFlowTests(unittest.TestCase): class CandidateFlowTests(unittest.TestCase):
def test_build_application_propagates_openclaw_signing_key_id(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
material_path = Path(temp_dir) / "signing-material.bin"
material_path.write_bytes(SIGNING_MATERIAL)
values = {
"AIOS_V2_STATE_DIR": temp_dir,
"AIOS_OPENCLAW_URL": "http://127.0.0.1:18789",
"AIOS_OPENCLAW_SIGNING_MATERIAL_FILE": str(material_path),
"AIOS_OPENCLAW_SIGNING_KEY_ID": "openclaw-v2r1-candidate-ed25519",
"AIOS_OPENCLAW_CREDENTIAL_REF": "secretref:test",
}
previous = {key: os.environ.get(key) for key in values}
try:
os.environ.update(values)
application = build_application()
finally:
for key, value in previous.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
self.assertEqual(
application.orchestrator.openclaw_key_id,
"openclaw-v2r1-candidate-ed25519",
)
def test_http_identity_headers_are_case_insensitive(self) -> None:
request_context = GatewayApplication.context_from_headers(
{
"X-Aios-Authenticated": "true",
"x-aios-tenant-id": "tenant-a",
"X-AIOS-PRINCIPAL-ID": "operator-a",
"X-Aios-Roles": "operator,auditor",
"x-aios-trace-id": "trace-http-header-case",
}
)
self.assertEqual(request_context.tenant_id, "tenant-a")
self.assertEqual(request_context.principal_id, "operator-a")
self.assertEqual(request_context.trace_id, "trace-http-header-case")
self.assertEqual(request_context.roles, frozenset({"operator", "auditor"}))
def test_ed25519_command_signature_accepts_valid_and_rejects_tampering(self) -> None: def test_ed25519_command_signature_accepts_valid_and_rejects_tampering(self) -> None:
command = OpenClawCommand( command = OpenClawCommand(
command_id="cmd-signature-test", command_id="cmd-signature-test",
@@ -108,12 +153,16 @@ class CandidateFlowTests(unittest.TestCase):
expected_revision=task.revision, expected_revision=task.revision,
) )
transport = InProcessRuntimeTransport( captured_commands = []
lambda command: {
def execute(command):
captured_commands.append(command)
return {
"state": CommandState.COMPLETED.value, "state": CommandState.COMPLETED.value,
"output": {"echo": command.payload["message"]}, "output": {"echo": command.payload["message"]},
} }
)
transport = InProcessRuntimeTransport(execute)
resolver = lambda tenant_id, key_id: SIGNING_MATERIAL resolver = lambda tenant_id, key_id: SIGNING_MATERIAL
adapter = OpenClawAdapter(transport, resolver) adapter = OpenClawAdapter(transport, resolver)
orchestrator = ExecutionOrchestrator( orchestrator = ExecutionOrchestrator(
@@ -122,13 +171,18 @@ class CandidateFlowTests(unittest.TestCase):
openclaw_signing_material_resolver=resolver, openclaw_signing_material_resolver=resolver,
) )
dispatch = orchestrator.dispatch(operator, task_id=approved.task_id) dispatch = orchestrator.dispatch(
operator, task_id=approved.task_id, timeout_seconds=600
)
completed = service.get_task(operator, task.task_id) completed = service.get_task(operator, task.task_id)
self.assertEqual(dispatch.runtime, "openclaw") self.assertEqual(dispatch.runtime, "openclaw")
self.assertEqual(dispatch.state, CommandState.COMPLETED.value) self.assertEqual(dispatch.state, CommandState.COMPLETED.value)
self.assertEqual(completed.state, TaskState.COMPLETED) self.assertEqual(completed.state, TaskState.COMPLETED)
self.assertEqual(completed.result, {"echo": "hello"}) self.assertEqual(completed.result, {"echo": "hello"})
created = datetime.fromisoformat(captured_commands[0].created_at.replace("Z", "+00:00"))
expires = datetime.fromisoformat(captured_commands[0].expires_at.replace("Z", "+00:00"))
self.assertLessEqual((expires - created).total_seconds(), 300)
def test_hermes_full_flow_reconciles_worker_result(self) -> None: def test_hermes_full_flow_reconciles_worker_result(self) -> None:
service = ControlPlaneService() service = ControlPlaneService()