Compare commits
231
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffeaff69bc | ||
|
|
bc4d5c6a0a | ||
|
|
a62101340a | ||
|
|
dc9157e679 | ||
|
|
6651753c7f | ||
|
|
6494a666ec | ||
|
|
56ca465003 | ||
|
|
6a8f7c8668 | ||
|
|
486e25d217 | ||
|
|
88aa2f89d9 | ||
|
|
8d9e8cba5d | ||
|
|
12fca283c5 | ||
|
|
349f952be5 | ||
|
|
89bec6de6c | ||
|
|
34d04a412a | ||
|
|
dbf0ccfdf7 | ||
|
|
e08b5ee05f | ||
|
|
828ad998e6 | ||
|
|
1bc55343c8 | ||
|
|
5fb2b08627 | ||
|
|
dcbcf93748 | ||
|
|
54e7dac80a | ||
|
|
658ddbd3e9 | ||
|
|
d31dd8a31b | ||
|
|
f0a9b943ac | ||
|
|
e45a775af2 | ||
|
|
ebf33ba34a | ||
|
|
1c1aa1700b | ||
|
|
fd8bea6a23 | ||
|
|
bd9cdd8daf | ||
|
|
c48ea38711 | ||
|
|
c4f2ac8903 | ||
|
|
47af809a8b | ||
|
|
77eb2aeda9 | ||
|
|
d57772ad3e | ||
|
|
838a8ed69f | ||
|
|
51b98041d9 | ||
|
|
4398caaedf | ||
|
|
0c0be7abc3 | ||
|
|
f96b712b26 | ||
|
|
8f55eff11d | ||
|
|
56d675166a | ||
|
|
d3b0307159 | ||
|
|
0e832bbe9a | ||
|
|
04ab2db654 | ||
|
|
bc18ae4446 | ||
|
|
ea74af9751 | ||
|
|
57c3ce6e96 | ||
|
|
4b57a6268c | ||
|
|
88dcebee78 | ||
|
|
12a973c2cb | ||
|
|
0babec654b | ||
|
|
d787e98554 | ||
|
|
c0450946e4 | ||
|
|
b4c30e16e0 | ||
|
|
f04473f657 | ||
|
|
dedef75f8c | ||
|
|
18c43e42a4 | ||
|
|
95b3728abd | ||
|
|
e1659069b1 | ||
|
|
4b35eb52cb | ||
|
|
05835675b6 | ||
|
|
a1a3a021ef | ||
|
|
40e21d3c07 | ||
|
|
46932c46ca | ||
|
|
9e0a40600b | ||
|
|
d54da54033 | ||
|
|
33152a7f52 | ||
|
|
3dd622b5a5 | ||
|
|
f73052cd5c | ||
|
|
1b69e89718 | ||
|
|
f3c00d7294 | ||
|
|
80e3f65cd6 | ||
|
|
0cf769ef67 | ||
|
|
5e53203a61 | ||
|
|
cf5afdf4c0 | ||
|
|
b6e714e3fc | ||
|
|
0022adf958 | ||
|
|
22888c3d53 | ||
|
|
c2c9eb8d81 | ||
|
|
6df9c92020 | ||
|
|
6bb8c530a4 | ||
|
|
024143efa5 | ||
|
|
06c6374126 | ||
|
|
82177403f6 | ||
|
|
ef074a0c97 | ||
|
|
00fd375e4c | ||
|
|
5b0c3eb404 | ||
|
|
3161470bd2 | ||
|
|
1852701236 | ||
|
|
21fc8c60ed | ||
|
|
77395fa9a0 | ||
|
|
490ed4f65a | ||
|
|
d6d2407a0d | ||
|
|
db3fcb89a8 | ||
|
|
20b7a41104 | ||
|
|
e739e67052 | ||
|
|
1e6c7b9ad8 | ||
|
|
1afbb11d3b | ||
|
|
850beec8d7 | ||
|
|
fb11c71ec0 | ||
|
|
8acdd104bb | ||
|
|
43d77e520f | ||
|
|
a3f731d01c | ||
|
|
8dba5b8c2e | ||
|
|
74158908e8 | ||
|
|
ab36e36d5d | ||
|
|
c1b14753a3 | ||
|
|
c62c8ee9f6 | ||
|
|
9626e66e5b | ||
|
|
b68e3c85a3 | ||
|
|
74014514da | ||
|
|
4e0e6dcb90 | ||
|
|
0778a31ec0 | ||
|
|
80c4cbde03 | ||
|
|
fb739f6e8f | ||
|
|
e6da39a3cd | ||
|
|
95d2e52cb5 | ||
|
|
283dec58ab | ||
|
|
7de1fe80ac | ||
|
|
89a502b39b | ||
|
|
61bec280e2 | ||
|
|
1791c91c5b | ||
|
|
e8c0f0250b | ||
|
|
94718308fc | ||
|
|
a4cbed0f5f | ||
|
|
25c7740739 | ||
|
|
8bbb049c39 | ||
|
|
1707cc4953 | ||
|
|
e397594200 | ||
|
|
68b7f2c1d2 | ||
|
|
fda98e61f5 | ||
|
|
ea9f599522 | ||
|
|
1aabcd8b62 | ||
|
|
4d68af9524 | ||
|
|
467b127804 | ||
|
|
50a73b211b | ||
|
|
f14d4b6e14 | ||
|
|
fc8bd8c8d4 | ||
|
|
382fe662a3 | ||
|
|
4754e5762a | ||
|
|
ce7d35a811 | ||
|
|
60ac7d9d0b | ||
|
|
2a10d1cdd9 | ||
|
|
677ea738e7 | ||
|
|
7154d62cca | ||
|
|
4e154c8baf | ||
|
|
b848361079 | ||
|
|
74224cfbd5 | ||
|
|
a7c09ba646 | ||
|
|
f055cb74bb | ||
|
|
071a378de6 | ||
|
|
a492b15df6 | ||
|
|
a9f0a4359b | ||
|
|
6a9fab894a | ||
|
|
e9111dea47 | ||
|
|
d25fbfa61e | ||
|
|
6b03c47625 | ||
|
|
770fe1ef66 | ||
|
|
7a025430f9 | ||
|
|
d079da4029 | ||
|
|
b8f7dc4112 | ||
|
|
947cdaff42 | ||
|
|
83b0c8bd74 | ||
|
|
fbd880cf8c | ||
|
|
819ac94c9f | ||
|
|
b02401e60d | ||
|
|
cbb2e232f5 | ||
|
|
9e46ac0504 | ||
|
|
ba7669c3b4 | ||
|
|
634f6b164f | ||
|
|
c9ce12d102 | ||
|
|
db50d2fa5a | ||
|
|
63fd23a464 | ||
|
|
2e03ed74f6 | ||
|
|
b86f51c9ab | ||
|
|
09448a5d3d | ||
|
|
18c719b2c6 | ||
|
|
b0a78fa9a8 | ||
|
|
0aca7dbcfc | ||
|
|
3f84d8991e | ||
|
|
5ac8f3ea6f | ||
|
|
bfb1c2fd52 | ||
|
|
cbfe1211c1 | ||
|
|
efd99e620c | ||
|
|
f23b694ad3 | ||
|
|
f97dea55ac | ||
|
|
efd08d7771 | ||
|
|
ff6cb2e217 | ||
|
|
d774620fea | ||
|
|
b26ff010fc | ||
|
|
4b49f2ba4b | ||
|
|
b4b8fe8828 | ||
|
|
4e8ab051a0 | ||
|
|
f61e35b218 | ||
|
|
b2f90f24a3 | ||
|
|
6721eb4752 | ||
|
|
a9e4fd1e40 | ||
|
|
621750976a | ||
|
|
5359e88806 | ||
|
|
76636931ba | ||
|
|
180c1fc66e | ||
|
|
c0ff052dab | ||
|
|
94cffb6ba3 | ||
|
|
9ad5628e69 | ||
|
|
6d397a7894 | ||
|
|
165fea6ba9 | ||
|
|
d079e2087c | ||
|
|
e3b36ebdcc | ||
|
|
5e791b11c0 | ||
|
|
50be65b76e | ||
|
|
7bb86e30bf | ||
|
|
ea56b45b7e | ||
|
|
f53682da7a | ||
|
|
71884ac088 | ||
|
|
969c278cb0 | ||
|
|
6fe2a1e036 | ||
|
|
9a60e0c68e | ||
|
|
46f553ca11 | ||
|
|
4f0a0c2184 | ||
|
|
c107b193d9 | ||
|
|
2db7ced7a4 | ||
|
|
6361d9a334 | ||
|
|
2cf54f04ef | ||
|
|
f840ce5c94 | ||
|
|
35e7b55490 | ||
|
|
1f8d1f23b3 | ||
|
|
e93011791e | ||
|
|
4460564a48 | ||
|
|
4d6658136e | ||
|
|
5767e665c2 |
@@ -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 或部署预生产/生产。
|
||||
@@ -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"''']
|
||||
@@ -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
|
||||
@@ -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 决策。
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## V2R1C1 - 2.1.0-rc.1
|
||||
|
||||
- 固定发布基线为 `fb739f6e8ff9c031d319221ace68dce157694086`。
|
||||
- 收口 Hermes 生命周期、Grant 隔离、Dispatch 恢复、运行证据和回滚/前滚验证。
|
||||
- Portal Adapter 与 Spark Provisioning 仅保留在 Archive,不属于产品发布图。
|
||||
- Future Incubator 保持隔离,不属于 V2R1C1 发布输入。
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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
@@ -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
|
||||
Executable
+86
@@ -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"
|
||||
Executable
+98
@@ -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"
|
||||
@@ -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
|
||||
+9
-10
@@ -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
|
||||
@@ -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 -
|
||||
@@ -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 和回滚演练实际执行并保存证据。
|
||||
@@ -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 和生产部署。
|
||||
@@ -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。
|
||||
@@ -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 和保存的证据,不得仅凭测试文件存在宣布通过。
|
||||
@@ -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 明确批准 |
|
||||
@@ -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 已通过。
|
||||
@@ -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。
|
||||
@@ -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 证据为准。
|
||||
@@ -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 已通过;必须有实际执行日志、退出码和证据。
|
||||
@@ -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
|
||||
@@ -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,并保留非敏感回执。
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user