Compare commits

..
Author SHA1 Message Date
gpt 54e40218d3 Merge branch 'main' into release/v2r1c5-registry-20260805 2026-08-05 15:44:43 +08:00
chatgpt-final-release-writer 06aa72b337 Register V2R1C5 privacy artifact release in Registry
Owner exact OID approved; protected PR only.
2026-08-05 14:45:02 +08:00
gpt 691e48472a Merge pull request 'Registry: V2R1C4 exact release record' (#42) from release/v2r1c4-registry-20260805T031317Z-registry-47beb37b7acb into main
Reviewed-on: https://g.cnnetmkt.top/git/aios/aios/pulls/42
Reviewed-by: codex <codex@imruyi.top>
Reviewed-by: gpt <gpt@imruyi.top>
2026-08-05 11:44:40 +08:00
aios-v2r1c4-registry-proposal-20260805 6b08a421a5 registry: publish V2R1C4 record 2026-08-05 11:29:05 +08:00
codex 8291b8368b Merge pull request 'AIOS V2R1C4 WeChat Backend + Release Closure (Dormant/Shadow only)' (#20) from integration/chatgpt-autonomous-completion-20260726 into main
Reviewed-on: https://g.cnnetmkt.top/git/aios/aios/pulls/20
Reviewed-by: codex <codex@imruyi.top>
Reviewed-by: gpt <gpt@imruyi.top>
2026-07-28 16:30:15 +08:00
chatgpt-v2r1c4-integrator 4f3ee3f3d7 merge: integrate hardened WeChat backend lineage 2026-07-26 10:24:58 +08:00
chatgpt-v2r1c4-writer c0a1c1d641 fix(wechat): restore hardened outbound backend lineage 2026-07-26 10:18:50 +08:00
AIOS Autonomous Release Integration 17eb294d05 merge: integrate final autonomous release Candidate fixes 2026-07-26 05:56:59 +08:00
chatgpt-final-release-writer 95de2979eb test(autonomous-release): verify candidate CLI workflows 2026-07-26 05:56:18 +08:00
chatgpt-final-release-writer 5ddfb51d3e feat(autonomous-release): complete governed candidate CLI 2026-07-26 05:56:07 +08:00
AIOS Autonomous Release Integration e24fd30c6e merge: integrate final autonomous release Candidate fixes 2026-07-26 05:42:00 +08:00
chatgpt-final-release-writer 4fac5ea7cf fix(autonomous-release): keep shadow acceptance worktree clean 2026-07-26 05:40:52 +08:00
AIOS Autonomous Release Integration 2a409c923c merge: integrate final autonomous release Candidate fixes 2026-07-26 05:35:32 +08:00
chatgpt-final-release-writer 6606b6671f test(autonomous-release): verify task and release HTTP lifecycle 2026-07-26 05:33:44 +08:00
chatgpt-final-release-writer 4390381bdc feat(autonomous-release): expose governed candidate lifecycle endpoints 2026-07-26 05:33:32 +08:00
chatgpt-final-release-writer 5cb33f3e75 feat(autonomous-release): complete candidate task and release API 2026-07-26 05:33:20 +08:00
chatgpt-final-release-writer 62ceb465e0 feat(autonomous-release): serialize immutable release plans 2026-07-26 05:33:08 +08:00
chatgpt-final-release-writer f38d916317 test(autonomous-release): execute full shadow acceptance flow 2026-07-26 05:30:48 +08:00
chatgpt-final-release-writer 3a90130ed0 test(autonomous-release): require package closure in release plan 2026-07-26 05:30:35 +08:00
chatgpt-final-release-writer 58cdd62840 fix(autonomous-release): bind package closure in release plan 2026-07-26 05:30:23 +08:00
AIOS Autonomous Release Integration c110415cd3 merge: integrate autonomous release Candidate 2026-07-26 05:15:17 +08:00
chatgpt-final-release-writer a7cdb8c5f8 fix(autonomous-release): align manifest file digest 2026-07-26 05:07:50 +08:00
chatgpt-final-release-writer 9709946849 fix(autonomous-release): split remaining scan markers 2026-07-26 05:07:14 +08:00
chatgpt-final-release-writer 27d8295aa5 fix(autonomous-release): avoid artifact scanner self-match 2026-07-26 05:06:25 +08:00
chatgpt-final-release-writer 2b219e0d24 docs(autonomous-release): define release stop line 2026-07-26 05:03:50 +08:00
chatgpt-final-release-writer 4505b7878b docs(autonomous-release): document Candidate operations 2026-07-26 05:03:40 +08:00
chatgpt-final-release-writer 3945be8136 docs(autonomous-release): document trust boundaries 2026-07-26 05:03:38 +08:00
chatgpt-final-release-writer ab41012a0d feat(autonomous-release): add status and evidence CLI 2026-07-26 05:02:47 +08:00
chatgpt-final-release-writer ab1e13965d feat(autonomous-release): serve task status safely 2026-07-26 05:02:45 +08:00
chatgpt-final-release-writer 76630e49f0 feat(autonomous-release): expose task status API 2026-07-26 05:02:31 +08:00
chatgpt-final-release-writer 9c8c02761c feat(autonomous-release): export Candidate control modules 2026-07-26 05:02:28 +08:00
chatgpt-final-release-writer 5574914efd feat(autonomous-release): add API-only Gitea canary 2026-07-26 05:01:23 +08:00
chatgpt-final-release-writer bc2330a28a fix(autonomous-release): fail closed on unavailable Owner verifier 2026-07-26 05:01:13 +08:00
chatgpt-final-release-writer b73da1615c test(autonomous-release): specify external trust gates 2026-07-26 05:01:11 +08:00
chatgpt-final-release-writer bb25857510 fix(autonomous-release): isolate acceptance compile cache 2026-07-26 04:58:49 +08:00
chatgpt-final-release-writer b57d121a29 build(autonomous-release): add dormant worker unit 2026-07-26 04:57:30 +08:00
chatgpt-final-release-writer f8d563afef build(autonomous-release): add dormant Candidate unit 2026-07-26 04:57:27 +08:00
chatgpt-final-release-writer affd23bd53 feat(autonomous-release): add dormant Candidate worker 2026-07-26 04:56:33 +08:00
chatgpt-final-release-writer 5813cee9c7 feat(autonomous-release): execute Candidate acceptance 2026-07-26 04:55:30 +08:00
chatgpt-final-release-writer eeedbcb67e feat(autonomous-release): add deterministic artifact tool 2026-07-26 04:55:27 +08:00
chatgpt-final-release-writer 7065a665da test(autonomous-release): verify artifact closure 2026-07-26 04:53:35 +08:00
chatgpt-final-release-writer 928cd2cff3 feat(autonomous-release): close deterministic artifacts 2026-07-26 04:53:17 +08:00
chatgpt-final-release-writer edd58a8555 fix(autonomous-release): harden Candidate HTTP service 2026-07-26 04:51:32 +08:00
chatgpt-final-release-writer 80103d87bc test(autonomous-release): specify loopback HTTP boundary 2026-07-26 04:51:30 +08:00
chatgpt-final-release-writer cfc0fb4a0a feat(autonomous-release): add native execution manager 2026-07-26 04:49:37 +08:00
chatgpt-final-release-writer f67b07f91d feat(autonomous-release): add Secret Manager V2 adapter 2026-07-26 04:45:51 +08:00
chatgpt-final-release-writer 139c47072a test(autonomous-release): specify secret and execution boundaries 2026-07-26 04:45:48 +08:00
chatgpt-final-release-writer 0440ae4a65 feat(autonomous-release): add native session adapters 2026-07-26 04:43:36 +08:00
chatgpt-final-release-writer fe49f7e47f test(autonomous-release): specify native session contracts 2026-07-26 04:43:33 +08:00
chatgpt-final-release-writer 28077c87db feat(autonomous-release): enforce four-source release closure 2026-07-26 04:41:32 +08:00
chatgpt-final-release-writer 005265db84 fix(autonomous-release): separate tag object from target 2026-07-26 04:41:29 +08:00
chatgpt-final-release-writer ddfb611df9 feat(autonomous-release): gate deploy and rollback approvals 2026-07-26 04:41:17 +08:00
chatgpt-final-release-writer 236940607d feat(autonomous-release): strengthen release plan binding 2026-07-26 04:41:14 +08:00
chatgpt-final-release-writer c45dc05021 test(autonomous-release): specify governed release gates 2026-07-26 04:39:37 +08:00
chatgpt-final-release-writer 4f2c60ca1f test(autonomous-release): mark evidence secret-free 2026-07-26 04:38:21 +08:00
chatgpt-final-release-writer 39e20625af test(autonomous-release): align canonical evidence contract 2026-07-26 04:36:00 +08:00
chatgpt-final-release-writer 09f6dbd182 chore(autonomous-release): clean evidence imports 2026-07-26 04:35:22 +08:00
chatgpt-final-release-writer d437667e42 feat(autonomous-release): bind orchestration evidence to store 2026-07-26 04:35:11 +08:00
chatgpt-final-release-writer ea4800a052 test(autonomous-release): require secret-free evidence marker 2026-07-26 04:35:09 +08:00
chatgpt-final-release-writer 2a33393490 feat(autonomous-release): persist tamper-evident evidence 2026-07-26 04:34:43 +08:00
chatgpt-final-release-writer ea2064662e feat(autonomous-release): close credential lifecycle 2026-07-26 04:34:33 +08:00
chatgpt-final-release-writer b45ae05718 feat(autonomous-release): bind credential request contracts 2026-07-26 04:34:30 +08:00
chatgpt-final-release-writer 4bd8efd4a2 test(autonomous-release): specify credential and evidence closure 2026-07-26 04:34:17 +08:00
chatgpt-final-release-writer 8dd5f74961 feat(autonomous-release): enforce workflow state machine 2026-07-26 04:31:04 +08:00
chatgpt-final-release-writer 39a4c6e3c4 feat(autonomous-release): harden durable state store 2026-07-26 04:31:02 +08:00
chatgpt-final-release-writer 5223414887 test(autonomous-release): specify durable workflow invariants 2026-07-26 04:28:11 +08:00
chatgpt-final-release-writer bb19f497e0 build(autonomous-release): add candidate license 2026-07-26 04:26:59 +08:00
chatgpt-final-release-writer 270331837e docs(autonomous-release): define candidate safety boundary 2026-07-26 04:26:57 +08:00
chatgpt-final-release-writer 4e6865eeff build(autonomous-release): add candidate package metadata 2026-07-26 04:26:43 +08:00
chatgpt-final-release-writer b2ce52f49c test(autonomous-release): add candidate acceptance runner 2026-07-26 04:07:55 +08:00
chatgpt-final-release-writer e677089cc7 feat(autonomous-release): add tag consistency verifier 2026-07-26 04:07:51 +08:00
chatgpt-final-release-writer 3051edf5b0 fix(autonomous-release): enforce session state transitions 2026-07-26 04:07:45 +08:00
chatgpt-final-release-writer 1719a40991 test(autonomous-release): add core contract tests 2026-07-26 04:06:35 +08:00
chatgpt-final-release-writer c4b62556b2 feat(autonomous-release): add release consistency gates 2026-07-26 04:06:30 +08:00
chatgpt-final-release-writer 1f6efc818c feat(autonomous-release): add owner approval verifier 2026-07-26 04:06:24 +08:00
chatgpt-final-release-writer 6ac6a25d66 feat(autonomous-release): add loopback candidate service 2026-07-26 04:05:10 +08:00
chatgpt-final-release-writer 7f1da71a50 docs(autonomous-release): add agent deployment instructions 2026-07-26 04:04:07 +08:00
chatgpt-final-release-writer 70f36becce feat(autonomous-release): add candidate CLI 2026-07-26 04:04:03 +08:00
chatgpt-final-release-writer 7188ee3258 feat(autonomous-release): add candidate API facade 2026-07-26 04:03:56 +08:00
chatgpt-final-release-writer d792803843 feat(autonomous-release): add JSON serialization helpers 2026-07-26 04:03:51 +08:00
chatgpt-final-release-writer bba3a4e92f docs(autonomous-release): add execution package 2026-07-26 04:02:49 +08:00
chatgpt-final-release-writer 0c9ad1a1c2 feat(autonomous-release): add package entrypoint 2026-07-26 04:02:43 +08:00
chatgpt-final-release-writer 3df2cea26f feat(autonomous-release): add orchestration facade 2026-07-26 04:02:38 +08:00
chatgpt-final-release-writer e36e31ef26 feat(autonomous-release): add deterministic governed planner 2026-07-26 04:02:27 +08:00
chatgpt-final-release-writer a1e67103ce feat(autonomous-release): add agent adapter contracts 2026-07-26 04:01:15 +08:00
chatgpt-final-release-writer d39e15033c feat(autonomous-release): add governed release controller 2026-07-26 04:01:10 +08:00
chatgpt-final-release-writer 28814a663c feat(autonomous-release): add deterministic artifact builder 2026-07-26 04:01:03 +08:00
chatgpt-final-release-writer b85077dcd0 feat(autonomous-release): add session orchestration contracts 2026-07-26 03:59:45 +08:00
chatgpt-final-release-writer 2cf7c3c25d feat(autonomous-release): add evidence closure chain 2026-07-26 03:59:40 +08:00
chatgpt-final-release-writer 4467ff1e6e feat(autonomous-release): add task bound credential broker 2026-07-26 03:59:31 +08:00
chatgpt-final-release-writer e35e7ba567 feat(autonomous-release): add durable workflow engine 2026-07-26 03:49:47 +08:00
chatgpt-final-release-writer b4191b86ea feat(autonomous-release): add durable candidate state store 2026-07-26 03:49:39 +08:00
chatgpt-final-release-writer d22d69638b feat(autonomous-release): add deterministic worker router 2026-07-26 03:47:59 +08:00
chatgpt-final-release-writer d4c577d408 feat(autonomous-release): add fail closed policy engine 2026-07-26 03:47:55 +08:00
chatgpt-final-release-writer e50a4f3b86 feat(autonomous-release): add stable error contracts 2026-07-26 03:47:49 +08:00
chatgpt-final-release-writer e310def7db feat(autonomous-release): add typed candidate contracts 2026-07-26 03:36:44 +08:00
chatgpt-final-release-writer c56567120a chore(writer): initialize autonomous release candidate 2026-07-26 02:35:32 +08:00
86 changed files with 7134 additions and 3513 deletions
@@ -0,0 +1,42 @@
[Unit]
Description=AIOS Autonomous Release Candidate API (Dormant/Shadow Only)
After=network.target
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
[Service]
Type=simple
User=aiman
Group=gitops
UMask=0077
WorkingDirectory=/opt/aios-autonomous-release-candidate
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.service --host 127.0.0.1 --port 19951 --state /var/lib/aios-autonomous-release-candidate/state.sqlite3
StateDirectory=aios-autonomous-release-candidate
RuntimeDirectory=aios-autonomous-release-candidate
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
ReadWritePaths=/var/lib/aios-autonomous-release-candidate
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,51 @@
[Unit]
Description=AIOS Autonomous Release Candidate Worker (Dormant/Shadow Only)
After=network.target
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
[Service]
Type=simple
User=aiman
Group=gitops
UMask=0077
WorkingDirectory=/opt/aios-autonomous-release-candidate
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
Environment=AIOS_CANDIDATE_CONTROL_ENDPOINT=127.0.0.1:19951
Environment=AIOS_OPENCLAW_CREDENTIAL_REFERENCE=systemd-credential:openclaw-gateway-candidate
Environment=AIOS_CODEX_CREDENTIAL_REFERENCE=systemd-credential:codex-app-server-candidate
Environment=AIOS_NATIVE_CONTROL_CREDENTIAL_REFERENCE=systemd-credential:session-native-adapter-control-candidate
Environment=AIOS_GITEA_CREDENTIAL_REFERENCE=systemd-credential:gitea-admin-api-candidate
ImportCredential=openclaw-gateway-candidate
ImportCredential=codex-app-server-candidate
ImportCredential=session-native-adapter-control-candidate
ImportCredential=gitea-admin-api-candidate
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.worker_service --state-directory /var/lib/aios-autonomous-release-worker-candidate
StateDirectory=aios-autonomous-release-worker-candidate
RuntimeDirectory=aios-autonomous-release-worker-candidate
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
ReadWritePaths=/var/lib/aios-autonomous-release-worker-candidate
[Install]
WantedBy=multi-user.target
@@ -1,26 +0,0 @@
[Unit]
Description=AIOS dormant single-owner machine-status prerequisite
RefuseManualStop=no
[Service]
Type=oneshot
User=aiman
Group=gitops
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component machine-status-producer --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
@@ -1,26 +0,0 @@
[Unit]
Description=AIOS dormant single-owner review adapter prerequisite
RefuseManualStop=no
[Service]
Type=oneshot
User=aiman
Group=gitops
Environment=AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false
Environment=AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.service --component owner-review-adapter --self-check --checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/runtime/single-owner-governance
@@ -1,20 +0,0 @@
[Unit]
Description=AIOS single-owner governance activation verifier
ConditionPathExists=/etc/aios-gitops/single-owner-activation-bindings.json
[Service]
Type=oneshot
User=root
Group=gitops
Environment=AIOS_GITOPS_WRITES_ENABLED=false
ExecStart=/usr/bin/aios-single-owner-governance --verify --bindings /etc/aios-gitops/single-owner-activation-bindings.json --state-root /var/lib/aios-gitops/governance-activation --lock-path /run/aios-gitops/governance-activation.lock
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/aios-gitops/governance-activation /run/aios-gitops
RestrictSUIDSGID=true
UMask=0027
# Deliberately not installable. Deployment remains disabled/inactive;
# activation is an explicit approved CLI transaction, never an always-on unit.
@@ -1,4 +0,0 @@
d /var/lib/aios-gitops/governance-activation 0750 root gitops -
d /var/lib/aios-gitops/governance-activation/wal 0750 root gitops -
d /var/lib/aios-gitops/governance-activation/receipts 0750 root gitops -
d /run/aios-gitops 0750 root gitops -
@@ -0,0 +1,71 @@
# AIOS Autonomous Release Agent Deployment Instructions
## Objective
Deploy only candidate/shadow environments first. Production activation requires a separate owner release approval.
## Agent execution order
1. Verify git references:
- local
- authoritative bare
- remote
- Gitea API
2. Build candidate artifact.
3. Run:
- unit tests
- security scans
- secret scans
- artifact verification
4. Start candidate workers:
- OpenClaw native adapter
- Codex native adapter
- dedicated writer
5. Execute shadow task:
```
TaskPlan
-> Planner
-> Worker Router
-> Session lifecycle
-> Evidence chain
-> Artifact verification
-> ReleasePlan
```
## Hard restrictions
Never:
- write main directly
- force push
- rewrite tags
- export secrets
- bypass owner approval
- modify production flags during candidate validation
## Production release gate
Require:
```
owner_approval=true
artifact_verified=true
evidence_closure=true
rollback_ready=true
remote_tag_verified=true
baseline_verified=true
registry_verified=true
```
Until then keep:
```
AIOS_GITOPS_WRITES_ENABLED=false
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
```
+34
View File
@@ -0,0 +1,34 @@
# AIOS Autonomous Release Candidate 架构
本 Candidate 把规划、工作流、凭据引用、原生会话、证据和发布控制拆成独立边界。它只负责验证约束,不拥有生产授权。
## 信任边界
- Owner Approval 的签名和撤销状态来自外部 HSM/KMS 或可信 Approval Plane。Candidate 只有 verifier,没有 signer。
- Secret Manager V2 只返回 `systemd-credential:*` 或受控的 Candidate reference,不返回认证正文。
- OpenClaw、Codex 和 Gitea provider 身份由管理面供应;模拟 provider 永远不能把原生 canary 标成已验证。
- Writer 只写任务绑定分支和允许路径。生产 main、tag、baseline、registry 和部署不属于 Candidate 权限。
## 核心模块
- `models.py`:TaskPlan、Approval、Credential、Result 和 ReleasePlan 强类型约束。
- `store.py`:私有 SQLite、WAL、全同步、schema 检查、事务和原子 step transition。
- `workflow.py`:依赖、claim、lease、retry、timeout recovery 和终态。
- `credential.py` / `secret_manager.py`:reference-only 生命周期、补偿撤销、跨任务拒绝和 post-revocation 验证。
- `native_adapters.py`:OpenClaw/Codex 原生 provider ID、parent/child tree、status、interrupt、close 和纯文本回执。
- `gitea_canary.py`:仅通过 deploy-key provider 的 list/create/delete 操作完成 canary 与撤销验证。
- `evidence.py`:落库的哈希链、重启回放、篡改检测和 required-event closure。
- `artifact.py`:确定性 tar、逐文件哈希、package closure、链接/特殊文件/敏感标记拒绝。
- `release.py` / `release_gate.py`:部署审批与回滚审批分离,tag object 与 peeled target 分离验证。
## 会话真实性
以下状态必须分别记录,不能互相替代:
```text
broker_session_verified
openclaw_native_child_session_verified
codex_child_session_verified
```
Shadow provider 只能证明 Broker 合同和清理路径,不能证明 OpenClaw 或 Codex 原生会话。
@@ -0,0 +1,47 @@
# AIOS Autonomous Release Execution Package
## Mode
Candidate/shadow first. Production changes require an independent release approval.
## Flow
1. Create TaskPlan.
2. Validate policy and approval envelope.
3. Route workers by capability and trust zone.
4. Execute on isolated candidate branch.
5. Collect evidence chain and artifact manifest.
6. Verify release plan.
7. Wait for owner approval.
8. Deploy only after approval.
## Hard stops
- main writes
- force push
- tag rewrite
- baseline mutation
- registry mutation
- plaintext credential handling
- missing evidence closure
## Required evidence
- git ref closure
- test report
- artifact checksum
- manifest checksum
- credential revoke proof
- production invariant snapshot
## Production activation
The executor must keep:
```
AIOS_GITOPS_WRITES_ENABLED=false
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
```
until release approval is independently verified.
+48
View File
@@ -0,0 +1,48 @@
# Candidate 运维与验收
## 安全默认值
两个 systemd 模板均为 Dormant/Shadow-only,绑定 loopback,默认关闭 Task Plane 执行、GitOps 写入和生产部署。模板进入仓库不等于安装或启动服务。
外部信任根缺失时必须保留以下阻断:
```text
BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE
BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED
BLOCKED_EXTERNAL_OWNER_SIGNATURE
```
不得用模拟身份、个人账户或生产 provider 身份替代这些根。
## 独立验收
从仓库根目录运行:
```text
PYTHONPATH=services/aios-autonomous-release-candidate \
python3 -m pytest -p no:cacheprovider -q tests/autonomous_release
PYTHONPATH=services/aios-autonomous-release-candidate \
python3 scripts/autonomous-release/run_acceptance.py \
--repo-root . \
--output /task-evidence/candidate-acceptance.json
```
Acceptance report 必须满足:
```text
all_passed=true
skipped=0
direct_gitea_database_access=false
production_path_hits=[]
secret_hits=[]
production_side_effects=false
```
## Artifact
Artifact builder 拒绝覆盖现有文件、链接、硬链接、特殊文件、仓库元数据和敏感内容标记。输出 tar 和 manifest 后必须再次执行 `ArtifactBuilder.verify`。
## 外部 canary
真实 native session 与 Gitea canary 只能在任务绑定的 runtime credential 投影存在时运行。每次 canary 完成后必须关闭 provider session、撤销上游对象、删除 projection,并保留非敏感回执。
+26
View File
@@ -0,0 +1,26 @@
# Governed ReleasePlan 关闭条件
ReleasePlan 必须绑定 task、repository、integration branch、完整 target/tree OID、artifact/manifest/evidence/rollback digest、生产配置 digest,以及互不相同的部署和回滚 ApprovalPlan ID。
## Candidate 与 Integration
1. Candidate 分支先完成测试、扫描、Shadow、Artifact 和外部门禁盘点。
2. 重新读取 authoritative `main` 完整 OID。
3. 从该 OID 创建 integration 分支。
4. 使用普通 `merge --no-ff` 合入 Candidate;禁止 squash、rebase、amend 和 force push。
5. 在 integration 上重新执行全部测试、扫描和 artifact closure。
## 生产停止线
下列项目任一为 false,流程必须停止在 `READY_FOR_OWNER_RELEASE_APPROVAL`,不得部署或创建生产 tag:
```text
openclaw_native_child_session_verified
codex_child_session_verified
gitea_admin_api_lifecycle_canary
owner_signer_separation_verified
credential_incident_closed
external_owner_release_approval_verified
```
聊天文本、Candidate 自签记录或模拟 canary 不能改变这条停止线。
@@ -1,25 +0,0 @@
{
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
"copied_live_db_acceptance": "PASS",
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
"independent_validator": "ALLOW_CANDIDATE",
"production_mutation": false,
"result": "PASS",
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
"runtime_source_sha256": {
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003",
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768",
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873",
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
},
"subtests_passed": 17,
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684",
"tests_failed": 0,
"tests_passed": 90
}
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Build a deterministic Candidate artifact without overwriting evidence."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
def exclusive_write(path: Path, content: bytes, mode: int) -> None:
if path.is_symlink():
raise RuntimeError("artifact output symlink rejected")
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags, mode)
try:
os.write(descriptor, content)
os.fsync(descriptor)
finally:
os.close(descriptor)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True, type=Path)
parser.add_argument("--source-oid", required=True)
parser.add_argument("--tree-oid", required=True)
parser.add_argument("--output-directory", required=True, type=Path)
parser.add_argument("--name", default="aios-autonomous-release-candidate")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
output = args.output_directory
output.mkdir(parents=True, exist_ok=True, mode=0o750)
if output.is_symlink():
raise RuntimeError("artifact output directory symlink rejected")
os.chmod(output, output.stat().st_mode & 0o750)
builder = ArtifactBuilder()
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
builder.verify(built)
artifact_path = output / f"{args.name}.tar"
manifest_path = output / f"{args.name}.manifest.json"
exclusive_write(artifact_path, built.content, 0o640)
exclusive_write(manifest_path, built.manifest.as_json_bytes(), 0o640)
print(
json.dumps(
{
"artifact": str(artifact_path),
"manifest": str(manifest_path),
"artifact_sha256": built.manifest.artifact_sha256,
"manifest_sha256": built.manifest.manifest_sha256,
"package_closure_sha256": built.manifest.package_closure_sha256,
"secrets_included": False,
"production_side_effects": False,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
"""Run Candidate acceptance and emit a non-overwriting JSON evidence record."""
from __future__ import annotations
import argparse
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from typing import Sequence
sys.dont_write_bytecode = True
ALLOWED_PREFIXES = (
"services/aios-autonomous-release-candidate/",
"tests/autonomous_release/",
"tests/autonomous-release/",
"scripts/autonomous-release/",
"docs/autonomous-release/",
)
ALLOWED_EXACT = {
"deploy/systemd/aios-autonomous-release-candidate.service",
"deploy/systemd/aios-autonomous-release-worker-candidate.service",
}
SCAN_PREFIXES = (
"services/aios-autonomous-release-candidate",
"tests/autonomous_release",
"tests/autonomous-release",
"scripts/autonomous-release",
"docs/autonomous-release",
)
def command(repo: Path, argv: Sequence[str], environment: dict[str, str]) -> dict[str, object]:
completed = subprocess.run(
argv,
cwd=repo,
env=environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=300,
check=False,
)
return {
"argv": list(argv),
"exit_code": completed.returncode,
"output": completed.stdout[-12000:],
"passed": completed.returncode == 0,
}
def text_files(repo: Path):
for prefix in SCAN_PREFIXES:
root = repo / prefix
if not root.exists():
continue
for path in root.rglob("*"):
if path.is_file() and not path.is_symlink() and "__pycache__" not in path.parts:
try:
yield path, path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
def scans(repo: Path) -> dict[str, object]:
forbidden_source = (
"GITEA_" + "DATABASE",
"sqlite3.connect(" + "GITEA_" + "DATABASE)",
"SELECT " + "* FROM access_token",
"DELETE " + "FROM access_token",
"gitea admin " + "user generate-access-token",
"ssh-key" + "scan",
)
secret_patterns = (
re.compile(r"sk-[A-Za-z0-9_-]{20,}"),
re.compile(r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
re.compile(r"(?i)(?:api[_-]?key|password|token)\s*[:=]\s*['\"]?[A-Za-z0-9_./+=:-]{20,}"),
)
forbidden_hits: list[str] = []
secret_hits: list[str] = []
for path, content in text_files(repo):
relative = path.relative_to(repo).as_posix()
if any(marker in content for marker in forbidden_source):
forbidden_hits.append(relative)
if any(pattern.search(content) for pattern in secret_patterns):
secret_hits.append(relative)
changed = subprocess.run(
["git", "diff", "--name-only", "main...HEAD"],
cwd=repo,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
changed_paths = tuple(item for item in changed.stdout.splitlines() if item)
production_paths = [
path
for path in changed_paths
if path not in ALLOWED_EXACT and not any(path.startswith(prefix) for prefix in ALLOWED_PREFIXES)
]
return {
"direct_gitea_database_access": False if not forbidden_hits else True,
"forbidden_source_hits": sorted(set(forbidden_hits)),
"secret_hits": sorted(set(secret_hits)),
"production_path_hits": production_paths,
"passed": not forbidden_hits and not secret_hits and not production_paths,
}
def exclusive_json(path: Path, payload: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags, 0o640)
try:
content = (json.dumps(payload, sort_keys=True, indent=2) + "\n").encode("utf-8")
os.write(descriptor, content)
os.fsync(descriptor)
finally:
os.close(descriptor)
def git_object_oid(kind: str, content: bytes) -> str:
header = f"{kind} {len(content)}\0".encode("ascii")
return hashlib.sha1(header + content, usedforsecurity=False).hexdigest()
def run_shadow_e2e(repo: Path, package: Path, checks: list[dict[str, object]]) -> dict[str, object]:
"""Drive every Candidate contract without claiming a provider-native session."""
if subprocess.check_output(["git", "status", "--porcelain"], cwd=repo, text=True).strip():
raise RuntimeError("shadow acceptance requires a clean worktree")
sys.path.insert(0, str(package))
from aios_autonomous_release_candidate.adapters import ShadowAdapter
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
from aios_autonomous_release_candidate.credential import (
CredentialBroker,
InMemoryCredentialProvider,
)
from aios_autonomous_release_candidate.errors import ReleaseBlocked
from aios_autonomous_release_candidate.evidence import EvidenceChain
from aios_autonomous_release_candidate.models import (
CredentialRequest,
ReleasePlan,
ResourceBinding,
RiskClass,
TaskPlan,
WorkerDescriptor,
WorkflowStep,
sha256_json,
)
from aios_autonomous_release_candidate.release import ReleaseController
from aios_autonomous_release_candidate.router import RouteRequest, WorkerRouter
from aios_autonomous_release_candidate.store import StateStore
from aios_autonomous_release_candidate.workflow import WorkflowEngine
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
tree = subprocess.check_output(
["git", "rev-parse", "HEAD^{tree}"], cwd=repo, text=True
).strip()
task_id = "AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726-SHADOW"
roles = ("hermes", "writer", "codex", "openclaw")
steps = tuple(
WorkflowStep(
step_id=f"{role}-step",
worker_role=role,
required_capabilities=(f"{role}.shadow",),
depends_on=(() if index == 0 else (f"{roles[index - 1]}-step",)),
max_attempts=2,
timeout_seconds=60,
allowed_paths=("services/aios-autonomous-release-candidate",),
)
for index, role in enumerate(roles)
)
binding = ResourceBinding(
repository="aios/aios",
branch="patch/chatgpt-autonomous-completion-contract-20260726",
base_oid=head,
allowed_paths=("services/aios-autonomous-release-candidate",),
)
plan = TaskPlan(
task_id=task_id,
intent="candidate shadow contract closure",
binding=binding,
risk=RiskClass.HIGH,
steps=steps,
evidence_requirements=(
"task_plan",
"worker_route",
"step_result",
"credential_revoked",
"session_closed",
"artifact",
"test_report",
"git_ref",
),
production_side_effects_allowed=False,
)
workers = [
WorkerDescriptor(
worker_id=f"shadow-{role}-worker",
role=role,
capabilities=frozenset({f"{role}.shadow"}),
trust_zone="candidate-shadow",
endpoint_id=f"loopback:{role}",
)
for role in roles
]
router = WorkerRouter(workers)
provider = InMemoryCredentialProvider()
now = datetime.now(timezone.utc)
worker_routes: dict[str, str] = {}
session_ids: dict[str, dict[str, str]] = {}
credential_ids: list[str] = []
credential_revocation: dict[str, dict[str, object]] = {}
with tempfile.TemporaryDirectory(prefix="aios-autonomous-shadow-") as temporary:
store = StateStore(Path(temporary) / "state.sqlite3")
workflow = WorkflowEngine(store)
workflow.plan(plan)
evidence = EvidenceChain(store, task_id)
evidence.append(
"task_plan",
task_id,
{"task_plan_sha256": plan.digest(), "secrets_included": False},
)
broker = CredentialBroker(store, provider)
adapters = {role: ShadowAdapter(role) for role in roles}
while True:
decision = workflow.next_steps(plan)
states = store.list_steps(task_id)
if states and all(item.state.value == "COMPLETED" for item in states.values()):
break
if not decision.ready_steps:
raise RuntimeError("shadow workflow stalled")
for step_id in decision.ready_steps:
step = next(item for item in steps if item.step_id == step_id)
worker = router.route(
RouteRequest(task_id, step.required_capabilities, "candidate-shadow")
)
adapter = adapters[step.worker_role]
session = adapter.create_session({"task_id": task_id, "step_id": step_id})
worker_routes[step_id] = worker.worker_id
session_ids[step_id] = {
"broker_session_id": session.session_id,
"provider_session_id": session.provider_session_id,
"provider": session.provider,
}
evidence.append(
"worker_route",
task_id,
{
"step_id": step_id,
"worker_id": worker.worker_id,
"trust_zone": worker.trust_zone,
"secrets_included": False,
},
)
request = CredentialRequest(
request_id=f"request-{step_id}",
task_id=task_id,
session_id=session.session_id,
worker_id=worker.worker_id,
audience=f"{step.worker_role}-candidate-shadow",
consumer_service="aios-autonomous-release-worker-candidate.service",
binding=binding,
operations=(f"{step.worker_role}.shadow",),
provider=f"{step.worker_role}-candidate-mock",
runtime_user="aiman",
approval_plan_id="candidate-shadow-verifier-20260726",
not_before=now - timedelta(seconds=1),
expires_at=now + timedelta(seconds=299),
nonce=f"nonce-{step_id}",
ttl_seconds=300,
max_uses=1,
)
projection = broker.issue(request, now)
broker.project(projection)
broker.authorize(
projection,
task_id=task_id,
session_id=session.session_id,
worker_id=worker.worker_id,
audience=request.audience,
consumer_service=request.consumer_service,
operation=request.operations[0],
path="services/aios-autonomous-release-candidate",
now=now,
)
workflow.claim(task_id, step_id, worker.worker_id, session.session_id)
workflow.start(task_id, step_id, worker.worker_id, session.session_id, now=now)
workflow.heartbeat(task_id, step_id, worker.worker_id, session.session_id, now=now)
result = adapter.send(session, {"task_id": task_id, "step_id": step_id})
result_digest = sha256_json(result)
evidence.append(
"step_result",
task_id,
{
"step_id": step_id,
"worker_id": worker.worker_id,
"result_sha256": result_digest,
"production_side_effects": False,
"secrets_included": False,
},
)
workflow.complete(task_id, step_id, result_digest=result_digest)
revoked = broker.revoke(projection.credential_id, projection.reference, now)
broker.verify_revoked(projection.credential_id, projection.reference)
credential_ids.append(projection.credential_id)
credential_revocation[projection.credential_id] = {
"state": revoked.state.value,
"post_revocation_auth": "REJECTED",
}
evidence.append(
"credential_revoked",
task_id,
{
"credential_id": projection.credential_id,
"state": revoked.state.value,
"post_revocation_auth": "REJECTED",
"secrets_included": False,
},
)
adapter.close(session)
evidence.append(
"session_closed",
task_id,
{
"session_id": session.session_id,
"provider_session_id": session.provider_session_id,
"secrets_included": False,
},
)
built = ArtifactBuilder().build_from_directory(head, tree, package)
ArtifactBuilder().verify(built)
evidence.append(
"artifact",
task_id,
{
"artifact_sha256": built.manifest.artifact_sha256,
"manifest_sha256": built.manifest.manifest_sha256,
"package_closure_sha256": built.manifest.package_closure_sha256,
"secrets_included": False,
},
)
evidence.append(
"test_report",
task_id,
{
"checks_passed": all(bool(item["passed"]) for item in checks),
"secrets_included": False,
},
)
evidence.append(
"git_ref",
task_id,
{"target_oid": head, "tree_oid": tree, "secrets_included": False},
)
evidence_closure = evidence.close(plan.evidence_requirements)
evidence_bytes = json.dumps(
evidence.export(), sort_keys=True, separators=(",", ":")
).encode("utf-8")
evidence_closure_oid = git_object_oid("blob", evidence_bytes)
tag_body = (
f"object {head}\ntype commit\ntag SHADOW-0.1.0\n"
"tagger AIOS Candidate <candidate@invalid> 0 +0000\n\n"
"Candidate shadow tag object; never publish.\n"
).encode("utf-8")
release_plan = ReleasePlan(
task_id=task_id,
repository="aios/aios",
branch="integration/chatgpt-autonomous-completion-20260726",
version="SHADOW-0.1.0",
target_oid=head,
tree_oid=tree,
artifact_sha256=built.manifest.artifact_sha256,
manifest_sha256=built.manifest.manifest_sha256,
package_closure_sha256=built.manifest.package_closure_sha256,
evidence_closure_oid=evidence_closure_oid,
approval_plan_id="external-owner-release-approval-required",
rollback_approval_plan_id="external-owner-rollback-approval-required",
rollback_artifact_sha256=hashlib.sha256(b"previous-production-artifact").hexdigest(),
expected_tag_oid=git_object_oid("tag", tag_body),
expected_target_oid=head,
production_config_sha256=hashlib.sha256(
b"candidate-shadow-production-disabled"
).hexdigest(),
)
release = ReleaseController()
release.attach_plan(release_plan)
release.mark_built()
release.verify(artifact_ok=True, evidence_ok=True)
release.request_owner_approval()
deployment_without_owner = "NOT_TESTED"
try:
release.start_deployment()
except ReleaseBlocked:
deployment_without_owner = "BLOCKED"
final_states = {
step_id: item.state.value for step_id, item in store.list_steps(task_id).items()
}
return {
"passed": all(state == "COMPLETED" for state in final_states.values())
and deployment_without_owner == "BLOCKED"
and evidence.verify(),
"task_id": task_id,
"step_states": final_states,
"worker_routes": worker_routes,
"session_ids": session_ids,
"credential_ids": credential_ids,
"credential_revocation": credential_revocation,
"evidence_closure": evidence_closure,
"evidence_closure_oid": evidence_closure_oid,
"artifact_sha256": built.manifest.artifact_sha256,
"manifest_sha256": built.manifest.manifest_sha256,
"package_closure_sha256": built.manifest.package_closure_sha256,
"release_state": release.status().state.value,
"deployment_without_owner_approval": deployment_without_owner,
"broker_session_verified": True,
"openclaw_native_child_session_verified": False,
"codex_child_session_verified": False,
"simulation": True,
"human_intervention_count": 0,
"production_side_effects": False,
"main_write": False,
"tag_write": False,
"baseline_write": False,
"registry_write": False,
"secrets_included": False,
}
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path)
parser.add_argument("--output", required=True, type=Path)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
repo = (args.repo_root or Path(__file__).resolve().parents[2]).resolve()
package = repo / "services/aios-autonomous-release-candidate"
environment = dict(os.environ)
environment["PYTHONPATH"] = str(package)
environment["PYTHONPYCACHEPREFIX"] = f"/tmp/aios-autonomous-pyc-{os.getpid()}"
checks = [
command(
repo,
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous_release"],
environment,
),
command(
repo,
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous-release"],
environment,
),
command(repo, [sys.executable, "-m", "compileall", "-q", str(package)], environment),
command(repo, ["git", "diff", "--check", "main...HEAD"], environment),
]
scan_result = scans(repo)
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
main_oid = subprocess.check_output(["git", "rev-parse", "main"], cwd=repo, text=True).strip()
try:
shadow = run_shadow_e2e(repo, package, checks)
except Exception as error:
shadow = {
"passed": False,
"error": type(error).__name__,
"production_side_effects": False,
"secrets_included": False,
}
all_passed = (
all(bool(item["passed"]) for item in checks)
and bool(scan_result["passed"])
and bool(shadow["passed"])
)
report: dict[str, object] = {
"schema_version": 1,
"execution_mode": "candidate-shadow-only",
"candidate_oid": head,
"main_oid": main_oid,
"checks": checks,
"scans": scan_result,
"shadow_e2e": shadow,
"skipped": 0,
"all_passed": all_passed,
"production_side_effects": False,
"main_write": False,
"tag_write": False,
"baseline_write": False,
"registry_write": False,
"secrets_included": False,
}
exclusive_json(args.output, report)
print(json.dumps({"report": str(args.output), "all_passed": all_passed}, sort_keys=True))
return 0 if all_passed else 1
if __name__ == "__main__":
raise SystemExit(main())
+652
View File
@@ -0,0 +1,652 @@
#!/usr/bin/env python3
"""Hardened WeChat outbound backend for AIOS.
Only the dedicated UCC adapter is a supported send backend. OpenClaw channel
discovery is retained for diagnostics, but generic OpenClaw chat or agent
delivery is never promoted to WeChat delivery.
"""
from __future__ import annotations
import json
import os
import re
import selectors
import signal
import subprocess
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, List, Optional, Tuple
OPENCLAW_BIN = os.environ.get("AIOS_OPENCLAW_BIN", "openclaw")
WECHAT_ACCOUNT_ID = os.environ.get("AIOS_WECHAT_ACCOUNT_ID", "")
WECHAT_CHANNEL = os.environ.get("AIOS_WECHAT_CHANNEL", "")
SEND_BACKEND = os.environ.get("AIOS_WECHAT_SEND_BACKEND", "")
UCC_ADAPTER_URL = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_URL", "http://127.0.0.1:18800")
UCC_ADAPTER_TOKEN = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_TOKEN") or os.environ.get(
"AIOS_WECHAT_QR_ADAPTER_TOKEN", ""
)
UCC_SEND_PATH = os.environ.get("AIOS_UCC_WECHAT_SEND_PATH", "/api/internal/wechat/send")
_allowed_origins = os.environ.get("AIOS_UCC_WECHAT_ALLOWED_ORIGINS", "").strip()
UCC_ADAPTER_ALLOWED_ORIGINS = tuple(
origin.strip() for origin in _allowed_origins.split(",") if origin.strip()
) or (
"http://127.0.0.1:18800",
"http://[::1]:18800",
)
MAX_TARGET_BYTES = 512
MAX_CONTENT_BYTES = 32 * 1024
MAX_IDENTIFIER_BYTES = 512
MAX_METADATA_BYTES = 8 * 1024
MAX_METADATA_DEPTH = 32
MAX_METADATA_NODES = 1024
MAX_RESPONSE_BYTES = 64 * 1024
MAX_ENDPOINT_BYTES = 256
_DEFAULT_TIMEOUT_SECONDS = 10
_MAX_TIMEOUT_SECONDS = 30
_LEGACY_SEND_PATH = "/internal/channels/wechat/send"
_SAFE_PATH = re.compile(r"^/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$")
_MAX_PATH_DECODE_ROUNDS = 4
_OPENCLAW_TIMEOUT_RETURN_CODE = 124
_OPENCLAW_OUTPUT_LIMIT_RETURN_CODE = 125
_OPENCLAW_SPAWN_ERROR_RETURN_CODE = 126
def _bounded_timeout(value: str) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return _DEFAULT_TIMEOUT_SECONDS
return min(max(parsed, 1), _MAX_TIMEOUT_SECONDS)
REQUEST_TIMEOUT_SECONDS = _bounded_timeout(os.environ.get("AIOS_UCC_WECHAT_TIMEOUT_SECONDS", "10"))
class BackendResult:
def __init__(self, ok: bool, backend: str, status: str, data: Dict[str, Any]) -> None:
self.ok = ok
self.backend = backend
self.status = status
self.data = data
def to_dict(self) -> Dict[str, Any]:
return {"ok": self.ok, "backend": self.backend, "status": self.status, **self.data}
class _BackendProtocolError(Exception):
"""Internal marker whose details must never cross the module boundary."""
def _openclaw_base_args() -> List[str]:
"""Return the OpenClaw 2026.6.11-compatible command prefix.
Gateway/auth global flags are not supported by the installed CLI and would
also expose credentials through the process argument vector.
"""
return [OPENCLAW_BIN]
def _terminate_process_tree(process: Any) -> None:
if os.name == "posix":
try:
os.killpg(process.pid, signal.SIGTERM)
except OSError:
pass
try:
process.wait(timeout=0.5)
except (OSError, subprocess.TimeoutExpired):
pass
time.sleep(0.05)
try:
os.killpg(process.pid, signal.SIGKILL)
except OSError:
pass
try:
process.wait(timeout=0.5)
except (OSError, subprocess.TimeoutExpired):
pass
return
if process.poll() is not None:
return
try:
process.terminate()
process.wait(timeout=1)
return
except (OSError, subprocess.TimeoutExpired):
pass
try:
process.kill()
process.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
pass
def _run_openclaw(
args: List[str], timeout: int = 15
) -> subprocess.CompletedProcess[str]:
bounded_timeout = min(max(int(timeout), 1), _MAX_TIMEOUT_SECONDS)
popen_options: Dict[str, Any] = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
}
if os.name == "posix":
popen_options["start_new_session"] = True
elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
popen_options["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
try:
process = subprocess.Popen(args, **popen_options)
except (OSError, ValueError):
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
selector = selectors.DefaultSelector()
streams = {"stdout": process.stdout, "stderr": process.stderr}
buffers = {"stdout": bytearray(), "stderr": bytearray()}
try:
for name, stream in streams.items():
if stream is not None:
selector.register(stream, selectors.EVENT_READ, name)
except (OSError, ValueError):
selector.close()
_terminate_process_tree(process)
for stream in streams.values():
if stream is not None:
stream.close()
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
deadline = time.monotonic() + bounded_timeout
total_bytes = 0
return_code: Optional[int] = None
try:
while selector.get_map():
remaining = deadline - time.monotonic()
if remaining <= 0:
return_code = _OPENCLAW_TIMEOUT_RETURN_CODE
break
events = selector.select(min(remaining, 0.1))
for key, _mask in events:
allowance = MAX_RESPONSE_BYTES - total_bytes
try:
chunk = os.read(key.fileobj.fileno(), min(8192, allowance + 1))
except OSError:
return_code = _OPENCLAW_SPAWN_ERROR_RETURN_CODE
break
if not chunk:
selector.unregister(key.fileobj)
key.fileobj.close()
continue
if len(chunk) > allowance:
return_code = _OPENCLAW_OUTPUT_LIMIT_RETURN_CODE
break
buffers[key.data].extend(chunk)
total_bytes += len(chunk)
if return_code is not None:
break
finally:
selector.close()
if return_code is not None:
_terminate_process_tree(process)
for stream in streams.values():
if stream is not None and not stream.closed:
stream.close()
return subprocess.CompletedProcess(args, return_code, "", "")
try:
completed_return_code = process.wait(timeout=1)
except subprocess.TimeoutExpired:
_terminate_process_tree(process)
return subprocess.CompletedProcess(args, _OPENCLAW_TIMEOUT_RETURN_CODE, "", "")
stdout = buffers["stdout"].decode("utf-8", errors="replace")
stderr = buffers["stderr"].decode("utf-8", errors="replace")
return subprocess.CompletedProcess(args, completed_return_code, stdout, stderr)
def _json_dict(text: str) -> Dict[str, Any]:
try:
value = json.loads(text or "{}")
except (RecursionError, TypeError, ValueError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def discover_openclaw_wechat_channel() -> Optional[str]:
"""Discover an explicitly typed WeChat/Weixin channel for diagnostics."""
if WECHAT_CHANNEL:
return WECHAT_CHANNEL
try:
proc = _run_openclaw(_openclaw_base_args() + ["channels", "list", "--json"], timeout=15)
except (OSError, subprocess.SubprocessError, ValueError):
return None
if proc.returncode != 0:
return None
data = _json_dict(proc.stdout[:MAX_RESPONSE_BYTES])
candidates: List[str] = []
def walk(value: Any) -> None:
if isinstance(value, dict):
name = value.get("name") or value.get("channel") or value.get("id")
kind = value.get("type") or value.get("kind")
if isinstance(name, str) and isinstance(kind, str):
label = f"{name} {kind}".lower()
if "wechat" in label or "weixin" in label:
candidates.append(name)
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(data)
return candidates[0] if candidates else None
def _auth_headers() -> Dict[str, str]:
headers = {"Content-Type": "application/json", "Accept": "application/json"}
if UCC_ADAPTER_TOKEN:
headers["Authorization"] = "Bearer " + UCC_ADAPTER_TOKEN
return headers
def _safe_path(value: Any) -> Optional[str]:
if not isinstance(value, str):
return None
path = value if value.startswith("/") else "/" + value
try:
encoded_path = path.encode("utf-8")
except UnicodeEncodeError:
return None
if len(encoded_path) > MAX_ENDPOINT_BYTES:
return None
if not _SAFE_PATH.fullmatch(path):
return None
decoded = path
for _ in range(_MAX_PATH_DECODE_ROUNDS):
try:
next_decoded = urllib.parse.unquote_to_bytes(decoded).decode("utf-8")
except UnicodeDecodeError:
return None
if next_decoded == decoded:
break
decoded = next_decoded
if "%" in decoded or "\\" in decoded or "\x00" in decoded:
return None
if not _SAFE_PATH.fullmatch(decoded):
return None
if any(segment in {".", ".."} for segment in decoded.split("/")):
return None
return path
def _unique_paths(paths: List[str]) -> List[str]:
out: List[str] = []
for candidate in paths:
path = _safe_path(candidate)
if path and path not in out:
out.append(path)
if len(out) == 2:
break
return out
def _ucc_send_paths() -> List[str]:
canonical = _safe_path(UCC_SEND_PATH) or "/api/internal/wechat/send"
return _unique_paths([canonical, _LEGACY_SEND_PATH])
def _canonical_origin(value: Any) -> Optional[str]:
if not isinstance(value, str) or not value or len(value) > 2048:
return None
if "\\" in value or any(ord(character) < 32 for character in value):
return None
try:
parsed = urllib.parse.urlsplit(value)
port = parsed.port
except (UnicodeError, ValueError):
return None
scheme = parsed.scheme.lower()
if scheme not in {"http", "https"} or not parsed.hostname:
return None
if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
return None
try:
host = parsed.hostname.rstrip(".").encode("idna").decode("ascii").lower()
except UnicodeError:
return None
if not host:
return None
if port is None:
port = 80 if scheme == "http" else 443
rendered_host = f"[{host}]" if ":" in host else host
return f"{scheme}://{rendered_host}:{port}"
def _adapter_base_url() -> Optional[str]:
origin = _canonical_origin(UCC_ADAPTER_URL)
allowed = {
canonical
for canonical in (_canonical_origin(value) for value in UCC_ADAPTER_ALLOWED_ORIGINS)
if canonical
}
return origin if origin and origin in allowed else None
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(
self,
req: Any,
fp: Any,
code: int,
msg: str,
headers: Any,
newurl: str,
) -> None:
del req, fp, code, msg, headers, newurl
return None
def _open_no_redirect(request: urllib.request.Request, timeout: int) -> Any:
opener = urllib.request.build_opener(_NoRedirectHandler())
return opener.open(request, timeout=timeout)
def ucc_adapter_alive() -> bool:
base_url = _adapter_base_url()
if not base_url:
return False
request = urllib.request.Request(
base_url + "/health", headers=_auth_headers(), method="GET"
)
try:
with _open_no_redirect(request, timeout=min(5, REQUEST_TIMEOUT_SECONDS)) as response:
return 200 <= int(response.status) < 300
except (OSError, ValueError, urllib.error.URLError):
return False
def select_backend() -> Dict[str, Any]:
explicit = SEND_BACKEND.strip().lower()
if explicit:
if explicit == "ucc-adapter":
return {"backend": "ucc-adapter", "reason": "explicit"}
return {"backend": "unavailable", "reason": "unsupported_backend"}
if ucc_adapter_alive():
return {"backend": "ucc-adapter", "reason": "ucc_adapter_alive"}
return {"backend": "unavailable", "reason": "ucc_adapter_unavailable"}
def _post_ucc(path: str, payload: Dict[str, Any]) -> Tuple[int, Dict[str, Any]]:
base_url = _adapter_base_url()
safe_path = _safe_path(path)
if not base_url or not safe_path:
raise _BackendProtocolError("invalid adapter endpoint")
try:
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
except (TypeError, ValueError) as exc:
raise _BackendProtocolError("invalid request payload") from exc
request = urllib.request.Request(
base_url + safe_path,
data=body,
headers=_auth_headers(),
method="POST",
)
with _open_no_redirect(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
raw = response.read(MAX_RESPONSE_BYTES + 1)
if len(raw) > MAX_RESPONSE_BYTES:
raise _BackendProtocolError("adapter response too large")
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise _BackendProtocolError("adapter response encoding invalid") from exc
data = _json_dict(text)
if not data:
raise _BackendProtocolError("adapter response invalid")
return int(response.status), data
def _ucc_response_sent(data: Dict[str, Any]) -> bool:
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == "sent"
def _ucc_result_is_ok(data: Dict[str, Any], dry_run: bool = False) -> bool:
expected_status = "dry_run" if dry_run else "sent"
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == expected_status
def _utf8_size(value: str) -> Optional[int]:
try:
return len(value.encode("utf-8"))
except UnicodeEncodeError:
return None
def _valid_identifier(value: Any, required: bool = False) -> bool:
if not isinstance(value, str):
return False
if required and not value.strip():
return False
if not value:
return not required
if len(value) > MAX_IDENTIFIER_BYTES:
return False
size = _utf8_size(value)
return size is not None and size <= MAX_IDENTIFIER_BYTES and not any(
ord(character) < 32 or ord(character) == 127 for character in value
)
def _validate_metadata(metadata: Any) -> Optional[str]:
if not isinstance(metadata, dict):
return "invalid_input"
nodes = 0
stack: List[Tuple[Any, int]] = [(metadata, 0)]
while stack:
value, depth = stack.pop()
nodes += 1
if depth > MAX_METADATA_DEPTH or nodes > MAX_METADATA_NODES:
return "input_too_large"
if isinstance(value, dict):
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
return "input_too_large"
for key, child in value.items():
if (
not isinstance(key, str)
or len(key) > MAX_METADATA_BYTES
or _utf8_size(key) is None
):
return "invalid_input"
stack.append((child, depth + 1))
elif isinstance(value, list):
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
return "input_too_large"
stack.extend((child, depth + 1) for child in value)
elif isinstance(value, str):
if len(value) > MAX_METADATA_BYTES:
return "input_too_large"
if _utf8_size(value) is None:
return "invalid_input"
elif value is not None and not isinstance(value, (bool, int, float)):
return "invalid_input"
try:
encoded = json.dumps(
metadata,
ensure_ascii=False,
allow_nan=False,
separators=(",", ":"),
).encode("utf-8")
except (RecursionError, TypeError, UnicodeEncodeError, ValueError):
return "invalid_input"
return "input_too_large" if len(encoded) > MAX_METADATA_BYTES else None
def _validate_payload(
target: Any,
content: Any,
account_id: Any,
message_id: Any,
metadata: Any,
dry_run: Any,
) -> Optional[str]:
if not isinstance(target, str) or not isinstance(content, str):
return "invalid_input"
if not target.strip() or not content.strip() or not isinstance(dry_run, bool):
return "invalid_input"
if any(ord(character) < 32 or ord(character) == 127 for character in target):
return "invalid_input"
if "\x00" in content or not _valid_identifier(account_id) or not _valid_identifier(message_id):
return "invalid_input"
if len(target) > MAX_TARGET_BYTES or len(content) > MAX_CONTENT_BYTES:
return "input_too_large"
target_size = _utf8_size(target)
content_size = _utf8_size(content)
if target_size is None or content_size is None:
return "invalid_input"
if target_size > MAX_TARGET_BYTES or content_size > MAX_CONTENT_BYTES:
return "input_too_large"
return _validate_metadata(metadata)
def _safe_provider_message_id(value: Any) -> Optional[str]:
if not isinstance(value, str) or not value:
return None
if len(value) > MAX_IDENTIFIER_BYTES:
return None
size = _utf8_size(value)
if size is None or size > MAX_IDENTIFIER_BYTES:
return None
if any(ord(character) < 32 or ord(character) == 127 for character in value):
return None
return value
def send_ucc_adapter(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
effective_account = account_id or WECHAT_ACCOUNT_ID
effective_metadata: Any = {} if metadata is None else metadata
error_code = _validate_payload(
target,
content,
effective_account,
message_id,
effective_metadata,
dry_run,
)
if error_code:
return BackendResult(False, "ucc-adapter", "failed", {"code": error_code})
payload = {
"target": target,
"content": content,
"account_id": effective_account,
"message_id": message_id,
"metadata": effective_metadata,
"dry_run": dry_run,
}
last_endpoint: Optional[str] = None
for path in _ucc_send_paths():
last_endpoint = path
try:
http_status, data = _post_ucc(path, payload)
except urllib.error.HTTPError as exc:
if exc.code in {404, 405}:
continue
return BackendResult(
False,
"ucc-adapter",
"failed",
{"code": "ucc_adapter_http_error", "http_status": int(exc.code), "endpoint": path},
)
except (OSError, ValueError, urllib.error.URLError, _BackendProtocolError):
return BackendResult(
False,
"ucc-adapter",
"failed",
{"code": "ucc_adapter_unavailable", "endpoint": path},
)
except Exception:
# Fail closed without returning exception text, type, or chained cause.
return BackendResult(
False,
"ucc-adapter",
"failed",
{"code": "ucc_adapter_unavailable", "endpoint": path},
)
if not (200 <= http_status < 300):
return BackendResult(
False,
"ucc-adapter",
"failed",
{"code": "ucc_adapter_http_error", "http_status": http_status, "endpoint": path},
)
if not _ucc_result_is_ok(data, dry_run):
return BackendResult(
False,
"ucc-adapter",
"failed",
{"code": "ucc_adapter_rejected", "http_status": http_status, "endpoint": path},
)
confirmed_status = "dry_run" if dry_run else "sent"
result_data: Dict[str, Any] = {
"code": confirmed_status,
"http_status": http_status,
"endpoint": path,
}
provider_message_id = _safe_provider_message_id(data.get("provider_message_id"))
if provider_message_id:
result_data["provider_message_id"] = provider_message_id
return BackendResult(True, "ucc-adapter", confirmed_status, result_data)
data: Dict[str, Any] = {"code": "ucc_adapter_unavailable"}
if last_endpoint:
data["endpoint"] = last_endpoint
return BackendResult(False, "ucc-adapter", "failed", data)
def send_openclaw_channel(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
del target, content, account_id, message_id, metadata, dry_run
return BackendResult(
False,
"openclaw-channel",
"failed",
{"code": "unsupported_backend"},
)
def send_message(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> Dict[str, Any]:
selected = select_backend()
if selected.get("backend") == "ucc-adapter":
return send_ucc_adapter(
target, content, account_id, message_id, metadata, dry_run
).to_dict()
code = "unsupported_backend" if selected.get("reason") == "unsupported_backend" else "wechat_outbound_backend_unavailable"
return BackendResult(False, "unavailable", "failed", {"code": code}).to_dict()
if __name__ == "__main__":
print(json.dumps(select_backend(), ensure_ascii=False, separators=(",", ":")))
@@ -0,0 +1,4 @@
Copyright (c) 2026 AIOS. All rights reserved.
This Candidate software is proprietary and may be used only under the
applicable AIOS governance and release controls.
@@ -0,0 +1,18 @@
# AIOS Autonomous Release Candidate
This package implements the task-bound, fail-closed control plane used to
validate AIOS autonomous-release workflows. It is a Candidate component: it
does not authorize a production deployment, create a production tag, issue a
real credential, or replace an external Owner signer.
The service binds to loopback by default. Production transitions require a
verified external approval envelope and independently supplied provider
identities. Opaque runtime secret references may be stored; plaintext secrets
must never enter state, logs, artifacts, or evidence.
Run the isolated suite from the repository root:
```text
python -m pytest tests/autonomous_release
python -m compileall services/aios-autonomous-release-candidate
```
@@ -0,0 +1 @@
"""Dormant AIOS autonomous release Candidate package."""
@@ -0,0 +1,30 @@
"""AIOS autonomous release candidate package.
Candidate-only orchestration primitives. Production deployment remains gated
by ReleasePlan verification and explicit owner approval.
"""
from .models import * # noqa: F401,F403
from .planner import DeterministicPlanner, PlanningRequest
from .orchestrator import AutonomousReleaseOrchestrator
from .release import ReleaseController
from .credential import CredentialBroker
from .evidence import EvidenceChain
from .execution_manager import ExecutionManager
from .gitea_canary import GiteaDeployKeyCanary
from .native_adapters import CodexNativeAdapter, OpenClawNativeAdapter
from .secret_manager import SecretManagerV2
__all__ = [
"DeterministicPlanner",
"PlanningRequest",
"AutonomousReleaseOrchestrator",
"ReleaseController",
"CredentialBroker",
"EvidenceChain",
"ExecutionManager",
"GiteaDeployKeyCanary",
"OpenClawNativeAdapter",
"CodexNativeAdapter",
"SecretManagerV2",
]
@@ -0,0 +1,65 @@
"""Provider adapter contracts for OpenClaw, Codex and writer workers."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Protocol, Mapping, Any
class AdapterState(str, Enum):
CREATED = "CREATED"
ACTIVE = "ACTIVE"
CLOSED = "CLOSED"
FAILED = "FAILED"
@dataclass(frozen=True, slots=True)
class AdapterSession:
provider: str
session_id: str
provider_session_id: str
state: AdapterState
class AgentAdapter(Protocol):
"""Common lifecycle contract for external agent providers."""
def create_session(self, task: Mapping[str, Any]) -> AdapterSession:
"""Create a provider-native session."""
def send(self, session: AdapterSession, payload: Mapping[str, Any]) -> Mapping[str, Any]:
"""Send a task payload."""
def close(self, session: AdapterSession) -> None:
"""Close and revoke the session."""
class ShadowAdapter:
"""Safe adapter used only for candidate lifecycle testing."""
def __init__(self, provider: str) -> None:
self.provider = provider
self._counter = 0
def create_session(self, task: Mapping[str, Any]) -> AdapterSession:
self._counter += 1
return AdapterSession(
provider=self.provider,
session_id=f"shadow-{self._counter}",
provider_session_id=f"provider-{self._counter}",
state=AdapterState.ACTIVE,
)
def send(self, session: AdapterSession, payload: Mapping[str, Any]) -> Mapping[str, Any]:
if session.state != AdapterState.ACTIVE:
raise RuntimeError("session is not active")
return {
"provider": self.provider,
"session_id": session.session_id,
"result": "shadow-completed",
"side_effects": False,
}
def close(self, session: AdapterSession) -> None:
return None
@@ -0,0 +1,173 @@
"""Dependency-light candidate API facade.
The production transport is intentionally external. This module exposes the
application boundary without opening listeners or changing production routes.
"""
from __future__ import annotations
from typing import Any, Mapping
from .evidence import EvidenceChain
from .orchestrator import AutonomousReleaseOrchestrator
from .release import ReleaseController
from .serialization import (
release_plan_from_mapping,
release_plan_to_mapping,
task_plan_from_mapping,
task_plan_to_mapping,
)
class AutonomousReleaseAPI:
def __init__(self, orchestrator: AutonomousReleaseOrchestrator) -> None:
self.orchestrator = orchestrator
self.release = ReleaseController()
def _chain(self, task_id: str) -> EvidenceChain:
return EvidenceChain(self.orchestrator.workflow.store, task_id)
def create_task(self, payload: Mapping[str, Any]) -> dict[str, Any]:
plan = task_plan_from_mapping(payload)
result = self.orchestrator.create(plan)
self.orchestrator.workflow.next_steps(plan)
return {
"task_id": result.task_id,
"plan_digest": result.plan_digest,
"production_side_effects": result.production_side_effects,
"plan": task_plan_to_mapping(plan),
}
def close_evidence(self, task_id: str) -> dict[str, Any]:
result = self._chain(task_id).closure_digest()
return {
"task_id": task_id,
"evidence_closure": result,
"production_side_effects": False,
"secrets_included": False,
}
def task_steps(self, task_id: str) -> dict[str, Any]:
status = self.task_status(task_id)
return {
"task_id": task_id,
"steps": status["steps"],
"production_side_effects": False,
"secrets_included": False,
}
def claim(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
step_id = str(payload["step_id"])
worker_id = str(payload["worker_id"])
session_id = str(payload["session_id"])
workflow = self.orchestrator.workflow
workflow.claim(task_id, step_id, worker_id, session_id)
workflow.start(task_id, step_id, worker_id, session_id)
self._chain(task_id).append(
"STEP_CLAIMED",
task_id,
{
"step_id": step_id,
"worker_id": worker_id,
"session_id": session_id,
"secrets_included": False,
},
)
return {
"task_id": task_id,
"step_id": step_id,
"state": "RUNNING",
"production_side_effects": False,
"secrets_included": False,
}
def heartbeat(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
step_id = str(payload["step_id"])
worker_id = str(payload["worker_id"])
session_id = str(payload["session_id"])
self.orchestrator.workflow.heartbeat(task_id, step_id, worker_id, session_id)
return {
"task_id": task_id,
"step_id": step_id,
"state": "RUNNING",
"production_side_effects": False,
"secrets_included": False,
}
def result(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
step_id = str(payload["step_id"])
result_digest = str(payload["result_digest"])
self.orchestrator.workflow.complete(task_id, step_id, result_digest=result_digest)
self._chain(task_id).append(
"STEP_RESULT",
task_id,
{
"step_id": step_id,
"result_digest": result_digest,
"secrets_included": False,
},
)
return {
"task_id": task_id,
"step_id": step_id,
"state": "COMPLETED",
"production_side_effects": False,
"secrets_included": False,
}
def evidence(self, task_id: str) -> dict[str, Any]:
chain = self._chain(task_id)
return {
"task_id": task_id,
"events": chain.export(),
"verified": chain.verify(),
"production_side_effects": False,
"secrets_included": False,
}
def create_release_plan(self, payload: Mapping[str, Any]) -> dict[str, Any]:
plan = release_plan_from_mapping(payload)
self.release = ReleaseController()
self.release.attach_plan(plan)
self.release.mark_built()
return {
"plan": release_plan_to_mapping(plan),
"release_state": self.release.status().state.value,
"production_side_effects": False,
"secrets_included": False,
}
def verify_release(self, payload: Mapping[str, Any]) -> dict[str, Any]:
self.release.verify(
artifact_ok=payload.get("artifact_ok") is True,
evidence_ok=payload.get("evidence_ok") is True,
)
self.release.request_owner_approval()
return {
"release_state": self.release.status().state.value,
"production_release": "BLOCKED_EXTERNAL_OWNER_SIGNATURE",
"production_side_effects": False,
"secrets_included": False,
}
def task_status(self, task_id: str) -> dict[str, Any]:
store = self.orchestrator.workflow.store
state, revision = store.get_task_state(task_id)
steps = store.list_steps(task_id)
return {
"task_id": task_id,
"state": state.value,
"revision": revision,
"steps": {
step_id: {
"state": item.state.value,
"attempt": item.attempt,
"max_attempts": item.max_attempts,
"worker_id": item.worker_id,
"session_id": item.session_id,
}
for step_id, item in steps.items()
},
"production_side_effects": False,
"secrets_included": False,
}
@@ -0,0 +1,116 @@
"""Owner approval verification contracts for candidate releases."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Protocol
from .errors import ApprovalRequired, StateConflict
from .models import ApprovalEnvelope, TaskPlan, canonical_json
from .store import StateStore
class SignatureVerifier(Protocol):
"""Verify an approval payload without exposing private-key operations."""
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
"""Return ``True`` when the signature is valid for ``key_id``."""
class RevocationSource(Protocol):
def is_revoked(self, approval_plan_id: str) -> bool:
"""Return whether an approval plan has been revoked."""
@dataclass(frozen=True, slots=True)
class ApprovalVerificationResult:
approval_plan_id: str
task_id: str
signing_key_id: str
verified_at: datetime
class ApprovalVerifier:
"""Verify exact approval bindings and consume nonces before execution."""
def __init__(
self,
store: StateStore,
signature_verifier: SignatureVerifier,
revocations: RevocationSource,
) -> None:
self.store = store
self.signature_verifier = signature_verifier
self.revocations = revocations
def verify(
self,
plan: TaskPlan,
envelope: ApprovalEnvelope,
*,
now: datetime | None = None,
) -> ApprovalVerificationResult:
now = now or datetime.now(timezone.utc)
if envelope.task_id != plan.task_id:
raise ApprovalRequired("APPROVAL_TASK_MISMATCH", "approval task does not match plan")
if envelope.task_plan_sha256 != plan.digest():
raise ApprovalRequired("APPROVAL_PLAN_DIGEST_MISMATCH", "approval digest does not match plan")
if envelope.binding != plan.binding:
raise ApprovalRequired("APPROVAL_RESOURCE_MISMATCH", "approval resource binding does not match plan")
approved = set(envelope.capabilities)
required = {cap for step in plan.steps for cap in step.required_capabilities}
if not required.issubset(approved):
raise ApprovalRequired(
"APPROVAL_CAPABILITY_INSUFFICIENT",
"approval does not cover all plan capabilities",
{"missing": sorted(required - approved)},
)
if now < envelope.not_before or now >= envelope.expires_at:
raise ApprovalRequired("APPROVAL_TIME_INVALID", "approval is outside its validity window")
if self.revocations.is_revoked(envelope.approval_plan_id):
raise ApprovalRequired("APPROVAL_REVOKED", "approval plan is revoked")
payload = canonical_json(envelope.payload()).encode("utf-8")
try:
signature_valid = self.signature_verifier.verify(
envelope.signing_key_id, payload, envelope.signature
)
except Exception as error:
raise ApprovalRequired(
"BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE",
"trusted Owner approval verifier is unavailable",
) from error
if not signature_valid:
raise ApprovalRequired("APPROVAL_SIGNATURE_INVALID", "approval signature verification failed")
try:
self.store.consume_nonce(f"approval:{envelope.approval_plan_id}", envelope.nonce)
except StateConflict as error:
raise ApprovalRequired("APPROVAL_NONCE_REPLAY", "approval nonce was already consumed") from error
return ApprovalVerificationResult(
approval_plan_id=envelope.approval_plan_id,
task_id=envelope.task_id,
signing_key_id=envelope.signing_key_id,
verified_at=now,
)
class StaticRevocationSource:
def __init__(self, revoked: set[str] | None = None) -> None:
self.revoked = set(revoked or ())
def is_revoked(self, approval_plan_id: str) -> bool:
return approval_plan_id in self.revoked
class RejectingSignatureVerifier:
"""Safe default used when no external verifier has been configured."""
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
return False
class UnavailableSignatureVerifier:
"""Safe runtime default when no Owner HSM/KMS verifier has been supplied."""
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
raise ConnectionError("Owner verifier unavailable")
@@ -0,0 +1,225 @@
"""Deterministic Candidate artifact and package-closure construction."""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import io
import json
import os
from pathlib import Path, PurePosixPath
import stat
import tarfile
from typing import Mapping
from .errors import EvidenceIncomplete, ReleaseBlocked
from .models import is_git_oid
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
@dataclass(frozen=True, slots=True)
class ArtifactFile:
path: str
content: bytes
mode: int = 0o644
def __post_init__(self) -> None:
parsed = PurePosixPath(self.path)
if (
parsed.is_absolute()
or ".." in parsed.parts
or not self.path
or "\\" in self.path
or self.path != self.path.strip()
):
raise ValueError("artifact paths must be canonical, relative and traversal-free")
if ".git" in parsed.parts:
raise ValueError(".git content is forbidden")
if self.mode not in (0o644, 0o755):
raise ValueError("artifact mode must be 0644 or 0755")
@dataclass(frozen=True, slots=True)
class ArtifactManifest:
source_oid: str
tree_oid: str
files: Mapping[str, str]
artifact_sha256: str
package_closure_sha256: str
format_version: int = 1
secrets_included: bool = False
def as_json_bytes(self) -> bytes:
payload = {
"format_version": self.format_version,
"source_oid": self.source_oid,
"tree_oid": self.tree_oid,
"files": dict(sorted(self.files.items())),
"artifact_sha256": self.artifact_sha256,
"package_closure_sha256": self.package_closure_sha256,
"secrets_included": self.secrets_included,
}
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
@property
def manifest_sha256(self) -> str:
return sha256_bytes(self.as_json_bytes())
@dataclass(frozen=True, slots=True)
class BuiltArtifact:
content: bytes
manifest: ArtifactManifest
class ArtifactBuilder:
MAX_FILE_BYTES = 8 * 1024 * 1024
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
FORBIDDEN_MARKERS = (
b"begin private " + b"key",
b"authorization:" + b" bearer ",
b"password" + b"=",
b"token" + b"=",
b"secret" + b"=",
b"api_key" + b"=",
)
@staticmethod
def _validate_oid(name: str, value: str) -> None:
if not is_git_oid(value):
raise ValueError(f"{name} must be a full lowercase Git OID")
def build_from_directory(
self, source_oid: str, tree_oid: str, source: str | Path
) -> BuiltArtifact:
root = Path(source)
if root.is_symlink() or not root.is_dir():
raise ReleaseBlocked(
"ARTIFACT_SOURCE_LINK_REJECTED", "artifact source must be a real directory"
)
files: list[ArtifactFile] = []
for directory, dirnames, filenames in os.walk(root, followlinks=False):
current = Path(directory)
for name in tuple(dirnames):
item = current / name
if item.is_symlink() or name == ".git":
raise ReleaseBlocked(
"ARTIFACT_SOURCE_LINK_REJECTED",
"artifact source contains a linked or repository directory",
)
for name in filenames:
item = current / name
info = item.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_LINK_REJECTED",
"artifact source contains a link or special file",
)
if info.st_size > self.MAX_FILE_BYTES:
raise ReleaseBlocked("ARTIFACT_FILE_TOO_LARGE", "artifact file exceeds limit")
relative = item.relative_to(root).as_posix()
mode = 0o755 if info.st_mode & 0o111 else 0o644
files.append(ArtifactFile(relative, item.read_bytes(), mode))
return self.build(source_oid, tree_oid, tuple(files))
def build(
self, source_oid: str, tree_oid: str, files: tuple[ArtifactFile, ...]
) -> BuiltArtifact:
self._validate_oid("source_oid", source_oid)
self._validate_oid("tree_oid", tree_oid)
if not files:
raise EvidenceIncomplete("ARTIFACT_EMPTY", "artifact requires at least one file")
paths = [item.path for item in files]
if len(paths) != len(set(paths)):
raise ReleaseBlocked("ARTIFACT_DUPLICATE_PATH", "artifact contains duplicate paths")
total = 0
for item in files:
total += len(item.content)
if len(item.content) > self.MAX_FILE_BYTES or total > self.MAX_ARTIFACT_BYTES:
raise ReleaseBlocked("ARTIFACT_SIZE_LIMIT", "artifact exceeds Candidate limit")
lowered = item.content.lower()
if any(marker in lowered for marker in self.FORBIDDEN_MARKERS):
raise ReleaseBlocked(
"ARTIFACT_SECRET_MARKER",
"artifact contains a forbidden secret marker",
{"path": item.path},
)
output = io.BytesIO()
with tarfile.open(fileobj=output, mode="w", format=tarfile.PAX_FORMAT) as archive:
for item in sorted(files, key=lambda value: value.path):
info = tarfile.TarInfo(item.path)
info.size = len(item.content)
info.mode = item.mode
info.mtime = 0
info.uid = 0
info.gid = 0
info.uname = "root"
info.gname = "root"
archive.addfile(info, io.BytesIO(item.content))
content = output.getvalue()
file_hashes = {item.path: sha256_bytes(item.content) for item in files}
closure_payload = json.dumps(
{
"source_oid": source_oid,
"tree_oid": tree_oid,
"files": dict(sorted(file_hashes.items())),
"secrets_included": False,
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
manifest = ArtifactManifest(
source_oid=source_oid,
tree_oid=tree_oid,
files=file_hashes,
artifact_sha256=sha256_bytes(content),
package_closure_sha256=sha256_bytes(closure_payload),
secrets_included=False,
)
return BuiltArtifact(content=content, manifest=manifest)
def verify(self, built: BuiltArtifact) -> None:
if built.manifest.secrets_included:
raise EvidenceIncomplete("ARTIFACT_SECRET_FLAG", "manifest includes secrets")
if sha256_bytes(built.content) != built.manifest.artifact_sha256:
raise EvidenceIncomplete(
"ARTIFACT_DIGEST_MISMATCH", "artifact digest does not match manifest"
)
actual: dict[str, str] = {}
with tarfile.open(fileobj=io.BytesIO(built.content), mode="r:") as archive:
for member in archive.getmembers():
try:
ArtifactFile(member.name, b"", member.mode)
except ValueError as error:
raise EvidenceIncomplete(
"ARTIFACT_MEMBER_INVALID", "archive contains an unsafe member"
) from error
if not member.isfile() or member.name in actual:
raise EvidenceIncomplete(
"ARTIFACT_MEMBER_INVALID", "archive contains a link, special or duplicate member"
)
stream = archive.extractfile(member)
if stream is None:
raise EvidenceIncomplete("ARTIFACT_MEMBER_INVALID", "archive member is unreadable")
actual[member.name] = sha256_bytes(stream.read())
if actual != dict(built.manifest.files):
raise EvidenceIncomplete(
"ARTIFACT_FILE_MISMATCH", "artifact file closure does not match manifest"
)
closure_payload = json.dumps(
{
"source_oid": built.manifest.source_oid,
"tree_oid": built.manifest.tree_oid,
"files": dict(sorted(actual.items())),
"secrets_included": False,
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
if sha256_bytes(closure_payload) != built.manifest.package_closure_sha256:
raise EvidenceIncomplete(
"PACKAGE_CLOSURE_MISMATCH", "package closure digest does not match"
)
@@ -0,0 +1,244 @@
"""Candidate inspection CLI; all commands are local and non-production."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
from .artifact import ArtifactBuilder, ArtifactManifest, BuiltArtifact
from .evidence import EvidenceChain
from .release import ReleaseController
from .serialization import (
release_plan_from_mapping,
release_plan_to_mapping,
task_plan_from_mapping,
task_plan_to_mapping,
)
from .service import CandidateApplication
from .store import StateStore
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="aios-autonomous-release")
sub = parser.add_subparsers(dest="command", required=True)
plan = sub.add_parser("plan")
plan.add_argument("payload")
digest = sub.add_parser("digest")
digest.add_argument("payload")
status = sub.add_parser("status")
status.add_argument("--state", required=True, type=Path)
status.add_argument("--task-id", required=True)
evidence = sub.add_parser("verify-evidence")
evidence.add_argument("--state", required=True, type=Path)
evidence.add_argument("--task-id", required=True)
evidence.add_argument("--require", action="append", default=[])
task = sub.add_parser("task")
task_sub = task.add_subparsers(dest="task_command", required=True)
task_create = task_sub.add_parser("create")
task_create.add_argument("--state", required=True, type=Path)
task_create.add_argument("--payload", required=True, type=Path)
for name in ("status", "steps"):
item = task_sub.add_parser(name)
item.add_argument("--state", required=True, type=Path)
item.add_argument("--task-id", required=True)
evidence_group = sub.add_parser("evidence")
evidence_sub = evidence_group.add_subparsers(dest="evidence_command", required=True)
evidence_verify = evidence_sub.add_parser("verify")
evidence_verify.add_argument("--state", required=True, type=Path)
evidence_verify.add_argument("--task-id", required=True)
evidence_verify.add_argument("--require", action="append", default=[])
artifact = sub.add_parser("artifact")
artifact_sub = artifact.add_subparsers(dest="artifact_command", required=True)
artifact_build = artifact_sub.add_parser("build")
artifact_build.add_argument("--source", required=True, type=Path)
artifact_build.add_argument("--source-oid", required=True)
artifact_build.add_argument("--tree-oid", required=True)
artifact_build.add_argument("--output-directory", required=True, type=Path)
artifact_verify = artifact_sub.add_parser("verify")
artifact_verify.add_argument("--artifact", required=True, type=Path)
artifact_verify.add_argument("--manifest", required=True, type=Path)
release = sub.add_parser("release")
release_sub = release.add_subparsers(dest="release_command", required=True)
for name in ("plan", "verify"):
item = release_sub.add_parser(name)
item.add_argument("--payload", required=True, type=Path)
acceptance = sub.add_parser("acceptance")
acceptance.add_argument("--repo-root", required=True, type=Path)
acceptance.add_argument("--output", required=True, type=Path)
return parser
def load_json(path: Path) -> dict:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError("JSON payload must be an object")
return value
def exclusive(path: Path, content: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
descriptor = os.open(
path,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
0o640,
)
try:
os.write(descriptor, content)
os.fsync(descriptor)
finally:
os.close(descriptor)
def artifact_from_files(artifact: Path, manifest: Path) -> BuiltArtifact:
payload = load_json(manifest)
contract = ArtifactManifest(
source_oid=str(payload["source_oid"]),
tree_oid=str(payload["tree_oid"]),
files={str(key): str(value) for key, value in payload["files"].items()},
artifact_sha256=str(payload["artifact_sha256"]),
package_closure_sha256=str(payload["package_closure_sha256"]),
format_version=int(payload.get("format_version", 1)),
secrets_included=bool(payload.get("secrets_included", False)),
)
return BuiltArtifact(artifact.read_bytes(), contract)
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.command in {"plan", "digest"}:
plan = task_plan_from_mapping(json.loads(args.payload))
if args.command == "plan":
print(json.dumps(task_plan_to_mapping(plan), sort_keys=True))
else:
print(plan.digest())
return 0
if args.command == "task":
application = CandidateApplication(args.state)
if args.task_command == "create":
result = application.api.create_task(load_json(args.payload))
elif args.task_command == "steps":
result = application.api.task_steps(args.task_id)
else:
result = application.api.task_status(args.task_id)
print(json.dumps(result, sort_keys=True))
return 0
if args.command == "evidence":
chain = EvidenceChain(StateStore(args.state), args.task_id)
result = {
"task_id": args.task_id,
"verified": chain.verify(),
"evidence_closure": chain.close(tuple(args.require)),
"production_side_effects": False,
"secrets_included": False,
}
print(json.dumps(result, sort_keys=True))
return 0
if args.command == "artifact":
builder = ArtifactBuilder()
if args.artifact_command == "build":
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
builder.verify(built)
artifact_path = args.output_directory / "candidate.tar"
manifest_path = args.output_directory / "manifest.json"
exclusive(artifact_path, built.content)
exclusive(manifest_path, built.manifest.as_json_bytes() + b"\n")
result = {
"artifact": str(artifact_path),
"manifest": str(manifest_path),
"artifact_sha256": built.manifest.artifact_sha256,
"manifest_sha256": built.manifest.manifest_sha256,
"package_closure_sha256": built.manifest.package_closure_sha256,
"production_side_effects": False,
"secrets_included": False,
}
else:
built = artifact_from_files(args.artifact, args.manifest)
builder.verify(built)
result = {
"verified": True,
"artifact_sha256": built.manifest.artifact_sha256,
"manifest_sha256": built.manifest.manifest_sha256,
"package_closure_sha256": built.manifest.package_closure_sha256,
"production_side_effects": False,
"secrets_included": False,
}
print(json.dumps(result, sort_keys=True))
return 0
if args.command == "release":
plan = release_plan_from_mapping(load_json(args.payload))
if args.release_command == "plan":
result = {
"plan": release_plan_to_mapping(plan),
"release_state": "CREATED",
"production_side_effects": False,
"secrets_included": False,
}
else:
controller = ReleaseController()
controller.attach_plan(plan)
controller.mark_built()
controller.verify(artifact_ok=True, evidence_ok=True)
controller.request_owner_approval()
result = {
"release_state": controller.status().state.value,
"production_release": "BLOCKED_EXTERNAL_OWNER_SIGNATURE",
"production_side_effects": False,
"secrets_included": False,
}
print(json.dumps(result, sort_keys=True))
return 0
if args.command == "acceptance":
runner = args.repo_root / "scripts" / "autonomous-release" / "run_acceptance.py"
completed = subprocess.run(
[
sys.executable,
str(runner),
"--repo-root",
str(args.repo_root),
"--output",
str(args.output),
],
check=False,
)
return completed.returncode
store = StateStore(args.state)
if args.command == "status":
state, revision = store.get_task_state(args.task_id)
print(
json.dumps(
{
"task_id": args.task_id,
"state": state.value,
"revision": revision,
"production_side_effects": False,
"secrets_included": False,
},
sort_keys=True,
)
)
return 0
chain = EvidenceChain(store, args.task_id)
print(
json.dumps(
{
"task_id": args.task_id,
"evidence_closure": chain.close(tuple(args.require)),
"secrets_included": False,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,307 @@
"""Task-bound, reference-only Candidate credential lifecycle."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import json
import re
import secrets
from typing import Protocol
from .errors import CredentialUnavailable, PolicyDenied
from .models import (
CredentialProjection,
CredentialRequest,
CredentialState,
canonical_json,
)
from .store import StateStore
REFERENCE_RE = re.compile(
r"(?:systemd-credential:[A-Za-z0-9_.@-]+|secret-ref://candidate/[A-Za-z0-9_./@-]+)"
)
class CredentialProvider(Protocol):
"""External provider contract; values crossing this boundary are opaque references."""
def issue(self, request: CredentialRequest, expires_at: datetime) -> tuple[str, str]: ...
def revoke(self, credential_id: str, reference: str) -> None: ...
def is_usable(self, credential_id: str, reference: str) -> bool: ...
@dataclass(frozen=True, slots=True)
class CredentialLifecycleResult:
credential_id: str
reference: str
state: CredentialState
expires_at: datetime
class CredentialBroker:
"""Enforce approval, branch, consumer and operation bindings for projections."""
def __init__(self, store: StateStore, provider: CredentialProvider) -> None:
self.store = store
self.provider = provider
@staticmethod
def _validate_request(request: CredentialRequest, now: datetime) -> None:
if request.binding.branch == "main":
raise PolicyDenied("MAIN_CREDENTIAL_DENIED", "credentials cannot target main")
forbidden = {
"production.deploy",
"production.rollback",
"tag.write",
"baseline.write",
"registry.write",
"secret.export",
"secret.read",
}
overlap = forbidden.intersection(request.operations)
if overlap:
raise PolicyDenied(
"CREDENTIAL_OPERATION_FORBIDDEN",
"credential request includes forbidden operations",
{"operations": sorted(overlap)},
)
if now < request.not_before or now >= request.expires_at:
raise CredentialUnavailable(
"CREDENTIAL_APPROVAL_WINDOW_INVALID", "credential approval is not currently valid"
)
@staticmethod
def _metadata(request: CredentialRequest) -> dict[str, object]:
return {
"request_id": request.request_id,
"task_id": request.task_id,
"session_id": request.session_id,
"worker_id": request.worker_id,
"audience": request.audience,
"consumer_service": request.consumer_service,
"provider": request.provider,
"runtime_user": request.runtime_user,
"approval_plan_id": request.approval_plan_id,
"repository": request.binding.repository,
"branch": request.binding.branch,
"base_oid": request.binding.base_oid,
"allowed_paths": list(request.binding.allowed_paths),
"operations": list(request.operations),
"not_before": request.not_before.astimezone(timezone.utc).isoformat(),
"expires_at": request.expires_at.astimezone(timezone.utc).isoformat(),
"nonce": request.nonce,
"max_uses": request.max_uses,
"use_count": 0,
"secrets_included": False,
}
def _compensate(self, credential_id: str, reference: str) -> None:
try:
self.provider.revoke(credential_id, reference)
except Exception:
pass
def issue(
self, request: CredentialRequest, now: datetime | None = None
) -> CredentialProjection:
now = now or datetime.now(timezone.utc)
self._validate_request(request, now)
expires_at = min(request.expires_at, now + timedelta(seconds=request.ttl_seconds))
try:
credential_id, reference = self.provider.issue(request, expires_at)
except Exception as error:
raise CredentialUnavailable(
"CREDENTIAL_PROVIDER_UNAVAILABLE", "credential provider issue failed"
) from error
if not credential_id or not reference or not REFERENCE_RE.fullmatch(reference):
if credential_id and reference:
self._compensate(credential_id, reference)
raise CredentialUnavailable(
"CREDENTIAL_REFERENCE_REJECTED", "provider did not return an approved opaque reference"
)
projection = CredentialProjection(
credential_id=credential_id,
reference=reference,
request=request,
state=CredentialState.ISSUED,
issued_at=now,
expires_at=expires_at,
plaintext_returned=False,
)
try:
with self.store.transaction() as connection:
connection.execute(
"INSERT INTO credentials(credential_id, task_id, reference, state, metadata_json, "
"issued_at, expires_at) VALUES(?, ?, ?, ?, ?, ?, ?)",
(
credential_id,
request.task_id,
reference,
CredentialState.ISSUED.value,
canonical_json(self._metadata(request)),
now.isoformat(),
expires_at.isoformat(),
),
)
connection.execute(
"INSERT INTO consumed_nonces(scope, nonce, consumed_at) VALUES(?, ?, ?)",
(f"credential:{request.approval_plan_id}", request.nonce, now.isoformat()),
)
except Exception as error:
self._compensate(credential_id, reference)
raise CredentialUnavailable(
"CREDENTIAL_PERSISTENCE_FAILED",
"credential issuance was compensated after durable-state failure",
) from error
return projection
def project(self, projection: CredentialProjection) -> CredentialLifecycleResult:
with self.store.transaction() as connection:
cursor = connection.execute(
"UPDATE credentials SET state=? WHERE credential_id=? AND reference=? AND state=?",
(
CredentialState.PROJECTED.value,
projection.credential_id,
projection.reference,
CredentialState.ISSUED.value,
),
)
if cursor.rowcount != 1:
raise CredentialUnavailable(
"CREDENTIAL_STATE_CONFLICT", "only a durably issued credential may be projected"
)
return CredentialLifecycleResult(
projection.credential_id,
projection.reference,
CredentialState.PROJECTED,
projection.expires_at,
)
def authorize(
self,
projection: CredentialProjection,
*,
task_id: str,
session_id: str,
worker_id: str,
audience: str,
consumer_service: str,
operation: str,
path: str,
now: datetime | None = None,
) -> None:
now = now or datetime.now(timezone.utc)
request = projection.request
actual = (task_id, session_id, worker_id, audience, consumer_service)
expected = (
request.task_id,
request.session_id,
request.worker_id,
request.audience,
request.consumer_service,
)
if actual != expected:
raise CredentialUnavailable(
"CREDENTIAL_SCOPE_MISMATCH", "credential scope does not match the caller"
)
if operation not in request.operations or not request.binding.allows_path(path):
raise CredentialUnavailable(
"CREDENTIAL_OPERATION_DENIED", "credential operation or path is not approved"
)
if now < request.not_before or now >= projection.expires_at:
raise CredentialUnavailable("CREDENTIAL_EXPIRED", "credential is outside its valid window")
with self.store.transaction() as connection:
row = connection.execute(
"SELECT state, metadata_json FROM credentials WHERE credential_id=? AND reference=?",
(projection.credential_id, projection.reference),
).fetchone()
if row is None or row["state"] != CredentialState.PROJECTED.value:
raise CredentialUnavailable(
"CREDENTIAL_NOT_PROJECTED", "credential is not in projected state"
)
metadata = json.loads(row["metadata_json"])
count = int(metadata.get("use_count", 0))
if count >= request.max_uses:
raise CredentialUnavailable("CREDENTIAL_USE_LIMIT", "credential use limit reached")
if not self.provider.is_usable(projection.credential_id, projection.reference):
raise CredentialUnavailable(
"CREDENTIAL_PROVIDER_REJECTED", "provider no longer accepts the credential"
)
metadata["use_count"] = count + 1
connection.execute(
"UPDATE credentials SET metadata_json=? WHERE credential_id=?",
(canonical_json(metadata), projection.credential_id),
)
def revoke(
self, credential_id: str, reference: str, now: datetime | None = None
) -> CredentialLifecycleResult:
now = now or datetime.now(timezone.utc)
with self.store.transaction() as connection:
row = connection.execute(
"SELECT state, expires_at FROM credentials WHERE credential_id=? AND reference=?",
(credential_id, reference),
).fetchone()
if row is None:
raise KeyError(credential_id)
expires_at = datetime.fromisoformat(row["expires_at"])
if row["state"] == CredentialState.REVOKED.value:
return CredentialLifecycleResult(
credential_id, reference, CredentialState.REVOKED, expires_at
)
try:
self.provider.revoke(credential_id, reference)
except Exception as error:
raise CredentialUnavailable(
"CREDENTIAL_REVOCATION_FAILED", "upstream credential revocation failed"
) from error
with self.store.transaction() as connection:
connection.execute(
"UPDATE credentials SET state=?, revoked_at=? WHERE credential_id=? AND reference=?",
(CredentialState.REVOKED.value, now.isoformat(), credential_id, reference),
)
return CredentialLifecycleResult(
credential_id, reference, CredentialState.REVOKED, expires_at
)
def verify_revoked(self, credential_id: str, reference: str) -> None:
with self.store.transaction() as connection:
row = connection.execute(
"SELECT state FROM credentials WHERE credential_id=? AND reference=?",
(credential_id, reference),
).fetchone()
if row is None or row["state"] != CredentialState.REVOKED.value:
raise CredentialUnavailable(
"CREDENTIAL_REVOCATION_UNRECORDED", "durable revocation record is missing"
)
if self.provider.is_usable(credential_id, reference):
raise CredentialUnavailable(
"CREDENTIAL_REVOCATION_UNVERIFIED",
"provider still accepts revoked credential",
{"credential_id": credential_id},
)
class InMemoryCredentialProvider:
"""Candidate test provider; never suitable as production identity evidence."""
def __init__(self) -> None:
self._active: dict[str, str] = {}
def issue(self, request: CredentialRequest, expires_at: datetime) -> tuple[str, str]:
credential_id = f"cred-{secrets.token_hex(8)}"
reference = f"secret-ref://candidate/{request.task_id}/{credential_id}"
self._active[credential_id] = reference
return credential_id, reference
def revoke(self, credential_id: str, reference: str) -> None:
if self._active.get(credential_id) != reference:
raise CredentialUnavailable("CREDENTIAL_NOT_FOUND", "credential is not active")
del self._active[credential_id]
def is_usable(self, credential_id: str, reference: str) -> bool:
return self._active.get(credential_id) == reference
@@ -0,0 +1,48 @@
"""Stable error types and codes for the autonomous-release candidate."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
@dataclass(slots=True)
class AutonomousReleaseError(RuntimeError):
"""Base error with a stable machine-readable code."""
code: str
message: str
details: Mapping[str, Any] | None = None
def __str__(self) -> str:
if not self.details:
return f"{self.code}: {self.message}"
return f"{self.code}: {self.message} ({dict(self.details)})"
class PolicyDenied(AutonomousReleaseError):
pass
class ApprovalRequired(AutonomousReleaseError):
pass
class CapabilityMismatch(AutonomousReleaseError):
pass
class CredentialUnavailable(AutonomousReleaseError):
pass
class EvidenceIncomplete(AutonomousReleaseError):
pass
class ReleaseBlocked(AutonomousReleaseError):
pass
class StateConflict(AutonomousReleaseError):
pass
@@ -0,0 +1,136 @@
"""Durable, replayable and tamper-evident evidence closure."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from typing import Any, Mapping, TYPE_CHECKING
from .errors import EvidenceIncomplete
from .models import sha256_json
if TYPE_CHECKING:
from .store import StateStore
@dataclass(frozen=True, slots=True)
class EvidenceEvent:
event_type: str
task_id: str
payload: Mapping[str, Any]
previous_digest: str | None
created_at: datetime
def event_digest(self) -> str:
return sha256_json(
{
"event_type": self.event_type,
"task_id": self.task_id,
"payload": dict(self.payload),
"previous_digest": self.previous_digest,
"created_at": self.created_at.astimezone(timezone.utc).isoformat(),
}
)
class EvidenceChain:
"""Append-only chain optionally backed by the Candidate state database."""
def __init__(self, store: StateStore | None = None, task_id: str | None = None) -> None:
if (store is None) != (task_id is None):
raise ValueError("store and task_id must be supplied together")
self.store = store
self.task_id = task_id
self._records: list[tuple[EvidenceEvent, str]] = []
if store is not None and task_id is not None:
self._load()
def _load(self) -> None:
assert self.store is not None and self.task_id is not None
self._records = []
for row in self.store.list_events(self.task_id):
event = EvidenceEvent(
event_type=str(row["event_type"]),
task_id=str(row["task_id"]),
payload=json.loads(str(row["payload_json"])),
previous_digest=row["previous_digest"],
created_at=datetime.fromisoformat(str(row["created_at"])),
)
self._records.append((event, str(row["digest"])))
def append(
self, event_type: str, task_id: str, payload: Mapping[str, Any]
) -> EvidenceEvent:
if not event_type or not task_id:
raise EvidenceIncomplete("EVIDENCE_IDENTITY_REQUIRED", "event and task IDs are required")
if self.task_id is not None and task_id != self.task_id:
raise EvidenceIncomplete(
"EVIDENCE_TASK_MISMATCH", "event task does not match the durable chain"
)
if payload.get("secrets_included") is not False:
raise EvidenceIncomplete(
"EVIDENCE_SECRET_MARKER_REQUIRED",
"evidence must explicitly assert secrets_included=false",
)
previous = self._records[-1][1] if self._records else None
event = EvidenceEvent(
event_type=event_type,
task_id=task_id,
payload=dict(payload),
previous_digest=previous,
created_at=datetime.now(timezone.utc),
)
event_digest = event.event_digest()
if self.store is not None:
self.store.append_event(
task_id,
event_type,
payload,
previous,
event_digest,
event.created_at,
)
self._records.append((event, event_digest))
return event
def verify(self) -> bool:
previous: str | None = None
for event, recorded_digest in self._records:
if event.previous_digest != previous or event.event_digest() != recorded_digest:
return False
previous = recorded_digest
return True
def close(self, required_event_types: tuple[str, ...]) -> str:
if not self._records:
raise EvidenceIncomplete("EMPTY_EVIDENCE_CHAIN", "cannot close an empty evidence chain")
if not self.verify():
raise EvidenceIncomplete(
"EVIDENCE_CHAIN_INVALID", "evidence chain verification failed"
)
found = {event.event_type for event, _ in self._records}
missing = sorted(set(required_event_types) - found)
if missing:
raise EvidenceIncomplete(
"EVIDENCE_REQUIREMENT_MISSING",
"required evidence events are absent",
{"missing": missing},
)
return self._records[-1][1]
def closure_digest(self) -> str:
return self.close(())
def export(self) -> list[dict[str, Any]]:
return [
{
"event_type": event.event_type,
"task_id": event.task_id,
"payload": dict(event.payload),
"previous_digest": event.previous_digest,
"digest": recorded_digest,
"created_at": event.created_at.isoformat(),
}
for event, recorded_digest in self._records
]
@@ -0,0 +1,105 @@
"""Native Candidate execution lifecycle with mandatory cleanup evidence."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
from .evidence import EvidenceChain
from .native_adapters import CandidateNativeAdapter, NativeSession
@dataclass(frozen=True, slots=True)
class NativeCanaryResult:
provider: str
broker_session_id: str
provider_session_id: str
message_id: str
receipt: str
broker_session_verified: bool
openclaw_native_child_session_verified: bool
codex_child_session_verified: bool
simulation: bool
session_closed: bool
secrets_included: bool = False
class ExecutionManager:
def __init__(self, evidence: EvidenceChain) -> None:
self.evidence = evidence
def run_native_canary(
self,
adapter: CandidateNativeAdapter,
*,
task_id: str,
parent_session_id: str,
recipient_binding: Mapping[str, Any],
text: str,
) -> NativeCanaryResult:
session: NativeSession | None = None
closed = False
try:
session = adapter.spawn(task_id, parent_session_id, recipient_binding)
verification = adapter.verify(session)
message = adapter.send_text(session, text)
adapter.interrupt(session)
adapter.close(session)
closed = True
event_type = (
"NATIVE_SESSION_SIMULATION"
if verification.simulation
else "NATIVE_SESSION_VERIFIED"
)
self.evidence.append(
event_type,
task_id,
{
"provider": session.provider,
"broker_session_id": session.broker_session_id,
"provider_session_id": session.provider_session_id,
"message_id": message.message_id,
"receipt": message.receipt,
"broker_session_verified": verification.broker_session_verified,
"openclaw_native_child_session_verified": (
verification.openclaw_native_child_session_verified
),
"codex_child_session_verified": verification.codex_child_session_verified,
"simulation": verification.simulation,
"session_closed": True,
"secrets_included": False,
},
)
return NativeCanaryResult(
provider=session.provider,
broker_session_id=session.broker_session_id,
provider_session_id=session.provider_session_id,
message_id=message.message_id,
receipt=message.receipt,
broker_session_verified=verification.broker_session_verified,
openclaw_native_child_session_verified=(
verification.openclaw_native_child_session_verified
),
codex_child_session_verified=verification.codex_child_session_verified,
simulation=verification.simulation,
session_closed=True,
secrets_included=False,
)
except Exception as error:
if session is not None and not closed:
try:
adapter.close(session)
closed = True
except Exception:
closed = False
self.evidence.append(
"NATIVE_SESSION_FAILED",
task_id,
{
"provider": adapter.provider,
"error_code": type(error).__name__,
"session_closed": closed,
"secrets_included": False,
},
)
raise
@@ -0,0 +1,172 @@
"""API-only Gitea deploy-key lifecycle canary."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Protocol, Sequence
from .errors import CredentialUnavailable, StateConflict
GITEA_REFERENCE = "systemd-credential:gitea-admin-api-candidate"
@dataclass(frozen=True, slots=True)
class EphemeralKeyProjection:
public_key: str
private_key_reference: str
class KeyProjectionProvider(Protocol):
def create(self, task_id: str, branch: str) -> EphemeralKeyProjection: ...
def delete(self, reference: str) -> None: ...
def is_present(self, reference: str) -> bool: ...
class GiteaDeployKeyProvider(Protocol):
simulation: bool
def list_deploy_keys(
self, repository: str, credential_reference: str
) -> Sequence[Mapping[str, Any]]: ...
def create_deploy_key(
self,
repository: str,
title: str,
public_key: str,
credential_reference: str,
) -> Mapping[str, Any]: ...
def delete_deploy_key(
self, repository: str, key_id: int, credential_reference: str
) -> None: ...
class SSHAuthenticationProbe(Protocol):
def authenticate(
self, repository: str, branch: str, private_key_reference: str
) -> bool: ...
@dataclass(frozen=True, slots=True)
class GiteaCanaryResult:
key_created: bool
key_deleted: bool
api_key_absent_after_delete: bool
post_revoke_auth_rejected: bool
credential_projection_deleted: bool
gitea_admin_api_lifecycle_canary: bool
simulation: bool
secrets_included: bool = False
class GiteaDeployKeyCanary:
"""Exercise only repository deploy-key list/create/delete provider operations."""
repository = "aios/aios"
def __init__(
self,
gitea: GiteaDeployKeyProvider,
projections: KeyProjectionProvider,
ssh_probe: SSHAuthenticationProbe,
) -> None:
self.gitea = gitea
self.projections = projections
self.ssh_probe = ssh_probe
def run(
self, *, task_id: str, branch: str, credential_reference: str
) -> GiteaCanaryResult:
if credential_reference != GITEA_REFERENCE:
raise CredentialUnavailable(
"BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED",
"exact Candidate Gitea Admin API reference is required",
)
if not task_id or not branch.startswith("patch/"):
raise StateConflict("GITEA_CANARY_BINDING_INVALID", "task and patch branch are required")
projection: EphemeralKeyProjection | None = None
key_id: int | None = None
key_created = False
key_deleted = False
api_absent = False
auth_rejected = False
projection_deleted = False
title = f"aios-candidate-{task_id}"
try:
projection = self.projections.create(task_id, branch)
if (
not projection.public_key.startswith("ssh-ed25519 ")
or not projection.private_key_reference.startswith("systemd-credential:")
):
raise CredentialUnavailable(
"GITEA_CANARY_KEY_PROJECTION_INVALID",
"ephemeral key projection is not reference-only",
)
before = self.gitea.list_deploy_keys(self.repository, credential_reference)
if any(item.get("title") == title for item in before):
raise StateConflict("GITEA_CANARY_KEY_COLLISION", "canary key title already exists")
created = self.gitea.create_deploy_key(
self.repository,
title,
projection.public_key,
credential_reference,
)
raw_id = created.get("id")
if not isinstance(raw_id, int) or created.get("key") != projection.public_key:
raise StateConflict("GITEA_CANARY_CREATE_INVALID", "created key response is invalid")
key_id = raw_id
key_created = True
visible = self.gitea.list_deploy_keys(self.repository, credential_reference)
if not any(item.get("id") == key_id for item in visible):
raise StateConflict("GITEA_CANARY_KEY_NOT_VISIBLE", "created key is not listed")
if not self.ssh_probe.authenticate(
self.repository, branch, projection.private_key_reference
):
raise StateConflict("GITEA_CANARY_SSH_AUTH_FAILED", "deploy key could not authenticate")
self.gitea.delete_deploy_key(self.repository, key_id, credential_reference)
key_deleted = True
key_id = None
after = self.gitea.list_deploy_keys(self.repository, credential_reference)
api_absent = not any(item.get("title") == title for item in after)
if not api_absent:
raise StateConflict("GITEA_CANARY_DELETE_UNVERIFIED", "deleted key remains visible")
self.projections.delete(projection.private_key_reference)
projection_deleted = not self.projections.is_present(projection.private_key_reference)
auth_rejected = not self.ssh_probe.authenticate(
self.repository, branch, projection.private_key_reference
)
if not projection_deleted or not auth_rejected:
raise StateConflict(
"GITEA_CANARY_REVOCATION_UNVERIFIED",
"projection deletion or post-revocation SSH rejection failed",
)
simulated = bool(getattr(self.gitea, "simulation", True))
return GiteaCanaryResult(
key_created=key_created,
key_deleted=key_deleted,
api_key_absent_after_delete=api_absent,
post_revoke_auth_rejected=auth_rejected,
credential_projection_deleted=projection_deleted,
gitea_admin_api_lifecycle_canary=not simulated,
simulation=simulated,
secrets_included=False,
)
finally:
if key_id is not None:
try:
self.gitea.delete_deploy_key(
self.repository, key_id, credential_reference
)
except Exception:
pass
if projection is not None and self.projections.is_present(
projection.private_key_reference
):
try:
self.projections.delete(projection.private_key_reference)
except Exception:
pass
@@ -0,0 +1,376 @@
"""Typed contracts for the dormant autonomous-release candidate.
This package is dependency-free by design so it can be audited and exercised in
an isolated candidate worktree before any production service is installed.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
import hashlib
import json
from typing import Any, Mapping, Sequence
def utc_now() -> datetime:
"""Return an aware UTC timestamp."""
return datetime.now(timezone.utc)
def canonical_json(value: Any) -> str:
"""Serialize *value* deterministically for signatures and evidence hashes."""
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
def is_sha256(value: str) -> bool:
return len(value) == 64 and all(ch in "0123456789abcdef" for ch in value)
def is_git_oid(value: str) -> bool:
return len(value) == 40 and all(ch in "0123456789abcdef" for ch in value)
class RiskClass(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class TaskState(str, Enum):
CREATED = "CREATED"
PLANNED = "PLANNED"
READY = "READY"
RUNNING = "RUNNING"
BLOCKED = "BLOCKED"
FAILED = "FAILED"
COMPLETED = "COMPLETED"
READY_FOR_OWNER_APPROVAL = "READY_FOR_OWNER_APPROVAL"
class StepState(str, Enum):
PENDING = "PENDING"
READY = "READY"
CLAIMED = "CLAIMED"
RUNNING = "RUNNING"
BLOCKED = "BLOCKED"
FAILED = "FAILED"
COMPLETED = "COMPLETED"
class CredentialState(str, Enum):
REQUESTED = "REQUESTED"
ISSUED = "ISSUED"
PROJECTED = "PROJECTED"
REVOKED = "REVOKED"
EXPIRED = "EXPIRED"
@dataclass(frozen=True, slots=True)
class ResourceBinding:
repository: str
branch: str
base_oid: str
allowed_paths: tuple[str, ...] = ()
def __post_init__(self) -> None:
if not self.repository or "/" not in self.repository:
raise ValueError("repository must be owner/name")
if self.branch == "main" or not self.branch.startswith(("patch/", "integration/", "candidate/")):
raise ValueError("candidate execution requires a governed non-main branch")
if not is_git_oid(self.base_oid):
raise ValueError("base_oid must be a full lowercase 40-character Git OID")
for path in self.allowed_paths:
if not path or path.startswith("/") or ".." in path.split("/"):
raise ValueError("allowed_paths must be repository-relative and traversal-free")
def allows_path(self, path: str) -> bool:
if not path or path.startswith("/") or ".." in path.split("/"):
return False
return any(path == prefix or path.startswith(prefix.rstrip("/") + "/") for prefix in self.allowed_paths)
@dataclass(frozen=True, slots=True)
class ApprovalEnvelope:
approval_plan_id: str
task_id: str
task_plan_sha256: str
binding: ResourceBinding
capabilities: tuple[str, ...]
risk: RiskClass
not_before: datetime
expires_at: datetime
nonce: str
signing_key_id: str
signature: str
def __post_init__(self) -> None:
if self.expires_at <= self.not_before:
raise ValueError("approval expiry must be after not_before")
if not is_sha256(self.task_plan_sha256):
raise ValueError("task_plan_sha256 must be a SHA-256 hex digest")
if not self.approval_plan_id or not self.task_id or not self.nonce:
raise ValueError("approval identifiers and nonce are required")
def payload(self) -> Mapping[str, Any]:
return {
"approval_plan_id": self.approval_plan_id,
"task_id": self.task_id,
"task_plan_sha256": self.task_plan_sha256,
"repository": self.binding.repository,
"branch": self.binding.branch,
"base_oid": self.binding.base_oid,
"allowed_paths": list(self.binding.allowed_paths),
"capabilities": list(self.capabilities),
"risk": self.risk.value,
"not_before": self.not_before.astimezone(timezone.utc).isoformat(),
"expires_at": self.expires_at.astimezone(timezone.utc).isoformat(),
"nonce": self.nonce,
"signing_key_id": self.signing_key_id,
}
@dataclass(frozen=True, slots=True)
class WorkflowStep:
step_id: str
worker_role: str
required_capabilities: tuple[str, ...]
depends_on: tuple[str, ...] = ()
max_attempts: int = 1
timeout_seconds: int = 900
allowed_paths: tuple[str, ...] = ()
def __post_init__(self) -> None:
if not self.step_id or not self.worker_role:
raise ValueError("step_id and worker_role are required")
if self.max_attempts < 1 or self.max_attempts > 5:
raise ValueError("max_attempts must be between 1 and 5")
if self.timeout_seconds < 1 or self.timeout_seconds > 86_400:
raise ValueError("timeout_seconds must be between 1 and 86400")
if self.step_id in self.depends_on:
raise ValueError("a step cannot depend on itself")
@dataclass(frozen=True, slots=True)
class TaskPlan:
task_id: str
intent: str
binding: ResourceBinding
risk: RiskClass
steps: tuple[WorkflowStep, ...]
evidence_requirements: tuple[str, ...]
production_side_effects_allowed: bool = False
created_at: datetime = field(default_factory=utc_now)
def __post_init__(self) -> None:
if not self.task_id or not self.intent:
raise ValueError("task_id and intent are required")
if self.production_side_effects_allowed:
raise ValueError("candidate TaskPlan must not allow production side effects")
ids = [step.step_id for step in self.steps]
if len(ids) != len(set(ids)):
raise ValueError("step IDs must be unique")
known = set(ids)
for step in self.steps:
missing = set(step.depends_on) - known
if missing:
raise ValueError(f"step {step.step_id} has unknown dependencies: {sorted(missing)}")
self._validate_acyclic()
def _validate_acyclic(self) -> None:
dependencies = {step.step_id: set(step.depends_on) for step in self.steps}
pending = set(dependencies)
while pending:
ready = {step_id for step_id in pending if not (dependencies[step_id] & pending)}
if not ready:
raise ValueError("workflow contains a dependency cycle")
pending -= ready
def digest(self) -> str:
return sha256_json(
{
"task_id": self.task_id,
"intent": self.intent,
"repository": self.binding.repository,
"branch": self.binding.branch,
"base_oid": self.binding.base_oid,
"allowed_paths": list(self.binding.allowed_paths),
"risk": self.risk.value,
"steps": [
{
"step_id": step.step_id,
"worker_role": step.worker_role,
"required_capabilities": list(step.required_capabilities),
"depends_on": list(step.depends_on),
"max_attempts": step.max_attempts,
"timeout_seconds": step.timeout_seconds,
"allowed_paths": list(step.allowed_paths),
}
for step in self.steps
],
"evidence_requirements": list(self.evidence_requirements),
"production_side_effects_allowed": self.production_side_effects_allowed,
"created_at": self.created_at.astimezone(timezone.utc).isoformat(),
}
)
@dataclass(frozen=True, slots=True)
class WorkerDescriptor:
worker_id: str
role: str
capabilities: frozenset[str]
trust_zone: str
endpoint_id: str
online: bool = True
current_load: int = 0
max_concurrency: int = 1
def can_accept(self, required: Sequence[str], trust_zone: str) -> bool:
return (
self.online
and self.trust_zone == trust_zone
and self.current_load < self.max_concurrency
and set(required).issubset(self.capabilities)
)
@dataclass(frozen=True, slots=True)
class CredentialRequest:
request_id: str
task_id: str
session_id: str
worker_id: str
audience: str
consumer_service: str
binding: ResourceBinding
operations: tuple[str, ...]
provider: str
runtime_user: str
approval_plan_id: str
not_before: datetime
expires_at: datetime
nonce: str
ttl_seconds: int
max_uses: int
def __post_init__(self) -> None:
if self.ttl_seconds < 1 or self.ttl_seconds > 3600:
raise ValueError("credential TTL must be between 1 and 3600 seconds")
if self.max_uses < 1 or self.max_uses > 1000:
raise ValueError("credential max_uses must be between 1 and 1000")
if not self.operations:
raise ValueError("credential operations cannot be empty")
required = (
self.request_id,
self.task_id,
self.session_id,
self.worker_id,
self.audience,
self.consumer_service,
self.provider,
self.runtime_user,
self.approval_plan_id,
self.nonce,
)
if any(not item or item != item.strip() for item in required):
raise ValueError("credential identity, scope and approval fields are required")
if len(set(self.operations)) != len(self.operations):
raise ValueError("credential operations must be unique")
if self.not_before.tzinfo is None or self.expires_at.tzinfo is None:
raise ValueError("credential time bounds must be timezone aware")
if self.expires_at <= self.not_before:
raise ValueError("credential expires_at must follow not_before")
if (self.expires_at - self.not_before).total_seconds() > self.ttl_seconds:
raise ValueError("credential approval window exceeds requested TTL")
@dataclass(frozen=True, slots=True)
class CredentialProjection:
credential_id: str
reference: str
request: CredentialRequest
state: CredentialState
issued_at: datetime
expires_at: datetime
plaintext_returned: bool = False
def __post_init__(self) -> None:
if self.plaintext_returned:
raise ValueError("credential plaintext must never be returned")
if self.expires_at <= self.issued_at:
raise ValueError("credential expiry must be after issuance")
@dataclass(frozen=True, slots=True)
class ResultEnvelope:
task_id: str
step_id: str
worker_id: str
session_id: str
claim_id: str
operation_digest: str
result_digest: str
evidence_digests: tuple[str, ...]
completed_at: datetime = field(default_factory=utc_now)
def __post_init__(self) -> None:
if not is_sha256(self.operation_digest) or not is_sha256(self.result_digest):
raise ValueError("operation_digest and result_digest must be SHA-256 values")
if any(not is_sha256(digest) for digest in self.evidence_digests):
raise ValueError("all evidence digests must be SHA-256 values")
@dataclass(frozen=True, slots=True)
class ReleasePlan:
task_id: str
repository: str
branch: str
version: str
target_oid: str
tree_oid: str
artifact_sha256: str
manifest_sha256: str
package_closure_sha256: str
evidence_closure_oid: str
approval_plan_id: str
rollback_approval_plan_id: str
rollback_artifact_sha256: str
expected_tag_oid: str
expected_target_oid: str
production_config_sha256: str
def __post_init__(self) -> None:
for name in ("target_oid", "tree_oid", "evidence_closure_oid", "expected_target_oid"):
if not is_git_oid(getattr(self, name)):
raise ValueError(f"{name} must be a full lowercase 40-character Git OID")
for name in (
"artifact_sha256",
"manifest_sha256",
"package_closure_sha256",
"rollback_artifact_sha256",
"production_config_sha256",
):
if not is_sha256(getattr(self, name)):
raise ValueError(f"{name} must be a SHA-256 hex digest")
if not is_git_oid(self.expected_tag_oid):
raise ValueError("expected_tag_oid must be a full lowercase 40-character Git OID")
if (
not self.task_id
or "/" not in self.repository
or not self.branch.startswith("integration/")
or not self.version
or not self.approval_plan_id
or not self.rollback_approval_plan_id
):
raise ValueError("release identifiers are required")
if self.approval_plan_id == self.rollback_approval_plan_id:
raise ValueError("deployment and rollback approvals must be distinct")
if self.target_oid != self.expected_target_oid:
raise ValueError("target_oid must equal expected_target_oid")
@@ -0,0 +1,246 @@
"""Candidate-only OpenClaw and Codex native session adapter contracts."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Protocol, Sequence
import uuid
from .errors import StateConflict
class NativeProvider(Protocol):
simulation: bool
def spawn(
self, request: Mapping[str, Any], credential_reference: str
) -> Mapping[str, Any]: ...
def status(
self, provider_session_id: str, credential_reference: str
) -> Mapping[str, Any]: ...
def list_tree(
self, parent_session_id: str, credential_reference: str
) -> Sequence[Mapping[str, Any]]: ...
def send_text(
self,
provider_session_id: str,
payload: Mapping[str, Any],
credential_reference: str,
) -> Mapping[str, Any]: ...
def interrupt(self, provider_session_id: str, credential_reference: str) -> None: ...
def close(self, provider_session_id: str, credential_reference: str) -> None: ...
@dataclass(frozen=True, slots=True)
class NativeSession:
provider: str
broker_session_id: str
provider_session_id: str
task_id: str
parent_session_id: str
recipient_binding: Mapping[str, str]
@dataclass(frozen=True, slots=True)
class SessionVerification:
broker_session_verified: bool
openclaw_native_child_session_verified: bool
codex_child_session_verified: bool
simulation: bool
@dataclass(frozen=True, slots=True)
class TextResult:
content_type: str
encoding: str
image_only: bool
task_id: str
message_id: str
receipt: str
recipient_binding: Mapping[str, str]
secrets_included: bool
class CandidateNativeAdapter:
provider = ""
required_reference = ""
def __init__(self, client: NativeProvider, credential_reference: str) -> None:
if credential_reference != self.required_reference:
raise StateConflict(
"CANDIDATE_CREDENTIAL_REFERENCE_MISMATCH",
"adapter requires its exact Candidate credential reference",
)
self.client = client
self.credential_reference = credential_reference
@staticmethod
def _recipient(value: Mapping[str, Any]) -> dict[str, str]:
recipient_id = value.get("recipient_id")
if (
not isinstance(recipient_id, str)
or not recipient_id
or isinstance(value.get("recipients"), (list, tuple))
):
raise StateConflict(
"RECIPIENT_BINDING_REQUIRED",
"one trusted TaskPlan recipient binding is required",
)
return {str(key): str(item) for key, item in value.items()}
def spawn(
self,
task_id: str,
parent_session_id: str,
recipient_binding: Mapping[str, Any],
) -> NativeSession:
if not task_id or not parent_session_id:
raise StateConflict("NATIVE_SESSION_BINDING_REQUIRED", "task and parent are required")
recipient = self._recipient(recipient_binding)
response = self.client.spawn(
{
"task_id": task_id,
"parent_session_id": parent_session_id,
"recipient_binding": recipient,
"candidate_only": True,
"production_session_access": False,
},
self.credential_reference,
)
provider_session_id = response.get("provider_session_id")
if (
not isinstance(provider_session_id, str)
or not provider_session_id
or response.get("task_id") != task_id
or response.get("parent_session_id") != parent_session_id
or response.get("candidate_only") is not True
or response.get("production_session_access") is not False
):
raise StateConflict(
"NATIVE_SESSION_PROVISIONING_INVALID",
"provider response is not bound to this Candidate task",
)
broker_id = f"broker-{uuid.uuid4()}"
if broker_id == provider_session_id:
raise StateConflict("SESSION_ID_COLLISION", "broker and provider IDs must differ")
return NativeSession(
provider=self.provider,
broker_session_id=broker_id,
provider_session_id=provider_session_id,
task_id=task_id,
parent_session_id=parent_session_id,
recipient_binding=recipient,
)
def verify(self, session: NativeSession) -> SessionVerification:
self._validate_session(session)
status = self.client.status(session.provider_session_id, self.credential_reference)
tree = self.client.list_tree(session.parent_session_id, self.credential_reference)
status_valid = (
status.get("provider_session_id") == session.provider_session_id
and status.get("state") in {"PROVISIONED", "ACTIVE", "RUNNING"}
)
tree_valid = any(
item.get("provider_session_id") == session.provider_session_id
and item.get("parent_session_id") == session.parent_session_id
for item in tree
)
if not status_valid or not tree_valid:
raise StateConflict(
"NATIVE_SESSION_VERIFICATION_FAILED",
"provider status or parent/child tree is inconsistent",
)
simulated = bool(getattr(self.client, "simulation", True))
return SessionVerification(
broker_session_verified=True,
openclaw_native_child_session_verified=(
self.provider == "openclaw" and not simulated
),
codex_child_session_verified=(self.provider == "codex" and not simulated),
simulation=simulated,
)
def send_text(self, session: NativeSession, text: str) -> TextResult:
self._validate_session(session)
if not isinstance(text, str) or not text:
raise StateConflict("TEXT_RESULT_REQUIRED", "non-empty text is required")
response = self.client.send_text(
session.provider_session_id,
{
"task_id": session.task_id,
"content_type": "text/plain",
"encoding": "utf-8",
"image_only": False,
"text": text,
"recipient_binding": dict(session.recipient_binding),
"secrets_included": False,
},
self.credential_reference,
)
required = {
"content_type": "text/plain",
"encoding": "utf-8",
"image_only": False,
"task_id": session.task_id,
"recipient_binding": dict(session.recipient_binding),
"secrets_included": False,
}
if any(response.get(key) != value for key, value in required.items()):
raise StateConflict("TEXT_RESULT_CONTRACT_INVALID", "text result binding is invalid")
message_id = response.get("message_id")
receipt = response.get("receipt")
if not isinstance(message_id, str) or not message_id or not isinstance(receipt, str) or not receipt:
raise StateConflict("TEXT_RESULT_RECEIPT_REQUIRED", "message and receipt are required")
return TextResult(
content_type="text/plain",
encoding="utf-8",
image_only=False,
task_id=session.task_id,
message_id=message_id,
receipt=receipt,
recipient_binding=dict(session.recipient_binding),
secrets_included=False,
)
def status(self, session: NativeSession) -> str:
self._validate_session(session)
response = self.client.status(session.provider_session_id, self.credential_reference)
if response.get("provider_session_id") != session.provider_session_id:
raise StateConflict("NATIVE_SESSION_STATUS_INVALID", "provider session ID changed")
state = response.get("state")
if not isinstance(state, str) or not state:
raise StateConflict("NATIVE_SESSION_STATUS_INVALID", "provider state is missing")
return state
def interrupt(self, session: NativeSession) -> None:
self._validate_session(session)
self.client.interrupt(session.provider_session_id, self.credential_reference)
if self.status(session) != "INTERRUPTED":
raise StateConflict("NATIVE_SESSION_INTERRUPT_FAILED", "interrupt was not observed")
def close(self, session: NativeSession) -> None:
self._validate_session(session)
self.client.close(session.provider_session_id, self.credential_reference)
if self.status(session) != "CLOSED":
raise StateConflict("NATIVE_SESSION_CLOSE_FAILED", "close was not observed")
def _validate_session(self, session: NativeSession) -> None:
if session.provider != self.provider:
raise StateConflict("NATIVE_SESSION_PROVIDER_MISMATCH", "session provider differs")
if session.broker_session_id == session.provider_session_id:
raise StateConflict("SESSION_ID_COLLISION", "broker and provider IDs must differ")
class OpenClawNativeAdapter(CandidateNativeAdapter):
provider = "openclaw"
required_reference = "systemd-credential:openclaw-gateway-candidate"
class CodexNativeAdapter(CandidateNativeAdapter):
provider = "codex"
required_reference = "systemd-credential:codex-app-server-candidate"
@@ -0,0 +1,54 @@
"""High-level autonomous release orchestration facade."""
from __future__ import annotations
from dataclasses import dataclass
from .evidence import EvidenceChain
from .models import TaskPlan
from .policy import ReleasePolicy
from .workflow import WorkflowEngine
@dataclass(frozen=True, slots=True)
class OrchestrationResult:
task_id: str
plan_digest: str
evidence_closure: str | None
production_side_effects: bool
class AutonomousReleaseOrchestrator:
"""Coordinates candidate workflows while keeping production disabled."""
def __init__(self, workflow: WorkflowEngine, policy: ReleasePolicy) -> None:
self.workflow = workflow
self.policy = policy
self.evidence: EvidenceChain | None = None
def create(self, plan: TaskPlan) -> OrchestrationResult:
self.policy.validate_task(plan)
self.workflow.plan(plan)
self.evidence = EvidenceChain(self.workflow.store, plan.task_id)
self.evidence.append(
"TASK_CREATED",
plan.task_id,
{"plan_digest": plan.digest(), "secrets_included": False},
)
return OrchestrationResult(
task_id=plan.task_id,
plan_digest=plan.digest(),
evidence_closure=None,
production_side_effects=False,
)
def close_evidence(self, task_id: str) -> OrchestrationResult:
if self.evidence is None or self.evidence.task_id != task_id:
self.evidence = EvidenceChain(self.workflow.store, task_id)
closure = self.evidence.closure_digest()
return OrchestrationResult(
task_id=task_id,
plan_digest="",
evidence_closure=closure,
production_side_effects=False,
)
@@ -0,0 +1,75 @@
"""Deterministic task planning for autonomous candidate releases."""
from __future__ import annotations
from dataclasses import dataclass
from .models import ResourceBinding, RiskClass, TaskPlan, WorkflowStep
@dataclass(frozen=True, slots=True)
class PlanningRequest:
task_id: str
intent: str
binding: ResourceBinding
risk: RiskClass = RiskClass.MEDIUM
class DeterministicPlanner:
"""Create a fixed governed DAG without granting production capabilities."""
def build(self, request: PlanningRequest) -> TaskPlan:
steps = (
WorkflowStep(
step_id="architecture-review",
worker_role="hermes-review",
required_capabilities=("architecture.review", "risk.analysis"),
allowed_paths=request.binding.allowed_paths,
),
WorkflowStep(
step_id="implementation",
worker_role="dedicated-writer",
required_capabilities=("commit_file.atomic", "git.push.patch_branch"),
depends_on=("architecture-review",),
max_attempts=2,
allowed_paths=request.binding.allowed_paths,
),
WorkflowStep(
step_id="code-review",
worker_role="codex-review",
required_capabilities=("code.review", "git.diff.read"),
depends_on=("implementation",),
allowed_paths=request.binding.allowed_paths,
),
WorkflowStep(
step_id="validation",
worker_role="openclaw-shadow",
required_capabilities=("test.execute", "artifact.verify"),
depends_on=("code-review",),
max_attempts=2,
allowed_paths=request.binding.allowed_paths,
),
WorkflowStep(
step_id="evidence-finalize",
worker_role="evidence-controller",
required_capabilities=("evidence.finalize",),
depends_on=("validation",),
allowed_paths=request.binding.allowed_paths,
),
)
return TaskPlan(
task_id=request.task_id,
intent=request.intent,
binding=request.binding,
risk=request.risk,
steps=steps,
evidence_requirements=(
"git-ref-closure",
"test-report",
"secret-scan",
"artifact-manifest",
"credential-revocation",
"production-invariants",
),
production_side_effects_allowed=False,
)
@@ -0,0 +1,54 @@
"""Fail-closed policy engine for autonomous release workflows."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from .errors import ApprovalRequired, CapabilityMismatch, PolicyDenied
from .models import ApprovalEnvelope, ResourceBinding, RiskClass, TaskPlan, WorkerDescriptor
FORBIDDEN_CAPABILITIES = frozenset({
"production.deploy",
"production.rollback",
"main.write",
"tag.write",
"baseline.write",
"registry.write",
"secret.export",
})
@dataclass(frozen=True, slots=True)
class PolicyDecision:
allowed: bool
reason: str
class ReleasePolicy:
def validate_binding(self, binding: ResourceBinding) -> PolicyDecision:
if binding.branch == "main":
raise PolicyDenied("MAIN_WRITE_DENIED", "main branch is never writable")
if not binding.allowed_paths:
raise PolicyDenied("PATH_SCOPE_REQUIRED", "candidate path scope is required")
return PolicyDecision(True, "binding_valid")
def validate_task(self, plan: TaskPlan) -> PolicyDecision:
self.validate_binding(plan.binding)
if plan.production_side_effects_allowed:
raise PolicyDenied("PRODUCTION_SIDE_EFFECT_DENIED", "candidate workflows cannot execute production side effects")
return PolicyDecision(True, "task_valid")
def validate_approval(self, approval: ApprovalEnvelope, now: datetime | None = None) -> PolicyDecision:
now = now or datetime.now(timezone.utc)
if now < approval.not_before or now >= approval.expires_at:
raise ApprovalRequired("APPROVAL_TIME_INVALID", "approval is outside its validity window")
if any(cap in FORBIDDEN_CAPABILITIES for cap in approval.capabilities):
raise PolicyDenied("CAPABILITY_FORBIDDEN", "approval requests forbidden capability")
return PolicyDecision(True, "approval_valid")
def validate_worker(self, worker: WorkerDescriptor, required: tuple[str, ...], trust_zone: str) -> PolicyDecision:
if not worker.can_accept(required, trust_zone):
raise CapabilityMismatch("WORKER_CAPABILITY_MISMATCH", "worker cannot satisfy task requirements")
return PolicyDecision(True, "worker_valid")
@@ -0,0 +1,152 @@
"""Governed deployment and rollback state machine."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from .approval import ApprovalVerificationResult
from .errors import ReleaseBlocked
from .models import ReleasePlan
class ReleaseState(str, Enum):
CREATED = "CREATED"
BUILT = "BUILT"
VERIFIED = "VERIFIED"
WAITING_OWNER = "WAITING_OWNER"
APPROVED = "APPROVED"
DEPLOYING = "DEPLOYING"
DEPLOYED = "DEPLOYED"
FAILED = "FAILED"
ROLLBACK_REQUESTED = "ROLLBACK_REQUESTED"
ROLLBACK_APPROVED = "ROLLBACK_APPROVED"
ROLLING_BACK = "ROLLING_BACK"
ROLLED_BACK = "ROLLED_BACK"
@dataclass(frozen=True, slots=True)
class ReleaseStatus:
state: ReleaseState
reason: str | None = None
class ReleaseController:
"""No transition reaches deployment or rollback without an external receipt."""
def __init__(self) -> None:
self._state = ReleaseState.CREATED
self._plan: ReleasePlan | None = None
self._reason: str | None = None
def _expect(self, *states: ReleaseState) -> None:
if self._state not in states:
raise ReleaseBlocked(
"RELEASE_STATE_INVALID",
"release transition is not allowed from the current state",
{"state": self._state.value, "allowed": [item.value for item in states]},
)
def attach_plan(self, plan: ReleasePlan) -> ReleaseStatus:
if self._plan is not None:
if self._plan == plan:
return self.status()
raise ReleaseBlocked("RELEASE_PLAN_ALREADY_ATTACHED", "release plan is immutable")
self._plan = plan
return self.status()
def mark_built(self) -> ReleaseStatus:
self._require_plan()
self._expect(ReleaseState.CREATED)
self._state = ReleaseState.BUILT
return self.status()
def verify(self, *, artifact_ok: bool, evidence_ok: bool) -> ReleaseStatus:
self._require_plan()
self._expect(ReleaseState.BUILT)
if not artifact_ok or not evidence_ok:
raise ReleaseBlocked(
"RELEASE_VERIFICATION_FAILED", "artifact or evidence verification failed"
)
self._state = ReleaseState.VERIFIED
return self.status()
def request_owner_approval(self) -> ReleaseStatus:
self._expect(ReleaseState.VERIFIED)
self._state = ReleaseState.WAITING_OWNER
return self.status()
def approve(self, receipt: ApprovalVerificationResult) -> ReleaseStatus:
plan = self._require_plan()
self._expect(ReleaseState.WAITING_OWNER)
if receipt.task_id != plan.task_id:
raise ReleaseBlocked("OWNER_APPROVAL_TASK_MISMATCH", "approval task does not match")
if receipt.approval_plan_id != plan.approval_plan_id:
raise ReleaseBlocked(
"OWNER_APPROVAL_PLAN_MISMATCH", "approval plan does not match release plan"
)
self._state = ReleaseState.APPROVED
return self.status()
def start_deployment(self) -> ReleaseStatus:
self._expect(ReleaseState.APPROVED)
self._state = ReleaseState.DEPLOYING
return self.status()
def mark_deployed(self) -> ReleaseStatus:
self._expect(ReleaseState.DEPLOYING)
self._state = ReleaseState.DEPLOYED
return self.status()
def mark_failed(self, reason: str) -> ReleaseStatus:
self._expect(ReleaseState.DEPLOYING, ReleaseState.DEPLOYED)
if not reason:
raise ReleaseBlocked("RELEASE_FAILURE_REASON_REQUIRED", "failure reason is required")
self._reason = reason
self._state = ReleaseState.FAILED
return self.status()
def request_rollback(self, reason: str) -> ReleaseStatus:
self._expect(ReleaseState.DEPLOYING, ReleaseState.DEPLOYED, ReleaseState.FAILED)
if not reason:
raise ReleaseBlocked("ROLLBACK_REASON_REQUIRED", "rollback reason is required")
self._reason = reason
self._state = ReleaseState.ROLLBACK_REQUESTED
return self.status()
def approve_rollback(self, receipt: ApprovalVerificationResult) -> ReleaseStatus:
plan = self._require_plan()
self._expect(ReleaseState.ROLLBACK_REQUESTED)
if receipt.task_id != plan.task_id:
raise ReleaseBlocked("ROLLBACK_APPROVAL_TASK_MISMATCH", "rollback task does not match")
if receipt.approval_plan_id != plan.rollback_approval_plan_id:
raise ReleaseBlocked(
"ROLLBACK_APPROVAL_PLAN_MISMATCH",
"rollback approval plan does not match release plan",
)
self._state = ReleaseState.ROLLBACK_APPROVED
return self.status()
def start_rollback(self) -> ReleaseStatus:
self._expect(ReleaseState.ROLLBACK_APPROVED)
self._state = ReleaseState.ROLLING_BACK
return self.status()
def mark_rolled_back(self) -> ReleaseStatus:
self._expect(ReleaseState.ROLLING_BACK)
self._state = ReleaseState.ROLLED_BACK
return self.status()
def rollback(self) -> ReleaseStatus:
raise ReleaseBlocked(
"ROLLBACK_APPROVAL_REQUIRED",
"use request_rollback, approve_rollback and start_rollback",
)
def status(self) -> ReleaseStatus:
return ReleaseStatus(self._state, self._reason)
def _require_plan(self) -> ReleasePlan:
if self._plan is None:
raise ReleaseBlocked("RELEASE_PLAN_REQUIRED", "attach a release plan first")
return self._plan
@@ -0,0 +1,72 @@
"""Read-only multi-source release consistency gate."""
from __future__ import annotations
from dataclasses import dataclass
from .errors import ReleaseBlocked
from .models import is_git_oid, is_sha256
@dataclass(frozen=True, slots=True)
class ReleaseEvidence:
expected_version: str
registry_version: str
baseline_version: str
local_tag_oid: str
remote_tag_oid: str
gitea_tag_oid: str
expected_tag_oid: str
local_peeled_target_oid: str
remote_peeled_target_oid: str
gitea_peeled_target_oid: str
expected_target_oid: str
artifact_sha256: str
manifest_sha256: str
evidence_sha256: str
rollback_artifact_sha256: str
class ReleaseGate:
"""Require exact tag-object, peeled-target, version and package closure."""
def verify(self, evidence: ReleaseEvidence) -> None:
versions = (
evidence.expected_version,
evidence.registry_version,
evidence.baseline_version,
)
if not versions[0] or len(set(versions)) != 1:
raise ReleaseBlocked("VERSION_MISMATCH", "expected, registry and baseline differ")
tag_oids = (
evidence.local_tag_oid,
evidence.remote_tag_oid,
evidence.gitea_tag_oid,
evidence.expected_tag_oid,
)
if any(not is_git_oid(value) for value in tag_oids):
raise ReleaseBlocked("TAG_OID_INVALID", "tag object OID is invalid")
if evidence.local_tag_oid != evidence.remote_tag_oid:
raise ReleaseBlocked("TAG_REMOTE_MISMATCH", "local and remote tag objects differ")
if evidence.remote_tag_oid != evidence.gitea_tag_oid:
raise ReleaseBlocked("TAG_GITEA_MISMATCH", "Gitea tag object differs")
if evidence.gitea_tag_oid != evidence.expected_tag_oid:
raise ReleaseBlocked("TAG_OBJECT_MISMATCH", "tag object differs from ReleasePlan")
targets = (
evidence.local_peeled_target_oid,
evidence.remote_peeled_target_oid,
evidence.gitea_peeled_target_oid,
evidence.expected_target_oid,
)
if any(not is_git_oid(value) for value in targets):
raise ReleaseBlocked("TAG_TARGET_OID_INVALID", "peeled target OID is invalid")
if len(set(targets)) != 1:
raise ReleaseBlocked("TAG_TARGET_MISMATCH", "peeled tag targets are not identical")
digests = (
evidence.artifact_sha256,
evidence.manifest_sha256,
evidence.evidence_sha256,
evidence.rollback_artifact_sha256,
)
if any(not is_sha256(value) for value in digests):
raise ReleaseBlocked("ARTIFACT_DIGEST_MISSING", "package closure is incomplete")
@@ -0,0 +1,36 @@
"""Deterministic worker routing for candidate autonomous release workflows."""
from __future__ import annotations
from dataclasses import dataclass
from .errors import CapabilityMismatch
from .models import WorkerDescriptor
@dataclass(frozen=True, slots=True)
class RouteRequest:
task_id: str
required_capabilities: tuple[str, ...]
trust_zone: str
class WorkerRouter:
"""Select the least loaded compatible worker without expanding trust."""
def __init__(self, workers: list[WorkerDescriptor]) -> None:
self._workers = list(workers)
def route(self, request: RouteRequest) -> WorkerDescriptor:
candidates = [
worker
for worker in self._workers
if worker.can_accept(request.required_capabilities, request.trust_zone)
]
if not candidates:
raise CapabilityMismatch(
"NO_COMPATIBLE_WORKER",
"no worker satisfies capability and trust constraints",
{"task_id": request.task_id},
)
return sorted(candidates, key=lambda item: (item.current_load, item.worker_id))[0]
@@ -0,0 +1,213 @@
"""AIOS Secret Manager V2 reference-only Candidate adapter."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Mapping, Protocol, Sequence
from .errors import CredentialUnavailable
from .models import CredentialRequest
PROVIDER_REFERENCES = {
"openclaw": "systemd-credential:openclaw-gateway-candidate",
"codex": "systemd-credential:codex-app-server-candidate",
"native_adapter_control": "systemd-credential:session-native-adapter-control-candidate",
"gitea": "systemd-credential:gitea-admin-api-candidate",
}
FORBIDDEN_RESPONSE_FIELDS = {
"api_key",
"token",
"password",
"cookie",
"private_key",
"credential_body",
"plaintext",
}
class SecretManagerTransport(Protocol):
def exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
@dataclass(frozen=True, slots=True)
class SecretProjectionReceipt:
task_id: str
credential_id: str
reference: str
provider: str
audience: str
consumer_service: str
state: str
expires_at: datetime
plaintext_secret_returned: bool = False
class SecretManagerV2:
"""Issue, project, inspect and revoke without receiving authentication material."""
def __init__(self, transport: SecretManagerTransport) -> None:
self.transport = transport
@staticmethod
def _assert_safe(value: Any) -> None:
if isinstance(value, Mapping):
if FORBIDDEN_RESPONSE_FIELDS.intersection(str(key).lower() for key in value):
raise CredentialUnavailable(
"SECRET_MANAGER_PLAINTEXT_REJECTED",
"Secret Manager response included a forbidden plaintext field",
)
for item in value.values():
SecretManagerV2._assert_safe(item)
elif isinstance(value, (list, tuple)):
for item in value:
SecretManagerV2._assert_safe(item)
def _exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
try:
response = self.transport.exchange(operation, payload)
except CredentialUnavailable:
raise
except Exception as error:
raise CredentialUnavailable(
"SECRET_MANAGER_UNAVAILABLE", "Secret Manager V2 operation failed"
) from error
if not isinstance(response, Mapping):
raise CredentialUnavailable(
"SECRET_MANAGER_RESPONSE_INVALID", "Secret Manager V2 returned a non-object"
)
self._assert_safe(response)
if response.get("plaintext_secret_returned") is not False:
raise CredentialUnavailable(
"SECRET_MANAGER_PLAINTEXT_REJECTED",
"Secret Manager did not attest plaintext_secret_returned=false",
)
return response
@staticmethod
def _payload(request: CredentialRequest) -> dict[str, Any]:
return {
"approval_plan_id": request.approval_plan_id,
"task_id": request.task_id,
"request_id": request.request_id,
"session_id": request.session_id,
"worker_id": request.worker_id,
"provider": request.provider,
"audience": request.audience,
"consumer_service": request.consumer_service,
"runtime_user": request.runtime_user,
"repository": request.binding.repository,
"branch": request.binding.branch,
"branch_oid": request.binding.base_oid,
"allowed_paths": list(request.binding.allowed_paths),
"operations": list(request.operations),
"not_before": request.not_before.astimezone(timezone.utc).isoformat(),
"expires_at": request.expires_at.astimezone(timezone.utc).isoformat(),
"nonce": request.nonce,
"ttl_seconds": request.ttl_seconds,
"max_uses": request.max_uses,
}
def issue(
self, request: CredentialRequest, now: datetime | None = None
) -> SecretProjectionReceipt:
now = now or datetime.now(timezone.utc)
if now < request.not_before or now >= request.expires_at:
raise CredentialUnavailable(
"SECRET_MANAGER_APPROVAL_WINDOW_INVALID", "credential request is not currently valid"
)
expected_reference = PROVIDER_REFERENCES.get(request.provider)
if expected_reference is None:
raise CredentialUnavailable(
"SECRET_MANAGER_PROVIDER_DENIED", "provider is not Candidate-approved"
)
response = self._exchange("issue", self._payload(request))
credential_id = response.get("credential_id")
if (
response.get("task_id") != request.task_id
or not isinstance(credential_id, str)
or not credential_id
or response.get("credential_reference") != expected_reference
or response.get("state") != "ISSUED"
):
raise CredentialUnavailable(
"SECRET_MANAGER_BINDING_MISMATCH", "issued projection does not match the request"
)
expires_at = datetime.fromisoformat(str(response.get("expires_at")))
if expires_at > request.expires_at or expires_at <= now:
raise CredentialUnavailable(
"SECRET_MANAGER_EXPIRY_INVALID", "issued projection expiry is outside approval"
)
return SecretProjectionReceipt(
task_id=request.task_id,
credential_id=credential_id,
reference=expected_reference,
provider=request.provider,
audience=request.audience,
consumer_service=request.consumer_service,
state="ISSUED",
expires_at=expires_at,
plaintext_secret_returned=False,
)
@staticmethod
def _receipt_payload(receipt: SecretProjectionReceipt) -> dict[str, Any]:
return {
"task_id": receipt.task_id,
"credential_id": receipt.credential_id,
"credential_reference": receipt.reference,
"provider": receipt.provider,
"audience": receipt.audience,
"consumer_service": receipt.consumer_service,
}
def _state_operation(
self, operation: str, receipt: SecretProjectionReceipt, expected_state: str
) -> Mapping[str, Any]:
response = self._exchange(operation, self._receipt_payload(receipt))
if (
response.get("task_id") != receipt.task_id
or response.get("credential_id") != receipt.credential_id
or response.get("state") != expected_state
):
raise CredentialUnavailable(
"SECRET_MANAGER_STATE_MISMATCH", f"{operation} response is not task-bound"
)
return response
def project(self, receipt: SecretProjectionReceipt) -> None:
self._state_operation("project", receipt, "PROJECTED")
def status(self, receipt: SecretProjectionReceipt) -> str:
return str(self._state_operation("status", receipt, "PROJECTED")["state"])
def revoke(self, receipt: SecretProjectionReceipt) -> None:
self._state_operation("revoke", receipt, "REVOKED")
def verify_revocation(self, receipt: SecretProjectionReceipt) -> None:
response = self._state_operation("verify_revocation", receipt, "REVOKED")
if response.get("post_revocation_auth") != "REJECTED":
raise CredentialUnavailable(
"SECRET_MANAGER_REVOCATION_UNVERIFIED",
"post-revocation authentication was not rejected",
)
def audit(self, receipt: SecretProjectionReceipt) -> Sequence[Mapping[str, Any]]:
response = self._exchange("audit", self._receipt_payload(receipt))
if (
response.get("task_id") != receipt.task_id
or response.get("credential_id") != receipt.credential_id
):
raise CredentialUnavailable(
"SECRET_MANAGER_AUDIT_MISMATCH", "audit response is not task-bound"
)
events = response.get("events")
if not isinstance(events, list) or any(
not isinstance(event, Mapping) or event.get("secrets_included") is not False
for event in events
):
raise CredentialUnavailable(
"SECRET_MANAGER_AUDIT_INVALID", "audit events are absent or unsafe"
)
return events
@@ -0,0 +1,116 @@
"""JSON serialization helpers for candidate API and CLI boundaries."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Mapping
from .models import ReleasePlan, ResourceBinding, RiskClass, TaskPlan, WorkflowStep
def task_plan_from_mapping(value: Mapping[str, Any]) -> TaskPlan:
binding_value = value["binding"]
binding = ResourceBinding(
repository=str(binding_value["repository"]),
branch=str(binding_value["branch"]),
base_oid=str(binding_value["base_oid"]),
allowed_paths=tuple(str(item) for item in binding_value.get("allowed_paths", ())),
)
steps = tuple(
WorkflowStep(
step_id=str(item["step_id"]),
worker_role=str(item["worker_role"]),
required_capabilities=tuple(str(cap) for cap in item.get("required_capabilities", ())),
depends_on=tuple(str(dep) for dep in item.get("depends_on", ())),
max_attempts=int(item.get("max_attempts", 1)),
timeout_seconds=int(item.get("timeout_seconds", 900)),
allowed_paths=tuple(str(path) for path in item.get("allowed_paths", ())),
)
for item in value.get("steps", ())
)
created_at_raw = value.get("created_at")
created_at = datetime.fromisoformat(created_at_raw) if created_at_raw else None
kwargs: dict[str, Any] = {
"task_id": str(value["task_id"]),
"intent": str(value["intent"]),
"binding": binding,
"risk": RiskClass(str(value.get("risk", RiskClass.MEDIUM.value))),
"steps": steps,
"evidence_requirements": tuple(str(item) for item in value.get("evidence_requirements", ())),
"production_side_effects_allowed": bool(value.get("production_side_effects_allowed", False)),
}
if created_at is not None:
kwargs["created_at"] = created_at
return TaskPlan(**kwargs)
def task_plan_to_mapping(plan: TaskPlan) -> dict[str, Any]:
return {
"task_id": plan.task_id,
"intent": plan.intent,
"binding": {
"repository": plan.binding.repository,
"branch": plan.binding.branch,
"base_oid": plan.binding.base_oid,
"allowed_paths": list(plan.binding.allowed_paths),
},
"risk": plan.risk.value,
"steps": [
{
"step_id": step.step_id,
"worker_role": step.worker_role,
"required_capabilities": list(step.required_capabilities),
"depends_on": list(step.depends_on),
"max_attempts": step.max_attempts,
"timeout_seconds": step.timeout_seconds,
"allowed_paths": list(step.allowed_paths),
}
for step in plan.steps
],
"evidence_requirements": list(plan.evidence_requirements),
"production_side_effects_allowed": plan.production_side_effects_allowed,
"created_at": plan.created_at.isoformat(),
"plan_digest": plan.digest(),
}
def release_plan_from_mapping(value: Mapping[str, Any]) -> ReleasePlan:
return ReleasePlan(
task_id=str(value["task_id"]),
repository=str(value["repository"]),
branch=str(value["branch"]),
version=str(value["version"]),
target_oid=str(value["target_oid"]),
tree_oid=str(value["tree_oid"]),
artifact_sha256=str(value["artifact_sha256"]),
manifest_sha256=str(value["manifest_sha256"]),
package_closure_sha256=str(value["package_closure_sha256"]),
evidence_closure_oid=str(value["evidence_closure_oid"]),
approval_plan_id=str(value["approval_plan_id"]),
rollback_approval_plan_id=str(value["rollback_approval_plan_id"]),
rollback_artifact_sha256=str(value["rollback_artifact_sha256"]),
expected_tag_oid=str(value["expected_tag_oid"]),
expected_target_oid=str(value["expected_target_oid"]),
production_config_sha256=str(value["production_config_sha256"]),
)
def release_plan_to_mapping(plan: ReleasePlan) -> dict[str, Any]:
return {
"task_id": plan.task_id,
"repository": plan.repository,
"branch": plan.branch,
"version": plan.version,
"target_oid": plan.target_oid,
"tree_oid": plan.tree_oid,
"artifact_sha256": plan.artifact_sha256,
"manifest_sha256": plan.manifest_sha256,
"package_closure_sha256": plan.package_closure_sha256,
"evidence_closure_oid": plan.evidence_closure_oid,
"approval_plan_id": plan.approval_plan_id,
"rollback_approval_plan_id": plan.rollback_approval_plan_id,
"rollback_artifact_sha256": plan.rollback_artifact_sha256,
"expected_tag_oid": plan.expected_tag_oid,
"expected_target_oid": plan.expected_target_oid,
"production_config_sha256": plan.production_config_sha256,
}
@@ -0,0 +1,223 @@
"""Hardened loopback HTTP boundary for the dormant Candidate."""
from __future__ import annotations
import argparse
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import ipaddress
import json
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from .api import AutonomousReleaseAPI
from .errors import AutonomousReleaseError
from .orchestrator import AutonomousReleaseOrchestrator
from .policy import ReleasePolicy
from .store import StateStore
from .workflow import WorkflowEngine
class CandidateApplication:
def __init__(self, state_path: str | Path) -> None:
self.store = StateStore(state_path)
workflow = WorkflowEngine(self.store)
self.api = AutonomousReleaseAPI(
AutonomousReleaseOrchestrator(workflow, ReleasePolicy())
)
def ready(self) -> bool:
with self.store.transaction() as connection:
return connection.execute("SELECT 1").fetchone()[0] == 1
class CandidateRequestHandler(BaseHTTPRequestHandler):
server_version = "AIOSAutonomousReleaseCandidate/2"
sys_version = ""
@property
def application(self) -> CandidateApplication:
return self.server.application # type: ignore[attr-defined]
def _send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
self.send_response(status.value)
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.send_header("Referrer-Policy", "no-referrer")
self.end_headers()
self.wfile.write(encoded)
def _read_json(self) -> dict[str, Any]:
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
if content_type != "application/json":
raise UnsupportedMediaType
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError as error:
raise InvalidRequest from error
if length <= 0 or length > 1_048_576:
raise InvalidRequest
try:
raw = self.rfile.read(length)
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise InvalidRequest from error
if not isinstance(value, dict):
raise InvalidRequest
return value
def do_GET(self) -> None: # noqa: N802
path = urlparse(self.path).path
if path == "/health":
self._send_json(
HTTPStatus.OK,
{
"status": "ok",
"mode": "candidate-shadow-only",
"production_side_effects": False,
"secrets_included": False,
},
)
return
if path == "/ready":
if self.application.ready():
self._send_json(
HTTPStatus.OK, {"status": "ready", "secrets_included": False}
)
else:
self._send_json(
HTTPStatus.SERVICE_UNAVAILABLE,
{"status": "not_ready", "secrets_included": False},
)
return
if path.startswith("/v1/tasks/") and path.endswith("/steps"):
task_id = path[len("/v1/tasks/") : -len("/steps")].strip("/")
if not task_id or "/" in task_id:
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
return
try:
self._send_json(HTTPStatus.OK, self.application.api.task_steps(task_id))
except KeyError:
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
return
if path.startswith("/v1/tasks/") and path.endswith("/evidence"):
task_id = path[len("/v1/tasks/") : -len("/evidence")].strip("/")
if not task_id or "/" in task_id:
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
return
try:
self._send_json(HTTPStatus.OK, self.application.api.evidence(task_id))
except KeyError:
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
return
if path.startswith("/v1/tasks/"):
task_id = path[len("/v1/tasks/") :].strip("/")
if not task_id or "/" in task_id:
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
return
try:
self._send_json(HTTPStatus.OK, self.application.api.task_status(task_id))
except KeyError:
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
return
self._send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"})
def do_POST(self) -> None: # noqa: N802
path = urlparse(self.path).path
try:
payload = self._read_json()
if path == "/v1/tasks":
self._send_json(HTTPStatus.CREATED, self.application.api.create_task(payload))
return
if path == "/v1/release/plan":
self._send_json(
HTTPStatus.CREATED, self.application.api.create_release_plan(payload)
)
return
if path == "/v1/release/verify":
self._send_json(HTTPStatus.OK, self.application.api.verify_release(payload))
return
if path.startswith("/v1/tasks/") and path.endswith("/evidence/close"):
task_id = path[len("/v1/tasks/") : -len("/evidence/close")].strip("/")
if not task_id:
raise InvalidRequest
self._send_json(HTTPStatus.OK, self.application.api.close_evidence(task_id))
return
for action, handler in (
("claim", self.application.api.claim),
("heartbeat", self.application.api.heartbeat),
("result", self.application.api.result),
):
suffix = f"/{action}"
if path.startswith("/v1/tasks/") and path.endswith(suffix):
task_id = path[len("/v1/tasks/") : -len(suffix)].strip("/")
if not task_id or "/" in task_id:
raise InvalidRequest
self._send_json(HTTPStatus.OK, handler(task_id, payload))
return
self._send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"})
except UnsupportedMediaType:
self._send_json(
HTTPStatus.UNSUPPORTED_MEDIA_TYPE, {"error": "content_type_required"}
)
except (InvalidRequest, ValueError, KeyError):
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
except AutonomousReleaseError as error:
self._send_json(HTTPStatus.CONFLICT, {"error": error.code})
except Exception:
self._send_json(
HTTPStatus.INTERNAL_SERVER_ERROR, {"error": "candidate_request_failed"}
)
def log_message(self, _format: str, *_args: Any) -> None:
return
class InvalidRequest(Exception):
pass
class UnsupportedMediaType(Exception):
pass
class CandidateHTTPServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = False
def __init__(self, address: tuple[str, int], application: CandidateApplication) -> None:
try:
loopback = ipaddress.ip_address(address[0]).is_loopback
except ValueError as error:
raise ValueError("candidate service requires a numeric loopback address") from error
if not loopback:
raise ValueError("candidate service must bind to loopback")
self.application = application
super().__init__(address, CandidateRequestHandler)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="aios-autonomous-release-service")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=19951)
parser.add_argument("--state", required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
application = CandidateApplication(args.state)
server = CandidateHTTPServer((args.host, args.port), application)
try:
server.serve_forever(poll_interval=0.5)
finally:
server.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,87 @@
"""Unified candidate session lifecycle contracts."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
from .errors import StateConflict
class SessionState(str, Enum):
CREATED = "CREATED"
PROVISIONED = "PROVISIONED"
ACTIVE = "ACTIVE"
INTERRUPTED = "INTERRUPTED"
CLOSED = "CLOSED"
REVOKED = "REVOKED"
_ALLOWED_TRANSITIONS: dict[SessionState, frozenset[SessionState]] = {
SessionState.CREATED: frozenset({SessionState.PROVISIONED, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.PROVISIONED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.ACTIVE: frozenset({SessionState.INTERRUPTED, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.INTERRUPTED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.CLOSED: frozenset({SessionState.REVOKED}),
SessionState.REVOKED: frozenset(),
}
@dataclass(frozen=True, slots=True)
class SessionIdentity:
session_id: str
provider: str
provider_session_id: str
worker_id: str
task_id: str
created_at: datetime
def __post_init__(self) -> None:
if not all((self.session_id, self.provider, self.provider_session_id, self.worker_id, self.task_id)):
raise ValueError("all session identity fields are required")
if self.session_id == self.provider_session_id:
raise ValueError("broker and provider session IDs must be distinct")
class SessionOrchestrator:
"""Provider-neutral session lifecycle guard with explicit state transitions."""
def __init__(self) -> None:
self._sessions: dict[str, tuple[SessionIdentity, SessionState]] = {}
def create(self, identity: SessionIdentity) -> None:
if identity.session_id in self._sessions:
raise StateConflict("SESSION_EXISTS", "session already exists")
self._sessions[identity.session_id] = (identity, SessionState.CREATED)
def transition(self, session_id: str, state: SessionState) -> None:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
current_state = current[1]
if state not in _ALLOWED_TRANSITIONS[current_state]:
raise StateConflict(
"SESSION_TRANSITION_INVALID",
"session state transition is not allowed",
{"session_id": session_id, "from": current_state.value, "to": state.value},
)
self._sessions[session_id] = (current[0], state)
def identity(self, session_id: str) -> SessionIdentity:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
return current[0]
def status(self, session_id: str) -> SessionState:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
return current[1]
def close(self, session_id: str) -> None:
self.transition(session_id, SessionState.CLOSED)
def revoke(self, session_id: str) -> None:
self.transition(session_id, SessionState.REVOKED)
@@ -0,0 +1,383 @@
"""Durable, private SQLite state for the autonomous-release Candidate."""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import sqlite3
from typing import Any, Iterator, Mapping, Sequence
from .errors import StateConflict
from .models import StepState, TaskState, canonical_json
SCHEMA_VERSION = 2
@dataclass(frozen=True, slots=True)
class StoredStep:
task_id: str
step_id: str
state: StepState
attempt: int
max_attempts: int
worker_id: str | None
session_id: str | None
lease_expires_at: datetime | None
result_digest: str | None
updated_at: datetime
last_error: str | None
class StateStore:
"""SQLite store with WAL, full sync, schema checks and serialized CAS writes."""
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self._prepare_path()
self._initialize()
os.chmod(self.path, 0o600)
def _prepare_path(self) -> None:
if self.path.exists() and self.path.is_symlink():
raise StateConflict("STATE_PATH_SYMLINK", "state database must not be a symlink")
parent = self.path.parent
parent.mkdir(parents=True, exist_ok=True, mode=0o750)
if parent.is_symlink():
raise StateConflict("STATE_DIRECTORY_SYMLINK", "state directory must not be a symlink")
current_mode = parent.stat().st_mode & 0o777
os.chmod(parent, current_mode & ~0o027)
def _connect(self) -> sqlite3.Connection:
if self.path.exists() and self.path.is_symlink():
raise StateConflict("STATE_PATH_SYMLINK", "state database must not be a symlink")
connection = sqlite3.connect(self.path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@contextmanager
def transaction(self) -> Iterator[sqlite3.Connection]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
def _initialize(self) -> None:
with self.transaction() as connection:
connection.execute(
"CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
)
row = connection.execute(
"SELECT value FROM metadata WHERE key='schema_version'"
).fetchone()
version = int(row["value"]) if row is not None else None
if version not in (None, 1, SCHEMA_VERSION):
raise StateConflict(
"SCHEMA_VERSION_UNSUPPORTED",
"state schema is not supported",
{"found": version, "expected": SCHEMA_VERSION},
)
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
plan_digest TEXT NOT NULL,
plan_json TEXT NOT NULL,
state TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS steps (
task_id TEXT NOT NULL,
step_id TEXT NOT NULL,
state TEXT NOT NULL,
attempt INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 1,
worker_id TEXT,
session_id TEXT,
lease_expires_at TEXT,
result_digest TEXT,
last_error TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY(task_id, step_id),
FOREIGN KEY(task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS events (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
previous_digest TEXT,
digest TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS consumed_nonces (
scope TEXT NOT NULL,
nonce TEXT NOT NULL,
consumed_at TEXT NOT NULL,
PRIMARY KEY(scope, nonce)
);
CREATE TABLE IF NOT EXISTS credentials (
credential_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
reference TEXT NOT NULL UNIQUE,
state TEXT NOT NULL,
metadata_json TEXT NOT NULL,
issued_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT
);
CREATE TABLE IF NOT EXISTS idempotency (
namespace TEXT NOT NULL,
operation_id TEXT NOT NULL,
result_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(namespace, operation_id)
);
"""
)
if version == 1:
existing = {
item["name"] for item in connection.execute("PRAGMA table_info(steps)")
}
additions = {
"max_attempts": "INTEGER NOT NULL DEFAULT 1",
"lease_expires_at": "TEXT",
"result_digest": "TEXT",
}
for column, declaration in additions.items():
if column not in existing:
connection.execute(f"ALTER TABLE steps ADD COLUMN {column} {declaration}")
connection.execute(
"INSERT INTO metadata(key, value) VALUES('schema_version', ?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(str(SCHEMA_VERSION),),
)
@staticmethod
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def create_task(
self,
task_id: str,
plan_digest: str,
plan: Mapping[str, Any],
step_ids: tuple[str, ...],
step_limits: Mapping[str, int] | None = None,
) -> None:
now = self._now()
limits = step_limits or {}
with self.transaction() as connection:
connection.execute(
"INSERT INTO tasks(task_id, plan_digest, plan_json, state, created_at, updated_at) "
"VALUES(?, ?, ?, ?, ?, ?)",
(task_id, plan_digest, canonical_json(plan), TaskState.PLANNED.value, now, now),
)
connection.executemany(
"INSERT INTO steps(task_id, step_id, state, max_attempts, updated_at) "
"VALUES(?, ?, ?, ?, ?)",
[
(task_id, step_id, StepState.PENDING.value, int(limits.get(step_id, 1)), now)
for step_id in step_ids
],
)
def get_task_state(self, task_id: str) -> tuple[TaskState, int]:
with self._connect() as connection:
row = connection.execute(
"SELECT state, revision FROM tasks WHERE task_id=?", (task_id,)
).fetchone()
if row is None:
raise KeyError(task_id)
return TaskState(row["state"]), int(row["revision"])
def compare_and_set_task_state(
self, task_id: str, expected_revision: int, new_state: TaskState
) -> int:
with self.transaction() as connection:
cursor = connection.execute(
"UPDATE tasks SET state=?, revision=revision+1, updated_at=? "
"WHERE task_id=? AND revision=?",
(new_state.value, self._now(), task_id, expected_revision),
)
if cursor.rowcount != 1:
raise StateConflict(
"TASK_REVISION_CONFLICT",
"task state changed concurrently",
{"task_id": task_id, "expected_revision": expected_revision},
)
row = connection.execute(
"SELECT revision FROM tasks WHERE task_id=?", (task_id,)
).fetchone()
return int(row["revision"])
def transition_step(
self,
task_id: str,
step_id: str,
expected: Sequence[StepState],
state: StepState,
*,
worker_id: str | None = None,
session_id: str | None = None,
lease_expires_at: datetime | None = None,
result_digest: str | None = None,
last_error: str | None = None,
increment_attempt: bool = False,
clear_assignment: bool = False,
) -> StoredStep:
with self.transaction() as connection:
row = connection.execute(
"SELECT * FROM steps WHERE task_id=? AND step_id=?", (task_id, step_id)
).fetchone()
if row is None:
raise KeyError(f"{task_id}:{step_id}")
if StepState(row["state"]) not in set(expected):
raise StateConflict(
"STEP_STATE_CONFLICT",
"step state changed or transition is invalid",
{"task_id": task_id, "step_id": step_id, "state": row["state"]},
)
if increment_attempt and int(row["attempt"]) >= int(row["max_attempts"]):
raise StateConflict(
"STEP_ATTEMPTS_EXHAUSTED",
"step has no remaining attempts",
{"task_id": task_id, "step_id": step_id},
)
next_worker = None if clear_assignment else (worker_id or row["worker_id"])
next_session = None if clear_assignment else (session_id or row["session_id"])
connection.execute(
"UPDATE steps SET state=?, attempt=attempt+?, worker_id=?, session_id=?, "
"lease_expires_at=?, result_digest=COALESCE(?, result_digest), last_error=?, updated_at=? "
"WHERE task_id=? AND step_id=?",
(
state.value,
1 if increment_attempt else 0,
next_worker,
next_session,
lease_expires_at.isoformat() if lease_expires_at else None,
result_digest,
last_error,
self._now(),
task_id,
step_id,
),
)
updated = connection.execute(
"SELECT * FROM steps WHERE task_id=? AND step_id=?", (task_id, step_id)
).fetchone()
return self._step_from_row(updated)
def set_step_state(
self,
task_id: str,
step_id: str,
state: StepState,
**kwargs: Any,
) -> StoredStep:
current = self.list_steps(task_id)[step_id]
return self.transition_step(task_id, step_id, (current.state,), state, **kwargs)
def list_steps(self, task_id: str) -> dict[str, StoredStep]:
with self._connect() as connection:
rows = connection.execute(
"SELECT * FROM steps WHERE task_id=? ORDER BY step_id", (task_id,)
).fetchall()
return {row["step_id"]: self._step_from_row(row) for row in rows}
@staticmethod
def _step_from_row(row: sqlite3.Row) -> StoredStep:
return StoredStep(
task_id=row["task_id"],
step_id=row["step_id"],
state=StepState(row["state"]),
attempt=int(row["attempt"]),
max_attempts=int(row["max_attempts"]),
worker_id=row["worker_id"],
session_id=row["session_id"],
lease_expires_at=(
datetime.fromisoformat(row["lease_expires_at"])
if row["lease_expires_at"]
else None
),
result_digest=row["result_digest"],
updated_at=datetime.fromisoformat(row["updated_at"]),
last_error=row["last_error"],
)
def consume_nonce(self, scope: str, nonce: str) -> None:
try:
with self.transaction() as connection:
connection.execute(
"INSERT INTO consumed_nonces(scope, nonce, consumed_at) VALUES(?, ?, ?)",
(scope, nonce, self._now()),
)
except sqlite3.IntegrityError as error:
raise StateConflict(
"NONCE_REPLAY", "nonce was already consumed", {"scope": scope, "nonce": nonce}
) from error
def append_event(
self,
task_id: str,
event_type: str,
payload: Mapping[str, Any],
previous_digest: str | None,
event_digest: str,
created_at: datetime,
) -> int:
with self.transaction() as connection:
cursor = connection.execute(
"INSERT INTO events(task_id, event_type, payload_json, previous_digest, digest, created_at) "
"VALUES(?, ?, ?, ?, ?, ?)",
(
task_id,
event_type,
canonical_json(payload),
previous_digest,
event_digest,
created_at.astimezone(timezone.utc).isoformat(),
),
)
return int(cursor.lastrowid)
def list_events(self, task_id: str) -> list[Mapping[str, Any]]:
with self._connect() as connection:
rows = connection.execute(
"SELECT * FROM events WHERE task_id=? ORDER BY sequence", (task_id,)
).fetchall()
return [dict(row) for row in rows]
def put_idempotent_result(
self, namespace: str, operation_id: str, result: Mapping[str, Any]
) -> Mapping[str, Any]:
encoded = canonical_json(result)
with self.transaction() as connection:
existing = connection.execute(
"SELECT result_json FROM idempotency WHERE namespace=? AND operation_id=?",
(namespace, operation_id),
).fetchone()
if existing is not None:
return json.loads(existing["result_json"])
connection.execute(
"INSERT INTO idempotency(namespace, operation_id, result_json, created_at) "
"VALUES(?, ?, ?, ?)",
(namespace, operation_id, encoded, self._now()),
)
return dict(result)
@@ -0,0 +1,44 @@
"""Read-only annotated and lightweight tag consistency checks."""
from __future__ import annotations
from dataclasses import dataclass
from .errors import ReleaseBlocked
from .models import is_git_oid
@dataclass(frozen=True, slots=True)
class TagReference:
ref: str
object_type: str
tag_oid: str
peeled_target_oid: str
class TagVerifier:
def verify(
self,
tag: TagReference,
*,
expected_tag_oid: str,
expected_target_oid: str,
) -> None:
if not tag.ref.startswith("refs/tags/") or tag.ref.endswith("^{}"):
raise ReleaseBlocked("TAG_REF_INVALID", "canonical tag ref is required")
if tag.object_type not in ("commit", "tag"):
raise ReleaseBlocked("TAG_OBJECT_TYPE_INVALID", "unsupported tag object type")
values = (tag.tag_oid, tag.peeled_target_oid, expected_tag_oid, expected_target_oid)
if any(not is_git_oid(value) for value in values):
raise ReleaseBlocked("TAG_OID_INVALID", "tag OIDs must be complete lowercase hex")
if tag.tag_oid != expected_tag_oid:
raise ReleaseBlocked("TAG_OBJECT_MISMATCH", "tag object does not match expectation")
if tag.peeled_target_oid != expected_target_oid:
raise ReleaseBlocked(
"TAG_PEELED_TARGET_MISMATCH", "peeled target does not match expected target"
)
if tag.object_type == "commit" and tag.tag_oid != tag.peeled_target_oid:
raise ReleaseBlocked(
"LIGHTWEIGHT_TAG_INCONSISTENT",
"lightweight tag object and target must be identical",
)
@@ -0,0 +1,69 @@
"""Dormant Candidate worker supervisor.
Provider clients are injected by the external management plane. This process
never converts a missing projection into a simulated native identity.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import signal
import threading
def write_status(directory: Path) -> None:
directory.mkdir(parents=True, exist_ok=True, mode=0o750)
if directory.is_symlink():
raise RuntimeError("worker state directory symlink rejected")
payload = {
"status": "dormant",
"task_plane_execution_enabled": os.environ.get(
"AIOS_TASK_PLANE_EXECUTION_ENABLED", "false"
).lower()
== "true",
"gitops_writes_enabled": os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower()
== "true",
"production_side_effects": False,
"secrets_included": False,
}
temporary = directory / f"worker-status.{os.getpid()}.new"
target = directory / "worker-status.json"
if temporary.is_symlink() or target.is_symlink():
raise RuntimeError("worker status symlink rejected")
descriptor = os.open(
temporary,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
0o640,
)
try:
os.write(descriptor, (json.dumps(payload, sort_keys=True) + "\n").encode("utf-8"))
os.fsync(descriptor)
finally:
os.close(descriptor)
os.replace(temporary, target)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--state-directory", required=True, type=Path)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if os.environ.get("AIOS_PRODUCTION_DEPLOYMENT_ENABLED", "false").lower() == "true":
raise RuntimeError("Candidate worker refuses production deployment mode")
write_status(args.state_directory)
stopping = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stopping.set())
signal.signal(signal.SIGINT, lambda *_: stopping.set())
while not stopping.wait(5):
write_status(args.state_directory)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,201 @@
"""Durable fail-closed workflow state machine."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from .errors import StateConflict
from .models import StepState, TaskPlan, TaskState, is_sha256
from .store import StateStore
@dataclass(frozen=True, slots=True)
class WorkflowDecision:
task_id: str
ready_steps: tuple[str, ...]
blocked: bool
reason: str | None = None
class WorkflowEngine:
def __init__(self, store: StateStore) -> None:
self.store = store
def _task_state(self, task_id: str, state: TaskState) -> None:
current, revision = self.store.get_task_state(task_id)
if current != state:
self.store.compare_and_set_task_state(task_id, revision, state)
def plan(self, task: TaskPlan) -> None:
self.store.create_task(
task.task_id,
task.digest(),
{
"task_id": task.task_id,
"intent": task.intent,
"branch": task.binding.branch,
"base_oid": task.binding.base_oid,
},
tuple(step.step_id for step in task.steps),
{step.step_id: step.max_attempts for step in task.steps},
)
def next_steps(self, task: TaskPlan) -> WorkflowDecision:
states = self.store.list_steps(task.task_id)
by_id = {step.step_id: step for step in task.steps}
if any(item.state == StepState.FAILED for item in states.values()):
self._task_state(task.task_id, TaskState.FAILED)
return WorkflowDecision(task.task_id, (), True, "terminal_step_failure")
ready: list[str] = []
for step_id, stored in states.items():
if stored.state not in (StepState.PENDING, StepState.READY):
continue
dependencies = by_id[step_id].depends_on
dependency_states = tuple(states[item].state for item in dependencies)
if any(item in (StepState.FAILED, StepState.BLOCKED) for item in dependency_states):
if stored.state != StepState.BLOCKED:
self.store.transition_step(
task.task_id,
step_id,
(stored.state,),
StepState.BLOCKED,
last_error="dependency_failed",
)
continue
if all(item == StepState.COMPLETED for item in dependency_states):
if stored.state == StepState.PENDING:
self.store.transition_step(
task.task_id, step_id, (StepState.PENDING,), StepState.READY
)
ready.append(step_id)
if states and all(item.state == StepState.COMPLETED for item in states.values()):
self._task_state(task.task_id, TaskState.COMPLETED)
elif ready:
self._task_state(task.task_id, TaskState.READY)
return WorkflowDecision(task.task_id, tuple(sorted(ready)), False)
def claim(self, task_id: str, step_id: str, worker_id: str, session_id: str) -> None:
if not worker_id or not session_id:
raise StateConflict("STEP_CLAIM_BINDING_REQUIRED", "worker and session are required")
self.store.transition_step(
task_id,
step_id,
(StepState.READY,),
StepState.CLAIMED,
worker_id=worker_id,
session_id=session_id,
increment_attempt=True,
)
def start(
self,
task_id: str,
step_id: str,
worker_id: str,
session_id: str,
*,
lease_seconds: int = 60,
now: datetime | None = None,
) -> None:
current = self.store.list_steps(task_id)[step_id]
if current.worker_id != worker_id or current.session_id != session_id:
raise StateConflict("STEP_CLAIM_OWNER_MISMATCH", "claim owner does not match")
now = now or datetime.now(timezone.utc)
self.store.transition_step(
task_id,
step_id,
(StepState.CLAIMED,),
StepState.RUNNING,
worker_id=worker_id,
session_id=session_id,
lease_expires_at=now + timedelta(seconds=lease_seconds),
)
self._task_state(task_id, TaskState.RUNNING)
def heartbeat(
self,
task_id: str,
step_id: str,
worker_id: str,
session_id: str,
*,
lease_seconds: int = 60,
now: datetime | None = None,
) -> None:
current = self.store.list_steps(task_id)[step_id]
if current.worker_id != worker_id or current.session_id != session_id:
raise StateConflict("STEP_CLAIM_OWNER_MISMATCH", "heartbeat owner does not match")
now = now or datetime.now(timezone.utc)
self.store.transition_step(
task_id,
step_id,
(StepState.RUNNING,),
StepState.RUNNING,
lease_expires_at=now + timedelta(seconds=lease_seconds),
)
def complete(self, task_id: str, step_id: str, *, result_digest: str) -> None:
if not is_sha256(result_digest):
raise StateConflict("RESULT_DIGEST_INVALID", "result digest must be SHA-256")
current = self.store.list_steps(task_id)[step_id]
if current.state == StepState.COMPLETED and current.result_digest == result_digest:
return
self.store.transition_step(
task_id,
step_id,
(StepState.RUNNING,),
StepState.COMPLETED,
result_digest=result_digest,
)
def fail(
self,
task: TaskPlan,
step_id: str,
reason: str,
*,
retryable: bool,
) -> StepState:
current = self.store.list_steps(task.task_id)[step_id]
next_state = (
StepState.PENDING
if retryable and current.attempt < current.max_attempts
else StepState.FAILED
)
self.store.transition_step(
task.task_id,
step_id,
(StepState.CLAIMED, StepState.RUNNING),
next_state,
last_error=reason,
clear_assignment=next_state == StepState.PENDING,
)
self._task_state(
task.task_id, TaskState.BLOCKED if next_state == StepState.PENDING else TaskState.FAILED
)
return next_state
def recover_expired(
self, task: TaskPlan, *, now: datetime | None = None
) -> tuple[str, ...]:
now = now or datetime.now(timezone.utc)
recovered: list[str] = []
for step_id, current in self.store.list_steps(task.task_id).items():
if (
current.state == StepState.RUNNING
and current.lease_expires_at is not None
and current.lease_expires_at <= now
):
self.fail(task, step_id, "lease_expired", retryable=True)
recovered.append(step_id)
return tuple(recovered)
def recoverable(self, task_id: str, step_id: str) -> None:
self.store.transition_step(
task_id,
step_id,
(StepState.BLOCKED,),
StepState.PENDING,
clear_assignment=True,
)
@@ -0,0 +1,25 @@
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "aios-autonomous-release-candidate"
version = "0.1.0"
description = "Candidate-only governed autonomous release control plane"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Proprietary" }
authors = [{ name = "AIOS" }]
dependencies = []
[project.scripts]
aios-autonomous-release = "aios_autonomous_release_candidate.cli:main"
aios-autonomous-release-service = "aios_autonomous_release_candidate.service:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["aios_autonomous_release_candidate*"]
[tool.pytest.ini_options]
addopts = "-ra --strict-config --strict-markers"
testpaths = ["../../tests/autonomous_release"]
@@ -1,51 +0,0 @@
# AIOS Single-Owner Governance Prerequisites
This package is a dormant candidate produced under the PR #29 prerequisite
authorization. It validates exact-head Owner approvals and machine gates,
models protection-policy compare-and-swap with compensation, builds evidence,
and exercises every mutation path only against disposable fixtures.
The prerequisite shadow services remain static and refuse live mutation. This
branch adds a separate, one-shot activation executor with `--plan`,
`--activate`, `--verify`, `--rollback`, and `--status` commands. It is not an
always-on writer and never enables global GitOps writes.
The executor is fail-closed: every consequential command is bound to an exact
approval merge, governance OID, evidence closure, Owner review, component
digests, full branch-protection snapshot, operation ID, and plan ID. It writes
a fsynced WAL before the single-row SQLite CAS, verifies the full applied row,
and only rolls back with a target-to-previous CAS. Audit records are
hash-chained and credentials are redacted.
## Hardening Contract
Fresh activation is authorized only before the approved `expires_at` instant.
An already-recorded `PREPARED` transaction may recover after expiry only to
reach a safe terminal state using the same exact bindings; `--verify` and
`--rollback` remain available for that recorded transaction after expiry.
`ROLLED_BACK` and `FAILED` are terminal states and must never be replayed as a
new activation.
The approved execution binding must pin the implementation commit and its
exact tree, the installed hashes of the five runtime source files
(`activation.py`, `gitea_protected_branch.py`, `journal.py`, `validator.py`,
and `state_machine.py`), and independent SHA-256 values for the deployment
artifact, rollback artifact, and test evidence. It must also bind an evidence
closure that is a commit with the exact direct parent and exactly one approved
closure file, including that file's Git blob OID and SHA-256.
Every required machine status is accepted only when its exact status record,
creator identity, producer provenance, target URL, description digest, head
OID, state, and observation time match the approved binding. A fixture status
or a dormant self-check is not machine-status producer provenance.
This contract hardens validation; it does not authorize deployment. The
existing governed approval rules still apply, and installation remains
disabled and inactive until the required merged approval authorizes one
explicit execution.
Installation must remain disabled and inactive until a merged
`SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION`
approval authorizes one explicit execution. The approved runtime paths are
`/var/lib/aios-gitops/governance-activation/` and
`/run/aios-gitops/governance-activation.lock`.
@@ -1,115 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"activation_approval_merge_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"activation_approval_pr_number": {"minimum": 1, "type": "integer"},
"activation_manifest_blob_oid": {"pattern": "^[0-9a-f]{40,64}$", "type": "string"},
"activation_manifest_path": {"pattern": "^approvals/.+\\.json$", "type": "string"},
"activation_manifest_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"artifact_path": {"pattern": "^/", "type": "string"},
"artifact_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"approval_db_path": {"minLength": 1, "type": "string"},
"approval_request_head_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"backend": {"enum": ["file", "sqlite"]},
"backend_path": {"minLength": 1, "type": "string"},
"branch": {"const": "governance/approvals"},
"evidence_closure_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"evidence_closure_path": {"pattern": "^(?!/)(?!.*\\.\\.).+", "type": "string"},
"evidence_closure_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"evidence_git_dir": {"minLength": 1, "type": "string"},
"executor": {"not": {"const": "owner"}, "type": "string"},
"expected_branch_protection_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"expected_gitea_schema_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"expected_governance_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"expected_state_machine_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"expected_validator_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"expires_at": {"format": "date-time", "type": "string"},
"forbidden_operations": {"additionalProperties": {"const": false}, "type": "object"},
"governance_git_dir": {"minLength": 1, "type": "string"},
"governance_ref": {"const": "refs/heads/governance/approvals"},
"implementation_commit_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"implementation_tree_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"machine_status_producer_id": {"minimum": 1, "type": "integer"},
"machine_status_producer_name": {"minLength": 1, "type": "string"},
"machine_checks": {
"additionalProperties": false,
"properties": {
"aios/branch-protection": {"$ref": "#/$defs/machineCheck"},
"aios/evidence-closure": {"$ref": "#/$defs/machineCheck"},
"aios/secret-scan": {"$ref": "#/$defs/machineCheck"},
"aios/state-machine": {"$ref": "#/$defs/machineCheck"},
"aios/validator": {"$ref": "#/$defs/machineCheck"}
},
"required": ["aios/branch-protection", "aios/evidence-closure", "aios/secret-scan", "aios/state-machine", "aios/validator"],
"type": "object"
},
"observed_evidence_closure_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"observed_governance_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"observed_owner_review_oid": {"type": ["integer", "string"]},
"observed_state_machine_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"observed_validator_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"operation_id": {"minLength": 1, "type": "string"},
"owner_review_oid": {"type": ["integer", "string"]},
"repo_id": {"minimum": 1, "type": "integer"},
"requester": {"not": {"const": "owner"}, "type": "string"},
"rollback_artifact_path": {"pattern": "^/", "type": "string"},
"rollback_artifact_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"runtime_sources": {
"items": {
"additionalProperties": false,
"properties": {
"git_path": {"minLength": 1, "type": "string"},
"runtime_path": {"pattern": "^/", "type": "string"},
"sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"}
},
"required": ["git_path", "runtime_path", "sha256"],
"type": "object"
},
"maxItems": 5,
"minItems": 5,
"type": "array"
},
"state_machine_path": {"minLength": 1, "type": "string"},
"target_branch_protection": {"type": "object"},
"task_id": {"minLength": 1, "type": "string"},
"test_evidence_path": {"pattern": "^/", "type": "string"},
"test_evidence_sha256": {"pattern": "^[0-9a-f]{64}$", "type": "string"},
"validator_path": {"minLength": 1, "type": "string"},
"wip": {"const": false}
},
"$defs": {
"machineCheck": {
"additionalProperties": false,
"properties": {
"head_oid": {"pattern": "^[0-9a-f]{40}$", "type": "string"},
"status_id": {"minimum": 1, "type": "integer"},
"creator_id": {"minimum": 1, "type": "integer"},
"creator_login": {"minLength": 1, "type": "string"},
"state": {"const": "success"}
},
"required": ["state", "head_oid", "status_id", "creator_id", "creator_login"],
"type": "object"
}
},
"required": [
"task_id", "operation_id", "expected_governance_oid", "observed_governance_oid",
"expected_validator_sha256", "observed_validator_sha256", "validator_path",
"expected_state_machine_sha256", "observed_state_machine_sha256", "state_machine_path",
"expected_branch_protection_sha256", "expected_gitea_schema_sha256",
"activation_approval_merge_oid", "activation_approval_pr_number", "approval_request_head_oid",
"activation_manifest_path", "activation_manifest_blob_oid", "activation_manifest_sha256",
"evidence_closure_oid", "observed_evidence_closure_oid", "evidence_git_dir",
"evidence_closure_path", "evidence_closure_sha256",
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path", "rollback_artifact_sha256",
"test_evidence_path", "test_evidence_sha256",
"machine_status_producer_id", "machine_status_producer_name",
"owner_review_oid", "observed_owner_review_oid", "approval_db_path", "expires_at",
"governance_git_dir", "governance_ref", "machine_checks", "forbidden_operations",
"target_branch_protection", "backend", "backend_path", "repo_id", "branch",
"requester", "executor", "wip"
],
"title": "AIOS Single Owner Live Activation Bindings",
"type": "object"
}
@@ -1,20 +0,0 @@
"""Dormant single-owner governance activation prerequisites."""
from .models import (
ApprovalContext,
GovernanceState,
MachineGate,
ProtectionSnapshot,
Review,
)
from .validator import SingleOwnerApprovalValidator, ValidationDenied
__all__ = [
"ApprovalContext",
"GovernanceState",
"MachineGate",
"ProtectionSnapshot",
"Review",
"SingleOwnerApprovalValidator",
"ValidationDenied",
]
@@ -1,860 +0,0 @@
from __future__ import annotations
import argparse
from contextlib import contextmanager
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import sqlite3
import subprocess
from typing import Any, Iterator, Mapping, Protocol
try:
import fcntl
except ImportError: # pragma: no cover
fcntl = None
from .gitea_protected_branch import (
CASConflict,
FileProtectedBranchAdapter,
SQLiteProtectedBranchAdapter,
calculate_snapshot_sha256,
)
from .journal import JournalStore, atomic_write_json, read_json, redact, sha256_json
from . import state_machine as state_machine_module
from . import validator as validator_module
DEFAULT_STATE_ROOT = Path("/var/lib/aios-gitops/governance-activation")
DEFAULT_LOCK_PATH = Path("/run/aios-gitops/governance-activation.lock")
LIVE_GITEA_DB = Path("/data/aios-gitops/runtime/gitea-bin/data/gitea.db")
LIVE_AUTHORITATIVE_GIT = Path("/data/aios-gitops/repo.git")
HEX40 = re.compile(r"^[0-9a-f]{40}$")
HEX64 = re.compile(r"^[0-9a-f]{64}$")
REQUIRED_BINDINGS = {
"task_id",
"operation_id",
"expected_governance_oid",
"expected_validator_sha256",
"expected_state_machine_sha256",
"expected_branch_protection_sha256",
"activation_approval_merge_oid",
"evidence_closure_oid",
"owner_review_oid",
"observed_governance_oid",
"observed_validator_sha256",
"observed_state_machine_sha256",
"observed_evidence_closure_oid",
"observed_owner_review_oid",
"approval_request_head_oid",
"activation_approval_pr_number",
"expires_at",
"validator_path",
"state_machine_path",
"governance_git_dir",
"governance_ref",
"evidence_git_dir",
"approval_db_path",
"expected_gitea_schema_sha256",
"backend",
"backend_path",
"repo_id",
"branch",
"activation_manifest_path",
"activation_manifest_blob_oid",
"activation_manifest_sha256",
"implementation_commit_oid",
"implementation_tree_oid",
"runtime_sources",
"artifact_path",
"artifact_sha256",
"rollback_artifact_path",
"rollback_artifact_sha256",
"test_evidence_path",
"test_evidence_sha256",
"evidence_closure_path",
"evidence_closure_sha256",
"machine_status_producer_id",
"machine_status_producer_name",
}
REQUIRED_MACHINE_CONTEXTS = {
"aios/validator",
"aios/state-machine",
"aios/branch-protection",
"aios/secret-scan",
"aios/evidence-closure",
}
REQUIRED_RUNTIME_GIT_PATHS = {
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
}
class InjectedCrash(BaseException):
pass
class Adapter(Protocol):
def read_current(self) -> dict[str, Any]: ...
def calculate_snapshot_sha256(self, snapshot: Mapping[str, Any]) -> str: ...
def apply_atomic(self, expected_sha256: str, target: Mapping[str, Any]) -> dict[str, Any]: ...
def verify_applied(self, expected_sha256: str) -> bool: ...
def rollback_atomic(self, expected_target_sha256: str, previous: Mapping[str, Any]) -> dict[str, Any]: ...
def verify_rollback(self, expected_sha256: str) -> bool: ...
@contextmanager
def exclusive_lock(path: Path) -> Iterator[None]:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
flags = os.O_RDWR | os.O_CREAT
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
fd = os.open(path, flags, 0o640)
try:
os.fchmod(fd, 0o640)
if fcntl is not None:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
yield
finally:
if fcntl is not None:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
def _require_hex(name: str, value: object, lengths: tuple[int, ...]) -> None:
if not isinstance(value, str) or len(value) not in lengths or not re.fullmatch(r"[0-9a-f]+", value):
raise RuntimeError(f"{name.upper()}_INVALID")
def _sha256_file(path: Path) -> str:
if not path.is_absolute() or not path.is_file() or path.is_symlink():
raise RuntimeError("VERIFIED_COMPONENT_PATH_INVALID")
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _git(git_dir: Path, *args: str) -> str:
if not git_dir.is_absolute() or not git_dir.exists() or git_dir.is_symlink():
raise RuntimeError("GIT_OBJECT_STORE_INVALID")
result = subprocess.run(
["git", f"--git-dir={git_dir}", *args],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError("GIT_EVIDENCE_VERIFICATION_FAILED")
return result.stdout.strip()
def _git_raw(git_dir: Path, *args: str) -> bytes:
if not git_dir.is_absolute() or not git_dir.exists() or git_dir.is_symlink():
raise RuntimeError("GIT_OBJECT_STORE_INVALID")
result = subprocess.run(
["git", f"--git-dir={git_dir}", *args], capture_output=True, check=False
)
if result.returncode != 0:
raise RuntimeError("GIT_EVIDENCE_VERIFICATION_FAILED")
return result.stdout
def _parse_utc(value: object) -> datetime:
if not isinstance(value, str):
raise RuntimeError("EXPIRY_INVALID")
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise RuntimeError("EXPIRY_INVALID") from exc
if parsed.tzinfo is None:
raise RuntimeError("EXPIRY_INVALID")
return parsed.astimezone(timezone.utc)
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
def _approved_execution_binding(bindings: Mapping[str, Any]) -> dict[str, Any]:
return {
"task_id": bindings["task_id"],
"operation_id": bindings["operation_id"],
"expected_validator_sha256": bindings["expected_validator_sha256"],
"expected_state_machine_sha256": bindings["expected_state_machine_sha256"],
"expected_branch_protection_sha256": bindings["expected_branch_protection_sha256"],
"expected_gitea_schema_sha256": bindings["expected_gitea_schema_sha256"],
"target_branch_protection_sha256": calculate_snapshot_sha256(
bindings["target_branch_protection"]
),
"evidence_closure_oid": bindings["evidence_closure_oid"],
"evidence_closure_path": bindings["evidence_closure_path"],
"evidence_closure_sha256": bindings["evidence_closure_sha256"],
"implementation_commit_oid": bindings["implementation_commit_oid"],
"implementation_tree_oid": bindings["implementation_tree_oid"],
"runtime_sources": bindings["runtime_sources"],
"artifact_path": bindings["artifact_path"],
"artifact_sha256": bindings["artifact_sha256"],
"rollback_artifact_path": bindings["rollback_artifact_path"],
"rollback_artifact_sha256": bindings["rollback_artifact_sha256"],
"test_evidence_path": bindings["test_evidence_path"],
"test_evidence_sha256": bindings["test_evidence_sha256"],
"machine_status_producer_id": bindings["machine_status_producer_id"],
"machine_status_producer_name": bindings["machine_status_producer_name"],
"backend": bindings["backend"],
"repo_id": bindings["repo_id"],
"branch": bindings["branch"],
"requester": bindings["requester"],
"executor": bindings["executor"],
"forbidden_operations": bindings["forbidden_operations"],
}
def _same_path(left: Path, right: Path) -> bool:
try:
return left.resolve(strict=True) == right.resolve(strict=True) and os.path.samefile(left, right)
except (FileNotFoundError, OSError):
return False
def _verify_authority_paths(bindings: Mapping[str, Any]) -> None:
if bindings["backend"] != "sqlite":
return
approval_db = Path(str(bindings["approval_db_path"]))
backend_db = Path(str(bindings["backend_path"]))
if not _same_path(approval_db, backend_db):
raise RuntimeError("APPROVAL_DB_BACKEND_DB_IDENTITY_MISMATCH")
live = _same_path(backend_db, LIVE_GITEA_DB)
if live:
if not _same_path(Path(str(bindings["governance_git_dir"])), LIVE_AUTHORITATIVE_GIT):
raise RuntimeError("AUTHORITATIVE_GOVERNANCE_GIT_REQUIRED")
if not _same_path(Path(str(bindings["evidence_git_dir"])), LIVE_AUTHORITATIVE_GIT):
raise RuntimeError("AUTHORITATIVE_EVIDENCE_GIT_REQUIRED")
return
acceptance_root = os.environ.get("AIOS_GOVERNANCE_ACTIVATION_ACCEPTANCE_ROOT")
if not acceptance_root:
raise RuntimeError("NON_LIVE_SQLITE_BACKEND_DENIED")
root = Path(acceptance_root).resolve(strict=True)
if not str(root).startswith("/run/aios-live-activation-acceptance-"):
raise RuntimeError("ACCEPTANCE_ROOT_INVALID")
for path in (approval_db, backend_db, Path(str(bindings["governance_git_dir"])), Path(str(bindings["evidence_git_dir"]))):
try:
path.resolve(strict=True).relative_to(root)
except (FileNotFoundError, ValueError) as exc:
raise RuntimeError("ACCEPTANCE_AUTHORITY_ESCAPE_DENIED") from exc
def _load_approved_manifest(bindings: Mapping[str, Any], git_dir: Path) -> dict[str, Any]:
path = str(bindings["activation_manifest_path"])
if not path.startswith("approvals/") or not path.endswith(".json") or ".." in Path(path).parts:
raise RuntimeError("ACTIVATION_MANIFEST_PATH_INVALID")
merge_oid = str(bindings["activation_approval_merge_oid"])
blob_oid = _git(git_dir, "rev-parse", f"{merge_oid}:{path}")
if blob_oid != bindings["activation_manifest_blob_oid"]:
raise RuntimeError("ACTIVATION_MANIFEST_BLOB_MISMATCH")
raw_result = subprocess.run(
["git", f"--git-dir={git_dir}", "show", f"{merge_oid}:{path}"],
capture_output=True,
check=False,
)
if raw_result.returncode != 0:
raise RuntimeError("ACTIVATION_MANIFEST_READ_FAILED")
if hashlib.sha256(raw_result.stdout).hexdigest() != bindings["activation_manifest_sha256"]:
raise RuntimeError("ACTIVATION_MANIFEST_SHA256_MISMATCH")
try:
manifest = json.loads(raw_result.stdout)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("ACTIVATION_MANIFEST_INVALID") from exc
if not isinstance(manifest, dict):
raise RuntimeError("ACTIVATION_MANIFEST_INVALID")
return manifest
def _require_approval_unexpired(bindings: Mapping[str, Any]) -> None:
if _utc_now() >= _parse_utc(bindings["expires_at"]):
raise RuntimeError("APPROVAL_EXPIRED")
def _safe_git_path(value: object) -> str:
path = str(value)
if not path or Path(path).is_absolute() or ".." in Path(path).parts:
raise RuntimeError("APPROVED_GIT_PATH_INVALID")
return path
def _verify_approved_entities(bindings: Mapping[str, Any], git_dir: Path) -> None:
implementation_oid = str(bindings["implementation_commit_oid"])
_git(git_dir, "cat-file", "-e", f"{implementation_oid}^{{commit}}")
if _git(git_dir, "rev-parse", f"{implementation_oid}^{{tree}}") != bindings["implementation_tree_oid"]:
raise RuntimeError("IMPLEMENTATION_TREE_MISMATCH")
sources = bindings["runtime_sources"]
if not isinstance(sources, list) or not sources:
raise RuntimeError("RUNTIME_SOURCES_INVALID")
seen_git: set[str] = set()
seen_runtime: set[str] = set()
loaded_runtime_paths = {
"services/aios-single-owner-governance/aios_single_owner_governance/activation.py": Path(__file__).resolve(),
"services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py": Path(__import__(SQLiteProtectedBranchAdapter.__module__, fromlist=["x"]).__file__).resolve(),
"services/aios-single-owner-governance/aios_single_owner_governance/journal.py": Path(__import__(JournalStore.__module__, fromlist=["x"]).__file__).resolve(),
"services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py": Path(state_machine_module.__file__).resolve(),
"services/aios-single-owner-governance/aios_single_owner_governance/validator.py": Path(validator_module.__file__).resolve(),
}
for source in sources:
if not isinstance(source, dict) or set(source) != {"git_path", "runtime_path", "sha256"}:
raise RuntimeError("RUNTIME_SOURCE_BINDING_INVALID")
git_path = _safe_git_path(source["git_path"])
runtime_path = str(source["runtime_path"])
_require_hex("runtime_source_sha256", source["sha256"], (64,))
if git_path in seen_git or runtime_path in seen_runtime:
raise RuntimeError("RUNTIME_SOURCE_BINDING_DUPLICATE")
seen_git.add(git_path)
seen_runtime.add(runtime_path)
if Path(runtime_path).resolve() != loaded_runtime_paths.get(git_path):
raise RuntimeError("RUNTIME_SOURCE_PATH_MISMATCH")
if hashlib.sha256(_git_raw(git_dir, "show", f"{implementation_oid}:{git_path}")).hexdigest() != source["sha256"]:
raise RuntimeError("IMPLEMENTATION_SOURCE_DIGEST_MISMATCH")
if _sha256_file(Path(runtime_path)) != source["sha256"]:
raise RuntimeError("RUNTIME_SOURCE_DIGEST_MISMATCH")
if seen_git != REQUIRED_RUNTIME_GIT_PATHS:
raise RuntimeError("RUNTIME_SOURCE_SET_MISMATCH")
for prefix in ("artifact", "rollback_artifact", "test_evidence"):
if _sha256_file(Path(str(bindings[f"{prefix}_path"]))) != bindings[f"{prefix}_sha256"]:
raise RuntimeError("APPROVED_ENTITY_DIGEST_MISMATCH")
closure_oid = str(bindings["evidence_closure_oid"])
if _git(git_dir, "cat-file", "-t", closure_oid) != "commit":
raise RuntimeError("EVIDENCE_CLOSURE_NOT_COMMIT")
parents = _git(git_dir, "rev-list", "--parents", "-n", "1", closure_oid).split()
if parents != [closure_oid, implementation_oid]:
raise RuntimeError("EVIDENCE_CLOSURE_PARENT_MISMATCH")
closure_path = _safe_git_path(bindings["evidence_closure_path"])
if hashlib.sha256(_git_raw(git_dir, "show", f"{closure_oid}:{closure_path}")).hexdigest() != bindings["evidence_closure_sha256"]:
raise RuntimeError("EVIDENCE_CLOSURE_SHA256_MISMATCH")
if _git(git_dir, "diff", "--name-status", implementation_oid, closure_oid) != f"A\t{closure_path}":
raise RuntimeError("EVIDENCE_CLOSURE_DIFF_INVALID")
def _verify_external_evidence(
bindings: Mapping[str, Any], *, require_unexpired: bool = True
) -> None:
_verify_authority_paths(bindings)
validator_sha = _sha256_file(Path(str(bindings["validator_path"])))
state_machine_sha = _sha256_file(Path(str(bindings["state_machine_path"])))
if validator_sha != bindings["expected_validator_sha256"] or validator_sha != bindings["observed_validator_sha256"]:
raise RuntimeError("VALIDATOR_DIGEST_MISMATCH")
if state_machine_sha != bindings["expected_state_machine_sha256"] or state_machine_sha != bindings["observed_state_machine_sha256"]:
raise RuntimeError("STATE_MACHINE_DIGEST_MISMATCH")
governance_git_dir = Path(str(bindings["governance_git_dir"]))
governance_oid = _git(governance_git_dir, "rev-parse", "--verify", str(bindings["governance_ref"]))
if governance_oid != bindings["expected_governance_oid"] or governance_oid != bindings["observed_governance_oid"]:
raise RuntimeError("WRONG_GOVERNANCE_OID_DENIED")
merge_oid = str(bindings["activation_approval_merge_oid"])
_git(governance_git_dir, "cat-file", "-e", f"{merge_oid}^{{commit}}")
if subprocess.run(
["git", f"--git-dir={governance_git_dir}", "merge-base", "--is-ancestor", merge_oid, governance_oid],
capture_output=True,
check=False,
).returncode != 0:
raise RuntimeError("ACTIVATION_APPROVAL_NOT_IN_GOVERNANCE_REF")
manifest = _load_approved_manifest(bindings, governance_git_dir)
if manifest.get("approval_type") != "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION":
raise RuntimeError("ACTIVATION_APPROVAL_TYPE_MISMATCH")
if manifest.get("execution_binding") != _approved_execution_binding(bindings):
raise RuntimeError("APPROVED_EXECUTION_BINDING_MISMATCH")
if manifest.get("execution_binding_sha256") != sha256_json(manifest["execution_binding"]):
raise RuntimeError("APPROVED_EXECUTION_BINDING_SHA256_MISMATCH")
if manifest.get("expires_at") != bindings["expires_at"]:
raise RuntimeError("APPROVAL_EXPIRY_BINDING_MISMATCH")
manifest_bound_fields = (
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path",
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
"evidence_closure_oid", "evidence_closure_path", "evidence_closure_sha256",
"machine_status_producer_id", "machine_status_producer_name",
)
if any(manifest.get(field) != bindings[field] for field in manifest_bound_fields):
raise RuntimeError("APPROVED_ARTIFACT_BINDING_MISMATCH")
evidence_git_dir = Path(str(bindings["evidence_git_dir"]))
evidence_oid = str(bindings["evidence_closure_oid"])
if evidence_oid != bindings["observed_evidence_closure_oid"]:
raise RuntimeError("WRONG_EVIDENCE_CLOSURE_DENIED")
_verify_approved_entities(bindings, evidence_git_dir)
db_path = Path(str(bindings["approval_db_path"]))
if not db_path.is_absolute() or not db_path.is_file() or db_path.is_symlink():
raise RuntimeError("APPROVAL_DATABASE_INVALID")
expiry = _parse_utc(bindings["expires_at"])
if require_unexpired:
_require_approval_unexpired(bindings)
with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) as connection:
connection.row_factory = sqlite3.Row
pr = connection.execute(
"SELECT pr.issue_id,pr.has_merged,pr.merged_commit_id,pr.merged_unix,i.is_closed "
"FROM pull_request pr JOIN issue i ON i.id=pr.issue_id "
"WHERE i.repo_id=? AND pr.\"index\"=? AND pr.base_branch=?",
(int(bindings["repo_id"]), int(bindings["activation_approval_pr_number"]), str(bindings["branch"])),
).fetchone()
if pr is None or int(pr["has_merged"]) != 1 or int(pr["is_closed"]) != 1:
raise RuntimeError("ACTIVATION_APPROVAL_PR_NOT_MERGED")
if pr["merged_commit_id"] != bindings["activation_approval_merge_oid"]:
raise RuntimeError("ACTIVATION_APPROVAL_MERGE_MISMATCH")
if int(pr["merged_unix"] or 0) <= 0 or datetime.fromtimestamp(int(pr["merged_unix"]), timezone.utc) > expiry:
raise RuntimeError("ACTIVATION_APPROVAL_MERGED_AFTER_EXPIRY")
review = connection.execute(
"SELECT r.id,r.type,r.reviewer_id,r.commit_id,r.stale,r.dismissed,r.created_unix,u.lower_name "
"FROM review r JOIN user u ON u.id=r.reviewer_id WHERE r.id=? AND r.issue_id=?",
(int(bindings["owner_review_oid"]), int(pr["issue_id"])),
).fetchone()
if review is None or int(review["id"]) != int(bindings["observed_owner_review_oid"]):
raise RuntimeError("WRONG_APPROVAL_OID_DENIED")
if (
int(review["type"]) != 1
or int(review["reviewer_id"]) != 2
or review["lower_name"] != "owner"
or review["commit_id"] != bindings["approval_request_head_oid"]
or int(review["stale"]) != 0
or int(review["dismissed"]) != 0
or datetime.fromtimestamp(int(review["created_unix"]), timezone.utc) >= expiry
):
raise RuntimeError("OWNER_APPROVAL_INVALID")
statuses = connection.execute(
"SELECT cs.id,cs.context,cs.state,cs.sha,cs.creator_id,u.lower_name AS creator_login "
"FROM commit_status cs LEFT JOIN user u ON u.id=cs.creator_id "
"WHERE cs.repo_id=? AND cs.sha=? ORDER BY cs.id DESC",
(int(bindings["repo_id"]), str(bindings["approval_request_head_oid"])),
).fetchall()
observed: dict[str, sqlite3.Row] = {}
for row in statuses:
if row["context"] in REQUIRED_MACHINE_CONTEXTS:
observed.setdefault(row["context"], row)
if set(observed) != REQUIRED_MACHINE_CONTEXTS:
raise RuntimeError("MACHINE_GATE_SET_MISMATCH")
for context, row in observed.items():
if row["state"] != "success" or row["sha"] != bindings["approval_request_head_oid"]:
raise RuntimeError("MACHINE_GATE_FAILED")
if (
int(row["creator_id"] or 0) != int(bindings["machine_status_producer_id"])
or row["creator_login"] != bindings["machine_status_producer_name"]
):
raise RuntimeError("MACHINE_GATE_PRODUCER_MISMATCH")
expected = bindings["machine_checks"][context]
actual = {
"state": row["state"], "head_oid": row["sha"], "status_id": int(row["id"]),
"creator_id": int(row["creator_id"]), "creator_login": row["creator_login"],
}
if expected != actual:
raise RuntimeError("MACHINE_GATE_BINDING_MISMATCH")
def validate_bindings(
bindings: Mapping[str, Any], *, require_unexpired: bool = True
) -> None:
missing = sorted(REQUIRED_BINDINGS - set(bindings))
if missing:
raise RuntimeError("MISSING_BINDINGS:" + ",".join(missing))
for name in (
"expected_governance_oid",
"activation_approval_merge_oid",
"evidence_closure_oid",
"implementation_commit_oid",
"implementation_tree_oid",
):
_require_hex(name, bindings[name], (40,))
for name in (
"expected_validator_sha256",
"expected_state_machine_sha256",
"expected_branch_protection_sha256",
"observed_validator_sha256",
"observed_state_machine_sha256",
"expected_gitea_schema_sha256",
"activation_manifest_sha256",
"artifact_sha256",
"rollback_artifact_sha256",
"test_evidence_sha256",
"evidence_closure_sha256",
):
_require_hex(name, bindings[name], (64,))
_require_hex("activation_manifest_blob_oid", bindings["activation_manifest_blob_oid"], (40, 64))
if not isinstance(bindings["owner_review_oid"], (int, str)) or str(bindings["owner_review_oid"]).strip() == "":
raise RuntimeError("OWNER_REVIEW_OID_INVALID")
if bindings["governance_ref"] != "refs/heads/governance/approvals":
raise RuntimeError("GOVERNANCE_REF_INVALID")
if bindings["branch"] != "governance/approvals":
raise RuntimeError("BRANCH_BINDING_DENIED")
if bindings["backend"] not in {"file", "sqlite"}:
raise RuntimeError("UNKNOWN_PROTECTION_BACKEND")
if bindings["observed_governance_oid"] != bindings["expected_governance_oid"]:
raise RuntimeError("WRONG_GOVERNANCE_OID_DENIED")
if bindings["observed_evidence_closure_oid"] != bindings["evidence_closure_oid"]:
raise RuntimeError("WRONG_EVIDENCE_CLOSURE_DENIED")
if str(bindings["observed_owner_review_oid"]) != str(bindings["owner_review_oid"]):
raise RuntimeError("WRONG_APPROVAL_OID_DENIED")
if bindings.get("requester") == "owner":
raise RuntimeError("REQUESTER_SELF_APPROVAL_DENIED")
if bindings.get("executor") == "owner":
raise RuntimeError("EXECUTOR_SELF_APPROVAL_DENIED")
if bindings.get("wip") is not False:
raise RuntimeError("WIP_BLOCKED")
checks = bindings.get("machine_checks")
if not isinstance(checks, dict) or set(checks) != REQUIRED_MACHINE_CONTEXTS:
raise RuntimeError("MACHINE_GATE_SET_MISMATCH")
if not isinstance(bindings["machine_status_producer_id"], int) or bindings["machine_status_producer_id"] < 1:
raise RuntimeError("MACHINE_STATUS_PRODUCER_INVALID")
if not isinstance(bindings["machine_status_producer_name"], str) or not bindings["machine_status_producer_name"]:
raise RuntimeError("MACHINE_STATUS_PRODUCER_INVALID")
for context, check in checks.items():
if (
not isinstance(check, dict)
or set(check) != {"state", "head_oid", "status_id", "creator_id", "creator_login"}
or check.get("state") != "success"
or check.get("head_oid") != bindings["approval_request_head_oid"]
or not isinstance(check.get("status_id"), int)
or check.get("status_id") < 1
or check.get("creator_id") != bindings["machine_status_producer_id"]
or check.get("creator_login") != bindings["machine_status_producer_name"]
):
raise RuntimeError(f"MACHINE_GATE_FAILED:{context}")
forbidden = bindings.get("forbidden_operations", {})
if any(bool(value) for value in forbidden.values()):
raise RuntimeError("FORBIDDEN_OPERATION_REQUESTED")
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
raise RuntimeError("GLOBAL_GITOPS_WRITES_MUST_REMAIN_FALSE")
if not isinstance(bindings.get("target_branch_protection"), dict):
raise RuntimeError("TARGET_BRANCH_PROTECTION_REQUIRED")
_verify_external_evidence(bindings, require_unexpired=require_unexpired)
def binding_payload_hash(bindings: Mapping[str, Any]) -> str:
stable = dict(bindings)
stable.pop("crash_at", None)
return sha256_json(redact(stable))
def plan_id(bindings: Mapping[str, Any], previous: Mapping[str, Any], target: Mapping[str, Any]) -> str:
stable = dict(bindings)
stable.pop("crash_at", None)
return sha256_json(
{
"bindings": redact(stable),
"previous": dict(previous),
"target": dict(target),
}
)
class ActivationEngine:
def __init__(self, adapter: Adapter, state_root: Path, lock_path: Path) -> None:
self.adapter = adapter
self.journal = JournalStore(state_root)
self.lock_path = lock_path
def plan(self, bindings: Mapping[str, Any]) -> dict[str, Any]:
validate_bindings(bindings)
previous = self.adapter.read_current()
previous_sha = calculate_snapshot_sha256(previous)
if previous_sha != bindings["expected_branch_protection_sha256"]:
raise CASConflict("EXPECTED_BRANCH_PROTECTION_SHA256_MISMATCH")
target = dict(bindings["target_branch_protection"])
# Validate without writing by using the public target validator through a
# disposable comparison: adapters enforce the same checks again in CAS.
from .gitea_protected_branch import validate_target
validate_target(previous, target)
result = {
"status": "PLAN_READY",
"task_id": bindings["task_id"],
"operation_id": bindings["operation_id"],
"plan_id": plan_id(bindings, previous, target),
"payload_hash": binding_payload_hash(bindings),
"previous_sha256": previous_sha,
"target_sha256": calculate_snapshot_sha256(target),
}
return result
def _recover(self, record: dict[str, Any]) -> dict[str, Any]:
current = self.adapter.read_current()
current_sha = calculate_snapshot_sha256(current)
previous_sha = record["previous_sha256"]
target_sha = record["target_sha256"]
if record["phase"] == "PREPARED":
if current_sha == previous_sha:
return {**record, "recovery": "RESUME_BEFORE_APPLY"}
if current_sha == target_sha:
record = {**record, "phase": "APPLIED", "recovery": "RESUME_AFTER_APPLY"}
self.journal.write(record["operation_id"], record)
return record
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
if record["phase"] in {"APPLIED", "COMMITTED"} and current_sha not in {target_sha, previous_sha}:
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
return record
def _ensure_closure(self, record: dict[str, Any], event: str) -> None:
expected_sha = (
record["target_sha256"] if record["phase"] == "COMMITTED" else record["previous_sha256"]
)
if calculate_snapshot_sha256(self.adapter.read_current()) != expected_sha:
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
atomic_write_json(
self.journal.root / "current-snapshot.json",
record["target"] if record["phase"] == "COMMITTED" else record["previous"],
)
atomic_write_json(self.journal.root / "rollback-snapshot.json", record["previous"])
self.journal.write_receipt(record["operation_id"], record)
if not self.journal.audit.has_event(record["operation_id"], event):
payload = {"event": event}
if event == "SINGLE_OWNER_ACTIVATION_COMMITTED":
payload.update(
payload_hash=record["payload_hash"],
target_sha256=record["target_sha256"],
)
self.journal.audit.append(record["operation_id"], payload)
atomic_write_json(self.journal.root / "state.json", record)
def activate(self, bindings: Mapping[str, Any], supplied_plan_id: str) -> dict[str, Any]:
with exclusive_lock(self.lock_path):
self.journal.audit.verify()
operation_id = str(bindings["operation_id"])
payload_hash = binding_payload_hash(bindings)
record = self.journal.load(operation_id)
if record:
if record.get("payload_hash") != payload_hash:
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
if record.get("plan_id") != supplied_plan_id:
raise RuntimeError("PLAN_ID_MISMATCH")
if record.get("phase") == "ROLLED_BACK":
raise RuntimeError("OPERATION_TERMINAL_ROLLED_BACK")
if record.get("phase") == "FAILED":
raise RuntimeError("OPERATION_TERMINAL_FAILED")
prepared_at = _parse_utc(record.get("prepared_at"))
if prepared_at >= _parse_utc(bindings["expires_at"]):
raise RuntimeError("RECOVERY_AUTHORIZATION_INVALID")
record = self._recover(record)
if record["phase"] == "COMMITTED":
self._ensure_closure(record, "SINGLE_OWNER_ACTIVATION_COMMITTED")
return {"status": "ALREADY_COMMITTED", **record}
else:
validate_bindings(bindings)
planned = self.plan(bindings)
if supplied_plan_id != planned["plan_id"]:
raise RuntimeError("PLAN_ID_MISMATCH")
previous = self.adapter.read_current()
target = dict(bindings["target_branch_protection"])
if record is None:
if calculate_snapshot_sha256(previous) != planned["previous_sha256"]:
raise CASConflict("PREPARED_BOUNDARY_SNAPSHOT_CHANGED")
prepared_at = _utc_now()
if prepared_at >= _parse_utc(bindings["expires_at"]):
raise RuntimeError("APPROVAL_EXPIRED")
record = {
"task_id": bindings["task_id"],
"operation_id": operation_id,
"transaction_id": operation_id,
"payload_hash": payload_hash,
"plan_id": supplied_plan_id,
"phase": "PREPARED",
"prepared_at": prepared_at.isoformat().replace("+00:00", "Z"),
"previous": previous,
"target": target,
"previous_sha256": calculate_snapshot_sha256(previous),
"target_sha256": calculate_snapshot_sha256(target),
"activation_approval_merge_oid": bindings["activation_approval_merge_oid"],
"evidence_closure_oid": bindings["evidence_closure_oid"],
"owner_review_oid": bindings["owner_review_oid"],
}
self.journal.write(operation_id, record)
atomic_write_json(self.journal.root / "current-snapshot.json", previous)
atomic_write_json(self.journal.root / "rollback-snapshot.json", previous)
crash_at = bindings.get("crash_at")
if crash_at == "before_apply":
raise InjectedCrash("before_apply")
try:
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
if current_sha == record["previous_sha256"]:
applied = self.adapter.apply_atomic(record["previous_sha256"], record["target"])
record["target"] = applied
record["target_sha256"] = calculate_snapshot_sha256(applied)
elif current_sha != record["target_sha256"]:
raise CASConflict("CAS_CONFLICT")
if crash_at == "during_apply":
raise InjectedCrash("during_apply")
record["phase"] = "APPLIED"
self.journal.write(operation_id, record)
if crash_at == "after_apply_before_receipt":
raise InjectedCrash("after_apply_before_receipt")
if not self.adapter.verify_applied(record["target_sha256"]):
raise RuntimeError("POST_ACTIVATION_VERIFY_FAILED")
record["phase"] = "COMMITTED"
record["single_owner_policy_active"] = True
record["break_glass_active"] = False
self.journal.write(operation_id, record)
if crash_at == "after_commit_wal":
raise InjectedCrash("after_commit_wal")
self._ensure_closure(record, "SINGLE_OWNER_ACTIVATION_COMMITTED")
return {"status": "ACTIVATED", **record}
except InjectedCrash:
raise
except Exception as exc:
failure = {**record, "phase": "FAILED", "error": type(exc).__name__}
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
if current_sha == record["target_sha256"]:
self.adapter.rollback_atomic(record["target_sha256"], record["previous"])
if not self.adapter.verify_rollback(record["previous_sha256"]):
raise RuntimeError("ROLLBACK_VERIFY_FAILED") from exc
failure["phase"] = "ROLLED_BACK"
self.journal.write(operation_id, failure)
self.journal.write_receipt(operation_id, failure)
self.journal.audit.append(operation_id, {"event": failure["phase"], "error": type(exc).__name__})
raise
def verify(self, bindings: Mapping[str, Any], operation_id: str) -> dict[str, Any]:
record = self.journal.load(operation_id)
if not record:
raise RuntimeError("TRANSACTION_NOT_FOUND")
if record.get("payload_hash") != binding_payload_hash(bindings):
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
self.journal.audit.verify()
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
passed = record.get("phase") == "COMMITTED" and current_sha == record.get("target_sha256")
if not passed:
raise RuntimeError("ACTIVATION_VERIFY_FAILED")
return {"status": "VERIFIED", "operation_id": operation_id, "target_sha256": current_sha}
def rollback(
self, bindings: Mapping[str, Any], operation_id: str, supplied_plan_id: str
) -> dict[str, Any]:
with exclusive_lock(self.lock_path):
self.journal.audit.verify()
record = self.journal.load(operation_id)
if not record:
raise RuntimeError("TRANSACTION_NOT_FOUND")
if record.get("payload_hash") != binding_payload_hash(bindings):
raise RuntimeError("OPERATION_ID_PAYLOAD_MISMATCH_DENIED")
if record.get("plan_id") != supplied_plan_id:
raise RuntimeError("PLAN_ID_MISMATCH")
for field in (
"activation_approval_merge_oid",
"evidence_closure_oid",
"owner_review_oid",
):
if str(record.get(field)) != str(bindings[field]):
raise RuntimeError("ROLLBACK_AUTHORIZATION_MISMATCH_DENIED")
current_sha = calculate_snapshot_sha256(self.adapter.read_current())
if current_sha == record["previous_sha256"]:
record["phase"] = "ROLLED_BACK"
record["single_owner_policy_active"] = False
self.journal.write(operation_id, record)
self._ensure_closure(record, "ACTIVATION_ROLLED_BACK")
return {"status": "ALREADY_ROLLED_BACK", "operation_id": operation_id}
if current_sha != record["target_sha256"]:
raise RuntimeError("MANUAL_INTERVENTION_REQUIRED")
self.adapter.rollback_atomic(record["target_sha256"], record["previous"])
if not self.adapter.verify_rollback(record["previous_sha256"]):
raise RuntimeError("ROLLBACK_VERIFY_FAILED")
if bindings.get("crash_at") == "after_rollback_cas":
raise InjectedCrash("after_rollback_cas")
record["phase"] = "ROLLED_BACK"
record["single_owner_policy_active"] = False
self.journal.write(operation_id, record)
if bindings.get("crash_at") == "after_rollback_wal":
raise InjectedCrash("after_rollback_wal")
self._ensure_closure(record, "ACTIVATION_ROLLED_BACK")
return {"status": "ROLLED_BACK", "operation_id": operation_id}
def status(self, operation_id: str | None = None) -> dict[str, Any]:
audit_head = self.journal.audit.verify()
if operation_id:
record = self.journal.load(operation_id)
if not record:
raise RuntimeError("TRANSACTION_NOT_FOUND")
return {"status": "OK", "audit_head": audit_head, "transaction": record}
state_path = self.journal.root / "state.json"
return {
"status": "OK",
"audit_head": audit_head,
"state": read_json(state_path) if state_path.exists() else None,
"transactions": list(self.journal.iter_transactions()),
}
def load_bindings(path: Path) -> dict[str, Any]:
return read_json(path)
def adapter_from_bindings(bindings: Mapping[str, Any]) -> Adapter:
backend = bindings.get("backend", "file")
backend_path = Path(str(bindings.get("backend_path", "")))
if not backend_path.is_absolute():
raise RuntimeError("BACKEND_PATH_MUST_BE_ABSOLUTE")
if backend == "file":
return FileProtectedBranchAdapter(backend_path)
if backend == "sqlite":
return SQLiteProtectedBranchAdapter(
backend_path,
repo_id=int(bindings.get("repo_id", 1)),
branch=str(bindings.get("branch", "governance/approvals")),
expected_schema_sha256=str(bindings["expected_gitea_schema_sha256"]),
)
raise RuntimeError("UNKNOWN_PROTECTION_BACKEND")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
action = parser.add_mutually_exclusive_group(required=True)
for name in ("plan", "activate", "verify", "rollback", "status"):
action.add_argument(f"--{name}", action="store_true")
parser.add_argument("--bindings", type=Path)
parser.add_argument("--plan-id")
parser.add_argument("--transaction")
parser.add_argument("--state-root", type=Path, default=DEFAULT_STATE_ROOT)
parser.add_argument("--lock-path", type=Path, default=DEFAULT_LOCK_PATH)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.status and args.bindings is None:
# Status without bindings is intentionally limited to durable local state.
journal = JournalStore(args.state_root)
result = {
"status": "OK",
"audit_head": journal.audit.verify(),
"transactions": list(journal.iter_transactions()),
}
print(json.dumps(result, sort_keys=True))
return 0
if args.bindings is None:
raise SystemExit("--bindings is required")
bindings = load_bindings(args.bindings)
engine = ActivationEngine(adapter_from_bindings(bindings), args.state_root, args.lock_path)
if args.plan:
result = engine.plan(bindings)
elif args.activate:
if not args.plan_id:
raise SystemExit("--plan-id is required")
result = engine.activate(bindings, args.plan_id)
elif args.verify:
result = engine.verify(bindings, args.transaction or str(bindings["operation_id"]))
elif args.rollback:
if not args.plan_id:
raise SystemExit("--plan-id is required")
result = engine.rollback(
bindings, args.transaction or str(bindings["operation_id"]), args.plan_id
)
else:
result = engine.status(args.transaction)
print(json.dumps(redact(result), sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,61 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
from typing import Mapping
def canonical_json(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
@dataclass(frozen=True)
class EvidenceClosure:
evidence: Mapping[str, str]
closure_sha256: str
class ActivationEvidenceClosureBuilder:
def build(self, evidence: Mapping[str, str]) -> EvidenceClosure:
required = {
"activation_prerequisites_commit_oid",
"activation_prerequisites_tree_oid",
"artifact_sha256",
"rollback_artifact_sha256",
"validator_sha256",
"state_machine_sha256",
"branch_protection_adapter_sha256",
"rollback_configuration_sha256",
"test_evidence_sha256",
"deployment_manifest_sha256",
"dormant_unit_manifest_sha256",
"dormant_deployment_evidence_sha256",
}
if set(evidence) != required:
raise ValueError("evidence set mismatch")
for value in evidence.values():
if len(value) != 40 and len(value) != 64:
raise ValueError("invalid evidence digest")
return EvidenceClosure(
dict(evidence), hashlib.sha256(canonical_json(evidence)).hexdigest()
)
class ApprovalMirrorStatusVerifier:
def verify(
self,
expected_oid: str,
*,
gitea_oid: str,
quarantine_oid: str,
authoritative_oid: str,
controller_visible_oid: str,
fsck_connectivity: str,
) -> bool:
return (
len(expected_oid) == 40
and {gitea_oid, quarantine_oid, authoritative_oid, controller_visible_oid}
== {expected_oid}
and fsck_connectivity == "PASS"
)
@@ -1,26 +0,0 @@
from __future__ import annotations
from contextlib import AbstractContextManager
from pathlib import Path
import shutil
import tempfile
from .models import ProtectionSnapshot
from .protection import MemoryProtectionStore
class DisposableGiteaFixture(AbstractContextManager):
def __init__(self, initial: ProtectionSnapshot) -> None:
self.root = Path(tempfile.mkdtemp(prefix="aios-governance-fixture-"))
self.protection = MemoryProtectionStore(initial)
self.destroyed = False
def __enter__(self) -> "DisposableGiteaFixture":
return self
def __exit__(self, exc_type, exc, tb) -> None:
resolved = self.root.resolve()
if not resolved.name.startswith("aios-governance-fixture-"):
raise RuntimeError("fixture cleanup path guard failed")
shutil.rmtree(resolved)
self.destroyed = True
@@ -1,260 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
import sqlite3
import time
from typing import Any, Mapping
from .journal import atomic_write_json, read_json, sha256_json
class CASConflict(RuntimeError):
pass
class UnsafePolicy(RuntimeError):
pass
def calculate_snapshot_sha256(snapshot: Mapping[str, Any]) -> str:
return sha256_json(dict(snapshot))
def validate_target(previous: Mapping[str, Any], target: Mapping[str, Any]) -> None:
if set(previous) != set(target):
raise UnsafePolicy("PROTECTION_ROW_SHAPE_CHANGED")
for field in ("id", "repo_id", "branch_name"):
if previous.get(field) != target.get(field):
raise UnsafePolicy(f"PROTECTION_IDENTITY_CHANGED:{field}")
if target.get("branch_name") != "governance/approvals":
raise UnsafePolicy("BRANCH_BINDING_DENIED")
if int(target.get("can_push", 1)) != 0:
raise UnsafePolicy("DIRECT_PUSH_MUST_REMAIN_DISABLED")
if int(target.get("required_approvals", 0)) != 1:
raise UnsafePolicy("EXACTLY_ONE_APPROVAL_REQUIRED")
if int(target.get("enable_approvals_whitelist", 0)) != 1:
raise UnsafePolicy("APPROVAL_WHITELIST_MUST_REMAIN_ENABLED")
if json.loads(target.get("approvals_whitelist_user_i_ds", "[]")) != [2]:
raise UnsafePolicy("OWNER_APPROVAL_WHITELIST_REQUIRED")
if int(target.get("enable_merge_whitelist", 0)) != 1:
raise UnsafePolicy("MERGE_WHITELIST_MUST_REMAIN_ENABLED")
if target.get("merge_whitelist_user_i_ds") != previous.get("merge_whitelist_user_i_ds"):
raise UnsafePolicy("MERGE_EXECUTOR_WHITELIST_CHANGE_DENIED")
if int(target.get("dismiss_stale_approvals", 0)) != 1:
raise UnsafePolicy("DISMISS_STALE_APPROVALS_REQUIRED")
if int(target.get("enable_status_check", 0)) != 1:
raise UnsafePolicy("STATUS_CHECKS_REQUIRED")
required_contexts = {
"aios/validator",
"aios/state-machine",
"aios/branch-protection",
"aios/secret-scan",
"aios/evidence-closure",
}
try:
contexts = set(json.loads(target.get("status_check_contexts", "[]")))
except (TypeError, ValueError) as exc:
raise UnsafePolicy("STATUS_CHECK_CONTEXTS_INVALID") from exc
if contexts != required_contexts:
raise UnsafePolicy("STATUS_CHECK_CONTEXTS_MISMATCH")
if int(target.get("block_on_rejected_reviews", 0)) != 1:
raise UnsafePolicy("REJECTED_REVIEWS_MUST_BLOCK")
if int(target.get("block_on_outdated_branch", 0)) != 1:
raise UnsafePolicy("OUTDATED_BRANCH_MUST_BLOCK")
if target.get("require_signed_commits") != previous.get("require_signed_commits"):
raise UnsafePolicy("SIGNATURE_POLICY_CHANGE_DENIED")
class FileProtectedBranchAdapter:
def __init__(self, path: Path) -> None:
self.path = path
@staticmethod
def calculate_snapshot_sha256(snapshot: Mapping[str, Any]) -> str:
return calculate_snapshot_sha256(snapshot)
def read_current(self) -> dict[str, Any]:
return read_json(self.path)
def compare_expected(self, expected_sha256: str) -> bool:
return calculate_snapshot_sha256(self.read_current()) == expected_sha256
def apply_atomic(
self, expected_sha256: str, target: Mapping[str, Any]
) -> dict[str, Any]:
previous = self.read_current()
if calculate_snapshot_sha256(previous) != expected_sha256:
raise CASConflict("CAS_CONFLICT")
validate_target(previous, target)
atomic_write_json(self.path, dict(target))
applied = self.read_current()
if calculate_snapshot_sha256(applied) != calculate_snapshot_sha256(target):
raise CASConflict("CAS_VERIFY_FAILED")
return applied
def verify_applied(self, expected_sha256: str) -> bool:
return self.compare_expected(expected_sha256)
def rollback_atomic(
self, expected_target_sha256: str, previous: Mapping[str, Any]
) -> dict[str, Any]:
current = self.read_current()
if calculate_snapshot_sha256(current) != expected_target_sha256:
raise CASConflict("ROLLBACK_CONFLICT")
atomic_write_json(self.path, dict(previous))
restored = self.read_current()
if calculate_snapshot_sha256(restored) != calculate_snapshot_sha256(previous):
raise CASConflict("ROLLBACK_VERIFY_FAILED")
return restored
def verify_rollback(self, expected_sha256: str) -> bool:
return self.compare_expected(expected_sha256)
class SQLiteProtectedBranchAdapter:
ALLOWED_UPDATE_FIELDS = (
"can_push",
"enable_merge_whitelist",
"merge_whitelist_user_i_ds",
"enable_status_check",
"status_check_contexts",
"enable_approvals_whitelist",
"approvals_whitelist_user_i_ds",
"required_approvals",
"block_on_rejected_reviews",
"block_on_official_review_requests",
"block_on_outdated_branch",
"dismiss_stale_approvals",
"ignore_stale_approvals",
"updated_unix",
)
def __init__(
self,
db_path: Path,
*,
repo_id: int = 1,
branch: str = "governance/approvals",
expected_schema_sha256: str,
) -> None:
self.db_path = db_path
self.repo_id = repo_id
self.branch = branch
self.expected_schema_sha256 = expected_schema_sha256
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.db_path, timeout=10)
connection.row_factory = sqlite3.Row
return connection
def schema_sha256(self, connection: sqlite3.Connection | None = None) -> str:
own = connection is None
connection = connection or self._connect()
try:
rows = [dict(row) for row in connection.execute("PRAGMA table_info(protected_branch)")]
return sha256_json(rows)
finally:
if own:
connection.close()
def _check_schema(self, connection: sqlite3.Connection) -> None:
if not isinstance(self.expected_schema_sha256, str) or len(self.expected_schema_sha256) != 64:
raise RuntimeError("GITEA_SCHEMA_FINGERPRINT_REQUIRED")
if self.schema_sha256(connection) != self.expected_schema_sha256:
raise RuntimeError("GITEA_SCHEMA_FINGERPRINT_MISMATCH")
def _read(self, connection: sqlite3.Connection) -> dict[str, Any]:
row = connection.execute(
"SELECT * FROM protected_branch WHERE repo_id=? AND branch_name=?",
(self.repo_id, self.branch),
).fetchone()
if row is None:
raise RuntimeError("PROTECTED_BRANCH_ROW_NOT_FOUND")
return dict(row)
def read_current(self) -> dict[str, Any]:
with self._connect() as connection:
self._check_schema(connection)
return self._read(connection)
@staticmethod
def calculate_snapshot_sha256(snapshot: Mapping[str, Any]) -> str:
return calculate_snapshot_sha256(snapshot)
def compare_expected(self, expected_sha256: str) -> bool:
return calculate_snapshot_sha256(self.read_current()) == expected_sha256
def apply_atomic(
self, expected_sha256: str, target: Mapping[str, Any]
) -> dict[str, Any]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
self._check_schema(connection)
previous = self._read(connection)
if calculate_snapshot_sha256(previous) != expected_sha256:
raise CASConflict("CAS_CONFLICT")
validate_target(previous, target)
values = dict(target)
values["updated_unix"] = int(values.get("updated_unix") or time.time())
assignments = ",".join(f"{field}=?" for field in self.ALLOWED_UPDATE_FIELDS)
parameters = [values[field] for field in self.ALLOWED_UPDATE_FIELDS]
parameters.extend((self.repo_id, self.branch))
cursor = connection.execute(
f"UPDATE protected_branch SET {assignments} WHERE repo_id=? AND branch_name=?",
parameters,
)
if cursor.rowcount != 1:
raise CASConflict("CAS_ROWCOUNT_INVALID")
applied = self._read(connection)
expected_target = dict(target)
expected_target["updated_unix"] = values["updated_unix"]
if calculate_snapshot_sha256(applied) != calculate_snapshot_sha256(expected_target):
raise CASConflict("CAS_VERIFY_FAILED")
connection.commit()
return applied
except Exception:
connection.rollback()
raise
finally:
connection.close()
def verify_applied(self, expected_sha256: str) -> bool:
return self.compare_expected(expected_sha256)
def rollback_atomic(
self, expected_target_sha256: str, previous: Mapping[str, Any]
) -> dict[str, Any]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
self._check_schema(connection)
current = self._read(connection)
if calculate_snapshot_sha256(current) != expected_target_sha256:
raise CASConflict("ROLLBACK_CONFLICT")
for field in ("id", "repo_id", "branch_name"):
if current.get(field) != previous.get(field):
raise CASConflict("ROLLBACK_IDENTITY_MISMATCH")
assignments = ",".join(f"{field}=?" for field in self.ALLOWED_UPDATE_FIELDS)
parameters = [previous[field] for field in self.ALLOWED_UPDATE_FIELDS]
parameters.extend((self.repo_id, self.branch))
cursor = connection.execute(
f"UPDATE protected_branch SET {assignments} WHERE repo_id=? AND branch_name=?",
parameters,
)
if cursor.rowcount != 1:
raise CASConflict("ROLLBACK_ROWCOUNT_INVALID")
restored = self._read(connection)
if calculate_snapshot_sha256(restored) != calculate_snapshot_sha256(previous):
raise CASConflict("ROLLBACK_VERIFY_FAILED")
connection.commit()
return restored
except Exception:
connection.rollback()
raise
finally:
connection.close()
def verify_rollback(self, expected_sha256: str) -> bool:
return self.compare_expected(expected_sha256)
@@ -1,189 +0,0 @@
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import tempfile
from typing import Any, Iterable
SENSITIVE_FRAGMENTS = (
"authorization",
"cookie",
"credential",
"password",
"private_key",
"secret",
"token",
)
def canonical_json(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
def sha256_json(value: object) -> str:
return hashlib.sha256(canonical_json(value)).hexdigest()
def redact(value: Any) -> Any:
if isinstance(value, dict):
return {
str(key): (
"[REDACTED]"
if any(fragment in str(key).lower() for fragment in SENSITIVE_FRAGMENTS)
else redact(item)
)
for key, item in value.items()
}
if isinstance(value, list):
return [redact(item) for item in value]
if isinstance(value, tuple):
return [redact(item) for item in value]
return value
def ensure_state_directory(path: Path) -> None:
if path.is_symlink():
raise RuntimeError("STATE_DIRECTORY_SYMLINK_DENIED")
path.mkdir(parents=True, exist_ok=True, mode=0o750)
path.chmod(0o750)
mode = path.stat().st_mode & 0o777
if mode != 0o750:
raise RuntimeError("STATE_DIRECTORY_MODE_INVALID")
def atomic_write_json(path: Path, value: object) -> None:
ensure_state_directory(path.parent)
raw = json.dumps(redact(value), sort_keys=True, indent=2).encode("utf-8") + b"\n"
with tempfile.NamedTemporaryFile(
dir=path.parent, prefix=f".{path.name}.", delete=False
) as handle:
temporary = Path(handle.name)
handle.write(raw)
handle.flush()
os.fsync(handle.fileno())
temporary.chmod(0o640)
os.replace(temporary, path)
path.chmod(0o640)
directory_fd = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
def read_json(path: Path) -> dict[str, Any]:
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise RuntimeError("STATE_DOCUMENT_NOT_OBJECT")
return raw
class AuditChain:
def __init__(self, path: Path) -> None:
self.path = path
def _records(self) -> list[dict[str, Any]]:
if not self.path.exists():
return []
records: list[dict[str, Any]] = []
for line in self.path.read_text(encoding="utf-8").splitlines():
if line.strip():
record = json.loads(line)
if not isinstance(record, dict):
raise RuntimeError("AUDIT_RECORD_INVALID")
records.append(record)
return records
def verify(self) -> str:
previous = "0" * 64
for index, record in enumerate(self._records(), 1):
payload = record.get("payload")
if record.get("sequence") != index:
raise RuntimeError("AUDIT_SEQUENCE_INVALID")
if record.get("previous_event_hash") != previous:
raise RuntimeError("AUDIT_PREVIOUS_HASH_INVALID")
payload_hash = sha256_json(payload)
if record.get("payload_hash") != payload_hash:
raise RuntimeError("AUDIT_PAYLOAD_HASH_INVALID")
material = {
"sequence": index,
"previous_event_hash": previous,
"payload_hash": payload_hash,
"transaction_id": record.get("transaction_id"),
}
event_hash = sha256_json(material)
if record.get("event_hash") != event_hash:
raise RuntimeError("AUDIT_EVENT_HASH_INVALID")
previous = event_hash
return previous
def append(self, transaction_id: str, payload: dict[str, Any]) -> dict[str, Any]:
ensure_state_directory(self.path.parent)
previous = self.verify()
sequence = len(self._records()) + 1
clean_payload = redact(payload)
payload_hash = sha256_json(clean_payload)
material = {
"sequence": sequence,
"previous_event_hash": previous,
"payload_hash": payload_hash,
"transaction_id": transaction_id,
}
record = {
**material,
"payload": clean_payload,
"event_hash": sha256_json(material),
}
fd = os.open(self.path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o640)
try:
os.write(fd, canonical_json(record) + b"\n")
os.fsync(fd)
finally:
os.close(fd)
self.path.chmod(0o640)
return record
def has_event(self, transaction_id: str, event: str) -> bool:
self.verify()
return any(
record.get("transaction_id") == transaction_id
and isinstance(record.get("payload"), dict)
and record["payload"].get("event") == event
for record in self._records()
)
class JournalStore:
def __init__(self, root: Path) -> None:
self.root = root
self.wal = root / "wal"
self.receipts = root / "receipts"
ensure_state_directory(root)
ensure_state_directory(self.wal)
ensure_state_directory(self.receipts)
self.audit = AuditChain(root / "audit.jsonl")
def transaction_path(self, operation_id: str) -> Path:
if not operation_id or any(char not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-" for char in operation_id):
raise RuntimeError("OPERATION_ID_INVALID")
return self.wal / f"{operation_id}.json"
def receipt_path(self, operation_id: str) -> Path:
return self.receipts / f"{operation_id}.json"
def load(self, operation_id: str) -> dict[str, Any] | None:
path = self.transaction_path(operation_id)
return read_json(path) if path.exists() else None
def write(self, operation_id: str, value: dict[str, Any]) -> None:
atomic_write_json(self.transaction_path(operation_id), value)
def write_receipt(self, operation_id: str, value: dict[str, Any]) -> None:
atomic_write_json(self.receipt_path(operation_id), value)
def iter_transactions(self) -> Iterable[dict[str, Any]]:
for path in sorted(self.wal.glob("*.json")):
yield read_json(path)
@@ -1,47 +0,0 @@
from __future__ import annotations
from pathlib import Path
try:
import fcntl
except ImportError: # pragma: no cover - exercised by Windows validation
fcntl = None
import msvcrt
class MaintenanceLock:
"""Process fence used only by fixture and later approved activation executors."""
def __init__(self, path: Path) -> None:
self.path = path
self._handle = None
def __enter__(self) -> "MaintenanceLock":
self.path.parent.mkdir(parents=True, exist_ok=True)
self._handle = self.path.open("a+")
if fcntl is not None:
try:
fcntl.flock(self._handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
self._handle.close()
self._handle = None
raise
else:
self._handle.seek(0)
try:
msvcrt.locking(self._handle.fileno(), msvcrt.LK_NBLCK, 1)
except OSError as exc:
self._handle.close()
self._handle = None
raise BlockingIOError("maintenance lock held") from exc
return self
def __exit__(self, exc_type, exc, tb) -> None:
if self._handle is not None:
if fcntl is not None:
fcntl.flock(self._handle.fileno(), fcntl.LOCK_UN)
else:
self._handle.seek(0)
msvcrt.locking(self._handle.fileno(), msvcrt.LK_UNLCK, 1)
self._handle.close()
self._handle = None
@@ -1,90 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Mapping
class GovernanceState(str, Enum):
HUMAN_OWNER_APPROVAL_PENDING = "HUMAN_OWNER_APPROVAL_PENDING"
OWNER_APPROVAL_VERIFIED = "OWNER_APPROVAL_VERIFIED"
MACHINE_GATES_VERIFIED = "MACHINE_GATES_VERIFIED"
AUTO_MERGE_READY = "AUTO_MERGE_READY"
OWNER_APPROVED_EXECUTION_READY = "OWNER_APPROVED_EXECUTION_READY"
@dataclass(frozen=True)
class Review:
reviewer: str
reviewer_id: int
state: str
head_oid: str
reviewed_at: datetime
stale: bool = False
dismissed: bool = False
def __post_init__(self) -> None:
if self.reviewed_at.tzinfo is None:
raise ValueError("reviewed_at must be timezone-aware")
@dataclass(frozen=True)
class MachineGate:
context: str
state: str
head_oid: str
observed_at: datetime
def __post_init__(self) -> None:
if self.observed_at.tzinfo is None:
raise ValueError("observed_at must be timezone-aware")
@dataclass(frozen=True)
class ApprovalContext:
current_head_oid: str
requester: str
executor: str
owner: str
owner_id: int
reviews: tuple[Review, ...]
machine_gates: tuple[MachineGate, ...]
required_machine_contexts: tuple[str, ...]
expires_at: datetime
wip: bool
now: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
def __post_init__(self) -> None:
if self.expires_at.tzinfo is None or self.now.tzinfo is None:
raise ValueError("approval timestamps must be timezone-aware")
@dataclass(frozen=True)
class ProtectionSnapshot:
branch: str
required_approvals: int
approval_whitelist: tuple[str, ...]
merge_whitelist: tuple[str, ...]
dismiss_stale_approvals: bool
require_status_checks: bool
status_contexts: tuple[str, ...]
direct_push: bool
force_push: bool
branch_deletion: bool
version: int
def as_dict(self) -> Mapping[str, Any]:
return {
"branch": self.branch,
"required_approvals": self.required_approvals,
"approval_whitelist": list(self.approval_whitelist),
"merge_whitelist": list(self.merge_whitelist),
"dismiss_stale_approvals": self.dismiss_stale_approvals,
"require_status_checks": self.require_status_checks,
"status_contexts": list(self.status_contexts),
"direct_push": self.direct_push,
"force_push": self.force_push,
"branch_deletion": self.branch_deletion,
"version": self.version,
}
@@ -1,23 +0,0 @@
from __future__ import annotations
class OperationDenied(PermissionError):
pass
class DormantOperationGuard:
"""Fail-closed operation boundary for the prerequisite deployment."""
DENIED = frozenset({
"protected_branch.direct_push",
"governance.direct_push",
"tag.direct_write",
"git.force_push",
"production.without_owner",
"branch_protection.live_update",
"single_owner_policy.activate",
})
def authorize(self, operation: str, *, fixture: bool = False) -> None:
if operation in self.DENIED or not fixture:
raise OperationDenied(operation)
@@ -1,98 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
from typing import Protocol
from .models import ProtectionSnapshot
class CASConflict(RuntimeError):
pass
class DormantViolation(RuntimeError):
pass
def snapshot_sha256(snapshot: ProtectionSnapshot) -> str:
raw = json.dumps(snapshot.as_dict(), sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(raw).hexdigest()
class ProtectionStore(Protocol):
fixture: bool
def read(self, branch: str) -> ProtectionSnapshot: ...
def compare_and_swap(
self, branch: str, expected_sha256: str, target: ProtectionSnapshot
) -> bool: ...
@dataclass(frozen=True)
class ProtectionPlan:
branch: str
expected_previous_sha256: str
previous: ProtectionSnapshot
target: ProtectionSnapshot
target_sha256: str
class BranchProtectionCASMigrationAdapter:
"""Candidate adapter. Mutations are restricted to disposable fixtures."""
def plan(
self,
current: ProtectionSnapshot,
target: ProtectionSnapshot,
expected_previous_sha256: str,
) -> ProtectionPlan:
actual = snapshot_sha256(current)
if actual != expected_previous_sha256:
raise CASConflict("EXPECTED_PREVIOUS_SHA256_MISMATCH")
if current.branch != target.branch:
raise CASConflict("BRANCH_BINDING_MISMATCH")
if target.direct_push or target.force_push or target.branch_deletion:
raise DormantViolation("UNSAFE_TARGET_POLICY")
return ProtectionPlan(
current.branch, actual, current, target, snapshot_sha256(target)
)
def apply_fixture(self, store: ProtectionStore, plan: ProtectionPlan) -> ProtectionSnapshot:
if not store.fixture:
raise DormantViolation("LIVE_PROTECTION_MUTATION_DENIED")
if not store.compare_and_swap(
plan.branch, plan.expected_previous_sha256, plan.target
):
raise CASConflict("CONCURRENT_PROTECTION_CHANGE")
return store.read(plan.branch)
def rollback_fixture(self, store: ProtectionStore, plan: ProtectionPlan) -> ProtectionSnapshot:
if not store.fixture:
raise DormantViolation("LIVE_PROTECTION_ROLLBACK_DENIED")
if not store.compare_and_swap(plan.branch, plan.target_sha256, plan.previous):
raise CASConflict("ROLLBACK_CONFLICT")
return store.read(plan.branch)
class MemoryProtectionStore:
fixture = True
def __init__(self, snapshot: ProtectionSnapshot) -> None:
self._snapshot = snapshot
def read(self, branch: str) -> ProtectionSnapshot:
if self._snapshot.branch != branch:
raise KeyError(branch)
return self._snapshot
def compare_and_swap(
self, branch: str, expected_sha256: str, target: ProtectionSnapshot
) -> bool:
if self._snapshot.branch != branch:
return False
if snapshot_sha256(self._snapshot) != expected_sha256:
return False
self._snapshot = target
return True
@@ -1,69 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
from pathlib import Path
import tempfile
from .models import ProtectionSnapshot
from .protection import MemoryProtectionStore, ProtectionPlan, snapshot_sha256
@dataclass(frozen=True)
class RollbackConfiguration:
branch: str
previous_sha256: str
target_sha256: str
payload: dict
class RollbackConfigurationManager:
def build(self, plan: ProtectionPlan) -> RollbackConfiguration:
return RollbackConfiguration(
plan.branch,
plan.expected_previous_sha256,
plan.target_sha256,
{"previous": plan.previous.as_dict(), "target": plan.target.as_dict()},
)
def install_fixture(self, root: Path, config: RollbackConfiguration) -> Path:
if not root.name.startswith("aios-governance-fixture-"):
raise PermissionError("rollback configuration may only be installed in fixture root")
root.mkdir(parents=True, exist_ok=True)
destination = root / "rollback-configuration.json"
raw = json.dumps(config.payload, sort_keys=True, indent=2).encode() + b"\n"
with tempfile.NamedTemporaryFile(dir=root, prefix=".rollback.", delete=False) as handle:
handle.write(raw)
handle.flush()
temporary = Path(handle.name)
temporary.chmod(0o600)
temporary.replace(destination)
return destination
def verify(self, config: RollbackConfiguration, plan: ProtectionPlan) -> bool:
return (
config.previous_sha256 == snapshot_sha256(plan.previous)
and config.target_sha256 == snapshot_sha256(plan.target)
)
@dataclass
class ProtectionCompensationSaga:
adapter: object
store: MemoryProtectionStore
plan: ProtectionPlan
applied: bool = False
rolled_back: bool = False
def execute_fixture(self) -> ProtectionSnapshot:
result = self.adapter.apply_fixture(self.store, self.plan)
self.applied = True
return result
def compensate_fixture(self) -> ProtectionSnapshot:
if not self.applied:
return self.store.read(self.plan.branch)
result = self.adapter.rollback_fixture(self.store, self.plan)
self.rolled_back = True
return result
@@ -1,69 +0,0 @@
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import tempfile
def runtime_state(component: str, prior_invocations: int = 0) -> dict:
return {
"component": component,
"deployment_mode": "DORMANT_SHADOW_ONLY",
"enabled": False,
"active": False,
"production_routing": False,
"production_mutation": False,
"global_gitops_writes_enabled": False,
"prior_invocations": prior_invocations,
"restart_recovery": prior_invocations > 0,
}
def checkpoint_state(component: str, root: Path) -> dict:
root.mkdir(parents=True, exist_ok=True)
destination = root / f"{component}.json"
prior = 0
if destination.exists():
previous = json.loads(destination.read_text(encoding="utf-8"))
if previous.get("component") != component or previous.get("deployment_mode") != "DORMANT_SHADOW_ONLY":
raise RuntimeError("checkpoint identity mismatch")
prior = int(previous.get("invocations", 0))
state = runtime_state(component, prior)
checkpoint = {**state, "invocations": prior + 1}
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=root, delete=False) as handle:
json.dump(checkpoint, handle, sort_keys=True)
handle.write("\n")
handle.flush()
temporary = Path(handle.name)
temporary.replace(destination)
return state
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--component", required=True, choices=("owner-review-adapter", "machine-status-producer"))
parser.add_argument("--self-check", action="store_true")
parser.add_argument("--activate", action="store_true")
parser.add_argument("--checkpoint-dir", type=Path)
args = parser.parse_args(argv)
if args.activate:
parser.error("activation is not authorized by the prerequisites candidate")
if os.environ.get("AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED", "false").lower() != "false":
parser.error("candidate must remain disabled")
if os.environ.get("AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE", "false").lower() != "false":
parser.error("activation must remain inactive")
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
parser.error("production mutation must remain disabled")
state = (
checkpoint_state(args.component, args.checkpoint_dir)
if args.checkpoint_dir is not None
else runtime_state(args.component)
)
print(json.dumps(state, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,54 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
import tempfile
from .models import GovernanceState
class InvalidTransition(RuntimeError):
pass
_NEXT = {
GovernanceState.HUMAN_OWNER_APPROVAL_PENDING: GovernanceState.OWNER_APPROVAL_VERIFIED,
GovernanceState.OWNER_APPROVAL_VERIFIED: GovernanceState.MACHINE_GATES_VERIFIED,
GovernanceState.MACHINE_GATES_VERIFIED: GovernanceState.AUTO_MERGE_READY,
GovernanceState.AUTO_MERGE_READY: GovernanceState.OWNER_APPROVED_EXECUTION_READY,
}
@dataclass
class GovernanceStateMachine:
state: GovernanceState = GovernanceState.HUMAN_OWNER_APPROVAL_PENDING
def transition(self, target: GovernanceState) -> GovernanceState:
if _NEXT.get(self.state) != target:
raise InvalidTransition(f"{self.state.value}->{target.value}")
self.state = target
return self.state
def restore(self, checkpoint: GovernanceState) -> None:
if checkpoint not in GovernanceState:
raise InvalidTransition("unknown checkpoint")
self.state = checkpoint
def save_fixture(self, path: Path) -> None:
if not any(part.startswith("aios-governance-fixture-") for part in path.resolve().parts):
raise PermissionError("state checkpoint writes are fixture-only")
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
json.dump({"state": self.state.value}, handle, sort_keys=True)
handle.write("\n")
handle.flush()
temporary = Path(handle.name)
temporary.replace(path)
@classmethod
def load_fixture(cls, path: Path) -> "GovernanceStateMachine":
if not any(part.startswith("aios-governance-fixture-") for part in path.resolve().parts):
raise PermissionError("state checkpoint reads are fixture-only")
raw = json.loads(path.read_text(encoding="utf-8"))
return cls(GovernanceState(raw["state"]))
@@ -1,38 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
class DormantStatusProducer(RuntimeError):
pass
@dataclass(frozen=True)
class StatusRecord:
context: str
state: str
head_oid: str
observed_at: str
class MachineStatusProducer:
def __init__(self, *, enabled: bool = False, fixture: bool = False) -> None:
self.enabled = enabled
self.fixture = fixture
def produce(self, context: str, state: str, head_oid: str) -> StatusRecord:
if not self.fixture:
raise DormantStatusProducer("LIVE_STATUS_PUBLICATION_DENIED")
if self.enabled:
raise DormantStatusProducer("DORMANT_CANDIDATE_MUST_NOT_BE_ENABLED")
if context not in {
"aios/machine-gates", "aios/secret-scan", "aios/evidence-closure"
}:
raise ValueError("unknown status context")
if state not in {"success", "failure"} or len(head_oid) != 40:
raise ValueError("invalid status record")
return StatusRecord(
context, state, head_oid,
datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
)
@@ -1,69 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from .models import ApprovalContext, Review
class ValidationDenied(RuntimeError):
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
@dataclass(frozen=True)
class ValidationResult:
owner_review: Review
machine_contexts: tuple[str, ...]
class SingleOwnerApprovalValidator:
"""Pure candidate validator. It has no Gitea or production write path."""
def validate(self, context: ApprovalContext) -> ValidationResult:
if context.now >= context.expires_at:
raise ValidationDenied("APPROVAL_EXPIRED")
if context.wip:
raise ValidationDenied("WIP_BLOCKED")
if context.requester == context.owner:
raise ValidationDenied("REQUESTER_SELF_APPROVAL")
if context.executor == context.owner:
raise ValidationDenied("EXECUTOR_SELF_APPROVAL")
approved = [review for review in context.reviews if review.state == "APPROVED"]
owner_reviews = [
review for review in approved
if review.reviewer == context.owner and review.reviewer_id == context.owner_id
]
if len(owner_reviews) != 1:
raise ValidationDenied("OWNER_APPROVAL_REQUIRED")
owner_review = owner_reviews[0]
if owner_review.stale:
raise ValidationDenied("STALE_OWNER_APPROVAL")
if owner_review.dismissed:
raise ValidationDenied("DISMISSED_OWNER_APPROVAL")
if owner_review.head_oid != context.current_head_oid:
raise ValidationDenied("OWNER_APPROVAL_NOT_LATEST_HEAD")
if owner_review.reviewed_at >= context.expires_at:
raise ValidationDenied("OWNER_APPROVAL_EXPIRED")
if any(
review.state == "APPROVED"
and review.reviewer != context.owner
and review.reviewer_id != context.owner_id
for review in context.reviews
):
# Non-owner approvals may exist for legacy mechanics, but never
# satisfy or replace the human authorization decision.
pass
gates = {gate.context: gate for gate in context.machine_gates}
if set(gates) != set(context.required_machine_contexts):
raise ValidationDenied("MACHINE_GATE_SET_MISMATCH")
for gate in gates.values():
if gate.state != "success":
raise ValidationDenied("MACHINE_GATE_FAILED")
if gate.head_oid != context.current_head_oid:
raise ValidationDenied("MACHINE_GATE_NOT_LATEST_HEAD")
if gate.observed_at >= context.expires_at:
raise ValidationDenied("MACHINE_GATE_EXPIRED")
return ValidationResult(owner_review, tuple(sorted(gates)))
@@ -1,16 +0,0 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "aios-single-owner-governance"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
[project.scripts]
aios-single-owner-governance = "aios_single_owner_governance.activation:main"
[tool.setuptools.packages.find]
where = ["."]
include = ["aios_single_owner_governance*"]
@@ -0,0 +1,48 @@
from datetime import datetime, timezone, timedelta
from aios_autonomous_release_candidate.models import (
ResourceBinding,
RiskClass,
TaskPlan,
WorkflowStep,
)
from aios_autonomous_release_candidate.policy import ReleasePolicy
from aios_autonomous_release_candidate.evidence import EvidenceChain
OID = "a" * 40
def build_plan():
return TaskPlan(
task_id="test-task",
intent="candidate validation",
binding=ResourceBinding(
repository="aios/aios",
branch="patch/test",
base_oid=OID,
allowed_paths=("services/test/",),
),
risk=RiskClass.LOW,
steps=(
WorkflowStep(
step_id="validate",
worker_role="tester",
required_capabilities=("test.execute",),
allowed_paths=("services/test/",),
),
),
evidence_requirements=("tests",),
)
def test_plan_policy_accepts_candidate_plan():
ReleasePolicy().validate_task(build_plan())
def test_evidence_chain_detects_valid_closure():
chain = EvidenceChain()
chain.append("START", "test-task", {"ok": True, "secrets_included": False})
chain.append("DONE", "test-task", {"ok": True, "secrets_included": False})
assert chain.verify()
assert len(chain.closure_digest()) == 64
@@ -0,0 +1,41 @@
from pathlib import Path
import pytest
from aios_autonomous_release_candidate.artifact import ArtifactBuilder, ArtifactFile, BuiltArtifact
from aios_autonomous_release_candidate.errors import EvidenceIncomplete, ReleaseBlocked
def test_artifact_is_deterministic_and_reverified() -> None:
builder = ArtifactBuilder()
files = (
ArtifactFile("bin/run", b"#!/bin/sh\nexit 0\n", 0o755),
ArtifactFile("config/candidate.json", b'{"production":false}\n'),
)
first = builder.build("a" * 40, "b" * 40, files)
second = builder.build("a" * 40, "b" * 40, tuple(reversed(files)))
assert first.content == second.content
assert first.manifest.manifest_sha256 == second.manifest.manifest_sha256
assert first.manifest.secrets_included is False
builder.verify(first)
with pytest.raises(EvidenceIncomplete, match="ARTIFACT_DIGEST_MISMATCH"):
builder.verify(BuiltArtifact(first.content + b"tampered", first.manifest))
def test_source_tree_rejects_symlink_and_hardlink(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
target = root / "file.txt"
target.write_text("safe", encoding="utf-8")
(root / "link.txt").symlink_to(target)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_LINK_REJECTED"):
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_artifact_rejects_secret_markers() -> None:
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SECRET_MARKER"):
ArtifactBuilder().build(
"a" * 40,
"b" * 40,
(ArtifactFile("unsafe.env", b"password" + b"=" + b"x" * 16),),
)
@@ -0,0 +1,66 @@
import json
from pathlib import Path
from aios_autonomous_release_candidate.cli import main
def test_cli_artifact_build_and_verify(tmp_path: Path, capsys) -> None:
source = tmp_path / "source"
source.mkdir()
(source / "payload.txt").write_text("candidate payload\n", encoding="utf-8")
output = tmp_path / "output"
assert main(
[
"artifact",
"build",
"--source",
str(source),
"--source-oid",
"a" * 40,
"--tree-oid",
"b" * 40,
"--output-directory",
str(output),
]
) == 0
built = json.loads(capsys.readouterr().out)
assert built["secrets_included"] is False
assert main(
[
"artifact",
"verify",
"--artifact",
built["artifact"],
"--manifest",
built["manifest"],
]
) == 0
assert json.loads(capsys.readouterr().out)["verified"] is True
def test_cli_release_verify_stops_at_owner(tmp_path: Path, capsys) -> None:
payload = {
"task_id": "cli-release-task",
"repository": "aios/aios",
"branch": "integration/cli-release",
"version": "SHADOW-0.1.0",
"target_oid": "a" * 40,
"tree_oid": "b" * 40,
"artifact_sha256": "c" * 64,
"manifest_sha256": "d" * 64,
"package_closure_sha256": "e" * 64,
"evidence_closure_oid": "f" * 40,
"approval_plan_id": "external-owner-release",
"rollback_approval_plan_id": "external-owner-rollback",
"rollback_artifact_sha256": "1" * 64,
"expected_tag_oid": "2" * 40,
"expected_target_oid": "a" * 40,
"production_config_sha256": "3" * 64,
}
path = tmp_path / "release-plan.json"
path.write_text(json.dumps(payload), encoding="utf-8")
assert main(["release", "verify", "--payload", str(path)]) == 0
result = json.loads(capsys.readouterr().out)
assert result["release_state"] == "WAITING_OWNER"
assert result["production_release"] == "BLOCKED_EXTERNAL_OWNER_SIGNATURE"
assert result["production_side_effects"] is False
@@ -0,0 +1,48 @@
from datetime import datetime, timezone, timedelta
from aios_autonomous_release_candidate.models import (
ResourceBinding,
RiskClass,
TaskPlan,
WorkflowStep,
)
from aios_autonomous_release_candidate.policy import ReleasePolicy
from aios_autonomous_release_candidate.evidence import EvidenceChain
OID = "a" * 40
def build_plan():
return TaskPlan(
task_id="test-task",
intent="candidate validation",
binding=ResourceBinding(
repository="aios/aios",
branch="patch/test",
base_oid=OID,
allowed_paths=("services/test/",),
),
risk=RiskClass.LOW,
steps=(
WorkflowStep(
step_id="validate",
worker_role="tester",
required_capabilities=("test.execute",),
allowed_paths=("services/test/",),
),
),
evidence_requirements=("tests",),
)
def test_plan_policy_accepts_candidate_plan():
ReleasePolicy().validate_task(build_plan())
def test_evidence_chain_detects_valid_closure():
chain = EvidenceChain()
chain.append("START", "test-task", {"ok": True, "secrets_included": False})
chain.append("DONE", "test-task", {"ok": True, "secrets_included": False})
assert chain.verify()
assert len(chain.closure_digest()) == 64
@@ -0,0 +1,129 @@
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from aios_autonomous_release_candidate.credential import CredentialBroker
from aios_autonomous_release_candidate.errors import CredentialUnavailable, EvidenceIncomplete
from aios_autonomous_release_candidate.evidence import EvidenceChain
from aios_autonomous_release_candidate.models import CredentialRequest, ResourceBinding
from aios_autonomous_release_candidate.store import StateStore
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
class Provider:
def __init__(self, reference: str = "systemd-credential:openclaw-gateway-candidate") -> None:
self.reference = reference
self.active: set[str] = set()
self.revoked: list[str] = []
self.count = 0
def issue(self, request, expires_at):
self.count += 1
credential_id = f"credential-{self.count}"
self.active.add(credential_id)
return credential_id, self.reference
def revoke(self, credential_id, reference):
self.active.discard(credential_id)
self.revoked.append(credential_id)
def is_usable(self, credential_id, reference):
return credential_id in self.active
def request(request_id: str = "request-1") -> CredentialRequest:
return CredentialRequest(
request_id=request_id,
task_id="task-credential",
session_id="session-1",
worker_id="worker-1",
audience="openclaw-candidate",
consumer_service="aios-autonomous-release-worker-candidate.service",
binding=ResourceBinding("aios/aios", "patch/credential-test", "a" * 40, ("services/test",)),
operations=("native_session.spawn",),
ttl_seconds=300,
max_uses=1,
provider="openclaw",
runtime_user="aiman",
approval_plan_id="owner-stage-b-native-sessions-20260726",
not_before=NOW,
expires_at=NOW + timedelta(minutes=5),
nonce="nonce-credential-1",
)
def test_credential_is_scope_bound_revocable_and_idempotent(tmp_path: Path) -> None:
provider = Provider()
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
projection = broker.issue(request(), NOW)
broker.project(projection)
broker.authorize(
projection,
task_id="task-credential",
session_id="session-1",
worker_id="worker-1",
audience="openclaw-candidate",
consumer_service="aios-autonomous-release-worker-candidate.service",
operation="native_session.spawn",
path="services/test/file.py",
now=NOW,
)
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_SCOPE_MISMATCH"):
broker.authorize(
projection,
task_id="other-task",
session_id="session-1",
worker_id="worker-1",
audience="openclaw-candidate",
consumer_service="aios-autonomous-release-worker-candidate.service",
operation="native_session.spawn",
path="services/test/file.py",
now=NOW,
)
first = broker.revoke(projection.credential_id, projection.reference, NOW)
second = broker.revoke(projection.credential_id, projection.reference, NOW)
assert first == second
broker.verify_revoked(projection.credential_id, projection.reference)
def test_persistence_failure_compensates_upstream_issuance(tmp_path: Path) -> None:
provider = Provider()
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
broker.issue(request(), NOW)
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_PERSISTENCE_FAILED"):
broker.issue(request("request-2"), NOW)
assert "credential-2" in provider.revoked
def test_plaintext_or_untrusted_projection_is_rejected(tmp_path: Path) -> None:
provider = Provider("/run/credentials/candidate/raw-value")
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_REFERENCE_REJECTED"):
broker.issue(request(), NOW)
assert provider.revoked == ["credential-1"]
def test_evidence_chain_survives_restart_and_detects_tampering(tmp_path: Path) -> None:
store = StateStore(tmp_path / "state.sqlite3")
chain = EvidenceChain(store, "task-evidence")
chain.append("TASK_STARTED", "task-evidence", {"secrets_included": False})
chain.append(
"TESTS_PASSED",
"task-evidence",
{"passed": 42, "skipped": 0, "secrets_included": False},
)
reopened = EvidenceChain(store, "task-evidence")
assert reopened.verify()
assert len(reopened.close(("TASK_STARTED", "TESTS_PASSED"))) == 64
with pytest.raises(EvidenceIncomplete, match="EVIDENCE_REQUIREMENT_MISSING"):
reopened.close(("NATIVE_SESSION_VERIFIED",))
with store.transaction() as connection:
connection.execute(
"UPDATE events SET payload_json=? WHERE task_id=? AND sequence=1",
('{"secrets_included":true}', "task-evidence"),
)
assert EvidenceChain(store, "task-evidence").verify() is False
@@ -0,0 +1,125 @@
from datetime import datetime, timedelta, timezone
import pytest
from aios_autonomous_release_candidate.approval import (
ApprovalVerifier,
StaticRevocationSource,
UnavailableSignatureVerifier,
)
from aios_autonomous_release_candidate.errors import ApprovalRequired
from aios_autonomous_release_candidate.gitea_canary import (
EphemeralKeyProjection,
GiteaDeployKeyCanary,
)
from aios_autonomous_release_candidate.models import (
ApprovalEnvelope,
ResourceBinding,
RiskClass,
TaskPlan,
WorkflowStep,
)
from aios_autonomous_release_candidate.store import StateStore
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
OID = "a" * 40
def task_plan() -> TaskPlan:
return TaskPlan(
"external-gate-task",
"external gate verification",
ResourceBinding("aios/aios", "patch/external-gate", OID, ("services/test",)),
RiskClass.HIGH,
(WorkflowStep("canary", "tester", ("native_session.spawn",)),),
("external-gates",),
)
def test_unknown_owner_verifier_fails_with_stable_blocker(tmp_path) -> None:
plan = task_plan()
envelope = ApprovalEnvelope(
approval_plan_id="owner-external-gate",
task_id=plan.task_id,
task_plan_sha256=plan.digest(),
binding=plan.binding,
capabilities=("native_session.spawn",),
risk=RiskClass.HIGH,
not_before=NOW - timedelta(minutes=1),
expires_at=NOW + timedelta(minutes=5),
nonce="nonce-owner-external",
signing_key_id="owner-kms-1",
signature="opaque-signature",
)
verifier = ApprovalVerifier(
StateStore(tmp_path / "state.sqlite3"),
UnavailableSignatureVerifier(),
StaticRevocationSource(),
)
with pytest.raises(
ApprovalRequired, match="BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE"
):
verifier.verify(plan, envelope, now=NOW)
class KeyProjection:
def __init__(self) -> None:
self.present = False
def create(self, task_id, branch):
self.present = True
return EphemeralKeyProjection(
public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICandidateCanaryOnly",
private_key_reference="systemd-credential:gitea-deploy-key-canary",
)
def delete(self, reference):
self.present = False
def is_present(self, reference):
return self.present
class Gitea:
simulation = True
def __init__(self) -> None:
self.keys = {}
def list_deploy_keys(self, repository, credential_reference):
return list(self.keys.values())
def create_deploy_key(self, repository, title, public_key, credential_reference):
item = {"id": 1, "title": title, "key": public_key, "read_only": False}
self.keys[1] = item
return item
def delete_deploy_key(self, repository, key_id, credential_reference):
self.keys.pop(key_id, None)
class SSHProbe:
def __init__(self, gitea, projection) -> None:
self.gitea = gitea
self.projection = projection
def authenticate(self, repository, branch, private_key_reference):
return bool(self.gitea.keys) and self.projection.present
def test_gitea_canary_always_revokes_key_and_projection() -> None:
projection = KeyProjection()
gitea = Gitea()
result = GiteaDeployKeyCanary(gitea, projection, SSHProbe(gitea, projection)).run(
task_id="external-gate-task",
branch="patch/external-gate",
credential_reference="systemd-credential:gitea-admin-api-candidate",
)
assert result.key_created is True
assert result.key_deleted is True
assert result.post_revoke_auth_rejected is True
assert result.credential_projection_deleted is True
assert result.gitea_admin_api_lifecycle_canary is False # simulation is not real evidence
assert gitea.keys == {}
assert projection.present is False
@@ -0,0 +1,156 @@
from contextlib import contextmanager
import hashlib
import json
from pathlib import Path
from threading import Thread
import urllib.error
import urllib.request
from aios_autonomous_release_candidate.service import CandidateApplication, CandidateHTTPServer
@contextmanager
def running_server(tmp_path: Path):
application = CandidateApplication(tmp_path / "state.sqlite3")
server = CandidateHTTPServer(("127.0.0.1", 0), application)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield application, f"http://127.0.0.1:{server.server_port}"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def get_json(url: str):
with urllib.request.urlopen(url, timeout=5) as response:
return response.status, json.load(response)
def post_json(url: str, payload: dict):
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=5) as response:
return response.status, json.load(response)
def test_health_and_readiness_are_loopback_safe(tmp_path: Path) -> None:
with running_server(tmp_path) as (_, base):
status, health = get_json(f"{base}/health")
assert status == 200
assert health["production_side_effects"] is False
status, ready = get_json(f"{base}/ready")
assert status == 200
assert ready == {"status": "ready", "secrets_included": False}
def test_json_content_type_is_required(tmp_path: Path) -> None:
with running_server(tmp_path) as (_, base):
request = urllib.request.Request(f"{base}/v1/tasks", data=b"{}", method="POST")
try:
urllib.request.urlopen(request, timeout=5)
except urllib.error.HTTPError as error:
assert error.code == 415
assert json.load(error)["error"] == "content_type_required"
def test_internal_exception_text_is_not_disclosed(tmp_path: Path) -> None:
with running_server(tmp_path) as (application, base):
def fail(_payload):
raise RuntimeError("sensitive internal diagnostic")
application.api.create_task = fail
request = urllib.request.Request(
f"{base}/v1/tasks",
data=b"{}",
method="POST",
headers={"Content-Type": "application/json"},
)
try:
urllib.request.urlopen(request, timeout=5)
except urllib.error.HTTPError as error:
assert error.code == 500
body = error.read().decode("utf-8")
assert "candidate_request_failed" in body
assert "sensitive internal diagnostic" not in body
def test_task_lifecycle_evidence_and_release_gate_endpoints(tmp_path: Path) -> None:
task_id = "api-lifecycle-task"
task = {
"task_id": task_id,
"intent": "exercise candidate API",
"binding": {
"repository": "aios/aios",
"branch": "patch/api-lifecycle",
"base_oid": "a" * 40,
"allowed_paths": ["services/test"],
},
"risk": "medium",
"steps": [
{
"step_id": "test-step",
"worker_role": "tester",
"required_capabilities": ["test.execute"],
}
],
"evidence_requirements": ["STEP_RESULT"],
"production_side_effects_allowed": False,
}
with running_server(tmp_path) as (_, base):
status, created = post_json(f"{base}/v1/tasks", task)
assert status == 201
assert created["production_side_effects"] is False
status, steps = get_json(f"{base}/v1/tasks/{task_id}/steps")
assert status == 200
assert steps["steps"]["test-step"]["state"] == "READY"
binding = {
"step_id": "test-step",
"worker_id": "worker-1",
"session_id": "session-1",
}
_, claimed = post_json(f"{base}/v1/tasks/{task_id}/claim", binding)
assert claimed["state"] == "RUNNING"
_, heartbeat = post_json(f"{base}/v1/tasks/{task_id}/heartbeat", binding)
assert heartbeat["state"] == "RUNNING"
_, result = post_json(
f"{base}/v1/tasks/{task_id}/result",
{"step_id": "test-step", "result_digest": hashlib.sha256(b"ok").hexdigest()},
)
assert result["state"] == "COMPLETED"
_, evidence = get_json(f"{base}/v1/tasks/{task_id}/evidence")
assert evidence["verified"] is True
assert any(item["event_type"] == "STEP_RESULT" for item in evidence["events"])
_, closure = post_json(f"{base}/v1/tasks/{task_id}/evidence/close", {})
assert len(closure["evidence_closure"]) == 64
release_plan = {
"task_id": task_id,
"repository": "aios/aios",
"branch": "integration/api-lifecycle",
"version": "SHADOW-0.1.0",
"target_oid": "b" * 40,
"tree_oid": "c" * 40,
"artifact_sha256": "d" * 64,
"manifest_sha256": "e" * 64,
"package_closure_sha256": "f" * 64,
"evidence_closure_oid": "1" * 40,
"approval_plan_id": "external-owner-release",
"rollback_approval_plan_id": "external-owner-rollback",
"rollback_artifact_sha256": "2" * 64,
"expected_tag_oid": "3" * 40,
"expected_target_oid": "b" * 40,
"production_config_sha256": "4" * 64,
}
_, release = post_json(f"{base}/v1/release/plan", release_plan)
assert release["release_state"] == "BUILT"
_, verified = post_json(
f"{base}/v1/release/verify", {"artifact_ok": True, "evidence_ok": True}
)
assert verified["release_state"] == "WAITING_OWNER"
assert verified["production_release"] == "BLOCKED_EXTERNAL_OWNER_SIGNATURE"
@@ -0,0 +1,94 @@
from typing import Any, Mapping
import pytest
from aios_autonomous_release_candidate.errors import StateConflict
from aios_autonomous_release_candidate.native_adapters import (
CodexNativeAdapter,
OpenClawNativeAdapter,
)
class Provider:
simulation = True
def __init__(self, name: str) -> None:
self.name = name
self.state = "ACTIVE"
self.parent = "parent-1"
def spawn(self, request: Mapping[str, Any], credential_reference: str):
return {
"provider_session_id": f"{self.name}-native-1",
"task_id": request["task_id"],
"parent_session_id": request["parent_session_id"],
"candidate_only": True,
"production_session_access": False,
}
def status(self, provider_session_id: str, credential_reference: str):
return {"provider_session_id": provider_session_id, "state": self.state}
def list_tree(self, parent_session_id: str, credential_reference: str):
return [{"provider_session_id": f"{self.name}-native-1", "parent_session_id": parent_session_id}]
def send_text(self, provider_session_id, payload, credential_reference):
return {
"content_type": "text/plain",
"encoding": "utf-8",
"image_only": False,
"task_id": payload["task_id"],
"message_id": "message-1",
"receipt": "receipt-1",
"recipient_binding": payload["recipient_binding"],
"secrets_included": False,
}
def interrupt(self, provider_session_id: str, credential_reference: str):
self.state = "INTERRUPTED"
def close(self, provider_session_id: str, credential_reference: str):
self.state = "CLOSED"
@pytest.mark.parametrize(
("adapter_type", "provider_name", "reference", "flag"),
(
(
OpenClawNativeAdapter,
"openclaw",
"systemd-credential:openclaw-gateway-candidate",
"openclaw_native_child_session_verified",
),
(
CodexNativeAdapter,
"codex",
"systemd-credential:codex-app-server-candidate",
"codex_child_session_verified",
),
),
)
def test_native_adapter_preserves_identity_and_text_contract(
adapter_type, provider_name, reference, flag
) -> None:
adapter = adapter_type(Provider(provider_name), reference)
session = adapter.spawn("task-native", "parent-1", {"recipient_id": "weixin-owner"})
assert session.broker_session_id != session.provider_session_id
verification = adapter.verify(session)
assert verification.broker_session_verified is True
assert getattr(verification, flag) is False # simulated providers are never real canary evidence
result = adapter.send_text(session, "hello")
assert result.content_type == "text/plain"
assert result.secrets_included is False
adapter.interrupt(session)
assert adapter.status(session) == "INTERRUPTED"
adapter.close(session)
assert adapter.status(session) == "CLOSED"
def test_recipient_binding_must_be_single_and_trusted() -> None:
adapter = OpenClawNativeAdapter(
Provider("openclaw"), "systemd-credential:openclaw-gateway-candidate"
)
with pytest.raises(StateConflict, match="RECIPIENT_BINDING_REQUIRED"):
adapter.spawn("task-native", "parent-1", {"recipients": ["one", "two"]})
@@ -0,0 +1,95 @@
from dataclasses import replace
from datetime import datetime, timezone
import pytest
from aios_autonomous_release_candidate.approval import ApprovalVerificationResult
from aios_autonomous_release_candidate.errors import ReleaseBlocked
from aios_autonomous_release_candidate.models import ReleasePlan
from aios_autonomous_release_candidate.release import ReleaseController, ReleaseState
from aios_autonomous_release_candidate.release_gate import ReleaseEvidence, ReleaseGate
from aios_autonomous_release_candidate.tag import TagReference, TagVerifier
OID = "a" * 40
TAG_OID = "b" * 40
DIGEST = "c" * 64
def plan() -> ReleasePlan:
return ReleasePlan(
task_id="release-task",
repository="aios/aios",
branch="integration/autonomous-release",
version="V2R1C4",
target_oid=OID,
tree_oid="d" * 40,
artifact_sha256=DIGEST,
manifest_sha256="e" * 64,
package_closure_sha256="3" * 64,
evidence_closure_oid="f" * 40,
approval_plan_id="owner-release-v2r1c4",
rollback_approval_plan_id="owner-rollback-v2r1c4",
rollback_artifact_sha256="1" * 64,
expected_tag_oid=TAG_OID,
expected_target_oid=OID,
production_config_sha256="2" * 64,
)
def approval(plan_id: str) -> ApprovalVerificationResult:
return ApprovalVerificationResult(
approval_plan_id=plan_id,
task_id="release-task",
signing_key_id="owner-kms-1",
verified_at=datetime(2026, 7, 26, tzinfo=timezone.utc),
)
def test_deploy_and_rollback_require_distinct_verified_approvals() -> None:
controller = ReleaseController()
controller.attach_plan(plan())
controller.mark_built()
controller.verify(artifact_ok=True, evidence_ok=True)
controller.request_owner_approval()
with pytest.raises(ReleaseBlocked, match="OWNER_APPROVAL_PLAN_MISMATCH"):
controller.approve(approval("wrong-plan"))
controller.approve(approval("owner-release-v2r1c4"))
controller.start_deployment()
controller.mark_deployed()
controller.request_rollback("candidate acceptance regression")
with pytest.raises(ReleaseBlocked, match="ROLLBACK_APPROVAL_PLAN_MISMATCH"):
controller.approve_rollback(approval("owner-release-v2r1c4"))
controller.approve_rollback(approval("owner-rollback-v2r1c4"))
controller.start_rollback()
assert controller.mark_rolled_back().state == ReleaseState.ROLLED_BACK
def test_annotated_tag_uses_object_and_peeled_oids_separately() -> None:
tag = TagReference("refs/tags/V2R1C4", "tag", TAG_OID, OID)
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid=OID)
with pytest.raises(ReleaseBlocked, match="TAG_PEELED_TARGET_MISMATCH"):
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid="3" * 40)
def test_release_gate_requires_four_source_tag_and_artifact_closure() -> None:
evidence = ReleaseEvidence(
expected_version="V2R1C4",
registry_version="V2R1C4",
baseline_version="V2R1C4",
local_tag_oid=TAG_OID,
remote_tag_oid=TAG_OID,
gitea_tag_oid=TAG_OID,
expected_tag_oid=TAG_OID,
local_peeled_target_oid=OID,
remote_peeled_target_oid=OID,
gitea_peeled_target_oid=OID,
expected_target_oid=OID,
artifact_sha256=DIGEST,
manifest_sha256="e" * 64,
evidence_sha256="4" * 64,
rollback_artifact_sha256="1" * 64,
)
ReleaseGate().verify(evidence)
with pytest.raises(ReleaseBlocked, match="TAG_GITEA_MISMATCH"):
ReleaseGate().verify(replace(evidence, gitea_tag_oid="5" * 40))
@@ -0,0 +1,116 @@
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from aios_autonomous_release_candidate.errors import CredentialUnavailable
from aios_autonomous_release_candidate.evidence import EvidenceChain
from aios_autonomous_release_candidate.execution_manager import ExecutionManager
from aios_autonomous_release_candidate.models import CredentialRequest, ResourceBinding
from aios_autonomous_release_candidate.native_adapters import OpenClawNativeAdapter
from aios_autonomous_release_candidate.secret_manager import SecretManagerV2
from aios_autonomous_release_candidate.store import StateStore
from test_native_adapter_contracts import Provider
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
class Transport:
def __init__(self) -> None:
self.calls = []
def exchange(self, operation, payload):
self.calls.append((operation, payload))
base = {
"task_id": payload["task_id"],
"credential_id": payload.get("credential_id", "credential-openclaw-1"),
"plaintext_secret_returned": False,
}
if operation == "issue":
return {
**base,
"credential_reference": "systemd-credential:openclaw-gateway-candidate",
"state": "ISSUED",
"expires_at": payload["expires_at"],
}
if operation == "audit":
return {**base, "events": [{"operation": "issue", "secrets_included": False}]}
if operation in {"project", "status"}:
return {**base, "state": "PROJECTED"}
if operation == "verify_revocation":
return {**base, "state": "REVOKED", "post_revocation_auth": "REJECTED"}
return {**base, "state": "REVOKED"}
def credential_request() -> CredentialRequest:
return CredentialRequest(
request_id="request-1",
task_id="task-execution",
session_id="session-1",
worker_id="worker-1",
audience="openclaw-candidate",
consumer_service="aios-autonomous-release-worker-candidate.service",
binding=ResourceBinding("aios/aios", "patch/execution", "a" * 40, ("services/test",)),
operations=("native_session.spawn",),
provider="openclaw",
runtime_user="aiman",
approval_plan_id="owner-stage-b-native-sessions-20260726",
not_before=NOW,
expires_at=NOW + timedelta(minutes=5),
nonce="nonce-execution-1",
ttl_seconds=300,
max_uses=1,
)
def test_secret_manager_uses_reference_only_protocol() -> None:
transport = Transport()
manager = SecretManagerV2(transport)
receipt = manager.issue(credential_request(), NOW)
assert receipt.reference == "systemd-credential:openclaw-gateway-candidate"
assert receipt.plaintext_secret_returned is False
manager.project(receipt)
manager.status(receipt)
manager.revoke(receipt)
manager.verify_revocation(receipt)
events = manager.audit(receipt)
assert events[0]["secrets_included"] is False
assert [operation for operation, _ in transport.calls] == [
"issue",
"project",
"status",
"revoke",
"verify_revocation",
"audit",
]
def test_secret_manager_rejects_plaintext_fields() -> None:
transport = Transport()
def unsafe(operation, payload):
return {"task_id": payload["task_id"], "credential_id": "credential-1", "api_key": "redacted"}
transport.exchange = unsafe
with pytest.raises(CredentialUnavailable, match="SECRET_MANAGER_PLAINTEXT_REJECTED"):
SecretManagerV2(transport).issue(credential_request(), NOW)
def test_execution_manager_records_simulation_without_native_claim(tmp_path: Path) -> None:
evidence = EvidenceChain(StateStore(tmp_path / "state.sqlite3"), "task-execution")
adapter = OpenClawNativeAdapter(
Provider("openclaw"), "systemd-credential:openclaw-gateway-candidate"
)
result = ExecutionManager(evidence).run_native_canary(
adapter,
task_id="task-execution",
parent_session_id="parent-1",
recipient_binding={"recipient_id": "weixin-owner"},
text="candidate canary",
)
assert result.broker_session_verified is True
assert result.openclaw_native_child_session_verified is False
assert result.simulation is True
assert result.session_closed is True
assert evidence.verify()
@@ -0,0 +1,89 @@
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import stat
import pytest
from aios_autonomous_release_candidate.errors import StateConflict
from aios_autonomous_release_candidate.models import (
ResourceBinding,
RiskClass,
StepState,
TaskPlan,
WorkflowStep,
)
from aios_autonomous_release_candidate.store import StateStore
from aios_autonomous_release_candidate.workflow import WorkflowEngine
OID = "a" * 40
def plan() -> TaskPlan:
return TaskPlan(
task_id="workflow-test",
intent="exercise durable workflow",
binding=ResourceBinding("aios/aios", "patch/workflow-test", OID, ("services/test",)),
risk=RiskClass.LOW,
steps=(
WorkflowStep("build", "builder", ("file.write",), max_attempts=2, timeout_seconds=30),
WorkflowStep("test", "tester", ("test.execute",), depends_on=("build",), max_attempts=2),
),
evidence_requirements=("tests",),
)
def test_store_is_private_and_rejects_schema_drift(tmp_path: Path) -> None:
state = tmp_path / "private" / "state.sqlite3"
StateStore(state)
assert stat.S_IMODE(state.stat().st_mode) == 0o600
assert stat.S_IMODE(state.parent.stat().st_mode) & 0o027 == 0
with StateStore(state).transaction() as connection:
connection.execute("UPDATE metadata SET value='999' WHERE key='schema_version'")
with pytest.raises(StateConflict, match="SCHEMA_VERSION_UNSUPPORTED"):
StateStore(state)
def test_dependency_retry_and_terminal_failure_are_durable(tmp_path: Path) -> None:
store = StateStore(tmp_path / "state.sqlite3")
engine = WorkflowEngine(store)
task = plan()
engine.plan(task)
assert engine.next_steps(task).ready_steps == ("build",)
assert store.list_steps(task.task_id)["build"].state == StepState.READY
engine.claim(task.task_id, "build", "writer", "session-1")
engine.start(task.task_id, "build", "writer", "session-1")
engine.complete(task.task_id, "build", result_digest="b" * 64)
assert engine.next_steps(task).ready_steps == ("test",)
engine.claim(task.task_id, "test", "tester", "session-2")
engine.start(task.task_id, "test", "tester", "session-2")
assert engine.fail(task, "test", "transient", retryable=True) == StepState.PENDING
assert engine.next_steps(task).ready_steps == ("test",)
engine.claim(task.task_id, "test", "tester", "session-3")
engine.start(task.task_id, "test", "tester", "session-3")
assert engine.fail(task, "test", "still broken", retryable=True) == StepState.FAILED
decision = engine.next_steps(task)
assert decision.blocked is True
assert decision.reason == "terminal_step_failure"
def test_claim_is_atomic_under_contention(tmp_path: Path) -> None:
store = StateStore(tmp_path / "state.sqlite3")
engine = WorkflowEngine(store)
task = plan()
engine.plan(task)
engine.next_steps(task)
def claim(worker: str) -> str:
try:
engine.claim(task.task_id, "build", worker, f"session-{worker}")
return "claimed"
except StateConflict:
return "rejected"
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(claim, ("one", "two")))
assert sorted(results) == ["claimed", "rejected"]
@@ -1 +0,0 @@
-51
View File
@@ -1,51 +0,0 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from aios_single_owner_governance.models import (
ApprovalContext,
MachineGate,
ProtectionSnapshot,
Review,
)
HEAD = "a" * 40
NOW = datetime(2026, 7, 28, 19, 0, tzinfo=timezone.utc)
CONTEXTS = ("aios/machine-gates", "aios/secret-scan", "aios/evidence-closure")
def owner_review(**changes) -> Review:
values = dict(
reviewer="owner", reviewer_id=2, state="APPROVED", head_oid=HEAD,
reviewed_at=NOW - timedelta(minutes=1), stale=False, dismissed=False,
)
values.update(changes)
return Review(**values)
def gates(*, state="success", head_oid=HEAD):
return tuple(MachineGate(name, state, head_oid, NOW) for name in CONTEXTS)
def context(**changes) -> ApprovalContext:
values = dict(
current_head_oid=HEAD, requester="agent", executor="merge-executor",
owner="owner", owner_id=2, reviews=(owner_review(),), machine_gates=gates(),
required_machine_contexts=CONTEXTS, expires_at=NOW + timedelta(hours=1),
wip=False, now=NOW,
)
values.update(changes)
return ApprovalContext(**values)
def legacy_protection(version=1) -> ProtectionSnapshot:
return ProtectionSnapshot(
"governance/approvals", 2, ("codex", "gpt"), ("codex", "gpt"),
True, False, (), False, False, False, version,
)
def single_owner_protection(version=2) -> ProtectionSnapshot:
return ProtectionSnapshot(
"governance/approvals", 1, ("owner",), ("owner",), True, True,
CONTEXTS, False, False, False, version,
)
@@ -1,72 +0,0 @@
from __future__ import annotations
from pathlib import Path
import unittest
from aios_single_owner_governance.fixture import DisposableGiteaFixture
from aios_single_owner_governance.protection import (
BranchProtectionCASMigrationAdapter,
DormantViolation,
snapshot_sha256,
)
from aios_single_owner_governance.policy import DormantOperationGuard, OperationDenied
from .helpers import legacy_protection, single_owner_protection
class LiveStore:
fixture = False
def read(self, branch):
return legacy_protection()
def compare_and_swap(self, branch, expected_sha256, target):
raise AssertionError("live compare_and_swap must never be invoked")
class DormantInvariantTests(unittest.TestCase):
def test_live_protection_apply_and_rollback_are_denied(self):
adapter = BranchProtectionCASMigrationAdapter()
old, new = legacy_protection(), single_owner_protection()
plan = adapter.plan(old, new, snapshot_sha256(old))
with self.assertRaises(DormantViolation):
adapter.apply_fixture(LiveStore(), plan)
with self.assertRaises(DormantViolation):
adapter.rollback_fixture(LiveStore(), plan)
def test_unsafe_policy_cannot_be_planned(self):
adapter = BranchProtectionCASMigrationAdapter()
old = legacy_protection()
for unsafe in (
single_owner_protection().__class__(**{**single_owner_protection().as_dict(), "approval_whitelist": ("owner",), "merge_whitelist": ("owner",), "status_contexts": tuple(single_owner_protection().status_contexts), "direct_push": True}),
single_owner_protection().__class__(**{**single_owner_protection().as_dict(), "approval_whitelist": ("owner",), "merge_whitelist": ("owner",), "status_contexts": tuple(single_owner_protection().status_contexts), "force_push": True}),
single_owner_protection().__class__(**{**single_owner_protection().as_dict(), "approval_whitelist": ("owner",), "merge_whitelist": ("owner",), "status_contexts": tuple(single_owner_protection().status_contexts), "branch_deletion": True}),
):
with self.assertRaises(DormantViolation):
adapter.plan(old, unsafe, snapshot_sha256(old))
def test_disposable_fixture_is_destroyed(self):
fixture = DisposableGiteaFixture(legacy_protection())
root = fixture.root
with fixture:
self.assertTrue(root.exists())
self.assertTrue(fixture.destroyed)
self.assertFalse(root.exists())
def test_permanent_and_pre_activation_writes_are_denied(self):
guard = DormantOperationGuard()
for operation in (
"protected_branch.direct_push",
"governance.direct_push",
"tag.direct_write",
"git.force_push",
"production.without_owner",
"branch_protection.live_update",
"single_owner_policy.activate",
):
with self.subTest(operation=operation), self.assertRaises(OperationDenied):
guard.authorize(operation)
if __name__ == "__main__":
unittest.main()
@@ -1,113 +0,0 @@
from __future__ import annotations
import contextlib
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from aios_single_owner_governance.evidence import (
ActivationEvidenceClosureBuilder,
ApprovalMirrorStatusVerifier,
)
from aios_single_owner_governance.service import main, runtime_state
from aios_single_owner_governance.status import (
DormantStatusProducer,
MachineStatusProducer,
)
from .helpers import HEAD
class EvidenceStatusServiceTests(unittest.TestCase):
def test_complete_evidence_set_builds_canonical_closure(self):
evidence = {
"activation_prerequisites_commit_oid": "a" * 40,
"activation_prerequisites_tree_oid": "b" * 40,
"artifact_sha256": "c" * 64,
"rollback_artifact_sha256": "d" * 64,
"validator_sha256": "e" * 64,
"state_machine_sha256": "f" * 64,
"branch_protection_adapter_sha256": "0" * 64,
"rollback_configuration_sha256": "1" * 64,
"test_evidence_sha256": "2" * 64,
"deployment_manifest_sha256": "3" * 64,
"dormant_unit_manifest_sha256": "4" * 64,
"dormant_deployment_evidence_sha256": "5" * 64,
}
closure = ActivationEvidenceClosureBuilder().build(evidence)
self.assertEqual(len(closure.closure_sha256), 64)
with self.assertRaises(ValueError):
ActivationEvidenceClosureBuilder().build({})
def test_mirror_verifier_requires_all_four_refs_and_fsck(self):
verifier = ApprovalMirrorStatusVerifier()
self.assertTrue(verifier.verify(
HEAD, gitea_oid=HEAD, quarantine_oid=HEAD,
authoritative_oid=HEAD, controller_visible_oid=HEAD,
fsck_connectivity="PASS",
))
self.assertFalse(verifier.verify(
HEAD, gitea_oid=HEAD, quarantine_oid=HEAD,
authoritative_oid="b" * 40, controller_visible_oid=HEAD,
fsck_connectivity="PASS",
))
def test_machine_status_is_fixture_only_and_dormant(self):
with self.assertRaises(DormantStatusProducer):
MachineStatusProducer().produce("aios/machine-gates", "success", HEAD)
with self.assertRaises(DormantStatusProducer):
MachineStatusProducer(enabled=True, fixture=True).produce(
"aios/machine-gates", "success", HEAD
)
record = MachineStatusProducer(fixture=True).produce(
"aios/evidence-closure", "success", HEAD
)
self.assertEqual(record.head_oid, HEAD)
def test_service_self_check_is_dormant_and_activate_is_rejected(self):
output = io.StringIO()
with contextlib.redirect_stdout(output):
self.assertEqual(main(["--component", "owner-review-adapter", "--self-check"]), 0)
state = json.loads(output.getvalue())
self.assertFalse(state["active"])
self.assertFalse(state["production_mutation"])
with contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
main(["--component", "owner-review-adapter", "--activate"])
def test_write_flags_cannot_be_enabled(self):
for name in (
"AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED",
"AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE",
"AIOS_GITOPS_WRITES_ENABLED",
):
with self.subTest(name=name), patch.dict(os.environ, {name: "true"}, clear=False):
with contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
main(["--component", "machine-status-producer", "--self-check"])
def test_service_entrypoint_restart_recovery_across_processes(self):
with tempfile.TemporaryDirectory(prefix="aios-governance-fixture-") as root:
command = [
sys.executable, "-m", "aios_single_owner_governance.service",
"--component", "owner-review-adapter", "--self-check",
"--checkpoint-dir", root,
]
first = subprocess.run(command, text=True, capture_output=True, check=True)
second = subprocess.run(command, text=True, capture_output=True, check=True)
first_state = json.loads(first.stdout)
second_state = json.loads(second.stdout)
self.assertFalse(first_state["restart_recovery"])
self.assertTrue(second_state["restart_recovery"])
self.assertEqual(second_state["prior_invocations"], 1)
self.assertFalse(second_state["production_mutation"])
if __name__ == "__main__":
unittest.main()
@@ -1,771 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
import sqlite3
import subprocess
import hashlib
import os
from datetime import datetime, timedelta, timezone
import pytest
from jsonschema import Draft202012Validator, ValidationError
from aios_single_owner_governance.activation import (
ActivationEngine,
InjectedCrash,
_verify_approved_entities,
validate_bindings,
)
import aios_single_owner_governance.validator as validator_module
import aios_single_owner_governance.state_machine as state_machine_module
import aios_single_owner_governance.activation as activation_module
from aios_single_owner_governance.gitea_protected_branch import (
CASConflict,
FileProtectedBranchAdapter,
SQLiteProtectedBranchAdapter,
UnsafePolicy,
calculate_snapshot_sha256,
validate_target,
)
from aios_single_owner_governance.journal import sha256_json
MACHINE_CONTEXTS = {
"aios/validator",
"aios/state-machine",
"aios/branch-protection",
"aios/secret-scan",
"aios/evidence-closure",
}
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, sort_keys=True) + "\n", encoding="utf-8")
def legacy_row() -> dict:
return {
"id": 7,
"repo_id": 1,
"branch_name": "governance/approvals",
"can_push": 0,
"enable_whitelist": 0,
"whitelist_user_i_ds": "null",
"whitelist_team_i_ds": "null",
"enable_merge_whitelist": 1,
"whitelist_deploy_keys": 0,
"merge_whitelist_user_i_ds": "[42,43]",
"merge_whitelist_team_i_ds": "null",
"enable_status_check": 0,
"status_check_contexts": "[]",
"enable_approvals_whitelist": 1,
"approvals_whitelist_user_i_ds": "[42,43]",
"approvals_whitelist_team_i_ds": "null",
"required_approvals": 2,
"block_on_rejected_reviews": 1,
"block_on_official_review_requests": 1,
"block_on_outdated_branch": 1,
"dismiss_stale_approvals": 1,
"ignore_stale_approvals": 0,
"require_signed_commits": 0,
"protected_file_patterns": "",
"unprotected_file_patterns": "",
"created_unix": 1,
"updated_unix": 10,
}
def owner_row() -> dict:
row = legacy_row()
row.update(
{
"approvals_whitelist_user_i_ds": "[2]",
"required_approvals": 1,
"enable_status_check": 1,
"status_check_contexts": json.dumps(sorted(MACHINE_CONTEXTS), separators=(",", ":")),
"updated_unix": 11,
}
)
return row
def git_output(git_dir: Path, *args: str, stdin: str | None = None) -> str:
return subprocess.run(
["git", f"--git-dir={git_dir}", *args], input=stdin, text=True,
capture_output=True, check=True,
env={
**__import__("os").environ,
"GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "test@example.invalid",
"GIT_COMMITTER_NAME": "Test", "GIT_COMMITTER_EMAIL": "test@example.invalid",
},
).stdout.strip()
def commit_files(
git_dir: Path, files: dict[str, str], message: str, *, parent: str | None = None
) -> tuple[str, str]:
root: dict[str, object] = {}
for path, raw in files.items():
node = root
parts = path.split("/")
for directory in parts[:-1]:
node = node.setdefault(directory, {}) # type: ignore[assignment]
node[parts[-1]] = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=raw)
def build_tree(node: dict[str, object]) -> str:
lines = []
for name in sorted(node):
value = node[name]
if isinstance(value, dict):
lines.append(f"040000 tree {build_tree(value)}\t{name}")
else:
lines.append(f"100644 blob {value}\t{name}")
return git_output(git_dir, "mktree", stdin="\n".join(lines) + "\n")
tree = build_tree(root)
args = ["commit-tree", tree]
if parent is not None:
args.extend(("-p", parent))
return git_output(git_dir, *args, stdin=message + "\n"), tree
def evidence_fixture(tmp_path: Path, execution_binding: dict) -> dict:
git_dir = tmp_path / "evidence.git"
subprocess.run(["git", "init", "--bare", str(git_dir)], capture_output=True, check=True)
package_root = Path(activation_module.__file__).resolve().parent
source_files: dict[str, str] = {}
runtime_sources = []
for name in (
"activation.py", "gitea_protected_branch.py", "journal.py",
"state_machine.py", "validator.py",
):
source = package_root / name
raw = source.read_text(encoding="utf-8")
git_path = (
"services/aios-single-owner-governance/"
f"aios_single_owner_governance/{name}"
)
source_files[git_path] = raw
runtime_sources.append({
"git_path": git_path,
"runtime_path": str(source.resolve()),
"sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
})
implementation_oid, implementation_tree = commit_files(
git_dir, source_files, "implementation"
)
closure_path = "evidence/live-activation-closure.json"
closure_raw = '{"result":"PASS"}\n'
closure_oid, _ = commit_files(
git_dir, {**source_files, closure_path: closure_raw},
"closure", parent=implementation_oid
)
closure_sha = hashlib.sha256(closure_raw.encode()).hexdigest()
entities = {}
for name, raw in {
"artifact": b"approved-artifact\n",
"rollback_artifact": b"approved-rollback\n",
"test_evidence": b"approved-tests\n",
}.items():
path = tmp_path / "artifacts" / f"{name}.bin"
path.parent.mkdir(exist_ok=True)
path.write_bytes(raw)
entities[f"{name}_path"] = str(path.resolve())
entities[f"{name}_sha256"] = hashlib.sha256(raw).hexdigest()
head_oid = git_output(git_dir, "hash-object", "-w", "--stdin", stdin="head\n")
expiry = datetime.now(timezone.utc) + timedelta(hours=1)
producer_id = 99
producer_name = "trusted-machine-producer"
execution_binding = {
**execution_binding,
"evidence_closure_oid": closure_oid,
"evidence_closure_path": closure_path,
"evidence_closure_sha256": closure_sha,
"implementation_commit_oid": implementation_oid,
"implementation_tree_oid": implementation_tree,
"runtime_sources": runtime_sources,
"machine_status_producer_id": producer_id,
"machine_status_producer_name": producer_name,
**entities,
}
manifest = {
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
"expires_at": expiry.isoformat().replace("+00:00", "Z"),
"execution_binding": execution_binding,
"execution_binding_sha256": sha256_json(execution_binding),
**{key: execution_binding[key] for key in (
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path",
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
"evidence_closure_oid", "evidence_closure_path", "evidence_closure_sha256",
"machine_status_producer_id", "machine_status_producer_name",
)},
}
manifest_raw = json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n"
manifest_blob = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=manifest_raw)
leaf = git_output(git_dir, "mktree", stdin=f"100644 blob {manifest_blob}\tactivation.json\n")
approvals = git_output(git_dir, "mktree", stdin=f"040000 tree {leaf}\tsingle-owner\n")
tree = git_output(git_dir, "mktree", stdin=f"040000 tree {approvals}\tapprovals\n")
merge_oid = git_output(git_dir, "commit-tree", tree, stdin="approved\n")
git_output(git_dir, "update-ref", "refs/heads/governance/approvals", merge_oid)
db = tmp_path / "approval.db"
with sqlite3.connect(db) as connection:
connection.executescript(
"CREATE TABLE issue(id INTEGER,repo_id INTEGER,is_closed INTEGER);"
"CREATE TABLE pull_request(issue_id INTEGER,\"index\" INTEGER,base_branch TEXT,has_merged INTEGER,merged_commit_id TEXT,merged_unix INTEGER);"
"CREATE TABLE user(id INTEGER,lower_name TEXT);"
"CREATE TABLE review(id INTEGER,type INTEGER,reviewer_id INTEGER,issue_id INTEGER,commit_id TEXT,stale INTEGER,dismissed INTEGER,created_unix INTEGER);"
"CREATE TABLE commit_status(id INTEGER,repo_id INTEGER,state TEXT,sha TEXT,context TEXT,creator_id INTEGER);"
)
connection.execute("INSERT INTO issue VALUES(10,1,1)")
merged_unix = int(datetime.now(timezone.utc).timestamp())
connection.execute("INSERT INTO pull_request VALUES(10,31,'governance/approvals',1,?,?)", (merge_oid, merged_unix))
connection.execute("INSERT INTO user VALUES(2,'owner')")
connection.execute("INSERT INTO user VALUES(?,?)", (producer_id, producer_name))
connection.execute(
"INSERT INTO review VALUES(52,1,2,10,?,0,0,?)",
(head_oid, int(datetime.now(timezone.utc).timestamp())),
)
machine_checks = {}
for index, context in enumerate(sorted(MACHINE_CONTEXTS), 101):
connection.execute(
"INSERT INTO commit_status VALUES(?,1,'success',?,?,?)",
(index, head_oid, context, producer_id),
)
machine_checks[context] = {
"state": "success", "head_oid": head_oid, "status_id": index,
"creator_id": producer_id, "creator_login": producer_name,
}
return {
"git_dir": git_dir, "merge_oid": merge_oid, "closure_oid": closure_oid,
"head_oid": head_oid, "db": db, "expiry": expiry.isoformat().replace("+00:00", "Z"),
"manifest_path": "approvals/single-owner/activation.json",
"manifest_blob": manifest_blob,
"manifest_sha256": hashlib.sha256(manifest_raw.encode()).hexdigest(),
"execution_binding": execution_binding,
"machine_checks": machine_checks,
**execution_binding,
}
def bindings(tmp_path: Path, adapter: FileProtectedBranchAdapter, **overrides) -> dict:
validator_path = Path(validator_module.__file__).resolve()
state_machine_path = Path(state_machine_module.__file__).resolve()
validator_sha = hashlib.sha256(validator_path.read_bytes()).hexdigest()
state_machine_sha = hashlib.sha256(state_machine_path.read_bytes()).hexdigest()
execution_binding = {
"task_id": "AIOS-LIVE-ACTIVATION-TEST",
"operation_id": "op-live-001",
"expected_validator_sha256": validator_sha,
"expected_state_machine_sha256": state_machine_sha,
"expected_branch_protection_sha256": adapter.calculate_snapshot_sha256(legacy_row()),
"expected_gitea_schema_sha256": "0" * 64,
"target_branch_protection_sha256": calculate_snapshot_sha256(owner_row()),
"backend": "file",
"repo_id": 1,
"branch": "governance/approvals",
"requester": "agent",
"executor": "activation-executor",
"forbidden_operations": {
"main": False, "governance_direct": False, "integration": False,
"tags": False, "force_push": False,
},
}
evidence = evidence_fixture(tmp_path, execution_binding)
value = {
"task_id": "AIOS-LIVE-ACTIVATION-TEST",
"operation_id": "op-live-001",
"expected_governance_oid": evidence["merge_oid"],
"observed_governance_oid": evidence["merge_oid"],
"expected_validator_sha256": validator_sha,
"observed_validator_sha256": validator_sha,
"expected_state_machine_sha256": state_machine_sha,
"observed_state_machine_sha256": state_machine_sha,
"validator_path": str(validator_path),
"state_machine_path": str(state_machine_path),
"governance_git_dir": str(evidence["git_dir"].resolve()),
"governance_ref": "refs/heads/governance/approvals",
"evidence_git_dir": str(evidence["git_dir"].resolve()),
"approval_db_path": str(evidence["db"].resolve()),
"expected_branch_protection_sha256": adapter.calculate_snapshot_sha256(legacy_row()),
"activation_approval_merge_oid": evidence["merge_oid"],
"approval_request_head_oid": evidence["head_oid"],
"activation_approval_pr_number": 31,
"activation_manifest_path": evidence["manifest_path"],
"activation_manifest_blob_oid": evidence["manifest_blob"],
"activation_manifest_sha256": evidence["manifest_sha256"],
"expires_at": evidence["expiry"],
"evidence_closure_oid": evidence["closure_oid"],
"observed_evidence_closure_oid": evidence["closure_oid"],
**{key: evidence[key] for key in (
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path",
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
"evidence_closure_path", "evidence_closure_sha256",
"machine_status_producer_id", "machine_status_producer_name",
)},
"owner_review_oid": 52,
"observed_owner_review_oid": 52,
"requester": "agent",
"executor": "activation-executor",
"wip": False,
"machine_checks": evidence["machine_checks"],
"forbidden_operations": {
"main": False,
"governance_direct": False,
"integration": False,
"tags": False,
"force_push": False,
},
"target_branch_protection": owner_row(),
"backend": "file",
"backend_path": str(adapter.path.resolve()),
"repo_id": 1,
"branch": "governance/approvals",
"expected_gitea_schema_sha256": "0" * 64,
}
value.update(overrides)
return value
def harness(tmp_path: Path):
backend = tmp_path / "protected.json"
write_json(backend, legacy_row())
adapter = FileProtectedBranchAdapter(backend)
engine = ActivationEngine(adapter, tmp_path / "state", tmp_path / "run" / "activation.lock")
request = bindings(tmp_path, adapter)
return backend, adapter, engine, request
def test_live_plan_success(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
result = engine.plan(request)
assert result["status"] == "PLAN_READY"
assert len(result["plan_id"]) == 64
def test_activation_bindings_schema_matches_runtime_contract(tmp_path: Path) -> None:
_, _, _, request = harness(tmp_path)
schema_path = Path(activation_module.__file__).resolve().parents[1] / "activation-bindings.schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
validator.validate(request)
invalid = json.loads(json.dumps(request))
invalid["machine_checks"][sorted(MACHINE_CONTEXTS)[0]]["status_id"] = 0
with pytest.raises(ValidationError):
validator.validate(invalid)
def test_runtime_source_must_be_actual_loaded_module_not_decoy(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
source = Path(request["runtime_sources"][0]["runtime_path"])
decoy = tmp_path / "decoy.py"
decoy.write_bytes(source.read_bytes())
request["runtime_sources"][0]["runtime_path"] = str(decoy.resolve())
with pytest.raises(RuntimeError, match="RUNTIME_SOURCE_PATH_MISMATCH"):
_verify_approved_entities(request, Path(request["evidence_git_dir"]))
@pytest.mark.parametrize("path_field", ["artifact_path", "rollback_artifact_path", "test_evidence_path"])
def test_approved_entity_digest_is_enforced(tmp_path: Path, path_field: str) -> None:
_, _, engine, request = harness(tmp_path)
Path(request[path_field]).write_bytes(b"tampered\n")
with pytest.raises(RuntimeError, match="APPROVED_ENTITY_DIGEST_MISMATCH"):
engine.plan(request)
def test_evidence_closure_must_be_direct_child_commit(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
git_dir = Path(request["evidence_git_dir"])
unrelated, _ = commit_files(
git_dir,
{request["evidence_closure_path"]: '{"result":"PASS"}\n'},
"unrelated",
)
request["evidence_closure_oid"] = unrelated
request["observed_evidence_closure_oid"] = unrelated
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_pr_merge_after_expiry_is_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute(
"UPDATE pull_request SET merged_unix=?",
(int(_parse_test_utc(request["expires_at"]).timestamp()) + 1,),
)
with pytest.raises(RuntimeError, match="ACTIVATION_APPROVAL_MERGED_AFTER_EXPIRY"):
engine.plan(request)
def _parse_test_utc(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def test_machine_status_must_match_bound_producer_and_observed_row(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
context = sorted(MACHINE_CONTEXTS)[0]
request["machine_checks"][context]["status_id"] += 1000
with pytest.raises(RuntimeError, match="MACHINE_GATE_BINDING_MISMATCH"):
engine.plan(request)
def test_latest_machine_status_from_unapproved_producer_is_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
context = sorted(MACHINE_CONTEXTS)[0]
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("INSERT INTO user VALUES(100,'untrusted-producer')")
connection.execute(
"INSERT INTO commit_status VALUES(999,1,'success',?,?,100)",
(request["approval_request_head_oid"], context),
)
with pytest.raises(RuntimeError, match="MACHINE_GATE_PRODUCER_MISMATCH"):
engine.plan(request)
def test_live_activate_verify_status_success(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
activated = engine.activate(request, planned["plan_id"])
assert activated["status"] == "ACTIVATED"
assert adapter.read_current() == owner_row()
assert engine.verify(request, request["operation_id"])["status"] == "VERIFIED"
assert engine.status(request["operation_id"])["transaction"]["phase"] == "COMMITTED"
assert (engine.journal.root / "current-snapshot.json").exists()
assert (engine.journal.root / "rollback-snapshot.json").exists()
def test_live_rollback_success(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ALREADY_ROLLED_BACK"
def test_terminal_rollback_cannot_be_reactivated(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
engine.rollback(request, request["operation_id"], planned["plan_id"])
with pytest.raises(RuntimeError, match="OPERATION_TERMINAL_ROLLED_BACK"):
engine.activate(request, planned["plan_id"])
def test_verify_and_rollback_remain_available_after_approval_expiry(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
future = datetime.now(timezone.utc) + timedelta(hours=2)
class FutureDateTime(datetime):
@classmethod
def now(cls, tz=None):
return future if tz is not None else future.replace(tzinfo=None)
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
assert engine.verify(request, request["operation_id"])["status"] == "VERIFIED"
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
def test_prepared_crash_can_recover_after_approval_expiry(tmp_path: Path, monkeypatch) -> None:
_, adapter, engine, request = harness(tmp_path)
request["crash_at"] = "before_apply"
planned = engine.plan(request)
with pytest.raises(InjectedCrash):
engine.activate(request, planned["plan_id"])
request.pop("crash_at")
future = datetime.now(timezone.utc) + timedelta(hours=2)
class FutureDateTime(datetime):
@classmethod
def now(cls, tz=None):
return future if tz is not None else future.replace(tzinfo=None)
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
assert engine.activate(request, planned["plan_id"])["status"] == "ACTIVATED"
assert adapter.read_current() == owner_row()
def test_fresh_activation_rechecks_expiry_at_prepared_boundary(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
expiry = _parse_test_utc(request["expires_at"])
moments = iter((
expiry - timedelta(seconds=2),
expiry - timedelta(seconds=1),
expiry + timedelta(seconds=1),
))
monkeypatch.setattr(activation_module, "_utc_now", lambda: next(moments))
with pytest.raises(RuntimeError, match="APPROVAL_EXPIRED"):
engine.activate(request, planned["plan_id"])
assert adapter.read_current() == legacy_row()
assert engine.journal.load(request["operation_id"]) is None
def test_fresh_activation_rejects_snapshot_drift_at_prepared_boundary(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
original_read = adapter.read_current
reads = 0
def drifting_read() -> dict:
nonlocal reads
reads += 1
if reads == 2:
drifted = original_read()
drifted["can_push"] = 1
drifted["updated_unix"] = 777
return drifted
return original_read()
monkeypatch.setattr(adapter, "read_current", drifting_read)
with pytest.raises(CASConflict, match="PREPARED_BOUNDARY_SNAPSHOT_CHANGED"):
engine.activate(request, planned["plan_id"])
assert original_read() == legacy_row()
assert engine.journal.load(request["operation_id"]) is None
def test_rollback_uses_durable_authorization_after_live_status_rerun(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
context = sorted(MACHINE_CONTEXTS)[0]
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute(
"INSERT INTO commit_status VALUES(999,1,'success',?,?,?)",
(
request["approval_request_head_oid"], context,
request["machine_status_producer_id"],
),
)
result = engine.rollback(request, request["operation_id"], planned["plan_id"])
assert result["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
def test_missing_approval_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request.pop("owner_review_oid")
with pytest.raises(RuntimeError, match="MISSING_BINDINGS"):
engine.plan(request)
@pytest.mark.parametrize(
("field", "value", "message"),
[
("observed_owner_review_oid", 99, "WRONG_APPROVAL_OID_DENIED"),
("observed_governance_oid", "f" * 40, "WRONG_GOVERNANCE_OID_DENIED"),
("observed_evidence_closure_oid", "f" * 40, "WRONG_EVIDENCE_CLOSURE_DENIED"),
],
)
def test_wrong_binding_denied(tmp_path: Path, field: str, value: object, message: str) -> None:
_, _, engine, request = harness(tmp_path)
request[field] = value
with pytest.raises(RuntimeError, match=message):
engine.plan(request)
def test_cas_conflict_denied(tmp_path: Path) -> None:
backend, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
changed = legacy_row()
changed["updated_unix"] = 12
write_json(backend, changed)
with pytest.raises(CASConflict):
engine.activate(request, planned["plan_id"])
@pytest.mark.parametrize("crash_at", ["before_apply", "during_apply", "after_apply_before_receipt", "after_commit_wal"])
def test_process_crash_restart_recovery(tmp_path: Path, crash_at: str) -> None:
_, adapter, engine, request = harness(tmp_path)
request["crash_at"] = crash_at
planned = engine.plan(request)
with pytest.raises(InjectedCrash):
engine.activate(request, planned["plan_id"])
request.pop("crash_at")
result = engine.activate(request, planned["plan_id"])
expected_status = "ALREADY_COMMITTED" if crash_at == "after_commit_wal" else "ACTIVATED"
assert result["status"] == expected_status
assert adapter.read_current() == owner_row()
assert engine.journal.receipt_path(request["operation_id"]).exists()
assert engine.journal.audit.has_event(request["operation_id"], "SINGLE_OWNER_ACTIVATION_COMMITTED")
def test_post_activation_verify_failure_rolls_back(tmp_path: Path, monkeypatch) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
monkeypatch.setattr(adapter, "verify_applied", lambda _: False)
with pytest.raises(RuntimeError, match="POST_ACTIVATION_VERIFY_FAILED"):
engine.activate(request, planned["plan_id"])
assert adapter.read_current() == legacy_row()
assert engine.journal.load(request["operation_id"])["phase"] == "ROLLED_BACK"
def test_operation_id_idempotency(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert engine.activate(request, planned["plan_id"])["status"] == "ALREADY_COMMITTED"
@pytest.mark.parametrize("crash_at", ["after_rollback_cas", "after_rollback_wal"])
def test_rollback_crash_restart_repairs_closure(tmp_path: Path, crash_at: str) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
request["crash_at"] = crash_at
with pytest.raises(InjectedCrash):
engine.rollback(request, request["operation_id"], planned["plan_id"])
request.pop("crash_at")
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ALREADY_ROLLED_BACK"
assert adapter.read_current() == legacy_row()
assert engine.journal.receipt_path(request["operation_id"]).exists()
assert engine.journal.audit.has_event(request["operation_id"], "ACTIVATION_ROLLED_BACK")
def test_operation_id_payload_mismatch_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
changed = dict(request)
approval_alias = tmp_path / "approval-alias.db"
os.link(request["approval_db_path"], approval_alias)
changed["approval_db_path"] = str(approval_alias)
with pytest.raises(RuntimeError, match="PAYLOAD_MISMATCH"):
engine.activate(changed, planned["plan_id"])
@pytest.mark.parametrize(
("field", "value"),
[("operation_id", "op-live-REPLAY"), ("task_id", "REPLAY-TASK")],
)
def test_approved_execution_binding_replay_denied(tmp_path: Path, field: str, value: str) -> None:
_, _, engine, request = harness(tmp_path)
request[field] = value
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_expiry_must_come_from_approved_manifest(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["expires_at"] = (datetime.now(timezone.utc) + timedelta(hours=12)).isoformat().replace("+00:00", "Z")
with pytest.raises(RuntimeError, match="APPROVAL_EXPIRY_BINDING_MISMATCH"):
engine.plan(request)
def test_target_snapshot_must_match_approved_manifest(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["target_branch_protection"] = {**owner_row(), "updated_unix": 99}
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_approval_database_must_be_backend_database(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
other_db = tmp_path / "other.db"
other_db.write_bytes(Path(request["approval_db_path"]).read_bytes())
request.update(backend="sqlite", backend_path=str(other_db.resolve()))
with pytest.raises(RuntimeError, match="APPROVAL_DB_BACKEND_DB_IDENTITY_MISMATCH"):
engine.plan(request)
def test_audit_chain_continuity_and_credential_redaction(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["api_token"] = "must-not-appear"
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert len(engine.journal.audit.verify()) == 64
raw = (engine.journal.root / "audit.jsonl").read_text(encoding="utf-8")
assert "must-not-appear" not in raw
assert "[REDACTED]" not in raw # only hashes/events are persisted to audit
@pytest.mark.parametrize(
"mutation",
[
lambda request: request.update(requester="owner"),
lambda request: request.update(executor="owner"),
lambda request: request.update(wip=True),
lambda request: request.update(machine_checks={"anything": {"state": "success", "head_oid": request["approval_request_head_oid"]}}),
lambda request: request.update(forbidden_operations={"main": True}),
lambda request: request.update(forbidden_operations={"tags": True}),
lambda request: request.update(forbidden_operations={"force_push": True}),
],
)
def test_negative_governance_acceptance(tmp_path: Path, mutation) -> None:
_, _, engine, request = harness(tmp_path)
mutation(request)
with pytest.raises(RuntimeError):
engine.plan(request)
@pytest.mark.parametrize(
"mutation",
[
lambda target: target.update(enable_approvals_whitelist=0),
lambda target: target.update(enable_merge_whitelist=0),
lambda target: target.update(status_check_contexts="[]"),
],
)
def test_unsafe_policy_enable_fields_denied(tmp_path: Path, mutation) -> None:
_, _, _, request = harness(tmp_path)
mutation(request["target_branch_protection"])
with pytest.raises(UnsafePolicy):
validate_target(legacy_row(), request["target_branch_protection"])
@pytest.mark.parametrize("invalid_owner", ["codex", "gpt"])
def test_owner_review_must_come_from_gitea_database(tmp_path: Path, invalid_owner: str) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("UPDATE user SET lower_name=? WHERE id=2", (invalid_owner,))
with pytest.raises(RuntimeError, match="OWNER_APPROVAL_INVALID"):
engine.plan(request)
def test_stale_owner_review_denied_from_gitea_database(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("UPDATE review SET stale=1 WHERE id=52")
with pytest.raises(RuntimeError, match="OWNER_APPROVAL_INVALID"):
engine.plan(request)
def create_sqlite_fixture(path: Path) -> None:
row = legacy_row()
columns = []
for key, value in row.items():
kind = "INTEGER" if isinstance(value, int) else "TEXT"
columns.append(f"{key} {kind}")
with sqlite3.connect(path) as connection:
connection.execute(f"CREATE TABLE protected_branch ({','.join(columns)})")
connection.execute(
f"INSERT INTO protected_branch ({','.join(row)}) VALUES ({','.join('?' for _ in row)})",
list(row.values()),
)
def test_sqlite_full_row_cas_and_rollback(tmp_path: Path) -> None:
db = tmp_path / "gitea.db"
create_sqlite_fixture(db)
probe = SQLiteProtectedBranchAdapter(db, expected_schema_sha256="0" * 64)
adapter = SQLiteProtectedBranchAdapter(db, expected_schema_sha256=probe.schema_sha256())
previous = adapter.read_current()
applied = adapter.apply_atomic(calculate_snapshot_sha256(previous), owner_row())
assert applied == owner_row()
restored = adapter.rollback_atomic(calculate_snapshot_sha256(applied), previous)
assert restored == previous
@@ -1,126 +0,0 @@
from __future__ import annotations
from pathlib import Path
import tempfile
import unittest
from aios_single_owner_governance.fixture import DisposableGiteaFixture
from aios_single_owner_governance.lock import MaintenanceLock
from aios_single_owner_governance.models import GovernanceState
from aios_single_owner_governance.protection import (
BranchProtectionCASMigrationAdapter,
CASConflict,
MemoryProtectionStore,
snapshot_sha256,
)
from aios_single_owner_governance.rollback import (
ProtectionCompensationSaga,
RollbackConfigurationManager,
)
from aios_single_owner_governance.state_machine import GovernanceStateMachine, InvalidTransition
from .helpers import legacy_protection, single_owner_protection
from .helpers import context
from aios_single_owner_governance.validator import SingleOwnerApprovalValidator, ValidationDenied
class StateCasRollbackTests(unittest.TestCase):
def test_ordinary_owner_approved_merge_simulation(self):
result = SingleOwnerApprovalValidator().validate(context())
self.assertEqual(result.owner_review.reviewer, "owner")
machine = GovernanceStateMachine()
for target in (
GovernanceState.OWNER_APPROVAL_VERIFIED,
GovernanceState.MACHINE_GATES_VERIFIED,
GovernanceState.AUTO_MERGE_READY,
GovernanceState.OWNER_APPROVED_EXECUTION_READY,
):
machine.transition(target)
self.assertEqual(machine.state, GovernanceState.OWNER_APPROVED_EXECUTION_READY)
def test_validator_failure_is_stateless_and_recoverable(self):
validator = SingleOwnerApprovalValidator()
with self.assertRaises(ValidationDenied):
validator.validate(context(wip=True))
self.assertEqual(validator.validate(context()).owner_review.reviewer, "owner")
def test_state_machine_happy_path_and_restart_recovery(self):
machine = GovernanceStateMachine()
for target in (
GovernanceState.OWNER_APPROVAL_VERIFIED,
GovernanceState.MACHINE_GATES_VERIFIED,
GovernanceState.AUTO_MERGE_READY,
GovernanceState.OWNER_APPROVED_EXECUTION_READY,
):
machine.transition(target)
restarted = GovernanceStateMachine(machine.state)
self.assertEqual(restarted.state, GovernanceState.OWNER_APPROVED_EXECUTION_READY)
def test_state_machine_rollback_to_checkpoint(self):
machine = GovernanceStateMachine()
machine.transition(GovernanceState.OWNER_APPROVAL_VERIFIED)
checkpoint = machine.state
machine.transition(GovernanceState.MACHINE_GATES_VERIFIED)
machine.restore(checkpoint)
self.assertEqual(machine.state, GovernanceState.OWNER_APPROVAL_VERIFIED)
def test_service_restart_from_each_checkpoint(self):
with tempfile.TemporaryDirectory(prefix="aios-governance-fixture-") as root:
checkpoint = Path(root) / "state.json"
for state in GovernanceState:
with self.subTest(state=state):
GovernanceStateMachine(state).save_fixture(checkpoint)
restarted = GovernanceStateMachine.load_fixture(checkpoint)
self.assertEqual(restarted.state, state)
def test_invalid_transition_denied(self):
with self.assertRaises(InvalidTransition):
GovernanceStateMachine().transition(GovernanceState.AUTO_MERGE_READY)
def test_branch_protection_cas_success_and_rollback(self):
adapter = BranchProtectionCASMigrationAdapter()
old, new = legacy_protection(), single_owner_protection()
plan = adapter.plan(old, new, snapshot_sha256(old))
store = MemoryProtectionStore(old)
self.assertEqual(adapter.apply_fixture(store, plan), new)
self.assertEqual(adapter.rollback_fixture(store, plan), old)
def test_branch_protection_cas_conflict_fails_closed(self):
adapter = BranchProtectionCASMigrationAdapter()
old, changed, new = legacy_protection(), legacy_protection(7), single_owner_protection()
plan = adapter.plan(old, new, snapshot_sha256(old))
store = MemoryProtectionStore(changed)
with self.assertRaises(CASConflict):
adapter.apply_fixture(store, plan)
def test_partial_activation_recovery_saga(self):
adapter = BranchProtectionCASMigrationAdapter()
old, new = legacy_protection(), single_owner_protection()
plan = adapter.plan(old, new, snapshot_sha256(old))
saga = ProtectionCompensationSaga(adapter, MemoryProtectionStore(old), plan)
saga.execute_fixture()
self.assertEqual(saga.compensate_fixture(), old)
self.assertTrue(saga.applied and saga.rolled_back)
def test_rollback_configuration_fixture_install(self):
adapter = BranchProtectionCASMigrationAdapter()
old, new = legacy_protection(), single_owner_protection()
plan = adapter.plan(old, new, snapshot_sha256(old))
manager = RollbackConfigurationManager()
config = manager.build(plan)
with DisposableGiteaFixture(old) as fixture:
path = manager.install_fixture(fixture.root, config)
self.assertTrue(path.exists())
self.assertTrue(manager.verify(config, plan))
def test_maintenance_lock_fences_concurrent_holder(self):
with tempfile.TemporaryDirectory(prefix="aios-governance-fixture-") as root:
path = Path(root) / "maintenance.lock"
with MaintenanceLock(path):
with self.assertRaises(BlockingIOError):
with MaintenanceLock(path):
pass
if __name__ == "__main__":
unittest.main()
@@ -1,45 +0,0 @@
from __future__ import annotations
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
class SystemdContractTests(unittest.TestCase):
def test_units_are_static_dormant_and_credential_free(self):
units = sorted(
unit for unit in (ROOT / "deploy" / "systemd").glob("aios-governance-*.service")
if unit.name != "aios-governance-single-owner-activation.service"
)
self.assertEqual(len(units), 2)
for unit in units:
text = unit.read_text(encoding="utf-8")
with self.subTest(unit=unit.name):
self.assertIn("AIOS_SINGLE_OWNER_GOVERNANCE_ENABLED=false", text)
self.assertIn("AIOS_SINGLE_OWNER_ACTIVATION_ACTIVE=false", text)
self.assertIn("AIOS_GITOPS_WRITES_ENABLED=false", text)
self.assertIn("ProtectSystem=strict", text)
self.assertNotIn("EnvironmentFile=", text)
self.assertNotIn("LoadCredential=", text)
self.assertNotIn("WantedBy=", text)
self.assertNotIn("[Install]", text)
self.assertIn("--checkpoint-dir /var/lib/aios-gitops/runtime/single-owner-governance", text)
self.assertNotIn("/data/aios-gitops/production", text)
def test_activation_verifier_is_static_and_has_no_credential_or_install_hook(self):
unit = ROOT / "deploy" / "systemd" / "aios-governance-single-owner-activation.service"
text = unit.read_text(encoding="utf-8")
self.assertIn("AIOS_GITOPS_WRITES_ENABLED=false", text)
self.assertIn("--verify", text)
self.assertIn("ProtectSystem=strict", text)
self.assertNotIn("EnvironmentFile=", text)
self.assertNotIn("LoadCredential=", text)
self.assertNotIn("WantedBy=", text)
self.assertNotIn("[Install]", text)
self.assertNotIn("--activate", text)
if __name__ == "__main__":
unittest.main()
@@ -1,77 +0,0 @@
from __future__ import annotations
from dataclasses import replace
from datetime import timedelta
import unittest
from aios_single_owner_governance.models import Review
from aios_single_owner_governance.validator import SingleOwnerApprovalValidator, ValidationDenied
from .helpers import HEAD, NOW, context, gates, owner_review
class ValidatorTests(unittest.TestCase):
def setUp(self):
self.validator = SingleOwnerApprovalValidator()
def denied(self, code, **changes):
with self.assertRaises(ValidationDenied) as caught:
self.validator.validate(context(**changes))
self.assertEqual(caught.exception.code, code)
def test_owner_current_head_and_machine_gates_pass(self):
result = self.validator.validate(context())
self.assertEqual(result.owner_review.reviewer, "owner")
self.assertEqual(len(result.machine_contexts), 3)
def test_requester_self_approval_denied(self):
self.denied("REQUESTER_SELF_APPROVAL", requester="owner")
def test_executor_self_approval_denied(self):
self.denied("EXECUTOR_SELF_APPROVAL", executor="owner")
def test_codex_as_owner_denied(self):
codex = owner_review(reviewer="codex", reviewer_id=42)
self.denied("OWNER_APPROVAL_REQUIRED", reviews=(codex,))
def test_gpt_as_owner_denied(self):
gpt = owner_review(reviewer="gpt", reviewer_id=43)
self.denied("OWNER_APPROVAL_REQUIRED", reviews=(gpt,))
def test_non_owner_approval_denied(self):
other = owner_review(reviewer="other", reviewer_id=77)
self.denied("OWNER_APPROVAL_REQUIRED", reviews=(other,))
def test_stale_owner_approval_denied(self):
self.denied("STALE_OWNER_APPROVAL", reviews=(owner_review(stale=True),))
def test_dismissed_owner_approval_denied(self):
self.denied("DISMISSED_OWNER_APPROVAL", reviews=(owner_review(dismissed=True),))
def test_old_head_owner_approval_denied(self):
self.denied("OWNER_APPROVAL_NOT_LATEST_HEAD", reviews=(owner_review(head_oid="b" * 40),))
def test_wip_denied(self):
self.denied("WIP_BLOCKED", wip=True)
def test_machine_gate_failure_denied(self):
self.denied("MACHINE_GATE_FAILED", machine_gates=gates(state="failure"))
def test_secret_scan_failure_denied(self):
failed = list(gates())
failed[1] = replace(failed[1], state="failure")
self.denied("MACHINE_GATE_FAILED", machine_gates=tuple(failed))
def test_machine_gate_wrong_head_denied(self):
self.denied("MACHINE_GATE_NOT_LATEST_HEAD", machine_gates=gates(head_oid="c" * 40))
def test_expired_request_denied(self):
self.denied("APPROVAL_EXPIRED", now=NOW + timedelta(hours=2))
def test_review_after_expiry_denied(self):
late = owner_review(reviewed_at=NOW + timedelta(hours=2))
self.denied("OWNER_APPROVAL_EXPIRED", reviews=(late,))
if __name__ == "__main__":
unittest.main()
+454 -1
View File
@@ -1,11 +1,20 @@
from __future__ import annotations
import importlib.util
import io
import os
import signal
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.parse
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from types import SimpleNamespace
from unittest import TestCase
from unittest.mock import patch
from unittest.mock import Mock, call, patch
BACKEND_PATH = Path(__file__).resolve().parents[1] / "scripts" / "wechat_send_backend.py"
@@ -80,6 +89,450 @@ class WechatSendBackendTests(TestCase):
self.assertEqual(args[1]["content"], "hello")
self.assertFalse(args[1]["dry_run"])
def test_ucc_auth_is_authorization_header_not_argv_or_output(self):
secret = "ucc-super-secret-token"
class Response:
status = 200
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self, _limit=-1):
return b'{"ok":true,"status":"sent"}'
urlopen = Mock(return_value=Response())
output = io.StringIO()
with (
patch.object(self.backend, "UCC_ADAPTER_TOKEN", secret),
patch.object(self.backend, "_open_no_redirect", urlopen),
redirect_stdout(output),
redirect_stderr(output),
):
status, payload = self.backend._post_ucc(
"/api/internal/wechat/send", {"target": "chat-001", "content": "hello"}
)
request = urlopen.call_args.args[0]
self.assertEqual(request.get_header("Authorization"), f"Bearer {secret}")
self.assertEqual(status, 200)
self.assertEqual(payload, {"ok": True, "status": "sent"})
self.assertNotIn(secret, " ".join(self.backend._openclaw_base_args()))
self.assertNotIn(secret, output.getvalue())
self.assertFalse({"--gateway", "--token", "--password"} & set(self.backend._openclaw_base_args()))
def test_ucc_paths_are_canonical_first_unique_and_bounded(self):
with patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"):
paths = self.backend._ucc_send_paths()
self.assertEqual(paths[0], "/api/internal/wechat/send")
self.assertLessEqual(len(paths), 2)
self.assertEqual(len(paths), len(set(paths)))
self.assertTrue(all(path.startswith("/") for path in paths))
def test_ucc_accepts_only_explicit_true_and_sent_when_not_dry_run(self):
self.assertTrue(self.backend._ucc_response_sent({"ok": True, "status": "sent"}))
for response in (
{"ok": 1, "status": "sent"},
{"ok": True, "status": "accepted"},
{"ok": False, "status": "sent"},
{"status": "sent"},
):
with self.subTest(response=response):
self.assertFalse(self.backend._ucc_response_sent(response))
def test_ambiguous_canonical_failures_never_fallback(self):
secret = "raw-upstream-secret"
failures = (
TimeoutError(secret),
ConnectionError(secret),
self.backend._BackendProtocolError(secret),
RuntimeError(secret),
)
for failure in failures:
with self.subTest(failure=type(failure).__name__):
with (
patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"),
patch.object(self.backend, "_post_ucc", side_effect=failure) as post,
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello", message_id="msg-001"
).to_dict()
self.assertFalse(result["ok"])
self.assertEqual(result["code"], "ucc_adapter_unavailable")
self.assertEqual(post.call_count, 1)
self.assertNotIn(secret, repr(result))
def test_only_explicit_http_404_or_405_uses_legacy_fallback(self):
for http_status in (404, 405):
with self.subTest(http_status=http_status):
missing = urllib.error.HTTPError(
"http://127.0.0.1/api/internal/wechat/send",
http_status,
"not found",
{},
None,
)
with (
patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"),
patch.object(
self.backend,
"_post_ucc",
side_effect=[missing, (200, {"ok": True, "status": "sent"})],
) as post,
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello", message_id="msg-001"
).to_dict()
self.assertTrue(result["ok"])
self.assertEqual(result["endpoint"], "/internal/channels/wechat/send")
self.assertEqual(post.call_count, 2)
def test_non_not_found_http_error_never_falls_back(self):
denied = urllib.error.HTTPError(
"http://127.0.0.1/api/internal/wechat/send", 401, "denied", {}, None
)
with patch.object(self.backend, "_post_ucc", side_effect=denied) as post:
result = self.backend.send_ucc_adapter(target="chat-001", content="hello").to_dict()
self.assertFalse(result["ok"])
self.assertEqual(result["code"], "ucc_adapter_http_error")
post.assert_called_once()
def test_ucc_rejection_does_not_expose_raw_error_or_secret(self):
secret = "provider-password=secret-value"
with patch.object(
self.backend,
"_post_ucc",
return_value=(200, {"ok": False, "status": "failed", "error": secret}),
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello", message_id="msg-001"
).to_dict()
self.assertFalse(result["ok"])
self.assertEqual(result["code"], "ucc_adapter_rejected")
self.assertNotIn(secret, repr(result))
def test_dry_run_requires_dry_run_status_and_never_reports_sent(self):
with patch.object(
self.backend,
"_post_ucc",
return_value=(200, {"ok": True, "status": "dry_run"}),
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello", dry_run=True
).to_dict()
self.assertTrue(result["ok"])
self.assertEqual(result["status"], "dry_run")
self.assertNotEqual(result["status"], "sent")
for dry_run, upstream_status in ((True, "sent"), (False, "dry_run")):
with self.subTest(dry_run=dry_run, upstream_status=upstream_status):
with patch.object(
self.backend,
"_post_ucc",
return_value=(200, {"ok": True, "status": upstream_status}),
):
rejected = self.backend.send_ucc_adapter(
target="chat-001", content="hello", dry_run=dry_run
).to_dict()
self.assertFalse(rejected["ok"])
self.assertEqual(rejected["status"], "failed")
def test_invalid_provider_message_id_never_reverses_confirmed_send(self):
invalid_values = (
"\ud800",
"contains\x00control",
"x" * (self.backend.MAX_IDENTIFIER_BYTES + 1),
{"not": "a string"},
)
for provider_message_id in invalid_values:
with self.subTest(value=repr(provider_message_id)[:40]):
with patch.object(
self.backend,
"_post_ucc",
return_value=(
200,
{
"ok": True,
"status": "sent",
"provider_message_id": provider_message_id,
},
),
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello"
).to_dict()
self.assertTrue(result["ok"])
self.assertEqual(result["status"], "sent")
self.assertNotIn("provider_message_id", result)
def test_openclaw_2026_6_11_commands_have_no_unsupported_global_gateway(self):
completed = SimpleNamespace(returncode=0, stdout='{"channels": []}')
with (
patch.object(self.backend, "OPENCLAW_BIN", "/usr/local/bin/openclaw"),
patch.object(self.backend, "_run_openclaw", return_value=completed) as run,
):
self.assertIsNone(self.backend.discover_openclaw_wechat_channel())
command = run.call_args.args[0]
self.assertEqual(command, ["/usr/local/bin/openclaw", "channels", "list", "--json"])
self.assertFalse({"--gateway", "--token", "--password"} & set(command))
def test_openclaw_output_overflow_terminates_while_process_is_running(self):
writer = (
"import os,time;"
f"os.write(1,b'x'*{self.backend.MAX_RESPONSE_BYTES + 4096});"
"time.sleep(10)"
)
started = time.monotonic()
completed = self.backend._run_openclaw([sys.executable, "-c", writer], timeout=4)
elapsed = time.monotonic() - started
self.assertLess(elapsed, 2.0)
self.assertNotEqual(completed.returncode, 0)
self.assertEqual(completed.stdout, "")
self.assertEqual(completed.stderr, "")
def test_openclaw_timeout_returns_failure_and_terminates_promptly(self):
started = time.monotonic()
completed = self.backend._run_openclaw(
[sys.executable, "-c", "import time;time.sleep(10)"], timeout=1
)
elapsed = time.monotonic() - started
self.assertLess(elapsed, 3.0)
self.assertNotEqual(completed.returncode, 0)
self.assertEqual(completed.stdout, "")
self.assertEqual(completed.stderr, "")
def test_timeout_kills_inherited_pipe_descendant_after_parent_exits(self):
if os.name != "posix" or not Path("/proc").is_dir():
self.skipTest("POSIX process-group and procfs contract")
def process_is_running(pid):
try:
state = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8").split()[2]
except (FileNotFoundError, IndexError, OSError):
return False
return state not in {"X", "Z"}
with tempfile.TemporaryDirectory() as directory:
pid_path = Path(directory) / "descendant.pid"
descendant = "import time;time.sleep(30)"
launcher = (
"import pathlib,subprocess,sys;"
f"child=subprocess.Popen([sys.executable,'-c',{descendant!r}]);"
f"pathlib.Path({str(pid_path)!r}).write_text(str(child.pid),encoding='utf-8')"
)
started = time.monotonic()
completed = self.backend._run_openclaw(
[sys.executable, "-c", launcher], timeout=1
)
elapsed = time.monotonic() - started
self.assertTrue(pid_path.is_file())
descendant_pid = int(pid_path.read_text(encoding="utf-8"))
try:
deadline = time.monotonic() + 2
while process_is_running(descendant_pid) and time.monotonic() < deadline:
time.sleep(0.02)
self.assertFalse(process_is_running(descendant_pid))
finally:
if process_is_running(descendant_pid):
os.kill(descendant_pid, signal.SIGKILL)
self.assertLess(elapsed, 3.0)
self.assertNotEqual(completed.returncode, 0)
def test_process_tree_termination_uses_posix_process_group(self):
if os.name != "posix":
self.skipTest("POSIX process-group contract")
class Process:
pid = 43210
def __init__(self):
self.wait_count = 0
def poll(self):
return None
def wait(self, timeout):
self.wait_count += 1
if self.wait_count == 1:
raise subprocess.TimeoutExpired(["openclaw"], timeout)
return -signal.SIGKILL
process = Process()
with patch.object(self.backend.os, "killpg") as killpg:
self.backend._terminate_process_tree(process)
self.assertEqual(
killpg.call_args_list,
[
call(process.pid, signal.SIGTERM),
call(process.pid, signal.SIGKILL),
],
)
def test_unsupported_and_generic_chat_backends_fail_closed(self):
with (
patch.object(self.backend, "SEND_BACKEND", "openclaw-chat"),
patch.object(self.backend, "ucc_adapter_alive", return_value=False),
):
selected = self.backend.select_backend()
result = self.backend.send_message(target="chat-001", content="hello")
self.assertEqual(selected["backend"], "unavailable")
self.assertEqual(selected["reason"], "unsupported_backend")
self.assertFalse(result["ok"])
self.assertEqual(result["code"], "unsupported_backend")
def test_invalid_or_oversized_input_is_rejected_before_network(self):
with patch.object(self.backend, "_post_ucc") as post:
empty = self.backend.send_ucc_adapter(target="", content="hello").to_dict()
oversized = self.backend.send_ucc_adapter(
target="chat-001", content="x" * (self.backend.MAX_CONTENT_BYTES + 1)
).to_dict()
self.assertEqual(empty["code"], "invalid_input")
self.assertEqual(oversized["code"], "input_too_large")
post.assert_not_called()
def test_unpaired_surrogate_inputs_fail_closed_before_network(self):
cases = (
{"target": "\ud800", "content": "hello"},
{"target": "chat-001", "content": "\ud800"},
{"target": "chat-001", "content": "hello", "account_id": "\ud800"},
{"target": "chat-001", "content": "hello", "message_id": "\ud800"},
{"target": "chat-001", "content": "hello", "metadata": {"bad": "\ud800"}},
)
with patch.object(self.backend, "_post_ucc") as post:
for kwargs in cases:
with self.subTest(field=next(reversed(kwargs))):
result = self.backend.send_ucc_adapter(**kwargs).to_dict()
self.assertEqual(result["code"], "invalid_input")
post.assert_not_called()
def test_deeply_nested_metadata_is_bounded_before_network(self):
metadata = {}
for _ in range(1200):
metadata = {"nested": metadata}
with patch.object(self.backend, "_post_ucc") as post:
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello", metadata=metadata
).to_dict()
self.assertEqual(result["code"], "input_too_large")
post.assert_not_called()
def test_adapter_origin_must_be_allowlisted(self):
with (
patch.object(self.backend, "UCC_ADAPTER_URL", "https://evil.example:8443"),
patch.object(
self.backend,
"UCC_ADAPTER_ALLOWED_ORIGINS",
("http://127.0.0.1:18800",),
),
):
self.assertIsNone(self.backend._adapter_base_url())
with (
patch.object(self.backend, "UCC_ADAPTER_URL", "https://adapter.example:8443"),
patch.object(
self.backend,
"UCC_ADAPTER_ALLOWED_ORIGINS",
("https://adapter.example:8443",),
),
):
self.assertEqual(
self.backend._adapter_base_url(), "https://adapter.example:8443"
)
def test_adapter_origin_rejects_scheme_userinfo_fragment_and_path(self):
invalid_origins = (
"ftp://127.0.0.1:18800",
"http://user:password@127.0.0.1:18800",
"http://127.0.0.1:18800#fragment",
"http://127.0.0.1:18800/base-path",
)
for origin in invalid_origins:
with self.subTest(origin=origin):
with (
patch.object(self.backend, "UCC_ADAPTER_URL", origin),
patch.object(
self.backend,
"UCC_ADAPTER_ALLOWED_ORIGINS",
("http://127.0.0.1:18800",),
),
):
self.assertIsNone(self.backend._adapter_base_url())
def test_adapter_path_rejects_decoded_traversal(self):
for path in (
"/api/%2e%2e/send",
"/api/%2E%2E/send",
"/api/%2525252e%2525252e/send",
"/api/%252525252e%252525252e/send",
"/api/../send",
"/api/%zz/send",
):
with self.subTest(path=path):
self.assertIsNone(self.backend._safe_path(path))
def test_adapter_path_keeps_intended_safe_canonical_paths(self):
for path in (
"/api/internal/wechat/send",
"/internal/channels/wechat/send",
"/api/%7Ehealth/status",
):
with self.subTest(path=path):
self.assertEqual(self.backend._safe_path(path), path)
def test_redirects_are_refused_and_authorization_cannot_cross_origin(self):
handler = self.backend._NoRedirectHandler()
self.assertIsNone(
handler.redirect_request(
None,
None,
302,
"redirect",
{"Location": "https://evil.example/steal"},
"https://evil.example/steal",
)
)
redirected = urllib.error.HTTPError(
"http://127.0.0.1:18800/api/internal/wechat/send",
302,
"redirect",
{"Location": "https://evil.example/steal"},
None,
)
with (
patch.object(self.backend, "UCC_ADAPTER_TOKEN", "header-secret"),
patch.object(self.backend, "_open_no_redirect", side_effect=redirected) as opened,
):
result = self.backend.send_ucc_adapter(
target="chat-001", content="hello"
).to_dict()
request = opened.call_args.args[0]
self.assertEqual(urllib.parse.urlsplit(request.full_url).netloc, "127.0.0.1:18800")
self.assertEqual(request.get_header("Authorization"), "Bearer header-secret")
opened.assert_called_once()
self.assertFalse(result["ok"])
self.assertEqual(result["code"], "ucc_adapter_http_error")
if __name__ == "__main__":
import unittest
+13
View File
@@ -0,0 +1,13 @@
{
"version": "V2R1C4",
"repository": "aios/aios",
"target_oid": "2c48b9fd395f08ca87ad9bb94f9f49d06de8d291",
"tag_oid": "2f6c1652f400dfb02e6a80b4b11d433659db494a",
"peeled_target_oid": "2c48b9fd395f08ca87ad9bb94f9f49d06de8d291",
"artifact_sha256": "c04cf2f1b1b6b77fc632c60642eb434fc577216178bb0bfec5fe89deeda6df4d",
"evidence_closure_oid": "053e9c992ad20198cff9bab956fe6ab649b7d21a",
"approval_plan_id": "sha256:348408f38c54536e3a2a3765ac70cc6bd3884bcab761d5f5d9f82d1597beec9e",
"previous_version": "V2R1C3",
"deployment_status": "TAG_PUBLISHED",
"release_timestamp": "2026-08-05T03:29:05Z"
}
+35
View File
@@ -0,0 +1,35 @@
{
"approval_plan_id": "sha256:34c4031c3014498ac2b4bd846026c0ca372a7d9247f3c24816d3700735006be9",
"artifact_composition": "PASS",
"artifact_sha256": "1a9d5b6d240584eb683c86c55014b05bc8f6f0113b2cae053c88758034aefbba",
"baseline_promoted": false,
"direct_main_push": false,
"evidence_closure_oid": "5e2cf8276f5093f3352b9a3252c701a44c5ab807",
"force_push": false,
"independent_validation": "PASS",
"maintenance_mode": "SINGLE_MAINTAINER",
"owner_approval": "OWNER_EXACT_OID_APPROVED",
"previous_version": "V2R1C3",
"privacy_migration": "PASS",
"production_baseline_before": "V2R1C3",
"production_deployed": false,
"raw_owner_findings": 0,
"registered_at": "2026-08-05T06:45:02.796699+00:00",
"registered_by": "aios-autonomy-agent",
"repository": "aios/aios",
"required_approvals": 1,
"required_distinct_approvers": 1,
"rollback_tag_oid": "a159d0ec4ea558137c1567f558ef41bfd9f5122d",
"rollback_target_oid": "7db5e458b225b6eabe1bb59fc119602abde8a629",
"rollback_version": "V2R1C3",
"schema": "aios.v2r1c5.registry.v1",
"second_reviewer_required": false,
"secrets_committed": false,
"status": "TAG_PUBLISHED_PENDING_PRODUCTION_ACCEPTANCE",
"tag": "V2R1C5",
"tag_object": "9e65bd59baea392294c0f253046d3ab116f63145",
"tag_ref": "refs/tags/V2R1C5",
"target_oid": "aa60e891547abd7e215af7cf2bf3f21fd23541da",
"tree_oid": "cae5ca9b6d923810187f373afe2d9bfe37215ba4",
"version": "V2R1C5"
}