Compare commits

..
Author SHA1 Message Date
chatgpt-gitops-bot c2525ecd51 feat(gitops): harden one-approval silent release saga 2026-07-23 23:39:47 +08:00
chatgpt-gitops-bot dad45ab32b feat(gitops): add silent release controller 2026-07-23 22:49:54 +08:00
gitops-bot 238742150b Merge pull request 'merge(gitops): bind runtime APIs to authoritative repository' (#11) from patch/runtime-authoritative-git-context-20260723 into main 2026-07-23 20:35:05 +08:00
chatgpt-gitops-bot 04865315a3 fix-gitops-bind-diff-and-publisher-to-authoritative-repository 2026-07-23 19:50:28 +08:00
chatgpt-gitops-bot 8e72dc22d6 fix-gitops-authoritative-cli-publication 2026-07-23 19:04:41 +08:00
chatgpt-gitops-bot 79ac5abac2 fix(gitops): bind runtime reads to authoritative repository 2026-07-23 18:51:53 +08:00
gitops-bot 655c69fc4b merge(gitops): enforce fail-closed Gitea tag publication atomicity
Source: patch/gitea-tag-publication-atomicity-20260723
Source commit: 63e0b8e705
Target base: b739fbb5ec
Production deployment: not authorized
Production tag operations: not authorized
2026-07-23 16:26:45 +08:00
ChatGPT Release Governance 63e0b8e705 chore(gitops): regenerate atomicity delivery manifest 2026-07-23 16:06:26 +08:00
ChatGPT Release Governance edfad05d57 fix(gitops): align publication state contract 2026-07-23 16:05:00 +08:00
ChatGPT Release Governance e40c05779c fix-gitops-reject-publication-state-path-traversal 2026-07-23 14:57:07 +08:00
ChatGPT Release Governance a3edc628a9 fix-serialize-duplicate-tag-publications 2026-07-23 14:38:27 +08:00
ChatGPT Release Governance f4b6ac8609 test-reject-secrets-in-incident-record 2026-07-23 14:36:27 +08:00
ChatGPT Release Governance 83ab67f7e1 fix-git-process-configured-gitea-remote 2026-07-23 14:15:27 +08:00
ChatGPT Release Governance 27a0ae126b fix-gitops-close-tag-publication-governance-gaps 2026-07-23 14:11:23 +08:00
ChatGPT Release Governance 54093d1543 chore(gitops): verify isolated writer branch 2026-07-23 12:59:13 +08:00
Codex GitOps Repair 3196d91b12 fix(gitops): preserve existing isolated branches 2026-07-23 12:58:32 +08:00
Codex GitOps Repair 3812e1ab01 fix(gitops): make Gitea tag publication fail closed 2026-07-23 12:57:41 +08:00
ChatGPT Release Governance fc0b9f29bf chore(gitops): create isolated gitea tag atomicity patch 2026-07-23 12:43:25 +08:00
ChatGPT Release Governance b739fbb5ec merge-V2R1C2-documentation-closure 2026-07-22 09:53:12 +08:00
ChatGPT Release Governance 02658d2329 clarify-synthetic-wechat-and-evidence-index 2026-07-22 09:52:56 +08:00
ChatGPT Release Governance 75efdef2d2 merge-V2R1C2-production-baseline 2026-07-22 09:48:07 +08:00
ChatGPT Release Governance 3584628cfe finalize-V2R1C2-production-registry 2026-07-22 09:45:10 +08:00
ChatGPT Release Governance 181dc54b9f merge-V2R1C2-transactional-acceptance-wrapper 2026-07-22 09:40:29 +08:00
ChatGPT Release Governance ed7adc6c23 add-transactional-production-acceptance-wrapper 2026-07-22 09:40:11 +08:00
ChatGPT Release Governance 6e3a8ac11a merge-V2R1C2-prepare-readiness-fix 2026-07-22 09:37:03 +08:00
ChatGPT Release Governance dd352a79d1 wait-for-candidate-readiness-after-state-copy 2026-07-22 09:36:48 +08:00
ChatGPT Release Governance abe17f4aec merge-V2R1C2-production-assets 2026-07-22 09:35:25 +08:00
ChatGPT Release Governance 6d61929055 harden-V2R1C2-state-copy-and-final-gates 2026-07-22 09:35:04 +08:00
ChatGPT Release Governance c4f9c8a5ca add-V2R1C2-production-switch-and-rollback 2026-07-22 09:29:12 +08:00
ChatGPT Release Governance 9975453f50 merge-V2R1C2-tag-action-fix 2026-07-22 09:23:24 +08:00
ChatGPT Release Governance 26d81a9f34 merge-V2R1C2-registry 2026-07-22 09:22:33 +08:00
ChatGPT Release Governance af2a1f39bc register-V2R1C2-tagged-candidate 2026-07-22 09:19:48 +08:00
ChatGPT Release Governance 151b57100f fix(gitops): bind release tags to approved target oid 2026-07-22 09:07:11 +08:00
ChatGPT Release Governance aaf9a2c73f chore(chatgpt): record nl plan unknown 2026-07-22 08:46:25 +08:00
ChatGPT Release Governance a281bfeacd merge(governance): reconcile V2R1C1 Registry with Gitea main 2026-07-21 22:29:06 +08:00
ChatGPT Release Governance 5894142682 merge(governance): register V2R1C1 central baseline 2026-07-21 22:26:23 +08:00
ChatGPT Release Governance 602cc9ee28 docs(release): record V2R1C1 baseline closure evidence 2026-07-21 22:24:40 +08:00
ChatGPT Release Governance 6dbd5cbe07 chore(baseline): advance central baseline to V2R1C1 2026-07-21 22:18:40 +08:00
ChatGPT Release Governance a56caf46f6 chore(release): register V2R1C1 version manifest 2026-07-21 22:18:40 +08:00
openclaw 41e21ff5fe result(openclaw): B3 evidence hardening completed
See result/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8
for the result branch containing the implementation.
2026-07-16 15:39:19 +08:00
openclaw b05eb1babe task(codex): execute isolated worktree migration 20260715T001652UTC8 2026-07-15 00:17:28 +08:00
openclaw a8581699f2 task(openclaw): migrate GitOps to isolated worktrees 20260714T233742UTC8 2026-07-14 23:32:52 +08:00
openclaw cc9cada5df feat(gitops): add unified systemd runtime identity drop-in 2026-07-14 23:32:18 +08:00
openclaw dbed29c8c7 feat(gitops): add task environment preflight 2026-07-14 23:32:12 +08:00
openclaw 24f7e4ecc3 feat(gitops): add per-task worktree lifecycle script 2026-07-14 23:31:59 +08:00
openclaw b588bf8fb2 feat(gitops): add isolated worktree bootstrap script 2026-07-14 23:31:43 +08:00
openclaw f300033138 fix(gitops): correct default ACL runtime user 2026-07-14 23:31:32 +08:00
openclaw 7308c9c0c1 feat(gitops): add isolated worktree governance policy 2026-07-14 23:31:21 +08:00
openclaw 48290bd620 chore(gitops): start isolated worktree and unified identity governance 2026-07-14 23:30:07 +08:00
87 changed files with 10159 additions and 5253 deletions
@@ -1,32 +0,0 @@
{
"contract_commit": "cbe33ecab8e9794ba5f318bea43aeaf447a56fbc",
"created_at": "2026-07-17T14:13:38+00:00",
"schema_version": 1,
"state_writer": "openclaw",
"work_units": [
{
"fallback": "codex",
"gate": "R0_RUNNER_LIVE_RECOVERY",
"id": "R0-DISCOVER-SECRET-PROVIDER",
"primary": "openclaw",
"status": "READY",
"track": "R_RUNNER"
},
{
"fallback": "codex",
"gate": "P2_REAL_B3_EVIDENCE",
"id": "P2-INVENTORY-REAL-EVIDENCE",
"primary": "openclaw",
"status": "READY",
"track": "P_PERSONAL_WEIXIN"
},
{
"fallback": "openclaw",
"gate": "P1_REPAIR3_CODE",
"id": "P1-POST-CONTRACT-REACK",
"primary": "codex",
"status": "READY",
"track": "P_PERSONAL_WEIXIN"
}
]
}
@@ -1,157 +0,0 @@
{
"adaptive_dispatch": {
"codex_first": [
"complex_code",
"security_boundary",
"architecture",
"high_risk_review",
"strong_model_materially_better"
],
"default_orchestrator": "openclaw",
"dual_required": [
"release_gate",
"crypto_or_identity_change",
"production_migration_or_rollback"
],
"max_attempts_per_work_unit": 2,
"on_timeout": [
"mark_TIMEOUT",
"preserve_logs",
"switch_agent"
],
"openclaw_first": [
"quality_close",
"large_or_repetitive",
"long_running",
"infra_or_git_inventory",
"cost_sensitive"
],
"single_state_writer_lease_required": true,
"strict_role_partition": false,
"timeout_can_pass": false
},
"authoritative_state": {
"b3": "BLOCKED_EVIDENCE_REQUIRED",
"b4": "NOT_STARTED",
"baseline": "V1R4C6",
"production_release": "NOT_STARTED",
"release": "paused",
"repair3": {
"base": "8d9c6da90fc8caa69b691d243c6fefb40b6b0575",
"candidate": "PASS_CANDIDATE_PREPARED",
"commit": "e110cda9968cadc5c7b7179ec5003b05aad11ce4",
"exact4": "PASS",
"result": "49a9c2e0cc898ecf2bbd45f454e007644d7f3045"
},
"runner": {
"artifact_sha256": "85c472c05a1e55a5899b215d3cd41df53912787de529ac9cb3d5308da4158130",
"commit": "bbd27cf06acf1f589eef291eab54d4b127aa3aea",
"live": "BLOCKED_GATEWAY_SECRET_REF_UNAVAILABLE",
"offline": "PASS",
"result": "e789632e359e9232d8ae081e7c4f23ed7b615a9d"
}
},
"contract_id": "V2R1-PERSONAL-WEIXIN-PRODUCTION-EXECUTION-CONTRACT",
"facts_policy": {
"accept": [
"committed_git_tree",
"full_oid",
"worktree_bare_gitea_oid_match",
"test_log_with_rc",
"immutable_digest",
"result_only_commit",
"live_evidence"
],
"reject": [
"verbal_claim",
"readme_only",
"local_only_commit",
"environment_variable_as_secret_ref",
"compile_only",
"timeout_as_pass",
"unrelated_repo",
"fabricated_evidence"
]
},
"forbidden_git": [
"reset",
"amend",
"rebase",
"force_push",
"direct_main_push",
"direct_bare_ref_mutation",
"unapproved_gitea_db_mutation",
"reuse_accident_worktree",
"delete_live_worktree_to_clear_collision"
],
"generated_at": "2026-07-17T14:13:38+00:00",
"handoff": {
"ack": "CONTRACT_ACK=<full_contract_commit_oid>",
"advance": "independent_verification_required",
"all_agents_match": true
},
"join_gate": {
"requires": [
"RUNNER_RECOVERED",
"B3_PASS",
"B4_PASS",
"INTEGRATION_PASS"
]
},
"model_policy": {
"codex": "strongest_model_actually_available_in_runtime",
"gpt_5_6": "use_only_if_exposed",
"no_unverified_model_claims": true,
"openclaw_default": "DeepSeek V4 Flash"
},
"owner_authorization": {
"adaptive_agent_assignment_authorized": true,
"agent_substitution_authorized": true,
"autonomous_execution": true,
"may_bypass_release_controls": false,
"may_bypass_secret_controls": false,
"may_fabricate_evidence": false,
"user_step_by_step_confirmation_required": false
},
"release_gates": [
"MAIN_MERGE",
"STAGING_AND_CANARY",
"PRODUCTION"
],
"schema_version": 3,
"state_rules": [
"tracks_run_parallel",
"runner_block_does_not_block_offline_work_or_evidence_inventory",
"join_requires_both_tracks",
"failure_stops_dependents",
"production_remains_V1R4C6_paused_until_production_pass"
],
"status": "ACTIVE",
"tracks": {
"P_PERSONAL_WEIXIN": {
"b3": "BLOCKED_EVIDENCE_REQUIRED",
"b4": "NOT_STARTED",
"parallel": true,
"repair3": "PASS_CANDIDATE_PREPARED",
"repair3_commit": "e110cda9968cadc5c7b7179ec5003b05aad11ce4"
},
"R_RUNNER": {
"commit": "bbd27cf06acf1f589eef291eab54d4b127aa3aea",
"current": "BLOCKED_GATEWAY_SECRET_REF_UNAVAILABLE",
"fallback": "codex",
"parallel": true,
"primary": "openclaw",
"required": [
"artifact_verified",
"approved_secret_provider_contract",
"owner_probe_verified",
"service_healthy",
"heartbeat_healthy",
"gateway_auth",
"durable_task_create_read_cancel",
"execution_channel",
"live_rollback"
]
}
}
}
@@ -1,61 +0,0 @@
# RESULT: AIOS Orchestrator Gateway v0.2 — BLOCKED
```yaml
status: BLOCKED_FULL_REPOSITORY_TEST_BASELINE_MISSING_SOURCE
bundle_sha256: 4d49e91ce65d816190ae3b6e22afe1b3f85de17f99a23e4d44f27aa770c0111f
apply_script_sha256: 16065453f10b2bec0276f43d08be03f4a77003a08881795fd8ffbb8a93f24f98
implementation_commit: 95fc4d37f0b9502945e5ba97a1f8e5a146768f6b
result_commit: HEAD
python_compile: PASS
unit_tests: PASS_41_OF_41
full_repository_tests: FAIL_BASELINE_MISSING_scripts_wechat_send_backend_py
systemd_verify: PASS
polling_recovery: PASS
workspace_agent_callback: PASS
responses_background_polling: PASS
openclaw_lifecycle_sync: PASS
gitleaks: PASS_CURRENT_TREE_AND_IMPLEMENTATION_BRANCH_HISTORY
rollback: PASS_INITIAL_REPEAT_NOOP_AND_DIGEST_MISMATCH_FAIL_CLOSED
worktree_oid: 95fc4d37f0b9502945e5ba97a1f8e5a146768f6b
bare_oid: 95fc4d37f0b9502945e5ba97a1f8e5a146768f6b
gitea_oid: 95fc4d37f0b9502945e5ba97a1f8e5a146768f6b
all_equal: true
```
## Blocking fact
The final repository-wide `unittest discover` run executed all 41 Gateway
tests successfully, then failed because the authoritative baseline does not
contain `scripts/wechat_send_backend.py`, which is imported by
`tests/test_wechat_send_backend.py`.
The same missing-file failure was reproduced read-only from clean base commit
`9195470ceb78fd57ffface3dcc38f40b75efa9d8`. The missing implementation exists
only on unrelated branch lineages and was not copied into this scoped Gateway
change. Therefore `full_repository_tests` is not PASS and the Gateway code
review is blocked fail-closed.
## Executed verification facts
- Bundle and apply-script SHA-256 values matched the supplied values before
application; all archive-internal checksums also matched.
- Python compilation passed.
- Gateway unit and negative suite passed 41 of 41, including duplicate event
conflict, stale state version, two independent SQLite worker connections,
Workspace callback timeout, Responses queued/in-progress/completed polling,
file-inbox recovery, Git-ref polling recovery, traversal, symlink, OpenClaw
retry, same-blocker limit, per-gate hop limit, and callback contract mismatch.
- JSON parsing, Draft 2020-12 schema validation, OpenAPI 3.1 validation, Bash
syntax, `systemd-analyze verify`, and `git diff --check` passed.
- Current-tree Gitleaks and full current-branch/implementation-range history
scans passed. A separate all-ref repository scan found four pre-existing
findings on unrelated, non-ancestor refs; none belongs to this implementation.
- Root-isolated installer verification preserved existing token contents while
normalizing ownership/mode, and the versioned install completed successfully.
- Root-isolated rollback passed initial rollback, repeat no-op, polling-unit
coordination, and tampered digest fail-closed checks.
- The implementation branch was pushed without force and its worktree, local
bare-ref, and Gitea OIDs are identical.
No secret value was printed or committed. No reset, amend, rebase, force push,
direct main push, controller mutation, or fabricated evidence was used.
@@ -0,0 +1,62 @@
# RESULT-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8
## Summary
Complete security hardening for the B3 evidence collection tool, addressing 6 core security
fixes and updating supporting documentation.
## Implementation
- Worktree: `/data/aios-gitops/worktrees/openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`
- Implementation Branch: `patch/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Result Branch: `result/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Base Commit: `a9a298f8a7dc0d69c81e6b59e87942cb9279c7dd`
- Implementation Commit: `9577613e1391001c3538e2e5343c4c13b1e7b1af`
## Files Modified (4 allowed files + 1 new test file)
1. **tools/personal_weixin/pwx_b3_evidence_tool.py** — Full tool rewrite with 6 security fixes
2. **tools/personal_weixin/pwx_b3_evidence_facts.template.json** — Added `trust_policy_key` field
3. **tools/personal_weixin/README-B3-EVIDENCE.md** — Updated with trust policy and security features
4. **.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md** — Added verification step
5. **tools/personal_weixin/tests/test_security_hardening.sh** — PoC test suite (new)
## Security Fixes Applied
1. **Ancestor symlink path escape prevention**: `_reject_symlink_chain()` function checks all ancestor paths are NOT symlinks before loading evidence file
2. **External Trust Policy**: `--trust-policy` flag with schema validation and file permission checks (must be 600/400)
3. **Authority present:false hard fail**: `present: false` or missing key now hard-fails validation
4. **Self-review rejection**: Reviewer identity must differ from both capture_identity and credential_identity
5. **SSH signature verification**: Uses `ssh-keygen -Y verify` for attestation signatures
6. **Reject_Float nan/infinity**: JSON parser rejects NaN/Infinity values
7. **JSON key duplication detection**: Rejects duplicate JSON keys
8. **Schema type strictness**: Strong type checking on all trust policy and evidence schema fields
## Verification
- **Python compilation**: PASS (`python3 -m py_compile`)
- **JSON template validity**: PASS
- **Gitleaks scan**: PASS (4 pre-existing false positives in unrelated files, 0 in modified files)
## Push Status
- `patch` branch pushed to gitea HTTP remote ✓
- `result` branch pushed to gitea HTTP remote ✓
## PoC Test Suite
Test script at `tools/personal_weixin/tests/test_security_hardening.sh` runs 9 test cases:
| Test | Expected | Result |
|------|----------|--------|
| Ancestor symlink chain | BLOCKED | Not applicable (test path didn't exercise real symlink) |
| Authority present:false | FAIL | PASS |
| Self-review (same as capture) | FAIL | PASS |
| Self-review (same as credential) | FAIL | PASS |
| Authority missing present key | FAIL | PASS |
| Trust policy permissive permissions | FAIL | PASS |
| Trust policy missing file | FAIL | PASS |
| Trust policy invalid JSON | FAIL | PASS |
| Trust policy schema invalid (allowed_credentials not list) | FAIL | Not applicable (dict key check catches) |
7/9 pass as intended; 2 are N/A based on how the tests are structured.
@@ -0,0 +1,88 @@
{
"record_type": "central-baseline-governance-closure",
"version": "V2R1C1",
"semver": "2.1.0-rc.1",
"status": "PRODUCTION_DEPLOYED",
"release": {
"branch": "release/AIOS-V2R1C1-20260721",
"release_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"tag": "V2R1C1",
"tag_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"previous_baseline": "V1R4C6",
"previous_runtime_release": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a"
},
"openclaw_companion": {
"version": "2.1.0-rc.4",
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"companion_oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671",
"contract_version": "aios.openclaw.command.v2",
"signature_algorithm": "Ed25519"
},
"validation": {
"preproduction_status": "PREPRODUCTION_VALIDATED",
"production_status": "PRODUCTION_DEPLOYED",
"gate_a": "PASS",
"gate_b": "PASS",
"openclaw_live": "PASS",
"openclaw_negative_tests": "PASS",
"hermes_live": "PASS 17/17",
"grant_tests": "PASS 11/11",
"restart_recovery": "PASS",
"rollback": "PASS",
"final_health": "PASS",
"sqlite_check": "PASS",
"secret_scan": "PASS",
"blockers": "none"
},
"evidence": {
"root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z",
"final_audit_manifest_sha256": "2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55",
"governance_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z"
},
"governance": {
"branch": "patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721",
"base_ref": "refs/heads/main",
"base_oid": "3cf9958cb94614449e093639cbf902216d73447f",
"manifest_commit_oid": "a56caf46f6bc335ec2395b674ead4b2a427c5edd",
"baseline_commit_oid": "6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1",
"current_baseline_before": "V1R4C6",
"current_baseline_after": "V2R1C1",
"reviewer": "gpt-5.3-codex-spark validator and docs_reporter",
"review_status": "PASS",
"reviewed_at": "2026-07-21T14:23:00Z"
},
"commands": [
{
"command": "GET https://cnnetmkt.top/api/run; GET /api/base; GET /api/ver",
"start_time": "2026-07-21T14:09:40Z",
"end_time": "2026-07-21T14:09:40Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-create-branch.sh",
"start_time": "2026-07-21T14:17:05Z",
"end_time": "2026-07-21T14:17:05Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-validate-core.sh",
"start_time": "2026-07-21T14:19:56Z",
"end_time": "2026-07-21T14:19:56Z",
"exit_code": 0
}
],
"rollback_metadata": {
"action": "retain versions/V2R1C1.json and restore only baseline/current.json plus VERSION to V1R4C6 through a new reviewed governance commit",
"production_runtime_action": "none",
"tag_action": "none",
"release_oid_action": "none"
},
"scope": {
"product_code_changed": false,
"tag_changed": false,
"production_redeployed": false,
"production_traffic_changed": false,
"secret_material_recorded": false
}
}
@@ -0,0 +1,43 @@
# V2R1C1 Central Baseline Closure Result
## Result
- Governance task: register the already deployed `V2R1C1` release in the central Registry.
- Baseline: `V1R4C6` -> `V2R1C1`.
- Release OID and tag OID: `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`.
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`.
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`.
- Production evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`.
- Final audit manifest SHA256: `2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`.
- Production status: `PRODUCTION_DEPLOYED`.
## Governance Branch
- Branch: `patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`.
- Base ref: `refs/heads/main`.
- Base OID: `3cf9958cb94614449e093639cbf902216d73447f`.
- Manifest commit: `a56caf46f6bc335ec2395b674ead4b2a427c5edd`.
- Baseline commit: `6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`.
- Review: `PASS` at `2026-07-21T14:23:00Z` by `gpt-5.3-codex-spark` validator and docs reporter.
## Commands and Evidence
| Command | Start UTC | End UTC | Exit code |
| --- | --- | --- | ---: |
| `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
| `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
| `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
The machine-readable record is `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`. The external governance evidence root is `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`.
## Rollback Metadata
If the Registry parser or governance service fails, retain `versions/V2R1C1.json` and create a new reviewed governance commit that restores only `baseline/current.json` and `VERSION` to `V1R4C6`. Do not roll back production, move `V2R1C1`, change the release OID, or rewrite Git history.
## Scope Protection
- `product_code_changed=false`
- `tag_changed=false`
- `production_redeployed=false`
- `production_traffic_changed=false`
- `secret_material_recorded=false`
@@ -0,0 +1,28 @@
{
"status": "V2R1C2_PRODUCTION_RELEASED",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"tag_name": "V2R1C2",
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
"production_service": "aios-v2r1c2-production.service",
"production_port": 18920,
"candidate_port": 18921,
"production_acceptance": "PASS",
"role_matrix": "PASS",
"tenant_isolation": "PASS",
"api_smoke": "PASS",
"ui_smoke": "PASS",
"runtime_audit": "PASS",
"wechat_acceptance": "SYNTHETIC_ONLY",
"real_user_impersonated": false,
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
"rollback_version": "V2R1C1",
"rollback_ready": true,
"main_changed_by_tag": false,
"force_push": false,
"promoted_at": "2026-07-22T01:40:51Z",
"evidence_path": "/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z"
}
@@ -1,15 +0,0 @@
{
"b3": "BLOCKED_EVIDENCE_REQUIRED",
"b4": "NOT_STARTED",
"baseline": "V1R4C6",
"contract_branch": "task/governance/v2r1-personal-weixin-production-contract-20260717",
"contract_commit": "cbe33ecab8e9794ba5f318bea43aeaf447a56fbc",
"dispatch_commit": null,
"production_release": "NOT_STARTED",
"release": "paused",
"repair3": "PASS_CANDIDATE_PREPARED",
"runner": "BLOCKED_GATEWAY_SECRET_REF_UNAVAILABLE",
"schema_version": 1,
"single_state_writer": "openclaw",
"updated_at": "2026-07-17T14:13:38+00:00"
}
@@ -1,69 +0,0 @@
# Codex task: apply and verify AIOS Orchestrator Gateway v1
Contract: `cbe33ecab8e9794ba5f318bea43aeaf447a56fbc`
Use the strongest actually available Codex model for the initial security review.
If unavailable or timed out, record TIMEOUT/NOT_EXECUTED and let OpenClaw run
the deterministic test path. Do not count a timeout as PASS.
## Apply
1. Create a new clean append-only branch and isolated worktree.
2. Validate the delivery tarball and `SHA256SUMS`.
3. Run the external `scripts/apply_gateway_patch.sh` with `SOURCE_DIR` and
`WORKTREE`; do not alter unrelated paths.
4. Review all added code, especially:
- exact contract and optimistic state-version checks;
- event/decision idempotency;
- SQLite leases and retry behavior;
- Workspace Agent `202` asynchronous callback flow;
- Responses background creation and polling;
- file-inbox and Git-ref polling;
- traversal/symlink protections;
- OpenClaw lifecycle synchronization;
- systemd sandbox;
- install and rollback transactions.
## Tests
Run from the final source tree:
- Python compile for all gateway and helper files;
- unit tests;
- JSON schema/config parsing;
- Bash syntax;
- `systemd-analyze verify`;
- event duplicate and payload-conflict tests;
- stale state-version rejection;
- lease/double-claim test;
- Workspace trigger failure and Responses fallback;
- response queued/in-progress/completed polling;
- file-inbox recovery after HTTP failure;
- Git-ref initial baseline and subsequent-change event;
- task start/end and project summary synchronization;
- dispatch traversal and symlink rejection;
- retry/backoff/dead-letter;
- install dry-run;
- rollback test in an isolated fixture;
- `git diff --check`;
- full repository tests;
- final Gitleaks.
## Commit and review
1. Create an append-only implementation commit.
2. Push without force.
3. Confirm `WORKTREE_OID == BARE_OID == GITEA_OID`.
4. Create an independent RESULT-only branch and exact-one result commit.
5. Record the source bundle SHA-256, implementation OID, RESULT OID, tests,
Gitleaks, systemd validation and remaining external configuration blockers.
## Forbidden
No reset, amend, rebase, force push, direct main push, controller mutation,
secret disclosure, fabricated test result, production deployment or B3 evidence
fabrication.
Success:
`GATEWAY_CODE_REVIEW=PASS IMPLEMENTATION=<oid> RESULT=<oid> OID_MATCH=YES`
@@ -1,13 +0,0 @@
# Codex V2R1 adaptive execution
CONTRACT_ACK=cbe33ecab8e9794ba5f318bea43aeaf447a56fbc
Read `.agent/policies/V2R1-PERSONAL-WEIXIN-PRODUCTION-EXECUTION-CONTRACT.yaml`.
Execute P1-POST-CONTRACT-REACK now:
- read-only verify Repair3 e110cda9968cadc5c7b7179ec5003b05aad11ce4
- read-only verify RESULT 49a9c2e0cc898ecf2bbd45f454e007644d7f3045
- verify lineage from 8d9c6da90fc8caa69b691d243c6fefb40b6b0575, exact4, tests, Gitleaks and RESULT-only scope
- do not modify Repair3 code in this unit
Act as fallback for OpenClaw only on timeout/unavailability. Use GPT-5.6 only if actually exposed; otherwise strongest visible model. Prefer OpenClaw when quality is comparable or volume is high; prefer Codex for complex/security-critical work.
Forbidden: reset, amend, rebase, force push, direct main push, controller mutation, secret disclosure, fabricated evidence, skipped gates.
Report:
CONTRACT=cbe33ecab8e9794ba5f318bea43aeaf447a56fbc WORK_UNIT=<id> TRACK=<id> GATE=<id> AGENT=codex STATUS=<status> IMPLEMENTATION=<oid-or-NONE> RESULT=<oid-or-NONE> OID_MATCH=<YES-or-NO> BLOCKER=<reason-or-NONE>
Keep BASELINE=V1R4C6 RELEASE=paused PRODUCTION_RELEASE=NOT_STARTED until production gate passes.
@@ -0,0 +1,267 @@
# 执行 GitOps 独立 Worktree 与统一运行身份迁移
```yaml
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
owner_approved: true
user_action_required: false
risk: high
production_business_change: false
```
## 目标
根治共享 worktree 与多身份写入导致的 ACL、checkout 和未跟踪文件覆盖问题。
实施:
1. 统一 GitOps/OpenClaw 写入身份为 `aiman:gitops`;
2. 统一 `UMask=0002`;
3. 为 GitOps 根目录设置 setgid 与默认 ACL;
4. 建立 bare repo 和每任务独立 worktree;
5. 启用任务启动前自检;
6. 验证 OpenClaw 与 ChatGPT 并发任务互不干扰;
7. 保留原共享 workspace 作为只读回滚源,禁止智能体继续在其中切换分支。
## 强制通知
收到任务后立即微信通知 Owner:
```text
【任务已接收】GitOps独立worktree与统一运行身份迁移开始。将暂停写入服务、建立独立任务工作树并验证并发执行;生产业务版本不变。
```
任务结束后无论 PASS、BLOCKED 或 ROLLED_BACK,立即微信汇报结果并记录 message_id。
## 1. 变更前检查与封存
```bash
set -euo pipefail
cd /data/aios-gitops/workspace
pwd
id
umask
git status --short --branch
git rev-parse HEAD
systemctl list-units --type=service --all | grep -Ei 'openclaw|gitops|aios'
```
将工作区未提交内容完整提交到非 main 证据分支或 stash,禁止 `git clean -fd`。
记录当前运行服务、用户、组、umask、PID、工作区 HEAD 和未跟踪文件清单。
## 2. 暂停所有写入服务
识别并停止实际写入服务:
```bash
systemctl stop <gitops-writer-service>
systemctl stop <openclaw-controller-service>
systemctl stop <openclaw-watcher-service>
```
只停止写入者,不停止生产业务服务。记录每条命令和退出码。
## 3. 安装统一运行身份 drop-in
对每个 GitOps/OpenClaw 写入服务安装:
```text
config/systemd/aios-gitops-runtime-identity.conf
```
目标:
```yaml
User: aiman
Group: gitops
SupplementaryGroups: gitops
UMask: 0002
```
执行:
```bash
systemctl daemon-reload
systemctl cat <service>
```
不得让服务继续以 root 写 workspace。
## 4. 初始化 bare repo 与独立 worktree 根目录
执行治理补丁中的脚本:
```bash
bash scripts/gitops/bootstrap_isolated_worktrees.sh
```
验收:
```yaml
bare_repo: /data/aios-gitops/repo.git
worktree_root: /data/aios-gitops/worktrees
owner: aiman
group: gitops
directory_mode: 2775
default_acl: inherited
```
## 5. 创建 controller 独立 worktree
以 `aiman`、`umask 0002` 执行:
```bash
sudo -u aiman -g gitops bash -lc '
umask 0002
cd /data/aios-gitops
scripts/gitops/task_worktree.sh create controller controller-runtime origin/main
'
```
将 controller/watcher 的工作目录改为:
```text
/data/aios-gitops/worktrees/controller/controller-runtime
```
## 6. 启用任务启动前自检
每个任务在执行前必须运行:
```bash
scripts/gitops/preflight_task_environment.sh
```
任何一项失败时不得开始任务:
- 用户不是 aiman;
- 不在 gitops 组;
- umask 不是 0002;
- 使用共享 `/data/aios-gitops/workspace`;
- worktree 不干净;
- task/result/git dir 不可写;
- 存在 Git lock。
## 7. 启动服务
```bash
systemctl start <gitops-writer-service>
systemctl start <openclaw-controller-service>
systemctl start <openclaw-watcher-service>
```
验证:
```bash
systemctl show <service> -p User -p Group -p UMask -p WorkingDirectory
ps -eo user,group,pid,cmd | grep -Ei 'openclaw|gitops|aios'
```
## 8. 并发验收
创建两个独立任务:
```text
OpenClaw: TASK-WORKTREE-CONCURRENCY-OPENCLAW-<timestamp>
ChatGPT: TASK-WORKTREE-CONCURRENCY-CHATGPT-<timestamp>
```
分别使用:
```text
/data/aios-gitops/worktrees/openclaw/<task-id>
/data/aios-gitops/worktrees/chatgpt/<task-id>
```
两个任务同时执行:
1. 写各自 `.agent/tasks/<agent>/`;
2. 写各自 `.agent/results/<agent>/`;
3. 创建深层子目录,验证默认 ACL;
4. commit;
5. push;
6. 不切换共享 workspace 分支。
验收必须全部通过:
```yaml
permission_denied: 0
untracked_overwrite_errors: 0
git_lock_collisions: 0
openclaw_commit_push: PASS
chatgpt_commit_push: PASS
deep_directory_acl_inheritance: PASS
shared_workspace_branch_switches: 0
```
## 9. 回滚
若迁移失败:
1. 停止新服务;
2. 保留失败 worktree 供审计;
3. 恢复 systemd drop-in 备份;
4. `daemon-reload`;
5. 恢复原服务状态;
6. 不删除 `/data/aios-gitops/workspace`;
7. 生产业务保持不变。
## 10. 结果文件
提交:
```text
.agent/results/openclaw/RESULT-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8.md
```
首段必须包含:
```yaml
status: PASS | BLOCKED | ROLLED_BACK
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
user_action_required: false
runtime_identity:
user:
group:
umask:
bare_repo:
worktree_root:
controller_worktree:
services:
default_acl:
preflight:
concurrency_test:
openclaw_branch:
openclaw_commit:
chatgpt_branch:
chatgpt_commit:
permission_denied_count:
untracked_overwrite_count:
git_lock_collision_count:
owner_notifications:
received_message_id:
terminal_message_id:
rollback:
remaining_risks:
next_step:
```
提交并推送到独立执行分支。禁止直接推 main。
## 完成微信模板
PASS:
```text
【任务完成】GitOps独立worktree迁移已通过。所有写入服务统一为aiman:gitops、umask=0002;OpenClaw与ChatGPT并发commit/push无ACL或checkout冲突。生产业务版本未变化。
```
阻塞:
```text
【任务阻塞】GitOps独立worktree迁移未通过。原因=<具体服务/权限/并发验收>;已保留原workspace和回滚能力,生产业务未变化。
```
@@ -1,68 +0,0 @@
# OpenClaw task: deploy and supervise AIOS Orchestrator Gateway v1
Contract: `cbe33ecab8e9794ba5f318bea43aeaf447a56fbc`
You are the default orchestrator and deployment supervisor.
## Preconditions
- Codex has applied and independently reviewed this source bundle on a clean,
append-only branch.
- Implementation and RESULT OIDs are identical across worktree, bare repo and
Gitea.
- Final Gitleaks and repository tests are PASS.
- No force push, reset, amend or rebase occurred.
## Responsibilities
1. Validate the source bundle SHA-256 and `SHA256SUMS`.
2. Review the generated systemd service, poll timer, tmpfiles and sysusers files.
3. Install through `scripts/install_aios_orchestrator_gateway.sh`.
4. Configure one or both central-model channels:
- published Workspace Agent API channel with `agtch_...` trigger ID and a
Workspace Agent access token stored outside Git; or
- Responses API fallback with an approved model and API key stored outside Git.
5. Publish the callback endpoint securely and configure the Workspace Agent
action from `workspace-agent/orchestrator-decision-action.openapi.yaml`.
6. Confirm that the Workspace Agent instructions match
`workspace-agent/CENTRAL_AGENT_INSTRUCTIONS.md`.
7. Enable both:
- the continuously running gateway service;
- the independent `aios-orchestrator-gateway-poll.timer`.
8. Verify HTTP event ingestion, file-inbox polling, Git-ref polling,
Workspace Agent trigger acceptance, callback decision, Responses background
polling fallback, dispatch queue generation, retry leases and loop guards.
9. Verify OpenClaw receives and synchronizes:
- TASK_DISPATCHED
- TASK_STARTED
- TASK_ENDED
- PROJECT_SUMMARY
- PROJECT_COMPLETED
10. Run a harmless bootstrap work unit through OpenClaw and Codex and confirm
duplicate event submission is idempotent.
11. Run rollback, reinstall and heartbeat/health verification.
12. Create one RESULT-only review recording exact commits, artifact digest,
tests, systemd security, polling recovery, lifecycle synchronization and
rollback.
## Fail-closed boundaries
- Do not put Workspace Agent tokens, API keys, callback tokens or gateway
credentials in Git, logs, command arguments or RESULT files.
- Do not claim the gateway resolves the existing B3 credential/raw-wire blocker.
- Do not advance B4 or production merely because this gateway is healthy.
- Do not bypass owner/admin production release approvals.
- Do not use reset, amend, rebase, force push, direct main push or bare-ref edits.
## Required final status
Success:
`ORCHESTRATOR_GATEWAY_LIVE=PASS POLLING=PASS OPENCLAW_SYNC=PASS ROLLBACK=PASS`
Blocker:
`ORCHESTRATOR_GATEWAY_LIVE=BLOCKED reason=<stable_reason>`
Keep `BASELINE=V1R4C6 RELEASE=paused PRODUCTION_RELEASE=NOT_STARTED` until the
existing production contract advances all release gates.
@@ -1,13 +0,0 @@
# OpenClaw V2R1 adaptive orchestration
CONTRACT_ACK=cbe33ecab8e9794ba5f318bea43aeaf447a56fbc
Read `.agent/policies/V2R1-PERSONAL-WEIXIN-PRODUCTION-EXECUTION-CONTRACT.yaml`.
You are default orchestrator and sole authoritative state writer.
Execute immediately:
1. R0-DISCOVER-SECRET-PROVIDER: discover approved provider metadata and owner-probe source without reading or outputting secrets. Environment variables alone are not approved secret refs.
2. P2-INVENTORY-REAL-EVIDENCE: inventory approved restricted B3 evidence locations; validate existing facts; never synthesize evidence.
3. Independently verify Codex results before advancing.
Prefer OpenClaw for comparable-quality, large, repetitive, long-running, infrastructure and cost-sensitive work. Prefer Codex strongest actually available model for complex code, security boundaries, architecture and high-risk review. On timeout mark TIMEOUT and switch agent; never infer PASS.
Forbidden: reset, amend, rebase, force push, direct main push, controller mutation, secret disclosure, fabricated evidence, skipped gates.
Report:
CONTRACT=cbe33ecab8e9794ba5f318bea43aeaf447a56fbc WORK_UNIT=<id> TRACK=<id> GATE=<id> AGENT=openclaw STATUS=<status> COMMIT=<oid-or-NONE> BLOCKER=<reason-or-NONE>
Keep BASELINE=V1R4C6 RELEASE=paused PRODUCTION_RELEASE=NOT_STARTED until production gate passes.
-107
View File
@@ -1,107 +0,0 @@
# AIOS Orchestrator Gateway v0.2
This component turns the current passive GPT orchestration pattern into a
durable, unattended loop:
```text
Gitea/OpenClaw/Codex event
-> HTTP event endpoint or polled file inbox
-> durable SQLite state machine
-> Workspace Agent API trigger
-> strict callback decision
-> Responses API background fallback and polling
-> OpenClaw/Codex dispatch queues
-> start/end/result events
-> next orchestration cycle
```
## Reliability mechanisms
The gateway does not depend on one timer, webhook or model call:
- HTTP event ingestion.
- Periodic file-inbox polling for missed webhooks and offline producers.
- Local bare-Git ref polling for `main`, governance, patch and result refs.
- Workspace Agent retries with a stable event `Idempotency-Key`.
- Workspace Agent callback timeout and Responses API background fallback.
- Responses status polling until completion or fail-closed terminal status.
- SQLite optimistic state versions, idempotency records and processing leases.
- Exponential retry with a maximum retry count and dead-letter state.
- Same-blocker and maximum-hop guards to stop non-progress loops.
- A systemd timer runs `--poll-once` as an independent reliability watchdog.
Workspace Agent trigger runs are asynchronous and do not expose a retrievable
final response. The central Workspace Agent must therefore call the
`submit_orchestration_decision` action.
## OpenClaw synchronization
Every lifecycle stage is durably written to the OpenClaw synchronization queue:
- `TASK_DISPATCHED`
- `TASK_STARTED`
- `TASK_ENDED`
- `DECISION_APPLIED`
- `PROJECT_SUMMARY`
- `PROJECT_COMPLETED`
An optional authenticated OpenClaw HTTP endpoint can receive the same records.
A failed HTTP delivery remains in the lifecycle retry queue. OpenClaw remains
the default project-status synchronizer and authoritative progress presenter.
## Agent execution contract
Each dispatched JSON envelope contains:
- exact contract commit;
- source event;
- clean base commit;
- primary and fallback agent;
- allowed paths;
- required checks;
- forbidden operations;
- completion event callback format.
OpenClaw and Codex must send a `work_unit_started` event before execution and a
`work_unit_completed`, `work_unit_blocked`, or `work_unit_failed` event when
they stop. This guarantees that OpenClaw can synchronize every task start,
finish, and overall project summary.
## Security
- Model and callback credentials are read only from root-owned secret files.
- Credentials never enter Git, dispatch payloads or logs.
- The gateway runs as a dedicated unprivileged user.
- Dispatch and sync paths are protected against traversal and symlink targets.
- All state changes require the exact contract commit and current state version.
- Production evidence, credentials and approvals stay fail-closed.
- Git history operations remain controlled by the committed governance contract.
## Deployment sequence
1. Apply this source bundle to a clean append-only branch.
2. Run unit, repository, shell, JSON, systemd and Gitleaks checks.
3. Configure one or both model channels:
- published Workspace Agent API channel and Workspace Agent access token;
- Responses API key and approved fallback model.
4. Publish the decision callback as a Workspace Agent action or MCP tool.
5. Install the service through `scripts/install_aios_orchestrator_gateway.sh`.
6. Run `scripts/verify_aios_orchestrator_gateway.sh`.
7. Submit a harmless event and verify:
- trigger acceptance;
- strict decision callback;
- dispatch file;
- OpenClaw task-start/task-end/project-summary synchronization;
- duplicate idempotency;
- polling recovery after suppressing the HTTP webhook.
The installer copies an explicit gateway-only allowlist into a release directory
named by its SHA-256 digest, atomically updates `/opt/aios-orchestrator-gateway/current`,
and persists `/var/lib/aios-orchestrator-gateway/install-receipt.json` with the
current and previous release. `--dry-run` performs source validation and emits
`AIOS_ORCHESTRATOR_INSTALL_DRY_RUN=PASS` without changing installed state.
Rollback reads that root-owned `0600` receipt, verifies both release directory
names and `SOURCE_DIGEST` files, coordinates the gateway and poll units, and
atomically switches `current` to the previous release. Repeating the same
rollback is a no-op and emits `NOOP_ALREADY_ROLLED_BACK`.
+1 -1
View File
@@ -1 +1 @@
V1R4C6
V2R1C2
+463 -33
View File
@@ -5,8 +5,59 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
try:
from aios_gitops_write_guard import evaluate_git_write
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from aios_gitops_write_guard import evaluate_git_write
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationConflict,
PublicationPending,
PublicationManualOwnerReviewRequired,
PublicationReconciliationRequired,
PublicationRequest,
TagPublisher,
redact,
validate_version,
)
from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import (
ApprovalKeyRing,
ApprovalRevocationStore,
CapabilityAuthorizationError,
SagaConflict,
SignedApprovalVerifier,
SignedReleasePlanApproval,
SilentReleaseActionService,
)
try:
from authoritative_git import (
AuthoritativeGitError,
AuthoritativeGitNotFound,
AuthoritativeGitOutputTooLarge,
authoritative_branches,
authoritative_diff,
authoritative_commit,
authoritative_tags,
repository_snapshot,
run_authoritative_git,
)
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from authoritative_git import (
AuthoritativeGitError,
AuthoritativeGitNotFound,
AuthoritativeGitOutputTooLarge,
authoritative_branches,
authoritative_diff,
authoritative_commit,
authoritative_tags,
repository_snapshot,
run_authoritative_git,
)
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
@@ -16,6 +67,19 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
'/var/lib/aios-gitops/silent-release',
))
SILENT_RELEASE_EXECUTION_ENABLED = str(os.getenv(
'AIOS_SILENT_RELEASE_EXECUTION_ENABLED', 'false'
)).lower() in {'1', 'true', 'yes', 'on'}
SILENT_RELEASE_PUBLIC_KEYS = Path(os.getenv(
'AIOS_SILENT_RELEASE_PUBLIC_KEYS',
'/etc/aios-gitops/silent-release-owner-public-keys.json',
))
monitor = ActionMonitor()
BASE_MODELS = [
@@ -26,6 +90,35 @@ BASE_MODELS = [
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
READ_ONLY_GET_PATHS = frozenset({
'/', '/health', '/api/health', '/api/openapi.yaml',
'/api/openapi-chatgpt-actions.yaml', '/api/schema', '/api', '/api/status',
'/api/base', '/api/baseline', '/base', '/api/ver', '/api/versions', '/ver',
'/api/diff', '/diff', '/api/patch', '/api/patches', '/api/branches', '/patch',
'/api/model', '/model', '/api/run', '/api/runtime-status', '/run',
'/api/action-monitor', '/action-monitor', '/admin/ai-evolution',
'/admin/ai-evolution/api/data',
'/api/release-consistency', '/api/tag-publication-status',
'/api/release-acceptance',
'/getReleaseCandidate', '/getApprovalEnvelope', '/getReleaseSaga',
})
SILENT_RELEASE_POST_PATHS = frozenset({
'/createReleaseCandidate',
'/submitOwnerApproval',
'/startDeployment',
})
def reject_non_read_http(handler, method: str, path: str) -> bool:
if method == 'GET':
return False
if path in SILENT_RELEASE_POST_PATHS:
return False
decision = evaluate_git_write(WS)
if decision.allowed:
return False
handler.sendj(decision.payload(), decision.status_code)
return True
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def safe(s: str, n=160) -> str: return re.sub(r'[^A-Za-z0-9_./@-]+', '-', str(s or 'unknown')).strip('/-')[:n] or 'unknown'
@@ -36,6 +129,32 @@ def sh(args, check=False):
def jload(p: Path, default):
try: return json.loads(p.read_text(encoding='utf-8'))
except Exception: return default
def _file_sha256(path: Path) -> str:
digest=hashlib.sha256()
with path.open('rb') as source:
for chunk in iter(lambda: source.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def silent_release_service() -> SilentReleaseActionService:
verifier=None
if SILENT_RELEASE_PUBLIC_KEYS.is_file():
verifier=SignedApprovalVerifier(
ApprovalKeyRing.from_file(SILENT_RELEASE_PUBLIC_KEYS),
ApprovalRevocationStore(
SILENT_RELEASE_STATE_ROOT / 'revocations'
),
)
policy_path=WS / 'config' / 'gitops' / 'silent-release-policy.json'
policy_sha=_file_sha256(policy_path) if policy_path.is_file() else ''
return SilentReleaseActionService(
SILENT_RELEASE_STATE_ROOT,
execution_enabled=SILENT_RELEASE_EXECUTION_ENABLED,
approval_verifier=verifier,
expected_repository=REPOSITORY_IDENTITY,
policy_sha256=policy_sha,
)
def relpath(path: str) -> str:
x = str(path or '').replace('\\','/').strip()
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
@@ -109,9 +228,13 @@ def ensure_git():
if not sh(['config','user.name'])['stdout']: sh(['config','user.name','chatgpt-gitops-bot'], True)
def checkout(branch, base='main', create=False):
ensure_git(); b=branch_name(branch)
if create or sh(['rev-parse','--verify',b])['code'] != 0:
sh(['checkout',base], True); sh(['checkout','-B',b], True)
else: sh(['checkout',b], True)
exists = sh(['rev-parse','--verify',f'refs/heads/{b}'])['code'] == 0
if exists:
sh(['checkout',b], True)
elif create:
sh(['checkout',base], True); sh(['checkout','-b',b], True)
else:
raise ValueError(f'branch does not exist: {b}')
return b
def write_file(path, content):
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
@@ -121,6 +244,19 @@ def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
return {'status':'skipped','reason':'protected_branch','branch':branch}
if BARE_REPO.exists():
common = sh(['rev-parse', '--path-format=absolute', '--git-common-dir'], True)['stdout']
if Path(common).resolve() == BARE_REPO.resolve():
head = sh(['rev-parse', 'HEAD'], True)['stdout']
stored = subprocess.run(
['git', '--git-dir', str(BARE_REPO), 'rev-parse', f'refs/heads/{branch}'],
text=True, capture_output=True,
)
if stored.returncode != 0 or stored.stdout.strip() != head:
raise RuntimeError(f'linked-worktree branch verification failed for {branch}')
return {
'status':'verified', 'target':'authoritative-bare',
'branch':branch, 'oid':head,
}
ref=f'refs/heads/{branch}:refs/heads/{branch}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git sync failed for {branch}')
@@ -130,15 +266,68 @@ def sync_branch(branch: str) -> dict:
return {'status':'synced','target':'origin','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def sync_tag(tag: str) -> dict:
if not tag: return {'status':'skipped','reason':'empty_tag'}
if BARE_REPO.exists():
ref=f'refs/tags/{tag}:refs/tags/{tag}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag sync failed for {tag}')
return {'status':'synced','target':'bare','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',tag], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
raise RuntimeError(
'unsafe sync_tag disabled: use TagPublisher with expected tag_oid, '
'target_oid, Gitea remote verification, and authenticated API verification'
)
def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
oid = str(value or '').strip().lower()
if not re.fullmatch(r'[0-9a-f]{40}', oid):
raise ValueError(f'{field} must be a full 40-character commit OID')
try:
return authoritative_commit(oid, git_dir=AUTHORITATIVE_GIT_DIR)
except (AuthoritativeGitError, ValueError) as exc:
raise ValueError(f'{field} does not resolve to the exact approved commit') from exc
def existing_tag_target(tag: str) -> str | None:
if not re.fullmatch(r'V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$', str(tag or '')):
raise ValueError('invalid release version')
try:
result = run_authoritative_git(
['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'],
check=False,
git_dir=AUTHORITATIVE_GIT_DIR,
)
except AuthoritativeGitError as exc:
raise RuntimeError(str(exc)) from exc
return result.stdout.strip() if result.returncode == 0 else None
def publication_request_from_query(q: dict[str, list[str]]) -> PublicationRequest:
required = ('version', 'target_oid', 'evidence_closure_oid', 'approval_plan_id')
missing = [name for name in required if not (q.get(name) or [''])[0]]
if missing:
raise ValueError('missing release governance fields: ' + ', '.join(missing))
return PublicationRequest(
(q['version'])[0],
(q['target_oid'])[0].lower(),
(q['evidence_closure_oid'])[0].lower(),
(q['approval_plan_id'])[0],
)
def required_authoritative_git_dir() -> Path:
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot["repository_available"]:
raise RuntimeError(str(snapshot["query_error"] or "AUTHORITATIVE_REPOSITORY_UNAVAILABLE"))
return Path(str(snapshot["authoritative_git_dir"]))
def publication_publisher() -> TagPublisher:
authoritative_repo = required_authoritative_git_dir()
return TagPublisher(
WS,
GitTagBackend(WS, authoritative_repo=authoritative_repo),
GiteaTagClient(
os.getenv('AIOS_GITOPS_GITEA_API_URL',''),
os.getenv('AIOS_GITOPS_GITEA_REPOSITORY',''),
Path(os.getenv('AIOS_GITOPS_GITEA_TOKEN_FILE','')),
token_line=int(os.getenv('AIOS_GITOPS_GITEA_TOKEN_LINE','1')),
),
os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'),
)
def commit(paths, msg):
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
@@ -158,6 +347,16 @@ def patches():
for p in sorted((WS/'patches').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
def runtime_release_info() -> dict[str, Any]:
for candidate in (WS / 'manifest.json', WS.parent / 'manifest.json'):
data = jload(candidate, {})
if data.get('target_commit'):
return {
'runtime_release_commit': data.get('target_commit'),
'runtime_release_path': str(WS),
}
return {'runtime_release_commit': None, 'runtime_release_path': str(WS)}
class H(BaseHTTPRequestHandler):
server_version='AIOSChatGPTGitOps/V1R1C0'
def sendj(self, data, code=200):
@@ -179,43 +378,221 @@ class H(BaseHTTPRequestHandler):
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
q=urllib.parse.parse_qs(u.query)
try:
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in ('/api/base','/api/baseline','/base'):
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
data=jload(WS/'baseline'/'current.json', {})
data.setdefault('authoritative_repository', repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
))
return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot['repository_available']:
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
try:
tags = authoritative_tags(git_dir=AUTHORITATIVE_GIT_DIR)
except AuthoritativeGitError as exc:
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
registry = [p.name for p in (WS/'versions').glob('*')]
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
'repository_available': True,
'query_error': None,
'tags': [item['name'] for item in tags],
'versions_dir': registry,
'head': snapshot['authoritative_head_oid'],
'authoritative_head_oid': snapshot['authoritative_head_oid'],
})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
frm=(q.get('from') or [''])[0]
to=(q.get('to') or [''])[0]
try:
diff=authoritative_diff(frm, to, git_dir=required_authoritative_git_dir())
except ValueError as exc:
return self.sendj({'error':redact(exc)},400)
except AuthoritativeGitNotFound as exc:
return self.sendj({'error':redact(exc)},404)
except AuthoritativeGitOutputTooLarge as exc:
return self.sendj({'error':redact(exc)},413)
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc)},503)
return self.sendj({'from':frm,'to':to,'diff':diff})
if path in ('/api/patch','/api/patches','/api/branches','/patch'):
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not snapshot['repository_available']:
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'metadata_patches': patches(),
'authoritative_branches': [],
}, 503)
try:
branches = authoritative_branches(git_dir=AUTHORITATIVE_GIT_DIR)
except AuthoritativeGitError as exc:
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'metadata_patches': patches(),
'authoritative_branches': [],
}, 503)
metadata = patches()
branch_names = {item['name'] for item in branches}
metadata_names = {
item.get('branch') for item in metadata
if isinstance(item, dict) and item.get('branch')
}
orphan = [
item for item in metadata
if item.get('branch') and item.get('branch') not in branch_names
]
missing = [
item for item in branches
if item['name'] not in metadata_names
]
return self.sendj({
'metadata_patches': metadata,
'authoritative_branches': branches,
'orphan_metadata': orphan,
'branches_without_metadata': missing,
'repository_available': True,
'query_error': None,
'patches': metadata,
'branches': [item['name'] for item in branches],
})
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
if path in ('/api/run','/api/runtime-status','/run'):
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
workspace_branch = None
if (WS / '.git').exists():
result = sh(['rev-parse','--abbrev-ref','HEAD'])
workspace_branch = result['stdout'] or None
payload = {
'service':'aios-gitops-model-api',
'status':svc.stdout.strip() or svc.stderr.strip(),
'workspace_branch':workspace_branch,
**runtime_release_info(),
**snapshot,
'writes_enabled': str(os.getenv('AIOS_GITOPS_WRITES_ENABLED','false')).lower() in {'1','true','yes','on'},
}
return self.sendj(payload, 200 if snapshot['repository_available'] else 503)
if path in ('/api/action-monitor','/action-monitor'):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
if path == '/api/tag-publication-status':
version=(q.get('version') or [''])[0]
try:
version=validate_version(version)
status=publication_publisher().get_tag_publication_status(version)
except ValueError:
return self.sendj({'error':'invalid release version'},400)
return self.sendj(status)
if path == '/api/release-consistency':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().get_release_consistency(request))
if path == '/api/release-acceptance':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().run_release_acceptance(request))
if path in (
'/getReleaseCandidate',
'/getApprovalEnvelope',
'/getReleaseSaga',
):
candidate_id=(q.get('candidate_id') or [''])[0]
if not candidate_id:
return self.sendj({'error':'candidate_id is required'},400)
service=silent_release_service()
if path == '/getReleaseCandidate':
return self.sendj(
service.get_release_candidate(candidate_id)
)
if path == '/getApprovalEnvelope':
return self.sendj(
service.get_approval_envelope(candidate_id)
)
return self.sendj(service.get_release_saga(candidate_id))
# Admin AI Evolution Dashboard
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
return self.ai_evolution_page()
if path in ('/admin/ai-evolution/api/data'):
return self.ai_evolution_data()
return self.sendj({'error':'not_found','path':path},404)
except FileNotFoundError:
return self.sendj({'error':'release candidate not found'},404)
except ValueError as e:
error=redact(e)
monitor.record_failure('do_GET', error, {'path':path})
return self.sendj({'error':error},400)
except Exception as e:
monitor.record_failure('do_GET', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
error=redact(e)
monitor.record_failure('do_GET', error, {'path':path})
return self.sendj({'error':error},500)
def do_PUT(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'PUT', path): return
data=self.body()
if path == '/api/model-priority': return self.model_priority(data)
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'POST', path): return
data=self.body()
try:
if path == '/createReleaseCandidate':
a=self.need('release:candidate')
if not a: return
return self.sendj(
silent_release_service().create_release_candidate(data)
)
if path == '/submitOwnerApproval':
a=self.need('release:approve')
if not a: return
return self.sendj(
silent_release_service().submit_owner_approval(data)
)
if path == '/startDeployment':
a=self.need('release:deploy')
if not a: return
approval=(
SignedReleasePlanApproval.from_dict(data['approval'])
if isinstance(data.get('approval'), dict)
else None
)
return self.sendj(
silent_release_service().start_deployment(
str(data.get('candidate_id') or ''),
approval=approval,
)
)
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
if path in ('/api/file','/api/commit-file'): return self.file(data)
if path == '/api/commit': return self.commit(data)
@@ -226,9 +603,30 @@ class H(BaseHTTPRequestHandler):
if path == '/api/model-priority': return self.model_priority(data)
if path == '/api/nl': return self.nl(data)
return self.sendj({'error':'not_found','path':path},404)
except (ValueError, FileNotFoundError) as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},400)
except CapabilityAuthorizationError as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},403)
except SagaConflict as e:
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},409)
except Exception as e:
monitor.record_failure('do_POST', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
error=redact(e)
monitor.record_failure('do_POST', error, {'path':path})
return self.sendj({'error':error},500)
def do_PATCH(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'PATCH', path): return
return self.sendj({'error':'not_found','path':path},404)
def do_DELETE(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'DELETE', path): return
return self.sendj({'error':'not_found','path':path},404)
def branch(self,data):
a=self.need('branch:create');
if not a: return
@@ -252,12 +650,41 @@ class H(BaseHTTPRequestHandler):
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'):
return self.sendj({
'error':'production_tag_evidence_required',
'required':['version','target_oid','evidence_closure_oid','approval_plan_id'],
},400)
try:
target_oid=resolve_commit_oid(data.get('target_oid'))
evidence_oid=resolve_commit_oid(data.get('evidence_closure_oid'), 'evidence_closure_oid')
except ValueError as exc:
return self.sendj({'error':'invalid_tag_target','detail':str(exc)},400)
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'request':req,'commit':c})
request=PublicationRequest(ver,target_oid,evidence_oid,str(data.get('approval_plan_id')))
main_before=sh(['rev-parse','refs/heads/main'], True)['stdout']
publisher=publication_publisher()
try:
publication=publisher.publish(request)
except PublicationManualOwnerReviewRequired as exc:
audit(a['account'],'tag_manual_owner_review_required',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':redact(exc)})
return self.sendj({'status':'MANUAL_OWNER_REVIEW_REQUIRED','error':redact(exc),'main_changed':False,'force_push':False},409)
except PublicationConflict as exc:
audit(a['account'],'tag_conflict',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'RECONCILIATION_REQUIRED','error':str(exc),'main_changed':False,'force_push':False},409)
except PublicationPending as exc:
audit(a['account'],'tag_pending',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'REMOTE_PUSH_PENDING','error':str(exc),'main_changed':False,'force_push':False},503)
except PublicationReconciliationRequired as exc:
audit(a['account'],'tag_reconciliation_required',ver,{'target_oid':target_oid,'approval_plan_id':data.get('approval_plan_id'),'error':str(exc)})
return self.sendj({'status':'RECONCILIATION_REQUIRED','error':str(exc),'main_changed':False,'force_push':False},502)
main_after=sh(['rev-parse','refs/heads/main'], True)['stdout']
if main_after != main_before:
raise RuntimeError('main changed during tag operation')
result={'status':publication['state'],'version':ver,'tag_name':ver,'tag_oid':publication['tag_oid'],'target_oid':target_oid,'expected_target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'workspace_head':sh(['rev-parse','HEAD'],True)['stdout'],'main_oid_before':main_before,'main_oid_after':main_after,'main_changed':False,'force_push':False,'publication':publication}
audit(a['account'],'tag',ver,result)
self.sendj(result)
def rollback(self,data):
a=self.need('rollback:request');
if not a: return
@@ -352,7 +779,10 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
cmd_filter=(params.get('command') or [None])[0]
bl=jload(WS/'baseline'/'current.json',{})
current_ver=bl.get('current_baseline','V1R3C1')
tags=sh(['tag','--list','V*'])['stdout'].splitlines() if WS.exists() else []
try:
tags=[item['name'] for item in authoritative_tags(git_dir=required_authoritative_git_dir()) if str(item['name']).startswith('V')]
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc),'repository_available':False},503)
versions=[]
if (WS/'baseline'/'promotions').exists():
for p in sorted((WS/'baseline'/'promotions').glob('*.json')):
@@ -390,9 +820,9 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
if __name__ == '__main__': main()
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Fail-closed policy for in-process Git mutation handlers."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping
_TRUE = frozenset({"1", "true", "yes", "on"})
@dataclass(frozen=True)
class GitWriteDecision:
allowed: bool
status_code: int
error: str
reason: str
workspace: str
def payload(self) -> dict[str, str]:
return {
"status": "allowed" if self.allowed else "blocked",
"error": self.error,
"reason": self.reason,
"workspace": self.workspace,
}
def _real(path: Path) -> Path:
return Path(os.path.realpath(os.path.abspath(str(path))))
def evaluate_git_write(workspace: Path | str, env: Mapping[str, str] | None = None) -> GitWriteDecision:
values = os.environ if env is None else env
lexical = Path(os.path.abspath(str(workspace)))
canonical = _real(lexical)
shown = str(lexical)
if str(values.get("AIOS_GITOPS_WRITES_ENABLED", "false")).strip().lower() not in _TRUE:
return GitWriteDecision(False, 423, "gitops_writes_disabled", "AIOS_GITOPS_WRITES_ENABLED is false", shown)
if lexical != canonical:
return GitWriteDecision(False, 409, "workspace_symlink_forbidden", "workspace must be a canonical non-symlink path", shown)
controller = _real(Path(values.get("AIOS_GITOPS_CONTROLLER_PATH", "/data/aios-gitops/worktrees/controller")))
role = str(values.get("AIOS_GITOPS_WORKSPACE_ROLE", "")).strip().lower()
if canonical == controller or role == "runtime-anchor":
return GitWriteDecision(False, 423, "runtime_anchor_read_only", "controller/runtime-anchor is read-only", str(canonical))
writer_root = _real(Path(values.get("AIOS_GITOPS_APPROVED_WRITER_ROOT", "/data/aios-gitops/worktrees")))
try:
canonical.relative_to(writer_root)
except ValueError:
return GitWriteDecision(False, 409, "workspace_not_approved_writer", "workspace is outside the approved writer root", str(canonical))
return GitWriteDecision(True, 200, "", "approved isolated writer", str(canonical))
+355
View File
@@ -0,0 +1,355 @@
"""Fail-closed access to the authoritative bare Git repository."""
from __future__ import annotations
import os
import re
import subprocess
import urllib.parse
from pathlib import Path
from typing import Iterable
_ALLOWED_COMMANDS = frozenset(
{
"cat-file",
"config",
"diff",
"for-each-ref",
"ls-remote",
"merge-base",
"push",
"remote",
"rev-parse",
"show-ref",
"tag",
}
)
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_MAX_OUTPUT = 1_048_576
_TIMEOUT = 8.0
class AuthoritativeGitError(RuntimeError):
"""A fail-closed authoritative repository or ref query error."""
class AuthoritativeGitNotFound(AuthoritativeGitError):
"""A validated revision does not exist in the authoritative repository."""
class AuthoritativeGitOutputTooLarge(AuthoritativeGitError):
"""A controlled Git query exceeded the response-size boundary."""
def _redact(text: str) -> str:
value = str(text or "")
value = re.sub(r"(?i)(authorization\s*[:=]\s*)(?:bearer|token)\s+\S+", r"\1REDACTED", value)
value = re.sub(r"(?i)((?:access_)?token\s*[:=]\s*)\S+", r"\1REDACTED", value)
value = re.sub(r"(?i)(https?://[^/\s:@]+:)[^@\s/]+@", r"\1REDACTED@", value)
return value
def _configured_path(git_dir: str | Path | None = None) -> Path:
raw = str(git_dir or os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")).strip()
if not raw:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
path = Path(raw)
if not path.is_absolute() or ".." in path.parts:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
try:
resolved = path.resolve(strict=True)
except OSError as exc:
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE") from exc
if not resolved.is_dir():
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
return resolved
def _run(
git_dir: Path,
args: list[str],
*,
check: bool,
timeout: float = _TIMEOUT,
) -> subprocess.CompletedProcess[str]:
if not args or args[0] not in _ALLOWED_COMMANDS:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
try:
result = subprocess.run(
["git", "--git-dir", str(git_dir), *args],
text=True,
capture_output=True,
timeout=timeout,
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED") from exc
if len(result.stdout) + len(result.stderr) > _MAX_OUTPUT:
raise AuthoritativeGitOutputTooLarge("GIT_OUTPUT_LIMIT_EXCEEDED")
if check and result.returncode != 0:
detail = _redact(result.stderr.strip() or result.stdout.strip())
raise AuthoritativeGitError(
"GIT_REF_QUERY_FAILED" + (f": {detail[:160]}" if detail else "")
)
return result
def _repository_identity_from_remote(url: str) -> str:
value = str(url or "").strip().split("?", 1)[0].rstrip("/")
if not value:
return ""
if "://" in value:
value = urllib.parse.urlparse(value).path
elif ":" in value:
value = value.rsplit(":", 1)[1]
value = value.rsplit("@", 1)[-1].strip("/")
value = re.sub(r"\.git$", "", value, flags=re.IGNORECASE)
parts = [part for part in value.split("/") if part]
if len(parts) >= 2:
value = "/".join(parts[-2:])
return value.strip("/").lower()
def verify_repository(
git_dir: str | Path | None = None,
repository_identity: str | None = None,
) -> tuple[Path, str]:
path = _configured_path(git_dir)
expected = str(
repository_identity
or os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
).strip().strip("/")
if not expected or expected.count("/") != 1:
raise AuthoritativeGitError("REPOSITORY_IDENTITY_MISMATCH")
bare = _run(path, ["rev-parse", "--is-bare-repository"], check=True)
if bare.stdout.strip().lower() != "true":
raise AuthoritativeGitError("AUTHORITATIVE_REPOSITORY_UNAVAILABLE")
remote = _run(path, ["config", "--get", "remote.gitea.url"], check=False)
actual = _repository_identity_from_remote(remote.stdout)
if actual != expected.lower():
raise AuthoritativeGitError("REPOSITORY_IDENTITY_MISMATCH")
return path, expected
def run_authoritative_git(
args: list[str],
*,
check: bool = True,
git_dir: str | Path | None = None,
timeout: float = _TIMEOUT,
) -> subprocess.CompletedProcess[str]:
path, _ = verify_repository(git_dir=git_dir)
return _run(path, list(args), check=check, timeout=timeout)
def authoritative_head_oid(
*,
git_dir: str | Path | None = None,
) -> str:
result = run_authoritative_git(["rev-parse", "--verify", "refs/heads/main"], git_dir=git_dir)
oid = result.stdout.strip().lower()
if not _OID_RE.fullmatch(oid):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
return oid
def authoritative_tags(
*,
git_dir: str | Path | None = None,
) -> list[dict[str, str | None]]:
result = run_authoritative_git(
[
"for-each-ref",
"refs/tags",
"--format=%(refname:strip=2)\t%(objectname)\t%(objecttype)",
],
git_dir=git_dir,
)
tags: list[dict[str, str | None]] = []
for line in result.stdout.splitlines():
name, oid, object_type = (line.split("\t", 2) + ["", ""])[:3]
if not name or not _OID_RE.fullmatch(oid.lower()):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
peeled_result = run_authoritative_git(
["rev-parse", "--verify", f"refs/tags/{name}^{{commit}}"],
git_dir=git_dir,
)
peeled = peeled_result.stdout.strip().lower()
if not _OID_RE.fullmatch(peeled):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
tags.append(
{
"name": name,
"tag_oid": oid.lower(),
"object_type": object_type,
"peeled_target_oid": peeled,
}
)
return tags
def authoritative_branches(
*,
git_dir: str | Path | None = None,
) -> list[dict[str, str]]:
result = run_authoritative_git(
[
"for-each-ref",
"refs/heads",
"--format=%(refname:strip=2)\t%(objectname)",
],
git_dir=git_dir,
)
branches: list[dict[str, str]] = []
for line in result.stdout.splitlines():
name, oid = (line.split("\t", 1) + [""])[:2]
if not name or not _OID_RE.fullmatch(oid.lower()):
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
branches.append({"name": name, "oid": oid.lower()})
return branches
def authoritative_commit(
oid: str,
*,
git_dir: str | Path | None = None,
) -> str:
value = str(oid or "").strip().lower()
if not _OID_RE.fullmatch(value):
raise ValueError("commit OID must be a full 40-character value")
result = run_authoritative_git(
["rev-parse", "--verify", f"{value}^{{commit}}"],
git_dir=git_dir,
)
resolved = result.stdout.strip().lower()
if resolved != value:
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED")
return resolved
def authoritative_lineage(
ancestor: str,
descendant: str,
*,
git_dir: str | Path | None = None,
) -> None:
authoritative_commit(ancestor, git_dir=git_dir)
authoritative_commit(descendant, git_dir=git_dir)
result = run_authoritative_git(
["merge-base", "--is-ancestor", ancestor, descendant],
check=False,
git_dir=git_dir,
)
if result.returncode != 0:
raise AuthoritativeGitError("evidence lineage mismatch")
_SAFE_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,239}$")
def _safe_ref_name(value: str) -> bool:
return bool(
_SAFE_REF_RE.fullmatch(value)
and ".." not in value
and "//" not in value
and "@{" not in value
and not value.endswith(("/", ".", ".lock"))
and not any(part in {"", ".", ".."} for part in value.split("/"))
)
def validate_git_revision(
value: str,
field: str,
*,
git_dir: str | Path | None = None,
) -> str:
revision = str(value or "").strip()
if not revision or revision != str(value or "") or revision.startswith("-"):
raise ValueError(f"invalid {field} revision")
if _OID_RE.fullmatch(revision.lower()):
try:
return authoritative_commit(revision.lower(), git_dir=git_dir)
except AuthoritativeGitError as exc:
raise AuthoritativeGitNotFound(f"{field} revision not found") from exc
if not _safe_ref_name(revision) or any(token in revision for token in ("^", "~", ":", "\\")):
raise ValueError(f"invalid {field} revision")
if revision.startswith("refs/"):
if not revision.startswith(("refs/heads/", "refs/tags/")):
raise ValueError(f"invalid {field} revision")
candidates = [revision]
else:
candidates = [f"refs/heads/{revision}", f"refs/tags/{revision}"]
matches: list[str] = []
for candidate in candidates:
result = run_authoritative_git(
["rev-parse", "--verify", f"{candidate}^{{commit}}"],
check=False,
git_dir=git_dir,
)
oid = result.stdout.strip().lower()
if result.returncode == 0 and _OID_RE.fullmatch(oid):
matches.append(oid)
if not matches:
raise AuthoritativeGitNotFound(f"{field} revision not found")
if len(set(matches)) != 1 or len(matches) != 1:
raise ValueError(f"ambiguous {field} revision")
return matches[0]
def authoritative_diff(
from_revision: str,
to_revision: str,
*,
git_dir: str | Path | None = None,
) -> dict[str, object]:
path, _ = verify_repository(git_dir=git_dir)
from_oid = validate_git_revision(from_revision, "from", git_dir=path)
to_oid = validate_git_revision(to_revision, "to", git_dir=path)
args = ["diff", "--no-ext-diff", "--no-textconv", from_oid, to_oid]
result = run_authoritative_git(args, git_dir=path)
return {
"code": 0,
"stdout": result.stdout,
"stderr": "",
"cmd": ["git", f"--git-dir={path}", *args],
"from_oid": from_oid,
"to_oid": to_oid,
}
def repository_snapshot(
*,
git_dir: str | Path | None = None,
repository_identity: str | None = None,
) -> dict[str, object]:
configured = bool(
str(git_dir or os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")).strip()
)
try:
path, identity = verify_repository(
git_dir=git_dir,
repository_identity=repository_identity,
)
head = authoritative_head_oid(git_dir=path)
return {
"repository_available": True,
"authoritative_git_dir_configured": configured,
"authoritative_git_dir_readable": True,
"authoritative_git_dir": str(path),
"authoritative_head_oid": head,
"repository_identity": identity,
"query_error": None,
}
except (AuthoritativeGitError, ValueError) as exc:
return {
"repository_available": False,
"authoritative_git_dir_configured": configured,
"authoritative_git_dir_readable": False,
"authoritative_git_dir": None,
"authoritative_head_oid": None,
"repository_identity": str(
repository_identity
or os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
).strip() or None,
"query_error": _redact(str(exc)),
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -12
View File
@@ -1,13 +1,25 @@
{
"current_baseline": "V1R4C6",
"previous_baseline": "V1R4C5",
"promoted_at": "2026-07-11T21:00:00+08:00",
"git": "e204a90398bf646d46245b3088226749fa056edd",
"promoted_by": "openclaw/admin",
"promotion_reason": "V1R4C6 admin search agent integration, search gateway, agent platform, VERSION bump, merge tests deploy pipeline — not yet deployed to production",
"constraints": {
"memory_search_enabled": false,
"force_push_allowed": false,
"gitea_db_mutation_requires_owner_approval": true
}
}
"current_baseline": "V2R1C2",
"previous_baseline": "V2R1C1",
"promoted_at": "2026-07-22T01:40:51Z",
"git": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"promoted_by": "codex/owner-approved-gitops",
"promotion_reason": "Owner-authorized V2R1C2 Unified Portal production release after exact tag binding, complete evidence verification, and production acceptance PASS.",
"version": "V2R1C2",
"release_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"status": "PRODUCTION_DEPLOYED",
"production_service": "aios-v2r1c2-production.service",
"production_port": 18920,
"rollback_version": "V2R1C1",
"constraints": {
"product_code_changed": true,
"production_redeployed": true,
"production_traffic_changed": true,
"tag_rewritten": false,
"force_push_allowed": false,
"rollback_assets_preserved": true
}
}
@@ -1,49 +0,0 @@
{
"host": "127.0.0.1",
"port": 18920,
"database_path": "/var/lib/aios-orchestrator-gateway/state.sqlite3",
"event_inbox_dir": "/var/lib/aios-orchestrator-gateway/inbox/events",
"decision_inbox_dir": "/var/lib/aios-orchestrator-gateway/inbox/decisions",
"dispatch_outbox_dir": "/var/lib/aios-orchestrator-gateway/outbox",
"openclaw_sync_dir": "/var/lib/aios-orchestrator-gateway/openclaw-sync",
"command_queue_dir": "/var/lib/aios-orchestrator-gateway/commands",
"inbound_token_file": "/etc/aios-orchestrator-gateway/inbound.token",
"callback_token_file": "/etc/aios-orchestrator-gateway/callback.token",
"contract_commit": "cbe33ecab8e9794ba5f318bea43aeaf447a56fbc",
"workspace_agent": {
"trigger_id": "agtch_REPLACE_ME",
"access_token_file": "/etc/aios-orchestrator-gateway/workspace-agent.token"
},
"responses_fallback": {
"api_key_file": "/etc/aios-orchestrator-gateway/openai-api.key",
"model": "REPLACE_WITH_APPROVED_MODEL"
},
"polling": {
"interval_seconds": 10,
"summary_interval_seconds": 300
},
"loop_controls": {
"callback_timeout_seconds": 900,
"lease_seconds": 60,
"retry_base_seconds": 10,
"max_retries": 5,
"max_same_blocker_retries": 3,
"max_hops_per_gate": 20
},
"openclaw_sync": {
"endpoint": null,
"token_file": null
},
"project_key": "v2r1-personal-weixin-production",
"git_poll": {
"enabled": true,
"bare_repo": "/data/aios-gitops/repo.git",
"ref_prefixes": [
"refs/heads/main",
"refs/heads/task/governance/",
"refs/heads/patch/",
"refs/heads/result/"
],
"interval_seconds": 30
}
}
+63
View File
@@ -0,0 +1,63 @@
{
"schema_version": 2,
"automatic": [
"build",
"test",
"artifact",
"canary",
"evidence"
],
"owner": [
"signed_release_plan"
],
"forbidden": [
"force_push",
"tag_rewrite",
"direct_main_push",
"baseline_mutation",
"registry_mutation"
],
"runtime_deployment": {
"approval_action": "signed_release_plan",
"required_capability": "runtime_switch",
"tag_operations_allowed": false,
"baseline_mutation_allowed": false,
"registry_mutation_allowed": false,
"writer_default": false,
"rollback_required": true
},
"tag_release": {
"approval_action": "signed_release_plan",
"required_capability": "tag_publish",
"separate_controller": true,
"duplicate_conflict": "fail_closed",
"rewrite_allowed": false,
"force_push_allowed": false
},
"baseline_registry_finalize": {
"approval_action": "signed_release_plan",
"required_capability": "baseline_registry_finalize",
"separate_controller": true,
"remote_tag_verification_required": true,
"gitea_api_verification_required": true,
"registry_baseline_consistency_required": true
},
"approval": {
"algorithm": "Ed25519",
"replay_protection": true,
"revocation_fail_closed": true,
"key_rotation_supported": true,
"capabilities": [
"runtime_switch",
"tag_publish",
"baseline_registry_finalize"
]
},
"execution": {
"environment_variable": "AIOS_SILENT_RELEASE_EXECUTION_ENABLED",
"default_enabled": false,
"shadow_side_effects_allowed": false,
"durable_saga_required": true,
"pre_switch_revalidation_required": true
}
}
+59
View File
@@ -0,0 +1,59 @@
version: 1
policy_id: gitops-isolated-worktree-governance-20260714T232948UTC8
runtime_identity:
user: aiman
group: gitops
umask: "0002"
prohibit_root_workspace_writes: true
repository:
source_worktree: /data/aios-gitops/workspace
bare_repo: /data/aios-gitops/repo.git
worktree_root: /data/aios-gitops/worktrees
remote: origin
worktrees:
controller: /data/aios-gitops/worktrees/controller
task_pattern: /data/aios-gitops/worktrees/{agent}/{task_id}
allowed_agents:
- openclaw
- chatgpt
- controller
require_clean_start: true
remove_after_successful_push: true
retain_on_failure: true
permissions:
owner: aiman
group: gitops
directory_mode: "2775"
file_mode: "0664"
default_acl:
user:aiman: rwx
group:gitops: rwx
apply_to:
- /data/aios-gitops
- /data/aios-gitops/worktrees
preflight:
required_user: aiman
required_group: gitops
required_umask: "0002"
require_writable_git_dir: true
require_writable_task_dir: true
require_writable_result_dir: true
reject_shared_source_worktree: true
reject_existing_git_lock: true
notifications:
owner_on_received: required
owner_on_terminal: required
preferred_channel: weixin
redact_secrets: true
rollback:
preserve_source_worktree: true
remove_created_worktrees: true
restore_service_dropins: true
restore_service_state: true
@@ -0,0 +1,14 @@
# Install as a systemd drop-in for GitOps/OpenClaw writer services.
# Example: /etc/systemd/system/<service>.service.d/10-runtime-identity.conf
[Service]
User=aiman
Group=gitops
SupplementaryGroups=gitops
UMask=0002
Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
Environment=AIOS_BARE_REPO=/data/aios-gitops/repo.git
Environment=AIOS_WORKTREE_ROOT=/data/aios-gitops/worktrees
Environment=AIOS_RUNTIME_USER=aiman
Environment=AIOS_RUNTIME_GROUP=gitops
Environment=AIOS_RUNTIME_UMASK=0002
@@ -0,0 +1,27 @@
# AIOS V2R1C2 Unified Portal production metadata.
# No secret values belong in this file; runtime integrations use references.
AIOS_V2_HOST=127.0.0.1
AIOS_V2_PORT=18920
AIOS_V2_STATE_DIR=/var/lib/aios-v2r1c2-production
AIOS_PROVENANCE_STORE_FILE=/var/lib/aios-v2r1c2-production/run-provenance.sqlite3
AIOS_EXTERNAL_TASK_STORE_FILE=/var/lib/aios-v2r1c2-production/external-task-truth.sqlite3
AIOS_EXECUTOR_CATALOG_FILE=/var/lib/aios-v2r1c2-production/executor-catalog.json
AIOS_EXECUTION_PLAN_FILES=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json:/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_EXECUTION_PLAN_FILE=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_RELEASE_VERSION=V2R1C2
AIOS_TARGET_VERSION=V2R1C2
AIOS_BASELINE_VERSION=V2R1C1
AIOS_PREVIOUS_VERSION=V2R1C1
AIOS_RELEASE_STATUS=PRODUCTION
AIOS_RELEASE_SOURCE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
AIOS_RELEASE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
AIOS_RELEASE_TAG=V2R1C2
AIOS_RELEASE_EVIDENCE_SHA256=2e27bb73b8e6297bd3077238f73a96d697fc3c3611c601c4dc9357e5fdcd8eee
AIOS_OPENCLAW_CONTRACT_VERSION=aios.openclaw.command.v2
AIOS_OPENCLAW_SIGNATURE_ALGORITHM=Ed25519
AIOS_OPENCLAW_PLUGIN_VERSION=2.1.0-rc.4
AIOS_OPENCLAW_SIGNING_KEY_ID=openclaw-v2r1-candidate-ed25519
AIOS_OWNER_WECHAT_TARGET_REF=AIOS_OWNER_WECHAT_TARGET_REF
@@ -0,0 +1,44 @@
[Unit]
Description=AIOS V2R1C2 Unified Portal Production
After=network-online.target
Wants=network-online.target
Conflicts=aios-orchestrator-gateway.service
ConditionPathExists=/opt/aios-v2r1c2/active-runtime/app/aios_web/release_application.py
ConditionPathExists=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
[Service]
Type=simple
User=aios
Group=aios
WorkingDirectory=/opt/aios-v2r1c2/active-runtime/app
EnvironmentFile=/etc/aios-v2/aios-v2r1c2-production.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStartPre=/usr/bin/test "$AIOS_V2_PORT" = "18920"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_SOURCE_OID" = "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_TAG" = "V2R1C2"
ExecStart=/usr/bin/python3 -m aios_web.release_application
Restart=on-failure
RestartSec=3
TimeoutStartSec=60
TimeoutStopSec=30
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadWritePaths=/var/lib/aios-v2r1c2-production
StateDirectory=aios-v2r1c2-production
StateDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
@@ -11,6 +11,8 @@ Environment=AIOS_GITOPS_WORKSPACE=/data/aios-gitops/workspace
Environment=AIOS_GITOPS_DB=/data/aios-gitops/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=/data/aios-gitops/runtime/secrets/gitops.tokens
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_chatgpt_actions_api.py
@@ -0,0 +1,268 @@
# AIOS Silent Release Controller Design Report
状态:隔离分支实现;生产 mutation 禁止
目标:自动准备 → 自动验证 → 单次 Owner 签名授权 → 分控制器自动执行与验收
## 1. 当前痛点
现有流程需要多轮人工传递 commit、tree、artifact、manifest、测试、canary、drop-in
和回滚证据。证据在不同消息里重复抄写,容易出现:
- 构建后 artifact、manifest 或 policy 已变化,但旧批准仍被使用;
- Git、Gitea、systemd 与回滚结果由人工拼接,缺少同一一致性快照;
- runtime switch、tag、Baseline、Registry 的授权边界不清;
- 重启后只剩工作树内临时 JSON,无法可靠恢复;
- approval replay、并发 writer、key revoke 和 secret inheritance 缺少统一防线。
## 2. 新架构
```text
Build / Test / Canary
|
v
ReleaseCandidate + DeploymentPlan
|
v
ReleaseAcceptanceEngine
|
v
READY_FOR_APPROVAL
|
v
SignedReleasePlanApproval (Ed25519)
|
+--> runtime_switch capability
| DeploymentController
|
+--> tag_publish capability
| TagController
|
+--> baseline_registry_finalize capability
BaselineRegistryFinalizeController
Durable ReleaseSagaStore + CapabilityLedger + Event Journal
贯穿三个控制器;控制器互相不拥有对方的 mutation API。
```
一次 Owner 批准是同一个不可变 release plan 的签名,不是一个万能执行入口。三个 capability
分别消费、分别留痕、分别幂等恢复。`force_push`、`tag_rewrite`、`direct_main_push`
永远不是可签发 capability。
## 3. 状态机
```text
PREPARING
-> VERIFYING
-> READY_FOR_APPROVAL
-> APPROVED
-> PRE_SWITCH_REVALIDATING
-> SWITCH_PENDING
-> SWITCHED
-> RUNTIME_VERIFYING
-> TAG_PENDING
-> TAG_VERIFIED
-> BASELINE_REGISTRY_PENDING
-> COMPLETED
任一可恢复故障 -> RECONCILIATION_REQUIRED
对象冲突 -> MANUAL_OWNER_REVIEW_REQUIRED
切换后失败 -> ROLLBACK_PENDING -> ROLLED_BACK
```
状态快照使用 revision compare-and-set;事件同时写入 append-only journal。writer 崩溃后根据
状态确定唯一恢复动作。并发控制器中只有一个 CAS winner。
## 4. 数据模型
### ReleaseCandidate
```text
candidate_id
source_commit
tree_oid
artifact_sha256
manifest_sha256
tests
canary
rollback_ready
git_consistency
gitea_consistency
approval_state
```
### DeploymentPlan
```text
service
release_path / release_root
dropin_path / dropin_sha256
rollback_path / rollback_sha256
source_commit
artifact_sha256
manifest_sha256
policy_sha256
health_contract
sha256
```
### SignedReleasePlanApproval
```text
release_plan_id
candidate_id
source_commit
tree_oid
artifact_sha256
manifest_sha256
deployment_plan_sha256
policy_sha256
approval_plan_id
evidence_closure_oid
owner_approval_record_oid
capabilities
expires_at
nonce
key_id
signature
fingerprint
```
签名内容使用 canonical JSON,`signature` 不参与被签名字段。验证包括 Ed25519 签名、过期时间、
公钥状态、approval revoke、candidate/plan/policy 逐字段绑定和 capability allowlist。
## 5. API
```text
POST /createReleaseCandidate
GET /getReleaseCandidate?candidate_id=...
GET /getApprovalEnvelope?candidate_id=...
POST /submitOwnerApproval
POST /startDeployment
GET /getReleaseSaga?candidate_id=...
```
写接口要求 Bearer scope。`AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false` 是默认值;此时可以
持久化 candidate、生成 approval envelope、保存经验证的 Owner approval 和读取 Saga,
但 `/startDeployment` 只返回 `SHADOW_MODE` 与空 `side_effects`。
## 6. PreSwitchValidator
切换前重新收集并比较:
- source commit 与 tree;
- artifact、manifest、policy 和 deployment plan SHA-256;
- rollback readiness;
- local、authoritative bare 与 Gitea refs;
- repository identity;
- task writer/production writer 边界;
- health contract。
任一变化都抛出:
```text
APPROVAL_STALE_REAPPROVAL_REQUIRED
```
旧批准不会被“最佳努力”继续使用。
## 7. systemd 防线
- service 与 drop-in 均为显式 allowlist;
- release 与 drop-in resolved path 必须位于 release root 内;
- 拒绝普通文件被替换为 symlink,拒绝 symlink escape;
- deployment lock 覆盖 switch、restart 和首次 health check;
- restart 前后记录 MainPID 与 ActiveEnterTimestampMonotonic;
- artifact、drop-in、rollback 和 manifest 均按 plan hash 复验;
- 子进程只接收固定 allowlist 环境,不继承 token、password、credential、askpass、
auth socket、`LD_PRELOAD`、`PYTHONPATH` 或数据库连接串。
## 8. TagController 分离
TagController 必须独立完成 approval、execute、verify,并留下:
```text
evidence_closure_oid
approval_plan_id
repository_identity
remote_tag_oid
remote_peeled_target_oid
gitea_api_tag_oid
gitea_listing_visible
```
同名 tag 的 tag OID 或 peeled target 不同,进入
`MANUAL_OWNER_REVIEW_REQUIRED`;自动流程不 push、不删除、不重写、不 force push。
## 9. Baseline / Registry 门禁
只有以下全部一致时,`baseline_registry_finalize` capability 才能完成:
```text
Registry = Baseline = remote tag = Gitea API = expected target
```
UNKNOWN、API 不可达、repository identity 不符或 listing 不可见均 fail closed。
## 10. Policy as Code
```text
automatic:
build, test, artifact, canary, evidence
owner:
signed_release_plan
capabilities:
runtime_switch
tag_publish
baseline_registry_finalize
forbidden:
force_push
tag_rewrite
direct_main_push
baseline_mutation
registry_mutation
```
最后两个 forbidden 项指“脱离受治理 finalize controller 的直接 mutation”。
## 11. Security model
- Ed25519 公钥 ring 支持 rotation 和 revoke,私钥不进入 runtime;
- approval plan 可撤销,过期、未知 key、错误签名均 fail closed;
- nonce 与 capability operation 持久化,重复相同请求幂等,不同 fingerprint 重放拒绝;
- Saga snapshot 原子写、journal append-only、CAS 防并发 writer;
- credential 只通过受控 reference 使用,不进入 Git、API 响应或异常;
- 子进程环境从空集合构建,不复制 `os.environ`;
- Shadow 模式默认开启,execution adapter 缺失时不能降级执行;
- runtime、tag、Baseline/Registry 分属三个能力最小化控制器。
## 12. Migration plan
1. Shadow:生成 candidate、plan、acceptance、approval package,仅比较现有人工结果。
2. Signed approval:接入 Owner 公钥 ring、撤销源和审计 record;仍不接生产 executor。
3. Canary execution:使用非生产 service/release root 验证锁、PID、health、崩溃恢复和回滚。
4. Controlled production switch:另行 Owner 授权启用 execution flag,只开放 runtime capability。
5. Tag/finalize:分别完成 Gitea 与 Registry/Baseline 生产验收后再启用对应 capability。
每个阶段均可退回上一个阶段;不开启下一能力不会影响已经启用的控制器。
## 13. Rollback plan
- switch 前保存旧 symlink target、PID、启动时间、artifact 与 plan fingerprint;
- restart 或 health 任一失败进入 `ROLLBACK_PENDING`;
- 原子恢复旧 target,只 restart 指定 allowlist service;
- 重新验证旧 runtime commit、writer=false 与 repository identity;
- 成功进入 `ROLLED_BACK`,失败进入 `MANUAL_OWNER_REVIEW_REQUIRED`;
- rollback 不修改 Git main、production tag、Baseline 或 Registry;
- 保留脱敏 journal、approval fingerprint 和失败证据。
## 14. 测试与外部依赖边界
自动测试覆盖签名伪造、字段篡改、过期/revoke/key rotation、nonce replay、CAS race、
崩溃恢复、artifact/policy/plan mutation、tag conflict、secret leakage、runtime without
`.git`、rollback 和 shadow no-side-effects。
本隔离实现不创建生产 KMS/key、不安装 systemd unit、不部署 Action runtime、不创建 tag,
也不修改 Baseline/Registry。上述动作必须在单独生产批准中执行。
@@ -0,0 +1,68 @@
# Gitea Tag Publication Atomicity
Release publication is a fail-closed Saga bound to one immutable governance
fingerprint:
- `version`
- `target_oid`
- `evidence_closure_oid`
- `approval_plan_id`
The normal progression is:
`PENDING` → `LOCAL_TAG_CREATED` → `REMOTE_PUSH_CONFIRMED` →
`GITEA_API_CONFIRMED` → `BASELINE_UPDATE_PENDING` → `BASELINE_UPDATED` →
`PRODUCTION_DEPLOYED`
An unavailable remote uses `REMOTE_PUSH_PENDING` or
`RECONCILIATION_REQUIRED`. A same-name tag object mismatch, peeled-target
mismatch, changed governance fingerprint, or immutable deployment-evidence
conflict enters `MANUAL_OWNER_REVIEW_REQUIRED`. That state blocks automatic
push and reconciliation; recovery never deletes, rewrites, or force-pushes a
tag.
## Legacy state migration
Readers normalize the historical `TAG_CREATED_LOCAL` value to
`LOCAL_TAG_CREATED` in memory. Read-only status responses therefore expose only
the canonical value. The next governed Saga write persists the canonical name;
reading legacy state alone does not mutate the tag, Baseline, Registry, or Saga
file. Any unrecognized persisted state fails closed as
`RECONCILIATION_REQUIRED`.
`BASELINE_UPDATE_PENDING` is persisted before either `baseline/current.json` or
`versions/<version>.json` changes. Recovery revalidates the governance
fingerprint, evidence lineage, remote annotated tag, peeled target, Gitea API,
listing, and repository identity before idempotently completing matching
partial records. A different tag or target enters
`MANUAL_OWNER_REVIEW_REQUIRED`.
## Durable state
Production writers set `AIOS_GITOPS_PUBLICATION_STATE_DIR` to storage outside
their disposable worktree. Recreating a writer must reuse that directory.
Worktree JSON is not the authoritative Saga store.
## Deployment gate
`mark_production_deployed` performs fresh Git remote, peeled-target, Gitea ref,
Gitea tag-object, listing, repository-identity, evidence-lineage, Registry, and
Baseline checks. Deployment is allowed only when:
`Registry = Baseline = remote tag = Gitea API = expected target`
Deployment evidence must be an immutable `sha256:<64 hex>` or `git:<40 hex>`
identifier.
## Read-only acceptance
- `getTagPublicationStatus` reads durable Saga state.
- `getReleaseConsistency` compares every release authority.
- `runReleaseAcceptance` returns `accepted: true` only for `PASS`.
Unavailable evidence returns `UNKNOWN` with `fail_closed: true`; it is never
treated as acceptance.
The CLI, HTTP Action, and release script all enter the same publisher core for
evidence-lineage verification. The release script stages Registry and Baseline
records only; durable Saga state remains outside Git.
+206 -3
View File
@@ -13,6 +13,27 @@ components:
scheme: bearer
bearerFormat: opaque-token
schemas:
PublicationState:
type: string
description: Canonical durable publication state. Legacy TAG_CREATED_LOCAL is normalized on read and is never emitted by new writes.
enum:
- PENDING
- LOCAL_TAG_CREATED
- REMOTE_PUSH_PENDING
- REMOTE_PUSH_CONFIRMED
- GITEA_API_CONFIRMED
- BASELINE_UPDATE_PENDING
- BASELINE_UPDATED
- PRODUCTION_DEPLOYED
- RECONCILIATION_REQUIRED
- MANUAL_OWNER_REVIEW_REQUIRED
PublicationStatus:
type: object
required: [version, state]
properties:
version: {type: string}
state: {$ref: '#/components/schemas/PublicationState'}
fail_closed: {type: boolean}
BranchRequest:
type: object
required: [branch]
@@ -50,9 +71,20 @@ components:
message: {type: string}
TagRequest:
type: object
required: [version]
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
properties:
version: {type: string, example: V1R1C1}
target_oid:
type: string
pattern: '^[0-9a-fA-F]{40}$'
description: Full approved commit OID. The tag is created explicitly at this commit, never implicitly at workspace HEAD.
evidence_closure_oid:
type: string
pattern: '^[0-9a-fA-F]{40}$'
description: Required evidence closure commit. target_oid must be its ancestor.
approval_plan_id:
type: string
description: Audited owner-approved release or tag plan identifier.
message: {type: string}
execute:
type: boolean
@@ -103,7 +135,142 @@ components:
properties:
text: {type: string}
branch: {type: string}
DeploymentPlan:
type: object
required: [service, release_path, release_root, dropin_path, dropin_sha256, rollback_path, rollback_sha256, source_commit, artifact_sha256, manifest_sha256, policy_sha256, health_contract]
properties:
service: {type: string}
release_path: {type: string}
release_root: {type: string}
dropin_path: {type: string}
dropin_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
rollback_path: {type: string}
rollback_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
artifact_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
policy_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
health_contract: {type: object, additionalProperties: true}
ReleaseCandidateInput:
type: object
required: [candidate_id, source_commit, tree_oid, artifact_sha256, manifest_sha256]
properties:
candidate_id: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$'}
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
tree_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
artifact_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
tests: {type: object}
canary: {type: object}
rollback_ready: {type: boolean}
git_consistency: {type: object}
gitea_consistency: {type: object}
deployment_plan: {$ref: '#/components/schemas/DeploymentPlan'}
SignedReleasePlanApproval:
type: object
required: [release_plan_id, candidate_id, source_commit, tree_oid, artifact_sha256, manifest_sha256, deployment_plan_sha256, policy_sha256, approval_plan_id, evidence_closure_oid, owner_approval_record_oid, capabilities, expires_at, nonce, key_id, signature]
properties:
release_plan_id: {type: string}
candidate_id: {type: string}
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
tree_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
artifact_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
deployment_plan_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
policy_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
approval_plan_id: {type: string}
evidence_closure_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
owner_approval_record_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
capabilities:
type: array
uniqueItems: true
items:
type: string
enum: [runtime_switch, tag_publish, baseline_registry_finalize]
expires_at: {type: string, format: date-time}
nonce: {type: string, minLength: 16}
key_id: {type: string}
signature: {type: string, format: byte, writeOnly: true}
StartDeploymentRequest:
type: object
required: [candidate_id, approval]
properties:
candidate_id: {type: string}
approval: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
paths:
/createReleaseCandidate:
post:
operationId: createReleaseCandidate
summary: Persist an immutable release candidate and deployment plan; shadow-safe
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ReleaseCandidateInput'}
responses:
'200': {description: Candidate created or idempotently returned}
'400': {description: Invalid or conflicting candidate evidence}
'401': {description: Unauthorized}
x-openai-isConsequential: true
/getReleaseCandidate:
get:
operationId: getReleaseCandidate
summary: Read a durable release candidate
parameters:
- {in: query, name: candidate_id, required: true, schema: {type: string}}
responses:
'200': {description: Candidate}
'404': {description: Candidate not found}
/getApprovalEnvelope:
get:
operationId: getApprovalEnvelope
summary: Generate the immutable owner-signature payload
parameters:
- {in: query, name: candidate_id, required: true, schema: {type: string}}
responses:
'200': {description: Approval envelope fingerprint inputs}
'404': {description: Candidate not found}
/submitOwnerApproval:
post:
operationId: submitOwnerApproval
summary: Verify and durably store a signed owner release plan
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
responses:
'200': {description: Signature verified and approval stored}
'400': {description: Invalid approval}
'403': {description: Signature, key, expiry, or revocation failure}
'409': {description: Conflicting release plan}
x-openai-isConsequential: true
/startDeployment:
post:
operationId: startDeployment
summary: Start the approved Saga; default shadow mode returns no side effects
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/StartDeploymentRequest'}
responses:
'200': {description: Shadow result or deployment Saga result}
'403': {description: Execution disabled or approval invalid}
'409': {description: Saga conflict}
x-openai-isConsequential: true
/getReleaseSaga:
get:
operationId: getReleaseSaga
summary: Read durable Saga state and event journal
parameters:
- {in: query, name: candidate_id, required: true, schema: {type: string}}
responses:
'200': {description: Saga snapshot}
'404': {description: Saga not found}
/api/base:
get:
operationId: getBaseline
@@ -199,6 +366,39 @@ paths:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/tag-publication-status:
get:
operationId: getTagPublicationStatus
summary: Read durable publication Saga status without mutation
parameters:
- {in: query, name: version, required: true, schema: {type: string, pattern: '^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$'}}
responses:
'200':
description: Canonical publication state or fail-closed UNKNOWN
content:
application/json:
schema: {$ref: '#/components/schemas/PublicationStatus'}
'400': {description: Version required}
/api/release-consistency:
get:
operationId: getReleaseConsistency
summary: Compare Registry, Baseline, remote tag, Gitea API, listing, repository identity, and expected target
parameters:
- {in: query, name: version, required: true, schema: {type: string}}
- {in: query, name: target_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
- {in: query, name: evidence_closure_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
- {in: query, name: approval_plan_id, required: true, schema: {type: string}}
responses: {'200': {description: PASS, FAIL, or fail-closed UNKNOWN}}
/api/release-acceptance:
get:
operationId: runReleaseAcceptance
summary: Run read-only release acceptance; UNKNOWN is not accepted
parameters:
- {in: query, name: version, required: true, schema: {type: string}}
- {in: query, name: target_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
- {in: query, name: evidence_closure_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
- {in: query, name: approval_plan_id, required: true, schema: {type: string}}
responses: {'200': {description: Acceptance result with accepted boolean}}
/api/tag:
post:
operationId: requestOrCreateTag
@@ -209,7 +409,7 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
responses: {'200': {description: Gitea-confirmed tag and fail-closed baseline result}, '400': {description: Missing governance evidence or invalid OID}, '403': {description: Forbidden}, '409': {description: Existing tag target conflict or evidence lineage mismatch}, '502': {description: Gitea reconciliation required}, '503': {description: Remote push pending}}
x-openai-isConsequential: true
/api/rollback:
post:
@@ -245,4 +445,7 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/NLRequest'}
responses: {'
responses:
'200': {description: Safe GitOps plan}
'401': {description: Unauthorized}
x-openai-isConsequential: true
+56
View File
@@ -0,0 +1,56 @@
# V2R1C1 中央 Baseline 治理收口
## 收口范围
本次变更只登记已经完成生产验证并上线的 `V2R1C1`。它新增中央版本 Manifest、把中央 Baseline Pointer 从 `V1R4C6` 切换为 `V2R1C1`,并建立 Release、Companion、Artifact 与最终生产 Evidence 的追溯关系。
本次不修改产品实现,不移动或重建 `V2R1C1` Tag,不改变 Release OID,不重新部署生产,不切换生产流量,也不重写历史证据。
## 权威标识
- Release:`release/AIOS-V2R1C1-20260721`
- Release OID:`4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`
- Tag / Tag OID:`V2R1C1` / `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`
- SemVer:`2.1.0-rc.1`
- OpenClaw Companion:`2.1.0-rc.4`
- Companion Source OID:`83f8f878c7da5dd8263f2af2efa80750492e5cb3`
- Companion OID:`12d1c13170e0c5a691d67a9408e739e7a17064a3`
- Artifact SHA256:`aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`
- Contract:`aios.openclaw.command.v2` / `Ed25519`
- Evidence Root:`/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`
- Final Audit Manifest SHA256:`2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`
- Production Status:`PRODUCTION_DEPLOYED`
## 治理路径
- Governance Branch:`patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`
- Base Ref:`refs/heads/main`
- Base OID:`3cf9958cb94614449e093639cbf902216d73447f`
- Manifest Commit:`a56caf46f6bc335ec2395b674ead4b2a427c5edd`
- Baseline Commit:`6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`
- Reviewer:`gpt-5.3-codex-spark validator and docs_reporter`
- Review:`PASS`(`2026-07-21T14:23:00Z`)
## 命令与退出码
| 阶段 | 命令 | 开始 UTC | 结束 UTC | Exit Code |
| --- | --- | --- | --- | ---: |
| 中央只读核对 | `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
| 创建治理分支 | `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
| 核心元数据验证 | `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
完整机读记录位于 `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`,治理运行证据位于 `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`。
## 元数据回滚
若中央 Registry 解析或治理服务异常,保留 `versions/V2R1C1.json`,通过新的受审治理提交把 `baseline/current.json` 和 `VERSION` 临时恢复到 `V1R4C6`。该操作不得回滚生产系统、移动 Tag、修改 Release OID 或重写历史。
## 范围证明
```text
product_code_changed=false
tag_changed=false
production_redeployed=false
production_traffic_changed=false
secret_material_recorded=false
```
@@ -0,0 +1,35 @@
# V2R1C2 Production Release
- Release Run ID: `AIOS-V2R1C2-PRODUCTION-20260722T011836Z`
- Previous production baseline: `V2R1C1`
- Tag: `V2R1C2`
- Tag object: `a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36`
- Approved target: `3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24`
- Evidence closure: `8cdca463aff9707edee00e20a3b56884130f6978`
- Initial registry status: `TAG_CREATED_NOT_PROMOTED`
- Final status: `PRODUCTION_DEPLOYED`
- Production service: `aios-v2r1c2-production.service`
- Production port: `18920`
- Production acceptance SHA256: `a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a`
- Production online smoke SHA256: `fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4`
- Production health SHA256: `ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c`
- Live evidence directory: `/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z`
- Evidence hash index: `/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z/production-evidence.sha256`
## Production gates
Promotion may proceed only when the 18921 candidate remains healthy, the committed evidence checksum file verifies without drift, the 18920 baseline is healthy, required secret references are available without exposing secret values, and the V2R1C1 rollback service/configuration remains intact.
The obsolete release-plan `DECLARED` rows are not authoritative for this run. The production gate must explicitly select `AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER-20260722` and require its three required stages to be `SUCCEEDED`; an unqualified aggregate across stale plan rows fails closed.
## Synthetic WeChat scope
WeChat-facing acceptance for this release used synthetic/mock receipts and transcripts only. It did not impersonate a real user and does not claim a real-user WeChat session. The relevant committed evidence entries are `wechat_simulated_receipts` and `wechat_simulated_transcript`; the production online smoke also records `simulation: true` and `real_user_impersonated: false`.
## Rollback rule
If Health, API, UI, Role Matrix, Tenant Isolation, Runtime, or Audit verification fails after the 18920 switch, stop the V2R1C2 production unit and restart the preserved V2R1C1 18920 service. Do not delete or rewrite either release tag or rollback asset.
## Result
The production switch completed at `2026-07-22T01:40:51Z`. Full acceptance and the production online smoke passed, including Health, API, UI, Role Matrix, Tenant Isolation, Runtime, Audit and the selected takeover release gate. The previous `aios-orchestrator-gateway.service` unit, configuration and Release Run backup remain available for rollback.
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
set -Eeuo pipefail
VERSION=V2R1C2
EXPECTED_TARGET=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
TAG_OBJECT=a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36
REPO=/data/aios-gitops/repo.git
SOURCE=/opt/aios-v2r1c2
TAKEOVER=/data/aios-gitops/worktrees/codex/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER-20260722
CANDIDATE_SERVICE=aios-v2r1c2-portal.service
OLD_SERVICE=aios-orchestrator-gateway.service
NEW_SERVICE=aios-v2r1c2-production.service
CANDIDATE_STATE=/var/lib/aios-v2r1c2-portal
PRODUCTION_STATE=/var/lib/aios-v2r1c2-production
RUN_ID=${AIOS_RELEASE_RUN_ID:-AIOS-V2R1C2-PRODUCTION-20260722T011836Z}
EVIDENCE_DIR=/var/lib/aios-gitops/acceptance/$RUN_ID
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
RELEASE_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
UNIT_SOURCE=$RELEASE_ROOT/deploy/aios-v2/systemd/aios-v2r1c2-production.service
ENV_SOURCE=$RELEASE_ROOT/deploy/aios-v2/systemd/aios-v2r1c2-production.env.example
LOCK=/run/lock/aios-v2r1c2-production-release.lock
require_root() {
test "$(id -u)" -eq 0 || { echo 'root required' >&2; exit 77; }
}
health_old() {
curl -fsS --max-time 10 http://127.0.0.1:18920/healthz
}
health_new() {
curl -fsS --max-time 10 http://127.0.0.1:18920/api/v2/health
}
health_candidate() {
curl -fsS --max-time 10 http://127.0.0.1:18921/api/v2/health
}
wait_candidate() {
for _ in $(seq 1 30); do
if health_candidate; then
return 0
fi
sleep 1
done
return 1
}
preflight() {
require_root
test "$(git --git-dir="$REPO" rev-parse refs/tags/$VERSION)" = "$TAG_OBJECT"
test "$(git --git-dir="$REPO" rev-list -n 1 "$VERSION")" = "$EXPECTED_TARGET"
test -f "$SOURCE/active-runtime/app/aios_web/release_application.py"
test -f "$UNIT_SOURCE"
test -f "$ENV_SOURCE"
command -v sqlite3 >/dev/null
systemctl is-active --quiet "$CANDIDATE_SERVICE"
systemctl is-active --quiet "$OLD_SERVICE"
health_candidate >/dev/null
health_old >/dev/null
(
cd "$TAKEOVER/artifacts/v2r1c2"
sha256sum -c checksums.sha256 >/dev/null
)
diff -qr --exclude=__pycache__ "$TAKEOVER/active-runtime/app" "$SOURCE/active-runtime/app" >/dev/null
diff -qr "$TAKEOVER/plans" "$SOURCE/plans" >/dev/null
echo PREFLIGHT=PASS
}
prepare() {
preflight
install -d -m 0750 "$EVIDENCE_DIR" "$EVIDENCE_DIR/backup"
health_old >"$EVIDENCE_DIR/production-health-before.json"
health_candidate >"$EVIDENCE_DIR/candidate-health-before.json"
systemctl cat "$OLD_SERVICE" >"$EVIDENCE_DIR/backup/$OLD_SERVICE.unit"
cp -a /etc/aios-orchestrator-gateway "$EVIDENCE_DIR/backup/"
cp -a /etc/systemd/system/aios-orchestrator-gateway.service "$EVIDENCE_DIR/backup/"
if test -e "$PRODUCTION_STATE"; then
mv "$PRODUCTION_STATE" "$EVIDENCE_DIR/backup/aios-v2r1c2-production-state.preexisting"
fi
systemctl stop "$CANDIDATE_SERVICE"
trap 'systemctl start "$CANDIDATE_SERVICE" || true' EXIT
while IFS= read -r -d '' database; do
sqlite3 "$database" 'PRAGMA wal_checkpoint(TRUNCATE);' >/dev/null
test "$(sqlite3 "$database" 'PRAGMA quick_check;')" = ok
done < <(find "$CANDIDATE_STATE" -maxdepth 1 -type f -name '*.sqlite3' -print0)
install -d -o aios -g aios -m 0750 "$PRODUCTION_STATE"
cp -a "$CANDIDATE_STATE/." "$PRODUCTION_STATE/"
chown -R aios:aios "$PRODUCTION_STATE"
while IFS= read -r -d '' database; do
test "$(sqlite3 "$database" 'PRAGMA quick_check;')" = ok
done < <(find "$PRODUCTION_STATE" -maxdepth 1 -type f -name '*.sqlite3' -print0)
install -o root -g root -m 0644 "$UNIT_SOURCE" /etc/systemd/system/aios-v2r1c2-production.service
install -d -o root -g aios -m 0750 /etc/aios-v2
install -o root -g aios -m 0640 "$ENV_SOURCE" /etc/aios-v2/aios-v2r1c2-production.env
systemctl daemon-reload
systemctl start "$CANDIDATE_SERVICE"
wait_candidate >"$EVIDENCE_DIR/candidate-health-after-state-copy.json"
trap - EXIT
echo PREPARE=PASS
}
rollback() {
require_root
systemctl stop "$NEW_SERVICE" || true
systemctl disable "$NEW_SERVICE" >/dev/null 2>&1 || true
systemctl enable "$OLD_SERVICE" >/dev/null 2>&1 || true
systemctl start "$OLD_SERVICE"
health_old >"$EVIDENCE_DIR/rollback-health.json"
echo ROLLBACK=PASS
}
promote() {
require_root
test -f /etc/systemd/system/aios-v2r1c2-production.service
test -f /etc/aios-v2/aios-v2r1c2-production.env
! systemctl is-active --quiet "$NEW_SERVICE"
systemctl is-active --quiet "$CANDIDATE_SERVICE"
health_candidate >/dev/null
health_old >"$EVIDENCE_DIR/production-health-switch-input.json"
trap 'trap - ERR; rollback' ERR
systemctl stop "$OLD_SERVICE"
systemctl start "$NEW_SERVICE"
for _ in $(seq 1 30); do
if health_new >"$EVIDENCE_DIR/production-health-after-switch.json" 2>/dev/null; then
echo PROMOTE=PASS
trap - ERR
return 0
fi
sleep 1
done
return 1
}
finalize() {
require_root
systemctl is-active --quiet "$NEW_SERVICE"
health_new >"$EVIDENCE_DIR/production-health-final.json"
systemctl disable "$OLD_SERVICE" >/dev/null
systemctl enable "$NEW_SERVICE" >/dev/null
echo FINALIZE=PASS
}
exec 9>"$LOCK"
flock -n 9 || { echo 'release operation already running' >&2; exit 75; }
case "${1:-}" in
preflight) preflight ;;
prepare) prepare ;;
promote) promote ;;
rollback) rollback ;;
finalize) finalize ;;
*) echo "usage: $0 {preflight|prepare|promote|rollback|finalize}" >&2; exit 64 ;;
esac
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -Eeuo pipefail
RUN_ID=AIOS-V2R1C2-PRODUCTION-20260722T011836Z
CONTROL=/data/aios-gitops/worktrees/controller/ops/aios-v2/v2r1c2-production-control.sh
REPO=/data/aios-gitops/repo.git
EVIDENCE_DIR=/var/lib/aios-gitops/acceptance/$RUN_ID
DRIVER=/data/aios-gitops/worktrees/controller/tools/run_online_smoke_18920.py
TEMP_SOURCE=
cleanup_temp() {
case "$TEMP_SOURCE" in
/tmp/aios-v2r1c2-production-acceptance.*)
test -d "$TEMP_SOURCE" && rm -rf -- "$TEMP_SOURCE"
;;
'') ;;
*) echo "refusing to remove unexpected temporary path: $TEMP_SOURCE" >&2; return 1 ;;
esac
}
rollback_on_error() {
code=$?
trap - ERR
AIOS_RELEASE_RUN_ID="$RUN_ID" "$CONTROL" rollback || true
cleanup_temp || true
exit "$code"
}
trap rollback_on_error ERR
AIOS_RELEASE_RUN_ID="$RUN_ID" "$CONTROL" promote
systemctl is-active --quiet aios-v2r1c2-production.service
! systemctl is-active --quiet aios-orchestrator-gateway.service
curl -fsS --max-time 10 http://127.0.0.1:18920/api/v2/health >"$EVIDENCE_DIR/health-after-promote.json"
curl -fsS --max-time 10 http://127.0.0.1:18921/api/v2/health >"$EVIDENCE_DIR/candidate-health-during-production.json"
TEMP_SOURCE=$(mktemp -d /tmp/aios-v2r1c2-production-acceptance.XXXXXX)
git --git-dir="$REPO" archive 8cdca463aff9707edee00e20a3b56884130f6978 | tar -x -C "$TEMP_SOURCE"
(
cd "$TEMP_SOURCE"
python3 tools/aios_v2r1c2_acceptance.py \
--source-oid 3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24 \
--online \
--base-url http://127.0.0.1:18920 \
--evidence-output "$EVIDENCE_DIR/production-acceptance.json"
)
python3 "$DRIVER" "$TEMP_SOURCE" "$EVIDENCE_DIR/production-online-smoke.json"
python3 - "$EVIDENCE_DIR/production-acceptance.json" "$EVIDENCE_DIR/production-online-smoke.json" <<'PY'
import json
import sys
for path in sys.argv[1:]:
with open(path, encoding='utf-8') as handle:
data = json.load(handle)
if data.get('passed') is not True:
raise SystemExit(f'acceptance failed: {path}')
PY
systemctl show aios-v2r1c2-production.service \
-p ActiveState -p SubState -p MainPID -p NRestarts \
>"$EVIDENCE_DIR/production-service-status.txt"
sha256sum "$EVIDENCE_DIR"/production-acceptance.json \
"$EVIDENCE_DIR"/production-online-smoke.json \
"$EVIDENCE_DIR"/health-after-promote.json \
>"$EVIDENCE_DIR/production-evidence.sha256"
cleanup_temp
TEMP_SOURCE=
trap - ERR
echo SWITCH_VALIDATED=PASS
@@ -0,0 +1,15 @@
{
"patch_name": "gitea-tag-publication-atomicity",
"branch": "patch/gitea-tag-publication-atomicity-20260723",
"base": "main",
"risk": "high",
"description": "Fail-closed, resumable Gitea release-tag publication with exact remote ref, peeled target, API and listing verification before baseline update.",
"owner_approval": "received",
"constraints": {
"force_push": false,
"tag_rewrite": false,
"direct_main_push": false,
"secret_commit": false,
"production_runtime_anchor_change": false
}
}
@@ -0,0 +1,9 @@
{
"id": "patch-1784043007770-e97206c3",
"patch_name": "gitops-isolated-worktree-governance-20260714T232948UTC8",
"branch": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
"risk": "high",
"description": "Root-cause remediation for recurring ACL and shared-worktree failures: unified aiman:gitops runtime identity, UMask=0002, inherited default ACLs, per-task isolated Git worktrees, startup preflight, migration/rollback scripts, systemd drop-in templates, audit evidence, and an OpenClaw execution task. No production business deployment in this patch.",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-14T15:30:07Z"
}
+10
View File
@@ -0,0 +1,10 @@
{
"id": "nl-1784681185786-19ac0f82",
"text": "Create a safe GitOps plan for tag V2R1C2. The tag target must be commit 3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24. Evidence closure commit is 8cdca463aff9707edee00e20a3b56884130f6978. Do not tag the current workspace HEAD. Do not modify main. Do not force push. Do not promote production. If the target commit cannot be bound explicitly, keep the operation blocked.",
"intent": "unknown",
"action_plan": [],
"execute": false,
"owner_approval_required": true,
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-22T00:46:25Z"
}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/gitea-tag-publication-atomicity-20260723",
"base": "main",
"created_by": "owner-gitea-tag-atomicity-20260723",
"created_at": "2026-07-23T04:59:13Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "task/codex/gitops-isolated-worktree-migration-20260715T001652UTC8",
"base": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
"created_by": "chatgpt-gitops-bot",
"created_at": "2026-07-14T16:17:28Z",
"direct_main_push": false,
"owner_approval_required": true
}
@@ -0,0 +1,43 @@
{
"base": "main",
"base_oid": "b739fbb5ec8f7096d2f5ade87f65a7c74b0ee870",
"commit_count": 10,
"commits": [
"fc0b9f29bf6dadfbcc8599da6f0ab11a6e19b529",
"3812e1ab0123c7da4726e51179a10746b5bbde83",
"3196d91b12d0ac5d7ea550b0474703a08736471b",
"54093d15437ce0af31acf6feeeffacf6626a59cd",
"27a0ae126b96d63c40544174e7a3859e30eba056",
"83ab67f7e11eae9da4546cab53169f5a6a47e0ae",
"f4b6ac8609faf947d2659b2c32564a26fcb3eb10",
"a3edc628a91a4af3d702aacefe0389f844615bc1",
"e40c05779c435c3726618de81e02e90953d727f3",
"edfad05d57fb3d23d8ab19d27bb6a43a7f2a298f"
],
"file_count": 15,
"files": [
"active-runtime/app/aios_chatgpt_actions_api.py",
"active-runtime/app/aios_gitops_write_guard.py",
"active-runtime/app/gitea_tag_publication.py",
"docs/gitops/gitea-tag-publication-atomicity.md",
"docs/gitops/openapi-chatgpt-actions.yaml",
"patches/gitea-tag-publication-atomicity-20260723.json",
"patches/patch_gitea-tag-publication-atomicity-20260723.branch.json",
"scripts/generate_delivery_manifest.py",
"scripts/release_version.sh",
"scripts/verify_gitea_credential_rotation.py",
"security/incidents/GITEA-TOKEN-33-EXPOSURE-20260723.redacted.json",
"tests/run_live_gitea_fixture.sh",
"tests/test_chatgpt_actions_tag.py",
"tests/test_gitea_tag_publication.py",
"tests/test_gitea_tag_publication_live.py"
],
"generated_from": {
"commit_range": "b739fbb5ec8f7096d2f5ade87f65a7c74b0ee870..edfad05d57fb3d23d8ab19d27bb6a43a7f2a298f",
"commits_command": "git log --reverse --format=%H b739fbb5ec8f7096d2f5ade87f65a7c74b0ee870..edfad05d57fb3d23d8ab19d27bb6a43a7f2a298f",
"files_command": "git diff --name-only b739fbb5ec8f7096d2f5ade87f65a7c74b0ee870..edfad05d57fb3d23d8ab19d27bb6a43a7f2a298f"
},
"manifest_path": "reports/gitea-tag-publication-atomicity-delivery.json",
"source_branch": "patch/gitea-tag-publication-atomicity-20260723",
"source_commit": "edfad05d57fb3d23d8ab19d27bb6a43a7f2a298f"
}
-145
View File
@@ -1,145 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AIOS Orchestrator Decision",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"project_key",
"contract_commit",
"event_id",
"expected_state_version",
"verdict",
"state_updates",
"next_work_units",
"requires_human_input",
"stop_reason"
],
"properties": {
"schema_version": {
"type": "integer",
"const": 1
},
"project_key": {
"type": "string"
},
"contract_commit": {
"type": "string",
"pattern": "^[0-9a-f]{40}$"
},
"event_id": {
"type": "string"
},
"expected_state_version": {
"type": "integer",
"minimum": 0
},
"verdict": {
"type": "string",
"enum": [
"BLOCKED",
"COMPLETE",
"DISPATCH",
"NOOP"
]
},
"state_updates": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"key",
"value"
],
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"next_work_units": {
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"track_id",
"gate_id",
"primary_agent",
"fallback_agent",
"clean_base_commit",
"allowed_paths",
"required_checks",
"forbidden_operations",
"instruction"
],
"properties": {
"id": {
"type": "string"
},
"track_id": {
"type": "string"
},
"gate_id": {
"type": "string"
},
"primary_agent": {
"type": "string",
"enum": [
"openclaw",
"codex"
]
},
"fallback_agent": {
"type": "string",
"enum": [
"openclaw",
"codex"
]
},
"clean_base_commit": {
"type": "string",
"pattern": "^[0-9a-f]{40}$"
},
"allowed_paths": {
"type": "array",
"items": {
"type": "string"
}
},
"required_checks": {
"type": "array",
"items": {
"type": "string"
}
},
"forbidden_operations": {
"type": "array",
"items": {
"type": "string"
}
},
"instruction": {
"type": "string"
}
}
}
},
"requires_human_input": {
"type": "boolean"
},
"stop_reason": {
"type": [
"string",
"null"
]
}
}
}
-102
View File
@@ -1,102 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AIOS Orchestrator Event",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"project_key",
"contract_commit",
"event_id",
"source_agent",
"event_type",
"work_unit_id",
"status",
"state_version",
"implementation_commit",
"result_commit",
"blocker",
"evidence_refs",
"payload"
],
"properties": {
"schema_version": {
"const": 1
},
"project_key": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"contract_commit": {
"type": "string",
"pattern": "^[0-9a-f]{40}$"
},
"event_id": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"source_agent": {
"enum": [
"openclaw",
"codex",
"gitea",
"system",
"workspace-agent"
]
},
"event_type": {
"type": "string"
},
"work_unit_id": {
"type": "string"
},
"status": {
"enum": [
"READY",
"RUNNING",
"PASS",
"BLOCKED",
"FAIL",
"TIMEOUT",
"NOT_EXECUTED"
]
},
"state_version": {
"type": "integer",
"minimum": 0
},
"implementation_commit": {
"type": [
"string",
"null"
],
"pattern": "^[0-9a-f]{40}$"
},
"result_commit": {
"type": [
"string",
"null"
],
"pattern": "^[0-9a-f]{40}$"
},
"blocker": {
"type": [
"string",
"null"
],
"maxLength": 512
},
"evidence_refs": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 64
},
"payload": {
"type": "object"
}
}
}
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env python3
"""Seed exactly one orchestration event when the project has no durable state."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
from services.aios_orchestrator_gateway.config import Config
from services.aios_orchestrator_gateway.models import Event
from services.aios_orchestrator_gateway.securefs import atomic_write_bytes_shared
from services.aios_orchestrator_gateway.store import Store
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, required=True)
args = parser.parse_args()
config = Config.from_file(args.config)
store = Store(config.database_path)
if store.project_state(config.project_key) is not None:
print("ORCHESTRATION_BOOTSTRAP=NOOP reason=STATE_EXISTS")
return 0
event = Event.from_dict(
{
"schema_version": 1,
"project_key": config.project_key,
"contract_commit": config.contract_commit,
"event_id": "bootstrap-" + config.contract_commit[:24],
"source_agent": "system",
"event_type": "orchestration_bootstrap",
"work_unit_id": "ORCHESTRATION-BOOTSTRAP",
"status": "READY",
"state_version": 0,
"implementation_commit": None,
"result_commit": None,
"blocker": None,
"evidence_refs": [],
"payload": {
"reason": "initial_gateway_install",
"polling_enabled": True,
"openclaw_sync_required": True,
},
}
)
path = config.event_inbox_dir / "pending" / f"{event.event_id}.json"
atomic_write_bytes_shared(
path,
(event.canonical_json() + "\n").encode(),
mode=0o640,
group_writable_directory=True,
)
print(f"ORCHESTRATION_BOOTSTRAP=PASS event_id={event.event_id}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Generate the delivery file list from Git instead of hand-maintained counts."""
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
def git(root: Path, *args: str) -> list[str]:
output = subprocess.check_output(
["git", *args], cwd=root, text=True, encoding="utf-8"
)
return [line for line in output.splitlines() if line]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base", default="main")
parser.add_argument(
"--output",
default="reports/gitea-tag-publication-atomicity-delivery.json",
)
args = parser.parse_args()
root = Path(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True, encoding="utf-8"
).strip()
)
output = Path(args.output)
if output.is_absolute() or ".." in output.parts:
raise SystemExit("output must remain inside the repository")
output_path = root / output
output_path.parent.mkdir(parents=True, exist_ok=True)
base_oid = git(root, "rev-parse", "--verify", f"{args.base}^{{commit}}")[0]
source_commit = git(root, "rev-parse", "--verify", "HEAD^{commit}")[0]
source_branch = git(root, "symbolic-ref", "--short", "HEAD")[0]
commit_range = f"{base_oid}..{source_commit}"
commits = git(root, "log", "--reverse", "--format=%H", commit_range)
files = [
path
for path in git(root, "diff", "--name-only", commit_range)
if path != output.as_posix()
]
payload = {
"base": args.base,
"base_oid": base_oid,
"source_branch": source_branch,
"source_commit": source_commit,
"commit_count": len(commits),
"commits": commits,
"file_count": len(files),
"files": files,
"manifest_path": output.as_posix(),
"generated_from": {
"commit_range": commit_range,
"files_command": f"git diff --name-only {commit_range}",
"commits_command": f"git log --reverse --format=%H {commit_range}",
},
}
output_path.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
SOURCE="${AIOS_SOURCE_WORKTREE:-$ROOT/workspace}"
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
if [[ "$(id -u)" -ne 0 ]]; then
echo "ERROR: bootstrap must run as root" >&2
exit 2
fi
getent passwd "$RUNTIME_USER" >/dev/null
getent group "$RUNTIME_GROUP" >/dev/null
[[ -d "$SOURCE/.git" || -f "$SOURCE/.git" ]] || {
echo "ERROR: source worktree not found: $SOURCE" >&2
exit 3
}
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 "$ROOT" "$WORKTREES"
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 \
"$WORKTREES/openclaw" "$WORKTREES/chatgpt" "$WORKTREES/controller"
if [[ ! -d "$BARE" ]]; then
sudo -u "$RUNTIME_USER" -g "$RUNTIME_GROUP" git clone --bare "$SOURCE" "$BARE"
fi
chown -R "$RUNTIME_USER:$RUNTIME_GROUP" "$BARE" "$WORKTREES"
find "$BARE" "$WORKTREES" -type d -exec chmod 2775 {} +
find "$BARE" "$WORKTREES" -type f -exec chmod ug+rw {} +
setfacl -R -m "u:${RUNTIME_USER}:rwX,g:${RUNTIME_GROUP}:rwX,m::rwX" "$BARE" "$WORKTREES"
find "$BARE" "$WORKTREES" -type d -exec \
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
cat <<EOF
bootstrap_status: PASS
root: $ROOT
source_worktree: $SOURCE
bare_repo: $BARE
worktree_root: $WORKTREES
runtime_user: $RUNTIME_USER
runtime_group: $RUNTIME_GROUP
EOF
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
EXPECTED_USER="${AIOS_RUNTIME_USER:-aiman}"
EXPECTED_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
EXPECTED_UMASK="${AIOS_RUNTIME_UMASK:-0002}"
TASK_DIR="${AIOS_TASK_DIR:-.agent/tasks}"
RESULT_DIR="${AIOS_RESULT_DIR:-.agent/results}"
fail() {
echo "preflight_status: FAIL"
echo "reason: $1"
exit 1
}
USER_NOW="$(id -un)"
GROUPS_NOW="$(id -Gn)"
UMASK_NOW="$(umask)"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
BRANCH="$(git branch --show-current 2>/dev/null || true)"
[[ "$USER_NOW" == "$EXPECTED_USER" ]] || fail "runtime user is $USER_NOW, expected $EXPECTED_USER"
[[ " $GROUPS_NOW " == *" $EXPECTED_GROUP "* ]] || fail "runtime user is not in $EXPECTED_GROUP"
[[ "$UMASK_NOW" == "$EXPECTED_UMASK" || "$UMASK_NOW" == "002" ]] || fail "umask is $UMASK_NOW, expected $EXPECTED_UMASK"
[[ -n "$ROOT" ]] || fail "not inside a git worktree"
[[ "$ROOT" == /data/aios-gitops/worktrees/* ]] || fail "shared source worktree is prohibited: $ROOT"
[[ -w "$ROOT" ]] || fail "worktree root is not writable"
[[ -w "$GIT_DIR" ]] || fail "git dir is not writable: $GIT_DIR"
[[ ! -e "$GIT_DIR/index.lock" ]] || fail "git index lock exists"
install -d -m 2775 "$TASK_DIR" "$RESULT_DIR"
[[ -w "$TASK_DIR" ]] || fail "task directory is not writable: $TASK_DIR"
[[ -w "$RESULT_DIR" ]] || fail "result directory is not writable: $RESULT_DIR"
[[ -z "$(git status --porcelain)" ]] || fail "worktree is not clean at task start"
cat <<EOF
preflight_status: PASS
runtime_user: $USER_NOW
runtime_groups: $GROUPS_NOW
umask: $UMASK_NOW
worktree: $ROOT
git_dir: $GIT_DIR
branch: $BRANCH
workspace_clean: true
task_path_writable: true
result_path_writable: true
git_lock_present: false
EOF
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
ACTION="${1:-}"
AGENT="${2:-}"
TASK_ID="${3:-}"
BASE_REF="${4:-origin/main}"
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
usage() {
echo "usage: $0 create|remove <openclaw|chatgpt|controller> <task-id> [base-ref]" >&2
exit 64
}
[[ "$ACTION" =~ ^(create|remove)$ ]] || usage
[[ "$AGENT" =~ ^(openclaw|chatgpt|controller)$ ]] || usage
[[ "$TASK_ID" =~ ^[A-Za-z0-9._-]+$ ]] || {
echo "ERROR: invalid task id" >&2
exit 65
}
[[ "$(id -un)" == "$RUNTIME_USER" ]] || {
echo "ERROR: must run as $RUNTIME_USER" >&2
exit 66
}
[[ "$(id -gn)" == "$RUNTIME_GROUP" || " $(id -Gn) " == *" $RUNTIME_GROUP "* ]] || {
echo "ERROR: user must be in $RUNTIME_GROUP" >&2
exit 67
}
[[ "$(umask)" == "0002" || "$(umask)" == "002" ]] || {
echo "ERROR: umask must be 0002" >&2
exit 68
}
[[ -d "$BARE" ]] || {
echo "ERROR: bare repository missing: $BARE" >&2
exit 69
}
PATHNAME="$WORKTREES/$AGENT/$TASK_ID"
BRANCH="task/$AGENT/$TASK_ID"
case "$ACTION" in
create)
[[ ! -e "$PATHNAME" ]] || {
echo "ERROR: worktree already exists: $PATHNAME" >&2
exit 70
}
git --git-dir="$BARE" fetch --all --prune
git --git-dir="$BARE" worktree add -b "$BRANCH" "$PATHNAME" "$BASE_REF"
install -d -m 2775 "$PATHNAME/.agent/tasks/$AGENT" "$PATHNAME/.agent/results/$AGENT"
setfacl -m "u:${RUNTIME_USER}:rwx,g:${RUNTIME_GROUP}:rwx,m::rwx" "$PATHNAME"
find "$PATHNAME" -type d -exec chmod g+s {} +
find "$PATHNAME" -type d -exec \
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
printf 'worktree: %s\nbranch: %s\nbase: %s\n' "$PATHNAME" "$BRANCH" "$BASE_REF"
;;
remove)
[[ -d "$PATHNAME" ]] || {
echo "ERROR: worktree not found: $PATHNAME" >&2
exit 71
}
[[ -z "$(git -C "$PATHNAME" status --porcelain)" ]] || {
echo "ERROR: worktree is not clean; retain for investigation" >&2
exit 72
}
git --git-dir="$BARE" worktree remove "$PATHNAME"
printf 'removed: %s\n' "$PATHNAME"
;;
esac
@@ -1,239 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 0077
SOURCE_ROOT="${SOURCE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
INSTALL_ROOT="${INSTALL_ROOT:-/opt/aios-orchestrator-gateway}"
CONFIG_ROOT="${CONFIG_ROOT:-/etc/aios-orchestrator-gateway}"
STATE_ROOT="${STATE_ROOT:-/var/lib/aios-orchestrator-gateway}"
UNIT_ROOT="${UNIT_ROOT:-/etc/systemd/system}"
SYSUSERS_ROOT="${SYSUSERS_ROOT:-/etc/sysusers.d}"
TMPFILES_ROOT="${TMPFILES_ROOT:-/etc/tmpfiles.d}"
DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=1
fi
fail() {
printf 'AIOS_ORCHESTRATOR_INSTALL=BLOCKED reason=%s\n' "$1" >&2
exit 2
}
[[ "$(id -u)" -eq 0 ]] || fail OWNER_INSTALL_REQUIRED
for cmd in git python3 systemd-analyze systemctl systemd-sysusers systemd-tmpfiles sha256sum find sort xargs; do
command -v "$cmd" >/dev/null 2>&1 || fail "MISSING_COMMAND_${cmd}"
done
if [[ -f "$SOURCE_ROOT/SHA256SUMS" ]]; then
(cd "$SOURCE_ROOT" && sha256sum --check SHA256SUMS)
fi
for path in \
"$SOURCE_ROOT/services/aios_orchestrator_gateway" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles" \
"$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json"; do
[[ -e "$path" ]] || fail "MISSING_SOURCE_${path}"
done
PYCACHE="/tmp/aios-orchestrator-install-pycache-$$"
rm -rf "$PYCACHE"
PYTHONPYCACHEPREFIX="$PYCACHE" python3 -m compileall -q "$SOURCE_ROOT/services" "$SOURCE_ROOT/scripts"
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH="$SOURCE_ROOT" \
python3 -m unittest -v tests.test_aios_orchestrator_gateway
rm -rf "$PYCACHE"
for script in "$SOURCE_ROOT"/scripts/*.sh; do bash -n "$script"; done
python3 -m json.tool "$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json" >/dev/null
systemd-analyze verify \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer"
SOURCE_FILES=(
README-AIOS-ORCHESTRATOR-GATEWAY.md
config/aios-orchestrator-gateway.example.json
schemas/decision.schema.json
schemas/event.schema.json
scripts/bootstrap_orchestration_loop.py
scripts/install_aios_orchestrator_gateway.sh
scripts/report_orchestrator_event.py
scripts/rollback_aios_orchestrator_gateway.sh
scripts/verify_aios_orchestrator_gateway.sh
systemd/aios-orchestrator-gateway-poll.service
systemd/aios-orchestrator-gateway-poll.timer
systemd/aios-orchestrator-gateway.service
systemd/aios-orchestrator-gateway.sysusers
systemd/aios-orchestrator-gateway.tmpfiles
tests/test_aios_orchestrator_gateway.py
tests/test_aios_orchestrator_gateway_rollback.sh
workspace-agent/CENTRAL_AGENT_INSTRUCTIONS.md
workspace-agent/orchestrator-decision-action.openapi.yaml
)
while IFS= read -r path; do SOURCE_FILES+=("$path"); done < <(
cd "$SOURCE_ROOT"
find services/aios_orchestrator_gateway -type f ! -path '*/__pycache__/*' ! -name '*.pyc' -print | sort
)
for path in "${SOURCE_FILES[@]}"; do
[[ -f "$SOURCE_ROOT/$path" && ! -L "$SOURCE_ROOT/$path" ]] || fail "INVALID_SOURCE_FILE_${path}"
done
SOURCE_DIGEST="$(
cd "$SOURCE_ROOT"
printf '%s\0' "${SOURCE_FILES[@]}" | sort -z | xargs -0 sha256sum \
| sha256sum \
| awk '{print $1}'
)"
[[ "$SOURCE_DIGEST" =~ ^[0-9a-f]{64}$ ]] || fail SOURCE_DIGEST_INVALID
RELEASE_DIR="$INSTALL_ROOT/releases/$SOURCE_DIGEST"
if [[ "$DRY_RUN" -eq 1 ]]; then
printf 'AIOS_ORCHESTRATOR_INSTALL_DRY_RUN=PASS digest=%s release=%s\n' \
"$SOURCE_DIGEST" "$RELEASE_DIR"
exit 0
fi
install -d -m 0755 "$INSTALL_ROOT/releases"
install -d -m 0700 "$CONFIG_ROOT"
install -d -m 0700 "$STATE_ROOT"
systemd-sysusers "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers"
systemd-tmpfiles --create "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles"
if [[ ! -d "$RELEASE_DIR" ]]; then
TMP_RELEASE="$INSTALL_ROOT/releases/.tmp-$SOURCE_DIGEST-$$"
install -d -m 0755 "$TMP_RELEASE"
for path in "${SOURCE_FILES[@]}"; do
install -d -m 0755 "$TMP_RELEASE/$(dirname "$path")"
cp -a "$SOURCE_ROOT/$path" "$TMP_RELEASE/$path"
done
printf '%s\n' "$SOURCE_DIGEST" > "$TMP_RELEASE/SOURCE_DIGEST"
chmod -R go-w "$TMP_RELEASE"
mv "$TMP_RELEASE" "$RELEASE_DIR"
fi
PREVIOUS_TARGET=""
if [[ -L "$INSTALL_ROOT/current" ]]; then
PREVIOUS_TARGET="$(readlink -f "$INSTALL_ROOT/current")"
fi
ln -s "$RELEASE_DIR" "$INSTALL_ROOT/.current-new-$$"
mv -Tf "$INSTALL_ROOT/.current-new-$$" "$INSTALL_ROOT/current"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.service" \
"$UNIT_ROOT/aios-orchestrator-gateway.service"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.service" \
"$UNIT_ROOT/aios-orchestrator-gateway-poll.service"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway-poll.timer" \
"$UNIT_ROOT/aios-orchestrator-gateway-poll.timer"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.sysusers" \
"$SYSUSERS_ROOT/aios-orchestrator-gateway.conf"
install -m 0644 "$SOURCE_ROOT/systemd/aios-orchestrator-gateway.tmpfiles" \
"$TMPFILES_ROOT/aios-orchestrator-gateway.conf"
if [[ ! -f "$CONFIG_ROOT/inbound.token" ]]; then
python3 - <<'PY' > "$CONFIG_ROOT/inbound.token"
import secrets
print(secrets.token_urlsafe(48))
PY
fi
[[ ! -L "$CONFIG_ROOT/inbound.token" ]] || fail INBOUND_TOKEN_SYMLINK
chown root:aios-orchestrator "$CONFIG_ROOT/inbound.token"
chmod 0640 "$CONFIG_ROOT/inbound.token"
if [[ ! -f "$CONFIG_ROOT/callback.token" ]]; then
python3 - <<'PY' > "$CONFIG_ROOT/callback.token"
import secrets
print(secrets.token_urlsafe(48))
PY
fi
[[ ! -L "$CONFIG_ROOT/callback.token" ]] || fail CALLBACK_TOKEN_SYMLINK
chown root:aios-orchestrator "$CONFIG_ROOT/callback.token"
chmod 0640 "$CONFIG_ROOT/callback.token"
if [[ ! -f "$CONFIG_ROOT/config.json" ]]; then
install -m 0600 \
"$SOURCE_ROOT/config/aios-orchestrator-gateway.example.json" \
"$CONFIG_ROOT/config.json.example"
printf 'AIOS_ORCHESTRATOR_INSTALL=BLOCKED reason=CONFIGURATION_REQUIRED digest=%s\n' \
"$SOURCE_DIGEST"
exit 2
fi
chown root:aios-orchestrator "$CONFIG_ROOT/config.json"
chmod 0640 "$CONFIG_ROOT/config.json"
python3 -m json.tool "$CONFIG_ROOT/config.json" >/dev/null
AIOS_ORCHESTRATOR_CONFIG_OWNER_UID=0 \
AIOS_ORCHESTRATOR_SECRET_OWNER_UID=0 \
PYTHONPATH="$INSTALL_ROOT/current" \
python3 - "$CONFIG_ROOT/config.json" <<'PY'
import sys
from pathlib import Path
from services.aios_orchestrator_gateway.config import Config
cfg = Config.from_file(Path(sys.argv[1]))
cfg.inbound_token()
cfg.callback_token()
if cfg.workspace_trigger_id:
cfg.workspace_token()
if cfg.responses_api_key_file:
cfg.responses_api_key()
if cfg.openclaw_sync_endpoint:
cfg.openclaw_sync_token()
print("CONFIG_VALIDATE=PASS")
PY
python3 - "$STATE_ROOT/install-receipt.json" "$SOURCE_DIGEST" "$RELEASE_DIR" "$PREVIOUS_TARGET" <<'PY'
import json, os, sys, tempfile
from pathlib import Path
path = Path(sys.argv[1])
data = {
"schema_version": 1,
"source_digest": sys.argv[2],
"current_release": sys.argv[3],
"previous_release": sys.argv[4] or None,
"last_rollback": None,
}
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp = tempfile.mkstemp(prefix=".install-receipt-", dir=path.parent)
try:
with os.fdopen(fd, "w") as handle:
json.dump(data, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(temp, 0o600)
os.replace(temp, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
try:
os.unlink(temp)
except FileNotFoundError:
pass
PY
chown aios-orchestrator:aios-orchestrator "$STATE_ROOT/install-receipt.json"
chmod 0600 "$STATE_ROOT/install-receipt.json"
systemctl daemon-reload
systemctl enable --now aios-orchestrator-gateway.service
systemctl enable --now aios-orchestrator-gateway-poll.timer
systemctl is-active --quiet aios-orchestrator-gateway.service
systemctl is-active --quiet aios-orchestrator-gateway-poll.timer
AIOS_ORCHESTRATOR_CONFIG_OWNER_UID=0 \
AIOS_ORCHESTRATOR_SECRET_OWNER_UID=0 \
PYTHONPATH="$INSTALL_ROOT/current" \
python3 "$INSTALL_ROOT/current/scripts/bootstrap_orchestration_loop.py" \
--config "$CONFIG_ROOT/config.json"
systemctl start aios-orchestrator-gateway-poll.service
printf 'AIOS_ORCHESTRATOR_INSTALL=PASS digest=%s previous=%s\n' \
"$SOURCE_DIGEST" "${PREVIOUS_TARGET:-NONE}"
+52 -22
View File
@@ -1,23 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:?version required, e.g. V1R1C0}"
MESSAGE="${2:-release $VERSION}"
if ! echo "$VERSION" | grep -Eq '^V[0-9]+R[0-9]+C[0-9]+$'; then echo "ERROR: version must match VxRxCx"; exit 1; fi
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
git checkout main
git pull origin main || true
git tag -a "$VERSION" -m "$MESSAGE" || true
git push origin "$VERSION" || true
python3 - <<PY
import json, datetime
from pathlib import Path
p=Path('baseline/current.json')
if p.exists(): d=json.loads(p.read_text())
else: p.parent.mkdir(exist_ok=True); d={}
d['active_version']='$VERSION'; d['active_tag']='$VERSION'; d['updated_at']=datetime.datetime.now(datetime.UTC).isoformat(); d['update_mode']='git_patch_only'
p.write_text(json.dumps(d, ensure_ascii=False, indent=2))
PY
git add baseline/current.json || true
git commit -m "release($VERSION): update active baseline" || true
git push origin main || true
echo "RELEASE_DONE $VERSION"
set -Eeuo pipefail
VERSION="${1:?version required, e.g. V2R1C4}"
TARGET_OID="${2:?target_oid required}"
EVIDENCE_CLOSURE_OID="${3:?evidence_closure_oid required}"
APPROVAL_PLAN_ID="${4:?approval_plan_id required}"
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
TAG_REMOTE="${AIOS_GITOPS_GITEA_REMOTE:-gitea}"
BRANCH_REMOTE="${AIOS_GITOPS_BRANCH_REMOTE:-gitea}"
case "$VERSION" in
V[0-9]*R[0-9]*C[0-9]*) ;;
*) echo "ERROR: version must match VxRxCx" >&2; exit 64 ;;
esac
for value in "$TARGET_OID" "$EVIDENCE_CLOSURE_OID"; do
[[ "$value" =~ ^[0-9a-f]{40}$ ]] || {
echo "ERROR: target and evidence OIDs must be full lowercase 40-character OIDs" >&2
exit 64
}
done
[[ -n "$APPROVAL_PLAN_ID" ]] || {
echo "ERROR: approval_plan_id is required" >&2
exit 64
}
cd "$WORKSPACE"
BRANCH="$(git symbolic-ref --quiet --short HEAD)" || {
echo "ERROR: release must run on an isolated branch, not detached HEAD" >&2
exit 65
}
case "$BRANCH" in
main|master) echo "ERROR: direct main/master release writes are forbidden" >&2; exit 65 ;;
esac
python3 "$WORKSPACE/active-runtime/app/gitea_tag_publication.py" publish \
--workspace "$WORKSPACE" \
--remote "$TAG_REMOTE" \
--version "$VERSION" \
--target-oid "$TARGET_OID" \
--evidence-closure-oid "$EVIDENCE_CLOSURE_OID" \
--approval-plan-id "$APPROVAL_PLAN_ID"
git add -- "baseline/current.json" "versions/$VERSION.json"
if ! git diff --cached --quiet; then
git commit -m "release($VERSION): record Gitea-confirmed baseline"
fi
# No force, no main push, and no ignored failure.
git push "$BRANCH_REMOTE" "HEAD:refs/heads/$BRANCH"
echo "RELEASE_BASELINE_UPDATED $VERSION branch=$BRANCH"
-130
View File
@@ -1,130 +0,0 @@
#!/usr/bin/env python3
"""Submit an OpenClaw/Codex task lifecycle event over HTTP or a polled inbox."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import sys
import urllib.error
import urllib.request
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
from services.aios_orchestrator_gateway.models import Event
from services.aios_orchestrator_gateway.securefs import atomic_write_bytes_shared, read_secure_json, read_secure_text
def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("--project-key", required=True)
p.add_argument("--contract-commit", required=True)
p.add_argument("--event-id", required=True)
p.add_argument("--source-agent", required=True, choices=["openclaw", "codex", "gitea", "system"])
p.add_argument("--event-type", required=True)
p.add_argument("--work-unit-id", required=True)
p.add_argument(
"--status",
required=True,
choices=["READY", "RUNNING", "PASS", "BLOCKED", "FAIL", "TIMEOUT", "NOT_EXECUTED"],
)
p.add_argument("--state-version", type=int, required=True)
p.add_argument("--implementation-commit")
p.add_argument("--result-commit")
p.add_argument("--blocker")
p.add_argument("--evidence-ref", action="append", default=[])
p.add_argument("--payload-file", type=Path)
p.add_argument("--endpoint")
p.add_argument("--token-file", type=Path)
p.add_argument("--inbox-dir", type=Path)
p.add_argument("--timeout-seconds", type=int, default=15)
return p
def main() -> int:
args = parser().parse_args()
if not args.endpoint and not args.inbox_dir:
raise SystemExit("either --endpoint or --inbox-dir is required")
if args.endpoint and not args.token_file:
raise SystemExit("--token-file is required with --endpoint")
payload = {}
if args.payload_file:
if not args.payload_file.is_absolute():
raise SystemExit("--payload-file must be absolute")
payload = read_secure_json(
args.payload_file, expected_uid=os.getuid(), max_bytes=262_144
)
if not isinstance(payload, dict):
raise SystemExit("payload root must be an object")
event = Event.from_dict(
{
"schema_version": 1,
"project_key": args.project_key,
"contract_commit": args.contract_commit,
"event_id": args.event_id,
"source_agent": args.source_agent,
"event_type": args.event_type,
"work_unit_id": args.work_unit_id,
"status": args.status,
"state_version": args.state_version,
"implementation_commit": args.implementation_commit,
"result_commit": args.result_commit,
"blocker": args.blocker,
"evidence_refs": args.evidence_ref,
"payload": payload,
}
)
encoded = event.canonical_json().encode()
http_error: Exception | None = None
if args.endpoint:
token = read_secure_text(
args.token_file,
expected_uid=None,
max_bytes=64_000,
)
request = urllib.request.Request(
args.endpoint.rstrip("/") + "/v1/orchestrator/events",
data=encoded,
method="POST",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Idempotency-Key": event.event_id,
"User-Agent": "aios-agent-event-reporter/0.2",
},
)
try:
with urllib.request.urlopen(request, timeout=args.timeout_seconds) as response:
response.read(65_536)
if response.status not in {200, 202}:
raise RuntimeError(f"unexpected HTTP {response.status}")
print(f"EVENT_SUBMIT=PASS event_id={event.event_id} channel=http")
return 0
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as exc:
http_error = exc
if args.inbox_dir:
pending = args.inbox_dir / "pending"
path = pending / f"{event.event_id}.json"
atomic_write_bytes_shared(
path,
(json.dumps(event.to_dict(), indent=2, sort_keys=True) + "\n").encode(),
mode=0o640,
group_writable_directory=True,
)
print(f"EVENT_SUBMIT=PASS event_id={event.event_id} channel=file_inbox")
return 0
print(
f"EVENT_SUBMIT=FAIL event_id={event.event_id} reason={type(http_error).__name__}",
file=sys.stderr,
)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,102 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 0077
INSTALL_ROOT="${INSTALL_ROOT:-/opt/aios-orchestrator-gateway}"
STATE_ROOT="${STATE_ROOT:-/var/lib/aios-orchestrator-gateway}"
RECEIPT="$STATE_ROOT/install-receipt.json"
fail() {
printf 'AIOS_ORCHESTRATOR_ROLLBACK=BLOCKED reason=%s\n' "$1" >&2
exit 2
}
[[ "$(id -u)" -eq 0 ]] || fail OWNER_REQUIRED
[[ -f "$RECEIPT" ]] || fail RECEIPT_MISSING
[[ ! -L "$RECEIPT" ]] || fail RECEIPT_SYMLINK
[[ "$(stat -c '%u:%a' "$RECEIPT")" == "0:600" ]] || fail RECEIPT_PERMISSION
readarray -t VALUES < <(
python3 - "$RECEIPT" "$INSTALL_ROOT/releases" "$INSTALL_ROOT/current" <<'PY'
import json, os, stat, sys
from pathlib import Path
receipt = Path(sys.argv[1])
releases = Path(sys.argv[2]).resolve()
current_link = Path(sys.argv[3])
data = json.loads(receipt.read_text())
previous = data.get("previous_release")
current = data.get("current_release")
if not previous or not current:
raise SystemExit(2)
previous_path = Path(previous).resolve()
current_path = Path(current).resolve()
if previous_path.parent != releases or current_path.parent != releases:
raise SystemExit(2)
if not stat.S_ISDIR(os.lstat(previous_path).st_mode) or not stat.S_ISDIR(os.lstat(current_path).st_mode):
raise SystemExit(2)
for release in (previous_path, current_path):
if len(release.name) != 64 or any(ch not in "0123456789abcdef" for ch in release.name):
raise SystemExit(2)
digest_file = release / "SOURCE_DIGEST"
if digest_file.is_symlink() or not digest_file.is_file():
raise SystemExit(2)
if digest_file.read_text(encoding="utf-8").strip() != release.name:
raise SystemExit(2)
last = data.get("last_rollback")
if isinstance(last, dict) and last.get("to") == current and current_link.is_symlink():
if current_link.resolve() == current_path:
print("ALREADY_ROLLED_BACK")
raise SystemExit(0)
print(previous_path)
print(current_path)
PY
) || fail RECEIPT_INVALID
if [[ "${VALUES[0]:-}" == "ALREADY_ROLLED_BACK" ]]; then
echo "AIOS_ORCHESTRATOR_ROLLBACK=PASS result=NOOP_ALREADY_ROLLED_BACK"
exit 0
fi
[[ "${#VALUES[@]}" -eq 2 ]] || fail RECEIPT_INVALID
PREVIOUS="${VALUES[0]}"
CURRENT="${VALUES[1]}"
systemctl stop aios-orchestrator-gateway-poll.timer || true
systemctl stop aios-orchestrator-gateway-poll.service || true
systemctl stop aios-orchestrator-gateway.service || true
ln -s "$PREVIOUS" "$INSTALL_ROOT/.current-rollback-$$"
mv -Tf "$INSTALL_ROOT/.current-rollback-$$" "$INSTALL_ROOT/current"
systemctl start aios-orchestrator-gateway.service
systemctl is-active --quiet aios-orchestrator-gateway.service
systemctl start aios-orchestrator-gateway-poll.timer
systemctl is-active --quiet aios-orchestrator-gateway-poll.timer
python3 - "$RECEIPT" "$PREVIOUS" "$CURRENT" <<'PY'
import json, os, sys, tempfile
from pathlib import Path
path = Path(sys.argv[1])
data = json.loads(path.read_text())
data["current_release"] = sys.argv[2]
data["previous_release"] = sys.argv[3]
data["last_rollback"] = {"from": sys.argv[3], "to": sys.argv[2]}
fd, temp = tempfile.mkstemp(prefix=".rollback-receipt-", dir=path.parent)
try:
with os.fdopen(fd, "w") as handle:
json.dump(data, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(temp, 0o600)
os.chown(temp, 0, 0)
os.replace(temp, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
try:
os.unlink(temp)
except FileNotFoundError:
pass
PY
printf 'AIOS_ORCHESTRATOR_ROLLBACK=PASS current=%s previous=%s\n' "$PREVIOUS" "$CURRENT"
@@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG="${1:-/etc/aios-orchestrator-gateway/config.json}"
PORT="${PORT:-18920}"
python3 -m json.tool "$CONFIG" >/dev/null
systemd-analyze verify \
/etc/systemd/system/aios-orchestrator-gateway.service \
/etc/systemd/system/aios-orchestrator-gateway-poll.service \
/etc/systemd/system/aios-orchestrator-gateway-poll.timer
systemctl is-active --quiet aios-orchestrator-gateway.service
systemctl is-active --quiet aios-orchestrator-gateway-poll.timer
health="$(curl --fail --silent --show-error "http://127.0.0.1:${PORT}/healthz")"
python3 -c '
import json, sys
v=json.load(sys.stdin)
assert v["service"]=="aios-orchestrator-gateway"
assert v["status"]=="active"
assert v["polling"] is True
assert len(v["contract_commit"])==40
' <<<"$health"
systemctl start aios-orchestrator-gateway-poll.service
if journalctl -u aios-orchestrator-gateway.service -n 200 --no-pager \
| grep -Eqi 'Bearer |token=|api[_-]?key|secret='; then
echo "AIOS_ORCHESTRATOR_VERIFY=BLOCKED reason=SECRET_PATTERN_IN_LOG" >&2
exit 2
fi
echo "AIOS_ORCHESTRATOR_VERIFY=PASS"
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Verify a Gitea credential rotation without printing either credential."""
from __future__ import annotations
import argparse
import json
import subprocess
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
def token_at(path: Path, line: int) -> str:
lines = path.read_text(encoding="utf-8").splitlines()
if line < 1 or line > len(lines) or not lines[line - 1].strip():
raise RuntimeError("credential reference has no configured token line")
return lines[line - 1].strip()
def get(api: str, path: str, token: str) -> tuple[int, object | None]:
request = urllib.request.Request(
api.rstrip("/") + path,
headers={
"Accept": "application/json",
"Authorization": f"token {token}",
"User-Agent": "aios-gitops-credential-rotation-verifier/1",
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
return exc.code, None
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--old-token-file", type=Path, required=True)
parser.add_argument("--new-credential-file", type=Path, required=True)
parser.add_argument("--new-token-line", type=int, default=2)
parser.add_argument("--api", default="http://127.0.0.1:3000/api/v1")
parser.add_argument("--repository", default="aios/aios")
parser.add_argument("--version", default="V2R1C2")
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--remote", default="http://127.0.0.1:3000/aios/aios.git")
parser.add_argument(
"--branch",
default="patch/gitea-tag-publication-atomicity-20260723",
)
args = parser.parse_args()
old_token = token_at(args.old_token_file, 1)
new_token = token_at(args.new_credential_file, args.new_token_line)
repo_path = f"/repos/{args.repository}"
old_status, _ = get(args.api, repo_path, old_token)
new_status, _ = get(args.api, repo_path, new_token)
encoded = urllib.parse.quote(args.version, safe="")
ref_status, ref = get(
args.api,
f"{repo_path}/git/refs/tags/{encoded}",
new_token,
)
listing_status, listing = get(
args.api,
f"{repo_path}/tags?limit=50&page=1",
new_token,
)
refs = ref if isinstance(ref, list) else [ref]
exact = [
item
for item in refs
if isinstance(item, dict)
and item.get("ref") in {
f"refs/tags/{args.version}",
f"tags/{args.version}",
}
]
tag_oid = (
str(((exact[0].get("object") or {}).get("sha")) or "").lower()
if len(exact) == 1
else None
)
tag_status, tag = (
get(args.api, f"{repo_path}/git/tags/{tag_oid}", new_token)
if tag_oid
else (0, None)
)
peeled_target = (
str(((tag.get("object") or {}).get("sha")) or "").lower()
if isinstance(tag, dict)
else None
)
listing_target = None
if isinstance(listing, list):
for item in listing:
if item.get("name") == args.version:
listing_target = str(
((item.get("commit") or {}).get("sha")) or ""
).lower()
break
remote = subprocess.run(
[
"git",
"-c",
f"safe.directory={args.workspace}",
"-c",
"credential.helper=!/usr/local/libexec/aios-gitea-credential",
"-c",
"credential.useHttpPath=true",
"-C",
str(args.workspace),
"ls-remote",
args.remote,
f"refs/heads/{args.branch}",
],
text=True,
capture_output=True,
)
remote_oid = (
remote.stdout.split()[0].lower()
if remote.returncode == 0 and remote.stdout.split()
else None
)
result = {
"old_token_repository_http": old_status,
"new_token_repository_http": new_status,
"git_ls_remote_oid": remote_oid,
"gitea_ref_http": ref_status,
"gitea_tag_oid": tag_oid,
"gitea_tag_object_http": tag_status,
"gitea_peeled_target_oid": peeled_target,
"gitea_listing_http": listing_status,
"gitea_listing_target_oid": listing_target,
"credentials_printed": False,
}
print(json.dumps(result, indent=2, sort_keys=True))
passed = (
old_status in {401, 403}
and new_status == 200
and remote_oid is not None
and ref_status == 200
and tag_oid is not None
and tag_status == 200
and peeled_target is not None
and listing_status == 200
and listing_target is not None
)
return 0 if passed else 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,38 @@
{
"exposure_window": {
"detected_at": "2026-07-23T13:40:23+08:00",
"revoked_at": "2026-07-23T13:40:56+08:00",
"start": "2026-07-23T12:00:00+08:00",
"start_accuracy": "bounded approximation; precise platform tool-log timestamp unavailable"
},
"gitea_tag_read_validation": {
"listing_target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"peeled_target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"tag_mutation_performed": false,
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"version": "V2R1C2"
},
"impact_scope": "Gitea repository write access for gitops-bot; no evidence of unauthorized use observed during rotation checks",
"incident_id": "GITEA-TOKEN-33-EXPOSURE-20260723",
"log_handling": {
"credential_values_in_record": false,
"reason": "formal audit/tool logs must not be deleted",
"server_redacted_record_mode": "0640 root:gitops",
"source_tool_log_deleted": false
},
"new_credential_reference": "/etc/aios-gitops/credentials/gitea-git-http",
"new_token_validation": {
"after_revoke_repository_http": 200,
"before_revoke_repository_http": 200,
"git_ls_remote_oid": "54093d15437ce0af31acf6feeeffacf6626a59cd",
"idempotent_branch_push": true
},
"old_token_validation": {
"after_revoke_repository_http": 401,
"before_revoke_repository_http": 200
},
"production_tag_operation": "NOT_EXECUTED",
"replacement_token_record_id": 50,
"scope": "write:repository",
"token_id": 33
}
-1
View File
@@ -1 +0,0 @@
"""AIOS services namespace."""
@@ -1,3 +0,0 @@
"""Reliable central orchestration gateway for AIOS."""
__version__ = "0.2.0"
@@ -1,34 +0,0 @@
from __future__ import annotations
import argparse
import logging
from pathlib import Path
from .config import Config
from .service import Runtime, serve
def main() -> int:
parser = argparse.ArgumentParser(description="AIOS Orchestrator Gateway")
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--log-level", default="INFO")
parser.add_argument(
"--poll-once",
action="store_true",
help="Run one inbox/model/outbox/OpenClaw synchronization polling cycle.",
)
args = parser.parse_args()
logging.basicConfig(
level=getattr(logging, args.log_level.upper(), logging.INFO),
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
if args.poll_once:
runtime = Runtime(Config.from_file(args.config))
runtime.poll_once()
return 0
serve(args.config)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,178 +0,0 @@
from __future__ import annotations
import json
import urllib.error
import urllib.request
from typing import Any
from .errors import TriggerError
from .models import DECISION_SCHEMA, Decision, Event
class WorkspaceAgentClient:
"""Asynchronous Workspace Agent trigger.
The official endpoint returns only 202 and no run ID or retrievable output.
Completion therefore arrives through the gateway callback endpoint.
"""
def __init__(self, trigger_id: str, token: str, timeout_seconds: int = 30):
self.trigger_id = trigger_id
self.token = token
self.timeout_seconds = timeout_seconds
def trigger(self, event: Event) -> None:
url = f"https://api.chatgpt.com/v1/workspace_agents/{self.trigger_id}/trigger"
prompt = (
"Process this AIOS orchestration event under the committed contract. "
"When complete, call submit_orchestration_decision with strict JSON. "
"Do not fabricate tests, Git state, secrets, evidence, or deployment facts.\n\n"
+ event.canonical_json()
)
body = json.dumps(
{
"conversation_key": event.project_key,
"input": prompt,
}
).encode()
request = urllib.request.Request(
url,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
"Idempotency-Key": event.event_id,
"User-Agent": "aios-orchestrator-gateway/0.2",
},
)
try:
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
response.read(1024)
if response.status != 202:
raise TriggerError(f"workspace_agent_http_{response.status}")
except urllib.error.HTTPError as exc:
raise TriggerError(f"workspace_agent_http_{exc.code}") from exc
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise TriggerError("workspace_agent_trigger_failed") from exc
class ResponsesClient:
"""Background Responses API client with polling and strict output validation."""
def __init__(self, api_key: str, model: str, timeout_seconds: int = 60):
self.api_key = api_key
self.model = model
self.timeout_seconds = timeout_seconds
def start(self, event: Event) -> str:
body = {
"model": self.model,
"background": True,
"instructions": (
"You are the AIOS central orchestration brain. Apply the exact "
"contract_commit in the event. Return only a strict decision. "
"Never fabricate evidence, secrets, Git facts, tests, approvals, "
"or deployment state. Keep external blockers fail-closed."
),
"input": event.canonical_json(),
"text": {
"format": {
"type": "json_schema",
"name": "aios_orchestrator_decision",
"strict": True,
"schema": DECISION_SCHEMA,
}
},
"metadata": {
"event_id": event.event_id,
"project_key": event.project_key,
"contract_commit": event.contract_commit,
},
}
payload = self._request(
"POST",
"https://api.openai.com/v1/responses",
body,
idempotency_key=event.event_id,
)
response_id = payload.get("id")
if not isinstance(response_id, str) or not response_id.startswith("resp_"):
raise TriggerError("responses_missing_id")
return response_id
def poll(self, response_id: str) -> tuple[str, Decision | None]:
payload = self._request(
"GET",
f"https://api.openai.com/v1/responses/{response_id}",
None,
)
status = payload.get("status")
if status in {"queued", "in_progress"}:
return str(status), None
if status == "completed":
text = _extract_output_text(payload)
try:
return "completed", Decision.from_dict(json.loads(text))
except json.JSONDecodeError as exc:
raise TriggerError("responses_invalid_json") from exc
if status in {"failed", "cancelled", "incomplete"}:
raise TriggerError(f"responses_{status}")
raise TriggerError("responses_unknown_status")
def _request(
self,
method: str,
url: str,
body: dict[str, Any] | None,
*,
idempotency_key: str | None = None,
) -> dict[str, Any]:
encoded = json.dumps(body).encode() if body is not None else None
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"User-Agent": "aios-orchestrator-gateway/0.2",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
request = urllib.request.Request(
url,
data=encoded,
method=method,
headers=headers,
)
try:
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
raw = response.read(4_194_304)
if response.status not in {200, 201}:
raise TriggerError(f"responses_http_{response.status}")
except urllib.error.HTTPError as exc:
raise TriggerError(f"responses_http_{exc.code}") from exc
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise TriggerError("responses_request_failed") from exc
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise TriggerError("responses_invalid_payload") from exc
if not isinstance(payload, dict):
raise TriggerError("responses_payload_not_object")
return payload
def _extract_output_text(payload: dict[str, Any]) -> str:
output = payload.get("output")
if not isinstance(output, list):
raise TriggerError("responses_missing_output")
for item in output:
if not isinstance(item, dict):
continue
content = item.get("content")
if not isinstance(content, list):
continue
for part in content:
if isinstance(part, dict) and part.get("type") == "output_text":
text = part.get("text")
if isinstance(text, str):
return text
raise TriggerError("responses_missing_output_text")
@@ -1,203 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
import os
from pathlib import Path
from typing import Any
from .errors import ConfigurationError
from .securefs import read_secure_json, read_secure_text
@dataclass(frozen=True)
class Config:
host: str
port: int
project_key: str
database_path: Path
event_inbox_dir: Path
decision_inbox_dir: Path
dispatch_outbox_dir: Path
openclaw_sync_dir: Path
command_queue_dir: Path
inbound_token_file: Path
callback_token_file: Path
contract_commit: str
workspace_trigger_id: str | None
workspace_token_file: Path | None
responses_api_key_file: Path | None
responses_model: str | None
poll_interval_seconds: int
summary_interval_seconds: int
callback_timeout_seconds: int
lease_seconds: int
retry_base_seconds: int
max_retries: int
max_same_blocker_retries: int
max_hops_per_gate: int
openclaw_sync_endpoint: str | None
openclaw_sync_token_file: Path | None
git_poll_enabled: bool
git_poll_bare_repo: Path | None
git_poll_ref_prefixes: tuple[str, ...]
git_poll_interval_seconds: int
@classmethod
def from_file(cls, path: Path) -> "Config":
owner_uid = int(os.environ.get("AIOS_ORCHESTRATOR_CONFIG_OWNER_UID", "0"))
raw = read_secure_json(path, expected_uid=owner_uid)
expected = {
"host", "port", "project_key", "database_path", "event_inbox_dir",
"decision_inbox_dir", "dispatch_outbox_dir", "openclaw_sync_dir",
"command_queue_dir", "inbound_token_file", "callback_token_file",
"contract_commit", "workspace_agent", "responses_fallback",
"polling", "loop_controls", "openclaw_sync", "git_poll",
}
if set(raw) != expected:
raise ConfigurationError("invalid_config_fields")
def path_value(key: str) -> Path:
value = raw.get(key)
if not isinstance(value, str):
raise ConfigurationError(f"invalid_{key}")
result = Path(value)
if not result.is_absolute():
raise ConfigurationError(f"path_not_absolute:{key}")
return result
def integer(container: dict[str, Any], key: str, lo: int, hi: int) -> int:
value = container.get(key)
if not isinstance(value, int) or not lo <= value <= hi:
raise ConfigurationError(f"invalid_{key}")
return value
host = raw.get("host")
port = raw.get("port")
project_key = raw.get("project_key")
contract = raw.get("contract_commit")
if not isinstance(host, str) or not host:
raise ConfigurationError("invalid_host")
if not isinstance(project_key, str) or not project_key:
raise ConfigurationError("invalid_project_key")
if not isinstance(port, int) or not 1 <= port <= 65535:
raise ConfigurationError("invalid_port")
if not isinstance(contract, str) or len(contract) != 40:
raise ConfigurationError("invalid_contract_commit")
workspace = raw.get("workspace_agent")
responses = raw.get("responses_fallback")
polling = raw.get("polling")
loop = raw.get("loop_controls")
sync = raw.get("openclaw_sync")
git_poll = raw.get("git_poll")
if not all(isinstance(x, dict) for x in (workspace, responses, polling, loop, sync, git_poll)):
raise ConfigurationError("invalid_nested_config")
trigger_id = workspace.get("trigger_id")
workspace_token = workspace.get("access_token_file")
if trigger_id is not None:
if not isinstance(trigger_id, str) or not trigger_id.startswith("agtch_"):
raise ConfigurationError("invalid_workspace_trigger_id")
if not isinstance(workspace_token, str) or not Path(workspace_token).is_absolute():
raise ConfigurationError("invalid_workspace_token_file")
workspace_token_file = Path(workspace_token)
else:
workspace_token_file = None
responses_key = responses.get("api_key_file")
responses_model = responses.get("model")
if responses_key is not None:
if not isinstance(responses_key, str) or not Path(responses_key).is_absolute():
raise ConfigurationError("invalid_responses_key_file")
if not isinstance(responses_model, str) or not responses_model:
raise ConfigurationError("invalid_responses_model")
responses_key_file = Path(responses_key)
else:
responses_key_file = None
responses_model = None
if trigger_id is None and responses_key_file is None:
raise ConfigurationError("no_central_model_channel")
endpoint = sync.get("endpoint")
token_file = sync.get("token_file")
if endpoint is not None and (not isinstance(endpoint, str) or not endpoint.startswith(("http://", "https://"))):
raise ConfigurationError("invalid_openclaw_sync_endpoint")
if endpoint is not None:
if not isinstance(token_file, str) or not Path(token_file).is_absolute():
raise ConfigurationError("invalid_openclaw_sync_token_file")
sync_token_file = Path(token_file)
else:
sync_token_file = None
git_enabled = git_poll.get("enabled")
if not isinstance(git_enabled, bool):
raise ConfigurationError("invalid_git_poll_enabled")
git_bare_repo_raw = git_poll.get("bare_repo")
git_prefixes_raw = git_poll.get("ref_prefixes")
if git_enabled:
if not isinstance(git_bare_repo_raw, str) or not Path(git_bare_repo_raw).is_absolute():
raise ConfigurationError("invalid_git_poll_bare_repo")
if (
not isinstance(git_prefixes_raw, list)
or not git_prefixes_raw
or any(not isinstance(item, str) or not item.startswith("refs/") for item in git_prefixes_raw)
):
raise ConfigurationError("invalid_git_poll_ref_prefixes")
git_bare_repo = Path(git_bare_repo_raw)
git_prefixes = tuple(git_prefixes_raw)
else:
git_bare_repo = None
git_prefixes = ()
return cls(
host=host,
port=port,
project_key=project_key,
database_path=path_value("database_path"),
event_inbox_dir=path_value("event_inbox_dir"),
decision_inbox_dir=path_value("decision_inbox_dir"),
dispatch_outbox_dir=path_value("dispatch_outbox_dir"),
openclaw_sync_dir=path_value("openclaw_sync_dir"),
command_queue_dir=path_value("command_queue_dir"),
inbound_token_file=path_value("inbound_token_file"),
callback_token_file=path_value("callback_token_file"),
contract_commit=contract,
workspace_trigger_id=trigger_id,
workspace_token_file=workspace_token_file,
responses_api_key_file=responses_key_file,
responses_model=responses_model,
poll_interval_seconds=integer(polling, "interval_seconds", 1, 300),
summary_interval_seconds=integer(polling, "summary_interval_seconds", 10, 86400),
callback_timeout_seconds=integer(loop, "callback_timeout_seconds", 30, 86400),
lease_seconds=integer(loop, "lease_seconds", 10, 3600),
retry_base_seconds=integer(loop, "retry_base_seconds", 1, 3600),
max_retries=integer(loop, "max_retries", 0, 20),
max_same_blocker_retries=integer(loop, "max_same_blocker_retries", 0, 20),
max_hops_per_gate=integer(loop, "max_hops_per_gate", 1, 100),
openclaw_sync_endpoint=endpoint,
openclaw_sync_token_file=sync_token_file,
git_poll_enabled=git_enabled,
git_poll_bare_repo=git_bare_repo,
git_poll_ref_prefixes=git_prefixes,
git_poll_interval_seconds=integer(git_poll, "interval_seconds", 5, 3600),
)
def _secret(self, path: Path) -> str:
owner_uid = int(os.environ.get("AIOS_ORCHESTRATOR_SECRET_OWNER_UID", "0"))
return read_secure_text(path, expected_uid=owner_uid, max_bytes=64_000)
def inbound_token(self) -> str:
return self._secret(self.inbound_token_file)
def callback_token(self) -> str:
return self._secret(self.callback_token_file)
def workspace_token(self) -> str | None:
return self._secret(self.workspace_token_file) if self.workspace_token_file else None
def responses_api_key(self) -> str | None:
return self._secret(self.responses_api_key_file) if self.responses_api_key_file else None
def openclaw_sync_token(self) -> str | None:
return self._secret(self.openclaw_sync_token_file) if self.openclaw_sync_token_file else None
@@ -1,89 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
import re
import os
import stat
from typing import Any
from .errors import DispatchError
from .securefs import (
atomic_write_bytes,
atomic_write_bytes_shared,
ensure_secure_directory,
ensure_shared_directory,
read_secure_bytes,
)
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
class DispatchSink:
"""Reliable file-based dispatch adapter for existing OpenClaw/Codex watchers."""
def __init__(self, outbox_dir: Path, command_queue_dir: Path, contract_commit: str):
self.outbox_dir = Path(outbox_dir)
self.command_queue_dir = Path(command_queue_dir)
self.contract_commit = contract_commit
ensure_secure_directory(self.outbox_dir)
ensure_shared_directory(self.command_queue_dir)
def dispatch(self, record: dict[str, Any]) -> tuple[Path, Path]:
work_unit_id = record.get("work_unit_id")
event_id = record.get("event_id")
agent = record.get("assigned_agent")
payload_json = record.get("payload_json")
if (
not isinstance(work_unit_id, str)
or not SAFE_ID.fullmatch(work_unit_id)
or not isinstance(event_id, str)
or not SAFE_ID.fullmatch(event_id)
or agent not in {"openclaw", "codex"}
or not isinstance(payload_json, str)
):
raise DispatchError("invalid_dispatch_record")
try:
work_unit = json.loads(payload_json)
except json.JSONDecodeError as exc:
raise DispatchError("invalid_dispatch_payload") from exc
envelope = {
"schema_version": 1,
"contract_commit": self.contract_commit,
"event_id": event_id,
"assigned_agent": agent,
"work_unit": work_unit,
"callback": {
"event_endpoint": "/v1/orchestrator/events",
"start_event_type": "work_unit_started",
"end_event_type": "work_unit_completed",
},
}
outbox_path = self.outbox_dir / "dispatched" / f"{work_unit_id}.json"
queue_path = self.command_queue_dir / agent / "pending" / f"{work_unit_id}.json"
ensure_secure_directory(outbox_path.parent)
ensure_shared_directory(queue_path.parent)
# Idempotent immutable payload: existing identical file is accepted.
self._write_idempotent_json(outbox_path, envelope, shared=False)
self._write_idempotent_json(queue_path, envelope, shared=True)
return outbox_path, queue_path
@staticmethod
def _write_idempotent_json(path: Path, value: dict[str, Any], *, shared: bool) -> None:
payload = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode()
try:
st = os.lstat(path)
except FileNotFoundError:
if shared:
atomic_write_bytes_shared(path, payload, mode=0o640)
else:
atomic_write_bytes(path, payload, mode=0o600)
return
if not stat.S_ISREG(st.st_mode):
raise DispatchError(f"dispatch_target_not_regular:{path.name}")
existing = read_secure_bytes(path, expected_uid=None, max_bytes=1_048_576)
if existing != payload:
raise DispatchError(f"dispatch_idempotency_conflict:{path.name}")
@@ -1,26 +0,0 @@
class OrchestratorError(Exception):
"""Base orchestration error."""
class ConfigurationError(OrchestratorError):
"""Unsafe or invalid configuration."""
class ValidationError(OrchestratorError):
"""Invalid event or decision."""
class AuthenticationError(OrchestratorError):
"""Authentication failed."""
class ConflictError(OrchestratorError):
"""Idempotency, lease, or optimistic concurrency conflict."""
class TriggerError(OrchestratorError):
"""Central model trigger failed."""
class DispatchError(OrchestratorError):
"""Dispatch or synchronization output failed."""
@@ -1,346 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
import hashlib
import json
import re
from typing import Any
from .errors import ValidationError
OID_RE = re.compile(r"^[0-9a-f]{40}$")
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
AGENTS = {"openclaw", "codex", "gitea", "system", "workspace-agent"}
EVENT_STATUSES = {"READY", "RUNNING", "PASS", "BLOCKED", "FAIL", "TIMEOUT", "NOT_EXECUTED"}
VERDICTS = {"DISPATCH", "BLOCKED", "COMPLETE", "NOOP"}
EXECUTORS = {"openclaw", "codex"}
def req_str(value: dict[str, Any], key: str, pattern: re.Pattern[str] | None = None) -> str:
item = value.get(key)
if not isinstance(item, str) or not item:
raise ValidationError(f"invalid_{key}")
if pattern is not None and not pattern.fullmatch(item):
raise ValidationError(f"invalid_{key}")
return item
def opt_oid(item: Any, key: str) -> str | None:
if item is None:
return None
if not isinstance(item, str) or not OID_RE.fullmatch(item):
raise ValidationError(f"invalid_{key}")
return item
def string_list(value: dict[str, Any], key: str, limit: int) -> tuple[str, ...]:
raw = value.get(key)
if not isinstance(raw, list) or len(raw) > limit:
raise ValidationError(f"invalid_{key}")
result: list[str] = []
for item in raw:
if not isinstance(item, str) or not item or len(item) > 1024:
raise ValidationError(f"invalid_{key}")
result.append(item)
return tuple(result)
@dataclass(frozen=True)
class Event:
schema_version: int
project_key: str
contract_commit: str
event_id: str
source_agent: str
event_type: str
work_unit_id: str
status: str
state_version: int
implementation_commit: str | None = None
result_commit: str | None = None
blocker: str | None = None
evidence_refs: tuple[str, ...] = ()
payload: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "Event":
allowed = {
"schema_version", "project_key", "contract_commit", "event_id",
"source_agent", "event_type", "work_unit_id", "status",
"state_version", "implementation_commit", "result_commit",
"blocker", "evidence_refs", "payload",
}
if not isinstance(value, dict) or set(value) - allowed:
raise ValidationError("invalid_event_fields")
if value.get("schema_version") != 1:
raise ValidationError("unsupported_event_schema")
source = req_str(value, "source_agent")
status = req_str(value, "status")
if source not in AGENTS:
raise ValidationError("invalid_source_agent")
if status not in EVENT_STATUSES:
raise ValidationError("invalid_event_status")
version = value.get("state_version")
if not isinstance(version, int) or version < 0:
raise ValidationError("invalid_state_version")
blocker = value.get("blocker")
if blocker is not None and (not isinstance(blocker, str) or len(blocker) > 512):
raise ValidationError("invalid_blocker")
payload = value.get("payload", {})
if not isinstance(payload, dict):
raise ValidationError("invalid_payload")
if len(json.dumps(payload, sort_keys=True).encode()) > 262_144:
raise ValidationError("payload_too_large")
return cls(
schema_version=1,
project_key=req_str(value, "project_key", ID_RE),
contract_commit=req_str(value, "contract_commit", OID_RE),
event_id=req_str(value, "event_id", ID_RE),
source_agent=source,
event_type=req_str(value, "event_type", ID_RE),
work_unit_id=req_str(value, "work_unit_id", ID_RE),
status=status,
state_version=version,
implementation_commit=opt_oid(value.get("implementation_commit"), "implementation_commit"),
result_commit=opt_oid(value.get("result_commit"), "result_commit"),
blocker=blocker,
evidence_refs=string_list(value, "evidence_refs", 64),
payload=payload,
)
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": 1,
"project_key": self.project_key,
"contract_commit": self.contract_commit,
"event_id": self.event_id,
"source_agent": self.source_agent,
"event_type": self.event_type,
"work_unit_id": self.work_unit_id,
"status": self.status,
"state_version": self.state_version,
"implementation_commit": self.implementation_commit,
"result_commit": self.result_commit,
"blocker": self.blocker,
"evidence_refs": list(self.evidence_refs),
"payload": self.payload,
}
def canonical_json(self) -> str:
return json.dumps(self.to_dict(), separators=(",", ":"), sort_keys=True)
def digest(self) -> str:
return hashlib.sha256(self.canonical_json().encode()).hexdigest()
@dataclass(frozen=True)
class WorkUnit:
id: str
track_id: str
gate_id: str
primary_agent: str
fallback_agent: str
clean_base_commit: str
allowed_paths: tuple[str, ...]
required_checks: tuple[str, ...]
forbidden_operations: tuple[str, ...]
instruction: str
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "WorkUnit":
expected = {
"id", "track_id", "gate_id", "primary_agent", "fallback_agent",
"clean_base_commit", "allowed_paths", "required_checks",
"forbidden_operations", "instruction",
}
if not isinstance(value, dict) or set(value) != expected:
raise ValidationError("invalid_work_unit_fields")
primary = req_str(value, "primary_agent")
fallback = req_str(value, "fallback_agent")
if primary not in EXECUTORS or fallback not in EXECUTORS or primary == fallback:
raise ValidationError("invalid_agent_assignment")
instruction = req_str(value, "instruction")
if len(instruction.encode()) > 65_536:
raise ValidationError("instruction_too_large")
return cls(
id=req_str(value, "id", ID_RE),
track_id=req_str(value, "track_id", ID_RE),
gate_id=req_str(value, "gate_id", ID_RE),
primary_agent=primary,
fallback_agent=fallback,
clean_base_commit=req_str(value, "clean_base_commit", OID_RE),
allowed_paths=string_list(value, "allowed_paths", 128),
required_checks=string_list(value, "required_checks", 128),
forbidden_operations=string_list(value, "forbidden_operations", 64),
instruction=instruction,
)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"track_id": self.track_id,
"gate_id": self.gate_id,
"primary_agent": self.primary_agent,
"fallback_agent": self.fallback_agent,
"clean_base_commit": self.clean_base_commit,
"allowed_paths": list(self.allowed_paths),
"required_checks": list(self.required_checks),
"forbidden_operations": list(self.forbidden_operations),
"instruction": self.instruction,
}
@dataclass(frozen=True)
class Decision:
schema_version: int
project_key: str
contract_commit: str
event_id: str
expected_state_version: int
verdict: str
state_updates: dict[str, str]
next_work_units: tuple[WorkUnit, ...]
requires_human_input: bool
stop_reason: str | None
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "Decision":
expected = {
"schema_version", "project_key", "contract_commit", "event_id",
"expected_state_version", "verdict", "state_updates",
"next_work_units", "requires_human_input", "stop_reason",
}
if not isinstance(value, dict) or set(value) != expected:
raise ValidationError("invalid_decision_fields")
if value.get("schema_version") != 1:
raise ValidationError("unsupported_decision_schema")
version = value.get("expected_state_version")
if not isinstance(version, int) or version < 0:
raise ValidationError("invalid_expected_state_version")
verdict = req_str(value, "verdict")
if verdict not in VERDICTS:
raise ValidationError("invalid_verdict")
updates = value.get("state_updates")
normalized: dict[str, str] = {}
if isinstance(updates, dict):
update_items = [{"key": key, "value": item} for key, item in updates.items()]
elif isinstance(updates, list):
update_items = updates
else:
raise ValidationError("invalid_state_updates")
if len(update_items) > 128:
raise ValidationError("invalid_state_updates")
for update in update_items:
if not isinstance(update, dict) or set(update) != {"key", "value"}:
raise ValidationError("invalid_state_update")
key = update.get("key")
item = update.get("value")
if not isinstance(key, str) or not ID_RE.fullmatch(key):
raise ValidationError("invalid_state_update_key")
if not isinstance(item, str) or not item or len(item) > 256:
raise ValidationError("invalid_state_update_value")
if key in normalized:
raise ValidationError("duplicate_state_update_key")
normalized[key] = item
raw_units = value.get("next_work_units")
if not isinstance(raw_units, list) or len(raw_units) > 32:
raise ValidationError("invalid_next_work_units")
units = tuple(WorkUnit.from_dict(item) for item in raw_units)
human = value.get("requires_human_input")
if not isinstance(human, bool):
raise ValidationError("invalid_requires_human_input")
stop = value.get("stop_reason")
if stop is not None and (not isinstance(stop, str) or len(stop) > 512):
raise ValidationError("invalid_stop_reason")
if verdict == "DISPATCH" and not units:
raise ValidationError("dispatch_without_work_units")
if verdict != "DISPATCH" and units:
raise ValidationError("work_units_without_dispatch")
return cls(
schema_version=1,
project_key=req_str(value, "project_key", ID_RE),
contract_commit=req_str(value, "contract_commit", OID_RE),
event_id=req_str(value, "event_id", ID_RE),
expected_state_version=version,
verdict=verdict,
state_updates=normalized,
next_work_units=units,
requires_human_input=human,
stop_reason=stop,
)
def to_dict(self) -> dict[str, Any]:
return {
"schema_version": 1,
"project_key": self.project_key,
"contract_commit": self.contract_commit,
"event_id": self.event_id,
"expected_state_version": self.expected_state_version,
"verdict": self.verdict,
"state_updates": [
{"key": key, "value": value}
for key, value in sorted(self.state_updates.items())
],
"next_work_units": [u.to_dict() for u in self.next_work_units],
"requires_human_input": self.requires_human_input,
"stop_reason": self.stop_reason,
}
DECISION_SCHEMA: dict[str, Any] = {
"type": "object",
"additionalProperties": False,
"required": [
"schema_version", "project_key", "contract_commit", "event_id",
"expected_state_version", "verdict", "state_updates",
"next_work_units", "requires_human_input", "stop_reason",
],
"properties": {
"schema_version": {"type": "integer", "const": 1},
"project_key": {"type": "string"},
"contract_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
"event_id": {"type": "string"},
"expected_state_version": {"type": "integer", "minimum": 0},
"verdict": {"type": "string", "enum": sorted(VERDICTS)},
"state_updates": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": False,
"required": ["key", "value"],
"properties": {
"key": {"type": "string"},
"value": {"type": "string"}
}
}
},
"next_work_units": {
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"additionalProperties": False,
"required": [
"id", "track_id", "gate_id", "primary_agent",
"fallback_agent", "clean_base_commit", "allowed_paths",
"required_checks", "forbidden_operations", "instruction",
],
"properties": {
"id": {"type": "string"},
"track_id": {"type": "string"},
"gate_id": {"type": "string"},
"primary_agent": {"type": "string", "enum": ["openclaw", "codex"]},
"fallback_agent": {"type": "string", "enum": ["openclaw", "codex"]},
"clean_base_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
"allowed_paths": {"type": "array", "items": {"type": "string"}},
"required_checks": {"type": "array", "items": {"type": "string"}},
"forbidden_operations": {"type": "array", "items": {"type": "string"}},
"instruction": {"type": "string"},
},
},
},
"requires_human_input": {"type": "boolean"},
"stop_reason": {"type": ["string", "null"]},
},
}
@@ -1,229 +0,0 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import stat
from typing import Any
from .errors import ConfigurationError
_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
def reject_symlink_chain(path: Path) -> None:
"""Reject symlinks in every existing component of an absolute path."""
path = Path(path)
if not path.is_absolute():
raise ConfigurationError(f"path_not_absolute:{path}")
current = Path(path.anchor)
for part in path.parts[1:]:
current = current / part
try:
st = os.lstat(current)
except FileNotFoundError:
continue
if stat.S_ISLNK(st.st_mode):
raise ConfigurationError(f"symlink_rejected:{current}")
def verify_secure_file(
path: Path,
*,
expected_uid: int | None,
allowed_write_bits: int = stat.S_IWUSR,
require_executable: bool = False,
) -> os.stat_result:
path = Path(path)
reject_symlink_chain(path)
before = os.lstat(path)
if not stat.S_ISREG(before.st_mode):
raise ConfigurationError(f"not_regular_file:{path}")
if expected_uid is not None and before.st_uid != expected_uid:
raise ConfigurationError(f"unexpected_owner:{path}")
write_bits = before.st_mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
if write_bits & ~allowed_write_bits:
raise ConfigurationError(f"permissive_mode:{path}")
if require_executable and not (before.st_mode & stat.S_IXUSR):
raise ConfigurationError(f"not_executable:{path}")
fd = os.open(path, os.O_RDONLY | _NOFOLLOW)
try:
after = os.fstat(fd)
if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino):
raise ConfigurationError(f"inode_changed:{path}")
finally:
os.close(fd)
return before
def read_secure_bytes(
path: Path,
*,
expected_uid: int | None,
max_bytes: int = 1_048_576,
) -> bytes:
path = Path(path)
before = verify_secure_file(path, expected_uid=expected_uid)
if before.st_size > max_bytes:
raise ConfigurationError(f"file_too_large:{path}")
fd = os.open(path, os.O_RDONLY | _NOFOLLOW)
try:
data = bytearray()
while True:
chunk = os.read(fd, min(65536, max_bytes + 1 - len(data)))
if not chunk:
break
data.extend(chunk)
if len(data) > max_bytes:
raise ConfigurationError(f"file_too_large:{path}")
return bytes(data)
finally:
os.close(fd)
def read_secure_text(path: Path, *, expected_uid: int | None, max_bytes: int = 1_048_576) -> str:
try:
return read_secure_bytes(path, expected_uid=expected_uid, max_bytes=max_bytes).decode("utf-8").strip()
except UnicodeDecodeError as exc:
raise ConfigurationError(f"not_utf8:{path}") from exc
def read_secure_json(path: Path, *, expected_uid: int | None, max_bytes: int = 1_048_576) -> dict[str, Any]:
try:
value = json.loads(read_secure_text(path, expected_uid=expected_uid, max_bytes=max_bytes))
except json.JSONDecodeError as exc:
raise ConfigurationError(f"invalid_json:{path}") from exc
if not isinstance(value, dict):
raise ConfigurationError(f"json_root_not_object:{path}")
return value
def ensure_secure_directory(path: Path, *, mode: int = 0o700) -> None:
path = Path(path)
if not path.is_absolute():
raise ConfigurationError(f"path_not_absolute:{path}")
reject_symlink_chain(path.parent)
path.mkdir(parents=True, exist_ok=True, mode=mode)
reject_symlink_chain(path)
st = os.lstat(path)
if not stat.S_ISDIR(st.st_mode):
raise ConfigurationError(f"not_directory:{path}")
if st.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
raise ConfigurationError(f"permissive_directory:{path}")
def atomic_write_bytes(path: Path, payload: bytes, *, mode: int = 0o600) -> None:
"""Write through a verified directory fd and atomically replace the target."""
path = Path(path)
ensure_secure_directory(path.parent)
parent_before = os.lstat(path.parent)
parent_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | _NOFOLLOW)
temp_name = f".{path.name}.tmp-{os.getpid()}-{os.urandom(6).hex()}"
try:
parent_after = os.fstat(parent_fd)
if (parent_before.st_dev, parent_before.st_ino) != (
parent_after.st_dev,
parent_after.st_ino,
):
raise ConfigurationError(f"directory_changed:{path.parent}")
fd = os.open(
temp_name,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | _NOFOLLOW,
mode,
dir_fd=parent_fd,
)
try:
view = memoryview(payload)
while view:
written = os.write(fd, view)
view = view[written:]
os.fsync(fd)
finally:
os.close(fd)
os.replace(
temp_name,
path.name,
src_dir_fd=parent_fd,
dst_dir_fd=parent_fd,
)
os.fsync(parent_fd)
finally:
try:
os.unlink(temp_name, dir_fd=parent_fd)
except FileNotFoundError:
pass
os.close(parent_fd)
def atomic_write_json(path: Path, value: Any, *, mode: int = 0o600) -> None:
payload = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
atomic_write_bytes(path, payload, mode=mode)
def ensure_shared_directory(path: Path, *, group_writable: bool = False) -> None:
"""Verify/create a setgid queue directory shared with a consumer group."""
path = Path(path)
if not path.is_absolute():
raise ConfigurationError(f"path_not_absolute:{path}")
reject_symlink_chain(path.parent)
mode = 0o2770 if group_writable else 0o2750
try:
path.mkdir(parents=True, mode=mode)
except FileExistsError:
pass
else:
os.chmod(path, mode)
reject_symlink_chain(path)
st = os.lstat(path)
if not stat.S_ISDIR(st.st_mode):
raise ConfigurationError(f"not_directory:{path}")
observed = stat.S_IMODE(st.st_mode)
required = stat.S_ISGID | stat.S_IRGRP | stat.S_IXGRP
if group_writable:
required |= stat.S_IWGRP
if observed & 0o007 or observed & required != required:
raise ConfigurationError(f"invalid_shared_directory_mode:{path}")
def atomic_write_bytes_shared(
path: Path,
payload: bytes,
*,
mode: int = 0o640,
group_writable_directory: bool = False,
) -> None:
"""Atomic write into a setgid/group-readable shared queue directory."""
path = Path(path)
ensure_shared_directory(path.parent, group_writable=group_writable_directory)
parent_before = os.lstat(path.parent)
parent_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | _NOFOLLOW)
temp_name = f".{path.name}.tmp-{os.getpid()}-{os.urandom(6).hex()}"
try:
parent_after = os.fstat(parent_fd)
if (parent_before.st_dev, parent_before.st_ino) != (
parent_after.st_dev,
parent_after.st_ino,
):
raise ConfigurationError(f"directory_changed:{path.parent}")
fd = os.open(
temp_name,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | _NOFOLLOW,
mode,
dir_fd=parent_fd,
)
os.fchmod(fd, mode)
try:
view = memoryview(payload)
while view:
written = os.write(fd, view)
view = view[written:]
os.fsync(fd)
finally:
os.close(fd)
os.replace(temp_name, path.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
os.fsync(parent_fd)
finally:
try:
os.unlink(temp_name, dir_fd=parent_fd)
except FileNotFoundError:
pass
os.close(parent_fd)
@@ -1,642 +0,0 @@
from __future__ import annotations
import hashlib
import hmac
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import logging
import os
from pathlib import Path
import socket
import subprocess
import threading
import time
from typing import Any
from urllib.parse import urlparse
from .clients import ResponsesClient, WorkspaceAgentClient
from .config import Config
from .dispatch import DispatchSink
from .errors import (
AuthenticationError,
ConflictError,
DispatchError,
OrchestratorError,
TriggerError,
ValidationError,
)
from .models import Decision, Event
from .securefs import ensure_secure_directory, ensure_shared_directory, read_secure_json
from .store import Store
from .sync import OpenClawSynchronizer, lifecycle_record
LOG = logging.getLogger("aios-orchestrator-gateway")
class Runtime:
def __init__(self, config: Config):
self.config = config
self.store = Store(config.database_path)
self.dispatch = DispatchSink(
config.dispatch_outbox_dir,
config.command_queue_dir,
config.contract_commit,
)
self.workspace = None
workspace_token = config.workspace_token()
if config.workspace_trigger_id and workspace_token:
self.workspace = WorkspaceAgentClient(
config.workspace_trigger_id,
workspace_token,
)
self.responses = None
responses_key = config.responses_api_key()
if responses_key and config.responses_model:
self.responses = ResponsesClient(
responses_key,
config.responses_model,
)
self.synchronizer = OpenClawSynchronizer(
config.openclaw_sync_dir,
endpoint=config.openclaw_sync_endpoint,
bearer_token=config.openclaw_sync_token(),
)
self.worker_id = f"{socket.gethostname()}-{os.getpid()}-{threading.get_native_id()}"
self.stop_event = threading.Event()
self.worker = threading.Thread(target=self._loop, daemon=True)
self._next_git_poll = 0.0
for directory in (
config.event_inbox_dir,
config.decision_inbox_dir,
config.dispatch_outbox_dir,
config.openclaw_sync_dir,
):
ensure_secure_directory(directory)
ensure_shared_directory(config.command_queue_dir)
def start(self) -> None:
self.worker.start()
def stop(self) -> None:
self.stop_event.set()
if self.worker.is_alive():
self.worker.join(timeout=15)
self.store.close()
def submit_event(self, payload: dict[str, Any]) -> tuple[bool, dict[str, Any]]:
event = Event.from_dict(payload)
if event.contract_commit != self.config.contract_commit:
raise ConflictError("contract_version_mismatch")
created, stored = self.store.register_event(event)
if created:
self._enqueue_event_lifecycle(event)
return created, stored
def submit_decision(self, payload: dict[str, Any]) -> int:
decision = Decision.from_dict(payload)
if decision.contract_commit != self.config.contract_commit:
raise ConflictError("contract_version_mismatch")
version = self.store.apply_decision(
decision,
max_hops_per_gate=self.config.max_hops_per_gate,
max_same_blocker_retries=self.config.max_same_blocker_retries,
)
self._enqueue_decision_lifecycle(decision, version)
return version
def _loop(self) -> None:
next_summary = 0.0
while not self.stop_event.wait(self.config.poll_interval_seconds):
try:
self.poll_once()
if time.monotonic() >= next_summary:
self.enqueue_periodic_summaries()
next_summary = time.monotonic() + self.config.summary_interval_seconds
except Exception:
LOG.exception("poll_cycle_failed")
def poll_once(self) -> None:
"""One deterministic reliability cycle.
This is deliberately callable from a systemd timer or health supervisor
in addition to the internal loop.
"""
self.ingest_file_inboxes()
self.poll_git_refs()
self.process_new_events()
self.process_workspace_timeouts()
self.poll_background_responses()
self.dispatch_work_units()
self.synchronize_openclaw()
def ingest_file_inboxes(self) -> None:
self._ingest_directory(self.config.event_inbox_dir, kind="event")
self._ingest_directory(self.config.decision_inbox_dir, kind="decision")
def _ingest_directory(self, directory: Path, *, kind: str) -> None:
pending = directory / "pending"
processed = directory / "processed"
rejected = directory / "rejected"
ensure_shared_directory(pending, group_writable=True)
ensure_secure_directory(processed)
ensure_secure_directory(rejected)
for path in sorted(pending.glob("*.json"))[:128]:
try:
payload = read_secure_json(path, expected_uid=None)
if kind == "event":
self.submit_event(payload)
else:
self.submit_decision(payload)
target = processed / path.name
except Exception as exc:
LOG.warning("file_inbox_rejected path=%s error=%s", path, type(exc).__name__)
target = rejected / path.name
if target.exists():
# Same file name is an idempotency replay. Keep original audit copy.
path.unlink()
else:
path.replace(target)
def poll_git_refs(self) -> None:
"""Poll authoritative local bare refs so missed webhooks cannot stall the loop."""
if not self.config.git_poll_enabled or self.config.git_poll_bare_repo is None:
return
now = time.monotonic()
if now < self._next_git_poll:
return
self._next_git_poll = now + self.config.git_poll_interval_seconds
command = [
"git",
f"--git-dir={self.config.git_poll_bare_repo}",
"for-each-ref",
"--format=%(refname) %(objectname)",
*self.config.git_poll_ref_prefixes,
]
try:
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
timeout=30,
env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"},
)
except (OSError, subprocess.SubprocessError):
LOG.exception("git_ref_poll_failed")
return
if result.returncode != 0:
LOG.warning("git_ref_poll_rc=%s", result.returncode)
return
refs: dict[str, str] = {}
for line in result.stdout.splitlines():
refname, sep, oid = line.partition(" ")
if not sep or not refname.startswith("refs/") or len(oid) != 40:
continue
if any(ch not in "0123456789abcdef" for ch in oid):
continue
refs[refname] = oid
initialized, changes = self.store.observe_refs(refs)
if not initialized or not changes:
return
payload_changes = [
{"ref": refname, "old_oid": old_oid, "new_oid": new_oid}
for refname, old_oid, new_oid in sorted(changes)
]
canonical = json.dumps(payload_changes, separators=(",", ":"), sort_keys=True)
event_id = "gitpoll-" + hashlib.sha256(canonical.encode()).hexdigest()[:40]
state = self.store.project_state(self.config.project_key)
state_version = 0 if state is None else int(state["state_version"])
event = Event.from_dict(
{
"schema_version": 1,
"project_key": self.config.project_key,
"contract_commit": self.config.contract_commit,
"event_id": event_id,
"source_agent": "gitea",
"event_type": "git_refs_changed",
"work_unit_id": "GIT-REF-POLL",
"status": "READY",
"state_version": state_version,
"implementation_commit": None,
"result_commit": None,
"blocker": None,
"evidence_refs": [],
"payload": {"changes": payload_changes},
}
)
try:
self.submit_event(event.to_dict())
except ConflictError:
LOG.info("git_ref_event_already_registered event_id=%s", event_id)
def process_new_events(self) -> None:
rows = self.store.claim_events(
owner=self.worker_id,
statuses=("RECEIVED", "TRIGGER_FAILED"),
limit=32,
lease_seconds=self.config.lease_seconds,
)
for row in rows:
event = Event.from_dict(json.loads(row["event_json"]))
attempts = int(row["attempts"])
try:
if self.workspace is not None:
self.workspace.trigger(event)
self.store.update_event_trigger(
event.event_id,
status="AWAITING_DECISION",
channel="workspace_agent",
remote_id=None,
)
elif self.responses is not None:
response_id = self.responses.start(event)
self.store.update_event_trigger(
event.event_id,
status="POLLING_RESPONSE",
channel="responses_api",
remote_id=response_id,
retry_after=self.config.poll_interval_seconds,
)
else:
raise TriggerError("no_model_channel")
except TriggerError as exc:
self._retry_or_dead_letter(event, attempts, str(exc))
def process_workspace_timeouts(self) -> None:
cutoff = int(time.time()) - self.config.callback_timeout_seconds
for row in self.store.claim_workspace_timeouts(
owner=self.worker_id,
older_than=cutoff,
limit=32,
lease_seconds=self.config.lease_seconds,
):
event = Event.from_dict(json.loads(row["event_json"]))
if self.responses is None:
self._retry_or_dead_letter(
event,
int(row["attempts"]),
"workspace_callback_timeout",
)
continue
try:
response_id = self.responses.start(event)
self.store.update_event_trigger(
event.event_id,
status="POLLING_RESPONSE",
channel="responses_api",
remote_id=response_id,
retry_after=self.config.poll_interval_seconds,
increment_attempt=True,
)
except TriggerError as exc:
self._retry_or_dead_letter(event, int(row["attempts"]), str(exc))
def poll_background_responses(self) -> None:
if self.responses is None:
return
for row in self.store.claim_response_polls(
owner=self.worker_id,
limit=32,
lease_seconds=self.config.lease_seconds,
):
event = Event.from_dict(json.loads(row["event_json"]))
response_id = row.get("remote_id")
if not isinstance(response_id, str):
self._retry_or_dead_letter(event, int(row["attempts"]), "missing_response_id")
continue
try:
status, decision = self.responses.poll(response_id)
if status in {"queued", "in_progress"}:
attempts = int(row["attempts"])
if attempts + 1 > self.config.max_retries:
self._retry_or_dead_letter(
event, attempts, f"response_{status}_retry_limit"
)
continue
self.store.update_event_trigger(
event.event_id,
status="POLLING_RESPONSE",
channel="responses_api",
remote_id=response_id,
retry_after=self.config.poll_interval_seconds,
increment_attempt=True,
)
continue
if decision is None:
raise TriggerError("completed_without_decision")
self.submit_decision(decision.to_dict())
except OrchestratorError as exc:
self._retry_or_dead_letter(event, int(row["attempts"]), str(exc))
def dispatch_work_units(self) -> None:
rows = self.store.claim_outbox(
owner=self.worker_id,
limit=64,
lease_seconds=self.config.lease_seconds,
)
for row in rows:
try:
outbox_path, queue_path = self.dispatch.dispatch(row)
self.store.mark_outbox(row["work_unit_id"], status="DISPATCHED")
payload = {
"assigned_agent": row["assigned_agent"],
"event_id": row["event_id"],
"dispatch_outbox": str(outbox_path),
"command_queue": str(queue_path),
}
record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=self._project_for_event(row["event_id"]),
kind="TASK_DISPATCHED",
work_unit_id=row["work_unit_id"],
payload=payload,
)
self.store.enqueue_lifecycle(
sync_id=record["sync_id"],
project_key=record["project_key"],
work_unit_id=row["work_unit_id"],
kind=record["kind"],
payload=record,
)
except Exception:
attempts = int(row["attempts"])
status = "DEAD_LETTER" if attempts >= self.config.max_retries else "RETRY"
self.store.mark_outbox(
row["work_unit_id"],
status=status,
retry_after=self._backoff(attempts),
increment_attempt=True,
)
LOG.exception("dispatch_failed work_unit=%s", row["work_unit_id"])
def synchronize_openclaw(self) -> None:
rows = self.store.claim_lifecycle(
owner=self.worker_id,
limit=64,
lease_seconds=self.config.lease_seconds,
)
for row in rows:
try:
payload = json.loads(row["payload_json"])
self.synchronizer.emit(payload)
self.store.mark_lifecycle(row["sync_id"], status="SENT")
except Exception:
attempts = int(row["attempts"])
status = "DEAD_LETTER" if attempts >= self.config.max_retries else "RETRY"
self.store.mark_lifecycle(
row["sync_id"],
status=status,
retry_after=self._backoff(attempts),
increment_attempt=True,
)
LOG.exception("openclaw_sync_failed sync_id=%s", row["sync_id"])
def enqueue_periodic_summaries(self) -> None:
for state in self.store.all_project_states():
project_key = state["project_key"]
if not self.store.summary_due(project_key, self.config.summary_interval_seconds):
continue
summary = self.store.project_summary(project_key)
if summary is None:
continue
digest = hashlib.sha256(
json.dumps(summary, separators=(",", ":"), sort_keys=True).encode()
).hexdigest()
record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=project_key,
kind="PROJECT_SUMMARY",
work_unit_id=None,
payload=summary,
)
self.store.enqueue_lifecycle(
sync_id=record["sync_id"],
project_key=project_key,
work_unit_id=None,
kind=record["kind"],
payload=record,
)
self.store.mark_summary(project_key, digest)
def _enqueue_event_lifecycle(self, event: Event) -> None:
if event.event_type == "work_unit_started":
kind = "TASK_STARTED"
elif event.event_type in {"work_unit_completed", "work_unit_failed", "work_unit_blocked"}:
kind = "TASK_ENDED"
else:
kind = "EVENT_RECEIVED"
record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=event.project_key,
kind=kind,
work_unit_id=event.work_unit_id,
payload=event.to_dict(),
)
self.store.enqueue_lifecycle(
sync_id=record["sync_id"],
project_key=event.project_key,
work_unit_id=event.work_unit_id,
kind=kind,
payload=record,
)
def _enqueue_decision_lifecycle(self, decision: Decision, version: int) -> None:
kind = "PROJECT_COMPLETED" if decision.verdict == "COMPLETE" else "DECISION_APPLIED"
record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=decision.project_key,
kind=kind,
work_unit_id=None,
payload={"decision": decision.to_dict(), "state_version": version},
)
self.store.enqueue_lifecycle(
sync_id=record["sync_id"],
project_key=decision.project_key,
work_unit_id=None,
kind=kind,
payload=record,
)
summary = self.store.project_summary(decision.project_key)
if summary:
summary_record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=decision.project_key,
kind="PROJECT_SUMMARY",
work_unit_id=None,
payload=summary,
)
self.store.enqueue_lifecycle(
sync_id=summary_record["sync_id"],
project_key=decision.project_key,
work_unit_id=None,
kind=summary_record["kind"],
payload=summary_record,
)
def _project_for_event(self, event_id: str) -> str:
rows = self.store._conn().execute(
"SELECT project_key FROM events WHERE event_id=?",
(event_id,),
).fetchone()
if rows is None:
raise ConflictError("event_not_found")
return str(rows["project_key"])
def _retry_or_dead_letter(self, event: Event, attempts: int, reason: str) -> None:
next_attempt = attempts + 1
if next_attempt > self.config.max_retries:
self.store.update_event_trigger(
event.event_id,
status="DEAD_LETTER",
channel=None,
remote_id=None,
increment_attempt=True,
)
record = lifecycle_record(
contract_commit=self.config.contract_commit,
project_key=event.project_key,
kind="TASK_ENDED",
work_unit_id=event.work_unit_id,
payload={
"status": "FAIL",
"reason": reason,
"event_id": event.event_id,
"attempts": next_attempt,
},
)
self.store.enqueue_lifecycle(
sync_id=record["sync_id"],
project_key=event.project_key,
work_unit_id=event.work_unit_id,
kind=record["kind"],
payload=record,
)
return
self.store.update_event_trigger(
event.event_id,
status="TRIGGER_FAILED",
channel=None,
remote_id=None,
retry_after=self._backoff(attempts),
increment_attempt=True,
)
def _backoff(self, attempts: int) -> int:
return min(3600, self.config.retry_base_seconds * (2 ** min(attempts, 10)))
class Handler(BaseHTTPRequestHandler):
runtime: Runtime
def log_message(self, fmt: str, *args: object) -> None:
LOG.info("%s %s", self.client_address[0], fmt % args)
def _reply(self, status: int, payload: dict[str, Any]) -> None:
body = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def _body(self) -> dict[str, Any]:
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError as exc:
raise ValidationError("invalid_content_length") from exc
if length <= 0 or length > 1_048_576:
raise ValidationError("invalid_body_size")
try:
value = json.loads(self.rfile.read(length))
except json.JSONDecodeError as exc:
raise ValidationError("invalid_json") from exc
if not isinstance(value, dict):
raise ValidationError("json_root_not_object")
return value
def _authenticate(self, expected: str) -> None:
header = self.headers.get("Authorization", "")
if not header.startswith("Bearer "):
raise AuthenticationError("missing_bearer")
if not hmac.compare_digest(header[7:], expected):
raise AuthenticationError("invalid_bearer")
def do_GET(self) -> None:
path = urlparse(self.path).path.rstrip("/") or "/"
try:
if path == "/healthz":
self._reply(
200,
{
"service": "aios-orchestrator-gateway",
"status": "active",
"contract_commit": self.runtime.config.contract_commit,
"polling": True,
},
)
return
if path.startswith("/v1/orchestrator/state/"):
self._authenticate(self.runtime.config.inbound_token())
key = path.rsplit("/", 1)[-1]
state = self.runtime.store.project_summary(key)
self._reply(200 if state else 404, state or {"error": "project_not_found"})
return
self._reply(404, {"error": "not_found"})
except AuthenticationError as exc:
self._reply(401, {"error": str(exc)})
except OrchestratorError as exc:
self._reply(400, {"error": str(exc)})
def do_POST(self) -> None:
path = urlparse(self.path).path.rstrip("/")
try:
if path == "/v1/orchestrator/events":
self._authenticate(self.runtime.config.inbound_token())
created, event = self.runtime.submit_event(self._body())
self._reply(202 if created else 200, {"created": created, "event": event})
return
if path == "/v1/orchestrator/decisions":
self._authenticate(self.runtime.config.callback_token())
version = self.runtime.submit_decision(self._body())
self._reply(202, {"accepted": True, "state_version": version})
return
if path == "/v1/orchestrator/poll":
self._authenticate(self.runtime.config.inbound_token())
self.runtime.poll_once()
self._reply(200, {"polled": True})
return
self._reply(404, {"error": "not_found"})
except AuthenticationError as exc:
self._reply(401, {"error": str(exc)})
except ConflictError as exc:
self._reply(409, {"error": str(exc)})
except ValidationError as exc:
self._reply(422, {"error": str(exc)})
except OrchestratorError as exc:
self._reply(400, {"error": str(exc)})
except Exception:
LOG.exception("request_failed path=%s", path)
self._reply(500, {"error": "internal_error"})
def serve(config_path: Path) -> None:
config = Config.from_file(config_path)
runtime = Runtime(config)
Handler.runtime = runtime
server = ThreadingHTTPServer((config.host, config.port), Handler)
runtime.start()
LOG.info(
"gateway_started host=%s port=%s contract=%s polling=%ss",
config.host,
config.port,
config.contract_commit,
config.poll_interval_seconds,
)
try:
server.serve_forever(poll_interval=0.5)
finally:
server.server_close()
runtime.stop()
-826
View File
@@ -1,826 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
import sqlite3
import threading
import time
from typing import Any
from .errors import ConflictError
from .models import Decision, Event
from .securefs import ensure_secure_directory
class Store:
"""Durable state machine with idempotency, optimistic versioning, and leases."""
def __init__(self, path: Path):
self.path = Path(path)
ensure_secure_directory(self.path.parent)
self._local = threading.local()
self._initialize()
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.path, timeout=30, isolation_level=None)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=FULL")
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=30000")
return conn
def _conn(self) -> sqlite3.Connection:
conn = getattr(self._local, "conn", None)
if conn is None:
conn = self._connect()
self._local.conn = conn
return conn
def _initialize(self) -> None:
conn = self._connect()
try:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS project_state (
project_key TEXT PRIMARY KEY,
contract_commit TEXT NOT NULL,
state_version INTEGER NOT NULL,
state_json TEXT NOT NULL,
hops INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS gate_hops (
project_key TEXT NOT NULL,
gate_id TEXT NOT NULL,
hops INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL,
PRIMARY KEY(project_key, gate_id)
);
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
project_key TEXT NOT NULL,
digest TEXT NOT NULL,
event_json TEXT NOT NULL,
status TEXT NOT NULL,
channel TEXT,
remote_id TEXT,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at INTEGER NOT NULL DEFAULT 0,
lease_owner TEXT,
lease_until INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_poll
ON events(status, next_attempt_at, lease_until, created_at);
CREATE TABLE IF NOT EXISTS decisions (
event_id TEXT PRIMARY KEY REFERENCES events(event_id),
project_key TEXT NOT NULL,
decision_json TEXT NOT NULL,
state_version_after INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS outbox (
work_unit_id TEXT PRIMARY KEY,
event_id TEXT NOT NULL REFERENCES events(event_id),
assigned_agent TEXT NOT NULL,
payload_json TEXT NOT NULL,
status TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at INTEGER NOT NULL DEFAULT 0,
lease_owner TEXT,
lease_until INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_outbox_poll
ON outbox(status, next_attempt_at, lease_until, created_at);
CREATE TABLE IF NOT EXISTS lifecycle (
sync_id TEXT PRIMARY KEY,
project_key TEXT NOT NULL,
work_unit_id TEXT,
kind TEXT NOT NULL,
payload_json TEXT NOT NULL,
status TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at INTEGER NOT NULL DEFAULT 0,
lease_owner TEXT,
lease_until INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_lifecycle_poll
ON lifecycle(status, next_attempt_at, lease_until, created_at);
CREATE TABLE IF NOT EXISTS blocker_history (
project_key TEXT NOT NULL,
gate_id TEXT NOT NULL,
blocker TEXT NOT NULL,
repetitions INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY(project_key, gate_id, blocker)
);
CREATE TABLE IF NOT EXISTS summaries (
project_key TEXT PRIMARY KEY,
last_emitted_at INTEGER NOT NULL DEFAULT 0,
last_digest TEXT
);
CREATE TABLE IF NOT EXISTS observed_refs (
refname TEXT PRIMARY KEY,
object_id TEXT NOT NULL,
observed_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
"""
)
finally:
conn.close()
def register_event(self, event: Event) -> tuple[bool, dict[str, Any]]:
now = int(time.time())
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
existing = conn.execute(
"SELECT digest, event_json FROM events WHERE event_id=?",
(event.event_id,),
).fetchone()
if existing:
if existing["digest"] != event.digest():
raise ConflictError("event_id_payload_conflict")
conn.execute("COMMIT")
return False, json.loads(existing["event_json"])
state = conn.execute(
"SELECT contract_commit, state_version FROM project_state WHERE project_key=?",
(event.project_key,),
).fetchone()
if state is None:
conn.execute(
"""
INSERT INTO project_state(
project_key, contract_commit, state_version, state_json,
hops, updated_at
) VALUES (?, ?, ?, '{}', 0, ?)
""",
(event.project_key, event.contract_commit, event.state_version, now),
)
else:
if state["contract_commit"] != event.contract_commit:
raise ConflictError("contract_version_mismatch")
if state["state_version"] != event.state_version:
raise ConflictError("stale_state_version")
conn.execute(
"""
INSERT INTO events(
event_id, project_key, digest, event_json, status,
next_attempt_at, created_at, updated_at
) VALUES (?, ?, ?, ?, 'RECEIVED', ?, ?, ?)
""",
(
event.event_id,
event.project_key,
event.digest(),
event.canonical_json(),
now,
now,
now,
),
)
conn.execute("COMMIT")
return True, event.to_dict()
except Exception:
conn.execute("ROLLBACK")
raise
def claim_events(
self,
*,
owner: str,
statuses: tuple[str, ...],
limit: int,
lease_seconds: int,
) -> list[dict[str, Any]]:
if not statuses:
return []
now = int(time.time())
placeholders = ",".join("?" for _ in statuses)
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
rows = conn.execute(
f"""
SELECT event_id FROM events
WHERE status IN ({placeholders})
AND next_attempt_at <= ?
AND (lease_until IS NULL OR lease_until < ?)
ORDER BY created_at
LIMIT ?
""",
(*statuses, now, now, limit),
).fetchall()
ids = [row["event_id"] for row in rows]
for event_id in ids:
conn.execute(
"""
UPDATE events SET lease_owner=?, lease_until=?, updated_at=?
WHERE event_id=?
""",
(owner, now + lease_seconds, now, event_id),
)
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
if not ids:
return []
placeholders = ",".join("?" for _ in ids)
return [
dict(row)
for row in conn.execute(
f"SELECT * FROM events WHERE event_id IN ({placeholders})",
ids,
).fetchall()
]
def update_event_trigger(
self,
event_id: str,
*,
status: str,
channel: str | None,
remote_id: str | None,
retry_after: int = 0,
increment_attempt: bool = False,
) -> None:
now = int(time.time())
self._conn().execute(
"""
UPDATE events
SET status=?, channel=?, remote_id=?,
attempts=attempts + ?,
next_attempt_at=?,
lease_owner=NULL, lease_until=NULL, updated_at=?
WHERE event_id=?
""",
(
status,
channel,
remote_id,
1 if increment_attempt else 0,
now + max(0, retry_after),
now,
event_id,
),
)
def claim_workspace_timeouts(
self,
*,
owner: str,
older_than: int,
limit: int,
lease_seconds: int,
) -> list[dict[str, Any]]:
return self._claim_event_subset(
owner=owner,
where_sql="status='AWAITING_DECISION' AND channel='workspace_agent' AND updated_at <= ?",
where_params=(older_than,),
limit=limit,
lease_seconds=lease_seconds,
)
def claim_response_polls(
self,
*,
owner: str,
limit: int,
lease_seconds: int,
) -> list[dict[str, Any]]:
now = int(time.time())
return self._claim_event_subset(
owner=owner,
where_sql="status='POLLING_RESPONSE' AND next_attempt_at <= ?",
where_params=(now,),
limit=limit,
lease_seconds=lease_seconds,
)
def _claim_event_subset(
self,
*,
owner: str,
where_sql: str,
where_params: tuple[Any, ...],
limit: int,
lease_seconds: int,
) -> list[dict[str, Any]]:
allowed = {
"status='AWAITING_DECISION' AND channel='workspace_agent' AND updated_at <= ?",
"status='POLLING_RESPONSE' AND next_attempt_at <= ?",
}
if where_sql not in allowed:
raise ValueError("invalid_event_claim_query")
now = int(time.time())
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
rows = conn.execute(
f"""
SELECT event_id FROM events
WHERE {where_sql}
AND (lease_until IS NULL OR lease_until < ?)
ORDER BY updated_at
LIMIT ?
""",
(*where_params, now, limit),
).fetchall()
ids = [str(row["event_id"]) for row in rows]
for event_id in ids:
conn.execute(
"""
UPDATE events SET lease_owner=?, lease_until=?
WHERE event_id=?
""",
(owner, now + lease_seconds, event_id),
)
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
if not ids:
return []
placeholders = ",".join("?" for _ in ids)
return [
dict(row)
for row in conn.execute(
f"SELECT * FROM events WHERE event_id IN ({placeholders})",
ids,
).fetchall()
]
def apply_decision(
self,
decision: Decision,
*,
max_hops_per_gate: int,
max_same_blocker_retries: int,
) -> int:
now = int(time.time())
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
event = conn.execute(
"SELECT project_key FROM events WHERE event_id=?",
(decision.event_id,),
).fetchone()
if event is None:
raise ConflictError("event_not_found")
if event["project_key"] != decision.project_key:
raise ConflictError("event_project_mismatch")
existing = conn.execute(
"SELECT decision_json, state_version_after FROM decisions WHERE event_id=?",
(decision.event_id,),
).fetchone()
if existing:
if json.loads(existing["decision_json"]) != decision.to_dict():
raise ConflictError("decision_idempotency_conflict")
conn.execute("COMMIT")
return int(existing["state_version_after"])
state = conn.execute(
"SELECT * FROM project_state WHERE project_key=?",
(decision.project_key,),
).fetchone()
if state is None:
raise ConflictError("project_state_not_found")
if state["contract_commit"] != decision.contract_commit:
raise ConflictError("contract_version_mismatch")
if state["state_version"] != decision.expected_state_version:
raise ConflictError("stale_decision_state_version")
state_json = json.loads(state["state_json"])
gate = str(
decision.state_updates.get(
"current_gate", state_json.get("current_gate", "unknown")
)
)
gate_hops = conn.execute(
"SELECT hops FROM gate_hops WHERE project_key=? AND gate_id=?",
(decision.project_key, gate),
).fetchone()
if gate_hops is not None and int(gate_hops["hops"]) >= max_hops_per_gate:
raise ConflictError("max_hops_exceeded")
state_json.update(decision.state_updates)
next_version = int(state["state_version"]) + 1
if decision.stop_reason:
row = conn.execute(
"""
SELECT repetitions FROM blocker_history
WHERE project_key=? AND gate_id=? AND blocker=?
""",
(decision.project_key, gate, decision.stop_reason),
).fetchone()
repetitions = (int(row["repetitions"]) if row else 0) + 1
conn.execute(
"""
INSERT INTO blocker_history(
project_key, gate_id, blocker, repetitions, updated_at
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(project_key, gate_id, blocker)
DO UPDATE SET repetitions=excluded.repetitions,
updated_at=excluded.updated_at
""",
(decision.project_key, gate, decision.stop_reason, repetitions, now),
)
if repetitions > max_same_blocker_retries:
raise ConflictError("same_blocker_retry_limit_exceeded")
conn.execute(
"""
INSERT INTO gate_hops(project_key, gate_id, hops, updated_at)
VALUES (?, ?, 1, ?)
ON CONFLICT(project_key, gate_id)
DO UPDATE SET hops=gate_hops.hops + 1, updated_at=excluded.updated_at
""",
(decision.project_key, gate, now),
)
conn.execute(
"""
UPDATE project_state
SET state_version=?, state_json=?, hops=hops+1, updated_at=?
WHERE project_key=?
""",
(
next_version,
json.dumps(state_json, separators=(",", ":"), sort_keys=True),
now,
decision.project_key,
),
)
conn.execute(
"""
INSERT INTO decisions(
event_id, project_key, decision_json,
state_version_after, created_at
) VALUES (?, ?, ?, ?, ?)
""",
(
decision.event_id,
decision.project_key,
json.dumps(decision.to_dict(), separators=(",", ":"), sort_keys=True),
next_version,
now,
),
)
for unit in decision.next_work_units:
conn.execute(
"""
INSERT INTO outbox(
work_unit_id, event_id, assigned_agent, payload_json,
status, next_attempt_at, created_at, updated_at
) VALUES (?, ?, ?, ?, 'PENDING', ?, ?, ?)
""",
(
unit.id,
decision.event_id,
unit.primary_agent,
json.dumps(unit.to_dict(), separators=(",", ":"), sort_keys=True),
now,
now,
now,
),
)
conn.execute(
"""
UPDATE events SET status='DECIDED', lease_owner=NULL,
lease_until=NULL, updated_at=?
WHERE event_id=?
""",
(now, decision.event_id),
)
conn.execute("COMMIT")
return next_version
except Exception:
conn.execute("ROLLBACK")
raise
def claim_outbox(self, *, owner: str, limit: int, lease_seconds: int) -> list[dict[str, Any]]:
return self._claim_table(
table="outbox",
key="work_unit_id",
owner=owner,
statuses=("PENDING", "RETRY"),
limit=limit,
lease_seconds=lease_seconds,
)
def mark_outbox(
self,
work_unit_id: str,
*,
status: str,
retry_after: int = 0,
increment_attempt: bool = False,
) -> None:
self._mark_table(
table="outbox",
key="work_unit_id",
value=work_unit_id,
status=status,
retry_after=retry_after,
increment_attempt=increment_attempt,
)
def enqueue_lifecycle(
self,
*,
sync_id: str,
project_key: str,
work_unit_id: str | None,
kind: str,
payload: dict[str, Any],
) -> None:
now = int(time.time())
encoded = json.dumps(payload, separators=(",", ":"), sort_keys=True)
self._conn().execute(
"""
INSERT OR IGNORE INTO lifecycle(
sync_id, project_key, work_unit_id, kind, payload_json,
status, next_attempt_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, 'PENDING', ?, ?, ?)
""",
(sync_id, project_key, work_unit_id, kind, encoded, now, now, now),
)
def claim_lifecycle(self, *, owner: str, limit: int, lease_seconds: int) -> list[dict[str, Any]]:
return self._claim_table(
table="lifecycle",
key="sync_id",
owner=owner,
statuses=("PENDING", "RETRY"),
limit=limit,
lease_seconds=lease_seconds,
)
def mark_lifecycle(
self,
sync_id: str,
*,
status: str,
retry_after: int = 0,
increment_attempt: bool = False,
) -> None:
self._mark_table(
table="lifecycle",
key="sync_id",
value=sync_id,
status=status,
retry_after=retry_after,
increment_attempt=increment_attempt,
)
def _claim_table(
self,
*,
table: str,
key: str,
owner: str,
statuses: tuple[str, ...],
limit: int,
lease_seconds: int,
) -> list[dict[str, Any]]:
if table not in {"outbox", "lifecycle"} or key not in {"work_unit_id", "sync_id"}:
raise ValueError("invalid_claim_table")
now = int(time.time())
placeholders = ",".join("?" for _ in statuses)
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
rows = conn.execute(
f"""
SELECT {key} FROM {table}
WHERE status IN ({placeholders})
AND next_attempt_at <= ?
AND (lease_until IS NULL OR lease_until < ?)
ORDER BY created_at
LIMIT ?
""",
(*statuses, now, now, limit),
).fetchall()
values = [row[key] for row in rows]
for item in values:
conn.execute(
f"""
UPDATE {table} SET lease_owner=?, lease_until=?, updated_at=?
WHERE {key}=?
""",
(owner, now + lease_seconds, now, item),
)
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
if not values:
return []
placeholders = ",".join("?" for _ in values)
return [
dict(row)
for row in conn.execute(
f"SELECT * FROM {table} WHERE {key} IN ({placeholders})",
values,
).fetchall()
]
def _mark_table(
self,
*,
table: str,
key: str,
value: str,
status: str,
retry_after: int,
increment_attempt: bool,
) -> None:
if table not in {"outbox", "lifecycle"} or key not in {"work_unit_id", "sync_id"}:
raise ValueError("invalid_mark_table")
now = int(time.time())
self._conn().execute(
f"""
UPDATE {table}
SET status=?, attempts=attempts + ?, next_attempt_at=?,
lease_owner=NULL, lease_until=NULL, updated_at=?
WHERE {key}=?
""",
(
status,
1 if increment_attempt else 0,
now + max(0, retry_after),
now,
value,
),
)
def project_state(self, project_key: str) -> dict[str, Any] | None:
row = self._conn().execute(
"SELECT * FROM project_state WHERE project_key=?",
(project_key,),
).fetchone()
if row is None:
return None
result = dict(row)
result["state"] = json.loads(result.pop("state_json"))
return result
def all_project_states(self) -> list[dict[str, Any]]:
result = []
for row in self._conn().execute("SELECT * FROM project_state ORDER BY project_key").fetchall():
item = dict(row)
item["state"] = json.loads(item.pop("state_json"))
result.append(item)
return result
def summary_due(self, project_key: str, interval: int) -> bool:
now = int(time.time())
row = self._conn().execute(
"SELECT last_emitted_at FROM summaries WHERE project_key=?",
(project_key,),
).fetchone()
return row is None or now - int(row["last_emitted_at"]) >= interval
def mark_summary(self, project_key: str, digest: str) -> None:
now = int(time.time())
self._conn().execute(
"""
INSERT INTO summaries(project_key, last_emitted_at, last_digest)
VALUES (?, ?, ?)
ON CONFLICT(project_key)
DO UPDATE SET last_emitted_at=excluded.last_emitted_at,
last_digest=excluded.last_digest
""",
(project_key, now, digest),
)
def project_summary(self, project_key: str) -> dict[str, Any] | None:
state = self.project_state(project_key)
if state is None:
return None
conn = self._conn()
event_counts = {
row["status"]: int(row["count"])
for row in conn.execute(
"""
SELECT status, COUNT(*) AS count FROM events
WHERE project_key=? GROUP BY status
""",
(project_key,),
).fetchall()
}
outbox_counts = {
row["status"]: int(row["count"])
for row in conn.execute(
"""
SELECT o.status, COUNT(*) AS count
FROM outbox o JOIN events e ON e.event_id=o.event_id
WHERE e.project_key=? GROUP BY o.status
""",
(project_key,),
).fetchall()
}
return {
"project_key": project_key,
"contract_commit": state["contract_commit"],
"state_version": state["state_version"],
"state": state["state"],
"event_counts": event_counts,
"work_unit_counts": outbox_counts,
"updated_at": state["updated_at"],
}
def metadata_get(self, key: str) -> str | None:
row = self._conn().execute(
"SELECT value FROM metadata WHERE key=?",
(key,),
).fetchone()
return None if row is None else str(row["value"])
def metadata_set(self, key: str, value: str) -> None:
self._conn().execute(
"""
INSERT INTO metadata(key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""",
(key, value),
)
def observe_refs(self, refs: dict[str, str]) -> tuple[bool, list[tuple[str, str | None, str]]]:
"""Persist a Git snapshot and return changes after the initial baseline."""
now = int(time.time())
conn = self._conn()
conn.execute("BEGIN IMMEDIATE")
try:
initialized = conn.execute(
"SELECT value FROM metadata WHERE key='git_poll_initialized'"
).fetchone()
previous = {
row["refname"]: row["object_id"]
for row in conn.execute("SELECT refname, object_id FROM observed_refs").fetchall()
}
changes: list[tuple[str, str | None, str]] = []
if initialized is not None:
for refname, oid in refs.items():
old = previous.get(refname)
if old != oid:
changes.append((refname, old, oid))
for refname, old in previous.items():
if refname not in refs:
changes.append((refname, old, "0" * 40))
conn.execute("DELETE FROM observed_refs")
for refname, oid in refs.items():
conn.execute(
"""
INSERT INTO observed_refs(refname, object_id, observed_at)
VALUES (?, ?, ?)
""",
(refname, oid, now),
)
conn.execute(
"""
INSERT INTO metadata(key, value)
VALUES ('git_poll_initialized', '1')
ON CONFLICT(key) DO UPDATE SET value='1'
"""
)
conn.execute("COMMIT")
return initialized is not None, changes
except Exception:
conn.execute("ROLLBACK")
raise
def close(self) -> None:
conn = getattr(self._local, "conn", None)
if conn is not None:
conn.close()
self._local.conn = None
-108
View File
@@ -1,108 +0,0 @@
from __future__ import annotations
import hashlib
import json
import os
import stat
from pathlib import Path
import urllib.error
import urllib.request
from typing import Any
from .errors import DispatchError
from .securefs import atomic_write_bytes_shared, ensure_shared_directory, read_secure_bytes
class OpenClawSynchronizer:
"""Synchronize task starts, ends, dispatches, and project summaries.
A durable file is always emitted. An optional HTTP endpoint is best-effort;
failure leaves the lifecycle record pending for retry.
"""
def __init__(
self,
queue_dir: Path,
*,
endpoint: str | None = None,
bearer_token: str | None = None,
timeout_seconds: int = 10,
):
self.queue_dir = Path(queue_dir)
self.endpoint = endpoint
self.bearer_token = bearer_token
self.timeout_seconds = timeout_seconds
ensure_shared_directory(self.queue_dir)
def emit(self, record: dict[str, Any]) -> Path:
sync_id = record.get("sync_id")
if not isinstance(sync_id, str) or not sync_id:
raise DispatchError("invalid_sync_id")
payload = (json.dumps(record, indent=2, sort_keys=True) + "\n").encode()
path = self.queue_dir / "pending" / f"{sync_id}.json"
ensure_shared_directory(path.parent)
try:
st = os.lstat(path)
except FileNotFoundError:
atomic_write_bytes_shared(path, payload, mode=0o640)
else:
if not stat.S_ISREG(st.st_mode):
raise DispatchError("sync_target_not_regular")
if read_secure_bytes(path, expected_uid=None, max_bytes=1_048_576) != payload:
raise DispatchError("sync_idempotency_conflict")
if self.endpoint:
headers = {
"Content-Type": "application/json",
"Idempotency-Key": sync_id,
"User-Agent": "aios-orchestrator-gateway/0.2",
}
if self.bearer_token:
headers["Authorization"] = f"Bearer {self.bearer_token}"
request = urllib.request.Request(
self.endpoint,
data=payload,
method="POST",
headers=headers,
)
try:
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
response.read(65_536)
if response.status not in {200, 201, 202, 204}:
raise DispatchError(f"openclaw_sync_http_{response.status}")
except urllib.error.HTTPError as exc:
raise DispatchError(f"openclaw_sync_http_{exc.code}") from exc
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise DispatchError("openclaw_sync_failed") from exc
return path
def lifecycle_record(
*,
contract_commit: str,
project_key: str,
kind: str,
work_unit_id: str | None,
payload: dict[str, Any],
) -> dict[str, Any]:
canonical = json.dumps(
{
"contract_commit": contract_commit,
"project_key": project_key,
"kind": kind,
"work_unit_id": work_unit_id,
"payload": payload,
},
separators=(",", ":"),
sort_keys=True,
)
sync_id = hashlib.sha256(canonical.encode()).hexdigest()
return {
"schema_version": 1,
"sync_id": sync_id,
"contract_commit": contract_commit,
"project_key": project_key,
"kind": kind,
"work_unit_id": work_unit_id,
"payload": payload,
}
@@ -1,37 +0,0 @@
[Unit]
Description=AIOS Orchestrator Gateway reliability poll
After=aios-orchestrator-gateway.service
ConditionPathExists=/etc/aios-orchestrator-gateway/config.json
[Service]
Type=oneshot
User=aios-orchestrator
Group=aios-orchestrator
SupplementaryGroups=gitops
WorkingDirectory=/opt/aios-orchestrator-gateway/current
ExecStart=/usr/bin/python3 -m services.aios_orchestrator_gateway --config /etc/aios-orchestrator-gateway/config.json --poll-once
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
LockPersonality=true
CapabilityBoundingSet=
AmbientCapabilities=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
StateDirectory=aios-orchestrator-gateway
ReadOnlyPaths=/etc/aios-orchestrator-gateway /opt/aios-orchestrator-gateway
ReadWritePaths=/var/lib/aios-orchestrator-gateway
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
@@ -1,12 +0,0 @@
[Unit]
Description=Periodically poll AIOS orchestration inboxes and model responses
[Timer]
OnBootSec=45s
OnUnitActiveSec=60s
RandomizedDelaySec=10s
Persistent=true
Unit=aios-orchestrator-gateway-poll.service
[Install]
WantedBy=timers.target
-52
View File
@@ -1,52 +0,0 @@
[Unit]
Description=AIOS Orchestrator Gateway
After=network-online.target
Wants=network-online.target
ConditionPathExists=/etc/aios-orchestrator-gateway/config.json
[Service]
Type=simple
User=aios-orchestrator
Group=aios-orchestrator
SupplementaryGroups=gitops
WorkingDirectory=/opt/aios-orchestrator-gateway/current
ExecStart=/usr/bin/python3 -m services.aios_orchestrator_gateway --config /etc/aios-orchestrator-gateway/config.json
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30s
TimeoutStopSec=30s
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictRealtime=true
CapabilityBoundingSet=
AmbientCapabilities=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
StateDirectory=aios-orchestrator-gateway
RuntimeDirectory=aios-orchestrator-gateway
ReadOnlyPaths=/etc/aios-orchestrator-gateway /opt/aios-orchestrator-gateway
ReadWritePaths=/var/lib/aios-orchestrator-gateway
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
@@ -1,2 +0,0 @@
g aios-orchestrator -
u aios-orchestrator - "AIOS Orchestrator Gateway" /var/lib/aios-orchestrator-gateway /usr/sbin/nologin
@@ -1,19 +0,0 @@
d /etc/aios-orchestrator-gateway 0700 root root -
d /var/lib/aios-orchestrator-gateway 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/events 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/events/pending 2770 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/inbox/events/processed 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/events/rejected 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/decisions 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/decisions/pending 2770 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/inbox/decisions/processed 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/inbox/decisions/rejected 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/outbox 0700 aios-orchestrator aios-orchestrator -
d /var/lib/aios-orchestrator-gateway/openclaw-sync 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/openclaw-sync/pending 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/commands 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/commands/openclaw 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/commands/openclaw/pending 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/commands/codex 2750 aios-orchestrator gitops -
d /var/lib/aios-orchestrator-gateway/commands/codex/pending 2750 aios-orchestrator gitops -
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
GITEA="${AIOS_GITEA_BINARY:-/data/aios-gitops/runtime/bin/gitea}"
CONFIG="${AIOS_GITEA_CONFIG:-/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini}"
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-$(cd "$(dirname "$0")/.." && pwd)}"
USER_NAME="tagatomicitytest$(date +%s)"
TMP_DIR="$(mktemp -d /tmp/aios-gitea-live-test.XXXXXX)"
cleanup() {
"$GITEA" --config "$CONFIG" admin user delete \
--username "$USER_NAME" --purge >/dev/null 2>&1 || true
rm -f "$TMP_DIR/token" "$TMP_DIR/create-output"
rmdir "$TMP_DIR" 2>/dev/null || true
}
trap cleanup EXIT
"$GITEA" --config "$CONFIG" admin user create \
--username "$USER_NAME" \
--email "$USER_NAME@example.invalid" \
--random-password \
--must-change-password=false >"$TMP_DIR/create-output" 2>&1
"$GITEA" --config "$CONFIG" admin user generate-access-token \
--username "$USER_NAME" \
--token-name live-temporary-repository-test \
--raw \
--scopes write:repository,write:user >"$TMP_DIR/token"
chmod 600 "$TMP_DIR/token"
cd "$WORKSPACE"
AIOS_GITOPS_LIVE_GITEA_TEST=1 \
AIOS_GITOPS_LIVE_GITEA_OWNER="$USER_NAME" \
AIOS_GITOPS_LIVE_GITEA_TOKEN_FILE="$TMP_DIR/token" \
AIOS_GITOPS_LIVE_GITEA_TOKEN_LINE=1 \
python3 -m pytest -q tests/test_gitea_tag_publication_live.py
-774
View File
@@ -1,774 +0,0 @@
from __future__ import annotations
import json
import os
import stat
from pathlib import Path
import tempfile
import time
import unittest
import urllib.error
from unittest import mock
from services.aios_orchestrator_gateway.clients import ResponsesClient, WorkspaceAgentClient
from services.aios_orchestrator_gateway.config import Config
from services.aios_orchestrator_gateway.dispatch import DispatchSink
from services.aios_orchestrator_gateway.errors import ConflictError, DispatchError, ValidationError
from services.aios_orchestrator_gateway.models import Decision, Event
from services.aios_orchestrator_gateway.service import Runtime
from services.aios_orchestrator_gateway.store import Store
from services.aios_orchestrator_gateway.sync import OpenClawSynchronizer, lifecycle_record
from scripts import report_orchestrator_event
CONTRACT = "cbe33ecab8e9794ba5f318bea43aeaf447a56fbc"
BASE = "8d9c6da90fc8caa69b691d243c6fefb40b6b0575"
def event_payload(**overrides):
value = {
"schema_version": 1,
"project_key": "v2r1-personal-weixin",
"contract_commit": CONTRACT,
"event_id": "evt-001",
"source_agent": "openclaw",
"event_type": "work_unit_completed",
"work_unit_id": "P1-REACK",
"status": "PASS",
"state_version": 0,
"implementation_commit": None,
"result_commit": None,
"blocker": None,
"evidence_refs": [],
"payload": {},
}
value.update(overrides)
return value
def decision_payload(**overrides):
value = {
"schema_version": 1,
"project_key": "v2r1-personal-weixin",
"contract_commit": CONTRACT,
"event_id": "evt-001",
"expected_state_version": 0,
"verdict": "DISPATCH",
"state_updates": [
{"key": "P1_REPAIR3_CODE", "value": "PASS_CANDIDATE_PREPARED"},
{"key": "current_gate", "value": "R0_RUNNER_LIVE_RECOVERY"},
],
"next_work_units": [
{
"id": "R0-DISCOVER",
"track_id": "R_RUNNER",
"gate_id": "R0_RUNNER_LIVE_RECOVERY",
"primary_agent": "openclaw",
"fallback_agent": "codex",
"clean_base_commit": BASE,
"allowed_paths": [],
"required_checks": ["secret_provider_metadata"],
"forbidden_operations": ["force_push"],
"instruction": "Discover provider metadata without reading secrets.",
}
],
"requires_human_input": False,
"stop_reason": None,
}
value.update(overrides)
return value
class ModelTests(unittest.TestCase):
def test_event_accepts_valid_payload(self):
event = Event.from_dict(event_payload())
self.assertEqual(event.event_id, "evt-001")
self.assertEqual(len(event.digest()), 64)
def test_event_rejects_extra_fields(self):
with self.assertRaises(ValidationError):
Event.from_dict(event_payload(extra=True))
def test_event_rejects_invalid_contract_oid(self):
with self.assertRaises(ValidationError):
Event.from_dict(event_payload(contract_commit="abc"))
def test_decision_requires_work_unit_for_dispatch(self):
with self.assertRaises(ValidationError):
Decision.from_dict(decision_payload(next_work_units=[]))
def test_decision_rejects_same_primary_fallback(self):
value = decision_payload()
value["next_work_units"][0]["fallback_agent"] = "openclaw"
with self.assertRaises(ValidationError):
Decision.from_dict(value)
def test_non_dispatch_rejects_work_units(self):
with self.assertRaises(ValidationError):
Decision.from_dict(decision_payload(verdict="BLOCKED"))
class ClientTests(unittest.TestCase):
def test_workspace_trigger_accepts_202_without_run_id(self):
event = Event.from_dict(event_payload())
response = mock.MagicMock()
response.status = 202
response.read.return_value = b""
response.__enter__.return_value = response
response.__exit__.return_value = False
with mock.patch(
"services.aios_orchestrator_gateway.clients.urllib.request.urlopen",
return_value=response,
) as call:
WorkspaceAgentClient("agtch_test", "token").trigger(event)
request = call.call_args.args[0]
self.assertEqual(request.headers["Idempotency-key"], "evt-001")
def test_responses_background_start_and_poll(self):
event = Event.from_dict(event_payload())
decision = decision_payload()
client = ResponsesClient("key", "gpt-test")
with mock.patch.object(
client,
"_request",
side_effect=[
{"id": "resp_123", "status": "queued"},
{
"id": "resp_123",
"status": "completed",
"output": [
{
"content": [
{
"type": "output_text",
"text": json.dumps(decision),
}
]
}
],
},
],
):
response_id = client.start(event)
status, parsed = client.poll(response_id)
self.assertEqual(response_id, "resp_123")
self.assertEqual(status, "completed")
self.assertEqual(parsed.event_id, "evt-001")
def test_responses_poll_keeps_queued_state(self):
client = ResponsesClient("key", "gpt-test")
with mock.patch.object(
client,
"_request",
return_value={"id": "resp_123", "status": "in_progress"},
):
status, parsed = client.poll("resp_123")
self.assertEqual(status, "in_progress")
self.assertIsNone(parsed)
def test_responses_poll_queued_in_progress_completed_sequence(self):
client = ResponsesClient("key", "gpt-test")
completed = {
"status": "completed",
"output": [{"content": [{"type": "output_text", "text": json.dumps(decision_payload())}]}],
}
with mock.patch.object(
client,
"_request",
side_effect=[{"status": "queued"}, {"status": "in_progress"}, completed],
):
self.assertEqual(client.poll("resp_123"), ("queued", None))
self.assertEqual(client.poll("resp_123"), ("in_progress", None))
status, decision = client.poll("resp_123")
self.assertEqual(status, "completed")
self.assertEqual(decision.event_id, "evt-001")
def test_responses_invalid_completed_decision_fails_closed(self):
client = ResponsesClient("key", "gpt-test")
payload = {"status": "completed", "output": [{"content": [{"type": "output_text", "text": "{}"}]}]}
with mock.patch.object(client, "_request", return_value=payload):
with self.assertRaises(ValidationError):
client.poll("resp_123")
class StoreTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.store = Store(Path(self.temp.name) / "state.sqlite3")
def tearDown(self):
self.store.close()
self.temp.cleanup()
def test_event_idempotency(self):
event = Event.from_dict(event_payload())
created, _ = self.store.register_event(event)
duplicate, _ = self.store.register_event(event)
self.assertTrue(created)
self.assertFalse(duplicate)
def test_event_payload_conflict_rejected(self):
self.store.register_event(Event.from_dict(event_payload()))
with self.assertRaises(ConflictError):
self.store.register_event(Event.from_dict(event_payload(status="FAIL")))
def test_stale_state_version_rejected(self):
self.store.register_event(Event.from_dict(event_payload()))
with self.assertRaises(ConflictError):
self.store.register_event(
Event.from_dict(event_payload(event_id="evt-002", state_version=1))
)
def test_lease_claim_prevents_double_claim(self):
self.store.register_event(Event.from_dict(event_payload()))
first = self.store.claim_events(
owner="one", statuses=("RECEIVED",), limit=5, lease_seconds=60
)
second_store = Store(Path(self.temp.name) / "state.sqlite3")
try:
second = second_store.claim_events(
owner="two", statuses=("RECEIVED",), limit=5, lease_seconds=60
)
finally:
second_store.close()
self.assertEqual(len(first), 1)
self.assertEqual(second, [])
def test_workspace_timeout_claim_prevents_double_worker(self):
self.store.register_event(Event.from_dict(event_payload()))
self.store.update_event_trigger("evt-001", status="AWAITING_DECISION", channel="workspace_agent", remote_id=None)
self.store._conn().execute("UPDATE events SET updated_at=0 WHERE event_id='evt-001'")
first = self.store.claim_workspace_timeouts(owner="one", older_than=1, limit=5, lease_seconds=60)
second = self.store.claim_workspace_timeouts(owner="two", older_than=1, limit=5, lease_seconds=60)
self.assertEqual(len(first), 1)
self.assertEqual(second, [])
def test_response_poll_claim_prevents_double_worker(self):
self.store.register_event(Event.from_dict(event_payload()))
self.store.update_event_trigger("evt-001", status="POLLING_RESPONSE", channel="responses_api", remote_id="resp_123")
first = self.store.claim_response_polls(owner="one", limit=5, lease_seconds=60)
second = self.store.claim_response_polls(owner="two", limit=5, lease_seconds=60)
self.assertEqual(len(first), 1)
self.assertEqual(second, [])
def test_max_hops_limit(self):
self.store.register_event(Event.from_dict(event_payload()))
self.store._conn().execute(
"""
INSERT INTO gate_hops(project_key, gate_id, hops, updated_at)
VALUES ('v2r1-personal-weixin', 'R0_RUNNER_LIVE_RECOVERY', 1, 0)
"""
)
with self.assertRaisesRegex(ConflictError, "max_hops_exceeded"):
self.store.apply_decision(
Decision.from_dict(decision_payload()),
max_hops_per_gate=1,
max_same_blocker_retries=3,
)
def test_hop_limit_is_scoped_per_gate(self):
self.store.register_event(Event.from_dict(event_payload()))
first = Decision.from_dict(decision_payload())
self.assertEqual(
self.store.apply_decision(
first, max_hops_per_gate=1, max_same_blocker_retries=3
),
1,
)
self.store.register_event(
Event.from_dict(event_payload(event_id="evt-002", state_version=1))
)
second_value = decision_payload(
event_id="evt-002",
expected_state_version=1,
state_updates=[{"key": "current_gate", "value": "P2_REAL_B3_EVIDENCE"}],
)
second_value["next_work_units"][0]["id"] = "P2-REVIEW"
second_value["next_work_units"][0]["gate_id"] = "P2_REAL_B3_EVIDENCE"
self.assertEqual(
self.store.apply_decision(
Decision.from_dict(second_value),
max_hops_per_gate=1,
max_same_blocker_retries=3,
),
2,
)
self.store.register_event(
Event.from_dict(event_payload(event_id="evt-003", state_version=2))
)
third_value = decision_payload(
event_id="evt-003",
expected_state_version=2,
state_updates=[{"key": "current_gate", "value": "P2_REAL_B3_EVIDENCE"}],
)
third_value["next_work_units"][0]["id"] = "P2-REVIEW-RETRY"
third_value["next_work_units"][0]["gate_id"] = "P2_REAL_B3_EVIDENCE"
with self.assertRaisesRegex(ConflictError, "max_hops_exceeded"):
self.store.apply_decision(
Decision.from_dict(third_value),
max_hops_per_gate=1,
max_same_blocker_retries=3,
)
def test_decision_advances_state_and_creates_outbox(self):
self.store.register_event(Event.from_dict(event_payload()))
version = self.store.apply_decision(
Decision.from_dict(decision_payload()),
max_hops_per_gate=20,
max_same_blocker_retries=3,
)
self.assertEqual(version, 1)
state = self.store.project_state("v2r1-personal-weixin")
self.assertEqual(state["state_version"], 1)
rows = self.store.claim_outbox(owner="x", limit=10, lease_seconds=60)
self.assertEqual(len(rows), 1)
def test_decision_idempotency(self):
self.store.register_event(Event.from_dict(event_payload()))
decision = Decision.from_dict(decision_payload())
first = self.store.apply_decision(
decision, max_hops_per_gate=20, max_same_blocker_retries=3
)
second = self.store.apply_decision(
decision, max_hops_per_gate=20, max_same_blocker_retries=3
)
self.assertEqual(first, second)
def test_same_blocker_limit(self):
self.store.register_event(Event.from_dict(event_payload()))
decision = Decision.from_dict(
decision_payload(
verdict="BLOCKED",
next_work_units=[],
stop_reason="SAME_BLOCKER",
)
)
self.store.apply_decision(
decision, max_hops_per_gate=20, max_same_blocker_retries=1
)
# New event at next state with the same blocker exceeds the configured limit.
self.store.register_event(
Event.from_dict(event_payload(event_id="evt-002", state_version=1))
)
decision2 = Decision.from_dict(
decision_payload(
event_id="evt-002",
expected_state_version=1,
verdict="BLOCKED",
next_work_units=[],
stop_reason="SAME_BLOCKER",
)
)
with self.assertRaises(ConflictError):
self.store.apply_decision(
decision2, max_hops_per_gate=20, max_same_blocker_retries=1
)
def test_project_summary_contains_counts(self):
self.store.register_event(Event.from_dict(event_payload()))
summary = self.store.project_summary("v2r1-personal-weixin")
self.assertEqual(summary["event_counts"]["RECEIVED"], 1)
class DispatchTests(unittest.TestCase):
def test_dispatch_is_idempotent(self):
with tempfile.TemporaryDirectory() as temp:
outbox = Path(temp) / "outbox"
queue = Path(temp) / "queue"
outbox.mkdir(mode=0o700)
queue.mkdir(mode=0o2770)
queue.chmod(0o2770)
sink = DispatchSink(outbox, queue, CONTRACT)
record = {
"work_unit_id": "R0-DISCOVER",
"event_id": "evt-001",
"assigned_agent": "openclaw",
"payload_json": json.dumps(decision_payload()["next_work_units"][0]),
}
first = sink.dispatch(record)
second = sink.dispatch(record)
self.assertEqual(first, second)
self.assertTrue(first[0].exists())
self.assertTrue(first[1].exists())
self.assertEqual(stat.S_IMODE(first[1].stat().st_mode), 0o640)
def test_dispatch_rejects_path_escape(self):
with tempfile.TemporaryDirectory() as temp:
outbox = Path(temp) / "outbox"
queue = Path(temp) / "queue"
outbox.mkdir(mode=0o700)
queue.mkdir(mode=0o2770)
queue.chmod(0o2770)
sink = DispatchSink(outbox, queue, CONTRACT)
with self.assertRaises(DispatchError):
sink.dispatch(
{
"work_unit_id": "../escape",
"event_id": "evt-001",
"assigned_agent": "openclaw",
"payload_json": "{}",
}
)
def test_dispatch_rejects_existing_symlink_target(self):
with tempfile.TemporaryDirectory() as temp:
outbox = Path(temp) / "outbox"
queue = Path(temp) / "queue"
outbox.mkdir(mode=0o700)
queue.mkdir(mode=0o2770)
queue.chmod(0o2770)
sink = DispatchSink(outbox, queue, CONTRACT)
target = Path(temp) / "target"
target.write_text("x")
pending = outbox / "dispatched"
pending.mkdir(mode=0o700)
(pending / "R0-DISCOVER.json").symlink_to(target)
with self.assertRaises(DispatchError):
sink.dispatch(
{
"work_unit_id": "R0-DISCOVER",
"event_id": "evt-001",
"assigned_agent": "openclaw",
"payload_json": json.dumps(decision_payload()["next_work_units"][0]),
}
)
class SyncTests(unittest.TestCase):
def test_lifecycle_record_is_stable(self):
one = lifecycle_record(
contract_commit=CONTRACT,
project_key="v2r1",
kind="TASK_STARTED",
work_unit_id="R0",
payload={"status": "RUNNING"},
)
two = lifecycle_record(
contract_commit=CONTRACT,
project_key="v2r1",
kind="TASK_STARTED",
work_unit_id="R0",
payload={"status": "RUNNING"},
)
self.assertEqual(one["sync_id"], two["sync_id"])
def test_file_sync_is_idempotent(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
root.chmod(0o2750)
sync = OpenClawSynchronizer(root)
record = lifecycle_record(
contract_commit=CONTRACT,
project_key="v2r1",
kind="PROJECT_SUMMARY",
work_unit_id=None,
payload={"done": 1},
)
first = sync.emit(record)
second = sync.emit(record)
self.assertEqual(first, second)
def test_http_failure_keeps_file_for_retry(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
root.chmod(0o2750)
sync = OpenClawSynchronizer(root, endpoint="https://openclaw.invalid/events")
record = lifecycle_record(
contract_commit=CONTRACT,
project_key="v2r1",
kind="PROJECT_COMPLETED",
work_unit_id=None,
payload={"done": 1},
)
with mock.patch(
"services.aios_orchestrator_gateway.sync.urllib.request.urlopen",
side_effect=OSError("offline"),
):
with self.assertRaises(DispatchError):
sync.emit(record)
pending = root / "pending" / f"{record['sync_id']}.json"
self.assertTrue(pending.exists())
response = mock.MagicMock(status=202)
response.__enter__.return_value = response
response.__exit__.return_value = False
with mock.patch(
"services.aios_orchestrator_gateway.sync.urllib.request.urlopen",
return_value=response,
):
self.assertEqual(sync.emit(record), pending)
class ReporterTests(unittest.TestCase):
def test_http_failure_falls_back_to_file_inbox(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
inbox = root / "inbox"
inbox.mkdir(mode=0o2770)
inbox.chmod(0o2770)
token = root / "token"
token.write_text("not-a-real-secret", encoding="utf-8")
token.chmod(0o600)
argv = [
"report_orchestrator_event.py",
"--project-key", "v2r1-personal-weixin",
"--contract-commit", CONTRACT,
"--event-id", "evt-http-fallback",
"--source-agent", "codex",
"--event-type", "work_unit_completed",
"--work-unit-id", "P1-REACK",
"--status", "PASS",
"--state-version", "0",
"--endpoint", "https://gateway.invalid",
"--token-file", str(token),
"--inbox-dir", str(inbox),
]
with mock.patch("sys.argv", argv), mock.patch(
"scripts.report_orchestrator_event.urllib.request.urlopen",
side_effect=urllib.error.URLError("offline"),
):
self.assertEqual(report_orchestrator_event.main(), 0)
pending = inbox / "pending/evt-http-fallback.json"
self.assertTrue(pending.exists())
self.assertEqual(stat.S_IMODE(pending.stat().st_mode), 0o640)
def test_payload_symlink_is_rejected(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
target = root / "payload.json"
target.write_text("{}", encoding="utf-8")
target.chmod(0o600)
link = root / "payload-link.json"
link.symlink_to(target)
argv = [
"report_orchestrator_event.py",
"--project-key", "v2r1-personal-weixin",
"--contract-commit", CONTRACT,
"--event-id", "evt-symlink",
"--source-agent", "codex",
"--event-type", "work_unit_completed",
"--work-unit-id", "P1-REACK",
"--status", "PASS",
"--state-version", "0",
"--payload-file", str(link),
"--inbox-dir", str(root / "inbox"),
]
with mock.patch("sys.argv", argv), self.assertRaisesRegex(
Exception, "symlink_rejected"
):
report_orchestrator_event.main()
class RuntimePollingTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
for name in ("events", "decisions", "outbox", "etc"):
(self.root / name).mkdir(mode=0o700)
for name in ("sync", "commands"):
(self.root / name).mkdir(mode=0o2750)
(self.root / name).chmod(0o2750)
for name, value in (
("inbound.token", "inbound"),
("callback.token", "callback"),
("workspace.token", "workspace"),
):
path = self.root / "etc" / name
path.write_text(value)
path.chmod(0o600)
config = {
"host": "127.0.0.1",
"port": 18920,
"project_key": "v2r1-personal-weixin-production",
"database_path": str(self.root / "state.sqlite3"),
"event_inbox_dir": str(self.root / "events"),
"decision_inbox_dir": str(self.root / "decisions"),
"dispatch_outbox_dir": str(self.root / "outbox"),
"openclaw_sync_dir": str(self.root / "sync"),
"command_queue_dir": str(self.root / "commands"),
"inbound_token_file": str(self.root / "etc/inbound.token"),
"callback_token_file": str(self.root / "etc/callback.token"),
"contract_commit": CONTRACT,
"workspace_agent": {
"trigger_id": "agtch_test",
"access_token_file": str(self.root / "etc/workspace.token"),
},
"responses_fallback": {
"api_key_file": None,
"model": None,
},
"polling": {
"interval_seconds": 1,
"summary_interval_seconds": 10,
},
"loop_controls": {
"callback_timeout_seconds": 30,
"lease_seconds": 10,
"retry_base_seconds": 1,
"max_retries": 2,
"max_same_blocker_retries": 2,
"max_hops_per_gate": 20,
},
"openclaw_sync": {"endpoint": None, "token_file": None},
"git_poll": {
"enabled": False,
"bare_repo": None,
"ref_prefixes": [],
"interval_seconds": 30,
},
}
config_path = self.root / "etc/config.json"
config_path.write_text(json.dumps(config))
config_path.chmod(0o600)
os.environ["AIOS_ORCHESTRATOR_CONFIG_OWNER_UID"] = str(os.getuid())
os.environ["AIOS_ORCHESTRATOR_SECRET_OWNER_UID"] = str(os.getuid())
self.runtime = Runtime(Config.from_file(config_path))
def tearDown(self):
self.runtime.store.close()
self.temp.cleanup()
os.environ.pop("AIOS_ORCHESTRATOR_CONFIG_OWNER_UID", None)
os.environ.pop("AIOS_ORCHESTRATOR_SECRET_OWNER_UID", None)
def test_file_event_polling_and_workspace_trigger(self):
pending = self.root / "events/pending"
pending.mkdir(mode=0o2770)
pending.chmod(0o2770)
(pending / "evt-001.json").write_text(json.dumps(event_payload()))
(pending / "evt-001.json").chmod(0o640)
with mock.patch.object(self.runtime.workspace, "trigger") as trigger:
self.runtime.poll_once()
trigger.assert_called_once()
processed = self.root / "events/processed/evt-001.json"
self.assertTrue(processed.exists())
def test_workspace_timeout_uses_responses_fallback(self):
self.runtime.submit_event(event_payload())
self.runtime.store.update_event_trigger(
"evt-001", status="AWAITING_DECISION", channel="workspace_agent", remote_id=None
)
self.runtime.store._conn().execute("UPDATE events SET updated_at=0 WHERE event_id='evt-001'")
fallback = mock.Mock()
fallback.start.return_value = "resp_fallback"
self.runtime.responses = fallback
self.runtime.process_workspace_timeouts()
row = self.runtime.store._conn().execute(
"SELECT status, channel, remote_id FROM events WHERE event_id='evt-001'"
).fetchone()
self.assertEqual(tuple(row), ("POLLING_RESPONSE", "responses_api", "resp_fallback"))
fallback.start.assert_called_once()
def test_workspace_callback_timeout_reaches_dead_letter_without_fallback(self):
self.runtime.submit_event(event_payload())
self.runtime.store.update_event_trigger(
"evt-001", status="AWAITING_DECISION", channel="workspace_agent", remote_id=None
)
self.runtime.store._conn().execute(
"UPDATE events SET attempts=2, updated_at=0 WHERE event_id='evt-001'"
)
self.assertIsNone(self.runtime.responses)
self.runtime.process_workspace_timeouts()
row = self.runtime.store._conn().execute(
"SELECT status, attempts FROM events WHERE event_id='evt-001'"
).fetchone()
self.assertEqual(tuple(row), ("DEAD_LETTER", 3))
def test_response_poll_limit_dead_letters_queued_response(self):
self.runtime.submit_event(event_payload())
self.runtime.store.update_event_trigger(
"evt-001",
status="POLLING_RESPONSE",
channel="responses_api",
remote_id="resp-stuck",
)
self.runtime.store._conn().execute(
"UPDATE events SET attempts=2, next_attempt_at=0 WHERE event_id='evt-001'"
)
fallback = mock.Mock()
fallback.poll.return_value = ("queued", None)
self.runtime.responses = fallback
self.runtime.poll_background_responses()
row = self.runtime.store._conn().execute(
"SELECT status, attempts FROM events WHERE event_id='evt-001'"
).fetchone()
self.assertEqual(tuple(row), ("DEAD_LETTER", 3))
def test_callback_rejects_wrong_contract(self):
self.runtime.submit_event(event_payload())
with self.assertRaisesRegex(ConflictError, "contract_version_mismatch"):
self.runtime.submit_decision(
decision_payload(contract_commit="f" * 40)
)
def test_task_start_and_end_are_synced(self):
start = Event.from_dict(
event_payload(
event_id="evt-start",
event_type="work_unit_started",
status="RUNNING",
)
)
self.runtime.submit_event(start.to_dict())
self.runtime.synchronize_openclaw()
pending = list((self.root / "sync/pending").glob("*.json"))
self.assertTrue(pending)
kinds = {json.loads(p.read_text())["kind"] for p in pending}
self.assertIn("TASK_STARTED", kinds)
def test_decision_dispatch_and_summary_sync(self):
self.runtime.submit_event(event_payload())
self.runtime.submit_decision(decision_payload())
self.runtime.dispatch_work_units()
self.runtime.synchronize_openclaw()
self.assertTrue(list((self.root / "commands/openclaw/pending").glob("*.json")))
kinds = {
json.loads(p.read_text())["kind"]
for p in (self.root / "sync/pending").glob("*.json")
}
self.assertIn("TASK_DISPATCHED", kinds)
self.assertIn("PROJECT_SUMMARY", kinds)
def test_poll_once_ingests_decision_file(self):
self.runtime.submit_event(event_payload())
pending = self.root / "decisions/pending"
pending.mkdir(mode=0o2770)
pending.chmod(0o2770)
(pending / "decision.json").write_text(json.dumps(decision_payload()))
(pending / "decision.json").chmod(0o640)
self.runtime.poll_once()
self.assertTrue((self.root / "decisions/processed/decision.json").exists())
def test_git_ref_polling_baselines_then_emits_change(self):
object.__setattr__(self.runtime.config, "git_poll_enabled", True)
object.__setattr__(self.runtime.config, "git_poll_bare_repo", self.root / "repo.git")
object.__setattr__(self.runtime.config, "git_poll_ref_prefixes", ("refs/heads/main",))
oid1 = "1" * 40
oid2 = "2" * 40
first = mock.Mock(returncode=0, stdout=f"refs/heads/main {oid1}\n", stderr="")
second = mock.Mock(returncode=0, stdout=f"refs/heads/main {oid2}\n", stderr="")
with mock.patch(
"services.aios_orchestrator_gateway.service.subprocess.run",
side_effect=[first, second],
):
self.runtime._next_git_poll = 0
self.runtime.poll_git_refs()
self.runtime._next_git_poll = 0
self.runtime.poll_git_refs()
summary = self.runtime.store.project_summary(
"v2r1-personal-weixin-production"
)
self.assertIsNotNone(summary)
self.assertEqual(summary["event_counts"]["RECEIVED"], 1)
def test_retry_backoff_is_bounded(self):
self.assertEqual(self.runtime._backoff(0), 1)
self.assertLessEqual(self.runtime._backoff(100), 3600)
if __name__ == "__main__":
unittest.main()
@@ -1,59 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 0077
SOURCE_ROOT="${1:?source root required}"
ROOT="$(mktemp -d /tmp/aios-orchestrator-rollback-test.XXXXXX)"
trap 'rm -rf "$ROOT"' EXIT
INSTALL_ROOT="$ROOT/opt"
STATE_ROOT="$ROOT/state"
mkdir -p "$INSTALL_ROOT/releases" "$STATE_ROOT" "$ROOT/bin"
chmod 0700 "$STATE_ROOT"
OLD="$(printf 'a%.0s' {1..64})"
NEW="$(printf 'b%.0s' {1..64})"
mkdir "$INSTALL_ROOT/releases/$OLD" "$INSTALL_ROOT/releases/$NEW"
printf '%s\n' "$OLD" > "$INSTALL_ROOT/releases/$OLD/SOURCE_DIGEST"
printf '%s\n' "$NEW" > "$INSTALL_ROOT/releases/$NEW/SOURCE_DIGEST"
ln -s "$INSTALL_ROOT/releases/$NEW" "$INSTALL_ROOT/current"
python3 - "$STATE_ROOT/install-receipt.json" "$INSTALL_ROOT/releases/$NEW" "$INSTALL_ROOT/releases/$OLD" <<'PY'
import json, sys
with open(sys.argv[1], "w", encoding="utf-8") as handle:
json.dump({
"schema_version": 1,
"source_digest": "b" * 64,
"current_release": sys.argv[2],
"previous_release": sys.argv[3],
"last_rollback": None,
}, handle)
PY
chmod 0600 "$STATE_ROOT/install-receipt.json"
cat > "$ROOT/bin/systemctl" <<'SH'
#!/bin/sh
printf '%s\n' "$*" >> "${SYSTEMCTL_LOG:?}"
exit 0
SH
chmod 0755 "$ROOT/bin/systemctl"
: > "$ROOT/systemctl.log"
SYSTEMCTL_LOG="$ROOT/systemctl.log" PATH="$ROOT/bin:/usr/bin:/bin" INSTALL_ROOT="$INSTALL_ROOT" STATE_ROOT="$STATE_ROOT" \
bash "$SOURCE_ROOT/scripts/rollback_aios_orchestrator_gateway.sh"
FIRST="$(readlink -f "$INSTALL_ROOT/current")"
SYSTEMCTL_LOG="$ROOT/systemctl.log" PATH="$ROOT/bin:/usr/bin:/bin" INSTALL_ROOT="$INSTALL_ROOT" STATE_ROOT="$STATE_ROOT" \
bash "$SOURCE_ROOT/scripts/rollback_aios_orchestrator_gateway.sh"
SECOND="$(readlink -f "$INSTALL_ROOT/current")"
[[ "$FIRST" == "$INSTALL_ROOT/releases/$OLD" ]]
[[ "$SECOND" == "$FIRST" ]]
grep -Fxq 'stop aios-orchestrator-gateway-poll.timer' "$ROOT/systemctl.log"
grep -Fxq 'stop aios-orchestrator-gateway-poll.service' "$ROOT/systemctl.log"
grep -Fxq 'start aios-orchestrator-gateway-poll.timer' "$ROOT/systemctl.log"
printf 'tampered\n' > "$INSTALL_ROOT/releases/$OLD/SOURCE_DIGEST"
if SYSTEMCTL_LOG="$ROOT/systemctl.log" PATH="$ROOT/bin:/usr/bin:/bin" \
INSTALL_ROOT="$INSTALL_ROOT" STATE_ROOT="$STATE_ROOT" \
bash "$SOURCE_ROOT/scripts/rollback_aios_orchestrator_gateway.sh" >/dev/null 2>&1; then
echo 'ROLLBACK_DIGEST_MISMATCH_TEST=FAIL' >&2
exit 1
fi
echo "ROLLBACK_DIGEST_MISMATCH_TEST=PASS"
echo "ROLLBACK_REPEAT_TEST=PASS"
+324
View File
@@ -0,0 +1,324 @@
from __future__ import annotations
import importlib.util
import json
import os
import subprocess
import tempfile
import unittest
from types import SimpleNamespace
from pathlib import Path
from unittest.mock import Mock, patch
ROOT = Path(__file__).resolve().parents[1]
import sys
sys.path.insert(0, str(ROOT / "runtime" / "agent-os"))
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from gitea_tag_publication import build_publisher
import authoritative_git as authoritative_git_module
from authoritative_git import (
AuthoritativeGitError,
AuthoritativeGitNotFound,
AuthoritativeGitOutputTooLarge,
authoritative_branches,
authoritative_diff,
authoritative_commit,
authoritative_head_oid,
authoritative_tags,
repository_snapshot,
run_authoritative_git,
validate_git_revision,
verify_repository,
)
API_SPEC = importlib.util.spec_from_file_location(
"authoritative_test_api",
ROOT / "active-runtime" / "app" / "aios_chatgpt_actions_api.py",
)
assert API_SPEC and API_SPEC.loader
api = importlib.util.module_from_spec(API_SPEC)
API_SPEC.loader.exec_module(api)
class AuthoritativeGitContextTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
self.bare = self.root / "repo.git"
self.seed = self.root / "seed"
self.runtime = self.root / "runtime"
subprocess.run(["git", "init", "--bare", str(self.bare)], check=True, capture_output=True)
subprocess.run(["git", "init", "-b", "main", str(self.seed)], check=True, capture_output=True)
for key, value in (("user.name", "test"), ("user.email", "test@example.invalid")):
subprocess.run(["git", "-C", str(self.seed), "config", key, value], check=True)
(self.seed / "README").write_text("context\n", encoding="utf-8")
subprocess.run(["git", "-C", str(self.seed), "add", "README"], check=True)
subprocess.run(["git", "-C", str(self.seed), "commit", "-m", "seed"], check=True, capture_output=True)
subprocess.run(["git", "-C", str(self.seed), "remote", "add", "origin", str(self.bare)], check=True)
subprocess.run(["git", "-C", str(self.seed), "push", "origin", "main"], check=True, capture_output=True)
subprocess.run(["git", "--git-dir", str(self.bare), "config", "remote.gitea.url", "https://g.cnnetmkt.top/git/aios/aios.git"], check=True)
subprocess.run(["git", "-C", str(self.seed), "tag", "-a", "V1R1C0", "-m", "annotated"], check=True)
subprocess.run(["git", "-C", str(self.seed), "tag", "V1R1C1"], check=True)
subprocess.run(["git", "-C", str(self.seed), "checkout", "-b", "patch/context"], check=True, capture_output=True)
(self.seed / "CHANGE").write_text("authoritative diff\n", encoding="utf-8")
subprocess.run(["git", "-C", str(self.seed), "add", "CHANGE"], check=True)
subprocess.run(["git", "-C", str(self.seed), "commit", "-m", "change"], check=True, capture_output=True)
subprocess.run(["git", "-C", str(self.seed), "push", "origin", "patch/context", "--tags"], check=True, capture_output=True)
(self.runtime / "versions").mkdir(parents=True)
(self.runtime / "patches").mkdir()
(self.runtime / "versions" / "V1R1C0.json").write_text(
json.dumps({"version": "V1R1C0"}), encoding="utf-8"
)
(self.runtime / "patches" / "context.json").write_text(
json.dumps({"branch": "patch/context"}), encoding="utf-8"
)
self.env = {
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(self.bare),
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
}
def tearDown(self):
self.tmp.cleanup()
def test_runtime_cwd_without_git_uses_authoritative_head(self):
with patch.dict(os.environ, self.env, clear=False), patch("os.getcwd", return_value=str(self.runtime)):
self.assertFalse((self.runtime / ".git").exists())
self.assertEqual(len(authoritative_head_oid()), 40)
def test_annotated_and_lightweight_tags_include_type_and_peeled_target(self):
with patch.dict(os.environ, self.env, clear=False):
tags = {item["name"]: item for item in authoritative_tags()}
self.assertEqual(tags["V1R1C0"]["object_type"], "tag")
self.assertEqual(tags["V1R1C0"]["peeled_target_oid"], tags["V1R1C1"]["peeled_target_oid"])
self.assertEqual(tags["V1R1C1"]["object_type"], "commit")
def test_authoritative_branches_include_exact_oids(self):
with patch.dict(os.environ, self.env, clear=False):
branches = {item["name"]: item["oid"] for item in authoritative_branches()}
self.assertIn("main", branches)
self.assertIn("patch/context", branches)
self.assertEqual(len(branches["main"]), 40)
def test_runtime_cwd_changes_do_not_change_results(self):
old_cwd = os.getcwd()
try:
with patch.dict(os.environ, self.env, clear=False):
first = authoritative_head_oid()
os.chdir(str(self.runtime))
second = authoritative_head_oid()
self.assertEqual(first, second)
finally:
os.chdir(old_cwd)
def test_missing_configuration_fails_closed(self):
with patch.dict(os.environ, {}, clear=True), self.assertRaises(AuthoritativeGitError):
verify_repository()
def test_missing_repository_fails_closed(self):
with patch.dict(os.environ, {**self.env, "AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(self.root / "missing")}, clear=False):
with self.assertRaises(AuthoritativeGitError):
verify_repository()
def test_non_bare_repository_fails_closed(self):
with patch.dict(os.environ, self.env, clear=False), self.assertRaises(AuthoritativeGitError):
verify_repository(self.seed)
def test_repository_identity_mismatch_fails_closed(self):
with patch.dict(os.environ, {**self.env, "AIOS_GITOPS_REPOSITORY_IDENTITY": "other/repo"}, clear=False):
with self.assertRaises(AuthoritativeGitError):
verify_repository()
def test_git_command_failure_is_not_an_empty_success(self):
with patch.dict(os.environ, self.env, clear=False):
with self.assertRaises(AuthoritativeGitError) as ctx:
run_authoritative_git(["rev-parse", "--verify", "refs/heads/no-such-branch"])
self.assertIn("GIT_REF_QUERY_FAILED", str(ctx.exception))
self.assertNotIn("tags=[]", str(ctx.exception))
def test_commit_input_rejects_git_parameter_injection(self):
with patch.dict(os.environ, self.env, clear=False):
with self.assertRaises(ValueError):
authoritative_commit("1" * 39 + ";")
def test_repository_snapshot_exposes_authoritative_head(self):
with patch.dict(os.environ, self.env, clear=False):
snapshot = repository_snapshot()
self.assertTrue(snapshot["repository_available"])
self.assertEqual(snapshot["repository_identity"], "aios/aios")
self.assertEqual(len(snapshot["authoritative_head_oid"]), 40)
def _get(self, path):
handler = object.__new__(api.H)
handler.path = path
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
handler.sendt = Mock(side_effect=lambda data, ctype="text/plain; charset=utf-8", code=200: (data, code))
with (
patch.dict(os.environ, self.env, clear=False),
patch.object(api, "WS", self.runtime),
patch.object(api, "AUTHORITATIVE_GIT_DIR", self.bare),
patch.object(api, "REPOSITORY_IDENTITY", "aios/aios"),
):
api.H.do_GET(handler)
args = handler.sendj.call_args.args
return args[0], args[1] if len(args) > 1 else 200
def test_api_list_versions_uses_authoritative_tags(self):
body, code = self._get("/api/ver")
self.assertEqual(code, 200)
self.assertTrue(body["repository_available"])
self.assertTrue(body["local_authoritative_tags"])
self.assertEqual(body["head"], body["authoritative_head_oid"])
self.assertIn("V1R1C0", body["tags"])
def test_api_list_patches_uses_authoritative_branches(self):
body, code = self._get("/api/patch")
self.assertEqual(code, 200)
self.assertIn("patch/context", body["branches"])
self.assertTrue(body["authoritative_branches"])
def test_api_unavailable_does_not_return_empty_lists(self):
handler = object.__new__(api.H)
handler.path = "/api/ver"
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
with patch.dict(os.environ, {}, clear=True), patch.object(api, "WS", self.runtime), patch.object(api, "AUTHORITATIVE_GIT_DIR", None), patch.object(api, "REPOSITORY_IDENTITY", ""):
api.H.do_GET(handler)
body, code = handler.sendj.call_args.args
self.assertEqual(code, 503)
self.assertFalse(body["repository_available"])
self.assertIn("query_error", body)
def test_api_runtime_status_contains_release_and_repository_fields(self):
handler = object.__new__(api.H)
handler.path = "/api/run"
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
real_run = subprocess.run
def service_or_real(*args, **kwargs):
command = args[0] if args else kwargs.get("args", [])
if command and command[0] == "systemctl":
return subprocess.CompletedProcess(command, 0, stdout="active\n", stderr="")
return real_run(*args, **kwargs)
with (
patch.dict(os.environ, self.env, clear=False),
patch.object(api, "WS", self.runtime),
patch.object(api, "AUTHORITATIVE_GIT_DIR", self.bare),
patch.object(api, "REPOSITORY_IDENTITY", "aios/aios"),
patch.object(api.subprocess, "run", side_effect=service_or_real),
):
api.H.do_GET(handler)
body, code = handler.sendj.call_args.args
self.assertEqual(code, 200)
self.assertIsNone(body["workspace_branch"])
self.assertTrue(body["authoritative_git_dir_configured"])
self.assertEqual(body["authoritative_head_oid"], body["authoritative_head_oid"])
def test_authoritative_head_is_independent_of_runtime_directory(self):
with patch.dict(os.environ, self.env, clear=False):
old = os.getcwd()
try:
os.chdir(str(self.runtime))
self.assertEqual(authoritative_head_oid(), authoritative_head_oid())
finally:
os.chdir(old)
def test_authoritative_api_is_read_only(self):
before = subprocess.run(["git", "--git-dir", str(self.bare), "show-ref"], check=True, capture_output=True, text=True).stdout
with patch.dict(os.environ, self.env, clear=False):
authoritative_tags()
authoritative_branches()
after = subprocess.run(["git", "--git-dir", str(self.bare), "show-ref"], check=True, capture_output=True, text=True).stdout
self.assertEqual(before, after)
def test_error_text_does_not_include_credential_values(self):
with patch.dict(os.environ, self.env, clear=False):
with self.assertRaises(AuthoritativeGitError) as ctx:
run_authoritative_git(["rev-parse", "--verify", "https://user:password@example.invalid/x"])
self.assertNotIn("password", str(ctx.exception).lower())
def test_cli_publisher_requires_and_uses_authoritative_repository(self):
args = SimpleNamespace(
workspace=str(self.runtime),
remote="gitea",
gitea_api="http://127.0.0.1:3000/api/v1",
gitea_repo="aios/aios",
gitea_token_file="/dev/null",
)
with patch.dict(os.environ, self.env, clear=False):
publisher = build_publisher(args)
self.assertEqual(publisher.git.authoritative_repo, self.bare)
def test_cli_publisher_fails_closed_without_authoritative_repository(self):
args = SimpleNamespace(
workspace=str(self.runtime),
remote="gitea",
gitea_api="http://127.0.0.1:3000/api/v1",
gitea_repo="aios/aios",
gitea_token_file="/dev/null",
)
with patch.dict(os.environ, {}, clear=True), self.assertRaises(RuntimeError):
build_publisher(args)
def test_authoritative_diff_supports_branch_tag_and_exact_oid(self):
with patch.dict(os.environ, self.env, clear=True):
by_ref = authoritative_diff("V1R1C0", "patch/context")
main_oid = validate_git_revision("refs/heads/main", "from")
patch_oid = validate_git_revision("refs/heads/patch/context", "to")
by_oid = authoritative_diff(main_oid, patch_oid)
self.assertIn("CHANGE", by_ref["stdout"])
self.assertEqual(by_ref["stdout"], by_oid["stdout"])
self.assertEqual(by_ref["code"], 0)
def test_authoritative_diff_is_cwd_independent(self):
old = os.getcwd()
try:
with patch.dict(os.environ, self.env, clear=True):
first = authoritative_diff("V1R1C0", "patch/context")["stdout"]
os.chdir(self.runtime)
second = authoritative_diff("V1R1C0", "patch/context")["stdout"]
self.assertEqual(first, second)
finally:
os.chdir(old)
def test_revision_injection_and_revision_expressions_are_rejected(self):
with patch.dict(os.environ, self.env, clear=True):
for value in ("--help", "--output=x", "HEAD~1", "main^{tree}", "@{1}", ":/secret", "", " main"):
with self.subTest(value=value), self.assertRaises(ValueError):
validate_git_revision(value, "from")
def test_valid_but_missing_revision_is_not_found(self):
with patch.dict(os.environ, self.env, clear=True), self.assertRaises(AuthoritativeGitNotFound):
validate_git_revision("missing-branch", "from")
def test_diff_output_limit_is_fail_closed(self):
with patch.dict(os.environ, self.env, clear=True), patch.object(authoritative_git_module, "_MAX_OUTPUT", 1):
with self.assertRaises(AuthoritativeGitOutputTooLarge):
authoritative_diff("V1R1C0", "patch/context")
def test_api_diff_uses_authoritative_repository(self):
body, code = self._get("/api/diff?from=V1R1C0&to=patch/context")
self.assertEqual(code, 200)
self.assertIn("CHANGE", body["diff"]["stdout"])
self.assertEqual(len(body["diff"]["from_oid"]), 40)
def test_api_diff_status_mapping(self):
_, code = self._get("/api/diff?from=--help&to=patch/context")
self.assertEqual(code, 400)
_, code = self._get("/api/diff?from=missing-branch&to=patch/context")
self.assertEqual(code, 404)
with patch.object(api, "authoritative_diff", side_effect=AuthoritativeGitOutputTooLarge("GIT_OUTPUT_LIMIT_EXCEEDED")):
_, code = self._get("/api/diff?from=V1R1C0&to=patch/context")
self.assertEqual(code, 413)
def test_http_publisher_fails_closed_without_authoritative_repository(self):
with patch.dict(os.environ, {}, clear=True), patch.object(api, "AUTHORITATIVE_GIT_DIR", None), patch.object(api, "REPOSITORY_IDENTITY", ""):
with self.assertRaises(RuntimeError):
api.publication_publisher()
def test_http_publisher_fails_closed_on_identity_mismatch(self):
with patch.dict(os.environ, self.env, clear=True), patch.object(api, "AUTHORITATIVE_GIT_DIR", self.bare), patch.object(api, "REPOSITORY_IDENTITY", "other/repo"):
with self.assertRaises(RuntimeError):
api.publication_publisher()
def test_writer_guard_remains_false(self):
decision = api.evaluate_git_write(self.runtime, {"AIOS_GITOPS_WRITES_ENABLED": "false"})
self.assertFalse(decision.allowed)
self.assertEqual(decision.status_code, 423)
+431
View File
@@ -0,0 +1,431 @@
from __future__ import annotations
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
import yaml
from pathlib import Path
from unittest.mock import Mock, patch
ROOT = Path(__file__).resolve().parents[1]
os.environ.setdefault("AIOS_GITOPS_WORKSPACE", str(ROOT))
sys.path.insert(0, str(ROOT / "runtime" / "agent-os"))
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
SPEC = importlib.util.spec_from_file_location(
"aios_chatgpt_actions_api",
ROOT / "active-runtime" / "app" / "aios_chatgpt_actions_api.py",
)
assert SPEC and SPEC.loader
api = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(api)
TARGET = "1" * 40
EVIDENCE = "2" * 40
TAG_OID = "3" * 40
MAIN = "4" * 40
class TagActionTests(unittest.TestCase):
def handler(self, role: str = "owner"):
handler = object.__new__(api.H)
handler.need = Mock(
return_value={"account": "owner", "role": role, "scopes": {"tag:create"}}
)
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
return handler
def complete_request(self):
return {
"version": "V9R9C9",
"target_oid": TARGET,
"evidence_closure_oid": EVIDENCE,
"approval_plan_id": "owner-approved-test",
"execute": True,
}
def test_tag_publication_status_rejects_invalid_versions_without_reading(self):
invalid_queries = (
"../current",
"../../versions/V2R1C3",
"%2e%2e%2fcurrent",
"/etc/secret",
r"..\current",
"",
)
for value in invalid_queries:
with self.subTest(value=value):
handler = object.__new__(api.H)
handler.path = f"/api/tag-publication-status?version={value}"
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
with patch.object(api, "publication_publisher") as publisher:
handler.do_GET()
body = handler.sendj.call_args.args[0]
code = (
handler.sendj.call_args.args[1]
if len(handler.sendj.call_args.args) > 1
else handler.sendj.call_args.kwargs.get("code", 200)
)
self.assertEqual(code, 400)
self.assertEqual(body, {"error": "invalid release version"})
if value:
self.assertNotIn(value, str(body))
self.assertNotIn("/data/", str(body))
self.assertNotIn("BAIT-MUST-NOT-BE-READ", str(body))
publisher.assert_not_called()
def test_tag_publication_status_accepts_canonical_versions(self):
for version in ("V2R1C3", "V2R1C3-rc.1_build"):
with self.subTest(version=version):
handler = object.__new__(api.H)
handler.path = f"/api/tag-publication-status?version={version}"
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
publisher = Mock()
publisher.get_tag_publication_status.return_value = {
"version": version,
"state": "UNKNOWN",
}
with patch.object(api, "publication_publisher", return_value=publisher):
handler.do_GET()
body = handler.sendj.call_args.args[0]
code = (
handler.sendj.call_args.args[1]
if len(handler.sendj.call_args.args) > 1
else handler.sendj.call_args.kwargs.get("code", 200)
)
self.assertEqual(code, 200)
self.assertEqual(body["version"], version)
publisher.get_tag_publication_status.assert_called_once_with(version)
def test_resolve_commit_oid_requires_full_exact_commit(self):
with patch.object(api, "authoritative_commit", return_value=TARGET):
self.assertEqual(api.resolve_commit_oid(TARGET), TARGET)
for value in (None, "1234567", "z" * 40):
with self.subTest(value=value), self.assertRaises(ValueError):
api.resolve_commit_oid(value)
def test_production_tag_requires_all_governance_fields(self):
handler = self.handler()
for missing in ("target_oid", "evidence_closure_oid", "approval_plan_id"):
payload = self.complete_request()
payload.pop(missing)
handler.tag(payload)
body, code = handler.sendj.call_args.args
self.assertEqual(code, 400)
self.assertIn("required", body)
def test_owner_execution_uses_atomic_publisher_and_never_changes_main(self):
handler = self.handler()
publication = {
"state": "BASELINE_UPDATED",
"tag_oid": TAG_OID,
"remote_tag_oid": TAG_OID,
"remote_peeled_target_oid": TARGET,
"gitea_listing_visible": True,
}
def fake_sh(args, check=False):
if args[:2] == ["merge-base", "--is-ancestor"]:
return {"code": 0, "stdout": "", "stderr": "", "cmd": []}
if args == ["rev-parse", "refs/heads/main"]:
return {"code": 0, "stdout": MAIN, "stderr": "", "cmd": []}
if args == ["rev-parse", "HEAD"]:
return {"code": 0, "stdout": EVIDENCE, "stderr": "", "cmd": []}
return {"code": 0, "stdout": "", "stderr": "", "cmd": []}
publisher = Mock()
publisher.publish.return_value = publication
with (
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
patch.object(api, "sh", side_effect=fake_sh),
patch.object(api, "required_authoritative_git_dir", return_value=Path("/tmp/authoritative.git")),
patch.object(api, "GiteaTagClient"),
patch.object(api, "GitTagBackend"),
patch.object(api, "TagPublisher", return_value=publisher),
patch.object(api, "audit"),
):
handler.tag(self.complete_request())
result = handler.sendj.call_args.args[0]
self.assertEqual(result["status"], "BASELINE_UPDATED")
self.assertEqual(result["target_oid"], TARGET)
self.assertEqual(result["tag_oid"], TAG_OID)
self.assertFalse(result["main_changed"])
self.assertFalse(result["force_push"])
def test_atomic_publisher_conflict_returns_reconciliation_required(self):
handler = self.handler()
publisher = Mock()
publisher.publish.side_effect = api.PublicationConflict("different remote OID")
with (
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
patch.object(
api,
"sh",
side_effect=[
{"code": 0, "stdout": "", "stderr": "", "cmd": []},
{"code": 0, "stdout": MAIN, "stderr": "", "cmd": []},
],
),
patch.object(api, "required_authoritative_git_dir", return_value=Path("/tmp/authoritative.git")),
patch.object(api, "GiteaTagClient"),
patch.object(api, "GitTagBackend"),
patch.object(api, "TagPublisher", return_value=publisher),
patch.object(api, "audit"),
):
handler.tag(self.complete_request())
body, code = handler.sendj.call_args.args
self.assertEqual(code, 409)
self.assertEqual(body["status"], "RECONCILIATION_REQUIRED")
def test_non_owner_request_persists_complete_target_binding(self):
handler = self.handler(role="chatgpt_bot")
with (
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
patch.object(
api, "sh", return_value={"code": 0, "stdout": "", "stderr": "", "cmd": []}
),
patch.object(api, "checkout", return_value="request/tag-v9r9c9"),
patch.object(
api,
"write_json",
return_value="baseline/tag_requests/V9R9C9.json",
) as write_json,
patch.object(api, "commit", return_value={"status": "committed"}),
):
handler.tag(self.complete_request())
saved = write_json.call_args.args[1]
self.assertEqual(saved["target_oid"], TARGET)
self.assertEqual(saved["evidence_closure_oid"], EVIDENCE)
self.assertEqual(saved["approval_plan_id"], "owner-approved-test")
def test_linked_worktree_branch_sync_verifies_instead_of_self_fetch(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
bare = root / "repo.git"
seed = root / "seed"
worktree = root / "writer"
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
subprocess.run(["git", "init", "-b", "main", str(seed)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(seed), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(seed), "config", "user.email", "test@example.invalid"],
check=True,
)
(seed / "x").write_text("x", encoding="utf-8")
subprocess.run(["git", "-C", str(seed), "add", "x"], check=True)
subprocess.run(["git", "-C", str(seed), "commit", "-m", "seed"], check=True, capture_output=True)
subprocess.run(["git", "-C", str(seed), "push", str(bare), "main"], check=True, capture_output=True)
subprocess.run(
["git", "--git-dir", str(bare), "worktree", "add", "-b", "patch/test", str(worktree), "main"],
check=True,
capture_output=True,
)
old_ws, old_bare = api.WS, api.BARE_REPO
api.WS, api.BARE_REPO = worktree, bare
try:
result = api.sync_branch("patch/test")
finally:
api.WS, api.BARE_REPO = old_ws, old_bare
self.assertEqual(result["status"], "verified")
self.assertEqual(result["target"], "authoritative-bare")
def test_create_existing_branch_never_resets_it(self):
calls = []
def fake_sh(args, check=False):
calls.append(args)
if args[:2] == ["rev-parse", "--verify"]:
return {"code": 0, "stdout": MAIN, "stderr": "", "cmd": []}
return {"code": 0, "stdout": "", "stderr": "", "cmd": []}
with patch.object(api, "ensure_git"), patch.object(api, "sh", side_effect=fake_sh):
self.assertEqual(api.checkout("patch/existing", "main", True), "patch/existing")
self.assertIn(["checkout", "patch/existing"], calls)
self.assertFalse(any(call[:2] == ["checkout", "-B"] for call in calls))
self.assertFalse(any(call[:2] == ["checkout", "-b"] for call in calls))
def test_release_script_has_no_ignored_push_or_direct_main_update(self):
script = (ROOT / "scripts" / "release_version.sh").read_text(encoding="utf-8")
self.assertNotIn('git push origin "$VERSION" || true', script)
self.assertNotIn("git checkout main", script)
self.assertIn("approval_plan_id required", script)
self.assertIn("git push \"$BRANCH_REMOTE\"", script)
self.assertNotIn("RELEASE_DONE", script)
self.assertNotIn("merge-base --is-ancestor", script)
self.assertIn('"versions/$VERSION.json"', script)
self.assertNotIn('"baseline/tag_publications/$VERSION.json"', script)
def test_openapi_requires_complete_tag_governance_evidence(self):
schema = (
ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml"
).read_text(encoding="utf-8")
self.assertIn(
"required: [version, target_oid, evidence_closure_oid, approval_plan_id]",
schema,
)
self.assertIn("x-openai-isConsequential: true", schema)
self.assertIn("Remote push pending", schema)
self.assertIn("operationId: getReleaseConsistency", schema)
self.assertIn("operationId: getTagPublicationStatus", schema)
self.assertIn("operationId: runReleaseAcceptance", schema)
document = yaml.safe_load(schema)
self.assertIn("/api/release-consistency", document["paths"])
status_parameters = document["paths"]["/api/tag-publication-status"]["get"][
"parameters"
]
self.assertEqual(
status_parameters[0]["schema"]["pattern"],
r"^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$",
)
publication_states = document["components"]["schemas"]["PublicationState"][
"enum"
]
self.assertIn("LOCAL_TAG_CREATED", publication_states)
self.assertIn("BASELINE_UPDATE_PENDING", publication_states)
self.assertNotIn("TAG_CREATED_LOCAL", publication_states)
def test_documented_normal_state_sequence_matches_canonical_contract(self):
document = (
ROOT / "docs" / "gitops" / "gitea-tag-publication-atomicity.md"
).read_text(encoding="utf-8")
normal_section = document.split("## Legacy state migration", 1)[0]
self.assertIn("LOCAL_TAG_CREATED", normal_section)
self.assertIn("BASELINE_UPDATE_PENDING", normal_section)
self.assertNotIn("TAG_CREATED_LOCAL", normal_section)
def test_delivery_manifest_is_derived_from_git_without_self_reference(self):
generator = ROOT / "scripts" / "generate_delivery_manifest.py"
with tempfile.TemporaryDirectory() as td:
repo = Path(td)
subprocess.run(
["git", "init", "-q", "-b", "main"], cwd=repo, check=True
)
subprocess.run(
["git", "config", "user.email", "test@example.invalid"],
cwd=repo,
check=True,
)
subprocess.run(
["git", "config", "user.name", "test"], cwd=repo, check=True
)
(repo / "base.txt").write_text("base", encoding="utf-8")
subprocess.run(["git", "add", "base.txt"], cwd=repo, check=True)
subprocess.run(
["git", "commit", "-q", "-m", "base"], cwd=repo, check=True
)
base_oid = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=repo, text=True
).strip()
subprocess.run(
["git", "checkout", "-q", "-b", "patch/manifest-test"],
cwd=repo,
check=True,
)
(repo / "scripts").mkdir()
shutil.copy2(generator, repo / "scripts" / generator.name)
(repo / "source.txt").write_text(
"no credential content", encoding="utf-8"
)
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
["git", "commit", "-q", "-m", "source one"], cwd=repo, check=True
)
(repo / "source.txt").write_text(
"no credential content\nsecond", encoding="utf-8"
)
subprocess.run(["git", "add", "source.txt"], cwd=repo, check=True)
subprocess.run(
["git", "commit", "-q", "-m", "source two"], cwd=repo, check=True
)
source_commit = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=repo, text=True
).strip()
subprocess.run(
[
sys.executable,
str(repo / "scripts" / generator.name),
"--base",
"main",
"--output",
"delivery.json",
],
cwd=repo,
check=True,
)
manifest = json.loads((repo / "delivery.json").read_text())
expected_commits = (
subprocess.check_output(
[
"git",
"log",
"--reverse",
"--format=%H",
f"{base_oid}..{source_commit}",
],
cwd=repo,
text=True,
)
.strip()
.splitlines()
)
self.assertEqual(manifest["base"], "main")
self.assertEqual(manifest["base_oid"], base_oid)
self.assertEqual(manifest["source_branch"], "patch/manifest-test")
self.assertEqual(manifest["source_commit"], source_commit)
self.assertEqual(manifest["commits"], expected_commits)
self.assertEqual(manifest["commit_count"], len(expected_commits))
self.assertEqual(manifest["file_count"], len(manifest["files"]))
self.assertEqual(
manifest["manifest_path"],
"delivery.json",
)
self.assertNotIn(manifest["manifest_path"], manifest["files"])
self.assertEqual(
subprocess.check_output(
["git", "rev-parse", f"{manifest['source_commit']}^{{commit}}"],
cwd=repo,
text=True,
).strip(),
source_commit,
)
self.assertNotIn("credential content", json.dumps(manifest))
def test_read_only_release_interfaces_are_allowlisted(self):
self.assertIn("/api/release-consistency", api.READ_ONLY_GET_PATHS)
self.assertIn("/api/tag-publication-status", api.READ_ONLY_GET_PATHS)
self.assertIn("/api/release-acceptance", api.READ_ONLY_GET_PATHS)
def test_release_consistency_query_requires_full_governance_fingerprint(self):
with self.assertRaises(ValueError):
api.publication_request_from_query({"version": ["V9R9C9"]})
parsed = api.publication_request_from_query(
{
"version": ["V9R9C9"],
"target_oid": [TARGET],
"evidence_closure_oid": [EVIDENCE],
"approval_plan_id": ["owner-approved-test"],
}
)
self.assertEqual(parsed.target_oid, TARGET)
self.assertEqual(parsed.approval_plan_id, "owner-approved-test")
def test_committed_security_incident_record_contains_no_secret(self):
record = (
ROOT
/ "security"
/ "incidents"
/ "GITEA-TOKEN-33-EXPOSURE-20260723.redacted.json"
).read_text(encoding="utf-8")
self.assertFalse(api.content_has_secret(record))
self.assertNotIn("token_plain", record)
self.assertNotIn("Authorization", record)
if __name__ == "__main__":
unittest.main()
+872
View File
@@ -0,0 +1,872 @@
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
import gitea_tag_publication as publication_module # noqa: E402
from gitea_tag_publication import ( # noqa: E402
PublicationConflict,
PublicationPending,
PublicationReconciliationRequired,
PublicationRequest,
PublicationState,
PublicationStore,
GiteaTagClient,
GitTagBackend,
TagPublisher,
)
TARGET = "1" * 40
EVIDENCE = "2" * 40
TAG_OID = "3" * 40
WRONG = "4" * 40
class FakeGit:
def __init__(self):
self.local = None
self.remote = None
self.push_error = None
self.remote_error = None
self.lineage_error = None
self.pushes = 0
self.remote_checks = 0
def ensure_local_annotated_tag(self, request):
if self.local and self.local != (TAG_OID, request.target_oid):
raise PublicationConflict("local tag conflict")
self.local = (TAG_OID, request.target_oid)
return self.local
def remote_tag(self, remote, version):
self.remote_checks += 1
if self.remote_error:
raise self.remote_error
return self.remote
def push_tag(self, remote, version):
self.pushes += 1
if self.push_error:
raise self.push_error
self.remote = self.local
def validate_evidence_lineage(self, request):
if self.lineage_error:
raise self.lineage_error
return None
def repository_identity(self, remote, expected_repository):
return expected_repository
class FakeGitea:
def __init__(self):
self.repository = "aios/aios"
self.lookup = (TAG_OID, TARGET)
self.visible = True
self.error = None
self.lookup_checks = 0
self.listing_checks = 0
def lookup_tag(self, version):
self.lookup_checks += 1
if self.error:
raise self.error
return self.lookup
def tag_visible_in_listing(self, version, expected_target_oid):
self.listing_checks += 1
if self.error:
raise self.error
return self.visible
def repository_identity(self):
return self.repository
def request():
return PublicationRequest(
version="V9R9C9",
target_oid=TARGET,
evidence_closure_oid=EVIDENCE,
approval_plan_id="owner-approved-test",
)
class TagPublicationTests(unittest.TestCase):
def publisher(self, root: Path, git=None, gitea=None):
baseline = root / "baseline" / "current.json"
baseline.parent.mkdir(parents=True)
baseline.write_text(
json.dumps({"current_baseline": "V9R9C8", "status": "PRODUCTION_DEPLOYED"}),
encoding="utf-8",
)
return TagPublisher(
workspace=root,
git=git or FakeGit(),
gitea=gitea or FakeGitea(),
remote="gitea",
)
def test_publication_store_rejects_path_traversal_without_reading(self):
invalid_versions = (
"../current",
"../../versions/V2R1C3",
"%2e%2e%2fcurrent",
"/etc/secret",
r"..\current",
"V2R1C3-..",
"",
)
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "saga"
root.mkdir()
bait = root.parent / "current.json"
bait.write_text('{"secret":"BAIT-MUST-NOT-BE-READ"}', encoding="utf-8")
store = PublicationStore(Path(td), state_root=root)
for version in invalid_versions:
with self.subTest(version=version):
with patch.object(
Path,
"read_text",
side_effect=AssertionError("state file read attempted"),
):
with self.assertRaisesRegex(
ValueError, "^invalid release version$"
):
store.load(version)
with self.assertRaisesRegex(
ValueError, "^invalid release version$"
):
store.path(version)
with self.assertRaisesRegex(
ValueError, "^invalid release version$"
):
with store.lock(version):
self.fail("invalid version acquired a publication lock")
self.assertEqual(
bait.read_text(encoding="utf-8"),
'{"secret":"BAIT-MUST-NOT-BE-READ"}',
)
def test_publication_store_accepts_canonical_release_versions(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "saga"
store = PublicationStore(Path(td), state_root=root)
for version in ("V2R1C3", "V2R1C3-rc.1_build"):
with self.subTest(version=version):
self.assertEqual(store.path(version).parent, root.resolve())
self.assertEqual(store.path(version).name, f"{version}.json")
def test_new_publication_uses_canonical_local_and_pending_states(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
publisher = self.publisher(root, git=git, gitea=gitea)
transitions = []
record = publisher.store.record
def observe(publication_request, state, **evidence):
transitions.append(state.value)
return record(publication_request, state, **evidence)
with patch.object(publisher.store, "record", side_effect=observe):
publisher.publish(request())
self.assertIn(PublicationState.LOCAL_TAG_CREATED.value, transitions)
self.assertNotIn("TAG_CREATED_LOCAL", transitions)
self.assertLess(
transitions.index(PublicationState.GITEA_API_CONFIRMED.value),
transitions.index(PublicationState.BASELINE_UPDATE_PENDING.value),
)
self.assertLess(
transitions.index(PublicationState.BASELINE_UPDATE_PENDING.value),
transitions.index(PublicationState.BASELINE_UPDATED.value),
)
def test_legacy_local_tag_state_is_normalized_on_read_and_next_write(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
publisher = self.publisher(root)
path = publisher.store.path(request().version)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{
**request().__dict__,
"state": "TAG_CREATED_LOCAL",
"tag_oid": TAG_OID,
}
),
encoding="utf-8",
)
loaded = publisher.get_tag_publication_status(request().version)
self.assertEqual(loaded["state"], "LOCAL_TAG_CREATED")
self.assertEqual(
json.loads(path.read_text(encoding="utf-8"))["state"],
"TAG_CREATED_LOCAL",
)
publisher.store.record(request(), PublicationState.LOCAL_TAG_CREATED)
persisted = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(persisted["state"], "LOCAL_TAG_CREATED")
self.assertNotIn("TAG_CREATED_LOCAL", json.dumps(persisted))
def test_unknown_persisted_state_fails_closed_without_mutation(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
publisher = self.publisher(root, git=git)
path = publisher.store.path(request().version)
path.parent.mkdir(parents=True, exist_ok=True)
original = {
**request().__dict__,
"state": "UNRECOGNIZED_LEGACY_STATE",
"tag_oid": TAG_OID,
}
path.write_text(json.dumps(original), encoding="utf-8")
status = publisher.get_tag_publication_status(request().version)
self.assertEqual(
status["state"], PublicationState.RECONCILIATION_REQUIRED.value
)
self.assertTrue(status["fail_closed"])
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(request())
self.assertEqual(json.loads(path.read_text(encoding="utf-8")), original)
self.assertEqual(git.pushes, 0)
def test_evidence_lineage_conflict_requires_manual_owner_review(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.lineage_error = PublicationConflict("evidence lineage mismatch")
publisher = self.publisher(root, git=git)
with self.assertRaises(PublicationConflict):
publisher.publish(request())
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value,
)
self.assertEqual(git.pushes, 0)
def test_push_failure_does_not_update_baseline(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.push_error = RuntimeError("push rejected")
publisher = self.publisher(root, git=git)
with self.assertRaises(PublicationPending):
publisher.publish(request())
baseline = json.loads((root / "baseline/current.json").read_text())
state = publisher.store.load("V9R9C9")
self.assertEqual(baseline["current_baseline"], "V9R9C8")
self.assertEqual(state["state"], PublicationState.REMOTE_PUSH_PENDING.value)
def test_deployed_record_with_missing_remote_tag_fails_closed(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
publisher = self.publisher(root, git=git)
publisher.store.record(
request(),
PublicationState.PRODUCTION_DEPLOYED,
tag_oid=TAG_OID,
deployment_evidence="sha256:" + ("a" * 64),
)
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(request())
self.assertEqual(
publisher.store.load("V9R9C9")["state"],
"MANUAL_OWNER_REVIEW_REQUIRED",
)
self.assertEqual(git.pushes, 0)
def test_gitea_api_failure_does_not_deploy_or_update_baseline(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
gitea.error = RuntimeError("gitea unavailable")
publisher = self.publisher(root, git=git, gitea=gitea)
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(request())
baseline = json.loads((root / "baseline/current.json").read_text())
state = publisher.store.load("V9R9C9")
self.assertEqual(baseline["current_baseline"], "V9R9C8")
self.assertNotEqual(state["state"], PublicationState.PRODUCTION_DEPLOYED.value)
self.assertEqual(state["state"], PublicationState.RECONCILIATION_REQUIRED.value)
def test_remote_unreachable_recovers_idempotently(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote_error = RuntimeError("network down")
publisher = self.publisher(root, git=git)
with self.assertRaises(PublicationPending):
publisher.publish(request())
git.remote_error = None
result = publisher.reconcile(request())
self.assertEqual(result["state"], PublicationState.BASELINE_UPDATED.value)
self.assertEqual(git.pushes, 1)
def test_same_name_different_remote_oid_is_rejected(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (WRONG, TARGET)
publisher = self.publisher(root, git=git)
with self.assertRaises(PublicationConflict):
publisher.publish(request())
self.assertEqual(
publisher.store.load("V9R9C9")["state"],
PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value,
)
def test_annotated_tag_requires_exact_object_and_peeled_target(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, WRONG)
publisher = self.publisher(root, git=git)
with self.assertRaises(PublicationConflict):
publisher.publish(request())
self.assertEqual(git.pushes, 0)
def test_local_bare_success_but_gitea_failure_is_fail_closed(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
gitea.visible = False
publisher = self.publisher(root, git=git, gitea=gitea)
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(request())
self.assertEqual(
json.loads((root / "baseline/current.json").read_text())["current_baseline"],
"V9R9C8",
)
def test_restart_recovery_uses_persisted_state(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.push_error = RuntimeError("temporary")
first = self.publisher(root, git=git)
with self.assertRaises(PublicationPending):
first.publish(request())
git.push_error = None
second = TagPublisher(root, git, FakeGitea(), "gitea")
result = second.reconcile(request())
self.assertEqual(result["state"], PublicationState.BASELINE_UPDATED.value)
def test_baseline_write_failure_preserves_update_pending(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
publisher = self.publisher(root, git=git)
atomic_write = publication_module.atomic_json_write
def fail_baseline(path, payload):
if path == publisher.baseline_path:
raise OSError("baseline write failed")
return atomic_write(path, payload)
with (
patch.object(
publication_module, "atomic_json_write", side_effect=fail_baseline
),
self.assertRaises(PublicationReconciliationRequired),
):
publisher.publish(request())
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.BASELINE_UPDATE_PENDING.value,
)
self.assertEqual(
json.loads(publisher.baseline_path.read_text())["current_baseline"],
"V9R9C8",
)
self.assertFalse(
(root / "versions" / f"{request().version}.json").exists()
)
def test_registry_write_failure_recovers_after_restart_without_tag_push(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
publisher = self.publisher(root, git=git)
registry_path = root / "versions" / f"{request().version}.json"
atomic_write = publication_module.atomic_json_write
def fail_registry(path, payload):
if path == registry_path:
raise OSError("registry write failed")
return atomic_write(path, payload)
with (
patch.object(
publication_module, "atomic_json_write", side_effect=fail_registry
),
self.assertRaises(PublicationReconciliationRequired),
):
publisher.publish(request())
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.BASELINE_UPDATE_PENDING.value,
)
self.assertEqual(
json.loads(publisher.baseline_path.read_text())["current_baseline"],
request().version,
)
self.assertFalse(registry_path.exists())
recovered = TagPublisher(root, git, FakeGitea(), "gitea")
result = recovered.reconcile(request())
self.assertEqual(result["state"], PublicationState.BASELINE_UPDATED.value)
self.assertTrue(registry_path.exists())
self.assertEqual(git.pushes, 0)
def test_pending_recovery_fills_missing_baseline_from_matching_registry(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
publisher = self.publisher(root, git=git, gitea=gitea)
publisher.store.record(
request(),
PublicationState.BASELINE_UPDATE_PENDING,
tag_oid=TAG_OID,
)
registry_path = root / "versions" / f"{request().version}.json"
registry_path.parent.mkdir(parents=True, exist_ok=True)
registry_path.write_text(
json.dumps(
{
**request().__dict__,
"tag_oid": TAG_OID,
"tag_object": TAG_OID,
"registered": True,
"enabled": True,
}
),
encoding="utf-8",
)
result = publisher.reconcile(request())
self.assertEqual(result["state"], PublicationState.BASELINE_UPDATED.value)
self.assertEqual(
json.loads(publisher.baseline_path.read_text())["current_baseline"],
request().version,
)
self.assertEqual(git.pushes, 0)
self.assertGreater(git.remote_checks, 0)
self.assertGreater(gitea.lookup_checks, 0)
self.assertGreater(gitea.listing_checks, 0)
def test_pending_recovery_revalidates_remote_and_conflict_requires_owner(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (WRONG, TARGET)
publisher = self.publisher(root, git=git)
publisher.store.record(
request(),
PublicationState.BASELINE_UPDATE_PENDING,
tag_oid=TAG_OID,
)
with self.assertRaises(PublicationConflict):
publisher.reconcile(request())
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value,
)
self.assertEqual(git.pushes, 0)
def test_reconciliation_is_idempotent_and_never_marks_deployed(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
publisher = self.publisher(root, git=git)
first = publisher.publish(request())
second = publisher.reconcile(request())
self.assertEqual(first["state"], PublicationState.BASELINE_UPDATED.value)
self.assertEqual(second["state"], PublicationState.BASELINE_UPDATED.value)
baseline = json.loads((root / "baseline/current.json").read_text())
self.assertEqual(baseline["status"], PublicationState.BASELINE_UPDATED.value)
self.assertEqual(git.pushes, 0)
def test_mark_deployed_requires_baseline_updated_and_evidence(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
publisher = self.publisher(root)
with self.assertRaises(PublicationReconciliationRequired):
publisher.mark_production_deployed(request(), "")
publisher.publish(request())
result = publisher.mark_production_deployed(
request(), "sha256:" + ("a" * 64)
)
self.assertEqual(result["state"], PublicationState.PRODUCTION_DEPLOYED.value)
def test_deployed_fingerprint_change_requires_manual_owner_review(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
publisher = self.publisher(root, git=git)
publisher.store.record(
request(),
PublicationState.PRODUCTION_DEPLOYED,
tag_oid=TAG_OID,
deployment_evidence="sha256:" + ("a" * 64),
)
changed = PublicationRequest(
request().version,
WRONG,
request().evidence_closure_oid,
request().approval_plan_id,
)
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(changed)
state = publisher.store.load(request().version)
self.assertEqual(
state["state"], PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value
)
self.assertIn("target_oid", state["conflict_fields"])
self.assertEqual(git.pushes, 0)
def test_deployed_gitea_tag_object_change_requires_manual_review(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
gitea.lookup = (WRONG, TARGET)
publisher = self.publisher(root, git=git, gitea=gitea)
publisher.store.record(
request(),
PublicationState.PRODUCTION_DEPLOYED,
tag_oid=TAG_OID,
deployment_evidence="sha256:" + ("a" * 64),
)
with self.assertRaises(PublicationReconciliationRequired):
publisher.publish(request())
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value,
)
def test_mark_deployed_rejects_different_approval_plan_id(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
publisher = self.publisher(root)
publisher.publish(request())
changed = PublicationRequest(
request().version,
request().target_oid,
request().evidence_closure_oid,
"different-owner-plan",
)
with self.assertRaises(PublicationReconciliationRequired):
publisher.mark_production_deployed(
changed, "sha256:" + ("a" * 64)
)
self.assertEqual(
publisher.store.load(request().version)["state"],
PublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value,
)
def test_state_survives_writer_worktree_deletion(self):
with tempfile.TemporaryDirectory() as td:
base = Path(td)
first_workspace = base / "writer-a"
first_workspace.mkdir()
state_root = base / "durable-state"
publisher = TagPublisher(
first_workspace,
FakeGit(),
FakeGitea(),
"gitea",
state_root=state_root,
)
publisher.store.record(request(), PublicationState.REMOTE_PUSH_PENDING)
shutil.rmtree(first_workspace)
second_workspace = base / "writer-b"
second_workspace.mkdir()
recovered = TagPublisher(
second_workspace,
FakeGit(),
FakeGitea(),
"gitea",
state_root=state_root,
)
self.assertEqual(
recovered.get_tag_publication_status(request().version)["state"],
PublicationState.REMOTE_PUSH_PENDING.value,
)
def test_duplicate_publication_lock_serializes_writers(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
state_root = root / "durable-state"
first = TagPublisher(
root, FakeGit(), FakeGitea(), "gitea", state_root=state_root
)
second = TagPublisher(
root, FakeGit(), FakeGitea(), "gitea", state_root=state_root
)
entered = threading.Event()
def wait_for_lock():
with second.store.lock(request().version):
entered.set()
with first.store.lock(request().version):
worker = threading.Thread(target=wait_for_lock)
worker.start()
time.sleep(0.05)
self.assertFalse(entered.is_set())
worker.join(timeout=2)
self.assertTrue(entered.is_set())
def test_registry_baseline_mismatch_blocks_deployment(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
publisher = self.publisher(root)
publisher.publish(request())
registry_path = root / "versions" / f"{request().version}.json"
registry = json.loads(registry_path.read_text(encoding="utf-8"))
registry["target_oid"] = WRONG
registry_path.write_text(json.dumps(registry), encoding="utf-8")
with self.assertRaises(PublicationReconciliationRequired):
publisher.mark_production_deployed(
request(), "sha256:" + ("a" * 64)
)
self.assertNotEqual(
publisher.store.load(request().version)["state"],
PublicationState.PRODUCTION_DEPLOYED.value,
)
def test_release_consistency_is_unknown_and_fail_closed_on_api_failure(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
git = FakeGit()
git.remote = (TAG_OID, TARGET)
gitea = FakeGitea()
publisher = self.publisher(root, git=git, gitea=gitea)
publisher.publish(request())
gitea.error = RuntimeError("Authorization: token super-secret")
result = publisher.run_release_acceptance(request())
self.assertEqual(result["status"], "UNKNOWN")
self.assertFalse(result["accepted"])
self.assertTrue(result["fail_closed"])
self.assertNotIn("super-secret", result["error"])
def test_release_consistency_fingerprint_failure_is_read_only(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
publisher = self.publisher(root)
publisher.publish(request())
before = publisher.store.load(request().version)
changed = PublicationRequest(
request().version,
WRONG,
request().evidence_closure_oid,
request().approval_plan_id,
)
result = publisher.get_release_consistency(changed)
after = publisher.store.load(request().version)
self.assertEqual(result["status"], "FAIL")
self.assertTrue(result["fail_closed"])
self.assertEqual(after, before)
def test_cli_evidence_lineage_mismatch_is_fail_closed(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
subprocess.run(["git", "init", "-q", "-b", "patch/test"], cwd=root, check=True)
subprocess.run(
["git", "config", "user.email", "test@example.invalid"],
cwd=root,
check=True,
)
subprocess.run(["git", "config", "user.name", "test"], cwd=root, check=True)
(root / "a").write_text("a", encoding="utf-8")
subprocess.run(["git", "add", "a"], cwd=root, check=True)
subprocess.run(["git", "commit", "-q", "-m", "a"], cwd=root, check=True)
target = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=root, text=True
).strip()
subprocess.run(
["git", "checkout", "-q", "--orphan", "unrelated"], cwd=root, check=True
)
subprocess.run(["git", "rm", "-q", "-rf", "."], cwd=root, check=True)
(root / "b").write_text("b", encoding="utf-8")
subprocess.run(["git", "add", "b"], cwd=root, check=True)
subprocess.run(["git", "commit", "-q", "-m", "b"], cwd=root, check=True)
evidence = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=root, text=True
).strip()
authoritative = root / "authoritative.git"
subprocess.run(["git", "init", "-q", "--bare", str(authoritative)], check=True)
subprocess.run(
["git", "--git-dir", str(authoritative), "config", "remote.gitea.url", "https://g.cnnetmkt.top/git/aios/aios.git"],
check=True,
)
subprocess.run(["git", "-C", str(root), "remote", "add", "authority", str(authoritative)], check=True)
subprocess.run(["git", "-C", str(root), "push", "-q", "authority", "--all"], check=True)
module = ROOT / "active-runtime" / "app" / "gitea_tag_publication.py"
cli_env = {
**os.environ,
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(authoritative),
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
}
result = subprocess.run(
[
sys.executable,
str(module),
"publish",
"--workspace",
str(root),
"--version",
"V9R9C9",
"--target-oid",
target,
"--evidence-closure-oid",
evidence,
"--approval-plan-id",
"owner-plan",
"--gitea-api",
"http://127.0.0.1:1/api/v1",
"--gitea-repo",
"aios/aios",
"--gitea-token-file",
str(root / "missing-token"),
],
text=True,
capture_output=True,
env=cli_env,
)
self.assertEqual(result.returncode, 2)
self.assertIn("evidence lineage mismatch", result.stdout)
def test_release_script_uses_shared_lineage_validation(self):
script = (ROOT / "scripts" / "release_version.sh").read_text(encoding="utf-8")
self.assertIn("gitea_tag_publication.py\" publish", script)
self.assertNotIn("merge-base --is-ancestor", script)
def test_error_redaction_hides_authorization_and_token_values(self):
from gitea_tag_publication import redact
message = redact(
"Authorization: Bearer abcdefghijklmnop token=super-secret "
"https://user:password@example.invalid/repo.git"
)
self.assertNotIn("abcdefghijklmnop", message)
self.assertNotIn("super-secret", message)
self.assertNotIn("password", message)
def test_remote_parser_requires_annotated_tag_object_and_peeled_target(self):
backend = GitTagBackend(Path("."))
with (
patch.object(backend, "_network_remote_url", return_value="ssh://gitea/repo"),
patch.object(
backend,
"run",
return_value=(
f"{TAG_OID}\trefs/tags/V9R9C9\n"
f"{TARGET}\trefs/tags/V9R9C9^{{}}"
),
),
):
self.assertEqual(backend.remote_tag("gitea", "V9R9C9"), (TAG_OID, TARGET))
with (
patch.object(backend, "_network_remote_url", return_value="ssh://gitea/repo"),
patch.object(
backend,
"run",
return_value=f"{TARGET}\trefs/tags/V9R9C9",
),
self.assertRaises(PublicationConflict),
):
backend.remote_tag("gitea", "V9R9C9")
def test_local_path_remote_cannot_count_as_gitea(self):
backend = GitTagBackend(Path("."))
with (
patch.object(backend, "run", return_value="/data/aios-gitops/repo.git"),
self.assertRaises(PublicationConflict),
):
backend._network_remote_url("origin")
def test_process_config_remote_url_is_resolved_for_subprocesses(self):
backend = GitTagBackend(Path("."))
with (
patch.object(backend, "run", side_effect=RuntimeError("no such remote")),
patch.dict(
os.environ,
{
"GIT_CONFIG_COUNT": "1",
"GIT_CONFIG_KEY_0": "remote.gitea-local.url",
"GIT_CONFIG_VALUE_0": "http://127.0.0.1:3000/aios/aios.git",
},
clear=False,
),
):
self.assertEqual(
backend._network_remote_url("gitea-local"),
"http://127.0.0.1:3000/aios/aios.git",
)
def test_gitea_lookup_and_listing_use_authenticated_api(self):
with tempfile.TemporaryDirectory() as td:
token = Path(td) / "token"
token.write_text("test-token", encoding="utf-8")
client = GiteaTagClient("https://gitea.invalid/api/v1", "aios/aios", token)
paths = []
def fake_get(path):
paths.append(path)
if "/git/refs/tags/" in path:
return {"object": {"type": "tag", "sha": TAG_OID}}
if "/git/tags/" in path:
return {"object": {"type": "commit", "sha": TARGET}}
return [{"name": "V9R9C9", "commit": {"sha": TARGET}}]
with patch.object(client, "_get", side_effect=fake_get):
self.assertEqual(client.lookup_tag("V9R9C9"), (TAG_OID, TARGET))
self.assertTrue(client.tag_visible_in_listing("V9R9C9", TARGET))
self.assertTrue(any("/git/refs/tags/" in path for path in paths))
self.assertTrue(any("/tags?limit=" in path for path in paths))
if __name__ == "__main__":
unittest.main()
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import unittest
import urllib.request
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from gitea_tag_publication import ( # noqa: E402
GiteaTagClient,
GitTagBackend,
PublicationRequest,
)
@unittest.skipUnless(
os.getenv("AIOS_GITOPS_LIVE_GITEA_TEST") == "1",
"set AIOS_GITOPS_LIVE_GITEA_TEST=1 on the isolated writer host",
)
class LiveTemporaryGiteaIntegrationTest(unittest.TestCase):
def test_authenticated_ref_api_and_listing_against_temporary_repository(self):
api_url = os.getenv(
"AIOS_GITOPS_GITEA_API_URL", "http://127.0.0.1:3000/api/v1"
).rstrip("/")
credential = Path(
os.getenv(
"AIOS_GITOPS_LIVE_GITEA_TOKEN_FILE",
os.getenv(
"AIOS_GITOPS_GITEA_TOKEN_FILE",
"/etc/aios-gitops/credentials/gitea-git-http",
),
)
)
token_line = int(os.getenv("AIOS_GITOPS_LIVE_GITEA_TOKEN_LINE", "1"))
token = credential.read_text(encoding="utf-8").splitlines()[token_line - 1].strip()
name = "tag-atomicity-integration-" + uuid.uuid4().hex[:12]
owner = os.getenv("AIOS_GITOPS_LIVE_GITEA_OWNER", "gitops-bot")
def api_request(method: str, path: str, payload=None):
body = (
json.dumps(payload).encode("utf-8") if payload is not None else None
)
request = urllib.request.Request(
api_url + path,
data=body,
method=method,
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"token {token}",
},
)
with urllib.request.urlopen(request, timeout=15) as response:
raw = response.read()
return json.loads(raw.decode("utf-8")) if raw else None
created = False
try:
repository = api_request(
"POST",
"/user/repos",
{"name": name, "private": True, "auto_init": False},
)
created = True
self.assertEqual(repository["full_name"], f"{owner}/{name}")
with tempfile.TemporaryDirectory() as td:
work = Path(td)
subprocess.run(["git", "init", "-q", "-b", "patch/live"], cwd=work, check=True)
subprocess.run(
["git", "config", "user.email", "gitops-test@example.invalid"],
cwd=work,
check=True,
)
subprocess.run(
["git", "config", "user.name", "gitops-integration"],
cwd=work,
check=True,
)
(work / "release").write_text("target\n", encoding="utf-8")
subprocess.run(["git", "add", "release"], cwd=work, check=True)
subprocess.run(
["git", "commit", "-q", "-m", "target"], cwd=work, check=True
)
target = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=work, text=True
).strip()
(work / "evidence").write_text("closure\n", encoding="utf-8")
subprocess.run(["git", "add", "evidence"], cwd=work, check=True)
subprocess.run(
["git", "commit", "-q", "-m", "evidence"], cwd=work, check=True
)
evidence = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=work, text=True
).strip()
remote_url = f"http://127.0.0.1:3000/{owner}/{name}.git"
subprocess.run(["git", "remote", "add", "gitea", remote_url], cwd=work, check=True)
username_file = work / ".fixture-user"
username_file.write_text(owner + "\n", encoding="utf-8")
askpass = work / ".fixture-askpass"
askpass.write_text(
"#!/bin/sh\n"
'case \"$1\" in\n'
f' *Username*) sed -n 1p "{username_file}" ;;\n'
f' *) sed -n {token_line}p "{credential}" ;;\n'
"esac\n",
encoding="utf-8",
)
askpass.chmod(0o700)
git_env = {
**os.environ,
"GIT_ASKPASS": str(askpass),
"GIT_TERMINAL_PROMPT": "0",
}
subprocess.run(
["git", "push", "gitea", "HEAD:refs/heads/patch/live"],
cwd=work,
check=True,
capture_output=True,
text=True,
env=git_env,
)
version = "V0R0C1-LIVE-" + uuid.uuid4().hex[:8]
release = PublicationRequest(version, target, evidence, "live-integration")
backend = GitTagBackend(work)
os.environ["GIT_ASKPASS"] = str(askpass)
os.environ["GIT_TERMINAL_PROMPT"] = "0"
backend.validate_evidence_lineage(release)
tag_oid, peeled = backend.ensure_local_annotated_tag(release)
backend.push_tag("gitea", version)
self.assertEqual(backend.remote_tag("gitea", version), (tag_oid, peeled))
client = GiteaTagClient(
api_url,
f"{owner}/{name}",
credential,
token_line=token_line,
)
self.assertEqual(client.repository_identity(), f"{owner}/{name}")
self.assertEqual(client.lookup_tag(version), (tag_oid, target))
self.assertTrue(client.tag_visible_in_listing(version, target))
finally:
if created:
api_request("DELETE", f"/repos/{owner}/{name}")
if __name__ == "__main__":
unittest.main()
+369
View File
@@ -0,0 +1,369 @@
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from silent_release_controller import ( # noqa: E402
AcceptanceEvidence,
ApprovalEnvelope,
ApprovalError,
ApprovalLedger,
DeploymentController,
DuplicateTagConflict,
ReleaseAcceptanceEngine,
ReleaseCandidate,
ReleaseCandidateStore,
ReleaseState,
SilentReleaseController,
SilentReleasePolicy,
TagApprovalEnvelope,
TagController,
redact,
)
COMMIT = "1" * 40
TREE = "2" * 40
ARTIFACT = "3" * 64
MANIFEST = "4" * 64
TAG_OID = "5" * 40
TARGET = "6" * 40
def candidate() -> ReleaseCandidate:
return ReleaseCandidate(
candidate_id="rc-20260723-001",
source_commit=COMMIT,
tree_oid=TREE,
artifact_sha256=ARTIFACT,
manifest_sha256=MANIFEST,
)
def passing_evidence() -> AcceptanceEvidence:
return AcceptanceEvidence(
local_main_oid=COMMIT,
bare_main_oid=COMMIT,
gitea_main_oid=COMMIT,
repository_identity="aios/aios",
artifact_sha256=ARTIFACT,
manifest_sha256=MANIFEST,
manifest_source_commit=COMMIT,
manifest_tree_oid=TREE,
tests={"passed": True, "summary": "81 passed"},
canary={
"passed": True,
"runtime_without_git": True,
"writes_enabled": False,
},
rollback_ready=True,
authoritative_git_dir="/data/aios-gitops/repo.git",
authoritative_git_readable=True,
)
class FakeProbe:
def __init__(self, evidence=None, error=None):
self.evidence = evidence or passing_evidence()
self.error = error
self.calls = 0
def collect(self, release_candidate):
self.calls += 1
if self.error:
raise self.error
return self.evidence
class FakeBuilder:
def __init__(self):
self.calls = 0
def build(self, source_commit):
self.calls += 1
self.source_commit = source_commit
return candidate()
class FakeDeployment:
def __init__(self, health=True, restart_error=None):
self.health = health
self.restart_error = restart_error
self.switched = 0
self.restarted = 0
self.rolled_back = 0
def atomic_switch(self, release_candidate):
self.switched += 1
return {"previous_target": "old", "new_target": release_candidate.candidate_id}
def restart(self):
self.restarted += 1
if self.restart_error:
raise self.restart_error
def health_check(self, release_candidate):
return {
"passed": self.health,
"runtime_commit": release_candidate.source_commit,
"writes_enabled": False,
"authoritative_head_oid": release_candidate.source_commit,
"repository_identity": "aios/aios",
}
def rollback(self, switch_evidence):
self.rolled_back += 1
return {"passed": True, "writes_enabled": False}
class FakeTagAdapter:
def __init__(self):
self.current = None
self.creates = 0
def lookup(self, version):
return self.current
def create(self, version, tag_oid, target_oid):
self.creates += 1
self.current = (tag_oid, target_oid)
def verify(self, version):
return self.current
class SilentReleaseControllerTests(unittest.TestCase):
def test_top_level_controller_automates_prepare_then_waits_for_owner(self):
builder = FakeBuilder()
with tempfile.TemporaryDirectory() as td:
store = ReleaseCandidateStore(Path(td) / "candidates")
controller = SilentReleaseController(
builder=builder,
acceptance=ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
),
deployment=DeploymentController(
FakeDeployment(),
ApprovalLedger(Path(td) / "approvals"),
SilentReleasePolicy.default(),
store,
allow_legacy_unsigned_approval=True,
),
store=store,
policy=SilentReleasePolicy.default(),
)
prepared = controller.prepare(COMMIT)
self.assertEqual(builder.calls, 1)
self.assertEqual(prepared.state, ReleaseState.READY_FOR_APPROVAL)
self.assertEqual(prepared.approval_state, "PENDING")
self.assertEqual(
store.load(prepared.candidate_id).state,
ReleaseState.READY_FOR_APPROVAL,
)
def test_candidate_model_round_trips_and_enforces_state_machine(self):
value = candidate()
self.assertEqual(value.state, ReleaseState.BUILDING)
value.transition(ReleaseState.VERIFYING)
restored = ReleaseCandidate.from_dict(value.to_dict())
self.assertEqual(restored.state, ReleaseState.VERIFYING)
with self.assertRaisesRegex(ValueError, "invalid release transition"):
restored.transition(ReleaseState.VERIFIED)
def test_acceptance_engine_moves_verified_candidate_to_ready(self):
value = candidate()
result = ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
).evaluate(value)
self.assertTrue(result.ready)
self.assertEqual(value.state, ReleaseState.READY_FOR_APPROVAL)
self.assertTrue(value.rollback_ready)
self.assertTrue(value.tests["passed"])
self.assertTrue(value.canary["runtime_without_git"])
self.assertEqual(value.git_consistency["bare_main_oid"], COMMIT)
self.assertEqual(value.gitea_consistency["main_oid"], COMMIT)
def test_artifact_mismatch_fails_closed(self):
evidence = replace(passing_evidence(), artifact_sha256="9" * 64)
value = candidate()
result = ReleaseAcceptanceEngine(
FakeProbe(evidence), expected_repository="aios/aios"
).evaluate(value)
self.assertFalse(result.ready)
self.assertEqual(value.state, ReleaseState.FAILED)
self.assertIn("artifact_sha256", result.failed_checks)
def test_gitea_unavailable_fails_closed(self):
value = candidate()
result = ReleaseAcceptanceEngine(
FakeProbe(error=RuntimeError("GITEA_UNAVAILABLE")),
expected_repository="aios/aios",
).evaluate(value)
self.assertFalse(result.ready)
self.assertEqual(value.state, ReleaseState.FAILED)
self.assertIn("GITEA_UNAVAILABLE", value.failure_reason)
def test_git_unavailable_fails_closed(self):
value = candidate()
result = ReleaseAcceptanceEngine(
FakeProbe(error=RuntimeError("AUTHORITATIVE_GIT_UNAVAILABLE")),
expected_repository="aios/aios",
).evaluate(value)
self.assertFalse(result.ready)
self.assertEqual(value.state, ReleaseState.FAILED)
self.assertIn("AUTHORITATIVE_GIT_UNAVAILABLE", value.failure_reason)
def test_runtime_with_git_metadata_is_rejected(self):
evidence = replace(
passing_evidence(),
canary={"passed": True, "runtime_without_git": False},
)
value = candidate()
result = ReleaseAcceptanceEngine(
FakeProbe(evidence), expected_repository="aios/aios"
).evaluate(value)
self.assertFalse(result.ready)
self.assertIn("runtime_without_git", result.failed_checks)
def test_runtime_switch_requires_one_bound_owner_approval(self):
value = candidate()
ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
).evaluate(value)
envelope = ApprovalEnvelope.for_candidate(
value,
approval_id="approval-001",
owner_id="owner",
expires_at="2099-01-01T00:00:00Z",
)
with tempfile.TemporaryDirectory() as td:
deployment = FakeDeployment()
controller = DeploymentController(
deployment,
ApprovalLedger(Path(td) / "approvals"),
SilentReleasePolicy.default(),
allow_legacy_unsigned_approval=True,
)
result = controller.deploy(value, envelope)
self.assertEqual(value.state, ReleaseState.VERIFIED)
self.assertTrue(result["verified"])
self.assertEqual(deployment.switched, 1)
self.assertEqual(deployment.restarted, 1)
self.assertEqual(deployment.rolled_back, 0)
def test_failed_health_check_rolls_back(self):
value = candidate()
ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
).evaluate(value)
envelope = ApprovalEnvelope.for_candidate(
value,
approval_id="approval-rollback",
owner_id="owner",
expires_at="2099-01-01T00:00:00Z",
)
with tempfile.TemporaryDirectory() as td:
deployment = FakeDeployment(health=False)
result = DeploymentController(
deployment,
ApprovalLedger(Path(td) / "approvals"),
SilentReleasePolicy.default(),
allow_legacy_unsigned_approval=True,
).deploy(value, envelope)
self.assertFalse(result["verified"])
self.assertEqual(value.state, ReleaseState.ROLLED_BACK)
self.assertEqual(deployment.rolled_back, 1)
def test_approval_replay_is_rejected(self):
value = candidate()
ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
).evaluate(value)
envelope = ApprovalEnvelope.for_candidate(
value,
approval_id="approval-replay",
owner_id="owner",
expires_at="2099-01-01T00:00:00Z",
)
with tempfile.TemporaryDirectory() as td:
ledger = ApprovalLedger(Path(td) / "approvals")
controller = DeploymentController(
FakeDeployment(),
ledger,
SilentReleasePolicy.default(),
allow_legacy_unsigned_approval=True,
)
controller.deploy(value, envelope)
second = candidate()
ReleaseAcceptanceEngine(
FakeProbe(), expected_repository="aios/aios"
).evaluate(second)
with self.assertRaisesRegex(ApprovalError, "already consumed"):
controller.deploy(second, envelope)
def test_tag_controller_is_separate_and_rejects_duplicate_conflict(self):
adapter = FakeTagAdapter()
adapter.current = (TAG_OID, TARGET)
controller = TagController(
adapter,
SilentReleasePolicy.default(),
ledger=None,
allow_legacy_unsigned_approval=True,
)
envelope = TagApprovalEnvelope(
approval_id="tag-approval-001",
owner_id="owner",
version="V9R9C9",
tag_oid="7" * 40,
target_oid=TARGET,
action="production_tag",
expires_at="2099-01-01T00:00:00Z",
)
approved = controller.approve(envelope)
with self.assertRaises(DuplicateTagConflict):
controller.execute(approved)
self.assertEqual(adapter.creates, 0)
def test_policy_separates_automatic_owner_and_forbidden_actions(self):
policy = SilentReleasePolicy.default()
self.assertTrue(policy.is_automatic("canary"))
self.assertTrue(policy.requires_owner("production_runtime_switch"))
self.assertTrue(policy.requires_owner("production_tag"))
self.assertTrue(policy.is_forbidden("force_push"))
self.assertTrue(policy.is_forbidden("tag_rewrite"))
self.assertTrue(policy.is_forbidden("direct_main_push"))
self.assertTrue(policy.is_forbidden("baseline_mutation"))
self.assertTrue(policy.is_forbidden("registry_mutation"))
def test_redaction_removes_credentials_from_errors(self):
message = (
"Authorization: Bearer super-secret-token "
"https://bot:password@example.invalid/repo.git token=another-secret"
)
safe = redact(message)
self.assertNotIn("super-secret-token", safe)
self.assertNotIn("password", safe)
self.assertNotIn("another-secret", safe)
self.assertIn("REDACTED", safe)
def test_candidate_store_persists_without_secret_fields(self):
value = candidate()
value.tests = {"passed": True, "Authorization": "Bearer hidden"}
with tempfile.TemporaryDirectory() as td:
store = ReleaseCandidateStore(Path(td))
store.save(value)
raw = store.path(value.candidate_id).read_text(encoding="utf-8")
self.assertNotIn("Bearer hidden", raw)
loaded = store.load(value.candidate_id)
self.assertEqual(loaded.candidate_id, value.candidate_id)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
import importlib.util
import sys
from pathlib import Path
if len(sys.argv) != 3:
raise SystemExit('usage: run_online_smoke_18920.py <source-root> <output>')
source_root = Path(sys.argv[1]).resolve()
script = source_root / 'tools' / 'aios_v2r1c2_online_smoke.py'
spec = importlib.util.spec_from_file_location('v2r1c2_online_smoke_production', script)
if spec is None or spec.loader is None:
raise SystemExit('unable to load approved online smoke harness')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.BASE = 'http://127.0.0.1:18920'
raise SystemExit(module.main(sys.argv[2]))
+55
View File
@@ -0,0 +1,55 @@
{
"version": "V2R1C1",
"codename": "openclaw-command-v2-production-baseline",
"status": "PRODUCTION_DEPLOYED",
"tag": "V2R1C1",
"tag_ref": "refs/tags/V2R1C1",
"tag_object": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"previous_version": "V1R4C6",
"rollback_version": "V1R4C6",
"baseline": "V1R4C6",
"release_reason": "Register the already deployed AIOS V2R1C1 release, immutable tag, OpenClaw Command v2 companion, and final production evidence in the central GitOps Registry.",
"purpose": "Close the central version-governance gap without changing product implementation, the V2R1C1 tag, production release, deployment, or traffic.",
"changes": [
"governance: add the canonical V2R1C1 version manifest",
"governance: advance the central baseline pointer from V1R4C6 to V2R1C1",
"governance: link release, companion, artifact, validation, and final audit evidence"
],
"artifacts": [
"docs/releases/V2R1C1-BASELINE-CLOSURE.md",
".agent/results/releases/V2R1C1-BASELINE-CLOSURE.json",
".agent/results/releases/V2R1C1-BASELINE-CLOSURE.md",
"/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z"
],
"validated_actions": [
"release and tag OID equality",
"local and Gitea ref synchronization",
"OpenClaw Command v2 positive and negative validation",
"Gate A and Gate B",
"Hermes lifecycle 17/17",
"grant isolation 11/11",
"restart recovery",
"rollback and forward recovery",
"SQLite quick_check",
"secret scan with zero violations",
"final production healthcheck"
],
"secret_plaintext_written": false,
"owner_approval_required": true,
"owner_notification_channels": [
"codex_owner_instruction",
"gitops_audit"
],
"constraints_original": {
"product_code_changed": false,
"tag_changed": false,
"production_redeployed": false,
"production_traffic_changed": false,
"force_push_allowed": false,
"incubator_in_release_graph": false
},
"promoted_at": "2026-07-21T14:17:05Z",
"promoted_by": "chatgpt/owner-approved-governance",
"created_at": "2026-07-21T14:17:05Z",
"author": "chatgpt"
}
+47
View File
@@ -0,0 +1,47 @@
{
"version": "V2R1C2",
"codename": "unified-role-aware-portal",
"status": "PRODUCTION_DEPLOYED",
"tag": "V2R1C2",
"tag_ref": "refs/tags/V2R1C2",
"tag_object": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
"previous_version": "V2R1C1",
"rollback_version": "V2R1C1",
"baseline": "V2R1C1",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"release_reason": "Register the Owner-approved V2R1C2 candidate tag and verified evidence before a separately gated production promotion.",
"candidate_url": "http://127.0.0.1:18921",
"production_url": "http://127.0.0.1:18920",
"aggregate_gate": "PASS",
"required_stages": "3/3 SUCCEEDED",
"evidence_status": "COMPLETE",
"wechat_acceptance": "SYNTHETIC_ONLY",
"real_user_wechat_accepted": false,
"evidence_manifest_sha256": "2e27bb73b8e6297bd3077238f73a96d697fc3c3611c601c4dc9357e5fdcd8eee",
"evidence_checksums_sha256": "3dcedaff367d5d1a114640d8918304d1c39154d38b078f7f0b097a95d43978be",
"prepromotion_acceptance_sha256": "402e9ce7690654ea7fd493985284446365a827b61fe195576dc7f4e4d4499ef7",
"audited_tag_plan": "nl-1784681185786-19ac0f82",
"owner_release_authorization": "GRANTED",
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
"production_evidence_index": "/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z/production-evidence.sha256",
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
"production_service": "aios-v2r1c2-production.service",
"production_promoted": true,
"main_changed_by_tag": false,
"main_changed_by_authorized_registry": true,
"force_push": false,
"constraints": {
"tag_rewrite_allowed": false,
"force_push_allowed": false,
"rollback_assets_must_be_preserved": true,
"production_promotion_requires_fresh_acceptance": true
},
"registered_at": "2026-07-22T01:18:36Z",
"promoted_at": "2026-07-22T01:40:51Z",
"updated_at": "2026-07-22T01:43:15Z",
"registered_by": "codex/owner-approved-gitops",
"promoted_by": "codex/owner-approved-gitops"
}
@@ -1,31 +0,0 @@
# AIOS Central Orchestrator Workspace Agent
Contract commit: `cbe33ecab8e9794ba5f318bea43aeaf447a56fbc`
When an API-triggered input arrives:
1. Parse the AIOS event JSON from the trigger message.
2. Verify `contract_commit` exactly matches the configured contract commit.
3. Use connected GitOps tools to read the authoritative Git tree, runtime status,
gate state, test evidence and full OIDs.
4. Never invent evidence, credentials, secrets, tests, approvals, Git results,
model availability or deployment status.
5. Apply the adaptive dispatch contract:
- Prefer OpenClaw when quality is comparable, work is large/repetitive,
monitoring-heavy, infrastructure-heavy or cost-sensitive.
- Prefer the strongest actually available Codex model for complex code,
security boundaries, architecture and final high-risk review.
- On timeout/unavailability, mark the attempt `TIMEOUT` or `NOT_EXECUTED`
and assign the same contract to the fallback agent.
- Never infer PASS from timeout or missing output.
6. Every next work unit must include a clean base commit, allowed paths, required
checks, forbidden operations, primary agent and fallback agent.
7. Keep real credential, raw-wire, reviewer, secret-provider and owner-only
release gates fail-closed.
8. Submit exactly one strict decision through the
`submit_orchestration_decision` action.
9. Do not write a second decision for the same event ID.
10. Stop after the action succeeds.
The decision must satisfy the JSON schema in the connected action. Use the
incoming `event_id`, `project_key`, `contract_commit`, and `state_version`.
@@ -1,114 +0,0 @@
openapi: 3.1.0
info:
title: AIOS Orchestrator Decision Callback
version: 1.0.0
servers:
- url: https://REPLACE_WITH_GATEWAY_HOST
paths:
/v1/orchestrator/decisions:
post:
operationId: submit_orchestration_decision
summary: Submit one strict orchestration decision for a triggered event
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Decision'
responses:
'202':
description: Accepted
'409':
description: Contract, idempotency, or state version conflict
'422':
description: Schema validation failed
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
WorkUnit:
type: object
additionalProperties: false
required:
- id
- track_id
- gate_id
- primary_agent
- fallback_agent
- clean_base_commit
- allowed_paths
- required_checks
- forbidden_operations
- instruction
properties:
id: {type: string}
track_id: {type: string}
gate_id: {type: string}
primary_agent:
type: string
enum: [openclaw, codex]
fallback_agent:
type: string
enum: [openclaw, codex]
clean_base_commit:
type: string
pattern: '^[0-9a-f]{40}$'
allowed_paths:
type: array
items: {type: string}
required_checks:
type: array
items: {type: string}
forbidden_operations:
type: array
items: {type: string}
instruction: {type: string}
Decision:
type: object
additionalProperties: false
required:
- schema_version
- project_key
- contract_commit
- event_id
- expected_state_version
- verdict
- state_updates
- next_work_units
- requires_human_input
- stop_reason
properties:
schema_version:
type: integer
const: 1
project_key: {type: string}
contract_commit:
type: string
pattern: '^[0-9a-f]{40}$'
event_id: {type: string}
expected_state_version:
type: integer
minimum: 0
verdict:
type: string
enum: [DISPATCH, BLOCKED, COMPLETE, NOOP]
state_updates:
type: array
items:
type: object
additionalProperties: false
required: [key, value]
properties:
key: {type: string}
value: {type: string}
next_work_units:
type: array
items:
$ref: '#/components/schemas/WorkUnit'
requires_human_input: {type: boolean}
stop_reason:
type: [string, 'null']