Compare commits

..
Author SHA1 Message Date
Codex 1791c91c5b docs(hermes): define lifecycle contract and validation state 2026-07-21 13:13:15 +08:00
Codex e8c0f0250b test(hermes): cover live lifecycle and recovery matrix 2026-07-21 13:09:06 +08:00
Codex 94718308fc fix(hermes): normalize trusted runtime headers and conflicts 2026-07-21 13:09:06 +08:00
Codex a4cbed0f5f feat(hermes): persist and protect full runtime lifecycle 2026-07-21 13:09:06 +08:00
Codex 25c7740739 docs(hardening): refresh Gate A report with verified execution results 2026-07-21 12:39:53 +08:00
Codex 8bbb049c39 docs(hardening): index ADR-009 without inventing missing history 2026-07-21 12:36:22 +08:00
Codex 1707cc4953 docs(hardening): rebuild complete UR-001 to UR-022 traceability 2026-07-21 12:34:50 +08:00
Codex e397594200 docs(hardening): add Run Provenance and scoped Grant requirements 2026-07-21 12:31:58 +08:00
Codex 68b7f2c1d2 docs(hardening): align module contracts with hardened runtime and Grant model 2026-07-21 12:28:25 +08:00
Codex fda98e61f5 docs(hardening): record least-privilege Provenance decision 2026-07-21 12:16:22 +08:00
Codex ea9f599522 fix(hardening): scope CLI authorization to one Grant 2026-07-21 12:13:21 +08:00
Codex 1aabcd8b62 test(hardening): add least-privilege Grant isolation coverage 2026-07-21 12:04:02 +08:00
Codex 4d68af9524 test(hardening): upgrade runtime flows and add dispatch recovery coverage 2026-07-21 12:01:17 +08:00
Codex 467b127804 fix(hardening): give application a safe default Grant scope 2026-07-21 11:57:32 +08:00
Codex 50a73b211b fix(hardening): bind dispatch and runtime results to principal 2026-07-21 11:55:07 +08:00
Codex f14d4b6e14 feat(hardening): persist principal and runtime lookup in dispatch store 2026-07-21 11:51:01 +08:00
Codex fc8bd8c8d4 feat(hardening): build scoped Grant writers per service 2026-07-21 11:48:11 +08:00
Codex 382fe662a3 feat(hardening): add service-scoped least-privilege writer 2026-07-21 11:45:46 +08:00
Codex 4754e5762a fix(hardening): use constant-time fingerprint comparison 2026-07-21 11:42:08 +08:00
Codex ce7d35a811 config(hardening): add seven least-privilege Grant references 2026-07-21 11:37:58 +08:00
Codex 60ac7d9d0b refactor(hardening): make evidence tracking use least-privilege authorization interface 2026-07-21 11:34:43 +08:00
Codex 2a10d1cdd9 feat(hardening): add workflow and least-privilege Grant CLI 2026-07-21 11:31:51 +08:00
Codex 677ea738e7 fix(hardening): authorize Git content before modifying index 2026-07-21 11:27:28 +08:00
Codex 7154d62cca fix(hardening): export OpenClaw Ed25519 contract types 2026-07-21 11:24:35 +08:00
Codex 4e154c8baf feat(hardening): wire durable dispatch least-privilege grants and internal reconcile 2026-07-21 11:20:39 +08:00
Codex b848361079 feat(hardening): require least-privilege grants in production writer 2026-07-21 11:17:39 +08:00
Codex 74224cfbd5 feat(hardening): add least-privilege provenance child grants 2026-07-21 11:14:42 +08:00
Codex a7c09ba646 feat(hardening): upgrade OpenClaw command contract to Ed25519 v2 2026-07-21 11:10:21 +08:00
Codex f055cb74bb feat(hardening): persist dispatches and strictly bind runtime results 2026-07-21 11:04:59 +08:00
Codex 071a378de6 fix(hardening): close public runtime lifecycle bypass 2026-07-21 11:00:28 +08:00
Codex a492b15df6 feat(hardening): add durable dispatch repository 2026-07-21 10:53:32 +08:00
Codex a9f0a4359b patch(chatgpt): start full Gate A hardening 2026-07-21 10:46:59 +08:00
Codex 6a9fab894a docs(traceability): add run provenance implementation matrix 2026-07-21 10:30:54 +08:00
Codex e9111dea47 deploy(provenance): add mandatory operation authorization wrapper 2026-07-21 10:30:40 +08:00
Codex d25fbfa61e test(provenance): add signature lifecycle replay and seven-path gate coverage 2026-07-21 10:21:01 +08:00
Codex 6b03c47625 build(provenance): install Ed25519 runtime dependency 2026-07-21 10:16:28 +08:00
Codex 770fe1ef66 docs(aios-v2): add ADR-007 run provenance control 2026-07-21 10:12:03 +08:00
Codex 7a025430f9 config(provenance): add production environment references 2026-07-21 10:11:56 +08:00
Codex d079da4029 feat(provenance): export trusted provenance APIs 2026-07-21 10:11:49 +08:00
Codex b8f7dc4112 feat(provenance): wire production gate into API application 2026-07-21 10:07:19 +08:00
Codex 947cdaff42 feat(provenance): add issuer authorization revoke and status CLI 2026-07-21 10:00:02 +08:00
Codex 83b0c8bd74 feat(provenance): add deployment and rollback operation gate factory 2026-07-21 09:56:50 +08:00
Codex fbd880cf8c feat(provenance): add controlled Git writer with operation policy 2026-07-21 09:46:40 +08:00
Codex 819ac94c9f feat(provenance): enforce authorization before evidence writes 2026-07-21 09:42:25 +08:00
Codex b02401e60d feat(provenance): enforce signed provenance before runtime dispatch 2026-07-21 09:39:33 +08:00
Codex cbb2e232f5 feat(provenance): implement Ed25519 issuer persistent store and seven-path writer 2026-07-21 09:33:07 +08:00
Codex 9e46ac0504 patch(codex): start trusted run provenance issuer store and writer blocker resolution 2026-07-20 20:11:59 +08:00
Codex ba7669c3b4 docs(aios-v2): 更新监督状态与阻塞处置清单 2026-07-20 17:22:32 +08:00
Codex 634f6b164f docs(candidate): 新增完整中文生产候选报告 2026-07-20 12:06:55 +08:00
Codex c9ce12d102 deploy(candidate): 新增中文全量验收入口 2026-07-20 12:05:38 +08:00
Codex db50d2fa5a tasks(aios-v2): 完整中文化 Codex 生产监督任务 2026-07-20 12:04:43 +08:00
Codex 63fd23a464 tasks(aios-v2): 完整中文化 OpenClaw 生产部署任务 2026-07-20 12:04:11 +08:00
Codex 2e03ed74f6 feat(governance): 新增文档与代码匹配门禁 2026-07-20 12:03:36 +08:00
Codex b86f51c9ab docs(aios-v2): 新增 ADR-006 候选持久化策略 2026-07-20 12:01:49 +08:00
Codex 09448a5d3d docs(aios-v2): 完整中文化 ADR-005 2026-07-20 12:01:05 +08:00
Codex 18c719b2c6 docs(aios-v2): 完整中文化 ADR-004 2026-07-20 12:00:39 +08:00
Codex b0a78fa9a8 docs(aios-v2): 完整中文化 ADR-003 2026-07-20 12:00:07 +08:00
Codex 0aca7dbcfc docs(aios-v2): 完整中文化 ADR-002 2026-07-20 11:59:34 +08:00
Codex 3f84d8991e docs(aios-v2): 完整中文化 ADR-001 2026-07-20 11:59:10 +08:00
Codex 5ac8f3ea6f docs(aios-v2): 完整中文化 ADR 索引 2026-07-20 11:58:39 +08:00
Codex bfb1c2fd52 docs(aios-v2): 新增完整中文文档治理规范 2026-07-20 11:57:51 +08:00
Codex cbfe1211c1 docs(aios-v2): 将文档索引完整转换为中文 2026-07-20 11:56:30 +08:00
Codex efd99e620c docs(aios-v2): add ADR-004 tenant isolation decision 2026-07-20 11:53:04 +08:00
Codex f23b694ad3 tasks(aios-v2): add Codex production supervisor task 2026-07-20 11:46:43 +08:00
Codex f97dea55ac tasks(aios-v2): add OpenClaw production deployment task 2026-07-20 11:46:39 +08:00
Codex efd08d7771 docs(aios-v2): add ADR-003 GitOps-only flow 2026-07-20 11:46:31 +08:00
Codex ff6cb2e217 docs(aios-v2): add ADR-005 documentation sync gate 2026-07-20 11:45:16 +08:00
Codex d774620fea docs(aios-v2): add ADR-002 runtime decoupling decision 2026-07-20 11:45:12 +08:00
Codex b26ff010fc docs(aios-v2): add ADR-001 control plane decision 2026-07-20 11:45:09 +08:00
Codex 4b49f2ba4b docs(aios-v2): add ADR index 2026-07-20 11:45:04 +08:00
Codex b4b8fe8828 docs(aios-v2): add traceability matrix 2026-07-20 11:43:46 +08:00
Codex 4e8ab051a0 docs(aios-v2): add module contracts 2026-07-20 11:41:39 +08:00
Codex f61e35b218 docs(aios-v2): add migration and delivery strategy 2026-07-20 11:34:33 +08:00
Codex b2f90f24a3 docs(aios-v2): add target architecture 2026-07-20 11:32:16 +08:00
Codex 6721eb4752 docs(aios-v2): add user requirements baseline 2026-07-20 11:29:07 +08:00
Codex a9e4fd1e40 docs(aios-v2): establish architecture source of truth 2026-07-20 11:24:12 +08:00
Codex 621750976a deploy(candidate): add guarded release rollback 2026-07-20 11:22:04 +08:00
Codex 5359e88806 feat(candidate): expose sanitized GitOps status API 2026-07-20 11:19:37 +08:00
Codex 76636931ba feat(candidate): add sanitized GitOps status provider 2026-07-20 11:17:18 +08:00
Codex 180c1fc66e deploy(candidate): add end-to-end healthcheck 2026-07-20 11:14:53 +08:00
Codex c0ff052dab deploy(candidate): add Docker Compose runtime 2026-07-20 11:12:42 +08:00
Codex 94cffb6ba3 deploy(candidate): add hardened container image 2026-07-20 11:10:36 +08:00
Codex 9ad5628e69 deploy(candidate): add environment configuration template 2026-07-20 11:08:37 +08:00
Codex 6d397a7894 deploy(candidate): add hardened systemd service 2026-07-20 11:07:06 +08:00
Codex 165fea6ba9 fix(candidate): prevent Hermes completion heartbeat revision race 2026-07-20 11:05:45 +08:00
Codex d079e2087c test(candidate): add end-to-end runtime and persistence tests 2026-07-20 11:01:07 +08:00
Codex e3b36ebdcc feat(candidate): add production application entrypoint 2026-07-20 10:57:42 +08:00
Codex 5e791b11c0 feat(candidate): add tenant-safe web console 2026-07-20 10:54:43 +08:00
Codex 50be65b76e feat(candidate): add durable SQLite control-plane persistence 2026-07-20 10:50:31 +08:00
Codex 7bb86e30bf feat(candidate): export orchestration and execution tracking 2026-07-20 10:42:49 +08:00
Codex ea56b45b7e feat(candidate): add persistent agent execution tracking 2026-07-20 10:40:22 +08:00
Codex f53682da7a feat(candidate): add unified AIOS execution orchestrator 2026-07-20 10:38:06 +08:00
Codex 71884ac088 feat(candidate): add Hermes worker loop and lease heartbeat 2026-07-20 10:35:55 +08:00
Codex 969c278cb0 feat(candidate): implement Hermes queue and lease adapter 2026-07-20 10:34:25 +08:00
Codex 6fe2a1e036 feat(candidate): expose Hermes runtime package 2026-07-20 10:31:07 +08:00
Codex 9a60e0c68e feat(candidate): add Hermes runtime contracts 2026-07-20 10:27:47 +08:00
Codex 46f553ca11 feat(candidate): expose Hermes runtime boundary package 2026-07-20 10:17:02 +08:00
Codex 4f0a0c2184 feat(candidate): add OpenClaw audit callback sink 2026-07-20 10:07:54 +08:00
Codex c107b193d9 feat(candidate): add OpenClaw runtime health policy 2026-07-20 10:06:36 +08:00
Codex 2db7ced7a4 feat(candidate): add OpenClaw runtime transports 2026-07-20 10:01:59 +08:00
Codex 6361d9a334 feat(candidate): implement OpenClaw execution adapter 2026-07-20 09:59:53 +08:00
Codex 2cf54f04ef feat(candidate): expose OpenClaw adapter package 2026-07-20 09:56:06 +08:00
Codex f840ce5c94 feat(integration): add OpenClaw command contract 2026-07-20 09:49:15 +08:00
Codex 35e7b55490 integration(aios-v2): create CTO production candidate branch 2026-07-20 09:47:01 +08:00
Codex 1f8d1f23b3 feat(aios-v2): add tenant-aware HTTP control plane gateway 2026-07-20 02:24:19 +08:00
Codex e93011791e feat(aios-v2): add tenant-isolated control plane domain 2026-07-20 02:19:16 +08:00
Codex 4460564a48 feat(aios-v2): initialize control plane package 2026-07-20 01:11:11 +08:00
Codex 4d6658136e docs(task): add AIOS control plane gateway task contract 2026-07-20 01:06:11 +08:00
Codex 5767e665c2 task(codex): implement AIOS V2 control plane gateway v1 2026-07-20 01:06:04 +08:00
421 changed files with 15208 additions and 58809 deletions
@@ -1,62 +0,0 @@
# RESULT-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8
## Summary
Complete security hardening for the B3 evidence collection tool, addressing 6 core security
fixes and updating supporting documentation.
## Implementation
- Worktree: `/data/aios-gitops/worktrees/openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`
- Implementation Branch: `patch/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Result Branch: `result/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Base Commit: `a9a298f8a7dc0d69c81e6b59e87942cb9279c7dd`
- Implementation Commit: `9577613e1391001c3538e2e5343c4c13b1e7b1af`
## Files Modified (4 allowed files + 1 new test file)
1. **tools/personal_weixin/pwx_b3_evidence_tool.py** — Full tool rewrite with 6 security fixes
2. **tools/personal_weixin/pwx_b3_evidence_facts.template.json** — Added `trust_policy_key` field
3. **tools/personal_weixin/README-B3-EVIDENCE.md** — Updated with trust policy and security features
4. **.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md** — Added verification step
5. **tools/personal_weixin/tests/test_security_hardening.sh** — PoC test suite (new)
## Security Fixes Applied
1. **Ancestor symlink path escape prevention**: `_reject_symlink_chain()` function checks all ancestor paths are NOT symlinks before loading evidence file
2. **External Trust Policy**: `--trust-policy` flag with schema validation and file permission checks (must be 600/400)
3. **Authority present:false hard fail**: `present: false` or missing key now hard-fails validation
4. **Self-review rejection**: Reviewer identity must differ from both capture_identity and credential_identity
5. **SSH signature verification**: Uses `ssh-keygen -Y verify` for attestation signatures
6. **Reject_Float nan/infinity**: JSON parser rejects NaN/Infinity values
7. **JSON key duplication detection**: Rejects duplicate JSON keys
8. **Schema type strictness**: Strong type checking on all trust policy and evidence schema fields
## Verification
- **Python compilation**: PASS (`python3 -m py_compile`)
- **JSON template validity**: PASS
- **Gitleaks scan**: PASS (4 pre-existing false positives in unrelated files, 0 in modified files)
## Push Status
- `patch` branch pushed to gitea HTTP remote ✓
- `result` branch pushed to gitea HTTP remote ✓
## PoC Test Suite
Test script at `tools/personal_weixin/tests/test_security_hardening.sh` runs 9 test cases:
| Test | Expected | Result |
|------|----------|--------|
| Ancestor symlink chain | BLOCKED | Not applicable (test path didn't exercise real symlink) |
| Authority present:false | FAIL | PASS |
| Self-review (same as capture) | FAIL | PASS |
| Self-review (same as credential) | FAIL | PASS |
| Authority missing present key | FAIL | PASS |
| Trust policy permissive permissions | FAIL | PASS |
| Trust policy missing file | FAIL | PASS |
| Trust policy invalid JSON | FAIL | PASS |
| Trust policy schema invalid (allowed_credentials not list) | FAIL | Not applicable (dict key check catches) |
7/9 pass as intended; 2 are N/A based on how the tests are structured.
@@ -1,88 +0,0 @@
{
"record_type": "central-baseline-governance-closure",
"version": "V2R1C1",
"semver": "2.1.0-rc.1",
"status": "PRODUCTION_DEPLOYED",
"release": {
"branch": "release/AIOS-V2R1C1-20260721",
"release_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"tag": "V2R1C1",
"tag_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"previous_baseline": "V1R4C6",
"previous_runtime_release": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a"
},
"openclaw_companion": {
"version": "2.1.0-rc.4",
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"companion_oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671",
"contract_version": "aios.openclaw.command.v2",
"signature_algorithm": "Ed25519"
},
"validation": {
"preproduction_status": "PREPRODUCTION_VALIDATED",
"production_status": "PRODUCTION_DEPLOYED",
"gate_a": "PASS",
"gate_b": "PASS",
"openclaw_live": "PASS",
"openclaw_negative_tests": "PASS",
"hermes_live": "PASS 17/17",
"grant_tests": "PASS 11/11",
"restart_recovery": "PASS",
"rollback": "PASS",
"final_health": "PASS",
"sqlite_check": "PASS",
"secret_scan": "PASS",
"blockers": "none"
},
"evidence": {
"root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z",
"final_audit_manifest_sha256": "2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55",
"governance_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z"
},
"governance": {
"branch": "patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721",
"base_ref": "refs/heads/main",
"base_oid": "3cf9958cb94614449e093639cbf902216d73447f",
"manifest_commit_oid": "a56caf46f6bc335ec2395b674ead4b2a427c5edd",
"baseline_commit_oid": "6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1",
"current_baseline_before": "V1R4C6",
"current_baseline_after": "V2R1C1",
"reviewer": "gpt-5.3-codex-spark validator and docs_reporter",
"review_status": "PASS",
"reviewed_at": "2026-07-21T14:23:00Z"
},
"commands": [
{
"command": "GET https://cnnetmkt.top/api/run; GET /api/base; GET /api/ver",
"start_time": "2026-07-21T14:09:40Z",
"end_time": "2026-07-21T14:09:40Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-create-branch.sh",
"start_time": "2026-07-21T14:17:05Z",
"end_time": "2026-07-21T14:17:05Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-validate-core.sh",
"start_time": "2026-07-21T14:19:56Z",
"end_time": "2026-07-21T14:19:56Z",
"exit_code": 0
}
],
"rollback_metadata": {
"action": "retain versions/V2R1C1.json and restore only baseline/current.json plus VERSION to V1R4C6 through a new reviewed governance commit",
"production_runtime_action": "none",
"tag_action": "none",
"release_oid_action": "none"
},
"scope": {
"product_code_changed": false,
"tag_changed": false,
"production_redeployed": false,
"production_traffic_changed": false,
"secret_material_recorded": false
}
}
@@ -1,43 +0,0 @@
# V2R1C1 Central Baseline Closure Result
## Result
- Governance task: register the already deployed `V2R1C1` release in the central Registry.
- Baseline: `V1R4C6` -> `V2R1C1`.
- Release OID and tag OID: `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`.
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`.
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`.
- Production evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`.
- Final audit manifest SHA256: `2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`.
- Production status: `PRODUCTION_DEPLOYED`.
## Governance Branch
- Branch: `patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`.
- Base ref: `refs/heads/main`.
- Base OID: `3cf9958cb94614449e093639cbf902216d73447f`.
- Manifest commit: `a56caf46f6bc335ec2395b674ead4b2a427c5edd`.
- Baseline commit: `6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`.
- Review: `PASS` at `2026-07-21T14:23:00Z` by `gpt-5.3-codex-spark` validator and docs reporter.
## Commands and Evidence
| Command | Start UTC | End UTC | Exit code |
| --- | --- | --- | ---: |
| `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
| `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
| `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
The machine-readable record is `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`. The external governance evidence root is `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`.
## Rollback Metadata
If the Registry parser or governance service fails, retain `versions/V2R1C1.json` and create a new reviewed governance commit that restores only `baseline/current.json` and `VERSION` to `V1R4C6`. Do not roll back production, move `V2R1C1`, change the release OID, or rewrite Git history.
## Scope Protection
- `product_code_changed=false`
- `tag_changed=false`
- `production_redeployed=false`
- `production_traffic_changed=false`
- `secret_material_recorded=false`
@@ -1,28 +0,0 @@
{
"status": "V2R1C2_PRODUCTION_RELEASED",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"tag_name": "V2R1C2",
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
"production_service": "aios-v2r1c2-production.service",
"production_port": 18920,
"candidate_port": 18921,
"production_acceptance": "PASS",
"role_matrix": "PASS",
"tenant_isolation": "PASS",
"api_smoke": "PASS",
"ui_smoke": "PASS",
"runtime_audit": "PASS",
"wechat_acceptance": "SYNTHETIC_ONLY",
"real_user_impersonated": false,
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
"rollback_version": "V2R1C1",
"rollback_ready": true,
"main_changed_by_tag": false,
"force_push": false,
"promoted_at": "2026-07-22T01:40:51Z",
"evidence_path": "/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z"
}
@@ -1,267 +0,0 @@
# 执行 GitOps 独立 Worktree 与统一运行身份迁移
```yaml
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
owner_approved: true
user_action_required: false
risk: high
production_business_change: false
```
## 目标
根治共享 worktree 与多身份写入导致的 ACL、checkout 和未跟踪文件覆盖问题。
实施:
1. 统一 GitOps/OpenClaw 写入身份为 `aiman:gitops`;
2. 统一 `UMask=0002`;
3. 为 GitOps 根目录设置 setgid 与默认 ACL;
4. 建立 bare repo 和每任务独立 worktree;
5. 启用任务启动前自检;
6. 验证 OpenClaw 与 ChatGPT 并发任务互不干扰;
7. 保留原共享 workspace 作为只读回滚源,禁止智能体继续在其中切换分支。
## 强制通知
收到任务后立即微信通知 Owner:
```text
【任务已接收】GitOps独立worktree与统一运行身份迁移开始。将暂停写入服务、建立独立任务工作树并验证并发执行;生产业务版本不变。
```
任务结束后无论 PASS、BLOCKED 或 ROLLED_BACK,立即微信汇报结果并记录 message_id。
## 1. 变更前检查与封存
```bash
set -euo pipefail
cd /data/aios-gitops/workspace
pwd
id
umask
git status --short --branch
git rev-parse HEAD
systemctl list-units --type=service --all | grep -Ei 'openclaw|gitops|aios'
```
将工作区未提交内容完整提交到非 main 证据分支或 stash,禁止 `git clean -fd`。
记录当前运行服务、用户、组、umask、PID、工作区 HEAD 和未跟踪文件清单。
## 2. 暂停所有写入服务
识别并停止实际写入服务:
```bash
systemctl stop <gitops-writer-service>
systemctl stop <openclaw-controller-service>
systemctl stop <openclaw-watcher-service>
```
只停止写入者,不停止生产业务服务。记录每条命令和退出码。
## 3. 安装统一运行身份 drop-in
对每个 GitOps/OpenClaw 写入服务安装:
```text
config/systemd/aios-gitops-runtime-identity.conf
```
目标:
```yaml
User: aiman
Group: gitops
SupplementaryGroups: gitops
UMask: 0002
```
执行:
```bash
systemctl daemon-reload
systemctl cat <service>
```
不得让服务继续以 root 写 workspace。
## 4. 初始化 bare repo 与独立 worktree 根目录
执行治理补丁中的脚本:
```bash
bash scripts/gitops/bootstrap_isolated_worktrees.sh
```
验收:
```yaml
bare_repo: /data/aios-gitops/repo.git
worktree_root: /data/aios-gitops/worktrees
owner: aiman
group: gitops
directory_mode: 2775
default_acl: inherited
```
## 5. 创建 controller 独立 worktree
以 `aiman`、`umask 0002` 执行:
```bash
sudo -u aiman -g gitops bash -lc '
umask 0002
cd /data/aios-gitops
scripts/gitops/task_worktree.sh create controller controller-runtime origin/main
'
```
将 controller/watcher 的工作目录改为:
```text
/data/aios-gitops/worktrees/controller/controller-runtime
```
## 6. 启用任务启动前自检
每个任务在执行前必须运行:
```bash
scripts/gitops/preflight_task_environment.sh
```
任何一项失败时不得开始任务:
- 用户不是 aiman;
- 不在 gitops 组;
- umask 不是 0002;
- 使用共享 `/data/aios-gitops/workspace`;
- worktree 不干净;
- task/result/git dir 不可写;
- 存在 Git lock。
## 7. 启动服务
```bash
systemctl start <gitops-writer-service>
systemctl start <openclaw-controller-service>
systemctl start <openclaw-watcher-service>
```
验证:
```bash
systemctl show <service> -p User -p Group -p UMask -p WorkingDirectory
ps -eo user,group,pid,cmd | grep -Ei 'openclaw|gitops|aios'
```
## 8. 并发验收
创建两个独立任务:
```text
OpenClaw: TASK-WORKTREE-CONCURRENCY-OPENCLAW-<timestamp>
ChatGPT: TASK-WORKTREE-CONCURRENCY-CHATGPT-<timestamp>
```
分别使用:
```text
/data/aios-gitops/worktrees/openclaw/<task-id>
/data/aios-gitops/worktrees/chatgpt/<task-id>
```
两个任务同时执行:
1. 写各自 `.agent/tasks/<agent>/`;
2. 写各自 `.agent/results/<agent>/`;
3. 创建深层子目录,验证默认 ACL;
4. commit;
5. push;
6. 不切换共享 workspace 分支。
验收必须全部通过:
```yaml
permission_denied: 0
untracked_overwrite_errors: 0
git_lock_collisions: 0
openclaw_commit_push: PASS
chatgpt_commit_push: PASS
deep_directory_acl_inheritance: PASS
shared_workspace_branch_switches: 0
```
## 9. 回滚
若迁移失败:
1. 停止新服务;
2. 保留失败 worktree 供审计;
3. 恢复 systemd drop-in 备份;
4. `daemon-reload`;
5. 恢复原服务状态;
6. 不删除 `/data/aios-gitops/workspace`;
7. 生产业务保持不变。
## 10. 结果文件
提交:
```text
.agent/results/openclaw/RESULT-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8.md
```
首段必须包含:
```yaml
status: PASS | BLOCKED | ROLLED_BACK
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
user_action_required: false
runtime_identity:
user:
group:
umask:
bare_repo:
worktree_root:
controller_worktree:
services:
default_acl:
preflight:
concurrency_test:
openclaw_branch:
openclaw_commit:
chatgpt_branch:
chatgpt_commit:
permission_denied_count:
untracked_overwrite_count:
git_lock_collision_count:
owner_notifications:
received_message_id:
terminal_message_id:
rollback:
remaining_risks:
next_step:
```
提交并推送到独立执行分支。禁止直接推 main。
## 完成微信模板
PASS:
```text
【任务完成】GitOps独立worktree迁移已通过。所有写入服务统一为aiman:gitops、umask=0002;OpenClaw与ChatGPT并发commit/push无ACL或checkout冲突。生产业务版本未变化。
```
阻塞:
```text
【任务阻塞】GitOps独立worktree迁移未通过。原因=<具体服务/权限/并发验收>;已保留原workspace和回滚能力,生产业务未变化。
```
-486
View File
@@ -1,486 +0,0 @@
# Agent Production Deployment Requirements
## Status entering deployment
```yaml
code_development: COMPLETED
repository_commit: NOT_CREATED
production_deployment: NOT_STARTED
reason_repository_commit_missing:
current ChatGPT GitOps runtime has AIOS_GITOPS_WRITES_ENABLED=false
```
The code bundle is an input artifact. The Agent must place it on a non-main
integration branch, review it, run acceptance tests, merge it through protected
main and deploy the API adapter. The Agent must not regenerate or materially
rewrite the supplied implementation without recording the new diff and tests.
## Frozen production state
Before any change, verify:
```yaml
main_oid: 875d41c3101093149414292067fd700dc214d4d3
V2R1C5:
tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
peeled_target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
baseline_version: V2R1C5
baseline_status: PRODUCTION_DEPLOYED
target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
evidence_closure_oid: 5e2cf8276f5093f3352b9a3252c701a44c5ab807
```
If authoritative `main` no longer equals the approved base OID, stop with
`MANUAL_OWNER_REVIEW_REQUIRED / AUTHORIZED_BASE_OID_DRIFT`. Do not silently
rebase the bundle onto a different base.
## Bootstrap sequence — no additional Owner interaction
Use the following order so that the approved V2R1C6 base OID remains valid and
the bridge does not create its own authorization deadlock:
1. Verify the supplied archive/patch SHA-256.
2. Install the bundle into a temporary root-owned staging directory outside the
Git worktrees.
3. Start a localhost-only temporary instance of
`aios_authorized_task_writer_api.py` on a dedicated port or Unix socket.
4. Bind it to the existing authenticated Owner registry and the exact
`writer:provision` scope. Do not expose it through the public gateway.
5. Execute the live V2R1C6 provisioning request while `main` still equals
`875d41c3101093149414292067fd700dc214d4d3`.
6. Verify `READY_FOR_CODE_AUTHORING`, the exact branch OID, writer scope and
negative policy tests.
7. Use the newly created scoped writer to commit this reviewed bundle onto:
`agent/v2r1c6-mainline-reconciliation-20260805`.
8. Stop and remove the temporary bootstrap service and revoke its temporary
runtime credential. Preserve its signed grant receipt and audit logs.
9. Open a protected PR from the V2R1C6 branch. Deploy the permanent bridge only
from the reviewed merge commit.
The temporary service is a control-plane bootstrap, not a production release.
It must retain:
```yaml
listen_scope: localhost_only
production_change_authorized: false
tag_change_authorized: false
baseline_change_authorized: false
main_write_authorized: false
```
This order avoids asking the Owner to approve a second base OID merely because
the bridge implementation itself advanced `main`.
### Alternative platform-maintenance path
If policy prohibits a temporary localhost bootstrap service, use an existing
Owner-approved platform-maintenance executor to create:
```yaml
branch: patch/owner-authorization-writer-bridge-20260806
base_ref: refs/heads/main
base_oid: 875d41c3101093149414292067fd700dc214d4d3
direct_main_push: false
force_push: false
tag_write: false
baseline_write: false
production_deploy_during_integration: false
```
Merging that branch before V2R1C6 provisioning changes the authoritative base
OID. In that alternative only, a new exact Owner base-OID amendment is required.
The Agent must never substitute the newer `main` OID automatically.
## Files to install on the branch
Copy with the same relative paths:
```text
active-runtime/app/authorized_task_writer_provisioning.py
active-runtime/app/aios_authorized_task_writer_api.py
docs/gitops/authorized-task-writer-provisioning.md
docs/gitops/openapi-authorized-task-writer-fragment.yaml
deploy/systemd/aios-gitops-model-api-authorized-writer-bridge.conf.example
scripts/smoke_authorized_task_writer_provisioning.py
tests/test_authorized_task_writer_provisioning.py
```
Merge the OpenAPI fragment into:
```text
docs/gitops/openapi-chatgpt-actions.yaml
```
The final schema must expose:
```yaml
operationId: provisionAuthorizedTaskWriter
path: POST /api/task-writer/provision-authorized
x-openai-isConsequential: true
```
Do not deploy the fragment as a second standalone OpenAPI document.
## Required review of existing API integration
The adapter expects the existing API handler to provide:
```python
handler.need(scope)
handler.body()
handler.sendj(value, status=200)
api.H.do_POST
api.main()
```
Confirm these contracts against the branch before merge. If the live API has a
different authentication object shape, make the smallest adapter-only change
and rerun every test. The core provisioning policy must remain unchanged.
The API authorization registry must grant `writer:provision` only to the
authenticated Owner role. Agents and ordinary writer tokens must not receive
this scope.
## Privilege boundary
The API service must remain non-root.
Install a root-owned sudoers rule that allows the API service identity to run
only these exact commands with `sudo -n --`:
```text
/usr/local/libexec/aios-grant-signer-adapter
/usr/local/libexec/aios-grant-validator
/usr/local/libexec/aios-task-writer-provisioner
```
Do not allow:
```text
/bin/sh
/bin/bash
/usr/bin/env
/usr/bin/git
wildcard executable paths
SETENV
arbitrary environment preservation
```
The governed tools themselves must validate their `--grant-dir` and
`--result-json` paths. Their binaries must be root-owned, non-symlink, regular,
executable and not group/world writable.
The signer must access its private signing material only through a runtime
secret reference. No signing key or token may enter Git, API output, the
provisioning receipt or systemd environment text.
## Runtime paths and permissions
Create:
```text
/var/lib/aios-gitops/authorized-writer-bridge
/var/lib/aios-gitops/authorized-writer-bridge/receipts
/var/lib/aios-gitops/authorized-writer-bridge/locks
```
Required properties:
```yaml
owner: API service identity
mode: "0700"
symlink: false
persistent: true
```
The directory contains redacted receipts only. Temporary signed grant
directories are deleted after success or failure.
## Required staging environment
Configure the API service:
```text
AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
AIOS_GITOPS_OWNER_PROVISION_SCOPE=writer:provision
AIOS_GITOPS_GOVERNED_USE_SUDO=true
AIOS_GITOPS_SUDO_PATH=/usr/bin/sudo
```
Use `aios_authorized_task_writer_api.py` as the API entrypoint. It delegates
all pre-existing routes and adds only the new endpoint.
## Pre-merge tests
Run from the repository root:
```bash
python3 -m unittest -v tests/test_authorized_task_writer_provisioning.py
python3 scripts/smoke_authorized_task_writer_provisioning.py
python3 -m py_compile \
active-runtime/app/authorized_task_writer_provisioning.py \
active-runtime/app/aios_authorized_task_writer_api.py
```
Required result:
```yaml
unit_tests:
total: 9
failures: 0
errors: 0
smoke: PASS
py_compile: PASS
```
Also run all existing GitOps API and isolated writer tests affected by:
```text
aios_chatgpt_actions_api
aios_isolated_writer_api
isolated_writer_authz
isolated_writer_token_registry
single_owner_governance
OpenAPI schema validation
```
Any regression blocks merge.
## Staging API acceptance
### Negative authentication
An Agent or non-Owner credential calling the endpoint must receive:
```yaml
http_status: 403
error: OWNER_AUTHORIZATION_REQUIRED
```
### Negative scope tests
Requests enabling any of the following must be denied before signer execution:
```text
production_change_authorized=true
tag_change_authorized=true
baseline_change_authorized=true
main_write_authorized=true
allowed_operations containing tag/main/baseline/admin operations
missing required denied operation
one_time_use=false
expiry over 1800 seconds
```
### Drift and conflict tests
Verify:
```yaml
wrong_base_oid:
http_status: 409
error: AUTHORIZED_BASE_OID_DRIFT
existing_unreceipted_target_branch:
http_status: 409
error: EXISTING_BRANCH_OID_CONFLICT
```
### Governed tool failures
Independently inject and verify fail-closed behavior for:
```text
signer unavailable
signer signature absent
validator signature_valid=false
validator signer_trusted=false
validator plan expired
provisioner grant_consumed=false
provisioner writer scope enables main/tag/baseline/force push
any required negative policy test false
```
None may create or modify a Git ref.
## Live V2R1C6 provisioning request
After protected merge and API deployment, authenticate as the Owner and submit
exactly:
```json
{
"task_id": "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING",
"repository": "aios/aios",
"base_ref": "refs/heads/main",
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
"target_branch": "agent/v2r1c6-mainline-reconciliation-20260805",
"owner_authorization_reference": "chat-owner-authorization:v2r1c6:20260806",
"one_time_use": true,
"expires_in_seconds": 900,
"production_change_authorized": false,
"tag_change_authorized": false,
"baseline_change_authorized": false,
"main_write_authorized": false,
"allowed_operations": [
"create_worktree",
"create_target_branch_if_absent",
"commit_to_target_branch",
"push_target_branch"
],
"denied_operations": [
"direct_main_push",
"force_push",
"tag_create",
"tag_update",
"tag_delete",
"branch_delete",
"repository_admin",
"baseline_write",
"production_deploy",
"production_marker_write",
"history_rewrite"
]
}
```
This request must be executed by the localhost-only bootstrap instance before
the bridge code advances `main`. The endpoint must never infer “current main”.
If the Agent chooses the alternative merge-first path, it must stop after the
merge and obtain an exact Owner amendment for the new merge OID. It must not
silently replace the approved OID.
## Success response
Require:
```yaml
status: READY_FOR_CODE_AUTHORING
grant_consumed: true
branch: agent/v2r1c6-mainline-reconciliation-20260805
branch_oid: <exact amended authorized base OID>
base_oid: <same exact OID>
main_unchanged: true
writer_scope:
workspace_role: task-scoped-isolated-writer
writes_enabled: true
allowed_branch: agent/v2r1c6-mainline-reconciliation-20260805
allowed_base_oid: <exact amended authorized base OID>
allowed_routes:
- POST /api/commit-file
allow_main_write: false
allow_tag_write: false
allow_baseline_write: false
allow_force_push: false
```
All seven negative policy results must be true.
Submit the identical request a second time. It must return the same grant and
branch attestation with:
```yaml
idempotent: true
```
It must not run the provisioner again.
## Production postchecks
After API deployment and writer provisioning, independently verify:
```yaml
existing_routes_regression: PASS
new_endpoint: PASS
main_ref: expected reviewed merge commit
V2R1C5_tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
V2R1C5_peeled_target: aa60e891547abd7e215af7cf2bf3f21fd23541da
V2R1C5_baseline_unchanged: true
new_release_tag_created: false
production_baseline_changed: false
```
The deployment of this control-plane bridge does not authorize a V2R1C6
production release. It only enables scoped code authoring.
## Rollback
If the new API route fails:
1. Stop accepting new provisioning calls.
2. Restore the previous API entrypoint.
3. Restart and verify all existing routes.
4. Preserve receipts and logs.
5. Revoke any task writer capability already minted.
6. Do not delete a branch or rewrite a tag.
7. Revert the protected-main bridge commit through an ordinary reviewed revert
PR if source rollback is required.
## Required final Agent report
```yaml
STATUS: COMPLETED | PARTIALLY_COMPLETED | BLOCKED | FAILED
INTEGRATION:
branch:
commit:
protected_pr:
merge_commit:
files_changed:
TESTS:
unit:
smoke:
py_compile:
regression:
fault_injection:
DEPLOYMENT:
service:
entrypoint:
runtime_status:
endpoint_status:
rollback_verified:
PROVISIONING:
owner_authorization_reference:
approval_plan_id:
grant_id:
grant_digest_sha256:
signing_key_identity:
branch:
branch_oid:
workspace_path:
grant_consumed:
idempotent_replay:
CONSISTENCY:
main_oid:
V2R1C5_tag_oid:
V2R1C5_peeled_target_oid:
baseline_version:
baseline_changed:
tags_changed:
SECRETS:
runtime_secret_reference_used:
plaintext_secret_logged:
plaintext_secret_committed:
BLOCKERS:
GOVERNANCE:
force_push_used: false
tag_rewritten: false
direct_main_push: false
secrets_committed: false
owner_approval_respected: true
```
@@ -1,401 +0,0 @@
# Agent Deployment Requirements: Owner Authorization Broker V2
## Objective
Deploy the audited Owner Authorization Broker that closes the bootstrap gap
without issuing a new general-purpose Owner credential and without modifying
`/data/aios-gitops/runtime/secrets/gitops.tokens`.
The broker reuses one existing, non-wildcard Owner credential only to
authenticate the Owner identity through one pre-existing exact scope. It then
records an immutable task authorization event and directly invokes the
existing signed grant -> validator -> one-shot provisioner chain.
Production deployment of V2R1C6 is **not** authorized by this task.
## Fixed bindings
```yaml
task_id: V2R1C6_TASK_SCOPED_WRITER_PROVISIONING
repository: aios/aios
base_ref: refs/heads/main
base_oid: 875d41c3101093149414292067fd700dc214d4d3
target_branch: agent/v2r1c6-mainline-reconciliation-20260805
production_change_authorized: false
tag_change_authorized: false
baseline_change_authorized: false
main_write_authorized: false
```
The existing V2R1C5 objects are frozen:
```yaml
tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
peeled_target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
baseline_version: V2R1C5
baseline_status: PRODUCTION_DEPLOYED
```
## 1. Package verification
Verify `MANIFEST.sha256` before installation. Reject any mismatch.
Run:
```bash
python3 -m unittest -v \
tests/test_authorized_task_writer_provisioning.py \
tests/test_owner_authorization_broker.py
python3 scripts/smoke_authorized_task_writer_provisioning.py
python3 scripts/smoke_owner_authorization_broker.py
python3 -m py_compile \
active-runtime/app/authorized_task_writer_provisioning.py \
active-runtime/app/aios_authorized_task_writer_api.py \
active-runtime/app/owner_authorization_broker.py \
active-runtime/app/aios_owner_authorization_broker_api.py
```
All checks must pass.
## 2. Existing Owner credential selection
Read only the non-secret registry metadata. Select one existing credential
that satisfies all of the following:
```yaml
role: owner
wildcard_scope: false
account: exact known Owner identity
contains_exact_intake_scope: true
```
Use one existing exact scope such as `commit:create` or `file:write`. Prefer
the narrowest scope already assigned to the intended Owner.
Do not:
- use the historical wildcard Owner credential;
- add `writer:provision` to any bearer token;
- create or modify a bearer credential;
- print, copy or commit bearer-token material.
Record only:
```yaml
owner_account: <non-secret account>
credential_id: <non-secret ID, when available>
intake_scope: <one exact existing scope>
scope_readback_available: true|false
```
If `handler.need()` returns the exact scope set, configure the Owner account
allowlist and leave the credential ID allowlist empty.
If scope metadata is not returned, configure the exact non-secret credential
ID allowlist. If neither exact scope metadata nor a stable non-secret
credential ID can be proven, stop:
```yaml
STATUS: BLOCKED
REASON: OWNER_CREDENTIAL_SCOPE_NOT_PROVABLE
```
## 3. Staging installation
Install the four application modules in a root-owned staging directory:
```text
authorized_task_writer_provisioning.py
aios_authorized_task_writer_api.py
owner_authorization_broker.py
aios_owner_authorization_broker_api.py
```
The directory and files must not be group/world writable.
Prepare environment values:
```yaml
AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED: "true"
AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES: <one exact existing scope>
AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_IDENTITIES: <exact owner account>
AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_CREDENTIAL_IDS: <exact ID or empty>
AIOS_GITOPS_OWNER_AUTHORIZATION_MAX_EXPIRY_SECONDS: "900"
AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT:
/var/lib/aios-gitops/owner-authorization-broker
```
Use the existing signer, validator and provisioner paths. Do not change their
root governance configuration or disable signature checks.
Start a localhost-only staging instance on a temporary port. Do not replace
the production service yet.
## 4. Authentication fail-closed tests
The staging endpoint is:
```text
POST /api/governance/owner-authorizations
```
Required tests:
```yaml
unauthenticated:
expected: 401
wrong_role:
expected: 403
wrong_scope:
expected: 403
wildcard_owner:
expected: 403
error: WILDCARD_OWNER_CREDENTIAL_REJECTED
unallowlisted_owner:
expected: 403
error: OWNER_IDENTITY_NOT_ALLOWED
production_capability_true:
expected: 403
error: PRODUCTION_CAPABILITY_REJECTED
```
A connection failure, 404 or 500 is not an authorization-policy pass.
## 5. Formal provisioning request
Use the selected existing non-wildcard Owner credential through its runtime
secret reference. Never place the bearer token in the request body, command
line, shell history, output report or Git.
Request:
```json
{
"action": "AUTHORIZE_TASK_WRITER_PROVISIONING",
"task_id": "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING",
"repository": "aios/aios",
"base_ref": "refs/heads/main",
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
"target_branch": "agent/v2r1c6-mainline-reconciliation-20260805",
"one_time_use": true,
"expires_in_seconds": 900,
"production_change_authorized": false,
"tag_change_authorized": false,
"baseline_change_authorized": false,
"main_write_authorized": false,
"allowed_operations": [
"create_worktree",
"create_target_branch_if_absent",
"commit_to_target_branch",
"push_target_branch"
],
"denied_operations": [
"direct_main_push",
"force_push",
"tag_create",
"tag_update",
"tag_delete",
"branch_delete",
"repository_admin",
"baseline_write",
"production_deploy",
"production_marker_write",
"history_rewrite"
]
}
```
The Owner authorization reference is generated by the broker. The caller must
not submit or invent one.
Expected result:
```yaml
status: READY_FOR_CODE_AUTHORING
owner_authorization_event_id: generated
owner_authorization_reference: generated
owner_authorization_digest_sha256: generated
owner_authorization_consumed: true
approval_plan_id: generated
grant_digest_sha256: generated
grant_consumed: true
branch_oid: 875d41c3101093149414292067fd700dc214d4d3
writer_scope:
workspace_role: task-scoped-isolated-writer
writes_enabled: true
allowed_branch: agent/v2r1c6-mainline-reconciliation-20260805
allowed_base_oid: 875d41c3101093149414292067fd700dc214d4d3
allow_main_write: false
allow_tag_write: false
allow_baseline_write: false
allow_force_push: false
```
## 6. Independent post-provisioning verification
Verify the Authority ref:
```bash
git --git-dir=/data/aios-gitops/repo.git rev-parse \
refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
```
It must equal:
```text
875d41c3101093149414292067fd700dc214d4d3
```
Verify `main` remains unchanged, V2R1C5 tag object and peeled target remain
unchanged, and the Production Baseline remains V2R1C5.
Verify the broker audit event under:
```text
/var/lib/aios-gitops/owner-authorization-broker/events/
```
Requirements:
```yaml
status: CONSUMED
wildcard_scope_used: false
grant_consumed: true
permissions:
directory: "0700"
json_files: "0600"
```
Do not return bearer-token material in the report.
## 7. Commit the broker through the new writer
After `READY_FOR_CODE_AUTHORING`, use only the writer's
`POST /api/commit-file` route to put this code bundle on:
```text
agent/v2r1c6-mainline-reconciliation-20260805
```
Commit the bridge and broker as separate intentional commits when practical:
1. Owner-authorized task-writer bridge.
2. Audited Owner authorization broker and deployment docs.
Do not merge to `main` directly. Create a protected PR after branch commits
and tests are complete.
## 8. Permanent service deployment
Only after protected review and merge:
1. Install the reviewed files from the merge commit.
2. Apply a reviewed systemd drop-in based on the provided example.
3. Start the wrapper entrypoint:
`aios_owner_authorization_broker_api.py`.
4. Verify all existing API routes still work.
5. Verify the broker remains localhost/reverse-proxy constrained as intended.
6. Re-run authentication and policy tests.
7. Remove the temporary staging instance.
This permanent bridge deployment does not authorize a V2R1C6 production tag,
Baseline update or application deployment.
## 9. Fail-closed conditions
Stop with `BLOCKED` for any of the following:
- Authority `main` no longer equals the fixed base OID.
- Target branch exists at a different OID.
- Only a wildcard Owner credential can authenticate.
- Exact credential scope cannot be proven.
- Owner identity is not explicitly allow-listed.
- Signer, validator or provisioner self-test fails.
- Grant is not marked one-time and consumed.
- Writer allows main, tag, Baseline, force-push or branch-delete operations.
- Audit event contains sensitive material.
- V2R1C5 tag or Baseline changes.
## 10. Required Agent result
```yaml
STATUS: READY_FOR_CODE_AUTHORING | BLOCKED | FAILED
PACKAGE:
manifest_verified:
unit_tests:
smoke_tests:
py_compile:
OWNER_CREDENTIAL:
account:
credential_id:
intake_scope:
exact_scope_proven:
wildcard_used: false
token_logged: false
BROKER:
staging_listener:
endpoint_status:
event_id:
authorization_reference:
authorization_digest_sha256:
event_consumed:
audit_permissions_valid:
GRANT:
approval_plan_id:
grant_digest_sha256:
signer_identity:
signature_valid:
one_time_use:
consumed:
BRANCH:
name:
oid:
base_oid:
ref_exists:
worktree_path:
worktree_clean:
WRITER:
status:
writes_enabled:
allowed_branch:
allowed_base_oid:
allowed_routes:
main_write_denied:
tag_write_denied:
baseline_write_denied:
force_push_denied:
POSTCHECK:
main_unchanged:
V2R1C5_tag_unchanged:
baseline_unchanged:
REPOSITORY_INTEGRATION:
commit_oids:
protected_pr:
merged:
permanent_bridge_deployed:
BLOCKERS:
GOVERNANCE:
force_push_used: false
tag_rewritten: false
direct_main_push: false
secrets_committed: false
owner_approval_respected: true
```
-562
View File
@@ -1,562 +0,0 @@
# Agent execution requirements: V2R1C6 task identity overlay
## Required result
The Agent must return one of:
- `READY_FOR_CODE_AUTHORING`
- `BLOCKED`
- `FAILED`
The Agent may not report success until the task branch, isolated worktree and
task writer are independently verified.
This stage is **not** a production deployment. It must not modify `main`,
V2R1C5, tags, Registry, Production Baseline or production services.
## Fixed authority
```yaml
task_id: V2R1C6_TASK_SCOPED_WRITER_PROVISIONING
repository: aios/aios
base_ref: refs/heads/main
base_oid: 875d41c3101093149414292067fd700dc214d4d3
target_branch: agent/v2r1c6-mainline-reconciliation-20260805
owner_identity: aiman
one_time_use: true
maximum_ttl_seconds: 900
production_change_authorized: false
tag_change_authorized: false
baseline_change_authorized: false
main_write_authorized: false
```
## 1. Verify package
Verify the externally supplied package against the published SHA-256 values and
then verify every entry in `MANIFEST.sha256`.
Do not synthesize a missing archive. Use either the verified ZIP or TAR.GZ.
Required checks:
```yaml
package_hash_match: true
manifest_entries_match: true
unit_tests_pass: true
smoke_tests_pass: true
py_compile_pass: true
```
## 2. Create root-owned staging
Extract to a fresh directory such as:
```text
/var/lib/aios-gitops/staging/v2r1c6-task-identity-overlay-<nonce>
```
Requirements:
```yaml
owner: root
group: root
directory_mode: "0700"
files_not_world_writable: true
symlinks_rejected: true
```
Do not overwrite the active runtime tree.
## 3. Re-run target-host tests
From the staging root:
```bash
python3 -m unittest discover -s tests -v
python3 scripts/smoke_authorized_task_writer_provisioning.py
python3 scripts/smoke_owner_authorization_broker.py
python3 scripts/smoke_task_identity_overlay.py
python3 -m py_compile \
active-runtime/app/authorized_task_writer_provisioning.py \
active-runtime/app/owner_authorization_broker.py \
active-runtime/app/task_identity_overlay.py \
active-runtime/app/aios_authorized_task_writer_api.py \
active-runtime/app/aios_owner_authorization_broker_api.py \
active-runtime/app/aios_task_identity_overlay_api.py \
active-runtime/app/aios_task_identity_overlay_cli.py \
scripts/call_task_identity_overlay.py
```
Any failure is `BLOCKED`; do not proceed.
## 4. Prestart Git and production freeze checks
Read from the authority repository:
```bash
git --git-dir=/data/aios-gitops/repo.git rev-parse refs/heads/main
git --git-dir=/data/aios-gitops/repo.git \
show-ref --verify --quiet \
refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
git --git-dir=/data/aios-gitops/repo.git rev-parse refs/tags/V2R1C5
git --git-dir=/data/aios-gitops/repo.git rev-parse 'refs/tags/V2R1C5^{}'
```
Expected:
```yaml
main_oid: 875d41c3101093149414292067fd700dc214d4d3
target_branch_exists: false
V2R1C5_tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
V2R1C5_peeled_target: aa60e891547abd7e215af7cf2bf3f21fd23541da
```
Read Baseline and require:
```yaml
version: V2R1C5
status: PRODUCTION_DEPLOYED
target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
evidence_closure_oid: 5e2cf8276f5093f3352b9a3252c701a44c5ab807
```
If `main` has drifted, return:
```yaml
STATUS: MANUAL_OWNER_REVIEW_REQUIRED
REASON: AUTHORIZED_BASE_OID_DRIFT
```
If the target branch already exists at another OID, return:
```yaml
STATUS: MANUAL_OWNER_REVIEW_REQUIRED
REASON: EXISTING_BRANCH_OID_CONFLICT
```
Do not delete, reset or force-push the branch.
## 5. Discover active secret references
The overlay does not read or modify `gitops.tokens`, but its issuer must compare
the new random secret against all active bearer secret references.
Discover references without printing secret contents. At minimum include the
currently active ChatGPT/GitOps bearer secret reference and any other bearer
files consumed by the active API.
Pass them as a comma-separated list in:
```text
AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS
```
Evidence must contain only paths and SHA-256 match counts, not token values.
If active secret references cannot be enumerated, return:
```yaml
STATUS: BLOCKED
REASON: ACTIVE_SECRET_REFERENCE_SET_UNRESOLVED
```
## 6. Install the transient systemd service
Use the provided template:
```text
deploy/systemd/aios-v2r1c6-task-identity-overlay.service.example
```
Substitute only:
- verified root-owned staging directory;
- verified active secret-ref path list.
Do not change task ID, repository, base OID, target branch or denied
capabilities.
The service must listen only on:
```text
127.0.0.1:19864
```
Before start, run:
```bash
systemd-analyze verify \
/etc/systemd/system/aios-v2r1c6-task-identity-overlay.service
```
Then:
```bash
sudo systemctl daemon-reload
sudo systemctl start aios-v2r1c6-task-identity-overlay.service
```
Confirm:
```bash
curl --fail --silent http://127.0.0.1:19864/healthz
```
Expected:
```yaml
service: aios-task-identity-overlay
status: active
global_token_registry_used: false
loopback_only: true
```
Do not expose the port externally.
## 7. Issue the one-time overlay
Use the root-only CLI. The authorization assertion is an audit label for this
already approved Owner task; it must not be described as an external signed
event.
Example:
```bash
sudo env \
PYTHONPATH=<staging>/active-runtime/app \
AIOS_GITOPS_IDENTITY_OVERLAY_STATE_ROOT=/var/lib/aios-gitops/task-identity-overlays \
AIOS_GITOPS_IDENTITY_OVERLAY_SECRET_ROOT=/run/aios-gitops/task-identity-overlays \
AIOS_GITOPS_IDENTITY_OVERLAY_TASK_ID=V2R1C6_TASK_SCOPED_WRITER_PROVISIONING \
AIOS_GITOPS_IDENTITY_OVERLAY_OWNER_IDENTITY=aiman \
AIOS_GITOPS_IDENTITY_OVERLAY_BASE_REF=refs/heads/main \
AIOS_GITOPS_IDENTITY_OVERLAY_BASE_OID=875d41c3101093149414292067fd700dc214d4d3 \
AIOS_GITOPS_IDENTITY_OVERLAY_TARGET_BRANCH=agent/v2r1c6-mainline-reconciliation-20260805 \
AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS=<verified-secret-ref-list> \
python3 <staging>/active-runtime/app/aios_task_identity_overlay_cli.py \
issue \
--ttl-seconds 900 \
--authorization-assertion-ref owner-session:V2R1C6:approved-20260806 \
--operator-identity <audited-agent-identity> \
--result-json /run/aios-gitops/v2r1c6-overlay-issued.json
```
The result must contain:
```yaml
status: OVERLAY_ACTIVE
idempotent: false
overlay_id: present
secret_ref: under /run/aios-gitops/task-identity-overlays
expires_at: within 900 seconds
```
It must not contain a token.
Verify:
```yaml
secret_file_mode: "0600"
metadata_contains_raw_token: false
token_hash_unique_across_configured_secret_refs: true
```
## 8. Negative tests
Before the valid request:
1. No Authorization header must return HTTP 401.
2. A random wrong bearer must return HTTP 401.
3. A request with `production_change_authorized=true` must be rejected.
4. A request with another base OID or branch must be rejected.
5. The valid overlay must remain `ACTIVE` after pre-auth request-validation
failures.
Do not use the global Owner or ChatGPT bearer credential for this endpoint.
## 9. Send the valid request
Use:
```text
deploy/v2r1c6-task-writer-authorization-request.json
```
Use the provided client so the bearer is not placed in argv, shell history or
logs:
```bash
sudo python3 <staging>/scripts/call_task_identity_overlay.py \
--secret-ref <secret_ref_from_issue_result> \
--request-json <staging>/deploy/v2r1c6-task-writer-authorization-request.json \
--url http://127.0.0.1:19864/api/governance/owner-authorizations \
--result-json /var/lib/aios-gitops/v2r1c6-overlay-provision-result.json
```
Required result:
```yaml
http_status: 200
status: READY_FOR_CODE_AUTHORING
owner_authorization_consumed: true
grant_consumed: true
identity_overlay_consumed: true
identity_overlay_secret_removed: true
branch: agent/v2r1c6-mainline-reconciliation-20260805
branch_oid: 875d41c3101093149414292067fd700dc214d4d3
```
The result must not include:
- bearer token;
- private key;
- signer secret;
- raw signature;
- credential value.
## 10. Independent acceptance
Verify the branch:
```bash
git --git-dir=/data/aios-gitops/repo.git \
rev-parse refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
```
It must equal:
```text
875d41c3101093149414292067fd700dc214d4d3
```
Verify worktree evidence returned by the provisioner:
```yaml
branch: agent/v2r1c6-mainline-reconciliation-20260805
head_oid: 875d41c3101093149414292067fd700dc214d4d3
detached_head: false
worktree_clean: true
writer_status: active
writes_enabled: true
allowed_routes:
- POST /api/commit-file
allow_main_write: false
allow_tag_write: false
allow_baseline_write: false
allow_force_push: false
```
Run policy negative tests against the task writer and require explicit policy
denials for:
- main write;
- another branch;
- tag create/update;
- Baseline write;
- force push;
- branch delete.
Network errors, 404, 500 and stopped-service failures do not count as policy
denials.
## 11. Stop and clean up the overlay
After `READY_FOR_CODE_AUTHORING`:
```bash
sudo systemctl stop aios-v2r1c6-task-identity-overlay.service
sudo systemctl disable aios-v2r1c6-task-identity-overlay.service || true
sudo rm -f /etc/systemd/system/aios-v2r1c6-task-identity-overlay.service
sudo systemctl daemon-reload
```
Verify:
```yaml
overlay_listener_stopped: true
overlay_secret_removed: true
global_token_registry_unchanged: true
disabled_owner_record_unchanged: true
active_chatgpt_bot_record_unchanged: true
audit_metadata_retained: true
```
The root-owned staging directory may be retained until the code is committed,
then removed after checksum-confirmed repository integration.
## 12. Commit the developed code through the task writer
Only after writer acceptance, use `POST /api/commit-file` to commit the
verified package contents to:
```text
agent/v2r1c6-mainline-reconciliation-20260805
```
Commit in reviewable groups:
1. authorized task-writer bridge;
2. Owner authorization broker;
3. task identity overlay;
4. tests, docs, OpenAPI and deployment templates.
Do not include:
- package archives;
- runtime tokens;
- `/run` or `/var/lib` state;
- test result files containing host paths;
- secret references containing values;
- generated credentials.
Run the complete 40-test suite from the branch after commits.
## 13. Protected PR
Create a protected PR from:
```text
agent/v2r1c6-mainline-reconciliation-20260805
```
to:
```text
main
```
The PR must state:
- this change closes Owner authorization bootstrap gaps;
- the overlay is localhost-only and one-time;
- the global token registry is not modified;
- no production release is authorized;
- V2R1C5 tag and Baseline remain unchanged.
The Agent must not merge without the repository's normal protected review.
## 14. Required Agent report
```yaml
STATUS:
READY_FOR_CODE_AUTHORING | BLOCKED | FAILED
PACKAGE:
zip_sha256:
tar_gz_sha256:
manifest_entries:
manifest_matched:
TESTS:
unit_total:
unit_passed:
smoke_bridge:
smoke_broker:
smoke_overlay:
py_compile:
PRESTART:
main_oid:
target_branch_previously_absent:
V2R1C5_tag_oid:
V2R1C5_peeled_target:
baseline_unchanged:
OVERLAY:
overlay_id:
status:
secret_ref:
secret_file_mode:
token_hash_unique:
global_registry_used: false
listener:
expires_at:
consumed:
secret_removed:
NEGATIVE_TESTS:
unauthenticated_401:
wrong_token_401:
production_capability_denied:
wrong_base_denied:
wrong_branch_denied:
OWNER_AUTHORIZATION:
event_id:
reference:
consumed:
GRANT:
approval_plan_id:
grant_id:
grant_digest_sha256:
signature_valid:
consumed:
BRANCH:
name:
oid:
base_oid:
ref_exists:
worktree_path:
worktree_clean:
WRITER:
status:
writes_enabled:
allowed_branch:
allowed_base_oid:
allowed_routes:
main_write_denied:
tag_write_denied:
baseline_write_denied:
force_push_denied:
REPOSITORY_INTEGRATION:
commits:
tests_after_commit:
protected_pr:
merged: false
POSTCHECK:
main_unchanged:
V2R1C5_tag_unchanged:
baseline_unchanged:
global_token_registry_unchanged:
overlay_listener_stopped:
overlay_secret_removed:
SECRETS:
plaintext_logged: false
plaintext_committed: false
BLOCKERS:
GOVERNANCE:
force_push_used: false
tag_rewritten: false
direct_main_push: false
secrets_committed: false
owner_approval_respected: true
```
## Fail-closed conditions
Return `BLOCKED` and stop on any of:
- authoritative main OID drift;
- target branch conflict;
- inability to enumerate active secret references;
- secret hash collision;
- overlay not bound to the exact task/base/branch;
- non-loopback listener;
- raw secret in result or logs;
- Owner Authorization Broker event not created;
- signer/validator/provisioner failure;
- grant not consumed;
- branch OID mismatch;
- writer exposes main/tag/Baseline/force-push permissions;
- V2R1C5 tag or Baseline changes.
Never fall back to the shared `gitops-write-bot.token`, a wildcard credential,
manual `gitops.tokens` edits or a direct Git ref write.
+394
View File
@@ -0,0 +1,394 @@
# AIOS V2R1 生产候选报告
- 候选版本:`V2R1C0-candidate`
- 候选分支:`integration/aios-v2r1-cto-production-candidate-20260720`
- 报告语言:中文
- 架构负责人:AIOS V2 CTO
- 当前状态:代码与文档闭环已完成;精确 Commit 测试、真实 Runtime 联调和预发布验证待执行
## 1. 执行摘要
AIOS V2R1 Candidate 已完成以下核心建设:
- AIOS 唯一控制面;
- Tenant、Agent、Task、Policy、Approval 和 Audit 权威模型;
- SQLite WAL 候选持久化与重启恢复;
- Execution Orchestrator;
- OpenClaw 签名命令、Adapter、Transport、Health 和 Audit Callback;
- Hermes tenant/cell/profile 分区、Queue、Worker Lease、Heartbeat 和 Result;
- Agent Execution Evidence;
- Web Console;
- GitOps Status;
- systemd、Docker、Compose、Healthcheck 和 Rollback;
- 用户需求、目标架构、迁移策略、模块契约、追踪矩阵、文档治理和 ADR;
- 文档—代码匹配检查器;
- 全量验收入口;
- OpenClaw 预发布部署任务;
- Codex 架构监督和阻塞处理任务。
本报告不把“代码已提交”误报为“生产已验证”。以下状态仍需由无人值守任务执行并形成证据:
- 精确 Candidate Commit 的自动测试;
- 真实 OpenClaw Runtime 联调;
- Hermes 长任务、Worker 崩溃和 Lease Recovery 演练;
- systemd/Docker 预发布部署;
- 重启恢复与回滚演练;
- 本地仓库与 Gitea Candidate OID 一致验证;
- 身份代理、安全、性能和容量验证;
- Owner 对合并、Tag 和生产部署的批准。
## 2. 用户需求完成情况
| 需求 | 代码状态 | 测试状态 | 预发布状态 |
|---|---|---|---|
| AIOS 唯一控制面 | 已实现 | 已定义 | 待验证 |
| 复用 OpenClaw/Hermes/Agent Router | 已实现 | 已定义 | 待真实联调 |
| OpenClaw/Hermes 解耦 | 已实现 | 已定义 | 待旁路冻结验证 |
| 全面 Web 化 | 候选实现完成 | 冒烟测试待执行 | 待反向代理接入 |
| 多租户隔离 | 已实现 | Domain 负向测试已定义 | 待全链路安全验证 |
| 统一任务生命周期 | 已实现 | 已定义 | 待精确 Commit 执行 |
| 策略与审批 | 已实现 | 部分测试待补强 | 待身份代理验证 |
| 幂等与并发安全 | 已实现 | 基础测试已定义 | 待压力和并发验证 |
| 执行证据 | 已实现 | 已定义 | 待外部执行器持续回写 |
| GitOps 唯一流程 | 已实现并修复 Controller 冲突 | Runtime 部分验证 | 待双目标持续镜像证据 |
| 持久化与恢复 | 已实现 | 重启恢复测试已定义 | 待预发布演练 |
| 审计与合规 | 已实现候选 | 部分断言待补强 | 待不可变外部固化 |
| 密钥安全 | 引用模式已实现 | 基础扫描纳入验收 | 待 Secret Provider 联调 |
| 部署与回滚 | 资产完成 | 验收脚本已定义 | 待实际演练 |
| 文档与代码同步 | 已实现门禁 | 待精确 Commit 执行 | 待接入持续验收 |
## 3. 目标架构
```text
浏览器 / Web Console / Public API
|
v
AIOS 唯一控制面
Tenant | Agent | Task | Policy | Approval | Audit
|
v
Execution Orchestrator / Agent Router
/ \
v v
OpenClaw Adapter Hermes Boundary
| |
v v
OpenClaw Runtime Hermes Workers
\ /
v v
结果回写 + 审计 + 执行证据
```
权威边界:
- AIOS:租户、身份、Agent、任务、策略、审批、审计和执行证据;
- Agent Router:调度与路由;
- OpenClaw:签名命令执行、取消、状态、健康和回调;
- Hermes:队列、Worker、Lease、Heartbeat 和结果;
- Web:只调用 AIOS API;
- GitOps:所有代码、文档、部署和修复变更入口。
## 4. 实现清单
### 4.1 Control Plane
```text
active-runtime/app/aios_v2/control_plane.py
active-runtime/app/aios_v2/persistence.py
active-runtime/app/aios_v2/gateway.py
active-runtime/app/aios_v2/app.py
active-runtime/app/aios_v2/orchestrator.py
active-runtime/app/aios_v2/execution_tracking.py
active-runtime/app/aios_v2/gitops_status.py
active-runtime/app/aios_v2/console.py
```
### 4.2 OpenClaw
```text
active-runtime/app/aios_openclaw/contract.py
active-runtime/app/aios_openclaw/adapter.py
active-runtime/app/aios_openclaw/transport.py
active-runtime/app/aios_openclaw/health.py
active-runtime/app/aios_openclaw/callback.py
```
### 4.3 Hermes
```text
active-runtime/app/aios_hermes/contract.py
active-runtime/app/aios_hermes/adapter.py
active-runtime/app/aios_hermes/worker.py
```
### 4.4 测试与治理
```text
tests/aios_v2_candidate/test_candidate_flow.py
tools/aios_v2_docs_code_check.py
deploy/aios-v2/acceptance/run-all.sh
```
### 4.5 部署
```text
deploy/aios-v2/systemd/aios-v2.service
deploy/aios-v2/env/aios-v2.env.example
deploy/aios-v2/docker/Dockerfile
deploy/aios-v2/docker/compose.yaml
deploy/aios-v2/healthcheck/check.sh
deploy/aios-v2/rollback/rollback.sh
```
### 4.6 文档与任务
```text
docs/aios-v2/
tasks/openclaw-production-deploy-task.yaml
tasks/codex-production-supervisor-task.yaml
```
## 5. 多租户与安全设计
### 5.1 权威上下文
```text
tenant_id
principal_id
session_id
trace_id
task_id
policy_version
idempotency_key
```
### 5.2 OpenClaw
- HMAC-SHA256 规范化签名;
- 签名绑定 tenant/task/trace/action/payload/idempotency/time;
- Runtime Result 身份字段必须匹配;
- Secret 只通过受保护文件或引用解析。
### 5.3 Hermes
- tenant/cell/profile 分区;
- Worker Lease 与 Heartbeat;
- revision 乐观并发;
- Lease 过期自动回到 QUEUED;
- Worker 完成前停止 Heartbeat,避免 revision 竞态。
### 5.4 Web/API
- 可信代理注入 `X-AIOS-*`;
- 浏览器不选择租户;
- CSP、no-store、nosniff、DENY frame、no-referrer;
- 应用端口不得直接暴露公网。
### 5.5 GitOps
- Controller detached + clean;
- 禁止 fetch 到已检出本地分支;
- 所有修改进入分支、Commit、测试和统一审查;
- 明文 Secret 门禁;
- 文档变化与代码变化同步门禁。
## 6. 持久化策略
候选版:
- SQLite WAL;
- `synchronous=FULL`;
- `BEGIN IMMEDIATE`;
- 原子 Snapshot;
- schema version;
- 状态目录与代码版本分离;
- Execution Evidence 租户分区原子 JSON。
已知限制:
- 单进程候选优先;
- 尚无生产 RLS、多实例 HA、PITR 和容量证据;
- 后续必须通过新 ADR 迁移到生产数据层。
## 7. 测试与验收
### 7.1 文档—代码匹配
```bash
python3 tools/aios_v2_docs_code_check.py \
--base-ref integration/aios-v2r1-cto-production-candidate-20260720
```
### 7.2 候选测试
```bash
python3 -m unittest discover \
-s tests/aios_v2_candidate \
-p 'test_*.py' \
-v
```
覆盖:
- OpenClaw 完整执行;
- Hermes 完整执行;
- 跨租户拒绝;
- SQLite 重启恢复;
- Execution Evidence。
### 7.3 全量验收
```bash
bash deploy/aios-v2/acceptance/run-all.sh
```
可选在线健康检查:
```bash
AIOS_RUN_LIVE_HEALTHCHECK=1 \
bash deploy/aios-v2/acceptance/run-all.sh
```
### 7.4 健康检查
```bash
bash deploy/aios-v2/healthcheck/check.sh
```
检查:
- systemd;
- HTTP Health;
- 状态目录;
- SQLite quick_check;
- GitOps detached/clean;
- Evidence 文件。
## 8. 部署方式
### 8.1 systemd
```text
/etc/aios-v2/aios-v2.env
/opt/aios/current
/var/lib/aios-v2
```
运行用户:`aios`。
安全选项:NoNewPrivileges、ProtectSystem、ProtectHome、PrivateTmp、Capability 限制。
### 8.2 Docker/Compose
- 非 root UID/GID;
- Read-only root filesystem;
- Drop all capabilities;
- 独立 State Volume;
- HTTP Healthcheck;
- Secret 文件只读挂载。
## 9. 回滚
```bash
bash deploy/aios-v2/rollback/rollback.sh <previous-release>
```
原则:
- 回滚前自动备份状态;
- 默认只回滚代码;
- 状态恢复需显式 `AIOS_V2_RESTORE_STATE=1`;
- 回滚后必须执行健康检查;
- 目标版本失败时自动恢复 previous release。
## 10. 无人值守任务
### OpenClaw
任务:`tasks/openclaw-production-deploy-task.yaml`
职责:
- 预发布部署;
- 健康检查;
- Runtime 联调;
- 小型部署与 Runtime Bug 修复;
- 证据回写。
### Codex
任务:`tasks/codex-production-supervisor-task.yaml`
职责:
- 架构监督;
- 阻塞分析和跨模块修复;
- 文档、测试和安全审查;
- 发布建议。
所有修改和阻塞必须进入 Git 和 Execution Evidence,最终统一审视。
## 11. 当前已知风险
| 风险 | 级别 | 当前处理 |
|---|---|---|
| 精确 Candidate Commit 尚未执行全部测试 | 高 | OpenClaw 执行,Codex 监督 |
| 真实 OpenClaw Runtime 尚未联调 | 高 | 预发布任务必须完成 |
| 候选 SQLite 不支持生产多实例 HA | 高 | Candidate 单实例;后续 ADR 迁移 |
| 身份代理 Header 防伪尚未环境验证 | 高 | 预发布安全检查 |
| Web Console 为候选只读 MVP | 中 | 生产前补充 E2E 与安全验证 |
| 部分策略负向、并发和审计测试待补强 | 中 | Codex 登记 blocker 并补充 |
| 本地/Gitea 自动双写状态仍需持续证明 | 中 | 验收记录 OID |
| 性能、容量和 SLO 尚无实测 | 中 | 预发布基准测试 |
| 外部不可变审计固化尚未实现 | 中 | 生产数据层阶段完成 |
## 12. 发布 Gate
### 允许进入预发布
条件:
- Candidate 分支可见;
- 文档完整;
- 任务合同已生成;
- 部署资产存在;
- 无明文 Secret。
### 允许进入合并审查
条件:
- 全量验收通过;
- OpenClaw/Hermes 联调通过;
- 重启和回滚通过;
- 严重/高风险 blocker 已解决;
- Codex 输出正向监督结论;
- 本地/Gitea OID 一致。
### 允许生产发布
条件:
- Owner 明确批准;
- Merge 与 Tag 通过受控流程;
- 生产配置、Secret 和反向代理准备完成;
- 发布窗口、观察指标、回滚负责人明确。
## 13. 当前结论
**代码和正式中文文档已经达到候选闭环状态,但尚不能宣称生产已验证。**
下一阶段由 OpenClaw 执行预发布部署和低风险 Bug 修复,由 Codex 监督架构、文档、安全、测试和阻塞。所有过程证据统一进入系统,满足 Gate 后再请求 Owner 批准。
## 14. 更新规则
以下变化必须更新本报告:
- Candidate Commit;
- 测试结果;
- 部署和回滚结果;
- Blocker 和风险;
- OpenClaw/Codex 任务状态;
- 文档—代码匹配状态;
- Gitea OID;
- 发布建议和 Owner 决策。
-9
View File
@@ -1,9 +0,0 @@
{
"schema": "aios.v2r1c7.engineering-build-receipt.v1",
"version": "V2R1C7",
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
"production_deployed": false,
"tag_created": false,
"baseline_changed": false,
"deployable": true
}
-21
View File
@@ -1,21 +0,0 @@
bec7da1adb75bf5feaa63c60ec6d03adf15d7508742a486207033d000f72d4ff AGENT_DEPLOYMENT_REQUIREMENTS.md
251b46e51ce09aa2fd843bee48ef24a5608f31d24c599841f086ceb25c1a1715 README.md
783f68ec4f00a11d43ceed536196bfedccaf6614f6db9805f7b36d24e5fa7bc2 TEST_RESULTS.json
9208325e5b2e6f5ba6b64e000bc21d14d9f2f214e3ad33dd79857e85218fd6c2 VERSION
c4b24b65195077716983acdc8611f705324413a74453858994ba51541957afd4 active-runtime/app/aios_authorized_task_writer_api.py
8a1935fa098d47ac02ce51bbd9fd5fad7cf9ab9374ea3de5f630679b96681d84 active-runtime/app/authorized_task_writer_provisioning.py
0997ab8332985c7124f334c4e49735d8833326578acc61a61a389bf792424de3 active-runtime/app/deployment_state.py
dd3f8b1cab88e005a291f077807873c381e8ca92fe863dc05817925f0dff9613 active-runtime/app/v2r1c6_privacy_tools.py
9e45025c41068cafdea2f360edfc4146a5459b0a6bcfbf1403206fc0161e7bac active-runtime/app/v2r1c7_health_service.py
c96ba560d69f6c2eb700b9f2b42a32e394dadd00dffb23bbefdb9fb3e1571c0d deploy/aios-v2/systemd/aios-v2r1c7-production.service
a05d4e8038fdb866ce17d4fb8a54798a268a1e8d44ff2cbc559069fa6ab2a667 deploy/systemd/aios-gitops-model-api-authorized-writer-bridge.conf.example
a60dac51802bff67c2cfff61acdeb0a3a05d74a4f6cb47e3f78295ceb5c6acd9 docs/gitops/authorized-task-writer-provisioning.md
2690f76452bceddda90c728278c600bcebd533db24776e2966423c5a8655e4b5 docs/gitops/openapi-authorized-task-writer-fragment.yaml
560e5d9af8a8e2ec60ec31cf8659a7982b93ea09b7adb486b738ea2fba460d70 release-manifests/V2R1C7.json
81a423c7814f81cb888b60a538c153e549b2be9099c881c968d55ae84efddc57 scripts/build_v2r1c7_release_artifact.py
64c78dabffb05c8851f87b2a27c43bd1f21728dca586397d03a6bc22906a906c scripts/probe_v2r1c7_health.py
5419dc7de0f50908ad0ef1dac74172fce811bb7b45f314a6e1c1bb9ce618324d scripts/rollback_v2r1c7_release.sh
54e2845d2c3c026323ad45515e082d661e3b461feb3bfefedf5adc52db2d7b78 scripts/smoke_authorized_task_writer_provisioning.py
32a860e28d5afc105f413d878a13fb8600fa74874dbd3bc4c031ef3efa17896a scripts/verify_v2r1c7_release.py
3cce2b744326fe12b89bcad3cd352c77738950964bf8519e72cb5e9c7a5dfbfc tests/test_authorized_task_writer_provisioning.py
8f3153c625241f60b65109eeac045e8aed7661d95f3a10f0b32ba3e0e9ac0a85 versions/V2R1C7.json
-25
View File
@@ -1,25 +0,0 @@
{
"git_branch_created": false,
"git_commit_created": false,
"production_mutation": false,
"py_compile": {
"files": [
"active-runtime/app/authorized_task_writer_provisioning.py",
"active-runtime/app/aios_authorized_task_writer_api.py"
],
"status": "PASS"
},
"schema": "aios.authorized-task-writer-bridge-test-report.v1",
"secrets_included": false,
"smoke": {
"command": "python3 scripts/smoke_authorized_task_writer_provisioning.py",
"status": "PASS"
},
"status": "PASS",
"unit_tests": {
"command": "python3 -m unittest -v tests/test_authorized_task_writer_provisioning.py",
"errors": 0,
"failures": 0,
"total": 9
}
}
+1 -1
View File
@@ -1 +1 @@
V2R1C7
V1R4C6
-201
View File
@@ -1,201 +0,0 @@
"""Fail-closed Agent Pull Runner primitives for AIOS autonomy v1.
The runner owns task discovery and reporting only. It never accepts shell
strings: executors receive argv, an allowlisted cwd and an allowlisted env.
Durable state is intentionally small so restart recovery and duplicate
suppression can be verified without coupling the runner to one database.
"""
from __future__ import annotations
import json
import os
import random
import shutil
import subprocess
import tempfile
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Mapping, Protocol, Sequence
class RunnerError(RuntimeError):
"""Raised when a runner contract would be violated."""
@dataclass(frozen=True)
class RunnerConfig:
worker_id: str
capabilities: tuple[str, ...] = ()
long_poll_seconds: int = 30
fallback_seconds: int = 60
jitter_seconds: int = 5
max_backoff_seconds: int = 300
executable_allowlist: tuple[str, ...] = ()
working_directory_allowlist: tuple[str, ...] = ()
environment_allowlist: tuple[str, ...] = ()
@dataclass
class RunnerState:
inflight: dict[str, dict[str, Any]] = field(default_factory=dict)
terminal: dict[str, dict[str, Any]] = field(default_factory=dict)
class RunnerTransport(Protocol):
def register_worker(self, worker_id: str, capabilities: Sequence[str]) -> Mapping[str, Any]: ...
def heartbeat(self, worker_id: str) -> Mapping[str, Any]: ...
def long_poll(self, worker_id: str, timeout_seconds: int) -> Mapping[str, Any] | None: ...
def poll(self, worker_id: str) -> Mapping[str, Any] | None: ...
def claim(self, worker_id: str, task: Mapping[str, Any]) -> Mapping[str, Any]: ...
def renew(self, worker_id: str, task_id: str) -> Mapping[str, Any]: ...
def report_progress(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
def report_result(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
def report_evidence(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
class JsonStateStore:
"""Atomic JSON state store for restart recovery."""
def __init__(self, path: str | os.PathLike[str]) -> None:
self.path = Path(path)
def load(self) -> RunnerState:
if not self.path.exists():
return RunnerState()
value = json.loads(self.path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise RunnerError("runner state must be an object")
return RunnerState(
inflight=dict(value.get("inflight", {})),
terminal=dict(value.get("terminal", {})),
)
def save(self, state: RunnerState) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=self.path.name + ".", dir=self.path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump({"inflight": state.inflight, "terminal": state.terminal}, handle, sort_keys=True)
handle.write("\n")
os.chmod(name, 0o600)
os.replace(name, self.path)
finally:
if os.path.exists(name):
os.unlink(name)
class AgentPullRunner:
"""Pull, claim, execute and report one task at a time."""
def __init__(
self,
config: RunnerConfig,
transport: RunnerTransport,
state_store: JsonStateStore | None = None,
*,
random_fn: Callable[[], float] = random.random,
) -> None:
if not config.worker_id.strip():
raise RunnerError("worker_id is required")
self.config = config
self.transport = transport
self.state_store = state_store
self.random_fn = random_fn
self.state = state_store.load() if state_store else RunnerState()
def _save(self) -> None:
if self.state_store:
self.state_store.save(self.state)
def register(self) -> Mapping[str, Any]:
return self.transport.register_worker(self.config.worker_id, self.config.capabilities)
def heartbeat(self) -> Mapping[str, Any]:
return self.transport.heartbeat(self.config.worker_id)
def discovery_delay(self, failures: int) -> float:
failures = max(0, int(failures))
base = min(self.config.max_backoff_seconds, self.config.fallback_seconds * (2**failures))
jitter = self.random_fn() * max(0, self.config.jitter_seconds)
return min(self.config.max_backoff_seconds, base + jitter)
def discover_once(self) -> Mapping[str, Any] | None:
try:
task = self.transport.long_poll(self.config.worker_id, self.config.long_poll_seconds)
except Exception:
task = None
if task is None:
task = self.transport.poll(self.config.worker_id)
if not task:
return None
task_id = str(task.get("task_id", ""))
if not task_id:
raise RunnerError("task_id is required")
if task_id in self.state.terminal:
return {"status": "duplicate_suppressed", "task_id": task_id}
claimed = dict(self.transport.claim(self.config.worker_id, task))
self.state.inflight[task_id] = claimed
self._save()
return claimed
def renew(self, task_id: str) -> Mapping[str, Any]:
if task_id not in self.state.inflight:
raise RunnerError("task is not inflight")
return self.transport.renew(self.config.worker_id, task_id)
def report_progress(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
if task_id not in self.state.inflight:
raise RunnerError("task is not inflight")
return self.transport.report_progress(self.config.worker_id, task_id, payload)
def report_evidence(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
if task_id not in self.state.inflight:
raise RunnerError("task is not inflight")
return self.transport.report_evidence(self.config.worker_id, task_id, payload)
def report_result(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
if task_id in self.state.terminal:
return {"status": "duplicate_terminal_suppressed", "task_id": task_id}
if task_id not in self.state.inflight:
raise RunnerError("task is not inflight")
result = dict(self.transport.report_result(self.config.worker_id, task_id, payload))
self.state.terminal[task_id] = {"result": result, "reported_at": time.time()}
self.state.inflight.pop(task_id, None)
self._save()
return result
def recover_after_restart(self) -> list[Mapping[str, Any]]:
recovered: list[Mapping[str, Any]] = []
for task_id in tuple(self.state.inflight):
recovered.append(self.renew(task_id))
return recovered
def validate_execution(self, argv: Sequence[str], cwd: str | os.PathLike[str], env: Mapping[str, str]) -> None:
if not argv or any(not isinstance(item, str) or not item for item in argv):
raise RunnerError("argv_only_execution_required")
if any(ch in argv[0] for ch in (";", "|", "&", "`", "\n")):
raise RunnerError("shell_syntax_denied")
executable = shutil.which(argv[0])
if not executable:
raise RunnerError("executable_not_found")
if self.config.executable_allowlist and not any(
os.path.realpath(executable).startswith(os.path.realpath(prefix).rstrip(os.sep) + os.sep)
or os.path.realpath(executable) == os.path.realpath(prefix)
for prefix in self.config.executable_allowlist
):
raise RunnerError("executable_not_allowlisted")
real_cwd = os.path.realpath(os.fspath(cwd))
if self.config.working_directory_allowlist and not any(
real_cwd == os.path.realpath(prefix)
or real_cwd.startswith(os.path.realpath(prefix).rstrip(os.sep) + os.sep)
for prefix in self.config.working_directory_allowlist
):
raise RunnerError("working_directory_not_allowlisted")
if self.config.environment_allowlist and any(key not in self.config.environment_allowlist for key in env):
raise RunnerError("environment_key_not_allowlisted")
def execute_argv(self, argv: Sequence[str], cwd: str | os.PathLike[str], env: Mapping[str, str]) -> Mapping[str, Any]:
self.validate_execution(argv, cwd, env)
completed = subprocess.run(list(argv), cwd=os.fspath(cwd), env=dict(env), shell=False, text=True, capture_output=True, check=False)
return {"returncode": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
@@ -1,36 +0,0 @@
"""Small operational CLI for the Agent Pull Runner contract."""
from __future__ import annotations
import argparse
import json
from agent_pull_runner import AgentPullRunner, RunnerConfig
class _NoopTransport:
def register_worker(self, worker_id, capabilities): return {"status": "registered", "worker_id": worker_id, "capabilities": list(capabilities)}
def heartbeat(self, worker_id): return {"status": "alive", "worker_id": worker_id}
def long_poll(self, worker_id, timeout_seconds): return None
def poll(self, worker_id): return None
def claim(self, worker_id, task): return task
def renew(self, worker_id, task_id): return {"status": "renewed", "task_id": task_id}
def report_progress(self, worker_id, task_id, payload): return {"status": "progress_recorded", "task_id": task_id}
def report_result(self, worker_id, task_id, payload): return {"status": "result_recorded", "task_id": task_id}
def report_evidence(self, worker_id, task_id, payload): return {"status": "evidence_recorded", "task_id": task_id}
def main() -> int:
parser = argparse.ArgumentParser(description="AIOS Agent Pull Runner")
parser.add_argument("--worker-id", default="self-test-worker")
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
runner = AgentPullRunner(RunnerConfig(worker_id=args.worker_id, fallback_seconds=60), _NoopTransport())
if args.self_test:
print(json.dumps({"status": "PASS", "registration": runner.register(), "fallback_seconds": runner.config.fallback_seconds}, sort_keys=True))
return 0
print(json.dumps({"status": "READY", "worker_id": args.worker_id}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,173 +0,0 @@
"""Fail-closed Authority Git side-effect boundary.
Only this module may issue the narrowly scoped ref CAS primitives used by the
durable executor. It never accepts force updates, arbitrary refs, tags, or
direct production mutations.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
import re
import subprocess
import tempfile
from typing import Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_BRANCH_RE = re.compile(r"^refs/heads/agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class AuthorityGitError(RuntimeError):
pass
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise AuthorityGitError(f"{field}_INVALID")
return value
@dataclass(frozen=True, slots=True)
class AuthorityGitExecutor:
git_dir: Path
worktree_root: Path = Path("/var/lib/aios-gitops/worktrees")
def __post_init__(self) -> None:
if not self.git_dir.is_absolute() or self.git_dir.is_symlink():
raise AuthorityGitError("AUTHORITY_GIT_DIR_INVALID")
if not self.worktree_root.is_absolute() or self.worktree_root.is_symlink():
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
def _git(self, args: Sequence[str], *, check: bool = True) -> str:
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", *args],
text=True,
capture_output=True,
check=False,
)
if check and result.returncode != 0:
raise AuthorityGitError("AUTHORITY_GIT_COMMAND_FAILED")
return result.stdout.strip()
def read_ref(self, ref: str) -> str | None:
if not ref.startswith("refs/") or ref.startswith(("refs/tags/", "refs/remotes/")):
raise AuthorityGitError("REF_READ_SCOPE_DENIED")
value = self._git(["rev-parse", "--verify", ref], check=False)
return value if value and OID_RE.fullmatch(value) else None
def commit_exists(self, oid: str) -> bool:
_oid(oid, "commit_oid")
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", oid + "^{commit}"], capture_output=True, check=False)
return result.returncode == 0
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
_oid(older_oid, "older_oid")
_oid(newer_oid, "newer_oid")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", older_oid, newer_oid],
capture_output=True,
check=False,
)
if result.returncode not in (0, 1):
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
return result.returncode == 0
def create_detached_worktree(
self,
base_oid: str,
worktree_name: str,
root: Path | None = None,
) -> Path:
_oid(base_oid, "base_oid")
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
raise AuthorityGitError("WORKTREE_NAME_INVALID")
parent = root or self.worktree_root
if not parent.is_absolute() or parent.is_symlink():
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
path = parent / worktree_name
if path.exists() or path.is_symlink():
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
parent.mkdir(parents=True, exist_ok=True)
self._git(["worktree", "add", "--detach", str(path), base_oid])
return path
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current != expected_oid:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
if not self.ancestry_check(expected_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, expected_oid])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CAS_READBACK_MISMATCH")
return readback
def ff_CAS_main_after_verified_protected_merge(self, expected_oid: str, new_oid: str) -> str:
_oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid")
if not self.ancestry_check(expected_oid, new_oid):
raise AuthorityGitError("MAIN_FF_REQUIRED")
current = self.read_ref("refs/heads/main")
if current != expected_oid:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
self._git(["update-ref", "refs/heads/main", new_oid, expected_oid])
readback = self.read_ref("refs/heads/main")
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
return readback
def commit_file_in_isolated_worktree(
self,
*,
base_oid: str,
files: Mapping[str, bytes],
message: str,
worktree_name: str,
) -> str:
_oid(base_oid, "base_oid")
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
raise AuthorityGitError("WORKTREE_NAME_INVALID")
root = self.worktree_root / worktree_name
if root.exists() or root.is_symlink():
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
self.worktree_root.mkdir(parents=True, exist_ok=True)
self._git(["worktree", "add", "--detach", str(root), base_oid])
try:
for relative, content in files.items():
path = Path(relative)
if path.is_absolute() or ".." in path.parts or not path.parts or path.parts[0] == ".git":
raise AuthorityGitError("FILE_PATH_INVALID")
target = root / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
result = subprocess.run(
["git", "-C", str(root), "add", "--", *files.keys()],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AuthorityGitError("WORKTREE_ADD_FAILED")
result = subprocess.run(
["git", "-C", str(root), "-c", "user.name=aios-durable-job", "-c", "user.email=aios-durable-job@localhost", "commit", "--no-verify", "-m", message],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AuthorityGitError("WORKTREE_COMMIT_FAILED")
return subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True).strip()
finally:
subprocess.run(["git", f"--git-dir={self.git_dir}", "worktree", "remove", "--force", str(root)], capture_output=True, check=False)
@@ -1,73 +0,0 @@
"""HTTP adapter for Owner-authorized task writer provisioning.
Run this module instead of aios_chatgpt_actions_api.py. All existing routes are
delegated unchanged; only POST /api/task-writer/provision-authorized is added.
"""
from __future__ import annotations
import os
import urllib.parse
import aios_chatgpt_actions_api as api
from authorized_task_writer_provisioning import (
AuthorizedTaskWriterProvisioner,
OwnerContext,
ProvisioningError,
)
_original_do_post = api.H.do_POST
_ENDPOINT = "/api/task-writer/provision-authorized"
def _owner_authorization(handler):
scope = os.getenv(
"AIOS_GITOPS_OWNER_PROVISION_SCOPE", "writer:provision"
)
auth = handler.need(scope)
if not auth:
return None
if auth.get("role") != "owner":
handler.sendj({"error": "OWNER_AUTHORIZATION_REQUIRED"}, 403)
return None
identity = str(auth.get("account") or "").strip()
if not identity:
handler.sendj({"error": "OWNER_IDENTITY_REQUIRED"}, 403)
return None
return auth
def _authorized_do_post(self):
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != _ENDPOINT:
return _original_do_post(self)
auth = _owner_authorization(self)
if not auth:
return
data = self.body()
owner_reference = str(data.get("owner_authorization_reference") or "")
try:
result = AuthorizedTaskWriterProvisioner().provision(
data,
OwnerContext(
identity=str(auth["account"]),
role=str(auth["role"]),
authorization_reference=owner_reference,
),
)
return self.sendj(result)
except ProvisioningError as exc:
body = {"error": exc.code}
if exc.details:
body["details"] = exc.details
return self.sendj(body, exc.status)
except Exception:
# Do not expose tool output, paths containing credentials, or traceback data.
return self.sendj({"error": "AUTHORIZED_WRITER_BRIDGE_INTERNAL_ERROR"}, 500)
api.H.do_POST = _authorized_do_post
if __name__ == "__main__":
api.main()
@@ -1,89 +0,0 @@
"""Governed API facade for branch-bound repair leases.
The facade is transport-neutral so the permanent control plane can expose it
through its existing authenticated loopback/API seam without creating a second
Git writer or a global credential registry.
"""
from __future__ import annotations
from typing import Any, Mapping
from branch_repair_lease import LeaseError, RepairLease, RepairLeaseManager, sanitized_lease_view
class BranchRepairLeaseAPI:
"""Validate operation envelopes and return sanitized lease metadata."""
def __init__(self, manager: RepairLeaseManager) -> None:
self.manager = manager
@staticmethod
def _required(request: Mapping[str, Any], name: str) -> Any:
value = request.get(name)
if value is None or value == "":
raise LeaseError(f"missing required field: {name}")
return value
@staticmethod
def _view(lease: RepairLease) -> dict[str, Any]:
return {"status": "ok", "lease": sanitized_lease_view(lease)}
def handle(self, request: Mapping[str, Any]) -> dict[str, Any]:
try:
operation = self._required(request, "operation")
owner = str(self._required(request, "owner_identity"))
if operation == "issue":
lease = self.manager.issue(
task_id=str(self._required(request, "task_id")),
owner_identity=owner,
branch=str(self._required(request, "branch")),
description=str(self._required(request, "description")),
allowed_paths=tuple(self._required(request, "allowed_paths")),
ttl_seconds=int(request.get("ttl_seconds", 3600)),
)
elif operation == "attach_existing_branch":
lease = self.manager.attach_existing_branch(
str(self._required(request, "lease_id")),
owner_identity=owner,
expected_head_oid=str(self._required(request, "expected_head_oid")),
)
elif operation == "authorize_commit":
lease = self.manager.authorize_commit(
str(self._required(request, "lease_id")),
owner_identity=owner,
expected_head_oid=str(self._required(request, "expected_head_oid")),
paths=tuple(self._required(request, "paths")),
content=request.get("content"),
)
elif operation == "record_commit":
lease = self.manager.record_commit(
str(self._required(request, "lease_id")),
owner_identity=owner,
expected_head_oid=str(self._required(request, "expected_head_oid")),
new_head_oid=str(self._required(request, "new_head_oid")),
paths=tuple(self._required(request, "paths")),
)
elif operation == "recover":
lease = self.manager.recover(
str(self._required(request, "lease_id")), owner_identity=owner
)
elif operation == "renew":
lease = self.manager.renew(
str(self._required(request, "lease_id")),
owner_identity=owner,
ttl_seconds=int(self._required(request, "ttl_seconds")),
)
elif operation == "complete":
lease = self.manager.complete(
str(self._required(request, "lease_id")), owner_identity=owner
)
elif operation == "revoke":
lease = self.manager.revoke(
str(self._required(request, "lease_id")), owner_identity=owner
)
else:
raise LeaseError("unsupported lease operation")
return self._view(lease)
except (LeaseError, TypeError, ValueError):
return {"status": "blocked", "error": "lease_request_rejected"}
@@ -1,48 +0,0 @@
"""Root-only CLI for the branch-bound repair lease API."""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from aios_branch_repair_lease_api import BranchRepairLeaseAPI
from branch_repair_lease import GitProbe, LeaseStore
from branch_repair_lease import RepairLeaseManager
def main() -> int:
parser = argparse.ArgumentParser(description="AIOS branch repair lease CLI")
parser.add_argument("--store", required=True)
parser.add_argument("--bare-repo", required=True)
parser.add_argument("--request-file")
parser.add_argument("--request-json")
args = parser.parse_args()
if hasattr(os, "geteuid") and os.geteuid() != 0:
print(json.dumps({"status": "blocked", "error": "root_required"}))
return 2
if bool(args.request_file) == bool(args.request_json):
print(json.dumps({"status": "blocked", "error": "one_request_source_required"}))
return 2
try:
raw = (
Path(args.request_file).read_text(encoding="utf-8")
if args.request_file
else str(args.request_json)
)
request = json.loads(raw)
if not isinstance(request, dict):
raise ValueError("request must be an object")
api = BranchRepairLeaseAPI(RepairLeaseManager(LeaseStore(args.store), GitProbe(args.bare_repo)))
response = api.handle(request)
print(json.dumps(response, ensure_ascii=True, sort_keys=True, separators=(",", ":")))
return 0 if response.get("status") == "ok" else 1
except Exception:
print(json.dumps({"status": "blocked", "error": "lease_cli_failed"}))
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,104 +0,0 @@
"""Read-only validation of recovered campaign authorization evidence."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import stat
from typing import Any
class RecoveryEvidenceError(RuntimeError):
pass
def _digest(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
h.update(block)
return h.hexdigest()
def _secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_MISSING")
info = path.stat(follow_symlinks=False)
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o022:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PERMISSIONS")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_INVALID") from exc
if not isinstance(value, dict):
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_OBJECT_REQUIRED")
return value
def load_recovered_authorization(path: str | Path) -> dict[str, Any]:
"""Verify the recovery receipt, sidecar, and all referenced immutable files."""
receipt_path = Path(path)
receipt = _secure_json(receipt_path)
sidecar = receipt_path.with_suffix(receipt_path.suffix + ".sha256")
if sidecar.is_symlink() or not sidecar.is_file():
raise RecoveryEvidenceError("RECOVERY_DIGEST_SIDECAR_MISSING")
expected = sidecar.read_text(encoding="ascii").split()[0]
actual = _digest(receipt_path)
if expected != actual:
raise RecoveryEvidenceError("RECOVERY_DIGEST_MISMATCH")
required = {
"schema",
"campaign_id",
"original_event_id",
"original_authorization_digest",
"signed_grant_id",
"signed_grant_digest",
"task_id",
"repository",
"original_base_oid",
"branch",
"expected_branch_oid",
"recovered_from_existing_evidence",
"new_owner_interaction",
"recovery_state",
"production_side_effect_executed",
"global_writes_enabled",
"grant_consumed",
"owner_authorization_consumed",
"original_receipts_immutable",
"recovered_at",
"evidence_files",
}
if set(receipt) != required or receipt["schema"] != "aios.owner-authorization-recovery.v1":
raise RecoveryEvidenceError("RECOVERY_SCHEMA_INVALID")
if receipt["recovered_from_existing_evidence"] is not True or receipt["new_owner_interaction"] is not False:
raise RecoveryEvidenceError("RECOVERY_NOT_APPEND_ONLY")
if receipt["production_side_effect_executed"] is not False or receipt["global_writes_enabled"] is not False:
raise RecoveryEvidenceError("RECOVERY_SCOPE_INVALID")
if receipt["recovery_state"] != "RECOVERED_PENDING_CONTINUATION_LEASE":
raise RecoveryEvidenceError("RECOVERY_STATE_INVALID")
evidence = receipt["evidence_files"]
if not isinstance(evidence, list) or not evidence:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_LIST_INVALID")
for item in evidence:
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_ENTRY_INVALID")
target = Path(item["path"])
if _digest(target) != item["sha256"]:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_DIGEST_MISMATCH")
return {
"schema": receipt["schema"],
"state": receipt["recovery_state"],
"campaign_id": receipt["campaign_id"],
"original_event_id": receipt["original_event_id"],
"original_authorization_digest": receipt["original_authorization_digest"],
"signed_grant_id": receipt["signed_grant_id"],
"task_id": receipt["task_id"],
"repository": receipt["repository"],
"original_base_oid": receipt["original_base_oid"],
"branch": receipt["branch"],
"expected_branch_oid": receipt["expected_branch_oid"],
"digest": actual,
"new_owner_interaction": False,
}
+37 -604
View File
@@ -1,69 +1,12 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
try:
from aios_gitops_write_guard import evaluate_git_write
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from aios_gitops_write_guard import evaluate_git_write
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationConflict,
PublicationPending,
PublicationManualOwnerReviewRequired,
PublicationReconciliationRequired,
PublicationRequest,
TagPublisher,
redact,
validate_version,
validate_gitea_api_url,
)
sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import (
ApprovalKeyRing,
ApprovalRevocationStore,
CapabilityAuthorizationError,
SagaConflict,
SignedApprovalVerifier,
SignedReleasePlanApproval,
SilentReleaseActionService,
)
try:
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
try:
from authoritative_git import (
AuthoritativeGitError,
AuthoritativeGitNotFound,
AuthoritativeGitOutputTooLarge,
authoritative_branches,
authoritative_diff,
authoritative_commit,
authoritative_tags,
repository_snapshot,
run_authoritative_git,
)
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from authoritative_git import (
AuthoritativeGitError,
AuthoritativeGitNotFound,
AuthoritativeGitOutputTooLarge,
authoritative_branches,
authoritative_diff,
authoritative_commit,
authoritative_tags,
repository_snapshot,
run_authoritative_git,
)
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
@@ -73,19 +16,6 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
'/var/lib/aios-gitops/silent-release',
))
SILENT_RELEASE_EXECUTION_ENABLED = str(os.getenv(
'AIOS_SILENT_RELEASE_EXECUTION_ENABLED', 'false'
)).lower() in {'1', 'true', 'yes', 'on'}
SILENT_RELEASE_PUBLIC_KEYS = Path(os.getenv(
'AIOS_SILENT_RELEASE_PUBLIC_KEYS',
'/etc/aios-gitops/silent-release-owner-public-keys.json',
))
monitor = ActionMonitor()
BASE_MODELS = [
@@ -94,49 +24,7 @@ BASE_MODELS = [
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
]
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
SECRET_VAL_RE = re.compile(
r'(?<![A-Za-z0-9_])'
r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})'
)
READ_ONLY_GET_PATHS = frozenset({
'/', '/health', '/api/health', '/api/openapi.yaml',
'/api/openapi-chatgpt-actions.yaml', '/api/schema', '/api', '/api/status',
'/api/base', '/api/baseline', '/base', '/api/ver', '/api/versions', '/ver',
'/api/diff', '/diff', '/api/patch', '/api/patches', '/api/branches', '/patch',
'/api/model', '/model', '/api/run', '/api/runtime-status', '/run',
'/api/action-monitor', '/action-monitor', '/admin/ai-evolution',
'/admin/ai-evolution/api/data',
'/api/release-consistency', '/api/tag-publication-status',
'/api/release-acceptance',
'/getReleaseCandidate', '/getReleasePlan', '/getReleaseAcceptance',
'/getApprovalEnvelope', '/getReleaseSaga', '/getCapabilityStatus',
'/getRecoveryStatus',
})
SILENT_RELEASE_POST_PATHS = frozenset({
'/createReleaseCandidate',
'/createReleaseIntent',
'/submitSignedApproval',
'/submitOwnerApproval',
'/startDeployment',
})
def reject_non_read_http(handler, method: str, path: str) -> bool:
if method == 'GET':
return False
if path in SILENT_RELEASE_POST_PATHS:
return False
if (
method == 'POST'
and path in ('/api/file', '/api/commit-file')
and os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() in {'1', 'true', 'yes', 'on'}
):
return False
decision = evaluate_git_write(WS)
if decision.allowed:
return False
handler.sendj(decision.payload(), decision.status_code)
return True
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
@@ -148,41 +36,6 @@ def sh(args, check=False):
def jload(p: Path, default):
try: return json.loads(p.read_text(encoding='utf-8'))
except Exception: return default
def _file_sha256(path: Path) -> str:
digest=hashlib.sha256()
with path.open('rb') as source:
for chunk in iter(lambda: source.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def _build_silent_release_service() -> SilentReleaseActionService:
verifier=None
if SILENT_RELEASE_PUBLIC_KEYS.is_file():
verifier=SignedApprovalVerifier(
ApprovalKeyRing.from_file(SILENT_RELEASE_PUBLIC_KEYS),
ApprovalRevocationStore(
SILENT_RELEASE_STATE_ROOT / 'revocations'
),
)
policy_path=WS / 'config' / 'gitops' / 'silent-release-policy.json'
policy_sha=_file_sha256(policy_path) if policy_path.is_file() else ''
return SilentReleaseActionService(
SILENT_RELEASE_STATE_ROOT,
execution_enabled=SILENT_RELEASE_EXECUTION_ENABLED,
approval_verifier=verifier,
expected_repository=REPOSITORY_IDENTITY,
policy_sha256=policy_sha,
)
# Construct once during service startup. If production execution is enabled
# without every trusted builder/orchestrator dependency, construction fails
# closed before the HTTP server accepts traffic.
_SILENT_RELEASE_SERVICE = _build_silent_release_service()
def silent_release_service() -> SilentReleaseActionService:
return _SILENT_RELEASE_SERVICE
def relpath(path: str) -> str:
x = str(path or '').replace('\\','/').strip()
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
@@ -256,13 +109,9 @@ def ensure_git():
if not sh(['config','user.name'])['stdout']: sh(['config','user.name','chatgpt-gitops-bot'], True)
def checkout(branch, base='main', create=False):
ensure_git(); b=branch_name(branch)
exists = sh(['rev-parse','--verify',f'refs/heads/{b}'])['code'] == 0
if exists:
sh(['checkout',b], True)
elif create:
sh(['checkout',base], True); sh(['checkout','-b',b], True)
else:
raise ValueError(f'branch does not exist: {b}')
if create or sh(['rev-parse','--verify',b])['code'] != 0:
sh(['checkout',base], True); sh(['checkout','-B',b], True)
else: sh(['checkout',b], True)
return b
def write_file(path, content):
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
@@ -272,19 +121,6 @@ def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
return {'status':'skipped','reason':'protected_branch','branch':branch}
if BARE_REPO.exists():
common = sh(['rev-parse', '--path-format=absolute', '--git-common-dir'], True)['stdout']
if Path(common).resolve() == BARE_REPO.resolve():
head = sh(['rev-parse', 'HEAD'], True)['stdout']
stored = subprocess.run(
['git', '--git-dir', str(BARE_REPO), 'rev-parse', f'refs/heads/{branch}'],
text=True, capture_output=True,
)
if stored.returncode != 0 or stored.stdout.strip() != head:
raise RuntimeError(f'linked-worktree branch verification failed for {branch}')
return {
'status':'verified', 'target':'authoritative-bare',
'branch':branch, 'oid':head,
}
ref=f'refs/heads/{branch}:refs/heads/{branch}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git sync failed for {branch}')
@@ -294,67 +130,15 @@ def sync_branch(branch: str) -> dict:
return {'status':'synced','target':'origin','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def sync_tag(tag: str) -> dict:
raise RuntimeError(
'unsafe sync_tag disabled: use TagPublisher with expected tag_oid, '
'target_oid, Gitea remote verification, and authenticated API verification'
)
def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
oid = str(value or '').strip().lower()
if not re.fullmatch(r'[0-9a-f]{40}', oid):
raise ValueError(f'{field} must be a full 40-character commit OID')
try:
return authoritative_commit(oid, git_dir=AUTHORITATIVE_GIT_DIR)
except (AuthoritativeGitError, ValueError) as exc:
raise ValueError(f'{field} does not resolve to the exact approved commit') from exc
def existing_tag_target(tag: str) -> str | None:
validate_version(tag)
try:
result = run_authoritative_git(
['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'],
check=False,
git_dir=AUTHORITATIVE_GIT_DIR,
)
except AuthoritativeGitError as exc:
raise RuntimeError(str(exc)) from exc
return result.stdout.strip() if result.returncode == 0 else None
def publication_request_from_query(q: dict[str, list[str]]) -> PublicationRequest:
required = ('version', 'target_oid', 'evidence_closure_oid', 'approval_plan_id')
missing = [name for name in required if not (q.get(name) or [''])[0]]
if missing:
raise ValueError('missing release governance fields: ' + ', '.join(missing))
return PublicationRequest(
(q['version'])[0],
(q['target_oid'])[0].lower(),
(q['evidence_closure_oid'])[0].lower(),
(q['approval_plan_id'])[0],
)
def required_authoritative_git_dir() -> Path:
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot["repository_available"]:
raise RuntimeError(str(snapshot["query_error"] or "AUTHORITATIVE_REPOSITORY_UNAVAILABLE"))
return Path(str(snapshot["authoritative_git_dir"]))
def publication_publisher() -> TagPublisher:
authoritative_repo = required_authoritative_git_dir()
return TagPublisher(
WS,
GitTagBackend(WS, authoritative_repo=authoritative_repo),
GiteaTagClient(
os.getenv('AIOS_GITOPS_GITEA_API_URL',''),
os.getenv('AIOS_GITOPS_GITEA_REPOSITORY',''),
Path(os.getenv('AIOS_GITOPS_GITEA_TOKEN_FILE','')),
token_line=int(os.getenv('AIOS_GITOPS_GITEA_TOKEN_LINE','1')),
),
os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'),
)
if not tag: return {'status':'skipped','reason':'empty_tag'}
if BARE_REPO.exists():
ref=f'refs/tags/{tag}:refs/tags/{tag}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag sync failed for {tag}')
return {'status':'synced','target':'bare','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',tag], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def commit(paths, msg):
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
@@ -374,16 +158,6 @@ def patches():
for p in sorted((WS/'patches').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
def runtime_release_info() -> dict[str, Any]:
for candidate in (WS / 'manifest.json', WS.parent / 'manifest.json'):
data = jload(candidate, {})
if data.get('target_commit'):
return {
'runtime_release_commit': data.get('target_commit'),
'runtime_release_path': str(WS),
}
return {'runtime_release_commit': None, 'runtime_release_path': str(WS)}
class H(BaseHTTPRequestHandler):
server_version='AIOSChatGPTGitOps/V1R1C0'
def sendj(self, data, code=200):
@@ -404,290 +178,44 @@ class H(BaseHTTPRequestHandler):
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a
def bridge_release(self, path, data):
route = {
'/createTagApprovalRequest': '/v2/tag/approval-request',
'/requestOrCreateTag': '/v2/tag/publish',
'/getTagPublicationStatus': '/v2/tag/status',
'/buildProductionArtifact': '/v2/release/artifact',
'/executeApprovedProductionRelease': '/v2/release/execute',
'/getReleaseConsistency': '/v2/release/consistency',
'/runReleaseAcceptance': '/v2/release/acceptance',
}.get(path)
if route is None:
raise ValueError('governed_release_route_invalid')
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
request = urllib.request.Request(
base + route,
data=json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8'),
headers={'Content-Type': 'application/json'},
method='POST',
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return self.sendj(result, response.status)
except urllib.error.HTTPError as exc:
try:
body = json.loads(exc.read().decode('utf-8'))
except Exception:
body = {'error': 'bridge_http_error'}
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
'/getReleaseConsistency', '/runReleaseAcceptance',
}:
actor = self.need('tag:create')
if not actor:
return
return self.bridge_release(path, self.body())
if reject_non_read_http(self, 'POST', path):
return
return self.sendj({'error': 'not_found', 'path': path}, 404)
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
q=urllib.parse.parse_qs(u.query)
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
try:
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in ('/api/base','/api/baseline','/base'):
data=jload(WS/'baseline'/'current.json', {})
data.setdefault('authoritative_repository', repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
))
return self.sendj(data)
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot['repository_available']:
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
try:
tags = authoritative_tags(git_dir=AUTHORITATIVE_GIT_DIR)
except AuthoritativeGitError as exc:
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
registry = [p.name for p in (WS/'versions').glob('*')]
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
'repository_available': True,
'query_error': None,
'tags': [item['name'] for item in tags],
'versions_dir': registry,
'head': snapshot['authoritative_head_oid'],
'authoritative_head_oid': snapshot['authoritative_head_oid'],
})
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or [''])[0]
to=(q.get('to') or [''])[0]
try:
diff=authoritative_diff(frm, to, git_dir=required_authoritative_git_dir())
except ValueError as exc:
return self.sendj({'error':redact(exc)},400)
except AuthoritativeGitNotFound as exc:
return self.sendj({'error':redact(exc)},404)
except AuthoritativeGitOutputTooLarge as exc:
return self.sendj({'error':redact(exc)},413)
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc)},503)
payload = {'from': frm, 'to': to, 'diff': diff}
payload.update({
key: diff[key]
for key in (
'from_oid', 'to_oid', 'changed_paths', 'stats',
'total_size_bytes', 'truncated', 'patch_limit_bytes',
)
})
return self.sendj(payload)
if path in ('/api/patch','/api/patches','/api/branches','/patch'):
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot['repository_available']:
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'metadata_patches': patches(),
'authoritative_branches': [],
}, 503)
try:
branches = authoritative_branches(git_dir=AUTHORITATIVE_GIT_DIR)
except AuthoritativeGitError as exc:
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'metadata_patches': patches(),
'authoritative_branches': [],
}, 503)
metadata = patches()
branch_names = {item['name'] for item in branches}
metadata_names = {
item.get('branch') for item in metadata
if isinstance(item, dict) and item.get('branch')
}
orphan = [
item for item in metadata
if item.get('branch') and item.get('branch') not in branch_names
]
missing = [
item for item in branches
if item['name'] not in metadata_names
]
return self.sendj({
'metadata_patches': metadata,
'authoritative_branches': branches,
'orphan_metadata': orphan,
'branches_without_metadata': missing,
'repository_available': True,
'query_error': None,
'patches': metadata,
'branches': [item['name'] for item in branches],
})
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
if path in ('/api/run','/api/runtime-status','/run'):
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
workspace_branch = None
if (WS / '.git').exists():
result = sh(['rev-parse','--abbrev-ref','HEAD'])
workspace_branch = result['stdout'] or None
payload = {
'service':'aios-gitops-model-api',
'status':svc.stdout.strip() or svc.stderr.strip(),
'workspace_branch':workspace_branch,
**runtime_release_info(),
**snapshot,
'writes_enabled': str(os.getenv('AIOS_GITOPS_WRITES_ENABLED','false')).lower() in {'1','true','yes','on'},
}
return self.sendj(payload, 200 if snapshot['repository_available'] else 503)
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
if path in ('/api/action-monitor','/action-monitor'):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
if path == '/api/tag-publication-status':
version=(q.get('version') or [''])[0]
try:
version=validate_version(version)
status=publication_publisher().get_tag_publication_status(version)
except ValueError:
return self.sendj({'error':'invalid release version'},400)
return self.sendj(status)
if path == '/api/release-consistency':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().get_release_consistency(request))
if path == '/api/release-acceptance':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().run_release_acceptance(request))
if path in (
'/getReleaseCandidate',
'/getReleasePlan',
'/getReleaseAcceptance',
'/getApprovalEnvelope',
'/getReleaseSaga',
'/getCapabilityStatus',
):
candidate_id=(q.get('candidate_id') or [''])[0]
if not candidate_id:
return self.sendj({'error':'candidate_id is required'},400)
service=silent_release_service()
if path == '/getReleaseCandidate':
return self.sendj(
service.get_release_candidate(candidate_id)
)
if path == '/getApprovalEnvelope':
return self.sendj(
service.get_approval_envelope(candidate_id)
)
if path == '/getReleasePlan':
return self.sendj(service.get_release_plan(candidate_id))
if path == '/getReleaseAcceptance':
return self.sendj(service.get_acceptance(candidate_id))
if path == '/getCapabilityStatus':
return self.sendj(
service.get_capability_status(candidate_id)
)
return self.sendj(service.get_release_saga(candidate_id))
if path == '/getRecoveryStatus':
return self.sendj(
silent_release_service().get_recovery_status()
)
# Admin AI Evolution Dashboard
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
return self.ai_evolution_page()
if path in ('/admin/ai-evolution/api/data'):
return self.ai_evolution_data()
return self.sendj({'error':'not_found','path':path},404)
except FileNotFoundError:
return self.sendj({'error':'release candidate not found'},404)
except ValueError as e:
error=redact(e)
monitor.record_failure('do_GET', error, {'path':path})
return self.sendj({'error':error},400)
except Exception as e:
error=redact(e)
monitor.record_failure('do_GET', error, {'path':path})
return self.sendj({'error':error},500)
monitor.record_failure('do_GET', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def do_PUT(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'PUT', path): return
data=self.body()
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
if path == '/api/model-priority': return self.model_priority(data)
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'POST', path): return
data=self.body()
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
if path == '/createReleaseCandidate':
a=self.need('release:candidate')
if not a: return
return self.sendj(
silent_release_service().create_release_candidate(data)
)
if path == '/createReleaseIntent':
a=self.need('release:candidate')
if not a: return
return self.sendj(
silent_release_service().create_release_intent(data)
)
if path in ('/submitOwnerApproval','/submitSignedApproval'):
a=self.need('release:approve')
if not a: return
return self.sendj(
silent_release_service().submit_owner_approval(data)
)
if path == '/startDeployment':
a=self.need('release:deploy')
if not a: return
return self.sendj(
silent_release_service().start_deployment(
str(data.get('candidate_id') or '')
)
)
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
if path in ('/api/file','/api/commit-file'): return self.file(data)
if path == '/api/commit': return self.commit(data)
@@ -698,30 +226,9 @@ class H(BaseHTTPRequestHandler):
if path == '/api/model-priority': return self.model_priority(data)
if path == '/api/nl': return self.nl(data)
return self.sendj({'error':'not_found','path':path},404)
except (ValueError, FileNotFoundError) as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},400)
except CapabilityAuthorizationError as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},403)
except SagaConflict as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},409)
except Exception as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},500)
def do_PATCH(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'PATCH', path): return
return self.sendj({'error':'not_found','path':path},404)
def do_DELETE(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'DELETE', path): return
return self.sendj({'error':'not_found','path':path},404)
monitor.record_failure('do_POST', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def branch(self,data):
a=self.need('branch:create');
if not a: return
@@ -730,44 +237,7 @@ class H(BaseHTTPRequestHandler):
def file(self,data):
a=self.need('file:write');
if not a: return
if os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() not in {'1','true','yes','on'}:
return self.sendj({'error':'DURABLE_JOB_CONTROLLER_UNAVAILABLE','retryable':True},503)
try:
path = relpath(str(data.get('path') or ''))
branch = str(data.get('branch') or '')
expected = str(data.get('expected_branch_oid') or '')
campaign_id = str(data.get('campaign_id') or os.getenv('AIOS_GITOPS_CAMPAIGN_ID') or '')
supplied_digest = str(data.get('payload_digest') or '')
content = data.get('content')
payload_bytes = None
if content is not None:
payload_bytes = content.encode('utf-8') if isinstance(content, str) else json.dumps(content, sort_keys=True, ensure_ascii=False).encode('utf-8')
payload_digest = supplied_digest or hashlib.sha256(payload_bytes or b'').hexdigest()
if not campaign_id or not branch or not expected:
return self.sendj({'error':'JOB_BINDING_REQUIRED','retryable':False},409)
request = {
'campaign_id': campaign_id,
'repository': REPOSITORY_IDENTITY or 'aios/aios',
'operation': 'commit-file',
'branch': branch,
'expected_branch_oid': expected,
'payload_digest': payload_digest,
# The controller persists only the binding and digest. Raw
# content is never copied into durable state or logs.
}
receipt = GitOpsJobController(
JobStore(Path(os.getenv('AIOS_GITOPS_JOB_ROOT', '/var/lib/aios-gitops/jobs')))
).submit(request, payload=payload_bytes)
return self.sendj({
'job_id': receipt.job_id,
'state': receipt.state.value,
'side_effect_state': receipt.side_effect_state,
'retryable': receipt.http_status == 503,
}, receipt.http_status)
except ServiceUnavailable as exc:
return self.sendj({'error':str(exc),'layer':'durable_job','retryable':True,'side_effect_state':'UNKNOWN'},503)
except JobError as exc:
return self.sendj({'error':str(exc),'retryable':False},409)
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
def commit(self,data):
a=self.need('commit:create');
if not a: return
@@ -781,45 +251,13 @@ class H(BaseHTTPRequestHandler):
a=self.need('tag:create');
if not a: return
ver=data.get('version') or data.get('tag')
try:
validate_version(ver)
except ValueError:
return self.sendj({'error':'version must match VxRxCx'},400)
if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'):
return self.sendj({
'error':'production_tag_evidence_required',
'required':['version','target_oid','evidence_closure_oid','approval_plan_id'],
},400)
try:
target_oid=resolve_commit_oid(data.get('target_oid'))
evidence_oid=resolve_commit_oid(data.get('evidence_closure_oid'), 'evidence_closure_oid')
except ValueError as exc:
return self.sendj({'error':'invalid_tag_target','detail':str(exc)},400)
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'request':req,'commit':c})
request=PublicationRequest(ver,target_oid,evidence_oid,str(data.get('approval_plan_id')))
main_before=sh(['rev-parse','refs/heads/main'], True)['stdout']
publisher=publication_publisher()
try:
publication=publisher.publish(request)
except PublicationManualOwnerReviewRequired as exc:
audit(a['account'],'tag_manual_owner_review_required',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':redact(exc)})
return self.sendj({'status':'MANUAL_OWNER_REVIEW_REQUIRED','error':redact(exc),'main_changed':False,'force_push':False},409)
except PublicationConflict as exc:
audit(a['account'],'tag_conflict',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'RECONCILIATION_REQUIRED','error':str(exc),'main_changed':False,'force_push':False},409)
except PublicationPending as exc:
audit(a['account'],'tag_pending',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'REMOTE_PUSH_PENDING','error':str(exc),'main_changed':False,'force_push':False},503)
except PublicationReconciliationRequired as exc:
audit(a['account'],'tag_reconciliation_required',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'RECONCILIATION_REQUIRED','error':str(exc),'main_changed':False,'force_push':False},502)
main_after=sh(['rev-parse','refs/heads/main'], True)['stdout']
if main_after != main_before:
raise RuntimeError('main changed during tag operation')
result={'status':publication['state'],'version':ver,'tag_name':ver,'tag_oid':publication['tag_oid'],'target_oid':target_oid,'expected_target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'workspace_head':sh(['rev-parse','HEAD'],True)['stdout'],'main_oid_before':main_before,'main_oid_after':main_after,'main_changed':False,'force_push':False,'publication':publication}
audit(a['account'],'tag',ver,result)
self.sendj(result)
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
def rollback(self,data):
a=self.need('rollback:request');
if not a: return
@@ -914,10 +352,7 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
cmd_filter=(params.get('command') or [None])[0]
bl=jload(WS/'baseline'/'current.json',{})
current_ver=bl.get('current_baseline','V1R3C1')
try:
tags=[item['name'] for item in authoritative_tags(git_dir=required_authoritative_git_dir()) if str(item['name']).startswith('V')]
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc),'repository_available':False},503)
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
versions=[]
if (WS/'baseline'/'promotions').exists():
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
@@ -955,11 +390,9 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
def main():
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
if __name__ == '__main__': main()
@@ -1,237 +0,0 @@
"""ChatGPT GitOps Bridge v2 contract and governed release operations."""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
from pathlib import Path
from typing import Any, Callable, Mapping
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
BRIDGE_OPERATIONS = (
"getRuntimeStatus",
"getGovernedWriteCapabilities",
"createGovernedTask",
"acquireWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"getGitOpsJob",
"resumeGitOpsJob",
"reconcileGitOpsJob",
"createTagApprovalRequest",
"requestOrCreateTag",
"getTagPublicationStatus",
"buildProductionArtifact",
"executeApprovedProductionRelease",
"getReleaseConsistency",
"runReleaseAcceptance",
"getControlPlaneSnapshot",
)
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
"resumeGitOpsJob": ("job_id", "expected_revision"),
"reconcileGitOpsJob": ("job_id", "expected_revision"),
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
"getTagPublicationStatus": ("version",),
"buildProductionArtifact": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"getControlPlaneSnapshot": (),
}
_OPERATION_PATHS = {
"createGovernedTask": "/v2/governed-task",
"acquireWriterLease": "/v2/writer-lease",
"commitFileGoverned": "/v2/commit-file",
"commitFilesGoverned": "/v2/commit-files",
"getGitOpsJob": "/v2/job",
"resumeGitOpsJob": "/v2/job/resume",
"reconcileGitOpsJob": "/v2/job/reconcile",
"getTagPublicationStatus": "/v2/tag/status",
"getReleaseConsistency": "/v2/release/consistency",
"runReleaseAcceptance": "/v2/release/acceptance",
}
def operation_contracts() -> dict[str, dict[str, object]]:
"""Return the runtime contract consumed by adapters and schema tooling."""
return {
operation: {
"method": "POST",
"path": _OPERATION_PATHS.get(operation),
"required_fields": list(fields),
}
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
}
def render_openapi_contract_fragment() -> str:
required = "\n".join(
f" - {field}"
for field in OPERATION_REQUIRED_FIELDS["commitFilesGoverned"]
)
return (
" /v2/commit-files:\n"
" post:\n"
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" requestBody:\n"
" required: true\n"
" content:\n"
" application/json:\n"
" schema:\n"
" type: object\n"
" additionalProperties: false\n"
" required:\n"
f"{required}\n"
" properties:\n"
" task_id: {type: string}\n"
" repository: {type: string, example: aios/aios}\n"
" expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}\n"
" target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}\n"
" generation: {type: integer, minimum: 1}\n"
" idempotency_key: {type: string}\n"
" files:\n"
" type: array\n"
" minItems: 1\n"
" items:\n"
" type: object\n"
" additionalProperties: false\n"
" required: [path, content_base64]\n"
" properties:\n"
" path: {type: string, minLength: 1, maxLength: 512}\n"
" content_base64: {type: string}\n"
" message: {type: string, minLength: 1, maxLength: 200}\n"
" responses:\n"
" '200': {description: Governed commit job and verified branch result}\n"
" '400': {description: Contract or policy rejection}\n"
" '409': {description: Branch CAS or idempotency conflict}\n"
)
class RuntimeDriftError(RuntimeError):
pass
class BridgeV2Error(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class ControlPlaneReleaseManifest:
control_plane_release_version: str
control_plane_target_oid: str
artifact_sha256: str
runtime_path: Path
bridge_schema_version: str
repository_identity: str
authoritative_git_dir: Path
deployment_state: str
def as_dict(self) -> dict[str, object]:
return {
"control_plane_release_version": self.control_plane_release_version,
"control_plane_target_oid": self.control_plane_target_oid,
"artifact_sha256": self.artifact_sha256,
"runtime_path": str(self.runtime_path),
"bridge_schema_version": self.bridge_schema_version,
"repository_identity": self.repository_identity,
"authoritative_git_dir": str(self.authoritative_git_dir),
"deployment_state": self.deployment_state,
}
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
try:
runtime = self.runtime_path.resolve(strict=True)
root = approved_root.resolve(strict=True)
artifact_path = artifact.resolve(strict=True)
runtime.relative_to(root)
artifact_path.relative_to(runtime)
except (OSError, ValueError):
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT") from None
mutable_markers = {"workspace", "workspaces", "worktree", "worktrees"}
if mutable_markers.intersection(part.lower() for part in runtime.parts):
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
valid = (
len(self.control_plane_target_oid) == 40
and observed_oid == self.control_plane_target_oid
and len(self.artifact_sha256) == 64
and hashlib.sha256(artifact_path.read_bytes()).hexdigest() == self.artifact_sha256
and self.bridge_schema_version == BRIDGE_SCHEMA_VERSION
and self.repository_identity == "aios/aios"
and self.authoritative_git_dir == Path("/data/aios-gitops/repo.git")
and self.authoritative_git_dir.is_absolute()
and self.deployment_state == "STAGING"
)
if not valid:
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
return {**self.as_dict(), "drift_status": "PASS"}
class BridgeV2Adapter:
"""Thin operation adapter; all mutating handlers are injected seams."""
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self.runtime_status = dict(runtime_status)
self.handlers = dict(handlers or {})
def getRuntimeStatus(self) -> dict[str, Any]:
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
return {
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": operation_contracts(),
"force_push": False,
"main_write": False,
"tag_write": False,
"baseline_write": False,
"production_write": False,
"approved_release_operations": True,
"approved_release_scope": {
"version": "V2R2C1",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
},
}
@staticmethod
def legacy_write_disabled() -> dict[str, object]:
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": True}
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
required = OPERATION_REQUIRED_FIELDS[operation]
if any(key not in request for key in required):
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_MISSING")
handler = self.handlers.get(operation)
if handler is None:
raise BridgeV2Error("GOVERNED_HANDLER_UNAVAILABLE")
return handler(dict(request))
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", request)
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
def resumeGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("resumeGitOpsJob", request)
def reconcileGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("reconcileGitOpsJob", request)
def createTagApprovalRequest(self, request: Mapping[str, Any]) -> Any: return self._call("createTagApprovalRequest", request)
def requestOrCreateTag(self, request: Mapping[str, Any]) -> Any: return self._call("requestOrCreateTag", request)
def getTagPublicationStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTagPublicationStatus", request)
def buildProductionArtifact(self, request: Mapping[str, Any]) -> Any: return self._call("buildProductionArtifact", request)
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
@@ -1,434 +0,0 @@
"""Loopback-only governed Bridge v2 HTTP gateway with durable handlers."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import base64
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import re
import sqlite3
from typing import Any, Mapping
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
from gitea_tag_publication import validate_gitea_api_url
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore, _RECOVERABLE
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from governed_production_release import build_executor_config
MAX_BODY = 2 * 1024 * 1024
ROUTES = {
"/v2/runtime": "getRuntimeStatus",
"/v2/capabilities": "getGovernedWriteCapabilities",
"/v2/governed-task": "createGovernedTask",
"/v2/writer-lease": "acquireWriterLease",
"/v2/commit-file": "commitFileGoverned",
"/v2/commit-files": "commitFilesGoverned",
"/v2/job": "getGitOpsJob",
"/v2/job/resume": "resumeGitOpsJob",
"/v2/job/reconcile": "reconcileGitOpsJob",
"/v2/tag/approval-request": "createTagApprovalRequest",
"/v2/tag/publish": "requestOrCreateTag",
"/v2/tag/status": "getTagPublicationStatus",
"/v2/release/artifact": "buildProductionArtifact",
"/v2/release/execute": "executeApprovedProductionRelease",
"/v2/release/consistency": "getReleaseConsistency",
"/v2/release/acceptance": "runReleaseAcceptance",
}
@dataclass(frozen=True, slots=True)
class RuntimeConfig:
target_oid: str
authoritative_git_dir: Path
state_root: Path
v2_job_root: Path
token_path: Path
ssh_key_path: Path
remote_name: str = "gitea-http"
repository: str = "aios/aios"
api_base: str = ""
v2_task_id: str = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809"
v2_branch: str = "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809"
target_version: str = "V2R2C1"
registry_root: Path = Path("/var/lib/aios-gitops/runtime/registry-projection")
baseline_path: Path = Path("/var/lib/aios-gitops-runtime-anchor/baseline/current.json")
health_url: str = "http://127.0.0.1:18927/health"
def _safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
class GovernedRuntime:
def __init__(self, config: RuntimeConfig) -> None:
self.config = config
config.state_root.mkdir(parents=True, exist_ok=True)
self.authority = AuthorityGitExecutor(config.authoritative_git_dir, config.state_root / "worktrees")
self.bootstrap = GovernedTaskBootstrap(self.authority, config.state_root, remote_name=config.remote_name, token_path=config.token_path, ssh_key_path=config.ssh_key_path, api_base=config.api_base)
self.runtimes: dict[tuple[str, int], tuple[Any, GitOpsJobController, ContinuationExecutor]] = {}
self.controllers: dict[str, GitOpsJobController] = {}
self.bindings: dict[str, dict[str, Any]] = {}
self.release = build_executor_config(config)
metadata_root = config.state_root / "task-bindings"
if metadata_root.is_dir():
for metadata in metadata_root.glob("*.json"):
try:
value = json.loads(metadata.read_text(encoding="utf-8"))
if isinstance(value, dict) and value.get("task_id"):
self.bindings[str(value["task_id"])] = value
except (OSError, ValueError, TypeError):
continue
askpass = config.state_root / "bridge-v2-askpass.sh"
if not askpass.exists():
askpass.write_text('#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR" ;; *) cat "$CREDENTIAL_FILE" ;; esac\n', encoding="utf-8")
askpass.chmod(0o700)
os.environ.setdefault("GIT_ASKPASS", str(askpass)); os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force"); os.environ.setdefault("GIT_TERMINAL_PROMPT", "0"); os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
def runtime_status(self) -> dict[str, Any]:
return {
"status": "active",
"health": 200,
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
"control_plane_release_version": self.config.target_version,
"control_plane_target_oid": self.config.target_oid,
"runtime_release_commit": self.config.target_oid,
"runtime_path": str(self.config.state_root),
"artifact_sha256": os.environ.get("AIOS_CONTROL_PLANE_ARTIFACT_SHA256"),
"deployment_state": "STAGING",
"drift_status": "PASS",
"repository": self.config.repository,
"repository_identity": self.config.repository,
"authoritative_git_dir": str(self.config.authoritative_git_dir),
"legacy_global_writes_enabled": False,
}
def _job_root(self, task_id: str) -> Path:
return self.config.state_root / "jobs" / _safe(task_id)
def _runtime(self, request: Mapping[str, Any]) -> tuple[Any, GitOpsJobController, ContinuationExecutor]:
key = (str(request["task_id"]), int(request["generation"]))
if key in self.runtimes: return self.runtimes[key]
lease = WriterLeaseRequestV2(task_id=str(request["task_id"]), repository=str(request["repository"]), branch_mode=str(request.get("branch_mode", "CONTINUE_EXISTING")), base_oid=str(request["base_oid"]), expected_branch_oid=str(request["expected_branch_oid"]), target_branch=str(request["target_branch"]), generation=int(request["generation"]), lease_seconds=int(request.get("lease_seconds", 900)))
lease.validate(AuthorityGitAdapter(self.authority))
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
if worktree_path.exists():
workspace = DetachedGitWorkspace(worktree_path)
else:
self.authority.create_detached_worktree(lease.expected_branch_oid, _safe(lease.task_id), self.config.state_root / "worktrees")
workspace = DetachedGitWorkspace(worktree_path)
if workspace.head_oid() != lease.expected_branch_oid or not workspace.is_detached(): raise RuntimeError("WORKTREE_HEAD_MISMATCH")
job_root = self._job_root(lease.task_id); controller = GitOpsJobController(JobStore(job_root, payload_root=job_root / "payloads")); credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path); remote = GiteaContinuationRemote(workspace=worktree_path, credential=credential, remote_name=self.config.remote_name, repository=lease.repository, api_base=self.config.api_base); executor = ContinuationExecutor(controller, AuthorityGitAdapter(self.authority), workspace, remote, credential)
binding = ContinuationRuntimeBinding(request=lease, workspace_path=worktree_path, lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1), lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=lease.lease_seconds), original_receipt_valid=True, original_grant_consumed=True, permissions=ContinuationPermissions())
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
def create_task(self, request: Mapping[str, Any]) -> dict[str, Any]:
result = self.bootstrap.create(request); self.bindings[result["task_id"]] = result; return result
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
binding, _, _ = self._runtime(request)
req = binding.request
return {"schema": WriterLeaseRequestV2.schema, "task_id": req.task_id, "repository": req.repository, "branch_mode": req.branch_mode, "base_oid": req.base_oid, "expected_branch_oid": req.expected_branch_oid, "target_branch": req.target_branch, "generation": req.generation, "lease_seconds": req.lease_seconds, "create_target_branch": req.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if req.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True}
@staticmethod
def _files(request: Mapping[str, Any]) -> dict[str, bytes]:
raw = request.get("files") or {request["path"]: request["content_base64"]}; pairs = raw.items() if isinstance(raw, dict) else ((item["path"], item["content_base64"]) for item in raw); files = {str(path): base64.b64decode(encoded, validate=True) for path, encoded in pairs}
if not files: raise ValueError("FILES_EMPTY")
return files
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
if runtime is None: raise RuntimeError("LEASE_NOT_FOUND")
binding, _, executor = runtime
if request["target_branch"] != binding.request.target_branch or request["expected_branch_oid"] != binding.request.expected_branch_oid: raise RuntimeError("LEASE_BINDING_CONFLICT")
commit = ContinuationCommit(idempotency_key=str(request["idempotency_key"]), files=self._files(request), message=str(request["message"]))
executor.workspace.materialize_files(commit.files)
return executor.execute(binding, commit)
def _controller_for_job(self, job_id: str) -> GitOpsJobController:
for controller in self.controllers.values():
try:
controller.store.read(job_id)
return controller
except Exception:
continue
jobs_root = self.config.state_root / "jobs"
if jobs_root.is_dir():
for root in jobs_root.iterdir():
if not root.is_dir():
continue
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
try:
controller.store.read(job_id)
self.controllers[str(job_id)] = controller
return controller
except Exception:
continue
raise RuntimeError("JOB_NOT_FOUND")
@staticmethod
def _valid_oid(value: object) -> bool:
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{40}", value) is not None
def _reconstruct_legacy_binding(
self,
row: Mapping[str, Any],
controller: GitOpsJobController,
) -> dict[str, Any]:
raw = json.loads(row["request_json"])
binding = raw.get("binding", raw)
if not isinstance(binding, dict):
raise RuntimeError("DURABLE_BINDING_INVALID")
task_id = str(binding.get("task_id") or binding.get("campaign_id") or "").strip()
repository = str(binding.get("repository") or "").strip()
operation = str(binding.get("operation") or row.get("operation") or "").strip()
branch = str(binding.get("target_branch") or binding.get("branch") or row.get("branch") or "").strip()
expected = str(binding.get("expected_branch_oid") or row.get("expected_branch_oid") or "").strip()
base = binding.get("base_oid") or binding.get("lineage_anchor_oid")
if not base:
with controller.store._connect() as connection:
lease_row = connection.execute(
"SELECT lineage_anchor_oid FROM branch_leases WHERE job_id=? ORDER BY revision DESC LIMIT 1",
(str(row["job_id"]),),
).fetchone()
if lease_row is not None:
base = lease_row["lineage_anchor_oid"]
base = str(base or "").strip()
branch_mode = str(
binding.get("branch_mode")
or ("CREATE_NEW" if operation.endswith("_CREATE_NEW") else "CONTINUE_EXISTING")
).strip()
try:
generation = int(binding.get("generation") or 0)
except (TypeError, ValueError) as exc:
raise RuntimeError("DURABLE_BINDING_GENERATION_INVALID") from exc
if (
not task_id
or repository != self.config.repository
or not branch.startswith("agent/")
or branch in {"main", "master"}
or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}
or generation < 1
or not self._valid_oid(base)
or not self._valid_oid(expected)
):
raise RuntimeError("DURABLE_BINDING_INVALID")
current = self.authority.read_ref("refs/heads/" + branch)
if (
current is None
or not self.authority.commit_exists(base)
or not self.authority.commit_exists(expected)
or not self.authority.ancestry_check(base, current)
or not self.authority.ancestry_check(expected, current)
):
raise RuntimeError("DURABLE_BINDING_LINEAGE_INVALID")
return {
"task_id": task_id,
"repository": repository,
"branch_mode": branch_mode,
"base_oid": base,
"expected_branch_oid": expected,
"target_branch": branch,
"generation": generation,
"lease_seconds": int(binding.get("lease_seconds") or 900),
}
def _executor_from_row(
self,
row: Mapping[str, Any],
controller: GitOpsJobController,
) -> ContinuationExecutor:
request = self._reconstruct_legacy_binding(row, controller)
lease = WriterLeaseRequestV2(**request)
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
if not worktree_path.exists():
current = self.authority.read_ref("refs/heads/" + lease.target_branch)
if current is None:
raise RuntimeError("DURABLE_WORKTREE_BRANCH_MISSING")
self.authority.create_detached_worktree(
current,
_safe(lease.task_id),
self.config.state_root / "worktrees",
)
workspace = DetachedGitWorkspace(worktree_path)
credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path)
remote = GiteaContinuationRemote(
workspace=worktree_path,
credential=credential,
remote_name=self.config.remote_name,
repository=lease.repository,
api_base=self.config.api_base,
)
return ContinuationExecutor(
controller,
AuthorityGitAdapter(self.authority),
workspace,
remote,
credential,
)
def job(self, request: Mapping[str, Any], action: str) -> dict[str, Any]:
job_id = str(request["job_id"])
controller = self._controller_for_job(job_id)
row = controller.store.read(job_id)
if action == "get":
return controller.store._receipt(row, duplicate=True).as_dict()
if action == "resume":
return controller.resume(job_id).as_dict()
if JobState(row["state"]) not in _RECOVERABLE:
return controller.store._receipt(row, duplicate=True).as_dict()
return self._executor_from_row(row, controller).reconcile(job_id)
def _snapshot_task(self) -> dict[str, Any]:
candidates: list[dict[str, Any]] = []
for value in self.bindings.values():
task_id = str(value.get("task_id") or "").strip()
branch = str(value.get("target_branch") or "").strip()
if not task_id or not branch.startswith("agent/"):
continue
authority = self.authority.read_ref("refs/heads/" + branch)
try:
generation = int(value.get("generation") or 0)
except (TypeError, ValueError):
generation = 0
reconciliation_state = "OBSERVATION_REQUIRED"
db = self.config.state_root / "jobs" / _safe(task_id) / "state.sqlite3"
if db.is_file():
connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
try:
lease = connection.execute(
"SELECT MAX(generation) FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
generation = max(generation, int((lease or (0,))[0] or 0))
latest = connection.execute(
"SELECT state, side_effect_state FROM jobs WHERE branch=? ORDER BY updated_at DESC LIMIT 1",
(branch,),
).fetchone()
if latest is not None:
reconciliation_state = (
"COMPLETED"
if latest[0] == "COMPLETED"
else str(latest[1] or latest[0])
)
finally:
connection.close()
candidates.append(
{
"task_id": task_id,
"branch": branch,
"authority_branch_oid": authority,
"gitea_git_oid": None,
"gitea_api_oid": None,
"commit_visible": None,
"generation": generation,
"reconciliation_state": reconciliation_state,
}
)
if not candidates:
return {}
return max(
candidates,
key=lambda item: (int(item["generation"] or 0), bool(item["authority_branch_oid"])),
)
def snapshot(self) -> dict[str, Any]:
task = self._snapshot_task()
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
def handlers(self) -> dict[str, Any]:
return {
"createGovernedTask": self.create_task,
"acquireWriterLease": self.acquire_lease,
"commitFileGoverned": lambda request: self.commit_files({**request, "files": {request["path"]: request["content_base64"]}}),
"commitFilesGoverned": self.commit_files,
"getGitOpsJob": lambda request: self.job(request, "get"),
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
"getControlPlaneSnapshot": lambda request: self.snapshot(),
"createTagApprovalRequest": self.release.approval_request,
"requestOrCreateTag": self.release.publish_tag,
"getTagPublicationStatus": self.release.tag_status,
"buildProductionArtifact": self.release.build_artifact,
"executeApprovedProductionRelease": self.release.execute_request,
"getReleaseConsistency": self.release.consistency,
"runReleaseAcceptance": self.release.acceptance,
}
def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -> Mapping[str, Any]:
if isinstance(runtime_config, Mapping): runtime_config = RuntimeConfig(**runtime_config)
return GovernedRuntime(runtime_config).handlers()
class _HTTPError(RuntimeError): pass
class BridgeHandler(BaseHTTPRequestHandler):
adapter: BridgeV2Adapter | None = None
def log_message(self, *_args: Any) -> None: return
def _json(self, value: Any, status: int = 200) -> None:
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode(); self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
def _body(self) -> dict[str, Any]:
try: length = int(self.headers.get("Content-Length", "-1"))
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
value = json.loads(self.rfile.read(length).decode("utf-8"))
if not isinstance(value, dict): raise _HTTPError("JSON_OBJECT_REQUIRED")
return value
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode"}
if operation == "commitFilesGoverned": allowed.add("files")
allowed.update({"owner_approval", "execute"})
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
def do_GET(self) -> None:
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
self._json({"error": "NOT_FOUND"}, 404)
def do_POST(self) -> None:
operation = ROUTES.get(self.path)
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities") or self.adapter is None: self._json({"error": "NOT_FOUND"}, 404); return
try:
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
def main() -> None:
state_root = Path("/var/lib/aios-gitops/bridge-v2-staging")
token_path = Path(os.environ.get("AIOS_GITEA_TOKEN_FILE", "/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token"))
ssh_key_path = Path(os.environ.get("AIOS_GITEA_SSH_KEY_PATH", "/home/aiman/.ssh/id_ed25519_codex_tmp"))
os.environ["CREDENTIAL_FILE"] = str(token_path)
os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
config = RuntimeConfig(
os.environ.get("AIOS_CONTROL_PLANE_TARGET_OID", ""),
Path("/data/aios-gitops/repo.git"),
state_root,
state_root / "jobs",
token_path,
ssh_key_path,
remote_name=os.environ.get("AIOS_GITEA_REMOTE", "gitea-http"),
api_base=validate_gitea_api_url(os.environ.get("AIOS_GITEA_API_BASE", "")),
target_version=os.environ.get("AIOS_CONTROL_PLANE_TARGET_VERSION", "V2R2C1"),
)
runtime = GovernedRuntime(config)
serve(
BridgeV2Adapter(runtime_status=runtime.runtime_status(), handlers=runtime.handlers()),
port=int(os.environ.get("AIOS_BRIDGE_V2_PORT", "18892")),
)
if __name__ == "__main__": main()
@@ -1,594 +0,0 @@
"""Governed continuation executor for exact existing task branches.
The executor composes the existing continuation lease and durable Job store.
All Git and Gitea side effects live behind injected narrow interfaces so the
state machine can prove Authority, Git-network, API-branch, and commit-object
agreement before it reports completion.
"""
from __future__ import annotations
import base64
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import subprocess
from typing import Callable, Mapping, Protocol
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlsplit
from urllib.request import Request, urlopen
from aios_authority_git_executor import AuthorityGitExecutor
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import GitOpsJobController, JobState
from aios_gitops_reconciliation_core import ReconciliationAction, RemoteObservation, decide_reconciliation
from authorized_task_writer_provisioning import validate_writer_lease_request_v2
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SECRET_PATTERNS = (
re.compile(rb"gh[pousr]_[A-Za-z0-9]{20,}"),
re.compile(rb"(?i)(?:token|password|secret|api[_-]?key)\s*[:=]\s*[\"'][^\"'\s]{12,}[\"']"),
re.compile(rb"-----BEGIN " + rb"(?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
)
class ContinuationExecutionError(RuntimeError):
"""Stable fail-closed continuation execution error."""
class DetachedWorkspace(Protocol):
def is_detached(self) -> bool: ...
def head_oid(self) -> str: ...
def commit_files(self, files: Mapping[str, bytes], message: str) -> str: ...
class AuthorityPort(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
class RemotePort(Protocol):
def repository_identity(self) -> str: ...
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
def commit_visible(self, oid: str) -> bool: ...
class CredentialProjection(Protocol):
def project(self) -> None: ...
def revoke(self) -> None: ...
def _run(argv: list[str], *, cwd: Path | None = None, environment: Mapping[str, str] | None = None) -> str:
result = subprocess.run(
argv,
cwd=cwd,
env=None if environment is None else dict(environment),
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise ContinuationExecutionError("SUBPROCESS_FAILED")
return result.stdout.strip()
@dataclass(slots=True)
class AuthorityGitAdapter:
"""Branch-name adapter over the existing narrow AuthorityGitExecutor."""
executor: AuthorityGitExecutor
@staticmethod
def _ref(branch: str) -> str:
return f"refs/heads/{branch}"
def branch_exists(self, branch: str) -> bool:
return self.executor.read_ref(self._ref(branch)) is not None
def branch_head(self, branch: str) -> str:
value = self.executor.read_ref(self._ref(branch))
if value is None:
raise ContinuationExecutionError("EXISTING_BRANCH_MISSING")
return value
def is_ancestor(self, older: str, newer: str) -> bool:
return self.executor.ancestry_check(older, newer)
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
@dataclass(slots=True)
class DetachedGitWorkspace:
path: Path
def __post_init__(self) -> None:
if not self.path.is_absolute() or self.path.is_symlink() or not self.path.is_dir():
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
top = Path(_run(["git", "rev-parse", "--show-toplevel"], cwd=self.path)).resolve()
if top != self.path.resolve():
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
def is_detached(self) -> bool:
result = subprocess.run(
["git", "symbolic-ref", "-q", "HEAD"],
cwd=self.path,
capture_output=True,
check=False,
)
return result.returncode == 1
def head_oid(self) -> str:
return _run(["git", "rev-parse", "HEAD"], cwd=self.path)
def _dirty_paths(self) -> set[str]:
command = subprocess.run(
["git", "status", "--porcelain=v1", "--untracked-files=all"],
cwd=self.path,
text=True,
capture_output=True,
check=False,
)
if command.returncode != 0:
raise ContinuationExecutionError("SUBPROCESS_FAILED")
output = command.stdout
result: set[str] = set()
for line in output.splitlines():
path = line[3:]
if " -> " in path:
path = path.split(" -> ", 1)[1]
result.add(path)
return result
def materialize_files(self, files: Mapping[str, bytes]) -> None:
dirty = self._dirty_paths()
expected_paths = set(files)
if dirty and dirty != expected_paths:
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
for relative, content in files.items():
target = (self.path / relative).resolve()
try:
target.relative_to(self.path.resolve())
except ValueError as exc:
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
if target.is_symlink():
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
def commit_files(self, files: Mapping[str, bytes], message: str) -> str:
expected_paths = set(files)
if self._dirty_paths() != expected_paths:
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
for relative, content in files.items():
target = (self.path / relative).resolve()
try:
target.relative_to(self.path.resolve())
except ValueError as exc:
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
if not target.is_file() or target.is_symlink() or target.read_bytes() != content:
raise ContinuationExecutionError("AUTHORING_PAYLOAD_MISMATCH")
_run(["git", "add", "--", *sorted(expected_paths)], cwd=self.path)
staged = set(_run(["git", "diff", "--cached", "--name-only"], cwd=self.path).splitlines())
if staged != expected_paths:
raise ContinuationExecutionError("STAGED_SCOPE_MISMATCH")
environment = os.environ.copy()
environment.update(
{
"GIT_AUTHOR_NAME": "AIOS Governed Continuation",
"GIT_AUTHOR_EMAIL": "aios-governed-continuation@localhost",
"GIT_COMMITTER_NAME": "AIOS Governed Continuation",
"GIT_COMMITTER_EMAIL": "aios-governed-continuation@localhost",
}
)
_run(["git", "commit", "--no-gpg-sign", "-m", message, "--", *sorted(expected_paths)], cwd=self.path, environment=environment)
if not self.is_detached():
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
return self.head_oid()
@dataclass(slots=True)
class ServerSideCredentialProjection:
ssh_key_path: Path
api_token_path: Path
active: bool = False
def project(self) -> None:
for path in (self.ssh_key_path, self.api_token_path):
if path.is_symlink() or not path.is_file() or path.stat().st_size < 32:
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
self.active = True
def revoke(self) -> None:
self.active = False
@dataclass(slots=True)
class GiteaContinuationRemote:
workspace: Path
credential: ServerSideCredentialProjection
remote_name: str = "gitea"
repository: str = "aios/aios"
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
expected_host: str = "g.cnnetmkt.top"
@staticmethod
def validate_remote_url(value: str, expected_host: str = "g.cnnetmkt.top") -> None:
if value.startswith(("/", "file:")) or value.endswith("/data/aios-gitops/repo.git"):
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
if "://" in value:
parsed = urlsplit(value)
host, path = parsed.hostname, parsed.path
else:
location = value.split("@", 1)[-1]
host, separator, path = location.partition(":")
if not separator:
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
path = "/" + path
if host != expected_host or path.rstrip("/") not in {"/aios/aios.git", "/git/aios/aios.git"}:
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
def repository_identity(self) -> str:
url = _run(["git", "remote", "get-url", self.remote_name], cwd=self.workspace)
self.validate_remote_url(url, self.expected_host)
if self.repository != "aios/aios":
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
return self.repository
def _require_projected(self) -> None:
if not self.credential.active:
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
def read_git_ref(self, branch: str) -> str | None:
self._require_projected()
output = _run(["git", "ls-remote", self.remote_name, f"refs/heads/{branch}"], cwd=self.workspace)
if not output:
return None
oid, ref = output.split(maxsplit=1)
if ref != f"refs/heads/{branch}" or not OID_RE.fullmatch(oid):
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_exact(self, branch: str, expected: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) != expected:
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
def _api(self, path: str) -> object:
self._require_projected()
token = self.credential.api_token_path.read_text(encoding="utf-8").strip()
request = Request(
self.api_base.rstrip("/") + path,
headers={"Accept": "application/json", "Authorization": f"token {token}"},
)
try:
with urlopen(request, timeout=20) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
if exc.code == 404:
return None
raise ContinuationExecutionError(f"GITEA_API_HTTP_{exc.code}") from exc
except (URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
raise ContinuationExecutionError("GITEA_API_UNAVAILABLE") from exc
def read_api_branch(self, branch: str) -> str | None:
owner, name = self.repository.split("/", 1)
page = 1
while page <= 100:
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/branches?page={page}&limit=50")
if value is None:
return None
if not isinstance(value, list):
raise ContinuationExecutionError("GITEA_API_BRANCH_COLLECTION_INVALID")
for item in value:
if not isinstance(item, dict) or item.get("name") != branch:
continue
oid = item.get("commit", {}).get("id")
if not isinstance(oid, str) or not OID_RE.fullmatch(oid):
raise ContinuationExecutionError("GITEA_API_BRANCH_INVALID")
return oid
if len(value) < 50:
return None
page += 1
raise ContinuationExecutionError("GITEA_API_BRANCH_PAGINATION_LIMIT")
def commit_visible(self, oid: str) -> bool:
owner, name = self.repository.split("/", 1)
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/git/commits/{oid}")
if not isinstance(value, dict):
return False
observed = value.get("sha") or value.get("id")
return observed == oid
@dataclass(frozen=True, slots=True)
class ContinuationPermissions:
create_target_branch: bool = False
force_push: bool = False
tag_write: bool = False
baseline_write: bool = False
production_write: bool = False
main_write: bool = False
def validate(self) -> None:
if any(asdict(self).values()):
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
@dataclass(frozen=True, slots=True)
class ContinuationRuntimeBinding:
request: WriterLeaseRequestV2
workspace_path: Path
lease_not_before: datetime
lease_expires_at: datetime
original_receipt_valid: bool
original_grant_consumed: bool
permissions: ContinuationPermissions
@dataclass(frozen=True, slots=True)
class ContinuationCommit:
idempotency_key: str
files: Mapping[str, bytes]
message: str
def payload(self) -> bytes:
if not self.idempotency_key or not self.message.strip() or not self.files:
raise ContinuationExecutionError("COMMIT_REQUEST_INVALID")
normalized: dict[str, str] = {}
for path, content in self.files.items():
pure = PurePosixPath(path)
if (
not isinstance(path, str)
or pure.is_absolute()
or ".." in pure.parts
or path.startswith((".git/", "baseline/", "production/"))
or not isinstance(content, bytes)
):
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
if any(pattern.search(content) for pattern in SECRET_PATTERNS):
raise ContinuationExecutionError("SECRET_LIKE_PAYLOAD_REJECTED")
normalized[path] = base64.b64encode(content).decode("ascii")
return json.dumps(
{"files_base64": normalized, "message": self.message},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
class ContinuationExecutor:
def __init__(
self,
controller: GitOpsJobController,
authority: AuthorityPort,
workspace: DetachedWorkspace,
remote: RemotePort,
credential: CredentialProjection,
*,
now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
) -> None:
self.controller = controller
self.authority = authority
self.workspace = workspace
self.remote = remote
self.credential = credential
self.now = now
self._observed_generations: dict[str, int] = {}
def observe_generation(self, branch: str, generation: int) -> None:
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
def _max_generation(self, branch: str) -> int:
observed = self._observed_generations.get(branch, 0)
with self.controller.store._connect() as connection:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
durable = int(row["generation"] or 0)
return max(observed, durable)
@staticmethod
def _request_mapping(request: WriterLeaseRequestV2) -> dict[str, object]:
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
"expected_branch_oid": request.expected_branch_oid,
"target_branch": request.target_branch,
"generation": request.generation,
"lease_seconds": request.lease_seconds,
}
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
request = binding.request
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
if not binding.original_receipt_valid or not binding.original_grant_consumed:
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
binding.permissions.validate()
now = self.now()
if now < binding.lease_not_before:
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
if now >= binding.lease_expires_at:
raise ContinuationExecutionError("LEASE_EXPIRED")
if request.generation <= self._max_generation(request.target_branch):
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
if not self.workspace.is_detached():
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
if self.workspace.head_oid() != request.expected_branch_oid:
raise ContinuationExecutionError("WORKTREE_HEAD_MISMATCH")
if self.remote.repository_identity() != request.repository:
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
def _job_request(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit, payload: bytes) -> dict[str, object]:
request = binding.request
return {
"idempotency_key": commit.idempotency_key,
"campaign_id": request.task_id,
"task_id": request.task_id,
"repository": request.repository,
"operation": "CONTINUATION_COMMIT_FILES",
"branch": request.target_branch,
"expected_branch_oid": request.expected_branch_oid,
"base_oid": request.base_oid,
"branch_mode": request.branch_mode,
"payload_digest": hashlib.sha256(payload).hexdigest(),
"lineage_anchor_oid": request.base_oid,
"generation": request.generation,
}
def execute(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit) -> dict[str, object]:
payload = commit.payload()
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
current = self.controller.store.read(submitted.job_id)
if JobState(current["state"]) == JobState.COMPLETED:
return self.controller.store._receipt(current, duplicate=True).as_dict()
self._validate_preflight(binding)
request = binding.request
projected = False
try:
self.credential.project()
projected = True
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.REQUESTED:
self.controller.advance(
submitted.job_id,
JobState.AUTHORIZED,
side_effect_state="AUTHORIZED",
receipt={"task_id": request.task_id, "original_grant_consumed": True},
)
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
self.controller.lease_job(
submitted.job_id,
lease_id=f"lease-{submitted.job_id}",
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
expected_revision=int(row["revision"]),
)
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
new_oid = self.workspace.commit_files(commit.files, commit.message)
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
if authority_oid != new_oid:
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
self.controller.advance(
submitted.job_id,
JobState.LOCAL_VERIFIED,
side_effect_state="LOCAL_VERIFIED",
receipt={"authority_oid": authority_oid, "force_push": False},
)
try:
self.controller.advance(submitted.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
except Exception:
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
finally:
if projected:
self.credential.revoke()
def _observe_remote(self, branch: str, oid: str) -> tuple[str | None, str | None, bool]:
return self.remote.read_git_ref(branch), self.remote.read_api_branch(branch), self.remote.commit_visible(oid)
def _remote_pending(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
receipt = {
"authority_oid": authority_oid,
"desired_remote_oid": new_oid,
"remote_verified": False,
"force_push": False,
}
result = self.controller.advance(
job_id,
JobState.RECONCILIATION_REQUIRED,
side_effect_state="REMOTE_PUSH_PENDING",
receipt=receipt,
)
return result.as_dict()
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
row = self.controller.store.read(job_id)
branch = row["branch"]
request = json.loads(row["request_json"])["binding"]
git_oid, api_oid, visible = self._observe_remote(branch, new_oid)
decision = decide_reconciliation(RemoteObservation("aios/aios", branch, new_oid, authority_oid, git_oid, api_oid, visible, datetime.now(timezone.utc).isoformat(), request.get("expected_branch_oid")))
if decision.action is not ReconciliationAction.MARK_REMOTE_VERIFIED:
return self._remote_pending(job_id, authority_oid, new_oid)
receipt = {
"authority_oid": authority_oid,
"git_remote_oid": git_oid,
"api_branch_oid": api_oid,
"commit_visible": True,
"remote_verified": True,
"reconciliation_decision": decision.action.value,
"force_push": False,
}
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
return self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt=receipt).as_dict()
def reconcile(self, job_id: str) -> dict[str, object]:
row = self.controller.store.read(job_id)
state = JobState(row["state"])
if state == JobState.COMPLETED:
return self.controller.store._receipt(row, duplicate=True).as_dict()
if state != JobState.RECONCILIATION_REQUIRED:
raise ContinuationExecutionError("JOB_NOT_RECONCILABLE")
request = json.loads(row["request_json"])["binding"]
receipt = json.loads(row["receipt_json"])
new_oid = receipt["desired_remote_oid"]
authority_oid = self.authority.branch_head(row["branch"])
try:
generation = int(request.get("generation") or 0)
except (TypeError, ValueError) as exc:
raise ContinuationExecutionError("DURABLE_BINDING_GENERATION_INVALID") from exc
if generation > 0:
with self.controller.store._connect() as connection:
earlier = connection.execute(
"SELECT 1 FROM jobs WHERE branch=? AND state=? "
"AND CAST(json_extract(request_json, '$.binding.generation') AS INTEGER) < ? LIMIT 1",
(row["branch"], JobState.RECONCILIATION_REQUIRED.value, generation),
).fetchone()
if earlier is not None:
raise ContinuationExecutionError("EARLIER_RECONCILIATION_REQUIRED")
self.credential.project()
try:
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
if authority_oid == git_oid == api_oid == new_oid and visible:
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
return self._finish_remote_verification(job_id, new_oid, new_oid)
expected_oid = request["expected_branch_oid"]
if (
self.authority.is_ancestor(new_oid, authority_oid)
and git_oid is not None
and git_oid == api_oid
and self.authority.is_ancestor(git_oid, new_oid)
):
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_CHAIN")
self.remote.push_exact(row["branch"], git_oid, new_oid)
return self._finish_remote_verification(job_id, new_oid, new_oid)
if authority_oid == new_oid and git_oid == api_oid == expected_oid:
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
self.remote.push_exact(row["branch"], expected_oid, new_oid)
return self._finish_remote_verification(job_id, new_oid, new_oid)
return self.controller.store._receipt(row, duplicate=True).as_dict()
finally:
self.credential.revoke()
@@ -1,154 +0,0 @@
"""Campaign-aware continuation leases for an existing Git branch.
This module is deliberately transport-neutral. It validates branch lineage
and hands ref mutation to an injected compare-and-swap broker; it does not run
Git, inspect credentials, or create a branch itself.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone
import re
from typing import Protocol
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
CAMPAIGN_HEAD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
class ContinuationError(RuntimeError):
"""Stable fail-closed error for lease or branch binding violations."""
class BranchProbe(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
class RefCAS(Protocol):
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise ContinuationError(f"{field}_invalid")
return value
@dataclass(frozen=True, slots=True)
class WriterLeaseRequestV2:
task_id: str
repository: str
branch_mode: str
base_oid: str
expected_branch_oid: str
target_branch: str
generation: int
lease_seconds: int = 900
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
_oid(self.expected_branch_oid, "expected_branch_oid")
if self.target_branch in {"main", "master"} or self.target_branch.startswith(("refs/", "tags/")):
raise ContinuationError("protected_branch_rejected")
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
raise ContinuationError("generation_invalid")
if not isinstance(self.lease_seconds, int) or not 1 <= self.lease_seconds <= 86_400:
raise ContinuationError("lease_duration_invalid")
if self.branch_mode == "CONTINUE_EXISTING":
if not probe.branch_exists(self.target_branch):
raise ContinuationError("EXISTING_BRANCH_MISSING")
actual = _oid(probe.branch_head(self.target_branch), "actual_branch_oid")
if actual != self.expected_branch_oid:
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
if not probe.is_ancestor(self.base_oid, actual):
raise ContinuationError("BASE_NOT_ANCESTOR_OF_BRANCH")
elif hasattr(probe, "commit_exists") and not probe.commit_exists(self.base_oid):
raise ContinuationError("BASE_COMMIT_NOT_FOUND")
elif self.branch_mode == "CREATE_NEW" and probe.branch_exists(self.target_branch):
raise ContinuationError("TARGET_BRANCH_ALREADY_EXISTS")
elif self.branch_mode == "CREATE_NEW" and self.base_oid != self.expected_branch_oid:
raise ContinuationError("CREATE_NEW_BASE_EXPECTATION_MISMATCH")
@dataclass(frozen=True, slots=True)
class CampaignWriterLease:
task_id: str
repository: str
branch: str
lineage_anchor_oid: str
current_head_oid: str
generation: int
lease_expires_at: datetime
append_only: bool = True
renewable: bool = True
multi_commit: bool = True
recoverable: bool = True
state: str = "ACTIVE"
branch_bound: bool = True
def __post_init__(self) -> None:
_oid(self.lineage_anchor_oid, "lineage_anchor_oid")
_oid(self.current_head_oid, "current_head_oid")
if not all((self.append_only, self.renewable, self.multi_commit, self.recoverable, self.branch_bound)):
raise ContinuationError("lease_capability_downgrade")
class ContinuationLeaseManager:
"""State machine for a multi-commit lease with CAS-only ref updates."""
def __init__(self, probe: BranchProbe, refs: RefCAS, *, now=datetime.now) -> None:
self.probe = probe
self.refs = refs
self.now = now
def open(self, request: WriterLeaseRequestV2) -> CampaignWriterLease:
request.validate(self.probe)
if request.branch_mode != "CONTINUE_EXISTING":
raise ContinuationError("CREATE_NEW_REQUIRES_SEPARATE_PROVISIONER")
return CampaignWriterLease(
task_id=request.task_id,
repository=request.repository,
branch=request.target_branch,
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=self.now(timezone.utc) + timedelta(seconds=request.lease_seconds),
)
def renew(self, lease: CampaignWriterLease, seconds: int) -> CampaignWriterLease:
if lease.state != "ACTIVE" or not lease.renewable or seconds < 1:
raise ContinuationError("lease_not_renewable")
return replace(lease, lease_expires_at=self.now(timezone.utc) + timedelta(seconds=seconds))
def append_commit(self, lease: CampaignWriterLease, new_head_oid: str) -> CampaignWriterLease:
if lease.state != "ACTIVE":
raise ContinuationError("lease_not_active")
_oid(new_head_oid, "new_head_oid")
if not self.probe.is_ancestor(lease.current_head_oid, new_head_oid):
raise ContinuationError("NON_FAST_FORWARD_COMMIT")
try:
actual = self.refs.compare_and_swap_branch(
lease.branch, lease.current_head_oid, new_head_oid
)
except Exception as exc:
raise ContinuationError("BRANCH_LEASE_STALE") from exc
if actual != new_head_oid:
raise ContinuationError("BRANCH_LEASE_STALE")
return replace(lease, current_head_oid=new_head_oid)
def supersede(self, lease: CampaignWriterLease) -> CampaignWriterLease:
if lease.state not in {"ACTIVE", "RECOVERABLE_FAILURE", "RECONCILIATION_REQUIRED"}:
raise ContinuationError("lease_not_supersedable")
return replace(lease, generation=lease.generation + 1, state="SUPERSEDED")
@@ -1,25 +0,0 @@
"""Separate read, write, and release plane readiness evaluation."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping
@dataclass(frozen=True, slots=True)
class RuntimeStatus:
read_plane: Mapping[str, bool]
write_plane: Mapping[str, bool]
release_plane: Mapping[str, bool]
@property
def release_control_plane_ready(self) -> bool:
return all(self.read_plane.values()) and all(self.write_plane.values()) and all(self.release_plane.values())
def as_dict(self) -> dict[str, object]:
return {
"READ_PLANE": dict(self.read_plane),
"WRITE_PLANE": dict(self.write_plane),
"RELEASE_PLANE": dict(self.release_plane),
"release_control_plane_ready": self.release_control_plane_ready,
}
@@ -1,58 +0,0 @@
"""Workspace-independent authority/network/Gitea ref sentinel."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
from typing import Callable, Mapping
class SentinelError(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class SentinelInputs:
authority_git_dir: Path
gitea_remote: str
gitea_api: str
ref: str
expected_oid: str
@dataclass(frozen=True, slots=True)
class SentinelResult:
authority_oid: str | None
network_oid: str | None
gitea_oid: str | None
equal: bool
error: str | None = None
def authority_oid(inputs: SentinelInputs) -> str:
result = subprocess.run(
["git", "--git-dir", str(inputs.authority_git_dir), "rev-parse", inputs.ref],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise SentinelError("AUTHORITY_REF_UNAVAILABLE")
return result.stdout.strip()
def evaluate(
inputs: SentinelInputs,
*,
network_reader: Callable[[str, str], str | None],
gitea_reader: Callable[[str, str], str | None],
) -> SentinelResult:
try:
local = authority_oid(inputs)
network = network_reader(inputs.gitea_remote, inputs.ref)
gitea = gitea_reader(inputs.gitea_api, inputs.ref)
equal = local == network == gitea == inputs.expected_oid
return SentinelResult(local, network, gitea, equal, None if equal else "OID_MISMATCH")
except SentinelError as exc:
return SentinelResult(None, None, None, False, str(exc))
@@ -1,26 +0,0 @@
"""Non-sensitive control-plane snapshot assembly."""
from __future__ import annotations
from typing import Any, Mapping
def _copy(value: Mapping[str, Any] | None, keys: tuple[str, ...]) -> dict[str, Any]:
value = value or {}
return {key: value.get(key) for key in keys}
def build_control_plane_snapshot(*, target_version: str, runtime: Mapping[str, Any] | None = None, repository: Mapping[str, Any] | None = None, production: Mapping[str, Any] | None = None, task: Mapping[str, Any] | None = None, capabilities: Mapping[str, Any] | None = None, credential_broker: Mapping[str, Any] | None = None) -> dict[str, Any]:
return {
"version": {"target_release": target_version},
"runtime": _copy(runtime, ("bridge_schema_version", "control_plane_release_version", "runtime_release_commit", "runtime_path", "artifact_sha256", "drift_status")),
"repository": _copy(repository, ("identity", "authority_git_dir", "authority_main_oid")),
"production": _copy(production, ("baseline_version", "target_oid", "deployment_state")),
"task": _copy(task, ("task_id", "branch", "authority_branch_oid", "gitea_git_oid", "gitea_api_oid", "commit_visible", "generation", "reconciliation_state")),
"capabilities": _copy(capabilities, ("legacy_writes_enabled", "governed_writes_enabled", "force_push", "main_write", "tag_write", "baseline_write", "production_write")),
"credential_broker": _copy(credential_broker, ("status", "provider")),
"secret_material_exposed": False,
}
def getControlPlaneSnapshot(**kwargs: Any) -> dict[str, Any]:
return build_control_plane_snapshot(**kwargs)
@@ -1,78 +0,0 @@
"""Metadata-only, server-side credential lease broker."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import os
from pathlib import Path
import secrets
from typing import Any, Callable, Mapping
@dataclass(frozen=True, slots=True)
class CredentialLease:
lease_id: str
task_id: str
repository: str
target_branch: str
generation: int
issued_at: str
expires_at: str
scopes: tuple[str, ...]
provider: str
state: str = "ACTIVE"
def as_dict(self) -> dict[str, Any]:
return {"lease_id": self.lease_id, "task_id": self.task_id, "repository": self.repository, "target_branch": self.target_branch, "generation": self.generation, "issued_at": self.issued_at, "expires_at": self.expires_at, "scopes": list(self.scopes), "provider": self.provider, "state": self.state}
class FileCredentialProvider:
def __init__(self, token_path: str | os.PathLike[str], *, actor: str = "aios-autonomy-agent") -> None:
self.token_path = Path(token_path)
self.actor = actor
def environment(self, lease: CredentialLease) -> dict[str, str]:
if not self.token_path.is_file():
raise RuntimeError("CREDENTIAL_PROVIDER_UNAVAILABLE")
return {"CREDENTIAL_FILE": str(self.token_path), "GITEA_MACHINE_ACTOR": self.actor}
class CredentialBroker:
def __init__(self, provider: Any, *, now: Callable[[], datetime] | None = None) -> None:
self.provider = provider
self.now = now or (lambda: datetime.now(timezone.utc))
self._leases: dict[str, CredentialLease] = {}
def issue(self, *, task_id: str, repository: str, target_branch: str, generation: int, scopes: tuple[str, ...] = ("git:push", "gitea:read"), lease_seconds: int = 900) -> dict[str, Any]:
if repository != "aios/aios" or not target_branch.startswith("agent/") or generation < 1 or not 1 <= lease_seconds <= 86400:
raise ValueError("CREDENTIAL_LEASE_BINDING_INVALID")
now = self.now().astimezone(timezone.utc)
lease = CredentialLease("lease-" + secrets.token_hex(16), task_id, repository, target_branch, generation, now.isoformat(), (now + timedelta(seconds=lease_seconds)).isoformat(), tuple(scopes), type(self.provider).__name__)
self._leases[lease.lease_id] = lease
return lease.as_dict()
def _get(self, lease_id: str) -> CredentialLease:
lease = self._leases.get(lease_id)
if lease is None:
raise KeyError("CREDENTIAL_LEASE_NOT_FOUND")
if lease.state == "ACTIVE" and self.now().astimezone(timezone.utc) >= datetime.fromisoformat(lease.expires_at):
lease = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "EXPIRED"})
self._leases[lease_id] = lease
return lease
def environment(self, lease_id: str) -> Mapping[str, str]:
lease = self._get(lease_id)
if lease.state != "ACTIVE":
raise RuntimeError("CREDENTIAL_LEASE_NOT_ACTIVE")
return self.provider.environment(lease)
def status(self, lease_id: str) -> dict[str, Any]:
return self._get(lease_id).as_dict()
def revoke(self, lease_id: str) -> dict[str, Any]:
lease = self._get(lease_id)
if lease.state == "REVOKED":
return lease.as_dict()
updated = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "REVOKED"})
self._leases[lease_id] = updated
return updated.as_dict()
@@ -1,125 +0,0 @@
"""Non-Owner Gitea machine capability and branch reconciliation boundary."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Protocol
class MachineBrokerError(RuntimeError):
pass
class GiteaTransport(Protocol):
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str: ...
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]: ...
def publish_status(self, branch: str, state: str, description: str) -> Mapping[str, Any]: ...
def protected_merge(self, pr_number: int) -> Mapping[str, Any]: ...
def read_branch(self, branch: str) -> str | None: ...
def whoami(self) -> Mapping[str, Any]: ...
def repository(self, owner: str, name: str) -> Mapping[str, Any]: ...
@dataclass(frozen=True, slots=True)
class MachineCredentialProjection:
"""Opaque systemd credential reference; the credential value is never accepted."""
reference: str
account: str
owner_account: str = "aiman"
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "MachineCredentialProjection":
if set(value) != {"scheme", "name", "account"}:
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
if value["scheme"] != "systemd-credential" or value["name"] != "gitea-machine-token":
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
return cls(f"{value['scheme']}:{value['name']}", str(value["account"]))
def __post_init__(self) -> None:
if self.reference != "systemd-credential:gitea-machine-token":
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
if not self.account or self.account == self.owner_account:
raise MachineBrokerError("OWNER_MACHINE_IDENTITY_REJECTED")
@dataclass(frozen=True, slots=True)
class CampaignCapabilityReceipt:
campaign_id: str
task_id: str
repository: str
branch: str
expected_oid: str
capabilities: frozenset[str]
denied: frozenset[str]
def require(self, capability: str) -> None:
if capability in self.denied or capability not in self.capabilities:
raise MachineBrokerError("CAPABILITY_DENIED")
if self.branch in {"main", "master"}:
raise MachineBrokerError("PROTECTED_BRANCH_DENIED")
class GiteaMachineBroker:
def __init__(self, transport: GiteaTransport, credential: MachineCredentialProjection, receipt: CampaignCapabilityReceipt) -> None:
self.transport = transport
self.credential = credential
self.receipt = receipt
def verify_identity(self) -> Mapping[str, Any]:
identity = dict(self.transport.whoami())
login = identity.get("login") or identity.get("username")
if not isinstance(login, str) or not login or login == self.credential.owner_account or login != self.credential.account:
raise MachineBrokerError("GITEA_MACHINE_IDENTITY_INVALID")
owner, name = self.receipt.repository.split("/", 1)
repo = dict(self.transport.repository(owner, name))
if repo.get("full_name") != self.receipt.repository:
raise MachineBrokerError("GITEA_REPOSITORY_MISMATCH")
return {"login": login, "repository": self.receipt.repository, "owner_account": self.credential.owner_account}
def push_branch(self, new_oid: str) -> str:
self.receipt.require("exact_task_branch_push")
return self.transport.compare_and_swap_branch(self.receipt.branch, self.receipt.expected_oid, new_oid)
def create_pr(self, title: str, body: str) -> Mapping[str, Any]:
self.receipt.require("pr_create_update")
return self.transport.create_or_update_pr(self.receipt.branch, "main", title, body)
def status(self, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
self.receipt.require("status_publish")
return self.transport.publish_status(self.receipt.branch, state, description, context)
def merge(self, pr_number: int) -> Mapping[str, Any]:
self.receipt.require("protected_merge")
return self.transport.protected_merge(pr_number)
def readback(self) -> str | None:
self.receipt.require("remote_readback")
return self.transport.read_branch(self.receipt.branch)
def reconcile_exact_branch(authority_oid: str, gitea_oid: str | None, expected_oid: str) -> str:
if authority_oid != expected_oid:
return "AUTHORITY_EXPECTATION_MISMATCH"
if gitea_oid is None:
return "CREATE_REQUIRED"
if gitea_oid == expected_oid:
return "ALREADY_IN_SYNC"
return "MANUAL_OWNER_REVIEW_REQUIRED"
def reconcile_fast_forward(
remote_oid: str | None,
expected_old_oid: str,
new_oid: str,
*,
old_is_ancestor_of_new: bool,
) -> str:
"""Return a bounded action for an active Job/lease branch scope."""
if remote_oid is None:
return "CREATE_REQUIRED"
if remote_oid == new_oid:
return "ALREADY_IN_SYNC"
if remote_oid == expected_old_oid and old_is_ancestor_of_new:
return "FAST_FORWARD_REQUIRED"
return "MANUAL_OWNER_REVIEW_REQUIRED"
@@ -1,208 +0,0 @@
"""Gitea HTTP/Git transport using only a projected systemd credential."""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
import tempfile
from typing import Any, Mapping
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
class GiteaTransportError(RuntimeError):
pass
def _safe_oid(value: str, field: str) -> str:
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise GiteaTransportError(f"{field}_INVALID")
return value
@dataclass(slots=True)
class GiteaMachineTransport:
api_base: str
repository_id: str
remote_url: str
authority_git_dir: Path
credential_name: str = "gitea-machine-token"
actor: str = "aios-autonomy-agent"
timeout: int = 20
expected_merge_head: str | None = None
expected_merge_base: str | None = None
def __post_init__(self) -> None:
if self.credential_name != "gitea-machine-token":
raise GiteaTransportError("CREDENTIAL_NAME_INVALID")
if "/" not in self.repository_id or self.repository_id.startswith("/"):
raise GiteaTransportError("REPOSITORY_INVALID")
if not self.api_base.startswith("https://") and not self.api_base.startswith("http://127.0.0.1"):
raise GiteaTransportError("GITEA_API_SCOPE_INVALID")
if not self.remote_url.startswith("https://"):
raise GiteaTransportError("GITEA_REMOTE_SCOPE_INVALID")
@property
def _credential_path(self) -> Path:
directory = os.environ.get("CREDENTIALS_DIRECTORY", "")
if not directory:
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
path = Path(directory) / self.credential_name
if path.is_symlink() or not path.is_file():
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
return path
def _token(self) -> str:
value = self._credential_path.read_text(encoding="utf-8").strip()
if not value:
raise GiteaTransportError("CREDENTIAL_EMPTY")
return value
def _request(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> tuple[int, Any]:
data = None if body is None else json.dumps(dict(body), separators=(",", ":")).encode()
headers = {"Accept": "application/json", "Authorization": f"token {self._token()}"}
if data is not None:
headers["Content-Type"] = "application/json"
request = Request(self.api_base.rstrip("/") + path, data=data, headers=headers, method=method)
try:
with urlopen(request, timeout=self.timeout) as response:
raw = response.read()
return response.status, json.loads(raw.decode()) if raw else {}
except HTTPError as exc:
# Never include response bodies: Gitea may echo sensitive request context.
raise GiteaTransportError(f"GITEA_HTTP_{exc.code}") from exc
except (URLError, TimeoutError, OSError) as exc:
raise GiteaTransportError("GITEA_NETWORK_ERROR") from exc
def whoami(self) -> Mapping[str, Any]:
return self._request("GET", "/user")[1]
def repository_info(self) -> Mapping[str, Any]:
owner, name = self.repository_id.split("/", 1)
return self._request("GET", f"/repos/{quote(owner)}/{quote(name)}")[1]
def repository(self, owner: str, name: str) -> Mapping[str, Any]:
if f"{owner}/{name}" != self.repository_id:
raise GiteaTransportError("REPOSITORY_SCOPE_DENIED")
return self.repository_info()
def read_branch(self, branch: str) -> str | None:
owner, name = self.repository_id.split("/", 1)
try:
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/branches/{quote(branch, safe='/')}")[1]
except GiteaTransportError as exc:
if str(exc) == "GITEA_HTTP_404":
return None
raise
sha = value.get("commit", {}).get("id") if isinstance(value, dict) else None
return _safe_oid(sha, "remote_branch_oid")
def _askpass(self) -> tuple[Path, dict[str, str]]:
credential = self._credential_path
fd, name = tempfile.mkstemp(prefix="aios-gitea-askpass-", dir="/tmp")
path = Path(name)
os.fchmod(fd, 0o700)
script = "#!/bin/sh\ncase \"$1\" in *Username*) printf '%s\\n' \"$GITEA_MACHINE_ACTOR\" ;; *) cat \"$CREDENTIAL_FILE\" ;; esac\n"
with os.fdopen(fd, "w", encoding="utf-8") as stream:
stream.write(script)
stream.flush()
os.fsync(stream.fileno())
return path, {
"GIT_ASKPASS": str(path),
"GIT_TERMINAL_PROMPT": "0",
"GITEA_MACHINE_ACTOR": self.actor,
"CREDENTIAL_FILE": str(credential),
}
def _push_branch_fast_forward(self, branch: str, new_oid: str) -> None:
askpass, additions = self._askpass()
environment = os.environ.copy()
environment.update(additions)
try:
result = subprocess.run(
["git", f"--git-dir={self.authority_git_dir}", "push", self.remote_url, f"{new_oid}:refs/heads/{branch}"],
text=True,
capture_output=True,
check=False,
env=environment,
timeout=self.timeout,
)
if result.returncode != 0:
raise GiteaTransportError("GITEA_BRANCH_CREATE_FAILED")
finally:
askpass.unlink(missing_ok=True)
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str:
_safe_oid(expected_oid, "expected_oid")
_safe_oid(new_oid, "new_oid")
current = self.read_branch(branch)
if current == new_oid:
return new_oid
if current is None:
self._push_branch_fast_forward(branch, new_oid)
readback = self.read_branch(branch)
if readback != new_oid:
raise GiteaTransportError("GITEA_BRANCH_CREATE_READBACK_MISMATCH")
return readback
if current != expected_oid:
raise GiteaTransportError("GITEA_BRANCH_EXPECTATION_MISMATCH")
# The preflight read binds the expected old tip. The Git receive-pack
# then enforces the actual fast-forward CAS while transferring the new
# objects; no force/lease override or arbitrary ref is permitted.
self._push_branch_fast_forward(branch, new_oid)
readback = self.read_branch(branch)
if readback != new_oid:
raise GiteaTransportError("GITEA_BRANCH_CAS_READBACK_MISMATCH")
return readback
def _pulls(self) -> list[Mapping[str, Any]]:
owner, name = self.repository_id.split("/", 1)
query = urlencode({"state": "all", "base_branch": "main", "limit": 100})
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls?{query}")[1]
return value if isinstance(value, list) else []
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]:
if target != "main":
raise GiteaTransportError("PR_BASE_SCOPE_DENIED")
head = self.read_branch(branch)
if head is None:
raise GiteaTransportError("PR_HEAD_MISSING")
matches = [p for p in self._pulls() if (p.get("head") or {}).get("ref") == branch and (p.get("base") or {}).get("ref") == target]
open_matches = [p for p in matches if p.get("state") == "open"]
if len(open_matches) > 1:
raise GiteaTransportError("MULTIPLE_MATCHING_PRS")
if open_matches:
if (open_matches[0].get("head") or {}).get("sha") != head:
raise GiteaTransportError("PR_HEAD_CONFLICT")
return open_matches[0]
owner, name = self.repository_id.split("/", 1)
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls", {"head": branch, "base": target, "title": title, "body": body, "allow_maintainer_edit": False})[1]
def publish_status(self, branch: str, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
oid = self.read_branch(branch)
if oid is None:
raise GiteaTransportError("STATUS_BRANCH_MISSING")
owner, name = self.repository_id.split("/", 1)
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/statuses/{oid}", {"state": state, "context": context, "description": description})[1]
def protected_merge(self, pr_number: int) -> Mapping[str, Any]:
if not isinstance(pr_number, int) or pr_number < 1:
raise GiteaTransportError("PR_NUMBER_INVALID")
owner, name = self.repository_id.split("/", 1)
pr = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}")[1]
if pr.get("state") != "open" or (pr.get("base") or {}).get("ref") != "main":
raise GiteaTransportError("PR_MERGE_BINDING_INVALID")
if pr.get("mergeable") is False:
raise GiteaTransportError("PR_NOT_MERGEABLE")
head_oid = (pr.get("head") or {}).get("sha")
base_oid = (pr.get("base") or {}).get("sha")
if self.expected_merge_head is not None and head_oid != self.expected_merge_head:
raise GiteaTransportError("PR_HEAD_EXPECTATION_MISMATCH")
if self.expected_merge_base is not None and base_oid != self.expected_merge_base:
raise GiteaTransportError("PR_BASE_EXPECTATION_MISMATCH")
body = {"do": "fast-forward-only", "force_merge": False, "merge_when_checks_succeed": False, "head_commit_id": head_oid}
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}/merge", body)[1]
@@ -1,93 +0,0 @@
"""Canonical Gitea transport and slash-branch verification."""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
from typing import Any, Callable, Mapping, Optional
from urllib.parse import quote
from urllib.request import Request, urlopen
from aios_gitops_reconciliation_core import RemoteObservation
@dataclass(frozen=True, slots=True)
class GiteaPublisherConfig:
repository: str = "aios/aios"
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
remote_name: str = "gitea-http"
class GiteaPublisher:
def __init__(self, workspace: Path, *, config: GiteaPublisherConfig = GiteaPublisherConfig(), credential: Any = None, api_get: Callable[[str], Any] | None = None, git_ref: Callable[[str], Optional[str]] | None = None) -> None:
self.workspace = Path(workspace)
self.config = config
self.credential = credential
self._api_get = api_get
self._git_ref = git_ref
@staticmethod
def validate_repository(repository: str) -> None:
if repository != "aios/aios":
raise ValueError("REPOSITORY_IDENTITY_MISMATCH")
@staticmethod
def validate_branch(branch: str) -> None:
if not isinstance(branch, str) or not branch.startswith("agent/") or branch in {"main", "master"}:
raise ValueError("BRANCH_SCOPE_DENIED")
def repository_identity(self) -> str:
self.validate_repository(self.config.repository)
return self.config.repository
def _environment(self) -> dict[str, str]:
if self.credential is not None and hasattr(self.credential, "environment"):
return dict(self.credential.environment())
return {}
def read_git_ref(self, branch: str) -> Optional[str]:
self.validate_branch(branch)
if self._git_ref is not None:
return self._git_ref(branch)
result = subprocess.run(["git", "-C", str(self.workspace), "ls-remote", self.config.remote_name, f"refs/heads/{branch}"], text=True, capture_output=True, check=False, env={**os.environ, **self._environment(), "GIT_TERMINAL_PROMPT": "0"})
if result.returncode != 0 or not result.stdout.strip():
return None
return result.stdout.split()[0]
def _api(self, path: str) -> Any:
if self._api_get is not None:
return self._api_get(path)
headers = {"Accept": "application/json"}
env = self._environment()
if env.get("AIOS_GITEA_TOKEN"):
headers["Authorization"] = "token " + env["AIOS_GITEA_TOKEN"]
with urlopen(Request(self.config.api_base.rstrip("/") + path, headers=headers), timeout=20) as response:
return json.loads(response.read() or b"{}")
def read_api_branch(self, branch: str) -> Optional[str]:
self.validate_branch(branch)
for page in range(1, 101):
value = self._api(f"/repos/aios/aios/branches?page={page}&limit=50")
if not isinstance(value, list):
return None
for item in value:
if isinstance(item, Mapping) and item.get("name") == branch:
commit = item.get("commit")
return commit.get("id") if isinstance(commit, Mapping) else None
if len(value) < 50:
return None
raise RuntimeError("GITEA_BRANCH_PAGINATION_LIMIT")
def commit_visible(self, oid: str) -> bool:
if not isinstance(oid, str) or len(oid) != 40:
return False
try:
value = self._api(f"/repos/aios/aios/git/commits/{quote(oid, safe='')}")
return isinstance(value, Mapping) and value.get("sha", oid) == oid
except Exception:
return False
def observe(self, branch: str, desired_oid: str, authority_oid: Optional[str], *, expected_oid: Optional[str] = None, observed_at: str = "") -> RemoteObservation:
return RemoteObservation(self.repository_identity(), branch, desired_oid, authority_oid, self.read_git_ref(branch), self.read_api_branch(branch), self.commit_visible(desired_oid), observed_at, expected_oid)
@@ -1,602 +0,0 @@
"""SQLite-backed durable GitOps Job Controller.
The controller owns durable intent, idempotency, lease metadata, state
transitions, receipts, and payload references. A worker owns Git side effects
behind the campaign capability and branch-CAS boundaries.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import hashlib
import json
import os
from pathlib import Path
import sqlite3
import tempfile
from datetime import datetime, timezone
from typing import Any, Callable, Mapping
class JobError(RuntimeError):
pass
class ServiceUnavailable(JobError):
pass
class BindingConflict(JobError):
pass
class JobState(str, Enum):
REQUESTED = "REQUESTED"
AUTHORIZED = "AUTHORIZED"
LEASED = "LEASED"
WORKTREE_READY = "WORKTREE_READY"
EXECUTING = "EXECUTING"
LOCAL_VERIFIED = "LOCAL_VERIFIED"
REMOTE_SYNCING = "REMOTE_SYNCING"
REMOTE_VERIFIED = "REMOTE_VERIFIED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
RECOVERABLE_FAILURE = "RECOVERABLE_FAILURE"
RECONCILING = "RECONCILING"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
MANUAL_OWNER_REVIEW_REQUIRED = "MANUAL_OWNER_REVIEW_REQUIRED"
_ORDER = {
JobState.REQUESTED: 0,
JobState.AUTHORIZED: 1,
JobState.LEASED: 2,
JobState.WORKTREE_READY: 3,
JobState.EXECUTING: 4,
JobState.LOCAL_VERIFIED: 5,
JobState.REMOTE_SYNCING: 6,
JobState.REMOTE_VERIFIED: 7,
JobState.COMPLETED: 8,
}
_RECOVERABLE = {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}
_TERMINAL = {
JobState.COMPLETED,
JobState.FAILED,
JobState.CANCELLED,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
).encode("utf-8")
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value)).hexdigest()
class PayloadStore:
"""Content-addressed filesystem payloads; raw content never enters SQLite."""
def __init__(self, root: str | os.PathLike[str]) -> None:
self.root = Path(root)
if not self.root.is_absolute() or self.root.is_symlink():
raise JobError("PAYLOAD_ROOT_INVALID")
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(self.root, 0o700)
def put(self, content: bytes, expected_digest: str) -> tuple[str, int]:
digest = hashlib.sha256(content).hexdigest()
if digest != expected_digest:
raise JobError("PAYLOAD_DIGEST_MISMATCH")
if len(expected_digest) != 64 or any(ch not in "0123456789abcdef" for ch in expected_digest):
raise JobError("PAYLOAD_DIGEST_INVALID")
target = self.root / expected_digest
if target.is_symlink():
raise JobError("PAYLOAD_PATH_SYMLINK")
if target.is_file():
if target.stat().st_size != len(content) or hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise JobError("PAYLOAD_EXISTING_MISMATCH")
return f"sha256/{digest}", len(content)
fd, temporary = tempfile.mkstemp(prefix=".payload-", dir=self.root)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
os.chmod(target, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return f"sha256/{digest}", len(content)
def read_verified(self, reference: str, expected_digest: str, expected_size: int) -> bytes:
if reference != f"sha256/{expected_digest}":
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
path = self.root / expected_digest
if path.is_symlink() or not path.is_file():
raise JobError("PAYLOAD_MISSING")
content = path.read_bytes()
if len(content) != expected_size or hashlib.sha256(content).hexdigest() != expected_digest:
raise JobError("PAYLOAD_VERIFY_FAILED")
return content
@dataclass(frozen=True, slots=True)
class JobReceipt:
job_id: str
idempotency_key: str
state: JobState
side_effect_state: str
http_status: int
revision: int
receipt: Mapping[str, Any] | None = None
def as_dict(self) -> dict[str, Any]:
return {
"job_id": self.job_id,
"idempotency_key": self.idempotency_key,
"state": self.state.value,
"side_effect_state": self.side_effect_state,
"http_status": self.http_status,
"revision": self.revision,
"receipt": dict(self.receipt) if self.receipt else None,
}
class JobStore:
SCHEMA_VERSION = 1
def __init__(self, root: str | os.PathLike[str], *, payload_root: str | os.PathLike[str] | None = None) -> None:
self.root = Path(root)
if not self.root.is_absolute() or self.root.is_symlink():
raise JobError("JOB_ROOT_INVALID")
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(self.root, 0o700)
self.db_path = self.root / "state.sqlite3"
self.payloads = PayloadStore(payload_root or self.root.parent / "payloads" / "sha256")
self._initialize()
def _connect(self) -> sqlite3.Connection:
if self.db_path.is_symlink():
raise ServiceUnavailable("JOB_DB_SYMLINK")
connection = sqlite3.connect(self.db_path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _initialize(self) -> None:
try:
with self._connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS jobs (
job_id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE,
campaign_id TEXT NOT NULL, repository TEXT NOT NULL, operation TEXT NOT NULL,
branch TEXT NOT NULL, expected_branch_oid TEXT NOT NULL,
payload_digest TEXT NOT NULL, payload_ref TEXT, payload_size INTEGER,
state TEXT NOT NULL, side_effect_state TEXT NOT NULL,
revision INTEGER NOT NULL, request_json TEXT NOT NULL,
receipt_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS idempotency_keys (
key TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
binding_json TEXT NOT NULL, created_at TEXT NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS branch_leases (
lease_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
branch TEXT NOT NULL, lineage_anchor_oid TEXT NOT NULL,
current_head_oid TEXT NOT NULL, generation INTEGER NOT NULL,
lease_expires_at TEXT NOT NULL, state TEXT NOT NULL,
revision INTEGER NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS state_transitions (
sequence INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT NOT NULL,
from_state TEXT, to_state TEXT NOT NULL, revision INTEGER NOT NULL,
side_effect_state TEXT NOT NULL, created_at TEXT NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS receipts (
receipt_id TEXT PRIMARY KEY, job_id TEXT NOT NULL,
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '1');
"""
)
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def _mirror(self, job: Mapping[str, Any]) -> None:
directory = self.root / str(job["job_id"])
directory.mkdir(mode=0o700, exist_ok=True)
(directory / "receipts").mkdir(mode=0o700, exist_ok=True)
(directory / "logs-digest").mkdir(mode=0o700, exist_ok=True)
for name, value in (("request.json", job["request"]), ("state.json", job)):
target = directory / name
fd, temporary = tempfile.mkstemp(prefix=".mirror-", dir=directory)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(canonical_json(value))
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def read(self, job_id: str) -> dict[str, Any]:
try:
with self._connect() as connection:
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
if row is None:
raise JobError("JOB_NOT_FOUND")
return dict(row)
def _receipt(self, row: Mapping[str, Any], *, duplicate: bool) -> JobReceipt:
state = JobState(row["state"])
if state == JobState.COMPLETED:
status, side = 200, "COMPLETED"
elif state in _RECOVERABLE:
status = 202
side = row["side_effect_state"] if row["side_effect_state"] == "REMOTE_PUSH_PENDING" else "RESUME_SAME_JOB"
elif row["side_effect_state"] == "UNKNOWN":
status, side = 503, "UNKNOWN"
else:
status, side = 202, "SAME_JOB" if duplicate else "QUEUED"
receipt = json.loads(row["receipt_json"]) if row["receipt_json"] else None
return JobReceipt(row["job_id"], row["idempotency_key"], state, side, status, int(row["revision"]), receipt)
class GitOpsJobController:
def __init__(self, store: JobStore) -> None:
self.store = store
@staticmethod
def _binding(request: Mapping[str, Any]) -> dict[str, str]:
fields = ("campaign_id", "repository", "operation", "branch", "expected_branch_oid", "payload_digest")
if any(not isinstance(request.get(field), str) or not request[field].strip() for field in fields):
raise JobError("JOB_BINDING_INVALID")
oid = request["expected_branch_oid"]
digest = request["payload_digest"]
if len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid):
raise JobError("EXPECTED_BRANCH_OID_INVALID")
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise JobError("PAYLOAD_DIGEST_INVALID")
if request["branch"] in {"main", "master"} or request["branch"].startswith(("refs/", "tags/")):
raise JobError("PROTECTED_BRANCH_REJECTED")
binding = {field: str(request[field]) for field in fields}
# Operation-specific, non-secret binding inputs must survive the
# durable round trip and participate in idempotency. Arbitrary request
# fields are intentionally discarded rather than persisted.
optional = (
"idempotency_key",
"task_id",
"new_branch_oid",
"authorization_recovery_digest",
"lineage_anchor_oid",
"base_oid",
"branch_mode",
"generation",
"expected_main_oid",
"pr_number",
"target_branch",
"check_context",
"check_state",
"check_description",
)
for field in optional:
value = request.get(field)
if value is None:
continue
if isinstance(value, bool) or not isinstance(value, (str, int)):
raise JobError("JOB_BINDING_OPTION_INVALID")
text = str(value).strip()
if not text or len(text) > 512:
raise JobError("JOB_BINDING_OPTION_INVALID")
binding[field] = text
return binding
def submit(self, request: Mapping[str, Any], *, payload: bytes | None = None) -> JobReceipt:
binding = self._binding(request)
payload_ref = request.get("payload_ref")
payload_size = request.get("payload_size")
if payload is not None:
payload_ref, payload_size = self.store.payloads.put(payload, binding["payload_digest"])
if payload_ref is not None and payload_ref != f"sha256/{binding['payload_digest']}":
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
raise JobError("PAYLOAD_SIZE_INVALID")
client_idempotency_key = binding.get("idempotency_key")
if client_idempotency_key is not None:
if len(client_idempotency_key) > 128 or not all(
character.isalnum() or character in "._:-" for character in client_idempotency_key
):
raise JobError("IDEMPOTENCY_KEY_INVALID")
key = sha256_json({"client_idempotency_key": client_idempotency_key})
else:
key = sha256_json(binding)
job_id = "job-" + key[:32]
durable_request = dict(binding)
if payload_ref is not None:
durable_request.update({"payload_ref": str(payload_ref), "payload_size": int(payload_size)})
now = utc_now()
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute("SELECT * FROM jobs WHERE idempotency_key=?", (key,)).fetchone()
if existing is not None:
if json.loads(existing["request_json"]).get("binding") != binding:
raise BindingConflict("BINDING_CONFLICT")
row = dict(existing)
connection.commit()
return self.store._receipt(row, duplicate=True)
connection.execute(
"INSERT INTO jobs(job_id,idempotency_key,campaign_id,repository,operation,branch,expected_branch_oid,payload_digest,payload_ref,payload_size,state,side_effect_state,revision,request_json,receipt_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(job_id, key, binding["campaign_id"], binding["repository"], binding["operation"], binding["branch"], binding["expected_branch_oid"], binding["payload_digest"], payload_ref, payload_size, JobState.REQUESTED.value, "NONE", 0, json.dumps({"binding": binding, **durable_request}, sort_keys=True), None, now, now),
)
connection.execute("INSERT INTO idempotency_keys(key,job_id,binding_json,created_at) VALUES(?,?,?,?)", (key, job_id, json.dumps(binding, sort_keys=True), now))
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, None, JobState.REQUESTED.value, 0, "NONE", now))
row = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except BindingConflict:
raise
except sqlite3.IntegrityError:
row = self.store.read(job_id)
return self.store._receipt(row, duplicate=True)
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
row["request"] = json.loads(row["request_json"])
self.store._mirror(row)
return self.store._receipt(row, duplicate=False)
def lease_job(
self,
job_id: str,
*,
lease_id: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
expected_revision: int,
) -> JobReceipt:
"""Atomically create a branch lease and move its Job to LEASED."""
if generation < 1 or any(len(oid) != 40 for oid in (lineage_anchor_oid, current_head_oid)):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if row["branch"] in {"main", "master"}:
raise JobError("PROTECTED_BRANCH_REJECTED")
now = utc_now()
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, lease_expires_at, "ACTIVE", 0))
next_revision = expected_revision + 1
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?", (JobState.LEASED.value, "LEASED", next_revision, now, job_id, expected_revision))
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, row["state"], JobState.LEASED.value, next_revision, "LEASED", now))
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except sqlite3.IntegrityError as exc:
raise JobError("LEASE_CONFLICT") from exc
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
if len(current_head_oid) != 40 or generation < 1:
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
state,
side_effect_state=side_effect_state,
receipt=receipt,
expected_revision=expected_revision,
)
def _advance(
self,
job_id: str,
state: JobState,
*,
side_effect_state: str,
receipt: Mapping[str, Any] | None = None,
expected_revision: int | None = None,
_allow_verified_external_recovery: bool = False,
) -> JobReceipt:
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
previous = JobState(row["state"])
revision = int(row["revision"])
if expected_revision is not None and expected_revision != revision:
raise JobError("REVISION_CAS_CONFLICT")
recovery_transition = (
_allow_verified_external_recovery
and previous == JobState.MANUAL_OWNER_REVIEW_REQUIRED
and state == JobState.COMPLETED
)
if previous in _TERMINAL and state != previous and not recovery_transition:
raise JobError("JOB_TERMINAL")
if state not in _RECOVERABLE | {JobState.RECONCILING, JobState.MANUAL_OWNER_REVIEW_REQUIRED}:
if previous in _RECOVERABLE:
raise JobError("RECONCILE_REQUIRED")
if previous != JobState.RECONCILING and _ORDER.get(state, -1) < _ORDER.get(previous, -1):
raise JobError("JOB_STATE_REGRESSION")
next_revision = revision + 1
receipt_json = json.dumps(dict(receipt), sort_keys=True) if receipt is not None else row["receipt_json"]
now = utc_now()
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,receipt_json=?,updated_at=? WHERE job_id=? AND revision=?", (state.value, side_effect_state, next_revision, receipt_json, now, job_id, revision))
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, previous.value, state.value, next_revision, side_effect_state, now))
if receipt is not None:
digest = hashlib.sha256(canonical_json(receipt)).hexdigest()
connection.execute("INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)", (f"receipt-{job_id}-{next_revision}", job_id, state.value, digest, json.dumps(dict(receipt), sort_keys=True), now))
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
@staticmethod
def _validate_verified_external_merge_proof(
job_id: str,
binding: Mapping[str, Any],
proof: Mapping[str, Any],
) -> dict[str, Any]:
if not isinstance(proof, Mapping):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
def require_oid(field: str) -> str:
value = proof.get(field)
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
return value
if proof.get("proof_schema") != "aios.job-recovery.v1":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
if proof.get("job_id") != job_id:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("operation") != "PROTECTED_MERGE" or binding.get("operation") != "PROTECTED_MERGE":
raise JobError("EXTERNAL_SIDE_EFFECT_OPERATION_REJECTED")
if proof.get("pr_number") != 53 or str(binding.get("pr_number")) != "53":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("pr_merged") is not True or proof.get("protected_merge") is not True:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("force_push") is not False:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("machine_actor") != "aios-autonomy-agent":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
expected_head = str(binding.get("expected_branch_oid", ""))
expected_base = str(binding.get("expected_main_oid", ""))
head_oid = require_oid("head_oid")
base_oid = require_oid("base_oid")
merged_main = require_oid("main_oid_after_protected_merge")
gitea_merged_main = require_oid("gitea_main_oid_after_protected_merge")
authority_merged_main = require_oid("authority_main_oid_after_protected_merge")
current_gitea = require_oid("current_gitea_main_oid")
current_authority = require_oid("current_authority_main_oid")
if head_oid != expected_head or base_oid != expected_base:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if not (merged_main == gitea_merged_main == authority_merged_main == head_oid):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if current_gitea != current_authority or proof.get("current_main_descends_from_protected_merge") is not True:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
return dict(proof)
def complete_verified_external_side_effect(
self,
job_id: str,
proof: Mapping[str, Any],
*,
expected_revision: int | None = None,
) -> JobReceipt:
"""Close one historical manual job from independently verified proof.
This is deliberately narrower than advance: it is only valid for the
known protected PR53 recovery shape and preserves the old receipt.
The actual state change still uses the normal revision-CAS path.
"""
current = self.store.read(job_id)
revision = int(current["revision"])
if expected_revision is not None and expected_revision != revision:
raise JobError("REVISION_CAS_CONFLICT")
request = json.loads(current["request_json"])
binding = request.get("binding", request)
normalized_proof = self._validate_verified_external_merge_proof(job_id, binding, proof)
existing_receipt = json.loads(current["receipt_json"]) if current["receipt_json"] else None
if JobState(current["state"]) == JobState.COMPLETED:
if (
existing_receipt
and existing_receipt.get("recovered_from_historical_manual") is True
and existing_receipt.get("proof") == normalized_proof
):
return self.store._receipt(current, duplicate=True)
raise JobError("JOB_TERMINAL")
if JobState(current["state"]) != JobState.MANUAL_OWNER_REVIEW_REQUIRED:
raise JobError("EXTERNAL_SIDE_EFFECT_RECOVERY_STATE_INVALID")
recovery_receipt = {
"recovered_from_historical_manual": True,
"recovery_kind": "VERIFIED_EXTERNAL_SIDE_EFFECT",
"historical_receipt": existing_receipt,
"proof": normalized_proof,
}
return self._advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=recovery_receipt,
expected_revision=revision,
_allow_verified_external_recovery=True,
)
def reconcile(self, job_id: str, observer: Callable[[Mapping[str, Any]], Mapping[str, Any]]) -> JobReceipt:
current = self.store.read(job_id)
if JobState(current["state"]) not in _RECOVERABLE:
return self.store._receipt(current, duplicate=True)
self.advance(job_id, JobState.RECONCILING, side_effect_state="IN_PROGRESS")
try:
observed = dict(observer(current))
stable = JobState(observed.get("state", JobState.REQUESTED.value))
if stable in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILING}:
raise JobError("RECONCILIATION_UNSTABLE")
return self.advance(job_id, stable, side_effect_state=str(observed.get("side_effect_state", "NONE")), receipt=observed.get("receipt"))
except Exception as exc:
self.advance(job_id, JobState.RECONCILIATION_REQUIRED, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
raise JobError("RECONCILIATION_REQUIRED") from exc
def resume(self, job_id: str) -> JobReceipt:
current = self.store.read(job_id)
if JobState(current["state"]) in _TERMINAL:
raise JobError("JOB_TERMINAL")
return self.reconcile(job_id, lambda _current: {"state": JobState.REQUESTED.value, "side_effect_state": "RESUME_SAME_JOB"})
@@ -1,279 +0,0 @@
"""Durable Job side-effect executor for the AIOS campaign control plane."""
from __future__ import annotations
import argparse
import base64
from dataclasses import dataclass, replace
import json
from pathlib import Path
from typing import Any, Mapping
from aios_authority_git_executor import AuthorityGitExecutor, AuthorityGitError
from aios_campaign_recovery import RecoveryEvidenceError, load_recovered_authorization
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineBrokerError
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
REPOSITORY = "aios/aios"
BRANCH = "agent/autonomous-release-campaign-v1-20260807"
EXPECTED_BRANCH_OID = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
AUTHORIZATION_DIGEST = "6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2"
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
class JobExecutionError(RuntimeError):
pass
@dataclass(slots=True)
class DurableJobExecutor:
controller: GitOpsJobController
authority: AuthorityGitExecutor
broker: GiteaMachineBroker
recovery_path: Path
def _request(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
row = self.controller.store.read(job_id)
try:
request = json.loads(row["request_json"])
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise JobExecutionError("JOB_REQUEST_INVALID") from exc
return row, dict(request.get("binding") or {})
def _validate_campaign(self, request: Mapping[str, Any]) -> str:
operation = str(request.get("operation", ""))
expected_branch_oid = request.get("expected_branch_oid")
required = {
"task_id": TASK_ID,
"campaign_id": CAMPAIGN_ID,
"repository": REPOSITORY,
"branch": BRANCH,
"authorization_recovery_digest": AUTHORIZATION_DIGEST,
}
for key, expected in required.items():
if request.get(key) != expected:
raise JobExecutionError(f"JOB_{key.upper()}_MISMATCH")
new_oid = request.get("new_branch_oid")
if not isinstance(new_oid, str) or len(new_oid) != 40 or any(ch not in "0123456789abcdef" for ch in new_oid):
raise JobExecutionError("NEW_BRANCH_OID_INVALID")
if not isinstance(expected_branch_oid, str) or len(expected_branch_oid) != 40 or any(ch not in "0123456789abcdef" for ch in expected_branch_oid):
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid == new_oid:
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
if operation != "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid != new_oid:
raise JobExecutionError("POST_ADVANCE_HEAD_BINDING_INVALID")
try:
recovery = load_recovered_authorization(self.recovery_path)
except RecoveryEvidenceError as exc:
raise JobExecutionError(str(exc)) from exc
if recovery["digest"] != AUTHORIZATION_DIGEST:
raise JobExecutionError("RECOVERY_DIGEST_MISMATCH")
for key, expected in required.items():
if key == "campaign_id":
actual = recovery.get(key)
elif key == "authorization_recovery_digest":
continue
else:
actual = recovery.get({"expected_branch_oid": "expected_branch_oid"}.get(key, key))
if actual is not None and actual != expected:
raise JobExecutionError("RECOVERY_BINDING_MISMATCH")
if recovery.get("expected_branch_oid") != EXPECTED_BRANCH_OID:
raise JobExecutionError("RECOVERY_BRANCH_OID_MISMATCH")
authority_head = self.authority.read_ref(f"refs/heads/{BRANCH}")
if authority_head not in {expected_branch_oid, new_oid}:
raise JobExecutionError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
if not self.authority.ancestry_check(expected_branch_oid, new_oid):
raise JobExecutionError("NEW_OID_NOT_DESCENDANT")
if not self.authority.ancestry_check(EXPECTED_BRANCH_OID, new_oid):
raise JobExecutionError("NEW_OID_OUTSIDE_AUTHORIZED_LINEAGE")
identity = self.broker.verify_identity()
if identity.get("repository") != REPOSITORY or identity.get("login") == "aiman":
raise JobExecutionError("MACHINE_IDENTITY_SCOPE_INVALID")
return new_oid
def dry_run(self, job_id: str) -> dict[str, Any]:
_row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
remote = self.broker.readback()
if remote is None:
action = "CREATE_REQUIRED"
elif remote == new_oid:
action = "ALREADY_IN_SYNC"
elif remote == request["expected_branch_oid"]:
action = "FAST_FORWARD_REQUIRED"
else:
action = "MANUAL_OWNER_REVIEW_REQUIRED"
if action == "MANUAL_OWNER_REVIEW_REQUIRED":
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
return {
"decision": "SAFE_TO_ADVANCE",
"authority_action": "CAS_REQUIRED",
"gitea_action": action,
"previous_branch_oid": request["expected_branch_oid"],
"final_branch_oid": new_oid,
"force_push": False,
}
def _recover_requested(self, job_id: str) -> dict[str, Any]:
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.resume(job_id)
row = self.controller.store.read(job_id)
return row
def execute(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
try:
return self._execute_unprotected(job_id, dry_run=dry_run)
except Exception as exc:
try:
row = self.controller.store.read(job_id)
state = JobState(row["state"])
if state not in {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED, JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.advance(job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
except Exception:
pass
raise
def _execute_unprotected(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
row, request = self._request(job_id)
operation = request.get("operation")
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
if dry_run:
return self.dry_run(job_id)
return self._advance_campaign(job_id)
if operation == "COMMIT_FILE":
return self._commit_file(job_id, request)
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
new_oid = self._validate_campaign(request)
final = self.broker.push_branch(new_oid)
return {"operation": operation, "remote_verified": final == new_oid, "final_network_oid": final}
if operation == "CREATE_PR":
self._validate_campaign(request)
return dict(self.broker.create_pr("AIOS autonomous release control plane", "Durable campaign continuation; exact head and recovery evidence are bound in the Job."))
if operation == "PUBLISH_CHECK":
self._validate_campaign(request)
context = request.get("check_context", "aios/durable-release")
state = request.get("check_state", "success")
description = request.get("check_description", "Durable campaign control-plane check passed")
return dict(self.broker.status(str(state), str(description), str(context)))
if operation == "PROTECTED_MERGE":
self._validate_campaign(request)
number = int(request.get("pr_number", "0"))
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
expected_main = request.get("expected_main_oid")
if expected_main is not None:
self.broker.transport.expected_merge_base = expected_main
return dict(self.broker.merge(number))
if operation == "RECONCILE_AUTHORITY_MAIN":
self._validate_campaign(request)
old = request.get("expected_main_oid")
new = request.get("new_branch_oid")
if not isinstance(old, str) or not isinstance(new, str):
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
gitea_main = self.broker.transport.read_branch("main")
if gitea_main != new:
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
if not self.authority.ancestry_check(old, new):
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new)}
raise JobExecutionError("OPERATION_NOT_ALLOWED")
def _advance_campaign(self, job_id: str) -> dict[str, Any]:
self._recover_requested(job_id)
row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
expected_oid = request["expected_branch_oid"]
state = JobState(row["state"])
if state == JobState.REQUESTED:
receipt = self.controller.advance(job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
self.controller.lease_job(job_id, lease_id=f"lease-{job_id}", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=expected_oid, generation=1, lease_expires_at="2099-01-01T00:00:00Z", expected_revision=int(row["revision"]))
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.LEASED:
self.controller.advance(job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY", receipt={"branch": BRANCH, "expected_oid": expected_oid})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.WORKTREE_READY:
self.controller.advance(job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
row = self.controller.store.read(job_id)
authority_before = self.authority.read_ref(f"refs/heads/{BRANCH}")
if authority_before == expected_oid:
authority_oid = self.authority.advance_task_branch_CAS(f"refs/heads/{BRANCH}", expected_oid, new_oid)
elif authority_before == new_oid:
authority_oid = new_oid
else:
raise JobExecutionError("AUTHORITY_BRANCH_CONFLICT")
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.EXECUTING:
self.controller.advance(job_id, JobState.LOCAL_VERIFIED, side_effect_state="LOCAL_VERIFIED", receipt={"authority": {"branch": BRANCH, "previous_oid": expected_oid, "new_oid": new_oid, "CAS_success": True, "readback_oid": authority_oid}})
row = self.controller.store.read(job_id)
remote_before = self.broker.readback()
if remote_before != new_oid:
if remote_before not in {None, expected_oid}:
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
if JobState(row["state"]) == JobState.LOCAL_VERIFIED:
if hasattr(self.broker, "receipt"):
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected_oid)
self.controller.advance(job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS", receipt={"gitea": {"previous_oid": remote_before, "desired_oid": new_oid, "force_push": False}})
final_network = self.broker.push_branch(new_oid)
else:
final_network = new_oid
final_api = self.broker.readback()
if final_api != new_oid:
raise JobExecutionError("GITEA_BRANCH_READBACK_MISMATCH")
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING}:
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt={"gitea": {"machine_actor": "aios-autonomy-agent", "previous_oid": remote_before, "desired_oid": new_oid, "final_network_oid": final_network, "final_API_oid": final_api, "force_push": False}})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.REMOTE_VERIFIED:
self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt={"authority_oid": authority_oid, "network_oid": final_network, "gitea_api_oid": final_api, "force_push": False})
return self.controller.store._receipt(self.controller.store.read(job_id), duplicate=True).as_dict()
def _commit_file(self, job_id: str, request: Mapping[str, Any]) -> dict[str, Any]:
row = self.controller.store.read(job_id)
payload_ref = request.get("payload_ref")
if not payload_ref:
raise JobExecutionError("PAYLOAD_REQUIRED")
payload = self.controller.store.payloads.read_verified(payload_ref, row["payload_digest"], int(request["payload_size"]))
try:
spec = json.loads(payload.decode())
path = str(spec["path"])
content = base64.b64decode(spec["content_base64"], validate=True)
except Exception as exc:
raise JobExecutionError("COMMIT_FILE_PAYLOAD_INVALID") from exc
new_oid = self.authority.commit_file_in_isolated_worktree(base_oid=row["expected_branch_oid"], files={path: content}, message="chore(gitops): durable commit-file", worktree_name=job_id)
return {"operation": "COMMIT_FILE", "new_oid": new_oid}
def build_from_environment(job_root: Path) -> DurableJobExecutor:
from aios_gitea_machine_transport import GiteaMachineTransport
from aios_gitea_machine_broker import MachineCredentialProjection
account = __import__("os").environ.get("AIOS_GITEA_MACHINE_ACCOUNT", "aios-autonomy-agent")
api = __import__("os").environ.get("AIOS_GITEA_API", "https://g.cnnetmkt.top/git/api/v1")
remote = __import__("os").environ.get("AIOS_GITEA_REMOTE", "https://g.cnnetmkt.top/git/aios/aios.git")
authority_dir = Path(__import__("os").environ.get("AIOS_GITOPS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
worktree_root = Path(__import__("os").environ.get("AIOS_GITOPS_WORKTREE_ROOT", "/var/lib/aios-gitops/worktrees"))
recovery = Path(__import__("os").environ.get("AIOS_RELEASE_RECOVERY_RECEIPT", "/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json"))
transport = GiteaMachineTransport(api, REPOSITORY, remote, authority_dir)
credential = MachineCredentialProjection.from_mapping({"scheme": "systemd-credential", "name": "gitea-machine-token", "account": account})
receipt = CampaignCapabilityReceipt(CAMPAIGN_ID, TASK_ID, REPOSITORY, BRANCH, EXPECTED_BRANCH_OID, frozenset({"exact_task_branch_push", "pr_create_update", "status_publish", "protected_merge", "remote_readback"}), frozenset({"direct_main_push", "force_push", "tag_update", "tag_delete", "baseline_write", "production_deploy"}))
return DurableJobExecutor(GitOpsJobController(JobStore(job_root)), AuthorityGitExecutor(authority_dir, worktree_root), GiteaMachineBroker(transport, credential, receipt), recovery)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--job-root", required=True, type=Path)
parser.add_argument("--job-id", required=True)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
result = build_from_environment(args.job_root).execute(args.job_id, dry_run=args.dry_run)
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -7
View File
@@ -5,8 +5,6 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from release_version import validate_release_version
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
@@ -220,11 +218,7 @@ class Handler(BaseHTTPRequestHandler):
a = self.require_write(['owner']);
if not a: return
ver=data.get('version') or data.get('tag')
try:
validate_release_version(ver)
except ValueError:
self.send_json({'error':'version must match VxRxCx'},400)
return
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
if data.get('push',False): git_cmd(['push','origin',ver])
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
@@ -1,80 +0,0 @@
"""Pure, observation-first decisions for governed GitOps reconciliation."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import re
from typing import Optional
OID_RE = re.compile(r"^[0-9a-f]{40}$")
class ReconciliationAction(str, Enum):
NOOP = "NOOP"
PUSH_EXACT = "PUSH_EXACT"
VERIFY_REMOTE = "VERIFY_REMOTE"
MARK_REMOTE_VERIFIED = "MARK_REMOTE_VERIFIED"
MARK_RECONCILIATION_REQUIRED = "MARK_RECONCILIATION_REQUIRED"
FAIL_CONFLICT = "FAIL_CONFLICT"
@dataclass(frozen=True, slots=True)
class RemoteObservation:
repository_identity: str
branch: str
desired_oid: str
authority_oid: Optional[str]
remote_git_oid: Optional[str]
remote_api_oid: Optional[str]
commit_visible: bool
observed_at: str
expected_oid: Optional[str] = None
@property
def all_equal(self) -> bool:
values = (self.authority_oid, self.remote_git_oid, self.remote_api_oid, self.desired_oid)
return all(isinstance(value, str) for value in values) and len(set(values)) == 1
@dataclass(frozen=True, slots=True)
class ReconciliationDecision:
action: ReconciliationAction
state: str
reason: str
retry_safe: bool = False
def _valid_oid(value: Optional[str]) -> bool:
return value is None or bool(OID_RE.fullmatch(value))
def decide_reconciliation(observation: RemoteObservation) -> ReconciliationDecision:
"""Decide from a fresh observation; never complete on a missing remote."""
if observation.repository_identity != "aios/aios":
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REPOSITORY_IDENTITY_MISMATCH")
if not observation.branch.startswith("agent/"):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "BRANCH_SCOPE_DENIED")
if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID")
if observation.authority_oid is None:
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_UNAVAILABLE")
if observation.authority_oid != observation.desired_oid:
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_CONFLICT")
present = [oid for oid in (observation.remote_git_oid, observation.remote_api_oid) if oid is not None]
if any(oid not in {observation.desired_oid, observation.expected_oid} for oid in present):
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REMOTE_OID_CONFLICT")
if observation.all_equal and observation.commit_visible:
return ReconciliationDecision(ReconciliationAction.MARK_REMOTE_VERIFIED, "REMOTE_VERIFIED", "ALL_OIDS_EQUAL_AND_VISIBLE")
if observation.remote_git_oid is None and observation.remote_api_oid is None:
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "REMOTE_PUSH_PENDING", "AUTHORITY_DESIRED_REMOTE_MISSING")
if observation.remote_git_oid == observation.desired_oid or observation.remote_api_oid == observation.desired_oid:
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "REMOTE_VERIFY_PENDING", "REMOTE_PROOF_PARTIAL")
if observation.expected_oid is not None and observation.remote_git_oid == observation.expected_oid and observation.remote_api_oid == observation.expected_oid:
return ReconciliationDecision(ReconciliationAction.PUSH_EXACT, "REMOTE_PUSH_PENDING", "SAFE_EXACT_FAST_FORWARD_RETRY", retry_safe=True)
if not observation.commit_visible:
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "COMMIT_VISIBILITY_PENDING", "COMMIT_VISIBILITY_LAG")
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "PARTIAL_REMOTE_OBSERVATION")
def reconcile_observation(**kwargs: object) -> ReconciliationDecision:
return decide_reconciliation(RemoteObservation(**kwargs))
@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Fail-closed policy for in-process Git mutation handlers."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping
_TRUE = frozenset({"1", "true", "yes", "on"})
@dataclass(frozen=True)
class GitWriteDecision:
allowed: bool
status_code: int
error: str
reason: str
workspace: str
def payload(self) -> dict[str, str]:
return {
"status": "allowed" if self.allowed else "blocked",
"error": self.error,
"reason": self.reason,
"workspace": self.workspace,
}
def _real(path: Path) -> Path:
return Path(os.path.realpath(os.path.abspath(str(path))))
def evaluate_git_write(workspace: Path | str, env: Mapping[str, str] | None = None) -> GitWriteDecision:
values = os.environ if env is None else env
lexical = Path(os.path.abspath(str(workspace)))
canonical = _real(lexical)
shown = str(lexical)
if str(values.get("AIOS_GITOPS_WRITES_ENABLED", "false")).strip().lower() not in _TRUE:
return GitWriteDecision(False, 423, "gitops_writes_disabled", "AIOS_GITOPS_WRITES_ENABLED is false", shown)
if lexical != canonical:
return GitWriteDecision(False, 409, "workspace_symlink_forbidden", "workspace must be a canonical non-symlink path", shown)
controller = _real(Path(values.get("AIOS_GITOPS_CONTROLLER_PATH", "/data/aios-gitops/worktrees/controller")))
role = str(values.get("AIOS_GITOPS_WORKSPACE_ROLE", "")).strip().lower()
if canonical == controller or role == "runtime-anchor":
return GitWriteDecision(False, 423, "runtime_anchor_read_only", "controller/runtime-anchor is read-only", str(canonical))
writer_root = _real(Path(values.get("AIOS_GITOPS_APPROVED_WRITER_ROOT", "/data/aios-gitops/worktrees")))
try:
canonical.relative_to(writer_root)
except ValueError:
return GitWriteDecision(False, 409, "workspace_not_approved_writer", "workspace is outside the approved writer root", str(canonical))
return GitWriteDecision(True, 200, "", "approved isolated writer", str(canonical))
@@ -1,100 +0,0 @@
"""Single supervised socket-activated worker for durable GitOps Jobs.
The worker consumes durable requests and reconciliation queries. Repository
mutation is a separate injected executor and remains impossible while the
legacy global write gate is false.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import signal
import socket
import threading
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore, ServiceUnavailable
def runtime_policy(environment: Mapping[str, str] | None = None) -> dict[str, bool]:
env = environment or os.environ
legacy = env.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
durable = env.get("AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
# Legacy direct Git mutation remains disabled. Authorized durable Jobs use
# their own campaign/task capability and may mutate only through the
# worker's lease/CAS path while the legacy gate stays false.
return {"legacy_global_write": legacy, "durable_job_execution": durable, "repository_mutation_allowed": durable}
def handle_request(controller: GitOpsJobController, request: Mapping[str, Any], executor: Any | None = None) -> dict[str, Any]:
job_id = str(request.get("job_id") or "")
if not job_id:
raise JobError("JOB_ID_REQUIRED")
if request.get("action") == "execute":
if executor is None:
raise JobError("JOB_EXECUTOR_UNAVAILABLE")
result = executor.execute(job_id, dry_run=bool(request.get("dry_run", False)))
receipt = controller.store._receipt(controller.store.read(job_id), duplicate=True)
return {"job": result, **receipt.as_dict()}
if request.get("action") == "resume":
receipt = controller.resume(job_id)
else:
row = controller.store.read(job_id)
receipt = controller.store._receipt(row, duplicate=True)
return receipt.as_dict()
def serve_socket(sock: socket.socket, controller: GitOpsJobController, stopping: threading.Event, executor: Any | None = None) -> None:
sock.listen(32)
sock.settimeout(1.0)
while not stopping.is_set():
try:
connection, _ = sock.accept()
except socket.timeout:
continue
with connection:
try:
raw = connection.makefile("rb").readline(1_048_576)
request = json.loads(raw.decode("utf-8"))
response = {"status": "ok", **handle_request(controller, request, executor)}
except ServiceUnavailable as exc:
response = {"status": "error", "error": str(exc), "retryable": True, "side_effect_state": "UNKNOWN"}
except Exception as exc:
response = {"status": "error", "error": type(exc).__name__, "retryable": False}
connection.sendall((json.dumps(response, sort_keys=True) + "\n").encode("utf-8"))
def socket_from_systemd() -> socket.socket:
if os.environ.get("LISTEN_FDS") != "1":
raise RuntimeError("SOCKET_ACTIVATION_REQUIRED")
return socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--job-root", required=True, type=Path)
args = parser.parse_args(argv)
policy = runtime_policy()
if policy["legacy_global_write"]:
raise RuntimeError("LEGACY_GLOBAL_WRITES_MUST_REMAIN_FALSE")
if not policy["durable_job_execution"]:
raise RuntimeError("DURABLE_JOB_EXECUTION_GATE_CLOSED")
controller = GitOpsJobController(JobStore(args.job_root))
from aios_gitops_job_executor import build_from_environment
executor = build_from_environment(args.job_root)
stopping = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stopping.set())
signal.signal(signal.SIGINT, lambda *_: stopping.set())
sock = socket_from_systemd()
try:
serve_socket(sock, controller, stopping, executor)
finally:
sock.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,69 +0,0 @@
"""CLI client for the governed local writer service."""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Send one governed-local-writer request")
parser.add_argument("--socket", required=True, help="AF_UNIX socket path")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--request", help="Request JSON string")
group.add_argument("--request-file", help="Path to JSON request file")
return parser.parse_args()
def _load_request_text(args: argparse.Namespace) -> str:
if args.request is not None:
return args.request
request_path = os.fspath(args.request_file)
with open(request_path, "r", encoding="utf-8") as request_file:
return request_file.read()
def send_request(socket_path: str, request_text: str) -> dict[str, object]:
"""Open one Unix socket, send one NDJSON request, and parse one NDJSON response."""
payload = json.loads(request_text)
if not isinstance(payload, dict):
raise ValueError("request must be a JSON object")
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.connect(socket_path)
client.sendall((json.dumps(payload) + "\n").encode("utf-8"))
response_chunks: list[str] = []
while True:
chunk = client.recv(1024 * 1024).decode("utf-8")
if not chunk:
break
response_chunks.append(chunk)
if "\n" in chunk:
break
response_text = "".join(response_chunks)
response_line = response_text.split("\n", 1)[0].strip()
if not response_line:
raise RuntimeError("empty response")
response = json.loads(response_line)
if not isinstance(response, dict):
raise RuntimeError("invalid response")
return response
def main() -> None:
args = _parse_args()
request_text = _load_request_text(args)
response = send_request(args.socket, request_text)
print(json.dumps(response, separators=(",", ":"), sort_keys=True))
if __name__ == "__main__":
try:
main()
except Exception as exc: # pragma: no cover - CLI entrypoint behavior
print(json.dumps({"status": "error", "error": str(exc)}), file=sys.stderr)
raise SystemExit(1)
@@ -1,931 +0,0 @@
"""Unix-socket service for governed local writer operations."""
from __future__ import annotations
import json
import hashlib
import argparse
import importlib.util
import os
import re
import socket
import subprocess
import sys
import tempfile
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Mapping, Protocol
from governed_local_writer import (
AppendCommitRequest,
AuthorizationError,
BranchPolicy,
CreateBranchRequest,
GovernedAuthorization,
PeerIdentity,
PRINCIPAL,
TASK_ID,
extract_peer_identity,
contains_secret_like_content,
load_authorization_file,
validate_file_changes,
)
class ExistingGitOpsHandler:
"""Delegate repository mutations to the installed GitOps handler seam."""
def __init__(
self,
*,
workspace_by_branch: Mapping[str, str | os.PathLike[str]],
bare_repo: str | os.PathLike[str],
api_source: str | os.PathLike[str],
) -> None:
self.workspace_by_branch = {str(k): Path(v) for k, v in workspace_by_branch.items()}
self.bare_repo = Path(bare_repo)
self.api_source = Path(api_source)
@staticmethod
def _restore_env(previous: Mapping[str, str | None]) -> None:
for key, value in previous.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def _load_api(self, workspace: Path):
if not self.api_source.is_file() or self.api_source.is_symlink():
raise RuntimeError("installed GitOps handler source is unavailable")
keys = (
"AIOS_GITOPS_WORKSPACE",
"AIOS_GITOPS_BARE_REPO",
"AIOS_GITOPS_WRITES_ENABLED",
"PYTHONDONTWRITEBYTECODE",
"GIT_CONFIG_COUNT",
"GIT_CONFIG_KEY_0",
"GIT_CONFIG_VALUE_0",
"GIT_CONFIG_KEY_1",
"GIT_CONFIG_VALUE_1",
)
previous = {key: os.environ.get(key) for key in keys}
os.environ.update(
{
"AIOS_GITOPS_WORKSPACE": str(workspace),
"AIOS_GITOPS_BARE_REPO": str(self.bare_repo),
"AIOS_GITOPS_WRITES_ENABLED": "true",
"PYTHONDONTWRITEBYTECODE": "1",
"GIT_CONFIG_COUNT": "2",
"GIT_CONFIG_KEY_0": "safe.directory",
"GIT_CONFIG_VALUE_0": str(workspace),
"GIT_CONFIG_KEY_1": "safe.directory",
"GIT_CONFIG_VALUE_1": str(self.bare_repo),
}
)
module_name = f"_governed_gitops_api_{uuid.uuid4().hex}"
try:
spec = importlib.util.spec_from_file_location(module_name, self.api_source)
if spec is None or spec.loader is None:
raise RuntimeError("unable to load installed GitOps handler")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
decision = module.evaluate_git_write(module.WS)
if not decision.allowed:
raise RuntimeError("existing GitOps write gate denied bootstrap")
return module, previous, module_name
except Exception:
sys.modules.pop(module_name, None)
self._restore_env(previous)
raise
def _workspace(self, branch: str) -> Path:
workspace = self.workspace_by_branch.get(branch)
if workspace is None or not workspace.is_dir():
raise RuntimeError("branch workspace is not authorized")
return workspace
@staticmethod
def _git(workspace: Path, args: list[str]) -> str:
result = subprocess.run(
["git", *args], cwd=workspace, text=True, capture_output=True, check=False
)
if result.returncode != 0:
raise RuntimeError("governed handler git probe failed")
return result.stdout.strip()
def create_branch(self, request: CreateBranchRequest) -> Mapping[str, object]:
workspace = self._workspace(request.branch)
# A prior handler call may have completed the fast-forward sync before
# the process observed a post-commit error. Reconcile only an exact,
# metadata-only result; never overwrite or move an existing ref here.
try:
existing = self._git(
workspace,
[
"--git-dir",
str(self.bare_repo),
"rev-parse",
f"refs/heads/{request.branch}",
],
)
except RuntimeError:
existing = ""
if existing:
parent = self._git(workspace, ["--git-dir", str(self.bare_repo), "rev-parse", f"{existing}^1"])
changed = self._git(
workspace,
[
"--git-dir",
str(self.bare_repo),
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
existing,
],
).splitlines()
safe_branch = re.sub(r"[^A-Za-z0-9_.-]+", "-", request.branch).strip("-")
if parent != request.base_oid or changed != [f"patches/{safe_branch}.branch.json"]:
raise RuntimeError("existing branch is not an exact metadata-only bootstrap")
return {"head_oid": existing, "parent_oid": parent, "handler_status": "reconciled"}
module, previous, module_name = self._load_api(workspace)
phase = "loaded"
try:
phase = "verify_base"
parent = self._git(
workspace,
["--git-dir", str(self.bare_repo), "rev-parse", "refs/heads/main"],
)
if parent != request.base_oid:
raise RuntimeError("authorized base OID drift")
phase = "checkout"
module.checkout(request.branch, base=request.base_oid, create=True)
phase = "write_metadata"
safe_branch = re.sub(r"[^A-Za-z0-9_.-]+", "-", request.branch).strip("-")
metadata_path = f"patches/{safe_branch}.branch.json"
module.write_json(
metadata_path,
{
"schema": "aios.governed-local-writer-bootstrap.v1",
"task_id": TASK_ID,
"principal": PRINCIPAL,
"branch": request.branch,
"parent_oid": request.base_oid,
"authorization_sha256": request.authorization_digest,
},
)
phase = "commit_and_sync"
result = module.commit(
[metadata_path], "chore(governance): bootstrap repair lease branch"
)
head_oid = self._git(workspace, ["rev-parse", "HEAD"])
first_parent = self._git(workspace, ["rev-parse", "HEAD^1"])
if first_parent != request.base_oid or (request.head_oid and head_oid != request.head_oid):
raise RuntimeError("bootstrap branch lineage check failed")
return {
"head_oid": head_oid,
"parent_oid": first_parent,
"handler_status": str(result.get("status")),
}
except Exception as exc:
raise RuntimeError(f"handler_phase={phase};error={exc.__class__.__name__}") from exc
finally:
sys.modules.pop(module_name, None)
self._restore_env(previous)
def append_commit(self, request: AppendCommitRequest) -> Mapping[str, object]:
workspace = self._workspace(request.branch)
module, previous, module_name = self._load_api(workspace)
phase = "loaded"
try:
phase = "verify_head"
current = self._git(workspace, ["rev-parse", "HEAD"])
if current != request.expected_head_oid:
raise RuntimeError("branch head advanced or is stale")
phase = "checkout"
module.checkout(request.branch, base=request.expected_head_oid, create=False)
phase = "write_files"
paths: list[str] = []
for change in request.changes:
paths.append(module.write_file(change.path, change.content))
phase = "commit_and_sync"
result = module.commit(paths, request.commit_message)
head_oid = self._git(workspace, ["rev-parse", "HEAD"])
first_parent = self._git(workspace, ["rev-parse", "HEAD^1"])
if first_parent != request.expected_head_oid or head_oid == first_parent:
raise RuntimeError("append commit lineage check failed")
return {
"head_oid": head_oid,
"parent_oid": first_parent,
"handler_status": str(result.get("status")),
}
except Exception as exc:
raise RuntimeError(f"handler_phase={phase};error={exc.__class__.__name__}") from exc
finally:
sys.modules.pop(module_name, None)
self._restore_env(previous)
class GovernedWriterHandler(Protocol):
"""Small adapter interface for real writer backend handlers."""
def create_branch(self, request: CreateBranchRequest) -> Mapping[str, object]:
"""Create a branch using internal repo services."""
def append_commit(self, request: AppendCommitRequest) -> Mapping[str, object]:
"""Append a commit using internal repo services."""
class AuditSink(Protocol):
"""Pluggable sink for sanitized audit records."""
def append(self, record: "AuditRecord") -> None:
"""Persist one audit record."""
@dataclass
class AuditRecord:
"""Sanitized audit log entry."""
authorization_digest: str
peer_pid: int
peer_uid: int
peer_gid: int
peer_executable: str | None
timestamp: datetime
operation: str
branch: str
result: str
class InMemoryAuditSink:
"""Simple in-memory audit log used by tests."""
def __init__(self) -> None:
self.records: list[AuditRecord] = []
def append(self, record: AuditRecord) -> None:
self.records.append(record)
@dataclass
class _AuthorizationState:
"""Tracks one-shot operation consumption."""
create_done: set[str] = field(default_factory=set)
append_done: set[str] = field(default_factory=set)
consumed: bool = False
class GovernedLocalWriterService:
"""Policy-aware AF_UNIX service that calls an injected handler."""
def __init__(
self,
socket_path: str | os.PathLike[str],
authorization: GovernedAuthorization,
handler: GovernedWriterHandler,
*,
audit_sink: AuditSink | None = None,
peer_identity_fn: Callable[[socket.socket], PeerIdentity] | None = None,
state_path: str | os.PathLike[str] | None = None,
) -> None:
self.socket_path = Path(socket_path)
self.authorization = authorization
self.handler = handler
self.peer_identity_fn = peer_identity_fn or extract_peer_identity
self.state = _AuthorizationState()
self.audit_sink = audit_sink or InMemoryAuditSink()
self.state_path = Path(state_path) if state_path is not None else None
self._load_state()
def _load_state(self) -> None:
"""Restore one-shot state without restoring request or secret material."""
if self.state_path is None or not self.state_path.exists():
return
try:
raw = json.loads(self.state_path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise ValueError("state must be an object")
if raw.get("authorization_sha256") != self.authorization.authorization_sha256:
raise ValueError("state authorization digest mismatch")
self.state.create_done = {str(v) for v in raw.get("create_done", [])}
self.state.append_done = {str(v) for v in raw.get("append_done", [])}
self.state.consumed = bool(raw.get("consumed", False))
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
raise AuthorizationError("authorization state is invalid") from exc
def _persist_state(self) -> None:
if self.state_path is None:
return
self.state_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"authorization_sha256": self.authorization.authorization_sha256,
"create_done": sorted(self.state.create_done),
"append_done": sorted(self.state.append_done),
"consumed": self.state.consumed,
}
fd, temp_name = tempfile.mkstemp(prefix=".writer-state-", dir=self.state_path.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
json.dump(payload, stream, sort_keys=True, separators=(",", ":"))
stream.write("\n")
os.replace(temp_name, self.state_path)
os.chmod(self.state_path, 0o600)
finally:
if os.path.exists(temp_name):
os.unlink(temp_name)
def _new_audit(self, peer: PeerIdentity, operation: str, branch: str, result: str) -> None:
self.audit_sink.append(
AuditRecord(
authorization_digest=self.authorization.authorization_sha256,
peer_pid=peer.pid,
peer_uid=peer.uid,
peer_gid=peer.gid,
peer_executable=peer.executable,
timestamp=datetime.now(tz=timezone.utc),
operation=operation,
branch=branch,
result=result,
)
)
def _require_request_field(self, request: Mapping[str, Any], field_name: str) -> str:
value = request.get(field_name)
if not isinstance(value, str) or not value:
raise AuthorizationError(f"{field_name} must be a non-empty string")
return value
def _branch_policy(self, branch: str) -> BranchPolicy:
policy = self.authorization.branch_operations.get(branch)
if policy is None:
raise AuthorizationError("branch is not authorized")
return policy
def _ensure_not_consumed(self) -> None:
if self.state.consumed:
raise AuthorizationError("authorization consumed")
if datetime.now(tz=timezone.utc) >= self.authorization.expires_at:
raise AuthorizationError("authorization expired")
def _mark_success(self, op: str, branch: str) -> None:
if op == "create_branch":
self.state.create_done.add(branch)
elif op == "append_commit":
self.state.append_done.add(branch)
if self.state.create_done.issuperset(self.authorization.create_branches) and self.state.append_done.issuperset(
self.authorization.append_branches
):
self.state.consumed = True
self._persist_state()
def _validate_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
task_id = self._require_request_field(request, "task_id")
if task_id != TASK_ID:
raise AuthorizationError("invalid task_id")
principal = self._require_request_field(request, "principal")
if principal != PRINCIPAL:
raise AuthorizationError("invalid principal")
operation = self._require_request_field(request, "op")
if operation not in {"create_branch", "append_commit"}:
raise AuthorizationError("unsupported operation")
if operation in self.authorization.denied_operations:
raise AuthorizationError("operation denied by policy")
branch = self._require_request_field(request, "branch")
self._ensure_not_consumed()
policy = self._branch_policy(branch)
if operation == "create_branch":
if policy.create_branch is None:
raise AuthorizationError("branch does not allow create_branch")
if branch in self.state.create_done:
raise AuthorizationError("create_branch already used for this branch")
base_oid = self._require_request_field(request, "base_oid")
raw_head_oid = request.get("head_oid")
if raw_head_oid is not None and (not isinstance(raw_head_oid, str) or not raw_head_oid):
raise AuthorizationError("head_oid must be a non-empty string when supplied")
head_oid = str(raw_head_oid) if raw_head_oid is not None else None
if base_oid != policy.create_branch.base_oid:
raise AuthorizationError("base_oid mismatch")
if policy.create_branch.head_oid is not None and head_oid != policy.create_branch.head_oid:
raise AuthorizationError("head_oid mismatch")
return {
"operation": operation,
"branch": branch,
"base_oid": base_oid,
"head_oid": head_oid,
}
if policy.append_commit is None:
raise AuthorizationError("branch does not allow append_commit")
if policy.create_branch is not None and branch not in self.state.create_done:
raise AuthorizationError("append_commit requires prior successful create_branch")
if branch in self.state.append_done:
raise AuthorizationError("append_commit already used for this branch")
expected_head = policy.append_commit.expected_head_oid
if "expected_head_oid" in request:
supplied_head = self._require_request_field(request, "expected_head_oid")
elif "current_head_oid" in request:
supplied_head = self._require_request_field(request, "current_head_oid")
else:
raise AuthorizationError("expected_head_oid or current_head_oid required")
if not re.fullmatch(r"[0-9a-fA-F]{40}", supplied_head):
raise AuthorizationError("head check must be a full commit OID")
if expected_head is not None and supplied_head != expected_head:
raise AuthorizationError("head check failed")
if request.get("force") is True or request.get("non_fast_forward") is True:
raise AuthorizationError("non-fast-forward operations are not allowed")
if request.get("history_rewrite") is True:
raise AuthorizationError("history rewrite is not allowed")
parent_oids = request.get("parent_oids")
if parent_oids is not None:
if not isinstance(parent_oids, list) or parent_oids != [supplied_head]:
raise AuthorizationError("parent_oids must contain exactly expected_head_oid")
changes = request.get("changes")
if not isinstance(changes, list):
raise AuthorizationError("changes must be a list")
validated_changes = validate_file_changes(changes, policy.append_commit.allowed_paths)
commit_message = request.get("commit_message", "chore(governance): append governed commit")
if not isinstance(commit_message, str) or not commit_message.strip() or len(commit_message) > 200:
raise AuthorizationError("commit_message must be a short non-empty string")
if "\n" in commit_message or "\r" in commit_message or contains_secret_like_content(commit_message):
raise AuthorizationError("commit_message failed policy")
return {
"operation": operation,
"branch": branch,
"expected_head_oid": supplied_head,
"changes": validated_changes,
"commit_message": commit_message.strip(),
}
def process_request(self, raw_request: Mapping[str, Any], peer: PeerIdentity) -> Mapping[str, Any]:
"""Run one request through policy checks and handler adapter."""
operation = raw_request.get("op") if isinstance(raw_request.get("op"), str) else "unknown"
branch = raw_request.get("branch") if isinstance(raw_request.get("branch"), str) else ""
try:
details = self._validate_request(raw_request)
operation = str(details["operation"])
branch = str(details["branch"])
if operation == "create_branch":
self.handler.create_branch(
CreateBranchRequest(
branch=branch,
base_oid=str(details["base_oid"]),
head_oid=(
str(details["head_oid"])
if details.get("head_oid") is not None
else None
),
authorization_digest=self.authorization.authorization_sha256,
)
)
self._mark_success("create_branch", branch)
self._new_audit(peer, operation, branch, "ok")
return {
"status": "ok",
"operation": operation,
"branch": branch,
"result": "applied",
}
self.handler.append_commit(
AppendCommitRequest(
branch=branch,
expected_head_oid=str(details["expected_head_oid"]),
changes=tuple(details["changes"]),
authorization_digest=self.authorization.authorization_sha256,
commit_message=str(details["commit_message"]),
)
)
self._mark_success("append_commit", branch)
self._new_audit(peer, operation, branch, "ok")
return {
"status": "ok",
"operation": operation,
"branch": branch,
"result": "applied",
}
except Exception as error:
self._new_audit(peer, operation or "unknown", branch, f"error:{error.__class__.__name__}")
if isinstance(error, PermissionError):
code = "permission_denied"
elif isinstance(error, AuthorizationError):
code = "request_denied"
else:
code = "handler_failed"
if code == "handler_failed" and str(error).startswith("handler_phase="):
code = str(error)
return {
"status": "error",
"operation": operation or "unknown",
"branch": branch,
"error": code,
}
def _write_json(self, connection: socket.socket, payload: Mapping[str, Any]) -> None:
connection.sendall((json.dumps(payload) + "\n").encode("utf-8"))
def _handle_client(self, connection: socket.socket) -> None:
with connection:
try:
peer = self.peer_identity_fn(connection)
except Exception:
self._write_json(
connection,
{
"status": "error",
"operation": "connection",
"branch": "",
"error": "peer identity validation failed",
},
)
return
with connection.makefile("r", encoding="utf-8") as stream:
for raw_line in stream:
line = raw_line.strip()
if not line:
continue
try:
request = json.loads(line)
except json.JSONDecodeError:
self._write_json(
connection,
{
"status": "error",
"operation": "request",
"branch": "",
"error": "invalid json",
},
)
continue
if not isinstance(request, Mapping):
self._write_json(
connection,
{
"status": "error",
"operation": "request",
"branch": "",
"error": "request must be a JSON object",
},
)
continue
self._write_json(connection, self.process_request(request, peer))
def _bind_socket(self) -> socket.socket:
if self.socket_path.exists():
raise RuntimeError("governed writer socket already exists")
parent = self.socket_path.parent
if not parent.is_dir():
raise RuntimeError("governed writer socket parent is missing")
if parent.is_symlink():
raise RuntimeError("governed writer socket parent must not be a symlink")
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(self.socket_path))
os.chmod(self.socket_path, 0o600)
server.listen(16)
return server
def serve(self) -> None:
"""Serve newline-delimited JSON over a filesystem AF_UNIX socket."""
server = self._bind_socket()
try:
while True:
connection, _ = server.accept()
self._handle_client(connection)
finally:
server.close()
if self.socket_path.exists():
self.socket_path.unlink()
class JsonAuditSink:
"""Append-only root-owned audit sink containing no request payloads."""
def __init__(self, path: str | os.PathLike[str]) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
def append(self, record: AuditRecord) -> None:
payload = {
"authorization_sha256": record.authorization_digest,
"peer_pid": record.peer_pid,
"peer_uid": record.peer_uid,
"peer_gid": record.peer_gid,
"peer_executable": record.peer_executable,
"timestamp": record.timestamp.isoformat(),
"operation": record.operation,
"branch": record.branch,
"result": record.result,
}
line = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
fd = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
os.fchmod(fd, 0o600)
os.write(fd, line)
finally:
os.close(fd)
# V2R1C6 durable receipt overlay. This subclasses the original root-only
# writer in-place so the existing AF_UNIX/SO_PEERCRED and handler policy remain
# the only mutation path.
_LegacyGovernedLocalWriterService = GovernedLocalWriterService
class GovernedLocalWriterService(_LegacyGovernedLocalWriterService):
"""Existing writer with durable request receipts and safe recovery."""
def _load_state(self) -> None:
super()._load_state()
self.state.receipts = {}
if self.state_path is None or not self.state_path.exists():
return
raw = json.loads(self.state_path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise AuthorizationError("authorization state is invalid")
receipts = raw.get("receipts", {})
if not isinstance(receipts, dict):
raise AuthorizationError("authorization receipts are invalid")
self.state.receipts = {
str(key): dict(value)
for key, value in receipts.items()
if isinstance(value, dict)
}
def _persist_state(self) -> None:
if self.state_path is None:
return
self.state_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"authorization_sha256": self.authorization.authorization_sha256,
"create_done": sorted(self.state.create_done),
"append_done": sorted(self.state.append_done),
"consumed": bool(self.state.consumed),
"receipts": self.state.receipts,
}
fd, temp_name = tempfile.mkstemp(prefix=".writer-state-", dir=self.state_path.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
json.dump(payload, stream, sort_keys=True, separators=(",", ":"))
stream.write("\n")
os.replace(temp_name, self.state_path)
os.chmod(self.state_path, 0o600)
finally:
if os.path.exists(temp_name):
os.unlink(temp_name)
def _request_identity(self, request: Mapping[str, Any]) -> dict[str, str]:
canonical = json.dumps(dict(request), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
fallback = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
request_id = request.get("request_id")
idempotency_key = request.get("idempotency_key")
request_id = request_id.strip() if isinstance(request_id, str) and request_id.strip() else "legacy-" + fallback
idempotency_key = idempotency_key.strip() if isinstance(idempotency_key, str) and idempotency_key.strip() else "legacy-" + fallback
for field_name, value in (("request_id", request_id), ("idempotency_key", idempotency_key)):
if len(value) > 200 or "\n" in value or "\r" in value:
raise AuthorizationError(field_name + " is invalid")
return {
"request_id": request_id,
"idempotency_key": idempotency_key,
"request_fingerprint": fallback,
}
def _find_receipt(self, details: Mapping[str, Any], compare_fingerprint: bool = True):
request_id = str(details["request_id"])
idempotency_key = str(details["idempotency_key"])
fingerprint = str(details.get("request_fingerprint", ""))
for receipt in self.state.receipts.values():
if receipt.get("request_id") == request_id or receipt.get("idempotency_key") == idempotency_key:
if compare_fingerprint and receipt.get("request_fingerprint") != fingerprint:
raise AuthorizationError("idempotency key reused with different request")
return receipt
return None
@staticmethod
def _receipt_response(receipt: Mapping[str, object], status: str = "receipt") -> dict[str, object]:
response = {
"status": status,
"request_id": receipt.get("request_id"),
"idempotency_key": receipt.get("idempotency_key"),
"operation": receipt.get("operation"),
"branch": receipt.get("branch", ""),
"receipt_status": receipt.get("status"),
}
for field_name in ("commit_oid", "final_head", "parent_oid", "audit_sha256", "error_code", "safe_to_retry"):
if field_name in receipt:
response[field_name] = receipt[field_name]
return response
def _new_receipt(self, details: Mapping[str, Any]) -> dict[str, object]:
return {
"request_id": str(details["request_id"]),
"idempotency_key": str(details["idempotency_key"]),
"request_fingerprint": str(details["request_fingerprint"]),
"operation": str(details["operation"]),
"branch": str(details.get("branch", "")),
"expected_head_oid": str(details.get("expected_head_oid", "")),
"status": "accepted",
"created_at": datetime.now(tz=timezone.utc).isoformat(),
"safe_to_retry": False,
}
def _new_audit(self, peer: PeerIdentity, operation: str, branch: str, result: str) -> str:
super()._new_audit(peer, operation, branch, result)
payload = {
"authorization_sha256": self.authorization.authorization_sha256,
"peer_pid": peer.pid,
"peer_uid": peer.uid,
"peer_gid": peer.gid,
"peer_executable": peer.executable,
"operation": operation,
"branch": branch,
"result": result,
}
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def _mark_success(self, op: str, branch: str) -> None:
if op == "create_branch":
self.state.create_done.add(branch)
elif op in {"append_commit", "append_files", "append_batch"}:
self.state.append_done.add(branch)
# The provisioning grant is one-shot; the task writer lease itself must
# support multiple append receipts for the exact authorized branches.
self.state.consumed = False
self._persist_state()
def _validate_durable(self, raw_request: Mapping[str, Any]) -> dict[str, Any]:
operation = self._require_request_field(raw_request, "op")
identity = self._request_identity(raw_request)
if operation == "query_receipt":
return {"operation": operation, **identity}
if operation == "recover_request":
branch = self._require_request_field(raw_request, "branch")
policy = self._branch_policy(branch)
if policy.append_commit is None:
raise AuthorizationError("branch does not allow recovery")
expected = raw_request.get("expected_head_oid") or policy.append_commit.expected_head_oid
if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", expected):
raise AuthorizationError("recovery requires expected_head_oid")
return {"operation": operation, "branch": branch, "expected_head_oid": expected.lower(), **identity}
if operation not in {"create_branch", "append_commit", "append_files", "append_batch"}:
raise AuthorizationError("unsupported operation")
request = dict(raw_request)
if operation in {"append_files", "append_batch"}:
request["op"] = "append_commit"
if operation in {"append_commit", "append_files", "append_batch"}:
self.state.consumed = False
branch = request.get("branch")
removed = False
dynamic_expected = False
if isinstance(branch, str) and branch in self.state.append_done:
self.state.append_done.remove(branch)
removed = True
policy = self.authorization.branch_operations.get(branch)
if policy is not None and policy.append_commit is not None and policy.append_commit.expected_head_oid is not None:
from governed_local_writer import BranchAppendPolicy, BranchPolicy
self.authorization.branch_operations[branch] = BranchPolicy(create_branch=policy.create_branch, append_commit=BranchAppendPolicy(branch=branch, expected_head_oid=None, allowed_paths=policy.append_commit.allowed_paths))
dynamic_expected = True
try:
details = super()._validate_request(request)
finally:
if dynamic_expected:
policy = self.authorization.branch_operations.get(str(branch))
if policy is not None and policy.append_commit is not None:
from governed_local_writer import BranchAppendPolicy, BranchPolicy
self.authorization.branch_operations[str(branch)] = BranchPolicy(create_branch=policy.create_branch, append_commit=BranchAppendPolicy(branch=str(branch), expected_head_oid=str(raw_request.get("expected_head_oid") or ""), allowed_paths=policy.append_commit.allowed_paths))
if removed:
self.state.append_done.add(str(branch))
details.update(identity)
details["operation"] = operation
return details
def _recover_backend(self, details: Mapping[str, Any]) -> dict[str, object]:
if not hasattr(self.handler, "_workspace") or not hasattr(self.handler, "_git"):
raise RuntimeError("recovery backend unavailable")
workspace = self.handler._workspace(str(details["branch"]))
expected = str(details["expected_head_oid"])
current = self.handler._git(workspace, ["rev-parse", "HEAD"])
if current == expected:
return {"status": "not_started", "final_head": current}
parent = self.handler._git(workspace, ["rev-parse", "HEAD^1"])
if parent != expected:
raise RuntimeError("external branch advance during recovery")
return {"status": "applied", "commit_oid": current, "final_head": current, "parent_oid": parent}
def process_request(self, raw_request: Mapping[str, Any], peer: PeerIdentity) -> Mapping[str, Any]:
operation = raw_request.get("op") if isinstance(raw_request.get("op"), str) else "unknown"
branch = raw_request.get("branch") if isinstance(raw_request.get("branch"), str) else ""
try:
details = self._validate_durable(raw_request)
operation = str(details["operation"])
branch = str(details.get("branch", ""))
if operation == "query_receipt":
receipt = self._find_receipt(details, compare_fingerprint=False)
return self._receipt_response(receipt) if receipt is not None else {"status": "not_found", "request_id": details["request_id"], "idempotency_key": details["idempotency_key"]}
if operation == "recover_request":
receipt = self._find_receipt(details, compare_fingerprint=False)
if receipt is None:
return {"status": "not_found", "request_id": details["request_id"], "idempotency_key": details["idempotency_key"], "safe_to_retry": True}
if receipt.get("status") == "applied":
return self._receipt_response(receipt)
recovered = self._recover_backend(details)
if recovered.get("status") == "applied":
receipt.update({"status": "applied", "commit_oid": recovered.get("commit_oid"), "final_head": recovered.get("final_head"), "parent_oid": recovered.get("parent_oid"), "safe_to_retry": False})
self._mark_success("append_commit", branch)
return self._receipt_response(receipt)
receipt["safe_to_retry"] = True
self._persist_state()
return self._receipt_response(receipt)
receipt = self._find_receipt(details)
if receipt is not None:
return self._receipt_response(receipt)
receipt = self._new_receipt(details)
self.state.receipts[str(details["request_id"])] = receipt
self._persist_state()
if operation == "create_branch":
result = self.handler.create_branch(CreateBranchRequest(branch=branch, base_oid=str(details["base_oid"]), head_oid=str(details["head_oid"]) if details.get("head_oid") is not None else None, authorization_digest=self.authorization.authorization_sha256))
commit_oid = str(result.get("head_oid", ""))
parent_oid = str(result.get("parent_oid", ""))
audit_sha = self._new_audit(peer, operation, branch, "ok")
receipt.update({"status": "applied", "commit_oid": commit_oid, "final_head": commit_oid, "parent_oid": parent_oid, "audit_sha256": audit_sha, "safe_to_retry": False})
self._mark_success("create_branch", branch)
return self._receipt_response(receipt, status="ok") | {"result": "applied"}
result = self.handler.append_commit(AppendCommitRequest(branch=branch, expected_head_oid=str(details["expected_head_oid"]), changes=tuple(details["changes"]), authorization_digest=self.authorization.authorization_sha256, commit_message=str(details["commit_message"])))
commit_oid = str(result.get("head_oid", ""))
parent_oid = str(result.get("parent_oid", ""))
audit_sha = self._new_audit(peer, operation, branch, "ok")
receipt.update({"status": "applied", "commit_oid": commit_oid, "final_head": commit_oid, "parent_oid": parent_oid, "audit_sha256": audit_sha, "safe_to_retry": False})
self._mark_success("append_commit", branch)
return self._receipt_response(receipt, status="ok") | {"result": "applied"}
except Exception as error:
try:
identity = self._request_identity(raw_request)
receipt = self.state.receipts.get(identity["request_id"])
if receipt is not None and receipt.get("status") == "accepted":
receipt.update({"status": "failed", "error_code": error.__class__.__name__, "safe_to_retry": False})
self._persist_state()
except Exception:
pass
self._new_audit(peer, operation, branch, "error:" + error.__class__.__name__)
return {"status": "error", "operation": operation, "branch": branch, "error": "request_denied" if isinstance(error, AuthorizationError) else "handler_failed"}
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="AIOS governed local writer")
parser.add_argument("--socket", required=True)
parser.add_argument("--authorization", required=True)
parser.add_argument("--state", required=True)
parser.add_argument("--audit", required=True)
parser.add_argument("--repair-branch", required=True)
parser.add_argument("--repair-workspace", required=True)
parser.add_argument("--candidate-branch", required=True)
parser.add_argument("--candidate-workspace", required=True)
parser.add_argument("--bare-repo", required=True)
parser.add_argument("--api-source", required=True)
return parser.parse_args()
def main() -> int:
if hasattr(os, "geteuid") and os.geteuid() != 0:
print("governed local writer requires uid 0", file=sys.stderr)
return 2
args = _parse_args()
authorization = load_authorization_file(args.authorization)
handler = ExistingGitOpsHandler(
workspace_by_branch={
args.repair_branch: args.repair_workspace,
args.candidate_branch: args.candidate_workspace,
},
bare_repo=args.bare_repo,
api_source=args.api_source,
)
service = GovernedLocalWriterService(
args.socket,
authorization,
handler,
audit_sink=JsonAuditSink(args.audit),
state_path=args.state,
)
service.serve()
return 0
if __name__ == "__main__": # pragma: no cover - systemd entrypoint
raise SystemExit(main())
@@ -1,59 +0,0 @@
"""Central fail-closed policy seam for governed task and continuation requests."""
from __future__ import annotations
from dataclasses import dataclass
import re
from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@dataclass(frozen=True, slots=True)
class GovernedPolicyInput:
identity: str
task_id: str
repository: str
operation: str
base_oid: str
expected_branch_oid: str
target_branch: str
generation: int
risk: str = "engineering"
requested_capabilities: tuple[str, ...] = ()
previous_generation: int | None = None
@dataclass(frozen=True, slots=True)
class PolicyDecision:
decision: str
reason: str
def as_dict(self) -> dict[str, str]:
return {"decision": self.decision, "reason": self.reason}
def evaluate_policy(value: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
if isinstance(value, Mapping):
value = GovernedPolicyInput(**value)
if value.repository != "aios/aios":
return PolicyDecision("DENY", "REPOSITORY_IDENTITY_MISMATCH")
if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid):
return PolicyDecision("DENY", "OID_INVALID")
if not value.target_branch.startswith("agent/") or value.target_branch in {"main", "master"}:
return PolicyDecision("DENY", "BRANCH_SCOPE_DENIED")
if "force_push" in value.requested_capabilities or "force" in value.operation.lower():
return PolicyDecision("DENY", "FORCE_PUSH_DENIED")
if {"main_write", "tag_write", "baseline_write", "production_write", "production_deploy", "tag_mutation", "baseline_mutation"}.intersection(value.requested_capabilities):
return PolicyDecision("DENY", "MUTATION_SCOPE_DENIED")
if not isinstance(value.generation, int) or isinstance(value.generation, bool) or value.generation < 1:
return PolicyDecision("DENY", "GENERATION_INVALID")
if value.previous_generation is not None and value.generation <= value.previous_generation:
return PolicyDecision("MANUAL_OWNER_REVIEW_REQUIRED", "GENERATION_NOT_MONOTONIC")
if not value.task_id:
return PolicyDecision("DENY", "TASK_ID_INVALID")
return PolicyDecision("ALLOW", "GOVERNED_ENGINEERING_SCOPE")
class GovernedPolicy:
def decide(self, request: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
return evaluate_policy(request)
@@ -1,84 +0,0 @@
"""Durable governed task binding for CREATE_NEW and CONTINUE_EXISTING."""
from __future__ import annotations
from dataclasses import dataclass, asdict
import hashlib
import json
from pathlib import Path
import re
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class BootstrapError(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class GovernedTaskBinding:
task_id: str
repository: str
base_oid: str
target_branch: str
risk: str
description: str
idempotency_key: str
expected_branch_oid: str
branch_mode: str
generation: int
job_id: str | None = None
def response(self, *, idempotent: bool = False) -> dict[str, Any]:
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
def _safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
class GovernedTaskBootstrap:
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
def _metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_safe(task_id) + ".json")
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
path = self._metadata(task_id)
if not path.is_file() or path.is_symlink(): return None
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
def _write(self, binding: GovernedTaskBinding) -> None:
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
return existing.response(idempotent=True)
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
expected = str(request.get("expected_branch_oid") or values["base_oid"])
if current is None:
mode, generation = "CREATE_NEW", 1
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
else:
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
if generation < 1: raise BootstrapError("GENERATION_INVALID")
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
_, controller = self._store(values["task_id"])
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
self._write(binding)
return binding.response()
@@ -0,0 +1,27 @@
"""AIOS-managed Hermes runtime boundary."""
from .adapter import (
HermesAdapter,
HermesConflict,
HermesNotFound,
HermesRuntimeError,
InMemoryHermesStore,
SQLiteHermesStore,
)
from .contract import HermesHealth, HermesJob, HermesJobState, HermesResult
from .worker import HermesWorker, WorkerRunResult
__all__ = [
"HermesAdapter",
"HermesConflict",
"HermesHealth",
"HermesJob",
"HermesJobState",
"HermesNotFound",
"HermesResult",
"HermesRuntimeError",
"HermesWorker",
"InMemoryHermesStore",
"SQLiteHermesStore",
"WorkerRunResult",
]
+591
View File
@@ -0,0 +1,591 @@
"""Tenant-partitioned Hermes queue and worker lease boundary."""
from __future__ import annotations
import hashlib
import json
import sqlite3
import threading
from dataclasses import asdict, replace
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Iterable, Mapping
from .contract import (
ALLOWED_TRANSITIONS,
TERMINAL_STATES,
HermesHealth,
HermesJob,
HermesJobState,
HermesResult,
utc_now,
)
class HermesRuntimeError(RuntimeError):
"""Base Hermes runtime-boundary failure."""
class HermesNotFound(HermesRuntimeError):
"""A tenant-scoped job was not found."""
class HermesConflict(HermesRuntimeError):
"""A state, lease, idempotency, or revision conflict occurred."""
class InMemoryHermesStore:
"""Thread-safe reference store; production backends implement this contract."""
def __init__(self) -> None:
self.lock = threading.RLock()
self.jobs: dict[str, HermesJob] = {}
self.results: dict[str, HermesResult] = {}
self.idempotency: dict[tuple[str, str], tuple[str, str]] = {}
self.worker_heartbeats: dict[str, tuple[str, str | None]] = {}
def persist_locked(self) -> None:
"""Persist a mutation while ``lock`` is held; memory stores are a no-op."""
class SQLiteHermesStore(InMemoryHermesStore):
"""Durable snapshot for the existing Hermes queue state machine.
The adapter remains the sole owner of transitions and lease rules. This
store only makes its jobs, results, idempotency keys and worker heartbeats
survive a process restart.
"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
super().__init__()
with self._connect() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS hermes_state (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
payload TEXT NOT NULL
)
"""
)
self._load()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(str(self.path), timeout=30)
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
return connection
@staticmethod
def _record(value: HermesJob | HermesResult) -> dict[str, object]:
record = asdict(value)
record["state"] = value.state.value
return record
def _load(self) -> None:
with self.lock, self._connect() as connection:
row = connection.execute(
"SELECT payload FROM hermes_state WHERE singleton = 1"
).fetchone()
if row is None:
return
payload = json.loads(str(row[0]))
self.jobs = {
str(item["job_id"]): HermesJob(**item)
for item in payload.get("jobs", [])
}
self.results = {
str(item["job_id"]): HermesResult(**item)
for item in payload.get("results", [])
}
self.idempotency = {
(str(item["tenant_id"]), str(item["idempotency_key"])): (
str(item["job_id"]),
str(item["digest"]),
)
for item in payload.get("idempotency", [])
}
self.worker_heartbeats = {
str(item["worker_id"]): (
str(item["heartbeat_at"]),
str(item["job_id"]) if item.get("job_id") is not None else None,
)
for item in payload.get("worker_heartbeats", [])
}
def persist_locked(self) -> None:
payload = {
"version": 1,
"jobs": [self._record(item) for item in self.jobs.values()],
"results": [self._record(item) for item in self.results.values()],
"idempotency": [
{
"tenant_id": tenant_id,
"idempotency_key": idempotency_key,
"job_id": job_id,
"digest": digest,
}
for (tenant_id, idempotency_key), (job_id, digest)
in self.idempotency.items()
],
"worker_heartbeats": [
{
"worker_id": worker_id,
"heartbeat_at": heartbeat_at,
"job_id": job_id,
}
for worker_id, (heartbeat_at, job_id)
in self.worker_heartbeats.items()
],
}
encoded = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"""
INSERT INTO hermes_state(singleton, payload) VALUES(1, ?)
ON CONFLICT(singleton) DO UPDATE SET payload=excluded.payload
""",
(encoded,),
)
connection.commit()
class HermesAdapter:
"""Queue and reconcile Hermes jobs without owning AIOS policy."""
def __init__(
self,
store: InMemoryHermesStore | None = None,
*,
runtime_id: str = "hermes-runtime",
runtime_version: str = "v1",
default_lease_seconds: int = 60,
) -> None:
if default_lease_seconds < 5:
raise ValueError("default_lease_seconds must be >= 5")
self.store = store or InMemoryHermesStore()
self.runtime_id = runtime_id
self.runtime_version = runtime_version
self.default_lease_seconds = int(default_lease_seconds)
@staticmethod
def _digest(job: HermesJob) -> str:
raw = json.dumps(
{
"tenant_id": job.tenant_id,
"cell_id": job.cell_id,
"profile_id": job.profile_id,
"task_id": job.task_id,
"trace_id": job.trace_id,
"action": job.action,
"payload": dict(job.payload),
"priority": job.priority,
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
@staticmethod
def _parse(value: str | None) -> datetime | None:
if value is None:
return None
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("timestamp must include timezone")
return parsed.astimezone(timezone.utc)
@staticmethod
def _format(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
@staticmethod
def _assert_tenant(job: HermesJob, tenant_id: str) -> None:
if job.tenant_id != tenant_id:
raise HermesNotFound("job not found")
def enqueue(self, job: HermesJob) -> HermesJob:
if job.state != HermesJobState.QUEUED or job.revision != 1:
raise HermesConflict("new job must be QUEUED at revision 1")
digest = self._digest(job)
idem_ref = (job.tenant_id, job.idempotency_key)
with self.store.lock:
existing = self.store.jobs.get(job.job_id)
if existing is not None:
if self._digest(existing) != digest:
raise HermesConflict("job_id reused with different content")
return existing
prior = self.store.idempotency.get(idem_ref)
if prior is not None:
prior_id, prior_digest = prior
if prior_digest != digest:
raise HermesConflict("idempotency key reused with different content")
return self.store.jobs[prior_id]
self.store.jobs[job.job_id] = job
self.store.idempotency[idem_ref] = (job.job_id, digest)
self.store.persist_locked()
return job
def get(self, job_id: str, *, tenant_id: str) -> HermesJob:
with self.store.lock:
job = self.store.jobs.get(job_id)
if job is None:
raise HermesNotFound("job not found")
self._assert_tenant(job, tenant_id)
return job
def list_jobs(
self,
*,
tenant_id: str,
cell_id: str | None = None,
profile_id: str | None = None,
states: Iterable[HermesJobState | str] | None = None,
) -> list[HermesJob]:
state_filter = {HermesJobState(item) for item in states} if states else None
with self.store.lock:
if self._requeue_expired_locked():
self.store.persist_locked()
jobs = [
job
for job in self.store.jobs.values()
if job.tenant_id == tenant_id
and (cell_id is None or job.cell_id == cell_id)
and (profile_id is None or job.profile_id == profile_id)
and (state_filter is None or job.state in state_filter)
]
return sorted(jobs, key=lambda item: (item.priority, item.created_at, item.job_id))
def claim(
self,
*,
tenant_id: str,
cell_id: str,
profile_id: str,
worker_id: str,
lease_seconds: int | None = None,
job_id: str | None = None,
) -> HermesJob | None:
worker_id = str(worker_id or "").strip()
if not worker_id:
raise ValueError("worker_id is required")
duration = int(lease_seconds or self.default_lease_seconds)
if duration < 5:
raise ValueError("lease_seconds must be >= 5")
with self.store.lock:
self._requeue_expired_locked()
candidates = [
job
for job in self.store.jobs.values()
if job.tenant_id == tenant_id
and job.cell_id == cell_id
and job.profile_id == profile_id
and (job_id is None or job.job_id == job_id)
and job.state == HermesJobState.QUEUED
]
self.store.worker_heartbeats[worker_id] = (utc_now(), None)
if not candidates:
self.store.persist_locked()
return None
current = sorted(
candidates,
key=lambda item: (item.priority, item.created_at, item.job_id),
)[0]
claimed = self._transition_locked(
current,
HermesJobState.CLAIMED,
assigned_worker_id=worker_id,
lease_expires_at=self._format(
datetime.now(timezone.utc) + timedelta(seconds=duration)
),
)
self.store.worker_heartbeats[worker_id] = (utc_now(), claimed.job_id)
self.store.persist_locked()
return claimed
def start(
self,
job_id: str,
*,
tenant_id: str,
worker_id: str,
expected_revision: int | None = None,
) -> HermesJob:
with self.store.lock:
self._requeue_expired_locked()
current = self._owned_job_locked(job_id, tenant_id, worker_id)
self._assert_revision(current, expected_revision)
updated = self._transition_locked(current, HermesJobState.RUNNING)
self.store.persist_locked()
return updated
def heartbeat(
self,
job_id: str,
*,
tenant_id: str,
worker_id: str,
lease_seconds: int | None = None,
) -> HermesJob:
duration = int(lease_seconds or self.default_lease_seconds)
with self.store.lock:
self._requeue_expired_locked()
current = self._owned_job_locked(job_id, tenant_id, worker_id)
if current.state not in {HermesJobState.CLAIMED, HermesJobState.RUNNING}:
raise HermesConflict("job does not accept heartbeat")
updated = replace(
current,
revision=current.revision + 1,
lease_expires_at=self._format(
datetime.now(timezone.utc) + timedelta(seconds=duration)
),
updated_at=utc_now(),
)
self.store.jobs[job_id] = updated
self.store.worker_heartbeats[worker_id] = (utc_now(), job_id)
self.store.persist_locked()
return updated
def complete(
self,
job_id: str,
*,
tenant_id: str,
worker_id: str,
output: Mapping[str, object],
expected_revision: int | None = None,
) -> HermesResult:
return self._finish(
job_id,
tenant_id=tenant_id,
worker_id=worker_id,
state=HermesJobState.COMPLETED,
output=output,
expected_revision=expected_revision,
)
def fail(
self,
job_id: str,
*,
tenant_id: str,
worker_id: str,
error: Mapping[str, object],
expected_revision: int | None = None,
) -> HermesResult:
return self._finish(
job_id,
tenant_id=tenant_id,
worker_id=worker_id,
state=HermesJobState.FAILED,
error=error,
expected_revision=expected_revision,
)
def cancel(
self,
job_id: str,
*,
tenant_id: str,
expected_revision: int | None = None,
) -> HermesResult:
with self.store.lock:
current = self.store.jobs.get(job_id)
if current is None:
raise HermesNotFound("job not found")
self._assert_tenant(current, tenant_id)
self._assert_revision(current, expected_revision)
if current.state in TERMINAL_STATES:
result = self.store.results.get(job_id)
if result is None:
raise HermesConflict("terminal result missing")
if result.state != HermesJobState.CANCELLED:
raise HermesConflict("job already has a different terminal result")
return result
updated = self._transition_locked(current, HermesJobState.CANCELLED)
result = HermesResult(
job_id=updated.job_id,
tenant_id=updated.tenant_id,
task_id=updated.task_id,
trace_id=updated.trace_id,
state=HermesJobState.CANCELLED,
worker_id=updated.assigned_worker_id,
error={"code": "cancelled", "message": "cancelled by AIOS"},
finished_at=utc_now(),
)
self.store.results[job_id] = result
self.store.persist_locked()
return result
def result(self, job_id: str, *, tenant_id: str) -> HermesResult:
with self.store.lock:
job = self.store.jobs.get(job_id)
if job is None:
raise HermesNotFound("job not found")
self._assert_tenant(job, tenant_id)
result = self.store.results.get(job_id)
if result is None:
raise HermesConflict("job has no terminal result")
return result
def requeue_expired(self) -> tuple[str, ...]:
"""Persistently release expired leases without changing policy state."""
with self.store.lock:
changed = self._requeue_expired_locked()
if changed:
self.store.persist_locked()
return tuple(sorted(changed))
def health(self) -> HermesHealth:
with self.store.lock:
if self._requeue_expired_locked():
self.store.persist_locked()
active_jobs = sum(
1 for job in self.store.jobs.values()
if job.state in {HermesJobState.CLAIMED, HermesJobState.RUNNING}
)
queued_jobs = sum(
1 for job in self.store.jobs.values()
if job.state == HermesJobState.QUEUED
)
partitions = {
(job.tenant_id, job.cell_id, job.profile_id)
for job in self.store.jobs.values()
}
active_workers = sum(
1 for heartbeat, _ in self.store.worker_heartbeats.values()
if self._parse(heartbeat)
and self._parse(heartbeat) >= datetime.now(timezone.utc) - timedelta(seconds=120)
)
return HermesHealth(
runtime_id=self.runtime_id,
status="healthy",
version=self.runtime_version,
heartbeat_at=utc_now(),
active_workers=active_workers,
active_jobs=active_jobs,
queued_jobs=queued_jobs,
tenant_partitions=len(partitions),
profile_isolation_enforced=True,
details={"store": type(self.store).__name__},
)
def _finish(
self,
job_id: str,
*,
tenant_id: str,
worker_id: str,
state: HermesJobState,
output: Mapping[str, object] | None = None,
error: Mapping[str, object] | None = None,
expected_revision: int | None,
) -> HermesResult:
with self.store.lock:
self._requeue_expired_locked()
current = self._owned_job_locked(job_id, tenant_id, worker_id)
if current.state in TERMINAL_STATES:
existing = self.store.results.get(job_id)
if existing is None:
raise HermesConflict("terminal result missing")
if (
existing.state == state
and existing.worker_id == worker_id
and existing.output == output
and existing.error == error
):
return existing
raise HermesConflict("job already has a different terminal result")
self._assert_revision(current, expected_revision)
updated = self._transition_locked(current, state)
result = HermesResult(
job_id=updated.job_id,
tenant_id=updated.tenant_id,
task_id=updated.task_id,
trace_id=updated.trace_id,
state=state,
worker_id=worker_id,
output=output,
error=error,
started_at=current.updated_at,
finished_at=utc_now(),
)
self.store.results[job_id] = result
self.store.worker_heartbeats[worker_id] = (utc_now(), None)
self.store.persist_locked()
return result
def _owned_job_locked(self, job_id: str, tenant_id: str, worker_id: str) -> HermesJob:
current = self.store.jobs.get(job_id)
if current is None:
raise HermesNotFound("job not found")
self._assert_tenant(current, tenant_id)
if current.assigned_worker_id != worker_id:
raise HermesConflict("worker does not own job lease")
return current
@staticmethod
def _assert_revision(job: HermesJob, expected: int | None) -> None:
if expected is not None and job.revision != expected:
raise HermesConflict("job revision changed")
def _transition_locked(
self,
current: HermesJob,
target: HermesJobState,
*,
assigned_worker_id: str | None = None,
lease_expires_at: str | None = None,
) -> HermesJob:
if target not in ALLOWED_TRANSITIONS[current.state]:
raise HermesConflict(
f"invalid transition {current.state.value} -> {target.value}"
)
updated = replace(
current,
state=target,
revision=current.revision + 1,
assigned_worker_id=(
assigned_worker_id
if assigned_worker_id is not None
else current.assigned_worker_id
),
lease_expires_at=(
lease_expires_at
if lease_expires_at is not None
else (None if target in TERMINAL_STATES else current.lease_expires_at)
),
updated_at=utc_now(),
)
self.store.jobs[current.job_id] = updated
return updated
def _requeue_expired_locked(self) -> set[str]:
now = datetime.now(timezone.utc)
changed: set[str] = set()
for current in list(self.store.jobs.values()):
expiry = self._parse(current.lease_expires_at)
if (
current.state in {HermesJobState.CLAIMED, HermesJobState.RUNNING}
and expiry is not None
and expiry <= now
):
self.store.jobs[current.job_id] = replace(
current,
state=HermesJobState.QUEUED,
revision=current.revision + 1,
assigned_worker_id=None,
lease_expires_at=None,
updated_at=utc_now(),
)
changed.add(current.job_id)
return changed
+153
View File
@@ -0,0 +1,153 @@
"""Tenant-scoped Hermes runtime contracts."""
from __future__ import annotations
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Mapping
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def _safe_mapping(value: Mapping[str, Any] | None) -> dict[str, Any]:
return json.loads(json.dumps(dict(value or {}), ensure_ascii=False, sort_keys=True, default=str))
class HermesJobState(str, Enum):
QUEUED = "QUEUED"
CLAIMED = "CLAIMED"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
ALLOWED_TRANSITIONS: Mapping[HermesJobState, frozenset[HermesJobState]] = {
HermesJobState.QUEUED: frozenset({HermesJobState.CLAIMED, HermesJobState.CANCELLED}),
HermesJobState.CLAIMED: frozenset({HermesJobState.RUNNING, HermesJobState.CANCELLED}),
HermesJobState.RUNNING: frozenset(
{HermesJobState.COMPLETED, HermesJobState.FAILED, HermesJobState.CANCELLED}
),
HermesJobState.COMPLETED: frozenset(),
HermesJobState.FAILED: frozenset(),
HermesJobState.CANCELLED: frozenset(),
}
TERMINAL_STATES = frozenset(
{HermesJobState.COMPLETED, HermesJobState.FAILED, HermesJobState.CANCELLED}
)
@dataclass(frozen=True)
class HermesJob:
job_id: str
tenant_id: str
cell_id: str
profile_id: str
task_id: str
trace_id: str
action: str
payload: Mapping[str, Any] = field(default_factory=dict)
idempotency_key: str = ""
priority: int = 100
state: HermesJobState = HermesJobState.QUEUED
revision: int = 1
assigned_worker_id: str | None = None
lease_expires_at: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
def __post_init__(self) -> None:
for name in (
"job_id", "tenant_id", "cell_id", "profile_id", "task_id",
"trace_id", "action", "idempotency_key", "created_at", "updated_at",
):
object.__setattr__(self, name, _required(getattr(self, name), name))
object.__setattr__(self, "payload", _safe_mapping(self.payload))
object.__setattr__(self, "state", HermesJobState(self.state))
if not 0 <= int(self.priority) <= 1000:
raise ValueError("priority must be between 0 and 1000")
if self.revision < 1:
raise ValueError("revision must be >= 1")
@classmethod
def create(
cls,
*,
tenant_id: str,
cell_id: str,
profile_id: str,
task_id: str,
trace_id: str,
action: str,
payload: Mapping[str, Any] | None,
idempotency_key: str,
priority: int = 100,
) -> "HermesJob":
return cls(
job_id=f"hermes_{uuid.uuid4().hex}",
tenant_id=tenant_id,
cell_id=cell_id,
profile_id=profile_id,
task_id=task_id,
trace_id=trace_id,
action=action,
payload=_safe_mapping(payload),
idempotency_key=idempotency_key,
priority=priority,
)
@dataclass(frozen=True)
class HermesResult:
job_id: str
tenant_id: str
task_id: str
trace_id: str
state: HermesJobState
worker_id: str | None = None
output: Mapping[str, Any] | None = None
error: Mapping[str, Any] | None = None
started_at: str | None = None
finished_at: str | None = None
def __post_init__(self) -> None:
for name in ("job_id", "tenant_id", "task_id", "trace_id"):
object.__setattr__(self, name, _required(getattr(self, name), name))
object.__setattr__(self, "state", HermesJobState(self.state))
if self.output is not None:
object.__setattr__(self, "output", _safe_mapping(self.output))
if self.error is not None:
object.__setattr__(self, "error", _safe_mapping(self.error))
@dataclass(frozen=True)
class HermesHealth:
runtime_id: str
status: str
version: str
heartbeat_at: str
active_workers: int = 0
active_jobs: int = 0
queued_jobs: int = 0
tenant_partitions: int = 0
profile_isolation_enforced: bool = True
details: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
for name in ("runtime_id", "status", "version", "heartbeat_at"):
object.__setattr__(self, name, _required(getattr(self, name), name))
for name in ("active_workers", "active_jobs", "queued_jobs", "tenant_partitions"):
if int(getattr(self, name)) < 0:
raise ValueError(f"{name} must be >= 0")
object.__setattr__(self, "details", _safe_mapping(self.details))
+155
View File
@@ -0,0 +1,155 @@
"""Hermes worker loop with lease renewal and bounded recovery."""
from __future__ import annotations
import threading
from dataclasses import dataclass
from typing import Any, Callable, Mapping
from .adapter import HermesAdapter, HermesConflict
from .contract import HermesJob, HermesJobState
JobHandler = Callable[[HermesJob], Mapping[str, Any]]
@dataclass(frozen=True)
class WorkerRunResult:
claimed: bool
job_id: str | None = None
state: str | None = None
error: str | None = None
class HermesWorker:
"""Poll one tenant/cell/profile partition and execute jobs safely."""
def __init__(
self,
adapter: HermesAdapter,
handler: JobHandler,
*,
worker_id: str,
tenant_id: str,
cell_id: str,
profile_id: str,
poll_seconds: float = 1.0,
heartbeat_seconds: float = 10.0,
) -> None:
if poll_seconds <= 0 or heartbeat_seconds <= 0:
raise ValueError("poll_seconds and heartbeat_seconds must be positive")
for name, value in {
"worker_id": worker_id,
"tenant_id": tenant_id,
"cell_id": cell_id,
"profile_id": profile_id,
}.items():
if not str(value or "").strip():
raise ValueError(f"{name} is required")
self.adapter = adapter
self.handler = handler
self.worker_id = worker_id
self.tenant_id = tenant_id
self.cell_id = cell_id
self.profile_id = profile_id
self.poll_seconds = float(poll_seconds)
self.heartbeat_seconds = float(heartbeat_seconds)
self._stop = threading.Event()
def stop(self) -> None:
self._stop.set()
def run_once(self) -> WorkerRunResult:
job = self.adapter.claim(
tenant_id=self.tenant_id,
cell_id=self.cell_id,
profile_id=self.profile_id,
worker_id=self.worker_id,
)
if job is None:
return WorkerRunResult(claimed=False)
running = self.adapter.start(
job.job_id,
tenant_id=self.tenant_id,
worker_id=self.worker_id,
expected_revision=job.revision,
)
heartbeat_stop = threading.Event()
heartbeat_thread = threading.Thread(
target=self._heartbeat_loop,
args=(running.job_id, heartbeat_stop),
name=f"hermes-heartbeat-{self.worker_id}",
daemon=True,
)
heartbeat_thread.start()
output: Mapping[str, Any] | None = None
execution_error: Exception | None = None
try:
output = dict(self.handler(running))
except Exception as exc: # runtime boundary: normalize below
execution_error = exc
finally:
# Stop the lease heartbeat before reading the final revision. This
# prevents a revision increment racing with complete/fail.
heartbeat_stop.set()
heartbeat_thread.join(timeout=max(self.heartbeat_seconds * 2, 1.0))
try:
current = self.adapter.get(running.job_id, tenant_id=self.tenant_id)
if execution_error is None:
result = self.adapter.complete(
running.job_id,
tenant_id=self.tenant_id,
worker_id=self.worker_id,
output=dict(output or {}),
expected_revision=current.revision,
)
return WorkerRunResult(
claimed=True,
job_id=result.job_id,
state=result.state.value,
)
result = self.adapter.fail(
running.job_id,
tenant_id=self.tenant_id,
worker_id=self.worker_id,
error={
"code": "worker_error",
"message": str(execution_error),
},
expected_revision=current.revision,
)
return WorkerRunResult(
claimed=True,
job_id=result.job_id,
state=result.state.value,
error=str(execution_error),
)
except HermesConflict as exc:
return WorkerRunResult(
claimed=True,
job_id=running.job_id,
state=HermesJobState.FAILED.value,
error=f"reconciliation_conflict: {exc}",
)
def run_forever(self) -> None:
while not self._stop.is_set():
result = self.run_once()
if not result.claimed:
self._stop.wait(self.poll_seconds)
def _heartbeat_loop(self, job_id: str, stop_event: threading.Event) -> None:
while not stop_event.wait(self.heartbeat_seconds):
try:
self.adapter.heartbeat(
job_id,
tenant_id=self.tenant_id,
worker_id=self.worker_id,
)
except Exception:
stop_event.set()
return
@@ -1,316 +0,0 @@
"""Task-scoped hardened wrapper around the existing Action API."""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
import urllib.parse
import aios_chatgpt_actions_api as api
import aios_isolated_writer_policy as policy
from isolated_writer_authz import AuthorizationError, authorize
from isolated_writer_commit_file import AtomicCommitFile, CommitFileError, OperationLedger
from isolated_writer_token_registry import TokenRegistry
_reject = api.reject_non_read_http
_write_file = api.write_file
_sync_branch = api.sync_branch
_original_do_get = api.H.do_GET
api.OPENAPI = Path(os.getenv("AIOS_GITOPS_ISOLATED_WRITER_OPENAPI", str(api.OPENAPI)))
def _registry() -> TokenRegistry:
return TokenRegistry(Path(os.environ["AIOS_GITOPS_TOKEN_REGISTRY_V2"]))
def _atomic_writer() -> AtomicCommitFile:
return AtomicCommitFile(
workspace=api.WS,
bare_repo=api.BARE_REPO,
ledger=OperationLedger(Path(os.environ["AIOS_GITOPS_WRITER_OPERATION_LEDGER"])),
remote=os.getenv("AIOS_GITOPS_GITEA_PUSH_REMOTE", ""),
audit=policy.append_audit_record,
)
def _bearer(handler) -> str:
value = handler.headers.get("Authorization", "")
return value[7:].strip() if value.startswith("Bearer ") else ""
def _authorize(handler, *, method: str, path: str, branch=None, target_path=None):
grant = authorize(
_registry(),
_bearer(handler),
method=method,
path=path,
source_ip=str(handler.client_address[0]),
workspace=api.WS,
branch=branch,
target_path=target_path,
)
grant.record["_source_ip"] = str(handler.client_address[0])
return grant
def _authorization_error(handler, exc: AuthorizationError):
return handler.sendj({"error": exc.code}, exc.status)
def _isolated_runtime_status(handler):
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
head = api.sh(["rev-parse", "HEAD"], True)["stdout"]
return handler.sendj({
"service": os.getenv(
"AIOS_GITOPS_SERVICE_AUDIENCE",
"aios-isolated-writer.service",
),
"status": "active",
"runtime_role": os.getenv("AIOS_GITOPS_RUNTIME_ROLE"),
"workspace_role": os.getenv("AIOS_GITOPS_WORKSPACE_ROLE"),
"writes_enabled": os.getenv("AIOS_GITOPS_WRITES_ENABLED", "").lower()
in {"1", "true", "yes", "on"},
"allowed_branch": policy.allowed_branch(),
"approved_base_oid": policy.allowed_base_oid(),
"workspace_branch": branch,
"workspace_head_oid": head,
"repository_identity": os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY"),
"token_policy_version": 2,
"atomic_commit_file": True,
"path_policy_sha256": policy.configured_path_policy_sha256(),
})
def _isolated_do_get(self):
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path in {"/health", "/api/health"}:
return self.sendj({
"status": "ok",
"service": os.getenv(
"AIOS_GITOPS_SERVICE_AUDIENCE",
"aios-isolated-writer.service",
),
})
if path not in {"/api/runtime-status", "/api/baseline", "/api/patches"}:
return self.sendj({"error": "isolated_writer_operation_forbidden"}, 403)
try:
_authorize(self, method="GET", path=path)
except AuthorizationError as exc:
return _authorization_error(self, exc)
if path == "/api/runtime-status":
return _isolated_runtime_status(self)
return _original_do_get(self)
def _read_commit_file_body(handler):
try:
length = int(handler.headers.get("Content-Length", "0"))
except ValueError:
raise CommitFileError("writer_content_length_rejected") from None
if length <= 0:
raise CommitFileError("writer_request_object_required")
if length > 1_100_000:
raise CommitFileError("writer_request_too_large", 413)
raw = handler.rfile.read(length)
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise CommitFileError("writer_request_json_rejected") from None
if not isinstance(value, dict):
raise CommitFileError("writer_request_object_required")
return value
def _isolated_do_post(self):
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != "/api/commit-file":
return self.sendj({"error": "isolated_writer_operation_forbidden"}, 403)
try:
data = _read_commit_file_body(self)
branch = str(data.get("branch") or "")
target_path = str(data.get("path") or "")
grant = _authorize(
self,
method="POST",
path=path,
branch=branch,
target_path=target_path,
)
if api.content_has_secret(str(data.get("content") or "")):
raise CommitFileError("writer_secret_content_rejected", 403)
result = _atomic_writer().execute(grant, data)
return self.sendj(result)
except AuthorizationError as exc:
return _authorization_error(self, exc)
except CommitFileError as exc:
return self.sendj({"error": exc.code}, exc.status)
except PermissionError:
return self.sendj({"error": "isolated_writer_policy_rejected"}, 403)
def reject_non_read_http(handler, method: str, path: str) -> bool:
if _reject(handler, method, path):
return True
try:
policy.validate_endpoint(method, path, api.WS, api.BARE_REPO)
return False
except PermissionError as exc:
handler.sendj({"error": str(exc)}, 403)
return True
def checkout(branch, base="main", create=False):
api.ensure_git()
target = api.branch_name(branch)
policy.validate_branch_request({"branch": target, "base": base})
exists = api.sh(["rev-parse", "--verify", target])["code"] == 0
if exists:
if api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"] != target:
api.sh(["checkout", target], True)
elif create:
api.sh(["checkout", "-b", target, base], True)
else:
raise PermissionError("isolated_writer_branch_missing")
policy.require_service_invariants(api.WS, api.BARE_REPO)
return target
def write_file(path, content):
policy.validate_path(path, api.WS)
return _write_file(path, content)
def sync_branch(branch: str):
policy.validate_sync_branch(branch)
local = _sync_branch(branch)
remote = os.getenv("AIOS_GITOPS_GITEA_PUSH_REMOTE", "").strip()
if remote != "gitea-http":
raise PermissionError("isolated_writer_gitea_remote_rejected")
head = api.sh(["rev-parse", "HEAD"], True)["stdout"]
pushed = subprocess.run(
["git", "push", "--porcelain", remote, f"HEAD:refs/heads/{branch}"],
cwd=api.WS,
text=True,
capture_output=True,
)
if pushed.returncode != 0:
raise RuntimeError("isolated_writer_gitea_push_failed")
verified = subprocess.run(
["git", "ls-remote", "--heads", remote, f"refs/heads/{branch}"],
cwd=api.WS,
text=True,
capture_output=True,
)
if verified.returncode != 0:
raise RuntimeError("isolated_writer_gitea_verification_failed")
fields = verified.stdout.strip().split()
if len(fields) != 2 or fields[0] != head or fields[1] != f"refs/heads/{branch}":
raise RuntimeError("isolated_writer_gitea_oid_mismatch")
return {
"status": "verified",
"target": "authoritative-bare+gitea",
"branch": branch,
"oid": head,
"local": local.get("status"),
}
def commit(paths, msg):
policy.validate_commit_paths(paths, api.WS)
api.ensure_git()
selected = [api.relpath(p) for p in paths] if paths else ["."]
api.sh(["add", *selected], True)
policy.validate_staged_paths(api.WS)
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
if not api.sh(["status", "--short"])["stdout"]:
return {"status": "no_changes", "branch": branch, "sync": sync_branch(branch)}
out = api.sh(["commit", "-m", msg], True)
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
return {
"status": "committed",
"branch": branch,
"rev": api.sh(["rev-parse", "--short", "HEAD"], True)["stdout"],
"git": out,
"sync": sync_branch(branch),
}
def audit(actor, action, target, payload):
policy.append_audit_record(
{
"at": api.now(),
"actor": actor,
"action": action,
"target": target,
"payload": api.sanitize(payload),
}
)
def _validate_request(self, data, validate):
try:
validate(data)
return True
except PermissionError as exc:
self.sendj({"error": str(exc)}, 403)
return False
def _checked_branch(self, data):
if not _validate_request(self, data, policy.validate_branch_request):
return
return _branch(self, data)
def _checked_file(self, data):
data = dict(data)
data.setdefault("branch", policy.allowed_branch())
if not _validate_request(self, data, policy.validate_branch_request):
return
if not _validate_request(
self,
{"path": data.get("path", "")},
lambda item: policy.validate_path(item["path"], api.WS),
):
return
return _file(self, data)
def _checked_commit(self, data):
data = dict(data)
data.setdefault("branch", policy.allowed_branch())
if not _validate_request(self, data, policy.validate_branch_request):
return
return _commit(self, data)
def _checked_patch(self, data):
data = dict(data)
data["branch"] = policy.allowed_branch()
if not _validate_request(self, data, policy.validate_branch_request):
return
return _patch(self, data)
_branch, _file, _commit, _patch = api.H.branch, api.H.file, api.H.commit, api.H.patch
api.reject_non_read_http = reject_non_read_http
api.checkout, api.write_file, api.sync_branch, api.commit, api.audit = (
checkout,
write_file,
sync_branch,
commit,
audit,
)
api.H.branch, api.H.file, api.H.commit, api.H.patch = (
_checked_branch,
_checked_file,
_checked_commit,
_checked_patch,
)
api.H.do_GET = _isolated_do_get
api.H.do_POST = _isolated_do_post
if __name__ == "__main__":
policy.require_service_invariants(api.WS, api.BARE_REPO)
api.ThreadingHTTPServer((api.HOST, api.PORT), api.H).serve_forever()
@@ -1,310 +0,0 @@
"""Fail-closed policy used only by the task-scoped Action writer."""
from __future__ import annotations
import os
import json
import stat
import subprocess
import re
from pathlib import Path, PurePosixPath, PureWindowsPath
from urllib.parse import unquote
import isolated_writer_path_policy as path_policy
WRITE_ENDPOINTS = frozenset({"/api/commit-file"})
ALLOWED_ROOTS = (
"active-runtime",
"docs",
"patches",
"reports",
"scripts",
"test",
"tests",
)
DENIED_ROOTS = ("baseline", "config", "deploy", "deployments", "ops", "runtime", "services")
_TRUE = frozenset({"1", "true", "yes", "on"})
_FALSE = frozenset({"0", "false", "no", "off"})
_DENY_FLAGS = (
"AIOS_GITOPS_ALLOW_MAIN_WRITE",
"AIOS_GITOPS_ALLOW_TAG_WRITE",
"AIOS_GITOPS_ALLOW_BASELINE_WRITE",
"AIOS_GITOPS_ALLOW_REGISTRY_WRITE",
"AIOS_GITOPS_ALLOW_ROLLBACK",
"AIOS_GITOPS_ALLOW_DEPLOYMENT",
)
def enforcement_required() -> bool:
return os.getenv("AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT", "") == "required"
def allowed_branch() -> str:
value = os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", "").strip()
if not value:
raise PermissionError("isolated_writer_missing_allowed_branch")
return value
def allowed_base_oid() -> str:
value = os.getenv("AIOS_GITOPS_ALLOWED_BASE_OID", "").strip().lower()
if len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise PermissionError("isolated_writer_invalid_allowed_base_oid")
return value
def _git(workspace: Path, *args: str) -> str:
result = subprocess.run(["git", *args], cwd=workspace, text=True, capture_output=True)
if result.returncode != 0:
raise PermissionError("isolated_writer_git_validation_failed")
return result.stdout.strip()
def require_service_invariants(workspace: Path, bare_repo: Path) -> None:
if not enforcement_required():
return
if os.getenv("AIOS_GITOPS_WRITES_ENABLED", "").strip().lower() not in _TRUE:
raise PermissionError("isolated_writer_write_gate_required")
if os.getenv("AIOS_GITOPS_RUNTIME_ROLE", "") != "isolated-writer":
raise PermissionError("isolated_writer_role_required")
if os.getenv("AIOS_GITOPS_WORKSPACE_ROLE", "") != "isolated-writer":
raise PermissionError("isolated_writer_workspace_role_required")
if os.getenv("AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED", "") != "2":
raise PermissionError("isolated_writer_token_policy_v2_required")
if not os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", "").strip():
raise PermissionError("isolated_writer_service_audience_required")
if not os.getenv("AIOS_GITOPS_TOKEN_REGISTRY_V2", "").strip():
raise PermissionError("isolated_writer_token_registry_required")
if not os.getenv("AIOS_GITOPS_WRITER_OPERATION_LEDGER", "").strip():
raise PermissionError("isolated_writer_operation_ledger_required")
path_policy.configured_path_policy()
for name in _DENY_FLAGS:
if os.getenv(name, "").strip().lower() not in _FALSE:
raise PermissionError("isolated_writer_forbidden_capability_flag")
if os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "") != "aios/aios":
raise PermissionError("isolated_writer_repository_identity_rejected")
configured_bare_value = os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "").strip()
if not configured_bare_value:
raise PermissionError("isolated_writer_authoritative_repo_rejected")
configured_bare = Path(configured_bare_value)
if configured_bare.resolve(strict=True) != bare_repo.resolve(strict=True):
raise PermissionError("isolated_writer_authoritative_repo_rejected")
writer_root_value = os.getenv("AIOS_GITOPS_APPROVED_WRITER_ROOT", "").strip()
if not writer_root_value:
raise PermissionError("isolated_writer_workspace_outside_approved_root")
writer_root = Path(writer_root_value).resolve(strict=True)
lexical_workspace = Path(os.path.abspath(str(workspace)))
canonical_workspace = lexical_workspace.resolve(strict=True)
if lexical_workspace != canonical_workspace:
raise PermissionError("isolated_writer_workspace_symlink_rejected")
try:
canonical_workspace.relative_to(writer_root)
except (ValueError, FileNotFoundError):
raise PermissionError("isolated_writer_workspace_outside_approved_root") from None
if _git(workspace, "rev-parse", "--abbrev-ref", "HEAD") != allowed_branch():
raise PermissionError("isolated_writer_current_branch_rejected")
approved_base = allowed_base_oid()
try:
_git(workspace, "cat-file", "-e", f"{approved_base}^{{commit}}")
subprocess.run(
["git", "merge-base", "--is-ancestor", approved_base, "HEAD"],
cwd=workspace,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (PermissionError, subprocess.CalledProcessError):
raise PermissionError("isolated_writer_approved_base_not_ancestor") from None
if _git(workspace, "merge-base", approved_base, "HEAD") != approved_base:
raise PermissionError("isolated_writer_merge_base_rejected")
common = Path(_git(workspace, "rev-parse", "--git-common-dir"))
if not common.is_absolute():
common = (workspace / common).resolve()
if common.resolve() != bare_repo.resolve():
raise PermissionError("isolated_writer_common_repo_rejected")
def approved_audit_log_path() -> Path:
root_value = os.getenv("AIOS_GITOPS_APPROVED_AUDIT_ROOT", "").strip()
log_value = os.getenv("AIOS_GITOPS_AUDIT_LOG", "").strip()
if not root_value or not log_value:
raise PermissionError("isolated_writer_audit_path_missing")
lexical_root = Path(os.path.abspath(root_value))
lexical_log = Path(os.path.abspath(log_value))
root = lexical_root.resolve(strict=True)
if lexical_root != root or root.is_symlink() or not root.is_dir():
raise PermissionError("isolated_writer_audit_root_rejected")
try:
lexical_log.relative_to(root)
except ValueError:
raise PermissionError("isolated_writer_audit_path_outside_approved_root") from None
if lexical_log.parent.resolve(strict=True) != lexical_log.parent:
raise PermissionError("isolated_writer_audit_parent_symlink_rejected")
if lexical_log.exists() and lexical_log.is_symlink():
raise PermissionError("isolated_writer_audit_log_symlink_rejected")
os.chmod(root, 0o750)
os.chmod(lexical_log.parent, 0o750)
return lexical_log
def append_audit_record(record: dict) -> None:
path = approved_audit_log_path()
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags, 0o600)
try:
info = os.fstat(descriptor)
if not stat.S_ISREG(info.st_mode):
raise PermissionError("isolated_writer_audit_log_not_regular")
os.fchmod(descriptor, 0o600)
line = json.dumps(
_audit_sanitize(record),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8") + b"\n"
os.write(descriptor, line)
os.fsync(descriptor)
finally:
os.close(descriptor)
parent_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(parent_fd)
finally:
os.close(parent_fd)
def _audit_sanitize(value):
if isinstance(value, dict):
return {
key: (
"[REDACTED]"
if any(
marker in str(key).lower()
for marker in (
"authorization",
"credential",
"password",
"secret",
"token",
"private_key",
)
)
and str(key).lower() != "token_id"
else _audit_sanitize(inner)
)
for key, inner in value.items()
}
if isinstance(value, list):
return [_audit_sanitize(item) for item in value]
if isinstance(value, str) and re.search(
r"(?i)(authorization\s*:\s*bearer|(?:token|password|secret)\s*[:=])",
value,
):
return "[REDACTED]"
return value
def validate_endpoint(method: str, path: str, workspace: Path, bare_repo: Path) -> None:
if not enforcement_required():
return
if method != "POST" or path not in WRITE_ENDPOINTS:
raise PermissionError("isolated_writer_operation_forbidden")
require_service_invariants(workspace, bare_repo)
def validate_branch_request(data: dict) -> None:
if not enforcement_required():
return
branch = allowed_branch() if "branch" not in data else str(data.get("branch") or "").strip()
if branch != allowed_branch():
raise PermissionError("isolated_writer_branch_rejected")
if str(data.get("base", "main")).strip() != "main":
raise PermissionError("isolated_writer_base_rejected")
if data.get("force") or data.get("force_push"):
raise PermissionError("isolated_writer_force_rejected")
def validate_path(path: str, workspace: Path | None = None) -> str:
if not isinstance(path, str):
raise PermissionError("isolated_writer_path_rejected")
value = path
if not value or value != value.strip() or "\x00" in value or "\\" in value:
raise PermissionError("isolated_writer_path_rejected")
try:
decoded = unquote(value, errors="strict")
except (UnicodeDecodeError, ValueError):
raise PermissionError("isolated_writer_path_rejected") from None
if decoded != value:
raise PermissionError("isolated_writer_path_rejected")
windows = PureWindowsPath(value)
posix = PurePosixPath(value)
parts = value.split("/")
if (
posix.is_absolute()
or windows.is_absolute()
or bool(windows.drive)
or any(part in {"", ".", ".."} for part in parts)
):
raise PermissionError("isolated_writer_path_rejected")
first = parts[0]
service_exact_path = False
if first in DENIED_ROOTS:
try:
path_policy.require_service_path("commit_file.atomic", value)
service_exact_path = True
except path_policy.PathPolicyError as exc:
raise PermissionError(str(exc)) from None
if value != "README.md" and first not in ALLOWED_ROOTS and not service_exact_path:
raise PermissionError("isolated_writer_path_outside_allowed_roots")
if workspace is not None:
lexical_root = Path(os.path.abspath(str(workspace)))
root = lexical_root.resolve(strict=True)
if lexical_root != root:
raise PermissionError("isolated_writer_workspace_symlink_rejected")
current = root
for part in parts:
current = current / part
if current.is_symlink():
raise PermissionError("isolated_writer_path_symlink_rejected")
candidate = (root / value).resolve(strict=False)
try:
candidate.relative_to(root)
except ValueError:
raise PermissionError("isolated_writer_path_escapes_workspace") from None
return value
def validate_content_for_path(path: str, content: str) -> None:
if path.split("/", 1)[0] not in DENIED_ROOTS:
return
try:
path_policy.validate_content("commit_file.atomic", path, content)
except path_policy.PathPolicyError as exc:
raise PermissionError(str(exc)) from None
def configured_path_policy_sha256() -> str | None:
try:
return path_policy.configured_path_policy_sha256()
except path_policy.PathPolicyError as exc:
raise PermissionError(str(exc)) from None
def validate_commit_paths(paths: list[str], workspace: Path | None = None) -> None:
if enforcement_required():
for path in paths:
if str(path).strip() != ".":
validate_path(str(path), workspace)
def validate_staged_paths(workspace: Path) -> None:
if not enforcement_required():
return
for path in _git(workspace, "diff", "--cached", "--name-only").splitlines():
validate_path(path, workspace)
def validate_sync_branch(branch: str) -> None:
if enforcement_required() and branch != allowed_branch():
raise PermissionError("isolated_writer_sync_branch_rejected")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
"""AIOS-managed OpenClaw Ed25519 execution adapter."""
from .adapter import (
AdapterError,
CommandConflict,
CommandNotFound,
CommandRejected,
InMemoryCallbackSink,
OpenClawAdapter,
)
from .contract import (
CONTRACT_VERSION,
SIGNATURE_ALGORITHM,
CommandState,
Ed25519CommandSigner,
Ed25519CommandVerifier,
OpenClawCommand,
OpenClawResult,
RuntimeHealth,
sign_command,
verify_command_signature,
)
from .transport import HTTPRuntimeTransport, InProcessRuntimeTransport, RuntimeTransport
__all__ = [
"AdapterError",
"CONTRACT_VERSION",
"CommandConflict",
"CommandNotFound",
"CommandRejected",
"CommandState",
"Ed25519CommandSigner",
"Ed25519CommandVerifier",
"HTTPRuntimeTransport",
"InMemoryCallbackSink",
"InProcessRuntimeTransport",
"OpenClawAdapter",
"OpenClawCommand",
"OpenClawResult",
"RuntimeHealth",
"RuntimeTransport",
"SIGNATURE_ALGORITHM",
"sign_command",
"verify_command_signature",
]
+228
View File
@@ -0,0 +1,228 @@
"""Signed, tenant-isolated execution adapter for OpenClaw runtimes."""
from __future__ import annotations
import threading
from dataclasses import replace
from typing import Callable, Mapping, Protocol
from .contract import (
CommandState,
OpenClawCommand,
OpenClawResult,
RuntimeHealth,
utc_now,
verify_command_signature,
)
class AdapterError(RuntimeError):
"""Base adapter failure."""
class CommandRejected(AdapterError):
"""A command failed signature or contract validation."""
class CommandNotFound(AdapterError):
"""A tenant-scoped command is not visible."""
class CommandConflict(AdapterError):
"""A command or idempotency key conflicts with an existing execution."""
class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ...
class CallbackSink(Protocol):
def on_started(self, result: OpenClawResult) -> None: ...
def on_completed(self, result: OpenClawResult) -> None: ...
def on_failed(self, result: OpenClawResult) -> None: ...
class InMemoryCallbackSink:
def __init__(self) -> None:
self._lock = threading.RLock()
self.started: list[OpenClawResult] = []
self.completed: list[OpenClawResult] = []
self.failed: list[OpenClawResult] = []
def on_started(self, result: OpenClawResult) -> None:
with self._lock:
self.started.append(result)
def on_completed(self, result: OpenClawResult) -> None:
with self._lock:
self.completed.append(result)
def on_failed(self, result: OpenClawResult) -> None:
with self._lock:
self.failed.append(result)
SigningMaterialResolver = Callable[[str, str], bytes | str]
class OpenClawAdapter:
"""Verify, de-duplicate, execute, cancel and reconcile commands."""
def __init__(
self,
transport: RuntimeTransport,
signing_material_resolver: SigningMaterialResolver,
*,
callback_sink: CallbackSink | None = None,
default_timeout_seconds: float = 300.0,
) -> None:
if default_timeout_seconds <= 0:
raise ValueError("default_timeout_seconds must be positive")
self.transport = transport
self.signing_material_resolver = signing_material_resolver
self.callback_sink = callback_sink or InMemoryCallbackSink()
self.default_timeout_seconds = float(default_timeout_seconds)
self._lock = threading.RLock()
self._commands: dict[str, OpenClawCommand] = {}
self._results: dict[str, OpenClawResult] = {}
self._idempotency: dict[tuple[str, str], tuple[str, str]] = {}
def _verify(self, command: OpenClawCommand) -> None:
try:
key_material = self.signing_material_resolver(command.tenant_id, command.key_id)
except Exception as exc:
raise CommandRejected("signing material unavailable") from exc
if not verify_command_signature(command, key_material):
raise CommandRejected("invalid or expired command signature")
@staticmethod
def _validate_result(command: OpenClawCommand, result: OpenClawResult) -> None:
if (
result.command_id != command.command_id
or result.tenant_id != command.tenant_id
or result.task_id != command.task_id
or result.trace_id != command.trace_id
):
raise AdapterError("runtime returned mismatched command identity")
if result.state not in {
CommandState.COMPLETED,
CommandState.FAILED,
CommandState.CANCELLED,
CommandState.TIMED_OUT,
}:
raise AdapterError("runtime returned a non-terminal result")
def execute(
self,
command: OpenClawCommand,
*,
timeout_seconds: float | None = None,
) -> OpenClawResult:
timeout = self.default_timeout_seconds if timeout_seconds is None else float(timeout_seconds)
if timeout <= 0:
raise ValueError("timeout_seconds must be positive")
self._verify(command)
digest = command.signature
idem_ref = (command.tenant_id, command.idempotency_key)
with self._lock:
prior_command = self._commands.get(command.command_id)
if prior_command is not None:
if prior_command.signature != command.signature:
raise CommandConflict("command_id reused with different content")
return self._results[command.command_id]
prior_idem = self._idempotency.get(idem_ref)
if prior_idem is not None:
prior_id, prior_digest = prior_idem
if prior_digest != digest:
raise CommandConflict("idempotency key reused with different content")
return self._results[prior_id]
running = OpenClawResult(
command_id=command.command_id,
tenant_id=command.tenant_id,
task_id=command.task_id,
trace_id=command.trace_id,
state=CommandState.RUNNING,
started_at=utc_now(),
)
self._commands[command.command_id] = command
self._results[command.command_id] = running
self._idempotency[idem_ref] = (command.command_id, digest)
self.callback_sink.on_started(running)
try:
result = self.transport.execute(command, timeout)
self._validate_result(command, result)
except TimeoutError as exc:
result = replace(
running,
state=CommandState.TIMED_OUT,
error={"code": "runtime_timeout", "message": str(exc) or "runtime timeout"},
finished_at=utc_now(),
)
except Exception as exc:
result = replace(
running,
state=CommandState.FAILED,
error={"code": "runtime_error", "message": str(exc)},
finished_at=utc_now(),
)
if result.started_at is None:
result = replace(result, started_at=running.started_at)
if result.finished_at is None:
result = replace(result, finished_at=utc_now())
with self._lock:
self._results[command.command_id] = result
if result.state == CommandState.COMPLETED:
self.callback_sink.on_completed(result)
else:
self.callback_sink.on_failed(result)
return result
def status(self, command_id: str, *, tenant_id: str) -> OpenClawResult:
with self._lock:
command = self._commands.get(command_id)
result = self._results.get(command_id)
if command is None or result is None or command.tenant_id != tenant_id:
raise CommandNotFound("command not found")
return result
def cancel(self, command_id: str, *, tenant_id: str) -> OpenClawResult:
with self._lock:
command = self._commands.get(command_id)
current = self._results.get(command_id)
if command is None or current is None or command.tenant_id != tenant_id:
raise CommandNotFound("command not found")
if current.state in {
CommandState.COMPLETED,
CommandState.FAILED,
CommandState.CANCELLED,
CommandState.TIMED_OUT,
CommandState.REJECTED,
}:
return current
if not self.transport.cancel(command_id):
raise AdapterError("runtime rejected cancellation")
cancelled = replace(
current,
state=CommandState.CANCELLED,
error={"code": "cancelled", "message": "cancelled by AIOS"},
finished_at=utc_now(),
)
with self._lock:
self._results[command_id] = cancelled
self.callback_sink.on_failed(cancelled)
return cancelled
def health(self) -> RuntimeHealth:
return self.transport.health()
def snapshot(self, *, tenant_id: str) -> Mapping[str, OpenClawResult]:
with self._lock:
return {
command_id: result
for command_id, result in self._results.items()
if self._commands[command_id].tenant_id == tenant_id
}
@@ -0,0 +1,97 @@
"""Audit-oriented lifecycle callbacks for OpenClaw execution."""
from __future__ import annotations
import json
import threading
from dataclasses import dataclass, field
from typing import Any, Mapping, Protocol
from .contract import OpenClawResult, utc_now
class EventPublisher(Protocol):
def publish(self, event: Mapping[str, Any]) -> None: ...
@dataclass(frozen=True)
class ExecutionEvent:
event_type: str
tenant_id: str
task_id: str
trace_id: str
command_id: str
state: str
created_at: str = field(default_factory=utc_now)
details: Mapping[str, Any] = field(default_factory=dict)
def as_dict(self) -> dict[str, Any]:
return {
"event_type": self.event_type,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"trace_id": self.trace_id,
"command_id": self.command_id,
"state": self.state,
"created_at": self.created_at,
"details": json.loads(
json.dumps(dict(self.details), ensure_ascii=False, default=str)
),
}
class InMemoryEventPublisher:
"""Thread-safe publisher used by tests and first integration slice."""
def __init__(self) -> None:
self._lock = threading.RLock()
self.events: list[dict[str, Any]] = []
def publish(self, event: Mapping[str, Any]) -> None:
with self._lock:
self.events.append(
json.loads(json.dumps(dict(event), ensure_ascii=False, default=str))
)
class AuditCallbackSink:
"""Convert runtime callbacks into tenant-scoped audit events."""
def __init__(self, publisher: EventPublisher) -> None:
self.publisher = publisher
@staticmethod
def _event(
event_type: str,
result: OpenClawResult,
) -> ExecutionEvent:
details: dict[str, Any] = {
"runtime_id": result.runtime_id,
"attempts": result.attempts,
}
if result.error:
details["error"] = dict(result.error)
if result.output:
details["output_present"] = True
return ExecutionEvent(
event_type=event_type,
tenant_id=result.tenant_id,
task_id=result.task_id,
trace_id=result.trace_id,
command_id=result.command_id,
state=result.state.value,
details=details,
)
def on_started(self, result: OpenClawResult) -> None:
self.publisher.publish(self._event("OPENCLAW_STARTED", result).as_dict())
def on_completed(self, result: OpenClawResult) -> None:
self.publisher.publish(self._event("OPENCLAW_COMPLETED", result).as_dict())
def on_failed(self, result: OpenClawResult) -> None:
event_type = {
"CANCELLED": "OPENCLAW_CANCELLED",
"TIMED_OUT": "OPENCLAW_TIMED_OUT",
"REJECTED": "OPENCLAW_REJECTED",
}.get(result.state.value, "OPENCLAW_FAILED")
self.publisher.publish(self._event(event_type, result).as_dict())
@@ -0,0 +1,383 @@
"""AIOS 到 OpenClaw 的 Ed25519 执行合同。"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import uuid
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Mapping, Protocol
CONTRACT_VERSION = "aios.openclaw.command.v2"
SIGNATURE_ALGORITHM = "Ed25519"
DEFAULT_CLOCK_SKEW_SECONDS = 300
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _parse_timestamp(value: str) -> datetime:
normalized = str(value or "").strip()
if not normalized:
raise ValueError("timestamp is required")
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("timestamp must include a timezone")
return parsed.astimezone(timezone.utc)
def _required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def _safe_mapping(value: Mapping[str, Any] | None) -> dict[str, Any]:
return json.loads(
json.dumps(dict(value or {}), ensure_ascii=False, sort_keys=True, default=str)
)
def _load_crypto() -> tuple[Any, Any, Any]:
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
except ImportError as exc: # pragma: no cover
raise RuntimeError("cryptography with Ed25519 support is required") from exc
return serialization, Ed25519PrivateKey, Ed25519PublicKey
class CommandSigner(Protocol):
key_id: str
fingerprint: str
def sign(self, payload: bytes) -> bytes: ...
class CommandVerifier(Protocol):
key_id: str
fingerprint: str
def verify(self, payload: bytes, signature: bytes) -> bool: ...
class Ed25519CommandSigner:
def __init__(self, private_key: Any, *, key_id: str) -> None:
serialization, private_cls, _ = _load_crypto()
if not isinstance(private_key, private_cls):
raise ValueError("private key is not Ed25519")
self.private_key = private_key
self.key_id = _required(key_id, "key_id")
raw = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_bytes(cls, value: bytes | str, *, key_id: str) -> "Ed25519CommandSigner":
serialization, private_cls, _ = _load_crypto()
raw = value.encode("utf-8") if isinstance(value, str) else bytes(value)
if raw.startswith(b"-----BEGIN"):
key = serialization.load_pem_private_key(raw, password=None)
elif len(raw) == 32:
key = private_cls.from_private_bytes(raw)
else:
raise ValueError("Ed25519 private key must be PEM or a raw 32-byte seed")
return cls(key, key_id=key_id)
def sign(self, payload: bytes) -> bytes:
return self.private_key.sign(bytes(payload))
def verifier(self) -> "Ed25519CommandVerifier":
return Ed25519CommandVerifier(self.private_key.public_key(), key_id=self.key_id)
class Ed25519CommandVerifier:
def __init__(self, public_key: Any, *, key_id: str) -> None:
serialization, _, public_cls = _load_crypto()
if not isinstance(public_key, public_cls):
raise ValueError("public key is not Ed25519")
self.public_key = public_key
self.key_id = _required(key_id, "key_id")
raw = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_bytes(cls, value: bytes | str, *, key_id: str) -> "Ed25519CommandVerifier":
serialization, private_cls, public_cls = _load_crypto()
raw = value.encode("utf-8") if isinstance(value, str) else bytes(value)
if raw.startswith(b"-----BEGIN"):
try:
key = serialization.load_pem_public_key(raw)
except ValueError:
private = serialization.load_pem_private_key(raw, password=None)
if not isinstance(private, private_cls):
raise ValueError("key is not Ed25519")
key = private.public_key()
elif len(raw) == 32:
key = public_cls.from_public_bytes(raw)
else:
raise ValueError("Ed25519 public key must be PEM or raw 32-byte key")
return cls(key, key_id=key_id)
def verify(self, payload: bytes, signature: bytes) -> bool:
try:
self.public_key.verify(bytes(signature), bytes(payload))
return True
except Exception:
return False
class CommandState(str, Enum):
ACCEPTED = "ACCEPTED"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
TIMED_OUT = "TIMED_OUT"
REJECTED = "REJECTED"
@dataclass(frozen=True)
class OpenClawCommand:
command_id: str
tenant_id: str
task_id: str
trace_id: str
action: str
payload: Mapping[str, Any] = field(default_factory=dict)
idempotency_key: str = ""
created_at: str = field(default_factory=utc_now)
expires_at: str | None = None
contract_version: str = CONTRACT_VERSION
signature_algorithm: str = SIGNATURE_ALGORITHM
key_id: str = "default"
public_key_fingerprint: str = ""
signature: str = ""
def __post_init__(self) -> None:
for field_name in (
"command_id",
"tenant_id",
"task_id",
"trace_id",
"action",
"idempotency_key",
"created_at",
"contract_version",
"signature_algorithm",
"key_id",
):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
if self.contract_version != CONTRACT_VERSION:
raise ValueError(f"unsupported contract_version: {self.contract_version}")
if self.signature_algorithm != SIGNATURE_ALGORITHM:
raise ValueError(
f"unsupported signature_algorithm: {self.signature_algorithm}"
)
object.__setattr__(self, "payload", _safe_mapping(self.payload))
object.__setattr__(
self, "public_key_fingerprint", str(self.public_key_fingerprint or "").strip()
)
object.__setattr__(self, "signature", str(self.signature or "").strip())
_parse_timestamp(self.created_at)
if self.expires_at is not None:
object.__setattr__(self, "expires_at", _required(self.expires_at, "expires_at"))
if _parse_timestamp(self.expires_at) <= _parse_timestamp(self.created_at):
raise ValueError("expires_at must be later than created_at")
@classmethod
def create(
cls,
*,
tenant_id: str,
task_id: str,
trace_id: str,
action: str,
payload: Mapping[str, Any] | None,
idempotency_key: str,
expires_at: str | None = None,
key_id: str = "default",
command_id: str | None = None,
) -> "OpenClawCommand":
return cls(
command_id=command_id or f"cmd_{uuid.uuid4().hex}",
tenant_id=tenant_id,
task_id=task_id,
trace_id=trace_id,
action=action,
payload=_safe_mapping(payload),
idempotency_key=idempotency_key,
expires_at=expires_at,
key_id=key_id,
)
def unsigned_dict(self) -> dict[str, Any]:
return {
"command_id": self.command_id,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"trace_id": self.trace_id,
"action": self.action,
"payload": _safe_mapping(self.payload),
"idempotency_key": self.idempotency_key,
"created_at": self.created_at,
"expires_at": self.expires_at,
"contract_version": self.contract_version,
"signature_algorithm": self.signature_algorithm,
"key_id": self.key_id,
"public_key_fingerprint": self.public_key_fingerprint,
}
@dataclass(frozen=True)
class OpenClawResult:
command_id: str
tenant_id: str
task_id: str
trace_id: str
state: CommandState
output: Mapping[str, Any] | None = None
error: Mapping[str, Any] | None = None
started_at: str | None = None
finished_at: str | None = None
runtime_id: str | None = None
attempts: int = 1
def __post_init__(self) -> None:
for field_name in ("command_id", "tenant_id", "task_id", "trace_id"):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
object.__setattr__(self, "state", CommandState(self.state))
if self.output is not None:
object.__setattr__(self, "output", _safe_mapping(self.output))
if self.error is not None:
object.__setattr__(self, "error", _safe_mapping(self.error))
if self.started_at is not None:
_parse_timestamp(self.started_at)
if self.finished_at is not None:
_parse_timestamp(self.finished_at)
if self.attempts < 1:
raise ValueError("attempts must be >= 1")
@dataclass(frozen=True)
class RuntimeHealth:
runtime: str
runtime_id: str
status: str
version: str
heartbeat_at: str
active_tasks: int = 0
workspace_isolated: bool = False
credential_ref_configured: bool = False
details: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
for field_name in (
"runtime",
"runtime_id",
"status",
"version",
"heartbeat_at",
):
object.__setattr__(
self, field_name, _required(getattr(self, field_name), field_name)
)
_parse_timestamp(self.heartbeat_at)
if self.active_tasks < 0:
raise ValueError("active_tasks must be >= 0")
object.__setattr__(self, "details", _safe_mapping(self.details))
def canonical_command_bytes(command: OpenClawCommand) -> bytes:
return json.dumps(
command.unsigned_dict(),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def _as_signer(command: OpenClawCommand, value: Any) -> CommandSigner:
if hasattr(value, "sign") and hasattr(value, "fingerprint") and hasattr(value, "key_id"):
return value
return Ed25519CommandSigner.from_bytes(value, key_id=command.key_id)
def _as_verifier(command: OpenClawCommand, value: Any) -> CommandVerifier:
if isinstance(value, Ed25519CommandSigner):
return value.verifier()
if hasattr(value, "verify") and hasattr(value, "fingerprint") and hasattr(value, "key_id"):
return value
return Ed25519CommandVerifier.from_bytes(value, key_id=command.key_id)
def sign_command(command: OpenClawCommand, signing_key: Any) -> OpenClawCommand:
signer = _as_signer(command, signing_key)
if signer.key_id != command.key_id:
raise ValueError("command key_id does not match signer")
prepared = replace(
command,
signature_algorithm=SIGNATURE_ALGORITHM,
public_key_fingerprint=signer.fingerprint,
signature="",
)
signature = base64.b64encode(
signer.sign(canonical_command_bytes(prepared))
).decode("ascii")
return replace(prepared, signature=signature)
def verify_command_signature(
command: OpenClawCommand,
verification_key: Any,
*,
now: datetime | None = None,
clock_skew_seconds: int = DEFAULT_CLOCK_SKEW_SECONDS,
) -> bool:
if (
not command.signature
or command.contract_version != CONTRACT_VERSION
or command.signature_algorithm != SIGNATURE_ALGORITHM
):
return False
try:
verifier = _as_verifier(command, verification_key)
signature = base64.b64decode(command.signature, validate=True)
except Exception:
return False
if verifier.key_id != command.key_id:
return False
if not command.public_key_fingerprint or not hmac.compare_digest(
command.public_key_fingerprint, verifier.fingerprint
):
return False
if not verifier.verify(canonical_command_bytes(command), signature):
return False
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
created = _parse_timestamp(command.created_at)
if created.timestamp() - current.timestamp() > clock_skew_seconds:
return False
if command.expires_at is not None:
expires = _parse_timestamp(command.expires_at)
if current.timestamp() - expires.timestamp() > clock_skew_seconds:
return False
return True
+103
View File
@@ -0,0 +1,103 @@
"""Health policy and self-healing decisions for OpenClaw runtimes."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Mapping
from .contract import RuntimeHealth
def _parse(value: str) -> datetime:
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("heartbeat timestamp must include timezone")
return parsed.astimezone(timezone.utc)
@dataclass(frozen=True)
class HealthPolicy:
max_heartbeat_age_seconds: int = 90
require_workspace_isolation: bool = True
require_credential_ref: bool = True
accepted_statuses: frozenset[str] = field(
default_factory=lambda: frozenset({"healthy", "ready"})
)
def __post_init__(self) -> None:
if self.max_heartbeat_age_seconds < 5:
raise ValueError("max_heartbeat_age_seconds must be >= 5")
if not self.accepted_statuses:
raise ValueError("accepted_statuses must not be empty")
@dataclass(frozen=True)
class HealthAssessment:
healthy: bool
reasons: tuple[str, ...]
recoverable: bool
recommended_action: str
details: Mapping[str, object] = field(default_factory=dict)
class RuntimeHealthMonitor:
"""Evaluate runtime health and recommend bounded recovery actions."""
def __init__(self, policy: HealthPolicy | None = None) -> None:
self.policy = policy or HealthPolicy()
def assess(
self,
health: RuntimeHealth,
*,
now: datetime | None = None,
) -> HealthAssessment:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
heartbeat = _parse(health.heartbeat_at)
age = max(0.0, current.timestamp() - heartbeat.timestamp())
reasons: list[str] = []
if health.status.lower() not in self.policy.accepted_statuses:
reasons.append("runtime_status_unhealthy")
if age > self.policy.max_heartbeat_age_seconds:
reasons.append("heartbeat_stale")
if self.policy.require_workspace_isolation and not health.workspace_isolated:
reasons.append("workspace_not_isolated")
if self.policy.require_credential_ref and not health.credential_ref_configured:
reasons.append("credential_reference_missing")
if not reasons:
return HealthAssessment(
healthy=True,
reasons=(),
recoverable=True,
recommended_action="none",
details={"heartbeat_age_seconds": age},
)
non_recoverable = {
"workspace_not_isolated",
"credential_reference_missing",
}.intersection(reasons)
if non_recoverable:
action = "quarantine_runtime"
recoverable = False
elif "heartbeat_stale" in reasons:
action = "restart_runtime"
recoverable = True
else:
action = "drain_and_restart"
recoverable = True
return HealthAssessment(
healthy=False,
reasons=tuple(reasons),
recoverable=recoverable,
recommended_action=action,
details={
"heartbeat_age_seconds": age,
"runtime_id": health.runtime_id,
"active_tasks": health.active_tasks,
},
)
@@ -0,0 +1,214 @@
"""Runtime transport implementations for the OpenClaw adapter."""
from __future__ import annotations
import json
import threading
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Callable, Mapping, Protocol
from .contract import (
CommandState,
OpenClawCommand,
OpenClawResult,
RuntimeHealth,
utc_now,
)
class RuntimeTransport(Protocol):
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult: ...
def cancel(self, command_id: str) -> bool: ...
def health(self) -> RuntimeHealth: ...
class TransportError(RuntimeError):
"""Runtime transport or protocol failure."""
RuntimeHandler = Callable[[OpenClawCommand], Mapping[str, Any] | OpenClawResult]
class InProcessRuntimeTransport:
"""Deterministic runtime used by tests and controlled local integrations."""
def __init__(
self,
handler: RuntimeHandler,
*,
runtime_id: str = "openclaw-in-process",
version: str = "v1",
workspace_isolated: bool = True,
credential_ref_configured: bool = True,
) -> None:
self.handler = handler
self.runtime_id = runtime_id
self.version = version
self.workspace_isolated = workspace_isolated
self.credential_ref_configured = credential_ref_configured
self._lock = threading.RLock()
self._running: set[str] = set()
self._cancelled: set[str] = set()
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult:
if timeout_seconds <= 0:
raise ValueError("timeout_seconds must be positive")
with self._lock:
if command.command_id in self._cancelled:
return OpenClawResult(
command_id=command.command_id,
tenant_id=command.tenant_id,
task_id=command.task_id,
trace_id=command.trace_id,
state=CommandState.CANCELLED,
error={"code": "cancelled", "message": "cancelled before execution"},
finished_at=utc_now(),
runtime_id=self.runtime_id,
)
self._running.add(command.command_id)
try:
raw = self.handler(command)
if isinstance(raw, OpenClawResult):
return raw
if not isinstance(raw, Mapping):
raise TransportError("runtime handler must return a mapping or OpenClawResult")
return OpenClawResult(
command_id=command.command_id,
tenant_id=command.tenant_id,
task_id=command.task_id,
trace_id=command.trace_id,
state=CommandState(str(raw.get("state", "COMPLETED"))),
output=raw.get("output"),
error=raw.get("error"),
started_at=str(raw.get("started_at") or utc_now()),
finished_at=str(raw.get("finished_at") or utc_now()),
runtime_id=str(raw.get("runtime_id") or self.runtime_id),
attempts=int(raw.get("attempts", 1)),
)
finally:
with self._lock:
self._running.discard(command.command_id)
def cancel(self, command_id: str) -> bool:
with self._lock:
self._cancelled.add(command_id)
return command_id in self._running
def health(self) -> RuntimeHealth:
with self._lock:
active = len(self._running)
return RuntimeHealth(
runtime="openclaw",
runtime_id=self.runtime_id,
status="healthy",
version=self.version,
heartbeat_at=utc_now(),
active_tasks=active,
workspace_isolated=self.workspace_isolated,
credential_ref_configured=self.credential_ref_configured,
details={"transport": "in_process"},
)
class HTTPRuntimeTransport:
"""Private HTTP transport for an OpenClaw runtime service."""
def __init__(
self,
base_url: str,
*,
runtime_id: str = "openclaw-http",
credential_ref: str | None = None,
max_response_bytes: int = 2_097_152,
) -> None:
normalized = str(base_url).rstrip("/")
if not normalized.startswith(("http://", "https://")):
raise ValueError("base_url must use http or https")
self.base_url = normalized
self.runtime_id = runtime_id
self.credential_ref = credential_ref
self.max_response_bytes = int(max_response_bytes)
def _request(
self,
method: str,
path: str,
*,
payload: Mapping[str, Any] | None,
timeout_seconds: float,
) -> Mapping[str, Any]:
body = None
headers = {"Accept": "application/json", "User-Agent": "AIOS-OpenClaw/1"}
if payload is not None:
body = json.dumps(dict(payload), separators=(",", ":")).encode("utf-8")
headers["Content-Type"] = "application/json"
if self.credential_ref:
headers["X-AIOS-Credential-Ref"] = self.credential_ref
request = urllib.request.Request(
f"{self.base_url}{path}",
data=body,
method=method,
headers=headers,
)
try:
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
raw = response.read(self.max_response_bytes + 1)
except urllib.error.HTTPError as exc:
raise TransportError(f"runtime returned HTTP {exc.code}") from exc
except urllib.error.URLError as exc:
raise TransportError(f"runtime unavailable: {exc.reason}") from exc
if len(raw) > self.max_response_bytes:
raise TransportError("runtime response exceeded size limit")
decoded = json.loads(raw.decode("utf-8"))
if not isinstance(decoded, Mapping):
raise TransportError("runtime response must be a JSON object")
return decoded
@staticmethod
def _command_payload(command: OpenClawCommand) -> dict[str, Any]:
return {**command.unsigned_dict(), "signature": command.signature}
def execute(self, command: OpenClawCommand, timeout_seconds: float) -> OpenClawResult:
value = self._request(
"POST",
"/v1/execute",
payload=self._command_payload(command),
timeout_seconds=timeout_seconds,
)
return OpenClawResult(
command_id=str(value["command_id"]),
tenant_id=str(value["tenant_id"]),
task_id=str(value["task_id"]),
trace_id=str(value["trace_id"]),
state=CommandState(str(value["state"])),
output=value.get("output"),
error=value.get("error"),
started_at=value.get("started_at"),
finished_at=value.get("finished_at"),
runtime_id=str(value.get("runtime_id") or self.runtime_id),
attempts=int(value.get("attempts", 1)),
)
def cancel(self, command_id: str) -> bool:
value = self._request(
"POST",
f"/v1/commands/{urllib.parse.quote(command_id, safe='')}/cancel",
payload={},
timeout_seconds=10.0,
)
return bool(value.get("accepted"))
def health(self) -> RuntimeHealth:
value = self._request("GET", "/v1/health", payload=None, timeout_seconds=5.0)
return RuntimeHealth(
runtime=str(value.get("runtime") or "openclaw"),
runtime_id=str(value.get("runtime_id") or self.runtime_id),
status=str(value.get("status") or "unknown"),
version=str(value.get("version") or "unknown"),
heartbeat_at=str(value.get("heartbeat_at") or utc_now()),
active_tasks=int(value.get("active_tasks", 0)),
workspace_isolated=bool(value.get("workspace_isolated", False)),
credential_ref_configured=bool(value.get("credential_ref_configured", False)),
details=value.get("details") if isinstance(value.get("details"), Mapping) else {},
)
@@ -1,144 +0,0 @@
"""HTTP adapter for audited Owner authorization and writer provisioning.
This module layers on top of ``aios_authorized_task_writer_api`` so existing
routes and the legacy exact ``writer:provision`` route remain available. The
new endpoint does not mint or modify bearer credentials. It authenticates an
existing, non-wildcard, explicitly allow-listed Owner credential and then
invokes the audited broker.
Endpoint:
POST /api/governance/owner-authorizations
"""
from __future__ import annotations
import os
import urllib.parse
from typing import Any, Mapping
import aios_authorized_task_writer_api as bridge_api
from authorized_task_writer_provisioning import ProvisioningError
from owner_authorization_broker import (
OwnerAuthorizationBroker,
OwnerAuthorizationError,
OwnerPrincipal,
)
api = bridge_api.api
_original_do_post = api.H.do_POST
_ENDPOINT = "/api/governance/owner-authorizations"
def _enabled() -> bool:
return os.getenv(
"AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED", ""
).strip().lower() in {"1", "true", "yes", "on"}
def _configured_intake_scope() -> str:
scopes = [
item.strip()
for item in os.getenv(
"AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES", ""
).split(",")
if item.strip()
]
if len(scopes) != 1:
raise OwnerAuthorizationError(
"EXACTLY_ONE_HTTP_INTAKE_SCOPE_REQUIRED", 503
)
scope = scopes[0]
if scope == "writer:provision" or scope == "*" or scope.endswith(":*"):
raise OwnerAuthorizationError("HTTP_INTAKE_SCOPE_REJECTED", 503)
return scope
def _scope_set(auth: Mapping[str, Any]) -> frozenset[str]:
raw = auth.get("scopes")
if raw is None:
raw = auth.get("scope")
if raw is None:
return frozenset()
if isinstance(raw, str):
candidates = raw.replace(",", " ").split()
elif isinstance(raw, (list, tuple, set, frozenset)):
candidates = [str(item) for item in raw]
else:
raise OwnerAuthorizationError("OWNER_SCOPE_EVIDENCE_INVALID", 403)
return frozenset(item.strip() for item in candidates if item.strip())
def _credential_id(auth: Mapping[str, Any]) -> str:
for name in (
"credential_id",
"token_id",
"key_id",
"auth_id",
):
value = str(auth.get(name) or "").strip()
if value:
return value
return ""
def _authenticate_existing_owner(handler) -> OwnerPrincipal | None:
scope = _configured_intake_scope()
auth = handler.need(scope)
if not auth:
return None
if auth.get("role") != "owner":
handler.sendj({"error": "OWNER_ROLE_REQUIRED"}, 403)
return None
identity = str(auth.get("account") or auth.get("identity") or "").strip()
if not identity:
handler.sendj({"error": "OWNER_IDENTITY_REQUIRED"}, 403)
return None
scopes = _scope_set(auth)
if any(item == "*" or item.endswith(":*") for item in scopes):
handler.sendj(
{"error": "WILDCARD_OWNER_CREDENTIAL_REJECTED"}, 403
)
return None
return OwnerPrincipal(
identity=identity,
role="owner",
authenticated_scope=scope,
token_scopes=scopes,
credential_id=_credential_id(auth),
)
def _broker_do_post(self):
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != _ENDPOINT:
return _original_do_post(self)
if not _enabled():
return self.sendj(
{"error": "OWNER_AUTHORIZATION_BROKER_DISABLED"}, 503
)
try:
principal = _authenticate_existing_owner(self)
if principal is None:
return
data = self.body()
result = OwnerAuthorizationBroker().authorize_and_provision(
data, principal
)
return self.sendj(result)
except (OwnerAuthorizationError, ProvisioningError) as exc:
body = {"error": exc.code}
if exc.details:
body["details"] = exc.details
return self.sendj(body, exc.status)
except Exception:
# Never expose registry data, bearer tokens, signer output or traceback.
return self.sendj(
{"error": "OWNER_AUTHORIZATION_BROKER_INTERNAL_ERROR"}, 500
)
api.H.do_POST = _broker_do_post
if __name__ == "__main__":
api.main()
@@ -1,160 +0,0 @@
#!/usr/bin/env python3
"""Loopback-only HTTP API for the task identity overlay.
Endpoints:
GET /healthz
POST /api/governance/owner-authorizations
The API authenticates only the short-lived overlay credential. It does not read
or modify the global bearer-token registry.
"""
from __future__ import annotations
import json
import os
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
from authorized_task_writer_provisioning import ProvisioningError
from owner_authorization_broker import OwnerAuthorizationError
from task_identity_overlay import (
MAX_BODY_BYTES,
IdentityOverlayConfig,
IdentityOverlayError,
TaskIdentityOverlayService,
)
_ENDPOINT = "/api/governance/owner-authorizations"
def _enabled() -> bool:
return os.getenv(
"AIOS_GITOPS_IDENTITY_OVERLAY_ENABLED", ""
).strip().lower() in {"1", "true", "yes", "on"}
def _bearer(header: str) -> str:
prefix = "Bearer "
if not header.startswith(prefix):
raise IdentityOverlayError("OVERLAY_AUTHENTICATION_REQUIRED", 401)
token = header[len(prefix):].strip()
if not token or len(token) > 4096:
raise IdentityOverlayError("OVERLAY_AUTHENTICATION_REQUIRED", 401)
return token
class OverlayHandler(BaseHTTPRequestHandler):
server_version = "aios-task-identity-overlay/1"
sys_version = ""
@property
def overlay(self) -> TaskIdentityOverlayService:
return self.server.overlay # type: ignore[attr-defined]
def log_message(self, format: str, *args: Any) -> None:
# Never log Authorization or request bodies.
return
def _send(self, status: int, value: dict[str, Any]) -> None:
payload = (
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
+ "\n"
).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def _body(self) -> dict[str, Any]:
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
raise IdentityOverlayError("OVERLAY_CONTENT_LENGTH_INVALID") from None
if length <= 0 or length > MAX_BODY_BYTES:
raise IdentityOverlayError("OVERLAY_BODY_SIZE_REJECTED", 413)
raw = self.rfile.read(length)
try:
value = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError):
raise IdentityOverlayError("OVERLAY_BODY_JSON_INVALID") from None
if not isinstance(value, dict):
raise IdentityOverlayError("OVERLAY_BODY_OBJECT_REQUIRED")
return value
def do_GET(self) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != "/healthz":
self._send(404, {"error": "NOT_FOUND"})
return
self._send(
200,
{
"service": "aios-task-identity-overlay",
"status": "active" if _enabled() else "disabled",
"global_token_registry_used": False,
"loopback_only": True,
},
)
def do_POST(self) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if path != _ENDPOINT:
self._send(404, {"error": "NOT_FOUND"})
return
if not _enabled():
self._send(503, {"error": "IDENTITY_OVERLAY_DISABLED"})
return
try:
token = _bearer(self.headers.get("Authorization", ""))
result = self.overlay.provision(self._body(), token)
self._send(200, result)
except (IdentityOverlayError, OwnerAuthorizationError, ProvisioningError) as exc:
body: dict[str, Any] = {"error": exc.code}
if getattr(exc, "details", None):
body["details"] = exc.details
self._send(exc.status, body)
except Exception:
self._send(500, {"error": "IDENTITY_OVERLAY_API_INTERNAL_ERROR"})
class OverlayServer(ThreadingHTTPServer):
allow_reuse_address = True
def __init__(
self,
address: tuple[str, int],
overlay: TaskIdentityOverlayService,
) -> None:
super().__init__(address, OverlayHandler)
self.overlay = overlay
def main() -> None:
host = os.getenv("AIOS_GITOPS_IDENTITY_OVERLAY_HOST", "127.0.0.1")
if host not in {"127.0.0.1", "::1", "localhost"}:
raise SystemExit("identity overlay must listen on loopback")
try:
port = int(os.getenv("AIOS_GITOPS_IDENTITY_OVERLAY_PORT", "19864"))
except ValueError:
raise SystemExit("invalid overlay port") from None
if not 1024 <= port <= 65535:
raise SystemExit("invalid overlay port")
config = IdentityOverlayConfig.from_environment()
server = OverlayServer(
(host, port),
TaskIdentityOverlayService(config),
)
server.serve_forever(poll_interval=0.25)
if __name__ == "__main__":
main()
@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""Root-governed CLI for issuing and revoking task identity overlays.
The CLI never prints the bearer token. It returns only the root-owned secret
reference and non-secret metadata.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from task_identity_overlay import (
IdentityOverlayConfig,
IdentityOverlayError,
TaskIdentityOverlayStore,
)
def _write_result(path: str, value: dict) -> None:
data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n"
if path == "-":
sys.stdout.write(data)
return
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(data, encoding="utf-8")
os.chmod(target, 0o600)
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(
description="Issue/status/revoke a task-scoped identity overlay"
)
sub = value.add_subparsers(dest="command", required=True)
issue = sub.add_parser("issue")
issue.add_argument("--ttl-seconds", type=int, default=900)
issue.add_argument("--authorization-assertion-ref", required=True)
issue.add_argument(
"--operator-identity",
default=os.getenv("SUDO_USER") or os.getenv("USER") or "root",
)
issue.add_argument("--result-json", default="-")
status = sub.add_parser("status")
status.add_argument("--overlay-id", required=True)
status.add_argument("--result-json", default="-")
revoke = sub.add_parser("revoke")
revoke.add_argument("--overlay-id", required=True)
revoke.add_argument("--reason", required=True)
revoke.add_argument("--result-json", default="-")
return value
def main() -> int:
args = parser().parse_args()
try:
store = TaskIdentityOverlayStore(
IdentityOverlayConfig.from_environment()
)
if args.command == "issue":
result = store.issue(
ttl_seconds=args.ttl_seconds,
authorization_assertion_ref=args.authorization_assertion_ref,
operator_identity=args.operator_identity,
)
elif args.command == "status":
result = store.status(args.overlay_id)
else:
result = store.revoke(args.overlay_id, reason=args.reason)
_write_result(args.result_json, result)
return 0
except IdentityOverlayError as exc:
body = {"status": "BLOCKED", "error": exc.code}
if exc.details:
body["details"] = exc.details
_write_result(getattr(args, "result_json", "-"), body)
return 2
except Exception:
_write_result(
getattr(args, "result_json", "-"),
{"status": "FAILED", "error": "IDENTITY_OVERLAY_CLI_INTERNAL_ERROR"},
)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+110
View File
@@ -0,0 +1,110 @@
"""AIOS V2 控制面、执行编排与可信运行来源证明。"""
from .control_plane import (
AgentRecord,
AuditEvent,
ControlPlaneError,
ControlPlaneService,
IdempotencyConflict,
InMemoryControlPlaneStore,
InvalidTransition,
NotFound,
PolicyDenied,
RequestContext,
RevisionConflict,
TaskRecord,
TaskState,
TenantIsolationError,
TenantRecord,
Unauthorized,
)
from .execution_tracking import ExecutionEvidence, ExecutionTracker
from .orchestrator import DispatchRecord, ExecutionOrchestrator, OrchestrationError
from .provenance import (
Ed25519PrivateKeySigner,
Ed25519PublicKeyVerifier,
ExecutionPath,
GitOperation,
PRODUCTION_POLICY_VERSION,
ProvenanceAuthorization,
ProvenanceControlledWriter,
ProvenanceDenied,
ProvenanceError,
ProvenanceExpired,
ProvenanceIssuer,
ProvenanceNotFound,
ProvenanceReplay,
ProvenanceRevoked,
ProvenanceSignatureError,
ProvenanceStatus,
ProvenanceVerifier,
RunProvenance,
SQLiteProvenanceStore,
digest_payload,
)
from .provenance_git import (
GitCommandResult,
GitExecutionContext,
ProvenanceGitError,
ProvenanceGitWriter,
)
from .provenance_operations import (
OperationContext,
OperationReceipt,
ProvenanceOperationGate,
build_provenance_writer_from_env,
context_from_environment,
)
__all__ = [
"AgentRecord",
"AuditEvent",
"ControlPlaneError",
"ControlPlaneService",
"DispatchRecord",
"Ed25519PrivateKeySigner",
"Ed25519PublicKeyVerifier",
"ExecutionEvidence",
"ExecutionOrchestrator",
"ExecutionPath",
"ExecutionTracker",
"GitCommandResult",
"GitExecutionContext",
"GitOperation",
"IdempotencyConflict",
"InMemoryControlPlaneStore",
"InvalidTransition",
"NotFound",
"OperationContext",
"OperationReceipt",
"OrchestrationError",
"PRODUCTION_POLICY_VERSION",
"PolicyDenied",
"ProvenanceAuthorization",
"ProvenanceControlledWriter",
"ProvenanceDenied",
"ProvenanceError",
"ProvenanceExpired",
"ProvenanceGitError",
"ProvenanceGitWriter",
"ProvenanceIssuer",
"ProvenanceNotFound",
"ProvenanceOperationGate",
"ProvenanceReplay",
"ProvenanceRevoked",
"ProvenanceSignatureError",
"ProvenanceStatus",
"ProvenanceVerifier",
"RequestContext",
"RevisionConflict",
"RunProvenance",
"SQLiteProvenanceStore",
"TaskRecord",
"TaskState",
"TenantIsolationError",
"TenantRecord",
"Unauthorized",
"build_provenance_writer_from_env",
"context_from_environment",
"digest_payload",
]
+792
View File
@@ -0,0 +1,792 @@
"""AIOS V2 production-candidate application entrypoint."""
from __future__ import annotations
import dataclasses
import json
import os
import stat
import urllib.parse
from enum import Enum
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any, Mapping
from aios_hermes import HermesAdapter, HermesJobState, HermesResult, SQLiteHermesStore
from aios_openclaw import (
Ed25519CommandSigner,
HTTPRuntimeTransport,
OpenClawAdapter,
)
from aios_openclaw.callback import AuditCallbackSink, EventPublisher
from .console import CONSOLE_HTML
from .control_plane import AuditEvent, RequestContext, Unauthorized
from .dispatch_store import SQLiteDispatchRepository
from .execution_tracking import ExecutionEvidence, ExecutionTracker
from .gateway import GatewayApplication, error_response
from .gitops_status import collect_gitops_status
from .orchestrator import ExecutionOrchestrator
from .persistence import DurableControlPlaneService, SQLiteSnapshotRepository
from .provenance_operations import build_provenance_writer_from_env
def _json_value(value: Any) -> Any:
if dataclasses.is_dataclass(value):
return {
item.name: _json_value(getattr(value, item.name))
for item in dataclasses.fields(value)
}
if isinstance(value, Enum):
return value.value
if isinstance(value, Mapping):
return {str(key): _json_value(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [_json_value(item) for item in value]
return value
def _enabled(value: str | None) -> bool:
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
def _header(headers: Mapping[str, str], name: str) -> str | None:
target = name.lower()
for key, value in headers.items():
if str(key).lower() == target:
normalized = str(value or "").strip()
return normalized or None
return None
def _provenance_context(
headers: Mapping[str, str],
payload: Mapping[str, Any],
) -> tuple[str | None, str | None]:
header_provenance = _header(headers, "X-AIOS-Provenance-ID")
header_run = _header(headers, "X-AIOS-Run-ID")
body_provenance = str(payload.get("provenance_id") or "").strip() or None
body_run = str(payload.get("run_id") or "").strip() or None
if header_provenance and body_provenance and header_provenance != body_provenance:
raise ValueError("provenance_id header/body mismatch")
if header_run and body_run and header_run != body_run:
raise ValueError("run_id header/body mismatch")
return header_provenance or body_provenance, header_run or body_run
def _read_protected_file(path: Path, *, private: bool) -> bytes:
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(str(path), flags)
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("key reference is not a regular file")
denied = 0o077 if private else 0o022
if metadata.st_mode & denied:
raise RuntimeError(
"private key file permissions must be 0600 or stricter"
if private
else "public key file must not be group/world writable"
)
chunks: list[bytes] = []
while True:
chunk = os.read(descriptor, 65536)
if not chunk:
break
chunks.append(chunk)
value = b"".join(chunks)
if not value:
raise RuntimeError("key file is empty")
return value
finally:
os.close(descriptor)
class FileCommandSignerResolver:
"""Resolve tenant/key-id scoped OpenClaw Ed25519 signing material."""
def __init__(self, path: str | Path, *, configured_key_id: str) -> None:
self.path = Path(path).resolve()
self.configured_key_id = str(configured_key_id or "").strip()
if not self.configured_key_id:
raise ValueError("configured_key_id is required")
def __call__(self, tenant_id: str, key_id: str) -> Ed25519CommandSigner:
if not str(tenant_id or "").strip():
raise RuntimeError("tenant_id is required for command signing")
if key_id != self.configured_key_id:
raise RuntimeError("OpenClaw signing key_id mismatch")
return Ed25519CommandSigner.from_bytes(
_read_protected_file(self.path, private=True),
key_id=key_id,
)
class ControlPlaneEventPublisher(EventPublisher):
"""Append Runtime callback events to the tenant audit stream.
Callback audit remains informational. Authoritative task reconciliation is
performed only by the orchestrator after dispatch/result validation.
"""
def __init__(self, service: DurableControlPlaneService) -> None:
self.service = service
def publish(self, event: Mapping[str, Any]) -> None:
tenant_id = str(event.get("tenant_id") or "").strip()
trace_id = str(event.get("trace_id") or "").strip()
command_id = str(event.get("command_id") or "").strip()
event_type = str(event.get("event_type") or "OPENCLAW_EVENT").strip()
if not tenant_id or not trace_id or not command_id:
raise ValueError("runtime audit event is missing tenant identity")
state_value = str(event.get("state") or "unknown").lower()
outcome = "allowed" if state_value == "completed" else state_value
runtime = str(event.get("runtime") or "openclaw").strip().lower()
principal_id = str(
event.get("principal_id") or f"{runtime}-runtime"
).strip()
audit = AuditEvent(
event_id=f"runtime_{command_id}_{len(self.service.store.audit_events) + 1}",
tenant_id=tenant_id,
principal_id=principal_id,
trace_id=trace_id,
action=event_type,
resource_type=f"{runtime}_execution",
resource_id=command_id,
outcome=outcome,
policy_version="runtime-v2",
details=dict(event.get("details") or {}),
)
with self.service.store.lock:
self.service.store.audit_events.append(audit)
self.service.repository.save(self.service._snapshot())
class CandidateApplication:
def __init__(
self,
gateway: GatewayApplication,
orchestrator: ExecutionOrchestrator,
tracker: ExecutionTracker,
hermes: HermesAdapter,
openclaw: OpenClawAdapter | None,
*,
hermes_lifecycle_api_enabled: bool = False,
) -> None:
self.gateway = gateway
self.orchestrator = orchestrator
self.tracker = tracker
self.hermes = hermes
self.openclaw = openclaw
self.hermes_lifecycle_api_enabled = bool(hermes_lifecycle_api_enabled)
self.runtime_events = ControlPlaneEventPublisher(orchestrator.control_plane)
def context(self, headers: Mapping[str, str]) -> RequestContext:
return self.gateway.context_from_headers(headers)
@staticmethod
def _require_runtime_role(context: RequestContext) -> None:
if not ({"runtime", "platform_admin"} & set(context.roles)):
raise Unauthorized("runtime reconciliation role is required")
@staticmethod
def _required_identity(payload: Mapping[str, Any], name: str) -> str:
value = str(payload.get(name) or "").strip()
if not value:
raise ValueError(f"{name} is required")
return value
def _record_hermes_lifecycle(
self,
context: RequestContext,
dispatch: Any,
job: Any,
*,
action: str,
worker_id: str | None,
provenance_id: str,
run_id: str,
) -> None:
executor = worker_id or "aios-control-plane"
evidence = ExecutionEvidence(
execution_id=job.job_id,
tenant_id=job.tenant_id,
task_id=job.task_id,
agent_id=dispatch.agent_id,
executor=executor,
branch="runtime/hermes",
status=job.state.value.lower(),
progress_completed=0,
progress_total=3,
current_step=action,
provenance_id=provenance_id,
run_id=run_id,
)
current_evidence = self.tracker.claim(
evidence,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
if job.state in {
HermesJobState.COMPLETED,
HermesJobState.FAILED,
HermesJobState.CANCELLED,
}:
if current_evidence.completed_at is None:
self.tracker.complete(
tenant_id=job.tenant_id,
execution_id=job.job_id,
status=job.state.value.lower(),
test_status="runtime_terminal",
test_summary={"lifecycle_action": action},
result_summary={
"state": job.state.value,
"worker_id": worker_id,
"dispatch_id": dispatch.dispatch_id,
},
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
else:
self.tracker.heartbeat(
tenant_id=job.tenant_id,
execution_id=job.job_id,
status=job.state.value.lower(),
current_step=action,
progress_completed=(1 if job.state == HermesJobState.RUNNING else 0),
progress_total=3,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
self.runtime_events.publish(
{
"runtime": "hermes",
"tenant_id": job.tenant_id,
"principal_id": context.principal_id,
"trace_id": job.trace_id,
"command_id": job.job_id,
"event_type": f"HERMES_{action.upper()}",
"state": job.state.value,
"details": {
"dispatch_id": dispatch.dispatch_id,
"task_id": job.task_id,
"worker_id": worker_id,
"cell_id": job.cell_id,
"profile_id": job.profile_id,
"revision": job.revision,
"provenance_id": provenance_id,
"run_id": run_id,
},
}
)
def handle_json(
self,
method: str,
raw_path: str,
headers: Mapping[str, str],
body: Any,
) -> tuple[int, Mapping[str, Any]]:
parsed = urllib.parse.urlparse(raw_path)
path = parsed.path.rstrip("/") or "/"
method = method.upper()
if method == "GET" and path == "/api/v2/gitops":
context = self.context(headers)
return HTTPStatus.OK, {
"tenant_id": context.tenant_id,
"gitops": collect_gitops_status().as_dict(),
}
if method == "GET" and path == "/api/v2/runtime":
context = self.context(headers)
runtimes: list[dict[str, Any]] = []
if self.openclaw is None:
runtimes.append(
{
"name": "openclaw",
"status": "disabled",
"details": {"reason": "runtime_not_configured"},
}
)
else:
try:
health = self.openclaw.health()
runtimes.append(
{
"name": "openclaw",
"status": health.status,
"details": _json_value(health),
}
)
except Exception as exc:
runtimes.append(
{
"name": "openclaw",
"status": "unavailable",
"details": {"message": str(exc)},
}
)
hermes_health = self.hermes.health()
runtimes.append(
{
"name": "hermes",
"status": hermes_health.status,
"details": _json_value(hermes_health),
}
)
return HTTPStatus.OK, {
"tenant_id": context.tenant_id,
"runtimes": runtimes,
}
if method == "GET" and path == "/api/v2/executions":
context = self.context(headers)
return HTTPStatus.OK, {
"executions": _json_value(
self.tracker.list(tenant_id=context.tenant_id)
)
}
dispatch_match = __import__("re").fullmatch(
r"/api/v2/tasks/([^/]+)/dispatch", path
)
if method == "POST" and dispatch_match:
context = self.context(headers)
task_id = urllib.parse.unquote(dispatch_match.group(1))
payload = body if isinstance(body, Mapping) else {}
timeout = payload.get("timeout_seconds")
provenance_id, run_id = _provenance_context(headers, payload)
record = self.orchestrator.dispatch(
context,
task_id=task_id,
timeout_seconds=float(timeout) if timeout is not None else None,
provenance_id=provenance_id,
run_id=run_id,
)
return HTTPStatus.ACCEPTED, {"dispatch": _json_value(record)}
lifecycle_match = __import__("re").fullmatch(
r"/api/v2/internal/runtime/hermes/lifecycle/"
r"(claim|start|heartbeat|complete|fail|cancel|status)",
path,
)
if method == "POST" and lifecycle_match:
if not self.hermes_lifecycle_api_enabled:
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "route not found",
}
context = self.context(headers)
self._require_runtime_role(context)
if not isinstance(body, Mapping):
raise ValueError("JSON body must be an object")
payload = body
action = lifecycle_match.group(1)
identity = {
name: self._required_identity(payload, name)
for name in (
"dispatch_id",
"job_id",
"tenant_id",
"task_id",
"trace_id",
"principal_id",
"provenance_id",
"run_id",
"cell_id",
"profile_id",
)
}
header_provenance, header_run = _provenance_context(headers, payload)
if header_provenance != identity["provenance_id"]:
raise ValueError("provenance_id context mismatch")
if header_run != identity["run_id"]:
raise ValueError("run_id context mismatch")
requeued_job_ids = self.hermes.requeue_expired()
dispatch, job = self.orchestrator.hermes_lifecycle_context(
context,
dispatch_id=identity["dispatch_id"],
job_id=identity["job_id"],
tenant_id=identity["tenant_id"],
task_id=identity["task_id"],
trace_id=identity["trace_id"],
principal_id=identity["principal_id"],
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
if job.cell_id != identity["cell_id"]:
raise ValueError("cell_id mismatch")
if job.profile_id != identity["profile_id"]:
raise ValueError("profile_id mismatch")
if action == "status":
if job.job_id in requeued_job_ids:
self._record_hermes_lifecycle(
context,
dispatch,
job,
action="lease_expired_requeue",
worker_id=None,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
task = self.orchestrator.control_plane.get_task(
context, dispatch.task_id
)
response: dict[str, Any] = {
"job": _json_value(job),
"dispatch": _json_value(dispatch),
"task": _json_value(task),
}
if job.state in {
HermesJobState.COMPLETED,
HermesJobState.FAILED,
HermesJobState.CANCELLED,
}:
response["result"] = _json_value(
self.hermes.result(job.job_id, tenant_id=job.tenant_id)
)
return HTTPStatus.OK, response
expected_revision = payload.get("expected_revision")
expected = (
int(expected_revision) if expected_revision is not None else None
)
if action == "cancel":
result = self.hermes.cancel(
job.job_id,
tenant_id=job.tenant_id,
expected_revision=expected,
)
else:
worker_id = self._required_identity(payload, "worker_id")
if action == "claim":
claimed = self.hermes.claim(
tenant_id=job.tenant_id,
cell_id=job.cell_id,
profile_id=job.profile_id,
worker_id=worker_id,
lease_seconds=(
int(payload["lease_seconds"])
if payload.get("lease_seconds") is not None
else None
),
job_id=job.job_id,
)
if claimed is None:
raise ValueError("Hermes job is not claimable")
bound = self.orchestrator.bind_hermes_worker(
context,
claimed,
dispatch_id=dispatch.dispatch_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
worker_id=worker_id,
)
self._record_hermes_lifecycle(
context,
bound,
claimed,
action="claim",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {
"job": _json_value(claimed),
"dispatch": _json_value(bound),
}
if action == "start":
started = self.hermes.start(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
expected_revision=expected,
)
self._record_hermes_lifecycle(
context,
dispatch,
started,
action="start",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {"job": _json_value(started)}
if action == "heartbeat":
heartbeat = self.hermes.heartbeat(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
lease_seconds=(
int(payload["lease_seconds"])
if payload.get("lease_seconds") is not None
else None
),
)
self._record_hermes_lifecycle(
context,
dispatch,
heartbeat,
action="heartbeat",
worker_id=worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {"job": _json_value(heartbeat)}
if action == "complete":
output = payload.get("output")
if not isinstance(output, Mapping):
raise ValueError("output must be an object")
result = self.hermes.complete(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
output=output,
expected_revision=expected,
)
elif action == "fail":
error = payload.get("error")
if not isinstance(error, Mapping):
raise ValueError("error must be an object")
result = self.hermes.fail(
job.job_id,
tenant_id=job.tenant_id,
worker_id=worker_id,
error=error,
expected_revision=expected,
)
else:
raise ValueError("unsupported Hermes lifecycle action")
task = self.orchestrator.reconcile_hermes(
context,
result,
dispatch_id=dispatch.dispatch_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
updated_dispatch = self.orchestrator.dispatch_repository.get(
dispatch.dispatch_id, tenant_id=dispatch.tenant_id
)
terminal_job = self.hermes.get(job.job_id, tenant_id=job.tenant_id)
self._record_hermes_lifecycle(
context,
updated_dispatch,
terminal_job,
action=action,
worker_id=result.worker_id,
provenance_id=identity["provenance_id"],
run_id=identity["run_id"],
)
return HTTPStatus.OK, {
"result": _json_value(result),
"task": _json_value(task),
"dispatch": _json_value(updated_dispatch),
}
if method == "POST" and path == "/api/v2/internal/runtime/hermes/reconcile":
context = self.context(headers)
self._require_runtime_role(context)
if not isinstance(body, Mapping):
raise ValueError("JSON body must be an object")
provenance_id, run_id = _provenance_context(headers, body)
if not provenance_id or not run_id:
raise ValueError("provenance_id and run_id are required")
dispatch_id = str(body.get("dispatch_id") or "").strip()
if not dispatch_id:
raise ValueError("dispatch_id is required")
result = HermesResult(
job_id=str(body["job_id"]),
tenant_id=str(body["tenant_id"]),
task_id=str(body["task_id"]),
trace_id=str(body["trace_id"]),
state=HermesJobState(str(body["state"])),
worker_id=(
str(body["worker_id"])
if body.get("worker_id") is not None
else None
),
output=body.get("output"),
error=body.get("error"),
started_at=body.get("started_at"),
finished_at=body.get("finished_at"),
)
task = self.orchestrator.reconcile_hermes(
context,
result,
dispatch_id=dispatch_id,
provenance_id=provenance_id,
run_id=run_id,
)
return HTTPStatus.OK, {"task": _json_value(task)}
if method == "POST" and path == "/api/v2/hermes/results/reconcile":
return HTTPStatus.GONE, {
"error": "RuntimeReconcileEndpointMoved",
"message": "use /api/v2/internal/runtime/hermes/reconcile",
}
return self.gateway.handle(method, raw_path, headers, body)
class CandidateRequestHandler(BaseHTTPRequestHandler):
server_version = "AIOSV2Candidate/2.0"
max_body_bytes = 1_048_576
@property
def application(self) -> CandidateApplication:
return self.server.application # type: ignore[attr-defined]
def _read_body(self) -> Any:
length = int(self.headers.get("Content-Length", "0") or "0")
if length < 0 or length > self.max_body_bytes:
raise ValueError("request body is too large")
if length == 0:
return {}
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip()
if content_type != "application/json":
raise ValueError("Content-Type must be application/json")
return json.loads(self.rfile.read(length).decode("utf-8"))
def _security_headers(self) -> None:
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Content-Security-Policy",
"default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'",
)
def _send_json(self, status_code: int, payload: Mapping[str, Any]) -> None:
encoded = json.dumps(
_json_value(payload), ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
self.send_response(int(status_code))
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self._security_headers()
self.end_headers()
self.wfile.write(encoded)
def _send_console(self) -> None:
encoded = CONSOLE_HTML.encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self._security_headers()
self.end_headers()
self.wfile.write(encoded)
def _dispatch(self, method: str) -> None:
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
if method == "GET" and path in {"/", "/console"}:
self._send_console()
return
try:
body = self._read_body() if method == "POST" else {}
status_code, payload = self.application.handle_json(
method,
self.path,
{key: value for key, value in self.headers.items()},
body,
)
except Exception as exc:
status_code, payload = error_response(exc)
self._send_json(status_code, payload)
def do_GET(self) -> None: # noqa: N802
self._dispatch("GET")
def do_POST(self) -> None: # noqa: N802
self._dispatch("POST")
class CandidateHTTPServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, address: tuple[str, int], application: CandidateApplication) -> None:
self.application = application
super().__init__(address, CandidateRequestHandler)
def build_application() -> CandidateApplication:
state_dir = Path(os.getenv("AIOS_V2_STATE_DIR", "/var/lib/aios-v2")).resolve()
state_dir.mkdir(parents=True, exist_ok=True)
repository = SQLiteSnapshotRepository(state_dir / "control-plane.sqlite3")
dispatch_repository = SQLiteDispatchRepository(state_dir / "dispatches.sqlite3")
service = DurableControlPlaneService(repository)
require_provenance = _enabled(os.getenv("AIOS_REQUIRE_RUN_PROVENANCE", "0"))
provenance_writer = build_provenance_writer_from_env(
state_dir=state_dir, required=require_provenance
)
tracker = ExecutionTracker(
state_dir / "executions",
provenance_writer=provenance_writer,
require_provenance=require_provenance,
)
hermes = HermesAdapter(store=SQLiteHermesStore(state_dir / "hermes.sqlite3"))
openclaw_url = os.getenv("AIOS_OPENCLAW_URL", "").strip()
signing_file = os.getenv("AIOS_OPENCLAW_SIGNING_MATERIAL_FILE", "").strip()
signing_key_id = os.getenv(
"AIOS_OPENCLAW_SIGNING_KEY_ID", "openclaw-v2r1-candidate-ed25519"
).strip()
openclaw = None
resolver = None
if openclaw_url and signing_file:
resolver = FileCommandSignerResolver(
signing_file,
configured_key_id=signing_key_id,
)
transport = HTTPRuntimeTransport(
openclaw_url,
credential_ref=os.getenv("AIOS_OPENCLAW_CREDENTIAL_REF") or None,
)
openclaw = OpenClawAdapter(
transport,
resolver,
callback_sink=AuditCallbackSink(ControlPlaneEventPublisher(service)),
)
gateway = GatewayApplication(service)
orchestrator = ExecutionOrchestrator(
service,
openclaw=openclaw,
hermes=hermes,
openclaw_signing_material_resolver=resolver,
openclaw_key_id=signing_key_id,
provenance_writer=provenance_writer,
require_provenance=require_provenance,
dispatch_repository=dispatch_repository,
)
return CandidateApplication(
gateway,
orchestrator,
tracker,
hermes,
openclaw,
hermes_lifecycle_api_enabled=_enabled(
os.getenv("AIOS_HERMES_LIFECYCLE_API_ENABLED", "0")
),
)
def main() -> None:
host = os.getenv("AIOS_V2_HOST", "127.0.0.1")
port = int(os.getenv("AIOS_V2_PORT", "18920"))
server = CandidateHTTPServer((host, port), build_application())
print(f"AIOS V2 candidate listening on {host}:{port}")
server.serve_forever()
if __name__ == "__main__":
main()
+69
View File
@@ -0,0 +1,69 @@
"""Dependency-free AIOS V2 web console assets.
Authentication and tenant identity are injected by the trusted reverse proxy.
The browser never selects a tenant and never calls OpenClaw or Hermes directly.
"""
CONSOLE_HTML = r'''<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<title>AIOS V2 Console</title>
<style>
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#152033;background:#f4f7fb}
*{box-sizing:border-box} body{margin:0} header{background:#111827;color:white;padding:18px 24px;display:flex;justify-content:space-between;align-items:center}
header h1{font-size:20px;margin:0} header span{font-size:12px;color:#cbd5e1}
main{padding:24px;max-width:1440px;margin:auto}.notice{background:#fff7ed;border:1px solid #fed7aa;padding:12px 14px;border-radius:10px;margin-bottom:18px;font-size:13px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;margin-bottom:20px}.card,.panel{background:white;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.card{padding:16px}.card b{display:block;font-size:28px;margin-top:8px}.card small{color:#64748b}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:16px}.panel{padding:16px;overflow:auto}.panel h2{font-size:16px;margin:0 0 12px}
table{width:100%;border-collapse:collapse;font-size:12px}th,td{text-align:left;border-bottom:1px solid #edf0f4;padding:8px;vertical-align:top}th{color:#64748b;font-weight:600}.state{font-weight:700}.ok{color:#047857}.bad{color:#b91c1c}.muted{color:#64748b}
button{border:0;border-radius:8px;padding:9px 13px;background:#2563eb;color:white;cursor:pointer}button:disabled{opacity:.5}.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:16px}.error{color:#b91c1c;white-space:pre-wrap}
</style>
</head>
<body>
<header><h1>AIOS V2 Control Plane</h1><span id="updated">Not loaded</span></header>
<main>
<div class="notice">Read-only candidate console. Identity and tenant context must be injected by the trusted reverse proxy. The console never calls OpenClaw or Hermes directly.</div>
<div class="toolbar"><button id="refresh">Refresh</button><span id="error" class="error"></span></div>
<section class="cards">
<div class="card"><small>Agents</small><b id="agentCount">—</b></div>
<div class="card"><small>Tasks</small><b id="taskCount">—</b></div>
<div class="card"><small>Audit events</small><b id="auditCount">—</b></div>
<div class="card"><small>Executions</small><b id="executionCount">—</b></div>
</section>
<section class="grid">
<div class="panel"><h2>Runtime health</h2><div id="runtime"></div></div>
<div class="panel"><h2>Tasks</h2><div id="tasks"></div></div>
<div class="panel"><h2>Agents</h2><div id="agents"></div></div>
<div class="panel"><h2>Execution evidence</h2><div id="executions"></div></div>
<div class="panel"><h2>Audit</h2><div id="audit"></div></div>
</section>
</main>
<script>
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const table=(cols,rows)=>`<table><thead><tr>${cols.map(c=>`<th>${esc(c[0])}</th>`).join('')}</tr></thead><tbody>${rows.map(r=>`<tr>${cols.map(c=>`<td>${esc(c[1](r))}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
async function api(path){const r=await fetch(path,{credentials:'same-origin',headers:{'Accept':'application/json'}});const data=await r.json().catch(()=>({}));if(!r.ok)throw new Error(`${r.status} ${data.message||data.error||path}`);return data}
async function refresh(){
const btn=document.getElementById('refresh');btn.disabled=true;document.getElementById('error').textContent='';
try{
const [agents,tasks,audit,runtime,executions]=await Promise.all([
api('/api/v2/agents'),api('/api/v2/tasks'),api('/api/v2/audit'),api('/api/v2/runtime'),api('/api/v2/executions')
]);
document.getElementById('agentCount').textContent=agents.agents.length;
document.getElementById('taskCount').textContent=tasks.tasks.length;
document.getElementById('auditCount').textContent=audit.events.length;
document.getElementById('executionCount').textContent=executions.executions.length;
document.getElementById('agents').innerHTML=table([['ID',x=>x.agent_id],['Runtime',x=>x.runtime],['Enabled',x=>x.enabled]],agents.agents);
document.getElementById('tasks').innerHTML=table([['Task',x=>x.task_id],['Action',x=>x.action],['State',x=>x.state],['Rev',x=>x.revision]],tasks.tasks);
document.getElementById('audit').innerHTML=table([['Time',x=>x.created_at],['Action',x=>x.action],['Outcome',x=>x.outcome],['Resource',x=>x.resource_id]],audit.events.slice(-100).reverse());
document.getElementById('executions').innerHTML=table([['Executor',x=>x.executor],['Task',x=>x.task_id],['Status',x=>x.status],['Progress',x=>`${x.progress_completed}/${x.progress_total}`],['Commit',x=>x.last_commit||'']],executions.executions);
document.getElementById('runtime').innerHTML=table([['Runtime',x=>x.name],['Status',x=>x.status],['Details',x=>JSON.stringify(x.details)]],runtime.runtimes);
document.getElementById('updated').textContent=`Updated ${new Date().toLocaleString()}`;
}catch(e){document.getElementById('error').textContent=e.message||String(e)}finally{btn.disabled=false}
}
document.getElementById('refresh').addEventListener('click',refresh);refresh();setInterval(refresh,30000);
</script>
</body>
</html>'''
+627
View File
@@ -0,0 +1,627 @@
"""Tenant-isolated AIOS V2 control-plane domain service.
This module deliberately contains no OpenClaw or Hermes implementation details.
It owns authoritative tenant, agent, task-lifecycle, idempotency and audit state.
Runtime adapters consume approved tasks through a separate boundary.
"""
from __future__ import annotations
import copy
import hashlib
import json
import threading
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Iterable, Mapping
def _utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _new_id(prefix: str) -> str:
return f"{prefix}_{uuid.uuid4().hex}"
def _canonical_digest(value: Mapping[str, Any]) -> str:
encoded = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
class ControlPlaneError(RuntimeError):
"""Base error returned by the control-plane domain."""
class Unauthorized(ControlPlaneError):
"""The caller is not authenticated or lacks the required role."""
class TenantIsolationError(ControlPlaneError):
"""A request attempted to cross a tenant boundary."""
class NotFound(ControlPlaneError):
"""A tenant-scoped resource does not exist or is not visible."""
class InvalidTransition(ControlPlaneError):
"""A task state transition violates the lifecycle contract."""
class IdempotencyConflict(ControlPlaneError):
"""An idempotency key was reused for a different command."""
class RevisionConflict(ControlPlaneError):
"""Optimistic task revision does not match current state."""
class PolicyDenied(ControlPlaneError):
"""The policy gate denied a control-plane operation."""
@dataclass(frozen=True)
class RequestContext:
"""Authenticated request identity propagated across all AIOS layers."""
tenant_id: str
principal_id: str
trace_id: str
roles: frozenset[str] = field(default_factory=frozenset)
session_id: str | None = None
policy_version: str = "v1"
def __post_init__(self) -> None:
for name in ("tenant_id", "principal_id", "trace_id"):
if not str(getattr(self, name, "")).strip():
raise ValueError(f"{name} is required")
@classmethod
def create(
cls,
*,
tenant_id: str,
principal_id: str,
roles: Iterable[str] = (),
trace_id: str | None = None,
session_id: str | None = None,
policy_version: str = "v1",
) -> "RequestContext":
return cls(
tenant_id=tenant_id.strip(),
principal_id=principal_id.strip(),
trace_id=trace_id or _new_id("trace"),
roles=frozenset(str(role).strip() for role in roles if str(role).strip()),
session_id=session_id,
policy_version=policy_version,
)
@dataclass(frozen=True)
class TenantRecord:
tenant_id: str
display_name: str
status: str = "active"
metadata: Mapping[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=_utc_now)
updated_at: str = field(default_factory=_utc_now)
@dataclass(frozen=True)
class AgentRecord:
agent_id: str
tenant_id: str
display_name: str
runtime: str
capabilities: tuple[str, ...] = field(default_factory=tuple)
enabled: bool = True
metadata: Mapping[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=_utc_now)
updated_at: str = field(default_factory=_utc_now)
class TaskState(str, Enum):
CREATED = "CREATED"
APPROVED = "APPROVED"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
TERMINAL_TASK_STATES = frozenset(
{TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED}
)
ALLOWED_TASK_TRANSITIONS: Mapping[TaskState, frozenset[TaskState]] = {
TaskState.CREATED: frozenset({TaskState.APPROVED, TaskState.CANCELLED}),
TaskState.APPROVED: frozenset({TaskState.RUNNING, TaskState.CANCELLED}),
TaskState.RUNNING: frozenset(
{TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED}
),
TaskState.COMPLETED: frozenset(),
TaskState.FAILED: frozenset(),
TaskState.CANCELLED: frozenset(),
}
@dataclass(frozen=True)
class TaskRecord:
task_id: str
tenant_id: str
agent_id: str
action: str
payload: Mapping[str, Any]
state: TaskState = TaskState.CREATED
revision: int = 1
idempotency_key: str | None = None
request_digest: str = ""
result: Mapping[str, Any] | None = None
error: Mapping[str, Any] | None = None
created_by: str = ""
approved_by: str | None = None
created_at: str = field(default_factory=_utc_now)
updated_at: str = field(default_factory=_utc_now)
@dataclass(frozen=True)
class AuditEvent:
event_id: str
tenant_id: str
principal_id: str
trace_id: str
action: str
resource_type: str
resource_id: str
outcome: str
policy_version: str
details: Mapping[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=_utc_now)
@dataclass(frozen=True)
class PolicyDecision:
allowed: bool
reason: str
policy_version: str
PolicyEvaluator = Callable[[RequestContext, str, Mapping[str, Any]], PolicyDecision]
class InMemoryControlPlaneStore:
"""Thread-safe reference store used by tests and the first gateway slice.
Production backends must preserve the same tenant-scoped interface and add
database row-level security, migrations, backup and HA evidence.
"""
def __init__(self) -> None:
self.lock = threading.RLock()
self.tenants: dict[str, TenantRecord] = {}
self.agents: dict[str, AgentRecord] = {}
self.tasks: dict[str, TaskRecord] = {}
self.idempotency: dict[tuple[str, str], tuple[str, str]] = {}
self.audit_events: list[AuditEvent] = []
class ControlPlaneService:
"""Authoritative tenant, agent and task lifecycle service."""
def __init__(
self,
store: InMemoryControlPlaneStore | None = None,
*,
policy_evaluator: PolicyEvaluator | None = None,
) -> None:
self.store = store or InMemoryControlPlaneStore()
self.policy_evaluator = policy_evaluator or self._default_policy
@staticmethod
def _default_policy(
context: RequestContext,
action: str,
resource: Mapping[str, Any],
) -> PolicyDecision:
del resource
role_requirements = {
"tenant.register": {"platform_admin"},
"tenant.read": {"tenant_admin", "operator", "auditor", "platform_admin"},
"agent.register": {"tenant_admin", "operator", "platform_admin"},
"agent.read": {"tenant_admin", "operator", "auditor", "platform_admin"},
"task.create": {"tenant_admin", "operator", "platform_admin"},
"task.read": {"tenant_admin", "operator", "auditor", "platform_admin"},
"task.approve": {"tenant_admin", "approver", "platform_admin"},
"task.start": {"runtime", "operator", "platform_admin"},
"task.complete": {"runtime", "operator", "platform_admin"},
"task.fail": {"runtime", "operator", "platform_admin"},
"task.cancel": {"tenant_admin", "operator", "approver", "platform_admin"},
"audit.read": {"tenant_admin", "auditor", "platform_admin"},
}
required = role_requirements.get(action, {"platform_admin"})
allowed = bool(context.roles.intersection(required))
return PolicyDecision(
allowed=allowed,
reason="role_allowed" if allowed else "required_role_missing",
policy_version=context.policy_version,
)
def _require_context(self, context: RequestContext) -> None:
if not context.principal_id or not context.tenant_id or not context.trace_id:
raise Unauthorized("authenticated tenant context is required")
def _authorize(
self,
context: RequestContext,
action: str,
resource: Mapping[str, Any],
) -> PolicyDecision:
self._require_context(context)
decision = self.policy_evaluator(context, action, resource)
if not decision.allowed:
self._audit(
context,
action=action,
resource_type=str(resource.get("resource_type", "policy")),
resource_id=str(resource.get("resource_id", "unknown")),
outcome="denied",
details={"reason": decision.reason},
)
raise PolicyDenied(f"{action} denied: {decision.reason}")
return decision
@staticmethod
def _assert_same_tenant(context: RequestContext, tenant_id: str) -> None:
if context.tenant_id != tenant_id and "platform_admin" not in context.roles:
raise TenantIsolationError("cross-tenant access denied")
def _audit(
self,
context: RequestContext,
*,
action: str,
resource_type: str,
resource_id: str,
outcome: str,
details: Mapping[str, Any] | None = None,
) -> AuditEvent:
event = AuditEvent(
event_id=_new_id("audit"),
tenant_id=context.tenant_id,
principal_id=context.principal_id,
trace_id=context.trace_id,
action=action,
resource_type=resource_type,
resource_id=resource_id,
outcome=outcome,
policy_version=context.policy_version,
details=copy.deepcopy(dict(details or {})),
)
self.store.audit_events.append(event)
return event
def register_tenant(
self,
context: RequestContext,
*,
tenant_id: str,
display_name: str,
metadata: Mapping[str, Any] | None = None,
) -> TenantRecord:
tenant_id = tenant_id.strip()
if not tenant_id or not display_name.strip():
raise ValueError("tenant_id and display_name are required")
self._authorize(
context,
"tenant.register",
{"resource_type": "tenant", "resource_id": tenant_id},
)
self._assert_same_tenant(context, tenant_id)
with self.store.lock:
if tenant_id in self.store.tenants:
raise ControlPlaneError(f"tenant already exists: {tenant_id}")
tenant = TenantRecord(
tenant_id=tenant_id,
display_name=display_name.strip(),
metadata=copy.deepcopy(dict(metadata or {})),
)
self.store.tenants[tenant_id] = tenant
self._audit(
context,
action="tenant.register",
resource_type="tenant",
resource_id=tenant_id,
outcome="allowed",
)
return tenant
def get_tenant(self, context: RequestContext, tenant_id: str | None = None) -> TenantRecord:
tenant_id = tenant_id or context.tenant_id
self._authorize(
context,
"tenant.read",
{"resource_type": "tenant", "resource_id": tenant_id},
)
self._assert_same_tenant(context, tenant_id)
with self.store.lock:
tenant = self.store.tenants.get(tenant_id)
if tenant is None:
raise NotFound("tenant not found")
return tenant
def register_agent(
self,
context: RequestContext,
*,
agent_id: str,
display_name: str,
runtime: str,
capabilities: Iterable[str] = (),
metadata: Mapping[str, Any] | None = None,
) -> AgentRecord:
self._authorize(
context,
"agent.register",
{"resource_type": "agent", "resource_id": agent_id},
)
tenant = self.get_tenant(context)
if tenant.status != "active":
raise ControlPlaneError("tenant is not active")
normalized_capabilities = tuple(
sorted({str(item).strip() for item in capabilities if str(item).strip()})
)
if not agent_id.strip() or not display_name.strip() or not runtime.strip():
raise ValueError("agent_id, display_name and runtime are required")
with self.store.lock:
existing = self.store.agents.get(agent_id)
if existing is not None and existing.tenant_id != context.tenant_id:
raise TenantIsolationError("agent id belongs to another tenant")
if existing is not None:
raise ControlPlaneError(f"agent already exists: {agent_id}")
agent = AgentRecord(
agent_id=agent_id.strip(),
tenant_id=context.tenant_id,
display_name=display_name.strip(),
runtime=runtime.strip(),
capabilities=normalized_capabilities,
metadata=copy.deepcopy(dict(metadata or {})),
)
self.store.agents[agent.agent_id] = agent
self._audit(
context,
action="agent.register",
resource_type="agent",
resource_id=agent.agent_id,
outcome="allowed",
details={"runtime": agent.runtime},
)
return agent
def get_agent(self, context: RequestContext, agent_id: str) -> AgentRecord:
self._authorize(
context,
"agent.read",
{"resource_type": "agent", "resource_id": agent_id},
)
with self.store.lock:
agent = self.store.agents.get(agent_id)
if agent is None:
raise NotFound("agent not found")
self._assert_same_tenant(context, agent.tenant_id)
return agent
def list_agents(self, context: RequestContext) -> list[AgentRecord]:
self._authorize(
context,
"agent.read",
{"resource_type": "agent_collection", "resource_id": context.tenant_id},
)
with self.store.lock:
return [
agent
for agent in self.store.agents.values()
if agent.tenant_id == context.tenant_id
]
def create_task(
self,
context: RequestContext,
*,
agent_id: str,
action: str,
payload: Mapping[str, Any] | None = None,
idempotency_key: str | None = None,
) -> TaskRecord:
self._authorize(
context,
"task.create",
{"resource_type": "task", "resource_id": idempotency_key or "new"},
)
agent = self.get_agent(context, agent_id)
if not agent.enabled:
raise ControlPlaneError("agent is disabled")
if action not in agent.capabilities and "*" not in agent.capabilities:
raise PolicyDenied(f"agent does not advertise capability: {action}")
normalized_payload = copy.deepcopy(dict(payload or {}))
request_digest = _canonical_digest(
{"agent_id": agent_id, "action": action, "payload": normalized_payload}
)
normalized_key = idempotency_key.strip() if idempotency_key else None
with self.store.lock:
if normalized_key:
idempotency_ref = (context.tenant_id, normalized_key)
existing = self.store.idempotency.get(idempotency_ref)
if existing is not None:
existing_task_id, existing_digest = existing
if existing_digest != request_digest:
raise IdempotencyConflict(
"idempotency key was reused with a different request"
)
return self.store.tasks[existing_task_id]
task = TaskRecord(
task_id=_new_id("task"),
tenant_id=context.tenant_id,
agent_id=agent_id,
action=action,
payload=normalized_payload,
idempotency_key=normalized_key,
request_digest=request_digest,
created_by=context.principal_id,
)
self.store.tasks[task.task_id] = task
if normalized_key:
self.store.idempotency[(context.tenant_id, normalized_key)] = (
task.task_id,
request_digest,
)
self._audit(
context,
action="task.create",
resource_type="task",
resource_id=task.task_id,
outcome="allowed",
details={"agent_id": agent_id, "action": action},
)
return task
def get_task(self, context: RequestContext, task_id: str) -> TaskRecord:
self._authorize(
context,
"task.read",
{"resource_type": "task", "resource_id": task_id},
)
with self.store.lock:
task = self.store.tasks.get(task_id)
if task is None:
raise NotFound("task not found")
self._assert_same_tenant(context, task.tenant_id)
return task
def list_tasks(
self,
context: RequestContext,
*,
states: Iterable[TaskState | str] | None = None,
) -> list[TaskRecord]:
self._authorize(
context,
"task.read",
{"resource_type": "task_collection", "resource_id": context.tenant_id},
)
state_filter = (
{TaskState(item) for item in states}
if states is not None
else None
)
with self.store.lock:
return [
task
for task in self.store.tasks.values()
if task.tenant_id == context.tenant_id
and (state_filter is None or task.state in state_filter)
]
def transition_task(
self,
context: RequestContext,
*,
task_id: str,
target_state: TaskState | str,
expected_revision: int | None = None,
result: Mapping[str, Any] | None = None,
error: Mapping[str, Any] | None = None,
) -> TaskRecord:
target = TaskState(target_state)
action_name = {
TaskState.APPROVED: "task.approve",
TaskState.RUNNING: "task.start",
TaskState.COMPLETED: "task.complete",
TaskState.FAILED: "task.fail",
TaskState.CANCELLED: "task.cancel",
}.get(target)
if action_name is None:
raise InvalidTransition(f"cannot transition to {target.value}")
self._authorize(
context,
action_name,
{"resource_type": "task", "resource_id": task_id},
)
with self.store.lock:
current = self.store.tasks.get(task_id)
if current is None:
raise NotFound("task not found")
self._assert_same_tenant(context, current.tenant_id)
if expected_revision is not None and current.revision != expected_revision:
raise RevisionConflict(
f"expected revision {expected_revision}, current {current.revision}"
)
if target not in ALLOWED_TASK_TRANSITIONS[current.state]:
raise InvalidTransition(
f"invalid transition {current.state.value} -> {target.value}"
)
if target == TaskState.COMPLETED and result is None:
raise ValueError("completed task requires result")
if target == TaskState.FAILED and error is None:
raise ValueError("failed task requires error")
updated = TaskRecord(
task_id=current.task_id,
tenant_id=current.tenant_id,
agent_id=current.agent_id,
action=current.action,
payload=current.payload,
state=target,
revision=current.revision + 1,
idempotency_key=current.idempotency_key,
request_digest=current.request_digest,
result=copy.deepcopy(dict(result)) if result is not None else current.result,
error=copy.deepcopy(dict(error)) if error is not None else current.error,
created_by=current.created_by,
approved_by=(
context.principal_id
if target == TaskState.APPROVED
else current.approved_by
),
created_at=current.created_at,
updated_at=_utc_now(),
)
self.store.tasks[task_id] = updated
self._audit(
context,
action=action_name,
resource_type="task",
resource_id=task_id,
outcome="allowed",
details={
"from": current.state.value,
"to": target.value,
"revision": updated.revision,
},
)
return updated
def list_audit_events(
self,
context: RequestContext,
*,
resource_id: str | None = None,
) -> list[AuditEvent]:
self._authorize(
context,
"audit.read",
{"resource_type": "audit", "resource_id": resource_id or context.tenant_id},
)
with self.store.lock:
return [
event
for event in self.store.audit_events
if event.tenant_id == context.tenant_id
and (resource_id is None or event.resource_id == resource_id)
]
@@ -0,0 +1,379 @@
"""AIOS V2 持久化 Dispatch 记录。
Dispatch 是 AIOS Task 与 Runtime command/job 之间的唯一可信绑定。所有结果回写
必须匹配 tenant/task/trace/runtime_execution/provenance/run/principal。
"""
from __future__ import annotations
import json
import sqlite3
import threading
import uuid
from dataclasses import asdict, dataclass, replace
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Protocol
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
class DispatchState(str, Enum):
PREPARED = "PREPARED"
RUNNING = "RUNNING"
QUEUED = "QUEUED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
TERMINAL_DISPATCH_STATES = frozenset(
{DispatchState.COMPLETED, DispatchState.FAILED, DispatchState.CANCELLED}
)
@dataclass(frozen=True)
class DispatchRecord:
dispatch_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
runtime: str
runtime_execution_id: str
trace_id: str
provenance_id: str
run_id: str
authorization_id: str
state: DispatchState = DispatchState.PREPARED
expected_worker_id: str | None = None
result_digest: str | None = None
created_at: str = ""
updated_at: str = ""
completed_at: str | None = None
def __post_init__(self) -> None:
for name in (
"dispatch_id",
"tenant_id",
"task_id",
"agent_id",
"principal_id",
"runtime",
"runtime_execution_id",
"trace_id",
"provenance_id",
"run_id",
"authorization_id",
):
object.__setattr__(self, name, _required(getattr(self, name), name))
object.__setattr__(self, "state", DispatchState(self.state))
now = utc_now()
object.__setattr__(self, "created_at", self.created_at or now)
object.__setattr__(self, "updated_at", self.updated_at or now)
if self.expected_worker_id is not None:
object.__setattr__(
self,
"expected_worker_id",
_required(self.expected_worker_id, "expected_worker_id"),
)
@classmethod
def create(
cls,
*,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
runtime: str,
runtime_execution_id: str,
trace_id: str,
provenance_id: str,
run_id: str,
authorization_id: str,
) -> "DispatchRecord":
return cls(
dispatch_id=f"dispatch_{uuid.uuid4().hex}",
tenant_id=tenant_id,
task_id=task_id,
agent_id=agent_id,
principal_id=principal_id,
runtime=runtime,
runtime_execution_id=runtime_execution_id,
trace_id=trace_id,
provenance_id=provenance_id,
run_id=run_id,
authorization_id=authorization_id,
)
def as_dict(self) -> dict[str, Any]:
value = asdict(self)
value["state"] = self.state.value
return value
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "DispatchRecord":
return cls(**dict(value))
class DispatchConflict(RuntimeError):
pass
class DispatchNotFound(RuntimeError):
pass
class DispatchRepository(Protocol):
def create(self, record: DispatchRecord) -> DispatchRecord: ...
def get(self, dispatch_id: str, *, tenant_id: str) -> DispatchRecord: ...
def get_by_task(self, task_id: str, *, tenant_id: str) -> DispatchRecord | None: ...
def get_by_runtime_execution(
self, runtime_execution_id: str, *, tenant_id: str
) -> DispatchRecord | None: ...
def update(
self,
dispatch_id: str,
*,
tenant_id: str,
state: DispatchState | str | None = None,
expected_worker_id: str | None = None,
result_digest: str | None = None,
) -> DispatchRecord: ...
def list(self, *, tenant_id: str) -> list[DispatchRecord]: ...
class InMemoryDispatchRepository:
def __init__(self) -> None:
self._lock = threading.RLock()
self._records: dict[str, DispatchRecord] = {}
self._by_task: dict[tuple[str, str], str] = {}
self._by_runtime: dict[tuple[str, str], str] = {}
def create(self, record: DispatchRecord) -> DispatchRecord:
with self._lock:
task_ref = (record.tenant_id, record.task_id)
runtime_ref = (record.tenant_id, record.runtime_execution_id)
if task_ref in self._by_task or runtime_ref in self._by_runtime:
existing = self.get_by_task(record.task_id, tenant_id=record.tenant_id)
if existing == record:
return record
raise DispatchConflict("task or runtime execution already has a dispatch")
self._records[record.dispatch_id] = record
self._by_task[task_ref] = record.dispatch_id
self._by_runtime[runtime_ref] = record.dispatch_id
return record
def get(self, dispatch_id: str, *, tenant_id: str) -> DispatchRecord:
with self._lock:
record = self._records.get(dispatch_id)
if record is None or record.tenant_id != tenant_id:
raise DispatchNotFound("dispatch not found")
return record
def get_by_task(self, task_id: str, *, tenant_id: str) -> DispatchRecord | None:
with self._lock:
dispatch_id = self._by_task.get((tenant_id, task_id))
return self._records.get(dispatch_id) if dispatch_id else None
def get_by_runtime_execution(
self, runtime_execution_id: str, *, tenant_id: str
) -> DispatchRecord | None:
with self._lock:
dispatch_id = self._by_runtime.get((tenant_id, runtime_execution_id))
return self._records.get(dispatch_id) if dispatch_id else None
def update(
self,
dispatch_id: str,
*,
tenant_id: str,
state: DispatchState | str | None = None,
expected_worker_id: str | None = None,
result_digest: str | None = None,
) -> DispatchRecord:
with self._lock:
current = self.get(dispatch_id, tenant_id=tenant_id)
target = DispatchState(state) if state is not None else current.state
if current.state in TERMINAL_DISPATCH_STATES and target != current.state:
raise DispatchConflict("terminal dispatch cannot transition")
updated = replace(
current,
state=target,
expected_worker_id=(
expected_worker_id
if expected_worker_id is not None
else current.expected_worker_id
),
result_digest=result_digest or current.result_digest,
updated_at=utc_now(),
completed_at=(utc_now() if target in TERMINAL_DISPATCH_STATES else None),
)
self._records[dispatch_id] = updated
return updated
def list(self, *, tenant_id: str) -> list[DispatchRecord]:
with self._lock:
return sorted(
[item for item in self._records.values() if item.tenant_id == tenant_id],
key=lambda item: (item.created_at, item.dispatch_id),
)
class SQLiteDispatchRepository:
SCHEMA_VERSION = 1
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
with self._connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS dispatch_records (
dispatch_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
task_id TEXT NOT NULL,
runtime_execution_id TEXT NOT NULL,
payload_json TEXT NOT NULL,
state TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(tenant_id, task_id),
UNIQUE(tenant_id, runtime_execution_id)
);
CREATE INDEX IF NOT EXISTS idx_dispatch_tenant_state
ON dispatch_records(tenant_id, state, updated_at);
"""
)
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(str(self.path), timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
return connection
@staticmethod
def _encode(record: DispatchRecord) -> str:
return json.dumps(
record.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
@staticmethod
def _decode(row: sqlite3.Row) -> DispatchRecord:
value = json.loads(row["payload_json"])
if not isinstance(value, Mapping):
raise RuntimeError("invalid dispatch record")
return DispatchRecord.from_dict(value)
def create(self, record: DispatchRecord) -> DispatchRecord:
with self._lock, self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
try:
connection.execute(
"INSERT INTO dispatch_records VALUES (?, ?, ?, ?, ?, ?, ?)",
(
record.dispatch_id,
record.tenant_id,
record.task_id,
record.runtime_execution_id,
self._encode(record),
record.state.value,
record.updated_at,
),
)
connection.commit()
return record
except sqlite3.IntegrityError as exc:
connection.rollback()
existing = self.get_by_task(record.task_id, tenant_id=record.tenant_id)
if existing is not None and existing == record:
return existing
raise DispatchConflict("task or runtime execution already has a dispatch") from exc
def get(self, dispatch_id: str, *, tenant_id: str) -> DispatchRecord:
with self._lock, self._connect() as connection:
row = connection.execute(
"SELECT payload_json FROM dispatch_records WHERE dispatch_id = ? AND tenant_id = ?",
(_required(dispatch_id, "dispatch_id"), _required(tenant_id, "tenant_id")),
).fetchone()
if row is None:
raise DispatchNotFound("dispatch not found")
return self._decode(row)
def get_by_task(self, task_id: str, *, tenant_id: str) -> DispatchRecord | None:
with self._lock, self._connect() as connection:
row = connection.execute(
"SELECT payload_json FROM dispatch_records WHERE task_id = ? AND tenant_id = ?",
(_required(task_id, "task_id"), _required(tenant_id, "tenant_id")),
).fetchone()
return self._decode(row) if row is not None else None
def get_by_runtime_execution(
self, runtime_execution_id: str, *, tenant_id: str
) -> DispatchRecord | None:
with self._lock, self._connect() as connection:
row = connection.execute(
"SELECT payload_json FROM dispatch_records WHERE runtime_execution_id = ? AND tenant_id = ?",
(
_required(runtime_execution_id, "runtime_execution_id"),
_required(tenant_id, "tenant_id"),
),
).fetchone()
return self._decode(row) if row is not None else None
def update(
self,
dispatch_id: str,
*,
tenant_id: str,
state: DispatchState | str | None = None,
expected_worker_id: str | None = None,
result_digest: str | None = None,
) -> DispatchRecord:
current = self.get(dispatch_id, tenant_id=tenant_id)
target = DispatchState(state) if state is not None else current.state
if current.state in TERMINAL_DISPATCH_STATES and target != current.state:
raise DispatchConflict("terminal dispatch cannot transition")
updated = replace(
current,
state=target,
expected_worker_id=(
expected_worker_id if expected_worker_id is not None else current.expected_worker_id
),
result_digest=result_digest or current.result_digest,
updated_at=utc_now(),
completed_at=(utc_now() if target in TERMINAL_DISPATCH_STATES else None),
)
with self._lock, self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"UPDATE dispatch_records SET payload_json = ?, state = ?, updated_at = ? WHERE dispatch_id = ? AND tenant_id = ?",
(
self._encode(updated),
updated.state.value,
updated.updated_at,
updated.dispatch_id,
updated.tenant_id,
),
)
connection.commit()
return updated
def list(self, *, tenant_id: str) -> list[DispatchRecord]:
with self._lock, self._connect() as connection:
rows = connection.execute(
"SELECT payload_json FROM dispatch_records WHERE tenant_id = ? ORDER BY updated_at, dispatch_id",
(_required(tenant_id, "tenant_id"),),
).fetchall()
return [self._decode(row) for row in rows]
@@ -0,0 +1,323 @@
"""AIOS V2 Agent 执行证据。
Evidence 按租户隔离并使用原子 JSON 写入。生产模式下,claim、heartbeat
和 complete 在落盘前必须通过最小权限 Evidence Grant 授权。
"""
from __future__ import annotations
import json
import os
import threading
from dataclasses import asdict, dataclass, field, replace
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, Protocol
from .provenance import ProvenanceDenied, digest_payload
class EvidenceAuthorizationWriter(Protocol):
def authorize_evidence_write(self, **values: Any) -> Any: ...
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def _safe(value: Mapping[str, Any] | None) -> dict[str, Any]:
return json.loads(json.dumps(dict(value or {}), ensure_ascii=False, default=str))
@dataclass(frozen=True)
class ExecutionEvidence:
execution_id: str
tenant_id: str
task_id: str
agent_id: str
executor: str
branch: str
status: str = "claimed"
progress_completed: int = 0
progress_total: int = 1
current_step: str = "claim"
last_commit: str | None = None
test_status: str = "not_run"
test_summary: Mapping[str, Any] = field(default_factory=dict)
result_summary: Mapping[str, Any] = field(default_factory=dict)
claimed_at: str = field(default_factory=utc_now)
heartbeat_at: str = field(default_factory=utc_now)
completed_at: str | None = None
provenance_id: str | None = None
run_id: str | None = None
last_authorization_id: str | None = None
def __post_init__(self) -> None:
for name in (
"execution_id",
"tenant_id",
"task_id",
"agent_id",
"executor",
"branch",
"status",
"current_step",
"claimed_at",
"heartbeat_at",
):
object.__setattr__(self, name, _required(getattr(self, name), name))
if self.progress_completed < 0 or self.progress_total < 1:
raise ValueError("invalid progress values")
if self.progress_completed > self.progress_total:
raise ValueError("progress_completed cannot exceed progress_total")
object.__setattr__(self, "test_summary", _safe(self.test_summary))
object.__setattr__(self, "result_summary", _safe(self.result_summary))
for optional in ("provenance_id", "run_id", "last_authorization_id"):
value = getattr(self, optional)
object.__setattr__(self, optional, str(value).strip() if value else None)
class ExecutionTracker:
"""每个租户、每次执行持久保存一份证据记录。"""
def __init__(
self,
root: str | Path,
*,
provenance_writer: EvidenceAuthorizationWriter | None = None,
require_provenance: bool = False,
) -> None:
self.root = Path(root).resolve()
self.root.mkdir(parents=True, exist_ok=True)
self.provenance_writer = provenance_writer
self.require_provenance = bool(require_provenance)
if self.require_provenance and self.provenance_writer is None:
raise ValueError("production evidence tracking requires a provenance writer")
self._lock = threading.RLock()
@staticmethod
def _component(value: str) -> str:
normalized = _required(value, "path component")
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
raise ValueError("invalid evidence path component")
return normalized
def _path(self, tenant_id: str, execution_id: str) -> Path:
tenant = self._component(tenant_id)
execution = self._component(execution_id)
return self.root / tenant / f"{execution}.json"
def _authorize(
self,
evidence: ExecutionEvidence,
*,
operation: str,
principal_id: str | None,
provenance_id: str | None,
run_id: str | None,
changes: Mapping[str, Any],
) -> str | None:
if not self.require_provenance:
return None
if self.provenance_writer is None:
raise RuntimeError("Run Provenance writer is not configured")
effective_provenance = provenance_id or evidence.provenance_id
effective_run = run_id or evidence.run_id
if not effective_provenance or not effective_run or not principal_id:
raise RuntimeError(
"production evidence write requires provenance_id, run_id and principal_id"
)
request_digest = digest_payload(
{
"operation": operation,
"execution_id": evidence.execution_id,
"tenant_id": evidence.tenant_id,
"task_id": evidence.task_id,
"agent_id": evidence.agent_id,
"executor": evidence.executor,
"branch": evidence.branch,
"changes": dict(changes),
}
)
try:
authorization = self.provenance_writer.authorize_evidence_write(
provenance_id=effective_provenance,
run_id=effective_run,
tenant_id=evidence.tenant_id,
task_id=evidence.task_id,
agent_id=evidence.agent_id,
principal_id=principal_id,
request_digest=request_digest,
)
except ProvenanceDenied as exc:
raise RuntimeError(f"Run Provenance denied evidence write: {exc}") from exc
return authorization.authorization_id
def claim(
self,
evidence: ExecutionEvidence,
*,
principal_id: str | None = None,
provenance_id: str | None = None,
run_id: str | None = None,
) -> ExecutionEvidence:
path = self._path(evidence.tenant_id, evidence.execution_id)
with self._lock:
if path.exists():
current = self._read(path)
if current.task_id != evidence.task_id or current.agent_id != evidence.agent_id:
raise RuntimeError("execution_id already belongs to another task")
if self.require_provenance and (
current.provenance_id != (provenance_id or evidence.provenance_id)
or current.run_id != (run_id or evidence.run_id)
):
raise RuntimeError(
"execution_id already belongs to another provenance context"
)
return current
effective_provenance = provenance_id or evidence.provenance_id
effective_run = run_id or evidence.run_id
authorization_id = self._authorize(
evidence,
operation="claim",
principal_id=principal_id,
provenance_id=effective_provenance,
run_id=effective_run,
changes={"status": evidence.status, "current_step": evidence.current_step},
)
persisted = replace(
evidence,
provenance_id=effective_provenance,
run_id=effective_run,
last_authorization_id=authorization_id,
)
self._write(path, persisted)
return persisted
def heartbeat(
self,
*,
tenant_id: str,
execution_id: str,
status: str | None = None,
current_step: str | None = None,
progress_completed: int | None = None,
progress_total: int | None = None,
last_commit: str | None = None,
principal_id: str | None = None,
provenance_id: str | None = None,
run_id: str | None = None,
) -> ExecutionEvidence:
path = self._path(tenant_id, execution_id)
with self._lock:
current = self._read(path)
changes = {
"status": status or current.status,
"current_step": current_step or current.current_step,
"progress_completed": (
progress_completed
if progress_completed is not None
else current.progress_completed
),
"progress_total": (
progress_total if progress_total is not None else current.progress_total
),
"last_commit": last_commit or current.last_commit,
}
authorization_id = self._authorize(
current,
operation="heartbeat",
principal_id=principal_id,
provenance_id=provenance_id,
run_id=run_id,
changes=changes,
)
updated = replace(
current,
**changes,
last_authorization_id=authorization_id or current.last_authorization_id,
heartbeat_at=utc_now(),
)
self._write(path, updated)
return updated
def complete(
self,
*,
tenant_id: str,
execution_id: str,
status: str,
test_status: str,
test_summary: Mapping[str, Any] | None = None,
result_summary: Mapping[str, Any] | None = None,
last_commit: str | None = None,
principal_id: str | None = None,
provenance_id: str | None = None,
run_id: str | None = None,
) -> ExecutionEvidence:
path = self._path(tenant_id, execution_id)
with self._lock:
current = self._read(path)
changes = {
"status": _required(status, "status"),
"current_step": "complete",
"progress_completed": current.progress_total,
"last_commit": last_commit or current.last_commit,
"test_status": _required(test_status, "test_status"),
"test_summary": _safe(test_summary),
"result_summary": _safe(result_summary),
}
authorization_id = self._authorize(
current,
operation="complete",
principal_id=principal_id,
provenance_id=provenance_id,
run_id=run_id,
changes=changes,
)
updated = replace(
current,
**changes,
last_authorization_id=authorization_id or current.last_authorization_id,
heartbeat_at=utc_now(),
completed_at=utc_now(),
)
self._write(path, updated)
return updated
def get(self, *, tenant_id: str, execution_id: str) -> ExecutionEvidence:
return self._read(self._path(tenant_id, execution_id))
def list(self, *, tenant_id: str) -> list[ExecutionEvidence]:
tenant_dir = self.root / self._component(tenant_id)
if not tenant_dir.exists():
return []
records = [self._read(path) for path in tenant_dir.glob("*.json")]
return sorted(records, key=lambda item: (item.heartbeat_at, item.execution_id), reverse=True)
@staticmethod
def _read(path: Path) -> ExecutionEvidence:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise KeyError("execution evidence not found") from exc
if not isinstance(value, Mapping):
raise RuntimeError("invalid execution evidence file")
return ExecutionEvidence(**dict(value))
@staticmethod
def _write(path: Path, evidence: ExecutionEvidence) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(
json.dumps(asdict(evidence), ensure_ascii=False, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
os.chmod(temporary, 0o640)
os.replace(temporary, path)
+353
View File
@@ -0,0 +1,353 @@
"""HTTP/JSON gateway for the AIOS V2 control plane.
The public gateway owns user-facing control operations only. Runtime lifecycle
transitions are deliberately absent: external callers cannot directly move a
task to RUNNING, COMPLETED or FAILED. Those transitions are available only
through the provenance-protected orchestrator and internal reconciliation API.
"""
from __future__ import annotations
import dataclasses
import json
import os
import re
import urllib.parse
from enum import Enum
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, Mapping
from aios_hermes import HermesConflict, HermesNotFound
from .control_plane import (
ControlPlaneError,
ControlPlaneService,
IdempotencyConflict,
InvalidTransition,
NotFound,
PolicyDenied,
RequestContext,
RevisionConflict,
TaskState,
TenantIsolationError,
Unauthorized,
)
from .dispatch_store import DispatchConflict, DispatchNotFound
from .orchestrator import OrchestrationError
from .provenance import ProvenanceDenied, ProvenanceReplay
TENANT_HEADER = "X-AIOS-Tenant-ID"
PRINCIPAL_HEADER = "X-AIOS-Principal-ID"
ROLES_HEADER = "X-AIOS-Roles"
TRACE_HEADER = "X-AIOS-Trace-ID"
SESSION_HEADER = "X-AIOS-Session-ID"
POLICY_HEADER = "X-AIOS-Policy-Version"
AUTHENTICATED_HEADER = "X-AIOS-Authenticated"
_RESOURCE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,191}$")
def _json_value(value: Any) -> Any:
if dataclasses.is_dataclass(value):
return {
field.name: _json_value(getattr(value, field.name))
for field in dataclasses.fields(value)
}
if isinstance(value, Enum):
return value.value
if isinstance(value, Mapping):
return {str(key): _json_value(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [_json_value(item) for item in value]
return value
def _require_mapping(value: Any, *, label: str = "JSON body") -> Mapping[str, Any]:
if not isinstance(value, Mapping):
raise ValueError(f"{label} must be an object")
return value
def _require_resource_id(value: Any, *, label: str) -> str:
normalized = str(value or "").strip()
if not _RESOURCE_ID_RE.fullmatch(normalized):
raise ValueError(f"invalid {label}")
return normalized
class GatewayApplication:
"""Pure user-facing request router that can be tested without a socket."""
def __init__(self, service: ControlPlaneService | None = None) -> None:
self.service = service or ControlPlaneService()
@staticmethod
def context_from_headers(headers: Mapping[str, str]) -> RequestContext:
normalized_headers = {
str(key).strip().lower(): value for key, value in headers.items()
}
def header(name: str, default: str = "") -> str:
return str(normalized_headers.get(name.lower(), default))
authenticated = header(AUTHENTICATED_HEADER).strip().lower()
if authenticated not in {"1", "true", "yes"}:
raise Unauthorized("trusted authentication assertion is required")
roles = [
role.strip()
for role in header(ROLES_HEADER).split(",")
if role.strip()
]
return RequestContext.create(
tenant_id=header(TENANT_HEADER),
principal_id=header(PRINCIPAL_HEADER),
trace_id=header(TRACE_HEADER).strip() or None,
session_id=header(SESSION_HEADER).strip() or None,
policy_version=header(POLICY_HEADER, "v1").strip() or "v1",
roles=roles,
)
def handle(
self,
method: str,
raw_path: str,
headers: Mapping[str, str],
body: Any = None,
) -> tuple[int, Mapping[str, Any]]:
parsed = urllib.parse.urlparse(raw_path)
path = parsed.path.rstrip("/") or "/"
query = urllib.parse.parse_qs(parsed.query, keep_blank_values=False)
method = method.upper()
if method == "GET" and path in {"/healthz", "/api/v2/health"}:
return HTTPStatus.OK, {
"service": "aios-v2-control-plane",
"status": "ok",
"store": type(self.service.store).__name__,
}
context = self.context_from_headers(headers)
payload = _require_mapping(body or {})
if method == "POST" and path == "/api/v2/tenants":
tenant = self.service.register_tenant(
context,
tenant_id=_require_resource_id(payload.get("tenant_id"), label="tenant_id"),
display_name=str(payload.get("display_name", "")),
metadata=_require_mapping(payload.get("metadata", {}), label="metadata"),
)
return HTTPStatus.CREATED, {"tenant": _json_value(tenant)}
if method == "GET" and path == "/api/v2/tenants/current":
return HTTPStatus.OK, {"tenant": _json_value(self.service.get_tenant(context))}
if method == "POST" and path == "/api/v2/agents":
agent = self.service.register_agent(
context,
agent_id=_require_resource_id(payload.get("agent_id"), label="agent_id"),
display_name=str(payload.get("display_name", "")),
runtime=str(payload.get("runtime", "")),
capabilities=payload.get("capabilities", ()),
metadata=_require_mapping(payload.get("metadata", {}), label="metadata"),
)
return HTTPStatus.CREATED, {"agent": _json_value(agent)}
if method == "GET" and path == "/api/v2/agents":
return HTTPStatus.OK, {"agents": _json_value(self.service.list_agents(context))}
agent_match = re.fullmatch(r"/api/v2/agents/([^/]+)", path)
if method == "GET" and agent_match:
agent_id = _require_resource_id(
urllib.parse.unquote(agent_match.group(1)), label="agent_id"
)
return HTTPStatus.OK, {
"agent": _json_value(self.service.get_agent(context, agent_id))
}
if method == "POST" and path == "/api/v2/tasks":
task = self.service.create_task(
context,
agent_id=_require_resource_id(payload.get("agent_id"), label="agent_id"),
action=str(payload.get("action", "")).strip(),
payload=_require_mapping(payload.get("payload", {}), label="payload"),
idempotency_key=(
str(payload["idempotency_key"]).strip()
if payload.get("idempotency_key") is not None
else None
),
)
return HTTPStatus.CREATED, {"task": _json_value(task)}
if method == "GET" and path == "/api/v2/tasks":
tasks = self.service.list_tasks(context, states=query.get("state"))
return HTTPStatus.OK, {"tasks": _json_value(tasks)}
task_match = re.fullmatch(r"/api/v2/tasks/([^/]+)", path)
if method == "GET" and task_match:
task_id = _require_resource_id(
urllib.parse.unquote(task_match.group(1)), label="task_id"
)
return HTTPStatus.OK, {
"task": _json_value(self.service.get_task(context, task_id))
}
bypass_match = re.fullmatch(
r"/api/v2/tasks/([^/]+)/(start|complete|fail)", path
)
if method == "POST" and bypass_match:
return HTTPStatus.FORBIDDEN, {
"error": "RuntimeLifecycleBypassDenied",
"message": (
"RUNNING, COMPLETED and FAILED transitions are internal; "
"use the provenance-protected dispatch/reconcile flow"
),
}
transition_match = re.fullmatch(
r"/api/v2/tasks/([^/]+)/(approve|cancel)", path
)
if method == "POST" and transition_match:
task_id = _require_resource_id(
urllib.parse.unquote(transition_match.group(1)), label="task_id"
)
target = (
TaskState.APPROVED
if transition_match.group(2) == "approve"
else TaskState.CANCELLED
)
expected_revision = payload.get("expected_revision")
task = self.service.transition_task(
context,
task_id=task_id,
target_state=target,
expected_revision=(
int(expected_revision) if expected_revision is not None else None
),
)
return HTTPStatus.OK, {"task": _json_value(task)}
if method == "GET" and path == "/api/v2/audit":
resource_id = query.get("resource_id", [None])[0]
events = self.service.list_audit_events(
context,
resource_id=(
_require_resource_id(resource_id, label="resource_id")
if resource_id
else None
),
)
return HTTPStatus.OK, {"events": _json_value(events)}
return HTTPStatus.NOT_FOUND, {
"error": "not_found",
"message": "route not found",
}
_ERROR_STATUS: tuple[tuple[type[BaseException], HTTPStatus], ...] = (
(Unauthorized, HTTPStatus.UNAUTHORIZED),
(PolicyDenied, HTTPStatus.FORBIDDEN),
(ProvenanceDenied, HTTPStatus.FORBIDDEN),
(TenantIsolationError, HTTPStatus.NOT_FOUND),
(NotFound, HTTPStatus.NOT_FOUND),
(DispatchNotFound, HTTPStatus.NOT_FOUND),
(HermesNotFound, HTTPStatus.NOT_FOUND),
(ProvenanceReplay, HTTPStatus.CONFLICT),
(IdempotencyConflict, HTTPStatus.CONFLICT),
(RevisionConflict, HTTPStatus.CONFLICT),
(InvalidTransition, HTTPStatus.CONFLICT),
(DispatchConflict, HTTPStatus.CONFLICT),
(HermesConflict, HTTPStatus.CONFLICT),
(OrchestrationError, HTTPStatus.CONFLICT),
(ValueError, HTTPStatus.BAD_REQUEST),
(ControlPlaneError, HTTPStatus.BAD_REQUEST),
)
def error_response(exc: BaseException) -> tuple[int, Mapping[str, Any]]:
for error_type, status in _ERROR_STATUS:
if isinstance(exc, error_type):
return status, {"error": error_type.__name__, "message": str(exc)}
return HTTPStatus.INTERNAL_SERVER_ERROR, {
"error": "InternalServerError",
"message": "internal server error",
}
class GatewayRequestHandler(BaseHTTPRequestHandler):
server_version = "AIOSV2Gateway/1.0"
max_body_bytes = 1_048_576
@property
def application(self) -> GatewayApplication:
return self.server.application # type: ignore[attr-defined]
def _read_body(self) -> Any:
length = int(self.headers.get("Content-Length", "0") or "0")
if length < 0 or length > self.max_body_bytes:
raise ValueError("request body is too large")
if length == 0:
return {}
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip()
if content_type != "application/json":
raise ValueError("Content-Type must be application/json")
return json.loads(self.rfile.read(length).decode("utf-8"))
def _send_json(self, status: int, payload: Mapping[str, Any]) -> None:
encoded = json.dumps(
_json_value(payload), ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
self.send_response(int(status))
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(encoded)
def _dispatch(self, method: str) -> None:
try:
body = self._read_body() if method in {"POST", "PUT", "PATCH"} else {}
status, payload = self.application.handle(
method,
self.path,
{key: value for key, value in self.headers.items()},
body,
)
except Exception as exc:
status, payload = error_response(exc)
self._send_json(status, payload)
def do_GET(self) -> None: # noqa: N802
self._dispatch("GET")
def do_POST(self) -> None: # noqa: N802
self._dispatch("POST")
def log_message(self, format: str, *args: Any) -> None:
super().log_message(format, *args)
class GatewayHTTPServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(
self,
server_address: tuple[str, int],
application: GatewayApplication,
) -> None:
self.application = application
super().__init__(server_address, GatewayRequestHandler)
def main() -> None:
host = os.getenv("AIOS_V2_GATEWAY_HOST", "127.0.0.1")
port = int(os.getenv("AIOS_V2_GATEWAY_PORT", "18920"))
server = GatewayHTTPServer((host, port), GatewayApplication())
print(f"AIOS V2 control-plane gateway listening on {host}:{port}")
server.serve_forever()
if __name__ == "__main__":
main()
+123
View File
@@ -0,0 +1,123 @@
"""Sanitized GitOps runtime status for the AIOS Web control plane."""
from __future__ import annotations
import os
import subprocess
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Mapping
@dataclass(frozen=True)
class GitOpsStatus:
available: bool
service_status: str
workspace_mode: str
workspace_branch: str | None
workspace_ref: str | None
workspace_dirty: bool | None
candidate_branch: str
release_channel: str
release_version: str
sync_status: str
details: Mapping[str, Any]
def as_dict(self) -> dict[str, Any]:
return asdict(self)
def _run_git(worktree: Path, *args: str) -> tuple[int, str, str]:
process = subprocess.run(
["git", "-C", str(worktree), *args],
text=True,
capture_output=True,
timeout=5,
)
return process.returncode, process.stdout.strip(), process.stderr.strip()
def collect_gitops_status() -> GitOpsStatus:
"""Return only non-sensitive Git/workspace metadata.
Remote URLs, environment values, credentials, filesystem contents and
command stderr are deliberately excluded from the Web response.
"""
candidate_branch = os.getenv(
"AIOS_CANDIDATE_BRANCH",
"integration/aios-v2r1-cto-production-candidate-20260720",
)
release_channel = os.getenv("AIOS_RELEASE_CHANNEL", "candidate")
release_version = os.getenv("AIOS_RELEASE_VERSION", "V2R1C0-candidate")
sync_status = os.getenv("AIOS_GITOPS_SYNC_STATUS", "unknown")
worktree = Path(
os.getenv(
"AIOS_GITOPS_CONTROLLER_WORKTREE",
"/data/aios-gitops/worktrees/controller",
)
)
if not worktree.is_dir():
return GitOpsStatus(
available=False,
service_status="unknown",
workspace_mode="unavailable",
workspace_branch=None,
workspace_ref=None,
workspace_dirty=None,
candidate_branch=candidate_branch,
release_channel=release_channel,
release_version=release_version,
sync_status=sync_status,
details={"reason": "controller_worktree_unavailable"},
)
inside_code, inside, _ = _run_git(worktree, "rev-parse", "--is-inside-work-tree")
if inside_code != 0 or inside != "true":
return GitOpsStatus(
available=False,
service_status="unknown",
workspace_mode="invalid",
workspace_branch=None,
workspace_ref=None,
workspace_dirty=None,
candidate_branch=candidate_branch,
release_channel=release_channel,
release_version=release_version,
sync_status=sync_status,
details={"reason": "controller_worktree_invalid"},
)
branch_code, branch, _ = _run_git(worktree, "branch", "--show-current")
ref_code, ref_value, _ = _run_git(worktree, "rev-parse", "HEAD")
dirty_code, dirty_value, _ = _run_git(worktree, "status", "--porcelain=v1")
service_status = "unknown"
try:
process = subprocess.run(
["systemctl", "is-active", "aios-gitops-model-api"],
text=True,
capture_output=True,
timeout=3,
)
service_status = process.stdout.strip() or "inactive"
except (FileNotFoundError, subprocess.TimeoutExpired):
service_status = "unavailable"
branch_value = branch if branch_code == 0 and branch else None
return GitOpsStatus(
available=True,
service_status=service_status,
workspace_mode="branch" if branch_value else "detached",
workspace_branch=branch_value,
workspace_ref=ref_value if ref_code == 0 else None,
workspace_dirty=bool(dirty_value) if dirty_code == 0 else None,
candidate_branch=candidate_branch,
release_channel=release_channel,
release_version=release_version,
sync_status=sync_status,
details={
"controller_clean": dirty_code == 0 and not dirty_value,
"controller_detached": branch_code == 0 and not branch,
},
)
+599
View File
@@ -0,0 +1,599 @@
"""AIOS V2 execution orchestrator.
The orchestrator is the only component allowed to translate an approved AIOS
Task into an OpenClaw command or Hermes job. Every dispatch is persisted before
Runtime side effects. Runtime results must bind to the exact dispatch,
command/job, tenant, task, trace, principal and provenance context.
"""
from __future__ import annotations
from typing import Any, Mapping, Protocol
from aios_hermes import HermesAdapter, HermesJob, HermesJobState, HermesResult
from aios_openclaw import (
CommandState,
OpenClawAdapter,
OpenClawCommand,
OpenClawResult,
sign_command,
)
from .control_plane import (
AgentRecord,
ControlPlaneService,
RequestContext,
TaskRecord,
TaskState,
)
from .dispatch_store import (
DispatchConflict,
DispatchRecord,
DispatchRepository,
DispatchState,
InMemoryDispatchRepository,
TERMINAL_DISPATCH_STATES,
)
from .provenance import ProvenanceDenied, digest_payload
class RuntimeAuthorizationWriter(Protocol):
def authorize_runtime_execute(self, **values: Any) -> Any: ...
class OrchestrationError(RuntimeError):
"""AIOS failed to dispatch or reconcile a runtime execution."""
class ExecutionOrchestrator:
"""Dispatch approved AIOS tasks and strictly reconcile Runtime results."""
def __init__(
self,
control_plane: ControlPlaneService,
*,
openclaw: OpenClawAdapter | None = None,
hermes: HermesAdapter | None = None,
openclaw_signing_material_resolver=None,
openclaw_key_id: str = "default",
default_cell_id: str = "default",
default_profile_id: str = "default",
provenance_writer: RuntimeAuthorizationWriter | None = None,
require_provenance: bool = False,
dispatch_repository: DispatchRepository | None = None,
) -> None:
self.control_plane = control_plane
self.openclaw = openclaw
self.hermes = hermes
self.openclaw_signing_material_resolver = openclaw_signing_material_resolver
self.openclaw_key_id = str(openclaw_key_id or "").strip() or "default"
self.default_cell_id = default_cell_id
self.default_profile_id = default_profile_id
self.provenance_writer = provenance_writer
self.require_provenance = bool(require_provenance)
self.dispatch_repository = dispatch_repository or InMemoryDispatchRepository()
if self.require_provenance and self.provenance_writer is None:
raise ValueError("production orchestration requires a provenance writer")
@staticmethod
def _record_provenance(value: str | None, *, required: bool, label: str) -> str:
normalized = str(value or "").strip()
if required and not normalized:
raise OrchestrationError(f"production dispatch requires {label}")
return normalized or f"{label}-not-required"
def _authorize_dispatch(
self,
context: RequestContext,
*,
task: TaskRecord,
agent: AgentRecord,
provenance_id: str | None,
run_id: str | None,
timeout_seconds: float | None,
) -> str:
if not self.require_provenance:
return "authorization-not-required"
if self.provenance_writer is None:
raise OrchestrationError("Run Provenance writer is not configured")
if not provenance_id or not run_id:
raise OrchestrationError(
"production dispatch requires provenance_id and run_id"
)
request_digest = digest_payload(
{
"operation": "dispatch",
"tenant_id": task.tenant_id,
"task_id": task.task_id,
"agent_id": agent.agent_id,
"principal_id": context.principal_id,
"runtime": agent.runtime.strip().lower(),
"action": task.action,
"payload": task.payload,
"trace_id": context.trace_id,
"timeout_seconds": timeout_seconds,
"task_revision": task.revision,
}
)
try:
authorization = self.provenance_writer.authorize_runtime_execute(
provenance_id=provenance_id,
run_id=run_id,
tenant_id=task.tenant_id,
task_id=task.task_id,
agent_id=agent.agent_id,
principal_id=context.principal_id,
request_digest=request_digest,
)
return authorization.authorization_id
except ProvenanceDenied as exc:
raise OrchestrationError(f"Run Provenance denied dispatch: {exc}") from exc
def dispatch(
self,
context: RequestContext,
*,
task_id: str,
timeout_seconds: float | None = None,
provenance_id: str | None = None,
run_id: str | None = None,
) -> DispatchRecord:
task = self.control_plane.get_task(context, task_id)
agent = self.control_plane.get_agent(context, task.agent_id)
persisted_provenance = self._record_provenance(
provenance_id, required=self.require_provenance, label="provenance_id"
)
persisted_run = self._record_provenance(
run_id, required=self.require_provenance, label="run_id"
)
existing = self.dispatch_repository.get_by_task(
task.task_id, tenant_id=task.tenant_id
)
if existing is not None:
if (
existing.provenance_id != persisted_provenance
or existing.run_id != persisted_run
or existing.principal_id != context.principal_id
):
raise OrchestrationError(
"existing dispatch belongs to another provenance or principal context"
)
return existing
if task.state != TaskState.APPROVED:
raise OrchestrationError("only APPROVED tasks may be dispatched")
authorization_id = self._authorize_dispatch(
context,
task=task,
agent=agent,
provenance_id=provenance_id,
run_id=run_id,
timeout_seconds=timeout_seconds,
)
runtime = agent.runtime.strip().lower()
prepared: DispatchRecord | None = None
try:
if runtime == "openclaw":
command = self._create_openclaw_command(context, task)
prepared = self.dispatch_repository.create(
DispatchRecord.create(
tenant_id=task.tenant_id,
task_id=task.task_id,
agent_id=agent.agent_id,
principal_id=context.principal_id,
runtime="openclaw",
runtime_execution_id=command.command_id,
trace_id=context.trace_id,
provenance_id=persisted_provenance,
run_id=persisted_run,
authorization_id=authorization_id,
)
)
self.control_plane.transition_task(
context,
task_id=task.task_id,
target_state=TaskState.RUNNING,
expected_revision=task.revision,
)
self.dispatch_repository.update(
prepared.dispatch_id,
tenant_id=prepared.tenant_id,
state=DispatchState.RUNNING,
)
if self.openclaw is None:
raise OrchestrationError("OpenClaw runtime is not configured")
result = self.openclaw.execute(
command, timeout_seconds=timeout_seconds
)
self.reconcile_openclaw(
context,
result,
dispatch_id=prepared.dispatch_id,
provenance_id=persisted_provenance,
run_id=persisted_run,
)
return self.dispatch_repository.get(
prepared.dispatch_id, tenant_id=prepared.tenant_id
)
if runtime == "hermes":
if self.hermes is None:
raise OrchestrationError("Hermes runtime is not configured")
job = self._create_hermes_job(context, agent, task)
prepared = self.dispatch_repository.create(
DispatchRecord.create(
tenant_id=task.tenant_id,
task_id=task.task_id,
agent_id=agent.agent_id,
principal_id=context.principal_id,
runtime="hermes",
runtime_execution_id=job.job_id,
trace_id=context.trace_id,
provenance_id=persisted_provenance,
run_id=persisted_run,
authorization_id=authorization_id,
)
)
self.control_plane.transition_task(
context,
task_id=task.task_id,
target_state=TaskState.RUNNING,
expected_revision=task.revision,
)
queued = self.hermes.enqueue(job)
return self.dispatch_repository.update(
prepared.dispatch_id,
tenant_id=prepared.tenant_id,
state=DispatchState.QUEUED,
result_digest=digest_payload(
{
"queued_job_id": queued.job_id,
"state": queued.state.value,
"revision": queued.revision,
}
),
)
raise OrchestrationError(f"unsupported runtime: {agent.runtime}")
except Exception as exc:
if prepared is not None:
try:
self.dispatch_repository.update(
prepared.dispatch_id,
tenant_id=prepared.tenant_id,
state=DispatchState.FAILED,
result_digest=digest_payload(
{
"code": "dispatch_failed",
"error_type": type(exc).__name__,
"message": str(exc),
}
),
)
except Exception:
pass
current = self.control_plane.get_task(context, task.task_id)
if current.state == TaskState.RUNNING:
self.control_plane.transition_task(
context,
task_id=task.task_id,
target_state=TaskState.FAILED,
expected_revision=current.revision,
error={"code": "dispatch_failed", "message": str(exc)},
)
raise
def _create_openclaw_command(
self,
context: RequestContext,
task: TaskRecord,
) -> OpenClawCommand:
if self.openclaw_signing_material_resolver is None:
raise OrchestrationError("OpenClaw signing material is not configured")
key_material = self.openclaw_signing_material_resolver(
task.tenant_id,
self.openclaw_key_id,
)
command = OpenClawCommand.create(
tenant_id=task.tenant_id,
task_id=task.task_id,
trace_id=context.trace_id,
action=task.action,
payload=task.payload,
idempotency_key=task.idempotency_key or task.task_id,
key_id=self.openclaw_key_id,
)
return sign_command(command, key_material)
def _create_hermes_job(
self,
context: RequestContext,
agent: AgentRecord,
task: TaskRecord,
) -> HermesJob:
metadata = dict(agent.metadata)
return HermesJob.create(
tenant_id=task.tenant_id,
cell_id=str(metadata.get("cell_id") or self.default_cell_id),
profile_id=str(metadata.get("profile_id") or self.default_profile_id),
task_id=task.task_id,
trace_id=context.trace_id,
action=task.action,
payload=task.payload,
idempotency_key=task.idempotency_key or task.task_id,
priority=int(metadata.get("priority", 100)),
)
@staticmethod
def _assert_dispatch_context(
dispatch: DispatchRecord,
*,
runtime: str,
runtime_execution_id: str,
tenant_id: str,
task_id: str,
trace_id: str,
principal_id: str,
provenance_id: str,
run_id: str,
) -> None:
expected = {
"runtime": runtime,
"runtime_execution_id": runtime_execution_id,
"tenant_id": tenant_id,
"task_id": task_id,
"trace_id": trace_id,
"principal_id": principal_id,
"provenance_id": provenance_id,
"run_id": run_id,
}
for field_name, expected_value in expected.items():
if getattr(dispatch, field_name) != expected_value:
raise OrchestrationError(f"dispatch {field_name} mismatch")
def hermes_lifecycle_context(
self,
context: RequestContext,
*,
dispatch_id: str,
job_id: str,
tenant_id: str,
task_id: str,
trace_id: str,
principal_id: str,
provenance_id: str,
run_id: str,
) -> tuple[DispatchRecord, HermesJob]:
"""Resolve a runtime lifecycle request against its durable dispatch."""
dispatch = self.dispatch_repository.get(
dispatch_id, tenant_id=context.tenant_id
)
self._assert_dispatch_context(
dispatch,
runtime="hermes",
runtime_execution_id=job_id,
tenant_id=tenant_id,
task_id=task_id,
trace_id=trace_id,
principal_id=principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
if context.tenant_id != tenant_id:
raise OrchestrationError("runtime context tenant_id mismatch")
if context.principal_id != principal_id:
raise OrchestrationError("runtime context principal_id mismatch")
if context.trace_id != trace_id:
raise OrchestrationError("runtime context trace_id mismatch")
if self.hermes is None:
raise OrchestrationError("Hermes runtime is not configured")
job = self.hermes.get(job_id, tenant_id=tenant_id)
if job.task_id != task_id or job.trace_id != trace_id:
raise OrchestrationError("Hermes job identity mismatch")
return dispatch, job
def bind_hermes_worker(
self,
context: RequestContext,
job: HermesJob,
*,
dispatch_id: str,
provenance_id: str,
run_id: str,
worker_id: str,
) -> DispatchRecord:
dispatch, current = self.hermes_lifecycle_context(
context,
dispatch_id=dispatch_id,
job_id=job.job_id,
tenant_id=job.tenant_id,
task_id=job.task_id,
trace_id=job.trace_id,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
if current.assigned_worker_id != worker_id:
raise OrchestrationError("Hermes worker does not own claimed job")
return self.dispatch_repository.update(
dispatch.dispatch_id,
tenant_id=dispatch.tenant_id,
expected_worker_id=worker_id,
)
def reconcile_openclaw(
self,
context: RequestContext,
result: OpenClawResult,
*,
dispatch_id: str,
provenance_id: str,
run_id: str,
) -> TaskRecord:
dispatch = self.dispatch_repository.get(
dispatch_id, tenant_id=context.tenant_id
)
self._assert_dispatch_context(
dispatch,
runtime="openclaw",
runtime_execution_id=result.command_id,
tenant_id=result.tenant_id,
task_id=result.task_id,
trace_id=result.trace_id,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
result_digest = digest_payload(
{
"command_id": result.command_id,
"state": result.state.value,
"output": result.output,
"error": result.error,
"runtime_id": result.runtime_id,
"attempts": result.attempts,
"finished_at": result.finished_at,
}
)
if dispatch.state in TERMINAL_DISPATCH_STATES:
if dispatch.result_digest != result_digest:
raise DispatchConflict("terminal dispatch received a different result")
return self.control_plane.get_task(context, dispatch.task_id)
task = self.control_plane.get_task(context, dispatch.task_id)
if task.state != TaskState.RUNNING:
raise OrchestrationError("task is not RUNNING for active dispatch")
if result.state == CommandState.COMPLETED:
target = TaskState.COMPLETED
values = {"result": result.output or {}}
dispatch_state = DispatchState.COMPLETED
elif result.state in {
CommandState.FAILED,
CommandState.TIMED_OUT,
CommandState.REJECTED,
}:
target = TaskState.FAILED
values = {
"error": result.error or {"code": result.state.value.lower()}
}
dispatch_state = DispatchState.FAILED
elif result.state == CommandState.CANCELLED:
target = TaskState.CANCELLED
values = {}
dispatch_state = DispatchState.CANCELLED
else:
self.dispatch_repository.update(
dispatch.dispatch_id,
tenant_id=dispatch.tenant_id,
state=DispatchState.RUNNING,
result_digest=result_digest,
)
return task
updated_task = self.control_plane.transition_task(
context,
task_id=task.task_id,
target_state=target,
expected_revision=task.revision,
**values,
)
self.dispatch_repository.update(
dispatch.dispatch_id,
tenant_id=dispatch.tenant_id,
state=dispatch_state,
result_digest=result_digest,
)
return updated_task
def reconcile_hermes(
self,
context: RequestContext,
result: HermesResult,
*,
dispatch_id: str,
provenance_id: str,
run_id: str,
) -> TaskRecord:
dispatch = self.dispatch_repository.get(
dispatch_id, tenant_id=context.tenant_id
)
self._assert_dispatch_context(
dispatch,
runtime="hermes",
runtime_execution_id=result.job_id,
tenant_id=result.tenant_id,
task_id=result.task_id,
trace_id=result.trace_id,
principal_id=context.principal_id,
provenance_id=provenance_id,
run_id=run_id,
)
if self.hermes is None:
raise OrchestrationError("Hermes runtime is not configured")
job = self.hermes.get(result.job_id, tenant_id=result.tenant_id)
if result.state in {HermesJobState.COMPLETED, HermesJobState.FAILED}:
if not result.worker_id:
raise OrchestrationError("terminal Hermes result requires worker_id")
if job.assigned_worker_id != result.worker_id:
raise OrchestrationError("Hermes result worker does not own job lease")
if (
dispatch.expected_worker_id is not None
and dispatch.expected_worker_id != result.worker_id
):
raise OrchestrationError("Hermes result worker mismatch")
result_digest = digest_payload(
{
"job_id": result.job_id,
"worker_id": result.worker_id,
"state": result.state.value,
"output": result.output,
"error": result.error,
"finished_at": result.finished_at,
}
)
if dispatch.state in TERMINAL_DISPATCH_STATES:
if dispatch.result_digest != result_digest:
raise DispatchConflict("terminal dispatch received a different result")
return self.control_plane.get_task(context, dispatch.task_id)
task = self.control_plane.get_task(context, dispatch.task_id)
if task.state != TaskState.RUNNING:
raise OrchestrationError("task is not RUNNING for active dispatch")
if result.state == HermesJobState.COMPLETED:
target = TaskState.COMPLETED
values = {"result": result.output or {}}
dispatch_state = DispatchState.COMPLETED
elif result.state == HermesJobState.FAILED:
target = TaskState.FAILED
values = {"error": result.error or {"code": "hermes_failed"}}
dispatch_state = DispatchState.FAILED
elif result.state == HermesJobState.CANCELLED:
target = TaskState.CANCELLED
values = {}
dispatch_state = DispatchState.CANCELLED
else:
raise OrchestrationError("Hermes reconcile only accepts terminal results")
updated_task = self.control_plane.transition_task(
context,
task_id=task.task_id,
target_state=target,
expected_revision=task.revision,
**values,
)
self.dispatch_repository.update(
dispatch.dispatch_id,
tenant_id=dispatch.tenant_id,
state=dispatch_state,
expected_worker_id=result.worker_id,
result_digest=result_digest,
)
return updated_task
def dispatch_snapshot(self, *, tenant_id: str) -> Mapping[str, DispatchRecord]:
return {
item.task_id: item
for item in self.dispatch_repository.list(tenant_id=tenant_id)
}
+181
View File
@@ -0,0 +1,181 @@
"""Durable single-process persistence for the AIOS V2 control plane.
The candidate uses an atomic SQLite snapshot so a gateway restart does not lose
tenants, agents, task state, idempotency records, or audit events. The domain
service remains storage-agnostic; a later HA backend can replace this module
without changing API or runtime-adapter contracts.
"""
from __future__ import annotations
import json
import sqlite3
import threading
from dataclasses import asdict
from pathlib import Path
from typing import Any, Callable
from .control_plane import (
AgentRecord,
AuditEvent,
ControlPlaneService,
InMemoryControlPlaneStore,
TaskRecord,
TaskState,
TenantRecord,
)
class SQLiteSnapshotRepository:
"""Store one versioned control-plane snapshot in SQLite."""
SCHEMA_VERSION = 1
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30.0)
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA foreign_keys=ON")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS control_plane_snapshot (
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
schema_version INTEGER NOT NULL,
payload_json TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
def load(self) -> dict[str, Any] | None:
with self._lock, self._connect() as connection:
row = connection.execute(
"SELECT schema_version, payload_json FROM control_plane_snapshot WHERE singleton_id = 1"
).fetchone()
if row is None:
return None
schema_version, payload_json = row
if int(schema_version) != self.SCHEMA_VERSION:
raise RuntimeError(
f"unsupported control-plane snapshot version: {schema_version}"
)
decoded = json.loads(str(payload_json))
if not isinstance(decoded, dict):
raise RuntimeError("invalid control-plane snapshot")
return decoded
def save(self, payload: dict[str, Any]) -> None:
encoded = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
)
with self._lock, self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"""
INSERT INTO control_plane_snapshot (
singleton_id, schema_version, payload_json, updated_at
) VALUES (1, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(singleton_id) DO UPDATE SET
schema_version = excluded.schema_version,
payload_json = excluded.payload_json,
updated_at = CURRENT_TIMESTAMP
""",
(self.SCHEMA_VERSION, encoded),
)
connection.commit()
class DurableControlPlaneService(ControlPlaneService):
"""Control-plane service that persists after every successful mutation."""
def __init__(
self,
repository: SQLiteSnapshotRepository,
*,
policy_evaluator=None,
) -> None:
self.repository = repository
store = InMemoryControlPlaneStore()
self._restore(store, repository.load())
super().__init__(store, policy_evaluator=policy_evaluator)
@staticmethod
def _restore(
store: InMemoryControlPlaneStore,
snapshot: dict[str, Any] | None,
) -> None:
if not snapshot:
return
for item in snapshot.get("tenants", []):
record = TenantRecord(**item)
store.tenants[record.tenant_id] = record
for item in snapshot.get("agents", []):
normalized = dict(item)
normalized["capabilities"] = tuple(normalized.get("capabilities", ()))
record = AgentRecord(**normalized)
store.agents[record.agent_id] = record
for item in snapshot.get("tasks", []):
normalized = dict(item)
normalized["state"] = TaskState(normalized["state"])
record = TaskRecord(**normalized)
store.tasks[record.task_id] = record
store.audit_events.extend(AuditEvent(**item) for item in snapshot.get("audit", []))
for item in snapshot.get("idempotency", []):
store.idempotency[(item["tenant_id"], item["key"])] = (
item["task_id"],
item["digest"],
)
def _snapshot(self) -> dict[str, Any]:
with self.store.lock:
return {
"schema_version": self.repository.SCHEMA_VERSION,
"tenants": [asdict(item) for item in self.store.tenants.values()],
"agents": [asdict(item) for item in self.store.agents.values()],
"tasks": [
{**asdict(item), "state": item.state.value}
for item in self.store.tasks.values()
],
"audit": [asdict(item) for item in self.store.audit_events],
"idempotency": [
{
"tenant_id": tenant_id,
"key": key,
"task_id": task_id,
"digest": digest,
}
for (tenant_id, key), (task_id, digest)
in self.store.idempotency.items()
],
}
def _mutate(self, operation: Callable[[], Any]) -> Any:
with self.store.lock:
result = operation()
self.repository.save(self._snapshot())
return result
def register_tenant(self, *args, **kwargs):
return self._mutate(lambda: super(DurableControlPlaneService, self).register_tenant(*args, **kwargs))
def register_agent(self, *args, **kwargs):
return self._mutate(lambda: super(DurableControlPlaneService, self).register_agent(*args, **kwargs))
def create_task(self, *args, **kwargs):
return self._mutate(lambda: super(DurableControlPlaneService, self).create_task(*args, **kwargs))
def transition_task(self, *args, **kwargs):
return self._mutate(lambda: super(DurableControlPlaneService, self).transition_task(*args, **kwargs))
+831
View File
@@ -0,0 +1,831 @@
"""AIOS V2 可信 Run Provenance 签发、持久化与受控写入。
生产策略要求 Provenance 同时覆盖七类路径:代码变更、Git 操作、部署、
Runtime 执行、Evidence 写入、回滚和审计。任何缺失、篡改、过期、撤销、
重放或上下文不匹配都默认拒绝。
"""
from __future__ import annotations
import base64
import hashlib
import json
import os
import sqlite3
import stat
import threading
import uuid
from dataclasses import asdict, dataclass, field, replace
from datetime import datetime, timedelta, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Protocol, Sequence
PROVENANCE_VERSION = "aios.v2.run-provenance.v1"
PRODUCTION_POLICY_VERSION = "aios.v2.production.v1"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_timestamp(value: str) -> datetime:
normalized = str(value or "").strip()
if not normalized:
raise ValueError("timestamp is required")
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("timestamp must include timezone")
return parsed.astimezone(timezone.utc)
def required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def canonical_json_bytes(value: Mapping[str, Any]) -> bytes:
return json.dumps(
dict(value),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
def digest_payload(value: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def _validate_digest(value: str) -> str:
normalized = required(value, "request_digest").lower()
if len(normalized) != 64 or any(ch not in "0123456789abcdef" for ch in normalized):
raise ValueError("request_digest must be a SHA-256 hexadecimal digest")
return normalized
class ExecutionPath(str, Enum):
CODE_CHANGE = "code_change"
GIT_OPERATION = "git_operation"
DEPLOYMENT = "deployment"
RUNTIME_EXECUTE = "runtime_execute"
EVIDENCE_WRITE = "evidence_write"
ROLLBACK = "rollback"
AUDIT = "audit"
REQUIRED_PRODUCTION_PATHS = frozenset(item.value for item in ExecutionPath)
class GitOperation(str, Enum):
READ_REF = "read_ref"
FETCH = "fetch"
CREATE_BRANCH = "create_branch"
COMMIT = "commit"
MERGE = "merge"
PUSH = "push"
UPDATE_EVIDENCE = "update_evidence"
class ProvenanceStatus(str, Enum):
ISSUED = "issued"
VERIFIED = "verified"
USED = "used"
EXPIRED = "expired"
REVOKED = "revoked"
class ProvenanceError(RuntimeError):
"""Run Provenance 基础错误。"""
class ProvenanceDenied(ProvenanceError):
"""策略拒绝当前操作。"""
class ProvenanceNotFound(ProvenanceDenied):
"""未找到 Provenance。"""
class ProvenanceSignatureError(ProvenanceDenied):
"""签名无效。"""
class ProvenanceExpired(ProvenanceDenied):
"""Provenance 已过期。"""
class ProvenanceRevoked(ProvenanceDenied):
"""Provenance 已撤销。"""
class ProvenanceReplay(ProvenanceDenied):
"""检测到相同授权摘要重放。"""
class Signer(Protocol):
key_id: str
def sign(self, payload: bytes) -> str: ...
class SignatureVerifier(Protocol):
key_id: str
fingerprint: str
def verify(self, payload: bytes, signature: str) -> bool: ...
def _load_crypto() -> tuple[Any, Any, Any]:
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
except ImportError as exc: # pragma: no cover - depends on deployment image
raise RuntimeError(
"cryptography package with Ed25519 support is required"
) from exc
return serialization, Ed25519PrivateKey, Ed25519PublicKey
def _check_private_key_file(path: Path) -> bytes:
resolved = path.resolve()
metadata = resolved.stat()
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("private key reference is not a regular file")
if metadata.st_mode & 0o077:
raise RuntimeError("private key file permissions must be 0600 or stricter")
return resolved.read_bytes()
def _check_public_key_file(path: Path) -> bytes:
resolved = path.resolve()
metadata = resolved.stat()
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("public key reference is not a regular file")
if metadata.st_mode & 0o022:
raise RuntimeError("public key file must not be group/world writable")
return resolved.read_bytes()
class Ed25519PrivateKeySigner:
"""Ed25519 Owner/Control Plane 签发器。"""
def __init__(self, private_key: Any, *, key_id: str) -> None:
self.private_key = private_key
self.key_id = required(key_id, "key_id")
@classmethod
def from_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PrivateKeySigner":
serialization, private_cls, _ = _load_crypto()
value = _check_private_key_file(Path(path))
if value.startswith(b"-----BEGIN"):
key = serialization.load_pem_private_key(value, password=None)
elif len(value) == 32:
key = private_cls.from_private_bytes(value)
else:
raise RuntimeError("private key must be PEM or a raw 32-byte Ed25519 seed")
if not isinstance(key, private_cls):
raise RuntimeError("private key is not Ed25519")
return cls(key, key_id=key_id)
def sign(self, payload: bytes) -> str:
return base64.b64encode(self.private_key.sign(bytes(payload))).decode("ascii")
def public_verifier(self) -> "Ed25519PublicKeyVerifier":
return Ed25519PublicKeyVerifier(self.private_key.public_key(), key_id=self.key_id)
class Ed25519PublicKeyVerifier:
"""Ed25519 签名验证器。"""
def __init__(self, public_key: Any, *, key_id: str) -> None:
serialization, _, public_cls = _load_crypto()
if not isinstance(public_key, public_cls):
raise RuntimeError("public key is not Ed25519")
self.public_key = public_key
self.key_id = required(key_id, "key_id")
raw = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PublicKeyVerifier":
serialization, _, public_cls = _load_crypto()
value = _check_public_key_file(Path(path))
if value.startswith(b"-----BEGIN"):
key = serialization.load_pem_public_key(value)
elif len(value) == 32:
key = public_cls.from_public_bytes(value)
else:
raise RuntimeError("public key must be PEM or raw 32-byte Ed25519 key")
if not isinstance(key, public_cls):
raise RuntimeError("public key is not Ed25519")
return cls(key, key_id=key_id)
def verify(self, payload: bytes, signature: str) -> bool:
try:
decoded = base64.b64decode(required(signature, "signature"), validate=True)
self.public_key.verify(decoded, bytes(payload))
return True
except Exception:
return False
@dataclass(frozen=True)
class RunProvenance:
provenance_id: str
run_id: str
issuer: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
allowed_paths: Sequence[str]
allowed_git_operations: Sequence[str]
issued_at: str
expires_at: str
policy_version: str
key_id: str
nonce: str
signature: str = ""
schema_version: str = PROVENANCE_VERSION
def __post_init__(self) -> None:
for name in (
"provenance_id",
"run_id",
"issuer",
"tenant_id",
"task_id",
"agent_id",
"principal_id",
"issued_at",
"expires_at",
"policy_version",
"key_id",
"nonce",
"schema_version",
):
object.__setattr__(self, name, required(getattr(self, name), name))
if self.schema_version != PROVENANCE_VERSION:
raise ValueError(f"unsupported schema_version: {self.schema_version}")
issued = parse_timestamp(self.issued_at)
expires = parse_timestamp(self.expires_at)
if expires <= issued:
raise ValueError("expires_at must be later than issued_at")
paths = tuple(sorted({ExecutionPath(item).value for item in self.allowed_paths}))
operations = tuple(
sorted({GitOperation(item).value for item in self.allowed_git_operations})
)
object.__setattr__(self, "allowed_paths", paths)
object.__setattr__(self, "allowed_git_operations", operations)
object.__setattr__(self, "signature", str(self.signature or "").strip())
def unsigned_dict(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"provenance_id": self.provenance_id,
"run_id": self.run_id,
"issuer": self.issuer,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"agent_id": self.agent_id,
"principal_id": self.principal_id,
"allowed_paths": list(self.allowed_paths),
"allowed_git_operations": list(self.allowed_git_operations),
"issued_at": self.issued_at,
"expires_at": self.expires_at,
"policy_version": self.policy_version,
"key_id": self.key_id,
"nonce": self.nonce,
}
def canonical_bytes(self) -> bytes:
return canonical_json_bytes(self.unsigned_dict())
def as_dict(self) -> dict[str, Any]:
value = self.unsigned_dict()
value["signature"] = self.signature
return value
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "RunProvenance":
return cls(**dict(value))
class ProvenanceIssuer:
"""只由 Owner 或 AIOS Control Plane 持有的签发服务。"""
def __init__(self, signer: Signer, store: "SQLiteProvenanceStore") -> None:
self.signer = signer
self.store = store
def issue(
self,
*,
run_id: str,
issuer: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
allowed_paths: Sequence[str] = tuple(REQUIRED_PRODUCTION_PATHS),
allowed_git_operations: Sequence[str] = tuple(item.value for item in GitOperation),
ttl_seconds: int = 3600,
policy_version: str = PRODUCTION_POLICY_VERSION,
provenance_id: str | None = None,
nonce: str | None = None,
now: datetime | None = None,
) -> RunProvenance:
if ttl_seconds <= 0:
raise ValueError("ttl_seconds must be positive")
normalized_paths = frozenset(ExecutionPath(item).value for item in allowed_paths)
if policy_version == PRODUCTION_POLICY_VERSION and normalized_paths != REQUIRED_PRODUCTION_PATHS:
missing = sorted(REQUIRED_PRODUCTION_PATHS - normalized_paths)
extra = sorted(normalized_paths - REQUIRED_PRODUCTION_PATHS)
raise ProvenanceDenied(
f"production provenance must cover exactly seven paths; missing={missing}, extra={extra}"
)
normalized_operations = tuple(
sorted({GitOperation(item).value for item in allowed_git_operations})
)
if ExecutionPath.GIT_OPERATION.value in normalized_paths and not normalized_operations:
raise ProvenanceDenied("git_operation path requires explicit allowed Git operations")
issued = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
unsigned = RunProvenance(
provenance_id=provenance_id or f"prov_{uuid.uuid4().hex}",
run_id=run_id,
issuer=issuer,
tenant_id=tenant_id,
task_id=task_id,
agent_id=agent_id,
principal_id=principal_id,
allowed_paths=tuple(normalized_paths),
allowed_git_operations=normalized_operations,
issued_at=issued.isoformat().replace("+00:00", "Z"),
expires_at=(issued + timedelta(seconds=ttl_seconds))
.isoformat()
.replace("+00:00", "Z"),
policy_version=policy_version,
key_id=self.signer.key_id,
nonce=nonce or uuid.uuid4().hex,
)
signed = replace(unsigned, signature=self.signer.sign(unsigned.canonical_bytes()))
self.store.save_issued(signed)
return signed
@dataclass(frozen=True)
class ProvenanceAuthorization:
authorization_id: str
provenance_id: str
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
execution_path: str
git_operation: str | None
request_digest: str
policy_version: str
key_id: str
public_key_fingerprint: str
authorized_at: str = field(default_factory=utc_now)
def as_dict(self) -> dict[str, Any]:
return asdict(self)
class SQLiteProvenanceStore:
"""持久化 Provenance、状态、授权使用和不可变事件。"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
self._connection = sqlite3.connect(
str(self.path), timeout=30.0, check_same_thread=False
)
self._connection.row_factory = sqlite3.Row
with self._lock:
self._connection.execute("PRAGMA journal_mode=WAL")
self._connection.execute("PRAGMA synchronous=FULL")
self._connection.execute("PRAGMA foreign_keys=ON")
self._connection.executescript(
"""
CREATE TABLE IF NOT EXISTS provenance_records (
provenance_id TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
status TEXT NOT NULL,
signature_verified INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS provenance_usage (
authorization_id TEXT PRIMARY KEY,
provenance_id TEXT NOT NULL,
execution_path TEXT NOT NULL,
git_operation TEXT NOT NULL DEFAULT '',
request_digest TEXT NOT NULL,
authorized_at TEXT NOT NULL,
receipt_json TEXT NOT NULL,
UNIQUE(provenance_id, execution_path, git_operation, request_digest),
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
);
CREATE TABLE IF NOT EXISTS provenance_events (
event_id TEXT PRIMARY KEY,
provenance_id TEXT NOT NULL,
event_type TEXT NOT NULL,
details_json TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
);
CREATE INDEX IF NOT EXISTS idx_provenance_usage_id
ON provenance_usage(provenance_id, authorized_at);
CREATE INDEX IF NOT EXISTS idx_provenance_events_id
ON provenance_events(provenance_id, created_at);
"""
)
self._connection.commit()
def close(self) -> None:
with self._lock:
self._connection.close()
def _event(
self,
provenance_id: str,
event_type: str,
details: Mapping[str, Any] | None = None,
) -> None:
self._connection.execute(
"INSERT INTO provenance_events VALUES (?, ?, ?, ?, ?)",
(
f"pevt_{uuid.uuid4().hex}",
provenance_id,
event_type,
json.dumps(dict(details or {}), ensure_ascii=False, sort_keys=True),
utc_now(),
),
)
def save_issued(self, provenance: RunProvenance) -> None:
payload = json.dumps(
provenance.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
now = utc_now()
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
prior = self._connection.execute(
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
(provenance.provenance_id,),
).fetchone()
if prior is not None:
if prior["payload_json"] != payload:
raise ProvenanceDenied("provenance_id reused with different payload")
self._connection.rollback()
return
self._connection.execute(
"INSERT INTO provenance_records VALUES (?, ?, ?, 0, ?, ?)",
(
provenance.provenance_id,
payload,
ProvenanceStatus.ISSUED.value,
now,
now,
),
)
self._event(provenance.provenance_id, "issued")
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def get(self, provenance_id: str) -> RunProvenance:
with self._lock:
row = self._connection.execute(
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance not found")
value = json.loads(row["payload_json"])
if not isinstance(value, Mapping):
raise ProvenanceError("stored provenance payload is invalid")
return RunProvenance.from_dict(value)
def status(self, provenance_id: str) -> ProvenanceStatus:
with self._lock:
row = self._connection.execute(
"SELECT status FROM provenance_records WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance not found")
return ProvenanceStatus(row["status"])
def mark_verified(self, provenance_id: str, *, fingerprint: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(provenance_id)
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
raise ProvenanceDenied(f"cannot verify provenance in state {current.value}")
next_status = (
ProvenanceStatus.USED.value
if current == ProvenanceStatus.USED
else ProvenanceStatus.VERIFIED.value
)
self._connection.execute(
"UPDATE provenance_records SET status = ?, signature_verified = 1, updated_at = ? WHERE provenance_id = ?",
(next_status, utc_now(), provenance_id),
)
self._event(
provenance_id,
"verified",
{"public_key_fingerprint": fingerprint},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def mark_expired(self, provenance_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(provenance_id)
if current == ProvenanceStatus.REVOKED:
self._connection.rollback()
return
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(ProvenanceStatus.EXPIRED.value, utc_now(), provenance_id),
)
self._event(provenance_id, "expired")
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def revoke(self, provenance_id: str, *, reason: str, principal_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
self.get(provenance_id)
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(ProvenanceStatus.REVOKED.value, utc_now(), provenance_id),
)
self._event(
provenance_id,
"revoked",
{
"reason": required(reason, "reason"),
"principal_id": required(principal_id, "principal_id"),
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def record_authorization(self, authorization: ProvenanceAuthorization) -> None:
receipt = json.dumps(
authorization.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(authorization.provenance_id)
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
raise ProvenanceDenied(f"provenance state is {current.value}")
try:
self._connection.execute(
"INSERT INTO provenance_usage VALUES (?, ?, ?, ?, ?, ?, ?)",
(
authorization.authorization_id,
authorization.provenance_id,
authorization.execution_path,
authorization.git_operation or "",
authorization.request_digest,
authorization.authorized_at,
receipt,
),
)
except sqlite3.IntegrityError as exc:
raise ProvenanceReplay(
"identical provenance path/operation/request digest already used"
) from exc
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(
ProvenanceStatus.USED.value,
authorization.authorized_at,
authorization.provenance_id,
),
)
self._event(
authorization.provenance_id,
"used",
{
"authorization_id": authorization.authorization_id,
"execution_path": authorization.execution_path,
"git_operation": authorization.git_operation,
"request_digest": authorization.request_digest,
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def authorization_count(self, provenance_id: str) -> int:
with self._lock:
row = self._connection.execute(
"SELECT COUNT(*) AS total FROM provenance_usage WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
return int(row["total"])
def events(self, provenance_id: str) -> list[dict[str, Any]]:
with self._lock:
rows = self._connection.execute(
"SELECT event_type, details_json, created_at FROM provenance_events WHERE provenance_id = ? ORDER BY created_at, event_id",
(required(provenance_id, "provenance_id"),),
).fetchall()
return [
{
"event_type": row["event_type"],
"details": json.loads(row["details_json"]),
"created_at": row["created_at"],
}
for row in rows
]
class ProvenanceVerifier:
"""验证 Owner 签名、上下文、路径、Git 操作、时效和重放。"""
def __init__(
self,
store: SQLiteProvenanceStore,
verifiers: Mapping[str, SignatureVerifier],
) -> None:
if not verifiers:
raise ValueError("at least one public key verifier is required")
self.store = store
self.verifiers = {required(key, "key_id"): value for key, value in verifiers.items()}
def authorize(
self,
*,
provenance_id: str,
run_id: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
execution_path: str,
request_digest: str,
git_operation: str | None = None,
now: datetime | None = None,
) -> ProvenanceAuthorization:
provenance = self.store.get(provenance_id)
status = self.store.status(provenance.provenance_id)
if status == ProvenanceStatus.REVOKED:
raise ProvenanceRevoked("provenance is revoked")
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
if current >= parse_timestamp(provenance.expires_at):
self.store.mark_expired(provenance.provenance_id)
raise ProvenanceExpired("provenance is expired")
verifier = self.verifiers.get(provenance.key_id)
if verifier is None:
raise ProvenanceSignatureError("no trusted public key for provenance key_id")
if not provenance.signature or not verifier.verify(
provenance.canonical_bytes(), provenance.signature
):
raise ProvenanceSignatureError("invalid provenance signature")
expected = {
"run_id": required(run_id, "run_id"),
"tenant_id": required(tenant_id, "tenant_id"),
"task_id": required(task_id, "task_id"),
"agent_id": required(agent_id, "agent_id"),
"principal_id": required(principal_id, "principal_id"),
}
for field_name, expected_value in expected.items():
if getattr(provenance, field_name) != expected_value:
raise ProvenanceDenied(f"provenance {field_name} mismatch")
path = ExecutionPath(execution_path).value
if path not in provenance.allowed_paths:
raise ProvenanceDenied(f"execution path is not allowed: {path}")
operation: str | None = None
if path == ExecutionPath.GIT_OPERATION.value:
if git_operation is None:
raise ProvenanceDenied("git operation is required for git_operation path")
operation = GitOperation(git_operation).value
if operation not in provenance.allowed_git_operations:
raise ProvenanceDenied(f"Git operation is not allowed: {operation}")
elif git_operation is not None:
raise ProvenanceDenied("git_operation is only valid for git_operation path")
digest = _validate_digest(request_digest)
self.store.mark_verified(
provenance.provenance_id, fingerprint=verifier.fingerprint
)
authorization = ProvenanceAuthorization(
authorization_id=f"pauth_{uuid.uuid4().hex}",
provenance_id=provenance.provenance_id,
run_id=provenance.run_id,
tenant_id=provenance.tenant_id,
task_id=provenance.task_id,
agent_id=provenance.agent_id,
principal_id=provenance.principal_id,
execution_path=path,
git_operation=operation,
request_digest=digest,
policy_version=provenance.policy_version,
key_id=provenance.key_id,
public_key_fingerprint=verifier.fingerprint,
)
self.store.record_authorization(authorization)
return authorization
class ProvenanceControlledWriter:
"""七路径统一策略入口并持久写入授权回执。"""
def __init__(
self,
verifier: ProvenanceVerifier,
receipt_root: str | Path,
*,
writer_id: str = "aios-v2-provenance-writer",
) -> None:
self.verifier = verifier
self.receipt_root = Path(receipt_root).resolve()
self.receipt_root.mkdir(parents=True, exist_ok=True)
self.writer_id = required(writer_id, "writer_id")
self._lock = threading.RLock()
@staticmethod
def _component(value: str) -> str:
normalized = required(value, "path component")
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
raise ProvenanceDenied("invalid receipt path component")
return normalized
def authorize(self, **values: Any) -> ProvenanceAuthorization:
authorization = self.verifier.authorize(**values)
tenant = self._component(authorization.tenant_id)
provenance_id = self._component(authorization.provenance_id)
target_dir = self.receipt_root / tenant / provenance_id
target = target_dir / f"{authorization.authorization_id}.json"
payload = authorization.as_dict()
payload["writer_id"] = self.writer_id
with self._lock:
target_dir.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
os.chmod(temporary, 0o640)
os.replace(temporary, target)
return authorization
def authorize_code_change(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.CODE_CHANGE.value, **values)
def authorize_git_operation(
self, *, git_operation: str, **values: Any
) -> ProvenanceAuthorization:
return self.authorize(
execution_path=ExecutionPath.GIT_OPERATION.value,
git_operation=git_operation,
**values,
)
def authorize_deployment(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.DEPLOYMENT.value, **values)
def authorize_runtime_execute(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values)
def authorize_evidence_write(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values)
def authorize_rollback(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.ROLLBACK.value, **values)
def authorize_audit(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)
@@ -0,0 +1,381 @@
#!/usr/bin/env python3
"""AIOS V2 Run Provenance 与最小权限 Grant 运维 CLI。
私钥只用于签发;授权命令只加载一个与 ``--path`` 对应的 Grant。所有输出均为
不含 Secret 的 JSON。旧 ``issue`` 仅为候选兼容,生产必须使用
``issue-workflow`` 与 ``issue-grant``。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Mapping, Sequence
from .provenance import (
Ed25519PrivateKeySigner,
Ed25519PublicKeyVerifier,
ExecutionPath,
GitOperation,
ProvenanceIssuer,
SQLiteProvenanceStore,
)
from .provenance_grants import (
GrantIssuer,
GrantType,
GrantVerifier,
SQLiteGrantStore,
WorkflowProvenanceIssuer,
)
from .provenance_operations import (
OperationContext,
ProvenanceOperationGate,
context_from_environment,
load_request_file,
)
from .provenance_scoped import ScopedLeastPrivilegeProvenanceWriter
def _json_object(value: str | None, file_path: str | None) -> Mapping[str, Any]:
if value and file_path:
raise ValueError("request-json and request-file are mutually exclusive")
if file_path:
return load_request_file(file_path)
parsed = json.loads(value or "{}")
if not isinstance(parsed, Mapping):
raise ValueError("request JSON must be an object")
return dict(parsed)
def _csv(value: str | None) -> tuple[str, ...]:
return tuple(item.strip() for item in str(value or "").split(",") if item.strip())
def _write_json(path: str | None, payload: Mapping[str, Any]) -> None:
encoded = json.dumps(dict(payload), ensure_ascii=False, sort_keys=True, indent=2) + "\n"
if path:
target = Path(path).resolve()
target.parent.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(encoded, encoding="utf-8")
os.chmod(temporary, 0o640)
os.replace(temporary, target)
print(encoded, end="")
def _parent_store(args: argparse.Namespace) -> SQLiteProvenanceStore:
return SQLiteProvenanceStore(args.store_file)
def _grant_store(args: argparse.Namespace) -> SQLiteGrantStore:
return SQLiteGrantStore(args.store_file)
def _context(args: argparse.Namespace) -> OperationContext:
if args.context_from_env:
return context_from_environment()
values = {
"provenance_id": args.provenance_id,
"run_id": args.run_id,
"tenant_id": args.tenant_id,
"task_id": args.task_id,
"agent_id": args.agent_id,
"principal_id": args.principal_id,
}
missing = [name for name, value in values.items() if not str(value or "").strip()]
if missing:
raise ValueError(f"missing context arguments: {missing}")
return OperationContext(**{name: str(value).strip() for name, value in values.items()})
def command_issue_legacy(args: argparse.Namespace) -> int:
store = _parent_store(args)
signer = Ed25519PrivateKeySigner.from_file(args.private_key_file, key_id=args.key_id)
provenance = ProvenanceIssuer(signer, store).issue(
run_id=args.run_id,
issuer=args.issuer,
tenant_id=args.tenant_id,
task_id=args.task_id,
agent_id=args.agent_id,
principal_id=args.principal_id,
allowed_paths=_csv(args.allowed_paths) or tuple(item.value for item in ExecutionPath),
allowed_git_operations=_csv(args.allowed_git_operations)
or tuple(item.value for item in GitOperation),
ttl_seconds=args.ttl_seconds,
policy_version=args.policy_version,
)
_write_json(
args.output,
{
"mode": "legacy-full-rights-candidate-only",
"provenance": provenance.as_dict(),
"status": store.status(provenance.provenance_id).value,
"secret_value_stored": False,
},
)
return 0
def command_issue_workflow(args: argparse.Namespace) -> int:
store = _parent_store(args)
signer = Ed25519PrivateKeySigner.from_file(args.private_key_file, key_id=args.key_id)
workflow = WorkflowProvenanceIssuer(signer, store).issue(
run_id=args.run_id,
issuer=args.issuer,
tenant_id=args.tenant_id,
task_id=args.task_id,
agent_id=args.agent_id,
principal_id=args.principal_id,
ttl_seconds=args.ttl_seconds,
)
_write_json(
args.output,
{
"mode": "least-privilege-workflow",
"workflow_provenance": workflow.as_dict(),
"status": store.status(workflow.provenance_id).value,
"secret_value_stored": False,
},
)
return 0
def command_issue_grant(args: argparse.Namespace) -> int:
parent_store = _parent_store(args)
grant_store = _grant_store(args)
signer = Ed25519PrivateKeySigner.from_file(args.private_key_file, key_id=args.key_id)
grant = GrantIssuer(signer, parent_store, grant_store).issue(
parent_provenance_id=args.parent_provenance_id,
grant_type=GrantType(args.grant_type),
ttl_seconds=args.ttl_seconds,
allowed_git_operations=_csv(args.allowed_git_operations),
)
_write_json(
args.output,
{
"grant": grant.as_dict(),
"status": grant_store.status(grant.grant_id).value,
"secret_value_stored": False,
},
)
return 0
def _single_scope_writer(args: argparse.Namespace) -> ScopedLeastPrivilegeProvenanceWriter:
path = ExecutionPath(args.path).value
parent_store = _parent_store(args)
grant_store = _grant_store(args)
verifier = Ed25519PublicKeyVerifier.from_file(args.public_key_file, key_id=args.key_id)
trusted = GrantVerifier(parent_store, grant_store, {args.key_id: verifier})
return ScopedLeastPrivilegeProvenanceWriter(
trusted,
args.receipt_dir,
{path: args.grant_id},
required_paths=(path,),
writer_id=f"aios-v2-cli-{path}",
)
def command_authorize(args: argparse.Namespace) -> int:
writer = _single_scope_writer(args)
context = _context(args)
receipt = ProvenanceOperationGate(writer).authorize(
context,
execution_path=args.path,
operation=args.operation,
request=_json_object(args.request_json, args.request_file),
)
_write_json(args.output, {"authorization": receipt.as_dict()})
return 0
def command_revoke(args: argparse.Namespace) -> int:
store = _parent_store(args)
store.revoke(args.provenance_id, reason=args.reason, principal_id=args.principal_id)
_write_json(
args.output,
{"provenance_id": args.provenance_id, "status": store.status(args.provenance_id).value},
)
return 0
def command_status(args: argparse.Namespace) -> int:
store = _parent_store(args)
provenance = store.get(args.provenance_id)
grant_store = _grant_store(args)
_write_json(
args.output,
{
"provenance": provenance.as_dict(),
"status": store.status(args.provenance_id).value,
"authorization_count": store.authorization_count(args.provenance_id),
"events": store.events(args.provenance_id),
"grants": [item.as_dict() for item in grant_store.list_for_parent(args.provenance_id)],
},
)
return 0
def command_grant_revoke(args: argparse.Namespace) -> int:
store = _grant_store(args)
store.revoke(args.grant_id, reason=args.reason, principal_id=args.principal_id)
_write_json(
args.output,
{"grant_id": args.grant_id, "status": store.status(args.grant_id).value},
)
return 0
def command_grant_status(args: argparse.Namespace) -> int:
store = _grant_store(args)
grant = store.get(args.grant_id)
_write_json(
args.output,
{"grant": grant.as_dict(), "status": store.status(args.grant_id).value},
)
return 0
def add_common_store(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--store-file",
default=os.getenv(
"AIOS_PROVENANCE_STORE_FILE",
"/var/lib/aios-v2/run-provenance.sqlite3",
),
)
parser.add_argument("--output", default=None)
def add_signing_context(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--private-key-file", required=True)
parser.add_argument("--key-id", required=True)
def add_workflow_identity(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--run-id", required=True)
parser.add_argument("--issuer", required=True)
parser.add_argument("--tenant-id", required=True)
parser.add_argument("--task-id", required=True)
parser.add_argument("--agent-id", required=True)
parser.add_argument("--principal-id", required=True)
def add_context(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--context-from-env", action="store_true")
parser.add_argument("--provenance-id")
parser.add_argument("--run-id")
parser.add_argument("--tenant-id")
parser.add_argument("--task-id")
parser.add_argument("--agent-id")
parser.add_argument("--principal-id")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AIOS V2 Run Provenance CLI")
subcommands = parser.add_subparsers(dest="command", required=True)
issue = subcommands.add_parser("issue", help="候选兼容:签发全权限 Provenance;生产禁止")
add_common_store(issue)
add_signing_context(issue)
add_workflow_identity(issue)
issue.add_argument("--allowed-paths")
issue.add_argument("--allowed-git-operations")
issue.add_argument("--ttl-seconds", type=int, default=3600)
issue.add_argument("--policy-version", default="aios.v2.candidate.legacy")
issue.set_defaults(handler=command_issue_legacy)
workflow = subcommands.add_parser(
"issue-workflow", help="签发不含操作权限的父 Workflow Provenance"
)
add_common_store(workflow)
add_signing_context(workflow)
add_workflow_identity(workflow)
workflow.add_argument("--ttl-seconds", type=int, default=14400)
workflow.set_defaults(handler=command_issue_workflow)
grant = subcommands.add_parser("issue-grant", help="签发单一路径最小权限 Grant")
add_common_store(grant)
add_signing_context(grant)
grant.add_argument("--parent-provenance-id", required=True)
grant.add_argument("--grant-type", choices=[item.value for item in GrantType], required=True)
grant.add_argument("--allowed-git-operations")
grant.add_argument("--ttl-seconds", type=int, default=900)
grant.set_defaults(handler=command_issue_grant)
authorize = subcommands.add_parser("authorize", help="使用单一子 Grant 授权一次运维操作")
add_common_store(authorize)
add_context(authorize)
authorize.add_argument("--public-key-file", required=True)
authorize.add_argument("--key-id", required=True)
authorize.add_argument("--grant-id", required=True)
authorize.add_argument(
"--receipt-dir",
default=os.getenv(
"AIOS_PROVENANCE_RECEIPT_DIR",
"/var/lib/aios-v2/provenance-receipts",
),
)
authorize.add_argument(
"--path",
required=True,
choices=[
ExecutionPath.CODE_CHANGE.value,
ExecutionPath.DEPLOYMENT.value,
ExecutionPath.RUNTIME_EXECUTE.value,
ExecutionPath.EVIDENCE_WRITE.value,
ExecutionPath.ROLLBACK.value,
ExecutionPath.AUDIT.value,
],
)
authorize.add_argument("--operation", required=True)
request_group = authorize.add_mutually_exclusive_group()
request_group.add_argument("--request-json")
request_group.add_argument("--request-file")
authorize.set_defaults(handler=command_authorize)
revoke = subcommands.add_parser("revoke", help="撤销父 Workflow Provenance")
add_common_store(revoke)
revoke.add_argument("--provenance-id", required=True)
revoke.add_argument("--reason", required=True)
revoke.add_argument("--principal-id", required=True)
revoke.set_defaults(handler=command_revoke)
status = subcommands.add_parser("status", help="查询父 Workflow 与子 Grant")
add_common_store(status)
status.add_argument("--provenance-id", required=True)
status.set_defaults(handler=command_status)
grant_revoke = subcommands.add_parser("grant-revoke", help="单独撤销子 Grant")
add_common_store(grant_revoke)
grant_revoke.add_argument("--grant-id", required=True)
grant_revoke.add_argument("--reason", required=True)
grant_revoke.add_argument("--principal-id", required=True)
grant_revoke.set_defaults(handler=command_grant_revoke)
grant_status = subcommands.add_parser("grant-status", help="查询子 Grant 状态")
add_common_store(grant_status)
grant_status.add_argument("--grant-id", required=True)
grant_status.set_defaults(handler=command_grant_status)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
return int(args.handler(args))
except Exception as exc:
print(
json.dumps(
{"status": "denied", "error": type(exc).__name__, "message": str(exc)},
ensure_ascii=False,
),
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,416 @@
"""受最小权限 Run Provenance Grant 约束的 Git 写入器。
所有 Git 操作都使用参数数组执行。对于 Commit 和 Evidence Commit,先计算
工作树内容摘要并完成 Provenance 授权,之后才允许执行 ``git add``,避免未授权
请求改变 Git Index。
"""
from __future__ import annotations
import hashlib
import os
import re
import subprocess
import uuid
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Mapping, Protocol, Sequence
from .provenance import GitOperation, ProvenanceDenied, digest_payload, utc_now
_BRANCH_PATTERN = re.compile(r"^(?![-./])(?!.*\.\.)(?!.*//)[A-Za-z0-9._/-]{1,240}$")
_SECRET_URL = re.compile(r"(?i)(https?://)([^/@\s]+)@")
_TOKENISH = re.compile(r"(?i)(token|password|secret|authorization)([=:]\s*)([^\s]+)")
class GitAuthorizationWriter(Protocol):
def authorize_code_change(self, **values: Any) -> Any: ...
def authorize_git_operation(self, *, git_operation: str, **values: Any) -> Any: ...
class ProvenanceGitError(RuntimeError):
"""Git 操作被拒绝或执行失败。"""
@dataclass(frozen=True)
class GitExecutionContext:
provenance_id: str
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
@dataclass(frozen=True)
class GitCommandResult:
operation_id: str
operation: str
authorization_ids: Sequence[str]
command: Sequence[str]
exit_code: int
stdout: str
stderr: str
head_before: str | None
head_after: str | None
started_at: str
completed_at: str
def as_dict(self) -> dict[str, Any]:
return asdict(self)
class ProvenanceGitWriter:
"""执行经过单路径 Grant 授权的窄范围 Git 操作。"""
def __init__(
self,
repo: str | Path,
writer: GitAuthorizationWriter,
*,
timeout_seconds: float = 120.0,
output_limit: int = 16000,
) -> None:
self.repo = Path(repo).resolve()
self.writer = writer
self.timeout_seconds = float(timeout_seconds)
self.output_limit = int(output_limit)
if self.timeout_seconds <= 0 or self.output_limit < 1024:
raise ValueError("invalid Git writer limits")
self._run_raw(["rev-parse", "--is-inside-work-tree"], check=True)
@staticmethod
def _validate_branch(branch: str) -> str:
normalized = str(branch or "").strip()
if not _BRANCH_PATTERN.fullmatch(normalized) or normalized.endswith((".", "/")):
raise ProvenanceGitError("invalid Git branch name")
if normalized in {"main", "master"}:
raise ProvenanceGitError("direct main/master operation is prohibited")
return normalized
def _validate_path(self, value: str | Path) -> str:
raw = Path(value)
candidate = (self.repo / raw).resolve() if not raw.is_absolute() else raw.resolve()
try:
relative = candidate.relative_to(self.repo)
except ValueError as exc:
raise ProvenanceGitError("Git path escapes repository") from exc
if str(relative) in {"", "."}:
raise ProvenanceGitError("repository root is not a valid file path")
return relative.as_posix()
@staticmethod
def _redact(value: str, limit: int) -> str:
text = _SECRET_URL.sub(r"\1***@", str(value or ""))
text = _TOKENISH.sub(r"\1\2***", text)
if len(text) > limit:
return text[:limit] + "\n...[truncated]"
return text
def _head(self) -> str | None:
process = self._run_raw(["rev-parse", "HEAD"], check=False)
return process.stdout.strip() if process.returncode == 0 else None
def _run_raw(
self,
args: Sequence[str],
*,
check: bool,
env: Mapping[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
command = ["git", "-C", str(self.repo), *[str(item) for item in args]]
safe_env = {
**os.environ,
"GIT_TERMINAL_PROMPT": "0",
"GIT_CONFIG_NOSYSTEM": os.environ.get("GIT_CONFIG_NOSYSTEM", "1"),
}
if env:
safe_env.update({str(key): str(value) for key, value in env.items()})
process = subprocess.run(
command,
text=True,
capture_output=True,
timeout=self.timeout_seconds,
env=safe_env,
)
if check and process.returncode != 0:
raise ProvenanceGitError(
self._redact(process.stderr or process.stdout, self.output_limit)
or f"Git command failed with exit code {process.returncode}"
)
return process
def _path_digest(self, relative: str) -> str:
target = self.repo / relative
if target.exists():
if not target.is_file():
raise ProvenanceGitError("Git writer accepts file paths only")
hasher = hashlib.sha256()
with target.open("rb") as handle:
while True:
block = handle.read(65536)
if not block:
break
hasher.update(block)
return hasher.hexdigest()
tracked = self._run_raw(["ls-files", "--error-unmatch", "--", relative], check=False)
if tracked.returncode != 0:
raise ProvenanceGitError(f"path does not exist and is not tracked: {relative}")
return "deleted"
def _content_manifest(self, paths: Sequence[str]) -> Mapping[str, str]:
return {path: self._path_digest(path) for path in sorted(set(paths))}
def _authorize(
self,
context: GitExecutionContext,
*,
operation: GitOperation,
request: Mapping[str, Any],
code_change: bool,
) -> list[str]:
request_digest = digest_payload(
{
"operation": operation.value,
"repo": self.repo.name,
"request": dict(request),
"head": self._head(),
}
)
ids: list[str] = []
try:
if code_change:
code_authorization = self.writer.authorize_code_change(
provenance_id=context.provenance_id,
run_id=context.run_id,
tenant_id=context.tenant_id,
task_id=context.task_id,
agent_id=context.agent_id,
principal_id=context.principal_id,
request_digest=digest_payload(
{
"git_operation": operation.value,
"request_digest": request_digest,
}
),
)
ids.append(code_authorization.authorization_id)
git_authorization = self.writer.authorize_git_operation(
provenance_id=context.provenance_id,
run_id=context.run_id,
tenant_id=context.tenant_id,
task_id=context.task_id,
agent_id=context.agent_id,
principal_id=context.principal_id,
git_operation=operation.value,
request_digest=request_digest,
)
ids.append(git_authorization.authorization_id)
return ids
except ProvenanceDenied as exc:
raise ProvenanceGitError(f"Run Provenance denied Git operation: {exc}") from exc
def _run_authorized(
self,
*,
operation: GitOperation,
args: Sequence[str],
authorization_ids: Sequence[str],
) -> GitCommandResult:
started = utc_now()
before = self._head()
process = self._run_raw(args, check=False)
after = self._head()
result = GitCommandResult(
operation_id=f"gitop_{uuid.uuid4().hex}",
operation=operation.value,
authorization_ids=tuple(authorization_ids),
command=tuple(["git", "-C", "<repo>", *[str(item) for item in args]]),
exit_code=int(process.returncode),
stdout=self._redact(process.stdout, self.output_limit),
stderr=self._redact(process.stderr, self.output_limit),
head_before=before,
head_after=after,
started_at=started,
completed_at=utc_now(),
)
if process.returncode != 0:
raise ProvenanceGitError(
result.stderr or result.stdout or f"Git {operation.value} failed"
)
return result
def _execute(
self,
context: GitExecutionContext,
*,
operation: GitOperation,
args: Sequence[str],
request: Mapping[str, Any],
code_change: bool,
) -> GitCommandResult:
authorization_ids = self._authorize(
context,
operation=operation,
request=request,
code_change=code_change,
)
return self._run_authorized(
operation=operation,
args=args,
authorization_ids=authorization_ids,
)
def read_ref(self, context: GitExecutionContext, ref: str) -> GitCommandResult:
normalized = str(ref or "").strip()
if not normalized or any(ch.isspace() for ch in normalized):
raise ProvenanceGitError("invalid Git ref")
return self._execute(
context,
operation=GitOperation.READ_REF,
args=["rev-parse", "--verify", normalized],
request={"ref": normalized},
code_change=False,
)
def fetch(
self,
context: GitExecutionContext,
*,
remote: str,
refspecs: Sequence[str],
) -> GitCommandResult:
normalized_remote = str(remote or "").strip()
normalized_refspecs = tuple(str(item).strip() for item in refspecs if str(item).strip())
if not normalized_remote or not normalized_refspecs:
raise ProvenanceGitError("remote and refspecs are required")
if any(item.startswith("+") and ":refs/heads/" in item for item in normalized_refspecs):
raise ProvenanceGitError(
"fetching directly into local branch refs is prohibited; use remote-tracking refs"
)
return self._execute(
context,
operation=GitOperation.FETCH,
args=["fetch", "--prune", normalized_remote, *normalized_refspecs],
request={"remote": normalized_remote, "refspecs": normalized_refspecs},
code_change=False,
)
def create_branch(
self,
context: GitExecutionContext,
*,
branch: str,
start_point: str,
) -> GitCommandResult:
normalized = self._validate_branch(branch)
start = str(start_point or "").strip()
if not start:
raise ProvenanceGitError("start_point is required")
return self._execute(
context,
operation=GitOperation.CREATE_BRANCH,
args=["branch", normalized, start],
request={"branch": normalized, "start_point": start},
code_change=False,
)
def commit(
self,
context: GitExecutionContext,
*,
paths: Sequence[str | Path],
message: str,
) -> GitCommandResult:
normalized_paths = tuple(self._validate_path(item) for item in paths)
normalized_message = str(message or "").strip()
if not normalized_paths or not normalized_message:
raise ProvenanceGitError("commit paths and message are required")
if len(normalized_message) > 500:
raise ProvenanceGitError("commit message is too long")
request = {
"paths": normalized_paths,
"message": normalized_message,
"content_manifest": self._content_manifest(normalized_paths),
}
authorization_ids = self._authorize(
context,
operation=GitOperation.COMMIT,
request=request,
code_change=True,
)
self._run_raw(["add", "--", *normalized_paths], check=True)
return self._run_authorized(
operation=GitOperation.COMMIT,
args=["commit", "--no-gpg-sign", "-m", normalized_message, "--", *normalized_paths],
authorization_ids=authorization_ids,
)
def merge(
self,
context: GitExecutionContext,
*,
source_ref: str,
message: str,
) -> GitCommandResult:
source = str(source_ref or "").strip()
normalized_message = str(message or "").strip()
if not source or not normalized_message:
raise ProvenanceGitError("source_ref and message are required")
return self._execute(
context,
operation=GitOperation.MERGE,
args=["merge", "--no-ff", "--no-edit", "-m", normalized_message, source],
request={"source_ref": source, "message": normalized_message},
code_change=True,
)
def push(
self,
context: GitExecutionContext,
*,
remote: str,
branch: str,
) -> GitCommandResult:
normalized_remote = str(remote or "").strip()
normalized_branch = self._validate_branch(branch)
if not normalized_remote:
raise ProvenanceGitError("remote is required")
return self._execute(
context,
operation=GitOperation.PUSH,
args=["push", normalized_remote, f"refs/heads/{normalized_branch}:refs/heads/{normalized_branch}"],
request={"remote": normalized_remote, "branch": normalized_branch},
code_change=False,
)
def update_evidence(
self,
context: GitExecutionContext,
*,
paths: Sequence[str | Path],
message: str,
) -> GitCommandResult:
normalized_paths = tuple(self._validate_path(item) for item in paths)
normalized_message = str(message or "").strip()
if not normalized_paths or not normalized_message:
raise ProvenanceGitError("evidence paths and message are required")
if any(not path.startswith(".agent/") for path in normalized_paths):
raise ProvenanceGitError("UPDATE_EVIDENCE may only commit .agent/ paths")
request = {
"paths": normalized_paths,
"message": normalized_message,
"content_manifest": self._content_manifest(normalized_paths),
}
authorization_ids = self._authorize(
context,
operation=GitOperation.UPDATE_EVIDENCE,
request=request,
code_change=False,
)
self._run_raw(["add", "--", *normalized_paths], check=True)
return self._run_authorized(
operation=GitOperation.UPDATE_EVIDENCE,
args=["commit", "--no-gpg-sign", "-m", normalized_message, "--", *normalized_paths],
authorization_ids=authorization_ids,
)
@@ -0,0 +1,744 @@
"""AIOS V2 最小权限 Run Provenance 子 Grant。
父 Workflow Provenance 只绑定 run/tenant/task/agent/principal 身份,不直接授予
操作权限。每个子 Grant 只允许一个 ExecutionPath;Git Grant 还必须限定具体
GitOperation。子 Grant 具备独立 TTL、nonce、状态、撤销和重放防护。
"""
from __future__ import annotations
import json
import os
import sqlite3
import threading
import uuid
from dataclasses import asdict, dataclass, field, replace
from datetime import datetime, timedelta, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Sequence
from .provenance import (
ExecutionPath,
GitOperation,
ProvenanceDenied,
ProvenanceExpired,
ProvenanceIssuer,
ProvenanceNotFound,
ProvenanceReplay,
ProvenanceRevoked,
ProvenanceSignatureError,
ProvenanceStatus,
ProvenanceVerifier,
RunProvenance,
SQLiteProvenanceStore,
Signer,
SignatureVerifier,
_validate_digest,
canonical_json_bytes,
parse_timestamp,
required,
utc_now,
)
WORKFLOW_POLICY_VERSION = "aios.v2.workflow.v1"
GRANT_SCHEMA_VERSION = "aios.v2.provenance-grant.v1"
class GrantType(str, Enum):
CODE_CHANGE = "code_change"
GIT = "git"
DEPLOYMENT = "deployment"
RUNTIME = "runtime"
EVIDENCE = "evidence"
ROLLBACK = "rollback"
AUDIT = "audit"
GRANT_PATHS: Mapping[GrantType, ExecutionPath] = {
GrantType.CODE_CHANGE: ExecutionPath.CODE_CHANGE,
GrantType.GIT: ExecutionPath.GIT_OPERATION,
GrantType.DEPLOYMENT: ExecutionPath.DEPLOYMENT,
GrantType.RUNTIME: ExecutionPath.RUNTIME_EXECUTE,
GrantType.EVIDENCE: ExecutionPath.EVIDENCE_WRITE,
GrantType.ROLLBACK: ExecutionPath.ROLLBACK,
GrantType.AUDIT: ExecutionPath.AUDIT,
}
class GrantStatus(str, Enum):
ISSUED = "issued"
VERIFIED = "verified"
USED = "used"
EXPIRED = "expired"
REVOKED = "revoked"
@dataclass(frozen=True)
class ProvenanceGrant:
grant_id: str
parent_provenance_id: str
grant_type: GrantType
execution_path: ExecutionPath
run_id: str
issuer: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
allowed_git_operations: Sequence[str]
issued_at: str
expires_at: str
policy_version: str
key_id: str
nonce: str
signature: str = ""
schema_version: str = GRANT_SCHEMA_VERSION
def __post_init__(self) -> None:
for name in (
"grant_id",
"parent_provenance_id",
"run_id",
"issuer",
"tenant_id",
"task_id",
"agent_id",
"principal_id",
"issued_at",
"expires_at",
"policy_version",
"key_id",
"nonce",
"schema_version",
):
object.__setattr__(self, name, required(getattr(self, name), name))
if self.schema_version != GRANT_SCHEMA_VERSION:
raise ValueError(f"unsupported grant schema: {self.schema_version}")
grant_type = GrantType(self.grant_type)
path = ExecutionPath(self.execution_path)
if GRANT_PATHS[grant_type] != path:
raise ValueError("grant_type does not match execution_path")
object.__setattr__(self, "grant_type", grant_type)
object.__setattr__(self, "execution_path", path)
operations = tuple(
sorted({GitOperation(item).value for item in self.allowed_git_operations})
)
if grant_type == GrantType.GIT and not operations:
raise ValueError("Git Grant requires explicit Git operations")
if grant_type != GrantType.GIT and operations:
raise ValueError("only Git Grant may contain Git operations")
object.__setattr__(self, "allowed_git_operations", operations)
issued = parse_timestamp(self.issued_at)
expires = parse_timestamp(self.expires_at)
if expires <= issued:
raise ValueError("grant expires_at must be later than issued_at")
object.__setattr__(self, "signature", str(self.signature or "").strip())
def unsigned_dict(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"grant_id": self.grant_id,
"parent_provenance_id": self.parent_provenance_id,
"grant_type": self.grant_type.value,
"execution_path": self.execution_path.value,
"run_id": self.run_id,
"issuer": self.issuer,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"agent_id": self.agent_id,
"principal_id": self.principal_id,
"allowed_git_operations": list(self.allowed_git_operations),
"issued_at": self.issued_at,
"expires_at": self.expires_at,
"policy_version": self.policy_version,
"key_id": self.key_id,
"nonce": self.nonce,
}
def canonical_bytes(self) -> bytes:
return canonical_json_bytes(self.unsigned_dict())
def as_dict(self) -> dict[str, Any]:
value = self.unsigned_dict()
value["signature"] = self.signature
return value
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "ProvenanceGrant":
return cls(**dict(value))
@dataclass(frozen=True)
class GrantAuthorization:
authorization_id: str
grant_id: str
parent_provenance_id: str
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
execution_path: str
git_operation: str | None
request_digest: str
policy_version: str
key_id: str
public_key_fingerprint: str
authorized_at: str = field(default_factory=utc_now)
@property
def provenance_id(self) -> str:
return self.parent_provenance_id
def as_dict(self) -> dict[str, Any]:
return asdict(self)
class WorkflowProvenanceIssuer:
"""签发不携带操作权限的父 Workflow Provenance。"""
def __init__(self, signer: Signer, store: SQLiteProvenanceStore) -> None:
self._issuer = ProvenanceIssuer(signer, store)
def issue(
self,
*,
run_id: str,
issuer: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
ttl_seconds: int = 14400,
provenance_id: str | None = None,
nonce: str | None = None,
now: datetime | None = None,
) -> RunProvenance:
return self._issuer.issue(
run_id=run_id,
issuer=issuer,
tenant_id=tenant_id,
task_id=task_id,
agent_id=agent_id,
principal_id=principal_id,
allowed_paths=(),
allowed_git_operations=(),
ttl_seconds=ttl_seconds,
policy_version=WORKFLOW_POLICY_VERSION,
provenance_id=provenance_id,
nonce=nonce,
now=now,
)
class SQLiteGrantStore:
"""在 Provenance SQLite 数据库中持久化子 Grant 与使用记录。"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
self._connection = sqlite3.connect(
str(self.path), timeout=30.0, check_same_thread=False
)
self._connection.row_factory = sqlite3.Row
with self._lock:
self._connection.execute("PRAGMA journal_mode=WAL")
self._connection.execute("PRAGMA synchronous=FULL")
self._connection.execute("PRAGMA foreign_keys=ON")
self._connection.executescript(
"""
CREATE TABLE IF NOT EXISTS provenance_grants (
grant_id TEXT PRIMARY KEY,
parent_provenance_id TEXT NOT NULL,
grant_type TEXT NOT NULL,
execution_path TEXT NOT NULL,
payload_json TEXT NOT NULL,
status TEXT NOT NULL,
signature_verified INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_grants_parent
ON provenance_grants(parent_provenance_id, grant_type, status);
CREATE TABLE IF NOT EXISTS provenance_grant_usage (
authorization_id TEXT PRIMARY KEY,
grant_id TEXT NOT NULL,
execution_path TEXT NOT NULL,
git_operation TEXT NOT NULL DEFAULT '',
request_digest TEXT NOT NULL,
authorized_at TEXT NOT NULL,
receipt_json TEXT NOT NULL,
UNIQUE(grant_id, execution_path, git_operation, request_digest),
FOREIGN KEY(grant_id) REFERENCES provenance_grants(grant_id)
);
CREATE TABLE IF NOT EXISTS provenance_grant_events (
event_id TEXT PRIMARY KEY,
grant_id TEXT NOT NULL,
event_type TEXT NOT NULL,
details_json TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY(grant_id) REFERENCES provenance_grants(grant_id)
);
"""
)
self._connection.commit()
def close(self) -> None:
with self._lock:
self._connection.close()
def _event(
self,
grant_id: str,
event_type: str,
details: Mapping[str, Any] | None = None,
) -> None:
self._connection.execute(
"INSERT INTO provenance_grant_events VALUES (?, ?, ?, ?, ?)",
(
f"gevt_{uuid.uuid4().hex}",
grant_id,
event_type,
json.dumps(dict(details or {}), ensure_ascii=False, sort_keys=True),
utc_now(),
),
)
def save_issued(self, grant: ProvenanceGrant) -> None:
payload = json.dumps(
grant.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
now = utc_now()
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
self._connection.execute(
"INSERT INTO provenance_grants VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)",
(
grant.grant_id,
grant.parent_provenance_id,
grant.grant_type.value,
grant.execution_path.value,
payload,
GrantStatus.ISSUED.value,
now,
now,
),
)
self._event(grant.grant_id, "issued")
self._connection.commit()
except sqlite3.IntegrityError as exc:
self._connection.rollback()
raise ProvenanceDenied("grant_id already exists") from exc
except Exception:
self._connection.rollback()
raise
def get(self, grant_id: str) -> ProvenanceGrant:
with self._lock:
row = self._connection.execute(
"SELECT payload_json FROM provenance_grants WHERE grant_id = ?",
(required(grant_id, "grant_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance grant not found")
value = json.loads(row["payload_json"])
if not isinstance(value, Mapping):
raise ProvenanceDenied("stored grant is invalid")
return ProvenanceGrant.from_dict(value)
def status(self, grant_id: str) -> GrantStatus:
with self._lock:
row = self._connection.execute(
"SELECT status FROM provenance_grants WHERE grant_id = ?",
(required(grant_id, "grant_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance grant not found")
return GrantStatus(row["status"])
def mark_verified(self, grant_id: str, *, fingerprint: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(grant_id)
if current in {GrantStatus.REVOKED, GrantStatus.EXPIRED}:
raise ProvenanceDenied(f"cannot verify grant in state {current.value}")
next_status = (
GrantStatus.USED.value
if current == GrantStatus.USED
else GrantStatus.VERIFIED.value
)
self._connection.execute(
"UPDATE provenance_grants SET status = ?, signature_verified = 1, updated_at = ? WHERE grant_id = ?",
(next_status, utc_now(), grant_id),
)
self._event(
grant_id,
"verified",
{"public_key_fingerprint": fingerprint},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def mark_expired(self, grant_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
if self.status(grant_id) == GrantStatus.REVOKED:
self._connection.rollback()
return
self._connection.execute(
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
(GrantStatus.EXPIRED.value, utc_now(), grant_id),
)
self._event(grant_id, "expired")
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def revoke(self, grant_id: str, *, reason: str, principal_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
self.get(grant_id)
self._connection.execute(
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
(GrantStatus.REVOKED.value, utc_now(), grant_id),
)
self._event(
grant_id,
"revoked",
{
"reason": required(reason, "reason"),
"principal_id": required(principal_id, "principal_id"),
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def record_authorization(self, authorization: GrantAuthorization) -> None:
receipt = json.dumps(
authorization.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(authorization.grant_id)
if current in {GrantStatus.REVOKED, GrantStatus.EXPIRED}:
raise ProvenanceDenied(f"grant state is {current.value}")
try:
self._connection.execute(
"INSERT INTO provenance_grant_usage VALUES (?, ?, ?, ?, ?, ?, ?)",
(
authorization.authorization_id,
authorization.grant_id,
authorization.execution_path,
authorization.git_operation or "",
authorization.request_digest,
authorization.authorized_at,
receipt,
),
)
except sqlite3.IntegrityError as exc:
raise ProvenanceReplay(
"identical grant path/operation/request digest already used"
) from exc
self._connection.execute(
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
(GrantStatus.USED.value, authorization.authorized_at, authorization.grant_id),
)
self._event(
authorization.grant_id,
"used",
{
"authorization_id": authorization.authorization_id,
"execution_path": authorization.execution_path,
"git_operation": authorization.git_operation,
"request_digest": authorization.request_digest,
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def list_for_parent(self, parent_provenance_id: str) -> list[ProvenanceGrant]:
with self._lock:
rows = self._connection.execute(
"SELECT payload_json FROM provenance_grants WHERE parent_provenance_id = ? ORDER BY grant_type, grant_id",
(required(parent_provenance_id, "parent_provenance_id"),),
).fetchall()
return [ProvenanceGrant.from_dict(json.loads(row["payload_json"])) for row in rows]
class GrantIssuer:
"""从父 Workflow Provenance 派生单一路径最小权限 Grant。"""
def __init__(
self,
signer: Signer,
parent_store: SQLiteProvenanceStore,
grant_store: SQLiteGrantStore,
) -> None:
self.signer = signer
self.parent_store = parent_store
self.grant_store = grant_store
def issue(
self,
*,
parent_provenance_id: str,
grant_type: GrantType | str,
ttl_seconds: int = 900,
allowed_git_operations: Sequence[str] = (),
grant_id: str | None = None,
nonce: str | None = None,
now: datetime | None = None,
) -> ProvenanceGrant:
if ttl_seconds <= 0:
raise ValueError("grant ttl_seconds must be positive")
parent = self.parent_store.get(parent_provenance_id)
if parent.policy_version != WORKFLOW_POLICY_VERSION:
raise ProvenanceDenied("child grants require a workflow provenance parent")
if parent.allowed_paths or parent.allowed_git_operations:
raise ProvenanceDenied("workflow provenance parent must not contain operation rights")
parent_status = self.parent_store.status(parent.provenance_id)
if parent_status == ProvenanceStatus.REVOKED:
raise ProvenanceRevoked("workflow provenance is revoked")
issued = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
parent_expiry = parse_timestamp(parent.expires_at)
expires = min(issued + timedelta(seconds=ttl_seconds), parent_expiry)
if expires <= issued:
raise ProvenanceExpired("workflow provenance has expired")
normalized_type = GrantType(grant_type)
operations = tuple(allowed_git_operations)
unsigned = ProvenanceGrant(
grant_id=grant_id or f"grant_{uuid.uuid4().hex}",
parent_provenance_id=parent.provenance_id,
grant_type=normalized_type,
execution_path=GRANT_PATHS[normalized_type],
run_id=parent.run_id,
issuer=parent.issuer,
tenant_id=parent.tenant_id,
task_id=parent.task_id,
agent_id=parent.agent_id,
principal_id=parent.principal_id,
allowed_git_operations=operations,
issued_at=issued.isoformat().replace("+00:00", "Z"),
expires_at=expires.isoformat().replace("+00:00", "Z"),
policy_version=f"{WORKFLOW_POLICY_VERSION}.{normalized_type.value}",
key_id=self.signer.key_id,
nonce=nonce or uuid.uuid4().hex,
)
signed = replace(unsigned, signature=self.signer.sign(unsigned.canonical_bytes()))
self.grant_store.save_issued(signed)
return signed
class GrantVerifier:
"""验证父 Workflow 与子 Grant,确保单一路径最小权限。"""
def __init__(
self,
parent_store: SQLiteProvenanceStore,
grant_store: SQLiteGrantStore,
verifiers: Mapping[str, SignatureVerifier],
) -> None:
if not verifiers:
raise ValueError("at least one trusted verifier is required")
self.parent_store = parent_store
self.grant_store = grant_store
self.verifiers = {required(key, "key_id"): value for key, value in verifiers.items()}
def _verify_parent(self, parent: RunProvenance, *, now: datetime) -> SignatureVerifier:
status = self.parent_store.status(parent.provenance_id)
if status == ProvenanceStatus.REVOKED:
raise ProvenanceRevoked("workflow provenance is revoked")
if now >= parse_timestamp(parent.expires_at):
self.parent_store.mark_expired(parent.provenance_id)
raise ProvenanceExpired("workflow provenance is expired")
verifier = self.verifiers.get(parent.key_id)
if verifier is None or not parent.signature or not verifier.verify(
parent.canonical_bytes(), parent.signature
):
raise ProvenanceSignatureError("invalid workflow provenance signature")
if parent.policy_version != WORKFLOW_POLICY_VERSION:
raise ProvenanceDenied("parent is not a workflow provenance")
if parent.allowed_paths or parent.allowed_git_operations:
raise ProvenanceDenied("workflow provenance must not contain operation rights")
return verifier
def authorize(
self,
*,
grant_id: str,
parent_provenance_id: str,
run_id: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
execution_path: str,
request_digest: str,
git_operation: str | None = None,
now: datetime | None = None,
) -> GrantAuthorization:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
parent = self.parent_store.get(parent_provenance_id)
self._verify_parent(parent, now=current)
grant = self.grant_store.get(grant_id)
grant_status = self.grant_store.status(grant.grant_id)
if grant_status == GrantStatus.REVOKED:
raise ProvenanceRevoked("provenance grant is revoked")
if current >= parse_timestamp(grant.expires_at):
self.grant_store.mark_expired(grant.grant_id)
raise ProvenanceExpired("provenance grant is expired")
verifier = self.verifiers.get(grant.key_id)
if verifier is None or not grant.signature or not verifier.verify(
grant.canonical_bytes(), grant.signature
):
raise ProvenanceSignatureError("invalid provenance grant signature")
expected = {
"parent_provenance_id": parent.provenance_id,
"run_id": required(run_id, "run_id"),
"tenant_id": required(tenant_id, "tenant_id"),
"task_id": required(task_id, "task_id"),
"agent_id": required(agent_id, "agent_id"),
"principal_id": required(principal_id, "principal_id"),
}
for field_name, expected_value in expected.items():
if getattr(grant, field_name) != expected_value:
raise ProvenanceDenied(f"grant {field_name} mismatch")
if field_name != "parent_provenance_id" and getattr(parent, field_name) != expected_value:
raise ProvenanceDenied(f"workflow {field_name} mismatch")
path = ExecutionPath(execution_path)
if grant.execution_path != path:
raise ProvenanceDenied(
f"grant permits {grant.execution_path.value}, not {path.value}"
)
operation: str | None = None
if path == ExecutionPath.GIT_OPERATION:
if git_operation is None:
raise ProvenanceDenied("Git operation is required")
operation = GitOperation(git_operation).value
if operation not in grant.allowed_git_operations:
raise ProvenanceDenied(f"Git operation is not allowed: {operation}")
elif git_operation is not None:
raise ProvenanceDenied("git_operation is only valid for a Git Grant")
digest = _validate_digest(request_digest)
self.grant_store.mark_verified(grant.grant_id, fingerprint=verifier.fingerprint)
authorization = GrantAuthorization(
authorization_id=f"gauth_{uuid.uuid4().hex}",
grant_id=grant.grant_id,
parent_provenance_id=parent.provenance_id,
run_id=grant.run_id,
tenant_id=grant.tenant_id,
task_id=grant.task_id,
agent_id=grant.agent_id,
principal_id=grant.principal_id,
execution_path=path.value,
git_operation=operation,
request_digest=digest,
policy_version=grant.policy_version,
key_id=grant.key_id,
public_key_fingerprint=verifier.fingerprint,
)
self.grant_store.record_authorization(authorization)
return authorization
class LeastPrivilegeProvenanceWriter:
"""按路径选择独立子 Grant 的生产 Writer。
``provenance_id`` 参数始终指父 Workflow Provenance;Grant ID 来自受保护
配置映射,调用方无法通过请求体临时扩大权限。
"""
def __init__(
self,
verifier: GrantVerifier,
receipt_root: str | Path,
grant_ids: Mapping[str, str],
*,
writer_id: str = "aios-v2-least-privilege-writer",
) -> None:
self.verifier = verifier
self.receipt_root = Path(receipt_root).resolve()
self.receipt_root.mkdir(parents=True, exist_ok=True)
self.grant_ids = {
ExecutionPath(path).value: required(grant_id, f"grant_id[{path}]")
for path, grant_id in grant_ids.items()
}
missing = sorted(path.value for path in ExecutionPath if path.value not in self.grant_ids)
if missing:
raise ValueError(f"missing least-privilege Grant mappings: {missing}")
self.writer_id = required(writer_id, "writer_id")
self._lock = threading.RLock()
@staticmethod
def _component(value: str) -> str:
normalized = required(value, "path component")
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
raise ProvenanceDenied("invalid receipt path component")
return normalized
def authorize(self, **values: Any) -> GrantAuthorization:
path = ExecutionPath(values["execution_path"]).value
grant_id = self.grant_ids[path]
parent_provenance_id = values.pop("provenance_id")
authorization = self.verifier.authorize(
grant_id=grant_id,
parent_provenance_id=parent_provenance_id,
**values,
)
tenant = self._component(authorization.tenant_id)
parent = self._component(authorization.parent_provenance_id)
grant = self._component(authorization.grant_id)
target_dir = self.receipt_root / tenant / parent / grant
target = target_dir / f"{authorization.authorization_id}.json"
payload = authorization.as_dict()
payload["writer_id"] = self.writer_id
with self._lock:
target_dir.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
os.chmod(temporary, 0o640)
os.replace(temporary, target)
return authorization
def authorize_code_change(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.CODE_CHANGE.value, **values)
def authorize_git_operation(
self, *, git_operation: str, **values: Any
) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.GIT_OPERATION.value,
git_operation=git_operation,
**values,
)
def authorize_deployment(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.DEPLOYMENT.value, **values)
def authorize_runtime_execute(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values)
def authorize_evidence_write(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values)
def authorize_rollback(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.ROLLBACK.value, **values)
def authorize_audit(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)
@@ -0,0 +1,265 @@
"""AIOS V2 运维操作 Provenance Gate 与服务级 Scoped Writer 工厂。"""
from __future__ import annotations
import json
import os
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Mapping, Protocol, Sequence
from .provenance import (
Ed25519PublicKeyVerifier,
ExecutionPath,
ProvenanceAuthorization,
ProvenanceControlledWriter,
ProvenanceDenied,
ProvenanceVerifier,
SQLiteProvenanceStore,
digest_payload,
)
from .provenance_grants import GrantAuthorization, GrantVerifier, SQLiteGrantStore
from .provenance_scoped import ScopedLeastPrivilegeProvenanceWriter
APPLICATION_PROVENANCE_SCOPE = (
ExecutionPath.RUNTIME_EXECUTE.value,
ExecutionPath.EVIDENCE_WRITE.value,
ExecutionPath.AUDIT.value,
)
class AuthorizationWriter(Protocol):
def authorize_deployment(self, **values: Any) -> Any: ...
def authorize_rollback(self, **values: Any) -> Any: ...
def authorize_audit(self, **values: Any) -> Any: ...
def authorize_code_change(self, **values: Any) -> Any: ...
def authorize_evidence_write(self, **values: Any) -> Any: ...
def authorize_runtime_execute(self, **values: Any) -> Any: ...
@dataclass(frozen=True)
class OperationContext:
provenance_id: str
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
def as_dict(self) -> dict[str, str]:
return asdict(self)
@dataclass(frozen=True)
class OperationReceipt:
operation: str
request_digest: str
authorization_id: str
provenance_id: str
grant_id: str | None
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
execution_path: str
authorized_at: str
def as_dict(self) -> dict[str, Any]:
return asdict(self)
class ProvenanceOperationGate:
"""部署、回滚、审计和运行操作的统一授权入口。"""
def __init__(self, writer: AuthorizationWriter) -> None:
self.writer = writer
def authorize(
self,
context: OperationContext,
*,
execution_path: str,
operation: str,
request: Mapping[str, Any],
) -> OperationReceipt:
path = ExecutionPath(execution_path).value
normalized_operation = str(operation or "").strip()
if not normalized_operation:
raise ValueError("operation is required")
digest = digest_payload(
{
"operation": normalized_operation,
"execution_path": path,
"request": dict(request),
}
)
values = {
"provenance_id": context.provenance_id,
"run_id": context.run_id,
"tenant_id": context.tenant_id,
"task_id": context.task_id,
"agent_id": context.agent_id,
"principal_id": context.principal_id,
"request_digest": digest,
}
if path == ExecutionPath.DEPLOYMENT.value:
authorization = self.writer.authorize_deployment(**values)
elif path == ExecutionPath.ROLLBACK.value:
authorization = self.writer.authorize_rollback(**values)
elif path == ExecutionPath.AUDIT.value:
authorization = self.writer.authorize_audit(**values)
elif path == ExecutionPath.CODE_CHANGE.value:
authorization = self.writer.authorize_code_change(**values)
elif path == ExecutionPath.EVIDENCE_WRITE.value:
authorization = self.writer.authorize_evidence_write(**values)
elif path == ExecutionPath.RUNTIME_EXECUTE.value:
authorization = self.writer.authorize_runtime_execute(**values)
else:
raise ProvenanceDenied(
"generic operation gate does not authorize git_operation; use ProvenanceGitWriter"
)
return self._receipt(normalized_operation, digest, authorization)
@staticmethod
def _receipt(
operation: str,
request_digest: str,
authorization: ProvenanceAuthorization | GrantAuthorization | Any,
) -> OperationReceipt:
return OperationReceipt(
operation=operation,
request_digest=request_digest,
authorization_id=authorization.authorization_id,
provenance_id=authorization.provenance_id,
grant_id=getattr(authorization, "grant_id", None),
run_id=authorization.run_id,
tenant_id=authorization.tenant_id,
task_id=authorization.task_id,
agent_id=authorization.agent_id,
principal_id=authorization.principal_id,
execution_path=authorization.execution_path,
authorized_at=authorization.authorized_at,
)
_GRANT_ENV_BY_PATH: Mapping[str, str] = {
ExecutionPath.CODE_CHANGE.value: "AIOS_PROVENANCE_GRANT_CODE_CHANGE_ID",
ExecutionPath.GIT_OPERATION.value: "AIOS_PROVENANCE_GRANT_GIT_ID",
ExecutionPath.DEPLOYMENT.value: "AIOS_PROVENANCE_GRANT_DEPLOYMENT_ID",
ExecutionPath.RUNTIME_EXECUTE.value: "AIOS_PROVENANCE_GRANT_RUNTIME_ID",
ExecutionPath.EVIDENCE_WRITE.value: "AIOS_PROVENANCE_GRANT_EVIDENCE_ID",
ExecutionPath.ROLLBACK.value: "AIOS_PROVENANCE_GRANT_ROLLBACK_ID",
ExecutionPath.AUDIT.value: "AIOS_PROVENANCE_GRANT_AUDIT_ID",
}
def grant_ids_from_environment(
*,
required_paths: Sequence[str] | None = None,
) -> dict[str, str]:
paths = tuple(
sorted(
{
ExecutionPath(path).value
for path in (required_paths or tuple(item.value for item in ExecutionPath))
}
)
)
values = {path: os.getenv(_GRANT_ENV_BY_PATH[path], "").strip() for path in paths}
missing = [_GRANT_ENV_BY_PATH[path] for path, value in values.items() if not value]
if missing:
raise RuntimeError(f"missing scoped Provenance Grant configuration: {missing}")
return values
def build_provenance_writer_from_env(
*,
state_dir: str | Path | None = None,
required: bool | None = None,
required_paths: Sequence[str] | None = None,
writer_id: str = "aios-v2-application-grant-writer",
) -> AuthorizationWriter | None:
"""从环境构建服务级最小权限 Writer。
生产应用默认只加载 Runtime、Evidence、Audit Grant。Git、部署和回滚进程
必须显式传入自己的 ``required_paths``,不能继承应用默认范围。旧全权限 Writer
只允许在显式关闭强制模式的候选/测试环境中使用。
"""
require_value = (
bool(required)
if required is not None
else os.getenv("AIOS_REQUIRE_RUN_PROVENANCE", "0").strip().lower()
in {"1", "true", "yes", "on"}
)
public_key_file = os.getenv("AIOS_PROVENANCE_PUBLIC_KEY_FILE", "").strip()
key_id = os.getenv("AIOS_PROVENANCE_KEY_ID", "").strip()
base = Path(state_dir or os.getenv("AIOS_V2_STATE_DIR", "/var/lib/aios-v2")).resolve()
store_file = os.getenv(
"AIOS_PROVENANCE_STORE_FILE", str(base / "run-provenance.sqlite3")
).strip()
receipt_root = os.getenv(
"AIOS_PROVENANCE_RECEIPT_DIR", str(base / "provenance-receipts")
).strip()
configured = bool(public_key_file and key_id)
if not configured:
if require_value:
raise RuntimeError(
"Run Provenance is required but AIOS_PROVENANCE_PUBLIC_KEY_FILE or AIOS_PROVENANCE_KEY_ID is missing"
)
return None
verifier = Ed25519PublicKeyVerifier.from_file(public_key_file, key_id=key_id)
parent_store = SQLiteProvenanceStore(store_file)
normalized_paths = tuple(
sorted(
{
ExecutionPath(path).value
for path in (
required_paths
if required_paths is not None
else APPLICATION_PROVENANCE_SCOPE
)
}
)
)
if require_value:
if not normalized_paths:
raise RuntimeError("production Run Provenance requires a non-empty service scope")
grant_ids = grant_ids_from_environment(required_paths=normalized_paths)
grant_store = SQLiteGrantStore(store_file)
trusted_grants = GrantVerifier(parent_store, grant_store, {key_id: verifier})
return ScopedLeastPrivilegeProvenanceWriter(
trusted_grants,
receipt_root,
grant_ids,
required_paths=normalized_paths,
writer_id=writer_id,
)
legacy = ProvenanceVerifier(parent_store, {key_id: verifier})
return ProvenanceControlledWriter(legacy, receipt_root)
def context_from_environment() -> OperationContext:
values = {
"provenance_id": os.getenv("AIOS_PROVENANCE_ID", ""),
"run_id": os.getenv("AIOS_RUN_ID", ""),
"tenant_id": os.getenv("AIOS_TENANT_ID", ""),
"task_id": os.getenv("AIOS_TASK_ID", ""),
"agent_id": os.getenv("AIOS_AGENT_ID", ""),
"principal_id": os.getenv("AIOS_PRINCIPAL_ID", ""),
}
missing = [name for name, value in values.items() if not str(value).strip()]
if missing:
raise RuntimeError(f"missing Provenance context environment variables: {missing}")
return OperationContext(**{name: str(value).strip() for name, value in values.items()})
def load_request_file(path: str | Path) -> Mapping[str, Any]:
value = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(value, Mapping):
raise ValueError("request file must contain a JSON object")
return dict(value)
@@ -0,0 +1,131 @@
"""按服务范围加载的最小权限 Provenance Writer。
该 Writer 只持有调用进程需要的 Grant ID。未配置路径的方法即使被调用也会
默认拒绝,避免应用进程获得 Git、部署或回滚权限。
"""
from __future__ import annotations
import json
import os
import threading
from pathlib import Path
from typing import Any, Mapping, Sequence
from .provenance import ExecutionPath, ProvenanceDenied, required
from .provenance_grants import GrantAuthorization, GrantVerifier
class ScopedLeastPrivilegeProvenanceWriter:
"""只暴露指定 ExecutionPath 的最小权限 Writer。"""
def __init__(
self,
verifier: GrantVerifier,
receipt_root: str | Path,
grant_ids: Mapping[str, str],
*,
required_paths: Sequence[str],
writer_id: str = "aios-v2-scoped-grant-writer",
) -> None:
self.verifier = verifier
self.receipt_root = Path(receipt_root).resolve()
self.receipt_root.mkdir(parents=True, exist_ok=True)
normalized_required = tuple(
sorted({ExecutionPath(path).value for path in required_paths})
)
if not normalized_required:
raise ValueError("required_paths must not be empty")
self.required_paths = frozenset(normalized_required)
self.grant_ids = {
path: required(grant_ids.get(path, ""), f"grant_id[{path}]")
for path in normalized_required
}
unexpected = sorted(set(grant_ids) - self.required_paths)
if unexpected:
raise ValueError(
f"scoped writer received unnecessary Grant mappings: {unexpected}"
)
self.writer_id = required(writer_id, "writer_id")
self._lock = threading.RLock()
@staticmethod
def _component(value: str) -> str:
normalized = required(value, "path component")
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
raise ProvenanceDenied("invalid receipt path component")
return normalized
def _grant_for(self, path: str) -> str:
normalized = ExecutionPath(path).value
grant_id = self.grant_ids.get(normalized)
if grant_id is None:
raise ProvenanceDenied(
f"this process is not allowed to authorize {normalized}"
)
return grant_id
def authorize(self, **values: Any) -> GrantAuthorization:
path = ExecutionPath(values["execution_path"]).value
grant_id = self._grant_for(path)
parent_provenance_id = values.pop("provenance_id")
authorization = self.verifier.authorize(
grant_id=grant_id,
parent_provenance_id=parent_provenance_id,
**values,
)
tenant = self._component(authorization.tenant_id)
parent = self._component(authorization.parent_provenance_id)
grant = self._component(authorization.grant_id)
target_dir = self.receipt_root / tenant / parent / grant
target = target_dir / f"{authorization.authorization_id}.json"
payload = authorization.as_dict()
payload["writer_id"] = self.writer_id
payload["scope"] = sorted(self.required_paths)
with self._lock:
target_dir.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2)
+ "\n",
encoding="utf-8",
)
os.chmod(temporary, 0o640)
os.replace(temporary, target)
return authorization
def authorize_code_change(self, **values: Any) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.CODE_CHANGE.value, **values
)
def authorize_git_operation(
self, *, git_operation: str, **values: Any
) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.GIT_OPERATION.value,
git_operation=git_operation,
**values,
)
def authorize_deployment(self, **values: Any) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.DEPLOYMENT.value, **values
)
def authorize_runtime_execute(self, **values: Any) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values
)
def authorize_evidence_write(self, **values: Any) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values
)
def authorize_rollback(self, **values: Any) -> GrantAuthorization:
return self.authorize(
execution_path=ExecutionPath.ROLLBACK.value, **values
)
def authorize_audit(self, **values: Any) -> GrantAuthorization:
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)
@@ -1,652 +0,0 @@
#!/usr/bin/env python3
"""One-shot governed executor for an already approved exact release binding.
This module deliberately does not build release artifacts. The build/staging
evidence is completed before Owner production approval. Production execution
consumes that immutable binding, revalidates it, and performs narrow
side-effects through adapters in a durable fail-closed order.
Adapter protocol:
stdin: JSON request with operation = "probe" | "apply" | "rollback"
stdout: one JSON object on the final non-empty line
shell: never used
The executor never rewrites tags, never writes refs/heads/main, and records
Baseline last.
"""
from __future__ import annotations
import argparse
import dataclasses
import fcntl
import hashlib
import json
import os
import re
import stat
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
REQUIRED_V2R1C7_MEMBERS = {
"payload/VERSION",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r1c7_health_service.py",
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
"payload/deploy/aios-v2/v2r1c7-rollback.json",
"payload/scripts/probe_v2r1c7_health.py",
"payload/scripts/rollback_v2r1c7_release.sh",
"payload/BUILD-METADATA.json",
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
class ReleaseBlocked(RuntimeError):
pass
class ManualOwnerReviewRequired(ReleaseBlocked):
pass
class ReconciliationRequired(ReleaseBlocked):
pass
@dataclasses.dataclass(frozen=True)
class Binding:
version: str
target_oid: str
evidence_closure_oid: str
artifact_sha256: str
approval_plan_id: str
artifact_path: str
artifact_size: int
production_service: str
production_port: int
repository_identity: str = "aios/aios"
def __post_init__(self) -> None:
if not self.version or not OID_RE.fullmatch(self.target_oid):
raise ValueError("invalid release target binding")
if not OID_RE.fullmatch(self.evidence_closure_oid):
raise ValueError("invalid evidence closure OID")
if not SHA_RE.fullmatch(self.artifact_sha256):
raise ValueError("invalid artifact SHA-256")
if not PLAN_RE.fullmatch(self.approval_plan_id):
raise ValueError("invalid approval plan id")
if self.artifact_size <= 0:
raise ValueError("invalid artifact size")
if not self.production_service:
raise ValueError("production service is required")
if not 1024 <= self.production_port <= 65535:
raise ValueError("invalid production port")
if self.repository_identity != "aios/aios":
raise ValueError("unexpected repository identity")
@property
def tag(self) -> str:
return self.version
@property
def fingerprint(self) -> str:
payload = {
"version": self.version,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"artifact_sha256": self.artifact_sha256,
"approval_plan_id": self.approval_plan_id,
"artifact_size": self.artifact_size,
"production_service": self.production_service,
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
return hashlib.sha256(canonical_json(payload)).hexdigest()
def request(self, operation: str) -> dict[str, Any]:
return {
"schema": "aios.production-executor-request.v1",
"operation": operation,
"binding_fingerprint": self.fingerprint,
"version": self.version,
"tag": self.tag,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"artifact_sha256": self.artifact_sha256,
"artifact_path": self.artifact_path,
"artifact_size": self.artifact_size,
"approval_plan_id": self.approval_plan_id,
"production_service": self.production_service,
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
def canonical_json(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def secure_json(path: Path) -> dict[str, Any]:
st = path.lstat()
if not stat.S_ISREG(st.st_mode) or path.is_symlink():
raise ReleaseBlocked(f"non-regular authorization file: {path}")
if st.st_uid != 0:
raise ReleaseBlocked(f"authorization file must be root-owned: {path}")
if st.st_mode & 0o022:
raise ReleaseBlocked(f"authorization file is group/world writable: {path}")
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ReleaseBlocked("authorization receipt must be a JSON object")
return data
def sha256_file(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked("artifact must be a regular non-symlink file")
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=check,
capture_output=True,
text=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
if not OID_RE.fullmatch(authority_main):
raise ReleaseBlocked("Authority main OID is invalid")
target_type = run_git(git_dir, "cat-file", "-t", binding.target_oid).stdout.strip()
if target_type != "commit":
raise ReleaseBlocked("target_oid is not a commit")
evidence_type = run_git(
git_dir, "cat-file", "-t", binding.evidence_closure_oid
).stdout.strip()
if evidence_type != "commit":
raise ReleaseBlocked("evidence_closure_oid is not a commit")
# A control-plane-only patch may advance main after release approval.
# The approved release target remains valid only while it is still on main.
target_on_main = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
authority_main,
check=False,
).returncode == 0
if not target_on_main:
raise ManualOwnerReviewRequired(
"approved target is no longer an ancestor of Authority main"
)
target_in_evidence = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
binding.evidence_closure_oid,
check=False,
).returncode == 0
if not target_in_evidence:
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
return {
"authority_main_oid": authority_main,
"target_oid": binding.target_oid,
"target_is_ancestor_of_main": True,
"evidence_closure_oid": binding.evidence_closure_oid,
"evidence_object_type": evidence_type,
"target_is_ancestor_of_evidence": True,
}
def verify_artifact(binding: Binding) -> dict[str, Any]:
path = Path(binding.artifact_path)
if path.stat().st_size != binding.artifact_size:
raise ReleaseBlocked("artifact size mismatch")
actual = sha256_file(path)
if actual != binding.artifact_sha256:
raise ReleaseBlocked("artifact SHA-256 mismatch")
if binding.version == "V2R1C7":
# Python tarfile may not have zstd support on the target host, so
# decompress into a private temporary tar and inspect it there.
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
tar_path = Path(temp_dir) / "release.tar"
with tar_path.open("wb") as output:
completed = subprocess.run(
["zstd", "-dc", str(path)],
check=False,
stdout=output,
stderr=subprocess.PIPE,
timeout=60,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("artifact zstd decompression failed")
import tarfile
with tarfile.open(tar_path, "r:") as archive:
names: set[str] = set()
for member in archive.getmembers():
candidate = Path(member.name)
if (
candidate.is_absolute()
or ".." in candidate.parts
or member.issym()
or member.islnk()
or not (member.isfile() or member.isdir())
):
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
names.add(member.name)
missing = sorted(REQUIRED_V2R1C7_MEMBERS - names)
if missing:
raise ReleaseBlocked(
"artifact missing required members: " + ",".join(missing)
)
return {
"artifact_path": str(path),
"artifact_sha256": actual,
"artifact_size": binding.artifact_size,
"verified": True,
}
def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[str, Any]:
if receipt.get("accepted") is not True:
raise ReleaseBlocked("Owner production authorization is not accepted")
if receipt.get("binding_replaced") not in (None, False):
raise ManualOwnerReviewRequired("Owner binding was replaced")
expected = {
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
for key, value in expected.items():
if receipt.get(key) != value:
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
return {"accepted": True, **expected}
class DurableState:
def __init__(self, root: Path, binding: Binding):
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
os.chmod(self.root, 0o700)
self.path = self.root / "execution.json"
self.binding = binding
self.value: dict[str, Any]
if self.path.exists():
existing = json.loads(self.path.read_text(encoding="utf-8"))
if existing.get("binding_fingerprint") != binding.fingerprint:
raise ManualOwnerReviewRequired(
"state root contains a different production binding"
)
self.value = existing
else:
self.value = {
"schema": "aios.production-execution-state.v1",
"binding_fingerprint": binding.fingerprint,
"version": binding.version,
"target_oid": binding.target_oid,
"state": "AUTHORIZED",
"completed": {},
"history": [],
}
self.save()
def save(self) -> None:
temporary = self.path.with_suffix(".tmp")
temporary.write_bytes(canonical_json(self.value))
os.chmod(temporary, 0o600)
os.replace(temporary, self.path)
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
now = time.time()
sanitized = json.loads(json.dumps(result))
self.value["state"] = state
self.value["completed"][step] = sanitized
self.value["history"].append(
{"at": now, "state": state, "step": step, "result": sanitized}
)
self.save()
def fail(self, state: str, reason: str) -> None:
self.value["state"] = state
self.value["history"].append(
{"at": time.time(), "state": state, "reason": reason}
)
self.save()
class FileLock:
def __init__(self, path: Path):
self.path = path
self.handle = None
def __enter__(self) -> "FileLock":
self.path.parent.mkdir(parents=True, exist_ok=True)
self.handle = self.path.open("a+", encoding="utf-8")
os.chmod(self.path, 0o600)
try:
fcntl.flock(
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
)
except BlockingIOError as exc:
raise ReleaseBlocked("production executor is already running") from exc
return self
def __exit__(self, exc_type, exc, tb) -> None:
if self.handle is not None:
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
self.handle.close()
def parse_command(values: Sequence[str]) -> list[str]:
command = [str(x) for x in values if str(x)]
if not command:
raise ValueError("adapter command is required")
if not os.path.isabs(command[0]):
raise ValueError("adapter executable must be an absolute path")
return command
def run_adapter(command: Sequence[str], request: Mapping[str, Any]) -> dict[str, Any]:
completed = subprocess.run(
list(command),
input=canonical_json(request),
check=False,
capture_output=True,
timeout=180,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked(
f"adapter failed: {Path(command[0]).name} rc={completed.returncode}"
)
lines = [line for line in completed.stdout.decode("utf-8").splitlines() if line.strip()]
if not lines:
raise ReleaseBlocked("adapter returned no JSON result")
try:
value = json.loads(lines[-1])
except ValueError as exc:
raise ReleaseBlocked("adapter returned invalid JSON") from exc
if not isinstance(value, dict):
raise ReleaseBlocked("adapter result must be an object")
return value
def require_exact_response(result: Mapping[str, Any], binding: Binding) -> None:
for key, expected in {
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}.items():
if key in result and result[key] != expected:
raise ManualOwnerReviewRequired(f"adapter binding mismatch: {key}")
def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
request = binding.request("probe")
probe = run_adapter(command, request)
require_exact_response(probe, binding)
state = probe.get("status")
if state == "CONFLICT":
raise ManualOwnerReviewRequired("same-name tag conflicts with approved binding")
if state == "INDETERMINATE":
raise ReconciliationRequired("tag publication state is indeterminate")
if state not in {"APPLIED_VERIFIED", "NOT_APPLIED", "PARTIAL_LOCAL"}:
raise ReleaseBlocked(f"unsupported tag probe status: {state}")
if state != "APPLIED_VERIFIED":
applied = run_adapter(command, binding.request("apply"))
require_exact_response(applied, binding)
if applied.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("tag conflict during publication")
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") != "APPLIED_VERIFIED":
raise ReconciliationRequired("tag was not independently verified")
if probe.get("peeled_target_oid") != binding.target_oid:
raise ManualOwnerReviewRequired("published tag peeled target mismatch")
tag_oid = probe.get("tag_oid")
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
raise ReleaseBlocked("verified annotated tag object OID missing")
return dict(probe)
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") == "ACTIVE_VERIFIED":
return dict(probe)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
applied = run_adapter(command, binding.request("apply"))
require_exact_response(applied, binding)
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") != "ACTIVE_VERIFIED":
try:
run_adapter(command, binding.request("rollback"))
except Exception:
pass
raise ReconciliationRequired("production runtime did not verify after apply")
if (
probe.get("deployment_state") != "ACTIVE"
or probe.get("production_deployed") is not True
):
raise ReconciliationRequired("production health contract is not ACTIVE")
return dict(probe)
def finalize_step(
command: Sequence[str],
binding: Binding,
*,
name: str,
tag_oid: str,
) -> dict[str, Any]:
request = binding.request("probe")
request["tag_oid"] = tag_oid
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired(f"{name} has conflicting release binding")
if probe.get("status") != "VERIFIED":
apply_request = binding.request("apply")
apply_request["tag_oid"] = tag_oid
applied = run_adapter(command, apply_request)
require_exact_response(applied, binding)
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") != "VERIFIED":
raise ReconciliationRequired(f"{name} did not verify after apply")
if probe.get("tag_oid") not in (None, tag_oid):
raise ManualOwnerReviewRequired(f"{name} tag object mismatch")
return dict(probe)
def execute(
*,
binding: Binding,
authorization_path: Path,
repository: Path,
state_root: Path,
tag_command: Sequence[str],
deploy_command: Sequence[str],
registry_command: Sequence[str],
baseline_command: Sequence[str],
) -> dict[str, Any]:
authorization = verify_authorization(secure_json(authorization_path), binding)
lineage = verify_git_lineage(repository, binding)
artifact = verify_artifact(binding)
state = DurableState(state_root, binding)
with FileLock(state_root / "executor.lock"):
state.record(
"PREFLIGHT_VERIFIED",
"preflight",
{"authorization": authorization, "lineage": lineage, "artifact": artifact},
)
tag: dict[str, Any] | None = None
runtime: dict[str, Any] | None = None
try:
tag = tag_step(tag_command, binding)
state.record("TAG_VERIFIED", "tag", tag)
runtime = deploy_step(deploy_command, binding)
state.record("RUNTIME_VERIFIED", "runtime", runtime)
registry = finalize_step(
registry_command,
binding,
name="registry",
tag_oid=tag["tag_oid"],
)
state.record("REGISTRY_VERIFIED", "registry", registry)
baseline = finalize_step(
baseline_command,
binding,
name="baseline",
tag_oid=tag["tag_oid"],
)
state.record("BASELINE_VERIFIED", "baseline", baseline)
# Final readback: no mutation, all four planes must still agree.
final_tag = tag_step(tag_command, binding)
final_runtime = run_adapter(deploy_command, binding.request("probe"))
final_registry_request = binding.request("probe")
final_registry_request["tag_oid"] = tag["tag_oid"]
final_registry = run_adapter(registry_command, final_registry_request)
final_baseline_request = binding.request("probe")
final_baseline_request["tag_oid"] = tag["tag_oid"]
final_baseline = run_adapter(baseline_command, final_baseline_request)
for value in (final_runtime, final_registry, final_baseline):
require_exact_response(value, binding)
if final_runtime.get("status") != "ACTIVE_VERIFIED":
raise ReconciliationRequired("final runtime readback failed")
if final_registry.get("status") != "VERIFIED":
raise ReconciliationRequired("final Registry readback failed")
if final_baseline.get("status") != "VERIFIED":
raise ReconciliationRequired("final Baseline readback failed")
final = {
"status": "PRODUCTION_ACCEPTED",
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
"tag_oid": tag["tag_oid"],
"tag": final_tag,
"runtime": final_runtime,
"registry": final_registry,
"baseline": final_baseline,
"force_push_used": False,
"tag_rewritten": False,
"direct_main_push": False,
"secrets_committed": False,
}
state.record("COMPLETED", "final_consistency", final)
return final
except ManualOwnerReviewRequired as exc:
state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
raise
except ReconciliationRequired as exc:
state.fail("RECONCILIATION_REQUIRED", str(exc))
raise
except Exception as exc:
# Once a formal tag has been verified, no later error is a clean
# pre-side-effect failure. Preserve the tag and require idempotent
# reconciliation rather than pretending the release never started.
if tag is not None:
reason = f"{type(exc).__name__}: {exc}"
state.fail("RECONCILIATION_REQUIRED", reason)
raise ReconciliationRequired(reason) from exc
state.fail("FAIL_CLOSED", f"{type(exc).__name__}: {exc}")
raise
def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("--authorization", required=True, type=Path)
p.add_argument("--repository", required=True, type=Path)
p.add_argument("--state-root", required=True, type=Path)
p.add_argument("--artifact", required=True, type=Path)
p.add_argument("--artifact-size", required=True, type=int)
p.add_argument("--version", required=True)
p.add_argument("--target-oid", required=True)
p.add_argument("--evidence-closure-oid", required=True)
p.add_argument("--artifact-sha256", required=True)
p.add_argument("--approval-plan-id", required=True)
p.add_argument("--production-service", required=True)
p.add_argument("--production-port", required=True, type=int)
p.add_argument("--repository-identity", default="aios/aios")
p.add_argument("--tag-adapter", required=True, nargs="+")
p.add_argument("--deploy-adapter", required=True, nargs="+")
p.add_argument("--registry-adapter", required=True, nargs="+")
p.add_argument("--baseline-adapter", required=True, nargs="+")
return p
def main() -> int:
args = parser().parse_args()
binding = Binding(
version=args.version,
target_oid=args.target_oid,
evidence_closure_oid=args.evidence_closure_oid,
artifact_sha256=args.artifact_sha256,
approval_plan_id=args.approval_plan_id,
artifact_path=str(args.artifact),
artifact_size=args.artifact_size,
production_service=args.production_service,
production_port=args.production_port,
repository_identity=args.repository_identity,
)
try:
result = execute(
binding=binding,
authorization_path=args.authorization,
repository=args.repository,
state_root=args.state_root,
tag_command=parse_command(args.tag_adapter),
deploy_command=parse_command(args.deploy_adapter),
registry_command=parse_command(args.registry_adapter),
baseline_command=parse_command(args.baseline_adapter),
)
print(json.dumps(result, sort_keys=True))
return 0
except ManualOwnerReviewRequired as exc:
print(json.dumps({"status": "MANUAL_OWNER_REVIEW_REQUIRED", "reason": str(exc)}, sort_keys=True))
return 20
except ReconciliationRequired as exc:
print(json.dumps({"status": "RECONCILIATION_REQUIRED", "reason": str(exc)}, sort_keys=True))
return 21
except Exception as exc:
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
return 22
if __name__ == "__main__":
raise SystemExit(main())
-418
View File
@@ -1,418 +0,0 @@
"""Fail-closed access to the authoritative bare Git repository."""
from __future__ import annotations
import os
import re
import subprocess
import time
import urllib.parse
from pathlib import Path
from typing import Iterable
_ALLOWED_COMMANDS = frozenset(
{
"cat-file",
"config",
"diff",
"for-each-ref",
"ls-remote",
"merge-base",
"push",
"remote",
"rev-parse",
"show-ref",
"tag",
}
)
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_MAX_OUTPUT = 1_048_576
_TIMEOUT = 8.0
class AuthoritativeGitError(RuntimeError):
"""A fail-closed authoritative repository or ref query error."""
class AuthoritativeGitNotFound(AuthoritativeGitError):
"""A validated revision does not exist in the authoritative repository."""
class AuthoritativeGitOutputTooLarge(AuthoritativeGitError):
"""A controlled Git query exceeded the response-size boundary."""
def _redact(text: str) -> str:
value = str(text or "")
value = re.sub(r"(?i)(authorization\s*[:=]\s*)(?:bearer|token)\s+\S+", r"\1REDACTED", value)
value = re.sub(r"(?i)((?:access_)?token\s*[:=]\s*)\S+", r"\1REDACTED", value)
value = re.sub(r"(?i)(https?://[^/\s:@]+:)[^@\s/]+@", r"\1REDACTED@", value)
return value
def _configured_path(git_dir: str | Path | None = None) -> Path:
raw = str(git_dir or os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")).strip()
if not raw:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
path = Path(raw)
if not path.is_absolute() or ".." in path.parts:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
try:
resolved = path.resolve(strict=True)
except OSError as exc:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE") from exc
if not resolved.is_dir():
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
return resolved
def _run(
git_dir: Path,
args: list[str],
*,
check: bool,
timeout: float = _TIMEOUT,
) -> subprocess.CompletedProcess[str]:
if not args or args[0] not in _ALLOWED_COMMANDS:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
try:
result = subprocess.run(
["git", "--git-dir", str(git_dir), *args],
text=True,
capture_output=True,
timeout=timeout,
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED") from exc
if len(result.stdout) + len(result.stderr) > _MAX_OUTPUT:
raise AuthoritativeGitOutputTooLarge("GIT_OUTPUT_LIMIT_EXCEEDED")
if check and result.returncode != 0:
detail = _redact(result.stderr.strip() or result.stdout.strip())
raise AuthoritativeGitError(
"GIT_REF_QUERY_FAILED" + (f": {detail[:160]}" if detail else "")
)
return result
def _repository_identity_from_remote(url: str) -> str:
value = str(url or "").strip().split("?", 1)[0].rstrip("/")
if not value:
return ""
if "://" in value:
value = urllib.parse.urlparse(value).path
elif ":" in value:
value = value.rsplit(":", 1)[1]
value = value.rsplit("@", 1)[-1].strip("/")
value = re.sub(r"\.git$", "", value, flags=re.IGNORECASE)
parts = [part for part in value.split("/") if part]
if len(parts) >= 2:
value = "/".join(parts[-2:])
return value.strip("/").lower()
def verify_repository(
git_dir: str | Path | None = None,
repository_identity: str | None = None,
) -> tuple[Path, str]:
path = _configured_path(git_dir)
expected = str(
repository_identity
or os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
).strip().strip("/")
if not expected or expected.count("/") != 1:
raise AuthoritativeGitError("REPOSITORY_IDENTITY_MISMATCH")
bare = _run(path, ["rev-parse", "--is-bare-repository"], check=True)
if bare.stdout.strip().lower() != "true":
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
remote = _run(path, ["config", "--get", "remote.gitea.url"], check=False)
actual = _repository_identity_from_remote(remote.stdout)
if actual != expected.lower():
raise AuthoritativeGitError("REPOSITORY_IDENTITY_MISMATCH")
return path, expected
def run_authoritative_git(
args: list[str],
*,
check: bool = True,
git_dir: str | Path | None = None,
timeout: float = _TIMEOUT,
) -> subprocess.CompletedProcess[str]:
path, _ = verify_repository(git_dir=git_dir)
return _run(path, list(args), check=check, timeout=timeout)
def authoritative_head_oid(
*,
git_dir: str | Path | None = None,
) -> str:
result = run_authoritative_git(["rev-parse", "--verify", "refs/heads/main"], git_dir=git_dir)
oid = result.stdout.strip().lower()
if not _OID_RE.fullmatch(oid):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
return oid
def authoritative_tags(
*,
git_dir: str | Path | None = None,
) -> list[dict[str, str | None]]:
result = run_authoritative_git(
[
"for-each-ref",
"refs/tags",
"--format=%(refname:strip=2)\t%(objectname)\t%(objecttype)",
],
git_dir=git_dir,
)
tags: list[dict[str, str | None]] = []
for line in result.stdout.splitlines():
name, oid, object_type = (line.split("\t", 2) + ["", ""])[:3]
if not name or not _OID_RE.fullmatch(oid.lower()):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
peeled_result = run_authoritative_git(
["rev-parse", "--verify", f"refs/tags/{name}^{{commit}}"],
git_dir=git_dir,
)
peeled = peeled_result.stdout.strip().lower()
if not _OID_RE.fullmatch(peeled):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
tags.append(
{
"name": name,
"tag_oid": oid.lower(),
"object_type": object_type,
"peeled_target_oid": peeled,
}
)
return tags
def authoritative_branches(
*,
git_dir: str | Path | None = None,
) -> list[dict[str, str]]:
result = run_authoritative_git(
[
"for-each-ref",
"refs/heads",
"--format=%(refname:strip=2)\t%(objectname)",
],
git_dir=git_dir,
)
branches: list[dict[str, str]] = []
for line in result.stdout.splitlines():
name, oid = (line.split("\t", 1) + [""])[:2]
if not name or not _OID_RE.fullmatch(oid.lower()):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
branches.append({"name": name, "oid": oid.lower()})
return branches
def authoritative_commit(
oid: str,
*,
git_dir: str | Path | None = None,
) -> str:
value = str(oid or "").strip().lower()
if not _OID_RE.fullmatch(value):
raise ValueError("commit OID must be a full 40-character value")
result = run_authoritative_git(
["rev-parse", "--verify", f"{value}^{{commit}}"],
git_dir=git_dir,
)
resolved = result.stdout.strip().lower()
if resolved != value:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
return resolved
def authoritative_lineage(
ancestor: str,
descendant: str,
*,
git_dir: str | Path | None = None,
) -> None:
authoritative_commit(ancestor, git_dir=git_dir)
authoritative_commit(descendant, git_dir=git_dir)
result = run_authoritative_git(
["merge-base", "--is-ancestor", ancestor, descendant],
check=False,
git_dir=git_dir,
)
if result.returncode != 0:
raise AuthoritativeGitError("evidence lineage mismatch")
_SAFE_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,239}$")
def _safe_ref_name(value: str) -> bool:
return bool(
_SAFE_REF_RE.fullmatch(value)
and ".." not in value
and "//" not in value
and "@{" not in value
and not value.endswith(("/", ".", ".lock"))
and not any(part in {"", ".", ".."} for part in value.split("/"))
)
def validate_git_revision(
value: str,
field: str,
*,
git_dir: str | Path | None = None,
) -> str:
revision = str(value or "").strip()
if not revision or revision != str(value or "") or revision.startswith("-"):
raise ValueError(f"invalid {field} revision")
if _OID_RE.fullmatch(revision.lower()):
try:
return authoritative_commit(revision.lower(), git_dir=git_dir)
except AuthoritativeGitError as exc:
raise AuthoritativeGitNotFound(f"{field} revision not found") from exc
if not _safe_ref_name(revision) or any(token in revision for token in ("^", "~", ":", "\\")):
raise ValueError(f"invalid {field} revision")
if revision.startswith("refs/"):
if not revision.startswith(("refs/heads/", "refs/tags/")):
raise ValueError(f"invalid {field} revision")
candidates = [revision]
else:
candidates = [f"refs/heads/{revision}", f"refs/tags/{revision}"]
matches: list[str] = []
for candidate in candidates:
result = run_authoritative_git(
["rev-parse", "--verify", f"{candidate}^{{commit}}"],
check=False,
git_dir=git_dir,
)
oid = result.stdout.strip().lower()
if result.returncode == 0 and _OID_RE.fullmatch(oid):
matches.append(oid)
if not matches:
raise AuthoritativeGitNotFound(f"{field} revision not found")
if len(set(matches)) != 1 or len(matches) != 1:
raise ValueError(f"ambiguous {field} revision")
return matches[0]
def authoritative_diff(
from_revision: str,
to_revision: str,
*,
git_dir: str | Path | None = None,
) -> dict[str, object]:
path, _ = verify_repository(git_dir=git_dir)
from_oid = validate_git_revision(from_revision, "from", git_dir=path)
to_oid = validate_git_revision(to_revision, "to", git_dir=path)
args = ["diff", "--no-ext-diff", "--no-textconv", from_oid, to_oid]
patch_limit = max(1, min(_MAX_OUTPUT, 256 * 1024))
process = subprocess.Popen(
["git", "--git-dir", str(path), *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
)
chunks: list[bytes] = []
stored_bytes = 0
total_bytes = 0
started = time.monotonic()
try:
while process.stdout is not None:
chunk = process.stdout.read(64 * 1024)
if not chunk:
break
total_bytes += len(chunk)
if stored_bytes < patch_limit:
remaining = patch_limit - stored_bytes
selected = chunk[:remaining]
chunks.append(selected)
stored_bytes += len(selected)
if time.monotonic() - started > _TIMEOUT:
process.kill()
raise AuthoritativeGitError("GIT_REF_QUERY_TIMEOUT")
stderr = (
process.stderr.read(8192).decode("utf-8", errors="replace")
if process.stderr is not None
else ""
)
returncode = process.wait(timeout=max(0.1, _TIMEOUT - (time.monotonic() - started)))
except (OSError, subprocess.TimeoutExpired) as exc:
process.kill()
process.wait()
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED") from exc
if returncode != 0:
raise AuthoritativeGitError(_redact(stderr.strip() or "GIT_REF_QUERY_FAILED")[:200])
try:
names_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--name-status", from_oid, to_oid],
git_dir=path,
)
changed_paths = [
line.split("\t", 1)[-1]
for line in names_result.stdout.splitlines()
if line.strip()
]
except AuthoritativeGitOutputTooLarge:
changed_paths = []
try:
stats_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--stat", from_oid, to_oid],
git_dir=path,
)
stats = stats_result.stdout.strip()
except AuthoritativeGitOutputTooLarge:
stats = "GIT_STAT_OUTPUT_TRUNCATED"
patch = b"".join(chunks).decode("utf-8", errors="replace")
return {
"code": 0,
"stdout": patch,
"stderr": "",
"cmd": ["git", f"--git-dir={path}", *args],
"from_oid": from_oid,
"to_oid": to_oid,
"changed_paths": changed_paths,
"stats": stats,
"total_size_bytes": total_bytes,
"truncated": total_bytes > patch_limit,
"patch_limit_bytes": patch_limit,
}
def repository_snapshot(
*,
git_dir: str | Path | None = None,
repository_identity: str | None = None,
) -> dict[str, object]:
configured = bool(
str(git_dir or os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")).strip()
)
try:
path, identity = verify_repository(
git_dir=git_dir,
repository_identity=repository_identity,
)
head = authoritative_head_oid(git_dir=path)
return {
"repository_available": True,
"authoritative_git_dir_configured": configured,
"authoritative_git_dir_readable": True,
"authoritative_git_dir": str(path),
"authoritative_head_oid": head,
"repository_identity": identity,
"query_error": None,
}
except (AuthoritativeGitError, ValueError) as exc:
return {
"repository_available": False,
"authoritative_git_dir_configured": configured,
"authoritative_git_dir_readable": False,
"authoritative_git_dir": None,
"authoritative_head_oid": None,
"repository_identity": str(
repository_identity
or os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
).strip() or None,
"query_error": _redact(str(exc)),
}
File diff suppressed because it is too large Load Diff
@@ -1,89 +0,0 @@
"""Durable Owner/GPT notification outbox with retry and acknowledgement."""
from __future__ import annotations
import json
import sqlite3
import time
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Mapping
@dataclass(frozen=True)
class OutboxEvent:
event_id: str
recipient: str
kind: str
payload: Mapping[str, Any]
attempts: int
available_at: float
acknowledged: bool
class DurableOutbox:
RECIPIENTS = ("owner", "gpt")
def __init__(self, path: str | Path) -> None:
self.path = str(path)
self.db = sqlite3.connect(self.path)
self.db.execute("PRAGMA journal_mode=WAL")
self.db.execute("CREATE TABLE IF NOT EXISTS outbox (event_id TEXT PRIMARY KEY, recipient TEXT NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, available_at REAL NOT NULL, delivered_at REAL, acknowledged INTEGER NOT NULL DEFAULT 0, last_error TEXT)")
self.db.commit()
def enqueue(self, kind: str, payload: Mapping[str, Any], *, event_id: str | None = None, recipients: tuple[str, ...] = RECIPIENTS) -> list[str]:
ids: list[str] = []
for recipient in recipients:
if recipient not in self.RECIPIENTS:
raise ValueError("recipient is outside the governed notification plane")
eid = event_id or str(uuid.uuid4())
if event_id is not None and len(recipients) > 1:
eid = f"{event_id}:{recipient}"
self.db.execute("INSERT OR IGNORE INTO outbox(event_id,recipient,kind,payload,available_at) VALUES(?,?,?,?,?)", (eid, recipient, kind, json.dumps(dict(payload), sort_keys=True), time.time()))
ids.append(eid)
self.db.commit()
return ids
def due(self, *, now: float | None = None, limit: int = 20) -> list[OutboxEvent]:
now = time.time() if now is None else now
rows = self.db.execute("SELECT event_id,recipient,kind,payload,attempts,available_at,acknowledged FROM outbox WHERE delivered_at IS NULL AND available_at<=? ORDER BY available_at,event_id LIMIT ?", (now, limit)).fetchall()
return [OutboxEvent(r[0], r[1], r[2], json.loads(r[3]), r[4], r[5], bool(r[6])) for r in rows]
def delivered(self, event_id: str, *, now: float | None = None) -> None:
self.db.execute("UPDATE outbox SET delivered_at=? WHERE event_id=?", (time.time() if now is None else now, event_id))
self.db.commit()
def failed(self, event_id: str, error: str, *, now: float | None = None, max_backoff: float = 300.0) -> None:
now = time.time() if now is None else now
row = self.db.execute("SELECT attempts FROM outbox WHERE event_id=?", (event_id,)).fetchone()
attempts = int(row[0]) + 1 if row else 1
delay = min(max_backoff, 2 ** min(attempts, 8))
self.db.execute("UPDATE outbox SET attempts=?,available_at=?,last_error=? WHERE event_id=?", (attempts, now + delay, error[:200], event_id))
self.db.commit()
def acknowledge(self, event_id: str) -> None:
self.db.execute("UPDATE outbox SET acknowledged=1 WHERE event_id=?", (event_id,))
self.db.commit()
def close(self) -> None:
self.db.close()
class NotificationPlane:
def __init__(self, outbox: DurableOutbox) -> None:
self.outbox = outbox
def emit(self, kind: str, payload: Mapping[str, Any], *, event_id: str | None = None) -> list[str]:
return self.outbox.enqueue(kind, payload, event_id=event_id)
def deliver_once(self, send: Callable[[OutboxEvent], None], *, now: float | None = None) -> list[str]:
delivered: list[str] = []
for event in self.outbox.due(now=now):
try:
send(event)
except Exception as exc:
self.outbox.failed(event.event_id, type(exc).__name__, now=now)
else:
self.outbox.delivered(event.event_id, now=now)
delivered.append(event.event_id)
return delivered
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""Single canonical Production Baseline authority for the GitOps reader.
The API and the one-shot writer import this module. Workspace and release
copies are projections only; they are never selected as a fallback source.
"""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
from typing import Any
AUTHORITY_PATH = Path(os.getenv(
"AIOS_GITOPS_BASELINE_AUTHORITY",
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
))
AUTHORITY_SHA_PATH = AUTHORITY_PATH.with_suffix(".sha256")
class BaselineAuthorityError(RuntimeError):
"""Fail-closed error raised for an unavailable or inconsistent authority."""
class BaselineAuthority:
def __init__(self, path: Path | str = AUTHORITY_PATH):
self.path = Path(path)
self.sha_path = self.path.with_suffix(".sha256")
def _raw(self) -> bytes:
try:
raw = self.path.read_bytes()
except OSError as exc:
raise BaselineAuthorityError(f"authority_unavailable:{exc}") from exc
if not raw:
raise BaselineAuthorityError("authority_empty")
return raw
def read(self) -> dict[str, Any]:
raw = self._raw()
digest = hashlib.sha256(raw).hexdigest()
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BaselineAuthorityError("authority_invalid_json") from exc
if not isinstance(value, dict):
raise BaselineAuthorityError("authority_not_object")
expected = str(value.get("authority_sha256", ""))
if expected and expected != digest:
raise BaselineAuthorityError("authority_embedded_digest_mismatch")
if self.sha_path.exists():
sidecar = self.sha_path.read_text(encoding="utf-8").strip().split()[0]
if sidecar != digest:
raise BaselineAuthorityError("authority_sidecar_digest_mismatch")
required = {
"current_baseline", "previous_baseline", "target_oid", "tag_oid",
"evidence_closure_oid", "approval_plan_id", "status", "generation",
"source_identity",
}
missing = sorted(required.difference(value))
if missing:
raise BaselineAuthorityError("authority_missing:" + ",".join(missing))
value = dict(value)
value["authority_sha256"] = digest
value["authority_path"] = str(self.path)
return value
def read_consistent(self, policy: dict[str, Any] | None = None) -> dict[str, Any]:
value = self.read()
if policy:
expected = policy.get("release_binding", policy)
for key, field in (("version", "current_baseline"),
("previous_version", "previous_baseline"),
("target_oid", "target_oid"),
("tag_oid", "tag_oid"),
("evidence_closure_oid", "evidence_closure_oid"),
("approval_plan_id", "approval_plan_id")):
wanted = expected.get(key)
if wanted is not None and value.get(field) != wanted:
raise BaselineAuthorityError(f"authority_binding_mismatch:{field}")
return value
-491
View File
@@ -1,491 +0,0 @@
"""Fail-closed, branch-bound repair leases for governed AIOS GitOps work."""
from __future__ import annotations
import hashlib
import json
import os
import posixpath
import re
import secrets
import subprocess
import tempfile
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterable, Mapping, Protocol, Sequence
TASK_ID = "V2R1C6_GOVERNED_LOCAL_BOOTSTRAP"
MAX_LEASE_SECONDS = 86_400
OID_RE = re.compile(r"^[0-9a-fA-F]{40}$")
DENIED_BRANCHES = {"main", "master", "HEAD"}
PROTECTED_PREFIXES = (
".git",
"VERSION",
"versions/V2R1C5.json",
"artifacts/V2R1C5",
"baseline",
"config/gitops.tokens",
"gitops.tokens",
"secrets",
"credentials",
)
SECRET_PATTERNS = (
re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*\S+"),
re.compile(r"\b(?:sk|HPUA|SDC)-[A-Za-z0-9_-]{16,}\b"),
)
class LeaseError(Exception):
"""Raised for any invalid, stale, expired, or prohibited lease action."""
class GitProbeProtocol(Protocol):
"""Read-only Git seam used by the lease manager."""
def branch_head(self, branch: str) -> str: ...
def parent(self, commit: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def changed_paths(self, commit: str) -> tuple[str, ...]: ...
def commits_between(self, older: str, newer: str) -> tuple[str, ...]: ...
def _now() -> datetime:
return datetime.now(timezone.utc)
def _ts(value: datetime) -> str:
return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _parse_ts(value: str) -> datetime:
if value.endswith("Z"):
value = value[:-1] + "+00:00"
parsed = datetime.fromisoformat(value)
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
def _oid(value: str, field: str = "oid") -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise LeaseError(f"{field} must be a full commit OID")
return value.lower()
def normalize_path(path: str) -> str:
"""Normalize a repository-relative path without resolving through the FS."""
if not isinstance(path, str) or not path.strip():
raise LeaseError("path must be a non-empty string")
value = path.replace("\\", "/").strip()
if value.startswith("/") or "\x00" in value:
raise LeaseError("path must be relative")
parts = value.split("/")
if any(part in {"", ".", ".."} for part in parts):
raise LeaseError("path contains a prohibited component")
normalized = posixpath.normpath(value)
if normalized == "." or normalized.startswith("../"):
raise LeaseError("path is not normalized")
return normalized
def path_is_protected(path: str) -> bool:
normalized = normalize_path(path)
lower = normalized.lower()
return any(
lower == prefix.lower() or lower.startswith(prefix.lower().rstrip("/") + "/")
for prefix in PROTECTED_PREFIXES
) or lower.startswith("refs/")
def contains_secret_like_content(content: str) -> bool:
return any(pattern.search(content or "") for pattern in SECRET_PATTERNS)
def validate_paths(paths: Iterable[str], allowed_paths: Sequence[str]) -> tuple[str, ...]:
allowed = tuple(normalize_path(path) for path in allowed_paths)
if not allowed:
raise LeaseError("allowed_paths must not be empty")
result: list[str] = []
for raw in paths:
path = normalize_path(raw)
if path_is_protected(path):
raise LeaseError("protected path is not lease-writable")
if not any(path == prefix or path.startswith(prefix + "/") for prefix in allowed):
raise LeaseError("path is outside the lease allowlist")
if path not in result:
result.append(path)
if not result:
raise LeaseError("at least one changed path is required")
return tuple(result)
@dataclass
class CommitEntry:
commit_oid: str
parent_oid: str
paths: tuple[str, ...]
recorded_at: str
@dataclass
class RepairLease:
lease_id: str
task_id: str
owner_identity: str
branch: str
description: str
allowed_paths: tuple[str, ...]
initial_head_oid: str
current_head_oid: str
created_at: str
expires_at: str
max_expires_at: str
state: str = "ACTIVE"
revision: int = 0
commits: list[CommitEntry] = field(default_factory=list)
attached: bool = False
def to_json(self) -> dict[str, Any]:
payload = asdict(self)
payload["allowed_paths"] = list(self.allowed_paths)
payload["commits"] = [asdict(entry) for entry in self.commits]
return payload
@classmethod
def from_json(cls, raw: dict[str, Any]) -> "RepairLease":
commits = tuple(
CommitEntry(
commit_oid=_oid(item["commit_oid"], "commit_oid"),
parent_oid=_oid(item["parent_oid"], "parent_oid"),
paths=tuple(normalize_path(path) for path in item["paths"]),
recorded_at=str(item["recorded_at"]),
)
for item in raw.get("commits", [])
)
return cls(
lease_id=str(raw["lease_id"]),
task_id=str(raw["task_id"]),
owner_identity=str(raw["owner_identity"]),
branch=str(raw["branch"]),
description=str(raw["description"]),
allowed_paths=tuple(normalize_path(path) for path in raw["allowed_paths"]),
initial_head_oid=_oid(raw["initial_head_oid"], "initial_head_oid"),
current_head_oid=_oid(raw["current_head_oid"], "current_head_oid"),
created_at=str(raw["created_at"]),
expires_at=str(raw["expires_at"]),
max_expires_at=str(raw["max_expires_at"]),
state=str(raw.get("state", "ACTIVE")),
revision=int(raw.get("revision", 0)),
commits=list(commits),
attached=bool(raw.get("attached", False)),
)
class LeaseStore:
"""Atomic JSON store for restart-recoverable lease state."""
def __init__(self, directory: str | os.PathLike[str]) -> None:
self.directory = Path(directory)
self.directory.mkdir(parents=True, exist_ok=True)
if self.directory.is_symlink():
raise LeaseError("lease store must not be a symlink")
os.chmod(self.directory, 0o700)
def _path(self, lease_id: str) -> Path:
if not re.fullmatch(r"lease-[0-9a-f]{32}", lease_id):
raise LeaseError("invalid lease id")
return self.directory / f"{lease_id}.json"
def create(self, lease: RepairLease) -> None:
path = self._path(lease.lease_id)
if path.exists() or path.is_symlink():
raise LeaseError("lease already exists")
self._write(path, lease.to_json(), exclusive=True)
def load(self, lease_id: str) -> RepairLease:
path = self._path(lease_id)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError, TypeError) as exc:
raise LeaseError("lease state is unavailable or invalid") from exc
if not isinstance(raw, dict):
raise LeaseError("lease state must be an object")
return RepairLease.from_json(raw)
def save(self, lease: RepairLease) -> None:
self._write(self._path(lease.lease_id), lease.to_json())
@staticmethod
def _write(path: Path, payload: dict[str, Any], *, exclusive: bool = False) -> None:
flags = os.O_WRONLY | os.O_CREAT | (os.O_EXCL if exclusive else 0)
fd, temp_name = tempfile.mkstemp(prefix=".lease-", dir=path.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
json.dump(payload, stream, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
stream.write("\n")
if exclusive:
os.link(temp_name, path)
os.unlink(temp_name)
else:
os.replace(temp_name, path)
os.chmod(path, 0o600)
except Exception:
try:
os.unlink(temp_name)
except FileNotFoundError:
pass
raise
class GitProbe:
"""Read-only Git implementation; it never changes a ref or working tree."""
def __init__(self, bare_repo: str | os.PathLike[str]) -> None:
self.bare_repo = Path(bare_repo)
def _run(self, args: list[str]) -> str:
result = subprocess.run(
["git", "--git-dir", str(self.bare_repo), *args],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise LeaseError("read-only Git probe failed")
return result.stdout.strip()
def branch_head(self, branch: str) -> str:
return _oid(self._run(["rev-parse", f"refs/heads/{branch}"]), "branch_head")
def parent(self, commit: str) -> str:
return _oid(self._run(["rev-parse", f"{_oid(commit)}^1"]), "parent")
def is_ancestor(self, older: str, newer: str) -> bool:
old, new = _oid(older), _oid(newer)
result = subprocess.run(
["git", "--git-dir", str(self.bare_repo), "merge-base", "--is-ancestor", old, new],
check=False,
)
return result.returncode == 0
def changed_paths(self, commit: str) -> tuple[str, ...]:
raw = self._run(["diff-tree", "--no-commit-id", "--name-only", "-r", _oid(commit)])
return tuple(normalize_path(line) for line in raw.splitlines() if line.strip())
def commits_between(self, older: str, newer: str) -> tuple[str, ...]:
raw = self._run(["rev-list", "--reverse", f"{_oid(older)}..{_oid(newer)}"])
return tuple(_oid(line, "commit") for line in raw.splitlines() if line.strip())
class RepairLeaseManager:
"""Issue, recover, renew, and close branch-bound repair leases."""
def __init__(self, store: LeaseStore, git: GitProbeProtocol, *, clock=_now) -> None:
self.store = store
self.git = git
self.clock = clock
def _require_owner(self, lease: RepairLease, owner_identity: str) -> None:
if owner_identity != lease.owner_identity:
raise LeaseError("lease identity mismatch")
def _active(self, lease: RepairLease) -> RepairLease:
now = self.clock()
if lease.state != "ACTIVE":
raise LeaseError(f"lease is {lease.state.lower()}")
if now >= _parse_ts(lease.expires_at):
lease.state = "EXPIRED"
self.store.save(lease)
raise LeaseError("lease expired")
return lease
def issue(
self,
*,
task_id: str,
owner_identity: str,
branch: str,
description: str,
allowed_paths: Sequence[str],
ttl_seconds: int = 3600,
) -> RepairLease:
if task_id != TASK_ID:
raise LeaseError("unexpected task id")
if not owner_identity or not description or len(description) > 240:
raise LeaseError("owner identity and description are required")
if "\n" in description or "\r" in description:
raise LeaseError("description must be a single line")
if branch in DENIED_BRANCHES or branch.startswith(("main/", "refs/", "tags/")):
raise LeaseError("protected branch cannot receive a repair lease")
if not isinstance(ttl_seconds, int) or not 0 < ttl_seconds <= MAX_LEASE_SECONDS:
raise LeaseError("lease TTL exceeds the maximum")
allowed = tuple(normalize_path(path) for path in allowed_paths)
if any(path_is_protected(path) for path in allowed):
raise LeaseError("protected path in lease allowlist")
head = self.git.branch_head(branch)
created = self.clock()
expires = created + timedelta(seconds=ttl_seconds)
max_expires = created + timedelta(seconds=MAX_LEASE_SECONDS)
lease_id = "lease-" + secrets.token_hex(16)
lease = RepairLease(
lease_id=lease_id,
task_id=task_id,
owner_identity=owner_identity,
branch=branch,
description=description,
allowed_paths=allowed,
initial_head_oid=head,
current_head_oid=head,
created_at=_ts(created),
expires_at=_ts(expires),
max_expires_at=_ts(max_expires),
)
self.store.create(lease)
return lease
def attach_existing_branch(
self, lease_id: str, *, owner_identity: str, expected_head_oid: str
) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
current = self.git.branch_head(lease.branch)
expected = _oid(expected_head_oid, "expected_head_oid")
if current != expected or current != lease.current_head_oid:
raise LeaseError("branch head does not match the lease")
lease.attached = True
lease.revision += 1
self.store.save(lease)
return lease
def authorize_commit(
self,
lease_id: str,
*,
owner_identity: str,
expected_head_oid: str,
paths: Sequence[str],
content: Mapping[str, str] | None = None,
) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
expected = _oid(expected_head_oid, "expected_head_oid")
current = self.git.branch_head(lease.branch)
if current != expected or current != lease.current_head_oid:
raise LeaseError("stale or externally advanced branch head")
if not lease.attached:
raise LeaseError("lease is not attached")
validate_paths(paths, lease.allowed_paths)
if content and any(contains_secret_like_content(value) for value in content.values()):
raise LeaseError("secret-like content rejected")
return lease
def record_commit(
self,
lease_id: str,
*,
owner_identity: str,
expected_head_oid: str,
new_head_oid: str,
paths: Sequence[str],
) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
new_head = _oid(new_head_oid, "new_head_oid")
expected = _oid(expected_head_oid, "expected_head_oid")
if not lease.attached or lease.current_head_oid != expected:
raise LeaseError("stale lease head")
if self.git.branch_head(lease.branch) != new_head:
raise LeaseError("branch head does not match committed head")
validate_paths(paths, lease.allowed_paths)
if self.git.parent(new_head) != expected or not self.git.is_ancestor(expected, new_head):
raise LeaseError("commit is not a fast-forward child of the expected head")
actual_paths = self.git.changed_paths(new_head)
allowed = validate_paths(actual_paths, lease.allowed_paths)
requested = set(validate_paths(paths, lease.allowed_paths))
if not set(allowed).issubset(requested):
raise LeaseError("commit changed an unrequested path")
lease.current_head_oid = new_head
lease.revision += 1
lease.commits.append(
CommitEntry(
commit_oid=new_head,
parent_oid=expected,
paths=allowed,
recorded_at=_ts(self.clock()),
)
)
self.store.save(lease)
return lease
def recover(self, lease_id: str, *, owner_identity: str) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
current = self.git.branch_head(lease.branch)
if current == lease.current_head_oid:
return lease
if not self.git.is_ancestor(lease.current_head_oid, current):
raise LeaseError("branch moved non-fast-forward")
commits = self.git.commits_between(lease.current_head_oid, current)
for commit in commits:
actual = self.git.changed_paths(commit)
validate_paths(actual, lease.allowed_paths)
lease.current_head_oid = current
lease.revision += 1
self.store.save(lease)
return lease
def renew(self, lease_id: str, *, owner_identity: str, ttl_seconds: int) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
if not isinstance(ttl_seconds, int) or ttl_seconds <= 0:
raise LeaseError("renewal TTL must be positive")
candidate = self.clock() + timedelta(seconds=ttl_seconds)
maximum = _parse_ts(lease.max_expires_at)
if candidate > maximum:
raise LeaseError("renewal exceeds maximum lease lifetime")
lease.expires_at = _ts(candidate)
lease.revision += 1
self.store.save(lease)
return lease
def complete(self, lease_id: str, *, owner_identity: str) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
lease.state = "COMPLETED"
lease.revision += 1
self.store.save(lease)
return lease
def revoke(self, lease_id: str, *, owner_identity: str) -> RepairLease:
lease = self._active(self.store.load(lease_id))
self._require_owner(lease, owner_identity)
lease.state = "REVOKED"
lease.revision += 1
self.store.save(lease)
return lease
def sanitized_lease_view(lease: RepairLease) -> dict[str, Any]:
"""Return lease metadata only; no content, token, or credential values."""
return {
"lease_id": lease.lease_id,
"task_id": lease.task_id,
"owner_identity": lease.owner_identity,
"branch": lease.branch,
"description": lease.description,
"initial_head_oid": lease.initial_head_oid,
"current_head_oid": lease.current_head_oid,
"created_at": lease.created_at,
"expires_at": lease.expires_at,
"state": lease.state,
"revision": lease.revision,
"commit_count": len(lease.commits),
"attached": lease.attached,
}
-108
View File
@@ -1,108 +0,0 @@
"""Atomic, predicate-bound activation of the durable release campaign."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
from typing import Any, Mapping
from release_campaign_authorization import CampaignError, validate_policy
OID_RE = re.compile(r"^[0-9a-f]{40}$")
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
class CampaignActivationError(CampaignError):
pass
def canonical_json(value: Any) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode("utf-8")
def digest_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def build_active_policy(current: Mapping[str, Any], facts: Mapping[str, Any]) -> dict[str, Any]:
policy = validate_policy(dict(current))
required = {
"campaign_id",
"repository",
"source_main_oid",
"gitea_main_oid",
"authority_main_oid",
"runtime_source_oid",
"durable_job_state",
"writer_ready",
"gitea_machine_authenticated",
"governance_executor_ready",
"preapproval_publisher_ready",
"runtime_exact_main",
"main_lineage_verified",
"campaign_revoked",
}
if set(facts) != required:
raise CampaignActivationError("ACTIVATION_FACTS_SCHEMA_INVALID")
if policy["activation_state"] != "PENDING_CONTROL_PLANE_MERGE" or policy["active"] is not False:
raise CampaignActivationError("CAMPAIGN_NOT_PENDING")
if policy["campaign_id"] != CAMPAIGN_ID or facts["campaign_id"] != CAMPAIGN_ID:
raise CampaignActivationError("CAMPAIGN_ID_MISMATCH")
if facts["repository"] != "aios/aios" or policy["repository"] != facts["repository"]:
raise CampaignActivationError("REPOSITORY_MISMATCH")
for name in ("source_main_oid", "gitea_main_oid", "authority_main_oid", "runtime_source_oid"):
if not isinstance(facts[name], str) or not OID_RE.fullmatch(facts[name]):
raise CampaignActivationError("ACTIVATION_OID_INVALID")
if not (
facts["source_main_oid"]
== facts["gitea_main_oid"]
== facts["authority_main_oid"]
== facts["runtime_source_oid"]
):
raise CampaignActivationError("ACTIVATION_MAIN_DIVERGENCE")
if facts["durable_job_state"] != "COMPLETED":
raise CampaignActivationError("DURABLE_JOB_NOT_COMPLETED")
boolean_fields = (
"writer_ready",
"gitea_machine_authenticated",
"governance_executor_ready",
"preapproval_publisher_ready",
"runtime_exact_main",
"main_lineage_verified",
)
if any(facts[name] is not True for name in boolean_fields) or facts["campaign_revoked"] is not False:
raise CampaignActivationError("ACTIVATION_PREDICATE_FAILED")
activated = dict(policy)
activated["activation_state"] = "ACTIVE"
activated["active"] = True
return validate_policy(activated)
def atomic_write_policy(path: Path, sidecar: Path, receipt_path: Path, policy: Mapping[str, Any], receipt: Mapping[str, Any]) -> str:
if path.is_symlink() or sidecar.is_symlink() or receipt_path.exists() or receipt_path.is_symlink():
raise CampaignActivationError("ACTIVATION_PATH_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
raw = canonical_json(dict(policy))
digest = digest_bytes(raw)
for target, data, mode in (
(path, raw, "wb"),
(sidecar, f"{digest} {path.name}\n".encode("ascii"), "wb"),
(receipt_path, canonical_json(dict(receipt)), "wb"),
):
fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, mode) as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
os.chmod(target, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return digest
@@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""Machine campaign approval gate for protected engineering PRs."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import subprocess
from typing import Any, Mapping
from release_campaign_authorization import CampaignAuthorization, CampaignError
class GateError(RuntimeError):
pass
def git_is_ancestor(repository: Path, ancestor: str, descendant: str) -> bool:
result = subprocess.run(
[
"git",
f"--git-dir={repository}",
"merge-base",
"--is-ancestor",
ancestor,
descendant,
],
check=False,
capture_output=True,
)
if result.returncode == 0:
return True
if result.returncode == 1:
return False
raise GateError("git_ancestor_check_failed")
def evaluate(
campaign: CampaignAuthorization,
facts: Mapping[str, Any],
repository: Path,
) -> dict[str, Any]:
required = {
"repository",
"pr_number",
"base_ref",
"base_oid",
"head_oid",
"required_checks_all_success",
"required_checks_same_head",
"unresolved_conversations",
"mergeable",
"protected_target",
}
if set(facts) != required:
raise GateError("pr_facts_fields_invalid")
if facts["repository"] != "aios/aios":
raise GateError("repository_mismatch")
if facts["base_ref"] != "refs/heads/main" or facts["protected_target"] != "main":
raise GateError("protected_main_required")
if facts["required_checks_all_success"] is not True:
raise GateError("required_checks_not_success")
if facts["required_checks_same_head"] is not True:
raise GateError("required_checks_stale")
if facts["unresolved_conversations"] != 0:
raise GateError("unresolved_conversations")
if facts["mergeable"] is not True:
raise GateError("pr_not_mergeable")
if not git_is_ancestor(repository, campaign.anchor_oid, str(facts["base_oid"])):
raise GateError("base_outside_campaign_lineage")
if not git_is_ancestor(repository, str(facts["base_oid"]), str(facts["head_oid"])):
raise GateError("head_outside_pr_lineage")
campaign.assert_operation("protected_merge")
return {
"schema": "aios.campaign-approval-gate-result.v1",
"status": "SUCCESS",
"campaign_id": campaign.campaign_id,
"pr_number": facts["pr_number"],
"head_oid": facts["head_oid"],
"owner_ui_interaction_required": False,
}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--campaign", type=Path, required=True)
p.add_argument("--revocation", type=Path)
p.add_argument("--pr-facts", type=Path, required=True)
p.add_argument("--repository", type=Path, required=True)
a = p.parse_args()
campaign = CampaignAuthorization.load(
a.campaign,
revocation=a.revocation,
require_active=True,
)
facts = json.loads(a.pr_facts.read_text(encoding="utf-8"))
print(json.dumps(evaluate(campaign, facts, a.repository), sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CampaignError, GateError) as exc:
print(json.dumps({"status": "FAILURE", "reason": str(exc)}, sort_keys=True))
raise SystemExit(2)
@@ -1,150 +0,0 @@
#!/usr/bin/env python3
"""Automatically accept an exact release binding under an ACTIVE campaign."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import stat
import tempfile
from typing import Any, Mapping
from release_campaign_authorization import CampaignAuthorization, CampaignError
class AcceptanceError(RuntimeError):
pass
def secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise AcceptanceError(f"invalid_file:{path}")
st = path.lstat()
if st.st_uid != 0 or stat.S_IMODE(st.st_mode) & 0o022:
raise AcceptanceError(f"unsafe_file:{path}")
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise AcceptanceError(f"json_object_required:{path}")
return value
def atomic_new(path: Path, value: Mapping[str, Any]) -> None:
if path.exists() or path.is_symlink():
raise AcceptanceError("acceptance_receipt_exists")
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(fd, 0o600)
if os.geteuid() == 0:
os.fchown(fd, 0, 0)
with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream:
json.dump(dict(value), stream, sort_keys=True, indent=2)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
os.chmod(path, 0o600)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def evaluate(
campaign: CampaignAuthorization,
binding: Mapping[str, Any],
evidence: Mapping[str, Any],
artifact: Mapping[str, Any],
staging: Mapping[str, Any],
main: Mapping[str, Any],
tag: Mapping[str, Any],
) -> dict[str, Any]:
required_binding = {
"version",
"target_oid",
"evidence_closure_oid",
"artifact_sha256",
"artifact_size",
"approval_plan_id",
}
if set(binding) != required_binding:
raise AcceptanceError("binding_fields_invalid")
if main.get("repository") != "aios/aios":
raise AcceptanceError("repository_identity_mismatch")
if main.get("target_in_main_lineage") is not True:
raise AcceptanceError("target_outside_main_lineage")
if evidence.get("object_type") != "commit":
raise AcceptanceError("evidence_not_commit")
if evidence.get("target_is_ancestor") is not True:
raise AcceptanceError("target_not_evidence_ancestor")
if evidence.get("closure_oid") != binding["evidence_closure_oid"]:
raise AcceptanceError("evidence_oid_mismatch")
if artifact.get("sha256") != binding["artifact_sha256"]:
raise AcceptanceError("artifact_digest_mismatch")
if artifact.get("size") != binding["artifact_size"]:
raise AcceptanceError("artifact_size_mismatch")
if artifact.get("retained") is not True:
raise AcceptanceError("artifact_not_retained")
if staging.get("install") != "PASS":
raise AcceptanceError("staging_install_failed")
if staging.get("health") != "PASS":
raise AcceptanceError("staging_health_failed")
if staging.get("restart") != "PASS":
raise AcceptanceError("staging_restart_failed")
if staging.get("rollback_readiness") != "PASS":
raise AcceptanceError("rollback_readiness_failed")
if tag.get("conflict") is not False:
raise AcceptanceError("tag_conflict")
campaign.assert_operation("derive_campaign_acceptance")
return {
"schema": "aios.campaign-release-acceptance.v1",
"accepted": True,
"binding_replaced": False,
"campaign_id": campaign.campaign_id,
"campaign_generation": campaign.data["generation"],
**dict(binding),
"owner_interaction_required": False,
}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--campaign", type=Path, required=True)
p.add_argument("--revocation", type=Path)
p.add_argument("--binding", type=Path, required=True)
p.add_argument("--evidence", type=Path, required=True)
p.add_argument("--artifact", type=Path, required=True)
p.add_argument("--staging", type=Path, required=True)
p.add_argument("--main-facts", type=Path, required=True)
p.add_argument("--tag-facts", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
a = p.parse_args()
campaign = CampaignAuthorization.load(
a.campaign,
revocation=a.revocation,
require_active=True,
)
receipt = evaluate(
campaign,
secure_json(a.binding),
secure_json(a.evidence),
secure_json(a.artifact),
secure_json(a.staging),
secure_json(a.main_facts),
secure_json(a.tag_facts),
)
atomic_new(a.output, receipt)
print(json.dumps(receipt, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CampaignError, AcceptanceError) as exc:
print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True))
raise SystemExit(2)
-67
View File
@@ -1,67 +0,0 @@
"""Runtime deployment-state provider for governed health contracts."""
from __future__ import annotations
import datetime as dt
import json
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
_OID = re.compile(r"^[0-9a-f]{40}$")
_SHA = re.compile(r"^[0-9a-f]{64}$")
_STATES = {"STAGED", "ACTIVE", "ROLLED_BACK"}
@dataclass(frozen=True)
class RuntimeDeploymentState:
version: str
commit_oid: str
artifact_sha256: str
deployment_state: str
@property
def production_deployed(self) -> bool:
return self.deployment_state == "ACTIVE"
def health_payload(self) -> dict[str, Any]:
return {
"status": "ok",
"version": self.version,
"commit_oid": self.commit_oid,
"artifact_sha256": self.artifact_sha256,
"deployment_state": self.deployment_state,
"production_deployed": self.production_deployed,
"health_timestamp": dt.datetime.now(dt.timezone.utc).isoformat(),
}
@classmethod
def from_environment(cls, environ: Mapping[str, str] | None = None) -> "RuntimeDeploymentState":
env = dict(environ or os.environ)
values: dict[str, Any] = {}
state_file = env.get("DEPLOYMENT_STATE_FILE", "").strip()
if state_file:
path = Path(state_file)
if path.is_file() and not path.is_symlink():
loaded = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(loaded, dict):
raise ValueError("deployment_state_file_object_required")
values.update(loaded)
file_deployment_state = values.get("deployment_state")
values.update({k: v for k, v in {
"version": env.get("RELEASE_VERSION", values.get("version", "")),
"commit_oid": env.get("TARGET_OID", values.get("commit_oid", "")),
"artifact_sha256": env.get("ARTIFACT_SHA256", values.get("artifact_sha256", "")),
"deployment_state": env.get("DEPLOYMENT_STATE", values.get("deployment_state", "")),
}.items() if v != ""})
if file_deployment_state not in (None, ""):
values["deployment_state"] = file_deployment_state
version = str(values.get("version", ""))
commit_oid = str(values.get("commit_oid", ""))
artifact_sha256 = str(values.get("artifact_sha256", ""))
deployment_state = str(values.get("deployment_state", ""))
if not version or not _OID.fullmatch(commit_oid) or not _SHA.fullmatch(artifact_sha256):
raise ValueError("deployment_binding_invalid")
if deployment_state not in _STATES:
raise ValueError("deployment_state_invalid")
return cls(version, commit_oid, artifact_sha256, deployment_state)
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
"""Permanent AIOS governance source components."""
@@ -1,75 +0,0 @@
#!/usr/bin/env python3
import json
import pathlib
import socket
import time
SOCKET = '/run/aios-governance-executor/executor.sock'
STATE = pathlib.Path('/var/lib/aios-gitops/governance-executor/drift-monitor.json')
APPROVED = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/protection-drift/semantic-analysis.json')
def call():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(20)
sock.connect(SOCKET)
request = {'operation': 'GET_BRANCH_PROTECTION', 'payload': {'repository': 'aios/aios', 'branch': 'main'}}
sock.sendall((json.dumps(request, separators=(',', ':')) + '\n').encode())
chunks = []
while True:
part = sock.recv(1024 * 1024)
if not part:
break
chunks.append(part)
if b'\n' in part:
break
sock.close()
return json.loads(b''.join(chunks).splitlines()[0])
def main():
try:
result = call()
if result.get('status') != 'PASS':
raise RuntimeError(str(result.get('code') or 'EXECUTOR_GET_FAILED'))
approved = json.loads(APPROVED.read_text(encoding='utf-8'))
approved_transport = approved['approved_transport_digest_from_evidence']
approved_semantic = approved['approved_semantic_digest']
transport = result['transport_digest']
semantic = result['semantic_digest']
if semantic != approved_semantic:
state = 'GOVERNANCE_POLICY_DRIFT'
elif transport != approved_transport:
state = 'GOVERNANCE_TRANSPORT_METADATA_CHANGED'
else:
state = 'NO_DRIFT'
payload = {
'schema': 'aios.governance-drift-monitor.v1',
'observed_at_unix': int(time.time()),
'status': state,
'release_blocking': False,
'approved_transport_digest': approved_transport,
'observed_transport_digest': transport,
'approved_semantic_digest': approved_semantic,
'observed_semantic_digest': semantic,
'semantic_schema': result.get('semantic_schema'),
'source': 'Dedicated Governance Executor GET_BRANCH_PROTECTION',
}
except Exception as exc:
payload = {
'schema': 'aios.governance-drift-monitor.v1',
'observed_at_unix': int(time.time()),
'status': 'MONITOR_UNAVAILABLE',
'release_blocking': False,
'error_code': str(exc),
}
STATE.parent.mkdir(parents=True, exist_ok=True)
temporary = STATE.with_suffix('.tmp')
temporary.write_text(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
temporary.replace(STATE)
STATE.chmod(0o640)
print(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False))
if __name__ == '__main__':
main()
@@ -1,417 +0,0 @@
#!/usr/bin/env python3
import base64
import copy
import hashlib
import json
import os
import pathlib
import socket
import time
import urllib.error
import urllib.parse
import urllib.request
SOURCE_GOVERNANCE = "/opt/aios/aios-control-plane/current/active-runtime/app/governance"
if SOURCE_GOVERNANCE not in __import__("sys").path:
__import__("sys").path.insert(0, SOURCE_GOVERNANCE)
from stage2_gate_migration import (
CAMPAIGN_GATE,
CAMPAIGN_ID,
OWNER_GATE,
STAGE1_SEMANTIC_SHA,
Stage2GateError,
build_target,
semantic_digest as stage2_semantic_digest,
validate_stage2_facts,
)
API = "http://127.0.0.1:3000"
REPO = "aios/aios"
SOCKET = "/run/aios-governance-executor/executor.sock"
JOURNAL = pathlib.Path("/var/lib/aios-gitops/governance-executor/executor-journal.jsonl")
RECONCILIATION_BINDING_PATH = pathlib.Path("/var/lib/aios-gitops/governance-executor/reconciliation-binding.json")
PR54_HEAD = "015185d7e5191d18c5c1596160807ac228782a0a"
PR54_REVIEW = 104
PR54_MANIFEST_SHA = "1b11ee42d53a9f6c1646e5f5ddd8ce5b7ecf301483b9f3d47a068290d541ba4b"
PR54_PREVIOUS_PROTECTION_SHA = "8b3370add21cd23c2b4f41d8d84d3eaaa09ddf0451549b72c7941bbd92d5d60a"
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
APPROVED_SEMANTIC_SHA = "ba15d512ed8c1f43e5f72c84125a3a71a7347ef59e18000bbfca24d6e69f1b19"
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
def canon(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
def sha(value):
return hashlib.sha256(canon(value)).hexdigest()
def token():
value = (pathlib.Path(os.environ["CREDENTIALS_DIRECTORY"]) / "gitea-governance-executor-token").read_text().strip()
if len(value) < 16:
raise RuntimeError("EXECUTOR_CREDENTIAL_INVALID")
return value
def api(method, path, value, body=None, expected=(200,)):
raw = None if body is None else json.dumps(body, separators=(",", ":")).encode()
req = urllib.request.Request(API + path, data=raw, method=method)
req.add_header("Authorization", "token " + value)
req.add_header("Accept", "application/json")
if raw is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=20) as response:
status = response.status
payload = response.read()
except urllib.error.HTTPError as exc:
if exc.code in expected:
payload = exc.read()
return exc.code, json.loads(payload) if payload else None
raise RuntimeError("GITEA_HTTP_" + str(exc.code))
if status not in expected:
raise RuntimeError("GITEA_HTTP_" + str(status))
if not payload or not payload.strip():
raise RuntimeError("GITEA_EMPTY_JSON:" + path)
try:
decoded = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("GITEA_INVALID_JSON:" + path) from exc
return status, decoded
def journal(event, **fields):
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
record = {"schema": "aios.governance-executor-journal.v2", "event": event, "unix": int(time.time()), **fields}
with JOURNAL.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n")
stream.flush()
os.fsync(stream.fileno())
def typed_json(value):
if value in (None, "null"):
return []
if isinstance(value, str):
return json.loads(value)
return value
def user_ids(names):
result = []
for name in names or []:
if name not in USER_IDS:
raise RuntimeError("UNKNOWN_GOVERNANCE_USER:" + str(name))
result.append(USER_IDS[name])
return sorted(result)
def semantic_policy(value):
policy = {
"schema_version": SEMANTIC_SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": value.get("branch_name"),
"can_push": value.get("enable_push"),
"can_force_push": value.get("enable_force_push"),
"enable_merge_whitelist": value.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": user_ids(value.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": value.get("required_approvals"),
"enable_status_check": value.get("enable_status_check"),
"status_check_contexts": sorted(value.get("status_check_contexts") or []),
"block_admin_merge_override": value.get("block_admin_merge_override"),
"block_on_official_review_requests": value.get("block_on_official_review_requests"),
"block_on_outdated_branch": value.get("block_on_outdated_branch"),
"block_on_rejected_reviews": value.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": value.get("dismiss_stale_approvals"),
"ignore_stale_approvals": value.get("ignore_stale_approvals"),
"enable_approvals_whitelist": value.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": user_ids(value.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": value.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": user_ids(value.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": value.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": user_ids(value.get("force_push_allowlist_usernames")),
"force_push_allowlist_team_ids": [],
"enable_whitelist": value.get("enable_push_whitelist"),
"whitelist_user_ids": user_ids(value.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": value.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": value.get("push_whitelist_deploy_keys"),
"require_signed_commits": value.get("require_signed_commits"),
"priority": value.get("priority"),
"protected_file_patterns": value.get("protected_file_patterns"),
"unprotected_file_patterns": value.get("unprotected_file_patterns"),
}
return policy
def semantic_sha(value):
return sha(semantic_policy(value))
def verify_binding(binding, value):
required = {
"governance_request_PR", "exact_request_head", "Owner_review_id", "Owner_review_exact_head",
"manifest_sha256", "expected_previous_snapshot_sha256", "approved_delta",
"semantic_reconciliation_request_id", "semantic_reconciliation_head",
"semantic_reconciliation_review_id", "semantic_reconciliation_review_head",
"approved_semantic_digest",
}
if set(binding) != required:
raise RuntimeError("GOVERNANCE_BINDING_SCHEMA_INVALID")
if binding["governance_request_PR"] != 54 or binding["exact_request_head"] != PR54_HEAD:
raise RuntimeError("GOVERNANCE_BINDING_PR54_MISMATCH")
if binding["Owner_review_id"] != PR54_REVIEW or binding["Owner_review_exact_head"] != PR54_HEAD:
raise RuntimeError("GOVERNANCE_BINDING_REVIEW_MISMATCH")
if binding["manifest_sha256"] != PR54_MANIFEST_SHA:
raise RuntimeError("GOVERNANCE_BINDING_MANIFEST_MISMATCH")
if binding["expected_previous_snapshot_sha256"] != PR54_PREVIOUS_PROTECTION_SHA:
raise RuntimeError("GOVERNANCE_BINDING_SNAPSHOT_MISMATCH")
if binding["approved_delta"] != {"add_merge_whitelist_user_id": 153}:
raise RuntimeError("GOVERNANCE_BINDING_DELTA_DENIED")
if binding["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA:
raise RuntimeError("GOVERNANCE_BINDING_SEMANTIC_DIGEST_MISMATCH")
if not RECONCILIATION_BINDING_PATH.is_file():
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_REQUIRED")
try:
reconciliation = json.loads(RECONCILIATION_BINDING_PATH.read_text(encoding="utf-8"))
except (OSError, ValueError):
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID") from None
required_reconciliation = {
"schema", "request_id", "pr_number", "request_head", "owner_review_id",
"owner_review_head", "approved_semantic_digest", "approved_delta",
}
if set(reconciliation) != required_reconciliation or reconciliation.get("schema") != "aios.semantic-reconciliation-binding.v1":
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID")
if reconciliation["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA or reconciliation["approved_delta"] != binding["approved_delta"]:
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
if (
reconciliation["request_id"] != binding["semantic_reconciliation_request_id"]
or reconciliation["request_head"] != binding["semantic_reconciliation_head"]
or reconciliation["owner_review_id"] != binding["semantic_reconciliation_review_id"]
or reconciliation["owner_review_head"] != binding["semantic_reconciliation_review_head"]
):
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
_, reconciliation_pr = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}", value)
if (
reconciliation_pr.get("state") != "open"
or reconciliation_pr.get("merged")
or reconciliation_pr.get("head", {}).get("sha") != reconciliation["request_head"]
or reconciliation_pr.get("base", {}).get("ref") != "governance/approvals"
):
raise RuntimeError("SEMANTIC_RECONCILIATION_REQUEST_STALE")
_, reconciliation_reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}/reviews", value)
reconciliation_review = next((x for x in reconciliation_reviews if x.get("id") == reconciliation["owner_review_id"]), None)
if (
not reconciliation_review
or reconciliation_review.get("user", {}).get("id") != 2
or reconciliation_review.get("state") != "APPROVED"
or reconciliation_review.get("commit_id") != reconciliation["owner_review_head"]
or reconciliation_review.get("dismissed") is not False
):
raise RuntimeError("SEMANTIC_RECONCILIATION_OWNER_REVIEW_STALE")
_, pr = api("GET", f"/api/v1/repos/{REPO}/pulls/54", value)
if pr.get("state") != "open" or pr.get("merged") or pr.get("head", {}).get("sha") != PR54_HEAD:
raise RuntimeError("PR54_APPROVAL_STALE")
_, reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/54/reviews", value)
review = next((x for x in reviews if x.get("id") == PR54_REVIEW), None)
if not review or review.get("user", {}).get("id") != 2 or review.get("state") != "APPROVED" or review.get("commit_id") != PR54_HEAD or review.get("dismissed") is not False:
raise RuntimeError("PR54_APPROVAL_STALE")
manifest_path = "approvals/governance-policy/AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1.json"
_, content = api("GET", f"/api/v1/repos/{REPO}/contents/{urllib.parse.quote(manifest_path, safe='/')}?ref={PR54_HEAD}", value)
raw = base64.b64decode("".join(content.get("content", "").split()))
if hashlib.sha256(raw).hexdigest() != PR54_MANIFEST_SHA:
raise RuntimeError("PR54_MANIFEST_STALE")
def get_protection(value, branch):
if branch not in {"main", "governance/approvals"}:
raise RuntimeError("BRANCH_DENIED")
_, protection = api("GET", f"/api/v1/repos/{REPO}/branch_protections/{urllib.parse.quote(branch, safe='')}", value)
return protection
def mutation_receipt(before, after, binding, target_semantic):
return {
"before": {"transport_digest": sha(before), "semantic_digest": semantic_sha(before)},
"approved": {
"mutation_predicate_digest": sha({"operation": "ADD_MERGE_EXECUTOR", "subject_user_id": 153, "preserve_all_unmodified_security_fields": True}),
"Owner_review_id": binding["Owner_review_id"],
"semantic_policy_digest": APPROVED_SEMANTIC_SHA,
},
"after": {"transport_digest": sha(after), "semantic_digest": semantic_sha(after)},
"delta": {"add_merge_whitelist_user_id": 153},
"target_semantic_digest": sha(target_semantic),
"unmodified_security_fields": {"verified": True},
}
def apply_stage1(binding, value):
verify_binding(binding, value)
before = get_protection(value, "main")
before_semantic = semantic_policy(before)
before_semantic_sha = sha(before_semantic)
names = list(before.get("merge_whitelist_usernames") or [])
name_ids = user_ids(names)
if name_ids not in ([2], [2, 153]):
raise RuntimeError("PROTECTION_WHITELIST_PRESTATE_INVALID")
target_names = ["owner", "aios-autonomy-agent"]
target_semantic = copy.deepcopy(before_semantic)
target_semantic["merge_whitelist_user_ids"] = [2, 153]
if name_ids == [2, 153]:
if before_semantic_sha != STAGE1_SEMANTIC_SHA:
journal("APPLY_BLOCKED_STAGE1_IDEMPOTENCY_SEMANTIC_CONFLICT", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=STAGE1_SEMANTIC_SHA)
raise RuntimeError("PROTECTION_STAGE1_SEMANTIC_CONFLICT")
receipt = mutation_receipt(before, before, binding, target_semantic)
journal("APPLY_APPROVED_PROTECTION_DELTA_IDEMPOTENT_SUCCESS", receipt=receipt)
return {"status": "IDEMPOTENT_SUCCESS", "protection": before, "transport_digest": sha(before), "semantic_digest": before_semantic_sha, "target_semantic_digest": sha(target_semantic)}
if before_semantic_sha != APPROVED_SEMANTIC_SHA:
journal("APPLY_BLOCKED_SEMANTIC_CAS", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=APPROVED_SEMANTIC_SHA)
raise RuntimeError("PROTECTION_SEMANTIC_CAS_CONFLICT")
after_api = api("PATCH", f"/api/v1/repos/{REPO}/branch_protections/main", value, {"merge_whitelist_usernames": target_names})[1]
after = get_protection(value, "main")
for key in before:
if key in {"updated_at", "merge_whitelist_usernames"}:
continue
if after.get(key) != before.get(key):
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
if user_ids(after.get("merge_whitelist_usernames")) != [2, 153]:
raise RuntimeError("PROTECTION_TARGET_READBACK_FAILED")
after_semantic = semantic_policy(after)
if after_semantic != target_semantic:
raise RuntimeError("PROTECTION_SEMANTIC_TARGET_READBACK_FAILED")
receipt = mutation_receipt(before, after, binding, target_semantic)
journal("APPLY_APPROVED_PROTECTION_DELTA_COMPLETED", receipt=receipt)
return {"status": "APPLIED", "protection": after, "previous_transport_digest": sha(before), "previous_semantic_digest": before_semantic_sha, "transport_digest": sha(after), "semantic_digest": sha(after_semantic), "receipt": receipt}
def apply_stage2(binding, facts, value):
verify_binding(binding, value)
validate_stage2_facts(facts)
before = get_protection(value, "main")
if OWNER_GATE in (before.get("status_check_contexts") or []) and stage2_semantic_digest(before) != STAGE1_SEMANTIC_SHA:
raise RuntimeError("STAGE2_SEMANTIC_PRECONDITION_INVALID")
try:
status, target = build_target(before)
except Stage2GateError as exc:
journal("MIGRATE_APPROVED_GATE_BLOCKED", reason=str(exc), semantic_digest=stage2_semantic_digest(before))
raise RuntimeError(str(exc)) from exc
target_digest = stage2_semantic_digest(target)
if status == "IDEMPOTENT_SUCCESS":
journal(
"MIGRATE_APPROVED_GATE_IDEMPOTENT_SUCCESS",
before_semantic_digest=stage2_semantic_digest(before),
target_semantic_digest=target_digest,
campaign_id=CAMPAIGN_ID,
)
return {
"status": status,
"protection": before,
"transport_digest": sha(before),
"semantic_digest": stage2_semantic_digest(before),
"target_semantic_digest": target_digest,
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
}
after_api = api(
"PATCH",
f"/api/v1/repos/{REPO}/branch_protections/main",
value,
{"status_check_contexts": target["status_check_contexts"]},
)[1]
after = get_protection(value, "main")
for key in before:
if key in {"updated_at", "status_check_contexts"}:
continue
if after.get(key) != before.get(key):
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
if after.get("status_check_contexts") != target["status_check_contexts"]:
raise RuntimeError("STAGE2_GATE_TARGET_READBACK_FAILED")
if stage2_semantic_digest(after) != target_digest:
raise RuntimeError("STAGE2_SEMANTIC_TARGET_READBACK_FAILED")
receipt = {
"before_transport_digest": sha(before),
"before_semantic_digest": stage2_semantic_digest(before),
"after_transport_digest": sha(after),
"after_semantic_digest": stage2_semantic_digest(after),
"target_semantic_digest": target_digest,
"campaign_id": CAMPAIGN_ID,
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
"preserved_security_fields": True,
"facts_digest": sha(facts),
}
journal("MIGRATE_APPROVED_GATE_COMPLETED", receipt=receipt)
return {
"status": "APPLIED",
"protection": after,
"previous_transport_digest": sha(before),
"previous_semantic_digest": stage2_semantic_digest(before),
"transport_digest": sha(after),
"semantic_digest": stage2_semantic_digest(after),
"target_semantic_digest": target_digest,
"receipt": receipt,
}
def handle(request, value):
if not isinstance(request, dict) or set(request) != {"operation", "payload"}:
raise RuntimeError("REQUEST_SCHEMA_INVALID")
operation = request["operation"]
payload = request["payload"]
if not isinstance(payload, dict) or payload.get("repository") != REPO:
raise RuntimeError("REPOSITORY_DENIED")
if operation == "GET_BRANCH_PROTECTION":
protection = get_protection(value, payload.get("branch"))
journal("GET_BRANCH_PROTECTION", branch=payload.get("branch"), transport_digest=sha(protection), semantic_digest=semantic_sha(protection))
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
if operation == "READBACK_BRANCH_PROTECTION":
protection = get_protection(value, payload.get("branch"))
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
if operation == "APPLY_APPROVED_PROTECTION_DELTA":
binding = payload.get("GovernanceExecutionBinding")
if not isinstance(binding, dict):
raise RuntimeError("GOVERNANCE_BINDING_REQUIRED")
return apply_stage1(binding, value)
if operation == "MIGRATE_APPROVED_GATE":
binding = payload.get("GovernanceExecutionBinding")
facts = payload.get("Stage2Facts")
if not isinstance(binding, dict) or not isinstance(facts, dict):
raise RuntimeError("STAGE2_BINDING_OR_FACTS_REQUIRED")
return apply_stage2(binding, facts, value)
raise RuntimeError("OPERATION_DENIED")
def main():
value = token()
sock_path = pathlib.Path(SOCKET)
sock_path.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET)
os.chmod(SOCKET, 0o660)
server.listen(16)
journal("EXECUTOR_STARTED", api_surface=["GET_BRANCH_PROTECTION", "APPLY_APPROVED_PROTECTION_DELTA", "MIGRATE_APPROVED_GATE", "READBACK_BRANCH_PROTECTION"], semantic_schema=SEMANTIC_SCHEMA, approved_semantic_digest=APPROVED_SEMANTIC_SHA, stage1_semantic_digest=STAGE1_SEMANTIC_SHA, campaign_gate=CAMPAIGN_GATE)
try:
while True:
conn, _ = server.accept()
with conn:
data = conn.recv(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
result = {"status": "FAIL", "code": "REQUEST_TOO_LARGE"}
else:
try:
request = json.loads(data.decode())
result = handle(request, value)
except Exception as exc:
result = {"status": "FAIL", "code": str(exc)}
conn.sendall((json.dumps(result, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode())
finally:
server.close()
sock_path.unlink(missing_ok=True)
if __name__ == "__main__":
main()
@@ -1,347 +0,0 @@
#!/usr/bin/env python3
import base64
import hashlib
import json
import os
import pathlib
import shutil
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
LOCAL_API = 'http://127.0.0.1:3000/api/v1'
NETWORK_API = 'https://g.cnnetmkt.top/git/api/v1'
NETWORK_REMOTE = 'https://g.cnnetmkt.top/git/aios/aios.git'
REPO = 'aios/aios'
BASE_BRANCH = 'governance/approvals'
REQUEST_BRANCH = 'governance/request/protection-semantic-reconciliation-v1-20260808'
SOCKET_PATH = '/run/aios-governance-preapproval-publisher/publisher.sock'
CREDENTIAL_NAME = 'gitea-machine-token'
AUTHORITY_REPO = '/data/aios-gitops/repo.git'
TASK_ROOT = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1')
INPUT_ROOT = TASK_ROOT / 'publication-input'
RECEIPT = TASK_ROOT / 'publication-receipt.json'
MANIFEST_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.json'
EVIDENCE_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.evidence.json'
def digest_bytes(data):
return hashlib.sha256(data).hexdigest()
def digest_json(value):
return digest_bytes(json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode())
def load_token():
path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
value = path.read_text(encoding='utf-8').strip()
if len(value) < 16:
raise RuntimeError('MACHINE_CREDENTIAL_INVALID')
return value
def api(base, method, path, token, body=None, expected=(200,)):
raw = None if body is None else json.dumps(body, separators=(',', ':'), ensure_ascii=False).encode()
request = urllib.request.Request(base + path, data=raw, method=method)
request.add_header('Authorization', 'token ' + token)
request.add_header('Accept', 'application/json')
if raw is not None:
request.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(request, timeout=20) as response:
payload = response.read()
if response.status not in expected:
raise RuntimeError('GITEA_HTTP_' + str(response.status))
return response.status, json.loads(payload) if payload else None
except urllib.error.HTTPError as exc:
payload = exc.read()
if exc.code in expected:
return exc.code, json.loads(payload) if payload else None
raise RuntimeError('GITEA_HTTP_' + str(exc.code) + ':' + method + ':' + path) from None
def authority_head():
result = subprocess.run(
['git', '--git-dir=' + AUTHORITY_REPO, 'rev-parse', 'refs/heads/' + BASE_BRANCH],
text=True, capture_output=True, check=False,
)
if result.returncode != 0:
raise RuntimeError('AUTHORITY_BASE_UNAVAILABLE')
return result.stdout.strip()
def branch_api(base, token, branch, expected=(200,)):
return api(base, 'GET', '/repos/' + REPO + '/branches/' + urllib.parse.quote(branch, safe=''), token, expected=expected)
def verify_base(token):
authority = authority_head()
_, local = branch_api(LOCAL_API, token, BASE_BRANCH)
_, network = branch_api(NETWORK_API, token, BASE_BRANCH)
local_oid = local.get('commit', {}).get('id')
network_oid = network.get('commit', {}).get('id')
if not authority or authority != local_oid or authority != network_oid:
raise RuntimeError('RECONCILIATION_REQUIRED')
return authority
def verify_identity(token):
_, user = api(LOCAL_API, 'GET', '/user', token)
if user.get('login') != 'aios-autonomy-agent' or user.get('id') != 153 or user.get('is_admin') is not False:
raise RuntimeError('MACHINE_IDENTITY_REJECTED')
return {'login': user['login'], 'id': user['id'], 'owner': user.get('is_admin')}
def load_inputs():
manifest = json.dumps(json.loads((INPUT_ROOT / 'manifest.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
evidence = json.dumps(json.loads((INPUT_ROOT / 'evidence.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
for data in (manifest, evidence):
if any(secret in data.lower() for secret in (b'authorization:', b'ghp_', b'github_pat_', b'secret=', b'token=')):
raise RuntimeError('SECRET_CONTENT_REJECTED')
return manifest, evidence
def publication_key(base_oid, manifest_sha, evidence_sha):
payload = REPO + base_oid + REQUEST_BRANCH + manifest_sha + evidence_sha
return hashlib.sha256(payload.encode()).hexdigest()
def write_receipt(state, **fields):
record = {
'schema': 'aios.governance-preapproval-publication.v1',
'operation': 'SEMANTIC_RECONCILIATION_REQUEST',
'state': state,
'recorded_unix': int(time.time()),
'request_branch': REQUEST_BRANCH,
'manifest_path': MANIFEST_REL,
'evidence_path': EVIDENCE_REL,
'machine_actor': 'aios-autonomy-agent',
'owner': False,
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
'governance_capability_executed': False,
'protection_mutation': False,
'force_push': False,
**fields,
}
RECEIPT.parent.mkdir(parents=True, exist_ok=True)
temporary = RECEIPT.with_suffix('.tmp')
temporary.write_text(json.dumps(record, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
temporary.replace(RECEIPT)
RECEIPT.chmod(0o640)
return record
def existing_prs(token):
_, prs = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls?state=all&base=' + urllib.parse.quote(BASE_BRANCH, safe='') + '&limit=100', token)
return [pr for pr in prs if pr.get('head', {}).get('ref') == REQUEST_BRANCH and pr.get('base', {}).get('ref') == BASE_BRANCH]
def verify_existing_branch(token, branch_oid, manifest, evidence):
_, commit = api(LOCAL_API, 'GET', '/repos/' + REPO + '/git/commits/' + branch_oid, token)
if commit.get('parents', [{}])[0].get('sha') is None:
raise RuntimeError('REQUEST_BRANCH_COMMIT_INVALID')
for relative, expected in ((MANIFEST_REL, manifest), (EVIDENCE_REL, evidence)):
_, content = api(LOCAL_API, 'GET', '/repos/' + REPO + '/contents/' + urllib.parse.quote(relative, safe='/') + '?ref=' + branch_oid, token)
actual = base64.b64decode(''.join(content.get('content', '').split()))
if actual != expected:
raise RuntimeError('GOVERNANCE_REQUEST_BRANCH_CONFLICT')
def push_one_commit(token, base_oid, manifest, evidence):
work_root = TASK_ROOT / 'publisher-work'
work_root.mkdir(parents=True, exist_ok=True)
temporary = pathlib.Path(tempfile.mkdtemp(prefix='semantic-reconciliation-', dir=work_root))
askpass = temporary / 'askpass.sh'
clone = temporary / 'repo'
credential_path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
askpass.write_text(
'#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR";; *) cat "$CREDENTIAL_FILE";; esac\n',
encoding='utf-8',
)
askpass.chmod(0o700)
env = os.environ.copy()
env.update({
'GIT_ASKPASS': str(askpass),
'GIT_TERMINAL_PROMPT': '0',
'GITEA_MACHINE_ACTOR': 'aios-autonomy-agent',
'CREDENTIAL_FILE': str(credential_path),
'GIT_CONFIG_COUNT': '1',
'GIT_CONFIG_KEY_0': 'safe.directory',
'GIT_CONFIG_VALUE_0': AUTHORITY_REPO,
})
try:
run = subprocess.run(
['git', 'clone', '--branch', BASE_BRANCH, '--single-branch', '--quiet', NETWORK_REMOTE, str(clone)],
text=True, capture_output=True, env=env, check=False,
)
if run.returncode != 0:
detail = ' '.join(run.stderr.strip().split())[:240]
raise RuntimeError('PUBLISHER_CLONE_FAILED:' + detail)
def git(*args, push_env=None):
result = subprocess.run(['git', *args], cwd=clone, text=True, capture_output=True, env=push_env or env, check=False)
if result.returncode != 0:
raise RuntimeError('PUBLISHER_GIT_OPERATION_FAILED')
return result.stdout.strip()
git('checkout', '--detach', base_oid)
git('switch', '--create', REQUEST_BRANCH)
manifest_path = clone / MANIFEST_REL
evidence_path = clone / EVIDENCE_REL
manifest_path.parent.mkdir(parents=True, exist_ok=True)
evidence_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_bytes(manifest)
evidence_path.write_bytes(evidence)
git('add', '--', MANIFEST_REL, EVIDENCE_REL)
staged = git('diff', '--cached', '--name-only').splitlines()
if staged != sorted([MANIFEST_REL, EVIDENCE_REL]):
raise RuntimeError('PUBLISHER_PATH_ALLOWLIST_FAILED')
git('-c', 'user.name=aios-autonomy-agent', '-c', 'user.email=aios-autonomy-agent@aios.invalid', 'commit', '--quiet', '-m', '[governance] publish semantic reconciliation evidence')
if git('rev-list', '--count', base_oid + '..HEAD') != '1':
raise RuntimeError('PUBLISHER_COMMIT_COUNT_FAILED')
git('push', '--porcelain', NETWORK_REMOTE, 'HEAD:refs/heads/' + REQUEST_BRANCH, push_env=env)
head = git('rev-parse', 'HEAD')
return head
finally:
shutil.rmtree(temporary, ignore_errors=True)
def publish():
token = load_token()
identity = verify_identity(token)
base_oid = verify_base(token)
manifest, evidence = load_inputs()
manifest_sha = digest_bytes(manifest)
evidence_sha = digest_bytes(evidence)
key = publication_key(base_oid, manifest_sha, evidence_sha)
write_receipt('REQUESTED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
write_receipt('LOCAL_CANDIDATE_VERIFIED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
branch_status, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
if branch_status == 200:
branch_oid = branch.get('commit', {}).get('id')
verify_existing_branch(token, branch_oid, manifest, evidence)
else:
branch_oid = push_one_commit(token, base_oid, manifest, evidence)
verify_existing_branch(token, branch_oid, manifest, evidence)
write_receipt('BRANCH_PUSHED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, machine_identity=identity)
prs = existing_prs(token)
if len(prs) > 1:
raise RuntimeError('MULTIPLE_MATCHING_APPROVAL_PRS')
if prs:
pr = prs[0]
if pr.get('head', {}).get('sha') != branch_oid:
raise RuntimeError('APPROVAL_PR_HEAD_MISMATCH')
pr_number = pr.get('number')
else:
_, pr = api(LOCAL_API, 'POST', '/repos/' + REPO + '/pulls', token, {
'title': '[GOVERNANCE RECONCILIATION] Semantic protection snapshot schema migration',
'head': REQUEST_BRANCH,
'base': BASE_BRANCH,
'body': 'Schema-only reconciliation for the existing PR54 Stage-1 and Stage-2 predicates. No new governance capability; Owner exact-head re-attestation required.',
}, expected=(201,))
pr_number = pr.get('number')
write_receipt('PR_CREATED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, machine_identity=identity)
_, remote_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
if remote_branch.get('commit', {}).get('id') != branch_oid:
raise RuntimeError('NETWORK_BRANCH_READBACK_MISMATCH')
_, read_pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
if read_pr.get('head', {}).get('sha') != branch_oid or read_pr.get('base', {}).get('ref') != BASE_BRANCH:
raise RuntimeError('PR_READBACK_MISMATCH')
result = write_receipt('COMPLETED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, pr_head_oid=read_pr.get('head', {}).get('sha'), machine_identity=identity)
return result
def self_test():
token = load_token()
identity = verify_identity(token)
base_oid = verify_base(token)
branch_status, _ = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
return {
'status': 'PASS',
'governance_base_oid': base_oid,
'machine_identity': identity,
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
'branch_namespace': 'refs/heads/governance/request/*',
'target_base': BASE_BRANCH,
'request_branch_status': 'EXISTS' if branch_status == 200 else 'ABSENT',
'operation_allowlist': ['READ_BASE', 'CREATE_ONE_REQUEST_BRANCH', 'WRITE_TWO_EVIDENCE_PATHS_ONE_COMMIT', 'CREATE_OR_READ_ONE_APPROVAL_PR', 'READBACK'],
'denied': ['MAIN_WRITE', 'DIRECT_APPROVALS_WRITE', 'BRANCH_PROTECTION', 'MERGE', 'FORCE_PUSH', 'TAG', 'PRODUCTION', 'CREDENTIAL_MUTATION'],
'governance_capability_executed': False,
}
def read_publication_status():
token = load_token()
receipt = json.loads(RECEIPT.read_text(encoding='utf-8'))
pr_number = receipt.get('pr_number')
if not pr_number:
raise RuntimeError('PUBLICATION_RECEIPT_PR_MISSING')
_, pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
_, reviews = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number) + '/reviews', token)
_, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH)
_, network_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
return {
'status': 'PASS',
'request_branch': REQUEST_BRANCH,
'branch_head': branch.get('commit', {}).get('id'),
'network_branch_head': network_branch.get('commit', {}).get('id'),
'pr_number': pr.get('number'),
'pr_state': pr.get('state'),
'pr_merged': pr.get('merged'),
'pr_head': pr.get('head', {}).get('sha'),
'pr_base': pr.get('base', {}).get('ref'),
'reviews': [
{
'id': item.get('id'),
'reviewer_id': item.get('user', {}).get('id'),
'state': item.get('state'),
'commit_id': item.get('commit_id'),
'dismissed': item.get('dismissed'),
}
for item in reviews
],
'governance_capability_executed': False,
'protection_mutation': False,
}
def handle(request):
if not isinstance(request, dict) or set(request) != {'operation'}:
raise RuntimeError('REQUEST_SCHEMA_INVALID')
if request['operation'] == 'DRY_RUN_SELF_TEST':
return self_test()
if request['operation'] == 'PUBLISH_SEMANTIC_RECONCILIATION':
return publish()
if request['operation'] == 'READ_SEMANTIC_RECONCILIATION_STATUS':
return read_publication_status()
raise RuntimeError('OPERATION_DENIED')
def serve(listener):
conn, _ = listener.accept()
with conn:
try:
data = conn.recv(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
raise RuntimeError('REQUEST_TOO_LARGE')
result = handle(json.loads(data.decode()))
except Exception as exc:
result = {'status': 'FAIL', 'code': str(exc)}
conn.sendall((json.dumps(result, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n').encode())
def main():
if os.environ.get('LISTEN_FDS') != '1':
raise SystemExit('SOCKET_ACTIVATION_REQUIRED')
listener = socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
serve(listener)
if __name__ == '__main__':
main()
@@ -1,73 +0,0 @@
"""Fail-closed Authority/Gitea ref reconciliation after protected merge."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
class RefReconciliationError(RuntimeError):
pass
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or len(value) != 40 or any(char not in "0123456789abcdef" for char in value):
raise RefReconciliationError(field + "_INVALID")
return value
@dataclass(frozen=True, slots=True)
class RefReconciler:
git_dir: Path
def read(self, ref: str) -> str:
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "rev-parse", "--verify", ref],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RefReconciliationError("AUTHORITY_REF_UNAVAILABLE")
return result.stdout.strip()
def is_ancestor(self, ancestor: str, descendant: str) -> bool:
_oid(ancestor, "ancestor")
_oid(descendant, "descendant")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
text=True,
capture_output=True,
check=False,
)
if result.returncode == 0:
return True
if result.returncode == 1:
return False
raise RefReconciliationError("AUTHORITY_ANCESTRY_CHECK_FAILED")
def reconcile_main(self, *, expected_oid: str, merged_oid: str, gitea_main_oid: str) -> str:
_oid(expected_oid, "expected_oid")
_oid(merged_oid, "merged_oid")
_oid(gitea_main_oid, "gitea_main_oid")
if gitea_main_oid != merged_oid:
raise RefReconciliationError("GITEA_MAIN_EXPECTATION_MISMATCH")
if not self.is_ancestor(expected_oid, merged_oid):
raise RefReconciliationError("MAIN_FF_REQUIRED")
current = self.read("refs/heads/main")
if current == merged_oid:
return merged_oid
if current != expected_oid:
raise RefReconciliationError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "update-ref", "refs/heads/main", merged_oid, expected_oid],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RefReconciliationError("AUTHORITY_MAIN_CAS_FAILED")
if self.read("refs/heads/main") != merged_oid:
raise RefReconciliationError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
return merged_oid
@@ -1,81 +0,0 @@
"""Canonical semantic branch-protection model used by AIOS governance gates."""
from __future__ import annotations
import hashlib
import json
from typing import Any, Mapping
SCHEMA = "aios.semantic-protection.v1"
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _typed_json(value: Any) -> list[Any]:
if value in (None, "null"):
return []
if isinstance(value, str):
return json.loads(value)
return list(value)
def _user_ids(names: Any) -> list[int]:
result = []
for name in names or []:
if name not in USER_IDS:
raise ValueError("UNKNOWN_GOVERNANCE_USER:" + str(name))
result.append(USER_IDS[name])
return sorted(result)
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
"""Drop transport metadata and canonicalize semantic security fields."""
return {
"schema_version": SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": protection.get("branch_name"),
"can_push": protection.get("enable_push"),
"can_force_push": protection.get("enable_force_push"),
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": protection.get("required_approvals"),
"enable_status_check": protection.get("enable_status_check"),
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
"block_admin_merge_override": protection.get("block_admin_merge_override"),
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
"enable_whitelist": protection.get("enable_push_whitelist"),
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
"require_signed_commits": protection.get("require_signed_commits"),
"priority": protection.get("priority"),
"protected_file_patterns": _typed_json(protection.get("protected_file_patterns")),
"unprotected_file_patterns": _typed_json(protection.get("unprotected_file_patterns")),
}
def semantic_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
def transport_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(protection)).hexdigest()
@@ -1,141 +0,0 @@
"""Fail-closed Stage-2 campaign gate migration predicates."""
from __future__ import annotations
import copy
import hashlib
import json
import re
from typing import Any, Mapping
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
OWNER_GATE = "owner-approval-gate"
CAMPAIGN_GATE = "campaign-approval-gate"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
class Stage2GateError(RuntimeError):
pass
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _typed_json(value: Any) -> list[Any]:
if value in (None, "", "null"):
return []
if isinstance(value, str):
return json.loads(value)
return list(value)
def _user_ids(names: Any) -> list[int]:
mapping = {"owner": 2, "aios-autonomy-agent": 153}
result = []
for name in names or []:
if name not in mapping:
raise Stage2GateError("UNKNOWN_GOVERNANCE_USER")
result.append(mapping[name])
return sorted(result)
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
return {
"schema_version": SEMANTIC_SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": protection.get("branch_name"),
"can_push": protection.get("enable_push"),
"can_force_push": protection.get("enable_force_push"),
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": protection.get("required_approvals"),
"enable_status_check": protection.get("enable_status_check"),
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
"block_admin_merge_override": protection.get("block_admin_merge_override"),
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
"force_push_allowlist_team_ids": [],
"enable_whitelist": protection.get("enable_push_whitelist"),
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
"require_signed_commits": protection.get("require_signed_commits"),
"priority": protection.get("priority"),
"protected_file_patterns": protection.get("protected_file_patterns"),
"unprotected_file_patterns": protection.get("unprotected_file_patterns"),
}
def semantic_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
def build_target(protection: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
current = list(protection.get("status_check_contexts") or [])
has_owner = OWNER_GATE in current
has_campaign = CAMPAIGN_GATE in current
if has_owner and has_campaign:
raise Stage2GateError("GATE_PRESTATE_CONFLICT")
if not has_owner and not has_campaign:
raise Stage2GateError("OWNER_GATE_MISSING")
if has_campaign:
return "IDEMPOTENT_SUCCESS", copy.deepcopy(dict(protection))
if semantic_digest(protection) != STAGE1_SEMANTIC_SHA:
raise Stage2GateError("SEMANTIC_PRECONDITION_INVALID")
target = copy.deepcopy(dict(protection))
target["status_check_contexts"] = sorted(
[gate for gate in current if gate != OWNER_GATE] + [CAMPAIGN_GATE]
)
return "READY", target
def validate_stage2_facts(facts: Mapping[str, Any]) -> None:
required = {
"campaign_id",
"campaign_active",
"campaign_revoked",
"machine_actor",
"required_checks_all_success",
"required_checks_same_head",
"unresolved_conversations",
"mergeable",
"protected_target",
"current_main_oid",
"gitea_main_oid",
"current_main_lineage_verified",
}
if set(facts) != required:
raise Stage2GateError("STAGE2_FACTS_SCHEMA_INVALID")
if facts["campaign_id"] != CAMPAIGN_ID or facts["campaign_active"] is not True or facts["campaign_revoked"] is not False:
raise Stage2GateError("CAMPAIGN_NOT_ACTIVE")
if facts["machine_actor"] != "aios-autonomy-agent":
raise Stage2GateError("MACHINE_ACTOR_MISMATCH")
if facts["required_checks_all_success"] is not True or facts["required_checks_same_head"] is not True:
raise Stage2GateError("REQUIRED_CHECKS_NOT_CURRENT")
if facts["unresolved_conversations"] != 0 or facts["mergeable"] is not True:
raise Stage2GateError("CAMPAIGN_GATE_FACTS_INVALID")
if facts["protected_target"] != "main" or facts["current_main_lineage_verified"] is not True:
raise Stage2GateError("PROTECTED_TARGET_OR_LINEAGE_INVALID")
if (
not isinstance(facts["current_main_oid"], str)
or not OID_RE.fullmatch(facts["current_main_oid"])
or facts["gitea_main_oid"] != facts["current_main_oid"]
):
raise Stage2GateError("MAIN_READBACK_MISMATCH")
-473
View File
@@ -1,473 +0,0 @@
"""Shared primitives for the V2R1C6 governed local writer contract."""
from __future__ import annotations
import hashlib
import json
import os
import posixpath
import re
import socket
import stat
import struct
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Mapping, Sequence
TASK_ID = "V2R1C6_GOVERNED_LOCAL_BOOTSTRAP"
REPO = "aios/aios"
PRINCIPAL = "governed-local-bootstrap"
OIDSIZ = 40
AUTHORIZATION_TTL = timedelta(hours=2)
class GovernedLocalWriterError(Exception):
"""Base error for governed-local-writer validation and runtime failures."""
class AuthorizationError(GovernedLocalWriterError):
"""Raised when authorization input is invalid, expired, or tampered."""
@dataclass(frozen=True)
class BranchCreatePolicy:
"""Exact branch creation contract."""
branch: str
base_oid: str
head_oid: str
@dataclass(frozen=True)
class BranchAppendPolicy:
"""Exact append-commit contract for candidate branch updates."""
branch: str
expected_head_oid: str | None
allowed_paths: tuple[str, ...]
@dataclass(frozen=True)
class BranchPolicy:
"""Per-branch operation policies loaded from authorization."""
create_branch: BranchCreatePolicy | None
append_commit: BranchAppendPolicy | None
@dataclass(frozen=True)
class GovernedAuthorization:
"""Parsed and validated authorization object."""
task_id: str
repo: str
created_at: datetime
expires_at: datetime
authorization_sha256: str
one_time_use: bool
branch_operations: dict[str, BranchPolicy]
denied_operations: tuple[str, ...]
@property
def create_branches(self) -> set[str]:
"""Branches that are allowed for branch-creation operations."""
return {
branch
for branch, policy in self.branch_operations.items()
if policy.create_branch is not None
}
@property
def append_branches(self) -> set[str]:
"""Branches that are allowed for commit-append operations."""
return {
branch
for branch, policy in self.branch_operations.items()
if policy.append_commit is not None
}
@dataclass(frozen=True)
class PeerIdentity:
"""Verified peer identity from SO_PEERCRED."""
pid: int
uid: int
gid: int
executable: str | None
@dataclass(frozen=True)
class FileChange:
"""Single file modification payload."""
path: str
content: str
@dataclass(frozen=True)
class CreateBranchRequest:
"""Validated create-branch request payload."""
branch: str
base_oid: str
head_oid: str | None
authorization_digest: str
@dataclass(frozen=True)
class AppendCommitRequest:
"""Validated append-commit request payload."""
branch: str
expected_head_oid: str
changes: tuple[FileChange, ...]
authorization_digest: str
commit_message: str = "chore(governance): append governed commit"
_OID_RE = re.compile(r"^[0-9a-fA-F]{40}$")
_DENIED_PATH_SEGMENTS = (
"main",
"tag",
"baseline",
"credential",
"credentials",
"secret",
"registry",
)
_SECRET_PATTERNS = (
re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"(?i)api[_-]?key\s*[:=]\s*\S+"),
re.compile(r"(?i)secret\s*[:=]\s*\S+"),
re.compile(r"(?i)password\s*[:=]\s*\S+"),
re.compile(r"(?i)token\s*[:=]\s*\S+"),
)
_SAFE_SECRET_MARKERS = re.compile(
r"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*(?:REDACTED|TEST_FIXTURE|NOT_A_SECRET)"
)
def canonicalize_authorization_payload(payload: Mapping[str, Any]) -> str:
"""Serialize authorization payload without the signature hash, in canonical form."""
canonical = dict(payload)
canonical.pop("authorization_sha256", None)
return json.dumps(
canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=True
)
def compute_authorization_sha256(payload: Mapping[str, Any]) -> str:
"""Compute canonical sha256 over authorization payload."""
digest = hashlib.sha256()
digest.update(canonicalize_authorization_payload(payload).encode("utf-8"))
return digest.hexdigest()
def _assert_authorization_file_permissions(path: Path) -> None:
"""Fail-closed checks for root-only secret-like authorization files."""
if path.is_symlink():
raise AuthorizationError("authorization path must not be a symlink")
st = path.stat()
if not stat.S_ISREG(st.st_mode):
raise AuthorizationError("authorization path must be a regular file")
if (st.st_mode & 0o777) != 0o600:
raise AuthorizationError("authorization file mode must be exactly 0600")
if hasattr(st, "st_uid") and os.name != "nt" and st.st_uid != 0:
raise AuthorizationError("authorization file owner must be uid 0")
def _parse_iso8601(value: str) -> datetime:
"""Parse ISO 8601 timestamps used by authorization payloads."""
try:
if value.endswith("Z"):
value = value[:-1] + "+00:00"
parsed = datetime.fromisoformat(value)
except (TypeError, ValueError) as exc:
raise AuthorizationError("created_at must be an ISO 8601 timestamp") from exc
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed
def _require_type(value: Any, expected: type, *, key: str) -> None:
if not isinstance(value, expected):
raise AuthorizationError(f"{key} must be {expected.__name__}")
def _parse_oid(value: Any, *, field: str) -> str:
if not isinstance(value, str):
raise AuthorizationError(f"{field} must be a string")
if not _OID_RE.fullmatch(value):
raise AuthorizationError(f"{field} must be a 40-char hex object id")
return value.lower()
def _parse_allowed_paths(paths: Any) -> tuple[str, ...]:
if not isinstance(paths, list) or not paths:
raise AuthorizationError("allowed_paths must be a non-empty list")
normalized: list[str] = []
for path in paths:
if not isinstance(path, str) or not path.strip():
raise AuthorizationError("allowed_paths entries must be non-empty strings")
normalized.append(path.strip())
return tuple(normalized)
def parse_authorization(
raw: Mapping[str, Any], *, now: datetime | None = None
) -> GovernedAuthorization:
"""Validate an authorization payload and return a typed object."""
_require_type(raw.get("task_id"), str, key="task_id")
if raw["task_id"] != TASK_ID:
raise AuthorizationError("unexpected task_id")
_require_type(raw.get("repo"), str, key="repo")
if raw["repo"] != REPO:
raise AuthorizationError("unexpected repo value")
created_at = _parse_iso8601(raw.get("created_at"))
check_time = now or datetime.now(tz=timezone.utc)
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
if created_at - check_time > timedelta(seconds=30):
raise AuthorizationError("authorization created_at is in the future")
if check_time - created_at > AUTHORIZATION_TTL:
raise AuthorizationError("authorization expired")
raw_expires_at = raw.get("expires_at")
expires_at = (
_parse_iso8601(raw_expires_at)
if raw_expires_at is not None
else created_at + AUTHORIZATION_TTL
)
if expires_at <= check_time:
raise AuthorizationError("authorization expiry is in the past")
if expires_at > created_at + AUTHORIZATION_TTL:
raise AuthorizationError("authorization expiry exceeds two-hour maximum")
_require_type(raw.get("one_time_use"), bool, key="one_time_use")
if raw["one_time_use"] is not True:
raise AuthorizationError("one_time_use must be true")
_require_type(raw.get("authorization_sha256"), str, key="authorization_sha256")
expected_hash = compute_authorization_sha256(raw)
provided_hash = str(raw["authorization_sha256"]).lower()
if provided_hash != expected_hash:
raise AuthorizationError("authorization_sha256 mismatch")
op_map = raw.get("allowed_branch_operations")
if not isinstance(op_map, dict) or not op_map:
raise AuthorizationError("allowed_branch_operations must be a non-empty object")
branch_operations: dict[str, BranchPolicy] = {}
for branch, operation in op_map.items():
if not isinstance(branch, str) or not branch.strip():
raise AuthorizationError("branch names must be non-empty strings")
if not isinstance(operation, dict):
raise AuthorizationError("operation policy must be an object")
create_block = operation.get("create_branch")
append_block = operation.get("append_commit")
create_policy: BranchCreatePolicy | None = None
append_policy: BranchAppendPolicy | None = None
if create_block is not None:
if not isinstance(create_block, dict):
raise AuthorizationError("create_branch block must be an object")
raw_head_oid = create_block.get("head_oid")
create_policy = BranchCreatePolicy(
branch=branch,
base_oid=_parse_oid(create_block.get("base_oid"), field="create_branch.base_oid"),
head_oid=(
_parse_oid(raw_head_oid, field="create_branch.head_oid")
if raw_head_oid is not None
else None
),
)
if append_block is not None:
if not isinstance(append_block, dict):
raise AuthorizationError("append_commit block must be an object")
raw_expected_head_oid = append_block.get("expected_head_oid")
append_policy = BranchAppendPolicy(
branch=branch,
expected_head_oid=(
_parse_oid(
raw_expected_head_oid,
field="append_commit.expected_head_oid",
)
if raw_expected_head_oid is not None
else None
),
allowed_paths=_parse_allowed_paths(append_block.get("allowed_paths")),
)
if create_policy is None and append_policy is None:
raise AuthorizationError("branch policy must define at least one operation")
branch_operations[branch] = BranchPolicy(
create_branch=create_policy, append_commit=append_policy
)
denied_raw = raw.get("denied_operations", ())
if not isinstance(denied_raw, list):
raise AuthorizationError("denied_operations must be a list")
denied_operations: list[str] = []
for value in denied_raw:
if not isinstance(value, str):
raise AuthorizationError("denied operations must be strings")
denied_operations.append(value.lower())
return GovernedAuthorization(
task_id=raw["task_id"],
repo=raw["repo"],
created_at=created_at,
expires_at=expires_at,
authorization_sha256=provided_hash,
one_time_use=True,
branch_operations=branch_operations,
denied_operations=tuple(denied_operations),
)
def load_authorization_file(
path: str | os.PathLike[str], *, now: datetime | None = None
) -> GovernedAuthorization:
"""Load and validate the root-only authorization file."""
auth_path = Path(path)
_assert_authorization_file_permissions(auth_path)
try:
raw_text = auth_path.read_text(encoding="utf-8")
except (OSError, ValueError) as exc:
raise AuthorizationError("unable to read authorization file") from exc
try:
raw_obj = json.loads(raw_text)
except json.JSONDecodeError as exc:
raise AuthorizationError("authorization must be JSON") from exc
if not isinstance(raw_obj, dict):
raise AuthorizationError("authorization top-level must be an object")
return parse_authorization(raw_obj, now=now)
def _resolve_peer_executable(pid: int) -> str | None:
"""Resolve peer executable from /proc/<pid>/exe when available."""
if pid <= 0:
return None
exe_path = f"/proc/{pid}/exe"
try:
return os.readlink(exe_path)
except OSError:
return None
def extract_peer_identity(
connection: socket.socket, *, require_root_uid: bool = True
) -> PeerIdentity:
"""Validate SO_PEERCRED and return peer identity."""
if not hasattr(socket, "SO_PEERCRED"):
raise AuthorizationError("SO_PEERCRED not available on this platform")
data = connection.getsockopt(
socket.SOL_SOCKET,
socket.SO_PEERCRED,
struct.calcsize("3i"),
)
if len(data) != struct.calcsize("3i"):
raise AuthorizationError("SO_PEERCRED payload size mismatch")
pid, uid, gid = struct.unpack("3i", data)
if require_root_uid and uid != 0:
raise PermissionError("peer uid must be 0")
return PeerIdentity(
pid=pid,
uid=uid,
gid=gid,
executable=_resolve_peer_executable(pid),
)
def _normalize_file_path(path: str) -> str:
normalized = posixpath.normpath(path.replace("\\", "/").strip())
return normalized.lstrip("/")
def _path_has_disallowed_segment(path: str) -> bool:
lowered = path.lower()
if ".." in path.split("/"):
return True
parts = [p for p in lowered.split("/") if p]
for part in parts:
for segment in _DENIED_PATH_SEGMENTS:
if part == segment or part.startswith(f"{segment}.") or segment in part:
return True
return False
def _is_allowed_path(path: str, allowed_paths: Sequence[str]) -> bool:
normalized = _normalize_file_path(path)
for prefix in allowed_paths:
normalized_prefix = _normalize_file_path(prefix)
if normalized == normalized_prefix:
return True
if normalized.startswith(normalized_prefix + "/"):
return True
return False
def contains_secret_like_content(content: str) -> bool:
"""Heuristic detector for obvious secret-like strings."""
content = _SAFE_SECRET_MARKERS.sub("", content or "")
for pattern in _SECRET_PATTERNS:
if pattern.search(content):
return True
return False
def validate_file_changes(
changes: Any, allowed_paths: Sequence[str]
) -> tuple[FileChange, ...]:
"""Validate append-commit file changes against path and content policy."""
if not isinstance(changes, list):
raise AuthorizationError("changes must be a list")
result: list[FileChange] = []
for change in changes:
if not isinstance(change, Mapping):
raise AuthorizationError("each file change must be an object")
raw_path = change.get("path")
raw_content = change.get("content")
if not isinstance(raw_path, str) or not raw_path.strip():
raise AuthorizationError("file change path must be a non-empty string")
if raw_path.startswith(("/", "\\")):
raise AuthorizationError("file change path must be relative")
if not isinstance(raw_content, str):
raise AuthorizationError("file change content must be a string")
normalized = _normalize_file_path(raw_path)
if not normalized or normalized.startswith("../") or normalized == "..":
raise AuthorizationError("file paths must be normalized relative paths")
if not _is_allowed_path(normalized, allowed_paths):
raise AuthorizationError("file path outside allowed_paths")
if _path_has_disallowed_segment(normalized):
raise AuthorizationError("file path uses denied segment")
if contains_secret_like_content(raw_content):
raise AuthorizationError("file content failed secret-like scan")
result.append(FileChange(path=normalized, content=raw_content))
return tuple(result)
-47
View File
@@ -1,47 +0,0 @@
"""Supplement Gitea native protection when review identity is not expressible."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Sequence
class MergeGateDenied(RuntimeError):
pass
@dataclass(frozen=True)
class MergeGateDecision:
allowed: bool
owner_review_id: int
required_contexts: tuple[str, ...]
def validate_protected_merge(
pull_request: Mapping[str, Any],
reviews: Sequence[Mapping[str, Any]],
statuses: Mapping[str, str],
*,
required_contexts: Sequence[str],
unresolved_conversations: int,
owner_login: str = "owner",
owner_id: int = 2,
) -> MergeGateDecision:
if pull_request.get("state") != "open" or pull_request.get("mergeable") is not True:
raise MergeGateDenied("pull_request_not_mergeable")
author = (pull_request.get("user") or {}).get("login")
owner_review = None
for review in reviews:
user = review.get("user") or {}
if review.get("state") == "APPROVED" and user.get("login") == owner_login and user.get("id") == owner_id:
if author == owner_login or (pull_request.get("author_id") == owner_id):
raise MergeGateDenied("pr_author_self_approval_denied")
owner_review = review
if owner_review is None:
raise MergeGateDenied("owner_review_required")
if unresolved_conversations != 0:
raise MergeGateDenied("unresolved_conversations_block_merge")
missing = [context for context in required_contexts if statuses.get(context) != "success"]
if missing:
raise MergeGateDenied("required_checks_missing:" + ",".join(missing))
return MergeGateDecision(True, int(owner_review.get("id", 0)), tuple(required_contexts))
@@ -1,568 +0,0 @@
"""Governed V2R2C1 production release orchestration.
This module is deliberately scoped to the Owner-approved V2R2C1 binding. It
uses the existing TagPublisher and metadata adapters; it does not introduce a
second Git writer, a second durable job store, or an alternate baseline.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
import re
import subprocess
import tarfile
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationManualOwnerReviewRequired,
PublicationPending,
PublicationReconciliationRequired,
PublicationRequest,
TagPublisher,
)
VERSION = "V2R2C1"
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
REPOSITORY = "aios/aios"
SERVICE = "aios-v2r1c7-production.service"
PORT = 18927
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
class ReleaseBlocked(RuntimeError):
"""A release gate failed and no unsafe recovery is permitted."""
class ManualOwnerReviewRequired(ReleaseBlocked):
pass
class ReconciliationRequired(ReleaseBlocked):
pass
def canonical(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
def sha256_file(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked("artifact must be a regular file")
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_bytes(canonical(dict(value)))
os.chmod(temporary, mode)
os.replace(temporary, path)
@dataclass(frozen=True)
class ReleaseBinding:
version: str = VERSION
target_oid: str = TARGET_OID
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
approval_plan_id: str = APPROVAL_PLAN_ID
repository: str = REPOSITORY
production_service: str = SERVICE
production_port: int = PORT
def validate(self) -> None:
if self.version != VERSION or self.repository != REPOSITORY:
raise ManualOwnerReviewRequired("release binding is outside V2R2C1 scope")
if not OID_RE.fullmatch(self.target_oid) or self.target_oid != TARGET_OID:
raise ManualOwnerReviewRequired("V2R2C1 target_oid mismatch")
if not OID_RE.fullmatch(self.evidence_closure_oid) or self.evidence_closure_oid != EVIDENCE_CLOSURE_OID:
raise ManualOwnerReviewRequired("V2R2C1 evidence closure mismatch")
if self.approval_plan_id != APPROVAL_PLAN_ID:
raise ManualOwnerReviewRequired("V2R2C1 approval plan mismatch")
if self.production_service != SERVICE or int(self.production_port) != PORT:
raise ManualOwnerReviewRequired("production service binding mismatch")
def as_dict(self) -> dict[str, Any]:
return {
"version": self.version,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"approval_plan_id": self.approval_plan_id,
"repository": self.repository,
"production_service": self.production_service,
"production_port": self.production_port,
}
class StateLock:
def __init__(self, path: Path):
self.path = path
self.handle = None
def __enter__(self) -> "StateLock":
self.path.parent.mkdir(parents=True, exist_ok=True)
self.handle = self.path.open("a+", encoding="utf-8")
fcntl.flock(self.handle.fileno(), fcntl.LOCK_EX)
return self
def __exit__(self, exc_type, exc, tb) -> None:
if self.handle is not None:
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
self.handle.close()
class ReleaseState:
def __init__(self, root: Path, binding: ReleaseBinding):
self.root = root
self.path = root / "release-state.json"
self.binding = binding
root.mkdir(parents=True, exist_ok=True)
if self.path.exists():
value = json.loads(self.path.read_text(encoding="utf-8"))
if value.get("binding") != binding.as_dict():
raise ManualOwnerReviewRequired("persisted release binding conflict")
self.value = value
else:
self.value = {
"schema": "aios.v2r2c1.governed-production-release-state.v1",
"binding": binding.as_dict(),
"state": "INITIAL",
"completed": {},
"history": [],
}
self.save()
def save(self) -> None:
atomic_json(self.path, self.value)
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
safe = json.loads(json.dumps(dict(result)))
self.value["state"] = state
self.value.setdefault("completed", {})[step] = safe
self.value.setdefault("history", []).append(
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "step": step, "result": safe}
)
self.save()
def fail(self, state: str, reason: str) -> None:
self.value["state"] = state
self.value.setdefault("history", []).append(
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "reason": str(reason)[:500]}
)
self.save()
def _git(git_dir: Path, *args: str, check: bool = True) -> str:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=False,
capture_output=True,
text=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if check and completed.returncode != 0:
raise ReconciliationRequired(completed.stderr.strip() or "authoritative git query failed")
return completed.stdout.strip()
def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
return subprocess.run(
["git", f"--git-dir={git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
).returncode == 0
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
binding.validate()
if _git(git_dir, "cat-file", "-t", binding.target_oid) != "commit":
raise ReleaseBlocked("approved target_oid is not a commit")
if _git(git_dir, "cat-file", "-t", binding.evidence_closure_oid) != "commit":
raise ReleaseBlocked("evidence closure is not a commit")
if not _ancestor(git_dir, binding.target_oid, binding.evidence_closure_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of evidence closure")
return {
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"target_is_ancestor_of_evidence": True,
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
}
def verify_owner_approval(value: Mapping[str, Any], binding: ReleaseBinding) -> dict[str, Any]:
approval = value.get("owner_approval")
if not isinstance(approval, Mapping):
raise ReleaseBlocked("OWNER_APPROVAL_RECEIPT_REQUIRED")
if approval.get("approved") is not True or approval.get("status") != "APPROVED":
raise ReleaseBlocked("OWNER_APPROVAL_NOT_ACCEPTED")
if approval.get("approved_by") != "aiman":
raise ManualOwnerReviewRequired("OWNER_APPROVAL_ACTOR_MISMATCH")
if approval.get("approval_source") != "explicit_owner_approval_in_chat":
raise ManualOwnerReviewRequired("OWNER_APPROVAL_SOURCE_MISMATCH")
for key, expected in binding.as_dict().items():
if key in {"repository", "production_service", "production_port"}:
continue
if approval.get(key) != expected:
raise ManualOwnerReviewRequired("OWNER_APPROVAL_BINDING_MISMATCH:" + key)
return dict(approval)
def _payload_file(git_dir: Path, oid: str, path: str) -> bytes:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", "show", f"{oid}:{path}"],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("required release source missing:" + path)
return completed.stdout
def _commit_epoch(git_dir: Path, oid: str) -> int:
return int(_git(git_dir, "show", "-s", "--format=%ct", oid))
def build_production_artifact(
git_dir: Path,
binding: ReleaseBinding,
output_root: Path,
) -> dict[str, Any]:
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
binding.validate()
verify_lineage(git_dir, binding)
epoch = _commit_epoch(git_dir, binding.target_oid)
output_root.mkdir(parents=True, exist_ok=True)
stem = f"aios-{VERSION}-{binding.target_oid[:12]}"
output = output_root / (stem + ".tar.zst")
with tempfile.TemporaryDirectory(prefix="v2r2c1-artifact-") as temp:
root = Path(temp) / "payload"
root.mkdir(parents=True)
source_paths = {
"active-runtime/app/deployment_state.py": "active-runtime/app/deployment_state.py",
"active-runtime/app/v2r1c6_privacy_tools.py": "active-runtime/app/v2r1c6_privacy_tools.py",
}
for destination, source in source_paths.items():
target = root / destination
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
generated: dict[str, str] = {
"VERSION": VERSION + "\n",
"active-runtime/app/v2r2c1_health_service.py": (
"from v2r1c6_privacy_tools import main\n\n"
"if __name__ == '__main__':\n raise SystemExit(main())\n"
),
"deploy/aios-v2/systemd/aios-v2r1c7-production.service": (
"[Unit]\nDescription=AIOS V2R2C1 governed health contract service\n"
"After=network.target\n\n[Service]\nType=simple\n"
"User=aios-v2r1c7\nGroup=aios-v2r1c7\n"
"WorkingDirectory=/opt/aios/v2r2c1\n"
"EnvironmentFile=/etc/aios/aios-v2r2c1.env\n"
"ExecStart=/usr/bin/python3 /opt/aios/v2r2c1/active-runtime/app/v2r2c1_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}\n"
"Restart=on-failure\nRestartSec=5\nNoNewPrivileges=true\nPrivateTmp=true\n"
"ProtectHome=true\nProtectSystem=strict\nProtectKernelLogs=true\n"
"ProtectKernelModules=true\nProtectKernelTunables=true\nProtectControlGroups=true\n"
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nIPAddressDeny=any\n"
"IPAddressAllow=localhost\nUMask=0077\n\n[Install]\nWantedBy=multi-user.target\n"
),
"deploy/aios-v2/v2r2c1-runtime-config.json": json.dumps({
"schema": "aios.v2r2c1.production-runtime.v1",
"version": VERSION,
"target_oid": binding.target_oid,
"service": SERVICE,
"port": PORT,
"health_path": "/health",
"deployment_state_source": "environment_and_state_file",
}, sort_keys=True, separators=(",", ":")) + "\n",
"deploy/aios-v2/v2r2c1-rollback.json": json.dumps({
"schema": "aios.v2r2c1.rollback-metadata.v1",
"rollback_version": "V2R1C7",
"rollback_service": SERVICE,
"rollback_target_is_previous_production": True,
"automatic_rollback": False,
}, sort_keys=True, separators=(",", ":")) + "\n",
"scripts/probe_v2r2c1_health.py": (
"import json,sys,urllib.request\n"
"url=sys.argv[1] if len(sys.argv)>1 else 'http://127.0.0.1:18927/health'\n"
"with urllib.request.urlopen(url, timeout=5) as r: print(json.dumps(json.load(r), sort_keys=True))\n"
),
"scripts/rollback_v2r2c1_release.sh": "#!/bin/sh\nset -eu\nprintf '%s\\n' 'rollback requires governed owner-approved executor'\nexit 2\n",
"release-manifests/V2R2C1.json": json.dumps({
"schema": "aios.release-source.v2",
"version": VERSION,
"repository": REPOSITORY,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"production_deployed": False,
"deployment_state": "STAGING",
"artifact_kind": "executable-production-health",
}, sort_keys=True, separators=(",", ":")) + "\n",
}
for rel, text in generated.items():
target = root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding="utf-8")
if rel.startswith("scripts/"):
target.chmod(0o755)
metadata = {
"schema": "aios.release-artifact-build.v3",
"version": VERSION,
"repository": REPOSITORY,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"source_date_epoch": epoch,
"clean_source": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
"secrets_included": False,
}
(root / "BUILD-METADATA.json").write_bytes(canonical(metadata))
(root / "ROLLBACK-METADATA.json").write_bytes(canonical(json.loads((root / "deploy/aios-v2/v2r2c1-rollback.json").read_text(encoding="utf-8"))))
names = sorted("payload/" + str(path.relative_to(root)).replace(os.sep, "/") for path in root.rglob("*") if path.is_file())
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
"schema": "aios.release-artifact-manifest.v3",
"version": VERSION,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"repository": REPOSITORY,
"files": names + ["payload/ARTIFACT-MANIFEST.json"],
"deployable": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
}))
tar_path = Path(temp) / "release.tar"
with tarfile.open(tar_path, "w") as archive:
for path in sorted(root.rglob("*")):
relative = str(path.relative_to(root)).replace(os.sep, "/")
info = archive.gettarinfo(str(path), arcname="payload/" + relative)
info.uid = info.gid = 0
info.uname = "root"
info.gname = "root"
info.mtime = epoch
info.mode = 0o755 if path.is_dir() or path.suffix == ".sh" else 0o644
if path.is_file():
with path.open("rb") as stream:
archive.addfile(info, stream)
else:
archive.addfile(info)
subprocess.run(["zstd", "-T1", "-19", "--no-progress", "-q", "-f", str(tar_path), "-o", str(output)], check=True, timeout=120)
digest = sha256_file(output)
receipt = {
"schema": "aios.release-artifact-receipt.v3",
"version": VERSION,
"repository": REPOSITORY,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact": str(output),
"artifact_sha256": digest,
"artifact_size": output.stat().st_size,
"deployable": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
}
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
return receipt
class GovernedProductionReleaseExecutor:
def __init__(self, config: Any):
self.config = config
self.binding = ReleaseBinding()
self.binding.validate()
self.root = Path(config.state_root) / "v2r2c1-governed-production-release"
self.state = ReleaseState(self.root, self.binding)
workspace = self.root / "publication-workspace"
workspace.mkdir(parents=True, exist_ok=True)
self.publisher = TagPublisher(
workspace,
GitTagBackend(workspace, authoritative_repo=Path(config.authoritative_git_dir)),
GiteaTagClient(config.api_base, REPOSITORY, Path(config.token_path)),
config.remote_name,
state_root=self.root / "tag-publication",
)
def _request(self, request: Mapping[str, Any]) -> PublicationRequest:
binding = self.binding
for key, expected in binding.as_dict().items():
if key in request and request[key] != expected:
raise ManualOwnerReviewRequired("release request binding mismatch:" + key)
return PublicationRequest(binding.version, binding.target_oid, binding.evidence_closure_oid, binding.approval_plan_id)
def approval_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
if request.get("execute") is not False:
raise ReleaseBlocked("TAG_APPROVAL_REQUEST_MUST_BE_DRY_RUN")
owner = verify_owner_approval(request, self.binding)
with StateLock(self.root / "release.lock"):
current = self.state.value.get("approval")
approval_digest = hashlib.sha256(canonical(owner)).hexdigest()
record = {
"schema": "aios.v2r2c1.tag-approval-request.v1",
"binding": self.binding.as_dict(),
"owner_approval": owner,
"owner_approval_digest_sha256": approval_digest,
"execute": False,
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
if current and current != record:
raise ManualOwnerReviewRequired("persisted Owner approval differs")
self.state.value["approval"] = record
self.state.record("APPROVAL_REQUEST_RECORDED", "tag_approval_request", record)
return {"status": "RECORDED", "execute": False, "binding": self.binding.as_dict(), "owner_approval_digest_sha256": approval_digest}
def _require_approval(self) -> None:
approval = self.state.value.get("approval")
if not isinstance(approval, Mapping) or approval.get("binding") != self.binding.as_dict():
raise ReleaseBlocked("OWNER_APPROVAL_REQUEST_NOT_RECORDED")
def publish_tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
if request.get("execute") is not True:
raise ReleaseBlocked("TAG_PUBLICATION_REQUIRES_EXECUTE_TRUE")
self._require_approval()
publication = self._request(request)
with StateLock(self.root / "release.lock"):
try:
verified = self.publisher._verify_publication(publication, allow_push=True, previously_deployed=False)
except PublicationManualOwnerReviewRequired as exc:
self.state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
raise ManualOwnerReviewRequired(str(exc)) from exc
except PublicationPending as exc:
self.state.fail("REMOTE_PUSH_PENDING", str(exc))
raise ReconciliationRequired(str(exc)) from exc
except PublicationReconciliationRequired as exc:
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
raise ReconciliationRequired(str(exc)) from exc
result = dict(verified)
self.state.record("TAG_VERIFIED", "tag", result)
return {"status": "REMOTE_VERIFIED", **result}
def tag_status(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._request(request)
return self.publisher.get_tag_publication_status(VERSION)
def build_artifact(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._require_approval()
self._request(request)
receipt = build_production_artifact(Path(self.config.authoritative_git_dir), self.binding, self.root / "artifacts")
self.state.record("ARTIFACT_VERIFIED", "production_artifact", receipt)
return {"status": "VERIFIED", **receipt}
def execute_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._require_approval()
publication = self._request(request)
tag_state = self.state.value.get("completed", {}).get("tag")
if not isinstance(tag_state, Mapping) or tag_state.get("state") not in {"GITEA_API_CONFIRMED", "REMOTE_VERIFIED"}:
raise ReleaseBlocked("REMOTE_TAG_PROOF_REQUIRED")
artifact = self.state.value.get("completed", {}).get("production_artifact")
if not isinstance(artifact, Mapping):
artifact = self.build_artifact(request)
value = {
**self.binding.as_dict(),
"tag_oid": tag_state.get("tag_oid") or tag_state.get("gitea_tag_oid"),
"artifact_path": artifact["artifact"],
"artifact_size": int(artifact["artifact_size"]),
"artifact_sha256": artifact["artifact_sha256"],
}
# The production adapter is privileged and is run through the root
# approved-release service boundary by the caller. This handler is
# still useful for a root-owned staging invocation and remains
# fail-closed when the adapter is not available.
import v2r2c1_production_adapter
runtime = v2r2c1_production_adapter.execute_request({"schema": "aios.production-executor-request.v1", "operation": "apply", **value})
if runtime.get("status") != "ACTIVE_VERIFIED":
self.state.fail("RECONCILIATION_REQUIRED", "production health did not verify")
raise ReconciliationRequired("production health did not verify")
self.state.record("PRODUCTION_VERIFIED", "production", runtime)
return {"status": "PRODUCTION_VERIFIED", "runtime": runtime, "artifact": artifact, "tag_oid": value["tag_oid"]}
def consistency(self, request: Mapping[str, Any]) -> dict[str, Any]:
publication = self._request(request)
result = self.publisher.get_release_consistency(publication)
result["release_binding"] = self.binding.as_dict()
result["production_health"] = _health(self.config.health_url, self.binding, self.state.value.get("completed", {}).get("production_artifact", {}))
result["registry"] = _read_json(Path(self.config.registry_root) / f"{VERSION}.json")
result["baseline"] = _read_json(Path(self.config.baseline_path))
return result
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
result = self.consistency(request)
result["accepted"] = result.get("status") == "PASS" and result.get("production_health", {}).get("status") == "ACTIVE_VERIFIED"
self.state.record("ACCEPTED" if result["accepted"] else "RECONCILIATION_REQUIRED", "acceptance", result)
return result
def _read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {"status": "MISSING", "path": str(path)}
if not isinstance(value, dict):
return {"status": "INVALID", "path": str(path)}
return {"status": "PRESENT", "path": str(path), **value}
def _health(url: str, binding: ReleaseBinding, artifact: Mapping[str, Any]) -> dict[str, Any]:
import urllib.request
try:
with urllib.request.urlopen(url, timeout=5) as response:
body = json.loads(response.read().decode("utf-8"))
status = response.status
except Exception as exc:
return {"status": "UNAVAILABLE", "error": type(exc).__name__}
expected_artifact = artifact.get("artifact_sha256")
exact = (
status == 200
and body.get("version") == binding.version
and body.get("commit_oid") == binding.target_oid
and (not expected_artifact or body.get("artifact_sha256") == expected_artifact)
and body.get("deployment_state") == "ACTIVE"
and body.get("production_deployed") is True
)
return {"status": "ACTIVE_VERIFIED" if exact else "CONFLICT", "http_status": status, **body}
def build_executor_config(config: Any) -> GovernedProductionReleaseExecutor:
return GovernedProductionReleaseExecutor(config)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("operation", choices=("build-artifact", "status"))
parser.add_argument("--git-dir", type=Path, default=Path("/data/aios-gitops/repo.git"))
parser.add_argument("--output-root", type=Path, default=Path("/var/lib/aios-gitops/approved-artifacts/V2R2C1"))
args = parser.parse_args()
binding = ReleaseBinding()
if args.operation == "build-artifact":
print(json.dumps(build_production_artifact(args.git_dir, binding, args.output_root), sort_keys=True))
else:
print(json.dumps({"binding": binding.as_dict(), "status": "READY"}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
-297
View File
@@ -1,297 +0,0 @@
#!/usr/bin/env python3
"""Thin production tag adapter over AIOS's existing mature TagPublisher.
This adapter deliberately does not call TagPublisher.publish(), because that
legacy method also mutates Registry/Baseline. The production executor requires:
Tag -> runtime ACTIVE -> Registry -> Baseline.
`apply` reuses TagPublisher._verify_publication() under the publisher's durable
per-version lock. That method performs the existing fail-closed annotated-tag,
network remote, Gitea API, peeled-target and listing verification, but stops
before Registry/Baseline mutation.
`probe` is read-only.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Any, Mapping
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationConflict,
PublicationManualOwnerReviewRequired,
PublicationPending,
PublicationReconciliationRequired,
PublicationRequest,
PublicationState,
TagPublisher,
)
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
def load_request() -> dict[str, Any]:
value = json.load(sys.stdin)
if not isinstance(value, dict):
raise ValueError("request_object_required")
if value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
try:
validate_release_version(value.get("version", ""))
except ValueError as exc:
raise ValueError("version_invalid") from exc
for name in ("target_oid", "evidence_closure_oid"):
if not OID_RE.fullmatch(str(value.get(name, ""))):
raise ValueError(f"{name}_invalid")
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
raise ValueError("approval_plan_id_invalid")
if value.get("repository_identity") != "aios/aios":
raise ValueError("repository_identity_invalid")
return value
def publication_request(value: Mapping[str, Any]) -> PublicationRequest:
return PublicationRequest(
str(value["version"]),
str(value["target_oid"]),
str(value["evidence_closure_oid"]),
str(value["approval_plan_id"]),
)
def response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
return {
"status": status,
"version": value["version"],
"target_oid": value["target_oid"],
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
**extra,
}
def build(args: argparse.Namespace) -> tuple[TagPublisher, GitTagBackend, GiteaTagClient]:
git = GitTagBackend(args.workspace, authoritative_repo=args.repository)
gitea = GiteaTagClient(
args.gitea_api,
args.gitea_repo,
args.gitea_token_file,
token_line=args.gitea_token_line,
)
publisher = TagPublisher(
args.workspace,
git,
gitea,
args.remote,
state_root=args.state_root,
)
return publisher, git, gitea
def _gitea_lookup(gitea: GiteaTagClient, version: str) -> tuple[str, str] | None:
try:
return gitea.lookup_tag(version)
except RuntimeError as exc:
if "HTTP 404" in str(exc):
return None
raise
def probe(
args: argparse.Namespace,
value: Mapping[str, Any],
publisher: TagPublisher,
git: GitTagBackend,
gitea: GiteaTagClient,
) -> dict[str, Any]:
request = publication_request(value)
request.validate()
git.validate_evidence_lineage(request)
local_oid = git._rev(f"refs/tags/{request.version}")
local_target = git._rev(f"refs/tags/{request.version}^{{commit}}")
remote = git.remote_tag(args.remote, request.version)
api = _gitea_lookup(gitea, request.version)
listing = False
if api is not None:
listing = gitea.tag_visible_in_listing(request.version, request.target_oid)
if local_oid is None and remote is None and api is None:
return response(value, "NOT_APPLIED")
if local_oid is not None:
if local_target is None or local_oid == local_target:
return response(value, "CONFLICT", reason="local_tag_not_annotated")
if local_target != request.target_oid:
return response(
value,
"CONFLICT",
reason="local_peeled_target_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
# A remote/Gitea tag without the Authority tag object is not safe to
# recreate, because a new annotated tag would have a different object OID.
if local_oid is None and (remote is not None or api is not None):
return response(
value,
"INDETERMINATE",
reason="remote_or_gitea_tag_exists_without_authority_tag_object",
)
if remote is not None:
if remote[0] != local_oid or remote[1] != request.target_oid:
return response(
value,
"CONFLICT",
reason="network_remote_tag_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
remote_tag_oid=remote[0],
remote_peeled_target_oid=remote[1],
)
if api is not None:
if api[0] != local_oid or api[1] != request.target_oid:
return response(
value,
"CONFLICT",
reason="gitea_api_tag_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
gitea_tag_oid=api[0],
gitea_peeled_target_oid=api[1],
)
if (
local_oid is not None
and remote is not None
and api is not None
and listing
):
return response(
value,
"APPLIED_VERIFIED",
tag_oid=local_oid,
peeled_target_oid=local_target,
remote_tag_oid=remote[0],
remote_peeled_target_oid=remote[1],
gitea_tag_oid=api[0],
gitea_peeled_target_oid=api[1],
gitea_listing_visible=True,
)
if local_oid is not None and remote is None and api is None:
return response(
value,
"PARTIAL_LOCAL",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
return response(
value,
"INDETERMINATE",
reason="tag_visibility_partial",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
def apply(
args: argparse.Namespace,
value: Mapping[str, Any],
publisher: TagPublisher,
git: GitTagBackend,
gitea: GiteaTagClient,
) -> dict[str, Any]:
before = probe(args, value, publisher, git, gitea)
if before["status"] in {"APPLIED_VERIFIED", "CONFLICT", "INDETERMINATE"}:
return before
request = publication_request(value)
with publisher.store.lock(request.version):
request.validate()
publisher._assert_fingerprint(request)
publisher.store.record(request, PublicationState.PENDING)
publisher._verify_publication(
request,
allow_push=True,
previously_deployed=False,
)
return probe(args, value, publisher, git, gitea)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--workspace", type=Path, default=Path(os.environ["AIOS_GITOPS_CONTROLLER_WORKSPACE"]))
parser.add_argument("--repository", type=Path, default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]))
parser.add_argument("--remote", default=os.getenv("AIOS_GITOPS_GITEA_REMOTE", "gitea"))
parser.add_argument("--gitea-api", default=os.getenv("AIOS_GITOPS_GITEA_API_URL", ""))
parser.add_argument("--gitea-repo", default=os.getenv("AIOS_GITOPS_GITEA_REPOSITORY", ""))
parser.add_argument(
"--gitea-token-file",
type=Path,
default=Path(os.getenv("AIOS_GITOPS_GITEA_TOKEN_FILE", "")),
)
parser.add_argument(
"--gitea-token-line",
type=int,
default=int(os.getenv("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")),
)
parser.add_argument(
"--state-root",
type=Path,
default=Path(
os.getenv(
"AIOS_GITOPS_PUBLICATION_STATE_DIR",
"/var/lib/aios-gitops/tag-publications",
)
),
)
args = parser.parse_args()
value = load_request()
publisher, git, gitea = build(args)
operation = value.get("operation")
if operation == "probe":
result = probe(args, value, publisher, git, gitea)
elif operation == "apply":
result = apply(args, value, publisher, git, gitea)
else:
raise ValueError("unsupported_operation")
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except PublicationManualOwnerReviewRequired as exc:
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
raise SystemExit(20)
except PublicationConflict as exc:
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
raise SystemExit(20)
except (PublicationPending, PublicationReconciliationRequired) as exc:
print(json.dumps({"status": "INDETERMINATE", "reason": str(exc)}, sort_keys=True))
raise SystemExit(21)
except Exception as exc:
print(
json.dumps(
{"status": "INDETERMINATE", "error": type(exc).__name__, "reason": str(exc)},
sort_keys=True,
)
)
raise SystemExit(22)
-167
View File
@@ -1,167 +0,0 @@
"""Fail-closed operation authorization for an isolated Git writer."""
from __future__ import annotations
import ipaddress
import os
import re
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from isolated_writer_token_registry import TokenRegistry
ROUTE_OPERATIONS = {
("GET", "/api/runtime-status"): "runtime.status.read",
("GET", "/api/baseline"): "baseline.read",
("GET", "/api/patches"): "patches.read",
("POST", "/api/commit-file"): "commit_file.atomic",
}
OID_RE = re.compile(r"[0-9a-f]{40}")
class AuthorizationError(PermissionError):
def __init__(self, code: str, status: int = 403):
super().__init__(code)
self.code = code
self.status = status
@dataclass(frozen=True)
class AuthorizedGrant:
record: dict[str, Any]
operation: str
use_count: int
@property
def token_id(self) -> str:
return str(self.record["token_id"])
@property
def task_id(self) -> str:
return str(self.record["task_id"])
def operation_for(method: str, path: str) -> str:
operation = ROUTE_OPERATIONS.get((method.upper(), path))
if not operation:
raise AuthorizationError("isolated_writer_operation_forbidden")
return operation
def _parse_time(value: Any) -> datetime:
try:
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
except (TypeError, ValueError):
raise AuthorizationError("writer_token_time_invalid") from None
if parsed.tzinfo is None:
raise AuthorizationError("writer_token_time_invalid")
return parsed.astimezone(timezone.utc)
def _git_is_ancestor(workspace: Path, base: str, branch: str) -> bool:
result = subprocess.run(
["git", "merge-base", "--is-ancestor", base, branch],
cwd=workspace,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return result.returncode == 0
def _validate_grant(
grant: dict[str, Any],
*,
audience: str,
source_ip: str,
operation: str,
branch: str | None,
path: str | None,
workspace: Path,
current_time: datetime,
) -> None:
if int(grant.get("schema_version", 0)) != 2:
raise AuthorizationError("writer_token_policy_v2_required", 401)
if str(grant.get("audience") or "") != audience:
raise AuthorizationError("writer_token_audience_rejected")
try:
peer = ipaddress.ip_address(source_ip)
networks = [ipaddress.ip_network(str(value), strict=True) for value in grant.get("source_cidrs", [])]
except ValueError:
raise AuthorizationError("writer_token_source_rejected") from None
if not networks or not any(peer in network for network in networks):
raise AuthorizationError("writer_token_source_rejected")
created = _parse_time(grant.get("created_at"))
not_before = _parse_time(grant.get("not_before"))
expires = _parse_time(grant.get("expires_at"))
if expires <= not_before or (expires - created).total_seconds() > 1800:
raise AuthorizationError("writer_token_lifetime_rejected")
if current_time < not_before:
raise AuthorizationError("writer_token_not_yet_valid", 401)
if current_time >= expires:
raise AuthorizationError("writer_token_expired", 401)
if bool(grant.get("revoked")):
raise AuthorizationError("writer_token_revoked", 401)
operations = grant.get("allowed_operations")
if not isinstance(operations, list) or operation not in operations:
raise AuthorizationError("writer_token_operation_rejected")
expected_branch = str(grant.get("allowed_branch") or "")
configured_branch = os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", "")
if expected_branch != configured_branch:
raise AuthorizationError("writer_token_branch_rejected")
if branch is not None and branch != expected_branch:
raise AuthorizationError("writer_token_branch_rejected")
if path is not None:
paths = grant.get("allowed_paths")
if not isinstance(paths, list) or path not in paths:
raise AuthorizationError("writer_token_path_rejected")
base = str(grant.get("approved_base_oid") or "").lower()
if not OID_RE.fullmatch(base):
raise AuthorizationError("writer_token_base_rejected")
if not _git_is_ancestor(workspace, base, expected_branch):
raise AuthorizationError("writer_token_base_not_ancestor")
def authorize(
registry: TokenRegistry,
token: str,
*,
method: str,
path: str,
source_ip: str,
workspace: Path,
branch: str | None = None,
target_path: str | None = None,
current_time: datetime | None = None,
) -> AuthorizedGrant:
if os.getenv("AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED", "") != "2":
raise AuthorizationError("writer_token_policy_v2_not_required")
if not token:
raise AuthorizationError("unauthorized", 401)
grant = registry.find(token)
if grant is None:
raise AuthorizationError("unauthorized", 401)
operation = operation_for(method, path)
_validate_grant(
grant,
audience=os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", ""),
source_ip=source_ip,
operation=operation,
branch=branch,
path=target_path,
workspace=workspace,
current_time=current_time or datetime.now(timezone.utc),
)
limits = grant.get("max_uses")
if not isinstance(limits, dict):
raise AuthorizationError("writer_token_use_limit_missing")
try:
used = registry.consume(str(grant["token_id"]), operation, int(limits[operation]))
except (KeyError, TypeError, ValueError):
raise AuthorizationError("writer_token_use_limit_missing") from None
except PermissionError as exc:
code = str(exc)
raise AuthorizationError(code, 401 if code == "writer_token_revoked" else 403) from None
return AuthorizedGrant(grant, operation, used)
@@ -1,403 +0,0 @@
"""Atomic, idempotent commit-file Saga for the isolated writer."""
from __future__ import annotations
import fcntl
import hashlib
import json
import os
import re
import sqlite3
import subprocess
import tempfile
import threading
from pathlib import Path
from typing import Any
import aios_isolated_writer_policy as policy
OID_RE = re.compile(r"[0-9a-f]{40}")
IDEMPOTENCY_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}")
STATES = frozenset({
"PENDING", "FILE_WRITTEN", "COMMITTED", "PUSHED", "VERIFIED", "COMPLETED",
"FAILED_RECOVERABLE", "MANUAL_REVIEW_REQUIRED",
})
_LOCKS: dict[str, threading.Lock] = {}
_LOCKS_GUARD = threading.Lock()
_LEDGER_INIT_LOCK = threading.Lock()
class CommitFileError(RuntimeError):
def __init__(self, code: str, status: int = 400):
super().__init__(code)
self.code = code
self.status = status
class SimulatedCrash(RuntimeError):
pass
def canonical_digest(data: dict[str, Any]) -> str:
payload = {
"branch": data["branch"],
"path": data["path"],
"content_sha256": hashlib.sha256(data["content"].encode("utf-8")).hexdigest(),
"message": data["message"],
"expected_head_oid": data["expected_head_oid"],
}
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
class OperationLedger:
def __init__(self, path: Path | str):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
if self.path.parent.is_symlink() or self.path.is_symlink():
raise PermissionError("writer_operation_ledger_symlink_rejected")
with _LEDGER_INIT_LOCK:
with self.connect() as db:
db.execute(
"""
CREATE TABLE IF NOT EXISTS writer_operations (
operation_id TEXT PRIMARY KEY,
token_id TEXT NOT NULL,
task_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
operation_digest TEXT NOT NULL,
branch TEXT NOT NULL,
path TEXT NOT NULL,
content_sha256 TEXT NOT NULL,
pre_head_oid TEXT NOT NULL,
commit_oid TEXT,
push_status TEXT,
verification_status TEXT,
state TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
error_code TEXT
)
"""
)
os.chmod(self.path, 0o600)
def connect(self) -> sqlite3.Connection:
db = sqlite3.connect(self.path, timeout=30)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
return db
def get_or_create(self, grant: Any, data: dict[str, Any], digest: str) -> dict[str, Any]:
key = data["idempotency_key"]
with self.connect() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute(
"SELECT * FROM writer_operations WHERE idempotency_key=?", (key,)
).fetchone()
if row:
db.commit()
record = dict(row)
if record["operation_digest"] != digest:
raise CommitFileError("writer_idempotency_conflict", 409)
if (
record["token_id"] != grant.token_id
or record["task_id"] != grant.task_id
):
raise CommitFileError("writer_idempotency_owner_conflict", 409)
return record
operation_id = hashlib.sha256(
f"{grant.task_id}\0{key}".encode("utf-8")
).hexdigest()
content_sha = hashlib.sha256(data["content"].encode("utf-8")).hexdigest()
db.execute(
"""
INSERT INTO writer_operations(
operation_id,token_id,task_id,idempotency_key,operation_digest,
branch,path,content_sha256,pre_head_oid,state
) VALUES(?,?,?,?,?,?,?,?,?,?)
""",
(
operation_id, grant.token_id, grant.task_id, key, digest,
data["branch"], data["path"], content_sha,
data["expected_head_oid"], "PENDING",
),
)
db.commit()
return self.get(key)
def get(self, key: str) -> dict[str, Any]:
with self.connect() as db:
row = db.execute(
"SELECT * FROM writer_operations WHERE idempotency_key=?", (key,)
).fetchone()
if row is None:
raise KeyError(key)
return dict(row)
def update(self, key: str, state: str, **fields: Any) -> dict[str, Any]:
if state not in STATES:
raise ValueError("invalid writer operation state")
allowed = {"commit_oid", "push_status", "verification_status", "error_code"}
assignments = ["state=?", "updated_at=CURRENT_TIMESTAMP"]
values: list[Any] = [state]
for name, value in fields.items():
if name not in allowed:
raise ValueError("invalid ledger field")
assignments.append(f"{name}=?")
values.append(value)
values.append(key)
with self.connect() as db:
db.execute("BEGIN IMMEDIATE")
db.execute(
f"UPDATE writer_operations SET {','.join(assignments)} WHERE idempotency_key=?",
values,
)
db.commit()
return self.get(key)
class AtomicCommitFile:
def __init__(
self,
*,
workspace: Path,
bare_repo: Path,
ledger: OperationLedger,
remote: str,
audit,
crash_after_state: str | None = None,
):
self.workspace = workspace
self.bare_repo = bare_repo
self.ledger = ledger
self.remote = remote
self.audit = audit
self.crash_after_state = crash_after_state
def _git(self, *args: str, check: bool = True) -> str:
result = subprocess.run(
["git", *args], cwd=self.workspace, text=True, capture_output=True
)
if check and result.returncode != 0:
raise CommitFileError("writer_git_operation_failed", 500)
return result.stdout.strip()
def _transition(self, key: str, state: str, **fields: Any) -> dict[str, Any]:
record = self.ledger.update(key, state, **fields)
if self.crash_after_state == state:
raise SimulatedCrash(state)
return record
def _validate(self, data: Any) -> dict[str, Any]:
if not isinstance(data, dict):
raise CommitFileError("writer_request_object_required")
required = ("branch", "path", "content", "message", "expected_head_oid", "idempotency_key")
if any(field not in data for field in required):
raise CommitFileError("writer_request_incomplete")
if data.get("force") or data.get("force_push"):
raise CommitFileError("isolated_writer_force_rejected", 403)
if set(data) != set(required):
raise CommitFileError("writer_request_fields_rejected")
item = {field: data[field] for field in required}
if item["branch"] != policy.allowed_branch():
raise CommitFileError("isolated_writer_branch_rejected", 403)
try:
item["path"] = policy.validate_path(item["path"], self.workspace)
except PermissionError as exc:
raise CommitFileError(str(exc), 403) from None
if not isinstance(item["content"], str) or "\x00" in item["content"]:
raise CommitFileError("writer_content_rejected")
try:
policy.validate_content_for_path(item["path"], item["content"])
except PermissionError as exc:
raise CommitFileError(str(exc), 403) from None
if len(item["content"].encode("utf-8")) > 1_048_576:
raise CommitFileError("writer_content_too_large", 413)
message = item["message"]
if (
not isinstance(message, str)
or not 1 <= len(message) <= 200
or "\x00" in message
or "\n" in message
or "\r" in message
):
raise CommitFileError("writer_message_rejected")
if not isinstance(item["expected_head_oid"], str) or not OID_RE.fullmatch(item["expected_head_oid"]):
raise CommitFileError("writer_expected_head_rejected")
if not isinstance(item["idempotency_key"], str) or not IDEMPOTENCY_RE.fullmatch(item["idempotency_key"]):
raise CommitFileError("writer_idempotency_key_rejected")
return item
def _atomic_write(self, relative: str, content: str) -> None:
target = self.workspace / relative
target.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".writer-", dir=target.parent)
try:
with os.fdopen(fd, "wb", closefd=True) as stream:
stream.write(content.encode("utf-8"))
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
parent_fd = os.open(target.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(parent_fd)
finally:
os.close(parent_fd)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def _verify_remote(self, branch: str, oid: str) -> None:
bare = subprocess.run(
["git", "--git-dir", str(self.bare_repo), "rev-parse", f"refs/heads/{branch}"],
text=True, capture_output=True,
)
if bare.returncode != 0 or bare.stdout.strip() != oid:
raise CommitFileError("writer_authoritative_bare_oid_mismatch", 500)
remote = subprocess.run(
["git", "ls-remote", "--heads", self.remote, f"refs/heads/{branch}"],
cwd=self.workspace, text=True, capture_output=True,
)
if remote.returncode != 0:
raise CommitFileError("writer_gitea_verification_failed", 503)
fields = remote.stdout.strip().split()
if len(fields) != 2:
raise CommitFileError("writer_gitea_ref_missing", 503)
if fields[0] != oid:
raise CommitFileError("writer_gitea_oid_conflict", 409)
def execute(self, grant: Any, raw_data: Any) -> dict[str, Any]:
data = self._validate(raw_data)
digest = canonical_digest(data)
branch = data["branch"]
with _LOCKS_GUARD:
lock = _LOCKS.setdefault(branch, threading.Lock())
lock_path = self.ledger.path.parent / f"{hashlib.sha256(branch.encode()).hexdigest()}.lock"
with lock, open(lock_path, "a+b") as branch_lock:
fcntl.flock(branch_lock.fileno(), fcntl.LOCK_EX)
return self._execute_locked(grant, data, digest)
def _execute_locked(self, grant: Any, data: dict[str, Any], digest: str) -> dict[str, Any]:
key = data["idempotency_key"]
record = self.ledger.get_or_create(grant, data, digest)
if record["state"] == "COMPLETED":
return self._result(record, idempotent=True)
if self._git("rev-parse", "--abbrev-ref", "HEAD") != data["branch"]:
record = self._transition(
key, "MANUAL_REVIEW_REQUIRED", error_code="writer_current_branch_conflict"
)
self._audit(grant, data, digest, record, "manual_review_required")
raise CommitFileError("writer_current_branch_conflict", 409)
head = self._git("rev-parse", "HEAD")
commit_oid = record.get("commit_oid")
if head != record["pre_head_oid"] and head != commit_oid:
record = self._transition(
key, "MANUAL_REVIEW_REQUIRED", error_code="writer_branch_head_conflict"
)
self._audit(grant, data, digest, record, "manual_review_required")
raise CommitFileError("writer_branch_head_conflict", 409)
try:
state = record["state"]
if state == "MANUAL_REVIEW_REQUIRED":
raise CommitFileError("writer_manual_review_required", 409)
if state == "FAILED_RECOVERABLE":
if record.get("push_status") == "confirmed":
state = "PUSHED"
else:
state = "COMMITTED" if commit_oid else "FILE_WRITTEN"
if state == "PENDING":
self._atomic_write(data["path"], data["content"])
record = self._transition(key, "FILE_WRITTEN")
state = record["state"]
if state == "FILE_WRITTEN":
actual_sha = hashlib.sha256(
(self.workspace / data["path"]).read_bytes()
).hexdigest()
if actual_sha != record["content_sha256"]:
raise CommitFileError("writer_file_content_conflict", 409)
staged_before = self._git("diff", "--cached", "--name-only", check=False).splitlines()
if staged_before:
raise CommitFileError("writer_extra_staged_files_rejected", 409)
self._git("add", "--", data["path"])
staged = self._git("diff", "--cached", "--name-only").splitlines()
if staged != [data["path"]]:
raise CommitFileError("writer_extra_staged_files_rejected", 409)
self._git("commit", "-m", data["message"])
commit_oid = self._git("rev-parse", "HEAD")
record = self._transition(key, "COMMITTED", commit_oid=commit_oid)
state = record["state"]
if state == "COMMITTED":
commit_oid = str(record["commit_oid"])
pushed = subprocess.run(
["git", "push", "--porcelain", self.remote, f"HEAD:refs/heads/{data['branch']}"],
cwd=self.workspace, text=True, capture_output=True,
)
if pushed.returncode != 0:
self._transition(
key, "FAILED_RECOVERABLE",
push_status="failed", error_code="writer_gitea_push_failed",
)
raise CommitFileError("writer_gitea_push_failed", 503)
record = self._transition(key, "PUSHED", push_status="confirmed")
state = record["state"]
if state == "PUSHED":
commit_oid = str(record["commit_oid"])
self._verify_remote(data["branch"], commit_oid)
record = self._transition(
key, "VERIFIED", verification_status="bare+gitea"
)
state = record["state"]
if state == "VERIFIED":
record = self._transition(key, "COMPLETED", error_code=None)
self._audit(grant, data, digest, record, "completed")
return self._result(record, idempotent=False)
except SimulatedCrash:
raise
except CommitFileError as exc:
latest = self.ledger.get(key)
if latest["state"] not in {"FAILED_RECOVERABLE", "MANUAL_REVIEW_REQUIRED"}:
failure_state = (
"MANUAL_REVIEW_REQUIRED"
if exc.code in {
"writer_gitea_oid_conflict",
"writer_file_content_conflict",
"writer_current_branch_conflict",
}
else "FAILED_RECOVERABLE"
)
latest = self._transition(key, failure_state, error_code=exc.code)
self._audit(grant, data, digest, latest, "failed")
raise
def _audit(self, grant: Any, data: dict[str, Any], digest: str, record: dict[str, Any], result: str) -> None:
self.audit({
"token_id": grant.token_id,
"task_id": grant.task_id,
"audience": grant.record["audience"],
"source_ip": grant.record.get("_source_ip", "socket-peer"),
"operation": grant.operation,
"branch": data["branch"],
"path": data["path"],
"idempotency_key": data["idempotency_key"],
"operation_digest": digest,
"path_policy_sha256": policy.configured_path_policy_sha256(),
"pre_head_oid": record["pre_head_oid"],
"commit_oid": record.get("commit_oid"),
"result": result,
})
@staticmethod
def _result(record: dict[str, Any], *, idempotent: bool) -> dict[str, Any]:
return {
"status": "completed",
"state": record["state"],
"operation_id": record["operation_id"],
"branch": record["branch"],
"path": record["path"],
"commit_oid": record.get("commit_oid"),
"push_status": record.get("push_status"),
"verification_status": record.get("verification_status"),
"idempotent": idempotent,
}
@@ -1,422 +0,0 @@
"""Strict service-level exact-path policy for isolated Git writers."""
from __future__ import annotations
import grp
import hashlib
import json
import os
import re
import stat
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any
from urllib.parse import unquote
MAX_POLICY_BYTES = 65_536
POLICY_FIELDS = frozenset(
{
"schema_version",
"policy_id",
"audience",
"repository_identity",
"allowed_branch",
"allowed_operations",
}
)
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"})
_SECRET_RE = re.compile(
r"(?i)(authorization\s*:|bearer\s+|(?:token|password|secret)\s*[:=])"
)
_SYSTEMD_NAME_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*\Z")
_SYSTEMD_MAX_LINE_BYTES = 4096
_SYSTEMD_ALLOWED_DIRECTIVES = {
"Unit": frozenset({"Description", "After", "Wants"}),
"Service": frozenset(
{
"Type",
"User",
"Group",
"WorkingDirectory",
"Environment",
"ExecStart",
"Restart",
"RestartSec",
"NoNewPrivileges",
"PrivateTmp",
"PrivateDevices",
"ProtectSystem",
"ProtectHome",
"ProtectKernelTunables",
"ProtectKernelModules",
"ProtectControlGroups",
"LockPersonality",
"RestrictSUIDSGID",
"RestrictRealtime",
"MemoryDenyWriteExecute",
"RuntimeDirectory",
"StateDirectory",
"CacheDirectory",
"ReadOnlyPaths",
"ReadWritePaths",
"UMask",
}
),
"Install": frozenset({"WantedBy"}),
}
_SYSTEMD_MULTI_DIRECTIVES = frozenset(
{
("Service", "Environment"),
("Service", "ReadOnlyPaths"),
("Service", "ReadWritePaths"),
}
)
_SYSTEMD_EXACT_SINGLE_VALUES = {
("Unit", "Description"): "AIOS Codex Review/Test Worker",
("Unit", "After"): "network-online.target",
("Unit", "Wants"): "network-online.target",
("Service", "Type"): "simple",
("Service", "User"): "aiman",
("Service", "Group"): "gitops",
(
"Service",
"WorkingDirectory",
): "/usr/libexec/aios-codex-review-test-worker/current",
(
"Service",
"ExecStart",
): "/usr/bin/python3 -m task_plane.workers.codex_review_test",
("Service", "Restart"): "on-failure",
("Service", "RestartSec"): "2",
("Service", "NoNewPrivileges"): "true",
("Service", "PrivateTmp"): "true",
("Service", "PrivateDevices"): "true",
("Service", "ProtectSystem"): "strict",
("Service", "ProtectHome"): "true",
("Service", "ProtectKernelTunables"): "true",
("Service", "ProtectKernelModules"): "true",
("Service", "ProtectControlGroups"): "true",
("Service", "LockPersonality"): "true",
("Service", "RestrictSUIDSGID"): "true",
("Service", "RestrictRealtime"): "true",
("Service", "MemoryDenyWriteExecute"): "true",
("Service", "RuntimeDirectory"): "aios-codex-review-test-worker",
("Service", "StateDirectory"): "aios-codex-review-test-worker",
("Service", "CacheDirectory"): "aios-codex-review-test-worker",
("Service", "UMask"): "0077",
("Install", "WantedBy"): "multi-user.target",
}
_SYSTEMD_EXACT_MULTI_VALUES = {
("Service", "Environment"): (
"AIOS_CODEX_WORKER_MODE=shadow",
"AIOS_TASK_PLANE_EXECUTION_ENABLED=false",
"PYTHONDONTWRITEBYTECODE=1",
),
("Service", "ReadOnlyPaths"): (
"/data/aios-gitops/repo.git",
"/usr/libexec/aios-codex-review-test-worker",
),
("Service", "ReadWritePaths"): (
"/run/aios-codex-review-test-worker",
"/var/lib/aios-codex-review-test-worker",
"/var/cache/aios-codex-review-test-worker",
),
}
class PathPolicyError(PermissionError):
pass
@dataclass(frozen=True)
class OperationPolicy:
exact_paths: tuple[str, ...]
max_content_bytes: int
content_profile: str
@dataclass(frozen=True)
class ServicePathPolicy:
schema_version: int
policy_id: str
audience: str
repository_identity: str
allowed_branch: str
allowed_operations: dict[str, OperationPolicy]
sha256: str
def operation(self, name: str) -> OperationPolicy:
try:
return self.allowed_operations[name]
except KeyError:
raise PathPolicyError("isolated_writer_service_operation_rejected") from None
def require_exact_path(self, operation: str, path: str) -> OperationPolicy:
selected = self.operation(operation)
if path not in selected.exact_paths:
raise PathPolicyError("isolated_writer_service_path_rejected")
return selected
def _canonical_json(value: Any) -> bytes:
return (
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
+ "\n"
).encode("utf-8")
def validate_relative_path_syntax(path: str) -> str:
if not isinstance(path, str):
raise PathPolicyError("isolated_writer_path_rejected")
if not path or path != path.strip() or "\x00" in path or "\\" in path:
raise PathPolicyError("isolated_writer_path_rejected")
try:
decoded = unquote(path, errors="strict")
except (UnicodeDecodeError, ValueError):
raise PathPolicyError("isolated_writer_path_rejected") from None
if decoded != path:
raise PathPolicyError("isolated_writer_path_rejected")
posix = PurePosixPath(path)
windows = PureWindowsPath(path)
parts = path.split("/")
if (
posix.is_absolute()
or windows.is_absolute()
or bool(windows.drive)
or any(part in {"", ".", ".."} for part in parts)
):
raise PathPolicyError("isolated_writer_path_rejected")
return path
def _require_fields(value: dict[str, Any], allowed: frozenset[str], code: str) -> None:
if set(value) != allowed:
raise PathPolicyError(code)
def _parse_policy(document: Any, canonical: bytes) -> ServicePathPolicy:
if not isinstance(document, dict):
raise PathPolicyError("isolated_writer_path_policy_object_required")
_require_fields(document, POLICY_FIELDS, "isolated_writer_path_policy_fields_rejected")
if document["schema_version"] != 1:
raise PathPolicyError("isolated_writer_path_policy_schema_rejected")
text_fields = (
"policy_id",
"audience",
"repository_identity",
"allowed_branch",
)
if any(
not isinstance(document[field], str) or not document[field].strip()
for field in text_fields
):
raise PathPolicyError("isolated_writer_path_policy_value_rejected")
raw_operations = document["allowed_operations"]
if not isinstance(raw_operations, dict) or not raw_operations:
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
if not set(raw_operations).issubset(SUPPORTED_OPERATIONS):
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
operations: dict[str, OperationPolicy] = {}
for name, raw in raw_operations.items():
if not isinstance(raw, dict):
raise PathPolicyError("isolated_writer_path_policy_operation_rejected")
_require_fields(
raw,
OPERATION_FIELDS,
"isolated_writer_path_policy_operation_fields_rejected",
)
paths = raw["exact_paths"]
if (
not isinstance(paths, list)
or not paths
or any(not isinstance(item, str) for item in paths)
):
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
validated = tuple(validate_relative_path_syntax(item) for item in paths)
if len(set(validated)) != len(validated):
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
maximum = raw["max_content_bytes"]
if not isinstance(maximum, int) or isinstance(maximum, bool) or not 1 <= maximum <= 1_048_576:
raise PathPolicyError("isolated_writer_path_policy_size_rejected")
profile = raw["content_profile"]
if profile not in SUPPORTED_CONTENT_PROFILES:
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
operations[name] = OperationPolicy(validated, maximum, profile)
return ServicePathPolicy(
schema_version=1,
policy_id=document["policy_id"],
audience=document["audience"],
repository_identity=document["repository_identity"],
allowed_branch=document["allowed_branch"],
allowed_operations=operations,
sha256=hashlib.sha256(canonical).hexdigest(),
)
def load_path_policy(
path: Path,
*,
expected_owner_uid: int = 0,
expected_group_gid: int | None = None,
expected_parent_mode: int = 0o750,
expected_file_mode: int = 0o640,
) -> ServicePathPolicy:
lexical = Path(os.path.abspath(str(path)))
if lexical.is_symlink():
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
try:
parent_info = lexical.parent.stat(follow_symlinks=False)
file_info = lexical.stat(follow_symlinks=False)
except FileNotFoundError:
raise PathPolicyError("isolated_writer_path_policy_missing") from None
if stat.S_ISLNK(parent_info.st_mode) or not stat.S_ISDIR(parent_info.st_mode):
raise PathPolicyError("isolated_writer_path_policy_parent_rejected")
if stat.S_ISLNK(file_info.st_mode) or not stat.S_ISREG(file_info.st_mode):
raise PathPolicyError("isolated_writer_path_policy_file_rejected")
if lexical.resolve(strict=True) != lexical:
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
if (
parent_info.st_uid != expected_owner_uid
or stat.S_IMODE(parent_info.st_mode) != expected_parent_mode
):
raise PathPolicyError("isolated_writer_path_policy_parent_permissions_rejected")
if (
file_info.st_uid != expected_owner_uid
or stat.S_IMODE(file_info.st_mode) != expected_file_mode
):
raise PathPolicyError("isolated_writer_path_policy_permissions_rejected")
if expected_group_gid is not None and (
parent_info.st_gid != expected_group_gid or file_info.st_gid != expected_group_gid
):
raise PathPolicyError("isolated_writer_path_policy_group_rejected")
if file_info.st_size <= 0 or file_info.st_size > MAX_POLICY_BYTES:
raise PathPolicyError("isolated_writer_path_policy_file_size_rejected")
raw = lexical.read_bytes()
try:
document = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise PathPolicyError("isolated_writer_path_policy_json_rejected") from None
canonical = _canonical_json(document)
if raw != canonical:
raise PathPolicyError("isolated_writer_path_policy_not_canonical")
return _parse_policy(document, canonical)
@lru_cache(maxsize=1)
def configured_path_policy() -> ServicePathPolicy | None:
configured = os.getenv("AIOS_GITOPS_PATH_POLICY_FILE", "").strip()
if not configured:
return None
if not Path(configured).is_absolute():
raise PathPolicyError("isolated_writer_path_policy_path_rejected")
try:
gitops_gid = grp.getgrnam("gitops").gr_gid
except KeyError:
raise PathPolicyError("isolated_writer_path_policy_group_rejected") from None
loaded = load_path_policy(Path(configured), expected_group_gid=gitops_gid)
if loaded.audience != os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", ""):
raise PathPolicyError("isolated_writer_path_policy_audience_rejected")
if loaded.repository_identity != os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", ""):
raise PathPolicyError("isolated_writer_path_policy_repository_rejected")
if loaded.allowed_branch != os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", ""):
raise PathPolicyError("isolated_writer_path_policy_branch_rejected")
return loaded
def clear_path_policy_cache() -> None:
configured_path_policy.cache_clear()
def require_service_path(operation: str, path: str) -> OperationPolicy:
loaded = configured_path_policy()
if loaded is None:
raise PathPolicyError("isolated_writer_service_path_policy_required")
return loaded.require_exact_path(operation, path)
def configured_path_policy_sha256() -> str | None:
loaded = configured_path_policy()
return None if loaded is None else loaded.sha256
def validate_content(operation: str, path: str, content: str) -> None:
selected = require_service_path(operation, path)
try:
encoded = content.encode("utf-8", errors="strict")
except (AttributeError, UnicodeEncodeError):
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
if len(encoded) > selected.max_content_bytes:
raise PathPolicyError("isolated_writer_service_content_too_large")
if selected.content_profile == "systemd-codex-review-worker-v1":
_validate_systemd_codex_review_worker(content)
return
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
def _validate_systemd_codex_review_worker(content: str) -> None:
if not isinstance(content, str):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
try:
content.encode("utf-8", errors="strict")
except UnicodeEncodeError:
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
if (
"\x00" in content
or "\r" in content
or _SECRET_RE.search(content)
or not content.endswith("\n")
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
sections: set[str] = set()
values: dict[tuple[str, str], list[str]] = {}
section: str | None = None
for line in content.splitlines():
if len(line.encode("utf-8")) > _SYSTEMD_MAX_LINE_BYTES:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if not line:
continue
if line[0].isspace() or line.startswith(("#", ";")):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if line.startswith("[") and line.endswith("]"):
candidate = line[1:-1]
if (
not _SYSTEMD_NAME_RE.fullmatch(candidate)
or candidate not in _SYSTEMD_ALLOWED_DIRECTIVES
or candidate in sections
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
sections.add(candidate)
section = candidate
continue
if section is None or "=" not in line:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
directive, value = line.split("=", 1)
if (
not _SYSTEMD_NAME_RE.fullmatch(directive)
or directive not in _SYSTEMD_ALLOWED_DIRECTIVES[section]
or not value
or value != value.strip()
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
key = (section, directive)
if key in values and key not in _SYSTEMD_MULTI_DIRECTIVES:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
values.setdefault(key, []).append(value)
if sections != set(_SYSTEMD_ALLOWED_DIRECTIVES):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
if set(values) != set(_SYSTEMD_EXACT_SINGLE_VALUES) | set(
_SYSTEMD_EXACT_MULTI_VALUES
):
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
for key, expected in _SYSTEMD_EXACT_SINGLE_VALUES.items():
if values.get(key) != [expected]:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
for key, expected in _SYSTEMD_EXACT_MULTI_VALUES.items():
observed = values.get(key, [])
if len(observed) != len(set(observed)) or tuple(observed) != expected:
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
@@ -1,241 +0,0 @@
"""Digest-only, task-scoped token registry for isolated writers."""
from __future__ import annotations
import argparse
import hashlib
import hmac
import json
import os
import grp
import pwd
import secrets
import sqlite3
import stat
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_REGISTRY_INIT_LOCK = threading.Lock()
def utc_now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def token_digest(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
class TokenRegistry:
def __init__(self, path: Path | str):
self.path = Path(path)
self._prepare()
def _prepare(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
if self.path.parent.is_symlink() or self.path.is_symlink():
raise PermissionError("writer_token_registry_symlink_rejected")
with _REGISTRY_INIT_LOCK:
with self._connect() as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS token_grants (
token_id TEXT PRIMARY KEY,
token_digest TEXT NOT NULL UNIQUE,
grant_json TEXT NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS token_uses (
token_id TEXT NOT NULL,
operation TEXT NOT NULL,
use_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(token_id, operation),
FOREIGN KEY(token_id) REFERENCES token_grants(token_id)
);
"""
)
os.chmod(self.path, 0o600)
def _connect(self) -> sqlite3.Connection:
db = sqlite3.connect(self.path, timeout=30, isolation_level=None)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute("PRAGMA foreign_keys=ON")
return db
def issue(self, grant: dict[str, Any], token: str | None = None) -> tuple[str, dict[str, Any]]:
value = token or secrets.token_urlsafe(32)
if len(value.encode("utf-8")) < 32:
raise ValueError("writer_token_entropy_insufficient")
record = dict(grant)
record["schema_version"] = 2
record["token_digest"] = token_digest(value)
record["revoked"] = False
record.setdefault("created_at", utc_now())
required = (
"token_id",
"audience",
"task_id",
"source_cidrs",
"allowed_branch",
"approved_base_oid",
"allowed_operations",
"allowed_paths",
"not_before",
"expires_at",
"max_uses",
"issued_by",
)
if any(not record.get(field) for field in required):
raise ValueError("writer_token_grant_incomplete")
encoded = json.dumps(record, sort_keys=True, separators=(",", ":"))
with self._connect() as db:
db.execute("BEGIN IMMEDIATE")
try:
db.execute(
"""
INSERT INTO token_grants(token_id, token_digest, grant_json, revoked, created_at)
VALUES(?,?,?,?,?)
""",
(
str(record["token_id"]),
str(record["token_digest"]),
encoded,
0,
str(record["created_at"]),
),
)
db.execute("COMMIT")
except Exception:
db.execute("ROLLBACK")
raise
return value, record
def find(self, token: str) -> dict[str, Any] | None:
candidate = token_digest(token)
with self._connect() as db:
rows = db.execute(
"SELECT token_digest, grant_json, revoked FROM token_grants"
).fetchall()
match = None
for row in rows:
if hmac.compare_digest(str(row["token_digest"]), candidate):
match = dict(json.loads(str(row["grant_json"])))
match["revoked"] = bool(row["revoked"])
return match
def consume(self, token_id: str, operation: str, maximum: int) -> int:
if maximum < 1:
raise PermissionError("writer_token_operation_exhausted")
with self._connect() as db:
db.execute("BEGIN IMMEDIATE")
try:
row = db.execute(
"SELECT revoked FROM token_grants WHERE token_id=?",
(token_id,),
).fetchone()
if row is None or bool(row["revoked"]):
raise PermissionError("writer_token_revoked")
used_row = db.execute(
"SELECT use_count FROM token_uses WHERE token_id=? AND operation=?",
(token_id, operation),
).fetchone()
used = int(used_row["use_count"]) if used_row else 0
if used >= maximum:
raise PermissionError("writer_token_operation_exhausted")
current = used + 1
db.execute(
"""
INSERT INTO token_uses(token_id, operation, use_count)
VALUES(?,?,?)
ON CONFLICT(token_id, operation)
DO UPDATE SET use_count=excluded.use_count
""",
(token_id, operation, current),
)
db.execute("COMMIT")
return current
except Exception:
db.execute("ROLLBACK")
raise
def revoke(self, token_id: str) -> bool:
with self._connect() as db:
db.execute("BEGIN IMMEDIATE")
changed = db.execute(
"UPDATE token_grants SET revoked=1 WHERE token_id=? AND revoked=0",
(token_id,),
).rowcount
db.execute("COMMIT")
return bool(changed)
def usage(self, token_id: str, operation: str) -> int:
with self._connect() as db:
row = db.execute(
"SELECT use_count FROM token_uses WHERE token_id=? AND operation=?",
(token_id, operation),
).fetchone()
return int(row["use_count"]) if row else 0
def _write_secret_file(
path: Path,
token: str,
*,
owner: str | None = None,
group: str | None = None,
) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.parent.is_symlink() or path.is_symlink():
raise PermissionError("writer_token_file_symlink_rejected")
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
fd = os.open(path, flags, 0o600)
try:
if not stat.S_ISREG(os.fstat(fd).st_mode):
raise PermissionError("writer_token_file_not_regular")
if owner is not None or group is not None:
uid = pwd.getpwnam(owner).pw_uid if owner is not None else -1
gid = grp.getgrnam(group).gr_gid if group is not None else -1
os.fchown(fd, uid, gid)
os.write(fd, token.encode("utf-8"))
os.fsync(fd)
finally:
os.close(fd)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--registry", required=True)
sub = parser.add_subparsers(dest="command", required=True)
issue = sub.add_parser("issue")
issue.add_argument("--grant-file", required=True)
issue.add_argument("--token-file", required=True)
issue.add_argument("--owner")
issue.add_argument("--group")
revoke = sub.add_parser("revoke")
revoke.add_argument("--token-id", required=True)
args = parser.parse_args(argv)
registry = TokenRegistry(args.registry)
if args.command == "issue":
grant = json.loads(Path(args.grant_file).read_text(encoding="utf-8"))
token, record = registry.issue(grant)
_write_secret_file(
Path(args.token_file),
token,
owner=args.owner,
group=args.group,
)
print(json.dumps({"token_id": record["token_id"], "created": True}, sort_keys=True))
return 0
changed = registry.revoke(args.token_id)
print(json.dumps({"token_id": args.token_id, "revoked": changed}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

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