Compare commits
21
Commits
@@ -1,22 +1,22 @@
|
||||
[Unit]
|
||||
Description=AIOS Autonomous Release Candidate API (Dormant/Shadow Only)
|
||||
After=network.target
|
||||
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
|
||||
Description=AIOS Autonomous Release Candidate (dormant shadow only)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[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
|
||||
WorkingDirectory=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||
Environment=AIOS_CANDIDATE_LOOPBACK_HOST=127.0.0.1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.service --host 127.0.0.1 --port 19951 --state /var/lib/aios-autonomous-release-candidate/state.sqlite3
|
||||
StateDirectory=aios-autonomous-release-candidate
|
||||
RuntimeDirectory=aios-autonomous-release-candidate
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
@@ -24,19 +24,19 @@ 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
|
||||
RuntimeDirectory=aios-autonomous-release-candidate
|
||||
StateDirectory=aios-autonomous-release-candidate
|
||||
CacheDirectory=aios-autonomous-release-candidate
|
||||
ReadOnlyPaths=/data/aios-gitops/worktrees/chatgpt-autonomous-completion-contract-20260726/services/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/run/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/var/lib/aios-autonomous-release-candidate
|
||||
ReadWritePaths=/var/cache/aios-autonomous-release-candidate
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# AIOS V2R1C4 Dormant Shadow-Only Release Execution
|
||||
|
||||
## Purpose
|
||||
|
||||
Deploy verified V2R1C4 code while keeping autonomous execution capabilities disabled.
|
||||
|
||||
This release separates code deployment from capability activation. Capability activation requires a later, independently approved release.
|
||||
|
||||
## Deployment mode
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
Required production flags:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
Required dispatch transport:
|
||||
|
||||
```text
|
||||
protectedDispatchTransport=sandbox
|
||||
```
|
||||
|
||||
Disabled capabilities:
|
||||
|
||||
```text
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
production_side_effects=false
|
||||
activation_deferred=true
|
||||
```
|
||||
|
||||
## Release flow
|
||||
|
||||
```text
|
||||
Integration verification
|
||||
|
|
||||
v
|
||||
Owner dormant release approval
|
||||
|
|
||||
v
|
||||
Merge PR
|
||||
|
|
||||
v
|
||||
Read final main merge OID
|
||||
|
|
||||
v
|
||||
Rebuild production artifact
|
||||
|
|
||||
v
|
||||
Deploy dormant runtime
|
||||
|
|
||||
v
|
||||
Create V2R1C4 tag
|
||||
|
|
||||
v
|
||||
Verify Registry and Baseline consistency
|
||||
```
|
||||
|
||||
## Two-stage approval model
|
||||
|
||||
### Pre-merge approval
|
||||
|
||||
The pre-merge approval binds:
|
||||
|
||||
- version
|
||||
- PR number
|
||||
- integration OID and tree OID
|
||||
- candidate artifact and evidence hashes
|
||||
- deployment mode
|
||||
- all production execution flags remaining false
|
||||
|
||||
It authorizes the governed merge and production artifact rebuild. It does not predeclare the final production target OID.
|
||||
|
||||
### Final release approval
|
||||
|
||||
After the governed merge, the final approval binds:
|
||||
|
||||
- final main merge OID
|
||||
- final main tree OID
|
||||
- production artifact, manifest, package closure, and evidence closure
|
||||
- rollback artifact and production configuration hashes
|
||||
- V2R1C4 tag target
|
||||
|
||||
The integration OID must never be used as the final production target OID.
|
||||
|
||||
## Deferred activation
|
||||
|
||||
The following are separate activation gates and must not block dormant code deployment:
|
||||
|
||||
- OpenClaw native session provider
|
||||
- Codex app-server provider
|
||||
- Gitea Admin API automation
|
||||
- Secret Manager V2
|
||||
- autonomous production execution
|
||||
|
||||
They require a later activation release with separate evidence and approval.
|
||||
|
||||
## Hard restrictions
|
||||
|
||||
Never:
|
||||
|
||||
- write main directly
|
||||
- force push
|
||||
- rewrite tags
|
||||
- export secrets
|
||||
- enable autonomous production execution without approval
|
||||
- treat integration OID as production target OID
|
||||
- update Registry or Baseline before remote and Gitea tag verification
|
||||
|
||||
## Dormant production acceptance
|
||||
|
||||
A dormant deployment is acceptable only when:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
production_side_effects=false
|
||||
protectedDispatchTransport=sandbox
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
The release report must preserve deferred activation blockers rather than falsely marking native-agent or automatic-release capabilities as enabled.
|
||||
@@ -1,47 +1,82 @@
|
||||
# AIOS Autonomous Release Execution Package
|
||||
|
||||
## Mode
|
||||
## Release Strategy
|
||||
|
||||
Candidate/shadow first. Production changes require an independent release approval.
|
||||
V2R1C4 uses a two-stage model:
|
||||
|
||||
## Flow
|
||||
1. Dormant code deployment.
|
||||
2. Capability activation through a separate approved release.
|
||||
|
||||
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.
|
||||
## Dormant Deployment
|
||||
|
||||
## Hard stops
|
||||
The first production release must keep autonomous execution disabled:
|
||||
|
||||
- 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:
|
||||
|
||||
```
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
until release approval is independently verified.
|
||||
Required mode:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
Required properties:
|
||||
|
||||
```text
|
||||
production_side_effects=false
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
```
|
||||
|
||||
## Deployment Sequence
|
||||
|
||||
```text
|
||||
Verify Integration
|
||||
|
|
||||
v
|
||||
Owner Dormant Release Approval
|
||||
|
|
||||
v
|
||||
Merge Pull Request
|
||||
|
|
||||
v
|
||||
Read final main merge OID
|
||||
|
|
||||
v
|
||||
Build Production Artifact
|
||||
|
|
||||
v
|
||||
Deploy Dormant Runtime
|
||||
|
|
||||
v
|
||||
Create Version Tag
|
||||
|
|
||||
v
|
||||
Verify Registry and Baseline
|
||||
```
|
||||
|
||||
## Capability Activation (Separate)
|
||||
|
||||
The following require separate approval and evidence:
|
||||
|
||||
- OpenClaw native provider
|
||||
- Codex app-server provider
|
||||
- Gitea Admin API lifecycle
|
||||
- Secret Manager V2
|
||||
- Autonomous production execution
|
||||
|
||||
## Hard Governance Rules
|
||||
|
||||
Never:
|
||||
|
||||
- direct write main
|
||||
- force push
|
||||
- rewrite tags
|
||||
- export credentials
|
||||
- enable autonomous production execution without approval
|
||||
- use integration OID as production target OID
|
||||
- update Baseline before remote tag verification
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
# AIOS V2R1C4 Dormant Release — Final Agent Execution Contract
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document is the machine/operator execution contract for completing the governed V2R1C4 dormant release after Candidate development is frozen.
|
||||
|
||||
It does not grant production authority. Every consequential action remains subject to the exact approval, Git reference, artifact, tag, Registry and Baseline gates defined below.
|
||||
|
||||
## 2. Release mode
|
||||
|
||||
The only permitted deployment mode for V2R1C4 is:
|
||||
|
||||
```text
|
||||
deployment_mode=dormant_shadow_only
|
||||
```
|
||||
|
||||
The following values are mandatory before, during and after deployment:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||
production_side_effects=false
|
||||
native_openclaw_enabled=false
|
||||
native_codex_enabled=false
|
||||
automatic_release_enabled=false
|
||||
activation_deferred=true
|
||||
protectedDispatchTransport=sandbox
|
||||
```
|
||||
|
||||
Any deviation is a hard stop.
|
||||
|
||||
## 3. Authoritative branches
|
||||
|
||||
```text
|
||||
candidate_branch=patch/chatgpt-autonomous-completion-contract-20260726
|
||||
integration_branch=integration/chatgpt-autonomous-completion-20260726
|
||||
production_branch=main
|
||||
version=V2R1C4
|
||||
pull_request=20
|
||||
```
|
||||
|
||||
Never infer a target OID from workspace HEAD. Always read and record full 40-character OIDs.
|
||||
|
||||
## 4. Phase A — Candidate freeze and verification
|
||||
|
||||
1. Read the current Candidate OID and tree OID.
|
||||
2. Verify the Candidate worktree is clean.
|
||||
3. Verify four-way Candidate reference closure:
|
||||
- local branch ref;
|
||||
- authoritative bare ref;
|
||||
- `git ls-remote` ref;
|
||||
- Gitea API branch ref.
|
||||
4. Run the complete test and static validation suite defined in `V2R1C4_TEST_AND_RELEASE_CHECKLIST.md`.
|
||||
5. Build the deterministic Candidate artifact and manifest from the exact Candidate OID.
|
||||
6. Record:
|
||||
- artifact SHA-256;
|
||||
- manifest SHA-256;
|
||||
- package closure SHA-256;
|
||||
- test evidence SHA-256;
|
||||
- rollback artifact SHA-256;
|
||||
- production configuration SHA-256.
|
||||
7. Do not reuse any digest generated from an older Candidate OID.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
CANDIDATE_ACCEPTANCE_BLOCKED
|
||||
```
|
||||
|
||||
## 5. Phase B — Integration refresh
|
||||
|
||||
1. Refresh the Integration branch from the latest Candidate using a governed non-fast-forward merge.
|
||||
2. Use:
|
||||
|
||||
```text
|
||||
merge --no-ff
|
||||
```
|
||||
|
||||
3. Do not squash, rebase, force push or rewrite Candidate history.
|
||||
4. Verify the Candidate OID is reachable from the new Integration OID.
|
||||
5. Verify four-way Integration reference closure.
|
||||
6. Run the full Candidate test suite again from the Integration worktree.
|
||||
7. Rebuild Integration evidence and Candidate package metadata if any tracked file changed during the merge.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
INTEGRATION_ACCEPTANCE_BLOCKED
|
||||
```
|
||||
|
||||
## 6. Phase C — Candidate acceptance
|
||||
|
||||
Create Candidate acceptance evidence and execute:
|
||||
|
||||
```text
|
||||
python scripts/autonomous-release/run_acceptance.py \
|
||||
--phase candidate \
|
||||
--input <candidate-evidence.json> \
|
||||
--output <candidate-acceptance.json>
|
||||
```
|
||||
|
||||
The result must contain:
|
||||
|
||||
```text
|
||||
phase=candidate
|
||||
status=READY
|
||||
production_action_authorized=false
|
||||
```
|
||||
|
||||
The Candidate gate must verify:
|
||||
|
||||
```text
|
||||
candidate_four_way_ref_closure=true
|
||||
integration_four_way_ref_closure=true
|
||||
candidate_reachable_from_integration=true
|
||||
worktree_clean=true
|
||||
tests_failed=0
|
||||
tests_passed>0
|
||||
```
|
||||
|
||||
It must also verify all dormant runtime flags remain disabled.
|
||||
|
||||
`READY` authorizes review only. It does not authorize merge, deployment, tagging, Registry writes or Baseline writes.
|
||||
|
||||
## 7. Phase D — Pre-merge owner approval
|
||||
|
||||
Obtain a governed Owner approval plan for the exact Integration package.
|
||||
|
||||
The approval subject must bind at least:
|
||||
|
||||
```text
|
||||
version
|
||||
pull_request
|
||||
candidate_oid
|
||||
candidate_tree_oid
|
||||
integration_oid
|
||||
integration_tree_oid
|
||||
candidate_acceptance_digest
|
||||
candidate_release_digest
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
package_closure_sha256
|
||||
evidence_closure_oid
|
||||
rollback_artifact_sha256
|
||||
production_config_sha256
|
||||
deployment_mode=dormant_shadow_only
|
||||
all execution flags=false
|
||||
```
|
||||
|
||||
The approval must not predeclare the final production target OID.
|
||||
|
||||
Without an Owner-approved plan, stop with:
|
||||
|
||||
```text
|
||||
BLOCKED_EXTERNAL_OWNER_SIGNATURE
|
||||
```
|
||||
|
||||
## 8. Phase E — Governed merge
|
||||
|
||||
1. Merge PR #20 only through the governed merge path.
|
||||
2. Do not push directly to `main`.
|
||||
3. After merge, read the exact resulting `main` merge OID and tree OID.
|
||||
4. Verify four-way `main` reference closure.
|
||||
5. Verify the merged Integration OID is reachable from the final `main` merge OID.
|
||||
6. Never use the Integration OID as the production target OID.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
MAIN_RECONCILIATION_REQUIRED
|
||||
```
|
||||
|
||||
## 9. Phase F — Production artifact rebuild
|
||||
|
||||
Rebuild all production artifacts from the exact final `main` merge OID.
|
||||
|
||||
Record new values for:
|
||||
|
||||
```text
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
package_closure_sha256
|
||||
evidence_closure_oid
|
||||
rollback_artifact_sha256
|
||||
production_config_sha256
|
||||
```
|
||||
|
||||
Candidate or Integration artifacts may not be promoted by renaming or metadata substitution. The final artifact must be demonstrably rebuilt from the final production target.
|
||||
|
||||
## 10. Phase G — Final release digest and Owner approval
|
||||
|
||||
1. Construct the `FinalDormantRelease` object using the final `main` target and rebuilt artifact evidence.
|
||||
2. Compute its exact `release_digest`.
|
||||
3. Obtain an Owner approval receipt bound to that exact digest.
|
||||
4. Record:
|
||||
|
||||
```text
|
||||
approval_plan_id
|
||||
approval_receipt_sha256
|
||||
approved_release_digest
|
||||
owner_approval_verified=true
|
||||
```
|
||||
|
||||
Required equality:
|
||||
|
||||
```text
|
||||
approved_release_digest == FinalDormantRelease.digest()
|
||||
```
|
||||
|
||||
A generic approval, previous approval, Candidate approval or Integration approval is insufficient.
|
||||
|
||||
## 11. Phase H — Final acceptance
|
||||
|
||||
Execute:
|
||||
|
||||
```text
|
||||
python scripts/autonomous-release/run_acceptance.py \
|
||||
--phase final \
|
||||
--input <final-evidence.json> \
|
||||
--output <final-acceptance.json>
|
||||
```
|
||||
|
||||
The result must contain:
|
||||
|
||||
```text
|
||||
phase=final
|
||||
status=READY
|
||||
production_action_authorized=false
|
||||
```
|
||||
|
||||
Required final evidence:
|
||||
|
||||
```text
|
||||
main_four_way_ref_closure=true
|
||||
target_is_main_head=true
|
||||
worktree_clean=true
|
||||
artifact_rebuilt_from_target=true
|
||||
rollback_verified=true
|
||||
runtime_boundary_verified=true
|
||||
owner_approval_verified=true
|
||||
approved_release_digest=<exact final release digest>
|
||||
```
|
||||
|
||||
The acceptance report is evidence, not an execution credential.
|
||||
|
||||
## 12. Phase I — Dormant deployment
|
||||
|
||||
Deployment requires a separate production execution approval referencing:
|
||||
|
||||
```text
|
||||
version=V2R1C4
|
||||
target_oid=<final main merge OID>
|
||||
evidence_closure_oid=<final evidence closure OID>
|
||||
approval_plan_id=<approved plan>
|
||||
final_acceptance_digest=<final acceptance digest>
|
||||
```
|
||||
|
||||
Deploy only the dormant runtime. Before starting services, verify all disabled flags and the sandbox transport in the rendered production configuration.
|
||||
|
||||
After start, verify:
|
||||
|
||||
```text
|
||||
services_active=true
|
||||
loopback_bindings_only=true
|
||||
production_side_effects=false
|
||||
write_endpoints_disabled=true
|
||||
task_plane_execution_disabled=true
|
||||
silent_release_execution_disabled=true
|
||||
native_agent_execution_disabled=true
|
||||
```
|
||||
|
||||
If any side-effect-capable path is active, stop services and mark:
|
||||
|
||||
```text
|
||||
DORMANT_RUNTIME_BOUNDARY_VIOLATION
|
||||
```
|
||||
|
||||
## 13. Phase J — Tag governance
|
||||
|
||||
Tagging is permitted only after dormant deployment verification and exact Owner approval.
|
||||
|
||||
Every tag request must carry:
|
||||
|
||||
```text
|
||||
version=V2R1C4
|
||||
target_oid=<full final main merge OID>
|
||||
evidence_closure_oid=<full final evidence closure OID>
|
||||
approval_plan_id=<approved plan ID>
|
||||
```
|
||||
|
||||
When an existing same-name tag is found:
|
||||
|
||||
- identical tag OID and peeled target: treat as idempotent and reverify;
|
||||
- different tag OID or peeled target: stop with `MANUAL_OWNER_REVIEW_REQUIRED`.
|
||||
|
||||
Never force push, delete, move or rewrite a formal tag.
|
||||
|
||||
## 14. Phase K — Remote and Gitea tag acceptance
|
||||
|
||||
Before any Registry or Baseline update, verify:
|
||||
|
||||
```text
|
||||
local_tag_exists=true
|
||||
remote_tag_exists=true
|
||||
gitea_api_tag_exists=true
|
||||
local_tag_oid == remote_tag_oid
|
||||
remote_peeled_target_oid == target_oid
|
||||
gitea_peeled_target_oid == target_oid
|
||||
repository_identity=aios/aios
|
||||
```
|
||||
|
||||
For annotated tags, verify both the tag object and peeled commit.
|
||||
|
||||
Failure state:
|
||||
|
||||
```text
|
||||
REMOTE_PUSH_PENDING
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
RECONCILIATION_REQUIRED
|
||||
```
|
||||
|
||||
## 15. Phase L — Registry and Baseline
|
||||
|
||||
Only after remote and Gitea tag acceptance:
|
||||
|
||||
1. register or update the V2R1C4 Registry entry;
|
||||
2. reread the Registry and verify the exact target OID;
|
||||
3. update the Production Baseline to V2R1C4;
|
||||
4. reread the Baseline;
|
||||
5. run final release consistency acceptance.
|
||||
|
||||
Required equality:
|
||||
|
||||
```text
|
||||
Registry version
|
||||
= Production Baseline
|
||||
= local refs/tags/V2R1C4
|
||||
= remote refs/tags/V2R1C4
|
||||
= Gitea API tag
|
||||
= expected final main target
|
||||
```
|
||||
|
||||
Do not write `PRODUCTION_DEPLOYED` before this equality is proven.
|
||||
|
||||
## 16. Rollback
|
||||
|
||||
If deployment verification fails:
|
||||
|
||||
1. disable the V2R1C4 Candidate services;
|
||||
2. preserve evidence and logs;
|
||||
3. submit a governed rollback request with the target version and reason;
|
||||
4. distinguish `requested`, `approved`, `executed` and `verified` states.
|
||||
|
||||
Do not delete tags, rewrite history or independently perform a production rollback.
|
||||
|
||||
## 17. Mandatory stop conditions
|
||||
|
||||
Stop without consequential execution when any of the following is missing or false:
|
||||
|
||||
```text
|
||||
four_way_ref_closure
|
||||
clean_worktree
|
||||
full_test_closure
|
||||
artifact_digest_verification
|
||||
rollback_evidence
|
||||
runtime_boundary_verification
|
||||
exact_owner_approval
|
||||
exact_target_oid
|
||||
exact_evidence_closure_oid
|
||||
exact_approval_plan_id
|
||||
remote_tag_verification
|
||||
gitea_tag_verification
|
||||
```
|
||||
|
||||
## 18. Final report schema
|
||||
|
||||
Return:
|
||||
|
||||
```text
|
||||
execution_status=COMPLETED|PARTIALLY_COMPLETED|BLOCKED|FAILED
|
||||
candidate_oid=<full OID>
|
||||
integration_oid=<full OID>
|
||||
main_target_oid=<full OID>
|
||||
artifact_sha256=<digest>
|
||||
manifest_sha256=<digest>
|
||||
package_closure_sha256=<digest>
|
||||
evidence_closure_oid=<full OID>
|
||||
candidate_acceptance_status=<status>
|
||||
final_acceptance_status=<status>
|
||||
owner_approval_verified=<bool>
|
||||
dormant_deployment_verified=<bool>
|
||||
local_tag_verified=<bool>
|
||||
remote_tag_verified=<bool>
|
||||
gitea_tag_verified=<bool>
|
||||
registry_verified=<bool>
|
||||
baseline_verified=<bool>
|
||||
release_consistency=<status>
|
||||
```
|
||||
|
||||
Always include:
|
||||
|
||||
```text
|
||||
force_push_used: false
|
||||
tag_rewritten: false
|
||||
direct_main_push: false
|
||||
secrets_committed: false
|
||||
owner_approval_respected: true
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# AIOS V2R1C4 Dormant Release Agent Execution
|
||||
|
||||
## Scope
|
||||
|
||||
This package is a dormant, shadow-only Candidate release. It does not authorize a production deployment, tag, baseline update, registry update, or a write to main.
|
||||
|
||||
## Required runtime boundary
|
||||
|
||||
deployment_mode=dormant_shadow_only
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
production_side_effects=false
|
||||
|
||||
## Governed sequence
|
||||
|
||||
1. Verify the Integration branch with the Candidate test suite and four-way Git reference closure.
|
||||
2. Obtain Owner approval for dormant review only.
|
||||
3. Merge only after a separate production release approval; this document does not grant that approval.
|
||||
4. Read the resulting main merge OID before building an artifact.
|
||||
5. Deploy only the dormant Candidate runtime after explicit production approval.
|
||||
6. Create a tag and update Registry or Baseline only under their separate governed approvals.
|
||||
|
||||
## Evidence requirements
|
||||
|
||||
The execution agent must retain task ID, commit OID, artifact and manifest digests, test evidence, rollback package reference, and approval receipt metadata. It must not include plaintext credentials or private keys.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
Stop with NOT_EXECUTED if any approval, four-way reference closure, artifact verification, or deployment gate is absent. Do not infer production authorization from Candidate authorization.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"AUTONOMOUS_ACCEPTANCE_COMPLETED": true,
|
||||
"CURRENT_VERSION_FAST_ACTIVATION": true,
|
||||
"OPENBAO_AUTONOMY_BOOTSTRAP_READY": true,
|
||||
"OPENBAO_PRODUCTION_ACTIVE": true,
|
||||
"OpenBao_version": "2.6.1",
|
||||
"RECONCILIATION_REQUIRED": false,
|
||||
"RUNTIME_PRODUCTION_ACTIVE": true,
|
||||
"activation_mode": "CURRENT_VERIFIED_RUNTIME_FAST_ACTIVATION",
|
||||
"completed_at": "2026-08-04T15:40:25.329762Z",
|
||||
"execution_status": "COMPLETED",
|
||||
"failure_code": null,
|
||||
"formal_new_version_created": false,
|
||||
"new_release_tag_created": false,
|
||||
"production_baseline": "V2R1C3",
|
||||
"production_baseline_promoted": false,
|
||||
"run_id": "20260804T152318Z-a0cb20f1",
|
||||
"schema": "aios.openbao.current-version-fast-activation.v1"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# AIOS OpenBao 2.6.1 当前版本快速生产激活报告
|
||||
|
||||
- 执行状态:`COMPLETED`
|
||||
- Run ID:`20260804T152318Z-a0cb20f1`
|
||||
- 激活模式:`CURRENT_VERIFIED_RUNTIME_FAST_ACTIVATION`
|
||||
- OpenBao:`2.6.1`,active、initialized、unsealed
|
||||
- Controller:重启后自动重连,受限 AppRole 读写事务通过,未授权读取被拒绝
|
||||
- 即时稳定窗口:`900` 秒,全部异常计数为 0
|
||||
- 回滚就绪:2.6.0 隔离回滚及 2.6.1 前向恢复证据有效
|
||||
- Production Baseline:`V2R1C3`,未变
|
||||
- Patch 提交:待受治理隔离 Writer 完成后写入独立审计回执
|
||||
|
||||
## 版本边界
|
||||
|
||||
当前 OpenBao 2.6.1 运行版本已上线,不等于新的 AIOS 正式版本已发布。
|
||||
|
||||
```text
|
||||
formal_new_version_created=false
|
||||
new_release_tag_created=false
|
||||
production_baseline_promoted=false
|
||||
```
|
||||
|
||||
## 治理声明
|
||||
|
||||
```text
|
||||
force_push_used=false
|
||||
tag_rewritten=false
|
||||
direct_main_push=false
|
||||
secrets_committed=false
|
||||
production_baseline_changed=false
|
||||
owner_approval_respected=true
|
||||
72_hour_gate_waived_by_owner=true
|
||||
current_version_fast_activation_authorized=true
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
{"base_oid":"875d41c3101093149414292067fd700dc214d4d3","branch":"patch/chatgpt-autonomous-completion-contract-20260726","created_at":"2026-08-04T09:30:00Z","final_ready_condition":{"all_required":["main_gitea_oid_reconciliation","plaintext_private_key_cleanup","writer_residual_cleanup","openbao_binary_rollback_drill"],"status":"BLOCKED_UNTIL_ALL_GATES_PASS"},"gates":{"main_gitea_oid_reconciliation":{"required_evidence":["authority_main_oid","gitea_main_ref_oid","gitea_api_main_oid","all_main_oids_equal"],"status":"REQUIRED","success_condition":"Authority, Gitea ref, and Gitea API main OIDs are identical."},"openbao_binary_rollback_drill":{"required_evidence":["installed_binary_sha256","rollback_binary_sha256","rollback_execution_receipt","restore_execution_receipt","post_restore_smoke_test"],"status":"REQUIRED","success_condition":"Verified rollback and restore complete without software-key fallback or production mutation."},"plaintext_private_key_cleanup":{"required_evidence":["scanned_path_manifest","plaintext_private_key_match_count_zero","cleanup_receipt_digest","post_cleanup_rescan_pass"],"status":"REQUIRED","success_condition":"No plaintext private-key material remains in governed runtime, evidence, logs, or repository scopes."},"writer_residual_cleanup":{"required_evidence":["task_credentials_revoked","runtime_projection_removed","temporary_writer_units_stopped","writer_socket_removed","writer_process_count_zero"],"status":"REQUIRED","success_condition":"All task-scoped Writer credentials and residual runtime processes are removed after closure."}},"governance":{"72_hour_gate_waived_by_owner":true,"baseline_update_allowed":false,"direct_main_push":false,"force_push":false,"main_write_allowed":false,"secrets_committed":false,"tag_operation_allowed":false},"repository":"aios/aios","runtime_state":{"OPENBAO_AUTONOMY_BOOTSTRAP_READY":false,"OPENBAO_PRODUCTION_ACTIVE":false,"RECONCILIATION_REQUIRED":true,"RUNTIME_PRODUCTION_ACTIVE":true},"schema_id":"AIOS_OPENBAO_HARD_GATE_CLOSURE_CONTRACT_V1","schema_version":1}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# AIOS OpenBao 硬门禁关闭报告
|
||||
|
||||
- 执行状态:`COMPLETED`
|
||||
- Run ID:`20260804T121711Z-3322267`
|
||||
- 生成时间:`2026-08-04T12:18:14.425028+00:00`
|
||||
- Production Baseline:`V2R1C3`(未修改)
|
||||
- Authority main:`875d41c3101093149414292067fd700dc214d4d3`(未修改)
|
||||
- 72 小时时间门禁:所有者豁免
|
||||
- 其他硬门禁:全部要求独立通过
|
||||
|
||||
## 门禁结果
|
||||
|
||||
```json
|
||||
{
|
||||
"BINARY_ROLLBACK_VERIFICATION": {
|
||||
"details": {
|
||||
"forward_restore_to": "2.6.1",
|
||||
"previous_binary_sha256": "291aaca2d5f9b4f371b4596c6ccbd7a4132f3667ddf5fee8efe11614d84cd928",
|
||||
"previous_version": "2.6.0"
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:44.578065+00:00"
|
||||
},
|
||||
"FINAL_GIT_BASELINE_REVALIDATION": {
|
||||
"details": {
|
||||
"main_after": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"main_before": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"main_unchanged": true,
|
||||
"tags_after": "56f2d68ea7361c2adfa232b87fc5753358d760829f6329b2685f62e0d364963e",
|
||||
"tags_before": "56f2d68ea7361c2adfa232b87fc5753358d760829f6329b2685f62e0d364963e",
|
||||
"tags_unchanged": true
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:14.423477+00:00"
|
||||
},
|
||||
"FINAL_RUNTIME_REVALIDATION": {
|
||||
"details": {
|
||||
"controlled_restart_verified": true,
|
||||
"controller_active": true,
|
||||
"openbao_active": true
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:10.671081+00:00"
|
||||
},
|
||||
"FINAL_SECRET_LEAK_SCAN": {
|
||||
"details": {
|
||||
"private_keys": 0,
|
||||
"text_findings": 0,
|
||||
"writer_runtime_paths": 0
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:10.863905+00:00"
|
||||
},
|
||||
"PLAINTEXT_KEYS_AND_STALE_WRITERS": {
|
||||
"details": {
|
||||
"managed_migrations": [
|
||||
"ca_key",
|
||||
"server_key",
|
||||
"seal_key",
|
||||
"approle_secret"
|
||||
],
|
||||
"plaintext_private_key_count": 0,
|
||||
"stale_key_actions": 0,
|
||||
"stale_writer_resource_count": 0
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:18.012919+00:00"
|
||||
},
|
||||
"REPOSITORY_MAIN_RECONCILIATION": {
|
||||
"details": {
|
||||
"authority_main": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"classification": "INTENTIONAL_BRANCH_SCOPED_PROJECTION",
|
||||
"gitea_main": "8291b8368bf690416a3159ac5d21d9f8139c2e61",
|
||||
"patch_oid": "c2391bd7f4b2d71e660a4caad8dc22276d99abc8"
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:17.133323+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最终状态
|
||||
|
||||
```text
|
||||
OPENBAO_AUTONOMY_BOOTSTRAP_READY=true
|
||||
OPENBAO_PRODUCTION_ACTIVE=true
|
||||
AUTONOMOUS_ACCEPTANCE_COMPLETED=true
|
||||
RECONCILIATION_REQUIRED=false
|
||||
```
|
||||
|
||||
## 治理声明
|
||||
|
||||
```text
|
||||
force_push_used=false
|
||||
tag_rewritten=false
|
||||
direct_main_push=false
|
||||
secrets_committed=false
|
||||
production_baseline_changed=false
|
||||
owner_approval_respected=true
|
||||
72_hour_gate_waived_by_owner=true
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"authority_main_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"execution_status": "COMPLETED",
|
||||
"gates": {
|
||||
"BINARY_ROLLBACK_VERIFICATION": {
|
||||
"details": {
|
||||
"forward_restore_to": "2.6.1",
|
||||
"previous_binary_sha256": "291aaca2d5f9b4f371b4596c6ccbd7a4132f3667ddf5fee8efe11614d84cd928",
|
||||
"previous_version": "2.6.0"
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:44.578065+00:00"
|
||||
},
|
||||
"FINAL_GIT_BASELINE_REVALIDATION": {
|
||||
"details": {
|
||||
"main_after": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"main_before": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"main_unchanged": true,
|
||||
"tags_after": "56f2d68ea7361c2adfa232b87fc5753358d760829f6329b2685f62e0d364963e",
|
||||
"tags_before": "56f2d68ea7361c2adfa232b87fc5753358d760829f6329b2685f62e0d364963e",
|
||||
"tags_unchanged": true
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:14.423477+00:00"
|
||||
},
|
||||
"FINAL_RUNTIME_REVALIDATION": {
|
||||
"details": {
|
||||
"controlled_restart_verified": true,
|
||||
"controller_active": true,
|
||||
"openbao_active": true
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:10.671081+00:00"
|
||||
},
|
||||
"FINAL_SECRET_LEAK_SCAN": {
|
||||
"details": {
|
||||
"private_keys": 0,
|
||||
"text_findings": 0,
|
||||
"writer_runtime_paths": 0
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:18:10.863905+00:00"
|
||||
},
|
||||
"PLAINTEXT_KEYS_AND_STALE_WRITERS": {
|
||||
"details": {
|
||||
"managed_migrations": [
|
||||
"ca_key",
|
||||
"server_key",
|
||||
"seal_key",
|
||||
"approle_secret"
|
||||
],
|
||||
"plaintext_private_key_count": 0,
|
||||
"stale_key_actions": 0,
|
||||
"stale_writer_resource_count": 0
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:18.012919+00:00"
|
||||
},
|
||||
"REPOSITORY_MAIN_RECONCILIATION": {
|
||||
"details": {
|
||||
"authority_main": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"classification": "INTENTIONAL_BRANCH_SCOPED_PROJECTION",
|
||||
"gitea_main": "8291b8368bf690416a3159ac5d21d9f8139c2e61",
|
||||
"patch_oid": "c2391bd7f4b2d71e660a4caad8dc22276d99abc8"
|
||||
},
|
||||
"passed": true,
|
||||
"verified_at": "2026-08-04T12:17:17.133323+00:00"
|
||||
}
|
||||
},
|
||||
"generated_at": "2026-08-04T12:18:14.425202+00:00",
|
||||
"production_baseline": "V2R1C3",
|
||||
"ready": true,
|
||||
"run_id": "20260804T121711Z-3322267",
|
||||
"schema": "aios.openbao.hard-gate-patch-summary.v1",
|
||||
"secrets_included": false
|
||||
}
|
||||
@@ -1,68 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic Candidate artifact without overwriting evidence."""
|
||||
"""Build deterministic dormant-release candidate artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
|
||||
import tarfile
|
||||
|
||||
|
||||
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 sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
def build(source: Path, output: Path, source_oid: str, tree_oid: str) -> dict[str, str]:
|
||||
if not source.exists() or not source.is_dir():
|
||||
raise SystemExit("source directory missing")
|
||||
files = []
|
||||
for item in sorted(source.rglob("*")):
|
||||
if item.is_file() and ".git" not in item.parts:
|
||||
files.append(item)
|
||||
artifact = output / "candidate.tar"
|
||||
manifest = output / "manifest.json"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(artifact, "w", format=tarfile.PAX_FORMAT) as archive:
|
||||
for item in files:
|
||||
info = archive.gettarinfo(str(item), arcname=str(item.relative_to(source)))
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
with item.open("rb") as handle:
|
||||
archive.addfile(info, handle)
|
||||
payload = {
|
||||
"source_oid": source_oid,
|
||||
"tree_oid": tree_oid,
|
||||
"artifact_sha256": sha256(artifact.read_bytes()),
|
||||
"files": {
|
||||
str(item.relative_to(source)): sha256(item.read_bytes())
|
||||
for item in files
|
||||
},
|
||||
}
|
||||
manifest.write_text(json.dumps(payload, sort_keys=True, indent=2), encoding="utf-8")
|
||||
return {
|
||||
"artifact_sha256": payload["artifact_sha256"],
|
||||
"manifest_sha256": sha256(manifest.read_bytes()),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
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,
|
||||
)
|
||||
)
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("source_oid")
|
||||
parser.add_argument("tree_oid")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(build(args.source, args.output, args.source_oid, args.tree_oid)))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -1,513 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run Candidate acceptance and emit a non-overwriting JSON evidence record."""
|
||||
"""Evaluate AIOS V2R1C4 dormant release evidence.
|
||||
|
||||
This command performs no Git, network, deployment, tag, Registry or Baseline
|
||||
write. It evaluates explicit evidence supplied by a governed execution agent
|
||||
and writes a deterministic JSON decision report.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Sequence
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
PACKAGE_ROOT = REPOSITORY_ROOT / "services" / "aios-autonomous-release-candidate"
|
||||
if str(PACKAGE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PACKAGE_ROOT))
|
||||
|
||||
|
||||
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",
|
||||
from aios_autonomous_release_candidate.acceptance import ( # noqa: E402
|
||||
AcceptancePhase,
|
||||
AcceptanceStatus,
|
||||
DormantAcceptanceGate,
|
||||
DormantAcceptanceInput,
|
||||
FinalDormantAcceptanceGate,
|
||||
FinalDormantAcceptanceInput,
|
||||
)
|
||||
from aios_autonomous_release_candidate.models import sha256_json # noqa: E402
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
REPORT_SCHEMA = "aios.dormant-release.acceptance/v1"
|
||||
|
||||
|
||||
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
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RunnerDecision:
|
||||
phase: str
|
||||
status: str
|
||||
report: Mapping[str, Any]
|
||||
exit_code: int
|
||||
|
||||
|
||||
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)
|
||||
def read_json_object(path: Path) -> Mapping[str, Any]:
|
||||
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)
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError as exc:
|
||||
raise ValueError(f"input file does not exist: {path}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"input is not valid JSON: {exc.msg}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("input must contain one JSON object")
|
||||
return value
|
||||
|
||||
|
||||
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,
|
||||
def atomic_write_json(path: Path, payload: Mapping[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(payload, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
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,
|
||||
temporary.chmod(0o600)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def evaluate(phase: AcceptancePhase, evidence: Mapping[str, Any]) -> RunnerDecision:
|
||||
if phase is AcceptancePhase.CANDIDATE:
|
||||
parsed = DormantAcceptanceInput.from_mapping(evidence)
|
||||
result = DormantAcceptanceGate().evaluate(parsed)
|
||||
else:
|
||||
parsed = FinalDormantAcceptanceInput.from_mapping(evidence)
|
||||
result = FinalDormantAcceptanceGate().evaluate(parsed)
|
||||
result_payload = dict(result.payload())
|
||||
report = {
|
||||
"schema": REPORT_SCHEMA,
|
||||
"phase": result.phase.value,
|
||||
"status": result.status.value,
|
||||
"decision": result_payload,
|
||||
"input_sha256": sha256_json(evidence),
|
||||
"production_action_authorized": False,
|
||||
}
|
||||
return RunnerDecision(
|
||||
phase=result.phase.value,
|
||||
status=result.status.value,
|
||||
report=report,
|
||||
exit_code=0 if result.status is AcceptanceStatus.READY else 3,
|
||||
)
|
||||
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)
|
||||
|
||||
def invalid_report(phase: str, message: str) -> RunnerDecision:
|
||||
report = {
|
||||
"schema": REPORT_SCHEMA,
|
||||
"phase": phase,
|
||||
"status": AcceptanceStatus.BLOCKED.value,
|
||||
"decision": {
|
||||
"blocking_code": "INVALID_ACCEPTANCE_INPUT",
|
||||
"blocking_message": message,
|
||||
},
|
||||
"production_action_authorized": False,
|
||||
}
|
||||
return RunnerDecision(
|
||||
phase=phase,
|
||||
status=AcceptanceStatus.BLOCKED.value,
|
||||
report=report,
|
||||
exit_code=2,
|
||||
)
|
||||
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 = argparse.ArgumentParser(prog="run_acceptance.py")
|
||||
parser.add_argument(
|
||||
"--phase",
|
||||
required=True,
|
||||
choices=[phase.value for phase in AcceptancePhase],
|
||||
)
|
||||
parser.add_argument("--input", required=True, 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
|
||||
phase = AcceptancePhase(args.phase)
|
||||
evidence = read_json_object(args.input)
|
||||
decision = evaluate(phase, evidence)
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
decision = invalid_report(args.phase, str(exc))
|
||||
|
||||
envelope = dict(decision.report)
|
||||
envelope["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
atomic_write_json(args.output, envelope)
|
||||
print(json.dumps(envelope, sort_keys=True))
|
||||
return decision.exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
"""Machine-verifiable acceptance contracts for dormant V2R1C4 delivery.
|
||||
|
||||
The contracts consume explicit evidence produced by a governed execution agent.
|
||||
They never infer remote Git consistency, approval validity or artifact
|
||||
provenance from a local checkout alone.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import MISSING, dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .dormant import (
|
||||
DeploymentMode,
|
||||
DormantReleaseCandidate,
|
||||
DormantReleaseGate,
|
||||
ExecutionFlags,
|
||||
FinalDormantRelease,
|
||||
)
|
||||
from .errors import ReleaseBlocked
|
||||
from .models import is_git_oid, is_sha256, sha256_json
|
||||
|
||||
|
||||
class AcceptanceStatus(str, Enum):
|
||||
READY = "READY"
|
||||
BLOCKED = "BLOCKED"
|
||||
|
||||
|
||||
class AcceptancePhase(str, Enum):
|
||||
CANDIDATE = "candidate"
|
||||
FINAL = "final"
|
||||
|
||||
|
||||
def _required_field_names(model: type[Any]) -> set[str]:
|
||||
return {
|
||||
name
|
||||
for name, field in model.__dataclass_fields__.items()
|
||||
if field.default is MISSING and field.default_factory is MISSING
|
||||
}
|
||||
|
||||
|
||||
def _strict_mapping(model: type[Any], value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("acceptance evidence must be a JSON object")
|
||||
allowed = set(model.__dataclass_fields__)
|
||||
unknown = sorted(set(value) - allowed)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown acceptance fields: {', '.join(unknown)}")
|
||||
missing = sorted(_required_field_names(model) - set(value))
|
||||
if missing:
|
||||
raise ValueError(f"missing acceptance fields: {', '.join(missing)}")
|
||||
return dict(value)
|
||||
|
||||
|
||||
def _require_bool(name: str, value: Any) -> None:
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(f"{name} must be a boolean")
|
||||
|
||||
|
||||
def _require_non_negative_int(name: str, value: Any) -> None:
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
||||
raise ValueError(f"{name} must be a non-negative integer")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DormantAcceptanceInput:
|
||||
"""Immutable pre-merge evidence for the dormant Candidate package."""
|
||||
|
||||
version: str
|
||||
pull_request: int
|
||||
candidate_oid: str
|
||||
candidate_tree_oid: str
|
||||
integration_oid: str
|
||||
integration_tree_oid: str
|
||||
artifact_sha256: str
|
||||
manifest_sha256: str
|
||||
package_closure_sha256: str
|
||||
evidence_closure_oid: str
|
||||
rollback_artifact_sha256: str
|
||||
production_config_sha256: str
|
||||
test_evidence_sha256: str
|
||||
tests_passed: int
|
||||
tests_failed: int
|
||||
audited_skips: int
|
||||
candidate_four_way_ref_closure: bool
|
||||
integration_four_way_ref_closure: bool
|
||||
candidate_reachable_from_integration: bool
|
||||
worktree_clean: bool
|
||||
gitops_writes_enabled: bool
|
||||
task_plane_execution_enabled: bool
|
||||
silent_release_execution_enabled: bool
|
||||
protected_dispatch_transport: str = "sandbox"
|
||||
native_openclaw_enabled: bool = False
|
||||
native_codex_enabled: bool = False
|
||||
automatic_release_enabled: bool = False
|
||||
production_side_effects: bool = False
|
||||
activation_deferred: bool = True
|
||||
deployment_mode: str = DeploymentMode.DORMANT_SHADOW_ONLY.value
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.version != "V2R1C4":
|
||||
raise ValueError("version must be V2R1C4")
|
||||
if isinstance(self.pull_request, bool) or not isinstance(self.pull_request, int) or self.pull_request < 1:
|
||||
raise ValueError("pull_request must be a positive integer")
|
||||
for name in (
|
||||
"candidate_oid",
|
||||
"candidate_tree_oid",
|
||||
"integration_oid",
|
||||
"integration_tree_oid",
|
||||
"evidence_closure_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",
|
||||
"test_evidence_sha256",
|
||||
):
|
||||
if not is_sha256(getattr(self, name)):
|
||||
raise ValueError(f"{name} must be a lowercase SHA-256 digest")
|
||||
for name in ("tests_passed", "tests_failed", "audited_skips"):
|
||||
_require_non_negative_int(name, getattr(self, name))
|
||||
for name in (
|
||||
"candidate_four_way_ref_closure",
|
||||
"integration_four_way_ref_closure",
|
||||
"candidate_reachable_from_integration",
|
||||
"worktree_clean",
|
||||
"gitops_writes_enabled",
|
||||
"task_plane_execution_enabled",
|
||||
"silent_release_execution_enabled",
|
||||
"native_openclaw_enabled",
|
||||
"native_codex_enabled",
|
||||
"automatic_release_enabled",
|
||||
"production_side_effects",
|
||||
"activation_deferred",
|
||||
):
|
||||
_require_bool(name, getattr(self, name))
|
||||
try:
|
||||
DeploymentMode(self.deployment_mode)
|
||||
except ValueError as exc:
|
||||
raise ValueError("deployment_mode is invalid") from exc
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: Mapping[str, Any]) -> "DormantAcceptanceInput":
|
||||
return cls(**_strict_mapping(cls, value))
|
||||
|
||||
def release_candidate(self) -> DormantReleaseCandidate:
|
||||
return DormantReleaseCandidate(
|
||||
version=self.version,
|
||||
pull_request=self.pull_request,
|
||||
integration_oid=self.integration_oid,
|
||||
integration_tree_oid=self.integration_tree_oid,
|
||||
artifact_sha256=self.artifact_sha256,
|
||||
manifest_sha256=self.manifest_sha256,
|
||||
package_closure_sha256=self.package_closure_sha256,
|
||||
evidence_closure_oid=self.evidence_closure_oid,
|
||||
rollback_artifact_sha256=self.rollback_artifact_sha256,
|
||||
production_config_sha256=self.production_config_sha256,
|
||||
execution_flags=ExecutionFlags(
|
||||
gitops_writes_enabled=self.gitops_writes_enabled,
|
||||
task_plane_execution_enabled=self.task_plane_execution_enabled,
|
||||
silent_release_execution_enabled=self.silent_release_execution_enabled,
|
||||
),
|
||||
deployment_mode=DeploymentMode(self.deployment_mode),
|
||||
protected_dispatch_transport=self.protected_dispatch_transport,
|
||||
native_openclaw_enabled=self.native_openclaw_enabled,
|
||||
native_codex_enabled=self.native_codex_enabled,
|
||||
automatic_release_enabled=self.automatic_release_enabled,
|
||||
production_side_effects=self.production_side_effects,
|
||||
activation_deferred=self.activation_deferred,
|
||||
)
|
||||
|
||||
def payload(self) -> Mapping[str, Any]:
|
||||
return {name: getattr(self, name) for name in self.__dataclass_fields__}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FinalDormantAcceptanceInput:
|
||||
"""Post-merge evidence bound to the exact main merge commit and approval."""
|
||||
|
||||
candidate: DormantAcceptanceInput
|
||||
target_oid: str
|
||||
target_tree_oid: str
|
||||
artifact_sha256: str
|
||||
manifest_sha256: str
|
||||
package_closure_sha256: str
|
||||
evidence_closure_oid: str
|
||||
rollback_artifact_sha256: str
|
||||
production_config_sha256: str
|
||||
approval_plan_id: str
|
||||
approval_receipt_sha256: str
|
||||
approved_release_digest: str
|
||||
main_four_way_ref_closure: bool
|
||||
target_is_main_head: bool
|
||||
worktree_clean: bool
|
||||
artifact_rebuilt_from_target: bool
|
||||
rollback_verified: bool
|
||||
runtime_boundary_verified: bool
|
||||
owner_approval_verified: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.candidate, DormantAcceptanceInput):
|
||||
raise ValueError("candidate must be DormantAcceptanceInput")
|
||||
for name in ("target_oid", "target_tree_oid", "evidence_closure_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",
|
||||
"approval_receipt_sha256",
|
||||
"approved_release_digest",
|
||||
):
|
||||
if not is_sha256(getattr(self, name)):
|
||||
raise ValueError(f"{name} must be a lowercase SHA-256 digest")
|
||||
if not self.approval_plan_id.strip():
|
||||
raise ValueError("approval_plan_id is required")
|
||||
for name in (
|
||||
"main_four_way_ref_closure",
|
||||
"target_is_main_head",
|
||||
"worktree_clean",
|
||||
"artifact_rebuilt_from_target",
|
||||
"rollback_verified",
|
||||
"runtime_boundary_verified",
|
||||
"owner_approval_verified",
|
||||
):
|
||||
_require_bool(name, getattr(self, name))
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: Mapping[str, Any]) -> "FinalDormantAcceptanceInput":
|
||||
data = _strict_mapping(cls, value)
|
||||
candidate = data.get("candidate")
|
||||
if isinstance(candidate, DormantAcceptanceInput):
|
||||
parsed_candidate = candidate
|
||||
elif isinstance(candidate, Mapping):
|
||||
parsed_candidate = DormantAcceptanceInput.from_mapping(candidate)
|
||||
else:
|
||||
raise ValueError("candidate must be a JSON object")
|
||||
data["candidate"] = parsed_candidate
|
||||
return cls(**data)
|
||||
|
||||
def payload(self) -> Mapping[str, Any]:
|
||||
return {
|
||||
"candidate": dict(self.candidate.payload()),
|
||||
**{
|
||||
name: getattr(self, name)
|
||||
for name in self.__dataclass_fields__
|
||||
if name != "candidate"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DormantAcceptanceResult:
|
||||
phase: AcceptancePhase
|
||||
status: AcceptanceStatus
|
||||
acceptance_digest: str
|
||||
release_digest: str | None
|
||||
blocking_code: str | None
|
||||
blocking_message: str | None
|
||||
evidence: Mapping[str, Any]
|
||||
|
||||
def payload(self) -> Mapping[str, Any]:
|
||||
return {
|
||||
"phase": self.phase.value,
|
||||
"status": self.status.value,
|
||||
"acceptance_digest": self.acceptance_digest,
|
||||
"release_digest": self.release_digest,
|
||||
"blocking_code": self.blocking_code,
|
||||
"blocking_message": self.blocking_message,
|
||||
"evidence": dict(self.evidence),
|
||||
}
|
||||
|
||||
|
||||
class DormantAcceptanceGate:
|
||||
"""Fail-closed pre-merge gate for a dormant release candidate."""
|
||||
|
||||
def verify(self, evidence: DormantAcceptanceInput) -> DormantReleaseCandidate:
|
||||
if evidence.candidate_oid == evidence.integration_oid:
|
||||
raise ReleaseBlocked(
|
||||
"INTEGRATION_MERGE_REQUIRED",
|
||||
"integration must be a distinct governed merge commit",
|
||||
)
|
||||
if not evidence.candidate_four_way_ref_closure:
|
||||
raise ReleaseBlocked(
|
||||
"CANDIDATE_REF_CLOSURE_REQUIRED",
|
||||
"candidate local, bare, remote and Gitea references are not closed",
|
||||
)
|
||||
if not evidence.integration_four_way_ref_closure:
|
||||
raise ReleaseBlocked(
|
||||
"INTEGRATION_REF_CLOSURE_REQUIRED",
|
||||
"integration local, bare, remote and Gitea references are not closed",
|
||||
)
|
||||
if not evidence.candidate_reachable_from_integration:
|
||||
raise ReleaseBlocked(
|
||||
"CANDIDATE_LINEAGE_REQUIRED",
|
||||
"candidate is not reachable from the integration commit",
|
||||
)
|
||||
if not evidence.worktree_clean:
|
||||
raise ReleaseBlocked("WORKTREE_NOT_CLEAN", "acceptance worktree contains uncommitted changes")
|
||||
if evidence.tests_failed:
|
||||
raise ReleaseBlocked("TEST_FAILURES_PRESENT", "candidate test failures are present")
|
||||
if evidence.tests_passed < 1:
|
||||
raise ReleaseBlocked("TEST_EVIDENCE_REQUIRED", "at least one passing test is required")
|
||||
candidate = evidence.release_candidate()
|
||||
DormantReleaseGate().verify_candidate(candidate)
|
||||
return candidate
|
||||
|
||||
def evaluate(self, evidence: DormantAcceptanceInput) -> DormantAcceptanceResult:
|
||||
acceptance_digest = sha256_json(evidence.payload())
|
||||
try:
|
||||
candidate = self.verify(evidence)
|
||||
except (ReleaseBlocked, ValueError) as exc:
|
||||
return DormantAcceptanceResult(
|
||||
phase=AcceptancePhase.CANDIDATE,
|
||||
status=AcceptanceStatus.BLOCKED,
|
||||
acceptance_digest=acceptance_digest,
|
||||
release_digest=None,
|
||||
blocking_code=getattr(exc, "code", "INVALID_ACCEPTANCE_INPUT"),
|
||||
blocking_message=str(exc),
|
||||
evidence=evidence.payload(),
|
||||
)
|
||||
return DormantAcceptanceResult(
|
||||
phase=AcceptancePhase.CANDIDATE,
|
||||
status=AcceptanceStatus.READY,
|
||||
acceptance_digest=acceptance_digest,
|
||||
release_digest=candidate.digest(),
|
||||
blocking_code=None,
|
||||
blocking_message=None,
|
||||
evidence=evidence.payload(),
|
||||
)
|
||||
|
||||
|
||||
class FinalDormantAcceptanceGate:
|
||||
"""Fail-closed post-merge gate before deployment and tag execution."""
|
||||
|
||||
def verify(self, evidence: FinalDormantAcceptanceInput) -> FinalDormantRelease:
|
||||
candidate = DormantAcceptanceGate().verify(evidence.candidate)
|
||||
if not evidence.main_four_way_ref_closure:
|
||||
raise ReleaseBlocked(
|
||||
"MAIN_REF_CLOSURE_REQUIRED",
|
||||
"main local, bare, remote and Gitea references are not closed",
|
||||
)
|
||||
if not evidence.target_is_main_head:
|
||||
raise ReleaseBlocked("TARGET_NOT_MAIN_HEAD", "target OID is not the verified main HEAD")
|
||||
if not evidence.worktree_clean:
|
||||
raise ReleaseBlocked("WORKTREE_NOT_CLEAN", "final release worktree contains changes")
|
||||
if not evidence.artifact_rebuilt_from_target:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_REBUILD_REQUIRED",
|
||||
"production artifact was not rebuilt from the final main target",
|
||||
)
|
||||
if not evidence.rollback_verified:
|
||||
raise ReleaseBlocked("ROLLBACK_EVIDENCE_REQUIRED", "rollback package is not verified")
|
||||
if not evidence.runtime_boundary_verified:
|
||||
raise ReleaseBlocked(
|
||||
"RUNTIME_BOUNDARY_REQUIRED",
|
||||
"dormant runtime flags and protected transport are not verified",
|
||||
)
|
||||
final = DormantReleaseGate().finalize(
|
||||
candidate,
|
||||
target_oid=evidence.target_oid,
|
||||
target_tree_oid=evidence.target_tree_oid,
|
||||
artifact_sha256=evidence.artifact_sha256,
|
||||
manifest_sha256=evidence.manifest_sha256,
|
||||
package_closure_sha256=evidence.package_closure_sha256,
|
||||
evidence_closure_oid=evidence.evidence_closure_oid,
|
||||
rollback_artifact_sha256=evidence.rollback_artifact_sha256,
|
||||
production_config_sha256=evidence.production_config_sha256,
|
||||
)
|
||||
if not evidence.owner_approval_verified:
|
||||
raise ReleaseBlocked("OWNER_APPROVAL_REQUIRED", "exact final release approval is absent")
|
||||
if evidence.approved_release_digest != final.digest():
|
||||
raise ReleaseBlocked(
|
||||
"APPROVAL_SUBJECT_MISMATCH",
|
||||
"owner approval is not bound to the exact final release digest",
|
||||
)
|
||||
return final
|
||||
|
||||
def evaluate(self, evidence: FinalDormantAcceptanceInput) -> DormantAcceptanceResult:
|
||||
acceptance_digest = sha256_json(evidence.payload())
|
||||
try:
|
||||
final = self.verify(evidence)
|
||||
except (ReleaseBlocked, ValueError) as exc:
|
||||
return DormantAcceptanceResult(
|
||||
phase=AcceptancePhase.FINAL,
|
||||
status=AcceptanceStatus.BLOCKED,
|
||||
acceptance_digest=acceptance_digest,
|
||||
release_digest=None,
|
||||
blocking_code=getattr(exc, "code", "INVALID_ACCEPTANCE_INPUT"),
|
||||
blocking_message=str(exc),
|
||||
evidence=evidence.payload(),
|
||||
)
|
||||
return DormantAcceptanceResult(
|
||||
phase=AcceptancePhase.FINAL,
|
||||
status=AcceptanceStatus.READY,
|
||||
acceptance_digest=acceptance_digest,
|
||||
release_digest=final.digest(),
|
||||
blocking_code=None,
|
||||
blocking_message=None,
|
||||
evidence=evidence.payload(),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AcceptancePhase",
|
||||
"AcceptanceStatus",
|
||||
"DormantAcceptanceInput",
|
||||
"FinalDormantAcceptanceInput",
|
||||
"DormantAcceptanceResult",
|
||||
"DormantAcceptanceGate",
|
||||
"FinalDormantAcceptanceGate",
|
||||
]
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
"""Dormant/shadow-only release profile for fast, governed production delivery.
|
||||
|
||||
This module separates verified code deployment from capability activation. A
|
||||
dormant release may install approved code in production while all autonomous
|
||||
execution, native-agent and automatic-release capabilities remain fail-closed.
|
||||
Activation is a later, independently approved operation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .errors import ReleaseBlocked
|
||||
from .models import is_git_oid, is_sha256, sha256_json
|
||||
|
||||
|
||||
class DeploymentMode(str, Enum):
|
||||
DORMANT_SHADOW_ONLY = "dormant_shadow_only"
|
||||
ACTIVATED_PRODUCTION = "activated_production"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExecutionFlags:
|
||||
"""Production execution switches captured in release evidence."""
|
||||
|
||||
gitops_writes_enabled: bool
|
||||
task_plane_execution_enabled: bool
|
||||
silent_release_execution_enabled: bool
|
||||
|
||||
def dormant(self) -> bool:
|
||||
return not (
|
||||
self.gitops_writes_enabled
|
||||
or self.task_plane_execution_enabled
|
||||
or self.silent_release_execution_enabled
|
||||
)
|
||||
|
||||
def as_mapping(self) -> Mapping[str, bool]:
|
||||
return {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": self.gitops_writes_enabled,
|
||||
"AIOS_TASK_PLANE_EXECUTION_ENABLED": self.task_plane_execution_enabled,
|
||||
"AIOS_SILENT_RELEASE_EXECUTION_ENABLED": self.silent_release_execution_enabled,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DormantReleaseCandidate:
|
||||
"""Immutable pre-merge release description for dormant production delivery."""
|
||||
|
||||
version: str
|
||||
pull_request: int
|
||||
integration_oid: str
|
||||
integration_tree_oid: str
|
||||
artifact_sha256: str
|
||||
manifest_sha256: str
|
||||
package_closure_sha256: str
|
||||
evidence_closure_oid: str
|
||||
rollback_artifact_sha256: str
|
||||
production_config_sha256: str
|
||||
execution_flags: ExecutionFlags
|
||||
deployment_mode: DeploymentMode = DeploymentMode.DORMANT_SHADOW_ONLY
|
||||
protected_dispatch_transport: str = "sandbox"
|
||||
native_openclaw_enabled: bool = False
|
||||
native_codex_enabled: bool = False
|
||||
automatic_release_enabled: bool = False
|
||||
production_side_effects: bool = False
|
||||
activation_deferred: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.version or self.pull_request < 1:
|
||||
raise ValueError("version and a positive pull request number are required")
|
||||
for name in ("integration_oid", "integration_tree_oid", "evidence_closure_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 digest")
|
||||
|
||||
def payload(self) -> Mapping[str, Any]:
|
||||
return {
|
||||
"version": self.version,
|
||||
"pull_request": self.pull_request,
|
||||
"integration_oid": self.integration_oid,
|
||||
"integration_tree_oid": self.integration_tree_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"manifest_sha256": self.manifest_sha256,
|
||||
"package_closure_sha256": self.package_closure_sha256,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"rollback_artifact_sha256": self.rollback_artifact_sha256,
|
||||
"production_config_sha256": self.production_config_sha256,
|
||||
"execution_flags": dict(self.execution_flags.as_mapping()),
|
||||
"deployment_mode": self.deployment_mode.value,
|
||||
"protected_dispatch_transport": self.protected_dispatch_transport,
|
||||
"native_openclaw_enabled": self.native_openclaw_enabled,
|
||||
"native_codex_enabled": self.native_codex_enabled,
|
||||
"automatic_release_enabled": self.automatic_release_enabled,
|
||||
"production_side_effects": self.production_side_effects,
|
||||
"activation_deferred": self.activation_deferred,
|
||||
"production_target_oid": "UNRESOLVED",
|
||||
}
|
||||
|
||||
def digest(self) -> str:
|
||||
return sha256_json(self.payload())
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FinalDormantRelease:
|
||||
"""Post-merge release binding to the exact production target commit."""
|
||||
|
||||
candidate: DormantReleaseCandidate
|
||||
target_oid: str
|
||||
target_tree_oid: str
|
||||
artifact_sha256: str
|
||||
manifest_sha256: str
|
||||
package_closure_sha256: str
|
||||
evidence_closure_oid: str
|
||||
rollback_artifact_sha256: str
|
||||
production_config_sha256: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for name in ("target_oid", "target_tree_oid", "evidence_closure_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 digest")
|
||||
|
||||
def payload(self) -> Mapping[str, Any]:
|
||||
return {
|
||||
**dict(self.candidate.payload()),
|
||||
"production_target_oid": self.target_oid,
|
||||
"production_target_tree_oid": self.target_tree_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"manifest_sha256": self.manifest_sha256,
|
||||
"package_closure_sha256": self.package_closure_sha256,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"rollback_artifact_sha256": self.rollback_artifact_sha256,
|
||||
"production_config_sha256": self.production_config_sha256,
|
||||
}
|
||||
|
||||
def digest(self) -> str:
|
||||
return sha256_json(self.payload())
|
||||
|
||||
|
||||
class DormantReleaseGate:
|
||||
"""Validate that a production deployment is dormant and side-effect free."""
|
||||
|
||||
_DORMANT_TRANSPORTS = frozenset({"mock", "sandbox"})
|
||||
|
||||
def verify_candidate(self, candidate: DormantReleaseCandidate) -> None:
|
||||
if candidate.deployment_mode is not DeploymentMode.DORMANT_SHADOW_ONLY:
|
||||
raise ReleaseBlocked("DORMANT_MODE_REQUIRED", "release is not dormant/shadow-only")
|
||||
if not candidate.execution_flags.dormant():
|
||||
raise ReleaseBlocked("DORMANT_FLAGS_REQUIRED", "all production execution flags must be false")
|
||||
if candidate.protected_dispatch_transport not in self._DORMANT_TRANSPORTS:
|
||||
raise ReleaseBlocked(
|
||||
"DORMANT_TRANSPORT_REQUIRED",
|
||||
"production dispatch transport must remain mock or sandbox",
|
||||
)
|
||||
if candidate.native_openclaw_enabled or candidate.native_codex_enabled:
|
||||
raise ReleaseBlocked(
|
||||
"NATIVE_AGENT_ACTIVATION_FORBIDDEN",
|
||||
"native agents must remain disabled",
|
||||
)
|
||||
if candidate.automatic_release_enabled:
|
||||
raise ReleaseBlocked(
|
||||
"AUTOMATIC_RELEASE_FORBIDDEN",
|
||||
"automatic release must remain disabled",
|
||||
)
|
||||
if candidate.production_side_effects:
|
||||
raise ReleaseBlocked(
|
||||
"PRODUCTION_SIDE_EFFECT_FORBIDDEN",
|
||||
"dormant release cannot enable side effects",
|
||||
)
|
||||
if not candidate.activation_deferred:
|
||||
raise ReleaseBlocked(
|
||||
"ACTIVATION_MUST_BE_DEFERRED",
|
||||
"capability activation requires a separate release",
|
||||
)
|
||||
|
||||
def finalize(
|
||||
self,
|
||||
candidate: DormantReleaseCandidate,
|
||||
*,
|
||||
target_oid: str,
|
||||
target_tree_oid: str,
|
||||
artifact_sha256: str,
|
||||
manifest_sha256: str,
|
||||
package_closure_sha256: str,
|
||||
evidence_closure_oid: str,
|
||||
rollback_artifact_sha256: str,
|
||||
production_config_sha256: str,
|
||||
) -> FinalDormantRelease:
|
||||
self.verify_candidate(candidate)
|
||||
final = FinalDormantRelease(
|
||||
candidate=candidate,
|
||||
target_oid=target_oid,
|
||||
target_tree_oid=target_tree_oid,
|
||||
artifact_sha256=artifact_sha256,
|
||||
manifest_sha256=manifest_sha256,
|
||||
package_closure_sha256=package_closure_sha256,
|
||||
evidence_closure_oid=evidence_closure_oid,
|
||||
rollback_artifact_sha256=rollback_artifact_sha256,
|
||||
production_config_sha256=production_config_sha256,
|
||||
)
|
||||
if final.target_oid == candidate.integration_oid:
|
||||
raise ReleaseBlocked(
|
||||
"FINAL_TARGET_NOT_MAIN_MERGE",
|
||||
"final production target must be the governed main merge commit, not the integration commit",
|
||||
)
|
||||
return final
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DeploymentMode",
|
||||
"ExecutionFlags",
|
||||
"DormantReleaseCandidate",
|
||||
"FinalDormantRelease",
|
||||
"DormantReleaseGate",
|
||||
]
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
"""Dormant candidate worker process.
|
||||
|
||||
The worker intentionally performs no autonomous production action. It exists to
|
||||
exercise lifecycle, heartbeat and shutdown behavior in candidate/shadow mode.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class WorkerRuntime:
|
||||
worker_id: str
|
||||
state_path: Path
|
||||
heartbeat_seconds: float
|
||||
running: bool = True
|
||||
|
||||
def stop(self, *_args: object) -> None:
|
||||
self.running = False
|
||||
|
||||
def heartbeat(self) -> None:
|
||||
payload = {
|
||||
"worker_id": self.worker_id,
|
||||
"mode": "candidate-shadow-only",
|
||||
"production_side_effects": False,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = self.state_path.with_suffix(self.state_path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(payload, sort_keys=True, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.chmod(0o600)
|
||||
temporary.replace(self.state_path)
|
||||
|
||||
def run(self) -> int:
|
||||
signal.signal(signal.SIGTERM, self.stop)
|
||||
signal.signal(signal.SIGINT, self.stop)
|
||||
while self.running:
|
||||
self.heartbeat()
|
||||
deadline = time.monotonic() + self.heartbeat_seconds
|
||||
while self.running and time.monotonic() < deadline:
|
||||
time.sleep(min(0.25, max(0.0, deadline - time.monotonic())))
|
||||
self.heartbeat()
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="aios-autonomous-release-worker")
|
||||
parser.add_argument("--worker-id", required=True)
|
||||
parser.add_argument("--state", type=Path, required=True)
|
||||
parser.add_argument("--heartbeat-seconds", type=float, default=5.0)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
if args.heartbeat_seconds < 1 or args.heartbeat_seconds > 60:
|
||||
raise SystemExit("heartbeat interval must be between 1 and 60 seconds")
|
||||
runtime = WorkerRuntime(
|
||||
worker_id=args.worker_id,
|
||||
state_path=args.state,
|
||||
heartbeat_seconds=args.heartbeat_seconds,
|
||||
)
|
||||
return runtime.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,15 +1,12 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=69"]
|
||||
requires = ["setuptools>=68"]
|
||||
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"
|
||||
description = "Dormant candidate orchestration and release governance for AIOS"
|
||||
requires-python = ">=3.11"
|
||||
license = { text = "Proprietary" }
|
||||
authors = [{ name = "AIOS" }]
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
@@ -19,7 +16,3 @@ aios-autonomous-release-service = "aios_autonomous_release_candidate.service:mai
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["aios_autonomous_release_candidate*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-ra --strict-config --strict-markers"
|
||||
testpaths = ["../../tests/autonomous_release"]
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from aios_autonomous_release_candidate.acceptance import (
|
||||
AcceptanceStatus,
|
||||
DormantAcceptanceGate,
|
||||
DormantAcceptanceInput,
|
||||
FinalDormantAcceptanceGate,
|
||||
FinalDormantAcceptanceInput,
|
||||
)
|
||||
from aios_autonomous_release_candidate.dormant import DormantReleaseGate
|
||||
|
||||
|
||||
OID_A = "a" * 40
|
||||
OID_B = "b" * 40
|
||||
OID_C = "c" * 40
|
||||
OID_D = "d" * 40
|
||||
OID_E = "e" * 40
|
||||
OID_F = "f" * 40
|
||||
SHA_A = "1" * 64
|
||||
SHA_B = "2" * 64
|
||||
SHA_C = "3" * 64
|
||||
SHA_D = "4" * 64
|
||||
SHA_E = "5" * 64
|
||||
|
||||
|
||||
def candidate_input(**overrides: object) -> DormantAcceptanceInput:
|
||||
data: dict[str, object] = {
|
||||
"version": "V2R1C4",
|
||||
"pull_request": 20,
|
||||
"candidate_oid": OID_A,
|
||||
"candidate_tree_oid": OID_B,
|
||||
"integration_oid": OID_C,
|
||||
"integration_tree_oid": OID_D,
|
||||
"artifact_sha256": SHA_A,
|
||||
"manifest_sha256": SHA_B,
|
||||
"package_closure_sha256": SHA_C,
|
||||
"evidence_closure_oid": OID_E,
|
||||
"rollback_artifact_sha256": SHA_D,
|
||||
"production_config_sha256": SHA_E,
|
||||
"test_evidence_sha256": SHA_A,
|
||||
"tests_passed": 10,
|
||||
"tests_failed": 0,
|
||||
"audited_skips": 1,
|
||||
"candidate_four_way_ref_closure": True,
|
||||
"integration_four_way_ref_closure": True,
|
||||
"candidate_reachable_from_integration": True,
|
||||
"worktree_clean": True,
|
||||
"gitops_writes_enabled": False,
|
||||
"task_plane_execution_enabled": False,
|
||||
"silent_release_execution_enabled": False,
|
||||
}
|
||||
data.update(overrides)
|
||||
return DormantAcceptanceInput(**data)
|
||||
|
||||
|
||||
def final_input(**overrides: object) -> FinalDormantAcceptanceInput:
|
||||
candidate = candidate_input()
|
||||
final_release = DormantReleaseGate().finalize(
|
||||
candidate.release_candidate(),
|
||||
target_oid=OID_D,
|
||||
target_tree_oid=OID_E,
|
||||
artifact_sha256=SHA_A,
|
||||
manifest_sha256=SHA_B,
|
||||
package_closure_sha256=SHA_C,
|
||||
evidence_closure_oid=OID_F,
|
||||
rollback_artifact_sha256=SHA_D,
|
||||
production_config_sha256=SHA_E,
|
||||
)
|
||||
data: dict[str, object] = {
|
||||
"candidate": candidate,
|
||||
"target_oid": OID_D,
|
||||
"target_tree_oid": OID_E,
|
||||
"artifact_sha256": SHA_A,
|
||||
"manifest_sha256": SHA_B,
|
||||
"package_closure_sha256": SHA_C,
|
||||
"evidence_closure_oid": OID_F,
|
||||
"rollback_artifact_sha256": SHA_D,
|
||||
"production_config_sha256": SHA_E,
|
||||
"approval_plan_id": "approval-v2r1c4-final",
|
||||
"approval_receipt_sha256": SHA_B,
|
||||
"approved_release_digest": final_release.digest(),
|
||||
"main_four_way_ref_closure": True,
|
||||
"target_is_main_head": True,
|
||||
"worktree_clean": True,
|
||||
"artifact_rebuilt_from_target": True,
|
||||
"rollback_verified": True,
|
||||
"runtime_boundary_verified": True,
|
||||
"owner_approval_verified": True,
|
||||
}
|
||||
data.update(overrides)
|
||||
return FinalDormantAcceptanceInput(**data)
|
||||
|
||||
|
||||
def test_candidate_acceptance_ready() -> None:
|
||||
result = DormantAcceptanceGate().evaluate(candidate_input())
|
||||
assert result.status is AcceptanceStatus.READY
|
||||
assert result.release_digest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides,blocking_code",
|
||||
[
|
||||
({"candidate_four_way_ref_closure": False}, "CANDIDATE_REF_CLOSURE_REQUIRED"),
|
||||
({"integration_four_way_ref_closure": False}, "INTEGRATION_REF_CLOSURE_REQUIRED"),
|
||||
({"candidate_reachable_from_integration": False}, "CANDIDATE_LINEAGE_REQUIRED"),
|
||||
({"worktree_clean": False}, "WORKTREE_NOT_CLEAN"),
|
||||
({"tests_failed": 1}, "TEST_FAILURES_PRESENT"),
|
||||
({"tests_passed": 0}, "TEST_EVIDENCE_REQUIRED"),
|
||||
({"task_plane_execution_enabled": True}, "DORMANT_FLAGS_REQUIRED"),
|
||||
({"native_openclaw_enabled": True}, "NATIVE_AGENT_ACTIVATION_FORBIDDEN"),
|
||||
({"production_side_effects": True}, "PRODUCTION_SIDE_EFFECT_FORBIDDEN"),
|
||||
],
|
||||
)
|
||||
def test_candidate_acceptance_fail_closed(
|
||||
overrides: dict[str, object], blocking_code: str
|
||||
) -> None:
|
||||
result = DormantAcceptanceGate().evaluate(candidate_input(**overrides))
|
||||
assert result.status is AcceptanceStatus.BLOCKED
|
||||
assert result.blocking_code == blocking_code
|
||||
|
||||
|
||||
def test_candidate_mapping_rejects_unknown_fields() -> None:
|
||||
payload = dict(candidate_input().payload())
|
||||
payload["unexpected"] = True
|
||||
with pytest.raises(ValueError, match="unknown acceptance fields"):
|
||||
DormantAcceptanceInput.from_mapping(payload)
|
||||
|
||||
|
||||
def test_final_acceptance_ready() -> None:
|
||||
result = FinalDormantAcceptanceGate().evaluate(final_input())
|
||||
assert result.status is AcceptanceStatus.READY
|
||||
assert result.release_digest == final_input().approved_release_digest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides,blocking_code",
|
||||
[
|
||||
({"main_four_way_ref_closure": False}, "MAIN_REF_CLOSURE_REQUIRED"),
|
||||
({"target_is_main_head": False}, "TARGET_NOT_MAIN_HEAD"),
|
||||
({"worktree_clean": False}, "WORKTREE_NOT_CLEAN"),
|
||||
({"artifact_rebuilt_from_target": False}, "ARTIFACT_REBUILD_REQUIRED"),
|
||||
({"rollback_verified": False}, "ROLLBACK_EVIDENCE_REQUIRED"),
|
||||
({"runtime_boundary_verified": False}, "RUNTIME_BOUNDARY_REQUIRED"),
|
||||
({"owner_approval_verified": False}, "OWNER_APPROVAL_REQUIRED"),
|
||||
({"approved_release_digest": "9" * 64}, "APPROVAL_SUBJECT_MISMATCH"),
|
||||
],
|
||||
)
|
||||
def test_final_acceptance_fail_closed(
|
||||
overrides: dict[str, object], blocking_code: str
|
||||
) -> None:
|
||||
result = FinalDormantAcceptanceGate().evaluate(final_input(**overrides))
|
||||
assert result.status is AcceptanceStatus.BLOCKED
|
||||
assert result.blocking_code == blocking_code
|
||||
|
||||
|
||||
def test_final_mapping_parses_nested_candidate() -> None:
|
||||
evidence = final_input()
|
||||
payload = dict(evidence.payload())
|
||||
parsed = FinalDormantAcceptanceInput.from_mapping(payload)
|
||||
assert parsed == evidence
|
||||
|
||||
|
||||
def test_final_rejects_integration_as_production_target() -> None:
|
||||
evidence = final_input()
|
||||
altered = replace(
|
||||
evidence,
|
||||
target_oid=evidence.candidate.integration_oid,
|
||||
approved_release_digest="9" * 64,
|
||||
)
|
||||
result = FinalDormantAcceptanceGate().evaluate(altered)
|
||||
assert result.status is AcceptanceStatus.BLOCKED
|
||||
assert result.blocking_code == "FINAL_TARGET_NOT_MAIN_MERGE"
|
||||
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from aios_autonomous_release_candidate.dormant import (
|
||||
DeploymentMode,
|
||||
DormantReleaseCandidate,
|
||||
DormantReleaseGate,
|
||||
ExecutionFlags,
|
||||
)
|
||||
from aios_autonomous_release_candidate.errors import ReleaseBlocked
|
||||
|
||||
|
||||
OID_A = "a" * 40
|
||||
OID_B = "b" * 40
|
||||
OID_C = "c" * 40
|
||||
SHA_A = "1" * 64
|
||||
SHA_B = "2" * 64
|
||||
SHA_C = "3" * 64
|
||||
SHA_D = "4" * 64
|
||||
SHA_E = "5" * 64
|
||||
|
||||
|
||||
def build_candidate(**overrides):
|
||||
values = {
|
||||
"version": "V2R1C4",
|
||||
"pull_request": 20,
|
||||
"integration_oid": OID_A,
|
||||
"integration_tree_oid": OID_B,
|
||||
"artifact_sha256": SHA_A,
|
||||
"manifest_sha256": SHA_B,
|
||||
"package_closure_sha256": SHA_C,
|
||||
"evidence_closure_oid": OID_C,
|
||||
"rollback_artifact_sha256": SHA_D,
|
||||
"production_config_sha256": SHA_E,
|
||||
"execution_flags": ExecutionFlags(False, False, False),
|
||||
}
|
||||
values.update(overrides)
|
||||
return DormantReleaseCandidate(**values)
|
||||
|
||||
|
||||
def test_dormant_candidate_is_accepted():
|
||||
candidate = build_candidate()
|
||||
DormantReleaseGate().verify_candidate(candidate)
|
||||
assert candidate.payload()["production_target_oid"] == "UNRESOLVED"
|
||||
assert len(candidate.digest()) == 64
|
||||
|
||||
|
||||
def test_dormant_candidate_rejects_enabled_execution_flag():
|
||||
candidate = build_candidate(execution_flags=ExecutionFlags(False, True, False))
|
||||
with pytest.raises(ReleaseBlocked) as error:
|
||||
DormantReleaseGate().verify_candidate(candidate)
|
||||
assert error.value.code == "DORMANT_FLAGS_REQUIRED"
|
||||
|
||||
|
||||
def test_dormant_candidate_rejects_production_transport():
|
||||
candidate = build_candidate(protected_dispatch_transport="task-plane")
|
||||
with pytest.raises(ReleaseBlocked) as error:
|
||||
DormantReleaseGate().verify_candidate(candidate)
|
||||
assert error.value.code == "DORMANT_TRANSPORT_REQUIRED"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"overrides,expected_code",
|
||||
[
|
||||
({"native_openclaw_enabled": True}, "NATIVE_AGENT_ACTIVATION_FORBIDDEN"),
|
||||
({"native_codex_enabled": True}, "NATIVE_AGENT_ACTIVATION_FORBIDDEN"),
|
||||
({"automatic_release_enabled": True}, "AUTOMATIC_RELEASE_FORBIDDEN"),
|
||||
({"production_side_effects": True}, "PRODUCTION_SIDE_EFFECT_FORBIDDEN"),
|
||||
({"activation_deferred": False}, "ACTIVATION_MUST_BE_DEFERRED"),
|
||||
({"deployment_mode": DeploymentMode.ACTIVATED_PRODUCTION}, "DORMANT_MODE_REQUIRED"),
|
||||
],
|
||||
)
|
||||
def test_dormant_candidate_rejects_activation(overrides, expected_code):
|
||||
candidate = build_candidate(**overrides)
|
||||
with pytest.raises(ReleaseBlocked) as error:
|
||||
DormantReleaseGate().verify_candidate(candidate)
|
||||
assert error.value.code == expected_code
|
||||
|
||||
|
||||
def test_finalize_binds_exact_main_merge_target():
|
||||
candidate = build_candidate()
|
||||
final = DormantReleaseGate().finalize(
|
||||
candidate,
|
||||
target_oid="d" * 40,
|
||||
target_tree_oid="e" * 40,
|
||||
artifact_sha256=SHA_A,
|
||||
manifest_sha256=SHA_B,
|
||||
package_closure_sha256=SHA_C,
|
||||
evidence_closure_oid="f" * 40,
|
||||
rollback_artifact_sha256=SHA_D,
|
||||
production_config_sha256=SHA_E,
|
||||
)
|
||||
assert final.target_oid == "d" * 40
|
||||
assert final.payload()["production_target_oid"] == "d" * 40
|
||||
assert len(final.digest()) == 64
|
||||
|
||||
|
||||
def test_finalize_rejects_integration_as_production_target():
|
||||
candidate = build_candidate()
|
||||
with pytest.raises(ReleaseBlocked) as error:
|
||||
DormantReleaseGate().finalize(
|
||||
candidate,
|
||||
target_oid=candidate.integration_oid,
|
||||
target_tree_oid="e" * 40,
|
||||
artifact_sha256=SHA_A,
|
||||
manifest_sha256=SHA_B,
|
||||
package_closure_sha256=SHA_C,
|
||||
evidence_closure_oid="f" * 40,
|
||||
rollback_artifact_sha256=SHA_D,
|
||||
production_config_sha256=SHA_E,
|
||||
)
|
||||
assert error.value.code == "FINAL_TARGET_NOT_MAIN_MERGE"
|
||||
Reference in New Issue
Block a user