Compare commits

..
Author SHA1 Message Date
chatgpt-final-release-writer ffeaff69bc test(session-broker): replace channel smoke with shadow fixture 2026-07-26 01:35:16 +08:00
chatgpt-final-release-writer bc4d5c6a0a test(session): require task-bound adapter control auth 2026-07-26 01:34:23 +08:00
chatgpt-final-release-writer a62101340a security(codex): project adapter control credential 2026-07-26 01:34:22 +08:00
chatgpt-final-release-writer dc9157e679 security(openclaw): project adapter control credential 2026-07-26 01:34:21 +08:00
chatgpt-final-release-writer 6651753c7f feat(session-broker): bind native provider provisioning 2026-07-26 01:34:21 +08:00
chatgpt-final-release-writer 6494a666ec security(session): authenticate native adapter callers 2026-07-26 01:32:45 +08:00
chatgpt-final-release-writer 56ca465003 fix-gitops-validate-candidate-python-with-secret-scanner 2026-07-26 01:32:10 +08:00
chatgpt-final-release-writer 6a8f7c8668 test(session): keep native gates false until acceptance 2026-07-26 01:27:47 +08:00
chatgpt-final-release-writer 486e25d217 fix(codex): bind exact candidate credential reference 2026-07-26 01:27:46 +08:00
chatgpt-final-release-writer 88aa2f89d9 fix(openclaw): bind exact candidate credential reference 2026-07-26 01:27:46 +08:00
chatgpt-final-release-writer 8d9e8cba5d fix(session): defer gate truth until lifecycle acceptance 2026-07-26 01:27:45 +08:00
chatgpt-final-release-writer 12fca283c5 test(session-broker): verify native provider identity binding 2026-07-26 01:24:47 +08:00
chatgpt-final-release-writer 349f952be5 feat(codex): add isolated candidate app-server unit 2026-07-26 01:24:47 +08:00
chatgpt-final-release-writer 89bec6de6c feat(openclaw): add hardened candidate native adapter unit 2026-07-26 01:24:46 +08:00
chatgpt-final-release-writer 34d04a412a feat(codex): add candidate credential login projection 2026-07-26 01:24:45 +08:00
chatgpt-final-release-writer dbf0ccfdf7 feat(openclaw): add native task-bound session adapters 2026-07-26 01:24:44 +08:00
chatgpt-final-release-writer e08b5ee05f feat-gitops-add-exact-candidate-source-content-profile 2026-07-26 01:23:38 +08:00
chatgpt-final-release-writer 828ad998e6 docs(release): bind ChatGPT final release candidate 2026-07-26 01:09:57 +08:00
chatgpt-final-release-writer 1bc55343c8 Merge branch 'refs/heads/integration/openclaw-real-dispatch-session-broker-20260725' into patch/chatgpt-session-broker-final-release-20260726 2026-07-26 01:09:35 +08:00
chatgpt-final-release-writer 5fb2b08627 test: verify task-bound Stage A writer 2026-07-26 01:03:10 +08:00
AIOS Candidate Integration dcbcf93748 Merge Candidate caller authorization hardening 2026-07-26 00:24:55 +08:00
AIOS Candidate Integration 54e7dac80a security: authorize candidate owner signer callers 2026-07-26 00:23:48 +08:00
AIOS Candidate Integration 658ddbd3e9 Merge Candidate owner signer separation into dormant integration 2026-07-25 23:09:34 +08:00
AIOS Candidate Integration d31dd8a31b security: separate candidate owner approval signer 2026-07-25 23:07:22 +08:00
AIOS Candidate Integration f0a9b943ac merge: integrate session broker candidate security fixes
# Conflicts:
#	VERSION
#	versions/V2R1C1.json
2026-07-25 19:52:56 +08:00
chatgpt-gitops-bot e45a775af2 fix(candidate): harden broker approval and Gitea trust 2026-07-25 19:44:09 +08:00
AIOS Candidate Writer ebf33ba34a fix(task-plane): emit ISO session expiry 2026-07-25 18:49:00 +08:00
AIOS Candidate Writer 1c1aa1700b feat(gitops): add scoped Gitea grant lifecycle 2026-07-25 18:44:10 +08:00
AIOS Candidate Writer fd8bea6a23 feat(gitops): add revocable Gitea credential projection 2026-07-25 18:39:03 +08:00
AIOS Candidate Writer bd9cdd8daf feat(gitops): add revocable Gitea credential projection 2026-07-25 18:39:03 +08:00
AIOS Candidate Writer c48ea38711 feat(gitops): add revocable Gitea credential projection 2026-07-25 18:39:03 +08:00
AIOS Candidate Writer c4f2ac8903 test(candidate): verify test_candidate_runtime.py 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 47af809a8b feat(gitops): add candidate candidate_writer.py 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 77eb2aeda9 feat(task-plane): add candidate candidate_session_broker.py 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer d57772ad3e feat(task-plane): add candidate candidate_common.py 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 838a8ed69f feat(gitops): add candidate bootstrap_candidate_runtime.py 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 51b98041d9 feat(openclaw): add task-plane task-plane.js 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 4398caaedf feat(openclaw): add task-plane sandbox.js 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 0c0be7abc3 feat(openclaw): add task-plane mock.js 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer f96b712b26 test(candidate): verify dispatch.test.js 2026-07-25 18:35:21 +08:00
AIOS Candidate Writer 8f55eff11d feat(openclaw): add task-plane session-broker-client.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 56d675166a feat(openclaw): add task-plane package.json 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer d3b0307159 feat(openclaw): add task-plane openclaw.plugin.json 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 0e832bbe9a feat(openclaw): add task-plane index.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 04ab2db654 feat(openclaw): add task-plane task-plane.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer bc18ae4446 feat(openclaw): add task-plane sandbox.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer ea74af9751 feat(openclaw): add task-plane mock.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 57c3ce6e96 feat(openclaw): add task-plane session-broker-client.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 4b57a6268c feat(openclaw): add task-plane index.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 88dcebee78 feat(openclaw): add task-plane dispatch-errors.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 12a973c2cb feat(openclaw): add task-plane dispatch-contract.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 0babec654b feat(openclaw): add task-plane dispatch-audit.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer d787e98554 feat(openclaw): add task-plane dispatch-errors.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer c0450946e4 feat(openclaw): add task-plane dispatch-contract.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer b4c30e16e0 feat(openclaw): add task-plane dispatch-audit.js 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer f04473f657 docs(openclaw): document candidate session provisioning 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer dedef75f8c feat(gitops): add candidate aios-session-broker-bootstrap-writer-candidate.service 2026-07-25 18:35:20 +08:00
AIOS Candidate Writer 18c43e42a4 feat(gitops): add candidate aios-openclaw-session-broker-candidate.service 2026-07-25 18:35:20 +08:00
ChatGPT Release Governance 95b3728abd fix(wechat): pseudonymize owner provenance principal 2026-07-22 13:46:53 +08:00
ChatGPT Release Governance e1659069b1 fix(wechat): bind dispatch provider server-side 2026-07-22 13:34:40 +08:00
ChatGPT Release Governance 4b35eb52cb fix(wechat): bind protected dispatch provider schema 2026-07-22 13:21:47 +08:00
ChatGPT Release Governance 05835675b6 fix(wechat): enforce singleton Owner binding 2026-07-22 12:54:32 +08:00
ChatGPT Release Governance a1a3a021ef fix(wechat): bind protected Owner identity explicitly 2026-07-22 12:37:29 +08:00
ChatGPT Release Governance 40e21d3c07 fix(wechat): await inbound metadata before reply 2026-07-22 11:55:20 +08:00
ChatGPT Release Governance 46932c46ca fix(wechat): retry dispatch without re-recording inbound 2026-07-22 11:43:40 +08:00
ChatGPT Release Governance 9e0a40600b fix(wechat): retry transient reply session conflicts 2026-07-22 11:25:12 +08:00
ChatGPT Release Governance d54da54033 fix(wechat): propagate sender identity and isolate conversations 2026-07-22 10:40:56 +08:00
ChatGPT Release Governance 33152a7f52 fix(wechat): separate ordinary conversation from Owner provenance 2026-07-22 10:19:09 +08:00
ChatGPT Release Governance 3dd622b5a5 v2r1c2_guard_plan_oid_supersession 2026-07-22 04:24:27 +08:00
ChatGPT Release Governance f73052cd5c v2r1c2_audit_manifest_supersession 2026-07-22 04:19:55 +08:00
ChatGPT Release Governance 1b69e89718 v2r1c2_add_audited_not_started_retry 2026-07-22 04:17:53 +08:00
ChatGPT Release Governance f3c00d7294 v2r1c2_harden_provenance_caches 2026-07-22 03:53:22 +08:00
ChatGPT Release Governance 80e3f65cd6 v2r1c2_takeover_gate 2026-07-22 03:46:34 +08:00
ChatGPT Release Governance 0cf769ef67 fix(v2r1c2): auto-issue Owner provenance for protected dispatch 2026-07-22 03:33:41 +08:00
ChatGPT Release Governance 5e53203a61 feat(v2r1c2): add legal ChatGPT takeover provider 2026-07-22 03:25:49 +08:00
ChatGPT Release Governance cf5afdf4c0 docs(task): add concise OpenClaw online execution handoff 2026-07-22 02:46:26 +08:00
ChatGPT Release Governance b6e714e3fc docs(v2r1c2): add unified portal and portable executor contract 2026-07-22 02:36:31 +08:00
ChatGPT Release Governance 0022adf958 test(release): add V2R1C2 offline and online acceptance gate 2026-07-22 02:33:37 +08:00
ChatGPT Release Governance 22888c3d53 test(orchestration): cover portable executor release gates 2026-07-22 02:29:33 +08:00
ChatGPT Release Governance c2c9eb8d81 fix(orchestration): retry failed owner notifications idempotently 2026-07-22 02:26:12 +08:00
ChatGPT Release Governance 6df9c92020 fix(web): repair execution center script and truth projection 2026-07-22 02:23:56 +08:00
ChatGPT Release Governance 6bb8c530a4 feat(web): render portable executor execution center 2026-07-22 02:18:31 +08:00
ChatGPT Release Governance 024143efa5 feat(deploy): add V2R1C2 candidate environment template 2026-07-22 02:10:29 +08:00
ChatGPT Release Governance 06c6374126 feat(deploy): add isolated V2R1C2 portal candidate service 2026-07-22 02:07:44 +08:00
ChatGPT Release Governance 82177403f6 feat(orchestration): add portable execution plan lifecycle CLI 2026-07-22 02:04:52 +08:00
ChatGPT Release Governance ef074a0c97 feat(release): add V2R1C2 portable multi-provider execution plan 2026-07-22 02:00:00 +08:00
ChatGPT Release Governance 00fd375e4c feat(web): wire portable executor plans into V2R1C2 candidate 2026-07-22 01:54:45 +08:00
ChatGPT Release Governance 5b0c3eb404 feat(web): expose provider-neutral execution plan APIs 2026-07-22 01:51:12 +08:00
ChatGPT Release Governance 3161470bd2 fix(orchestration): activate dependent stages only after upstream gates pass 2026-07-22 01:46:28 +08:00
ChatGPT Release Governance 1852701236 feat(web): add execution-center capability and navigation 2026-07-22 01:39:46 +08:00
ChatGPT Release Governance 21fc8c60ed refactor(orchestration): authorize task API from dynamic Provider trust 2026-07-22 01:33:56 +08:00
ChatGPT Release Governance 77395fa9a0 feat(orchestration): add official portable executor providers 2026-07-22 01:29:46 +08:00
ChatGPT Release Governance 490ed4f65a feat(orchestration): add provider-neutral executor worker runtime 2026-07-22 01:26:54 +08:00
ChatGPT Release Governance d6d2407a0d feat(orchestration): add portable multi-provider execution plans 2026-07-22 01:22:48 +08:00
ChatGPT Release Governance db3fcb89a8 feat(orchestration): persist and migrate executor provider catalogs 2026-07-22 01:18:40 +08:00
ChatGPT Release Governance 20b7a41104 refactor(orchestration): make executor protocol provider-based and portable 2026-07-22 01:10:32 +08:00
ChatGPT Release Governance e739e67052 feat(orchestration): unify OpenClaw Hermes and Codex executor protocol 2026-07-22 01:05:56 +08:00
ChatGPT Release Governance 1e6c7b9ad8 feat(orchestration): add protected external task lifecycle API 2026-07-22 00:59:25 +08:00
ChatGPT Release Governance 1afbb11d3b feat(orchestration): add OpenClaw-compatible external task CLI 2026-07-22 00:54:49 +08:00
ChatGPT Release Governance 850beec8d7 feat(orchestration): add external task discovery read model 2026-07-22 00:49:45 +08:00
ChatGPT Release Governance fb11c71ec0 feat(orchestration): require owner notification receipts in external task gate 2026-07-22 00:42:50 +08:00
ChatGPT Release Governance 8acdd104bb feat(orchestration): add durable external task truth registry 2026-07-22 00:38:48 +08:00
ChatGPT Release Governance 43d77e520f feat(web): add tenant and Agent resource center to candidate portal 2026-07-22 00:34:16 +08:00
ChatGPT Release Governance a3f731d01c fix(web): bridge portal-only roles to domain read policy 2026-07-22 00:13:45 +08:00
ChatGPT Release Governance 8dba5b8c2e fix(web): provide unified portal HTML package boundary 2026-07-22 00:06:22 +08:00
ChatGPT Release Governance 74158908e8 feat(web): add V2R1C2 portal release entrypoint and resource aggregation 2026-07-22 00:04:12 +08:00
ChatGPT Release Governance ab36e36d5d feat(web): expose unified portal package 2026-07-22 00:00:17 +08:00
ChatGPT Release Governance c1b14753a3 feat(web): add tenant and Agent resource capabilities 2026-07-21 23:54:08 +08:00
ChatGPT Release Governance c62c8ee9f6 feat(web): replace read-only console with unified role-aware portal 2026-07-21 23:49:52 +08:00
ChatGPT Release Governance 9626e66e5b feat(web): add unified portal aggregation and protected action API 2026-07-21 23:44:06 +08:00
ChatGPT Release Governance b68e3c85a3 feat(web): add server-side role capability matrix 2026-07-21 23:39:20 +08:00
ChatGPT Release Governance 74014514da feat(web): create unified role-aware portal branch from V2R1C1 2026-07-21 23:17:54 +08:00
Codex 4e0e6dcb90 chore(release): close V2R1C1 companion validation metadata 2026-07-21 19:05:28 +08:00
Codex 0778a31ec0 chore(release): record OpenClaw v2 companion validation 2026-07-21 18:29:45 +08:00
Codex 80c4cbde03 release:prepare-V2R1C1-validation-metadata 2026-07-21 17:04:24 +08:00
Codex fb739f6e8f docs(hermes): preserve formal acceptance blocker 2026-07-21 13:59:01 +08:00
Codex e6da39a3cd docs(hermes): record final evidence and worktree blocker 2026-07-21 13:55:08 +08:00
Codex 95d2e52cb5 fix(hermes): reserve cancellation for control plane 2026-07-21 13:41:34 +08:00
Codex 283dec58ab fix(hermes): enforce minimum heartbeat lease duration 2026-07-21 13:21:09 +08:00
Codex 7de1fe80ac docs(hermes): retain key-id and rollback contracts 2026-07-21 13:19:21 +08:00
Codex 89a502b39b test(hermes): preserve validated ten-case release gate 2026-07-21 13:19:21 +08:00
Codex 61bec280e2 deploy(hermes): preserve bounded rollback readiness 2026-07-21 13:19:21 +08:00
Codex 1791c91c5b docs(hermes): define lifecycle contract and validation state 2026-07-21 13:13:15 +08:00
Codex e8c0f0250b test(hermes): cover live lifecycle and recovery matrix 2026-07-21 13:09:06 +08:00
Codex 94718308fc fix(hermes): normalize trusted runtime headers and conflicts 2026-07-21 13:09:06 +08:00
Codex a4cbed0f5f feat(hermes): persist and protect full runtime lifecycle 2026-07-21 13:09:06 +08:00
Codex 25c7740739 docs(hardening): refresh Gate A report with verified execution results 2026-07-21 12:39:53 +08:00
Codex 8bbb049c39 docs(hardening): index ADR-009 without inventing missing history 2026-07-21 12:36:22 +08:00
Codex 1707cc4953 docs(hardening): rebuild complete UR-001 to UR-022 traceability 2026-07-21 12:34:50 +08:00
Codex e397594200 docs(hardening): add Run Provenance and scoped Grant requirements 2026-07-21 12:31:58 +08:00
Codex 68b7f2c1d2 docs(hardening): align module contracts with hardened runtime and Grant model 2026-07-21 12:28:25 +08:00
Codex fda98e61f5 docs(hardening): record least-privilege Provenance decision 2026-07-21 12:16:22 +08:00
Codex ea9f599522 fix(hardening): scope CLI authorization to one Grant 2026-07-21 12:13:21 +08:00
Codex 1aabcd8b62 test(hardening): add least-privilege Grant isolation coverage 2026-07-21 12:04:02 +08:00
Codex 4d68af9524 test(hardening): upgrade runtime flows and add dispatch recovery coverage 2026-07-21 12:01:17 +08:00
Codex 467b127804 fix(hardening): give application a safe default Grant scope 2026-07-21 11:57:32 +08:00
Codex 50a73b211b fix(hardening): bind dispatch and runtime results to principal 2026-07-21 11:55:07 +08:00
Codex f14d4b6e14 feat(hardening): persist principal and runtime lookup in dispatch store 2026-07-21 11:51:01 +08:00
Codex fc8bd8c8d4 feat(hardening): build scoped Grant writers per service 2026-07-21 11:48:11 +08:00
Codex 382fe662a3 feat(hardening): add service-scoped least-privilege writer 2026-07-21 11:45:46 +08:00
Codex 4754e5762a fix(hardening): use constant-time fingerprint comparison 2026-07-21 11:42:08 +08:00
Codex ce7d35a811 config(hardening): add seven least-privilege Grant references 2026-07-21 11:37:58 +08:00
Codex 60ac7d9d0b refactor(hardening): make evidence tracking use least-privilege authorization interface 2026-07-21 11:34:43 +08:00
Codex 2a10d1cdd9 feat(hardening): add workflow and least-privilege Grant CLI 2026-07-21 11:31:51 +08:00
Codex 677ea738e7 fix(hardening): authorize Git content before modifying index 2026-07-21 11:27:28 +08:00
Codex 7154d62cca fix(hardening): export OpenClaw Ed25519 contract types 2026-07-21 11:24:35 +08:00
Codex 4e154c8baf feat(hardening): wire durable dispatch least-privilege grants and internal reconcile 2026-07-21 11:20:39 +08:00
Codex b848361079 feat(hardening): require least-privilege grants in production writer 2026-07-21 11:17:39 +08:00
Codex 74224cfbd5 feat(hardening): add least-privilege provenance child grants 2026-07-21 11:14:42 +08:00
Codex a7c09ba646 feat(hardening): upgrade OpenClaw command contract to Ed25519 v2 2026-07-21 11:10:21 +08:00
Codex f055cb74bb feat(hardening): persist dispatches and strictly bind runtime results 2026-07-21 11:04:59 +08:00
Codex 071a378de6 fix(hardening): close public runtime lifecycle bypass 2026-07-21 11:00:28 +08:00
Codex a492b15df6 feat(hardening): add durable dispatch repository 2026-07-21 10:53:32 +08:00
Codex a9f0a4359b patch(chatgpt): start full Gate A hardening 2026-07-21 10:46:59 +08:00
Codex 6a9fab894a docs(traceability): add run provenance implementation matrix 2026-07-21 10:30:54 +08:00
Codex e9111dea47 deploy(provenance): add mandatory operation authorization wrapper 2026-07-21 10:30:40 +08:00
Codex d25fbfa61e test(provenance): add signature lifecycle replay and seven-path gate coverage 2026-07-21 10:21:01 +08:00
Codex 6b03c47625 build(provenance): install Ed25519 runtime dependency 2026-07-21 10:16:28 +08:00
Codex 770fe1ef66 docs(aios-v2): add ADR-007 run provenance control 2026-07-21 10:12:03 +08:00
Codex 7a025430f9 config(provenance): add production environment references 2026-07-21 10:11:56 +08:00
Codex d079da4029 feat(provenance): export trusted provenance APIs 2026-07-21 10:11:49 +08:00
Codex b8f7dc4112 feat(provenance): wire production gate into API application 2026-07-21 10:07:19 +08:00
Codex 947cdaff42 feat(provenance): add issuer authorization revoke and status CLI 2026-07-21 10:00:02 +08:00
Codex 83b0c8bd74 feat(provenance): add deployment and rollback operation gate factory 2026-07-21 09:56:50 +08:00
Codex fbd880cf8c feat(provenance): add controlled Git writer with operation policy 2026-07-21 09:46:40 +08:00
Codex 819ac94c9f feat(provenance): enforce authorization before evidence writes 2026-07-21 09:42:25 +08:00
Codex b02401e60d feat(provenance): enforce signed provenance before runtime dispatch 2026-07-21 09:39:33 +08:00
Codex cbb2e232f5 feat(provenance): implement Ed25519 issuer persistent store and seven-path writer 2026-07-21 09:33:07 +08:00
Codex 9e46ac0504 patch(codex): start trusted run provenance issuer store and writer blocker resolution 2026-07-20 20:11:59 +08:00
Codex ba7669c3b4 docs(aios-v2): 更新监督状态与阻塞处置清单 2026-07-20 17:22:32 +08:00
Codex 634f6b164f docs(candidate): 新增完整中文生产候选报告 2026-07-20 12:06:55 +08:00
Codex c9ce12d102 deploy(candidate): 新增中文全量验收入口 2026-07-20 12:05:38 +08:00
Codex db50d2fa5a tasks(aios-v2): 完整中文化 Codex 生产监督任务 2026-07-20 12:04:43 +08:00
Codex 63fd23a464 tasks(aios-v2): 完整中文化 OpenClaw 生产部署任务 2026-07-20 12:04:11 +08:00
Codex 2e03ed74f6 feat(governance): 新增文档与代码匹配门禁 2026-07-20 12:03:36 +08:00
Codex b86f51c9ab docs(aios-v2): 新增 ADR-006 候选持久化策略 2026-07-20 12:01:49 +08:00
Codex 09448a5d3d docs(aios-v2): 完整中文化 ADR-005 2026-07-20 12:01:05 +08:00
Codex 18c719b2c6 docs(aios-v2): 完整中文化 ADR-004 2026-07-20 12:00:39 +08:00
Codex b0a78fa9a8 docs(aios-v2): 完整中文化 ADR-003 2026-07-20 12:00:07 +08:00
Codex 0aca7dbcfc docs(aios-v2): 完整中文化 ADR-002 2026-07-20 11:59:34 +08:00
Codex 3f84d8991e docs(aios-v2): 完整中文化 ADR-001 2026-07-20 11:59:10 +08:00
Codex 5ac8f3ea6f docs(aios-v2): 完整中文化 ADR 索引 2026-07-20 11:58:39 +08:00
Codex bfb1c2fd52 docs(aios-v2): 新增完整中文文档治理规范 2026-07-20 11:57:51 +08:00
Codex cbfe1211c1 docs(aios-v2): 将文档索引完整转换为中文 2026-07-20 11:56:30 +08:00
Codex efd99e620c docs(aios-v2): add ADR-004 tenant isolation decision 2026-07-20 11:53:04 +08:00
Codex f23b694ad3 tasks(aios-v2): add Codex production supervisor task 2026-07-20 11:46:43 +08:00
Codex f97dea55ac tasks(aios-v2): add OpenClaw production deployment task 2026-07-20 11:46:39 +08:00
Codex efd08d7771 docs(aios-v2): add ADR-003 GitOps-only flow 2026-07-20 11:46:31 +08:00
Codex ff6cb2e217 docs(aios-v2): add ADR-005 documentation sync gate 2026-07-20 11:45:16 +08:00
Codex d774620fea docs(aios-v2): add ADR-002 runtime decoupling decision 2026-07-20 11:45:12 +08:00
Codex b26ff010fc docs(aios-v2): add ADR-001 control plane decision 2026-07-20 11:45:09 +08:00
Codex 4b49f2ba4b docs(aios-v2): add ADR index 2026-07-20 11:45:04 +08:00
Codex b4b8fe8828 docs(aios-v2): add traceability matrix 2026-07-20 11:43:46 +08:00
Codex 4e8ab051a0 docs(aios-v2): add module contracts 2026-07-20 11:41:39 +08:00
Codex f61e35b218 docs(aios-v2): add migration and delivery strategy 2026-07-20 11:34:33 +08:00
Codex b2f90f24a3 docs(aios-v2): add target architecture 2026-07-20 11:32:16 +08:00
Codex 6721eb4752 docs(aios-v2): add user requirements baseline 2026-07-20 11:29:07 +08:00
Codex a9e4fd1e40 docs(aios-v2): establish architecture source of truth 2026-07-20 11:24:12 +08:00
Codex 621750976a deploy(candidate): add guarded release rollback 2026-07-20 11:22:04 +08:00
Codex 5359e88806 feat(candidate): expose sanitized GitOps status API 2026-07-20 11:19:37 +08:00
Codex 76636931ba feat(candidate): add sanitized GitOps status provider 2026-07-20 11:17:18 +08:00
Codex 180c1fc66e deploy(candidate): add end-to-end healthcheck 2026-07-20 11:14:53 +08:00
Codex c0ff052dab deploy(candidate): add Docker Compose runtime 2026-07-20 11:12:42 +08:00
Codex 94cffb6ba3 deploy(candidate): add hardened container image 2026-07-20 11:10:36 +08:00
Codex 9ad5628e69 deploy(candidate): add environment configuration template 2026-07-20 11:08:37 +08:00
Codex 6d397a7894 deploy(candidate): add hardened systemd service 2026-07-20 11:07:06 +08:00
Codex 165fea6ba9 fix(candidate): prevent Hermes completion heartbeat revision race 2026-07-20 11:05:45 +08:00
Codex d079e2087c test(candidate): add end-to-end runtime and persistence tests 2026-07-20 11:01:07 +08:00
Codex e3b36ebdcc feat(candidate): add production application entrypoint 2026-07-20 10:57:42 +08:00
Codex 5e791b11c0 feat(candidate): add tenant-safe web console 2026-07-20 10:54:43 +08:00
Codex 50be65b76e feat(candidate): add durable SQLite control-plane persistence 2026-07-20 10:50:31 +08:00
Codex 7bb86e30bf feat(candidate): export orchestration and execution tracking 2026-07-20 10:42:49 +08:00
Codex ea56b45b7e feat(candidate): add persistent agent execution tracking 2026-07-20 10:40:22 +08:00
Codex f53682da7a feat(candidate): add unified AIOS execution orchestrator 2026-07-20 10:38:06 +08:00
Codex 71884ac088 feat(candidate): add Hermes worker loop and lease heartbeat 2026-07-20 10:35:55 +08:00
Codex 969c278cb0 feat(candidate): implement Hermes queue and lease adapter 2026-07-20 10:34:25 +08:00
Codex 6fe2a1e036 feat(candidate): expose Hermes runtime package 2026-07-20 10:31:07 +08:00
Codex 9a60e0c68e feat(candidate): add Hermes runtime contracts 2026-07-20 10:27:47 +08:00
Codex 46f553ca11 feat(candidate): expose Hermes runtime boundary package 2026-07-20 10:17:02 +08:00
Codex 4f0a0c2184 feat(candidate): add OpenClaw audit callback sink 2026-07-20 10:07:54 +08:00
Codex c107b193d9 feat(candidate): add OpenClaw runtime health policy 2026-07-20 10:06:36 +08:00
Codex 2db7ced7a4 feat(candidate): add OpenClaw runtime transports 2026-07-20 10:01:59 +08:00
Codex 6361d9a334 feat(candidate): implement OpenClaw execution adapter 2026-07-20 09:59:53 +08:00
Codex 2cf54f04ef feat(candidate): expose OpenClaw adapter package 2026-07-20 09:56:06 +08:00
Codex f840ce5c94 feat(integration): add OpenClaw command contract 2026-07-20 09:49:15 +08:00
Codex 35e7b55490 integration(aios-v2): create CTO production candidate branch 2026-07-20 09:47:01 +08:00
Codex 1f8d1f23b3 feat(aios-v2): add tenant-aware HTTP control plane gateway 2026-07-20 02:24:19 +08:00
Codex e93011791e feat(aios-v2): add tenant-isolated control plane domain 2026-07-20 02:19:16 +08:00
Codex 4460564a48 feat(aios-v2): initialize control plane package 2026-07-20 01:11:11 +08:00
Codex 4d6658136e docs(task): add AIOS control plane gateway task contract 2026-07-20 01:06:11 +08:00
Codex 5767e665c2 task(codex): implement AIOS V2 control plane gateway v1 2026-07-20 01:06:04 +08:00
318 changed files with 16852 additions and 29256 deletions
@@ -0,0 +1,3 @@
d23bb1ec24a7f49359314e04cd2c04c8f38876e45f25a701506499d1b34f270f MANIFEST-AIOS-V2R1-HERMES-LIFECYCLE-20260721.json
5ce2894fdf8a22622954a1a10fb09fbab74af04f0260b249615c70236ff31731 RESULT-AIOS-V2R1-HERMES-LIFECYCLE-20260721.md
ca2ab3c22c2b2b33bbc03e59f1206b999ad1271de3e1938fd3a0b4234c613eef ../../../docs/aios-v2/AIOS-V2R1-HARDENING-GATE-A-REPORT.md
@@ -0,0 +1,44 @@
{
"schema_version": "aios.evidence.manifest.v1",
"generated_at": "2026-07-21T05:50:00Z",
"branch": "patch/chatgpt/AIOS-V2R1-HERMES-LIFECYCLE-20260721",
"validated_oid": "95d2e52cb55c7b913e89bc6bed2bcdacf67512d4",
"evidence_root": "/var/lib/aios-gitops/acceptance/aios-v2r1-hermes-lifecycle-20260721T054205Z",
"evidence_hashes": {
"steps.tsv": "b2ad20585ba7f576e85657827a0b1db6f252887bbd73bcd718df38623e9d95cf",
"final-live-gate.log": "82040c674759dacf587658c5e936c313a1f50e8e2425be48ad891927befdf4fa",
"16R.log": "641246927a372ebaa9e6dde8b018b88275b2ba15a442473759b23fb1f215d9cf",
"15.log": "b0a474e2a74164f560c4f7572091911f32011c75faccd37a3c7315046275011e",
"13.log": "b9f637cca6e97ed893bf38530e05cd33b78e324c5e8865d4c602dfeac22a7710",
"12R2.log": "0c431f5f5f1c1b17acda160c917b08c7f4036485016c23e1236b9b6f4a78de67"
},
"validation": {
"candidate_tests": {"passed": 49, "failed": 0},
"hermes_lifecycle": {"passed": 17, "failed": 0},
"gate_b": {"passed": 10, "failed": 0},
"offline_acceptance": {"passed": 8, "failed": 0, "expected_warnings": 1},
"openclaw_real_http": "PASS",
"restart_recovery": "PASS",
"rollback_forward": "PASS",
"state_preservation": "PASS",
"final_health_corrected_16R": "PASS"
},
"git_gate": {
"oid_match": true,
"candidate_clean": true,
"active_controller_clean": true,
"locked": 0,
"prunable": 0,
"worktree_total": 88,
"worktree_dirty": 10,
"global_worktree_clean": false
},
"resolved_blockers": [],
"active_blockers": [
"BLK-LIVE-HERMES-FULL-LIFECYCLE-EVIDENCE-MISSING",
"BLK-GLOBAL-WORKTREE-CLEANLINESS"
],
"overall": "PARTIAL / BLOCKED_GLOBAL_WORKTREE_CLEANLINESS",
"release_decision": "STOP_PREPRODUCTION",
"secrets_recorded": false
}
@@ -0,0 +1,16 @@
# AIOS V2R1 Hermes Lifecycle Result
- Branch: `patch/chatgpt/AIOS-V2R1-HERMES-LIFECYCLE-20260721`
- Validated OID: `95d2e52cb55c7b913e89bc6bed2bcdacf67512d4`
- Evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1-hermes-lifecycle-20260721T054205Z`
- Hermes blocker: `EVIDENCE_COMPLETE / FORMAL_CLOSURE_BLOCKED`
- Overall: `PARTIAL / BLOCKED_GLOBAL_WORKTREE_CLEANLINESS`
- Release decision: `STOP_PREPRODUCTION`
Hermes lifecycle `17/17`, candidate `49/49`, Gate B `10/10`, offline acceptance `8/0/1`, OpenClaw real HTTP, restart recovery, rollback/forward, state preservation and corrected final health all passed at the validated OID. The Owner acceptance contract nevertheless forbids formally closing the Hermes blocker until every global gate also passes.
The candidate worktree and active Controller worktree are clean; local, worktree and Gitea OIDs match; locked/prunable are zero. The global authority currently has 10 dirty worktrees out of 88, so the Owner hard gate requiring every worktree to be clean fails.
No RC, Tag Approval Request, Tag, main mutation, force push, production traffic switch or secret disclosure was performed.
See `MANIFEST-AIOS-V2R1-HERMES-LIFECYCLE-20260721.json` and `docs/aios-v2/AIOS-V2R1-HARDENING-GATE-A-REPORT.md`.
@@ -0,0 +1,12 @@
# V2R1C1 Cleanup Closure
- `CLEANUP_REVIEW=PASS`
- `ARCHIVE_VALIDATION=PASS`
- `GLOBAL_WORKTREE_CLEAN=true`
- `global_dirty_count=0`
- `locked=0`
- `prunable=0`
- Portal Archive OID: `99671cc14d2631f989e0763dc882e4001c50a1ea`
- Spark Archive OID: `d97cd3642528bd3373fe4cf5161a5d99c0b9285f`
Closure evidence: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-blocker-clear-20260721T075647Z`.
@@ -0,0 +1,57 @@
{
"schema_version": "aios.v2r1c1.deployment-manifest.v1",
"vrc": "V2R1C1",
"release_branch": "release/AIOS-V2R1C1-20260721",
"old_release_oid": "80c4cbde03a82c30648a09b817bc2f03d5402936",
"validated_release_parent_oid": "0778a31ec073e7c90003ef091f1d093c16bf22e5",
"product_baseline_oid": "fb739f6e8ff9c031d319221ace68dce157694086",
"companion": {
"branch": "patch/openclaw/AIOS-V2R1C1-COMMAND-V2-COMPANION-20260721",
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
"plugin_version": "2.1.0-rc.4",
"contract_version": "aios.openclaw.command.v2",
"signature_algorithm": "Ed25519",
"verification_key_id": "openclaw-v2r1-candidate-ed25519",
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671"
},
"isolated_validation": {
"service": "aios-v2r1c1-openclaw-companion.service",
"account": "aios-v2r1c1-openclaw",
"listen": "127.0.0.1:28789",
"accept_v2": true,
"accept_v1": false,
"public_key_only": true,
"production_traffic_switched": false,
"result": "PASS"
},
"previous_plugin": {
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"plugin_version": "2.1.0-rc.3",
"artifact_sha256": "d0fd11be4aec03eee9ca24bf1585d9ef3597254ad4b1db0d0137eab4456b5737"
},
"rollback": {
"production_plugin_unchanged": true,
"failed_isolated_attempts_rolled_back": true,
"previous_plugin_evidence": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z/00-baseline",
"previous_oid": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a",
"isolated_rollback_forward": "PASS",
"production_symlink_modified": false
},
"release_validation": {
"gate_a": "PASS",
"gate_b": "PASS",
"openclaw_v2_live": "PASS",
"openclaw_negative_live": "PASS",
"hermes_live": "PASS",
"restart_recovery": "PASS",
"rollback_forward": "PASS",
"final_health": "PASS",
"secret_violations": 0,
"global_dirty_count": 0,
"sync_status": "fully_synced"
},
"tag_status": "ABSENT",
"preproduction_status": "NOT_STARTED",
"production_status": "NOT_STARTED"
}
@@ -0,0 +1,55 @@
{
"schema_version": "aios.v2r1c1.release-evidence.v2",
"vrc": "V2R1C1",
"semver": "2.1.0-rc.1",
"product_baseline_oid": "fb739f6e8ff9c031d319221ace68dce157694086",
"old_release_oid": "80c4cbde03a82c30648a09b817bc2f03d5402936",
"validated_release_parent_oid": "0778a31ec073e7c90003ef091f1d093c16bf22e5",
"release_branch": "release/AIOS-V2R1C1-20260721",
"release_evidence_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-release-20260721",
"companion_evidence_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z",
"openclaw_companion": {
"branch": "patch/openclaw/AIOS-V2R1C1-COMMAND-V2-COMPANION-20260721",
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"companion_oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
"plugin_version": "2.1.0-rc.4",
"contract_version": "aios.openclaw.command.v2",
"signature_algorithm": "Ed25519",
"verification_key_id": "openclaw-v2r1-candidate-ed25519",
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671"
},
"step13": {
"original_result": "FAIL_HTTP_401",
"original_diagnosis_manifest_sha256": "320017cbfc0f31392b4d0fb5996d0e32709982a967d102592ac45efdbdaad91f",
"remediated_result": "PASS",
"online_negative_evidence_sha256": "57ab8efd19d3bf659a65cbeda830785f6cea9172925a3cac9e027b9b842e644f",
"remediated_evidence_sha256": "5db59096c0586fcc0a51851d9a25d0a0b94f2bade45063fab16cc2a729f23130",
"task_state": "COMPLETED",
"dispatch_state": "COMPLETED",
"task_dispatch_audit_consistency": "PASS"
},
"release_validation": {
"static_revalidation": "PASS",
"static_evidence_sha256": "c33ffaf9a0f509d6e78bdfdc0848940c166b8a800940e259b7196adab04ec604",
"gate_a": "PASS",
"gate_b": "PASS",
"documentation_sync": "PASS",
"secret_scan_violations": 0,
"release_metadata": "PASS",
"hermes_live": "PASS",
"hermes_evidence_sha256": "e6553c99db55c3cdf9e16a4c1e6712064e00c41dc9965d0d179b6b1a2a1ad3ac",
"restart_recovery": "PASS",
"restart_evidence_sha256": "6031c92cd890dffd1de28ba75d81bed908e7da11a24aa129bcfde8efb23f0149",
"rollback_forward": "PASS",
"rollback_evidence_sha256": "f3d49c54e3f722facf68df2da3d82a9c82eb576f2c7c8dacb0f62c424d25737c",
"final_health": "PASS",
"final_health_evidence_sha256": "121cd52c0a4852b818e06be69211ae18b9fcccf976fa064e2a09c54eb8a01300",
"global_dirty_count": 0,
"sync_status": "fully_synced"
},
"validation_status": "PASS",
"tag_status": "ABSENT",
"preproduction_status": "NOT_STARTED",
"production_status": "NOT_STARTED",
"secrets_recorded": false
}
@@ -0,0 +1,52 @@
# V2R1C1 Validation Result
Status: `RELEASE_VALIDATION_PASS / FINAL_METADATA_COMMIT_REVALIDATION_REQUIRED`
Release decision: `STOP_PREPRODUCTION`
## OpenClaw Command v2 Companion
- Source OID: `83f8f878c7da5dd8263f2af2efa80750492e5cb3`
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`
- Plugin version: `2.1.0-rc.4`
- Contract: `aios.openclaw.command.v2`
- Signature: `Ed25519`
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`
- Companion unit/contract tests: `PASS`
- V1 fallback: `REJECTED`
- Isolated Gateway health: `PASS`
- Online positive/negative contract: `PASS`
- AIOS -> OpenClaw real HTTP: `PASS`
- Task state: `COMPLETED`
- Dispatch state: `COMPLETED`
- Task/Dispatch/Audit consistency: `PASS`
## Evidence linkage
- Original failed Release OID: `80c4cbde03a82c30648a09b817bc2f03d5402936`
- Original HTTP 401 diagnosis: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-release-20260721/pass1-80c4cbde/13-diagnosis`
- Original manifest SHA256: `320017cbfc0f31392b4d0fb5996d0e32709982a967d102592ac45efdbdaad91f`
- Companion evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`
- Validated Release parent OID: `0778a31ec073e7c90003ef091f1d093c16bf22e5`
- Online negative evidence SHA256: `57ab8efd19d3bf659a65cbeda830785f6cea9172925a3cac9e027b9b842e644f`
- Remediated Step 13 evidence SHA256: `5db59096c0586fcc0a51851d9a25d0a0b94f2bade45063fab16cc2a729f23130`
## Release validation closure
- Gate A: `PASS`
- Gate B: `PASS`
- Documentation sync: `PASS`
- Secret violations: `0`
- Release Metadata: `PASS`
- Hermes live: `PASS`
- Restart recovery: `PASS`
- Previous ↔ V2R1C1 isolated rollback/forward: `PASS`
- Final health: `PASS`
- Global dirty / locked / prunable: `0 / 0 / 0`
- OID sync: `fully_synced`
- Production traffic switched: `false`
- Tag: `ABSENT`
## Remaining mandatory validation
本次最终元数据闭环提交将形成新的 Release OID。必须针对该最终 OID 再次复验 Gate A、Gate B、文档同步、Secret Scan、Release Metadata 和 OpenClaw 第 13 步正向/负向验证。复验通过前不得创建 Tag 或部署预生产/生产。
-12
View File
@@ -1,12 +0,0 @@
title = "AIOS secret scan policy"
[extend]
useDefault = true
[[allowlists]]
description = "Known non-secret idempotency key used twice by the shadow RBAC test"
targetRules = ["generic-api-key"]
condition = "AND"
regexTarget = "line"
paths = ['''tests/task_plane/public_interface/test_shadow_client_rbac\.py$''']
regexes = ['''idempotency_key="shadow-rbac-idempotent-v1"''']
-2
View File
@@ -1,2 +0,0 @@
# Exact historical fingerprint for the removed test-only scanner self-reference.
a42f16d4f73b11fd23448da2711d49d5e9f49d9e:tests/fastlane/test_fastlane_assets_contract.py:generic-api-key:135
+394
View File
@@ -0,0 +1,394 @@
# 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 决策。
+8
View File
@@ -0,0 +1,8 @@
# Changelog
## V2R1C1 - 2.1.0-rc.1
- 固定发布基线为 `fb739f6e8ff9c031d319221ace68dce157694086`。
- 收口 Hermes 生命周期、Grant 隔离、Dispatch 恢复、运行证据和回滚/前滚验证。
- Portal Adapter 与 Spark Provisioning 仅保留在 Archive,不属于产品发布图。
- Future Incubator 保持隔离,不属于 V2R1C1 发布输入。
+1 -1
View File
@@ -1 +1 @@
V2R1C4
V2R1C2
@@ -1,379 +0,0 @@
#!/usr/bin/env python3
"""Task-scoped continuous authorization controller for the AIOS Candidate writer."""
from __future__ import annotations
import base64, fcntl, hashlib, json, os, pwd, grp, re, socket, sqlite3, stat, subprocess, sys, tempfile, time, urllib.error, urllib.parse, urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any
TASK='AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726'
AUTH_ID='authz-aios-autonomous-release-20260726'
BRANCH='patch/chatgpt-autonomous-completion-contract-20260726'
BASE='875d41c3101093149414292067fd700dc214d4d3'
REPO='aios/aios'
WRITER_AUDIENCE='chatgpt-autonomous-release-writer'
ACTOR='chatgpt-gitops-bot'
WRITER_INSTANCE='writer-6e32f321-1240-46f8-9693-0b0631d9c77b'
CHILD_OPERATION_MAX_USES=20
ROOT=Path('/data/aios-gitops')
WORKTREE=ROOT/'worktrees/chatgpt-autonomous-completion-contract-20260726'
BARE=ROOT/'repo.git'
TASK_ROOT=ROOT/'runtime/tasks'/TASK
AUTH_DIR=Path('/etc/aios-gitops/task-authorizations')
PARENT=AUTH_DIR/'authz-aios-autonomous-release-20260726.json'
STATE_DIR=Path('/var/lib/aios-autonomous-release-authorization')
STATE=STATE_DIR/'state.json'
LOCK=STATE_DIR/'controller.lock'
AUDIT=TASK_ROOT/'continuous-authorization.audit.jsonl'
SOCKET_DIR=Path('/run/aios-autonomous-release-authorization')
SOCKET_PATH=SOCKET_DIR/'controller.sock'
PROJECTION=Path('/etc/aios-gitops/credentials/chatgpt-autonomous-release-writer')
REGISTRY=Path('/var/lib/aios-chatgpt-autonomous-release-writer/token-registry.sqlite3')
WRITER_LEDGER=Path('/var/lib/aios-chatgpt-autonomous-release-writer/operation-ledger.sqlite3')
ASKPASS=Path('/usr/libexec/aios-autonomous-release-authorization/gitea_askpass.py')
PATH_POLICY=Path('/etc/aios-gitops/policies-chatgpt-autonomous-release/autonomous-release-path-policy.json')
ROUTE_BINDING=Path('/etc/aios-gitops/action-route-bindings/chatgpt-autonomous-release-20260726.json')
OID_RE=re.compile(r'^[0-9a-f]{40}$')
OP_RE=re.compile(r'^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$')
TERMINAL={'COMPLETED','CANCELLED','FAILED_FINAL','REVOKED','REVOKED_IDLE_TIMEOUT','REVOKED_WALL_CLOCK','RECONCILIATION_REQUIRED'}
sys.path.insert(0,'/usr/libexec/aios-chatgpt-autonomous-release-writer-fullprofile-8ac2f90f')
from isolated_writer_token_registry import TokenRegistry
def now() -> datetime: return datetime.now(timezone.utc)
def stamp() -> str: return now().replace(microsecond=0).isoformat()
def canonical(value: Any) -> bytes: return json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode('utf-8')
def fail(code: str, status: int=503, **details: Any) -> None: raise ControlError(code,status,details)
class ControlError(RuntimeError):
def __init__(self,code:str,status:int=503,details:dict[str,Any]|None=None): self.code,self.status,self.details=code,status,details or {}; super().__init__(code)
def owned_regular(path:Path, mode:int) -> os.stat_result:
try: info=os.lstat(path)
except FileNotFoundError: fail('authorization_config_missing')
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)&~mode: fail('authorization_config_permissions_rejected')
return info
def load_json(path:Path,mode:int=0o600) -> dict[str,Any]:
owned_regular(path,mode)
try: value=json.loads(path.read_text(encoding='utf-8'))
except Exception: fail('authorization_config_json_rejected')
if not isinstance(value,dict): fail('authorization_config_schema_rejected')
return value
def atomic_json(path:Path,value:dict[str,Any],mode:int=0o600) -> None:
path.parent.mkdir(parents=True,exist_ok=True)
raw=canonical(value)+b'\n'
fd,tmp=tempfile.mkstemp(prefix='.new-',dir=path.parent)
try:
os.fchmod(fd,mode); os.write(fd,raw); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,path)
os.chmod(path,mode)
finally:
try: os.close(fd)
except OSError: pass
if os.path.exists(tmp): os.unlink(tmp)
def append_audit(event:dict[str,Any]) -> None:
TASK_ROOT.mkdir(parents=True,exist_ok=True)
flags=os.O_WRONLY|os.O_CREAT|os.O_APPEND|getattr(os,'O_NOFOLLOW',0)
fd=os.open(AUDIT,flags,0o600)
try: os.write(fd,canonical({'at':stamp(),'task_id':TASK,'secrets_included':False,**event})+b'\n'); os.fsync(fd)
finally: os.close(fd)
def parent() -> dict[str,Any]:
p=load_json(PARENT)
required={'schema_version','development_authorization_id','authorization_type','task_id','actor_identity','audience','repository','exact_branch','approved_base_oid','allowed_path_roots','allowed_exact_paths','allowed_operations','created_at','valid_until','maximum_wall_clock_seconds','idle_timeout_seconds','manual_reapproval_per_commit','maximum_concurrent_writes','path_policy_sha256','writer_instance_id'}
if set(p)!=required or p.get('schema_version')!=1: fail('parent_authorization_schema_rejected')
fixed={'development_authorization_id':AUTH_ID,'authorization_type':'TASK_SCOPED_CONTINUOUS_DEVELOPMENT','task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'exact_branch':BRANCH,'approved_base_oid':BASE,'manual_reapproval_per_commit':False,'maximum_concurrent_writes':1,'writer_instance_id':WRITER_INSTANCE}
if any(p.get(k)!=v for k,v in fixed.items()): fail('parent_authorization_binding_rejected')
if p.get('maximum_wall_clock_seconds')!=86400 or p.get('idle_timeout_seconds')!=2700: fail('parent_authorization_lifetime_rejected')
if not isinstance(p.get('allowed_path_roots'),list) or not isinstance(p.get('allowed_exact_paths'),list) or not isinstance(p.get('allowed_operations'),list): fail('parent_authorization_scope_rejected')
raw=PATH_POLICY.read_bytes()
if hashlib.sha256(raw).hexdigest()!=p.get('path_policy_sha256'): fail('parent_authorization_path_policy_changed')
return p
def parse_time(v:Any) -> datetime:
try:
d=datetime.fromisoformat(str(v).replace('Z','+00:00'))
return d.astimezone(timezone.utc)
except Exception: fail('authorization_time_rejected')
def components(path:str)->tuple[str,...]:
if not isinstance(path,str) or not path or path!=path.strip() or '\\' in path or '\x00' in path: fail('authorization_path_rejected',403)
pp=PurePosixPath(path); wp=PureWindowsPath(path)
parts=tuple(path.split('/'))
if pp.is_absolute() or wp.is_absolute() or bool(wp.drive) or any(x in {'','.','..'} for x in parts): fail('authorization_path_rejected',403)
return parts
def path_allowed(p:dict[str,Any],path:str)->bool:
item=components(path)
for root in p['allowed_path_roots']:
r=components(str(root).rstrip('/'))
if item[:len(r)]==r: return True
return any(item==components(x) for x in p['allowed_exact_paths'])
def git(*args:str,cwd:Path|None=None,env:dict[str,str]|None=None)->str:
e=os.environ.copy(); e.update(env or {}); e['GIT_CONFIG_COUNT']='1'; e['GIT_CONFIG_KEY_0']='safe.directory'; e['GIT_CONFIG_VALUE_0']=str(WORKTREE)
r=subprocess.run(['git',*args],cwd=cwd,text=True,capture_output=True,env=e)
if r.returncode: fail('authorization_git_probe_failed',503)
return r.stdout.strip()
def credential_lines()->list[str]:
cp=Path(os.environ.get('CREDENTIALS_DIRECTORY',''))/'gitea_git_http'
try:
info=os.lstat(cp); lines=cp.read_text(encoding='utf-8').splitlines()
except OSError: fail('authorization_gitea_credential_unavailable')
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or len(lines)!=2 or lines[0]!='gitops-bot' or not lines[1]: fail('authorization_gitea_credential_rejected')
return lines
def remote_oid()->str:
credential_lines()
e={'GIT_ASKPASS':str(ASKPASS),'GIT_TERMINAL_PROMPT':'0'}
out=git('ls-remote','--heads','http://127.0.0.1:3000/aios/aios.git',f'refs/heads/{BRANCH}',cwd=WORKTREE,env=e)
fields=out.split()
if len(fields)!=2 or not OID_RE.fullmatch(fields[0]): fail('authorization_git_ls_remote_rejected')
return fields[0]
def api_oid()->str:
user,password=credential_lines(); branch=urllib.parse.quote(BRANCH,safe='')
encoded=base64.b64encode((user+':'+password).encode()).decode()
request=urllib.request.Request('http://127.0.0.1:3000/api/v1/repos/aios/aios/branches/'+branch,headers={'Authorization':'Basic '+encoded,'Host':'g.cnnetmkt.top'})
try:
with urllib.request.urlopen(request,timeout=10) as response: value=json.loads(response.read())
except Exception: fail('authorization_gitea_api_unavailable')
oid=str(value.get('commit',{}).get('id',''))
if not OID_RE.fullmatch(oid): fail('authorization_gitea_api_oid_rejected')
return oid
def four_way(expected:str)->None:
local=git('rev-parse','HEAD',cwd=WORKTREE)
bare=git('--git-dir',str(BARE),'rev-parse',f'refs/heads/{BRANCH}')
remote=remote_oid(); api=api_oid()
if any(x!=expected for x in (local,bare,remote,api)): fail('RECONCILIATION_REQUIRED',409,current_head_oid=local)
if git('status','--porcelain',cwd=WORKTREE)!='': fail('authorization_worktree_dirty',409)
def state()->dict[str,Any]:
s=load_json(STATE)
required={'schema_version','development_authorization_id','task_id','status','current_head_oid','last_activity_at','writer_child','test_child','inflight','updated_at'}
if set(s)!=required or s.get('schema_version')!=1 or s.get('development_authorization_id')!=AUTH_ID or s.get('task_id')!=TASK or not OID_RE.fullmatch(str(s.get('current_head_oid',''))): fail('authorization_state_schema_rejected')
return s
def save_state(s:dict[str,Any])->None: s['updated_at']=stamp(); atomic_json(STATE,s)
def lock():
STATE_DIR.mkdir(parents=True,exist_ok=True); os.chmod(STATE_DIR,0o700)
f=open(LOCK,'a+b'); os.chmod(LOCK,0o600); fcntl.flock(f.fileno(),fcntl.LOCK_EX); return f
def route_binding_valid(p:dict[str,Any])->None:
b=load_json(ROUTE_BINDING,0o640)
identities=b.get('action_identities')
if b.get('task_id')!=TASK or b.get('writer_instance_id')!=WRITER_INSTANCE or b.get('repository')!=REPO or b.get('branch')!=BRANCH or b.get('approved_base_oid')!=BASE or b.get('path_policy_sha256')!=p.get('path_policy_sha256') or not isinstance(identities,list) or len(identities)!=1 or identities[0].get('account')!=ACTOR:
fail('authorization_route_binding_changed',409)
def task_live(p:dict[str,Any],s:dict[str,Any],*,touch:bool)->None:
route_binding_valid(p)
if s['status'] in TERMINAL: fail('authorization_task_terminal',409)
if now()>=parse_time(p['valid_until']):
s['status']='REVOKED_WALL_CLOCK'; save_state(s); cleanup(s,'wall_clock'); fail('authorization_task_terminal',409)
if (now()-parse_time(s['last_activity_at'])).total_seconds()>p['idle_timeout_seconds']:
s['status']='REVOKED_IDLE_TIMEOUT'; save_state(s); cleanup(s,'idle_timeout'); fail('authorization_task_terminal',409)
if touch: s['last_activity_at']=stamp(); save_state(s)
def projection_token()->str|None:
if not PROJECTION.exists(): return None
try:
info=os.lstat(PROJECTION)
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)!=0o600: fail('authorization_projection_rejected')
token=PROJECTION.read_text(encoding='utf-8').strip()
return token if len(token.encode())>=32 else None
except OSError: fail('authorization_projection_rejected')
def write_projection(token:str)->None:
PROJECTION.parent.mkdir(parents=True,exist_ok=True)
fd,tmp=tempfile.mkstemp(prefix='.writer-',dir=PROJECTION.parent)
try:
os.fchmod(fd,0o600); os.write(fd,(token+'\n').encode()); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,PROJECTION); os.chmod(PROJECTION,0o600)
finally:
try: os.close(fd)
except OSError: pass
if os.path.exists(tmp): os.unlink(tmp)
def post_revoke_401(token:str)->None:
req=urllib.request.Request('http://127.0.0.1:19893/api/runtime-status',headers={'Authorization':'Bearer '+token})
try: urllib.request.urlopen(req,timeout=5)
except urllib.error.HTTPError as e:
if e.code==401:return
except Exception: pass
fail('authorization_old_credential_revocation_unverified')
def rotate_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
old=projection_token(); registry=TokenRegistry(REGISTRY)
old_record=registry.find(old) if old else None
if old_record and (old_record.get('task_id')!=TASK or old_record.get('development_authorization_id')!=AUTH_ID):
if parse_time(old_record.get('expires_at'))>=now(): fail('authorization_existing_projection_binding_rejected')
if not registry.revoke(str(old_record['token_id'])): fail('authorization_historical_child_revoke_failed')
post_revoke_401(old)
append_audit({'event':'historical_expired_child_revoked','old_credential_http_status':401})
old_record=None
issued=now().replace(microsecond=0); expires=issued+timedelta(minutes=20)
prefixes=[str(x).rstrip('/')+'/' for x in p['allowed_path_roots']]
grant={'token_id':'continuous-child-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'credential_id':'credential-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'development_authorization_id':AUTH_ID,'audience':WRITER_AUDIENCE,'task_id':TASK,'actor_identity':ACTOR,'writer_instance_id':WRITER_INSTANCE,'repository_identity':REPO,'source_cidrs':['127.0.0.1/32'],'allowed_branch':BRANCH,'current_expected_head_oid':s['current_head_oid'],'approved_base_oid':BASE,'allowed_operations':p['allowed_operations'],'allowed_paths':p['allowed_exact_paths'],'allowed_path_prefixes':prefixes,'not_before':issued.isoformat(),'expires_at':expires.isoformat(),'renewable':False,'max_uses':{operation:CHILD_OPERATION_MAX_USES for operation in p['allowed_operations']},'issued_by':'task_authorization_controller'}
token,record=registry.issue(grant)
try:
write_projection(token)
if old_record:
if not registry.revoke(str(old_record['token_id'])): fail('authorization_old_child_revoke_failed')
post_revoke_401(old)
except Exception:
registry.revoke(str(record['token_id']))
try: PROJECTION.unlink()
except FileNotFoundError: pass
raise
s['writer_child']={'credential_id':record['credential_id'],'issued_at':record['not_before'],'expires_at':record['expires_at'],'current_expected_head_oid':s['current_head_oid'],'rotation_reason':reason}
save_state(s); append_audit({'event':'child_grant_rotated','reason':reason,'current_head_oid':s['current_head_oid'],'old_grant_revoked':bool(old_record),'old_credential_http_status':401 if old_record else None})
return token
def child_operation_quota_exhausted(rec:dict[str,Any],registry:TokenRegistry)->bool:
token_id=str(rec.get('token_id',''))
allowed=rec.get('allowed_operations')
limits=rec.get('max_uses')
if not token_id or not isinstance(allowed,list) or not isinstance(limits,dict): fail('authorization_child_quota_schema_rejected')
for operation in allowed:
maximum=limits.get(operation)
if not isinstance(operation,str) or not isinstance(maximum,int) or maximum<1: fail('authorization_child_quota_schema_rejected')
if registry.usage(token_id,operation)>=maximum: return True
return False
def ensure_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
token=projection_token(); meta=s.get('writer_child') or {}
expired=True
try: expired=(parse_time(meta.get('expires_at'))-now()).total_seconds()<=300
except Exception: pass
if not token or expired or meta.get('current_expected_head_oid')!=s['current_head_oid']:
return rotate_child(p,s,reason)
registry=TokenRegistry(REGISTRY)
rec=registry.find(token)
if not rec or rec.get('revoked') or rec.get('development_authorization_id')!=AUTH_ID or rec.get('current_expected_head_oid')!=s['current_head_oid']:
return rotate_child(p,s,reason)
if child_operation_quota_exhausted(rec,registry):
return rotate_child(p,s,'operation_quota_exhausted')
return token
def ledger_verified(operation_id:str,commit_oid:str)->None:
with sqlite3.connect(WRITER_LEDGER) as db:
row=db.execute('select operation_id,branch,commit_oid,state,verification_status from writer_operations where operation_id=?',(operation_id,)).fetchone()
if not row or row[1]!=BRANCH or row[2]!=commit_oid or row[3]!='COMPLETED' or row[4]!='bare+gitea': fail('authorization_writer_ledger_rejected',409)
def reconcile(p:dict[str,Any],s:dict[str,Any],previous:str,new:str,operation_id:str)->dict[str,Any]:
inflight=s.get('inflight')
if not isinstance(inflight,dict) or inflight.get('previous_head_oid')!=previous or inflight.get('operation_id')!=operation_id: fail('authorization_inflight_rejected',409)
if previous!=s['current_head_oid'] or not OID_RE.fullmatch(new): fail('authorization_head_transition_rejected',409)
ledger_verified(operation_id,new); four_way(new)
s['current_head_oid']=new; s['inflight']=None; s['last_activity_at']=stamp(); save_state(s)
ensure_child(p,s,'head_advance')
append_audit({'event':'authorization_head_advanced','previous_head_oid':previous,'new_commit_oid':new,'four_way_ref_status':True,'worktree_clean':True})
return {'authorization_current_head_oid':new,'automatic_head_advance':True,'four_way_ref_status':True}
def prepare(p:dict[str,Any],s:dict[str,Any],data:dict[str,Any])->dict[str,Any]:
required={'expected_current_head_oid','operation_id','idempotency_key','exact_path','content_digest','commit_message_digest','branch','path','message'}
if set(data)!=required: fail('authorization_request_schema_rejected',403)
if data['branch']!=BRANCH or data['path']!=data['exact_path'] or not path_allowed(p,str(data['path'])): fail('authorization_path_rejected',403)
if data['expected_current_head_oid']!=s['current_head_oid'] or not OID_RE.fullmatch(str(data['expected_current_head_oid'])): fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
if not OP_RE.fullmatch(str(data['operation_id'])) or not OP_RE.fullmatch(str(data['idempotency_key'])): fail('authorization_operation_id_rejected',403)
if not all(isinstance(data[k],str) and re.fullmatch(r'[0-9a-f]{64}',data[k]) for k in ('content_digest','commit_message_digest')): fail('authorization_digest_rejected',403)
if hashlib.sha256(data['message'].encode()).hexdigest()!=data['commit_message_digest']: fail('authorization_commit_message_digest_rejected',403)
inflight=s.get('inflight')
if inflight:
if inflight.get('operation_id')==data['operation_id'] and inflight.get('idempotency_key')==data['idempotency_key']: return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
four_way(s['current_head_oid'])
s['inflight']={'operation_id':data['operation_id'],'idempotency_key':data['idempotency_key'],'previous_head_oid':s['current_head_oid'],'path':data['path'],'created_at':stamp()}; s['last_activity_at']=stamp(); save_state(s)
append_audit({'event':'write_prepared','operation_id':data['operation_id'],'path':data['path'],'expected_head_oid':s['current_head_oid']})
return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
def test_authorize(p:dict[str,Any],s:dict[str,Any],operation:str)->dict[str,Any]:
allowed={'pytest_dormant','pytest_autonomous_release','compileall_autonomous_release','git_diff_check'}
if operation not in allowed: fail('candidate_test_executor_operation_rejected',403)
four_way(s['current_head_oid'])
child=s.get('test_child') or {}; refresh=True
try: refresh=(parse_time(child.get('expires_at'))-now()).total_seconds()<=300
except Exception: pass
if refresh:
issued=now().replace(microsecond=0); child={'test_authorization_id':'test-authz-aios-autonomous-release-20260726','grant_id':'test-child-'+issued.strftime('%Y%m%dT%H%M%SZ'),'issued_at':issued.isoformat(),'expires_at':(issued+timedelta(minutes=20)).isoformat()}; s['test_child']=child
s['last_activity_at']=stamp(); save_state(s); append_audit({'event':'test_authorized','operation':operation})
return {'test_authorization_id':child['test_authorization_id'],'operation':operation,'expires_at':child['expires_at']}
def cleanup(s:dict[str,Any],reason:str)->None:
old=projection_token()
if old:
reg=TokenRegistry(REGISTRY); rec=reg.find(old)
if rec: reg.revoke(str(rec['token_id']))
try: PROJECTION.unlink()
except FileNotFoundError: pass
try: post_revoke_401(old)
except ControlError: pass
for unit in ('aios-chatgpt-autonomous-release-route.service','aios-chatgpt-autonomous-release-writer.service'):
subprocess.run(['/usr/bin/systemctl','stop',unit],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
subprocess.run(['/usr/bin/systemctl','stop','aios-autonomous-release-dormant-test-executor@*.service'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
try: ROUTE_BINDING.unlink()
except FileNotFoundError: pass
try: SOCKET_PATH.unlink()
except FileNotFoundError: pass
append_audit({'event':'authorization_cleanup','reason':reason,'projection_removed':not PROJECTION.exists(),'route_overlay_removed':not ROUTE_BINDING.exists(),'task_services_stop_requested':True})
def request(data:dict[str,Any])->dict[str,Any]:
op=data.get('op'); f=lock()
try:
p=parent(); s=state(); task_live(p,s,touch=False)
if op=='describe_authorization':
four_way(s['current_head_oid'])
return {'development_authorization_id':AUTH_ID,'task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'branch':BRANCH,'approved_base_oid':BASE,'authorization_status':s['status'],'current_head_oid':s['current_head_oid']}
if op=='get_writer_token':
four_way(s['current_head_oid']); token=ensure_child(p,s,'scheduled_or_requested_rotation'); return {'token':token,'current_head_oid':s['current_head_oid']}
if op=='prepare_write':
ensure_child(p,s,'pre_write_quota_check')
return prepare(p,s,data.get('request') if isinstance(data.get('request'),dict) else {})
if op=='reconcile_commit':
r=data.get('result') if isinstance(data.get('result'),dict) else {}; return reconcile(p,s,str(r.get('previous_head_oid','')),str(r.get('new_commit_oid','')),str(r.get('operation_id','')))
if op=='fail_write':
r=data.get('result') if isinstance(data.get('result'),dict) else {}; inflight=s.get('inflight')
if isinstance(inflight,dict) and inflight.get('operation_id')==r.get('operation_id'): s['inflight']=None; save_state(s)
return {'cleared':True}
if op=='authorize_test': return test_authorize(p,s,str(data.get('operation','')))
fail('authorization_operation_rejected',403)
finally:
fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
def peer_allowed(conn:socket.socket)->bool:
try:
import struct
_pid,uid,gid=struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
return uid==pwd.getpwnam('aiman').pw_uid and gid==grp.getgrnam('gitops').gr_gid
except Exception: return False
def serve()->None:
SOCKET_DIR.mkdir(parents=True,exist_ok=True); os.chown(SOCKET_DIR,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_DIR,0o750)
try: SOCKET_PATH.unlink()
except FileNotFoundError: pass
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server:
server.bind(str(SOCKET_PATH)); os.chown(SOCKET_PATH,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_PATH,0o660); server.listen(32); server.settimeout(1.0)
last=0.0
while True:
if time.monotonic()-last>30:
last=time.monotonic()
try:
f=lock(); p=parent(); s=state(); task_live(p,s,touch=False); four_way(s['current_head_oid']); ensure_child(p,s,'scheduled_rotation'); fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
except ControlError as e:
append_audit({'event':'scheduled_check_blocked','error':e.code})
if e.code=='authorization_task_terminal': return
except Exception: append_audit({'event':'scheduled_check_blocked','error':'authorization_controller_internal_error'})
try: conn,_=server.accept()
except socket.timeout: continue
with conn:
if not peer_allowed(conn): conn.sendall(canonical({'ok':False,'status':403,'error':'authorization_peer_rejected'})+b'\n'); continue
try:
raw=conn.recv(131072); data=json.loads(raw.decode()); result=request(data); conn.sendall(canonical({'ok':True,'result':result})+b'\n')
except ControlError as e: conn.sendall(canonical({'ok':False,'status':e.status,'error':e.code,**e.details})+b'\n')
except Exception as exc:
append_audit({'event':'controller_internal_error','error_type':type(exc).__name__})
conn.sendall(canonical({'ok':False,'status':503,'error':'authorization_controller_internal_error'})+b'\n')
if __name__=='__main__': serve()
@@ -1,567 +0,0 @@
#!/usr/bin/env python3
"""Task-bound bridge from the legacy ChatGPT Action to an isolated Writer V2."""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import re
import sqlite3
import socket
import stat
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any
CONFIG_PATH = Path(os.environ["AIOS_ACTION_ROUTE_BINDING"])
POLICY_PATH = Path(os.environ["AIOS_ACTION_ROUTE_PATH_POLICY"])
KNOWN_IDENTITIES_PATH = Path(os.environ["AIOS_ACTION_ROUTE_KNOWN_IDENTITIES"])
STATE_PATH = Path(os.environ["AIOS_ACTION_ROUTE_STATE"])
AUDIT_PATH = Path(os.environ["AIOS_ACTION_ROUTE_AUDIT"])
HOST = os.environ.get("AIOS_ACTION_ROUTE_HOST", "127.0.0.1")
PORT = int(os.environ.get("AIOS_ACTION_ROUTE_PORT", "19894"))
EXPECTED_TASK_ID = os.environ["AIOS_ACTION_ROUTE_TASK_ID"]
EXPECTED_ROUTE_HEADER = os.environ["AIOS_ACTION_ROUTE_HEADER"]
AUTHORIZATION_SOCKET = Path(os.environ["AIOS_CONTINUOUS_AUTHORIZATION_SOCKET"])
OID_RE = re.compile(r"[0-9a-f]{40}")
SECRET_RE = re.compile(
r"(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)"
r"\s*[:=]\s*[\"']?([A-Za-z0-9_./+=:-]{16,})"
)
SECRET_VALUE_RE = re.compile(
r"(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,}|"
r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----)"
)
ALLOWED_REQUEST_FIELDS = frozenset({"base","branch","content","create_branch","message","path","task_id","development_authorization_id","actor_identity","audience","approved_base_oid","expected_current_head_oid","operation_id","idempotency_key","exact_path","content_digest","commit_message_digest"})
class RouteError(RuntimeError):
def __init__(self, code: str, status: int = 400, **details: Any):
super().__init__(code)
self.code = code
self.status = status
self.details = details
def utc_now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def canonical_json(value: Any) -> bytes:
return json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
def load_root_managed_json(
path: Path, *, expected_mode: int = 0o640
) -> tuple[dict[str, Any], bytes]:
info = os.lstat(path)
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
raise RouteError("writer_route_config_type_rejected", 503)
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & ~expected_mode:
raise RouteError("writer_route_config_permissions_rejected", 503)
raw = path.read_bytes()
try:
value = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError):
raise RouteError("writer_route_config_json_rejected", 503) from None
if not isinstance(value, dict):
raise RouteError("writer_route_config_object_required", 503)
return value, raw
def load_binding() -> dict[str, Any]:
binding, _ = load_root_managed_json(CONFIG_PATH)
required = {
"schema_version",
"task_id",
"writer_instance_id",
"repository",
"branch",
"approved_base_oid",
"writer_url",
"path_policy_sha256",
"action_identities",
}
if set(binding) != required or binding.get("schema_version") != 1:
raise RouteError("writer_route_binding_schema_rejected", 503)
if binding["task_id"] != EXPECTED_TASK_ID:
raise RouteError("writer_route_task_binding_rejected", 503)
if binding["repository"] != "aios/aios":
raise RouteError("writer_route_repository_rejected", 503)
if not OID_RE.fullmatch(str(binding["approved_base_oid"])):
raise RouteError("writer_route_base_oid_rejected", 503)
if binding["writer_url"] != "http://127.0.0.1:19893":
raise RouteError("writer_route_upstream_rejected", 503)
identities = binding.get("action_identities")
if not isinstance(identities, list) or len(identities) != 1:
raise RouteError("writer_route_action_identity_rejected", 503)
identity = identities[0]
if (
not isinstance(identity, dict)
or set(identity) != {"account", "token_sha256"}
or identity.get("account") != "chatgpt-gitops-bot"
or not re.fullmatch(
r"[0-9a-f]{64}", str(identity.get("token_sha256", ""))
)
):
raise RouteError("writer_route_action_identity_rejected", 503)
return binding
def allowed_path_scope(binding: dict[str, Any]) -> tuple[set[str], tuple[str, ...]]:
policy, raw = load_root_managed_json(POLICY_PATH)
actual = hashlib.sha256(raw).hexdigest()
if actual != binding["path_policy_sha256"]:
raise RouteError("writer_route_path_policy_digest_mismatch", 503)
if policy.get("allowed_branch") != binding["branch"]:
raise RouteError("writer_route_path_policy_branch_mismatch", 503)
operation = (policy.get("allowed_operations") or {}).get("commit_file.atomic")
paths = operation.get("exact_paths") if isinstance(operation, dict) else None
prefixes = operation.get("prefix_paths") if isinstance(operation, dict) else None
if not isinstance(paths, list) or not paths or not all(
isinstance(x, str) for x in paths
):
raise RouteError("writer_route_path_policy_rejected", 503)
if not isinstance(prefixes, list) or not prefixes or not all(
isinstance(x, str) and x.endswith("/") for x in prefixes
):
raise RouteError("writer_route_path_policy_rejected", 503)
if any(not valid_relative_path(x[:-1]) for x in prefixes):
raise RouteError("writer_route_path_policy_rejected", 503)
if any(not valid_relative_path(x) for x in paths):
raise RouteError("writer_route_path_policy_rejected", 503)
return set(paths), tuple(prefixes)
def valid_relative_path(path: str) -> bool:
if not isinstance(path, str) or not path or path != path.strip():
return False
if "\x00" in path or "\\" in path:
return False
try:
decoded = urllib.parse.unquote(path, errors="strict")
except (UnicodeDecodeError, ValueError):
return False
if decoded != path:
return False
posix = PurePosixPath(path)
windows = PureWindowsPath(path)
return not (
posix.is_absolute()
or windows.is_absolute()
or bool(windows.drive)
or any(part in {"", ".", ".."} for part in path.split("/"))
)
def path_allowed(binding: dict[str, Any], path: str) -> bool:
if not valid_relative_path(path):
return False
exact_paths, prefixes = allowed_path_scope(binding)
return path in exact_paths or any(path.startswith(prefix) for prefix in prefixes)
def bearer(headers: Any) -> str:
value = headers.get("Authorization", "")
if not value.startswith("Bearer "):
raise RouteError("unauthorized", 401)
token = value[7:].strip()
if not token:
raise RouteError("unauthorized", 401)
return token
def authorize_action(headers: Any, binding: dict[str, Any]) -> str:
digest = hashlib.sha256(bearer(headers).encode("utf-8")).hexdigest()
identity = binding["action_identities"][0]
if not hmac.compare_digest(identity["token_sha256"], digest):
known, _ = load_root_managed_json(KNOWN_IDENTITIES_PATH)
identities = known.get("identities")
if known.get("schema_version") != 1 or not isinstance(identities, list):
raise RouteError("writer_route_identity_catalog_rejected", 503)
observed = "unregistered"
allowed_accounts = {
"chatgpt-gitops-bot",
"gitops-write-bot",
"codex-gitops-bot",
}
for candidate in identities:
if not isinstance(candidate, dict) or set(candidate) != {
"account",
"token_sha256",
}:
raise RouteError("writer_route_identity_catalog_rejected", 503)
account = str(candidate.get("account", ""))
token_sha256 = str(candidate.get("token_sha256", ""))
if account not in allowed_accounts or not re.fullmatch(
r"[0-9a-f]{64}", token_sha256
):
raise RouteError("writer_route_identity_catalog_rejected", 503)
if hmac.compare_digest(token_sha256, digest):
observed = account
raise RouteError(
"writer_route_action_identity_rejected",
403,
accepted_action_identity=str(identity["account"]),
observed_action_identity=observed,
identity_alias_accepted=False,
)
return str(identity["account"])
def authorization_call(operation: str, **payload: Any) -> dict[str, Any]:
request = {"op": operation, **payload}
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.settimeout(15)
client.connect(str(AUTHORIZATION_SOCKET))
client.sendall(canonical_json(request))
raw = client.recv(131072)
response = json.loads(raw.decode("utf-8"))
except (OSError, TimeoutError, UnicodeDecodeError, json.JSONDecodeError):
raise RouteError("authorization_controller_unavailable", 503) from None
if not isinstance(response, dict) or not response.get("ok"):
if not isinstance(response, dict):
raise RouteError("authorization_controller_response_rejected", 503)
details = {k: v for k, v in response.items() if k not in {"ok", "status", "error"}}
raise RouteError(str(response.get("error") or "authorization_controller_rejected"), int(response.get("status") or 503), **details)
result = response.get("result")
if not isinstance(result, dict):
raise RouteError("authorization_controller_response_rejected", 503)
return result
def writer_token() -> str:
value = authorization_call("get_writer_token").get("token")
if not isinstance(value, str) or len(value.encode("utf-8")) < 32:
raise RouteError("writer_route_credential_rejected", 503)
return value
def writer_call(
binding: dict[str, Any],
method: str,
path: str,
payload: dict[str, Any] | None = None,
) -> tuple[int, dict[str, Any]]:
data = canonical_json(payload) if payload is not None else None
headers = {"Authorization": "Bearer " + writer_token()}
if data is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
binding["writer_url"] + path, data=data, method=method, headers=headers
)
try:
with urllib.request.urlopen(request, timeout=90) as response:
body = json.loads(response.read())
return response.status, body
except urllib.error.HTTPError as exc:
try:
body = json.loads(exc.read())
except Exception:
body = {"error": "writer_route_upstream_non_json_error"}
return exc.code, body
except (OSError, TimeoutError):
raise RouteError("writer_route_upstream_unavailable", 503) from None
def validate_request(data: Any, binding: dict[str, Any]) -> dict[str, Any]:
if not isinstance(data, dict):
raise RouteError("writer_route_request_object_required")
if not {"branch", "path", "content"}.issubset(data):
raise RouteError("writer_route_request_incomplete")
if not set(data).issubset(ALLOWED_REQUEST_FIELDS):
raise RouteError("writer_route_request_fields_rejected")
if data["branch"] != binding["branch"]:
raise RouteError("writer_route_request_binding_failed", 403)
if data.get("create_branch") not in (None, False):
raise RouteError("writer_route_request_binding_failed", 403)
if data.get("base", "main") not in {"main", binding["approved_base_oid"]}:
raise RouteError("writer_route_request_binding_failed", 403)
path = data["path"]
if not isinstance(path, str) or not path_allowed(binding, path):
raise RouteError("writer_route_request_binding_failed", 403)
content = data["content"]
if not isinstance(content, str) or "\x00" in content or "\r" in content:
raise RouteError("writer_route_content_rejected")
if len(content.encode("utf-8")) > 1_048_576:
raise RouteError("writer_route_content_too_large", 413)
if SECRET_RE.search(content) or SECRET_VALUE_RE.search(content):
raise RouteError("writer_route_secret_content_rejected", 403)
message = data.get("message") or f"chore(chatgpt): update {path}"
if not isinstance(message, str) or not 1 <= len(message) <= 200 or "\x00" in message or "\n" in message or "\r" in message:
raise RouteError("writer_route_message_rejected")
return {"branch": binding["branch"], "path": path, "content": content, "message": message}
def complete_request(data: dict[str, Any], normalized: dict[str, Any], binding: dict[str, Any], account: str) -> dict[str, Any]:
authorization = authorization_call("describe_authorization")
expected = {
"task_id": binding["task_id"],
"development_authorization_id": authorization.get("development_authorization_id"),
"actor_identity": account,
"audience": authorization.get("audience"),
"branch": binding["branch"],
"approved_base_oid": binding["approved_base_oid"],
}
required = {
"task_id": EXPECTED_TASK_ID,
"development_authorization_id": "authz-aios-autonomous-release-20260726",
"actor_identity": "chatgpt-gitops-bot",
"audience": "chatgpt-autonomous-release-writer",
"branch": binding["branch"],
"approved_base_oid": binding["approved_base_oid"],
}
if expected != required or authorization.get("authorization_status") != "ACTIVE" or authorization.get("current_head_oid") is None:
raise RouteError("writer_route_request_binding_failed", 403)
for key, value in required.items():
if key in data and data[key] != value:
raise RouteError("writer_route_request_binding_failed", 403)
if "exact_path" in data and data["exact_path"] != normalized["path"]:
raise RouteError("writer_route_request_binding_failed", 403)
current_head = str(authorization["current_head_oid"])
if not OID_RE.fullmatch(current_head):
raise RouteError("authorization_current_head_rejected", 503)
if "expected_current_head_oid" in data and data["expected_current_head_oid"] != current_head:
raise RouteError("BRANCH_HEAD_CHANGED", 409, current_head_oid=current_head)
content_digest = hashlib.sha256(normalized["content"].encode("utf-8")).hexdigest()
message_digest = hashlib.sha256(normalized["message"].encode("utf-8")).hexdigest()
if "content_digest" in data and data["content_digest"] != content_digest:
raise RouteError("writer_route_request_binding_failed", 403)
if "commit_message_digest" in data and data["commit_message_digest"] != message_digest:
raise RouteError("writer_route_request_binding_failed", 403)
seed = {**required, "expected_current_head_oid": current_head, "path": normalized["path"], "content_digest": content_digest, "commit_message_digest": message_digest}
request_digest = hashlib.sha256(canonical_json(seed)).hexdigest()
idempotency_key = "action-" + request_digest
operation_id = hashlib.sha256((EXPECTED_TASK_ID + "\0" + idempotency_key).encode("utf-8")).hexdigest()
if "idempotency_key" in data and data["idempotency_key"] != idempotency_key:
raise RouteError("writer_route_request_binding_failed", 403)
if "operation_id" in data and data["operation_id"] != operation_id:
raise RouteError("writer_route_request_binding_failed", 403)
return {**normalized, **required, "expected_current_head_oid": current_head, "operation_id": operation_id, "idempotency_key": idempotency_key, "exact_path": normalized["path"], "content_digest": content_digest, "commit_message_digest": message_digest, "request_digest": request_digest}
def current_writer_head(binding: dict[str, Any]) -> str:
status, body = writer_call(binding, "GET", "/api/runtime-status")
if status != 200:
raise RouteError(
"writer_route_runtime_status_failed", 503, upstream_status=status
)
required = {
"service": "chatgpt-autonomous-release-writer",
"writes_enabled": True,
"allowed_branch": binding["branch"],
"approved_base_oid": binding["approved_base_oid"],
"path_policy_sha256": binding["path_policy_sha256"],
}
if any(body.get(key) != value for key, value in required.items()):
raise RouteError("writer_route_runtime_binding_mismatch", 503)
head = str(body.get("workspace_head_oid", ""))
if not OID_RE.fullmatch(head):
raise RouteError("writer_route_runtime_head_rejected", 503)
return head
def operation_binding(
request_digest: str, binding: dict[str, Any]
) -> tuple[str, str]:
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(STATE_PATH, timeout=30, isolation_level=None) as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute(
"""
CREATE TABLE IF NOT EXISTS action_operations (
request_digest TEXT PRIMARY KEY,
expected_head_oid TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
)
"""
)
row = db.execute(
"SELECT expected_head_oid,idempotency_key FROM action_operations "
"WHERE request_digest=?",
(request_digest,),
).fetchone()
if row:
return str(row[0]), str(row[1])
head = current_writer_head(binding)
key = "action-" + request_digest
with sqlite3.connect(STATE_PATH, timeout=30, isolation_level=None) as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute("BEGIN IMMEDIATE")
db.execute(
"INSERT OR IGNORE INTO action_operations"
"(request_digest,expected_head_oid,idempotency_key,created_at) "
"VALUES(?,?,?,?)",
(request_digest, head, key, utc_now()),
)
row = db.execute(
"SELECT expected_head_oid,idempotency_key FROM action_operations "
"WHERE request_digest=?",
(request_digest,),
).fetchone()
db.execute("COMMIT")
return str(row[0]), str(row[1])
def append_audit(event: dict[str, Any]) -> None:
AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0)
fd = os.open(AUDIT_PATH, flags, 0o600)
try:
os.write(fd, canonical_json(event) + b"\n")
os.fsync(fd)
finally:
os.close(fd)
class Handler(BaseHTTPRequestHandler):
server_version = "AIOSActionTaskRouter/1"
def log_message(self, _format: str, *_args: Any) -> None:
return
def send_json(self, status: int, value: dict[str, Any]) -> None:
body = json.dumps(value, ensure_ascii=False, sort_keys=True).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != "/health":
return self.send_json(404, {"error": "not_found"})
try:
binding = load_binding()
self.send_json(
200,
{
"status": "ok",
"service": "aios-action-task-router",
"task_id": binding["task_id"],
"writer_instance_id": binding["writer_instance_id"],
"branch": binding["branch"],
},
)
except RouteError as exc:
self.send_json(exc.status, {"error": exc.code, **exc.details})
def do_POST(self) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != "/api/file":
return self.send_json(404, {"error": "not_found"})
if self.client_address[0] not in {"127.0.0.1", "::1"}:
return self.send_json(403, {"error": "writer_route_source_rejected"})
if self.headers.get("X-AIOS-Route-Task") != EXPECTED_ROUTE_HEADER:
return self.send_json(403, {"error": "writer_route_header_rejected"})
try:
length = int(self.headers.get("Content-Length", "0"))
if not 0 < length <= 1_100_000:
raise RouteError("writer_route_content_length_rejected", 413)
try:
data = json.loads(self.rfile.read(length).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise RouteError("writer_route_request_json_rejected") from None
binding = load_binding()
account = authorize_action(self.headers, binding)
normalized = validate_request(data, binding)
normalized = complete_request(data, normalized, binding, account)
prepared = authorization_call("prepare_write", request={key: normalized[key] for key in ("expected_current_head_oid", "operation_id", "idempotency_key", "exact_path", "content_digest", "commit_message_digest", "branch", "path", "message")})
expected_head = str(prepared["expected_head_oid"])
writer_payload = {"branch": normalized["branch"], "path": normalized["path"], "content": normalized["content"], "message": normalized["message"], "expected_head_oid": expected_head, "idempotency_key": str(prepared["idempotency_key"])}
request_digest = hashlib.sha256(canonical_json({"task_id": binding["task_id"], "operation_id": normalized["operation_id"], "idempotency_key": normalized["idempotency_key"], "content_digest": normalized["content_digest"], "commit_message_digest": normalized["commit_message_digest"]})).hexdigest()
status, result = writer_call(
binding, "POST", "/api/commit-file", writer_payload
)
audit = {
"at": utc_now(),
"task_id": binding["task_id"],
"writer_instance_id": binding["writer_instance_id"],
"account": account,
"branch": normalized["branch"],
"path": normalized["path"],
"request_digest": request_digest,
"expected_head_oid": expected_head,
"status": status,
"operation_id": result.get("operation_id"),
"commit_oid": result.get("commit_oid"),
"error": result.get("error"),
"secrets_included": False,
}
append_audit(audit)
if status != 200:
try:
authorization_call("fail_write", result={"operation_id": normalized["operation_id"]})
except RouteError:
pass
return self.send_json(status, result)
advanced = authorization_call("reconcile_commit", result={"previous_head_oid": expected_head, "new_commit_oid": result.get("commit_oid"), "operation_id": result.get("operation_id")})
return self.send_json(
200,
{
"status": "committed",
"branch": normalized["branch"],
"path": normalized["path"],
"commit": {
"status": "committed",
"branch": normalized["branch"],
"rev": result.get("commit_oid"),
"previous_head_oid": expected_head,
"new_tree_oid": result.get("new_tree_oid"),
"push_status": result.get("push_status"),
"four_way_ref_status": advanced.get("four_way_ref_status") is True,
"sync": {"status": "verified", "target": "bare+git-ls-remote+gitea-api"},
},
"writer_instance_id": binding["writer_instance_id"],
"operation_id": result.get("operation_id"),
"idempotent": result.get("idempotent"),
"secrets_included": False,
},
)
except RouteError as exc:
if exc.code == "writer_route_action_identity_rejected":
try:
append_audit(
{
"at": utc_now(),
"task_id": EXPECTED_TASK_ID,
"event": "action_identity_rejected",
"accepted_action_identity": exc.details.get(
"accepted_action_identity"
),
"observed_action_identity": exc.details.get(
"observed_action_identity"
),
"identity_alias_accepted": False,
"secrets_included": False,
}
)
except Exception:
pass
self.send_json(exc.status, {"error": exc.code, **exc.details})
except Exception:
self.send_json(500, {"error": "writer_route_internal_error"})
def main() -> None:
load_binding()
server = ThreadingHTTPServer((HOST, PORT), Handler)
server.serve_forever()
if __name__ == "__main__":
main()
@@ -1,23 +0,0 @@
"""Task-bound Integration Refresh control-plane primitives."""
from .integration_merge_writer import IntegrationMergeWriter, MergeError, MergeRequest
from .integration_refresh import (
AppendOnlyAudit,
Binding,
GiteaBranchReadProjection,
GrantStore,
IntegrationRefreshProvisioner,
ProvisioningError,
)
__all__ = [
"AppendOnlyAudit",
"Binding",
"GiteaBranchReadProjection",
"GrantStore",
"IntegrationMergeWriter",
"IntegrationRefreshProvisioner",
"MergeError",
"MergeRequest",
"ProvisioningError",
]
@@ -1,90 +0,0 @@
"""Constrained integration-only merge writer with rollback and reconciliation states."""
from __future__ import annotations
import subprocess
from dataclasses import dataclass
from pathlib import Path
import re
class MergeError(RuntimeError):
def __init__(self, code: str, status: int = 409):
super().__init__(code); self.code, self.status = code, status
@dataclass(frozen=True)
class MergeRequest:
integration_branch: str
source_branch: str
approved_source_oid: str
class IntegrationMergeWriter:
def __init__(self, workspace: Path | str, remote: str, projection):
self.workspace, self.remote, self.projection = Path(workspace), remote, projection
def _git(self, *args: str, check=True) -> str:
p = subprocess.run(["git", *args], cwd=self.workspace, text=True, capture_output=True)
if check and p.returncode:
raise MergeError("git_operation_failed", 500)
return p.stdout.strip()
def merge(self, request: MergeRequest) -> dict:
def bad(value: str) -> bool:
return (
value == "main"
or value.startswith("main/")
or value == "governance"
or value.startswith("governance/")
or value == "approvals"
or value.startswith("refs/")
or value.startswith("tags/")
)
if (bad(request.integration_branch) or bad(request.source_branch) or
not request.integration_branch.startswith("integration/") or
re.fullmatch(r"[0-9a-fA-F]{40}", request.approved_source_oid) is None):
raise MergeError("integration_merge_scope_rejected", 403)
for branch in (request.integration_branch, request.source_branch):
if subprocess.run(["git", "check-ref-format", "--branch", branch], cwd=self.workspace,
text=True, capture_output=True).returncode:
raise MergeError("integration_merge_scope_rejected", 403)
if self._git("rev-parse", "--abbrev-ref", "HEAD") != request.integration_branch:
raise MergeError("integration_branch_checkout_required")
before = self._git("rev-parse", "HEAD")
remote_before = self.projection(request.integration_branch)
if remote_before != before:
raise MergeError("integration_remote_advanced")
source = self._git("rev-parse", "--verify", f"refs/heads/{request.source_branch}^{{commit}}")
if source != request.approved_source_oid:
raise MergeError("integration_source_not_approved", 403)
attempt = subprocess.run(["git", "merge", "--no-ff", "--no-commit", request.source_branch], cwd=self.workspace, text=True, capture_output=True)
if attempt.returncode:
subprocess.run(["git", "merge", "--abort"], cwd=self.workspace, text=True, capture_output=True)
raise MergeError("integration_merge_conflict")
try:
self._git("commit", "-m", "merge(integration): approved source")
merged = self._git("rev-parse", "HEAD")
parents = self._git("show", "-s", "--format=%P", "HEAD").split()
if parents != [before, source]:
raise MergeError("integration_parent_invariant_failed", 500)
pushed = subprocess.run(["git", "push", "--porcelain", self.remote, f"HEAD:refs/heads/{request.integration_branch}"], cwd=self.workspace, text=True, capture_output=True)
if pushed.returncode:
try:
observed = self.projection(request.integration_branch)
except Exception:
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
if observed == before:
self._git("reset", "--hard", before)
return {"state": "PUSH_FAILED_RESTORED", "pre_merge_oid": before}
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
try:
observed = self.projection(request.integration_branch)
except Exception:
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
if observed != merged:
return {"state": "RECONCILIATION_REQUIRED", "pre_merge_oid": before, "merge_oid": merged}
return {"state": "COMPLETED", "pre_merge_oid": before, "merge_oid": merged}
except MergeError:
if self._git("rev-parse", "HEAD") != before:
self._git("reset", "--hard", before)
raise
@@ -1,257 +0,0 @@
"""Fail-closed, task-bound integration refresh provisioning primitives.
This module deliberately has no HTTP or credential-provider dependency. The
caller supplies a branch-read projection and an apply callback, making the
authorization and recovery boundary independently testable.
"""
from __future__ import annotations
import hashlib
import json
import os
import argparse
import secrets
import sqlite3
import threading
import re
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Callable
class ProvisioningError(RuntimeError):
def __init__(self, code: str, status: int = 403):
super().__init__(code)
self.code, self.status = code, status
def _now() -> datetime:
return datetime.now(timezone.utc)
def _digest(secret: str) -> str:
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
def _canonical(value: dict) -> str:
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def _is_oid(value: object) -> bool:
return isinstance(value, str) and re.fullmatch(r"[0-9a-fA-F]{40}", value) is not None
@dataclass(frozen=True)
class Binding:
task_id: str
repository: str
branch: str
actor_identity: str
audience: str
paths: tuple[str, ...]
quota: int
class AppendOnlyAudit:
"""Append only redacted JSONL audit, durable before a result is returned."""
def __init__(self, path: Path | str):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
def append(self, event: dict) -> None:
with self._lock:
safe = {key: value for key, value in event.items() if key not in {"token", "credential", "authorization"}}
previous = "0" * 64
if self.path.exists():
lines = self.path.read_text(encoding="utf-8").splitlines()
if lines:
previous = json.loads(lines[-1])["event_hash"]
safe["previous_event_hash"] = previous
safe["event_hash"] = _canonical(safe)
encoded = (json.dumps(safe, sort_keys=True, separators=(",", ":")) + "\n").encode()
fd = os.open(self.path, os.O_APPEND | os.O_CREAT | os.O_WRONLY, 0o600)
try:
os.write(fd, encoded)
os.fsync(fd)
finally:
os.close(fd)
class GrantStore:
"""Digest-only short grant registry; SQLite transactions serialize quota use."""
def __init__(self, path: Path | str):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._db() as db:
db.execute("""CREATE TABLE IF NOT EXISTS grants(
token_digest TEXT PRIMARY KEY, binding_json TEXT NOT NULL,
expires_at TEXT NOT NULL, quota INTEGER NOT NULL, uses INTEGER NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0)""")
@contextmanager
def _db(self):
db = sqlite3.connect(self.path, timeout=15, isolation_level=None)
db.row_factory = sqlite3.Row
try:
yield db
finally:
db.close()
def issue(self, binding: Binding, *, ttl_seconds: int = 300) -> str:
if ttl_seconds < 1 or ttl_seconds > 1800 or binding.quota < 1 or not binding.paths:
raise ValueError("invalid_short_grant")
token = secrets.token_urlsafe(32)
payload = json.dumps(binding.__dict__ | {"paths": list(binding.paths)}, sort_keys=True)
with self._db() as db:
db.execute("INSERT INTO grants VALUES(?,?,?,?,?,0)", (_digest(token), payload,
(_now() + timedelta(seconds=ttl_seconds)).isoformat(), binding.quota, 0))
return token
def rotate(self, old_token: str, binding: Binding, *, revocation_probe, ttl_seconds: int = 300) -> str:
self.revoke(old_token)
try: status = revocation_probe(old_token)
except TimeoutError as exc: raise ProvisioningError("revocation_probe_timeout", 503) from exc
if status != 401: raise ProvisioningError("revocation_probe_rejected", 503)
return self.issue(binding, ttl_seconds=ttl_seconds)
def revoke(self, token: str) -> None:
with self._db() as db:
db.execute("BEGIN IMMEDIATE")
db.execute("UPDATE grants SET revoked=1 WHERE token_digest=?", (_digest(token),))
db.execute("COMMIT")
def consume(self, token: str, binding: Binding, path: str) -> None:
with self._db() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM grants WHERE token_digest=?", (_digest(token),)).fetchone()
if not row or row["revoked"]:
db.execute("ROLLBACK"); raise ProvisioningError("credential_rejected", 401)
if _now() >= datetime.fromisoformat(row["expires_at"]):
db.execute("ROLLBACK"); raise ProvisioningError("credential_expired", 401)
stored = json.loads(row["binding_json"])
requested = binding.__dict__ | {"paths": list(binding.paths)}
if stored != requested or path not in stored["paths"]:
db.execute("ROLLBACK"); raise ProvisioningError("grant_binding_rejected")
if row["uses"] >= row["quota"]:
db.execute("ROLLBACK"); raise ProvisioningError("grant_quota_exhausted")
db.execute("UPDATE grants SET uses=uses+1 WHERE token_digest=?", (_digest(token),))
db.execute("COMMIT")
class GiteaBranchReadProjection:
"""Normalizes a trusted task-scoped Gitea reader; every ambiguity fails closed."""
def __init__(self, reader: Callable[[str, str], dict]):
self.reader = reader
def head(self, repository: str, branch: str) -> str:
try:
reply = self.reader(repository, branch)
except TimeoutError as exc:
raise ProvisioningError("gitea_projection_timeout", 503) from exc
except Exception as exc:
raise ProvisioningError("gitea_projection_unavailable", 503) from exc
if not isinstance(reply, dict) or reply.get("status") in {401, 403}:
raise ProvisioningError("gitea_projection_forbidden", 503)
oid = reply.get("oid")
if reply.get("status") != 200 or not _is_oid(oid):
raise ProvisioningError("gitea_projection_invalid", 503)
return oid.lower()
class IntegrationRefreshProvisioner:
_locks: dict[tuple[str, str], threading.Lock] = {}
_locks_guard = threading.Lock()
def __init__(self, *, grants: GrantStore, projection: GiteaBranchReadProjection,
ledger_path: Path | str, audit: AppendOnlyAudit, approved_binding: Binding):
self.grants, self.projection, self.audit, self.approved_binding = grants, projection, audit, approved_binding
self.ledger_path = Path(ledger_path); self.ledger_path.parent.mkdir(parents=True, exist_ok=True)
with self._ledger() as db:
db.execute("CREATE TABLE IF NOT EXISTS refreshes(operation_id TEXT PRIMARY KEY, digest TEXT NOT NULL, state TEXT NOT NULL, result_oid TEXT)")
@contextmanager
def _ledger(self):
db = sqlite3.connect(self.ledger_path)
try:
yield db
db.commit()
finally:
db.close()
def refresh(self, *, token: str, binding: Binding, path: str, operation_id: str,
target_head_oid: str, apply: Callable[[], str]) -> dict:
if not operation_id or not _is_oid(target_head_oid):
raise ProvisioningError("refresh_request_rejected")
if binding != self.approved_binding:
raise ProvisioningError("grant_binding_rejected")
request = {"binding": binding.__dict__ | {"paths": list(binding.paths)}, "path": path, "target": target_head_oid}
digest = _canonical(request)
with self._locks_guard:
lock = self._locks.setdefault((binding.repository, binding.branch), threading.Lock())
with lock, self._ledger() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM refreshes WHERE operation_id=?", (operation_id,)).fetchone()
new_operation = row is None
if row:
if row[1] != digest:
db.execute("ROLLBACK"); raise ProvisioningError("operation_id_reuse_rejected", 409)
if row[2] == "COMPLETED":
db.execute("COMMIT")
return {"state": row[2], "operation_id": operation_id, "oid": row[3], "idempotent": True}
if row[2] not in {"PENDING", "RECOVERABLE"}:
db.execute("COMMIT")
return {"state": row[2], "operation_id": operation_id, "oid": row[3], "idempotent": True}
else:
db.execute("INSERT INTO refreshes VALUES(?,?,?,NULL)", (operation_id, digest, "PENDING"))
db.execute("COMMIT")
if new_operation:
self.grants.consume(token, binding, path)
current = self.projection.head(binding.repository, binding.branch)
if current != target_head_oid.lower():
self._state(operation_id, "STALE_TARGET")
raise ProvisioningError("stale_target_head", 409)
try:
oid = apply()
if not _is_oid(oid):
raise ProvisioningError("apply_result_invalid", 500)
# Detect a competing remote advancement before claiming completion.
observed = self.projection.head(binding.repository, binding.branch)
if observed != oid.lower():
self._state(operation_id, "RECONCILIATION_REQUIRED")
raise ProvisioningError("remote_concurrent_advance", 409)
self._state(operation_id, "COMPLETED", oid)
self.audit.append({"operation_id": operation_id, "task_id": binding.task_id, "repository": binding.repository,
"branch": binding.branch, "actor_identity": binding.actor_identity, "audience": binding.audience, "path": path, "state": "COMPLETED"})
return {"state": "COMPLETED", "operation_id": operation_id, "oid": oid, "idempotent": False}
except ProvisioningError:
raise
except Exception as exc:
self._state(operation_id, "RECOVERABLE")
self.audit.append({"operation_id": operation_id, "task_id": binding.task_id, "repository": binding.repository,
"branch": binding.branch, "actor_identity": binding.actor_identity, "audience": binding.audience, "path": path, "state": "RECOVERABLE"})
raise ProvisioningError("refresh_apply_failed", 503) from exc
def _state(self, operation_id: str, state: str, oid: str | None = None) -> None:
with self._ledger() as db:
db.execute("UPDATE refreshes SET state=?, result_oid=COALESCE(?,result_oid) WHERE operation_id=?", (state, oid, operation_id))
def _sandbox_preflight(state_dir: Path) -> int:
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED") != "false":
raise ProvisioningError("global_gitops_writes_must_remain_disabled", 500)
state_dir.mkdir(parents=True, exist_ok=True)
audit = AppendOnlyAudit(state_dir / "preflight-audit.jsonl")
audit.append({"event": "SANDBOX_PREFLIGHT", "writes_enabled": False})
print(json.dumps({"status": "READY", "mode": "sandbox", "writes_enabled": False}, sort_keys=True))
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--sandbox-preflight", action="store_true", required=True)
parser.add_argument("--state-dir", type=Path, required=True)
options = parser.parse_args()
raise SystemExit(_sandbox_preflight(options.state_dir))
@@ -27,7 +27,12 @@ POLICY_FIELDS = frozenset(
)
OPERATION_FIELDS = frozenset({"exact_paths", "max_content_bytes", "content_profile"})
SUPPORTED_OPERATIONS = frozenset({"commit_file.atomic"})
SUPPORTED_CONTENT_PROFILES = frozenset({"systemd-codex-review-worker-v1"})
SUPPORTED_CONTENT_PROFILES = frozenset(
{
"systemd-codex-review-worker-v1",
"candidate-session-broker-source-v1",
}
)
_SECRET_RE = re.compile(
r"(?i)(authorization\s*:|bearer\s+|(?:token|password|secret)\s*[:=])"
)
@@ -353,9 +358,73 @@ def validate_content(operation: str, path: str, content: str) -> None:
if selected.content_profile == "systemd-codex-review-worker-v1":
_validate_systemd_codex_review_worker(content)
return
if selected.content_profile == "candidate-session-broker-source-v1":
_validate_candidate_session_broker_source(path, content)
return
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
def _validate_candidate_session_broker_source(path: str, content: str) -> None:
if (
not isinstance(content, str)
or "\x00" in content
or "\r" in content
or not content.endswith("\n")
):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
forbidden = (
"BEGIN PRIVATE KEY",
"GITEA_DATABASE",
"gitea admin",
"ssh-keyscan",
"/home/aiman/.codex",
"/root/.codex",
"AIOS_GITOPS_ALLOW_MAIN_WRITE=true",
"AIOS_GITOPS_ALLOW_TAG_WRITE=true",
"AIOS_GITOPS_ALLOW_BASELINE_WRITE=true",
"AIOS_GITOPS_ALLOW_REGISTRY_WRITE=true",
"AIOS_TASK_PLANE_EXECUTION_ENABLED=true",
"AIOS_SILENT_RELEASE_EXECUTION_ENABLED=true",
)
if any(value in content for value in forbidden):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
if path.endswith(".py"):
try:
compile(content, path, "exec")
except (SyntaxError, ValueError, TypeError):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected") from None
return
if path.endswith(".service"):
if _SECRET_RE.search(content):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
required = (
"User=aiman",
"Group=gitops",
"UMask=0077",
"NoNewPrivileges=true",
"PrivateTmp=true",
"PrivateDevices=true",
"ProtectSystem=strict",
"ProtectHome=true",
"ProtectKernelTunables=true",
"ProtectKernelModules=true",
"ProtectControlGroups=true",
"LockPersonality=true",
"RestrictSUIDSGID=true",
"RestrictRealtime=true",
"127.0.0.1",
)
if any(value not in content for value in required):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
for line in content.splitlines():
if line.startswith("Environment=") and re.search(
r"(?i)(token|password|secret|private[_-]?key)=", line
):
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
return
raise PathPolicyError("isolated_writer_candidate_source_profile_rejected")
def _validate_systemd_codex_review_worker(content: str) -> None:
if not isinstance(content, str):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
set -Eeuo pipefail
REPO_ROOT="${AIOS_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
BASE_REF="${AIOS_DOCS_BASE_REF:-}"
RUN_LIVE_HEALTHCHECK="${AIOS_RUN_LIVE_HEALTHCHECK:-0}"
RESULT_DIR="${AIOS_ACCEPTANCE_RESULT_DIR:-$REPO_ROOT/.aios-evidence/acceptance}"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
RUN_DIR="$RESULT_DIR/$STAMP"
SUMMARY="$RUN_DIR/summary.json"
install -d -m 0750 "$RUN_DIR"
pass=0
failed=0
warnings=0
steps=()
record() {
local name="$1" status="$2" code="$3" log="$4"
steps+=("$name|$status|$code|$log")
case "$status" in
通过) pass=$((pass + 1)) ;;
失败) failed=$((failed + 1)) ;;
警告) warnings=$((warnings + 1)) ;;
esac
printf '%s:%s(退出码 %s)\n' "$status" "$name" "$code"
}
run_step() {
local name="$1"
shift
local safe_name
safe_name="$(printf '%s' "$name" | tr ' /:' '___')"
local log="$RUN_DIR/${safe_name}.log"
set +e
(cd "$REPO_ROOT" && "$@") >"$log" 2>&1
local code=$?
set -e
if [[ "$code" -eq 0 ]]; then
record "$name" "通过" "$code" "$log"
else
record "$name" "失败" "$code" "$log"
fi
}
run_optional_step() {
local name="$1"
shift
local safe_name
safe_name="$(printf '%s' "$name" | tr ' /:' '___')"
local log="$RUN_DIR/${safe_name}.log"
set +e
(cd "$REPO_ROOT" && "$@") >"$log" 2>&1
local code=$?
set -e
if [[ "$code" -eq 0 ]]; then
record "$name" "通过" "$code" "$log"
else
record "$name" "警告" "$code" "$log"
fi
}
printf 'AIOS V2R1 Candidate 全量验收开始\n'
printf '仓库:%s\n' "$REPO_ROOT"
printf '结果目录:%s\n' "$RUN_DIR"
[[ -d "$REPO_ROOT" ]] || { echo "错误:仓库不存在" >&2; exit 2; }
DOCS_ARGS=(python3 tools/aios_v2_docs_code_check.py --repo "$REPO_ROOT")
if [[ -n "$BASE_REF" ]]; then
DOCS_ARGS+=(--base-ref "$BASE_REF")
fi
run_step "文档与代码匹配" "${DOCS_ARGS[@]}"
run_step "Python 语法编译" python3 -m compileall -q \
active-runtime/app/aios_v2 \
active-runtime/app/aios_openclaw \
active-runtime/app/aios_hermes
run_step "候选单元与集成测试" python3 -m unittest discover \
-s tests/aios_v2_candidate \
-p 'test_*.py' \
-v
run_step "Shell 脚本语法" bash -c '
set -Eeuo pipefail
while IFS= read -r -d "" path; do
bash -n "$path"
done < <(find deploy/aios-v2 -type f -name "*.sh" -print0)
'
run_step "文档 Git 冲突标记" bash -c '
set -Eeuo pipefail
if grep -RInE "^(<<<<<<<|=======|>>>>>>>)" docs/aios-v2; then
exit 1
fi
'
run_step "明文密钥基础扫描" python3 -c '
from pathlib import Path
import re
import sys
roots = [Path("active-runtime/app"), Path("deploy/aios-v2"), Path("docs/aios-v2"), Path("tasks")]
patterns = [
re.compile(r"(?i)(api[_-]?key|password|private[_-]?key|access[_-]?token)\s*[:=]\s*[\"\x27][^\"\x27\n]{12,}[\"\x27]"),
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
]
violations = []
for root in roots:
if not root.exists():
continue
for path in root.rglob("*"):
if not path.is_file() or path.suffix in {".pyc", ".db", ".sqlite3"}:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
if any(pattern.search(text) for pattern in patterns):
violations.append(str(path))
if violations:
print("疑似明文密钥文件:", *violations, sep="\n", file=sys.stderr)
raise SystemExit(1)
'
if command -v systemd-analyze >/dev/null 2>&1; then
run_optional_step "systemd 单元验证" systemd-analyze verify \
deploy/aios-v2/systemd/aios-v2.service
else
record "systemd 单元验证" "警告" "127" "未安装 systemd-analyze"
fi
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
run_optional_step "Docker Compose 配置验证" docker compose \
-f deploy/aios-v2/docker/compose.yaml config
else
record "Docker Compose 配置验证" "警告" "127" "未安装 Docker Compose"
fi
if [[ "$RUN_LIVE_HEALTHCHECK" == "1" ]]; then
run_step "在线健康检查" bash deploy/aios-v2/healthcheck/check.sh
else
record "在线健康检查" "警告" "0" "未启用 AIOS_RUN_LIVE_HEALTHCHECK=1"
fi
branch="$(git -C "$REPO_ROOT" branch --show-current 2>/dev/null || true)"
commit="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)"
dirty="$(git -C "$REPO_ROOT" status --porcelain=v1 2>/dev/null || true)"
python3 - "$SUMMARY" "$branch" "$commit" "$dirty" "$pass" "$failed" "$warnings" "${steps[@]}" <<'PY'
import json
import pathlib
import sys
from datetime import datetime, timezone
summary_path = pathlib.Path(sys.argv[1])
branch, commit, dirty = sys.argv[2:5]
passed, failed, warnings = map(int, sys.argv[5:8])
steps = []
for item in sys.argv[8:]:
name, status, code, log = item.split("|", 3)
steps.append({"name": name, "status": status, "exit_code": int(code), "log": log})
payload = {
"schema": "aios.v2.acceptance.v1",
"generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"branch": branch or None,
"commit": commit or None,
"workspace_dirty": bool(dirty),
"passed": passed,
"failed": failed,
"warnings": warnings,
"steps": steps,
"final_status": "passed" if failed == 0 else "failed",
}
summary_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
printf '验收结果:通过 %s,失败 %s,警告 %s\n' "$pass" "$failed" "$warnings"
printf '证据:%s\n' "$SUMMARY"
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
+24
View File
@@ -0,0 +1,24 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/opt/aios/app \
AIOS_V2_HOST=0.0.0.0 \
AIOS_V2_PORT=18920 \
AIOS_V2_STATE_DIR=/var/lib/aios-v2
RUN groupadd --system --gid 12001 aios \
&& useradd --system --uid 12001 --gid aios --home-dir /nonexistent --shell /usr/sbin/nologin aios \
&& install -d -o aios -g aios -m 0750 /opt/aios/app /var/lib/aios-v2 \
&& python3 -m pip install --no-cache-dir 'cryptography>=42'
WORKDIR /opt/aios/app
COPY --chown=aios:aios active-runtime/app/ /opt/aios/app/
USER aios:aios
EXPOSE 18920
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=4 \
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18920/api/v2/health', timeout=3).read()"
CMD ["python3", "-m", "aios_v2.app"]
+44
View File
@@ -0,0 +1,44 @@
services:
aios-v2:
build:
context: ../../..
dockerfile: deploy/aios-v2/docker/Dockerfile
image: aios-v2:V2R1C0-candidate
init: true
restart: unless-stopped
ports:
- "127.0.0.1:${AIOS_V2_PORT:-18920}:18920"
environment:
AIOS_V2_HOST: 0.0.0.0
AIOS_V2_PORT: 18920
AIOS_V2_STATE_DIR: /var/lib/aios-v2
AIOS_OPENCLAW_URL: ${AIOS_OPENCLAW_URL:-}
AIOS_OPENCLAW_CREDENTIAL_REF: ${AIOS_OPENCLAW_CREDENTIAL_REF:-}
AIOS_OPENCLAW_SIGNING_MATERIAL_FILE: ${AIOS_OPENCLAW_SIGNING_MATERIAL_FILE:-}
AIOS_RELEASE_CHANNEL: candidate
AIOS_RELEASE_VERSION: V2R1C0-candidate
volumes:
- aios-v2-state:/var/lib/aios-v2
# Mount runtime signing material explicitly when OpenClaw is enabled.
# The host file must be mode 0600; never store it in this repository.
# - /etc/aios-v2/openclaw-signing-material:/run/secrets/openclaw-signing-material:ro
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=64m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test:
- CMD
- python3
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18920/api/v2/health', timeout=3).read()"
interval: 15s
timeout: 5s
retries: 4
start_period: 10s
volumes:
aios-v2-state:
+45
View File
@@ -0,0 +1,45 @@
# AIOS V2R1 Candidate 环境模板
# 正式环境禁止保存明文 Secret。
# Provenance/OpenClaw 私钥只用于签发端,不进入应用容器或 Git。
# Runtime
AIOS_V2_HOST=127.0.0.1
AIOS_V2_PORT=18920
AIOS_V2_STATE_DIR=/var/lib/aios-v2
# OpenClaw Ed25519 Command v2
AIOS_OPENCLAW_URL=
AIOS_OPENCLAW_CREDENTIAL_REF=
AIOS_OPENCLAW_SIGNING_MATERIAL_FILE=/run/credentials/aios-v2.service/openclaw-command-private-key
AIOS_OPENCLAW_SIGNING_KEY_ID=openclaw-v2r1-candidate-ed25519
# Parent Workflow Provenance
AIOS_REQUIRE_RUN_PROVENANCE=true
AIOS_PROVENANCE_PUBLIC_KEY_FILE=/etc/aios-v2/provenance/public-key.pem
AIOS_PROVENANCE_KEY_ID=openclaw-v2r1-candidate-ed25519
AIOS_PROVENANCE_STORE_FILE=/var/lib/aios-v2/run-provenance.sqlite3
AIOS_PROVENANCE_RECEIPT_DIR=/var/lib/aios-v2/provenance-receipts
# Least-privilege child Grants: production requires all seven IDs.
AIOS_PROVENANCE_GRANT_CODE_CHANGE_ID=
AIOS_PROVENANCE_GRANT_GIT_ID=
AIOS_PROVENANCE_GRANT_DEPLOYMENT_ID=
AIOS_PROVENANCE_GRANT_RUNTIME_ID=
AIOS_PROVENANCE_GRANT_EVIDENCE_ID=
AIOS_PROVENANCE_GRANT_ROLLBACK_ID=
AIOS_PROVENANCE_GRANT_AUDIT_ID=
# Per-operation context. Deployment/rollback wrappers populate these from the
# approved task execution context; do not hard-code shared production values.
AIOS_PROVENANCE_ID=
AIOS_RUN_ID=
AIOS_TENANT_ID=
AIOS_TASK_ID=
AIOS_AGENT_ID=
AIOS_PRINCIPAL_ID=
# 禁止:
# AIOS_PROVENANCE_PRIVATE_KEY_FILE
# AIOS_OPENCLAW_TOKEN
# API_KEY=value
# PASSWORD=value
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BASE_URL="${AIOS_V2_BASE_URL:-http://127.0.0.1:18920}"
STATE_DIR="${AIOS_V2_STATE_DIR:-/var/lib/aios-v2}"
CONTROLLER_WT="${AIOS_GITOPS_CONTROLLER_WORKTREE:-/data/aios-gitops/worktrees/controller}"
SERVICE="${AIOS_V2_SERVICE:-aios-v2}"
fail() {
echo "FAIL: $*" >&2
exit 1
}
pass() {
echo "PASS: $*"
}
command -v python3 >/dev/null || fail "python3 not installed"
if command -v systemctl >/dev/null && systemctl list-unit-files "$SERVICE.service" >/dev/null 2>&1; then
systemctl is-active --quiet "$SERVICE" || fail "$SERVICE is not active"
pass "$SERVICE is active"
fi
python3 - "$BASE_URL" <<'PY'
import json
import sys
import urllib.request
base_url = sys.argv[1].rstrip("/")
with urllib.request.urlopen(f"{base_url}/api/v2/health", timeout=5) as response:
payload = json.loads(response.read().decode("utf-8"))
if payload.get("status") not in {"ok", "healthy", "ready"}:
raise SystemExit(f"unexpected health payload: {payload}")
print(f"PASS: HTTP health status={payload.get('status')}")
PY
[[ -d "$STATE_DIR" ]] || fail "state directory missing: $STATE_DIR"
[[ -w "$STATE_DIR" ]] || fail "state directory not writable: $STATE_DIR"
pass "state directory writable"
if [[ -e "$STATE_DIR/control-plane.sqlite3" ]]; then
python3 - "$STATE_DIR/control-plane.sqlite3" <<'PY'
import sqlite3
import sys
path = sys.argv[1]
connection = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=5)
try:
row = connection.execute("PRAGMA quick_check").fetchone()
finally:
connection.close()
if not row or row[0] != "ok":
raise SystemExit(f"SQLite quick_check failed: {row}")
print("PASS: SQLite quick_check=ok")
PY
else
echo "WARN: control-plane.sqlite3 not created yet"
fi
if [[ -d "$CONTROLLER_WT" ]] && git -C "$CONTROLLER_WT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
branch="$(git -C "$CONTROLLER_WT" branch --show-current)"
dirty="$(git -C "$CONTROLLER_WT" status --porcelain=v1)"
[[ -z "$branch" ]] || fail "GitOps controller attached to branch: $branch"
[[ -z "$dirty" ]] || fail "GitOps controller workspace is dirty"
pass "GitOps controller detached and clean"
fi
python3 - "$STATE_DIR/executions" <<'PY'
import json
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
if not root.exists():
print("WARN: execution evidence directory not created yet")
raise SystemExit(0)
for path in root.glob("*/*.json"):
value = json.loads(path.read_text(encoding="utf-8"))
for key in ("execution_id", "tenant_id", "task_id", "executor", "status"):
if not value.get(key):
raise SystemExit(f"invalid execution evidence {path}: missing {key}")
print("PASS: execution evidence files valid")
PY
pass "AIOS V2 candidate healthcheck complete"
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -Eeuo pipefail
OPERATION_PATH="${1:-}"
OPERATION_NAME="${2:-}"
REQUEST_FILE="${3:-}"
fail() {
echo "PROVENANCE_DENIED: $*" >&2
exit 1
}
[[ -n "$OPERATION_PATH" ]] || fail "usage: $0 <deployment|rollback|audit|code_change|runtime_execute|evidence_write> <operation> <request-json-file>"
[[ -n "$OPERATION_NAME" ]] || fail "operation name is required"
[[ -n "$REQUEST_FILE" && -f "$REQUEST_FILE" ]] || fail "request JSON file is required"
: "${AIOS_PROVENANCE_PUBLIC_KEY_FILE:?missing AIOS_PROVENANCE_PUBLIC_KEY_FILE}"
: "${AIOS_PROVENANCE_KEY_ID:?missing AIOS_PROVENANCE_KEY_ID}"
: "${AIOS_PROVENANCE_STORE_FILE:?missing AIOS_PROVENANCE_STORE_FILE}"
: "${AIOS_PROVENANCE_RECEIPT_DIR:?missing AIOS_PROVENANCE_RECEIPT_DIR}"
: "${AIOS_PROVENANCE_ID:?missing AIOS_PROVENANCE_ID}"
: "${AIOS_RUN_ID:?missing AIOS_RUN_ID}"
: "${AIOS_TENANT_ID:?missing AIOS_TENANT_ID}"
: "${AIOS_TASK_ID:?missing AIOS_TASK_ID}"
: "${AIOS_AGENT_ID:?missing AIOS_AGENT_ID}"
: "${AIOS_PRINCIPAL_ID:?missing AIOS_PRINCIPAL_ID}"
PYTHONPATH_VALUE="${AIOS_V2_PYTHONPATH:-/opt/aios/current/active-runtime/app}"
PYTHONPATH="$PYTHONPATH_VALUE${PYTHONPATH:+:$PYTHONPATH}" \
python3 -m aios_v2.provenance_cli authorize \
--context-from-env \
--store-file "$AIOS_PROVENANCE_STORE_FILE" \
--public-key-file "$AIOS_PROVENANCE_PUBLIC_KEY_FILE" \
--key-id "$AIOS_PROVENANCE_KEY_ID" \
--receipt-dir "$AIOS_PROVENANCE_RECEIPT_DIR" \
--path "$OPERATION_PATH" \
--operation "$OPERATION_NAME" \
--request-file "$REQUEST_FILE"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SERVICE="${AIOS_V2_SERVICE:-aios-v2}"
RELEASE_ROOT="${AIOS_V2_RELEASE_ROOT:-/opt/aios/releases}"
CURRENT_LINK="${AIOS_V2_CURRENT_LINK:-/opt/aios/current}"
STATE_DIR="${AIOS_V2_STATE_DIR:-/var/lib/aios-v2}"
BACKUP_ROOT="${AIOS_V2_BACKUP_ROOT:-/var/backups/aios-v2}"
TARGET_RELEASE="${1:-}"
RESTORE_STATE="${AIOS_V2_RESTORE_STATE:-0}"
HEALTH_RETRIES="${AIOS_V2_ROLLBACK_HEALTH_RETRIES:-30}"
HEALTH_RETRY_DELAY="${AIOS_V2_ROLLBACK_HEALTH_RETRY_DELAY:-1}"
fail() {
echo "ERROR: $*" >&2
exit 1
}
run_healthcheck() {
local release="$1"
local attempt
for ((attempt = 1; attempt <= HEALTH_RETRIES; attempt++)); do
if AIOS_V2_SERVICE="$SERVICE" \
AIOS_V2_STATE_DIR="$STATE_DIR" \
"$release/deploy/aios-v2/healthcheck/check.sh"; then
return 0
fi
[[ "$attempt" -lt "$HEALTH_RETRIES" ]] && sleep "$HEALTH_RETRY_DELAY"
done
return 1
}
[[ "${EUID}" -eq 0 ]] || fail "rollback must run as root"
[[ -n "$TARGET_RELEASE" ]] || fail "usage: $0 <release-name-or-absolute-path>"
if [[ "$TARGET_RELEASE" = /* ]]; then
TARGET_PATH="$(readlink -f "$TARGET_RELEASE")"
else
[[ "$TARGET_RELEASE" != *"/"* && "$TARGET_RELEASE" != "." && "$TARGET_RELEASE" != ".." ]] \
|| fail "invalid release name"
TARGET_PATH="$(readlink -f "$RELEASE_ROOT/$TARGET_RELEASE")"
fi
[[ -d "$TARGET_PATH/active-runtime/app" ]] || fail "target release is invalid: $TARGET_PATH"
[[ -f "$TARGET_PATH/deploy/aios-v2/healthcheck/check.sh" ]] \
|| fail "target release has no healthcheck"
PREVIOUS_PATH=""
if [[ -L "$CURRENT_LINK" ]]; then
PREVIOUS_PATH="$(readlink -f "$CURRENT_LINK")"
fi
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
BACKUP_DIR="$BACKUP_ROOT/rollback-$STAMP"
install -d -m 0750 "$BACKUP_DIR"
if [[ -d "$STATE_DIR" ]]; then
tar --xattrs --acls -C "$(dirname "$STATE_DIR")" \
-czf "$BACKUP_DIR/state-before-rollback.tar.gz" \
"$(basename "$STATE_DIR")"
fi
printf '%s\n' "$PREVIOUS_PATH" > "$BACKUP_DIR/previous-release.txt"
printf '%s\n' "$TARGET_PATH" > "$BACKUP_DIR/target-release.txt"
systemctl stop "$SERVICE"
ln -sfn "$TARGET_PATH" "${CURRENT_LINK}.next"
mv -Tf "${CURRENT_LINK}.next" "$CURRENT_LINK"
systemctl daemon-reload
systemctl start "$SERVICE"
if run_healthcheck "$TARGET_PATH"; then
echo "ROLLBACK_COMPLETE target=$TARGET_PATH backup=$BACKUP_DIR"
exit 0
fi
echo "Target release failed healthcheck; restoring previous release." >&2
systemctl stop "$SERVICE" || true
if [[ -n "$PREVIOUS_PATH" && -d "$PREVIOUS_PATH" ]]; then
ln -sfn "$PREVIOUS_PATH" "${CURRENT_LINK}.previous"
mv -Tf "${CURRENT_LINK}.previous" "$CURRENT_LINK"
fi
if [[ "$RESTORE_STATE" == "1" && -f "$BACKUP_DIR/state-before-rollback.tar.gz" ]]; then
rm -rf "$STATE_DIR"
tar --xattrs --acls -C "$(dirname "$STATE_DIR")" \
-xzf "$BACKUP_DIR/state-before-rollback.tar.gz"
fi
systemctl daemon-reload
systemctl start "$SERVICE" || true
if [[ -n "$PREVIOUS_PATH" && -x "$PREVIOUS_PATH/deploy/aios-v2/healthcheck/check.sh" ]]; then
run_healthcheck "$PREVIOUS_PATH" || true
fi
fail "rollback target failed; previous release restored; evidence=$BACKUP_DIR"
+37
View File
@@ -0,0 +1,37 @@
[Unit]
Description=AIOS V2 Control Plane Candidate
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=aios
Group=aios
WorkingDirectory=/opt/aios/current/active-runtime/app
EnvironmentFile=-/etc/aios-v2/aios-v2.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 -m aios_v2.app
Restart=on-failure
RestartSec=3
TimeoutStartSec=30
TimeoutStopSec=30
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadWritePaths=/var/lib/aios-v2
StateDirectory=aios-v2
StateDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,30 @@
# AIOS V2R1C2 Unified Portal Candidate
# Install as /etc/aios-v2/aios-v2r1c2-portal.env with mode 0640.
# Do not place secret values in this file. Use secret references only.
AIOS_V2_HOST=127.0.0.1
AIOS_V2_PORT=18921
AIOS_V2_STATE_DIR=/var/lib/aios-v2r1c2-portal
AIOS_PROVENANCE_STORE_FILE=/var/lib/aios-v2r1c2-portal/run-provenance.sqlite3
AIOS_EXTERNAL_TASK_STORE_FILE=/var/lib/aios-v2r1c2-portal/external-task-truth.sqlite3
AIOS_EXECUTOR_CATALOG_FILE=/var/lib/aios-v2r1c2-portal/executor-catalog.json
AIOS_EXECUTION_PLAN_FILES=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json:/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_EXECUTION_PLAN_FILE=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_RELEASE_VERSION=V2R1C2-CANDIDATE
AIOS_TARGET_VERSION=V2R1C2
AIOS_BASELINE_VERSION=V2R1C1
AIOS_PREVIOUS_VERSION=V2R1C1
AIOS_RELEASE_STATUS=CANDIDATE_VALIDATION
AIOS_RELEASE_SOURCE_OID=REPLACE_WITH_EXACT_40_CHARACTER_GIT_OID
AIOS_RELEASE_OID=candidate-not-tagged
AIOS_RELEASE_TAG=NOT_CREATED
AIOS_RELEASE_EVIDENCE_SHA256=pending
AIOS_OPENCLAW_CONTRACT_VERSION=aios.openclaw.command.v2
AIOS_OPENCLAW_SIGNATURE_ALGORITHM=Ed25519
AIOS_OPENCLAW_PLUGIN_VERSION=2.1.0-rc.4
AIOS_OPENCLAW_SIGNING_KEY_ID=openclaw-v2r1-candidate-ed25519
# Runtime resolves this reference through its secret store.
AIOS_OWNER_WECHAT_TARGET_REF=AIOS_OWNER_WECHAT_TARGET_REF
@@ -1,28 +1,28 @@
[Unit]
Description=AIOS V2R1C4 Unified Portal Candidate
Description=AIOS V2R1C2 Unified Portal Candidate
After=network-online.target
Wants=network-online.target
ConditionPathExists=/opt/aios/releases/V2R1C4/active-runtime/app/aios_web/release_application.py
ConditionPathExists=/opt/aios-v2r1c2/active-runtime/app/aios_web/release_application.py
ConditionPathExists=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json
ConditionPathExists=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
[Service]
Type=simple
User=aios
Group=aios
WorkingDirectory=/opt/aios/releases/V2R1C4/active-runtime/app
EnvironmentFile=/etc/aios-v2/aios-v2r1c4-candidate.env
WorkingDirectory=/opt/aios-v2r1c2/active-runtime/app
EnvironmentFile=/etc/aios-v2/aios-v2r1c2-portal.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStartPre=/usr/bin/test "$AIOS_V2_PORT" = "18921"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_VERSION" = "V2R1C4"
ExecStartPre=/usr/bin/test -n "$AIOS_RELEASE_SOURCE_OID"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_SOURCE_OID" != "candidate-not-tagged"
ExecStart=/usr/bin/python3 -m aios_web.release_application
Restart=on-failure
RestartSec=3
TimeoutStartSec=60
TimeoutStopSec=30
KillSignal=SIGTERM
LimitCORE=0
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
@@ -34,9 +34,8 @@ RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadOnlyPaths=/opt/aios/releases/V2R1C4
ReadWritePaths=/var/lib/aios-v2r1c4-candidate
StateDirectory=aios-v2r1c4-candidate
ReadWritePaths=/var/lib/aios-v2r1c2-portal
StateDirectory=aios-v2r1c2-portal
StateDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
@@ -1,2 +0,0 @@
[Service]
LimitCORE=0
@@ -1,45 +0,0 @@
[Unit]
Description=AIOS V2R1C4 Unified Portal Production
After=network-online.target
Wants=network-online.target
ConditionPathExists=/opt/aios/releases/V2R1C4/active-runtime/app/aios_web/release_application.py
[Service]
Type=simple
User=aios
Group=aios
WorkingDirectory=/opt/aios/releases/V2R1C4/active-runtime/app
EnvironmentFile=/etc/aios-v2/aios-v2r1c4-production.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStartPre=/usr/bin/test "$AIOS_V2_PORT" = "18920"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_VERSION" = "V2R1C4"
ExecStartPre=/usr/bin/test -n "$AIOS_RELEASE_SOURCE_OID"
ExecStart=/usr/bin/python3 -m aios_web.release_application
Restart=on-failure
RestartSec=3
TimeoutStartSec=60
TimeoutStopSec=30
KillSignal=SIGTERM
LimitCORE=0
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadOnlyPaths=/opt/aios/releases/V2R1C4
ReadWritePaths=/var/lib/aios-v2r1c4-production
StateDirectory=aios-v2r1c4-production
StateDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
@@ -1,12 +0,0 @@
[Unit]
Description=Watch natural AIOS V2R1C4 Protected Dispatch evidence
After=openclaw-gateway-v2r1c4.service aios-v2r1c4-production.service
[Path]
PathChanged=/var/lib/aios/openclaw-session-v2r1c4/agents/main/sessions
PathChanged=/var/lib/aios/openclaw-provenance-v2r1c4/issued/.witnesses
PathChanged=/var/lib/aios/openclaw-session-v2r1c4/tmp
Unit=aios-v2r1c4-protected-dispatch-evidence-collector.service
[Install]
WantedBy=multi-user.target
@@ -1,53 +0,0 @@
[Unit]
Description=AIOS V2R1C4 natural Protected Dispatch evidence collector
After=openclaw-gateway-v2r1c4.service aios-v2r1c4-production.service
Wants=openclaw-gateway-v2r1c4.service aios-v2r1c4-production.service
[Service]
Type=oneshot
User=root
Group=root
UMask=0027
Environment=PYTHONDONTWRITEBYTECODE=1
EnvironmentFile=/etc/aios/v2r1c4-evidence-collector.env
ExecStartPre=/usr/bin/sleep 3
ExecStart=/usr/bin/python3 -B /opt/aios/releases/V2R1C4/tools/v2r1c4_protected_dispatch_collector.py --once
TimeoutStartSec=180
Nice=10
LimitCORE=0
NoNewPrivileges=yes
PrivateNetwork=yes
RestrictAddressFamilies=AF_UNIX
CapabilityBoundingSet=CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_DAC_READ_SEARCH
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
PrivateDevices=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
ReadOnlyPaths=/opt/aios/releases/V2R1C4
ReadOnlyPaths=/var/lib/aios/openclaw-session-v2r1c4/agents/main/sessions
ReadOnlyPaths=/var/lib/aios/openclaw-session-v2r1c4/state
ReadOnlyPaths=/var/lib/aios/openclaw-session-v2r1c4/openclaw-weixin
ReadOnlyPaths=/var/lib/aios/openclaw-session-v2r1c4/openclaw.json
ReadOnlyPaths=/var/lib/aios/openclaw-session-v2r1c4/tmp
ReadOnlyPaths=/var/lib/aios/openclaw-provenance-v2r1c4/issued
ReadOnlyPaths=/var/lib/aios/openclaw-provenance-v2r1c4/public
ReadOnlyPaths=-/run/log/journal
ReadOnlyPaths=-/var/log/journal
ReadOnlyPaths=-/var/lib/systemd/coredump
ReadOnlyPaths=-/var/crash
ReadWritePaths=/var/lib/aios/openclaw-session-v2r1c4/evidence/v2r1c4-protected-dispatch-acceptance
[Install]
WantedBy=multi-user.target
@@ -1,50 +0,0 @@
[Unit]
Description=OpenClaw Gateway V2R1C4 isolated candidate
After=network-online.target
Wants=network-online.target
ConditionPathExists=/opt/aios/releases/V2R1C4/openclaw/openclaw.mjs
ConditionPathExists=/var/lib/aios/openclaw-session-v2r1c4/openclaw.json
ConditionPathIsDirectory=/var/lib/aios/openclaw-session-v2r1c4/tmp
[Service]
Type=simple
EnvironmentFile=/etc/aios/openclaw-gateway-v2r1c4.env
Environment=OPENCLAW_HOME=/var/lib/aios/openclaw-session-v2r1c4/home
Environment=OPENCLAW_STATE_DIR=/var/lib/aios/openclaw-session-v2r1c4
Environment=OPENCLAW_CONFIG_PATH=/var/lib/aios/openclaw-session-v2r1c4/openclaw.json
Environment=AIOS_GITOPS_WRITES_ENABLED=false
WorkingDirectory=/var/lib/aios/openclaw-session-v2r1c4
ExecStart=/usr/bin/node /opt/aios/releases/V2R1C4/openclaw/openclaw.mjs gateway run --port 18790 --bind loopback --auth none --allow-unconfigured
Restart=on-failure
RestartSec=5
User=aios
Group=aios
KillMode=control-group
LimitCORE=0
UMask=0027
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/opt/aios/releases/V2R1C4
ReadWritePaths=/var/lib/aios/openclaw-session-v2r1c4
ReadWritePaths=/var/lib/aios/openclaw-provenance-v2r1c4
BindPaths=/var/lib/aios/openclaw-session-v2r1c4/tmp:/tmp/openclaw
[Install]
WantedBy=multi-user.target
@@ -1,51 +0,0 @@
[Unit]
Description=OpenClaw Gateway V2R1C4 with pseudonymous Weixin state
After=network-online.target
Wants=network-online.target
Conflicts=openclaw-gateway.service
ConditionPathExists=/opt/aios/releases/V2R1C4/openclaw/openclaw.mjs
ConditionPathExists=/var/lib/aios/openclaw-session-v2r1c4/openclaw.json
ConditionPathIsDirectory=/var/lib/aios/openclaw-session-v2r1c4/tmp
[Service]
Type=simple
EnvironmentFile=/etc/aios/openclaw-gateway-v2r1c4.env
Environment=OPENCLAW_HOME=/var/lib/aios/openclaw-session-v2r1c4/home
Environment=OPENCLAW_STATE_DIR=/var/lib/aios/openclaw-session-v2r1c4
Environment=OPENCLAW_CONFIG_PATH=/var/lib/aios/openclaw-session-v2r1c4/openclaw.json
Environment=AIOS_GITOPS_WRITES_ENABLED=false
WorkingDirectory=/var/lib/aios/openclaw-session-v2r1c4
ExecStart=/usr/bin/node /opt/aios/releases/V2R1C4/openclaw/openclaw.mjs gateway run --port 18789 --bind loopback --auth none --allow-unconfigured
Restart=always
RestartSec=5
User=aios
Group=aios
KillMode=control-group
LimitCORE=0
UMask=0027
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/opt/aios/releases/V2R1C4
ReadWritePaths=/var/lib/aios/openclaw-session-v2r1c4
ReadWritePaths=/var/lib/aios/openclaw-provenance-v2r1c4
BindPaths=/var/lib/aios/openclaw-session-v2r1c4/tmp:/tmp/openclaw
[Install]
WantedBy=multi-user.target
@@ -1,3 +0,0 @@
AIOS_V2R1C4_SOURCE_OID=<40-lowercase-hex-source-oid>
AIOS_V2R1C4_INDEX_SHA256=<64-lowercase-hex-index-sha256>
AIOS_V2R1C4_POLICY_SHA256=<64-lowercase-hex-policy-sha256>
@@ -1,47 +0,0 @@
#!/bin/sh
set -eu
if [ "$(id -u)" -ne 0 ]; then
echo "install-fastlane-candidate must run as root" >&2
exit 77
fi
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
REPOSITORY_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd -P)
test -f "$REPOSITORY_ROOT/schemas/aios-fastlane-release-plan-v1.schema.json"
test -f "$REPOSITORY_ROOT/services/aios-release-controller/aios_release_controller/cli.py"
test -f "$REPOSITORY_ROOT/services/aios-deploy-helper/aios_deploy_helper.py"
if ! getent group aios-release >/dev/null 2>&1; then
/usr/sbin/groupadd --system aios-release
fi
if ! getent passwd aios-release >/dev/null 2>&1; then
/usr/sbin/useradd --system --gid aios-release --home-dir /var/lib/aios-release --shell /usr/sbin/nologin aios-release
fi
/usr/bin/install -d -o root -g root -m 0700 \
/var/lib/aios/openclaw-session-v2r1c4 \
/var/lib/aios/openclaw-provenance-v2r1c4
/usr/bin/install -d -o root -g root -m 0755 /etc/aios /etc/aios-v2
/usr/bin/install -d -o root -g root -m 0755 /usr/lib/aios-release-controller/aios_release_controller
for source in "$REPOSITORY_ROOT"/services/aios-release-controller/aios_release_controller/*.py; do
name=${source##*/}
/usr/bin/install -o root -g root -m 0644 "$source" "/usr/lib/aios-release-controller/aios_release_controller/$name"
done
/usr/bin/install -o root -g root -m 0755 "$REPOSITORY_ROOT/deploy/fastlane/libexec/aios-release-controller" /usr/libexec/aios-release-controller
/usr/bin/install -o root -g root -m 0755 "$REPOSITORY_ROOT/services/aios-deploy-helper/aios_deploy_helper.py" /usr/libexec/aios-deploy-helper
for unit in "$REPOSITORY_ROOT"/deploy/fastlane/systemd/*.service; do
/usr/bin/install -o root -g root -m 0644 "$unit" "/etc/systemd/system/${unit##*/}"
done
/usr/bin/install -o root -g root -m 0644 "$REPOSITORY_ROOT/deploy/fastlane/polkit/49-aios-release-controller.rules" /etc/polkit-1/rules.d/49-aios-release-controller.rules
/usr/bin/install -o root -g root -m 0644 "$REPOSITORY_ROOT/deploy/fastlane/tmpfiles.d/aios-release.conf" /etc/tmpfiles.d/aios-release.conf
/usr/bin/systemd-tmpfiles --create /etc/tmpfiles.d/aios-release.conf
/usr/bin/systemctl daemon-reload
echo "candidate_install=PASS"
echo "controller_started=false"
echo "production_changed=false"
@@ -1,10 +0,0 @@
#!/usr/bin/python3
from __future__ import annotations
import sys
sys.path.insert(0, "/usr/lib/aios-release-controller")
from aios_release_controller.cli import main
raise SystemExit(main())
@@ -1,16 +0,0 @@
polkit.addRule(function(action, subject) {
if (action.id !== "org.freedesktop.systemd1.manage-units" || subject.user !== "aios-release") {
return polkit.Result.NOT_HANDLED;
}
var allowed = [
"aios-deploy-helper-health-check.service",
"aios-deploy-helper-install-candidate.service",
"aios-deploy-helper-promote.service",
"aios-deploy-helper-rollback.service",
"aios-deploy-helper-verify.service"
];
if (action.lookup("verb") === "start" && allowed.indexOf(action.lookup("unit")) !== -1) {
return polkit.Result.YES;
}
return polkit.Result.NOT_HANDLED;
});
@@ -1,33 +0,0 @@
[Unit]
Description=AIOS V2R1C4 candidate health check
After=aios-v2r1c4-candidate.service
[Service]
Type=oneshot
User=root
UMask=0077
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sig:/var/lib/aios-release/release-plan.sig
LoadCredential=kms-public-key.der:/var/lib/aios-release/kms-public-key.der
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-deploy-helper health-check
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/usr/libexec/aios-deploy-helper
ReadWritePaths=/var/lib/aios-release
@@ -1,42 +0,0 @@
[Unit]
Description=AIOS V2R1C4 install isolated candidate
ConditionPathExists=/var/lib/aios-release/release-plan.json
[Service]
Type=oneshot
User=root
UMask=0077
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sig:/var/lib/aios-release/release-plan.sig
LoadCredential=kms-public-key.der:/var/lib/aios-release/kms-public-key.der
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-deploy-helper install-candidate
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/usr/libexec/aios-deploy-helper
ReadOnlyPaths=/var/lib/aios-release/artifacts
ReadOnlyPaths=/var/lib/aios/openclaw-session
ReadOnlyPaths=/var/lib/aios/openclaw-provenance
ReadWritePaths=/opt/aios/releases
ReadWritePaths=/etc/systemd/system
ReadWritePaths=/etc/aios-v2
ReadWritePaths=/etc/aios
ReadWritePaths=/var/lib/aios-release
ReadWritePaths=/var/lib/aios/openclaw-session-v2r1c4
ReadWritePaths=/var/lib/aios/openclaw-provenance-v2r1c4
@@ -1,34 +0,0 @@
[Unit]
Description=AIOS V2R1C4 promote candidate to production
After=aios-v2r1c4-candidate.service
[Service]
Type=oneshot
User=root
UMask=0077
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sig:/var/lib/aios-release/release-plan.sig
LoadCredential=kms-public-key.der:/var/lib/aios-release/kms-public-key.der
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-deploy-helper promote
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/usr/libexec/aios-deploy-helper
ReadWritePaths=/etc/systemd/system
ReadWritePaths=/var/lib/aios-release
@@ -1,32 +0,0 @@
[Unit]
Description=AIOS V2R1C4 rollback to frozen V2R1C3
[Service]
Type=oneshot
User=root
UMask=0077
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sig:/var/lib/aios-release/release-plan.sig
LoadCredential=kms-public-key.der:/var/lib/aios-release/kms-public-key.der
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-deploy-helper rollback
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/usr/libexec/aios-deploy-helper
ReadWritePaths=/var/lib/aios-release
@@ -1,32 +0,0 @@
[Unit]
Description=AIOS V2R1C4 verify active production target
[Service]
Type=oneshot
User=root
UMask=0077
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sig:/var/lib/aios-release/release-plan.sig
LoadCredential=kms-public-key.der:/var/lib/aios-release/kms-public-key.der
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-deploy-helper verify
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/usr/libexec/aios-deploy-helper
ReadWritePaths=/var/lib/aios-release
@@ -1,45 +0,0 @@
[Unit]
Description=AIOS V2R1C4 Fastlane Release Controller
After=network-online.target
Wants=network-online.target
ConditionPathExists=/var/lib/aios-release/release-plan.json
[Service]
Type=oneshot
User=aios-release
Group=aios-release
SupplementaryGroups=gitops
UMask=0077
StateDirectory=aios-release
StateDirectoryMode=0700
LoadCredentialEncrypted=gitea-task-token:/etc/credstore.encrypted/aios-v2r1c4-gitea-task-token
LoadCredentialEncrypted=aws-workload-identity-reference:/etc/credstore.encrypted/aios-v2r1c4-aws-workload-identity-reference
LoadCredential=release-plan:/var/lib/aios-release/release-plan.json
LoadCredential=release-plan.sha256:/var/lib/aios-release/release-plan.sha256
LoadCredential=kms-binding:/var/lib/aios-release/kms-binding.json
ExecStart=/usr/libexec/aios-release-controller run
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
ReadWritePaths=/data/aios-gitops/repo.git
ReadOnlyPaths=/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
ReadOnlyPaths=/usr/libexec/aios-release-controller
ReadWritePaths=/var/lib/aios-release
SystemCallArchitectures=native
@@ -1,4 +0,0 @@
d /var/lib/aios-release 0700 aios-release aios-release -
d /var/lib/aios-release/receipts 0700 root root -
d /var/lib/aios-release/artifacts 0750 root aios-release -
d /opt/aios/releases 0755 root root -
@@ -1,71 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 022
INSTALL_ROOT="${AIOS_INSTALL_ROOT:-/}"
SOURCE_ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
PYTHON_BIN="${AIOS_PYTHON_BIN:-/usr/bin/python3}"
[[ "$INSTALL_ROOT" = /* ]] || {
echo "AIOS_INSTALL_ROOT must be absolute" >&2
exit 2
}
[[ -x "$PYTHON_BIN" ]] || {
echo "python executable unavailable: $PYTHON_BIN" >&2
exit 2
}
packages=(
"$SOURCE_ROOT/services/aios-autonomous-control-plane"
"$SOURCE_ROOT/services/aios-autonomous-release-candidate"
"$SOURCE_ROOT/services/aios-single-owner-governance"
)
for package in "${packages[@]}"; do
[[ -f "$package/pyproject.toml" ]] || {
echo "package missing: $package" >&2
exit 2
}
done
"$PYTHON_BIN" -m pip install \
--disable-pip-version-check \
--no-input \
--no-deps \
--no-build-isolation \
--root "$INSTALL_ROOT" \
--prefix /usr \
"${packages[@]}"
generated_bin="$INSTALL_ROOT/usr/local/bin"
target_bin="$INSTALL_ROOT/usr/bin"
install -d -m 0755 "$target_bin"
entrypoints=(
aios-autonomous-control-plane
aios-autonomous-release
aios-autonomous-release-service
aios-autonomous-renewal-controller
aios-development-writer-revoker
aios-development-writer-server
aios-governance-approval-watcher
aios-governance-mirror-worker
aios-governance-request-publisher
aios-release-hook
aios-release-object-projector
aios-runtime-source-projector
aios-single-owner-governance
aios-v2r1c4-release-executor
)
for entrypoint in "${entrypoints[@]}"; do
source_path="$generated_bin/$entrypoint"
[[ -x "$source_path" ]] || {
echo "generated entrypoint missing: $source_path" >&2
exit 3
}
install -m 0755 "$source_path" "$target_bin/$entrypoint"
done
echo "AIOS_GOVERNANCE_ENTRYPOINTS_INSTALLED=${#entrypoints[@]}"
echo "AIOS_GOVERNANCE_INSTALL_ROOT=$INSTALL_ROOT"
@@ -1 +0,0 @@
{"checks": {"72h_acceptance_gate": true, "approle_implemented": true, "approle_secret_not_persisted_in_hcl": true, "approle_secret_survives_isolated_restore": true, "audit_implemented": true, "autonomy_tls_readers_use_ca_only_bind": true, "backup_implemented": true, "backup_restore_share_without_openbao_private_key_group": true, "branch_protection_gate_not_bypassable": true, "charter_store_sign_verify": true, "continuous_optimization_implemented": true, "controller_uses_charter_only_bind": true, "declarative_self_initialization": true, "embedded_shell_syntax": true, "exact_checksum_duplicate_rejected": true, "exact_checksum_good_fixture": true, "exact_checksum_missing_rejected": true, "fault_injection_does_not_require_service_it_kills": true, "fault_injection_failure_phase_evidence": true, "fault_injection_implemented": true, "fault_injection_missing_seal_reaches_static_seal_validation": true, "fault_injection_systemd_sandbox": true, "gitea_1_22_6_rejected": true, "gitea_1_23_0_accepted": true, "gitea_1_27_nonempty_tag_allowlist": true, "gitea_probe_traversal_and_cleanup": true, "gitea_tag_api_version_gate": true, "isolated_restore_implemented": true, "isolated_restore_uses_private_ephemeral_audit_sink": true, "kv_v2_implemented": true, "machine_credential_persisted": true, "masked_systemd_units_do_not_change_dev_null": true, "no_git_force_push": true, "no_git_tag_mutation": true, "openbao_2_6_heredoc_compatible": true, "openbao_2_6_runtime_verifier": true, "openbao_config_memfd_injection": true, "openbao_ready_gate_not_bypassable": true, "outer_bash_syntax": true, "policy_implemented": true, "ready_not_predeclared": true, "restored_approle_login_has_bounded_readiness_retry": true, "root_token_not_persisted": true, "runtime_state_directory_remains_writable": true, "self_contained_no_sibling_preflight": true, "self_init_marker_bound_to_raft_lifecycle": true, "self_init_secret_removal_implemented": true, "systemd_credentials_mandatory": true, "systemd_runtime_directories": true, "tag_protection_gate_implemented": true, "transit_charter_key_implemented": true, "transit_runtime_key_implemented": true}, "passed": 52, "runtime_claims": false, "schema": "AIOS_OPENBAO_BOOTSTRAP_STATIC_SELF_TEST_V3", "scope": "static_and_fixture_only", "script_sha256": "f687e17b1806e4f4265013b85e2cc3b4472f508720856fc8374607e7d20dbcc6", "total": 52}
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
f687e17b1806e4f4265013b85e2cc3b4472f508720856fc8374607e7d20dbcc6 admin-bootstrap-openbao-autonomy-v1-v3.sh
-4
View File
@@ -1,4 +0,0 @@
6bd0d51fbdd1c90df2f6ea8c667edecfaf892aaf76744c41757979407af0d424 admin-bootstrap-openbao-autonomy-v1-v3.self-test.json
f687e17b1806e4f4265013b85e2cc3b4472f508720856fc8374607e7d20dbcc6 admin-bootstrap-openbao-autonomy-v1-v3.sh
b7aa70280a417cebaa0363380024dc378714267de5d2eeb681b16a4f610a6ce5 admin-bootstrap-openbao-autonomy-v1-v3.sha256
c355df01c83c2801ae3a0df43806c562ee70f97c9aa558e3f67dd097efa5ac99 test-admin-bootstrap-openbao-autonomy-v1-v3.sh
@@ -1,39 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
script="${1:-$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/admin-bootstrap-openbao-autonomy-v1-v3.sh}"
report="${2:-$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/admin-bootstrap-openbao-autonomy-v1-v3.self-test.json}"
[[ -f "$script" ]] || {
printf 'missing script: %s\n' "$script" >&2
exit 66
}
bash -n "$script"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
bash "$script" --self-test >"$tmp"
python3 - "$script" "$tmp" <<'PY'
import hashlib
import json
import pathlib
import sys
script, report = map(pathlib.Path, sys.argv[1:])
data = json.loads(report.read_text())
actual_hash = hashlib.sha256(script.read_bytes()).hexdigest()
assert data["schema"] == "AIOS_OPENBAO_BOOTSTRAP_STATIC_SELF_TEST_V3"
assert data["scope"] == "static_and_fixture_only"
assert data["runtime_claims"] is False
assert data["script_sha256"] == actual_hash
assert data["passed"] == data["total"]
assert all(data["checks"].values())
PY
install -m 0600 "$tmp" "$report"
printf 'PASS bash-n + embedded static/fixture tests\n'
printf 'report=%s\n' "$report"
sha256sum "$script"
@@ -1,42 +0,0 @@
[Unit]
Description=AIOS Autonomous Release Candidate (dormant shadow only)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=aiman
Group=gitops
WorkingDirectory=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
Environment=AIOS_CANDIDATE_LOOPBACK_HOST=127.0.0.1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.service --host 127.0.0.1 --port 19951 --state /var/lib/aios-autonomous-release-candidate/state.sqlite3
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RuntimeDirectory=aios-autonomous-release-candidate
StateDirectory=aios-autonomous-release-candidate
CacheDirectory=aios-autonomous-release-candidate
ReadOnlyPaths=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
ReadWritePaths=/run/aios-autonomous-release-candidate
ReadWritePaths=/var/lib/aios-autonomous-release-candidate
ReadWritePaths=/var/cache/aios-autonomous-release-candidate
UMask=0077
[Install]
WantedBy=multi-user.target
@@ -1,51 +0,0 @@
[Unit]
Description=AIOS Autonomous Release Candidate Worker (Dormant/Shadow Only)
After=network.target
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
[Service]
Type=simple
User=aiman
Group=gitops
UMask=0077
WorkingDirectory=/opt/aios-autonomous-release-candidate
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
Environment=AIOS_CANDIDATE_CONTROL_ENDPOINT=127.0.0.1:19951
Environment=AIOS_OPENCLAW_CREDENTIAL_REFERENCE=systemd-credential:openclaw-gateway-candidate
Environment=AIOS_CODEX_CREDENTIAL_REFERENCE=systemd-credential:codex-app-server-candidate
Environment=AIOS_NATIVE_CONTROL_CREDENTIAL_REFERENCE=systemd-credential:session-native-adapter-control-candidate
Environment=AIOS_GITEA_CREDENTIAL_REFERENCE=systemd-credential:gitea-admin-api-candidate
ImportCredential=openclaw-gateway-candidate
ImportCredential=codex-app-server-candidate
ImportCredential=session-native-adapter-control-candidate
ImportCredential=gitea-admin-api-candidate
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.worker_service --state-directory /var/lib/aios-autonomous-release-worker-candidate
StateDirectory=aios-autonomous-release-worker-candidate
RuntimeDirectory=aios-autonomous-release-worker-candidate
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
ReadWritePaths=/var/lib/aios-autonomous-release-worker-candidate
[Install]
WantedBy=multi-user.target
@@ -1,31 +0,0 @@
[Unit]
Description=AIOS autonomous execution renewal controller
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/renewal.json
[Service]
Type=simple
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-autonomous-renewal-controller --config /etc/aios-gitops/autonomous-control-plane/renewal.json
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/renewal /run/aios-gitops/autonomous-control-plane
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
@@ -1,24 +0,0 @@
[Unit]
Description=AIOS governance approval watcher
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/watcher.json
[Service]
Type=simple
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-governance-approval-watcher --config /etc/aios-gitops/autonomous-control-plane/watcher.json
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ReadOnlyPaths=/data/aios-gitops/runtime/gitea-bin/data/gitea.db
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane /run/aios-gitops/autonomous-control-plane
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,26 +0,0 @@
[Unit]
Description=AIOS dormant single-owner machine-status prerequisite
RefuseManualStop=no
[Service]
Type=oneshot
User=aiman
Group=gitops
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component machine-status-producer --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
@@ -1,31 +0,0 @@
[Unit]
Description=AIOS governance approval mirror worker
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/mirror.json
[Service]
Type=simple
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-governance-mirror-worker --config /etc/aios-gitops/autonomous-control-plane/mirror.json
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/data/aios-gitops/repo.git /var/lib/aios-gitops/autonomous-control-plane/mirror /run/aios-gitops/autonomous-control-plane
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
@@ -1,26 +0,0 @@
[Unit]
Description=AIOS dormant single-owner review adapter prerequisite
RefuseManualStop=no
[Service]
Type=oneshot
User=aiman
Group=gitops
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component owner-review-adapter --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
@@ -1,27 +0,0 @@
[Unit]
Description=AIOS governance request publisher
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/publisher.json
[Service]
Type=simple
User=aios-governance-request-publisher
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-governance-request-publisher --config /etc/aios-gitops/autonomous-control-plane/publisher.json
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-governance-request-publisher.gitea_http
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane /run/aios-gitops/autonomous-control-plane
IPAddressAllow=127.0.0.1
IPAddressDeny=any
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,20 +0,0 @@
[Unit]
Description=AIOS single-owner governance activation verifier
ConditionPathExists=/etc/aios-gitops/single-owner-activation-bindings.json
[Service]
Type=oneshot
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-single-owner-governance --verify --bindings /etc/aios-gitops/single-owner-activation-bindings.json --state-root /var/lib/aios-gitops/governance-activation --lock-path /run/aios-gitops/governance-activation.lock
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/aios-gitops/governance-activation /run/aios-gitops
RestrictSUIDSGID=true
UMask=0027
# Deliberately not installable. Deployment remains disabled/inactive;
# activation is an explicit approved CLI transaction, never an always-on unit.
@@ -1,28 +0,0 @@
[Unit]
Description=AIOS governed development writer credential revoker
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/development-policy.json
[Service]
Type=oneshot
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-development-writer-revoker --config /etc/aios-gitops/autonomous-control-plane/revoker.json
LoadCredentialEncrypted=writer_gitea_http:/etc/credstore.encrypted/aios-governed-development-writer.gitea_http
LoadCredentialEncrypted=admin_gitea_http:/etc/credstore.encrypted/aios-governance-admin.gitea_http
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/writer /run/aios-gitops/autonomous-control-plane -/etc/credstore.encrypted/aios-governed-development-writer.gitea_http
@@ -1,11 +0,0 @@
[Unit]
Description=AIOS governed development writer revocation timer
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
AccuracySec=30s
Unit=aios-governed-development-writer-revoker.service
[Install]
WantedBy=timers.target
@@ -1,33 +0,0 @@
[Unit]
Description=AIOS governed development writer
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/development-policy.json
[Service]
Type=simple
User=aios-governed-development-writer
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-development-writer-server --config /etc/aios-gitops/autonomous-control-plane/writer-server.json
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-governed-development-writer.gitea_http
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressAllow=127.0.0.1
IPAddressDeny=any
ReadWritePaths=/data/aios-gitops/worktrees /var/lib/aios-gitops/autonomous-control-plane/writer /run/aios-gitops/autonomous-control-plane
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,23 +0,0 @@
[Unit]
Description=AIOS Integration Refresh Provisioner (sandbox control plane)
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
User=aios
Group=aios
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_INTEGRATION_REFRESH_MODE=sandbox
Environment=AIOS_CREDENTIAL_PROVIDER=disabled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/aios/integration-refresh
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
ExecStart=/usr/bin/python3 /opt/aios/active-runtime/app/aios_isolated_writer_api/integration_refresh.py --sandbox-preflight --state-dir /var/lib/aios/integration-refresh
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,45 @@
[Unit]
Description=AIOS Candidate OpenClaw Session Broker
After=aios-session-broker-bootstrap-writer-candidate.service
Requires=aios-session-broker-bootstrap-writer-candidate.service
ConditionPathIsDirectory=/data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725
ConditionPathExists=/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/broker-policy.json
[Service]
Type=simple
User=aiman
Group=gitops
WorkingDirectory=/data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725
Environment=PYTHONDONTWRITEBYTECODE=1
RuntimeDirectory=aios-openclaw-session-broker-candidate
RuntimeDirectoryMode=0700
RuntimeDirectoryPreserve=restart
LoadCredential=broker_control_token:/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/secrets/broker-control-token.source
ExecStart=/usr/bin/python3 /data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725/services/aios-session-broker-candidate/candidate_session_broker.py --policy /data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/broker-policy.json --database /data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/evidence/broker-state.sqlite3 --projection-root /run/aios-openclaw-session-broker-candidate/sessions --writer-endpoint http://127.0.0.1:19932 --host 127.0.0.1 --port 19931
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
UMask=0077
CapabilityBoundingSet=
AmbientCapabilities=
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
ReadWritePaths=/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725
ReadWritePaths=/run/aios-openclaw-session-broker-candidate
[Install]
WantedBy=multi-user.target
@@ -1,31 +0,0 @@
[Unit]
Description=AIOS exact release object projection
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/release-projection.json
[Service]
Type=simple
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-release-object-projector --config /etc/aios-gitops/autonomous-control-plane/release-projection.json
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/projection /run/aios-gitops/autonomous-control-plane
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
@@ -1,28 +0,0 @@
[Unit]
Description=AIOS exact runtime source read projection
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/runtime-projection.json
[Service]
Type=oneshot
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-runtime-source-projector --config /etc/aios-gitops/autonomous-control-plane/runtime-projection.json
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/projection /run/aios-gitops/autonomous-control-plane
PrivateNetwork=true
@@ -0,0 +1,46 @@
[Unit]
Description=AIOS task-scoped Candidate Bootstrap Writer
After=network.target
ConditionPathIsDirectory=/data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725
ConditionPathExists=/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/writer-policy.json
[Service]
Type=simple
User=aiman
Group=gitops
WorkingDirectory=/data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725
Environment=PYTHONDONTWRITEBYTECODE=1
RuntimeDirectory=aios-session-broker-bootstrap-writer-candidate
RuntimeDirectoryMode=0700
RuntimeDirectoryPreserve=restart
LoadCredential=writer_token:/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/secrets/writer-token.source
LoadCredential=gitea_git_http:/etc/aios-gitops/credentials/gitea-git-http
ExecStart=/usr/bin/python3 /data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725/services/aios-session-broker-candidate/candidate_writer.py --policy /data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/writer-policy.json --worktree /data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725 --git-dir /run/aios-session-broker-bootstrap-writer-candidate/candidate.git --state /data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/evidence/writer-state.json --audit /data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725/evidence/writer-audit.jsonl --host 127.0.0.1 --port 19932
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
UMask=0077
CapabilityBoundingSet=
AmbientCapabilities=
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
ReadWritePaths=/data/aios-gitops/worktrees/openclaw-real-dispatch-session-broker-20260725
ReadWritePaths=/data/aios-gitops/runtime/tasks/AIOS-SESSION-BROKER-BOOTSTRAP-20260725
ReadWritePaths=/run/aios-session-broker-bootstrap-writer-candidate
[Install]
WantedBy=multi-user.target
@@ -1,29 +0,0 @@
[Unit]
Description=AIOS V2R1C4 unified governed release envelope executor
After=network-online.target
ConditionPathExists=/etc/aios-gitops/autonomous-control-plane/release-hooks.json
ConditionPathExists=/var/lib/aios-gitops/autonomous-control-plane/release/envelope.json
[Service]
Type=oneshot
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-v2r1c4-release-executor --config /etc/aios-gitops/autonomous-control-plane/release-hooks.json --envelope /var/lib/aios-gitops/autonomous-control-plane/release/envelope.json --operation-id v2r1c4-production-release
LoadCredentialEncrypted=gitea_http:/etc/credstore.encrypted/aios-release-executor.gitea_http
LoadCredentialEncrypted=tag_api:/etc/credstore.encrypted/aios-release-executor.tag_api
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
UMask=0027
ReadWritePaths=/var/lib/aios-gitops/autonomous-control-plane/release /var/lib/aios-gitops/release /opt/aios /run/aios-gitops/autonomous-control-plane
@@ -1,19 +0,0 @@
d /var/lib/aios-gitops/autonomous-control-plane 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/writer 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/mirror 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/projection 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/renewal 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/release 0750 root gitops -
d /run/aios-gitops/autonomous-control-plane 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/publisher 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/watcher 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/watcher/requests 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/watcher/status 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/mirror/requests 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/mirror/receipts 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/projection/requests 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/projection/receipts 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/projection/releases 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/renewal/requests 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/renewal/receipts 0750 root gitops -
d /var/lib/aios-gitops/autonomous-control-plane/release/metadata-snapshots 0750 root gitops -
@@ -1,4 +0,0 @@
d /var/lib/aios-gitops/governance-activation 0750 root gitops -
d /var/lib/aios-gitops/governance-activation/wal 0750 root gitops -
d /var/lib/aios-gitops/governance-activation/receipts 0750 root gitops -
d /run/aios-gitops 0750 root gitops -
+342
View File
@@ -0,0 +1,342 @@
# AIOS V2 用户需求规格
- 文档版本:`V2R1-hardening-candidate`
- 状态:加固候选版本需求基线
- 适用范围:AIOS Control Plane、Agent Router、OpenClaw Adapter、Hermes Runtime、Web Console、GitOps、Run Provenance、Dispatch、审计、部署与回滚
## 1. 背景与目标
现有系统已经具备 OpenClaw、Hermes、Agent Router、GitOps、局部 WebUI、审计和多租户候选能力。AIOS V2 的目标不是推倒重写,而是将这些分散能力收敛到统一、可审计、可 Web 化、可多租户隔离的控制面。
AIOS V2 必须成为唯一控制面,负责租户、身份、Agent、策略、任务生命周期、审批、审计、Runtime 目录、执行证据、Run Provenance 和 Dispatch。OpenClaw 与 Hermes 只承担执行职责,不得持有第二套控制面状态。
## 2. 用户角色
| 角色 | 主要诉求 |
|---|---|
| 平台管理员 | 管理租户、运行时、发布、GitOps 状态和全局策略 |
| 租户管理员 | 管理本租户 Agent、任务、审批、审计和成员权限 |
| 操作员 | 创建任务、发起执行、取消任务、查看运行状态 |
| 审批人 | 审批高风险任务,查看策略判定依据 |
| 审计员 | 查询本租户完整审计链和执行证据 |
| Runtime Worker | 领取已批准任务、心跳、返回执行结果 |
| 开发与运维人员 | 通过 GitOps 提交、测试、部署、回滚和修复 Bug |
## 3. 核心用户需求
### UR-001 唯一控制面
所有任务创建、审批、取消、状态变化、策略判定和审计必须由 AIOS Control Plane 统一处理。
验收:
- Web Console、API、Codex、OpenClaw 和 Hermes 均不得绕过 AIOS 创建或修改任务生命周期。
- OpenClaw/Hermes 的公开接口仅限执行、状态、取消、心跳和结果回传。
### UR-002 复用现有能力
系统不得重写已经稳定的 OpenClaw 执行能力、Hermes Runtime、Agent Router 或 GitOps 基础设施。
验收:
- 通过 Adapter/Boundary 接入现有能力。
- 替换控制逻辑时保留原执行接口和可迁移数据。
### UR-003 OpenClaw 与 Hermes 解耦
OpenClaw 与 Hermes 必须从策略、租户、审批和任务生命周期中逐步解耦。
验收:
- OpenClaw 仅接收已签名、已批准、带租户上下文的命令。
- Hermes 仅接收已批准的租户分区 Job。
- 二者均不得直接写 Control Plane 权威状态。
### UR-004 全面 Web 化
所有面向用户和运维的能力必须通过 Web Console 或稳定 API 访问。
Web Console 至少展示:
- Tenant、Agent、Task、Approval、Runtime、Audit、Execution Evidence、GitOps Status。
- 所有写操作由 AIOS API 完成,浏览器不得直接调用 OpenClaw/Hermes。
### UR-005 多租户隔离
所有模块必须支持租户隔离,且默认拒绝跨租户访问。
隔离范围:
- API 与会话
- Tenant/Agent/Task 数据
- OpenClaw 命令
- Hermes Queue、Cell、Profile、Worker Lease
- Memory、Cache、Artifact、SecretRef
- Audit、Execution Evidence、日志和指标
验收:
- 资源记录的 `tenant_id` 不可为空。
- 跨租户访问返回拒绝或“资源不存在”,不得泄露存在性。
- 必须有跨租户负向测试。
### UR-006 权威任务生命周期
任务必须遵循统一状态机:
```text
CREATED -> APPROVED -> RUNNING -> COMPLETED
| | |
+----------+----------+-> CANCELLED
+-> FAILED
```
验收:
- 禁止越级变化,例如 `CREATED -> COMPLETED`。
- 每次变化包含 revision、principal、trace、时间和审计事件。
- 公共 API 不允许直接调用 `start`、`complete` 或 `fail` 推进权威状态。
### UR-007 策略与审批
所有有副作用任务必须经过策略判定;高风险任务必须支持人工审批。
验收:
- Policy Decision 包含 allow/deny、reason 和 policy version。
- 拒绝事件进入审计。
- Runtime 不得自行放宽策略。
### UR-008 幂等与并发安全
任务和 Runtime 执行必须支持幂等与乐观并发控制。
验收:
- 相同租户与幂等键、相同内容返回同一任务/执行。
- 相同幂等键不同内容返回冲突。
- revision 或 branch lease 冲突不得静默覆盖。
### UR-009 可观测与执行证据
平台必须能够确认 Codex、OpenClaw、Hermes 等执行器是否领取任务、当前进度、最近提交、测试结果和完成状态。
验收:
- 存在 claim、heartbeat、progress、result 和 evidence 记录。
- Web/API 可按租户查询。
- 执行证据不得包含明文密钥。
### UR-010 GitOps 唯一变更流程
所有代码、配置、文档和部署资产修改必须进入非 `main` 分支,通过 commit、测试、审查和受控提升。
验收:
- Controller worktree 必须 detached 且 clean。
- 本地 bare repo 与 Gitea 目标分支 OID 一致。
- 不允许直接 push `main`、直接创建生产 Tag 或绕过审批部署。
### UR-011 持久化与恢复
租户、Agent、Task、Audit、Idempotency、Dispatch、Run Provenance、Grant 使用记录和 Execution Evidence 必须在服务重启后恢复。
验收:
- 候选版支持 SQLite WAL 或等价原子持久化。
- 有 Control Plane 与 Dispatch restart recovery 测试。
- 生产版需进一步提供迁移、备份、恢复、HA 和容量证据。
### UR-012 审计与合规
所有控制与执行动作必须生成不可混租户的审计事件。
至少记录:
- tenant、principal、trace、action、resource、outcome、policy version、timestamp。
- OpenClaw/Hermes Runtime 生命周期。
- GitOps、部署和回滚动作。
- Provenance Grant、Scope、Authorization Receipt 和 Dispatch 结果摘要。
### UR-013 密钥安全
Git、日志、Web、任务 payload 和审计记录中不得出现明文 API Key、密码、Token 或私钥。
验收:
- Git 只存 `secret_ref`、`credential_ref`、`key_id` 或受保护文件路径。
- Secret 文件权限为 `0600` 或更严格。
- Secret Scanner 默认拒绝可疑提交。
- OpenClaw Command 和 Run Provenance 使用 Ed25519;生产应用只读取公钥。
### UR-014 部署与回滚
系统必须提供 systemd、Docker/Compose、健康检查和受保护回滚入口。
验收:
- 服务以非 root 权限运行。
- 状态目录独立持久化。
- 回滚前自动备份状态。
- 默认只回滚代码,状态恢复需要显式授权。
### UR-015 文档与代码同步
任何影响用户行为、架构边界、API、数据模型、状态机、安全策略、部署或运维方式的变更,必须在同一分支同步更新 `docs/aios-v2/`。
验收:
- 架构/功能代码变化但无文档变化时,文档同步门禁失败。
- Traceability Matrix 必须指向当前代码和测试。
- 新架构决策必须增加或更新 ADR。
### UR-016 Workflow Run Provenance
每次生产 Run 必须具有一个可验证、可撤销、可过期的 Workflow Provenance,用于绑定 Run、Tenant、Task、Agent 与 Principal,但父 Workflow 本身不得授予操作权限。
验收:
- Workflow Provenance 使用 Ed25519 签名。
- `allowed_paths` 与 `allowed_git_operations` 为空。
- 上下文、签名、TTL、Nonce、Key ID 不合法时默认拒绝。
- 撤销父 Workflow 后全部子 Grant 失效。
### UR-017 最小权限子 Grant
所有生产副作用必须通过与单一 ExecutionPath 对应的子 Grant 授权。
验收:
- Runtime、Evidence、Git、Deployment、Rollback、Audit 和 Code Change 使用独立 Grant。
- 一个 Grant 只能包含一个路径。
- Git Grant 必须限定具体 GitOperation。
- 子 Grant 具有独立 TTL、撤销、状态和重放防护。
### UR-018 Runtime Result 信任绑定
OpenClaw/Hermes 结果只有在与持久 Dispatch 的身份和执行上下文完全匹配时,才能更新权威 Task。
验收:
- 至少校验 tenant、task、agent、principal、trace、runtime、runtime execution、provenance 和 run。
- 不同终态结果摘要不得覆盖已有终态。
- Hermes 终态结果必须来自持有 Job Lease 的 Worker。
- Hermes Worker 生命周期验收入口必须是默认关闭的内部接口,仅接受可信
`runtime` 或 `platform_admin` 身份;普通 Operator、浏览器和公共 API 不得直接调用。
- 每次 Claim/Start/Heartbeat/Complete/Fail/Cancel 必须绑定
tenant、task、trace、principal、provenance、run、dispatch、job、cell 和 profile。
- Complete/Fail/Cancel 必须通过 `ExecutionOrchestrator.reconcile_hermes` 更新权威
Task/Dispatch,并同步生成 Audit/Event 与 Execution Evidence。
- Lease 到期必须持久化清除旧 Owner、递增 revision 并允许安全重领;旧 Worker
后续 Result 必须拒绝。
- Runtime callback 审计不得绕过 Orchestrator Reconcile。
### UR-019 Dispatch 持久化与恢复
Runtime 副作用之前必须持久化唯一 Dispatch,重启后能够恢复并继续严格 Reconcile。
验收:
- 同租户 Task 和 Runtime Execution 各自唯一绑定一个 Dispatch。
- Dispatch 支持按 ID、Task 与 Runtime Execution 查询。
- 终态 Dispatch 不得迁移到不同终态。
- Hermes Job、Result、Lease、幂等键和 Worker heartbeat 必须与 Dispatch 一样在
服务重启后恢复;不得以只存在于进程内的 Map 作为在线验收依据。
- 有 SQLite Repository 重启恢复测试。
### UR-020 OpenClaw Ed25519 命令合同
OpenClaw 生产命令必须使用版本化 Ed25519 合同,不得继续使用 HMAC 作为新命令信任根。
验收:
- 合同版本为 `aios.openclaw.command.v2`。
- 签名覆盖 Key ID、算法、公钥 Fingerprint、Tenant、Task、Trace、Payload、Idempotency 与时间字段。
- Key ID/Fingerprint/签名/过期或时钟偏差不合法时拒绝。
- Fingerprint 使用恒定时间比较。
### UR-021 服务级权限隔离
每个生产服务或 Worker 只能加载其职责所需的 Grant,不得加载全部 Grant 备用。
验收:
- 默认 AIOS Application 仅加载 Runtime、Evidence 与 Audit Grant。
- Git、Deployment、Rollback 与 Code Change 由独立 Scope 加载。
- 空 Scope、多余 Grant 映射和未声明路径调用全部拒绝。
- Authorization Receipt 记录 Grant ID、Writer ID 和 Scope。
### UR-022 CLI 单 Grant 授权
运维 CLI 的一次授权操作只能加载一个与请求路径一致的 Grant。
验收:
- `authorize` 使用一个 `--path` 和一个 `--grant-id`。
- Grant 类型与 Path 不一致时拒绝。
- CLI 不提供一次加载七个生产 Grant 的授权入口。
- 旧全权限 `issue` 仅允许候选兼容,不得进入生产流程。
## 4. 非功能需求
### NFR-001 安全
- 默认拒绝、最小权限、租户隔离、身份可信链、SecretRef、审计、可撤销 Provenance 和 Runtime 结果绑定。
### NFR-002 可靠性
- 幂等、revision、worker lease、heartbeat、重试边界、原子持久化、Dispatch 恢复、健康检查和回滚。
### NFR-003 可维护性
- 模块边界稳定,Control Plane 与 Runtime 解耦;代码、文档、测试和部署可追踪。
### NFR-004 可扩展性
- Store、Transport、Policy、EventPublisher、Runtime Adapter、Provenance Verifier 与 Writer 均通过接口扩展。
### NFR-005 可运维性
- Web/API 状态、systemd/Docker、健康检查、日志、GitOps 状态、候选报告、Grant 状态和 Receipt。
### NFR-006 性能候选指标
候选版上线前至少验证:
- API P95、任务调度延迟、队列等待、Worker 心跳、SQLite 锁等待、审计写入时间、Grant 授权延迟和 Dispatch 恢复时间。
- 具体 SLO 在预发布基准测试后固化,禁止无证据承诺。
## 5. 本候选版本范围
### 已纳入代码
- Control Plane、持久化、Gateway、Orchestrator、OpenClaw Adapter、Hermes Boundary、Execution Tracking、Web Console、GitOps Status、Run Provenance、Least-Privilege Grant、Scoped Writer、Dispatch Repository、部署和回滚资产。
### 已纳入测试代码
- Control Plane/Runtime 候选流程。
- OpenClaw Ed25519 签名与验证。
- Hermes Worker Lease 与严格 Reconcile。
- Dispatch SQLite 重启恢复。
- Workflow/Grant 撤销、过期、重放和服务 Scope 隔离。
### 尚需环境验证
- 完整 Acceptance 实际执行证据。
- OpenClaw 真实 Runtime 联调。
- 生产数据库/队列替换或容量验证。
- 预发布性能、安全、重启和回滚演练。
- Owner 对 merge、Tag 和生产部署的批准。
## 6. 用户验收场景
1. 平台管理员创建租户并注册 OpenClaw/Hermes Agent。
2. 操作员创建任务,审批人批准。
3. Owner 或受控签发端签发 Workflow Provenance 与所需最小权限 Grant。
4. AIOS Orchestrator 持久化 Dispatch,并创建 Ed25519 命令或租户分区 Job。
5. Runtime 执行并返回与 Dispatch 完全匹配的结果。
6. AIOS 更新 Task、Audit、Authorization Receipt 和 Execution Evidence。
7. 租户用户在 Web Console 查看完整链路。
8. 跨租户、跨 Principal、跨 Grant Path 和重放请求被拒绝。
9. 服务重启后 Control Plane、Dispatch 和 Provenance 状态恢复。
10. 候选部署健康检查、Acceptance 和回滚演练实际执行并保存证据。
+461
View File
@@ -0,0 +1,461 @@
# AIOS V2 目标架构
- 文档版本:`V2R1C0-candidate`
- 架构状态:目标架构已实现为候选代码,待预发布验证
- 适用范围:Control Plane、Agent Router、OpenClaw、Hermes、Web Console、GitOps、审计、部署与运维
## 1. 架构目标
AIOS V2 的核心目标是把分散在 OpenClaw、Hermes、Agent Router、局部 WebUI 和 GitOps 中的控制能力收敛为一个权威控制面,同时保留现有执行能力,避免推倒重写。
目标状态:
```text
用户 / Web Console / Public API
|
v
AIOS Control Plane
Tenant | Identity | Agent | Task | Policy | Approval | Audit
|
v
Execution Orchestrator / Agent Router
/ \
v v
OpenClaw Adapter Hermes Boundary
| |
v v
OpenClaw Runtime Hermes Workers
\ /
v v
Result Reconciliation + Execution Evidence
```
设计结论:
- AIOS 是唯一控制面。
- Agent Router 是调度平面,不是第二控制面。
- OpenClaw 是执行适配与 Runtime。
- Hermes 是队列、Worker、Profile 与执行 Runtime。
- Web Console 只调用 AIOS API。
- GitOps 是代码、配置、文档和发布变更的唯一入口。
## 2. 从现有架构到目标架构
### 2.1 现有能力
现有系统已经具备:
- OpenClaw 执行与 Fleet 能力。
- Hermes Worker、Profile、队列和工作区能力。
- Agent Router 路由经验和部分调度接口。
- GitOps 分支、提交、审批、回滚和审计能力。
- Tenant Context、Tenant Router、Memory Scope、SecretRef、Message Ledger 和审计候选实现。
- 局部 WebUI、Gateway 和自动任务领取能力。
### 2.2 现有问题
- 控制逻辑分散,可能存在多个任务事实源。
- OpenClaw/Hermes 内部仍可能保存租户、策略或任务状态。
- 候选能力分散在多个分支,主线缺少权威实现。
- WebUI 与 API 入口不统一。
- 分支名称、代码、测试和部署证据之间缺少映射。
- 多租户隔离多为逻辑约束,尚缺全链路生产证据。
### 2.3 演进原则
- 先加边界和适配器,再迁移控制权。
- 先复用接口,再替换内部实现。
- 先在候选分支验证,再受控提升。
- 不允许同时存在两套权威任务生命周期。
- 不以“大分支存在”代替验收证据。
## 3. 逻辑分层
### 3.1 Web 与 API 层
职责:
- 提供 Tenant、Agent、Task、Approval、Runtime、Audit、Execution Evidence 和 GitOps 页面/API。
- 依赖可信身份代理注入 `X-AIOS-*` 身份头。
- 不允许浏览器直接访问 OpenClaw 或 Hermes。
- 不允许客户端自行选择或覆盖 `tenant_id`。
候选实现:
- `active-runtime/app/aios_v2/app.py`
- `active-runtime/app/aios_v2/gateway.py`
- `active-runtime/app/aios_v2/console.py`
### 3.2 AIOS Control Plane
权威职责:
- Tenant Registry
- Agent Registry
- Task Lifecycle
- Policy Decision
- Approval
- Audit Event
- Runtime Inventory
- Execution Evidence
- Deployment/GitOps Status
不负责:
- OpenClaw 内部执行细节。
- Hermes Worker 内部执行细节。
- 直接持有 Runtime 凭据明文。
候选实现:
- `active-runtime/app/aios_v2/control_plane.py`
- `active-runtime/app/aios_v2/persistence.py`
- `active-runtime/app/aios_v2/execution_tracking.py`
- `active-runtime/app/aios_v2/gitops_status.py`
### 3.3 Execution Orchestrator / Agent Router
职责:
- 只接受 `APPROVED` 任务。
- 根据 Agent Registry 和 Policy Result 选择 Runtime。
- 生成 OpenClawCommand 或 HermesJob。
- 传播 tenant、task、trace、idempotency 和 policy version。
- 将 Runtime Result 回写 AIOS Task。
不负责:
- 创建租户。
- 修改策略。
- 绕过审批。
- 保存第二套任务状态机。
候选实现:
- `active-runtime/app/aios_v2/orchestrator.py`
### 3.4 OpenClaw Adapter
职责:
- 验证签名命令。
- 执行、查询、取消、健康检查和回调。
- 支持幂等、超时和租户隔离。
- 把 Runtime 生命周期转换为 AIOS 审计事件。
不负责:
- Tenant、Policy、Approval、Task Lifecycle。
- 向浏览器暴露公网控制接口。
候选实现:
- `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`
### 3.5 Hermes Runtime Boundary
职责:
- Tenant/Cell/Profile 分区队列。
- Worker claim、start、heartbeat、complete、fail、cancel。
- Lease 超时和自动回队列。
- Result 回传和 Runtime Health。
不负责:
- Policy、Approval、Tenant Registry、权威 Task Lifecycle。
候选实现:
- `active-runtime/app/aios_hermes/contract.py`
- `active-runtime/app/aios_hermes/adapter.py`
- `active-runtime/app/aios_hermes/worker.py`
### 3.6 Persistence
候选实现使用 SQLite WAL 原子快照,持久化:
- Tenant
- Agent
- Task
- Audit
- Idempotency
- Execution Evidence 文件
生产演进:
- Store 接口保持稳定。
- 可替换为 PostgreSQL、对象存储或分布式事件存储。
- 生产替换必须提供迁移、备份、恢复、HA、容量和租户隔离证据。
## 4. 多租户架构
### 4.1 权威租户上下文
每次请求和执行至少传播:
```text
tenant_id
principal_id
session_id
trace_id
task_id
policy_version
```
Runtime 还传播:
```text
command_id / job_id
cell_id
profile_id
idempotency_key
worker_id
```
### 4.2 隔离层级
| 层级 | 隔离要求 |
|---|---|
| HTTP/API | 可信身份头、租户不可由浏览器覆盖 |
| Control Plane | 所有资源带 `tenant_id`,跨租户默认拒绝 |
| OpenClaw | 签名命令绑定 tenant/task/trace |
| Hermes | tenant/cell/profile 三层分区 |
| Storage | 候选为显式租户字段;生产需 RLS 或物理隔离策略 |
| Evidence/Audit | 仅按当前租户查询 |
| Cache/Memory | key 必须包含 tenant namespace |
| Secret | 只保存引用,不保存明文 |
| Logs/Metrics | 必须包含 tenant/task/trace 标签,并限制查询范围 |
### 4.3 失败行为
- 跨租户资源访问返回拒绝或“未找到”。
- 不泄露其他租户资源存在性。
- 缺少 tenant context 时 fail closed。
- Runtime 返回的 tenant/task/trace 不匹配时拒绝回写。
## 5. 任务与执行数据流
### 5.1 任务创建与审批
```text
User/API
|
v
Trusted Identity
|
v
AIOS Gateway
|
v
Policy Gate
|
v
Task CREATED
|
v
Approval
|
v
Task APPROVED
```
### 5.2 OpenClaw 执行
```text
APPROVED Task
|
v
Execution Orchestrator
|
v
Signed OpenClawCommand
|
v
OpenClaw Adapter
|
v
Runtime Result
|
v
Task Reconciliation + Audit + Evidence
```
### 5.3 Hermes 执行
```text
APPROVED Task
|
v
Execution Orchestrator
|
v
Tenant/Cell/Profile HermesJob
|
v
Worker Claim + Lease + Heartbeat
|
v
HermesResult
|
v
Task Reconciliation + Audit + Evidence
```
## 6. 安全架构
### 6.1 身份边界
- Public API 位于可信反向代理之后。
- 反向代理完成登录、会话、MFA 和身份头注入。
- 候选应用拒绝未认证身份断言。
- 生产不得允许客户端直接伪造 `X-AIOS-*`。
### 6.2 Runtime 命令完整性
OpenClawCommand 使用规范化序列化和 HMAC-SHA256:
- 签名覆盖 tenant、task、trace、action、payload、idempotency 和时间。
- 支持 key ID、到期时间和时钟偏差。
- Runtime 密钥通过受保护文件或外部 Secret Provider 解析。
### 6.3 Secret 边界
Git 允许:
```text
secret_ref
credential_ref
key_id
受保护文件路径
```
Git 禁止:
```text
API Key
密码
Token
私钥
签名材料
```
### 6.4 Web 安全
候选应用设置:
- no-store
- nosniff
- DENY frame
- no-referrer
- CSP
- 请求体大小限制
生产还需验证:
- CSRF/CORS
- Rate Limit
- Session Timeout
- CSP Nonce/Hash
- WAF 和反向代理访问策略
## 7. 可靠性架构
### 7.1 幂等
- Control Plane:`tenant_id + idempotency_key + request_digest`。
- OpenClaw:`tenant_id + idempotency_key + signature`。
- Hermes:`tenant_id + idempotency_key + job_digest`。
### 7.2 并发
- Task 使用 revision。
- Hermes 使用 revision + worker lease。
- GitOps 使用 detached controller + branch lease。
- 持久化使用 SQLite `BEGIN IMMEDIATE` 和原子快照。
### 7.3 恢复
- Controller restart:detached guard。
- AIOS restart:SQLite restore。
- Worker crash:lease 到期后 Job 回到 QUEUED。
- Release failure:健康检查失败自动恢复 previous release。
## 8. 可观测架构
统一关联键:
```text
tenant_id
principal_id
trace_id
task_id
agent_id
command_id / job_id
worker_id
commit
```
Web Console 展示:
- Tasks
- Agents
- Runtime Health
- Audit
- Execution Evidence
- GitOps Status
生产还需接入:
- 结构化日志
- Metrics
- Distributed Tracing
- Alerting
- SLO Dashboard
## 9. 部署架构
支持两种候选部署:
### systemd
- 非 root `aios` 用户。
- `ProtectSystem=strict`、`NoNewPrivileges` 等安全选项。
- 状态目录 `/var/lib/aios-v2`。
### Docker/Compose
- 非 root UID/GID。
- Read-only root filesystem。
- Drop all capabilities。
- State volume。
- HTTP healthcheck。
## 10. 架构约束
以下行为视为架构违规:
1. OpenClaw/Hermes 直接创建或批准 AIOS Task。
2. Web Console 直接调用 Runtime。
3. 新模块不携带 `tenant_id`。
4. 新增第二套任务状态机或租户事实源。
5. 在 Git 中提交明文凭据。
6. 绕过 GitOps 修改生产代码/配置。
7. 架构或功能变化未同步更新 `docs/aios-v2/`。
8. 未通过测试和预发布门禁直接提升候选版本。
## 11. 目标架构完成判定
目标架构只有在以下证据齐全时才视为完成:
- 文档与代码映射完整。
- Candidate tests 对精确 commit 执行通过。
- OpenClaw/Hermes 实际 Runtime 联调通过。
- 多租户负向测试通过。
- 重启恢复和回滚演练通过。
- GitOps 本地/Gitea OID 一致。
- Owner 批准 merge、Tag 和生产部署。
+313
View File
@@ -0,0 +1,313 @@
# AIOS V2 迁移与交付策略
- 文档版本:`V2R1C0-candidate`
- 交付原则:增量迁移、能力复用、控制权收敛、GitOps 唯一变更入口
## 1. 迁移目标
AIOS V2 不通过一次性重写替换现有系统,而是在保留 OpenClaw、Hermes、Agent Router、GitOps 和既有运行能力的前提下,将租户、策略、任务、审批、审计和发布治理收敛到 AIOS Control Plane。
迁移完成的标志不是“新代码存在”,而是:
- AIOS 成为唯一权威控制面;
- OpenClaw/Hermes 不再保存第二套控制状态;
- 所有控制功能 Web/API 化;
- 所有模块通过租户隔离、测试和预发布门禁;
- 本地 Git、Gitea、测试、部署和回滚证据可追踪。
## 2. 为什么采用增量迁移
### 2.1 避免能力回退
OpenClaw、Hermes 和 Agent Router 已经包含大量运行经验、边界处理和执行能力。整体重写会扩大风险面,并导致业务能力、操作流程和历史数据回退。
### 2.2 保持可回滚
每个迁移步骤必须:
- 独立分支;
- 小范围提交;
- 独立测试;
- 可单独回滚;
- 不要求立即关闭旧执行路径。
### 2.3 先收敛控制权,再优化内部实现
迁移优先级:
```text
唯一入口
-> 权威租户/任务/策略
-> Runtime Adapter
-> 数据层生产化
-> Web 与可观测
-> 性能与 HA
```
## 3. 分阶段迁移路线
### Phase 0:GitOps 与架构事实源
目标:建立可靠变更链和权威设计基线。
已完成候选能力:
- Controller detached guard;
- 避免 fetch 到已检出本地分支;
- Gitea SSH 镜像;
- `docs/aios-v2/` 架构事实源;
- 候选分支、部署和回滚资产。
退出条件:
- Controller detached 且 clean;
- 本地/Gitea OID 一致;
- 架构与需求文档纳入版本。
### Phase 1:Control Plane 权威化
目标:将 Tenant、Agent、Task、Policy、Approval 和 Audit 收敛到 AIOS。
候选实现:
- `aios_v2/control_plane.py`
- `aios_v2/persistence.py`
- `aios_v2/gateway.py`
- `aios_v2/app.py`
退出条件:
- 任务状态机统一;
- 幂等、revision、审计和跨租户拒绝通过测试;
- 服务重启后权威状态恢复。
### Phase 2:OpenClaw 解耦
目标:OpenClaw 从控制逻辑转为执行适配器。
步骤:
1. 定义签名命令合同;
2. 引入 Adapter;
3. 将直接调用迁移到 AIOS Orchestrator;
4. Runtime 只返回执行结果和健康状态;
5. 关闭或网络隔离旧公共控制入口。
退出条件:
- 未批准任务无法生成有效命令;
- 命令绑定 tenant/task/trace/idempotency;
- 旧控制接口冻结并记录替代路径。
### Phase 3:Hermes 解耦
目标:Hermes 只承担租户分区队列和 Worker Runtime。
步骤:
1. 定义 HermesJob/HermesResult;
2. 强制 tenant/cell/profile 分区;
3. 引入 claim/lease/heartbeat;
4. 将结果统一回写 AIOS;
5. 移除 Hermes 内部策略和任务事实源。
退出条件:
- Worker 无权创建 AIOS Task;
- 租约过期后 Job 可恢复;
- 跨租户 claim/read/result 被拒绝。
### Phase 4:Web 与运维统一
目标:所有用户与运维功能通过 AIOS Web/API。
范围:
- Tenant、Agent、Task、Runtime、Audit、Execution Evidence、GitOps;
- systemd/Docker/Compose;
- healthcheck、rollback、release report。
退出条件:
- 浏览器不直接调用 Runtime;
- 生产身份代理和权限模型通过验证;
- 部署、重启和回滚演练通过。
### Phase 5:生产数据层与 HA
目标:将候选 SQLite/内存运行状态替换或强化为生产级后端。
要求:
- 数据迁移;
- RLS 或物理隔离;
- 备份恢复;
- HA 与容量;
- 多实例并发;
- 灾难恢复目标。
该阶段不得改变上层合同,避免 Runtime 和 Web 再次耦合存储实现。
## 4. 分支与提交策略
### 4.1 分支类型
| 类型 | 用途 |
|---|---|
| `task/*` | 可执行任务合同与主体实现 |
| `patch/*` | 独立修复或窄范围增强 |
| `integration/*` | 多模块候选收敛 |
| `result/*` | 自动执行结果与证据 |
| `release/*` | 受控发布候选 |
### 4.2 规则
- 禁止直接修改 `main`;
- 一个提交应可独立审阅和回滚;
- 大文件和大范围变更应拆分;
- 所有行为变化必须同步文档和测试;
- 生产 Tag、部署和回滚需要 Owner 批准。
## 5. GitOps 交付流程
```text
需求/架构更新
|
v
任务分支 + 文档 + 测试
|
v
代码提交 + Secret Gate
|
v
本地 bare repo 同步
|
v
Gitea 镜像与 OID 校验
|
v
Candidate 集成
|
v
自动测试与预发布
|
v
Owner 审批
|
v
Merge / Tag / Production Deploy
```
### 5.1 同步状态
GitOps 返回必须区分:
- local sync;
- Gitea sync;
- branch OID;
- workspace mode;
- dirty state。
仅返回 `common_git_dir synced` 不足以证明 Web 远端可见。
## 6. 发布门禁
### Gate A:设计完整性
- 用户需求、目标架构、模块契约、ADR 和追踪矩阵同步;
- 无冲突标记、断链或过期路径。
### Gate B:代码质量
- 包可导入;
- 静态编译通过;
- Secret Scanner 通过;
- 无已知越权旁路。
### Gate C:测试
- 单元、集成、多租户负向、重启恢复、Runtime 边界测试通过;
- 记录 commit、命令、exit code、pass/fail 和 artifact digest。
### Gate D:预发布
- systemd 或 Compose 启动成功;
- 健康检查通过;
- OpenClaw/Hermes 真实联调通过;
- 备份、重启和回滚演练通过;
- 日志中无敏感信息。
### Gate E:生产批准
- Owner 批准 merge、Tag 和生产部署;
- 发布窗口、回滚负责人和观察指标明确;
- 生产变更通过 GitOps 记录。
## 7. 回滚策略
### 7.1 代码回滚
- 使用 `/opt/aios/current` 原子 symlink 切换;
- 回滚前备份状态;
- 健康检查失败自动恢复 previous release。
### 7.2 状态回滚
默认不自动回滚状态,避免覆盖候选运行期间新增的 Task、Audit 和 Evidence。
状态恢复只有在:
- 明确确认 schema 不兼容;
- 数据备份可验证;
- Owner 显式授权;
时启用。
### 7.3 Runtime 回滚
OpenClaw/Hermes Adapter 采用稳定合同。若候选执行层失败,可恢复旧 Runtime 版本,但不得恢复旧控制权旁路。
## 8. Bug 修复流程
生产或预发布 Bug:
1. 创建 `patch/bugfix/*`;
2. 添加可复现测试;
3. 修复代码;
4. 更新相关需求/架构/契约/ADR;
5. 运行候选验收;
6. 进入新的 release candidate;
7. Owner 批准部署。
禁止在生产目录直接编辑文件后不回写 Git。
## 9. 文档同步规则
以下变化必须更新文档:
- 用户行为或页面;
- API 路径、Header、请求/响应;
- 数据模型、状态机、策略;
- OpenClaw/Hermes/Router 边界;
- 多租户与安全模型;
- 部署、健康检查、回滚;
- 测试或发布门禁。
对应关系:
- 需求变化:更新 `01-USER-REQUIREMENTS.md`;
- 架构变化:更新 `02-TARGET-ARCHITECTURE.md` 和 ADR;
- 迁移/发布变化:更新本文件;
- 合同变化:更新 `04-MODULE-CONTRACTS.md`;
- 代码/测试路径变化:更新 `05-TRACEABILITY-MATRIX.md`。
## 10. 当前候选后续步骤
1. 完成文档同步门禁和 Acceptance Runner;
2. 在精确 Candidate commit 执行测试;
3. 推送 Gitea 并校验 OID;
4. 部署隔离预发布环境;
5. 联调真实 OpenClaw/Hermes;
6. 执行重启、性能、安全和回滚演练;
7. 生成最终 evidence;
8. 请求 Owner 的 merge/Tag/production approval。
+608
View File
@@ -0,0 +1,608 @@
# AIOS V2 模块契约
- 文档版本:`V2R1-hardening-candidate`
- 状态:加固候选契约基线
- 原则:Control Plane 权威、Runtime 仅执行、默认拒绝、服务级最小权限、全链路租户隔离、所有权威状态可审计和恢复
核心模型名称与实现保持逐字同步:`RequestContext` 承载可信租户、主体、角色、trace、
session 与 policy version;`TaskState` 是 AIOS 权威 Task 生命周期枚举,Runtime Worker
不得直接写入或复制该状态机。
## 1. 通用可信上下文契约
所有控制与执行请求必须携带以下上下文:
| 字段 | 必填 | 说明 |
|---|---:|---|
| `tenant_id` | 是 | 权威租户标识 |
| `principal_id` | 控制和 Reconcile 是 | 已认证主体 |
| `trace_id` | 是 | 全链路追踪 |
| `task_id` | 执行阶段是 | AIOS 权威任务 |
| `agent_id` | 执行和 Provenance 是 | 任务绑定 Agent |
| `provenance_id` | 生产副作用操作是 | 父 Workflow Provenance |
| `run_id` | 生产副作用操作是 | 本次执行 Run |
| `dispatch_id` | Runtime Result 是 | 持久化 Dispatch 记录 |
| `policy_version` | 控制请求是 | 策略版本 |
| `idempotency_key` | 创建/执行是 | 幂等键 |
规则:
- 缺少必填字段时 fail closed。
- 客户端不得覆盖可信代理或 AIOS 生成的租户、身份和追踪字段。
- Runtime 返回的 tenant/task/trace/runtime execution 必须与持久 Dispatch 完全匹配。
- Runtime Result 还必须匹配 Principal、Parent Provenance 和 Run。
- 所有持久化键、缓存键、队列分区、Receipt 和 Evidence 路径必须包含租户命名空间。
## 2. Tenant Contract
### 2.1 TenantRecord
```text
tenant_id
display_name
status
metadata
created_at
updated_at
```
约束:
- `tenant_id` 不可为空且不可变。
- Tenant 只由 AIOS Control Plane 创建和维护。
- Runtime 不得创建或修改租户。
- 跨租户读取默认返回拒绝或资源不存在,不得泄露存在性。
实现:`active-runtime/app/aios_v2/control_plane.py`
## 3. Agent Contract
### 3.1 AgentRecord
```text
agent_id
tenant_id
display_name
runtime
capabilities
enabled
metadata
created_at
updated_at
```
约束:
- Agent 属于唯一租户。
- `runtime` 由 Registry 决定,可取 `openclaw`、`hermes` 或后续注册 Runtime。
- `capabilities` 是允许调度的动作集合。
- `metadata` 仅保存非敏感引用,不得保存明文凭据。
## 4. Task Contract
### 4.1 TaskRecord
```text
task_id
tenant_id
agent_id
action
payload
state
revision
idempotency_key
request_digest
result
error
created_by
approved_by
created_at
updated_at
```
### 4.2 权威状态机
```text
CREATED -> APPROVED -> RUNNING -> COMPLETED
| | |
+----------+----------+-> CANCELLED
+-> FAILED
```
允许转换:
| 当前 | 允许目标 | 权威入口 |
|---|---|---|
| CREATED | APPROVED, CANCELLED | Control Plane Approval/Cancel |
| APPROVED | RUNNING, CANCELLED | Orchestrator Dispatch / Control Plane Cancel |
| RUNNING | COMPLETED, FAILED, CANCELLED | Orchestrator Reconcile / Control Plane Cancel |
| COMPLETED | 无 | 终态 |
| FAILED | 无 | 终态 |
| CANCELLED | 无 | 终态 |
约束:
- 每次转换必须校验 `revision`。
- COMPLETED 必须带 `result`;FAILED 必须带 `error`。
- 每次转换生成 AuditEvent。
- Runtime 不得直接写 Task Store。
- 公共 API 不得直接把任务推进到 `RUNNING`、`COMPLETED` 或 `FAILED`。
## 5. Policy Contract
### 5.1 PolicyDecision
```text
allowed
reason
policy_version
```
约束:
- Policy 默认拒绝。
- Runtime 不得覆盖 PolicyDecision。
- 拒绝必须审计。
- 高风险动作必须经过 Approval。
## 6. Audit Contract
### 6.1 AuditEvent
```text
event_id
tenant_id
principal_id
trace_id
action
resource_type
resource_id
outcome
policy_version
details
created_at
```
约束:
- 按租户写入和查询。
- `details` 不得包含明文凭据、私钥或完整敏感 Payload。
- Runtime callback 可以生成非权威审计事件,但不得单独触发 Task 状态变化。
- Task 状态变化必须经过 Orchestrator 严格 Reconcile。
- 生产版需增加不可变外部固化和完整性验证。
## 7. Workflow Provenance Contract
### 7.1 RunProvenance
父 Workflow Provenance 只绑定身份与 Run,不授予操作权限:
```text
provenance_id
run_id
issuer
tenant_id
task_id
agent_id
principal_id
allowed_paths = []
allowed_git_operations = []
issued_at
expires_at
policy_version
key_id
nonce
signature
schema_version
```
约束:
- 使用 Ed25519 签名。
- `allowed_paths` 和 `allowed_git_operations` 必须为空。
- 撤销或过期父 Workflow 后,全部子 Grant 自动失效。
- 私钥只存在签发端,生产应用只读取公钥。
实现:`active-runtime/app/aios_v2/provenance.py`
## 8. Provenance Grant Contract
### 8.1 ProvenanceGrant
```text
grant_id
parent_provenance_id
grant_type
execution_path
run_id
issuer
tenant_id
task_id
agent_id
principal_id
allowed_git_operations
issued_at
expires_at
policy_version
key_id
nonce
signature
schema_version
```
Grant 类型与唯一允许路径:
| GrantType | ExecutionPath |
|---|---|
| `code_change` | `code_change` |
| `git` | `git_operation` |
| `deployment` | `deployment` |
| `runtime` | `runtime_execute` |
| `evidence` | `evidence_write` |
| `rollback` | `rollback` |
| `audit` | `audit` |
约束:
- 一个 Grant 只允许一个路径。
- Git Grant 必须显式声明具体 GitOperation;非 Git Grant 不得包含 Git 操作。
- 子 Grant 身份字段必须与父 Workflow 完全一致。
- 子 Grant TTL 不得超过父 Workflow。
- `(grant_id, path, git_operation, request_digest)` 重复使用时拒绝。
- Grant 支持独立 `issued/verified/used/expired/revoked` 生命周期。
实现:
- `active-runtime/app/aios_v2/provenance_grants.py`
- `active-runtime/app/aios_v2/provenance_scoped.py`
## 9. Service Scoped Writer Contract
生产进程只加载职责所需 Grant:
- AIOS Application:`runtime_execute`、`evidence_write`、`audit`。
- Git Worker:`git_operation`,代码提交场景另需独立 `code_change` Grant。
- Deployment Worker:`deployment`。
- Rollback Worker:`rollback`。
- CLI 单次授权:一个 `--path` + 一个 `--grant-id`。
约束:
- 空 Scope 拒绝。
- 多余 Grant 映射拒绝。
- 未配置路径的方法调用拒绝。
- Receipt 必须记录 `grant_id`、`writer_id` 和服务 Scope。
## 10. Dispatch Contract
### 10.1 DispatchRecord
```text
dispatch_id
tenant_id
task_id
agent_id
principal_id
runtime
runtime_execution_id
trace_id
provenance_id
run_id
authorization_id
state
expected_worker_id
result_digest
created_at
updated_at
completed_at
```
状态:
```text
PREPARED
RUNNING
QUEUED
COMPLETED
FAILED
CANCELLED
```
约束:
- Dispatch 必须在 Runtime 副作用之前持久化。
- 同一租户的 Task 和 Runtime Execution 各只能绑定一个 Dispatch。
- 终态 Dispatch 不得跳转到不同终态。
- 相同终态结果允许幂等读取,不同结果摘要拒绝。
- SQLite Repository 必须支持按 Task 和 Runtime Execution 恢复。
实现:`active-runtime/app/aios_v2/dispatch_store.py`
## 11. OpenClaw Contract
### 11.1 OpenClawCommand v2
```text
contract_version = aios.openclaw.command.v2
signature_algorithm = Ed25519
command_id
tenant_id
task_id
trace_id
action
payload
idempotency_key
created_at
expires_at
key_id
public_key_fingerprint
signature
```
签名覆盖全部未签名字段,包括算法、Key ID 和公钥 Fingerprint。
约束:
- 使用规范化 JSON 与 Ed25519。
- HMAC-SHA256 不再是生产命令合同。
- 只接受由已批准任务和已授权 Dispatch 生成的命令。
- 过期、未来时间超差、签名错误、Key ID 或 Fingerprint 不匹配时拒绝。
- Fingerprint 使用恒定时间比较。
- `key_id` 只引用密钥,不包含密钥材料。
- Candidate 组合层必须把 `AIOS_OPENCLAW_SIGNING_KEY_ID` 原样传入 Orchestrator,
不得在已有批准 Key ID 时回落为默认值。
### 11.2 OpenClawResult
```text
command_id
tenant_id
task_id
trace_id
state
output
error
started_at
finished_at
runtime_id
attempts
```
终态:`COMPLETED`、`FAILED`、`CANCELLED`、`TIMED_OUT`、`REJECTED`。
约束:
- Result 必须匹配持久 Dispatch 的 command、tenant、task、trace、principal、provenance 和 run。
- 只有 Orchestrator 可以更新 AIOS Task。
- callback 审计不是权威 Reconcile。
实现:`active-runtime/app/aios_openclaw/`
## 12. Hermes Contract
### 12.0 内部生命周期入口
- 路径:`/api/v2/internal/runtime/hermes/lifecycle/{claim,start,heartbeat,complete,fail,cancel,status}`。
- `AIOS_HERMES_LIFECYCLE_API_ENABLED` 默认关闭;关闭时按不存在的路由 fail closed。
- 仅可信反向代理注入且具备 `runtime` 或 `platform_admin` 角色的身份可调用。
- 所有写请求必须携带并严格匹配 tenant、task、trace、principal、provenance、run、
dispatch、job、cell、profile;Claim 还必须指定精确 job,不能从分区中误领其他 Job。
- Worker 不得直接修改 Task Store。Complete、Fail 和 Cancel 产生的 HermesResult
必须经持久 Dispatch 校验并由 `ExecutionOrchestrator.reconcile_hermes` 终态化。
- 每次成功生命周期写入同时落盘 Hermes 状态、Audit/Event 和 Execution Evidence。
### 12.1 HermesJob
```text
job_id
tenant_id
cell_id
profile_id
task_id
trace_id
action
payload
idempotency_key
priority
state
revision
assigned_worker_id
lease_expires_at
created_at
updated_at
```
约束:
- `tenant_id + cell_id + profile_id` 形成执行分区。
- claim 只能领取同分区 Job。
- Lease 到期 Job 回到 QUEUED。
- Hermes Store 使用 `AIOS_V2_STATE_DIR/hermes.sqlite3` 保存 Job、Result、Lease、
Idempotency 和 Worker heartbeat;Adapter 仍是唯一 Queue/Lease 状态机。
- Lease 到期重排必须清空 `assigned_worker_id`、清空 `lease_expires_at`、递增 revision
并持久化。重启后由显式 lifecycle status/claim 或健康检查安全触发重排。
- revision 冲突不得覆盖。
### 12.2 HermesResult
```text
job_id
tenant_id
task_id
trace_id
state
worker_id
output
error
started_at
finished_at
```
约束:
- 终态 Result 必须携带 Worker ID。
- Worker 必须持有 Job Lease。
- Result 必须匹配 Dispatch、Principal、Parent Provenance 和 Run。
- 相同 Worker、相同终态和相同 output/error 的重复提交返回原 Result;不同终态或
不同结果内容返回 Conflict,不得覆盖已持久化终态。
- 只允许内部入口 `POST /api/v2/internal/runtime/hermes/reconcile` 处理。
- 旧入口 `POST /api/v2/hermes/results/reconcile` 返回 `410 Gone`。
实现:`active-runtime/app/aios_hermes/`
## 13. Execution Evidence Contract
### 13.1 ExecutionEvidence
```text
execution_id
tenant_id
task_id
agent_id
executor
branch
status
progress_completed
progress_total
current_step
last_commit
test_status
test_summary
result_summary
claimed_at
heartbeat_at
completed_at
provenance_id
run_id
last_authorization_id
```
约束:
- 按租户存储和查询。
- 生产 `claim/heartbeat/complete` 落盘前必须通过 Evidence Grant。
- Evidence ID 不得被不同 Task、Agent 或 Provenance Context 复用。
- 不得包含 Secret 或完整敏感 Payload。
实现:`active-runtime/app/aios_v2/execution_tracking.py`
## 14. Git Writer Contract
允许操作:`READ_REF`、`FETCH`、`CREATE_BRANCH`、`COMMIT`、`MERGE`、`PUSH`、`UPDATE_EVIDENCE`,具体能力由 Git Grant 限定。
约束:
- 禁止直接操作 `main` 或 `master`。
- 所有命令使用参数数组,禁止 Shell 字符串拼接。
- Commit 和 Evidence Commit 必须先计算文件内容摘要并完成 Grant 授权,再执行 `git add`。
- `UPDATE_EVIDENCE` 只允许 `.agent/` 路径。
- 输出必须脱敏并截断。
实现:`active-runtime/app/aios_v2/provenance_git.py`
## 15. Web/API Contract
### 15.1 可信身份头
```text
X-AIOS-Authenticated
X-AIOS-Tenant-ID
X-AIOS-Principal-ID
X-AIOS-Roles
X-AIOS-Trace-ID
X-AIOS-Session-ID
X-AIOS-Policy-Version
X-AIOS-Provenance-ID
X-AIOS-Run-ID
```
生产约束:
- 可信代理必须删除客户端同名头后重新注入。
- 应用不得直接暴露公网端口。
- Header 与 Body 中重复的 Provenance/Run 值必须一致。
### 15.2 核心 API
```text
GET /api/v2/health
POST /api/v2/tenants
GET /api/v2/tenants/current
POST /api/v2/agents
GET /api/v2/agents
POST /api/v2/tasks
GET /api/v2/tasks
GET /api/v2/tasks/{id}
POST /api/v2/tasks/{id}/approve
POST /api/v2/tasks/{id}/cancel
POST /api/v2/tasks/{id}/dispatch
GET /api/v2/audit
GET /api/v2/runtime
GET /api/v2/executions
GET /api/v2/gitops
POST /api/v2/internal/runtime/hermes/reconcile
```
公共生命周期旁路:
```text
POST /api/v2/tasks/{id}/start -> 403
POST /api/v2/tasks/{id}/complete -> 403
POST /api/v2/tasks/{id}/fail -> 403
```
## 16. Persistence Contract
候选版:
- Control Plane:SQLite Snapshot。
- Dispatch:独立 SQLite Repository。
- Provenance/Grant/Usage/Event:SQLite WAL。
- Evidence/Receipt:租户隔离原子文件写入。
- 写事务使用原子替换或 `BEGIN IMMEDIATE`。
生产替换要求:
- 不改变 Domain/API Contract。
- 提供 migration、backup、restore、HA、隔离和容量测试。
- 恢复时保留已使用 Grant 与终态 Dispatch,禁止旧授权重放。
## 17. Deployment Contract
### systemd
- 非 root `aios` 用户。
- 状态目录 `/var/lib/aios-v2`。
- 私钥通过受保护 credential 注入,不写入 Git 或普通环境文件。
- 私钥文件必须是 regular file、禁止 symlink、权限 `0600` 或更严格。
### Docker
- 非 root UID/GID。
- read-only root filesystem。
- drop all capabilities。
- 独立状态 volume 和 Secret mount。
### Rollback
- 回滚前备份状态。
- 默认只回滚代码。
- 状态恢复需独立 Rollback Grant 和显式批准。
- 回滚后必须执行 Healthcheck 与恢复验证。
- Release Archive 必须保留 `rollback.sh` 与 `healthcheck/check.sh` 的 executable bit;
验收测试必须阻止非执行模式进入候选包。
- `systemctl start` 后的回滚健康检查必须执行有界 readiness 重试,默认最多 30 次、
每次间隔 1 秒;不得因服务尚未监听的一次瞬时拒绝误判目标 Release 失败。
## 18. 契约变更规则
以下变化必须在同一分支更新本文档、追踪矩阵和相关 ADR:
- 字段、状态、Header、API、错误语义;
- 租户、安全和 Provenance 约束;
- OpenClaw/Hermes 边界;
- Dispatch、Persistence、Deployment、Rollback Contract;
- Evidence、Audit、Receipt 格式;
- 服务 Scope 或 Grant 类型。
不兼容变化必须新增 ADR 和版本号,不得静默修改。正式 Gate A 结论必须基于实际执行 Acceptance 和保存的证据,不得仅凭测试文件存在宣布通过。
+79
View File
@@ -0,0 +1,79 @@
# AIOS V2 需求—架构—代码—测试—部署追踪矩阵
- 文档版本:`V2R1-hardening-candidate`
- 状态:实现与测试代码已提交;测试执行和 Acceptance 证据待完成
- 基线需求:`UR-001`~`UR-022`
- 判定规则:测试文件存在不等于测试通过;只有实际执行并保存证据后,状态才能改为“已验证”
## 1. 状态定义
| 状态 | 含义 |
|---|---|
| 已实现 | 代码、配置或文档已经提交到当前加固分支 |
| 测试已提交 | 自动测试代码已经提交,但本报告没有实际执行结果 |
| 待验收 | 需要运行 `deploy/aios-v2/acceptance/run-all.sh` 或对应预发布检查并保存证据 |
| 未批准 | 尚未获得 Owner 的 Merge、Tag 或生产部署批准 |
## 2. 用户需求追踪
| 需求 | 架构/决策 | 主要实现 | 测试代码 | 部署/验收入口 | 当前状态 |
|---|---|---|---|---|---|
| UR-001 唯一控制面 | ADR-001;模块契约第 2~6 节 | `active-runtime/app/aios_v2/control_plane.py`、`gateway.py`、`orchestrator.py` | `tests/aios_v2_candidate/test_candidate_flow.py` | `deploy/aios-v2/acceptance/run-all.sh`;公共生命周期旁路负向检查 | 已实现;测试已提交;待验收 |
| UR-002 复用现有能力 | ADR-002 | `active-runtime/app/aios_openclaw/`、`active-runtime/app/aios_hermes/`、`active-runtime/app/aios_v2/orchestrator.py` | `test_candidate_flow.py` | OpenClaw/Hermes 联调 | 已实现;真实 Runtime 联调待验收 |
| UR-003 OpenClaw/Hermes 解耦 | ADR-002、ADR-010;模块契约第 11、12 节 | OpenClaw Adapter、Hermes Adapter/Worker、内部 Lifecycle/Reconcile | `test_candidate_flow.py`、`test_hermes_live_lifecycle.py` | 默认关闭的内部 Runtime API;Runtime audit/evidence 检查 | 已实现;Hermes 专项 17/17 |
| UR-004 全面 Web 化 | 目标架构;模块契约第 15 节 | `active-runtime/app/aios_v2/app.py`、`console.py`、`gateway.py` | `test_candidate_flow.py` | Web/API 冒烟、Healthcheck | 已实现;UI/浏览器验收待执行 |
| UR-005 多租户隔离 | ADR-004 | `control_plane.py`、`persistence.py`、`execution_tracking.py`、`dispatch_store.py` | `test_candidate_flow.py::test_cross_tenant_task_read_is_denied` | 跨租户负向 Acceptance | 已实现;测试已提交;待验收 |
| UR-006 权威任务生命周期 | ADR-001;模块契约第 4 节 | `control_plane.py`、`gateway.py`、`orchestrator.py` | `test_candidate_flow.py` | `/start`、`/complete`、`/fail` 应返回拒绝;Runtime 严格 Reconcile | 已实现;测试/接口验收待执行 |
| UR-007 策略与审批 | ADR-001 | `control_plane.py`、`gateway.py` | `test_candidate_flow.py` | Approval、deny audit、policy version 检查 | 已实现;策略场景验收待执行 |
| UR-008 幂等与并发安全 | 模块契约第 4、10、12 节 | Task idempotency/revision、Hermes lease/revision、Dispatch 唯一约束 | `test_candidate_flow.py`、`test_hermes_live_lifecycle.py` | Lease 到期重排、换主、相同终态幂等、不同终态冲突 | 已实现;专项测试通过 |
| UR-009 可观测与执行证据 | 模块契约第 6、13 节 | `execution_tracking.py`、`gitops_status.py`、`console.py` | `test_candidate_flow.py::test_execution_tracking_persists_claim_heartbeat_and_result` | `/api/v2/executions`、Evidence Receipt 检查 | 已实现;测试已提交;待验收 |
| UR-010 GitOps 唯一变更流程 | ADR-003 | `active-runtime/app/aios_v2/provenance_git.py`、GitOps 工具与任务资产 | `test_provenance.py`、Grant 隔离测试 | Branch/Commit/Push 门禁、Controller clean、OID 同步 | 已实现候选控制;环境 GitOps 验收待执行 |
| UR-011 持久化与恢复 | ADR-006、ADR-010;模块契约第 10、12、16 节 | `persistence.py`、`dispatch_store.py`、`aios_hermes/adapter.py::SQLiteHermesStore`、Provenance/Grant Store | `test_candidate_flow.py`、`test_hermes_live_lifecycle.py` | 运行中 Job 重启恢复、重启后 Reconcile、SQLite quick_check | Hermes/Dispatch/Task 恢复测试通过;生产 HA/容量待验收 |
| UR-012 审计与合规 | ADR-001、ADR-004、ADR-009 | `control_plane.py` AuditEvent、`app.py` callback audit、Provenance/Grant Event/Receipt | `test_candidate_flow.py`、`test_provenance.py`、`test_provenance_grant_isolation.py` | `/api/v2/audit`、Receipt/Event 完整性检查 | 已实现候选审计;不可变外部固化待预发布 |
| UR-013 密钥安全 | ADR-009;模块契约第 6、11、17 节 | Ed25519 Signer/Verifier、protected file reader、SecretRef 配置 | `test_provenance.py`、`test_candidate_flow.py` | Secret scanner、权限检查、容器/systemd credential 检查 | 已实现;环境密钥注入验收待执行 |
| UR-014 部署与回滚 | 迁移与交付文档;模块契约第 17 节 | `deploy/aios-v2/systemd/`、`docker/`、`healthcheck/`、`rollback/` | Acceptance 脚本与候选测试 | systemd/Docker 安装、Healthcheck、备份与回滚演练 | 资产已提交;实际部署/回滚未执行 |
| UR-015 文档与代码同步 | ADR-005 | `tools/aios_v2_docs_code_check.py`、`docs/aios-v2/` | 文档门禁脚本 | Acceptance Runner 调用文档门禁 | 已实现;门禁尚未实际执行 |
| UR-016 Workflow Run Provenance | ADR-009;模块契约第 7 节 | `active-runtime/app/aios_v2/provenance.py`、`provenance_grants.py::WorkflowProvenanceIssuer` | `test_provenance.py`、`test_provenance_grant_isolation.py::test_parent_workflow_has_no_operational_rights` | CLI `issue-workflow`、状态/撤销检查 | 已实现;测试已提交;待验收 |
| UR-017 最小权限子 Grant | ADR-009;模块契约第 8 节 | `provenance_grants.py`、`provenance_scoped.py` | `test_provenance_grant_isolation.py` | `issue-grant`、Grant status/revoke、Receipt 检查 | 已实现;专项测试已提交;待验收 |
| UR-018 Runtime Result 信任绑定 | ADR-009、ADR-010;模块契约第 10~12 节 | `orchestrator.py`、`dispatch_store.py`、内部 Hermes Lifecycle/Reconcile | `test_hermes_live_lifecycle.py` | tenant/task/trace/principal/provenance/run/dispatch/job/cell/profile/worker 负向矩阵 | 已实现;专项 17/17 |
| UR-019 Dispatch 持久化与恢复 | ADR-009、ADR-010;模块契约第 10、12 节 | `dispatch_store.py`、`orchestrator.py`、`app.py`、`SQLiteHermesStore` | Dispatch recovery 与 Hermes restart-reconcile 测试 | 进程重启后读取 Dispatch/Job 并完成 Reconcile | 已实现;测试通过 |
| UR-020 OpenClaw Ed25519 命令合同 | ADR-009;模块契约第 11 节 | `active-runtime/app/aios_openclaw/contract.py`、`adapter.py`、`app.py` | `test_candidate_flow.py`、`test_provenance.py` | 真实 OpenClaw v2 合同联调、Key rotation、clock skew/expiry 负向验收 | 已实现;测试代码已升级;真实联调待执行 |
| UR-021 服务级权限隔离 | ADR-009;模块契约第 9 节 | `provenance_scoped.py`、`provenance_operations.py`、`app.py` | `test_provenance_grant_isolation.py` | Application 仅加载 Runtime/Evidence/Audit;Git/Deploy/Rollback 单独 Scope | 已实现;专项测试已提交;配置验收待执行 |
| UR-022 CLI 单 Grant 授权 | ADR-009;模块契约第 9 节 | `provenance_cli.py` | `test_provenance_grant_isolation.py` 提供 Writer/Grant 基础验证;CLI 端到端待补执行证据 | `authorize --path ... --grant-id ...`;旧批量 Grant 参数应不存在 | 已实现;CLI 端到端验收待执行 |
## 3. P0 加固项追踪
| P0 | 风险 | 实现 | 测试/证据 | 状态 |
|---|---|---|---|---|
| P0-1 公共生命周期旁路 | 客户端直接推进 RUNNING/COMPLETED/FAILED | `gateway.py` 公共入口拒绝;`orchestrator.py` 权威推进 | API 负向 Acceptance | 代码完成;待执行 |
| P0-2 Dispatch 持久化 | 重启丢失 Runtime 绑定 | `dispatch_store.py` SQLite Repository;`app.py` 接线 | Dispatch restart recovery 测试 | 代码/测试已提交;待执行 |
| P0-3 Result 严格绑定 | 伪造或错配 Runtime Result 更新 Task | `orchestrator.py` 校验 Runtime Execution、Tenant、Task、Trace、Principal、Provenance、Run | Runtime 正向恢复测试;负向 Acceptance | 代码完成;完整验收待执行 |
| P0-4 OpenClaw Ed25519 | HMAC 共享密钥和合同不一致 | `aios_openclaw/contract.py` v2 Ed25519;安全文件读取 | 候选 Flow 测试;真实 Runtime 联调 | 代码/测试已提交;联调待执行 |
| P0-5 最小权限 Grant | 单凭证拥有全部生产路径 | Workflow + Child Grant + Scoped Writer + 单 Grant CLI | Grant isolation 专项测试 | 代码/测试已提交;待执行 |
## 4. Acceptance 证据要求
完整 Acceptance 至少应保存:
1. 测试命令、开始/结束时间、退出码和完整日志。
2. `test_candidate_flow.py`、`test_provenance.py`、`test_provenance_grant_isolation.py` 的实际结果。
3. 文档—代码同步门禁实际结果。
4. Secret scanner、依赖和容器/systemd 安全检查结果。
5. OpenClaw Ed25519 v2 真实联调结果。
6. Hermes 内部 Reconcile 正向与伪造上下文负向结果。
7. Control Plane、Dispatch、Provenance/Grant 的重启恢复结果。
8. 部署、Healthcheck、备份和回滚演练结果。
9. 分支、Commit、Controller clean、远端 OID 同步证据。
10. Owner 对 Merge、Tag 和生产部署的独立批准。
## 5. 当前发布门禁
| Gate | 状态 | 说明 |
|---|---|---|
| 实现 | 完成 | 五项 P0 代码与配置已提交当前加固分支 |
| 测试代码 | 完成 | 回归、Dispatch recovery、Grant isolation 测试已提交 |
| 测试执行 | 未完成 | 当前没有可验证的实际运行日志和退出码 |
| 文档同步 | 已提交 | 需求、契约、ADR、追踪矩阵已更新;门禁尚未执行 |
| Acceptance | 未完成 | `run-all.sh` 尚无本轮实际执行证据 |
| Gate A | 未批准 | 只能在 Acceptance 通过并完成独立审查后批准 |
| Merge/Tag/生产部署 | 未批准 | 需要 AIOS Owner 明确批准 |
+272
View File
@@ -0,0 +1,272 @@
# AIOS V2 文档治理规范
- 文档版本:`V2R1C0-candidate`
- 适用范围:`docs/aios-v2/`、架构决策记录、生产候选报告、在线任务合同、部署与验收说明
- 状态:已采纳
## 1. 目的
本规范用于保证 AIOS V2 的需求、架构、代码、测试、部署和运行事实始终保持一致,避免出现以下问题:
- 代码已经变化,但架构文档仍描述旧边界;
- API、字段或状态机变化后,测试与追踪矩阵未同步;
- OpenClaw、Hermes 或 Agent Router 的控制职责发生漂移;
- 候选分支存在实现,但版本无法解释为什么这样设计;
- 部署、回滚或安全策略发生变化,却没有形成可审计文档;
- 归档包只包含摘要或占位符,无法作为正式交付材料。
## 2. 文档语言要求
所有面向用户、产品、架构、运维、审计和发布的正式文档统一使用中文。
以下内容保留原始英文技术标识:
- 类名、函数名、字段名、环境变量;
- API 路径、HTTP 方法和 Header;
- Git 分支、Commit、Tag;
- OpenClaw、Hermes、Agent Router、GitOps 等专有名称;
- systemd、Docker、SQLite、HMAC、HTTP、JSON 等标准术语。
禁止在同一文档中无必要地切换中英文叙述。
## 3. 文档完整性要求
每份正式文档至少包含:
1. 文档目的;
2. 适用范围;
3. 当前版本和状态;
4. 完整正文;
5. 与代码、测试或部署的映射;
6. 验收规则;
7. 已知限制;
8. 后续演进方向;
9. 更新触发条件。
正式归档包禁止使用以下内容代替正文:
- “后续补充”;
- “待完善”;
- 只有标题没有说明;
- 只包含目录摘要;
- 与候选分支代码不一致的旧文档。
确实尚未验证的项目,应明确标记为“已实现但尚未验证”,并说明验证命令、责任方和进入下一状态的条件。
## 4. 文档分类与维护责任
| 文档 | 主要内容 | 触发更新的变化 |
|---|---|---|
| `README.md` | 文档索引、版本状态、实现地图 | 新增模块、目录、版本或交付物 |
| `01-USER-REQUIREMENTS.md` | 用户目标、功能需求、非功能需求、验收场景 | 用户行为、产品范围、验收标准变化 |
| `02-TARGET-ARCHITECTURE.md` | 分层、边界、数据流、安全、租户、部署架构 | 控制权、Runtime 边界、数据流或安全模型变化 |
| `03-MIGRATION-AND-DELIVERY.md` | 迁移阶段、GitOps、发布门禁、回滚、Bug 修复 | 迁移路线、发布流程、门禁或回滚方式变化 |
| `04-MODULE-CONTRACTS.md` | 数据模型、API、状态机、错误语义、Runtime Contract | 字段、接口、状态机、Header、错误行为变化 |
| `05-TRACEABILITY-MATRIX.md` | 需求到代码、测试、部署、验收的映射 | 代码、测试、部署路径或状态变化 |
| `adr/*` | 跨模块、长期或不可逆架构决策 | 关键决策新增、替换或废弃 |
| `AIOS-V2R1-PRODUCTION-CANDIDATE.md` | 候选版本总览、测试、部署、风险和发布结论 | Candidate SHA、测试证据、部署或风险变化 |
| `tasks/*` | OpenClaw、Codex 等在线执行任务 | 职责、边界、阻塞、验收或执行流程变化 |
## 5. 代码变化与文档同步规则
以下代码或配置路径发生变化时,必须同步更新 `docs/aios-v2/`:
### 5.1 Control Plane
触发路径:
```text
active-runtime/app/aios_v2/control_plane.py
active-runtime/app/aios_v2/persistence.py
active-runtime/app/aios_v2/gateway.py
active-runtime/app/aios_v2/app.py
active-runtime/app/aios_v2/orchestrator.py
active-runtime/app/aios_v2/execution_tracking.py
active-runtime/app/aios_v2/gitops_status.py
```
至少检查:
- 用户需求;
- 目标架构;
- 模块契约;
- 追踪矩阵;
- 必要时新增或更新 ADR。
### 5.2 OpenClaw
触发路径:
```text
active-runtime/app/aios_openclaw/
```
至少检查:
- OpenClawCommand、OpenClawResult、RuntimeHealth;
- 签名、幂等、取消、超时和审计边界;
- ADR-002;
- 真实 Runtime 联调要求。
### 5.3 Hermes
触发路径:
```text
active-runtime/app/aios_hermes/
```
至少检查:
- HermesJob、HermesResult、HermesHealth;
- tenant/cell/profile 分区;
- Worker Lease、Heartbeat、Recovery;
- ADR-002、ADR-004。
### 5.4 部署与运维
触发路径:
```text
deploy/aios-v2/
```
至少检查:
- 迁移与交付策略;
- 部署契约;
- 健康检查;
- 回滚;
- 候选报告。
### 5.5 API、Web 与用户行为
任何 API 路径、Header、请求/响应、页面或用户操作变化,必须同步:
- `01-USER-REQUIREMENTS.md`;
- `02-TARGET-ARCHITECTURE.md`;
- `04-MODULE-CONTRACTS.md`;
- `05-TRACEABILITY-MATRIX.md`。
## 6. ADR 治理
必须新增或更新 ADR 的场景:
- AIOS、OpenClaw、Hermes、Agent Router 的控制边界变化;
- 多租户隔离模型变化;
- 持久化、消息、审计、密钥或部署出现跨模块变化;
- 不兼容 API、字段或状态机变化;
- GitOps、文档门禁或发布策略变化;
- 替换核心基础设施或引入新的权威事实源。
ADR 状态:
- `已提出`:尚未正式采纳;
- `已采纳`:当前有效;
- `已替代`:被新 ADR 替代;
- `已废弃`:保留历史但禁止新实现采用。
任何 ADR 替代必须说明:
- 被替代的 ADR;
- 迁移影响;
- 兼容性和回滚方案;
- 受影响代码、测试和文档。
## 7. 文档—代码匹配门禁
验收必须执行:
```text
python3 tools/aios_v2_docs_code_check.py
```
门禁至少检查:
1. 必需文档存在且非空;
2. 文档语言以中文为主;
3. 文档引用的关键代码、测试和部署路径存在;
4. 文档列出的核心 API 能在代码中找到;
5. 文档列出的核心模型和字段能在代码中找到;
6. Traceability Matrix 覆盖所有 `UR-*` 需求;
7. ADR 索引引用的文件存在;
8. 架构或功能代码变化时,当前交付范围包含 `docs/aios-v2/` 变化;
9. 文档中不存在未解决 Git 冲突标记;
10. 正式文档中不存在摘要占位符或空章节。
门禁失败时:
- Candidate 不得标记为 `Test-verified`;
- OpenClaw 不得继续生产部署;
- Codex 必须登记 blocker 并给出修复建议;
- 修复必须进入 Git 分支并统一审视。
## 8. 在线任务文档要求
OpenClaw 和 Codex 任务必须使用中文说明,并保留可执行字段名。
每个任务至少包含:
- 任务目标;
- 责任范围;
- 禁止事项;
- 输入和输出;
- 分支;
- 执行状态;
- 验收命令;
- claim、heartbeat、progress、blocker、commit、test_status、result 证据要求;
- 阻塞升级路径。
## 9. 归档包要求
正式归档包必须包含完整正文:
```text
docs/aios-v2/
tasks/openclaw-production-deploy-task.yaml
tasks/codex-production-supervisor-task.yaml
deploy/aios-v2/
AIOS-V2R1-PRODUCTION-CANDIDATE.md
```
归档包必须同时生成清单:
- 文件路径;
- 文件大小;
- SHA-256;
- Candidate branch;
- Candidate commit;
- 生成时间。
禁止只打包摘要副本。
## 10. 审查职责
### OpenClaw
- 部署和运行修复时同步相关部署、运维和已知问题文档;
- 不得修改架构边界;
- 发现架构影响时升级给 Codex。
### Codex
- 监督需求、架构、契约、追踪和 ADR 一致性;
- 解决阻塞;
- 拒绝缺少文档或证据的补丁;
- 输出统一发布建议。
### AIOS Owner
- 审批合并、Tag 和生产部署;
- 对重大架构决策和风险接受做最终确认。
## 11. 验收状态
文档完成状态只有在以下条件全部满足时才标记为“完整”:
- 所有必需文档和 ADR 已提交;
- 文档—代码匹配检查通过;
- 归档包包含仓库完整正文;
- 任务合同为中文完整版本;
- Candidate commit、清单和 SHA-256 已记录;
- Codex 监督结论不存在未解决阻塞。
@@ -0,0 +1,211 @@
# AIOS V2R1C2 统一门户与可迁移执行器补充契约
- 文档版本:`V2R1C2-candidate`
- 状态:实现与测试资产已提交;在线 Acceptance、Owner 审批与生产发布未完成
- 适用范围:统一角色门户、External Task Truth、Executor Provider、Execution Plan、通知回执、候选部署与 Release Gate
- 关联决策:`ADR-011`
## 1. 新增用户需求
### UR-023 统一角色感知门户
AIOS 必须使用同一 Web Portal 服务平台管理员、租户管理员、操作员、审批人、审计员、Runtime、发布经理和只读用户。服务端能力矩阵是唯一授权依据,前端隐藏按钮不得替代服务端授权。
验收:
- 门户至少包含总览、租户与 Agent、任务、执行中心、授权、Runtime、Evidence、Audit、GitOps/Release。
- `GET /api/v2/me/capabilities` 返回可信身份解析后的角色、能力和导航。
- 所有写操作必须再次进行服务端 Capability 检查。
- 浏览器不得直接调用 OpenClaw、Hermes、Codex 或未来执行器。
### UR-024 外部执行任务真相
任何由 OpenClaw、Hermes、Codex 或未来 Provider 执行的任务,必须进入同一持久 Execution Truth Registry。任务文件、进程存在、日志字符串或退出码不能单独证明任务已经执行。
统一生命周期:
```text
DECLARED -> REGISTERED -> CLAIMED -> RUNNING -> SUCCEEDED
| -> FAILED
+ timeout -> STALLED
REGISTER deadline exceeded -> NOT_STARTED
```
验收:
- 每次执行绑定 `task_id`、`executor_type`、`executor_id` 和 `executor_run_id`。
- Claim、Start、Heartbeat、Success、Fail 均持久化事件。
- Success 只有在所有步骤和 Required Evidence SHA256 完整时才允许。
- 启动超时和心跳超时必须产生可查询的阻塞状态。
### UR-025 可迁移 Executor Provider
执行器类型不得由核心枚举固定。OpenClaw、Hermes 和 Codex 只是内置 Provider;未来执行器必须通过版本化 Provider Descriptor、Catalog 和 Adapter 接入,复用相同生命周期、Evidence、Notification 与 Gate。
验收:
- Provider Descriptor 包含 `executor_type`、`adapter_version`、`protocol_version`、`trust_roles`、`capabilities` 和配置 Schema。
- Catalog 支持原子持久化、SHA256、防篡改导入导出和无 Secret 迁移包。
- 新增 Provider 不修改 Truth Registry、Execution Plan 状态机、Portal 或 Release Gate。
- API 的可信角色由 Provider Catalog 动态解析,不硬编码执行器名称。
### UR-026 多 Provider Execution Plan
发布任务必须可表达为任意 Provider 组成的 DAG。下游 Stage 在全部依赖 Gate 通过前保持 `WAITING_DEPENDENCY`,不得提前声明并消耗启动时限。
验收:
- Plan 使用 `aios.execution-plan.v2`,执行协议使用 `aios.executor.v1`。
- Stage 包含 Provider、角色、步骤、依赖、Required Evidence、通知事件和超时。
- Plan 创建时只声明根 Stage;依赖通过后原子激活下游 Stage。
- 聚合 Release Gate 只有在全部 Required Stage 的 Truth、Evidence、Dependency 和 Notification Gate 通过时才允许放行。
### UR-027 Owner 主动通知与发布门禁
候选部署开始、成功和失败必须生成持久 Notification Outbox 事件。投递目标使用 Secret Reference,真实微信投递必须回写 Receipt SHA256。通知失败可幂等重试,但不得生成重复事件。
验收:
- `(task_id, event_type)` 唯一,同一 STARTED/SUCCEEDED/FAILED 事件只有一个 Notification ID。
- `PENDING` 和 `FAILED` 均可重试;成功后原地更新为 `DELIVERED`。
- Release Gate 要求配置的 STARTED 和 SUCCEEDED 通知均有 Receipt。
- Git、日志、Portal 和任务资产不得存储微信账号、Token 或 Secret。
## 2. 补充模块契约
### 2.1 Executor Provider Catalog
权威实现:
- `active-runtime/app/aios_v2/executor_protocol.py`
- `active-runtime/app/aios_v2/executor_catalog.py`
- `active-runtime/app/aios_v2/official_executor_providers.py`
Catalog 只包含可迁移描述符,不包含 Endpoint Credential、Token、私钥或 Runtime Secret。导入迁移包必须同时校验 Package SHA256 与 Catalog SHA256。
### 2.2 External Execution Truth
权威实现:
- `active-runtime/app/aios_v2/task_execution_registry.py`
- `active-runtime/app/aios_v2/external_task_control.py`
- `active-runtime/app/aios_v2/external_task_query.py`
Truth Record 是外部执行状态的唯一事实源。`SUCCEEDED` 不等于 Release 可放行;仍需 Required Notification Receipt 和 Plan Dependency Gate。
### 2.3 Execution Plan
权威实现:
- `active-runtime/app/aios_v2/execution_plan.py`
- `active-runtime/app/aios_v2/executor_worker.py`
- `tools/aios_execution_plan_cli.py`
Plan 是 Stage 定义的唯一事实源。不得再复制维护三个独立 Task Manifest,否则会产生 Plan/Task 漂移并可能提前启动下游计时。
### 2.4 Portal/API
权威实现:
- `active-runtime/app/aios_web/capabilities.py`
- `active-runtime/app/aios_web/application.py`
- `active-runtime/app/aios_web/release_application.py`
- `active-runtime/app/aios_web/external_task_api.py`
- `active-runtime/app/aios_web/execution_plan_api.py`
- `active-runtime/app/aios_web/console.py`
核心新增 API:
```text
GET /api/v2/portal/execution
GET /api/v2/portal/external-tasks
GET /api/v2/internal/execution-plans/{plan_id}/discover?executor={provider}
GET /api/v2/internal/execution-plans/{plan_id}/snapshot?executor={provider}
GET /api/v2/internal/execution-plans/{plan_id}/gate?executor={provider}
GET /api/v2/internal/external-tasks/discover?executor={provider}
POST /api/v2/internal/external-tasks/{task_id}/{register|claim|start|heartbeat|succeed|fail}
GET /api/v2/internal/external-tasks/notifications?executor={provider}
POST /api/v2/internal/external-tasks/notifications/{notification_id}/ack
```
内部 API 只接受可信代理注入身份。Provider Trust Role、Task Manifest Executor 与 Principal/Executor ID 必须一致。
### 2.5 Candidate Deployment
候选服务:
- `deploy/aios-v2/systemd/aios-v2r1c2-portal.service`
- `deploy/aios-v2/systemd/aios-v2r1c2-portal.env.example`
约束:
- V2R1C1 生产继续监听 `127.0.0.1:18920`。
- V2R1C2 Candidate 只监听 `127.0.0.1:18921`。
- Candidate 使用 `/var/lib/aios-v2r1c2-portal`,不得复用生产状态目录。
- `AIOS_RELEASE_SOURCE_OID` 必须解析为精确 40 位 Git OID,否则拒绝启动。
- Portal 不直接创建 Tag、修改 `main` 或部署生产。
## 3. V2R1C2 发布计划
权威计划:
- `plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json`
DAG:
```text
openclaw-candidate-deploy
|
v
hermes-runtime-validation
|
v
codex-release-review
|
v
AIOS Owner approval -> tag/release request
```
OpenClaw 负责隔离候选部署、离线/在线 Acceptance、Role/Tenant 负向测试、重启与回滚演练;Hermes 负责 Queue/Lease/Worker/恢复验证;Codex 负责源代码、证据、安全边界、文档追踪和发布建议审查。所有 Provider 使用同一 Truth Registry。
## 4. 需求追踪矩阵
| 需求 | 决策/契约 | 实现 | 测试/验收 | 状态 |
|---|---|---|---|---|
| UR-023 统一门户 | ADR-011;本文 2.4 | `aios_web/*` | `tests/web/test_portable_executor_release.py`;在线 Portal Acceptance | 已实现;测试资产已提交;在线未执行 |
| UR-024 执行真相 | ADR-011;本文 2.2 | `task_execution_registry.py`、`external_task_control.py` | Truth/Evidence/Notification tests;CLI Gate | 已实现;实际执行证据待 OpenClaw/Hermes/Codex |
| UR-025 Provider 可迁移 | ADR-011;本文 2.1 | `executor_protocol.py`、`executor_catalog.py`、`official_executor_providers.py` | Catalog migration/tamper/dynamic Provider tests | 已实现;测试资产已提交 |
| UR-026 Execution Plan DAG | ADR-011;本文 2.3 | `execution_plan.py`、`executor_worker.py` | 下游延迟激活、聚合 Gate tests | 已实现;在线 DAG 未执行 |
| UR-027 主动通知 | ADR-011;本文 2.2 | Notification Outbox、Worker retry | 首投失败、幂等重试、Receipt Gate test | 已实现;真实微信投递未执行 |
## 5. Acceptance 与证据
离线/在线验收入口:
```text
python3 tools/aios_v2r1c2_acceptance.py \
--source-oid <exact-40-character-oid> \
[--online] \
--evidence-output artifacts/v2r1c2/acceptance-evidence.json
```
专项测试:
- `tests/web/test_portable_executor_release.py`
- `tests/aios_v2_candidate/`
机器证据至少包含:命令、时间、退出码、输出 SHA256、HTTP 响应 SHA256、Source OID、Plan SHA256、Catalog SHA256、Service/Env SHA256、回滚结果和微信 Receipt SHA256。
## 6. 当前门禁状态
| Gate | 状态 | 说明 |
|---|---|---|
| 代码与部署资产 | 已提交 | 当前候选分支包含实现、Plan、systemd、CLI 和测试资产 |
| 测试执行 | 未验证 | 当前 GitOps 工具未执行 Python 测试,禁止声称通过 |
| OpenClaw Candidate Stage | `NOT_STARTED` | 必须由目标环境实际 Discover/Register/Claim/Start |
| Hermes Runtime Stage | `WAITING_DEPENDENCY` | OpenClaw Gate 通过后才激活 |
| Codex Review Stage | `WAITING_DEPENDENCY` | OpenClaw 与 Hermes Gate 通过后才激活 |
| 微信通知 | 未投递 | 需要目标环境 Sender 和 Receipt 回写 |
| Tag/生产发布 | 未批准 | 需要 Plan Gate 通过和 AIOS Owner 明确批准 |
测试文件、Acceptance 脚本、Task YAML、systemd 文件或日志文本的存在都不等于实际 Gate 已通过。
+224
View File
@@ -0,0 +1,224 @@
# 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。
@@ -0,0 +1,51 @@
# V2R1C2 Owner Provenance 与微信接管迁移
## 边界
本迁移不关闭或降低 `aios-tenant-boundary`。普通、已识别的消息可以进入只读对话;
只有服务端确认的 Owner 才能为 `aios_protected_dispatch` 自动签发
`aios.owner-provenance/v2`。未知、阻止名单和受保护操作的非 Owner 请求继续 fail closed。
客户端不得提交可信身份 Header、私钥、签名或 Provenance Token。插件从 OpenClaw
可信 hook 上下文取得 `principal_id`、`agent_id`、`run_id` 和 execution path,并对
精确工具参数计算 SHA256。服务端签发记录绑定:
- `tenant_id`
- `principal_id`
- `task_id`
- `agent_id`
- `run_id`
- `execution_path`
- `request_digest`
- `issued_at` / `expires_at`
- `key_id` 与覆盖全部字段的 Ed25519 signature
参数在 hook 与 tool execute 之间改变、错误身份绑定、过期、撤销或单次 permit 重放
都会被拒绝。
## 配置迁移
私钥仍位于仓库外。部署系统只写入路径引用,并验证目标文件存在、为普通文件且权限
不向 group/other 开放。需要在 Candidate 专用配置中增加:
```text
provenancePrivateKeyPath=<outside-git path>
provenancePublicKeyPath=<outside-git path>
provenanceStoreDir=/var/lib/aios-v2r1c2-portal/provenance
provenanceKeyId=<non-secret key id>
tenantId=<candidate tenant>
protectedDispatchProvider=chatgpt-supervisor
protectedDispatchTransport=mock|sandbox
ownerProvenanceTtlMs<=300000
```
`mockFailFirstDelivery` 只允许在明确的模拟验收配置中启用。模拟输出必须保留:
`simulation=true`、`transport=mock|sandbox`、`real_user_impersonated=false`。
## 部署与回滚
1. 从精确 Candidate OID 构建插件与 Portal;不得从 live 目录回写源码。
2. 备份 Candidate 插件、配置和 unit;不修改 18920 服务或状态目录。
3. 在 18921 完成正向链、负向矩阵、mock/sandbox 微信、restart recovery。
4. 回滚只停止 `aios-v2r1c2-portal.service` 并恢复 Candidate 备份;随后重新验证 18920。
5. 未经 Owner 明确批准,不创建 Tag、不修改 `main`、不替换生产。
@@ -0,0 +1,104 @@
# AIOS V2R1 Hermes 生命周期最终验证报告
- 报告版本:`V2R1-hermes-lifecycle-20260721.3`
- 日期:`2026-07-21`
- 分支:`patch/chatgpt/AIOS-V2R1-HERMES-LIFECYCLE-20260721`
- 固定验证 OID:`95d2e52cb55c7b913e89bc6bed2bcdacf67512d4`
- 总体状态:`PARTIAL / BLOCKED_GLOBAL_WORKTREE_CLEANLINESS`
- 发布决策:`STOP_PREPRODUCTION`
- 生产发布:`NOT_APPROVED`
## 1. 执行结论
`BLK-LIVE-HERMES-FULL-LIFECYCLE-EVIDENCE-MISSING` 的 Hermes 功能与证据子条件已在固定 OID 上全部通过。候选实现复用现有 Hermes Adapter、Queue、Lease、Task 与 Orchestrator 状态机,新增持久 SQLite Store 和默认关闭的受保护内部生命周期接口。Hermes 完整生命周期 `17/17`、候选测试 `49/49`、Gate A、Gate B `10/10`、离线 Acceptance `8/0/1`、真实 OpenClaw HTTP、重启恢复、双向回滚和最终健康检查均通过。
但 Owner 指令规定只有全部 Acceptance 条件同时满足时才允许正式解除该 Blocker。由于“所有 Worktree clean”失败,本报告不得把它标记为正式 `RESOLVED`;准确状态是 `EVIDENCE_COMPLETE / FORMAL_CLOSURE_BLOCKED`。
最终发布门禁仍失败:权威 Bare Repo 登记的 88 个 worktree 中有 10 个脏 worktree。候选 worktree 和活动 Controller worktree 均 clean,且无 locked/prunable;但 Owner 的验收条件要求所有 worktree clean,因此不得准备 RC、不得发起 Tag Approval Request、不得进入预生产评审。
## 2. 关键实现
- `SQLiteHermesStore` 持久保存 Job、Result、Idempotency 与 Worker Heartbeat。
- 现有 `HermesAdapter` 保持唯一 Queue/Lease 状态机,支持指定 Job claim、租约到期 requeue、重启恢复与严格终态冲突。
- 生命周期身份绑定覆盖 tenant、task、trace、principal、provenance、run、dispatch、job、cell、profile。
- start/heartbeat/complete/fail 强制 Lease Owner;complete/fail/cancel 通过 `ExecutionOrchestrator.reconcile_hermes` 更新 Task 与 Dispatch。
- cancel 仅允许同时具备 `runtime` 与 `platform_admin` 的 AIOS Control Plane,要求 `cancellation_source=aios-control-plane`,并拒绝携带 `worker_id`,Worker 只能观察取消。
- 所有成功生命周期操作写入 Audit/Event/Execution Evidence;接口由 `AIOS_HERMES_LIFECYCLE_API_ENABLED` 控制,默认关闭。
## 3. 固定 OID 验证
| 项目 | 结果 |
|---|---|
| Python syntax/import | PASS |
| 全部 candidate tests | `49/49` |
| Grant isolation | `11/11` |
| Dispatch recovery | `1/1` |
| Hermes lifecycle | `17/17` |
| Docs code gate / Gate A | `0 errors / 0 warnings` |
| Secret scan | `547 tracked files / 0 violations` |
| SQLite quick_check | `2/2 ok` |
| Gate B | `10/10` |
| Offline Acceptance | `8 passed / 0 failed / 1 expected warning` |
| OpenClaw real HTTP | `COMPLETED` |
| Hermes full live rerun | `17/17` |
| Restart recovery | `2/2` |
| Previous → Current rollback/forward | PASS |
| State preservation | `STATE_PRESERVED_EXACTLY=PASS` |
| Corrected final health (`16R`) | PASS,三服务 active |
原 `16.log` 由非服务用户执行健康脚本,打印 state-dir 权限失败后被外层脚本错误覆盖为退出码 0,因此不作为通过证据。纠错后的 `16R.log` 使用受控 `sudo env`,子 shell 保留失败码,所有健康项实际 PASS 且 `EXIT_CODE=0`。原文件保留用于审计。
回滚日志中的短暂 connection-refused 发生于 systemd 重启窗口内,受 `AIOS_V2_ROLLBACK_HEALTH_RETRIES=30` 与 1 秒间隔限制;前后两次均在界限内达到健康、输出 `ROLLBACK_COMPLETE`,并完成状态文件精确哈希对账。
## 4. Evidence
- 根目录:`/var/lib/aios-gitops/acceptance/aios-v2r1-hermes-lifecycle-20260721T054205Z`
- `steps.tsv` SHA256:`b2ad20585ba7f576e85657827a0b1db6f252887bbd73bcd718df38623e9d95cf`
- `final-live-gate.log` SHA256:`82040c674759dacf587658c5e936c313a1f50e8e2425be48ad891927befdf4fa`
- `16R.log` SHA256:`641246927a372ebaa9e6dde8b018b88275b2ba15a442473759b23fb1f215d9cf`
- `15.log` SHA256:`b0a474e2a74164f560c4f7572091911f32011c75faccd37a3c7315046275011e`
- `13.log` SHA256:`b9f637cca6e97ed893bf38530e05cd33b78e324c5e8865d4c602dfeac22a7710`
- `12R2.log` SHA256:`0c431f5f5f1c1b17acda160c917b08c7f4036485016c23e1236b9b6f4a78de67`
证据只记录身份、状态、命令、时间、退出码与 Hash,不记录 Secret 值。
## 5. Git 与运行态门禁
| 门禁 | 当前值 | 结果 |
|---|---|---|
| Bare/Gitea/Candidate OID | 全部 `95d2e52cb55c7b913e89bc6bed2bcdacf67512d4` | PASS |
| Candidate worktree | clean | PASS |
| Active Controller | `6188da26453505dacde9d6d298191437164c8d27`,detached、clean | PASS |
| Locked worktree | `0` | PASS |
| Prunable worktree | `0` | PASS |
| 全局 worktree clean | `10 dirty / 88 total` | FAIL |
| 当前隔离 Release | `b158041b8633e658c34ce182f3c8aa97b4bf5f9a` | 已恢复 |
| AIOS/OpenClaw/Controller services | active | PASS |
10 个脏 worktree 均不属于本任务允许清理的范围:
1. `chatgpt/TASK-V2R1-PERSONAL-WEIXIN-GOLDEN-CONTRACT-R2-20260715`(36)
2. `chatgpt/TASK-V2R1-PERSONAL-WEIXIN-PORTAL-ADAPTER-20260715`(2)
3. `chatgpt/TASK-WORKTREE-CONCURRENCY-CHATGPT-20260715T055106UTC8`(2)
4. `codex`(56)
5. `codex/TASK-CODEX-AIOS-SPARK-RUNNER-PROVISIONING-20260717T103500UTC8`(6)
6. `controller`(11)
7. `openclaw`(5)
8. `openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`(3)
9. `openclaw/TASK-V2R1-OPENCLAW-WEIXIN-WEB-LOGIN-CONTRACT-20260715`(20)
10. `openclaw/TASK-WORKTREE-CONCURRENCY-OPENCLAW-20260715T055106UTC8`(3)
## 6. Reviewer
独立 Spark Reviewer 首先发现 cancel 权限边界不够明确;该问题已在 `95d2e52` 修复并新增三条 HTTP 断言。纠错后 Reviewer 确认代码安全边界、16 步证据与有界回滚均 PASS。对发布门禁的最终布尔复核为:`DIRTY_WORKTREE_COUNT=10` 且 `GLOBAL_WORKTREE_CLEAN=false`,所以全局门禁 FAIL,不能进入预生产评审。
## 7. Blocker 与发布决策
- 功能与证据子条件通过、正式关闭受阻:`BLK-LIVE-HERMES-FULL-LIFECYCLE-EVIDENCE-MISSING`
- 直接阻断条件:`BLK-GLOBAL-WORKTREE-CLEANLINESS`
- 状态:`BLOCKED_EVIDENCE_REQUIRED`
- 决策:`STOP_PREPRODUCTION`
本轮未修改 `main`、未创建 Tag、未 Force Push、未切换生产流量、未部署新 RC、未输出 Secret。
下一步唯一允许动作:由各 worktree Owner 审查并提交、归档或可信清理上述 10 个脏 worktree,然后重新执行全局 Git 门禁。只有全局 dirty 数量降为 0 后,才允许准备 RC 与 Tag Approval Request。
+129
View File
@@ -0,0 +1,129 @@
# AIOS V2 架构与交付文档索引
- 版本线:`V2R1C0-candidate`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 架构负责人:AIOS V2 CTO
- 当前状态:候选代码闭环已完成,运行时、预发布和生产验证尚待执行
## 1. 文档目的
本目录是 AIOS V2 用户需求、目标架构、模块边界、迁移策略、发布门禁和架构决策的唯一事实源。
所有后续架构、功能、接口、数据模型、安全策略、部署方式和验收流程变化,都必须在同一交付流程中同步更新本目录。
AIOS V2 遵循以下不可协商原则:
1. 不推倒重写现有平台。
2. 优先复用 OpenClaw、Hermes 和 Agent Router 能力。
3. OpenClaw 与 Hermes 从控制面职责中逐步解耦。
4. AIOS 是唯一控制面。
5. 所有面向用户和运维的能力必须通过 Web 或 API 访问。
6. 所有模块必须强制多租户隔离。
7. 所有修改必须通过 Git 分支、提交、测试、审查和受控提升完成。
8. 代码变化必须同步更新需求、架构、契约、追踪矩阵或 ADR。
## 2. 文档地图
| 文档 | 用途 |
|---|---|
| [01-USER-REQUIREMENTS.md](01-USER-REQUIREMENTS.md) | 产品目标、用户角色、功能需求、非功能需求和验收标准 |
| [02-TARGET-ARCHITECTURE.md](02-TARGET-ARCHITECTURE.md) | 目标逻辑架构、控制边界、数据流、安全模型和多租户模型 |
| [03-MIGRATION-AND-DELIVERY.md](03-MIGRATION-AND-DELIVERY.md) | 增量迁移、能力复用、GitOps 流程、发布门禁和回滚策略 |
| [04-MODULE-CONTRACTS.md](04-MODULE-CONTRACTS.md) | 租户上下文、任务生命周期、OpenClaw、Hermes、审计和执行证据契约 |
| [05-TRACEABILITY-MATRIX.md](05-TRACEABILITY-MATRIX.md) | 需求、设计、代码、测试、部署和验收之间的追踪关系 |
| [06-DOCUMENT-GOVERNANCE.md](06-DOCUMENT-GOVERNANCE.md) | 文档语言、完整性、同步门禁、版本与审查要求 |
| [adr/README.md](adr/README.md) | 架构决策记录索引与治理规则 |
## 3. 当前实现映射
| 架构能力 | 候选实现 |
|---|---|
| Tenant、Agent、Task、Policy 和 Audit 控制面 | `active-runtime/app/aios_v2/control_plane.py` |
| 控制面持久化 | `active-runtime/app/aios_v2/persistence.py` |
| Web/API Gateway | `active-runtime/app/aios_v2/gateway.py` |
| 统一应用入口 | `active-runtime/app/aios_v2/app.py` |
| Runtime 调度与结果回写 | `active-runtime/app/aios_v2/orchestrator.py` |
| Agent 执行证据 | `active-runtime/app/aios_v2/execution_tracking.py` |
| GitOps 状态可视化 | `active-runtime/app/aios_v2/gitops_status.py` |
| Web Console | `active-runtime/app/aios_v2/console.py` |
| OpenClaw 执行边界 | `active-runtime/app/aios_openclaw/` |
| Hermes Runtime 边界 | `active-runtime/app/aios_hermes/` |
| 候选测试 | `tests/aios_v2_candidate/` |
| 部署、健康检查与回滚 | `deploy/aios-v2/` |
| OpenClaw 生产部署任务 | `tasks/openclaw-production-deploy-task.yaml` |
| Codex 生产监督任务 | `tasks/codex-production-supervisor-task.yaml` |
## 4. 状态术语
项目必须严格区分“代码存在”和“可生产发布”:
- **已设计**:文档中存在明确边界和验收标准。
- **已实现**:代码已经提交到候选分支。
- **已定义测试**:Git 中存在自动化测试代码。
- **已验证测试**:已对精确 commit 执行测试并保存证据。
- **已验证预发布**:已在隔离环境完成部署、健康检查、重启和回滚演练。
- **已生产发布**:已取得 Owner 批准并完成合并、Tag 和生产部署。
分支、文件、测试脚本或部署脚本存在,并不自动代表能力已通过生产验收。
## 5. 设计摘要
```text
浏览器 / Web Console / Public API
|
v
AIOS 唯一控制面
Tenant | Agent | Task | Policy | Audit
|
v
Execution Orchestrator / Agent Router
/ \
v v
OpenClaw Adapter Hermes Boundary
| |
v v
OpenClaw Runtime Hermes Workers
\
v
结果回写 + 审计 + 执行证据
```
控制面拥有权威身份、策略、审批、任务生命周期和审计状态。OpenClaw 与 Hermes 只接收带租户上下文、已经批准的执行工作,并返回运行结果。
## 6. 发布规则
候选版本在满足以下条件前不得提升:
- 需求到代码的追踪关系完整;
- 文档—代码匹配检查通过;
- 多租户负向测试通过;
- 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 等标准技术术语。
@@ -0,0 +1,108 @@
# ADR-001:AIOS 作为唯一控制面
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Control Plane、Agent Router、OpenClaw、Hermes、Web Console、GitOps、审计
## 1. 背景
现有系统已经包含多个执行、调度和治理能力。如果 OpenClaw、Hermes、Agent Router 或 Web 层分别保存自己的租户、策略、审批和任务状态,就会形成多个权威事实源,带来以下问题:
- 同一任务在不同模块中状态不一致;
- 审计链不完整;
- 多租户隔离难以统一验证;
- Web、Runtime 和运维入口可能绕过策略;
- Bug 修复和发布无法判断应该修改哪个事实源。
## 2. 决策
AIOS Control Plane 是唯一权威控制面,负责:
- Tenant Registry;
- Identity 与角色上下文;
- Agent Registry;
- Task Lifecycle;
- Policy Decision;
- Approval;
- Audit Event;
- Runtime Inventory;
- Execution Evidence;
- GitOps 与发布状态可视化。
OpenClaw、Hermes 和 Agent Router 只能执行、路由或调度已经由 AIOS 批准的工作,不得拥有第二套租户、策略、审批或任务生命周期。
## 3. 目标边界
```text
Web / API
|
v
AIOS Control Plane
|
v
Execution Orchestrator / Agent Router
|
+--> OpenClaw Adapter --> OpenClaw Runtime
|
+--> Hermes Boundary --> Hermes Workers
```
## 4. 备选方案
### 方案 A:保持多个控制面
拒绝原因:状态会持续分裂,无法形成统一审计和租户隔离。
### 方案 B:由 OpenClaw 或 Hermes 成为主控制面
拒绝原因:二者主要价值是执行与 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`
@@ -0,0 +1,108 @@
# ADR-002:OpenClaw 与 Hermes 从控制面职责中解耦
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:OpenClaw、Hermes、Execution Orchestrator、Agent Router、Audit、Web Console
## 1. 背景
OpenClaw 和 Hermes 已经具备成熟的执行能力,但历史实现可能同时承担部分租户、策略、任务状态或运维控制职责。如果继续扩展这种模式,AIOS 将无法成为唯一控制面,也无法统一 Web、审计和多租户隔离。
## 2. 决策
OpenClaw 与 Hermes 通过稳定的 Adapter/Boundary 接入 AIOS:
```text
AIOS Control Plane
|
v
Execution Orchestrator
|
+--> OpenClaw Adapter --> OpenClaw Runtime
|
+--> Hermes Boundary --> Hermes Workers
```
OpenClaw 只负责:
- 验证签名命令;
- 执行;
- 查询状态;
- 取消;
- 健康检查;
- 返回结果和生命周期事件。
Hermes 只负责:
- tenant/cell/profile 分区队列;
- Worker claim;
- Lease 与 Heartbeat;
- 执行;
- 完成、失败、取消和结果回传。
二者均不得:
- 创建或批准 AIOS Task;
- 修改租户或策略;
- 维护第二套任务生命周期;
- 绕过 AIOS Audit;
- 向浏览器提供直接控制入口。
## 3. 备选方案
### 方案 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`
@@ -0,0 +1,121 @@
# ADR-003:所有变更必须通过 GitOps
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:代码、配置、文档、测试、部署、回滚、生产修复
## 1. 背景
直接在生产目录、长期 worktree、主分支或运行容器中修改代码,会造成以下问题:
- 变更不可追踪;
- 无法稳定回滚;
- 本地仓库与 Gitea 不一致;
- Worktree 和 Branch Ref 冲突;
- 紧急修复无法统一审视;
- 文档、测试和部署资产无法同步。
## 2. 决策
所有代码、配置、文档、测试、部署和回滚资产必须经过以下流程:
```text
非 main 分支
-> Commit
-> 本地仓库同步
-> Gitea 镜像与 OID 校验
-> 自动测试
-> Codex 监督审查
-> 候选集成
-> Owner 批准
-> Merge / Tag / Deploy
```
GitOps Controller worktree 必须长期保持 detached 且 clean。相同分支写入必须串行,任务工作区必须可回收。
## 3. 必需证据
每个任务必须记录:
- 任务 ID;
- 执行器;
- 分支;
- Commit;
- 修改文件;
- claim;
- heartbeat;
- progress;
- blocker;
- 测试命令、exit code 和结果;
- 本地/Gitea 同步状态;
- 回滚说明;
- 最终结果和监督结论。
## 4. 禁止事项
- 直接 push `main`;
- 未审批创建生产 Tag;
- 未记录任务直接修改生产;
- 在生产目录手工修复后不回写 Git;
- fetch 到已被 worktree 检出的本地 branch ref;
- 以新分支名反复规避 Worktree 生命周期缺陷;
- 在 Git 中提交明文凭据;
- 缺少文档和测试时标记为完成。
## 5. 备选方案
### 方案 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`
@@ -0,0 +1,150 @@
# ADR-004:多租户隔离采用默认拒绝模型
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Web/API、Control Plane、OpenClaw、Hermes、Persistence、Audit、Execution Evidence、Logs、Metrics、Secret
## 1. 背景
AIOS V2 同时承载多个租户的用户请求、控制面状态、OpenClaw 命令、Hermes 队列、执行证据、审计和 GitOps 状态。任何一层缺失租户边界,都可能造成数据泄露、跨租户任务执行或错误回写。
## 2. 决策
多租户隔离采用:
```text
显式租户上下文
+
默认拒绝
+
全链路传播
+
负向测试
```
所有资源、命令、Job、证据和审计记录都必须显式绑定 `tenant_id`。缺少上下文、上下文不匹配或无法确认所有权时,系统必须 fail closed。
## 3. 权威上下文
控制请求至少包含:
- `tenant_id`;
- `principal_id`;
- `trace_id`;
- `policy_version`;
- 可选 `session_id`。
执行请求增加:
- `task_id`;
- `command_id` 或 `job_id`;
- `idempotency_key`。
Hermes 额外包含:
- `cell_id`;
- `profile_id`;
- `worker_id`。
## 4. 各层执行规则
### Web/API
- 可信代理必须删除客户端同名 `X-AIOS-*` Header 后重新注入;
- 浏览器不得自行选择或覆盖租户;
- 未认证身份断言必须拒绝。
### Control Plane
- Tenant、Agent、Task、Audit 全部带不可为空的 `tenant_id`;
- 跨租户读取返回拒绝或“未找到”;
- 不泄露其他租户资源存在性。
### OpenClaw
- 签名覆盖 tenant/task/trace/action/payload/idempotency/time;
- Result 身份字段必须与命令完全一致;
- 跨租户 status/cancel 返回“未找到”。
### Hermes
- Job 按 tenant/cell/profile 分区;
- Worker 只能 claim 自己的分区;
- Worker ID 不得在未授权分区之间迁移;
- Result 必须匹配 tenant/task/trace。
### Persistence
- 候选版使用显式租户字段;
- 生产数据库必须提供 RLS 或物理隔离策略;
- 备份、恢复和迁移必须保持租户边界。
### Audit 与 Evidence
- 仅按当前租户查询;
- 不记录明文 Secret;
- 统一包含 tenant/task/trace。
### Cache、Memory、Artifact、Logs、Metrics
- Key 或标签必须包含租户命名空间;
- 查询和导出必须受租户权限限制;
- 不允许使用无租户前缀的全局缓存保存业务数据。
## 5. 备选方案
### 方案 A:仅在 API 层检查租户
拒绝原因:后台任务、Runtime、缓存和结果回写仍可能跨租户。
### 方案 B:仅依赖数据库隔离
拒绝原因:命令、队列、审计和文件系统仍需要显式边界。
### 方案 C:发生冲突时自动推断租户
拒绝原因:推断可能产生跨租户副作用,必须默认拒绝。
## 6. 正面影响
- 安全模型一致;
- Runtime 无法绕过租户边界;
- 负向测试可自动化;
- 审计和问题定位更明确。
## 7. 负面影响与风险
- 所有接口和模型都需要传播租户上下文;
- 日志、指标和缓存设计更复杂;
- 候选 SQLite 只有逻辑隔离,生产仍需更强存储证据;
- 身份代理配置错误可能破坏入口隔离。
## 8. 测试要求
至少覆盖:
- Tenant B 读取 Tenant A Task;
- Tenant B 查询或取消 Tenant A OpenClawCommand;
- Worker 跨 tenant/cell/profile claim;
- Runtime 返回错误 tenant/task/trace;
- Evidence 和 Audit 跨租户查询;
- 缺少 Tenant Context 的请求;
- 客户端伪造身份 Header。
## 9. 合规检查
任何新模块没有以下内容时不得进入 Candidate:
- 显式 `tenant_id`;
- 所有权检查;
- 跨租户负向测试;
- 审计映射;
- 文档与追踪矩阵更新。
## 10. 实现与测试映射
- Control Plane:`active-runtime/app/aios_v2/control_plane.py`
- OpenClaw:`active-runtime/app/aios_openclaw/`
- Hermes:`active-runtime/app/aios_hermes/`
- Evidence:`active-runtime/app/aios_v2/execution_tracking.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
@@ -0,0 +1,116 @@
# ADR-005:文档与代码同步门禁
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:需求、架构、代码、测试、部署、在线任务、发布和归档
## 1. 背景
架构、功能和部署变化如果不同步文档,会导致版本无法审计,后续 Agent 无法判断真实设计,发布人员也无法确认代码是否符合用户需求。历史上还出现过文档目录不存在、文档含冲突标记、分支名与代码内容不一致等问题。
## 2. 决策
所有影响以下内容的变更必须在同一交付流程中同步 `docs/aios-v2/`:
- 用户行为和功能范围;
- API、Header、请求和响应;
- 数据模型、字段和状态机;
- Control Plane、Agent Router、OpenClaw、Hermes 的边界;
- 多租户和安全策略;
- 持久化、审计和执行证据;
- 部署、健康检查和回滚;
- 测试和发布门禁。
正式文档统一使用中文,技术标识保留代码中的原始英文名称。
## 3. 门禁要求
验收必须执行:
```text
python3 tools/aios_v2_docs_code_check.py
```
检查至少包括:
1. 必需文档和 ADR 存在且非空;
2. 文档以中文叙述为主;
3. 文档引用的代码、测试和部署路径存在;
4. 核心 API 能在代码中找到;
5. 核心模型和字段能在代码中找到;
6. Traceability Matrix 覆盖所有 `UR-*`;
7. ADR 索引引用的文件存在;
8. 文档中不存在 Git 冲突标记;
9. 不存在摘要占位符或空章节;
10. 架构/功能代码变化时,同一交付范围包含文档变化。
## 4. 状态影响
门禁失败时:
- Candidate 不得标记为 `Test-verified`;
- OpenClaw 不得继续生产部署;
- Codex 必须登记 blocker;
- 修复必须进入 Git 分支;
- Owner 不得批准 Tag 或生产部署。
## 5. 备选方案
### 方案 A:依赖人工记忆更新文档
拒绝原因:长期必然产生文档漂移。
### 方案 B:发布前集中补文档
拒绝原因:无法确定每次代码变化对应的真实设计,也会增加错误归因。
### 方案 C:只检查文件存在
拒绝原因:文件存在不代表内容完整、路径有效或与代码一致。
## 6. 正面影响
- 架构和代码长期一致;
- Agent 有明确执行上下文;
- Bug 修复可以追踪需求和决策;
- 归档包可以作为正式交付材料;
- 发布风险和未验证项更透明。
## 7. 负面影响与风险
- 小功能也需要更新对应文档;
- 检查器需要随代码结构维护;
- 自动检查不能替代架构审查;
- 文档完整性会增加提交内容,但这是可审计交付的必要成本。
## 8. 文档更新映射
- 用户需求变化:`01-USER-REQUIREMENTS.md`;
- 架构边界变化:`02-TARGET-ARCHITECTURE.md` 和 ADR;
- 迁移/发布变化:`03-MIGRATION-AND-DELIVERY.md`;
- Contract 变化:`04-MODULE-CONTRACTS.md`;
- 路径和状态变化:`05-TRACEABILITY-MATRIX.md`;
- 文档规则变化:`06-DOCUMENT-GOVERNANCE.md`。
## 9. 在线任务要求
OpenClaw 和 Codex 的任何修改或阻塞必须回写:
- claim;
- heartbeat;
- progress;
- blocker;
- branch;
- commit;
- test_status;
- result;
- 文档同步状态。
OpenClaw 发现架构影响时必须停止扩大修复范围,并升级给 Codex。
## 10. 实现与验收映射
- 检查器:`tools/aios_v2_docs_code_check.py`
- 验收入口:`deploy/aios-v2/acceptance/run-all.sh`
- 治理规范:`docs/aios-v2/06-DOCUMENT-GOVERNANCE.md`
- 追踪矩阵:`docs/aios-v2/05-TRACEABILITY-MATRIX.md`
@@ -0,0 +1,142 @@
# ADR-006:候选持久化采用可替换 SQLite Snapshot
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Control Plane、Persistence、Deployment、Backup、Recovery、HA 演进
## 1. 背景
AIOS V2 候选版本需要在服务重启后恢复 Tenant、Agent、Task、Audit 和 Idempotency 状态,同时又必须尽快进入预发布部署与 Bug 修复阶段。
候选阶段如果立即引入完整分布式数据库、消息系统和 HA,会显著扩大开发范围,推迟控制面、Runtime Boundary 和多租户模型的验证。
## 2. 决策
候选版本使用可替换的 SQLite WAL 原子 Snapshot:
- `SQLiteSnapshotRepository` 保存版本化 Control Plane Snapshot;
- `DurableControlPlaneService` 在成功变更后持久化;
- 使用 WAL、`synchronous=FULL` 和 `BEGIN IMMEDIATE`;
- 状态目录独立于代码版本;
- 上层 Domain、API、Orchestrator 和 Runtime Contract 不依赖 SQLite 实现。
执行证据使用租户分区 JSON 文件,由 `ExecutionTracker` 原子写入。
## 3. 持久化范围
候选 Snapshot 包含:
- Tenant;
- Agent;
- Task;
- Audit;
- Idempotency。
独立 Evidence 目录包含:
- claim;
- heartbeat;
- progress;
- blocker;
- commit;
- test_status;
- result。
## 4. 选择理由
- 无额外外部依赖;
- 易于快速部署和回滚;
- 支持事务、WAL 和完整性检查;
- 可以验证上层控制面和 Runtime Contract;
- 便于在预发布阶段复现和修复 Bug;
- 可通过 Repository 边界替换为生产数据库。
## 5. 备选方案
### 方案 A:候选阶段直接使用 PostgreSQL 集群
暂不采用原因:会增加部署、迁移、HA、Secret、网络和容量范围,延迟核心架构验证。
### 方案 B:继续使用纯内存状态
拒绝原因:服务重启会丢失权威状态,无法进入预发布。
### 方案 C:每个模块各自使用 SQLite
拒绝原因:会形成多个事实源和跨模块事务问题。
### 方案 D:将完整状态写入 Git
拒绝原因:运行状态不应成为 Git 内容,可能暴露敏感业务数据并造成提交风暴。
## 6. 正面影响
- 候选版具备重启恢复能力;
- 状态与代码版本解耦;
- 回滚默认只切换代码,不覆盖新状态;
- 可快速进入预发布测试;
- 生产后端替换不改变上层 Contract。
## 7. 负面影响与风险
- SQLite 主要适用于单进程候选部署;
- 尚未提供多实例写入和分布式锁;
- 租户隔离是逻辑隔离,不是 RLS 或物理隔离;
- 大规模任务、审计和并发容量尚未验证;
- Evidence 文件数量增长需要归档策略;
- Snapshot 粒度可能导致状态增长后的写放大。
## 8. 生产迁移要求
生产数据层替换必须满足:
1. 保持 Control Plane Domain/API Contract 不变;
2. 提供 schema migration;
3. 提供租户 RLS 或物理隔离;
4. 提供备份、恢复、PITR 和灾难恢复;
5. 提供 HA、多实例并发和故障转移;
6. 提供容量、延迟和锁等待测试;
7. 保持 Audit 与 Evidence 可追踪;
8. 提供从 SQLite Candidate 到生产后端的数据迁移工具;
9. 通过新的 ADR 说明生产持久化决策。
## 9. 回滚策略
- 回滚前备份 `/var/lib/aios-v2`;
- 默认只回滚代码;
- 状态恢复必须由 Owner 显式授权;
- 恢复前必须验证 Snapshot schema 兼容性;
- 回滚后执行 SQLite `PRAGMA quick_check` 和 AIOS Healthcheck。
## 10. 验收要求
候选版至少通过:
- 创建 Tenant、Agent、Task;
- 服务重启;
- 读取恢复后的 Task;
- Idempotency 仍有效;
- Audit 仍存在;
- SQLite `quick_check=ok`;
- 状态目录权限正确;
- 回滚演练不丢失新增状态。
## 11. 实现与测试映射
- Repository:`active-runtime/app/aios_v2/persistence.py`
- 应用入口:`active-runtime/app/aios_v2/app.py`
- Evidence:`active-runtime/app/aios_v2/execution_tracking.py`
- 测试:`tests/aios_v2_candidate/test_candidate_flow.py`
- 健康检查:`deploy/aios-v2/healthcheck/check.sh`
- 回滚:`deploy/aios-v2/rollback/rollback.sh`
## 12. 更新触发条件
以下变化必须更新或替代本 ADR:
- 引入生产数据库;
- 支持多实例写入;
- 修改 Snapshot 格式;
- 改变 Evidence 存储;
- 增加 RLS、HA、分片或事件溯源;
- 修改状态回滚策略。
@@ -0,0 +1,107 @@
# ADR-007:AIOS V2 Run Provenance 可信执行来源控制
- 状态:已采纳
- 日期:2026-07-20
- 影响范围:Runtime 执行、Evidence、GitOps、部署、回滚、审计
## 1. 背景
AIOS V2 在进入预生产阶段前,需要证明每一次高权限动作来源可信。仅依靠进程内 runId、sender 或任务字段无法证明:
- 谁授权了执行;
- 执行是否属于当前任务;
- Git、部署、回滚和 Evidence 写入是否经过批准;
- 是否存在重放或伪造运行记录。
## 2. 决策
引入 Run Provenance:
```text
Owner / Control Plane
|
v
Ed25519 Signed Provenance
|
v
Persistent Provenance Store
|
v
Controlled Writer
|
v
Runtime / GitOps / Deploy / Evidence / Rollback
```
## 3. 七类受保护路径
生产 Provenance 必须覆盖:
1. code_change
2. git_operation
3. deployment
4. runtime_execute
5. evidence_write
6. rollback
7. audit
任何未授权路径默认拒绝。
## 4. 安全规则
- 私钥只用于签发端。
- Runtime 只持有公钥验证能力。
- Provenance 使用 Ed25519 签名。
- 状态持久保存 issued、verified、used、expired、revoked。
- 相同 provenance/path/request_digest 不允许重复授权。
- Secret 不进入 Git、日志和 Evidence。
## 5. 实现映射
代码:
```text
active-runtime/app/aios_v2/provenance.py
active-runtime/app/aios_v2/provenance_git.py
active-runtime/app/aios_v2/provenance_operations.py
active-runtime/app/aios_v2/provenance_cli.py
```
接入:
```text
orchestrator.py
execution_tracking.py
app.py
```
## 6. 验收要求
必须验证:
- 签名正确执行;
- 篡改拒绝;
- 过期拒绝;
- revoke 拒绝;
- replay 拒绝;
- 无 Provenance Runtime Dispatch 拒绝;
- 无 Evidence 授权写入拒绝;
- 无 Git 授权操作拒绝;
- 无 Deployment/Rollback 授权拒绝。
## 7. 回滚
若 Provenance 服务异常:
- 不允许降级绕过安全检查进入生产;
- 候选环境可以在明确测试配置中关闭强制模式;
- 生产模式必须保持 AIOS_REQUIRE_RUN_PROVENANCE=true。
## 8. 后续演进
生产阶段可进一步接入:
- HSM/KMS;
- 外部不可变审计;
- 多签审批;
- 硬件密钥认证。
@@ -0,0 +1,156 @@
# ADR-009:Workflow Provenance 与最小权限子 Grant
- 状态:已采纳
- 日期:2026-07-21
- 决策范围:AIOS V2R1 生产授权、Runtime 调度、Evidence、Git、部署、回滚与审计
## 1. 背景
早期候选实现使用一个 Run Provenance 同时承载身份绑定和七类生产操作权限。该模型可以证明一次执行来自哪个租户、任务、Agent 与 Principal,但同一凭证能够授权 Runtime、Evidence、Git、部署、回滚、审计和代码变更,违反最小权限原则。任何持有该凭证的进程都可能获得超出职责的能力。
同时,Runtime Result 回写、Git 写入和部署包装器需要可撤销、可过期、可重放防护且可持久恢复的独立授权。仅依赖进程角色或环境变量不足以形成可审计的生产信任链。
## 2. 决策
采用两层 Provenance 模型:
1. **Workflow Provenance** 只绑定 `run_id`、`tenant_id`、`task_id`、`agent_id`、`principal_id`、签发者、TTL、Key ID、Nonce 和签名,不授予任何操作路径。
2. **Provenance Grant** 由 Workflow Provenance 派生,每个 Grant 只绑定一个 `ExecutionPath`,拥有独立的 TTL、Nonce、状态、撤销和重放记录。
3. Git Grant 还必须显式限定允许的 `GitOperation`,例如只允许 `READ_REF` 而不允许 `PUSH`。
4. 子 Grant 的身份字段必须与父 Workflow 完全一致,且子 Grant 的有效期不得超过父 Workflow。
5. 父 Workflow 被撤销或过期后,其全部子 Grant 自动失效。
6. 授权使用 `(grant_id, execution_path, git_operation, request_digest)` 建立唯一使用记录,重复请求默认拒绝。
## 3. Grant 类型
| GrantType | 唯一允许路径 |
|---|---|
| `code_change` | `code_change` |
| `git` | `git_operation` |
| `deployment` | `deployment` |
| `runtime` | `runtime_execute` |
| `evidence` | `evidence_write` |
| `rollback` | `rollback` |
| `audit` | `audit` |
Grant 类型与路径不一致时,构造阶段即拒绝。非 Git Grant 不得包含 Git 操作;Git Grant 未声明具体操作时不得签发。
## 4. 服务级 Scope
生产进程不得加载全部 Grant。`ScopedLeastPrivilegeProvenanceWriter` 只接受当前服务声明的路径和对应 Grant ID,多余 Grant 映射会导致启动失败。
默认 AIOS Application Scope 仅包含:
- `runtime_execute`
- `evidence_write`
- `audit`
Git、部署、回滚和代码变更必须由独立进程或包装器显式声明自己的单一路径 Scope。运维 CLI 的一次授权命令只接受一个 `--grant-id`,并要求它与 `--path` 一致。
## 5. Runtime 与 Dispatch 绑定
Runtime 执行还必须经过持久化 `DispatchRecord`。记录至少绑定:
- `dispatch_id`
- `tenant_id`
- `task_id`
- `agent_id`
- `principal_id`
- `runtime`
- `runtime_execution_id`
- `trace_id`
- `provenance_id`
- `run_id`
- `authorization_id`
OpenClaw/Hermes 结果只有在上述上下文完全匹配时才能由 Orchestrator 更新 Task。公开 Task API 不允许直接把任务推进到 `RUNNING`、`COMPLETED` 或 `FAILED`;Hermes 结果只通过内部 Runtime Reconcile 入口处理。
## 6. 密钥与签名
Workflow Provenance、子 Grant 和 OpenClaw Command 均采用 Ed25519。生产应用只读取公钥;私钥仅存在于受保护的签发端或 systemd credential。Git、日志、环境模板和审计中不得出现私钥内容。
Key ID 用于选择受信任密钥,不包含密钥材料。公钥 Fingerprint 使用恒定时间比较。OpenClaw Command 合同版本为 `aios.openclaw.command.v2`。
## 7. 备选方案
### 7.1 单一全权限 Provenance
拒绝。实现简单,但任何服务泄漏都可能获得全部生产能力,无法独立撤销 Runtime、Git 或部署权限。
### 7.2 仅依赖 RBAC
拒绝。RBAC 可以限制 API 调用者,但不能把具体 Run、Task、Agent、请求摘要和 Runtime Result 绑定到一次可验证授权。
### 7.3 每个操作重新签发完整 Workflow
拒绝。会复制身份数据和生命周期状态,难以统一撤销整次工作流,也增加签发和审计复杂度。
## 8. 正面影响
- 服务与操作权限按职责隔离。
- Runtime、Evidence、Git、部署、回滚和审计可以独立撤销。
- 父 Workflow 可作为整次 Run 的紧急停止开关。
- SQLite 持久化支持重启后的授权与 Dispatch 恢复。
- Receipt 明确记录 Grant、Scope、请求摘要和授权时间。
- Git Index 只有在 Grant 授权完成后才允许修改。
## 9. 负面影响与风险
- 签发和部署配置比单凭证模式复杂。
- 每个生产服务必须配置正确的 Grant ID。
- Grant 状态与 Dispatch 状态需要备份、迁移和容量治理。
- 当前候选实现使用 SQLite;高可用生产部署仍需外部持久化和并发容量证据。
- OpenClaw callback 目前只作为非权威审计事件;状态变化仍必须经过 Orchestrator 严格 Reconcile。
## 10. 迁移与回滚
迁移步骤:
1. 签发无操作权限的 Workflow Provenance。
2. 按所需路径签发独立子 Grant。
3. 为每个服务配置其最小 Grant ID。
4. 启用 `AIOS_REQUIRE_RUN_PROVENANCE=true`。
5. 验证 Scope 拒绝、撤销、过期、重放和重启恢复测试。
6. 完成预发布 Acceptance 后,禁止旧全权限 `issue` 命令进入生产流程。
回滚仅允许回到前一个已验证候选版本,不得回滚为生产全权限 Provenance。状态恢复必须保留父 Workflow、Grant 使用记录、Dispatch 和 Receipt,防止旧授权被再次使用。
## 11. 合规检查
必须满足:
- 父 Workflow 无操作路径。
- 子 Grant 只包含一个路径。
- 生产服务只加载职责所需 Grant。
- 上下文不匹配、签名错误、过期、撤销和重放全部 fail closed。
- Runtime Result 必须匹配持久 Dispatch。
- 测试与文档在同一分支提交。
- 正式 Gate A 结论必须基于实际执行 Acceptance,不得仅凭测试文件存在声明通过。
## 12. 受影响资产
代码:
- `active-runtime/app/aios_v2/provenance.py`
- `active-runtime/app/aios_v2/provenance_grants.py`
- `active-runtime/app/aios_v2/provenance_scoped.py`
- `active-runtime/app/aios_v2/provenance_operations.py`
- `active-runtime/app/aios_v2/provenance_cli.py`
- `active-runtime/app/aios_v2/dispatch_store.py`
- `active-runtime/app/aios_v2/orchestrator.py`
- `active-runtime/app/aios_v2/execution_tracking.py`
- `active-runtime/app/aios_v2/provenance_git.py`
- `active-runtime/app/aios_openclaw/contract.py`
测试:
- `tests/aios_v2_candidate/test_provenance.py`
- `tests/aios_v2_candidate/test_provenance_grant_isolation.py`
- `tests/aios_v2_candidate/test_candidate_flow.py`
部署与文档:
- `deploy/aios-v2/env/aios-v2.env.example`
- `docs/aios-v2/04-MODULE-CONTRACTS.md`
- `docs/aios-v2/05-TRACEABILITY-MATRIX.md`
- `docs/aios-v2/AIOS-V2R1-HARDENING-GATE-A-REPORT.md`
@@ -0,0 +1,80 @@
# ADR-010:Hermes 在线生命周期使用默认关闭的内部 Runtime API
- 状态:Accepted for Release Candidate
- 日期:2026-07-21
- 范围:AIOS V2R1 Hermes Worker Lifecycle Acceptance
## 决策
使用现有 `HermesAdapter` Queue/Lease 状态机,增加 SQLite 持久 Store,并通过默认关闭的
内部 Runtime API 执行 Claim、Start、Heartbeat、Complete、Fail、Cancel 与 Status。
不建立第二套 Queue、Lease 或 Task 状态机。
内部 API 只接受可信代理注入且具有 `runtime`/`platform_admin` 角色的身份。请求必须绑定
tenant、task、trace、principal、provenance、run、dispatch、job、cell 和 profile。
Complete、Fail、Cancel 只能经 `ExecutionOrchestrator.reconcile_hermes` 更新权威 Task。
## 背景
原 Candidate 已具有进程内 Hermes Claim/Lease/Worker 能力及持久 Dispatch,但在线服务
只暴露 enqueue/reconcile,且 Hermes Job/Lease 本身在重启后丢失,不能形成完整在线证据。
## 原因
直接验收驱动会形成 Adapter 之外的额外变更入口,难以证明 HTTP 身份、Dispatch 与
Orchestrator 边界。受保护的内部 API 可以在真实 localhost 服务、真实 SQLite Store 和
真实 HTTP 解析路径上验证完整生命周期,同时在生产通过配置保持关闭。
## 备选方案
- 受信验收驱动直接导入 Adapter:改动较小,但不能证明内部 HTTP 身份与请求解析边界,拒绝。
- 新建独立 Queue/Lease 服务:会产生第二状态机和双写一致性问题,禁止。
- 开放公共 Worker API:扩大攻击面并允许浏览器/Operator 触发 Runtime 状态,禁止。
## 安全不变量
1. 只有 APPROVED Task 可由 Orchestrator 创建 Hermes Job。
2. Dispatch 在 Runtime 副作用前持久化。
3. Worker 只能操作精确绑定且由其持有 Lease 的 Job。
4. Runtime 结果必须通过完整身份元组与持久 Dispatch 校验。
5. 只有 Orchestrator 能把 Task/Dispatch 推进到 Runtime 终态。
6. 生命周期写入同步生成 Audit/Event 与 Execution Evidence。
7. 相同终态重放幂等,不同终态或不同内容 fail closed。
## 恢复语义
`SQLiteHermesStore` 持久保存 Job、Result、Lease、幂等键与 Worker heartbeat。服务重启
加载同一状态;过期 Lease 被持久重排为 QUEUED,清理旧 Owner 并递增 revision。新 Worker
可重领,旧 Worker 的 Start/Heartbeat/Result 被拒绝。
## 影响与风险
- 正面:同一 Adapter 状态机获得持久恢复、真实 HTTP 验收、严格身份绑定和可审计证据。
- 负面:SQLite Snapshot 每次生命周期变更都执行同步事务,需在预生产观察锁等待与吞吐。
- 风险:可信代理头是身份边界;因此接口必须保持 localhost/内部路由、默认关闭,并由部署
配置保证外部请求不能自行注入身份头。
## 迁移与回滚
新 Store 文件为新增的 `hermes.sqlite3`,不修改既有 Control Plane/Dispatch schema。回滚到
Previous OID 时保留该文件但旧版本不读取;不得用旧备份覆盖新业务状态。重新升级时可继续
加载。接口回滚只需关闭环境开关并恢复 Previous Release OID。
## 合规检查
- 不修改 main,不创建 Tag,不绕过 Provenance/Grant。
- 生产默认关闭生命周期验收入口。
- Secret 不进入请求、Audit、Evidence 或测试数据。
- Result 由 Orchestrator 与持久 Dispatch 校验,负向场景 fail closed。
## 受影响资产
- 代码:`aios_hermes/adapter.py`、`aios_v2/app.py`、`orchestrator.py`、`gateway.py`。
- 测试:`test_hermes_live_lifecycle.py` 及 Candidate/Provenance 回归。
- 文档:用户需求、模块合同、追踪矩阵、Gate A 报告与本 ADR。
## 验证
`tests/aios_v2_candidate/test_hermes_live_lifecycle.py` 通过真实 localhost HTTP Server 与
临时 SQLite 状态目录覆盖 17 项正向、负向、Lease、幂等和重启场景。最终发布结论仍必须
以提交后的 Gate、在线回归、Secret Audit、SQLite 检查、回滚与独立 Reviewer 证据为准。
+82
View File
@@ -0,0 +1,82 @@
# AIOS V2 架构决策记录索引
- 文档版本:`V2R1-hardening-candidate`
- 适用范围:影响多个模块、长期演进、兼容性、安全、数据、部署或交付流程的关键架构决策
## 1. 架构决策记录列表
| 编号 | 决策 | 状态 |
|---|---|---|
| [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-007 | 当前仓库未分配;禁止补写不存在的历史决策 | 未分配 |
| ADR-008 | 当前仓库未分配;禁止补写不存在的历史决策 | 未分配 |
| [ADR-009](ADR-009-provenance-least-privilege.md) | Workflow Provenance、最小权限子 Grant 与服务级 Scope | 已采纳 |
| [ADR-010](ADR-010-hermes-live-lifecycle.md) | Hermes 在线生命周期使用默认关闭的内部 Runtime API | RC 已采纳;待 Owner 发布批准 |
## 2. 编号规则
- ADR 编号一经发布不得重用。
- 编号缺口可以保留,不能通过伪造历史文件填补。
- 新决策使用下一个未占用编号,并在同一分支更新本索引、模块契约和追踪矩阵。
- ADR-007 与 ADR-008 在当前仓库没有可验证历史文件,因此明确标记为未分配。
## 3. 状态定义
- **已提出**:决策已提出,但尚未被正式采纳。
- **已采纳**:当前有效,所有新实现必须遵守。
- **已替代**:已被新的 ADR 替代,保留用于历史追踪。
- **已废弃**:保留历史,但禁止新实现继续采用。
- **未分配**:该编号当前没有 ADR 文件,不代表存在隐含历史决策。
## 4. 必须新增或更新 ADR 的场景
以下变化必须新增或更新 ADR:
1. AIOS、OpenClaw、Hermes、Agent Router 的控制边界变化;
2. 多租户隔离模型变化;
3. API、数据模型或状态机出现不兼容变化;
4. 持久化、消息、密钥、审计或部署方案发生跨模块变化;
5. GitOps、发布、回滚或文档门禁变化;
6. 替换 OpenClaw、Hermes、Agent Router 或核心基础设施;
7. 引入新的权威事实源、任务生命周期或租户状态;
8. 生产安全模型、身份代理、Run Provenance、Grant 或 Secret 管理方式变化;
9. Runtime Result、Dispatch 或服务 Scope 信任边界变化。
## 5. ADR 内容要求
每份 ADR 必须包含:
- 背景;
- 决策;
- 备选方案;
- 采用理由;
- 正面影响;
- 负面影响和风险;
- 迁移与回滚;
- 合规检查;
- 受影响的代码、测试和文档。
## 6. ADR 变更联动
ADR 新增、替代或废弃时,必须同步检查并更新:
- `01-USER-REQUIREMENTS.md`;
- `02-TARGET-ARCHITECTURE.md`;
- `03-MIGRATION-AND-DELIVERY.md`;
- `04-MODULE-CONTRACTS.md`;
- `05-TRACEABILITY-MATRIX.md`;
- 相关代码、测试和部署资产;
- 候选或 Gate 报告中的风险与发布结论。
## 7. 审查规则
- OpenClaw 不得自行采纳改变控制面边界的 ADR;发现需求后必须升级给 Codex。
- Codex 负责审查 ADR 与代码、测试和文档是否一致。
- 涉及合并、Tag、生产部署或重大风险接受的 ADR,必须由 AIOS Owner 最终批准。
- ADR 未完成审查前,相关变更不得标记为 `Test-verified` 或 `Production released`。
- ADR、测试文件或 Acceptance 脚本存在,不等于 Gate 已通过;必须有实际执行日志、退出码和证据。
-34
View File
@@ -1,34 +0,0 @@
# AIOS Autonomous Release Candidate 架构
本 Candidate 把规划、工作流、凭据引用、原生会话、证据和发布控制拆成独立边界。它只负责验证约束,不拥有生产授权。
## 信任边界
- Owner Approval 的签名和撤销状态来自外部 HSM/KMS 或可信 Approval Plane。Candidate 只有 verifier,没有 signer。
- Secret Manager V2 只返回 `systemd-credential:*` 或受控的 Candidate reference,不返回认证正文。
- OpenClaw、Codex 和 Gitea provider 身份由管理面供应;模拟 provider 永远不能把原生 canary 标成已验证。
- Writer 只写任务绑定分支和允许路径。生产 main、tag、baseline、registry 和部署不属于 Candidate 权限。
## 核心模块
- `models.py`:TaskPlan、Approval、Credential、Result 和 ReleasePlan 强类型约束。
- `store.py`:私有 SQLite、WAL、全同步、schema 检查、事务和原子 step transition。
- `workflow.py`:依赖、claim、lease、retry、timeout recovery 和终态。
- `credential.py` / `secret_manager.py`:reference-only 生命周期、补偿撤销、跨任务拒绝和 post-revocation 验证。
- `native_adapters.py`:OpenClaw/Codex 原生 provider ID、parent/child tree、status、interrupt、close 和纯文本回执。
- `gitea_canary.py`:仅通过 deploy-key provider 的 list/create/delete 操作完成 canary 与撤销验证。
- `evidence.py`:落库的哈希链、重启回放、篡改检测和 required-event closure。
- `artifact.py`:确定性 tar、逐文件哈希、package closure、链接/特殊文件/敏感标记拒绝。
- `release.py` / `release_gate.py`:部署审批与回滚审批分离,tag object 与 peeled target 分离验证。
## 会话真实性
以下状态必须分别记录,不能互相替代:
```text
broker_session_verified
openclaw_native_child_session_verified
codex_child_session_verified
```
Shadow provider 只能证明 Broker 合同和清理路径,不能证明 OpenClaw 或 Codex 原生会话。
@@ -1,82 +0,0 @@
# AIOS Autonomous Release Execution Package
## Release Strategy
V2R1C4 uses a two-stage model:
1. Dormant code deployment.
2. Capability activation through a separate approved release.
## Dormant Deployment
The first production release must keep autonomous execution disabled:
```text
AIOS_GITOPS_WRITES_ENABLED=false
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
```
Required mode:
```text
deployment_mode=dormant_shadow_only
```
Required properties:
```text
production_side_effects=false
native_openclaw_enabled=false
native_codex_enabled=false
automatic_release_enabled=false
```
## Deployment Sequence
```text
Verify Integration
|
v
Owner Dormant Release Approval
|
v
Merge Pull Request
|
v
Read final main merge OID
|
v
Build Production Artifact
|
v
Deploy Dormant Runtime
|
v
Create Version Tag
|
v
Verify Registry and Baseline
```
## Capability Activation (Separate)
The following require separate approval and evidence:
- OpenClaw native provider
- Codex app-server provider
- Gitea Admin API lifecycle
- Secret Manager V2
- Autonomous production execution
## Hard Governance Rules
Never:
- direct write main
- force push
- rewrite tags
- export credentials
- enable autonomous production execution without approval
- use integration OID as production target OID
- update Baseline before remote tag verification
-48
View File
@@ -1,48 +0,0 @@
# Candidate 运维与验收
## 安全默认值
两个 systemd 模板均为 Dormant/Shadow-only,绑定 loopback,默认关闭 Task Plane 执行、GitOps 写入和生产部署。模板进入仓库不等于安装或启动服务。
外部信任根缺失时必须保留以下阻断:
```text
BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE
BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED
BLOCKED_EXTERNAL_OWNER_SIGNATURE
```
不得用模拟身份、个人账户或生产 provider 身份替代这些根。
## 独立验收
从仓库根目录运行:
```text
PYTHONPATH=services/aios-autonomous-release-candidate \
python3 -m pytest -p no:cacheprovider -q tests/autonomous_release
PYTHONPATH=services/aios-autonomous-release-candidate \
python3 scripts/autonomous-release/run_acceptance.py \
--repo-root . \
--output /task-evidence/candidate-acceptance.json
```
Acceptance report 必须满足:
```text
all_passed=true
skipped=0
direct_gitea_database_access=false
production_path_hits=[]
secret_hits=[]
production_side_effects=false
```
## Artifact
Artifact builder 拒绝覆盖现有文件、链接、硬链接、特殊文件、仓库元数据和敏感内容标记。输出 tar 和 manifest 后必须再次执行 `ArtifactBuilder.verify`。
## 外部 canary
真实 native session 与 Gitea canary 只能在任务绑定的 runtime credential 投影存在时运行。每次 canary 完成后必须关闭 provider session、撤销上游对象、删除 projection,并保留非敏感回执。
-26
View File
@@ -1,26 +0,0 @@
# Governed ReleasePlan 关闭条件
ReleasePlan 必须绑定 task、repository、integration branch、完整 target/tree OID、artifact/manifest/evidence/rollback digest、生产配置 digest,以及互不相同的部署和回滚 ApprovalPlan ID。
## Candidate 与 Integration
1. Candidate 分支先完成测试、扫描、Shadow、Artifact 和外部门禁盘点。
2. 重新读取 authoritative `main` 完整 OID。
3. 从该 OID 创建 integration 分支。
4. 使用普通 `merge --no-ff` 合入 Candidate;禁止 squash、rebase、amend 和 force push。
5. 在 integration 上重新执行全部测试、扫描和 artifact closure。
## 生产停止线
下列项目任一为 false,流程必须停止在 `READY_FOR_OWNER_RELEASE_APPROVAL`,不得部署或创建生产 tag:
```text
openclaw_native_child_session_verified
codex_child_session_verified
gitea_admin_api_lifecycle_canary
owner_signer_separation_verified
credential_incident_closed
external_owner_release_approval_verified
```
聊天文本、Candidate 自签记录或模拟 canary 不能改变这条停止线。
@@ -0,0 +1,7 @@
# Session Broker Final Release Candidate
Task: `AIOS-CHATGPT-SESSION-BROKER-FINAL-RELEASE-20260726`
This branch is based on the exact approved main OID `875d41c3101093149414292067fd700dc214d4d3` and preserves the dormant Candidate/Integration history through an ordinary no-fast-forward merge.
Current scope remains Candidate/shadow-only. Production transport, services, execution flags, tags, Baseline, and Registry are unchanged.
+25
View File
@@ -0,0 +1,25 @@
# AIOS V2R1C1 Release Notes
- VRC: `V2R1C1`
- SemVer: `2.1.0-rc.1`
- 固定产品基线: `fb739f6e8ff9c031d319221ace68dce157694086`
- 当前状态: `RELEASE_VALIDATION_PASS / FINAL_METADATA_COMMIT_REVALIDATION_REQUIRED`
- 发布决策: `STOP_PREPRODUCTION`
OpenClaw Command v2 Companion:
- 分支: `patch/openclaw/AIOS-V2R1C1-COMMAND-V2-COMPANION-20260721`
- 源 OID: `83f8f878c7da5dd8263f2af2efa80750492e5cb3`
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`
- 插件版本: `2.1.0-rc.4`
- 合同: `aios.openclaw.command.v2` / `Ed25519`
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`
- 隔离验证: `AIOS -> OpenClaw V2 PASS`,正向与负向在线合同均通过。
旧 Release OID `80c4cbde03a82c30648a09b817bc2f03d5402936` 保留为原始 HTTP 401 失败诊断基线。Release OID `0778a31ec073e7c90003ef091f1d093c16bf22e5` 已实际完成 Gate A/B、文档同步、Secret Scan、Release Metadata、OpenClaw 第 13 步正负向、Hermes、重启恢复、隔离回滚/前滚和最终健康门禁,全部通过。
本次最终元数据闭环仍只修改五个允许的 Release 元数据文件;提交后形成的最终 OID 必须再次执行 Gate A/B、文档同步、Secret Scan、Release Metadata 和 OpenClaw 第 13 步正负向复验。复验通过前 Tag 保持不存在,生产流量保持未切换。
本分支只包含发布元数据。Personal Weixin Portal Adapter、Spark 专有 Provisioning、Future Incubator、Worktree 并发实验和长期 Agent 工作目录均未进入发布图。
只有最终 Release OID 的全部复验和步骤 14–17 通过后,才允许创建不可变 `V2R1C1` Tag。
@@ -1,98 +0,0 @@
# AIOS V2R1C4 Fastlane Release Design
## Frozen scope
This release replaces the generic Governance Bootstrap deployment route with one non-root
`aios-release-controller` oneshot and five explicit root `aios-deploy-helper` oneshots. The
Bootstrap branch and commit remain immutable history and are marked
`SUPERSEDED_BY_V2R1C4_FASTLANE_RELEASE`; they are not production prerequisites.
The release controller is the only component allowed to call Gitea, AWS KMS, and the fixed
authority repository. The root helper has no Git, Gitea, KMS, Registry, Baseline, tag, arbitrary
command, arbitrary path, template-instance, or network-service interface.
## Object model and the non-circular PR carrier
`release-plan.json` cannot contain the Git OID of the commit that contains that same file. The
plan therefore separates immutable release content from the exact reviewed PR carrier:
- `head_oid`, `source_commit_oid`, `expected_tag_oid`, and `expected_peeled_target_oid` identify
the immutable release-content commit.
- `evidence_closure_oid` identifies the later commit containing all pre-review test evidence.
- the exact PR carrier commit contains the plan and its pending signature descriptor; its OID is
written to root-owned `controller.json` as `reviewed_pr_head_oid` after the PR exists.
- the formal Owner Review and all five statuses bind the exact PR carrier, while the lightweight
`V2R1C4` tag binds the release-content commit specified by the signed plan.
The controller verifies that the carrier is a commit and that both the authoritative base and the
observed Gitea base are ancestors. KMS signs only the canonical plan digest. The signature envelope
also records the real Review ID and exact reviewed carrier OID.
## Existing Gitea drift reconciliation
Live discovery on 2026-08-02 found:
```text
authority_main=875d41c3101093149414292067fd700dc214d4d3
gitea_main=8291b8368bf690416a3159ac5d21d9f8139c2e61
authority_is_ancestor_of_gitea=true
gitea_pr20_owner_review=false
```
No ref is rewound. The Fastlane carrier has the authoritative lineage as first parent and the
observed Gitea head as a reconciliation parent, with the Fastlane tree selected explicitly. One
formal Owner Review therefore approves the exact reconciled carrier. Gitea advances with
`fast-forward-only`; the controller then imports that exact commit into the authority repository
and advances authority `main` with `git update-ref <new> <expected-old>` compare-and-swap. No
force-push or direct human/Agent write to `main` is used.
Registry and Baseline commits are created sequentially through the Gitea content API. After the
Baseline readback passes, the controller imports the exact final commit and advances authority
`main` by the same compare-and-swap rule. The lightweight tag is created in Gitea, verified as a
commit ref, imported, and created in authority only if the authority tag does not already exist.
## Candidate and rollback surfaces
Candidate acceptance binds both product surfaces:
```text
AIOS candidate: aios-v2r1c4-candidate.service 127.0.0.1:18921
OpenClaw candidate: openclaw-gateway-v2r1c4-candidate.service 127.0.0.1:18790
AIOS production: aios-v2r1c4-production.service 127.0.0.1:18920
OpenClaw production:openclaw-gateway-v2r1c4.service 127.0.0.1:18789
```
The helper copies the active V2R1C3 OpenClaw state into versioned roots without mutating the
source. The two observed source symlinks are accepted only by exact relative path and exact target;
they are replaced by regular files/directories from the signed V2R1C4 artifact. Any symlink drift,
source change during the snapshot, SQLite integrity failure, raw Owner identity remainder, or
artifact conflict fails closed before the candidate units start.
Promotion stops both candidates, stops the V2R1C3 AIOS and Gateway units, then starts and checks
both V2R1C4 production units. Any promotion failure immediately restores both V2R1C3 units.
The rollback test restores V2R1C3, verifies ports 18920 and 18789, then redeploys and re-verifies
both V2R1C4 candidate units.
## Credential and retry rules
Only these encrypted credentials are projected:
```text
gitea-task-token
aws-workload-identity-reference
```
The Gitea token is non-admin, repository-bound, valid for at most two hours, carried only in the
HTTP Authorization header, and revoked with Gitea's current-token endpoint. The AWS credential
contains a standard temporary workload-identity configuration; long-lived Access Keys and software
private-key fallback are forbidden.
SQLite owns one V2R1C4 plan digest and nonce. A completed plan retry returns the original run ID
without side effects. A failed run resumes only after its last durable stage, so statuses and merge
are not repeated. Every invocation revokes the task token supplied to that invocation.
## Success invariant
Completion requires equality of authority/Gitea main refs, authority/Gitea lightweight tag refs,
Registry target, Baseline target, and production target. Any mismatch leaves Baseline unchanged or
marks the run `RECONCILIATION_REQUIRED`; it is never reported as `PRODUCTION_DEPLOYED`.
@@ -1,13 +0,0 @@
{
"schema": "aios.fastlane-supersession/v1",
"recorded_at": "2026-08-02T00:00:00Z",
"status": "SUPERSEDED_BY_V2R1C4_FASTLANE_RELEASE",
"superseded_branch": "feat/aios-governance-bootstrap-v1-delivery",
"frozen_commit": "48d4f1e471390e5e8f55d684d39b98e45ac41485",
"superseded_installer": "AIOS-GOVERNANCE-M1-ONECLICK.zip",
"deleted": false,
"history_rewritten": false,
"production_deployed": false,
"replacement_branch": "release/v2r1c4-fastlane",
"reason": "The V2R1C4 release now uses the bounded single-controller Fastlane path."
}

Some files were not shown because too many files have changed in this diff Show More