Compare commits
-83
@@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
"approval_request_branch": "governance/request/integration-refresh-control-plane-deployment-20260728T093428Z",
|
|
||||||
"approval_type": "CONTROL_PLANE_DEPLOYMENT_APPROVAL_REQUEST",
|
|
||||||
"authorization_lineage": {
|
|
||||||
"execution_renewal_merge_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
|
||||||
"grant_id": "grant-11354281aafdd074df5899a3925dd2a0370cffa9edb06f5c7d7227ce8de00d91",
|
|
||||||
"source_mapping_approval_merge_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6"
|
|
||||||
},
|
|
||||||
"deployment_status": "NOT_EXECUTED",
|
|
||||||
"evidence": {
|
|
||||||
"control_plane_artifact": {
|
|
||||||
"path": "/var/lib/aios-integration-refresh-source-writer-v2/artifacts/control-plane-patch-112d995545e3e79126a062b2643a9f0c17ae7bd4.tar",
|
|
||||||
"sha256": "bf101ade1aaf956f40c0bba74ed4b347f5f38d0de6a0c1b7694ae107c4b43c14",
|
|
||||||
"size_bytes": 13363200
|
|
||||||
},
|
|
||||||
"rendered_systemd_config_sha256": "182a3cba48e752798cf24323a8c0249c53d7386371388a8db0435a29c43dc969",
|
|
||||||
"rollback_artifact": {
|
|
||||||
"path": "/var/lib/aios-gitops/runtime/tasks/AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726/implementation/rollback-to-85426f4f570e4d40b212318d47e52e4aed5cb06a.tar",
|
|
||||||
"sha256": "f12a2a95e475da5d02eaff99e93615cb0186019ca30edae013ab054a2c6418ce"
|
|
||||||
},
|
|
||||||
"source_map": {
|
|
||||||
"path": "docs/autonomous-release/INTEGRATION_REFRESH_SOURCE_MAP_20260728.json",
|
|
||||||
"sha256": "01ddd56cfa2a525559e2d174cf7df117a9465ffce44ef25bc7a15b8e74d06bf5"
|
|
||||||
},
|
|
||||||
"test_evidence": {
|
|
||||||
"collection_errors": 0,
|
|
||||||
"path": "/var/lib/aios-gitops/runtime/tasks/AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726/implementation/test-evidence.json",
|
|
||||||
"sha256": "d8933553f56462a3f1b3040dec989ff0adf717a17728d55ba0fe9e127d93c206",
|
|
||||||
"tests_failed": 0,
|
|
||||||
"tests_passed": 41
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"execution_conditions": {
|
|
||||||
"candidate_only": true,
|
|
||||||
"integration_unchanged": true,
|
|
||||||
"main_unchanged": true,
|
|
||||||
"no_credentials_or_secrets_in_request": true,
|
|
||||||
"production_baseline_unchanged": true,
|
|
||||||
"registry_unchanged": true
|
|
||||||
},
|
|
||||||
"expected_governance_base_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
|
||||||
"expires_at": "2026-07-28T21:34:28Z",
|
|
||||||
"implementation": {
|
|
||||||
"commit_oid": "112d995545e3e79126a062b2643a9f0c17ae7bd4",
|
|
||||||
"tree_oid": "4ed88399fb8e7e4c235413ebbd783c9b6ad3c624"
|
|
||||||
},
|
|
||||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
|
||||||
"issued_at": "2026-07-28T09:34:28Z",
|
|
||||||
"prohibited_operations": [
|
|
||||||
"production-baseline.write",
|
|
||||||
"production-tag.write",
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"registry.write",
|
|
||||||
"force-push",
|
|
||||||
"direct-main-push",
|
|
||||||
"direct-tag-write"
|
|
||||||
],
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"request_status": "PENDING_HUMAN_APPROVAL",
|
|
||||||
"requested_operations": [
|
|
||||||
"control-plane.candidate.deploy",
|
|
||||||
"control-plane.candidate.verify",
|
|
||||||
"control-plane.candidate.rollback"
|
|
||||||
],
|
|
||||||
"required_controls": [
|
|
||||||
"Two distinct human approvals, one Codex and one GPT, both APPROVED against the same current approval-request PR head.",
|
|
||||||
"Re-verify artifact, test evidence, source map, rendered systemd configuration, and rollback artifact paths and SHA-256 digests immediately before execution.",
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED=false throughout candidate deployment, verification, and rollback.",
|
|
||||||
"No force push, direct main write, or direct tag write."
|
|
||||||
],
|
|
||||||
"required_reviewers": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"schema": "aios.control-plane-deployment-approval-request/v1",
|
|
||||||
"target_governance": {
|
|
||||||
"approved_base_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
|
||||||
"binding_rule": "Human approvals must bind to the same approval-request PR head; its first parent must equal the active governance base."
|
|
||||||
},
|
|
||||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
|
||||||
"validity_hours": 12
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"allowed_governance_operations":["ADD_EXISTING_MACHINE_TO_MAIN_MERGE_WHITELIST","MIGRATE_LEGACY_OWNER_APPROVAL_GATE_TO_CAMPAIGN_APPROVAL_GATE"],"campaign":{"authorization_recovery_digest":"6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2","id":"AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"},"denied_operations":["direct_main_push","force_push","force_with_lease","disable_branch_protection","disable_status_checks","bypass_failed_checks","Owner_impersonation","administrator_grant","formal_tag_rewrite","formal_tag_delete","history_rewrite","Registry_bypass","Baseline_bypass","production_bypass"],"governance_base":{"oid":"9303a5d684e972bd15fe19f61f721075f4dda8b6","ref":"refs/heads/governance/approvals","tree_oid":"cf6ad76c810413b3601891dba6bcb755e630baef"},"new_executor":{"credential_ref":{"name":"gitea-machine-token","scheme":"systemd-credential"},"login":"aios-autonomy-agent","owner":false,"user_id":153},"owner_approval":{"count":1,"dismiss_stale_approvals":true,"pr53_reviewed":false,"require_latest_head":true,"required":true,"target":"GOVERNANCE_REQUEST_EXACT_HEAD"},"pr53":{"base":"69b45e81dcd2c3707c367003b08c55fd361faae8","head":"103943cb59d5680705999e3834c3fd657f97ecc7","number":53,"reviewed_by_owner":false},"preapproval_evidence":{"path":"approvals/governance-policy/AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1.preapproval-evidence.json","sha256":"c031602b120264e6a74565fc91c2d0f3750d984de538349b95c51d9477fdcd14"},"protection_before":{"merge_whitelist_user_ids":[2],"sha256":"8b3370add21cd23c2b4f41d8d84d3eaaa09ddf0451549b72c7941bbd92d5d60a"},"repository":"aios/aios","request_id":"AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1","schema":"aios.governance-scope-extension.v1","stage_1_delta":{"direct_main_push":false,"force_push":false,"merge_whitelist_user_ids_after":[2,153],"merge_whitelist_user_ids_before":[2]},"stage_2_gate_migration":{"add":["campaign-approval-gate"],"all_other_required_contexts":"unchanged","remove":["owner-approval-gate"]}}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"campaign":{"authorization_recovery_digest":"6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2","id":"AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"},"governance_base":{"authority_oid":"9303a5d684e972bd15fe19f61f721075f4dda8b6","authority_tree_oid":"cf6ad76c810413b3601891dba6bcb755e630baef","gitea_api_oid":"9303a5d684e972bd15fe19f61f721075f4dda8b6","gitea_api_tree_claim":"9303a5d684e972bd15fe19f61f721075f4dda8b6","gitea_tree_endpoint_read":"PASS","network_oid":"9303a5d684e972bd15fe19f61f721075f4dda8b6","ref":"refs/heads/governance/approvals"},"machine_identity":{"credential_ref":{"name":"gitea-machine-token","scheme":"systemd-credential"},"login":"aios-autonomy-agent","owner":false,"user_id":153},"observed_at":"2026-08-08T10:46:49Z","pr53":{"base":"69b45e81dcd2c3707c367003b08c55fd361faae8","head":"103943cb59d5680705999e3834c3fd657f97ecc7","number":53},"preapproval_validation":{"canonical_json":"PASS","changed_path_whitelist":"PENDING_COMMIT","git_diff_check":"PENDING_COMMIT","machine_user_153_binding":"PASS","pr53_exact_head_binding":"PASS","protection_delta_calculation":"PASS","schema_validation":"PASS","secret_scan":"PASS_NO_SECRET_VALUE"},"protection_before":{"sha256":"8b3370add21cd23c2b4f41d8d84d3eaaa09ddf0451549b72c7941bbd92d5d60a","snapshot":{"approvals_whitelist_team_i_ds":"null","approvals_whitelist_user_i_ds":"[2]","block_admin_merge_override":1,"block_on_official_review_requests":1,"block_on_outdated_branch":1,"block_on_rejected_reviews":1,"branch_name":"main","bypass_allowlist_team_i_ds":"null","bypass_allowlist_user_i_ds":"null","can_force_push":0,"can_push":0,"created_unix":1785093201,"dismiss_stale_approvals":1,"enable_approvals_whitelist":1,"enable_bypass_allowlist":0,"enable_force_push_allowlist":0,"enable_merge_whitelist":1,"enable_status_check":1,"enable_whitelist":0,"force_push_allowlist_deploy_keys":0,"force_push_allowlist_team_i_ds":"null","force_push_allowlist_user_i_ds":"null","id":2,"ignore_stale_approvals":0,"merge_whitelist_team_i_ds":"null","merge_whitelist_user_i_ds":"[2]","priority":0,"protected_file_patterns":"","repo_id":1,"require_signed_commits":0,"required_approvals":0,"status_check_contexts":"[\"autonomy-unit-tests\",\"autonomy-smoke-tests\",\"git-diff-check\",\"py-compile\",\"reconciliation-contract\",\"release-state-machine\",\"secret-scan\",\"signer-validator-contract\",\"owner-approval-gate\"]","unprotected_file_patterns":"","updated_unix":1786060534,"whitelist_deploy_keys":0,"whitelist_team_i_ds":"null","whitelist_user_i_ds":"null"}},"protection_delta":{"after":[2,153],"before":[2],"unchanged":["required_approvals","enable_status_check","status_check_contexts","can_push","can_force_push","review_protections","tag_permissions"]},"publisher":{"administrator":false,"allowed_operations":["create_governance_request_branch","create_governance_approval_pr"],"denied_operations":["main_write","branch_protection_change","merge","tag_write","Registry_write","Baseline_write","production_deploy","force_push","credential_mint","Owner_impersonation"],"owner":false},"repository":"aios/aios","request_branch":"governance/request/autonomous-release-protected-merge-executor-v1-20260808","request_id":"AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1","schema":"aios.governance-scope-extension-preapproval-evidence.v1"}
|
|
||||||
-127
@@ -1,127 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "aios.governance-approval/v1",
|
|
||||||
"approval_type": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP_POLICY_ACTIVATION",
|
|
||||||
"approval_plan_id": "AIOS-TSG-BS-PHASE-A-RENEWAL-20260731T081147Z",
|
|
||||||
"task_id": "AIOS-V2R1C4-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-RENEWAL",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"issued_at": "2026-07-31T08:11:47Z",
|
|
||||||
"expires_at": "2026-07-31T20:11:47Z",
|
|
||||||
"expected_governance_base_oid": "ebd74398743e36523b593fcc395b73945d4924c5",
|
|
||||||
"expected_governance_base_tree_oid": "ad1255a1127204fad1d1208d8f31dc94fa64e6e1",
|
|
||||||
"approval_request_branch": "governance/request/two-stage-owner-campaign-v2-phase-a-renewal-20260731T081147Z",
|
|
||||||
"request_head_binding": "GITEA_EXACT_HEAD_REVIEW_POST_COMMIT",
|
|
||||||
"human_authorizer": "owner",
|
|
||||||
"authorization_mode": "GITEA_EXACT_HEAD_REVIEW",
|
|
||||||
"required_human_approvals": 1,
|
|
||||||
"allowed_human_approvers": [
|
|
||||||
"owner"
|
|
||||||
],
|
|
||||||
"require_latest_head_approval": true,
|
|
||||||
"dismiss_stale_approvals": true,
|
|
||||||
"auto_merge_after_machine_gates": true,
|
|
||||||
"phase_a_scope": {
|
|
||||||
"schema_id": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP",
|
|
||||||
"schema_version": 1,
|
|
||||||
"activate_schema_and_validator_only": true,
|
|
||||||
"phase_b_release_authorized": false,
|
|
||||||
"publisher_authorized": false,
|
|
||||||
"writer_authorized": false,
|
|
||||||
"credential_projection_authorized": false,
|
|
||||||
"tag_authorized": false,
|
|
||||||
"registry_authorized": false,
|
|
||||||
"baseline_authorized": false,
|
|
||||||
"production_deployment_authorized": false
|
|
||||||
},
|
|
||||||
"content_bindings": {
|
|
||||||
"schema_document": {
|
|
||||||
"path": "services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json",
|
|
||||||
"blob_oid": "9b117667c0d98f995ac059b0e1b5ffed06ca6444",
|
|
||||||
"sha256": "6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd"
|
|
||||||
},
|
|
||||||
"validator_policy": {
|
|
||||||
"path": "services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py",
|
|
||||||
"blob_oid": "c18d15f31340f19fc5da87944901bdfb867ab6dc",
|
|
||||||
"sha256": "de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac"
|
|
||||||
},
|
|
||||||
"evidence_manifest": {
|
|
||||||
"path": "evidence/governance-policy/AIOS-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-20260730T023551Z.json",
|
|
||||||
"blob_oid": "c3d215b3ea525a25de514c32c5aad91d47e140b4",
|
|
||||||
"sha256": "1b0a993940b41a4b16724b557f6abd8b6cebb56dc64cc8f813bd605b6a3399c2"
|
|
||||||
},
|
|
||||||
"checksums_manifest": {
|
|
||||||
"path": "evidence/governance-policy/AIOS-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-20260730T023551Z.sha256",
|
|
||||||
"blob_oid": "45ed084234dae6a00e69a41cd4f716f46a97d9fb",
|
|
||||||
"sha256": "51c4b029c0792dc98d41f754e484ba3861cf7604443d9c5f289b3b0af768a41a"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"offline_candidate_bindings": {
|
|
||||||
"schema_seed_commit_oid": "8da47545a507cb798d3d6ab30ff8552a2d0a5ecf",
|
|
||||||
"schema_seed_tree_oid": "991e68577a469657c1c4996814b6634ce5ed752f",
|
|
||||||
"implementation_commit_oid": "a637a9dbb6e0021cb7dcf0d6b0ca00dfe10dce38",
|
|
||||||
"implementation_tree_oid": "bf6e0c16fcad7515465bec4aefdd2e77b1e9bef0",
|
|
||||||
"evidence_closure_commit_oid": "1e4885197f508f7921b148d71e545b3de0029b0a",
|
|
||||||
"evidence_closure_tree_oid": "e55cf9ef19d1c6804f161de54fd3395a06149807",
|
|
||||||
"source_diff_sha256": "c06d002db06da6cbf16f9c8321f1acbfd431f009b5127a13fd2cb5d63b278867",
|
|
||||||
"source_pathset_sha256": "5061402d516c0e9413208ce64e2b52f77d224e6eec1f7b07c5b3a10b58df5eaf",
|
|
||||||
"implementation_bundle_sha256": "15711b225b9ef41cca0588d21b2b95d7a95ac155efc27fcb8772c435fcdab147",
|
|
||||||
"evidence_bundle_sha256": "ec97b8460151fc0e7e7ea9ae044ca13a1543c4b79c41a437ef087d3b1c271857"
|
|
||||||
},
|
|
||||||
"runtime_dependency_bindings": {
|
|
||||||
"handoff_zip_sha256": "668239a549b4a2b2094fe9b5104b84703fe8ed75011fd8ed71ed3958a6dc0706",
|
|
||||||
"installed_wheel_sha256": "abbf616366eb3467d4e651bb5306e22ae5e931b613df5362a0892935c4a1c0c7",
|
|
||||||
"anchor_service": "aios-campaign-anchor.service",
|
|
||||||
"anchor_uid": 988,
|
|
||||||
"runner_uid": 986,
|
|
||||||
"anchor_database": "/var/lib/aios-campaign-anchor/anchor.sqlite3",
|
|
||||||
"anchor_socket": "/run/aios-campaign-anchor/anchor.sock",
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false
|
|
||||||
},
|
|
||||||
"required_machine_status_contexts": [
|
|
||||||
"governance/schema-contract",
|
|
||||||
"governance/validator-security",
|
|
||||||
"governance/fault-injection",
|
|
||||||
"governance/runtime-isolation",
|
|
||||||
"governance/authority-mirror"
|
|
||||||
],
|
|
||||||
"allowed_operations_after_exact_head_owner_approval": [
|
|
||||||
"observe_governance_pr",
|
|
||||||
"wait_governance_gates",
|
|
||||||
"observe_governance_merge",
|
|
||||||
"observe_validator_activation",
|
|
||||||
"complete_policy_activation"
|
|
||||||
],
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"registry.write",
|
|
||||||
"baseline.write",
|
|
||||||
"production.deploy",
|
|
||||||
"force_push",
|
|
||||||
"history_rewrite",
|
|
||||||
"governance.direct_push",
|
|
||||||
"approval.impersonate",
|
|
||||||
"publisher.create",
|
|
||||||
"writer.create",
|
|
||||||
"credential.project",
|
|
||||||
"phase_b.execute"
|
|
||||||
],
|
|
||||||
"merge_constraints": {
|
|
||||||
"allowed_modes": [
|
|
||||||
"FAST_FORWARD_TO_APPROVED_HEAD",
|
|
||||||
"EXACT_TWO_PARENT_MERGE_WITH_APPROVED_TREE"
|
|
||||||
],
|
|
||||||
"force": false,
|
|
||||||
"plus_refspec": false,
|
|
||||||
"tags": false,
|
|
||||||
"prune": false
|
|
||||||
},
|
|
||||||
"governance_declarations": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"force_push_used": false,
|
|
||||||
"tag_rewritten": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"secrets_committed": false,
|
|
||||||
"owner_approval_respected": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-185
@@ -1,185 +0,0 @@
|
|||||||
{
|
|
||||||
"approval_type": "TASK_STATUS_MERGE_BROKER_BOOTSTRAP_RFC3339_FIX",
|
|
||||||
"schema_version": 2,
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"expected_governance_base_oid": "8814dff20222625b60454c613b61ccf15fa20e51",
|
|
||||||
"request_branch": "patch/codex/AIOS-V2R1C4-BOOTSTRAP-RFC3339-OFFSET-FIX",
|
|
||||||
"target_ref": "refs/heads/governance/approvals",
|
|
||||||
"issued_at": "2026-08-01T02:50:00Z",
|
|
||||||
"expires_at": "2026-08-01T14:50:00Z",
|
|
||||||
"required_human_approvals": 1,
|
|
||||||
"allowed_human_approvers": ["owner"],
|
|
||||||
"require_latest_head_approval": true,
|
|
||||||
"approval_scopes": [
|
|
||||||
"BOOTSTRAP_FIX_APPROVAL",
|
|
||||||
"GITEA_INTERNAL_TOKEN_ROTATION_APPROVAL"
|
|
||||||
],
|
|
||||||
"supersedes": {
|
|
||||||
"pr_number": 37,
|
|
||||||
"head_oid": "57697d81cf3d1fef23798ec390418efe8b64f4e9",
|
|
||||||
"tree_oid": "b58af1925318a0eefa6cc709e8d52ba57bb20d7f",
|
|
||||||
"reason": "APPROVED_BOOTSTRAP_RUNTIME_CONTRACT_MISMATCH",
|
|
||||||
"old_review_reusable": false,
|
|
||||||
"old_pr_immutable": true
|
|
||||||
},
|
|
||||||
"exact_head_binding": {
|
|
||||||
"method": "GITEA_EXACT_HEAD_OWNER_REVIEW_AND_PR_METADATA",
|
|
||||||
"head_oid_resolved_after_commit": true,
|
|
||||||
"tree_oid_resolved_after_commit": true,
|
|
||||||
"manifest_blob_oid_resolved_after_commit": true,
|
|
||||||
"manifest_sha256_resolved_after_commit": true,
|
|
||||||
"head_change_invalidates_review": true
|
|
||||||
},
|
|
||||||
"timestamp_contract": {
|
|
||||||
"id": "RFC3339_OFFSET_AWARE_V2",
|
|
||||||
"canonical_timezone": "UTC",
|
|
||||||
"raw_timestamp_preserved": true,
|
|
||||||
"naive_datetime_allowed": false,
|
|
||||||
"accepted_timezone_forms": ["Z", "+HH:MM", "-HH:MM"],
|
|
||||||
"fractional_seconds_allowed": true,
|
|
||||||
"lowercase_z_allowed": false,
|
|
||||||
"expiry_boundary": "REVIEW_MUST_BE_STRICTLY_BEFORE_EXPIRY"
|
|
||||||
},
|
|
||||||
"internal_token_rotation": {
|
|
||||||
"secret_class": "GITEA_INTERNAL_TOKEN",
|
|
||||||
"old_secret_fingerprint_algorithm": "SHA256",
|
|
||||||
"old_secret_fingerprint": "42e2af774700405ae32d55c98ca010fdb5da890b08f9e12e77372133c68e0562",
|
|
||||||
"old_secret_reference": "gitea-config://security/INTERNAL_TOKEN",
|
|
||||||
"affected_consumers": [
|
|
||||||
"gitea-config-residue:/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini.bak-20260706-auth-gitops",
|
|
||||||
"gitea-config:/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini",
|
|
||||||
"systemd-service:aios-gitea.service"
|
|
||||||
],
|
|
||||||
"replacement_secret_reference": "systemd-credential-encrypted://gitea-internal-token-v2",
|
|
||||||
"rotation_method": "ATOMIC_REPLACE_THEN_REVOKE",
|
|
||||||
"rollback_plan_path": "evidence/governance/bootstrap-rfc3339-fix-20260801/ROLLBACK_PLAN.json",
|
|
||||||
"rollback_plan_sha256": "3283f4c3d6bb7f34ac893789f7e9f5df6c68b7807203e6f471ad92e8e007ff3f",
|
|
||||||
"audit_window_start": "2026-08-01T00:00:00Z",
|
|
||||||
"allowed_operations": [
|
|
||||||
"generate_replacement_internal_token_to_encrypted_runtime_credential",
|
|
||||||
"validate_replacement_internal_token_without_logging_plaintext",
|
|
||||||
"atomically_replace_exact_gitea_internal_token_config_value",
|
|
||||||
"restart_exact_aios_gitea_service",
|
|
||||||
"verify_exact_consumers_healthy",
|
|
||||||
"verify_old_internal_token_authentication_rejected",
|
|
||||||
"remove_or_redact_exact_old_secret_config_residue",
|
|
||||||
"audit_exposure_window",
|
|
||||||
"write_immutable_security_event_receipt"
|
|
||||||
],
|
|
||||||
"access_token_table_exception_applicable": false,
|
|
||||||
"plaintext_secret_in_manifest_allowed": false,
|
|
||||||
"plaintext_secret_in_argv_allowed": false,
|
|
||||||
"plaintext_secret_in_logs_allowed": false,
|
|
||||||
"plaintext_secret_in_git_allowed": false
|
|
||||||
},
|
|
||||||
"phase_b": {
|
|
||||||
"approval_mode": "TWO_STAGE_REQUIRED",
|
|
||||||
"reason": "CURRENT_SCHEMAS_DO_NOT_ATOMICALLY_BIND_BOOTSTRAP_ACTIVATION_AND_FULL_TASK_SCOPED_RELEASE_PAYLOAD",
|
|
||||||
"old_pr_number": 36,
|
|
||||||
"old_head_oid": "a8644fc172bd8ebbba17a16e6bf586c93fd1e793",
|
|
||||||
"old_approval_reusable": false,
|
|
||||||
"successor_required_after_bootstrap_activation": true,
|
|
||||||
"frozen_release_objects_unchanged": true
|
|
||||||
},
|
|
||||||
"frozen_release_objects": {
|
|
||||||
"source_commit_oid": "1857b88690cd1c0119df984534c260b6f217a21a",
|
|
||||||
"source_tree_oid": "17c153de2338f570f70cb7407e7dda3c596b524b",
|
|
||||||
"evidence_closure_oid": "057f5923f72cdb8d002ae40bc0cd584961825801",
|
|
||||||
"evidence_tree_oid": "30be66d153fe9d3758f5fdba89ca4169beab38f8",
|
|
||||||
"git_bundle_sha256": "762e280e240988282bc2c2e65fafecedd596400ab6829016947e84559508a015",
|
|
||||||
"tag_oid": "7f82d77cf7702a6d695b4f0b74da7a9a9ecd7fa3",
|
|
||||||
"peeled_target_oid": "057f5923f72cdb8d002ae40bc0cd584961825801",
|
|
||||||
"release_artifact_sha256": "39523caa48f39b33e5e5df785bb568e0381d8b20301746c280442f73f294f2b2",
|
|
||||||
"rollback_artifact_sha256": "dd0dbbe21db14722996636347ea1d22917436c688d5f0b7055b5bfb1776c4d95",
|
|
||||||
"release_root_sha256": "16e4e881287cef4a02525a6a5b337d3e12995e510832fe04fea2ac196885f3ef",
|
|
||||||
"evidence_manifest_sha256": "3b0b72af400997385e817e9bec677942288f422b69c2d2b52a8a00407fc395e0"
|
|
||||||
},
|
|
||||||
"allowed_status_contexts": [
|
|
||||||
"aios/branch-protection",
|
|
||||||
"aios/evidence-closure",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/state-machine",
|
|
||||||
"aios/validator"
|
|
||||||
],
|
|
||||||
"allowed_identities": ["codex", "gpt"],
|
|
||||||
"allowed_operations": [
|
|
||||||
"install_exact_approved_bootstrap_files",
|
|
||||||
"sign_task_grant_after_owner_exact_head_review",
|
|
||||||
"issue_task_status_credential",
|
|
||||||
"issue_task_merge_credential",
|
|
||||||
"publish_exact_five_statuses",
|
|
||||||
"fast_forward_only_merge",
|
|
||||||
"readback_status_and_ref",
|
|
||||||
"revoke_task_credentials",
|
|
||||||
"remove_credential_projection",
|
|
||||||
"sweep_completed_or_expired_task_credentials"
|
|
||||||
],
|
|
||||||
"operation_limits": {
|
|
||||||
"maximum_status_writes_per_grant": 5,
|
|
||||||
"maximum_merges_per_grant": 1,
|
|
||||||
"maximum_grant_lifetime_seconds": 43200,
|
|
||||||
"automatic_revocation_required": true
|
|
||||||
},
|
|
||||||
"denied_operations": [
|
|
||||||
"branch_push_outside_request_branch",
|
|
||||||
"main_push",
|
|
||||||
"tag_write",
|
|
||||||
"registry_write",
|
|
||||||
"baseline_write",
|
|
||||||
"production_write",
|
|
||||||
"branch_protection_change",
|
|
||||||
"force_push",
|
|
||||||
"squash",
|
|
||||||
"rebase",
|
|
||||||
"merge_commit",
|
|
||||||
"approval_impersonation",
|
|
||||||
"commit_status_database_write",
|
|
||||||
"database_write_outside_exact_task_access_token_revocation",
|
|
||||||
"reuse_pr36_or_pr37_review",
|
|
||||||
"plaintext_secret_persistence"
|
|
||||||
],
|
|
||||||
"runtime_controls": {
|
|
||||||
"global_gitops_writes_enabled": false,
|
|
||||||
"systemd_encrypted_credentials_required": true,
|
|
||||||
"systemd_projection_mode": "0440_ROOT_ROOT_CREDENTIALS_DIRECTORY_ONLY",
|
|
||||||
"systemd_gitea_token_embedded_name": "gitea-token",
|
|
||||||
"sqlite_parent_directory_metadata_write_required": true,
|
|
||||||
"sqlite_sibling_paths_read_only": true,
|
|
||||||
"sqlite_quota_journal_mode": "WAL",
|
|
||||||
"sqlite_quota_synchronous": "FULL",
|
|
||||||
"status_evidence_digest_bound_in_signed_grant": true,
|
|
||||||
"merge_method": "fast-forward-only",
|
|
||||||
"commit_status_database_write_allowed": false
|
|
||||||
},
|
|
||||||
"approved_files": {
|
|
||||||
"deploy/systemd/aios-task-credential-broker@.service": "95f8da02600b3c908013ffa723d79f6fca0c7a4332a077a7b673e97ce35bcd69",
|
|
||||||
"deploy/systemd/aios-task-credential-expiry-sweeper.service": "72a336523983b2486300c4e52f2f01dce392aaf6ffd953041c209d204df85717",
|
|
||||||
"deploy/systemd/aios-task-credential-expiry-sweeper.timer": "49ff9c80cbd0b44fd584dc7d29f4db0d042e33f325b1ed7af81a5be42f1cd011",
|
|
||||||
"deploy/systemd/aios-task-merge-broker@.service": "4e6b8fcf541f225e58ea080107fc30deb0c4ac9eac70217ff9e57081245c106a",
|
|
||||||
"deploy/systemd/aios-task-status-broker@.service": "378a2e8f658cb56cac5dcc151772ec6a00d86f6c1ce3106509b9aa87549dc9da",
|
|
||||||
"evidence/governance/bootstrap-rfc3339-fix-20260801/ROLLBACK_PLAN.json": "3283f4c3d6bb7f34ac893789f7e9f5df6c68b7807203e6f471ad92e8e007ff3f",
|
|
||||||
"evidence/governance/bootstrap-rfc3339-fix-20260801/TEST_SUMMARY.json": "c7d28139fddb9666e60fd62a9a27803dd00a1ebbccbbb75a5121cd8ed3d20fc2",
|
|
||||||
"services/aios-single-owner-governance/aios_single_owner_governance/task_credential_broker.py": "3b2fbbf2698a2d8385a2bf9eccdebbea629642908cbc7a1e8251476475e0d647",
|
|
||||||
"services/aios-single-owner-governance/aios_single_owner_governance/task_credential_expiry_sweeper.py": "8d383768eb0322c84a35282c126bee26a3e6419b125780424ffc7793a504efdf",
|
|
||||||
"services/aios-single-owner-governance/aios_single_owner_governance/task_status_merge_broker.py": "697d8deccdf2fec902dab38a0436075d78a95c1c5b410e115ee74cf79f477e63",
|
|
||||||
"services/aios-single-owner-governance/task-status-merge-grant-v1.schema.json": "34472624261203b5c1a215c141887ea3fed5d68fb3c503c7db2ad9a717862597",
|
|
||||||
"tests/single_owner_governance/test_task_status_merge_broker.py": "af1c5844ee0f7d6907f8cb68d5f503f3bdbd59bb05150e5364eaf7bf2b37be1a"
|
|
||||||
},
|
|
||||||
"aggregate_bindings": {
|
|
||||||
"broker_module_set_sha256": "c73ccaf8f60cf2446a5d4c0b4baf5456cb77961fda682285cb0a4f509ed49caa",
|
|
||||||
"systemd_definition_set_sha256": "3591be96f1717e7a0a57957aafbb233eec2b1617a8fb938889ccd312d812d066",
|
|
||||||
"grant_schema_sha256": "34472624261203b5c1a215c141887ea3fed5d68fb3c503c7db2ad9a717862597",
|
|
||||||
"test_summary_sha256": "c7d28139fddb9666e60fd62a9a27803dd00a1ebbccbbb75a5121cd8ed3d20fc2",
|
|
||||||
"python_executable_sha256": "e1efa562c2cc2e35521a5c9c9b9939921001ff8ca9708a13ef15ace68cc2ccd7"
|
|
||||||
},
|
|
||||||
"machine_gate_requirements": {
|
|
||||||
"compileall": "PASS",
|
|
||||||
"unit_tests": "PASS",
|
|
||||||
"rfc3339_offset_tests": "PASS",
|
|
||||||
"json_schema": "PASS",
|
|
||||||
"systemd_validation": "PASS",
|
|
||||||
"changed_path_secret_scan": "PASS",
|
|
||||||
"fault_injection": "PASS",
|
|
||||||
"scope_boundary_tests": "PASS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-79
@@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"acknowledges_pr30_owner_approval_was_missing": true,
|
|
||||||
"activation_prerequisites_artifact_sha256": "729077800c6510bf8930c97a78781218d1bce9ec40157aa99fc2bd205466e895",
|
|
||||||
"activation_prerequisites_commit_oid": "fa038d17b29001e26f1ac88019a407a4b3ebaa7e",
|
|
||||||
"activation_prerequisites_rollback_artifact_sha256": "00e40eace10a0aae1731eb98851fec71a0c13c48e1e6e5a113e30a4356c2086f",
|
|
||||||
"activation_prerequisites_tree_oid": "d5ff67f848807ef2c7a695c05cdd0bb5f2644fc2",
|
|
||||||
"approval_request_branch": "governance/request/single-owner-atomic-activation-owner-ratification-20260729T014918Z-e503d96665ee",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ATOMIC_ACTIVATION_OWNER_RATIFICATION",
|
|
||||||
"branch_protection_adapter_sha256": "2706dc6095ff639838f72c91a66c1f80711893c8c0498555b388feaacf2db569",
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"registry.write",
|
|
||||||
"baseline.write",
|
|
||||||
"production.deploy",
|
|
||||||
"force_push",
|
|
||||||
"history_rewrite",
|
|
||||||
"governance.direct_push",
|
|
||||||
"approval.impersonate"
|
|
||||||
],
|
|
||||||
"direct_protected_branch_push_allowed": false,
|
|
||||||
"dismiss_stale_approvals": true,
|
|
||||||
"dormant_deployment_evidence_sha256": "c4c326784a3d66d62e32c46be3506c801585c06a34e62c9a1f210492a6c1bc06",
|
|
||||||
"evidence_closure_oid": "5f45139ca5f7f7f3da244141c5115d12f5af58ae",
|
|
||||||
"evidence_closure_sha256": "53e5f2fdc01a6225048dfdf6eac973e99743f0295f17f3a42f01dce7e7050ab8",
|
|
||||||
"expected_governance_base_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
|
||||||
"expires_at": "2026-07-29T13:49:18Z",
|
|
||||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
|
||||||
"approval_validator.activate",
|
|
||||||
"governance_state_machine.activate",
|
|
||||||
"gitea_branch_protection.atomic_update",
|
|
||||||
"rollback_configuration.install",
|
|
||||||
"single_owner_policy.activate",
|
|
||||||
"break_glass.expire",
|
|
||||||
"post_activation_tests.execute"
|
|
||||||
],
|
|
||||||
"force_push_allowed": false,
|
|
||||||
"human_authorizer": "owner",
|
|
||||||
"issued_at": "2026-07-29T01:49:18Z",
|
|
||||||
"legacy_mechanical_approvals": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
|
||||||
"pr30_codex_approval_review_id": 45,
|
|
||||||
"pr30_codex_reviewed_at": "2026-07-29T01:24:38Z",
|
|
||||||
"pr30_codex_reviewed_head_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
|
||||||
"pr30_gpt_approval_review_id": 46,
|
|
||||||
"pr30_gpt_reviewed_at": "2026-07-29T01:25:02Z",
|
|
||||||
"pr30_gpt_reviewed_head_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
|
||||||
"pr30_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-ATOMIC-ACTIVATION-20260728T204808Z-4cab1d5829d6.json",
|
|
||||||
"pr30_merge_tree_oid": "219974f085efe473fcc6ad2170f67312d8e57c54",
|
|
||||||
"pr30_merged_at": "2026-07-29T01:25:08Z",
|
|
||||||
"pr30_owner_review_id": 42,
|
|
||||||
"pr30_owner_review_state_observed": "REQUESTED_NOT_APPROVED",
|
|
||||||
"pr30_owner_reviewed_head_oid": null,
|
|
||||||
"pr30_premerge_base_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
|
||||||
"production_deployment_allowed": false,
|
|
||||||
"ratifies_pr30_manifest_blob_oid": "38c95dde2d3831f8e3c80b342369f1c3e1cbf34c",
|
|
||||||
"ratifies_pr30_manifest_sha256": "bc19ada0e8eeb47d421a6360cf1d66b6d1129eb6056a447d9ff70ebd5ada1718",
|
|
||||||
"ratifies_pr30_merge_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
|
||||||
"ratifies_pr30_request_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
|
||||||
"ratifies_pr_number": 30,
|
|
||||||
"require_latest_head_approval": true,
|
|
||||||
"required_reviewers": [
|
|
||||||
"owner",
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"rollback_configuration_sha256": "eb44fc5f655f8c30c4b687185055ba3a2725189e19538e3a9a46a2c0cf141b8c",
|
|
||||||
"schema": "aios.governance-approval/v1",
|
|
||||||
"state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
|
||||||
"task_id": "AIOS-PR30-SINGLE-OWNER-ATOMIC-ACTIVATION-OWNER-RATIFICATION-20260729",
|
|
||||||
"test_evidence_sha256": "42168da7437028440bece3432d8aa38a8722b4db18244423d9daaa380675c45a",
|
|
||||||
"validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
|
||||||
"wip_blocks_merge": true
|
|
||||||
}
|
|
||||||
-91
@@ -1,91 +0,0 @@
|
|||||||
{
|
|
||||||
"approval_request_branch": "governance/request/single-owner-governance-activation-prerequisites-20260728T183025127107Z",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ACTIVATION_PREREQUISITES",
|
|
||||||
"binds_final_ratification_manifest_blob_oid": "2865fbec766901eb61fc8aed0542970e951e0d1d",
|
|
||||||
"binds_final_ratification_manifest_sha256": "216e54503d8e6a3f4c908084e19ce1e6242d3ba15f7b8011a9a9d9f7975e63e7",
|
|
||||||
"binds_final_ratification_merge_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
|
||||||
"binds_mirror_receipt_sha256": "6873133f7233bfed0d3b78b01847ad4251b8b67f3b6db7011efd07664f8d088f",
|
|
||||||
"expected_governance_base_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
|
||||||
"expires_at": "2026-07-29T06:30:25Z",
|
|
||||||
"explicitly_allowed_after_owner_approval_and_legacy_merge": [
|
|
||||||
"task_bound_activation_writer_grant.create",
|
|
||||||
"gitea_owner_review_adapter.create_and_dormant_deploy",
|
|
||||||
"machine_status_producer.create_and_dormant_deploy",
|
|
||||||
"machine_status_producer.credential_rotation_and_revocation",
|
|
||||||
"maintenance_lock.create",
|
|
||||||
"branch_protection_compensation_saga.create",
|
|
||||||
"targeted_online_rollback_executor.create",
|
|
||||||
"disposable_gitea_fixture.create_test_destroy",
|
|
||||||
"single_owner_activation_prerequisite_tests.execute",
|
|
||||||
"activation_prerequisite_evidence.create"
|
|
||||||
],
|
|
||||||
"implementation_branch": "patch/single-owner-governance-activation-prerequisites-20260729",
|
|
||||||
"implementation_paths": [
|
|
||||||
"services/aios-single-owner-governance/**",
|
|
||||||
"tests/single_owner_governance/**",
|
|
||||||
"deploy/systemd/aios-governance-*.service",
|
|
||||||
"deploy/systemd/aios-governance-*.timer"
|
|
||||||
],
|
|
||||||
"issued_at": "2026-07-28T18:30:25Z",
|
|
||||||
"machine_status_contexts": [
|
|
||||||
"aios/machine-gates",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/evidence-closure"
|
|
||||||
],
|
|
||||||
"observed_blockers": {
|
|
||||||
"commit_status_row_count": 0,
|
|
||||||
"disposable_governance_fixture_present": false,
|
|
||||||
"gitea_owner_review_validator_present": false,
|
|
||||||
"gitea_version": "1.22.6",
|
|
||||||
"native_branch_protection_etag_cas_present": false,
|
|
||||||
"required_status_context_producers_present": false,
|
|
||||||
"targeted_online_rollback_executor_present": false
|
|
||||||
},
|
|
||||||
"owner_approval_contract": {
|
|
||||||
"agent_must_not_submit_review": true,
|
|
||||||
"dismissed": false,
|
|
||||||
"head_change_invalidates_approval": true,
|
|
||||||
"only_human_approver": "owner",
|
|
||||||
"owner_user_id": 2,
|
|
||||||
"review_state": "APPROVED",
|
|
||||||
"reviewed_before_expires_at": true,
|
|
||||||
"reviewed_head_oid_must_equal_current_head_oid": true,
|
|
||||||
"stale": false
|
|
||||||
},
|
|
||||||
"permanent_constraints": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"force_push": false,
|
|
||||||
"history_rewrite": false,
|
|
||||||
"secret_commit": false,
|
|
||||||
"tag_rewrite": false
|
|
||||||
},
|
|
||||||
"pre_approval_non_authorizations": {
|
|
||||||
"activation_allowed": false,
|
|
||||||
"baseline_write_allowed": false,
|
|
||||||
"branch_protection_change_allowed": false,
|
|
||||||
"fixture_deployment_allowed": false,
|
|
||||||
"integration_write_allowed": false,
|
|
||||||
"main_write_allowed": false,
|
|
||||||
"merge_allowed": false,
|
|
||||||
"production_write_allowed": false,
|
|
||||||
"registry_write_allowed": false,
|
|
||||||
"tag_write_allowed": false,
|
|
||||||
"writer_grant_allowed": false
|
|
||||||
},
|
|
||||||
"reason": "safe-atomic-activation-prerequisites-absent",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
|
||||||
"schema": "aios.single-owner-governance-activation-prerequisites/v1",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-PREREQUISITES-20260729",
|
|
||||||
"wip": true,
|
|
||||||
"wip_transition_contract": {
|
|
||||||
"codex_and_gpt_not_requested_before_owner_approval": true,
|
|
||||||
"remove_wip_without_head_change_only_after_owner_approval": true,
|
|
||||||
"then_request_legacy_mechanical_reviews": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"wip_must_remain_until_owner_exact_head_approval": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"activation_prerequisites_artifact_sha256": "729077800c6510bf8930c97a78781218d1bce9ec40157aa99fc2bd205466e895",
|
|
||||||
"activation_prerequisites_commit_oid": "fa038d17b29001e26f1ac88019a407a4b3ebaa7e",
|
|
||||||
"activation_prerequisites_rollback_artifact_sha256": "00e40eace10a0aae1731eb98851fec71a0c13c48e1e6e5a113e30a4356c2086f",
|
|
||||||
"activation_prerequisites_tree_oid": "d5ff67f848807ef2c7a695c05cdd0bb5f2644fc2",
|
|
||||||
"approval_request_branch": "governance/request/single-owner-governance-atomic-activation-20260728T204808Z-4cab1d5829d6",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ATOMIC_ACTIVATION",
|
|
||||||
"branch_protection_adapter_sha256": "2706dc6095ff639838f72c91a66c1f80711893c8c0498555b388feaacf2db569",
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"registry.write",
|
|
||||||
"baseline.write",
|
|
||||||
"production.deploy",
|
|
||||||
"force_push",
|
|
||||||
"history_rewrite",
|
|
||||||
"governance.direct_push",
|
|
||||||
"approval.impersonate"
|
|
||||||
],
|
|
||||||
"deployment_mode_before_execution": "DORMANT_SHADOW_ONLY",
|
|
||||||
"direct_protected_branch_push_allowed": false,
|
|
||||||
"dismiss_stale_approvals": true,
|
|
||||||
"dormant_deployment_evidence_sha256": "c4c326784a3d66d62e32c46be3506c801585c06a34e62c9a1f210492a6c1bc06",
|
|
||||||
"evidence_closure_oid": "5f45139ca5f7f7f3da244141c5115d12f5af58ae",
|
|
||||||
"evidence_closure_sha256": "53e5f2fdc01a6225048dfdf6eac973e99743f0295f17f3a42f01dce7e7050ab8",
|
|
||||||
"expected_governance_base_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
|
||||||
"expires_at": "2026-07-29T08:48:08Z",
|
|
||||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
|
||||||
"approval_validator.activate",
|
|
||||||
"governance_state_machine.activate",
|
|
||||||
"gitea_branch_protection.atomic_update",
|
|
||||||
"rollback_configuration.install",
|
|
||||||
"single_owner_policy.activate",
|
|
||||||
"break_glass.expire",
|
|
||||||
"post_activation_tests.execute"
|
|
||||||
],
|
|
||||||
"force_push_allowed": false,
|
|
||||||
"issued_at": "2026-07-28T20:48:08Z",
|
|
||||||
"legacy_mechanical_approvals": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"machine_gates_required": true,
|
|
||||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
|
||||||
"pr26_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"pr27_merge_oid": "4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
|
||||||
"pr28_merge_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
|
||||||
"pr29_merge_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
|
||||||
"production_deployment_allowed": false,
|
|
||||||
"require_latest_head_approval": true,
|
|
||||||
"required_human_authorizer": "owner",
|
|
||||||
"required_reviewers": [
|
|
||||||
"owner",
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"rollback_configuration_sha256": "eb44fc5f655f8c30c4b687185055ba3a2725189e19538e3a9a46a2c0cf141b8c",
|
|
||||||
"rollback_required_on_any_activation_failure": true,
|
|
||||||
"schema": "aios.governance-approval/v1",
|
|
||||||
"single_owner_policy_active_before_execution": false,
|
|
||||||
"state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ATOMIC-ACTIVATION-20260729",
|
|
||||||
"test_evidence_sha256": "42168da7437028440bece3432d8aa38a8722b4db18244423d9daaa380675c45a",
|
|
||||||
"validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
|
||||||
"wip_blocks_merge": true
|
|
||||||
}
|
|
||||||
-268
@@ -1,268 +0,0 @@
|
|||||||
{
|
|
||||||
"acknowledges_pr26_owner_approval_was_missing": true,
|
|
||||||
"approval_request_branch": "governance/request/single-owner-control-plane-deployment-20260728T170740Z-ea7a102dac417c23",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_CONTROL_PLANE_DEPLOYMENT",
|
|
||||||
"control_plane_targets": {
|
|
||||||
"allowed_human_approvers": [
|
|
||||||
"owner"
|
|
||||||
],
|
|
||||||
"branch_deletion": false,
|
|
||||||
"direct_protected_branch_push": false,
|
|
||||||
"dismiss_stale_approvals": true,
|
|
||||||
"executor_must_not_approve": true,
|
|
||||||
"force_push": false,
|
|
||||||
"governance_model": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
|
||||||
"historical_records_immutable": true,
|
|
||||||
"machine_gates_required": true,
|
|
||||||
"policy_effective_at_set_only_after_activation_tests_pass": true,
|
|
||||||
"protected_branches": [
|
|
||||||
"governance/approvals",
|
|
||||||
"main",
|
|
||||||
"release/**",
|
|
||||||
"integration/**"
|
|
||||||
],
|
|
||||||
"requester_must_not_approve": true,
|
|
||||||
"require_approval_for_latest_commit": true,
|
|
||||||
"required_human_approvals": 1,
|
|
||||||
"required_owner_approvals": 1,
|
|
||||||
"required_status_contexts": [
|
|
||||||
"aios/machine-gates",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/evidence-closure"
|
|
||||||
],
|
|
||||||
"tag_rewrite": false,
|
|
||||||
"wip_blocks_merge": true
|
|
||||||
},
|
|
||||||
"does_not_claim_pr26_met_old_two_natural_person_policy": true,
|
|
||||||
"expected_authoritative_governance_base_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
|
||||||
"expected_gitea_governance_base_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"expires_at": "2026-07-29T05:07:40Z",
|
|
||||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
|
||||||
"approval_mirror.fast_forward_governance_only",
|
|
||||||
"approval_validator.update",
|
|
||||||
"state_machine.update",
|
|
||||||
"gitea_branch_protection.update_via_authenticated_api_compare_and_swap",
|
|
||||||
"governance_policy.activate",
|
|
||||||
"rollback_configuration.install",
|
|
||||||
"governance_acceptance_tests.execute",
|
|
||||||
"single_owner_governance_activation_receipt.create"
|
|
||||||
],
|
|
||||||
"explicitly_denied_operations": [
|
|
||||||
"force_push",
|
|
||||||
"plus_refspec",
|
|
||||||
"tag_fetch",
|
|
||||||
"prune",
|
|
||||||
"direct_git_database_write",
|
|
||||||
"direct_protected_ref_push",
|
|
||||||
"main_write",
|
|
||||||
"integration_write",
|
|
||||||
"tag_write",
|
|
||||||
"registry_write",
|
|
||||||
"baseline_write",
|
|
||||||
"production_write",
|
|
||||||
"history_rewrite",
|
|
||||||
"branch_delete",
|
|
||||||
"secret_read",
|
|
||||||
"approval_impersonation"
|
|
||||||
],
|
|
||||||
"global_constraints": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"force_push_used": false,
|
|
||||||
"owner_approval_respected": true,
|
|
||||||
"secrets_committed": false,
|
|
||||||
"tag_rewritten": false
|
|
||||||
},
|
|
||||||
"implementation_surface": {
|
|
||||||
"approval_validator": [
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py",
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py",
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py"
|
|
||||||
],
|
|
||||||
"runtime": [
|
|
||||||
"active-runtime/app/silent_release_controller.py",
|
|
||||||
"active-runtime/app/silent_release_hardening.py"
|
|
||||||
],
|
|
||||||
"state_machine": [
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py",
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py",
|
|
||||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py"
|
|
||||||
],
|
|
||||||
"systemd": [
|
|
||||||
"deploy/systemd/aios-autonomous-release-candidate.service",
|
|
||||||
"deploy/systemd/aios-silent-release-controller.service"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"issued_at": "2026-07-28T17:07:40Z",
|
|
||||||
"legacy_gitea_protection_gate": {
|
|
||||||
"account_reviews_do_not_prove_distinct_natural_persons": true,
|
|
||||||
"both_must_approve_this_exact_head": true,
|
|
||||||
"current_approval_whitelist": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"current_required_approvals": 2
|
|
||||||
},
|
|
||||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET_RATIFICATION",
|
|
||||||
"mirror_contract": {
|
|
||||||
"expected_chain_contains_policy_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"expected_pre_sync_authoritative_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
|
||||||
"fast_forward_only": true,
|
|
||||||
"force": false,
|
|
||||||
"fsck_connectivity_only_required": true,
|
|
||||||
"only_ref": "refs/heads/governance/approvals",
|
|
||||||
"plus_refspec": false,
|
|
||||||
"prune": false,
|
|
||||||
"quarantine_required": true,
|
|
||||||
"source": "gitea_repository_file_url_only",
|
|
||||||
"tags": false
|
|
||||||
},
|
|
||||||
"owner_break_glass_ratification": {
|
|
||||||
"attests_reason": "single-maintainer-system",
|
|
||||||
"attests_scope": "governance-policy-migration-only",
|
|
||||||
"created_before_expires_at": true,
|
|
||||||
"dismissed": false,
|
|
||||||
"required": true,
|
|
||||||
"review_state": "APPROVED",
|
|
||||||
"reviewed_head_oid_must_equal_current_pr_head_oid": true,
|
|
||||||
"reviewer_user_id": 2,
|
|
||||||
"reviewer_username": "owner",
|
|
||||||
"stale": false
|
|
||||||
},
|
|
||||||
"pre_approval_non_authorizations": {
|
|
||||||
"approval_validator_update_allowed": false,
|
|
||||||
"development_allowed": false,
|
|
||||||
"gitea_branch_protection_update_allowed": false,
|
|
||||||
"governance_policy_activation_allowed": false,
|
|
||||||
"merge_allowed": false,
|
|
||||||
"mirror_allowed": false,
|
|
||||||
"production_change_allowed": false,
|
|
||||||
"rollback_install_allowed": false,
|
|
||||||
"standing_writer_policy_allowed": false,
|
|
||||||
"state_machine_update_allowed": false
|
|
||||||
},
|
|
||||||
"pre_cutover_protection_snapshot": {
|
|
||||||
"protected_branch": [
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "governance/approvals",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 1,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "integration/chatgpt-autonomous-completion-20260726",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 3,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "main",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 2,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"protected_tag": []
|
|
||||||
},
|
|
||||||
"pre_cutover_protection_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
|
||||||
"ratifies_policy_manifest_blob_oid": "65f44007b10fc2a912d69e3f9081c0cfff77dfad",
|
|
||||||
"ratifies_policy_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-MIGRATION-20260728T162246Z.json",
|
|
||||||
"ratifies_policy_manifest_sha256": "c097475afbe3074e6482f125b635981f8c0284f3d763c926e2a63e1a3ee1c6c3",
|
|
||||||
"ratifies_policy_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"ratifies_policy_pr_number": 26,
|
|
||||||
"ratifies_policy_request_oid": "312d3b483049a748675e887a516ec739c81ff474",
|
|
||||||
"ratifies_policy_tree_oid": "5c6974c058bba6210dcc1bdd0968fb7e6c84a844",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"request_status": "HUMAN_OWNER_AND_LEGACY_PROTECTION_APPROVAL_PENDING",
|
|
||||||
"required_machine_gates": [
|
|
||||||
"owner_exact_head_review_valid",
|
|
||||||
"legacy_protection_reviews_valid",
|
|
||||||
"ordinary_merge_commit",
|
|
||||||
"parent_order_verified",
|
|
||||||
"manifest_add_only",
|
|
||||||
"quarantine_verification",
|
|
||||||
"approval_mirror_fast_forward",
|
|
||||||
"validator_tests",
|
|
||||||
"state_machine_tests",
|
|
||||||
"protection_compare_and_swap",
|
|
||||||
"positive_governance_tests",
|
|
||||||
"negative_governance_tests",
|
|
||||||
"secret_scan",
|
|
||||||
"rollback_integrity",
|
|
||||||
"production_write_false"
|
|
||||||
],
|
|
||||||
"rollback_configuration_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/rollback-configuration.tar",
|
|
||||||
"rollback_configuration_sha256": "fc8dcf68aaf53edb0cda45833ee45ba5b8b205791af9073258ca617c8b8187d6",
|
|
||||||
"schema": "aios.single-owner-governance-control-plane-deployment/v1",
|
|
||||||
"scope": "pr26-ratification-mirror-and-single-owner-control-plane-deployment-only",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-CONTROL-PLANE-DEPLOYMENT-20260729"
|
|
||||||
}
|
|
||||||
-260
@@ -1,260 +0,0 @@
|
|||||||
{
|
|
||||||
"acknowledges_pr26_and_pr27_owner_approval_was_missing": true,
|
|
||||||
"approval_request_branch": "governance/request/single-owner-governance-final-owner-ratification-20260728T173634412838Z",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_FINAL_OWNER_RATIFICATION",
|
|
||||||
"backup_checksums_sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
|
||||||
"backup_root": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/pre-migration-backup",
|
|
||||||
"break_glass_expires_immediately_after_activation": true,
|
|
||||||
"does_not_claim_old_two_natural_person_compliance": true,
|
|
||||||
"expires_at": "2026-07-29T05:36:34Z",
|
|
||||||
"inherits_only_pr27_control_plane_allowlist": [
|
|
||||||
"approval_mirror.fast_forward_governance_only",
|
|
||||||
"approval_validator.update",
|
|
||||||
"state_machine.update",
|
|
||||||
"gitea_branch_protection.update_via_authenticated_api_compare_and_swap",
|
|
||||||
"governance_policy.activate",
|
|
||||||
"rollback_configuration.install",
|
|
||||||
"governance_acceptance_tests.execute",
|
|
||||||
"single_owner_governance_activation_receipt.create"
|
|
||||||
],
|
|
||||||
"issued_at": "2026-07-28T17:36:34Z",
|
|
||||||
"legacy_mechanical_protection_gate": {
|
|
||||||
"account_reviews_do_not_count_as_owner_ratification": true,
|
|
||||||
"account_reviews_do_not_prove_distinct_natural_persons": true,
|
|
||||||
"approval_and_merge_whitelist_user_ids": [
|
|
||||||
42,
|
|
||||||
43
|
|
||||||
],
|
|
||||||
"both_reviews_must_bind_current_request_head": true,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"reviewers": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"merge_contract": {
|
|
||||||
"first_parent_must_equal_premerge_governance_oid": true,
|
|
||||||
"force": false,
|
|
||||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
|
||||||
"parent_count": 2,
|
|
||||||
"rebase": false,
|
|
||||||
"second_parent_must_equal_final_ratification_request_oid": true,
|
|
||||||
"single_file_add_only": true,
|
|
||||||
"squash": false
|
|
||||||
},
|
|
||||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET",
|
|
||||||
"mirror_contract": {
|
|
||||||
"expected_authoritative_pre_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
|
||||||
"fast_forward_only": true,
|
|
||||||
"force": false,
|
|
||||||
"fsck_connectivity_only_required": true,
|
|
||||||
"only_ref": "refs/heads/governance/approvals",
|
|
||||||
"plus_refspec": false,
|
|
||||||
"prune": false,
|
|
||||||
"quarantine_required": true,
|
|
||||||
"required_chain": [
|
|
||||||
"ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
|
||||||
"a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
|
||||||
"FINAL_RATIFICATION_MERGE_OID"
|
|
||||||
],
|
|
||||||
"source": "gitea_repository_file_url_only",
|
|
||||||
"tags": false
|
|
||||||
},
|
|
||||||
"owner_ratification_contract": {
|
|
||||||
"agent_must_not_submit_review": true,
|
|
||||||
"dismissed": false,
|
|
||||||
"only_valid_human_approver": "owner",
|
|
||||||
"owner_user_id": 2,
|
|
||||||
"review_state": "APPROVED",
|
|
||||||
"review_type_must_be_approved_not_requested": true,
|
|
||||||
"reviewed_before_expires_at": true,
|
|
||||||
"reviewed_before_merge": true,
|
|
||||||
"reviewed_head_oid_must_equal_current_request_head_oid": true,
|
|
||||||
"stale": false
|
|
||||||
},
|
|
||||||
"permanent_constraints": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"force_push_used": false,
|
|
||||||
"owner_approval_respected": true,
|
|
||||||
"secrets_committed": false,
|
|
||||||
"tag_rewritten": false
|
|
||||||
},
|
|
||||||
"post_merge_activation_preconditions": [
|
|
||||||
"owner_exact_head_approval_valid",
|
|
||||||
"legacy_mechanical_reviews_valid",
|
|
||||||
"ordinary_merge_commit",
|
|
||||||
"one_file_add_only",
|
|
||||||
"quarantine_chain_verified",
|
|
||||||
"approval_mirror_fast_forward_closed",
|
|
||||||
"rollback_integrity_pass",
|
|
||||||
"validator_and_state_machine_tests_pass",
|
|
||||||
"protection_compare_and_swap_pass",
|
|
||||||
"positive_and_negative_governance_tests_pass",
|
|
||||||
"production_write_false"
|
|
||||||
],
|
|
||||||
"pre_activation_protection_snapshot": {
|
|
||||||
"protected_branch": [
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "governance/approvals",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 1,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "integration/chatgpt-autonomous-completion-20260726",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 3,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "main",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 2,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"protected_tag": []
|
|
||||||
},
|
|
||||||
"pre_activation_protection_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
|
||||||
"pre_approval_non_authorizations": {
|
|
||||||
"baseline_write_allowed": false,
|
|
||||||
"branch_protection_update_allowed": false,
|
|
||||||
"control_plane_activation_allowed": false,
|
|
||||||
"development_allowed": false,
|
|
||||||
"integration_write_allowed": false,
|
|
||||||
"main_write_allowed": false,
|
|
||||||
"merge_allowed": false,
|
|
||||||
"mirror_allowed": false,
|
|
||||||
"production_write_allowed": false,
|
|
||||||
"registry_write_allowed": false,
|
|
||||||
"rollback_install_allowed": false,
|
|
||||||
"standing_writer_allowed": false,
|
|
||||||
"state_machine_update_allowed": false,
|
|
||||||
"tag_write_allowed": false,
|
|
||||||
"validator_update_allowed": false
|
|
||||||
},
|
|
||||||
"ratification_scope": "GOVERNANCE_MIGRATION_AND_CONTROL_PLANE_ACTIVATION_ONLY",
|
|
||||||
"ratifies_pr26_manifest_blob_oid": "65f44007b10fc2a912d69e3f9081c0cfff77dfad",
|
|
||||||
"ratifies_pr26_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-MIGRATION-20260728T162246Z.json",
|
|
||||||
"ratifies_pr26_manifest_sha256": "c097475afbe3074e6482f125b635981f8c0284f3d763c926e2a63e1a3ee1c6c3",
|
|
||||||
"ratifies_pr26_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
|
||||||
"ratifies_pr26_request_oid": "312d3b483049a748675e887a516ec739c81ff474",
|
|
||||||
"ratifies_pr26_tree_oid": "5c6974c058bba6210dcc1bdd0968fb7e6c84a844",
|
|
||||||
"ratifies_pr27_manifest_blob_oid": "77f3c2ab85cba2c30c8c0aa36e591232d0f4c396",
|
|
||||||
"ratifies_pr27_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-CONTROL-PLANE-DEPLOYMENT-20260728T170740Z-ea7a102dac417c23.json",
|
|
||||||
"ratifies_pr27_manifest_sha256": "2ed09180c0aeadbef2e5d6310efe7c2d0de15e0e87b6135e7a8494cb4f0967c7",
|
|
||||||
"ratifies_pr27_merge_oid": "4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
|
||||||
"ratifies_pr27_request_oid": "80feef309fd8e1db7795f68d5bf0f15ce805b365",
|
|
||||||
"ratifies_pr27_tree_oid": "85af0d1e88e8484a9c3a0562a81cbc1de09250ae",
|
|
||||||
"ratifies_pr_numbers": [
|
|
||||||
26,
|
|
||||||
27
|
|
||||||
],
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
|
||||||
"retroactive_authorization_for_other_operations": false,
|
|
||||||
"rollback_archive_member_hashes": {
|
|
||||||
"pre-migration-backup/app.ini": "0693e4fa8296a22533429339faeee2ac1856b6c966c6c1009bb139ab4784f687",
|
|
||||||
"pre-migration-backup/authoritative-aios.bundle": "4fb4ccc979c3061065cde451af0e733326cb56fa6f860124bb73f153a60907c9",
|
|
||||||
"pre-migration-backup/authoritative-refs.txt": "85cb664c8edf3902bebfc6c2253a3ee16cff8534f86561ba771a73df160186c6",
|
|
||||||
"pre-migration-backup/backup-metadata.json": "a38ef6dacb52ae61c4332bf8d087f479fb0d5f51d5f636a132a711bddd6e7732",
|
|
||||||
"pre-migration-backup/checksums.sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
|
||||||
"pre-migration-backup/gitea-aios.bundle": "a343c7437f3a1a2aa842dd9d986a1d3cf80691058faae978565224320ff5ed90",
|
|
||||||
"pre-migration-backup/gitea-refs.txt": "1792b2c4f1cf1f232704601ee968e16425f926a6eaab1bdcddf273525d046e4d",
|
|
||||||
"pre-migration-backup/gitea.db": "aadcc1587a05827ed3b2c4ffc2403f26139020cd0328afaa03f7afbf6083a2f1",
|
|
||||||
"pre-migration-backup/governance-identities.json": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
|
||||||
"pre-migration-backup/protected-branch.json": "5cc4ae4fe5614baa3d4bb83d71872a4ad1ce3c5e39061e02b336be40d5b5b75d",
|
|
||||||
"pre-migration-backup/protected-tag.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/aios_isolated_writer_policy.py": "d86ec23fd5d8b7e9844c0895c50385b6f8f3071c3dac3c0f26c70768ae00a188",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_authz.py": "cfae1cdf70c1dabbe013e9365c308a45f11acf20035881a3bb6143774682bbf0",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_path_policy.py": "c6c9ea8e45dfc03c4703503f4dc9c92f887bbacdbf1c7779a487567eee6c257c",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/silent_release_controller.py": "524a0a9961b15c2a35b669d1b89b2142377c13ddd3c721780f374331b7d2b6eb",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/silent_release_hardening.py": "524aba7034f2cd89d897fe01e9799c835bae9f11ca738182ce469aa59e05cc1a",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py": "cc937a81aef881206cb1d6fd2ad1feb1a1c2e0ad91c908095d56fd96402c3373",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py": "aa3b3dd41c03c17e4e116ce444d9f4f779694a9cde684e8329cf892bd319069a",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py": "24b3f3326e6dcd1a4c376d892c53ad20c68e0dd6f861a3a34e11c9a2bba4c756",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py": "db535e5006f8e4a3ab80c2f689213045c120b01cda9b43ef461a3022096c9d19",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py": "31e1f3b904b0bfc8822d21bfd166a45388f122398d1ec76b803e90ee06dc92f0",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py": "5f6a8e3a4d66f47717e95842f2eaa443e3cfa321cb08026cc82d74c4ad353bcf",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_core_contracts.py": "71852070db50a04901c141213cadc987ae17ec678fe27a901fee165b6d1c3e2a",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_external_gates.py": "91cca37eb1d40f0cf9fc38b12e2123045ffebd794a34d6cdef3f1dec470fffd2",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_release_gates.py": "d82b23ac00f5389f1a0de628bda9dff898d33f80d375cf6ff74f1531fd138134",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_state_and_workflow.py": "c0395a901732adb0b2f1402e6a1cf5202d54aa597bcaec9584a7ea7589c53917"
|
|
||||||
},
|
|
||||||
"rollback_configuration_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/rollback-configuration.tar",
|
|
||||||
"rollback_configuration_sha256": "fc8dcf68aaf53edb0cda45833ee45ba5b8b205791af9073258ca617c8b8187d6",
|
|
||||||
"schema": "aios.single-owner-governance-final-owner-ratification/v1",
|
|
||||||
"single_owner_policy_effective_at": "FINAL_RATIFICATION_MERGE_OID",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-FINAL-RATIFICATION-20260729"
|
|
||||||
}
|
|
||||||
@@ -1,484 +0,0 @@
|
|||||||
{
|
|
||||||
"allowed_human_approvers": [
|
|
||||||
"owner"
|
|
||||||
],
|
|
||||||
"approval_expires_at": "2026-07-29T16:22:46Z",
|
|
||||||
"approval_request_branch": "governance/request/single-owner-governance-migration-20260728T162246Z",
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_POLICY",
|
|
||||||
"approval_types_migrated_after_policy_effective_at": [
|
|
||||||
"SOURCE_MAPPING_AND_IMPORT_EXECUTION_RENEWAL",
|
|
||||||
"CONTROL_PLANE_DEPLOYMENT_APPROVAL",
|
|
||||||
"DEVELOPMENT_MERGE_APPROVAL",
|
|
||||||
"RELEASE_CANDIDATE_APPROVAL",
|
|
||||||
"PRODUCTION_RELEASE_APPROVAL",
|
|
||||||
"TAG_APPROVAL",
|
|
||||||
"REGISTRY_BASELINE_PROMOTION_APPROVAL",
|
|
||||||
"PRODUCTION_DEPLOYMENT_APPROVAL",
|
|
||||||
"ROLLBACK_APPROVAL"
|
|
||||||
],
|
|
||||||
"atomic_migration": {
|
|
||||||
"backup_checksums_sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
|
||||||
"backup_root": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/pre-migration-backup",
|
|
||||||
"direct_database_update": false,
|
|
||||||
"direct_ref_update": false,
|
|
||||||
"dual_capable_validator_shadow_required": true,
|
|
||||||
"identity_snapshot_sha256": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
|
||||||
"live_protection_canonical_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
|
||||||
"maintenance_lock_required": true,
|
|
||||||
"old_rules_remain_active_until_owner_approval_and_machine_gates_pass": true,
|
|
||||||
"protected_branch_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
|
||||||
"protection_update_method": "authenticated_admin_API_compare_and_swap",
|
|
||||||
"reviewable_backup_file_hashes": {
|
|
||||||
"app.ini": "0693e4fa8296a22533429339faeee2ac1856b6c966c6c1009bb139ab4784f687",
|
|
||||||
"authoritative-aios.bundle": "4fb4ccc979c3061065cde451af0e733326cb56fa6f860124bb73f153a60907c9",
|
|
||||||
"authoritative-refs.txt": "85cb664c8edf3902bebfc6c2253a3ee16cff8534f86561ba771a73df160186c6",
|
|
||||||
"backup-metadata.json": "a38ef6dacb52ae61c4332bf8d087f479fb0d5f51d5f636a132a711bddd6e7732",
|
|
||||||
"checksums.sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
|
||||||
"gitea-aios.bundle": "a343c7437f3a1a2aa842dd9d986a1d3cf80691058faae978565224320ff5ed90",
|
|
||||||
"gitea-refs.txt": "1792b2c4f1cf1f232704601ee968e16425f926a6eaab1bdcddf273525d046e4d",
|
|
||||||
"gitea.db": "aadcc1587a05827ed3b2c4ffc2403f26139020cd0328afaa03f7afbf6083a2f1",
|
|
||||||
"governance-identities.json": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
|
||||||
"protected-branch.json": "5cc4ae4fe5614baa3d4bb83d71872a4ad1ce3c5e39061e02b336be40d5b5b75d",
|
|
||||||
"protected-tag.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
||||||
"repository/active-runtime/app/aios_isolated_writer_policy.py": "d86ec23fd5d8b7e9844c0895c50385b6f8f3071c3dac3c0f26c70768ae00a188",
|
|
||||||
"repository/active-runtime/app/isolated_writer_authz.py": "cfae1cdf70c1dabbe013e9365c308a45f11acf20035881a3bb6143774682bbf0",
|
|
||||||
"repository/active-runtime/app/isolated_writer_path_policy.py": "c6c9ea8e45dfc03c4703503f4dc9c92f887bbacdbf1c7779a487567eee6c257c",
|
|
||||||
"repository/active-runtime/app/silent_release_controller.py": "524a0a9961b15c2a35b669d1b89b2142377c13ddd3c721780f374331b7d2b6eb",
|
|
||||||
"repository/active-runtime/app/silent_release_hardening.py": "524aba7034f2cd89d897fe01e9799c835bae9f11ca738182ce469aa59e05cc1a",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py": "cc937a81aef881206cb1d6fd2ad1feb1a1c2e0ad91c908095d56fd96402c3373",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py": "aa3b3dd41c03c17e4e116ce444d9f4f779694a9cde684e8329cf892bd319069a",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py": "24b3f3326e6dcd1a4c376d892c53ad20c68e0dd6f861a3a34e11c9a2bba4c756",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py": "db535e5006f8e4a3ab80c2f689213045c120b01cda9b43ef461a3022096c9d19",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py": "31e1f3b904b0bfc8822d21bfd166a45388f122398d1ec76b803e90ee06dc92f0",
|
|
||||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py": "5f6a8e3a4d66f47717e95842f2eaa443e3cfa321cb08026cc82d74c4ad353bcf",
|
|
||||||
"repository/tests/autonomous_release/test_core_contracts.py": "71852070db50a04901c141213cadc987ae17ec678fe27a901fee165b6d1c3e2a",
|
|
||||||
"repository/tests/autonomous_release/test_external_gates.py": "91cca37eb1d40f0cf9fc38b12e2123045ffebd794a34d6cdef3f1dec470fffd2",
|
|
||||||
"repository/tests/autonomous_release/test_release_gates.py": "d82b23ac00f5389f1a0de628bda9dff898d33f80d375cf6ff74f1531fd138134",
|
|
||||||
"repository/tests/autonomous_release/test_state_and_workflow.py": "c0395a901732adb0b2f1402e6a1cf5202d54aa597bcaec9584a7ea7589c53917"
|
|
||||||
},
|
|
||||||
"reviewable_pre_migration_governance_identity_snapshot": [
|
|
||||||
{
|
|
||||||
"created_unix": 1783321252,
|
|
||||||
"id": 1,
|
|
||||||
"is_active": 1,
|
|
||||||
"is_admin": 0,
|
|
||||||
"last_login_unix": 0,
|
|
||||||
"lower_name": "aios",
|
|
||||||
"prohibit_login": 1,
|
|
||||||
"type": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created_unix": 1783335380,
|
|
||||||
"id": 2,
|
|
||||||
"is_active": 1,
|
|
||||||
"is_admin": 1,
|
|
||||||
"last_login_unix": 1785086126,
|
|
||||||
"lower_name": "owner",
|
|
||||||
"prohibit_login": 0,
|
|
||||||
"type": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created_unix": 1783411566,
|
|
||||||
"id": 3,
|
|
||||||
"is_active": 1,
|
|
||||||
"is_admin": 1,
|
|
||||||
"last_login_unix": 0,
|
|
||||||
"lower_name": "gito",
|
|
||||||
"prohibit_login": 0,
|
|
||||||
"type": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created_unix": 1785078826,
|
|
||||||
"id": 42,
|
|
||||||
"is_active": 1,
|
|
||||||
"is_admin": 0,
|
|
||||||
"last_login_unix": 1785206253,
|
|
||||||
"lower_name": "codex",
|
|
||||||
"prohibit_login": 0,
|
|
||||||
"type": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created_unix": 1785078882,
|
|
||||||
"id": 43,
|
|
||||||
"is_active": 1,
|
|
||||||
"is_admin": 0,
|
|
||||||
"last_login_unix": 1785206380,
|
|
||||||
"lower_name": "gpt",
|
|
||||||
"prohibit_login": 0,
|
|
||||||
"type": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"reviewable_pre_migration_protection_snapshot": {
|
|
||||||
"protected_branch": [
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "governance/approvals",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 1,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "integration/chatgpt-autonomous-completion-20260726",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 3,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "main",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 0,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 2,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 2,
|
|
||||||
"status_check_contexts": "[]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785093422,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"protected_tag": []
|
|
||||||
},
|
|
||||||
"rollback_archive_members": {
|
|
||||||
"pre-migration-backup/app.ini": "0693e4fa8296a22533429339faeee2ac1856b6c966c6c1009bb139ab4784f687",
|
|
||||||
"pre-migration-backup/authoritative-aios.bundle": "4fb4ccc979c3061065cde451af0e733326cb56fa6f860124bb73f153a60907c9",
|
|
||||||
"pre-migration-backup/authoritative-refs.txt": "85cb664c8edf3902bebfc6c2253a3ee16cff8534f86561ba771a73df160186c6",
|
|
||||||
"pre-migration-backup/backup-metadata.json": "a38ef6dacb52ae61c4332bf8d087f479fb0d5f51d5f636a132a711bddd6e7732",
|
|
||||||
"pre-migration-backup/checksums.sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
|
||||||
"pre-migration-backup/gitea-aios.bundle": "a343c7437f3a1a2aa842dd9d986a1d3cf80691058faae978565224320ff5ed90",
|
|
||||||
"pre-migration-backup/gitea-refs.txt": "1792b2c4f1cf1f232704601ee968e16425f926a6eaab1bdcddf273525d046e4d",
|
|
||||||
"pre-migration-backup/gitea.db": "aadcc1587a05827ed3b2c4ffc2403f26139020cd0328afaa03f7afbf6083a2f1",
|
|
||||||
"pre-migration-backup/governance-identities.json": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
|
||||||
"pre-migration-backup/protected-branch.json": "5cc4ae4fe5614baa3d4bb83d71872a4ad1ce3c5e39061e02b336be40d5b5b75d",
|
|
||||||
"pre-migration-backup/protected-tag.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/aios_isolated_writer_policy.py": "d86ec23fd5d8b7e9844c0895c50385b6f8f3071c3dac3c0f26c70768ae00a188",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_authz.py": "cfae1cdf70c1dabbe013e9365c308a45f11acf20035881a3bb6143774682bbf0",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_path_policy.py": "c6c9ea8e45dfc03c4703503f4dc9c92f887bbacdbf1c7779a487567eee6c257c",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/silent_release_controller.py": "524a0a9961b15c2a35b669d1b89b2142377c13ddd3c721780f374331b7d2b6eb",
|
|
||||||
"pre-migration-backup/repository/active-runtime/app/silent_release_hardening.py": "524aba7034f2cd89d897fe01e9799c835bae9f11ca738182ce469aa59e05cc1a",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py": "cc937a81aef881206cb1d6fd2ad1feb1a1c2e0ad91c908095d56fd96402c3373",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py": "aa3b3dd41c03c17e4e116ce444d9f4f779694a9cde684e8329cf892bd319069a",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py": "24b3f3326e6dcd1a4c376d892c53ad20c68e0dd6f861a3a34e11c9a2bba4c756",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py": "db535e5006f8e4a3ab80c2f689213045c120b01cda9b43ef461a3022096c9d19",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py": "31e1f3b904b0bfc8822d21bfd166a45388f122398d1ec76b803e90ee06dc92f0",
|
|
||||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py": "5f6a8e3a4d66f47717e95842f2eaa443e3cfa321cb08026cc82d74c4ad353bcf",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_core_contracts.py": "71852070db50a04901c141213cadc987ae17ec678fe27a901fee165b6d1c3e2a",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_external_gates.py": "91cca37eb1d40f0cf9fc38b12e2123045ffebd794a34d6cdef3f1dec470fffd2",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_release_gates.py": "d82b23ac00f5389f1a0de628bda9dff898d33f80d375cf6ff74f1531fd138134",
|
|
||||||
"pre-migration-backup/repository/tests/autonomous_release/test_state_and_workflow.py": "c0395a901732adb0b2f1402e6a1cf5202d54aa597bcaec9584a7ea7589c53917"
|
|
||||||
},
|
|
||||||
"rollback_configuration_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/rollback-configuration.tar",
|
|
||||||
"rollback_configuration_sha256": "fc8dcf68aaf53edb0cda45833ee45ba5b8b205791af9073258ca617c8b8187d6",
|
|
||||||
"rollback_restores_protection_before_validator": true
|
|
||||||
},
|
|
||||||
"auto_merge_after_owner_approval": false,
|
|
||||||
"auto_merge_machine_conditions": [
|
|
||||||
"owner_approval_valid",
|
|
||||||
"head_oid_unchanged",
|
|
||||||
"required_checks_pass",
|
|
||||||
"secret_scan_pass",
|
|
||||||
"evidence_closure_pass",
|
|
||||||
"approval_not_expired",
|
|
||||||
"protected_branch_rules_pass"
|
|
||||||
],
|
|
||||||
"automatic_renewal_policy": {
|
|
||||||
"approval_ref_fast_forward_descendant_required": true,
|
|
||||||
"changed_condition_fallback": "HUMAN_OWNER_APPROVAL_PENDING",
|
|
||||||
"secret_scan": "PASS_REQUIRED",
|
|
||||||
"single_renewal_max_hours": 12,
|
|
||||||
"status": "PROPOSED_AFTER_MIGRATION",
|
|
||||||
"total_window_max_days": 7,
|
|
||||||
"unchanged_fields_required": [
|
|
||||||
"original_policy_sha256",
|
|
||||||
"allowed_paths",
|
|
||||||
"allowed_operations",
|
|
||||||
"denied_operations",
|
|
||||||
"source_sha256",
|
|
||||||
"source_blob_oid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"break_glass_executor_may_merge_only_after_owner_approval_machine_gates_and_atomic_cutover_preconditions": true,
|
|
||||||
"break_glass_expires_at": "2026-07-29T16:22:46Z",
|
|
||||||
"desired_branch_protection": {
|
|
||||||
"approval_whitelist_user_ids": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"approval_whitelist_usernames": [
|
|
||||||
"owner"
|
|
||||||
],
|
|
||||||
"block_on_official_review_requests": true,
|
|
||||||
"block_on_outdated_branch": true,
|
|
||||||
"block_on_rejected_reviews": true,
|
|
||||||
"branch_deletion": false,
|
|
||||||
"branch_patterns": [
|
|
||||||
"governance/approvals",
|
|
||||||
"main",
|
|
||||||
"release/**",
|
|
||||||
"integration/**"
|
|
||||||
],
|
|
||||||
"direct_push": false,
|
|
||||||
"dismiss_stale_approvals": true,
|
|
||||||
"force_push": false,
|
|
||||||
"require_status_checks": true,
|
|
||||||
"required_approvals": 1,
|
|
||||||
"required_status_contexts": [
|
|
||||||
"aios/machine-gates",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/evidence-closure"
|
|
||||||
],
|
|
||||||
"wip_blocks_merge": true
|
|
||||||
},
|
|
||||||
"executor_must_not_approve": true,
|
|
||||||
"expected_governance_base_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
|
||||||
"expires_at": "2026-07-29T16:22:46Z",
|
|
||||||
"global_constraints": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"approval_impersonation": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"fake_review": false,
|
|
||||||
"force_push": false,
|
|
||||||
"production_change_without_owner_approval": false,
|
|
||||||
"secret_commit": false,
|
|
||||||
"tag_rewrite": false
|
|
||||||
},
|
|
||||||
"governance_model": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
|
||||||
"historical_policy": {
|
|
||||||
"historical_manifests_modified": false,
|
|
||||||
"old_policy_decoder_retained_read_only": true,
|
|
||||||
"requests_created_before_policy_effective_at_use_old_policy": true
|
|
||||||
},
|
|
||||||
"issued_at": "2026-07-28T16:22:46Z",
|
|
||||||
"machine_gates": {
|
|
||||||
"development_branch": [
|
|
||||||
"allowed_branch",
|
|
||||||
"allowed_paths",
|
|
||||||
"compileall",
|
|
||||||
"targeted_tests",
|
|
||||||
"secret_scan",
|
|
||||||
"git_diff_check"
|
|
||||||
],
|
|
||||||
"production": [
|
|
||||||
"owner_approval",
|
|
||||||
"tag_consistency",
|
|
||||||
"registry_consistency",
|
|
||||||
"deployment_plan",
|
|
||||||
"rollback_plan",
|
|
||||||
"artifact_integrity",
|
|
||||||
"production_preflight"
|
|
||||||
],
|
|
||||||
"protected_branch_merge": [
|
|
||||||
"expected_base_oid",
|
|
||||||
"current_head_oid",
|
|
||||||
"ordinary_merge_commit",
|
|
||||||
"parent_order_verified",
|
|
||||||
"artifact_binding",
|
|
||||||
"audit_chain_valid"
|
|
||||||
],
|
|
||||||
"release": [
|
|
||||||
"version_manifest",
|
|
||||||
"explicit_release_target_oid",
|
|
||||||
"artifact_sha256",
|
|
||||||
"rollback_artifact_sha256",
|
|
||||||
"sbom",
|
|
||||||
"test_evidence",
|
|
||||||
"evidence_closure"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
|
||||||
"migration_acceptance_tests": [
|
|
||||||
"agent_request_pass",
|
|
||||||
"owner_current_head_approval_pass",
|
|
||||||
"machine_gates_then_merge_commit_pass",
|
|
||||||
"agent_self_approval_denied",
|
|
||||||
"non_owner_approval_denied",
|
|
||||||
"stale_head_approval_denied",
|
|
||||||
"wip_merge_denied",
|
|
||||||
"machine_test_failure_merge_denied",
|
|
||||||
"secret_scan_failure_merge_denied",
|
|
||||||
"main_direct_write_denied",
|
|
||||||
"tag_direct_write_denied",
|
|
||||||
"force_push_denied",
|
|
||||||
"missing_tag_target_oid_denied",
|
|
||||||
"missing_evidence_closure_oid_denied",
|
|
||||||
"production_without_owner_denied",
|
|
||||||
"dormant_only_production_activation_denied"
|
|
||||||
],
|
|
||||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET",
|
|
||||||
"new_policy": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
|
||||||
"old_policy": "DUAL_HUMAN_APPROVAL",
|
|
||||||
"owner_approval_contract": {
|
|
||||||
"dismissed": false,
|
|
||||||
"does_not_claim_old_dual_approval_compliance": true,
|
|
||||||
"head_change_invalidates_owner_approval": true,
|
|
||||||
"owner_review_is_break_glass_signature": true,
|
|
||||||
"review_created_before_approval_expires_at": true,
|
|
||||||
"review_state": "APPROVED",
|
|
||||||
"reviewed_head_oid_must_equal_current_pr_head_oid": true,
|
|
||||||
"reviewer_must_equal": "owner",
|
|
||||||
"stale": false
|
|
||||||
},
|
|
||||||
"owner_break_glass_declaration_to_be_attested_by_review": {
|
|
||||||
"attestation_status": "PENDING_OWNER_REVIEW",
|
|
||||||
"expires_at": "2026-07-29T16:22:46Z",
|
|
||||||
"new_policy": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
|
||||||
"old_policy": "DUAL_HUMAN_APPROVAL",
|
|
||||||
"owner_acknowledges_old_policy_exception": true,
|
|
||||||
"reason": "single-maintainer-system",
|
|
||||||
"scope": "governance-policy-migration-only"
|
|
||||||
},
|
|
||||||
"owner_is_only_human_approver": true,
|
|
||||||
"policy_effective_at": null,
|
|
||||||
"pre_approval_non_authorizations": {
|
|
||||||
"approval_validator_update_allowed": false,
|
|
||||||
"gitea_branch_protection_update_allowed": false,
|
|
||||||
"merge_allowed": false,
|
|
||||||
"old_dual_rule_disable_allowed": false,
|
|
||||||
"production_change_allowed": false,
|
|
||||||
"state_machine_update_allowed": false,
|
|
||||||
"writer_credential_issue_allowed": false
|
|
||||||
},
|
|
||||||
"prohibited_merge_modes": [
|
|
||||||
"squash",
|
|
||||||
"rebase",
|
|
||||||
"force",
|
|
||||||
"fast_forward_policy_bypass"
|
|
||||||
],
|
|
||||||
"reason": "single-maintainer-system",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
|
||||||
"requester_must_not_approve": true,
|
|
||||||
"requesters": [
|
|
||||||
"codex-chartgpt-bridge",
|
|
||||||
"aios-governance-agent",
|
|
||||||
"aios-release-agent",
|
|
||||||
"aios-governed-development-writer"
|
|
||||||
],
|
|
||||||
"required_distinct_human_approvers": 1,
|
|
||||||
"required_human_approvals": 1,
|
|
||||||
"required_owner_approvals": 1,
|
|
||||||
"resolved_owner": {
|
|
||||||
"confidence_before_review": "MEDIUM",
|
|
||||||
"confirmation_rule": "valid APPROVED review by owner on this exact immutable head",
|
|
||||||
"email_sha256": "f5dc2928f6202c24fa4db8bb33db84e1b77c6eff30d1b95abdadeaae209bc4c9",
|
|
||||||
"identity_source": [
|
|
||||||
"gitea_repository_owner_account_aios_is_login_prohibited",
|
|
||||||
"all_login_enabled_site_admins_enumerated_owner_and_never_logged_in_service_gito",
|
|
||||||
"only_interactive_site_admin_named_owner",
|
|
||||||
"gitea_owner_last_login_present",
|
|
||||||
"historical_governance_records_use_approved_by_role_owner",
|
|
||||||
"user_declared_single_natural_person_maintainer"
|
|
||||||
],
|
|
||||||
"login_enabled_site_admins": [
|
|
||||||
{
|
|
||||||
"observed_prior_login": true,
|
|
||||||
"user_id": 2,
|
|
||||||
"username": "owner"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"classification": "service_admin_not_human_approver",
|
|
||||||
"observed_prior_login": false,
|
|
||||||
"user_id": 3,
|
|
||||||
"username": "gito"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"user_id": 2,
|
|
||||||
"username": "owner"
|
|
||||||
},
|
|
||||||
"schema": "aios.single-owner-governance-policy/v1",
|
|
||||||
"scope": "governance-policy-migration-only",
|
|
||||||
"standing_autonomous_development_policy": {
|
|
||||||
"allowed_without_repeated_owner_approval": [
|
|
||||||
"patch_branch_create",
|
|
||||||
"allowlisted_development_write",
|
|
||||||
"patch_branch_commit_and_push",
|
|
||||||
"test_execute",
|
|
||||||
"artifact_build",
|
|
||||||
"approval_pr_create",
|
|
||||||
"read_only_evidence",
|
|
||||||
"task_writer_renewal"
|
|
||||||
],
|
|
||||||
"owner_approval_still_required": [
|
|
||||||
"protected_branch_merge",
|
|
||||||
"tag",
|
|
||||||
"registry_update",
|
|
||||||
"production_baseline_update",
|
|
||||||
"production_deploy",
|
|
||||||
"production_rollback",
|
|
||||||
"governance_policy_change"
|
|
||||||
],
|
|
||||||
"status": "PROPOSED_AFTER_MIGRATION"
|
|
||||||
},
|
|
||||||
"standing_single_owner_governance_migration_authorization": true,
|
|
||||||
"state_machine": {
|
|
||||||
"auto_merge_ready_state": "AUTO_MERGE_READY",
|
|
||||||
"execution_ready_state": "OWNER_APPROVED_EXECUTION_READY",
|
|
||||||
"machine_verified_state": "MACHINE_GATES_VERIFIED",
|
|
||||||
"owner_approval_alone_is_not_execution_ready": true,
|
|
||||||
"owner_verified_state": "OWNER_APPROVAL_VERIFIED",
|
|
||||||
"pending_state": "HUMAN_OWNER_APPROVAL_PENDING"
|
|
||||||
},
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729",
|
|
||||||
"this_manifest_authorizes_only_governance_policy_migration": true,
|
|
||||||
"this_manifest_grants_standing_development_authorization": false
|
|
||||||
}
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "aios.gitea-native-approval/v1",
|
|
||||||
"approval_type": "IMPLEMENTATION",
|
|
||||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"branch": "patch/integration-refresh-grant-provisioner-20260726",
|
|
||||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
|
||||||
"actor_identity": "chatgpt-gitops-bot",
|
|
||||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer",
|
|
||||||
"risk": "high",
|
|
||||||
"allowed_paths": [
|
|
||||||
"active-runtime/app/**",
|
|
||||||
"tests/test_integration_refresh_provisioner.py",
|
|
||||||
"tests/test_isolated_writer_integration_merge.py",
|
|
||||||
"tests/test_gitea_branch_read_projection.py",
|
|
||||||
"tests/test_isolated_writer_token_policy_v2.py",
|
|
||||||
"deploy/systemd/aios-integration-refresh-*.service",
|
|
||||||
"docs/autonomous-release/**"
|
|
||||||
],
|
|
||||||
"allowed_operations": [
|
|
||||||
"repository.source_map.read",
|
|
||||||
"runtime.unit_metadata.read",
|
|
||||||
"git.ref.read",
|
|
||||||
"git.diff.read",
|
|
||||||
"git.branch.patch.create",
|
|
||||||
"commit_file.atomic",
|
|
||||||
"git.push.patch_branch",
|
|
||||||
"test.execute.allowlisted",
|
|
||||||
"artifact.build.control_plane_patch",
|
|
||||||
"evidence.write.task_runtime"
|
|
||||||
],
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"main.merge",
|
|
||||||
"tag.write",
|
|
||||||
"tag.delete",
|
|
||||||
"tag.rewrite",
|
|
||||||
"force_push",
|
|
||||||
"history.rewrite",
|
|
||||||
"baseline.write",
|
|
||||||
"registry.write",
|
|
||||||
"production.deploy",
|
|
||||||
"production.rollback",
|
|
||||||
"secret.read",
|
|
||||||
"secret.export",
|
|
||||||
"bare_ref_direct_write"
|
|
||||||
],
|
|
||||||
"expires_at": "2026-07-27T03:35:55Z"
|
|
||||||
}
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": "aios.gitea-native-approval/v1",
|
|
||||||
"approval_type": "IMPLEMENTATION",
|
|
||||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"branch": "patch/integration-refresh-grant-provisioner-20260726",
|
|
||||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
|
||||||
"actor_identity": "chatgpt-gitops-bot",
|
|
||||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer",
|
|
||||||
"risk": "high",
|
|
||||||
"allowed_paths": [
|
|
||||||
"active-runtime/app/**",
|
|
||||||
"tests/test_integration_refresh_provisioner.py",
|
|
||||||
"tests/test_isolated_writer_integration_merge.py",
|
|
||||||
"tests/test_gitea_branch_read_projection.py",
|
|
||||||
"tests/test_isolated_writer_token_policy_v2.py",
|
|
||||||
"deploy/systemd/aios-integration-refresh-*.service",
|
|
||||||
"docs/autonomous-release/**"
|
|
||||||
],
|
|
||||||
"allowed_operations": [
|
|
||||||
"repository.source_map.read",
|
|
||||||
"runtime.unit_metadata.read",
|
|
||||||
"git.ref.read",
|
|
||||||
"git.diff.read",
|
|
||||||
"git.branch.patch.create",
|
|
||||||
"commit_file.atomic",
|
|
||||||
"git.push.patch_branch",
|
|
||||||
"test.execute.allowlisted",
|
|
||||||
"artifact.build.control_plane_patch",
|
|
||||||
"evidence.write.task_runtime"
|
|
||||||
],
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"main.merge",
|
|
||||||
"tag.write",
|
|
||||||
"tag.delete",
|
|
||||||
"tag.rewrite",
|
|
||||||
"force_push",
|
|
||||||
"history.rewrite",
|
|
||||||
"baseline.write",
|
|
||||||
"registry.write",
|
|
||||||
"production.deploy",
|
|
||||||
"production.rollback",
|
|
||||||
"secret.read",
|
|
||||||
"secret.export",
|
|
||||||
"bare_ref_direct_write"
|
|
||||||
],
|
|
||||||
"expires_at": "2026-07-27T13:00:11Z"
|
|
||||||
}
|
|
||||||
-73
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
|
|
||||||
"artifact_sha256": "01e0226f0302bb8e2f8d81299393075bf0332c6e22e00877469fb297c51f3802",
|
|
||||||
"authorization_scope": [
|
|
||||||
"deploy_live_activation_implementation",
|
|
||||||
"execute_one_atomic_governance_activation",
|
|
||||||
"execute_acceptance_tests",
|
|
||||||
"control_plane_rollback_on_failure",
|
|
||||||
"write_activation_receipt"
|
|
||||||
],
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"registry.write",
|
|
||||||
"baseline.write",
|
|
||||||
"production.deploy",
|
|
||||||
"force_push",
|
|
||||||
"history_rewrite"
|
|
||||||
],
|
|
||||||
"evidence_closure_oid": "c6316943d2485f06f97242df6338ded21de1ee38",
|
|
||||||
"evidence_closure_sha256": "bd521103c907d6194acbbbd42a947c5af2ccbda91797e5dc75e81887a042becd",
|
|
||||||
"execution_binding": {
|
|
||||||
"backend": "sqlite",
|
|
||||||
"branch": "governance/approvals",
|
|
||||||
"evidence_closure_oid": "c6316943d2485f06f97242df6338ded21de1ee38",
|
|
||||||
"executor": "aios-governance-single-owner-activation",
|
|
||||||
"expected_branch_protection_sha256": "f27992c8024426708d93bce072d231707bae47a927aa2393d566aae64d5ed079",
|
|
||||||
"expected_gitea_schema_sha256": "3524f4912165939cb746c2c65d70583a909f5abbd00c4e07ae76f7206d49aa54",
|
|
||||||
"expected_state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
|
||||||
"expected_validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
|
||||||
"forbidden_operations": {
|
|
||||||
"baseline": false,
|
|
||||||
"force_push": false,
|
|
||||||
"governance_direct": false,
|
|
||||||
"history_rewrite": false,
|
|
||||||
"integration": false,
|
|
||||||
"main": false,
|
|
||||||
"production": false,
|
|
||||||
"registry": false,
|
|
||||||
"tags": false
|
|
||||||
},
|
|
||||||
"operation_id": "single-owner-live-activation-20260729T033228Z",
|
|
||||||
"repo_id": 1,
|
|
||||||
"requester": "codex-agent",
|
|
||||||
"target_branch_protection_sha256": "61b424b0c495eed71c73171c5c4434ac66d074de36886a73ec4c445fd1905db8",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729"
|
|
||||||
},
|
|
||||||
"execution_binding_sha256": "6b2ed671a94fa83704383728fd2bb2df3b67523b4273100fa371777d984be102",
|
|
||||||
"expected_governance_base_oid": "8b7f9d9028f6ff9beb225bf85f53dbeaac6830e7",
|
|
||||||
"expires_at": "2026-07-29T15:32:28Z",
|
|
||||||
"human_authorizer": "owner",
|
|
||||||
"implementation_commit_oid": "e1e438564ce8b987a36df0947e6eaa324dfede74",
|
|
||||||
"implementation_tree_oid": "dd93dc1df10cb8b8fe59b09d3214a9ae6fee671e",
|
|
||||||
"issued_at": "2026-07-29T03:32:28Z",
|
|
||||||
"legacy_mechanical_approvals": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"machine_gate_contexts": [
|
|
||||||
"aios/branch-protection",
|
|
||||||
"aios/evidence-closure",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/state-machine",
|
|
||||||
"aios/validator"
|
|
||||||
],
|
|
||||||
"pr30_merge_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
|
||||||
"pr31_merge_oid": "8b7f9d9028f6ff9beb225bf85f53dbeaac6830e7",
|
|
||||||
"rollback_artifact_sha256": "ed9307cc2f07767b75866fcb370fa3f98b76c5147e4b7fc49ecf04cf21cf7c31",
|
|
||||||
"signed_merge_requirement_effective": false,
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
|
||||||
"test_evidence_sha256": "172a7cf812c6b901524138fb72851a9b7d2e895dedc98893407d0bab9b52c6c1"
|
|
||||||
}
|
|
||||||
-171
@@ -1,171 +0,0 @@
|
|||||||
{
|
|
||||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
|
|
||||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
|
||||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
|
||||||
"authorization_scope": [
|
|
||||||
"deploy_exact_approved_artifact",
|
|
||||||
"publish_exact_head_machine_statuses",
|
|
||||||
"execute_one_atomic_governance_activation",
|
|
||||||
"verify_and_rollback_exact_transaction"
|
|
||||||
],
|
|
||||||
"denied_operations": [
|
|
||||||
"main.write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"registry.write",
|
|
||||||
"baseline.write",
|
|
||||||
"production.deploy",
|
|
||||||
"force_push",
|
|
||||||
"history_rewrite"
|
|
||||||
],
|
|
||||||
"evidence_closure_blob_oid": "6af6d017f22c4e0c512b0cd62644cdc6ab2135d5",
|
|
||||||
"evidence_closure_oid": "310ab2af902a9fde9108d227077f433cc743ce7c",
|
|
||||||
"evidence_closure_path": "evidence/single-owner-governance/AIOS-SINGLE-OWNER-LIVE-ACTIVATION-HARDENING-20260729T043010Z.json",
|
|
||||||
"evidence_closure_sha256": "e482fe52c8381f765ee508d2de1fe2e77dff52e6e342f01be4ca481ff2f76820",
|
|
||||||
"execution_binding": {
|
|
||||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
|
||||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
|
||||||
"backend": "sqlite",
|
|
||||||
"branch": "governance/approvals",
|
|
||||||
"evidence_closure_oid": "310ab2af902a9fde9108d227077f433cc743ce7c",
|
|
||||||
"evidence_closure_path": "evidence/single-owner-governance/AIOS-SINGLE-OWNER-LIVE-ACTIVATION-HARDENING-20260729T043010Z.json",
|
|
||||||
"evidence_closure_sha256": "e482fe52c8381f765ee508d2de1fe2e77dff52e6e342f01be4ca481ff2f76820",
|
|
||||||
"executor": "aios-governance-single-owner-activation",
|
|
||||||
"expected_branch_protection_sha256": "f27992c8024426708d93bce072d231707bae47a927aa2393d566aae64d5ed079",
|
|
||||||
"expected_gitea_schema_sha256": "3524f4912165939cb746c2c65d70583a909f5abbd00c4e07ae76f7206d49aa54",
|
|
||||||
"expected_state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
|
||||||
"expected_validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
|
||||||
"forbidden_operations": {
|
|
||||||
"baseline": false,
|
|
||||||
"force_push": false,
|
|
||||||
"governance_direct": false,
|
|
||||||
"history_rewrite": false,
|
|
||||||
"integration": false,
|
|
||||||
"main": false,
|
|
||||||
"production": false,
|
|
||||||
"registry": false,
|
|
||||||
"tags": false
|
|
||||||
},
|
|
||||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
|
||||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
|
||||||
"machine_status_producer_id": 13,
|
|
||||||
"machine_status_producer_name": "gitops-bot",
|
|
||||||
"operation_id": "single-owner-live-activation-hardening-20260729T043149Z",
|
|
||||||
"repo_id": 1,
|
|
||||||
"requester": "codex-agent",
|
|
||||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
|
||||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
|
||||||
"runtime_sources": [
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
|
||||||
"sha256": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
|
||||||
"sha256": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
|
||||||
"sha256": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
|
||||||
"sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
|
||||||
"sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"target_branch_protection_sha256": "4713f0494b79103668fbe5bd8fd7da5d60a43e56fb545aa6e7c773f43dc07c10",
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
|
||||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
|
||||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684"
|
|
||||||
},
|
|
||||||
"execution_binding_sha256": "63705dd0f5cc25debde132179f96819a844a9703b33c092db4ac48d5680d536a",
|
|
||||||
"expected_governance_base_oid": "1071979079deab8a4028473d967c2a205c3b111e",
|
|
||||||
"expires_at": "2026-07-29T16:31:49Z",
|
|
||||||
"human_authorizer": "owner",
|
|
||||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
|
||||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
|
||||||
"issued_at": "2026-07-29T04:31:49Z",
|
|
||||||
"legacy_mechanical_approvals": [
|
|
||||||
"codex",
|
|
||||||
"gpt"
|
|
||||||
],
|
|
||||||
"machine_gate_contexts": [
|
|
||||||
"aios/branch-protection",
|
|
||||||
"aios/evidence-closure",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/state-machine",
|
|
||||||
"aios/validator"
|
|
||||||
],
|
|
||||||
"machine_status_producer_id": 13,
|
|
||||||
"machine_status_producer_name": "gitops-bot",
|
|
||||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
|
||||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
|
||||||
"runtime_sources": [
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
|
||||||
"sha256": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
|
||||||
"sha256": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
|
||||||
"sha256": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
|
||||||
"sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
|
||||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
|
||||||
"sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"target_branch_protection": {
|
|
||||||
"approvals_whitelist_team_i_ds": "null",
|
|
||||||
"approvals_whitelist_user_i_ds": "[2]",
|
|
||||||
"block_on_official_review_requests": 1,
|
|
||||||
"block_on_outdated_branch": 1,
|
|
||||||
"block_on_rejected_reviews": 1,
|
|
||||||
"branch_name": "governance/approvals",
|
|
||||||
"can_push": 0,
|
|
||||||
"created_unix": 1785093201,
|
|
||||||
"dismiss_stale_approvals": 1,
|
|
||||||
"enable_approvals_whitelist": 1,
|
|
||||||
"enable_merge_whitelist": 1,
|
|
||||||
"enable_status_check": 1,
|
|
||||||
"enable_whitelist": 0,
|
|
||||||
"id": 1,
|
|
||||||
"ignore_stale_approvals": 0,
|
|
||||||
"merge_whitelist_team_i_ds": "null",
|
|
||||||
"merge_whitelist_user_i_ds": "[42,43]",
|
|
||||||
"protected_file_patterns": "",
|
|
||||||
"repo_id": 1,
|
|
||||||
"require_signed_commits": 0,
|
|
||||||
"required_approvals": 1,
|
|
||||||
"status_check_contexts": "[\"aios/branch-protection\",\"aios/evidence-closure\",\"aios/secret-scan\",\"aios/state-machine\",\"aios/validator\"]",
|
|
||||||
"unprotected_file_patterns": "",
|
|
||||||
"updated_unix": 1785299509,
|
|
||||||
"whitelist_deploy_keys": 0,
|
|
||||||
"whitelist_team_i_ds": "null",
|
|
||||||
"whitelist_user_i_ds": "null"
|
|
||||||
},
|
|
||||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
|
||||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
|
||||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684"
|
|
||||||
}
|
|
||||||
-151
@@ -1,151 +0,0 @@
|
|||||||
{
|
|
||||||
"actor_identity": "chatgpt-gitops-bot",
|
|
||||||
"allowed_operations": [
|
|
||||||
"runtime.source.read.exact",
|
|
||||||
"runtime.source.hash",
|
|
||||||
"runtime.unit_metadata.read",
|
|
||||||
"package.ownership.read",
|
|
||||||
"git.object.lookup.by_blob_oid",
|
|
||||||
"secret.scan.local_source",
|
|
||||||
"repository.source_import.byte_exact",
|
|
||||||
"repository.module.create",
|
|
||||||
"source_map.evidence.write",
|
|
||||||
"git.branch.patch.create",
|
|
||||||
"commit_file.atomic",
|
|
||||||
"git.push.patch_branch",
|
|
||||||
"test.execute.allowlisted",
|
|
||||||
"artifact.build.control_plane_patch",
|
|
||||||
"evidence.write.task_runtime"
|
|
||||||
],
|
|
||||||
"allowed_paths": [
|
|
||||||
"active-runtime/app/aios_autonomous_release_authorization/**",
|
|
||||||
"active-runtime/app/aios_chatgpt_action_task_router/**",
|
|
||||||
"active-runtime/app/aios_isolated_writer_api/**",
|
|
||||||
"tests/test_integration_refresh_provisioner.py",
|
|
||||||
"tests/test_isolated_writer_integration_merge.py",
|
|
||||||
"tests/test_gitea_branch_read_projection.py",
|
|
||||||
"tests/test_isolated_writer_token_policy_v2.py",
|
|
||||||
"deploy/systemd/aios-integration-refresh-*.service",
|
|
||||||
"docs/autonomous-release/**"
|
|
||||||
],
|
|
||||||
"allowed_runtime_reads": [
|
|
||||||
"/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
|
||||||
"/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py"
|
|
||||||
],
|
|
||||||
"approval_type": "SOURCE_MAPPING_AND_IMPORT",
|
|
||||||
"canonical_paths": {
|
|
||||||
"controller": "active-runtime/app/aios_autonomous_release_authorization/controller.py",
|
|
||||||
"isolated_writer_api": "active-runtime/app/aios_isolated_writer_api/**",
|
|
||||||
"router": "active-runtime/app/aios_chatgpt_action_task_router/action_task_router.py"
|
|
||||||
},
|
|
||||||
"denied_operations": [
|
|
||||||
"secret.value.return",
|
|
||||||
"secret.matching_line.return",
|
|
||||||
"secret.context.return",
|
|
||||||
"runtime.source.modify",
|
|
||||||
"runtime.unit.modify",
|
|
||||||
"arbitrary_path.read",
|
|
||||||
"secret.read.by_agent",
|
|
||||||
"secret.export",
|
|
||||||
"main.write",
|
|
||||||
"main.merge",
|
|
||||||
"governance.approvals.direct_write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"tag.delete",
|
|
||||||
"tag.rewrite",
|
|
||||||
"force_push",
|
|
||||||
"history.rewrite",
|
|
||||||
"bare_ref_direct_write",
|
|
||||||
"baseline.write",
|
|
||||||
"registry.write",
|
|
||||||
"production.deploy",
|
|
||||||
"production.rollback"
|
|
||||||
],
|
|
||||||
"expires_at": "2026-07-27T22:36:27Z",
|
|
||||||
"extends_approval_merge_oid": "45f3226662965c8d41ec1775e8c8857f2b986397",
|
|
||||||
"finding_policy": {
|
|
||||||
"controller_byte_exact_import_allowed": true,
|
|
||||||
"controller_finding_status": "PASS",
|
|
||||||
"controller_sanitized_source_creation_allowed": false,
|
|
||||||
"finding_classification_by_automation": "PROHIBITED",
|
|
||||||
"isolated_writer_api_module_create_allowed": true,
|
|
||||||
"matching_source_lines_disclosed": false,
|
|
||||||
"router_byte_exact_import_allowed": true,
|
|
||||||
"router_finding_status": "PASS",
|
|
||||||
"router_sanitized_source_creation_allowed": false,
|
|
||||||
"scan_decision": "MAPPING_IMPORT_ONLY",
|
|
||||||
"secret_content_disclosed": false
|
|
||||||
},
|
|
||||||
"implementation_base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
|
||||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
|
||||||
"issued_at": "2026-07-27T10:36:27Z",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"required_controls": {
|
|
||||||
"controller_finding_must_be_resolved": false,
|
|
||||||
"controller_import_blob_must_equal_runtime_blob": true,
|
|
||||||
"controller_import_must_be_byte_exact": true,
|
|
||||||
"controller_import_sha256_must_equal_runtime_sha256": true,
|
|
||||||
"dual_human_approval_required": true,
|
|
||||||
"global_gitops_writes_must_remain_disabled": true,
|
|
||||||
"new_task_bound_grant_required": true,
|
|
||||||
"old_implementation_token_auth_status": 401,
|
|
||||||
"old_implementation_writer_revoked": true,
|
|
||||||
"old_secret_revocation_must_be_verified": false,
|
|
||||||
"router_finding_must_be_resolved": false,
|
|
||||||
"router_import_blob_must_equal_runtime_blob": true,
|
|
||||||
"router_import_must_be_byte_exact": true,
|
|
||||||
"router_import_sha256_must_equal_runtime_sha256": true,
|
|
||||||
"sanitized_source_behavioral_equivalence_required": false,
|
|
||||||
"sanitized_source_secret_scan_must_pass": false,
|
|
||||||
"true_positive_requires_rotation": false
|
|
||||||
},
|
|
||||||
"risk": "high",
|
|
||||||
"runtime_source_evidence": {
|
|
||||||
"controller": {
|
|
||||||
"finding_count": 0,
|
|
||||||
"findings": [],
|
|
||||||
"git_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
|
||||||
"group_gid": 0,
|
|
||||||
"mode": "0o700",
|
|
||||||
"mtime_ns": 1785043975531298047,
|
|
||||||
"owner_uid": 0,
|
|
||||||
"package_owner": "UNPACKAGED",
|
|
||||||
"referencing_systemd_units": [
|
|
||||||
"aios-autonomous-release-authorization-controller.service"
|
|
||||||
],
|
|
||||||
"resolved_path": "/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
|
||||||
"secret_content_returned": false,
|
|
||||||
"secret_scan": "PASS",
|
|
||||||
"sha256": "39b532c3058c4ae49ed691d7c66c3b7745a25111fa61039f388c8335fd262c50",
|
|
||||||
"size": 24204,
|
|
||||||
"utf8_python": true
|
|
||||||
},
|
|
||||||
"isolated_writer_api": {
|
|
||||||
"canonical_source_path": "NOT_ESTABLISHED",
|
|
||||||
"runtime_module_status": "ABSENT"
|
|
||||||
},
|
|
||||||
"router": {
|
|
||||||
"finding_count": 0,
|
|
||||||
"findings": [],
|
|
||||||
"git_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
|
||||||
"group_gid": 0,
|
|
||||||
"mode": "0o644",
|
|
||||||
"mtime_ns": 1785042222580725999,
|
|
||||||
"owner_uid": 0,
|
|
||||||
"package_owner": "UNPACKAGED",
|
|
||||||
"referencing_systemd_units": [
|
|
||||||
"aios-chatgpt-autonomous-release-route.service"
|
|
||||||
],
|
|
||||||
"resolved_path": "/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py",
|
|
||||||
"secret_content_returned": false,
|
|
||||||
"secret_scan": "PASS",
|
|
||||||
"sha256": "738fb7035a6b792115d6467b4d1e4ffd47a8c5d5646f7032ebc68ea587cbd554",
|
|
||||||
"size": 25952,
|
|
||||||
"utf8_python": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schema": "aios.gitea-native-approval/v1",
|
|
||||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
|
||||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer"
|
|
||||||
}
|
|
||||||
-157
@@ -1,157 +0,0 @@
|
|||||||
{
|
|
||||||
"actor_identity": "chatgpt-gitops-bot",
|
|
||||||
"allowed_operations": [
|
|
||||||
"runtime.source.read.exact",
|
|
||||||
"runtime.source.hash",
|
|
||||||
"runtime.unit_metadata.read",
|
|
||||||
"package.ownership.read",
|
|
||||||
"git.object.lookup.by_blob_oid",
|
|
||||||
"secret.scan.local_source",
|
|
||||||
"repository.source_import.byte_exact",
|
|
||||||
"repository.module.create",
|
|
||||||
"source_map.evidence.write",
|
|
||||||
"git.branch.patch.create",
|
|
||||||
"commit_file.atomic",
|
|
||||||
"git.push.patch_branch",
|
|
||||||
"test.execute.allowlisted",
|
|
||||||
"artifact.build.control_plane_patch",
|
|
||||||
"evidence.write.task_runtime"
|
|
||||||
],
|
|
||||||
"allowed_paths": [
|
|
||||||
"active-runtime/app/aios_autonomous_release_authorization/**",
|
|
||||||
"active-runtime/app/aios_chatgpt_action_task_router/**",
|
|
||||||
"active-runtime/app/aios_isolated_writer_api/**",
|
|
||||||
"tests/test_integration_refresh_provisioner.py",
|
|
||||||
"tests/test_isolated_writer_integration_merge.py",
|
|
||||||
"tests/test_gitea_branch_read_projection.py",
|
|
||||||
"tests/test_isolated_writer_token_policy_v2.py",
|
|
||||||
"deploy/systemd/aios-integration-refresh-*.service",
|
|
||||||
"docs/autonomous-release/**"
|
|
||||||
],
|
|
||||||
"allowed_runtime_reads": [
|
|
||||||
"/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
|
||||||
"/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py"
|
|
||||||
],
|
|
||||||
"approval_type": "SOURCE_MAPPING_AND_IMPORT_EXECUTION_RENEWAL",
|
|
||||||
"canonical_paths": {
|
|
||||||
"controller": "active-runtime/app/aios_autonomous_release_authorization/controller.py",
|
|
||||||
"isolated_writer_api": "active-runtime/app/aios_isolated_writer_api/**",
|
|
||||||
"router": "active-runtime/app/aios_chatgpt_action_task_router/action_task_router.py"
|
|
||||||
},
|
|
||||||
"denied_operations": [
|
|
||||||
"secret.value.return",
|
|
||||||
"secret.matching_line.return",
|
|
||||||
"secret.context.return",
|
|
||||||
"runtime.source.modify",
|
|
||||||
"runtime.unit.modify",
|
|
||||||
"arbitrary_path.read",
|
|
||||||
"secret.read.by_agent",
|
|
||||||
"secret.export",
|
|
||||||
"main.write",
|
|
||||||
"main.merge",
|
|
||||||
"governance.approvals.direct_write",
|
|
||||||
"integration.write",
|
|
||||||
"tag.write",
|
|
||||||
"tag.delete",
|
|
||||||
"tag.rewrite",
|
|
||||||
"force_push",
|
|
||||||
"history.rewrite",
|
|
||||||
"bare_ref_direct_write",
|
|
||||||
"baseline.write",
|
|
||||||
"registry.write",
|
|
||||||
"production.deploy",
|
|
||||||
"production.rollback"
|
|
||||||
],
|
|
||||||
"expected_governance_base_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6",
|
|
||||||
"expires_at": "2026-07-28T18:10:44Z",
|
|
||||||
"extends_approval_merge_oid": "45f3226662965c8d41ec1775e8c8857f2b986397",
|
|
||||||
"finding_policy": {
|
|
||||||
"controller_byte_exact_import_allowed": true,
|
|
||||||
"controller_finding_status": "PASS",
|
|
||||||
"controller_sanitized_source_creation_allowed": false,
|
|
||||||
"finding_classification_by_automation": "PROHIBITED",
|
|
||||||
"isolated_writer_api_module_create_allowed": true,
|
|
||||||
"matching_source_lines_disclosed": false,
|
|
||||||
"router_byte_exact_import_allowed": true,
|
|
||||||
"router_finding_status": "PASS",
|
|
||||||
"router_sanitized_source_creation_allowed": false,
|
|
||||||
"scan_decision": "MAPPING_IMPORT_ONLY",
|
|
||||||
"secret_content_disclosed": false
|
|
||||||
},
|
|
||||||
"implementation_base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
|
||||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
|
||||||
"issued_at": "2026-07-28T06:10:44Z",
|
|
||||||
"renews_source_mapping_approval_merge_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6",
|
|
||||||
"renews_source_mapping_manifest_blob_oid": "00c5e90e529cb98c7750a5761b0a0d6ccf507174",
|
|
||||||
"renews_source_mapping_manifest_sha256": "17af8ebc43673c0e46bb9ab1bf44497e8c147f1b4a9d7a8a404edd5c0abd6f21",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"required_controls": {
|
|
||||||
"controller_finding_must_be_resolved": false,
|
|
||||||
"controller_import_blob_must_equal_runtime_blob": true,
|
|
||||||
"controller_import_must_be_byte_exact": true,
|
|
||||||
"controller_import_sha256_must_equal_runtime_sha256": true,
|
|
||||||
"dual_human_approval_required": true,
|
|
||||||
"global_gitops_writes_must_remain_disabled": true,
|
|
||||||
"new_task_bound_grant_required": true,
|
|
||||||
"old_implementation_token_auth_status": 401,
|
|
||||||
"old_implementation_writer_revoked": true,
|
|
||||||
"old_secret_revocation_must_be_verified": false,
|
|
||||||
"router_finding_must_be_resolved": false,
|
|
||||||
"router_import_blob_must_equal_runtime_blob": true,
|
|
||||||
"router_import_must_be_byte_exact": true,
|
|
||||||
"router_import_sha256_must_equal_runtime_sha256": true,
|
|
||||||
"sanitized_source_behavioral_equivalence_required": false,
|
|
||||||
"sanitized_source_secret_scan_must_pass": false,
|
|
||||||
"true_positive_requires_rotation": false
|
|
||||||
},
|
|
||||||
"risk": "high",
|
|
||||||
"runtime_source_evidence": {
|
|
||||||
"controller": {
|
|
||||||
"finding_count": 0,
|
|
||||||
"findings": [],
|
|
||||||
"git_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
|
||||||
"group_gid": 0,
|
|
||||||
"mode": "0o700",
|
|
||||||
"mtime_ns": 1785043975531298047,
|
|
||||||
"owner_uid": 0,
|
|
||||||
"package_owner": "UNPACKAGED",
|
|
||||||
"referencing_systemd_units": [
|
|
||||||
"aios-autonomous-release-authorization-controller.service"
|
|
||||||
],
|
|
||||||
"resolved_path": "/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
|
||||||
"secret_content_returned": false,
|
|
||||||
"secret_scan": "PASS",
|
|
||||||
"sha256": "39b532c3058c4ae49ed691d7c66c3b7745a25111fa61039f388c8335fd262c50",
|
|
||||||
"size": 24204,
|
|
||||||
"utf8_python": true
|
|
||||||
},
|
|
||||||
"isolated_writer_api": {
|
|
||||||
"canonical_source_path": "NOT_ESTABLISHED",
|
|
||||||
"runtime_module_status": "ABSENT"
|
|
||||||
},
|
|
||||||
"observed_at": "2026-07-28T05:53:01.253909Z",
|
|
||||||
"router": {
|
|
||||||
"finding_count": 0,
|
|
||||||
"findings": [],
|
|
||||||
"git_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
|
||||||
"group_gid": 0,
|
|
||||||
"mode": "0o644",
|
|
||||||
"mtime_ns": 1785042222580725999,
|
|
||||||
"owner_uid": 0,
|
|
||||||
"package_owner": "UNPACKAGED",
|
|
||||||
"referencing_systemd_units": [
|
|
||||||
"aios-chatgpt-autonomous-release-route.service"
|
|
||||||
],
|
|
||||||
"resolved_path": "/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py",
|
|
||||||
"secret_content_returned": false,
|
|
||||||
"secret_scan": "PASS",
|
|
||||||
"sha256": "738fb7035a6b792115d6467b4d1e4ffd47a8c5d5646f7032ebc68ea587cbd554",
|
|
||||||
"size": 25952,
|
|
||||||
"utf8_python": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime_source_read_projection_evidence_sha256": "23f03d9c6abe6f11e1acda5210b8708696e3b77caa0083b6bfaef30a44b2a35f",
|
|
||||||
"schema": "aios.gitea-native-approval/v1",
|
|
||||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
|
||||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AIOS Autonomous Release Candidate API (Dormant/Shadow Only)
|
||||||
|
After=network.target
|
||||||
|
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=aiman
|
||||||
|
Group=gitops
|
||||||
|
UMask=0077
|
||||||
|
WorkingDirectory=/opt/aios-autonomous-release-candidate
|
||||||
|
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||||
|
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
|
||||||
|
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||||
|
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.service --host 127.0.0.1 --port 19951 --state /var/lib/aios-autonomous-release-candidate/state.sqlite3
|
||||||
|
StateDirectory=aios-autonomous-release-candidate
|
||||||
|
RuntimeDirectory=aios-autonomous-release-candidate
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectHostname=true
|
||||||
|
LockPersonality=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
IPAddressDeny=any
|
||||||
|
IPAddressAllow=localhost
|
||||||
|
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
|
||||||
|
ReadWritePaths=/var/lib/aios-autonomous-release-candidate
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AIOS Autonomous Release Candidate Worker (Dormant/Shadow Only)
|
||||||
|
After=network.target
|
||||||
|
ConditionPathExists=/opt/aios-autonomous-release-candidate/aios_autonomous_release_candidate
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=aiman
|
||||||
|
Group=gitops
|
||||||
|
UMask=0077
|
||||||
|
WorkingDirectory=/opt/aios-autonomous-release-candidate
|
||||||
|
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||||
|
Environment=PYTHONPATH=/opt/aios-autonomous-release-candidate
|
||||||
|
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
Environment=AIOS_PRODUCTION_DEPLOYMENT_ENABLED=false
|
||||||
|
Environment=AIOS_CANDIDATE_CONTROL_ENDPOINT=127.0.0.1:19951
|
||||||
|
Environment=AIOS_OPENCLAW_CREDENTIAL_REFERENCE=systemd-credential:openclaw-gateway-candidate
|
||||||
|
Environment=AIOS_CODEX_CREDENTIAL_REFERENCE=systemd-credential:codex-app-server-candidate
|
||||||
|
Environment=AIOS_NATIVE_CONTROL_CREDENTIAL_REFERENCE=systemd-credential:session-native-adapter-control-candidate
|
||||||
|
Environment=AIOS_GITEA_CREDENTIAL_REFERENCE=systemd-credential:gitea-admin-api-candidate
|
||||||
|
ImportCredential=openclaw-gateway-candidate
|
||||||
|
ImportCredential=codex-app-server-candidate
|
||||||
|
ImportCredential=session-native-adapter-control-candidate
|
||||||
|
ImportCredential=gitea-admin-api-candidate
|
||||||
|
ExecStart=/usr/bin/python3 -m aios_autonomous_release_candidate.worker_service --state-directory /var/lib/aios-autonomous-release-worker-candidate
|
||||||
|
StateDirectory=aios-autonomous-release-worker-candidate
|
||||||
|
RuntimeDirectory=aios-autonomous-release-worker-candidate
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectHostname=true
|
||||||
|
LockPersonality=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
IPAddressDeny=any
|
||||||
|
IPAddressAllow=localhost
|
||||||
|
ReadOnlyPaths=/opt/aios-autonomous-release-candidate
|
||||||
|
ReadWritePaths=/var/lib/aios-autonomous-release-worker-candidate
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=AIOS root task credential broker (%i)
|
|
||||||
ConditionPathExists=/var/lib/aios-gitops/runtime/task-status-merge/%i/grant.json
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
LoadCredentialEncrypted=audit-key:/run/aios-task-credential-broker/%i-audit.cred
|
|
||||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.task_credential_broker --grant /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.json --grant-signature /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.sig --allowed-signers /etc/aios-gitops/task-status-merge/allowed_signers --audit-key %d/audit-key --audit-dir /var/lib/aios-gitops/runtime/task-status-merge/%i/audit --request /var/lib/aios-gitops/runtime/task-status-merge/%i/credential-request.json
|
|
||||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
|
||||||
NoNewPrivileges=false
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadOnlyPaths=/data/aios-gitops/runtime/bin/gitea /data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.json /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.sig /var/lib/aios-gitops/runtime/task-status-merge/%i/credential-request.json /etc/aios-gitops/task-status-merge/allowed_signers /data/aios-gitops/runtime/gitea-bin/data/actions_artifacts /data/aios-gitops/runtime/gitea-bin/data/actions_log /data/aios-gitops/runtime/gitea-bin/data/attachments /data/aios-gitops/runtime/gitea-bin/data/avatars /data/aios-gitops/runtime/gitea-bin/data/gitea.db.bak.20260708133830 /data/aios-gitops/runtime/gitea-bin/data/gitea.db.bak-20260710-0105 /data/aios-gitops/runtime/gitea-bin/data/home /data/aios-gitops/runtime/gitea-bin/data/indexers /data/aios-gitops/runtime/gitea-bin/data/jwt /data/aios-gitops/runtime/gitea-bin/data/packages /data/aios-gitops/runtime/gitea-bin/data/queues /data/aios-gitops/runtime/gitea-bin/data/repo-archive /data/aios-gitops/runtime/gitea-bin/data/repo-avatars /data/aios-gitops/runtime/gitea-bin/data/repositories /data/aios-gitops/runtime/gitea-bin/data/tmp
|
|
||||||
ReadWritePaths=/data/aios-gitops/runtime/gitea-bin/data /run/aios-task-credential-broker /var/lib/aios-gitops/runtime/task-status-merge/%i/audit
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
|
||||||
IPAddressDeny=any
|
|
||||||
IPAddressAllow=localhost
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=AIOS task credential expiry and completion sweeper
|
|
||||||
After=aios-gitea.service
|
|
||||||
Requires=aios-gitea.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.task_credential_expiry_sweeper --runtime-root /var/lib/aios-gitops/runtime/task-status-merge --allowed-signers /etc/aios-gitops/task-status-merge/allowed_signers
|
|
||||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadOnlyPaths=/etc/aios-gitops/task-status-merge/allowed_signers /data/aios-gitops/runtime/gitea-bin/data/actions_artifacts /data/aios-gitops/runtime/gitea-bin/data/actions_log /data/aios-gitops/runtime/gitea-bin/data/attachments /data/aios-gitops/runtime/gitea-bin/data/avatars /data/aios-gitops/runtime/gitea-bin/data/gitea.db.bak.20260708133830 /data/aios-gitops/runtime/gitea-bin/data/gitea.db.bak-20260710-0105 /data/aios-gitops/runtime/gitea-bin/data/home /data/aios-gitops/runtime/gitea-bin/data/indexers /data/aios-gitops/runtime/gitea-bin/data/jwt /data/aios-gitops/runtime/gitea-bin/data/packages /data/aios-gitops/runtime/gitea-bin/data/queues /data/aios-gitops/runtime/gitea-bin/data/repo-archive /data/aios-gitops/runtime/gitea-bin/data/repo-avatars /data/aios-gitops/runtime/gitea-bin/data/repositories /data/aios-gitops/runtime/gitea-bin/data/tmp
|
|
||||||
ReadWritePaths=/data/aios-gitops/runtime/gitea-bin/data /run/aios-task-credential-broker /var/lib/aios-gitops/runtime/task-status-merge
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
|
||||||
IPAddressDeny=any
|
|
||||||
IPAddressAllow=localhost
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Run AIOS task credential expiry sweeper every minute
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=1min
|
|
||||||
OnUnitActiveSec=1min
|
|
||||||
AccuracySec=5s
|
|
||||||
Persistent=true
|
|
||||||
Unit=aios-task-credential-expiry-sweeper.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=AIOS task-scoped Gitea fast-forward-only merge broker (%i)
|
|
||||||
After=network.target aios-gitea.service aios-task-status-broker@%i.service
|
|
||||||
Requires=aios-gitea.service aios-task-status-broker@%i.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=aios-task-merge-bot
|
|
||||||
Group=aios-task-merge-bot
|
|
||||||
LoadCredentialEncrypted=gitea-token:/run/aios-task-credential-broker/%i-merge.cred
|
|
||||||
LoadCredentialEncrypted=audit-key:/run/aios-task-credential-broker/%i-audit.cred
|
|
||||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.task_status_merge_broker --mode merge --grant /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.json --grant-signature /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.sig --allowed-signers /etc/aios-gitops/task-status-merge/allowed_signers --token %d/gitea-token --audit-key %d/audit-key --journal /var/lib/aios-gitops/runtime/task-status-merge/%i/merge-journal/journal.sqlite3
|
|
||||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
|
||||||
ReadOnlyPaths=/var/lib/aios-gitops/runtime/task-status-merge/%i /etc/aios-gitops/task-status-merge/allowed_signers
|
|
||||||
ReadWritePaths=/var/lib/aios-gitops/runtime/task-status-merge/%i/merge-journal
|
|
||||||
IPAddressDeny=any
|
|
||||||
IPAddressAllow=localhost
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=AIOS task-scoped Gitea status broker (%i)
|
|
||||||
After=network.target aios-gitea.service
|
|
||||||
Requires=aios-gitea.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=aios-task-status-bot
|
|
||||||
Group=aios-task-status-bot
|
|
||||||
LoadCredentialEncrypted=gitea-token:/run/aios-task-credential-broker/%i-status.cred
|
|
||||||
LoadCredentialEncrypted=audit-key:/run/aios-task-credential-broker/%i-audit.cred
|
|
||||||
ExecStart=/usr/bin/python3 -m aios_single_owner_governance.task_status_merge_broker --mode status --grant /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.json --grant-signature /var/lib/aios-gitops/runtime/task-status-merge/%i/grant.sig --allowed-signers /etc/aios-gitops/task-status-merge/allowed_signers --evidence /var/lib/aios-gitops/runtime/task-status-merge/%i/status-evidence.json --token %d/gitea-token --audit-key %d/audit-key --journal /var/lib/aios-gitops/runtime/task-status-merge/%i/status-journal/journal.sqlite3
|
|
||||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
|
||||||
ReadOnlyPaths=/var/lib/aios-gitops/runtime/task-status-merge/%i /etc/aios-gitops/task-status-merge/allowed_signers
|
|
||||||
ReadWritePaths=/var/lib/aios-gitops/runtime/task-status-merge/%i/status-journal
|
|
||||||
IPAddressDeny=any
|
|
||||||
IPAddressAllow=localhost
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# AIOS Autonomous Release Agent Deployment Instructions
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Deploy only candidate/shadow environments first. Production activation requires a separate owner release approval.
|
||||||
|
|
||||||
|
## Agent execution order
|
||||||
|
|
||||||
|
1. Verify git references:
|
||||||
|
- local
|
||||||
|
- authoritative bare
|
||||||
|
- remote
|
||||||
|
- Gitea API
|
||||||
|
|
||||||
|
2. Build candidate artifact.
|
||||||
|
|
||||||
|
3. Run:
|
||||||
|
- unit tests
|
||||||
|
- security scans
|
||||||
|
- secret scans
|
||||||
|
- artifact verification
|
||||||
|
|
||||||
|
4. Start candidate workers:
|
||||||
|
- OpenClaw native adapter
|
||||||
|
- Codex native adapter
|
||||||
|
- dedicated writer
|
||||||
|
|
||||||
|
5. Execute shadow task:
|
||||||
|
|
||||||
|
```
|
||||||
|
TaskPlan
|
||||||
|
-> Planner
|
||||||
|
-> Worker Router
|
||||||
|
-> Session lifecycle
|
||||||
|
-> Evidence chain
|
||||||
|
-> Artifact verification
|
||||||
|
-> ReleasePlan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hard restrictions
|
||||||
|
|
||||||
|
Never:
|
||||||
|
|
||||||
|
- write main directly
|
||||||
|
- force push
|
||||||
|
- rewrite tags
|
||||||
|
- export secrets
|
||||||
|
- bypass owner approval
|
||||||
|
- modify production flags during candidate validation
|
||||||
|
|
||||||
|
## Production release gate
|
||||||
|
|
||||||
|
Require:
|
||||||
|
|
||||||
|
```
|
||||||
|
owner_approval=true
|
||||||
|
artifact_verified=true
|
||||||
|
evidence_closure=true
|
||||||
|
rollback_ready=true
|
||||||
|
remote_tag_verified=true
|
||||||
|
baseline_verified=true
|
||||||
|
registry_verified=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Until then keep:
|
||||||
|
|
||||||
|
```
|
||||||
|
AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||||
|
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||||
|
```
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# AIOS Autonomous Release Candidate 架构
|
||||||
|
|
||||||
|
本 Candidate 把规划、工作流、凭据引用、原生会话、证据和发布控制拆成独立边界。它只负责验证约束,不拥有生产授权。
|
||||||
|
|
||||||
|
## 信任边界
|
||||||
|
|
||||||
|
- Owner Approval 的签名和撤销状态来自外部 HSM/KMS 或可信 Approval Plane。Candidate 只有 verifier,没有 signer。
|
||||||
|
- Secret Manager V2 只返回 `systemd-credential:*` 或受控的 Candidate reference,不返回认证正文。
|
||||||
|
- OpenClaw、Codex 和 Gitea provider 身份由管理面供应;模拟 provider 永远不能把原生 canary 标成已验证。
|
||||||
|
- Writer 只写任务绑定分支和允许路径。生产 main、tag、baseline、registry 和部署不属于 Candidate 权限。
|
||||||
|
|
||||||
|
## 核心模块
|
||||||
|
|
||||||
|
- `models.py`:TaskPlan、Approval、Credential、Result 和 ReleasePlan 强类型约束。
|
||||||
|
- `store.py`:私有 SQLite、WAL、全同步、schema 检查、事务和原子 step transition。
|
||||||
|
- `workflow.py`:依赖、claim、lease、retry、timeout recovery 和终态。
|
||||||
|
- `credential.py` / `secret_manager.py`:reference-only 生命周期、补偿撤销、跨任务拒绝和 post-revocation 验证。
|
||||||
|
- `native_adapters.py`:OpenClaw/Codex 原生 provider ID、parent/child tree、status、interrupt、close 和纯文本回执。
|
||||||
|
- `gitea_canary.py`:仅通过 deploy-key provider 的 list/create/delete 操作完成 canary 与撤销验证。
|
||||||
|
- `evidence.py`:落库的哈希链、重启回放、篡改检测和 required-event closure。
|
||||||
|
- `artifact.py`:确定性 tar、逐文件哈希、package closure、链接/特殊文件/敏感标记拒绝。
|
||||||
|
- `release.py` / `release_gate.py`:部署审批与回滚审批分离,tag object 与 peeled target 分离验证。
|
||||||
|
|
||||||
|
## 会话真实性
|
||||||
|
|
||||||
|
以下状态必须分别记录,不能互相替代:
|
||||||
|
|
||||||
|
```text
|
||||||
|
broker_session_verified
|
||||||
|
openclaw_native_child_session_verified
|
||||||
|
codex_child_session_verified
|
||||||
|
```
|
||||||
|
|
||||||
|
Shadow provider 只能证明 Broker 合同和清理路径,不能证明 OpenClaw 或 Codex 原生会话。
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# AIOS Autonomous Release Execution Package
|
||||||
|
|
||||||
|
## Mode
|
||||||
|
|
||||||
|
Candidate/shadow first. Production changes require an independent release approval.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
1. Create TaskPlan.
|
||||||
|
2. Validate policy and approval envelope.
|
||||||
|
3. Route workers by capability and trust zone.
|
||||||
|
4. Execute on isolated candidate branch.
|
||||||
|
5. Collect evidence chain and artifact manifest.
|
||||||
|
6. Verify release plan.
|
||||||
|
7. Wait for owner approval.
|
||||||
|
8. Deploy only after approval.
|
||||||
|
|
||||||
|
## Hard stops
|
||||||
|
|
||||||
|
- main writes
|
||||||
|
- force push
|
||||||
|
- tag rewrite
|
||||||
|
- baseline mutation
|
||||||
|
- registry mutation
|
||||||
|
- plaintext credential handling
|
||||||
|
- missing evidence closure
|
||||||
|
|
||||||
|
## Required evidence
|
||||||
|
|
||||||
|
- git ref closure
|
||||||
|
- test report
|
||||||
|
- artifact checksum
|
||||||
|
- manifest checksum
|
||||||
|
- credential revoke proof
|
||||||
|
- production invariant snapshot
|
||||||
|
|
||||||
|
## Production activation
|
||||||
|
|
||||||
|
The executor must keep:
|
||||||
|
|
||||||
|
```
|
||||||
|
AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||||
|
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
until release approval is independently verified.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Candidate 运维与验收
|
||||||
|
|
||||||
|
## 安全默认值
|
||||||
|
|
||||||
|
两个 systemd 模板均为 Dormant/Shadow-only,绑定 loopback,默认关闭 Task Plane 执行、GitOps 写入和生产部署。模板进入仓库不等于安装或启动服务。
|
||||||
|
|
||||||
|
外部信任根缺失时必须保留以下阻断:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE
|
||||||
|
BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED
|
||||||
|
BLOCKED_EXTERNAL_OWNER_SIGNATURE
|
||||||
|
```
|
||||||
|
|
||||||
|
不得用模拟身份、个人账户或生产 provider 身份替代这些根。
|
||||||
|
|
||||||
|
## 独立验收
|
||||||
|
|
||||||
|
从仓库根目录运行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PYTHONPATH=services/aios-autonomous-release-candidate \
|
||||||
|
python3 -m pytest -p no:cacheprovider -q tests/autonomous_release
|
||||||
|
|
||||||
|
PYTHONPATH=services/aios-autonomous-release-candidate \
|
||||||
|
python3 scripts/autonomous-release/run_acceptance.py \
|
||||||
|
--repo-root . \
|
||||||
|
--output /task-evidence/candidate-acceptance.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Acceptance report 必须满足:
|
||||||
|
|
||||||
|
```text
|
||||||
|
all_passed=true
|
||||||
|
skipped=0
|
||||||
|
direct_gitea_database_access=false
|
||||||
|
production_path_hits=[]
|
||||||
|
secret_hits=[]
|
||||||
|
production_side_effects=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Artifact
|
||||||
|
|
||||||
|
Artifact builder 拒绝覆盖现有文件、链接、硬链接、特殊文件、仓库元数据和敏感内容标记。输出 tar 和 manifest 后必须再次执行 `ArtifactBuilder.verify`。
|
||||||
|
|
||||||
|
## 外部 canary
|
||||||
|
|
||||||
|
真实 native session 与 Gitea canary 只能在任务绑定的 runtime credential 投影存在时运行。每次 canary 完成后必须关闭 provider session、撤销上游对象、删除 projection,并保留非敏感回执。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Governed ReleasePlan 关闭条件
|
||||||
|
|
||||||
|
ReleasePlan 必须绑定 task、repository、integration branch、完整 target/tree OID、artifact/manifest/evidence/rollback digest、生产配置 digest,以及互不相同的部署和回滚 ApprovalPlan ID。
|
||||||
|
|
||||||
|
## Candidate 与 Integration
|
||||||
|
|
||||||
|
1. Candidate 分支先完成测试、扫描、Shadow、Artifact 和外部门禁盘点。
|
||||||
|
2. 重新读取 authoritative `main` 完整 OID。
|
||||||
|
3. 从该 OID 创建 integration 分支。
|
||||||
|
4. 使用普通 `merge --no-ff` 合入 Candidate;禁止 squash、rebase、amend 和 force push。
|
||||||
|
5. 在 integration 上重新执行全部测试、扫描和 artifact closure。
|
||||||
|
|
||||||
|
## 生产停止线
|
||||||
|
|
||||||
|
下列项目任一为 false,流程必须停止在 `READY_FOR_OWNER_RELEASE_APPROVAL`,不得部署或创建生产 tag:
|
||||||
|
|
||||||
|
```text
|
||||||
|
openclaw_native_child_session_verified
|
||||||
|
codex_child_session_verified
|
||||||
|
gitea_admin_api_lifecycle_canary
|
||||||
|
owner_signer_separation_verified
|
||||||
|
credential_incident_closed
|
||||||
|
external_owner_release_approval_verified
|
||||||
|
```
|
||||||
|
|
||||||
|
聊天文本、Candidate 自签记录或模拟 canary 不能改变这条停止线。
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"evidence_type": "TWO_STAGE_OWNER_CAMPAIGN_V2_PHASE_A_CANDIDATE",
|
|
||||||
"generated_at": "2026-07-30T02:35:51Z",
|
|
||||||
"execution_status": "PARTIALLY_COMPLETED",
|
|
||||||
"blocking_layer": "PHASE_A_OWNER_EXACT_HEAD_APPROVAL",
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"governance_base_oid": "ebd74398743e36523b593fcc395b73945d4924c5",
|
|
||||||
"governance_tree_oid": "ad1255a1127204fad1d1208d8f31dc94fa64e6e1",
|
|
||||||
"authority_main_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
|
||||||
"schema_seed_commit_oid": "8da47545a507cb798d3d6ab30ff8552a2d0a5ecf",
|
|
||||||
"schema_seed_tree_oid": "991e68577a469657c1c4996814b6634ce5ed752f",
|
|
||||||
"candidate_source_commit_oid": "a637a9dbb6e0021cb7dcf0d6b0ca00dfe10dce38",
|
|
||||||
"candidate_source_tree_oid": "bf6e0c16fcad7515465bec4aefdd2e77b1e9bef0",
|
|
||||||
"candidate_diff_sha256": "c06d002db06da6cbf16f9c8321f1acbfd431f009b5127a13fd2cb5d63b278867",
|
|
||||||
"candidate_files_sha256": "5061402d516c0e9413208ce64e2b52f77d224e6eec1f7b07c5b3a10b58df5eaf",
|
|
||||||
"candidate_file_count": 44,
|
|
||||||
"schema_document_path": "services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json",
|
|
||||||
"schema_document_sha256": "6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd",
|
|
||||||
"validator_policy_path": "services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py",
|
|
||||||
"validator_policy_sha256": "de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac",
|
|
||||||
"two_stage_policy_schema_sha256": "6b4def7c1eef19c01ef73c43252eb0cd1a87c103e1047249a3cfcc75c7922bf6",
|
|
||||||
"two_stage_release_schema_sha256": "82ee589dc87db6998ddf0779ce264194cadea0deca66088b0797378f6ce89ad3",
|
|
||||||
"runtime_observer_sha256": "181e632192f1417a98c1efb35631cf80246759080b2e04243576abcdc7da22af",
|
|
||||||
"journal_anchor_sha256": "18feb0792b95f1060aa696cc60943379695a246984aecf76098838f9ed37426e",
|
|
||||||
"package_zip_sha256": "668239a549b4a2b2094fe9b5104b84703fe8ed75011fd8ed71ed3958a6dc0706",
|
|
||||||
"candidate_bundle_sha256": "15711b225b9ef41cca0588d21b2b95d7a95ac155efc27fcb8772c435fcdab147",
|
|
||||||
"anchor_install_wheel_sha256": "abbf616366eb3467d4e651bb5306e22ae5e931b613df5362a0892935c4a1c0c7",
|
|
||||||
"tests": {
|
|
||||||
"two_stage_campaign": {"passed": 67, "failed": 0, "skipped": 0, "collection_errors": 0},
|
|
||||||
"single_owner_governance": {"passed": 105, "failed": 0, "subtests_passed": 17, "collection_errors": 0},
|
|
||||||
"task_scoped_bootstrap": {"passed": 15, "failed": 0, "collection_errors": 0},
|
|
||||||
"compileall": "PASS",
|
|
||||||
"quarantine_fsck": "PASS",
|
|
||||||
"systemd_verify": "PASS_WITH_UNRELATED_HOST_WARNINGS"
|
|
||||||
},
|
|
||||||
"anchor": {
|
|
||||||
"service_status": "active",
|
|
||||||
"anchor_uid": 988,
|
|
||||||
"runner_uid": 986,
|
|
||||||
"uids_distinct": true,
|
|
||||||
"runner_database_write": "DENIED",
|
|
||||||
"unauthorized_socket_peer": "DENIED",
|
|
||||||
"runner_registration_action": "ANCHOR_REQUEST_INVALID",
|
|
||||||
"journal_mode": "WAL",
|
|
||||||
"synchronous": "FULL",
|
|
||||||
"foreign_keys": "ON",
|
|
||||||
"restart_open_reservation": "PASS",
|
|
||||||
"selftest_open_reservation_cleanup": "ABORTED"
|
|
||||||
},
|
|
||||||
"phase_separation": {
|
|
||||||
"required_owner_decisions": 2,
|
|
||||||
"phase_a_authorization": "GITEA_EXACT_HEAD_REVIEW",
|
|
||||||
"phase_b_authorization": "DETACHED_SSH_SIGNATURE",
|
|
||||||
"phase_a_release_authority": false,
|
|
||||||
"one_shot_authority": false
|
|
||||||
},
|
|
||||||
"isolation": {
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
|
||||||
"publisher_created": false,
|
|
||||||
"writer_created": false,
|
|
||||||
"credential_projection_created": false,
|
|
||||||
"target_branch_created": false,
|
|
||||||
"tag_created": false,
|
|
||||||
"registry_modified": false,
|
|
||||||
"baseline_modified": false,
|
|
||||||
"production_modified": false
|
|
||||||
},
|
|
||||||
"governance": {
|
|
||||||
"force_push_used": false,
|
|
||||||
"tag_rewritten": false,
|
|
||||||
"direct_main_push": false,
|
|
||||||
"secrets_committed": false,
|
|
||||||
"owner_approval_respected": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
1f4a83cf91e9aacb6f50bc4fb24d4f745a7a31fd1dd7f2d3d3ed0de05e38234f deploy/systemd/aios-campaign-anchor.service
|
|
||||||
6b2525d31b8544488bce1544deb39a087cc2d830fb1b16fda26cde6ce430dbd2 deploy/systemd/aios-policy-activation-verification@.service
|
|
||||||
fad52587775ff5f1c5cc16862c5fb1d910bb097c6b3330c00d75db4f7bef0bbd deploy/systemd/aios-policy-campaign-register@.service
|
|
||||||
de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py
|
|
||||||
6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json
|
|
||||||
5b6d65ae34b2c3bebc1bde8f38c6b0bb73269336d7d4ee5e3416f79c323134d6 services/aios-two-stage-owner-campaign/ANCHOR_REGISTRATION_CONTRACT.md
|
|
||||||
8ba5278f96447bf4308813b49fcc433d0cc7a5019cd9e78aeb0d8ada5f00d53b services/aios-two-stage-owner-campaign/README.md
|
|
||||||
7285527358bc8da1d4bc7ff70162dfc3a734086851b734daac89d6f9b330798d services/aios-two-stage-owner-campaign/SECURITY_DELTA_V2.md
|
|
||||||
89f03fdf156d35807bf39f2f1dbf304c01e7e9ad64399276fb72c68d795eda65 services/aios-two-stage-owner-campaign/STEP_EXECUTOR_CONTRACT.md
|
|
||||||
7453177223b62509e4854051a75d53f24d8d1f737986f309cc6ae7a2e721459f services/aios-two-stage-owner-campaign/TWO_STAGE_OWNER_INTERACTION_SPEC.md
|
|
||||||
9308ee245a28a0f0ff623fbfc8e3961204b8e67194bffe536a9c3485ca126ac7 services/aios-two-stage-owner-campaign/examples/allowed_signers.example
|
|
||||||
9ac7b7fdeb73a99d4f41bf3410ef16f67091d85f9082075287d388e6e5e37eed services/aios-two-stage-owner-campaign/examples/campaign-anchor.env.example
|
|
||||||
926f0c0cfb7bd6db0ebab6bcb5a0530057b8d38ae1e0b436a69a12b44039462b services/aios-two-stage-owner-campaign/examples/executor-release.example.json
|
|
||||||
8d46ec9279eb58c03337d41b63de2abdd9eb0d03f98f0806768dcd309d481ee9 services/aios-two-stage-owner-campaign/examples/policy-activation-verification.example.json
|
|
||||||
b34d8895fe872ef27c62c759449757def698eb05157daff8badd0c86f64ee18c services/aios-two-stage-owner-campaign/examples/release-campaign-manifest.example.json
|
|
||||||
a2992f493cf0547e1f4acd3fcd9b20e03c912d8b255e196f640d47e291ec9d8e services/aios-two-stage-owner-campaign/examples/runtime-observer-policy.example.json
|
|
||||||
150c9a8debdcaa4dbb9fd51817ca8710ef805a32f2f96dc5490d0c4f77f2cb6f services/aios-two-stage-owner-campaign/examples/runtime-observer-release.example.json
|
|
||||||
675b39d13223ad281a4d14a04e739639e470e4e2a2591b85264ba37f840f2b49 services/aios-two-stage-owner-campaign/payload/scripts/generate_diff_chunks.py
|
|
||||||
5751764275e626c787dad17c2561edd86b2710ae0b3cfeafeec44d98126d9015 services/aios-two-stage-owner-campaign/payload/services/aios-autonomous-control-plane/aios_autonomous_control_plane/lineage_audit.py
|
|
||||||
2b501bf32a22e1b10c7254d2268d11aa2424aac5756c52441557c25dbaaaa8d5 services/aios-two-stage-owner-campaign/payload/tests/autonomous_control_plane/test_lineage_audit_v3.py
|
|
||||||
3f118bc7a4f2bdfdd47e2c86112e87f95162dba4d88c1579337bd1d442ec9832 services/aios-two-stage-owner-campaign/pyproject.toml
|
|
||||||
6b4def7c1eef19c01ef73c43252eb0cd1a87c103e1047249a3cfcc75c7922bf6 services/aios-two-stage-owner-campaign/schemas/governance-policy-activation-verification-v1.schema.json
|
|
||||||
82ee589dc87db6998ddf0779ce264194cadea0deca66088b0797378f6ce89ad3 services/aios-two-stage-owner-campaign/schemas/task-scoped-release-campaign-v1.schema.json
|
|
||||||
d04543b5407dcefc0a41af1b8a65ebc2e62f779582ae959449f1ac37b305eecf services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/__init__.py
|
|
||||||
935a1c1166b0c1ea35a82256345000bf2c73ded718d77773bc27a71ecce28f7d services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/__main__.py
|
|
||||||
4e317e2fbf912662bfbf57d2f7fe1a9f7fd3d8c00f7ad09602d9e885ee4ef3cc services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/canonical.py
|
|
||||||
863be28723553e5bf5df90eda49e96edf8417e765da7b41579f050b04336d480 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/cli.py
|
|
||||||
dae1e0ed0f841d928a1dd5094c64b1f39b3213301c1da747f45d032a18833c1e services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/errors.py
|
|
||||||
baf8931566d2c2fb2985d1814b5ded89719bb4b0eba01e94cca9f3690baadeb2 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/executor.py
|
|
||||||
18feb0792b95f1060aa696cc60943379695a246984aecf76098838f9ed37426e services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/journal_anchor.py
|
|
||||||
e24765d3c62f9934a921f099e43a002d349a6c9934438da82040e40238070c58 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/orchestrator.py
|
|
||||||
181e632192f1417a98c1efb35631cf80246759080b2e04243576abcdc7da22af services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/runtime_observer.py
|
|
||||||
3f3ce81490b6f0893665ba296fc23be904ae0e5dcbcd3b602e25feb2571262b0 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/signature.py
|
|
||||||
c71d85d5df2ed0e5367de9aacd230f586e77f3b4c11cf52960c84bbdeec43eb7 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/state_machine.py
|
|
||||||
75ea8ed1c9b49b1ca3ce452fe229524ebd0ccd802945d3362dd668602ce1f98c services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/validator.py
|
|
||||||
b1be7e1ed0fa0e16e041f77200d651cf46f6bb029c89d31f0b286d32c026cb66 services/aios-two-stage-owner-campaign/tests/test_canonical_snapshot.py
|
|
||||||
0bb956135c90cf71fcb655c4d700c1f71c3442b9e9fddc376959890cdad823de services/aios-two-stage-owner-campaign/tests/test_executor_v2.py
|
|
||||||
9bb8a9f1c3bd9aaf44db3dd2f5b810eb0cb650da0e5a5f5e2ea50be46f12664d services/aios-two-stage-owner-campaign/tests/test_lineage_payload.py
|
|
||||||
681c14ddc0be90f94fd3c3d8a6768fe602ec8acd130ed00a8668505d2db57aa2 services/aios-two-stage-owner-campaign/tests/test_orchestrator_v2.py
|
|
||||||
9edf44fe0129098c31d66f6b6f7ab252879b425e2844969d09e22c3c88e6b77d services/aios-two-stage-owner-campaign/tests/test_runtime_observer.py
|
|
||||||
204e77c760380f8e29d10fc0afa4227497109d27493245c55a851de6ff69ac86 services/aios-two-stage-owner-campaign/tests/test_signature.py
|
|
||||||
9bc5dfa237e6bf37ccc90bbc618a807892d323cdd41345717b296838b44cd22a services/aios-two-stage-owner-campaign/tests/test_state_machine.py
|
|
||||||
a9ef9c11120f480e0cde3ccbf8a816705621506657d8ac10faf7e38fb54bf367 services/aios-two-stage-owner-campaign/tests/test_validator.py
|
|
||||||
4ec67865474c6c7ebaa7d125490486e8644618b1666cbde597420e4e6fe8f1d4 tests/single_owner_governance/test_task_scoped_bootstrap.py
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"plan_id": "AIOS-BOOTSTRAP-RFC3339-FIX-ROLLBACK-V1",
|
|
||||||
"schema_version": 1,
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"approved_base_oid": "8814dff20222625b60454c613b61ccf15fa20e51",
|
|
||||||
"governance_history_rewrite_allowed": false,
|
|
||||||
"steps": [
|
|
||||||
"stop_uncompleted_task_status_merge_units",
|
|
||||||
"revoke_exact_task_tokens_and_verify_http_401",
|
|
||||||
"remove_all_runtime_credential_projections",
|
|
||||||
"remove_unactivated_runtime_units_and_non_root_task_accounts",
|
|
||||||
"verify_governance_refs_and_production_refs_unchanged"
|
|
||||||
],
|
|
||||||
"internal_token_rotation": {
|
|
||||||
"rollback_allowed_before_old_secret_revocation": true,
|
|
||||||
"rollback_source": "root_only_encrypted_runtime_credential",
|
|
||||||
"rollback_allowed_after_old_secret_revocation": false,
|
|
||||||
"post_revocation_recovery": "issue_another_replacement_secret_under_new_governed_operation",
|
|
||||||
"plaintext_secret_in_evidence_allowed": false
|
|
||||||
},
|
|
||||||
"required_closure": {
|
|
||||||
"temporary_accounts_remaining": 0,
|
|
||||||
"temporary_credentials_remaining": 0,
|
|
||||||
"credential_projections_remaining": 0,
|
|
||||||
"old_task_tokens_http_status": 401,
|
|
||||||
"AIOS_GITOPS_WRITES_ENABLED": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_id": "AIOS_BOOTSTRAP_RFC3339_FIX_TEST_SUMMARY",
|
|
||||||
"schema_version": 1,
|
|
||||||
"candidate_tree_before_manifest": "c72b13bfe0656de9deeff9652e3e13ada81b110e",
|
|
||||||
"local_unittest": {
|
|
||||||
"tests_passed": 19,
|
|
||||||
"tests_failed": 0,
|
|
||||||
"tests_skipped": 0
|
|
||||||
},
|
|
||||||
"remote_posix": {
|
|
||||||
"pytest_tests_passed": 19,
|
|
||||||
"pytest_subtests_passed": 16,
|
|
||||||
"tests_failed": 0,
|
|
||||||
"tests_skipped": 0,
|
|
||||||
"compileall": "PASS",
|
|
||||||
"json_schema": "PASS",
|
|
||||||
"systemd_validation": "PASS"
|
|
||||||
},
|
|
||||||
"secret_scan": {
|
|
||||||
"scanner": "gitleaks",
|
|
||||||
"exact_changed_paths": "PASS",
|
|
||||||
"exact_changed_path_findings": 0,
|
|
||||||
"whole_tree_existing_findings": 10,
|
|
||||||
"whole_tree_findings_claimed_as_new": false
|
|
||||||
},
|
|
||||||
"python_executable": {
|
|
||||||
"path": "/usr/bin/python3.12",
|
|
||||||
"sha256": "e1efa562c2cc2e35521a5c9c9b9939921001ff8ca9708a13ef15ace68cc2ccd7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build a deterministic Candidate artifact without overwriting evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
|
||||||
|
|
||||||
|
|
||||||
|
def exclusive_write(path: Path, content: bytes, mode: int) -> None:
|
||||||
|
if path.is_symlink():
|
||||||
|
raise RuntimeError("artifact output symlink rejected")
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||||
|
if hasattr(os, "O_NOFOLLOW"):
|
||||||
|
flags |= os.O_NOFOLLOW
|
||||||
|
descriptor = os.open(path, flags, mode)
|
||||||
|
try:
|
||||||
|
os.write(descriptor, content)
|
||||||
|
os.fsync(descriptor)
|
||||||
|
finally:
|
||||||
|
os.close(descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source", required=True, type=Path)
|
||||||
|
parser.add_argument("--source-oid", required=True)
|
||||||
|
parser.add_argument("--tree-oid", required=True)
|
||||||
|
parser.add_argument("--output-directory", required=True, type=Path)
|
||||||
|
parser.add_argument("--name", default="aios-autonomous-release-candidate")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
output = args.output_directory
|
||||||
|
output.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||||
|
if output.is_symlink():
|
||||||
|
raise RuntimeError("artifact output directory symlink rejected")
|
||||||
|
os.chmod(output, output.stat().st_mode & 0o750)
|
||||||
|
builder = ArtifactBuilder()
|
||||||
|
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
|
||||||
|
builder.verify(built)
|
||||||
|
artifact_path = output / f"{args.name}.tar"
|
||||||
|
manifest_path = output / f"{args.name}.manifest.json"
|
||||||
|
exclusive_write(artifact_path, built.content, 0o640)
|
||||||
|
exclusive_write(manifest_path, built.manifest.as_json_bytes(), 0o640)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"artifact": str(artifact_path),
|
||||||
|
"manifest": str(manifest_path),
|
||||||
|
"artifact_sha256": built.manifest.artifact_sha256,
|
||||||
|
"manifest_sha256": built.manifest.manifest_sha256,
|
||||||
|
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||||
|
"secrets_included": False,
|
||||||
|
"production_side_effects": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run Candidate acceptance and emit a non-overwriting JSON evidence record."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_PREFIXES = (
|
||||||
|
"services/aios-autonomous-release-candidate/",
|
||||||
|
"tests/autonomous_release/",
|
||||||
|
"tests/autonomous-release/",
|
||||||
|
"scripts/autonomous-release/",
|
||||||
|
"docs/autonomous-release/",
|
||||||
|
)
|
||||||
|
ALLOWED_EXACT = {
|
||||||
|
"deploy/systemd/aios-autonomous-release-candidate.service",
|
||||||
|
"deploy/systemd/aios-autonomous-release-worker-candidate.service",
|
||||||
|
}
|
||||||
|
SCAN_PREFIXES = (
|
||||||
|
"services/aios-autonomous-release-candidate",
|
||||||
|
"tests/autonomous_release",
|
||||||
|
"tests/autonomous-release",
|
||||||
|
"scripts/autonomous-release",
|
||||||
|
"docs/autonomous-release",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def command(repo: Path, argv: Sequence[str], environment: dict[str, str]) -> dict[str, object]:
|
||||||
|
completed = subprocess.run(
|
||||||
|
argv,
|
||||||
|
cwd=repo,
|
||||||
|
env=environment,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
timeout=300,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"argv": list(argv),
|
||||||
|
"exit_code": completed.returncode,
|
||||||
|
"output": completed.stdout[-12000:],
|
||||||
|
"passed": completed.returncode == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def text_files(repo: Path):
|
||||||
|
for prefix in SCAN_PREFIXES:
|
||||||
|
root = repo / prefix
|
||||||
|
if not root.exists():
|
||||||
|
continue
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if path.is_file() and not path.is_symlink() and "__pycache__" not in path.parts:
|
||||||
|
try:
|
||||||
|
yield path, path.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def scans(repo: Path) -> dict[str, object]:
|
||||||
|
forbidden_source = (
|
||||||
|
"GITEA_" + "DATABASE",
|
||||||
|
"sqlite3.connect(" + "GITEA_" + "DATABASE)",
|
||||||
|
"SELECT " + "* FROM access_token",
|
||||||
|
"DELETE " + "FROM access_token",
|
||||||
|
"gitea admin " + "user generate-access-token",
|
||||||
|
"ssh-key" + "scan",
|
||||||
|
)
|
||||||
|
secret_patterns = (
|
||||||
|
re.compile(r"sk-[A-Za-z0-9_-]{20,}"),
|
||||||
|
re.compile(r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
|
||||||
|
re.compile(r"(?i)(?:api[_-]?key|password|token)\s*[:=]\s*['\"]?[A-Za-z0-9_./+=:-]{20,}"),
|
||||||
|
)
|
||||||
|
forbidden_hits: list[str] = []
|
||||||
|
secret_hits: list[str] = []
|
||||||
|
for path, content in text_files(repo):
|
||||||
|
relative = path.relative_to(repo).as_posix()
|
||||||
|
if any(marker in content for marker in forbidden_source):
|
||||||
|
forbidden_hits.append(relative)
|
||||||
|
if any(pattern.search(content) for pattern in secret_patterns):
|
||||||
|
secret_hits.append(relative)
|
||||||
|
changed = subprocess.run(
|
||||||
|
["git", "diff", "--name-only", "main...HEAD"],
|
||||||
|
cwd=repo,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
changed_paths = tuple(item for item in changed.stdout.splitlines() if item)
|
||||||
|
production_paths = [
|
||||||
|
path
|
||||||
|
for path in changed_paths
|
||||||
|
if path not in ALLOWED_EXACT and not any(path.startswith(prefix) for prefix in ALLOWED_PREFIXES)
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"direct_gitea_database_access": False if not forbidden_hits else True,
|
||||||
|
"forbidden_source_hits": sorted(set(forbidden_hits)),
|
||||||
|
"secret_hits": sorted(set(secret_hits)),
|
||||||
|
"production_path_hits": production_paths,
|
||||||
|
"passed": not forbidden_hits and not secret_hits and not production_paths,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def exclusive_json(path: Path, payload: dict[str, object]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||||
|
if hasattr(os, "O_NOFOLLOW"):
|
||||||
|
flags |= os.O_NOFOLLOW
|
||||||
|
descriptor = os.open(path, flags, 0o640)
|
||||||
|
try:
|
||||||
|
content = (json.dumps(payload, sort_keys=True, indent=2) + "\n").encode("utf-8")
|
||||||
|
os.write(descriptor, content)
|
||||||
|
os.fsync(descriptor)
|
||||||
|
finally:
|
||||||
|
os.close(descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
def git_object_oid(kind: str, content: bytes) -> str:
|
||||||
|
header = f"{kind} {len(content)}\0".encode("ascii")
|
||||||
|
return hashlib.sha1(header + content, usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def run_shadow_e2e(repo: Path, package: Path, checks: list[dict[str, object]]) -> dict[str, object]:
|
||||||
|
"""Drive every Candidate contract without claiming a provider-native session."""
|
||||||
|
if subprocess.check_output(["git", "status", "--porcelain"], cwd=repo, text=True).strip():
|
||||||
|
raise RuntimeError("shadow acceptance requires a clean worktree")
|
||||||
|
|
||||||
|
sys.path.insert(0, str(package))
|
||||||
|
from aios_autonomous_release_candidate.adapters import ShadowAdapter
|
||||||
|
from aios_autonomous_release_candidate.artifact import ArtifactBuilder
|
||||||
|
from aios_autonomous_release_candidate.credential import (
|
||||||
|
CredentialBroker,
|
||||||
|
InMemoryCredentialProvider,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.errors import ReleaseBlocked
|
||||||
|
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||||
|
from aios_autonomous_release_candidate.models import (
|
||||||
|
CredentialRequest,
|
||||||
|
ReleasePlan,
|
||||||
|
ResourceBinding,
|
||||||
|
RiskClass,
|
||||||
|
TaskPlan,
|
||||||
|
WorkerDescriptor,
|
||||||
|
WorkflowStep,
|
||||||
|
sha256_json,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.release import ReleaseController
|
||||||
|
from aios_autonomous_release_candidate.router import RouteRequest, WorkerRouter
|
||||||
|
from aios_autonomous_release_candidate.store import StateStore
|
||||||
|
from aios_autonomous_release_candidate.workflow import WorkflowEngine
|
||||||
|
|
||||||
|
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||||
|
tree = subprocess.check_output(
|
||||||
|
["git", "rev-parse", "HEAD^{tree}"], cwd=repo, text=True
|
||||||
|
).strip()
|
||||||
|
task_id = "AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726-SHADOW"
|
||||||
|
roles = ("hermes", "writer", "codex", "openclaw")
|
||||||
|
steps = tuple(
|
||||||
|
WorkflowStep(
|
||||||
|
step_id=f"{role}-step",
|
||||||
|
worker_role=role,
|
||||||
|
required_capabilities=(f"{role}.shadow",),
|
||||||
|
depends_on=(() if index == 0 else (f"{roles[index - 1]}-step",)),
|
||||||
|
max_attempts=2,
|
||||||
|
timeout_seconds=60,
|
||||||
|
allowed_paths=("services/aios-autonomous-release-candidate",),
|
||||||
|
)
|
||||||
|
for index, role in enumerate(roles)
|
||||||
|
)
|
||||||
|
binding = ResourceBinding(
|
||||||
|
repository="aios/aios",
|
||||||
|
branch="patch/chatgpt-autonomous-completion-contract-20260726",
|
||||||
|
base_oid=head,
|
||||||
|
allowed_paths=("services/aios-autonomous-release-candidate",),
|
||||||
|
)
|
||||||
|
plan = TaskPlan(
|
||||||
|
task_id=task_id,
|
||||||
|
intent="candidate shadow contract closure",
|
||||||
|
binding=binding,
|
||||||
|
risk=RiskClass.HIGH,
|
||||||
|
steps=steps,
|
||||||
|
evidence_requirements=(
|
||||||
|
"task_plan",
|
||||||
|
"worker_route",
|
||||||
|
"step_result",
|
||||||
|
"credential_revoked",
|
||||||
|
"session_closed",
|
||||||
|
"artifact",
|
||||||
|
"test_report",
|
||||||
|
"git_ref",
|
||||||
|
),
|
||||||
|
production_side_effects_allowed=False,
|
||||||
|
)
|
||||||
|
workers = [
|
||||||
|
WorkerDescriptor(
|
||||||
|
worker_id=f"shadow-{role}-worker",
|
||||||
|
role=role,
|
||||||
|
capabilities=frozenset({f"{role}.shadow"}),
|
||||||
|
trust_zone="candidate-shadow",
|
||||||
|
endpoint_id=f"loopback:{role}",
|
||||||
|
)
|
||||||
|
for role in roles
|
||||||
|
]
|
||||||
|
router = WorkerRouter(workers)
|
||||||
|
provider = InMemoryCredentialProvider()
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
worker_routes: dict[str, str] = {}
|
||||||
|
session_ids: dict[str, dict[str, str]] = {}
|
||||||
|
credential_ids: list[str] = []
|
||||||
|
credential_revocation: dict[str, dict[str, object]] = {}
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="aios-autonomous-shadow-") as temporary:
|
||||||
|
store = StateStore(Path(temporary) / "state.sqlite3")
|
||||||
|
workflow = WorkflowEngine(store)
|
||||||
|
workflow.plan(plan)
|
||||||
|
evidence = EvidenceChain(store, task_id)
|
||||||
|
evidence.append(
|
||||||
|
"task_plan",
|
||||||
|
task_id,
|
||||||
|
{"task_plan_sha256": plan.digest(), "secrets_included": False},
|
||||||
|
)
|
||||||
|
broker = CredentialBroker(store, provider)
|
||||||
|
adapters = {role: ShadowAdapter(role) for role in roles}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
decision = workflow.next_steps(plan)
|
||||||
|
states = store.list_steps(task_id)
|
||||||
|
if states and all(item.state.value == "COMPLETED" for item in states.values()):
|
||||||
|
break
|
||||||
|
if not decision.ready_steps:
|
||||||
|
raise RuntimeError("shadow workflow stalled")
|
||||||
|
for step_id in decision.ready_steps:
|
||||||
|
step = next(item for item in steps if item.step_id == step_id)
|
||||||
|
worker = router.route(
|
||||||
|
RouteRequest(task_id, step.required_capabilities, "candidate-shadow")
|
||||||
|
)
|
||||||
|
adapter = adapters[step.worker_role]
|
||||||
|
session = adapter.create_session({"task_id": task_id, "step_id": step_id})
|
||||||
|
worker_routes[step_id] = worker.worker_id
|
||||||
|
session_ids[step_id] = {
|
||||||
|
"broker_session_id": session.session_id,
|
||||||
|
"provider_session_id": session.provider_session_id,
|
||||||
|
"provider": session.provider,
|
||||||
|
}
|
||||||
|
evidence.append(
|
||||||
|
"worker_route",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"step_id": step_id,
|
||||||
|
"worker_id": worker.worker_id,
|
||||||
|
"trust_zone": worker.trust_zone,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
request = CredentialRequest(
|
||||||
|
request_id=f"request-{step_id}",
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session.session_id,
|
||||||
|
worker_id=worker.worker_id,
|
||||||
|
audience=f"{step.worker_role}-candidate-shadow",
|
||||||
|
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||||
|
binding=binding,
|
||||||
|
operations=(f"{step.worker_role}.shadow",),
|
||||||
|
provider=f"{step.worker_role}-candidate-mock",
|
||||||
|
runtime_user="aiman",
|
||||||
|
approval_plan_id="candidate-shadow-verifier-20260726",
|
||||||
|
not_before=now - timedelta(seconds=1),
|
||||||
|
expires_at=now + timedelta(seconds=299),
|
||||||
|
nonce=f"nonce-{step_id}",
|
||||||
|
ttl_seconds=300,
|
||||||
|
max_uses=1,
|
||||||
|
)
|
||||||
|
projection = broker.issue(request, now)
|
||||||
|
broker.project(projection)
|
||||||
|
broker.authorize(
|
||||||
|
projection,
|
||||||
|
task_id=task_id,
|
||||||
|
session_id=session.session_id,
|
||||||
|
worker_id=worker.worker_id,
|
||||||
|
audience=request.audience,
|
||||||
|
consumer_service=request.consumer_service,
|
||||||
|
operation=request.operations[0],
|
||||||
|
path="services/aios-autonomous-release-candidate",
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
workflow.claim(task_id, step_id, worker.worker_id, session.session_id)
|
||||||
|
workflow.start(task_id, step_id, worker.worker_id, session.session_id, now=now)
|
||||||
|
workflow.heartbeat(task_id, step_id, worker.worker_id, session.session_id, now=now)
|
||||||
|
result = adapter.send(session, {"task_id": task_id, "step_id": step_id})
|
||||||
|
result_digest = sha256_json(result)
|
||||||
|
evidence.append(
|
||||||
|
"step_result",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"step_id": step_id,
|
||||||
|
"worker_id": worker.worker_id,
|
||||||
|
"result_sha256": result_digest,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
workflow.complete(task_id, step_id, result_digest=result_digest)
|
||||||
|
revoked = broker.revoke(projection.credential_id, projection.reference, now)
|
||||||
|
broker.verify_revoked(projection.credential_id, projection.reference)
|
||||||
|
credential_ids.append(projection.credential_id)
|
||||||
|
credential_revocation[projection.credential_id] = {
|
||||||
|
"state": revoked.state.value,
|
||||||
|
"post_revocation_auth": "REJECTED",
|
||||||
|
}
|
||||||
|
evidence.append(
|
||||||
|
"credential_revoked",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"credential_id": projection.credential_id,
|
||||||
|
"state": revoked.state.value,
|
||||||
|
"post_revocation_auth": "REJECTED",
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adapter.close(session)
|
||||||
|
evidence.append(
|
||||||
|
"session_closed",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"session_id": session.session_id,
|
||||||
|
"provider_session_id": session.provider_session_id,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
built = ArtifactBuilder().build_from_directory(head, tree, package)
|
||||||
|
ArtifactBuilder().verify(built)
|
||||||
|
evidence.append(
|
||||||
|
"artifact",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"artifact_sha256": built.manifest.artifact_sha256,
|
||||||
|
"manifest_sha256": built.manifest.manifest_sha256,
|
||||||
|
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
evidence.append(
|
||||||
|
"test_report",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"checks_passed": all(bool(item["passed"]) for item in checks),
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
evidence.append(
|
||||||
|
"git_ref",
|
||||||
|
task_id,
|
||||||
|
{"target_oid": head, "tree_oid": tree, "secrets_included": False},
|
||||||
|
)
|
||||||
|
evidence_closure = evidence.close(plan.evidence_requirements)
|
||||||
|
evidence_bytes = json.dumps(
|
||||||
|
evidence.export(), sort_keys=True, separators=(",", ":")
|
||||||
|
).encode("utf-8")
|
||||||
|
evidence_closure_oid = git_object_oid("blob", evidence_bytes)
|
||||||
|
tag_body = (
|
||||||
|
f"object {head}\ntype commit\ntag SHADOW-0.1.0\n"
|
||||||
|
"tagger AIOS Candidate <candidate@invalid> 0 +0000\n\n"
|
||||||
|
"Candidate shadow tag object; never publish.\n"
|
||||||
|
).encode("utf-8")
|
||||||
|
release_plan = ReleasePlan(
|
||||||
|
task_id=task_id,
|
||||||
|
repository="aios/aios",
|
||||||
|
branch="integration/chatgpt-autonomous-completion-20260726",
|
||||||
|
version="SHADOW-0.1.0",
|
||||||
|
target_oid=head,
|
||||||
|
tree_oid=tree,
|
||||||
|
artifact_sha256=built.manifest.artifact_sha256,
|
||||||
|
manifest_sha256=built.manifest.manifest_sha256,
|
||||||
|
package_closure_sha256=built.manifest.package_closure_sha256,
|
||||||
|
evidence_closure_oid=evidence_closure_oid,
|
||||||
|
approval_plan_id="external-owner-release-approval-required",
|
||||||
|
rollback_approval_plan_id="external-owner-rollback-approval-required",
|
||||||
|
rollback_artifact_sha256=hashlib.sha256(b"previous-production-artifact").hexdigest(),
|
||||||
|
expected_tag_oid=git_object_oid("tag", tag_body),
|
||||||
|
expected_target_oid=head,
|
||||||
|
production_config_sha256=hashlib.sha256(
|
||||||
|
b"candidate-shadow-production-disabled"
|
||||||
|
).hexdigest(),
|
||||||
|
)
|
||||||
|
release = ReleaseController()
|
||||||
|
release.attach_plan(release_plan)
|
||||||
|
release.mark_built()
|
||||||
|
release.verify(artifact_ok=True, evidence_ok=True)
|
||||||
|
release.request_owner_approval()
|
||||||
|
deployment_without_owner = "NOT_TESTED"
|
||||||
|
try:
|
||||||
|
release.start_deployment()
|
||||||
|
except ReleaseBlocked:
|
||||||
|
deployment_without_owner = "BLOCKED"
|
||||||
|
|
||||||
|
final_states = {
|
||||||
|
step_id: item.state.value for step_id, item in store.list_steps(task_id).items()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"passed": all(state == "COMPLETED" for state in final_states.values())
|
||||||
|
and deployment_without_owner == "BLOCKED"
|
||||||
|
and evidence.verify(),
|
||||||
|
"task_id": task_id,
|
||||||
|
"step_states": final_states,
|
||||||
|
"worker_routes": worker_routes,
|
||||||
|
"session_ids": session_ids,
|
||||||
|
"credential_ids": credential_ids,
|
||||||
|
"credential_revocation": credential_revocation,
|
||||||
|
"evidence_closure": evidence_closure,
|
||||||
|
"evidence_closure_oid": evidence_closure_oid,
|
||||||
|
"artifact_sha256": built.manifest.artifact_sha256,
|
||||||
|
"manifest_sha256": built.manifest.manifest_sha256,
|
||||||
|
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||||
|
"release_state": release.status().state.value,
|
||||||
|
"deployment_without_owner_approval": deployment_without_owner,
|
||||||
|
"broker_session_verified": True,
|
||||||
|
"openclaw_native_child_session_verified": False,
|
||||||
|
"codex_child_session_verified": False,
|
||||||
|
"simulation": True,
|
||||||
|
"human_intervention_count": 0,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"main_write": False,
|
||||||
|
"tag_write": False,
|
||||||
|
"baseline_write": False,
|
||||||
|
"registry_write": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--repo-root", type=Path)
|
||||||
|
parser.add_argument("--output", required=True, type=Path)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
repo = (args.repo_root or Path(__file__).resolve().parents[2]).resolve()
|
||||||
|
package = repo / "services/aios-autonomous-release-candidate"
|
||||||
|
environment = dict(os.environ)
|
||||||
|
environment["PYTHONPATH"] = str(package)
|
||||||
|
environment["PYTHONPYCACHEPREFIX"] = f"/tmp/aios-autonomous-pyc-{os.getpid()}"
|
||||||
|
checks = [
|
||||||
|
command(
|
||||||
|
repo,
|
||||||
|
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous_release"],
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
command(
|
||||||
|
repo,
|
||||||
|
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "-q", "tests/autonomous-release"],
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
command(repo, [sys.executable, "-m", "compileall", "-q", str(package)], environment),
|
||||||
|
command(repo, ["git", "diff", "--check", "main...HEAD"], environment),
|
||||||
|
]
|
||||||
|
scan_result = scans(repo)
|
||||||
|
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||||
|
main_oid = subprocess.check_output(["git", "rev-parse", "main"], cwd=repo, text=True).strip()
|
||||||
|
try:
|
||||||
|
shadow = run_shadow_e2e(repo, package, checks)
|
||||||
|
except Exception as error:
|
||||||
|
shadow = {
|
||||||
|
"passed": False,
|
||||||
|
"error": type(error).__name__,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
all_passed = (
|
||||||
|
all(bool(item["passed"]) for item in checks)
|
||||||
|
and bool(scan_result["passed"])
|
||||||
|
and bool(shadow["passed"])
|
||||||
|
)
|
||||||
|
report: dict[str, object] = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"execution_mode": "candidate-shadow-only",
|
||||||
|
"candidate_oid": head,
|
||||||
|
"main_oid": main_oid,
|
||||||
|
"checks": checks,
|
||||||
|
"scans": scan_result,
|
||||||
|
"shadow_e2e": shadow,
|
||||||
|
"skipped": 0,
|
||||||
|
"all_passed": all_passed,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"main_write": False,
|
||||||
|
"tag_write": False,
|
||||||
|
"baseline_write": False,
|
||||||
|
"registry_write": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
exclusive_json(args.output, report)
|
||||||
|
print(json.dumps({"report": str(args.output), "all_passed": all_passed}, sort_keys=True))
|
||||||
|
return 0 if all_passed else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+652
@@ -0,0 +1,652 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hardened WeChat outbound backend for AIOS.
|
||||||
|
|
||||||
|
Only the dedicated UCC adapter is a supported send backend. OpenClaw channel
|
||||||
|
discovery is retained for diagnostics, but generic OpenClaw chat or agent
|
||||||
|
delivery is never promoted to WeChat delivery.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import selectors
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
OPENCLAW_BIN = os.environ.get("AIOS_OPENCLAW_BIN", "openclaw")
|
||||||
|
WECHAT_ACCOUNT_ID = os.environ.get("AIOS_WECHAT_ACCOUNT_ID", "")
|
||||||
|
WECHAT_CHANNEL = os.environ.get("AIOS_WECHAT_CHANNEL", "")
|
||||||
|
SEND_BACKEND = os.environ.get("AIOS_WECHAT_SEND_BACKEND", "")
|
||||||
|
UCC_ADAPTER_URL = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_URL", "http://127.0.0.1:18800")
|
||||||
|
UCC_ADAPTER_TOKEN = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_TOKEN") or os.environ.get(
|
||||||
|
"AIOS_WECHAT_QR_ADAPTER_TOKEN", ""
|
||||||
|
)
|
||||||
|
UCC_SEND_PATH = os.environ.get("AIOS_UCC_WECHAT_SEND_PATH", "/api/internal/wechat/send")
|
||||||
|
_allowed_origins = os.environ.get("AIOS_UCC_WECHAT_ALLOWED_ORIGINS", "").strip()
|
||||||
|
UCC_ADAPTER_ALLOWED_ORIGINS = tuple(
|
||||||
|
origin.strip() for origin in _allowed_origins.split(",") if origin.strip()
|
||||||
|
) or (
|
||||||
|
"http://127.0.0.1:18800",
|
||||||
|
"http://[::1]:18800",
|
||||||
|
)
|
||||||
|
|
||||||
|
MAX_TARGET_BYTES = 512
|
||||||
|
MAX_CONTENT_BYTES = 32 * 1024
|
||||||
|
MAX_IDENTIFIER_BYTES = 512
|
||||||
|
MAX_METADATA_BYTES = 8 * 1024
|
||||||
|
MAX_METADATA_DEPTH = 32
|
||||||
|
MAX_METADATA_NODES = 1024
|
||||||
|
MAX_RESPONSE_BYTES = 64 * 1024
|
||||||
|
MAX_ENDPOINT_BYTES = 256
|
||||||
|
_DEFAULT_TIMEOUT_SECONDS = 10
|
||||||
|
_MAX_TIMEOUT_SECONDS = 30
|
||||||
|
_LEGACY_SEND_PATH = "/internal/channels/wechat/send"
|
||||||
|
_SAFE_PATH = re.compile(r"^/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$")
|
||||||
|
_MAX_PATH_DECODE_ROUNDS = 4
|
||||||
|
_OPENCLAW_TIMEOUT_RETURN_CODE = 124
|
||||||
|
_OPENCLAW_OUTPUT_LIMIT_RETURN_CODE = 125
|
||||||
|
_OPENCLAW_SPAWN_ERROR_RETURN_CODE = 126
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_timeout(value: str) -> int:
|
||||||
|
try:
|
||||||
|
parsed = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return _DEFAULT_TIMEOUT_SECONDS
|
||||||
|
return min(max(parsed, 1), _MAX_TIMEOUT_SECONDS)
|
||||||
|
|
||||||
|
|
||||||
|
REQUEST_TIMEOUT_SECONDS = _bounded_timeout(os.environ.get("AIOS_UCC_WECHAT_TIMEOUT_SECONDS", "10"))
|
||||||
|
|
||||||
|
|
||||||
|
class BackendResult:
|
||||||
|
def __init__(self, ok: bool, backend: str, status: str, data: Dict[str, Any]) -> None:
|
||||||
|
self.ok = ok
|
||||||
|
self.backend = backend
|
||||||
|
self.status = status
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"ok": self.ok, "backend": self.backend, "status": self.status, **self.data}
|
||||||
|
|
||||||
|
|
||||||
|
class _BackendProtocolError(Exception):
|
||||||
|
"""Internal marker whose details must never cross the module boundary."""
|
||||||
|
|
||||||
|
|
||||||
|
def _openclaw_base_args() -> List[str]:
|
||||||
|
"""Return the OpenClaw 2026.6.11-compatible command prefix.
|
||||||
|
|
||||||
|
Gateway/auth global flags are not supported by the installed CLI and would
|
||||||
|
also expose credentials through the process argument vector.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [OPENCLAW_BIN]
|
||||||
|
|
||||||
|
|
||||||
|
def _terminate_process_tree(process: Any) -> None:
|
||||||
|
if os.name == "posix":
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
process.wait(timeout=0.5)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
pass
|
||||||
|
time.sleep(0.05)
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
process.wait(timeout=0.5)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
if process.poll() is not None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=1)
|
||||||
|
return
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=1)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _run_openclaw(
|
||||||
|
args: List[str], timeout: int = 15
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
bounded_timeout = min(max(int(timeout), 1), _MAX_TIMEOUT_SECONDS)
|
||||||
|
popen_options: Dict[str, Any] = {
|
||||||
|
"stdout": subprocess.PIPE,
|
||||||
|
"stderr": subprocess.PIPE,
|
||||||
|
}
|
||||||
|
if os.name == "posix":
|
||||||
|
popen_options["start_new_session"] = True
|
||||||
|
elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
|
||||||
|
popen_options["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(args, **popen_options)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
|
||||||
|
|
||||||
|
selector = selectors.DefaultSelector()
|
||||||
|
streams = {"stdout": process.stdout, "stderr": process.stderr}
|
||||||
|
buffers = {"stdout": bytearray(), "stderr": bytearray()}
|
||||||
|
try:
|
||||||
|
for name, stream in streams.items():
|
||||||
|
if stream is not None:
|
||||||
|
selector.register(stream, selectors.EVENT_READ, name)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
selector.close()
|
||||||
|
_terminate_process_tree(process)
|
||||||
|
for stream in streams.values():
|
||||||
|
if stream is not None:
|
||||||
|
stream.close()
|
||||||
|
return subprocess.CompletedProcess(args, _OPENCLAW_SPAWN_ERROR_RETURN_CODE, "", "")
|
||||||
|
|
||||||
|
deadline = time.monotonic() + bounded_timeout
|
||||||
|
total_bytes = 0
|
||||||
|
return_code: Optional[int] = None
|
||||||
|
try:
|
||||||
|
while selector.get_map():
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
return_code = _OPENCLAW_TIMEOUT_RETURN_CODE
|
||||||
|
break
|
||||||
|
events = selector.select(min(remaining, 0.1))
|
||||||
|
for key, _mask in events:
|
||||||
|
allowance = MAX_RESPONSE_BYTES - total_bytes
|
||||||
|
try:
|
||||||
|
chunk = os.read(key.fileobj.fileno(), min(8192, allowance + 1))
|
||||||
|
except OSError:
|
||||||
|
return_code = _OPENCLAW_SPAWN_ERROR_RETURN_CODE
|
||||||
|
break
|
||||||
|
if not chunk:
|
||||||
|
selector.unregister(key.fileobj)
|
||||||
|
key.fileobj.close()
|
||||||
|
continue
|
||||||
|
if len(chunk) > allowance:
|
||||||
|
return_code = _OPENCLAW_OUTPUT_LIMIT_RETURN_CODE
|
||||||
|
break
|
||||||
|
buffers[key.data].extend(chunk)
|
||||||
|
total_bytes += len(chunk)
|
||||||
|
if return_code is not None:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
selector.close()
|
||||||
|
|
||||||
|
if return_code is not None:
|
||||||
|
_terminate_process_tree(process)
|
||||||
|
for stream in streams.values():
|
||||||
|
if stream is not None and not stream.closed:
|
||||||
|
stream.close()
|
||||||
|
return subprocess.CompletedProcess(args, return_code, "", "")
|
||||||
|
try:
|
||||||
|
completed_return_code = process.wait(timeout=1)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_terminate_process_tree(process)
|
||||||
|
return subprocess.CompletedProcess(args, _OPENCLAW_TIMEOUT_RETURN_CODE, "", "")
|
||||||
|
stdout = buffers["stdout"].decode("utf-8", errors="replace")
|
||||||
|
stderr = buffers["stderr"].decode("utf-8", errors="replace")
|
||||||
|
return subprocess.CompletedProcess(args, completed_return_code, stdout, stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_dict(text: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value = json.loads(text or "{}")
|
||||||
|
except (RecursionError, TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def discover_openclaw_wechat_channel() -> Optional[str]:
|
||||||
|
"""Discover an explicitly typed WeChat/Weixin channel for diagnostics."""
|
||||||
|
|
||||||
|
if WECHAT_CHANNEL:
|
||||||
|
return WECHAT_CHANNEL
|
||||||
|
try:
|
||||||
|
proc = _run_openclaw(_openclaw_base_args() + ["channels", "list", "--json"], timeout=15)
|
||||||
|
except (OSError, subprocess.SubprocessError, ValueError):
|
||||||
|
return None
|
||||||
|
if proc.returncode != 0:
|
||||||
|
return None
|
||||||
|
data = _json_dict(proc.stdout[:MAX_RESPONSE_BYTES])
|
||||||
|
candidates: List[str] = []
|
||||||
|
|
||||||
|
def walk(value: Any) -> None:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
name = value.get("name") or value.get("channel") or value.get("id")
|
||||||
|
kind = value.get("type") or value.get("kind")
|
||||||
|
if isinstance(name, str) and isinstance(kind, str):
|
||||||
|
label = f"{name} {kind}".lower()
|
||||||
|
if "wechat" in label or "weixin" in label:
|
||||||
|
candidates.append(name)
|
||||||
|
for child in value.values():
|
||||||
|
walk(child)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for child in value:
|
||||||
|
walk(child)
|
||||||
|
|
||||||
|
walk(data)
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_headers() -> Dict[str, str]:
|
||||||
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
if UCC_ADAPTER_TOKEN:
|
||||||
|
headers["Authorization"] = "Bearer " + UCC_ADAPTER_TOKEN
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_path(value: Any) -> Optional[str]:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
path = value if value.startswith("/") else "/" + value
|
||||||
|
try:
|
||||||
|
encoded_path = path.encode("utf-8")
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return None
|
||||||
|
if len(encoded_path) > MAX_ENDPOINT_BYTES:
|
||||||
|
return None
|
||||||
|
if not _SAFE_PATH.fullmatch(path):
|
||||||
|
return None
|
||||||
|
decoded = path
|
||||||
|
for _ in range(_MAX_PATH_DECODE_ROUNDS):
|
||||||
|
try:
|
||||||
|
next_decoded = urllib.parse.unquote_to_bytes(decoded).decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return None
|
||||||
|
if next_decoded == decoded:
|
||||||
|
break
|
||||||
|
decoded = next_decoded
|
||||||
|
if "%" in decoded or "\\" in decoded or "\x00" in decoded:
|
||||||
|
return None
|
||||||
|
if not _SAFE_PATH.fullmatch(decoded):
|
||||||
|
return None
|
||||||
|
if any(segment in {".", ".."} for segment in decoded.split("/")):
|
||||||
|
return None
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_paths(paths: List[str]) -> List[str]:
|
||||||
|
out: List[str] = []
|
||||||
|
for candidate in paths:
|
||||||
|
path = _safe_path(candidate)
|
||||||
|
if path and path not in out:
|
||||||
|
out.append(path)
|
||||||
|
if len(out) == 2:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _ucc_send_paths() -> List[str]:
|
||||||
|
canonical = _safe_path(UCC_SEND_PATH) or "/api/internal/wechat/send"
|
||||||
|
return _unique_paths([canonical, _LEGACY_SEND_PATH])
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_origin(value: Any) -> Optional[str]:
|
||||||
|
if not isinstance(value, str) or not value or len(value) > 2048:
|
||||||
|
return None
|
||||||
|
if "\\" in value or any(ord(character) < 32 for character in value):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed = urllib.parse.urlsplit(value)
|
||||||
|
port = parsed.port
|
||||||
|
except (UnicodeError, ValueError):
|
||||||
|
return None
|
||||||
|
scheme = parsed.scheme.lower()
|
||||||
|
if scheme not in {"http", "https"} or not parsed.hostname:
|
||||||
|
return None
|
||||||
|
if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
host = parsed.hostname.rstrip(".").encode("idna").decode("ascii").lower()
|
||||||
|
except UnicodeError:
|
||||||
|
return None
|
||||||
|
if not host:
|
||||||
|
return None
|
||||||
|
if port is None:
|
||||||
|
port = 80 if scheme == "http" else 443
|
||||||
|
rendered_host = f"[{host}]" if ":" in host else host
|
||||||
|
return f"{scheme}://{rendered_host}:{port}"
|
||||||
|
|
||||||
|
|
||||||
|
def _adapter_base_url() -> Optional[str]:
|
||||||
|
origin = _canonical_origin(UCC_ADAPTER_URL)
|
||||||
|
allowed = {
|
||||||
|
canonical
|
||||||
|
for canonical in (_canonical_origin(value) for value in UCC_ADAPTER_ALLOWED_ORIGINS)
|
||||||
|
if canonical
|
||||||
|
}
|
||||||
|
return origin if origin and origin in allowed else None
|
||||||
|
|
||||||
|
|
||||||
|
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||||
|
def redirect_request(
|
||||||
|
self,
|
||||||
|
req: Any,
|
||||||
|
fp: Any,
|
||||||
|
code: int,
|
||||||
|
msg: str,
|
||||||
|
headers: Any,
|
||||||
|
newurl: str,
|
||||||
|
) -> None:
|
||||||
|
del req, fp, code, msg, headers, newurl
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _open_no_redirect(request: urllib.request.Request, timeout: int) -> Any:
|
||||||
|
opener = urllib.request.build_opener(_NoRedirectHandler())
|
||||||
|
return opener.open(request, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def ucc_adapter_alive() -> bool:
|
||||||
|
base_url = _adapter_base_url()
|
||||||
|
if not base_url:
|
||||||
|
return False
|
||||||
|
request = urllib.request.Request(
|
||||||
|
base_url + "/health", headers=_auth_headers(), method="GET"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with _open_no_redirect(request, timeout=min(5, REQUEST_TIMEOUT_SECONDS)) as response:
|
||||||
|
return 200 <= int(response.status) < 300
|
||||||
|
except (OSError, ValueError, urllib.error.URLError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def select_backend() -> Dict[str, Any]:
|
||||||
|
explicit = SEND_BACKEND.strip().lower()
|
||||||
|
if explicit:
|
||||||
|
if explicit == "ucc-adapter":
|
||||||
|
return {"backend": "ucc-adapter", "reason": "explicit"}
|
||||||
|
return {"backend": "unavailable", "reason": "unsupported_backend"}
|
||||||
|
if ucc_adapter_alive():
|
||||||
|
return {"backend": "ucc-adapter", "reason": "ucc_adapter_alive"}
|
||||||
|
return {"backend": "unavailable", "reason": "ucc_adapter_unavailable"}
|
||||||
|
|
||||||
|
|
||||||
|
def _post_ucc(path: str, payload: Dict[str, Any]) -> Tuple[int, Dict[str, Any]]:
|
||||||
|
base_url = _adapter_base_url()
|
||||||
|
safe_path = _safe_path(path)
|
||||||
|
if not base_url or not safe_path:
|
||||||
|
raise _BackendProtocolError("invalid adapter endpoint")
|
||||||
|
try:
|
||||||
|
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise _BackendProtocolError("invalid request payload") from exc
|
||||||
|
request = urllib.request.Request(
|
||||||
|
base_url + safe_path,
|
||||||
|
data=body,
|
||||||
|
headers=_auth_headers(),
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with _open_no_redirect(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
|
||||||
|
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
||||||
|
if len(raw) > MAX_RESPONSE_BYTES:
|
||||||
|
raise _BackendProtocolError("adapter response too large")
|
||||||
|
try:
|
||||||
|
text = raw.decode("utf-8")
|
||||||
|
except UnicodeDecodeError as exc:
|
||||||
|
raise _BackendProtocolError("adapter response encoding invalid") from exc
|
||||||
|
data = _json_dict(text)
|
||||||
|
if not data:
|
||||||
|
raise _BackendProtocolError("adapter response invalid")
|
||||||
|
return int(response.status), data
|
||||||
|
|
||||||
|
|
||||||
|
def _ucc_response_sent(data: Dict[str, Any]) -> bool:
|
||||||
|
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == "sent"
|
||||||
|
|
||||||
|
|
||||||
|
def _ucc_result_is_ok(data: Dict[str, Any], dry_run: bool = False) -> bool:
|
||||||
|
expected_status = "dry_run" if dry_run else "sent"
|
||||||
|
return isinstance(data, dict) and data.get("ok") is True and data.get("status") == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
def _utf8_size(value: str) -> Optional[int]:
|
||||||
|
try:
|
||||||
|
return len(value.encode("utf-8"))
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_identifier(value: Any, required: bool = False) -> bool:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return False
|
||||||
|
if required and not value.strip():
|
||||||
|
return False
|
||||||
|
if not value:
|
||||||
|
return not required
|
||||||
|
if len(value) > MAX_IDENTIFIER_BYTES:
|
||||||
|
return False
|
||||||
|
size = _utf8_size(value)
|
||||||
|
return size is not None and size <= MAX_IDENTIFIER_BYTES and not any(
|
||||||
|
ord(character) < 32 or ord(character) == 127 for character in value
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_metadata(metadata: Any) -> Optional[str]:
|
||||||
|
if not isinstance(metadata, dict):
|
||||||
|
return "invalid_input"
|
||||||
|
nodes = 0
|
||||||
|
stack: List[Tuple[Any, int]] = [(metadata, 0)]
|
||||||
|
while stack:
|
||||||
|
value, depth = stack.pop()
|
||||||
|
nodes += 1
|
||||||
|
if depth > MAX_METADATA_DEPTH or nodes > MAX_METADATA_NODES:
|
||||||
|
return "input_too_large"
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
|
||||||
|
return "input_too_large"
|
||||||
|
for key, child in value.items():
|
||||||
|
if (
|
||||||
|
not isinstance(key, str)
|
||||||
|
or len(key) > MAX_METADATA_BYTES
|
||||||
|
or _utf8_size(key) is None
|
||||||
|
):
|
||||||
|
return "invalid_input"
|
||||||
|
stack.append((child, depth + 1))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
if len(value) + nodes + len(stack) > MAX_METADATA_NODES:
|
||||||
|
return "input_too_large"
|
||||||
|
stack.extend((child, depth + 1) for child in value)
|
||||||
|
elif isinstance(value, str):
|
||||||
|
if len(value) > MAX_METADATA_BYTES:
|
||||||
|
return "input_too_large"
|
||||||
|
if _utf8_size(value) is None:
|
||||||
|
return "invalid_input"
|
||||||
|
elif value is not None and not isinstance(value, (bool, int, float)):
|
||||||
|
return "invalid_input"
|
||||||
|
try:
|
||||||
|
encoded = json.dumps(
|
||||||
|
metadata,
|
||||||
|
ensure_ascii=False,
|
||||||
|
allow_nan=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
except (RecursionError, TypeError, UnicodeEncodeError, ValueError):
|
||||||
|
return "invalid_input"
|
||||||
|
return "input_too_large" if len(encoded) > MAX_METADATA_BYTES else None
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_payload(
|
||||||
|
target: Any,
|
||||||
|
content: Any,
|
||||||
|
account_id: Any,
|
||||||
|
message_id: Any,
|
||||||
|
metadata: Any,
|
||||||
|
dry_run: Any,
|
||||||
|
) -> Optional[str]:
|
||||||
|
if not isinstance(target, str) or not isinstance(content, str):
|
||||||
|
return "invalid_input"
|
||||||
|
if not target.strip() or not content.strip() or not isinstance(dry_run, bool):
|
||||||
|
return "invalid_input"
|
||||||
|
if any(ord(character) < 32 or ord(character) == 127 for character in target):
|
||||||
|
return "invalid_input"
|
||||||
|
if "\x00" in content or not _valid_identifier(account_id) or not _valid_identifier(message_id):
|
||||||
|
return "invalid_input"
|
||||||
|
if len(target) > MAX_TARGET_BYTES or len(content) > MAX_CONTENT_BYTES:
|
||||||
|
return "input_too_large"
|
||||||
|
target_size = _utf8_size(target)
|
||||||
|
content_size = _utf8_size(content)
|
||||||
|
if target_size is None or content_size is None:
|
||||||
|
return "invalid_input"
|
||||||
|
if target_size > MAX_TARGET_BYTES or content_size > MAX_CONTENT_BYTES:
|
||||||
|
return "input_too_large"
|
||||||
|
return _validate_metadata(metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_provider_message_id(value: Any) -> Optional[str]:
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
return None
|
||||||
|
if len(value) > MAX_IDENTIFIER_BYTES:
|
||||||
|
return None
|
||||||
|
size = _utf8_size(value)
|
||||||
|
if size is None or size > MAX_IDENTIFIER_BYTES:
|
||||||
|
return None
|
||||||
|
if any(ord(character) < 32 or ord(character) == 127 for character in value):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def send_ucc_adapter(
|
||||||
|
target: str,
|
||||||
|
content: str,
|
||||||
|
account_id: str = "",
|
||||||
|
message_id: str = "",
|
||||||
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> BackendResult:
|
||||||
|
effective_account = account_id or WECHAT_ACCOUNT_ID
|
||||||
|
effective_metadata: Any = {} if metadata is None else metadata
|
||||||
|
error_code = _validate_payload(
|
||||||
|
target,
|
||||||
|
content,
|
||||||
|
effective_account,
|
||||||
|
message_id,
|
||||||
|
effective_metadata,
|
||||||
|
dry_run,
|
||||||
|
)
|
||||||
|
if error_code:
|
||||||
|
return BackendResult(False, "ucc-adapter", "failed", {"code": error_code})
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"target": target,
|
||||||
|
"content": content,
|
||||||
|
"account_id": effective_account,
|
||||||
|
"message_id": message_id,
|
||||||
|
"metadata": effective_metadata,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
last_endpoint: Optional[str] = None
|
||||||
|
for path in _ucc_send_paths():
|
||||||
|
last_endpoint = path
|
||||||
|
try:
|
||||||
|
http_status, data = _post_ucc(path, payload)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
if exc.code in {404, 405}:
|
||||||
|
continue
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"ucc-adapter",
|
||||||
|
"failed",
|
||||||
|
{"code": "ucc_adapter_http_error", "http_status": int(exc.code), "endpoint": path},
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, urllib.error.URLError, _BackendProtocolError):
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"ucc-adapter",
|
||||||
|
"failed",
|
||||||
|
{"code": "ucc_adapter_unavailable", "endpoint": path},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Fail closed without returning exception text, type, or chained cause.
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"ucc-adapter",
|
||||||
|
"failed",
|
||||||
|
{"code": "ucc_adapter_unavailable", "endpoint": path},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (200 <= http_status < 300):
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"ucc-adapter",
|
||||||
|
"failed",
|
||||||
|
{"code": "ucc_adapter_http_error", "http_status": http_status, "endpoint": path},
|
||||||
|
)
|
||||||
|
if not _ucc_result_is_ok(data, dry_run):
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"ucc-adapter",
|
||||||
|
"failed",
|
||||||
|
{"code": "ucc_adapter_rejected", "http_status": http_status, "endpoint": path},
|
||||||
|
)
|
||||||
|
confirmed_status = "dry_run" if dry_run else "sent"
|
||||||
|
result_data: Dict[str, Any] = {
|
||||||
|
"code": confirmed_status,
|
||||||
|
"http_status": http_status,
|
||||||
|
"endpoint": path,
|
||||||
|
}
|
||||||
|
provider_message_id = _safe_provider_message_id(data.get("provider_message_id"))
|
||||||
|
if provider_message_id:
|
||||||
|
result_data["provider_message_id"] = provider_message_id
|
||||||
|
return BackendResult(True, "ucc-adapter", confirmed_status, result_data)
|
||||||
|
|
||||||
|
data: Dict[str, Any] = {"code": "ucc_adapter_unavailable"}
|
||||||
|
if last_endpoint:
|
||||||
|
data["endpoint"] = last_endpoint
|
||||||
|
return BackendResult(False, "ucc-adapter", "failed", data)
|
||||||
|
|
||||||
|
|
||||||
|
def send_openclaw_channel(
|
||||||
|
target: str,
|
||||||
|
content: str,
|
||||||
|
account_id: str = "",
|
||||||
|
message_id: str = "",
|
||||||
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> BackendResult:
|
||||||
|
del target, content, account_id, message_id, metadata, dry_run
|
||||||
|
return BackendResult(
|
||||||
|
False,
|
||||||
|
"openclaw-channel",
|
||||||
|
"failed",
|
||||||
|
{"code": "unsupported_backend"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(
|
||||||
|
target: str,
|
||||||
|
content: str,
|
||||||
|
account_id: str = "",
|
||||||
|
message_id: str = "",
|
||||||
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
selected = select_backend()
|
||||||
|
if selected.get("backend") == "ucc-adapter":
|
||||||
|
return send_ucc_adapter(
|
||||||
|
target, content, account_id, message_id, metadata, dry_run
|
||||||
|
).to_dict()
|
||||||
|
code = "unsupported_backend" if selected.get("reason") == "unsupported_backend" else "wechat_outbound_backend_unavailable"
|
||||||
|
return BackendResult(False, "unavailable", "failed", {"code": code}).to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(json.dumps(select_backend(), ensure_ascii=False, separators=(",", ":")))
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Copyright (c) 2026 AIOS. All rights reserved.
|
||||||
|
|
||||||
|
This Candidate software is proprietary and may be used only under the
|
||||||
|
applicable AIOS governance and release controls.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# AIOS Autonomous Release Candidate
|
||||||
|
|
||||||
|
This package implements the task-bound, fail-closed control plane used to
|
||||||
|
validate AIOS autonomous-release workflows. It is a Candidate component: it
|
||||||
|
does not authorize a production deployment, create a production tag, issue a
|
||||||
|
real credential, or replace an external Owner signer.
|
||||||
|
|
||||||
|
The service binds to loopback by default. Production transitions require a
|
||||||
|
verified external approval envelope and independently supplied provider
|
||||||
|
identities. Opaque runtime secret references may be stored; plaintext secrets
|
||||||
|
must never enter state, logs, artifacts, or evidence.
|
||||||
|
|
||||||
|
Run the isolated suite from the repository root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python -m pytest tests/autonomous_release
|
||||||
|
python -m compileall services/aios-autonomous-release-candidate
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Dormant AIOS autonomous release Candidate package."""
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
"""AIOS autonomous release candidate package.
|
||||||
|
|
||||||
|
Candidate-only orchestration primitives. Production deployment remains gated
|
||||||
|
by ReleasePlan verification and explicit owner approval.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .models import * # noqa: F401,F403
|
||||||
|
from .planner import DeterministicPlanner, PlanningRequest
|
||||||
|
from .orchestrator import AutonomousReleaseOrchestrator
|
||||||
|
from .release import ReleaseController
|
||||||
|
from .credential import CredentialBroker
|
||||||
|
from .evidence import EvidenceChain
|
||||||
|
from .execution_manager import ExecutionManager
|
||||||
|
from .gitea_canary import GiteaDeployKeyCanary
|
||||||
|
from .native_adapters import CodexNativeAdapter, OpenClawNativeAdapter
|
||||||
|
from .secret_manager import SecretManagerV2
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DeterministicPlanner",
|
||||||
|
"PlanningRequest",
|
||||||
|
"AutonomousReleaseOrchestrator",
|
||||||
|
"ReleaseController",
|
||||||
|
"CredentialBroker",
|
||||||
|
"EvidenceChain",
|
||||||
|
"ExecutionManager",
|
||||||
|
"GiteaDeployKeyCanary",
|
||||||
|
"OpenClawNativeAdapter",
|
||||||
|
"CodexNativeAdapter",
|
||||||
|
"SecretManagerV2",
|
||||||
|
]
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
"""Provider adapter contracts for OpenClaw, Codex and writer workers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Protocol, Mapping, Any
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterState(str, Enum):
|
||||||
|
CREATED = "CREATED"
|
||||||
|
ACTIVE = "ACTIVE"
|
||||||
|
CLOSED = "CLOSED"
|
||||||
|
FAILED = "FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AdapterSession:
|
||||||
|
provider: str
|
||||||
|
session_id: str
|
||||||
|
provider_session_id: str
|
||||||
|
state: AdapterState
|
||||||
|
|
||||||
|
|
||||||
|
class AgentAdapter(Protocol):
|
||||||
|
"""Common lifecycle contract for external agent providers."""
|
||||||
|
|
||||||
|
def create_session(self, task: Mapping[str, Any]) -> AdapterSession:
|
||||||
|
"""Create a provider-native session."""
|
||||||
|
|
||||||
|
def send(self, session: AdapterSession, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||||
|
"""Send a task payload."""
|
||||||
|
|
||||||
|
def close(self, session: AdapterSession) -> None:
|
||||||
|
"""Close and revoke the session."""
|
||||||
|
|
||||||
|
|
||||||
|
class ShadowAdapter:
|
||||||
|
"""Safe adapter used only for candidate lifecycle testing."""
|
||||||
|
|
||||||
|
def __init__(self, provider: str) -> None:
|
||||||
|
self.provider = provider
|
||||||
|
self._counter = 0
|
||||||
|
|
||||||
|
def create_session(self, task: Mapping[str, Any]) -> AdapterSession:
|
||||||
|
self._counter += 1
|
||||||
|
return AdapterSession(
|
||||||
|
provider=self.provider,
|
||||||
|
session_id=f"shadow-{self._counter}",
|
||||||
|
provider_session_id=f"provider-{self._counter}",
|
||||||
|
state=AdapterState.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, session: AdapterSession, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||||
|
if session.state != AdapterState.ACTIVE:
|
||||||
|
raise RuntimeError("session is not active")
|
||||||
|
return {
|
||||||
|
"provider": self.provider,
|
||||||
|
"session_id": session.session_id,
|
||||||
|
"result": "shadow-completed",
|
||||||
|
"side_effects": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def close(self, session: AdapterSession) -> None:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
"""Dependency-light candidate API facade.
|
||||||
|
|
||||||
|
The production transport is intentionally external. This module exposes the
|
||||||
|
application boundary without opening listeners or changing production routes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from .evidence import EvidenceChain
|
||||||
|
from .orchestrator import AutonomousReleaseOrchestrator
|
||||||
|
from .release import ReleaseController
|
||||||
|
from .serialization import (
|
||||||
|
release_plan_from_mapping,
|
||||||
|
release_plan_to_mapping,
|
||||||
|
task_plan_from_mapping,
|
||||||
|
task_plan_to_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutonomousReleaseAPI:
|
||||||
|
def __init__(self, orchestrator: AutonomousReleaseOrchestrator) -> None:
|
||||||
|
self.orchestrator = orchestrator
|
||||||
|
self.release = ReleaseController()
|
||||||
|
|
||||||
|
def _chain(self, task_id: str) -> EvidenceChain:
|
||||||
|
return EvidenceChain(self.orchestrator.workflow.store, task_id)
|
||||||
|
|
||||||
|
def create_task(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
plan = task_plan_from_mapping(payload)
|
||||||
|
result = self.orchestrator.create(plan)
|
||||||
|
self.orchestrator.workflow.next_steps(plan)
|
||||||
|
return {
|
||||||
|
"task_id": result.task_id,
|
||||||
|
"plan_digest": result.plan_digest,
|
||||||
|
"production_side_effects": result.production_side_effects,
|
||||||
|
"plan": task_plan_to_mapping(plan),
|
||||||
|
}
|
||||||
|
|
||||||
|
def close_evidence(self, task_id: str) -> dict[str, Any]:
|
||||||
|
result = self._chain(task_id).closure_digest()
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"evidence_closure": result,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def task_steps(self, task_id: str) -> dict[str, Any]:
|
||||||
|
status = self.task_status(task_id)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"steps": status["steps"],
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def claim(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
step_id = str(payload["step_id"])
|
||||||
|
worker_id = str(payload["worker_id"])
|
||||||
|
session_id = str(payload["session_id"])
|
||||||
|
workflow = self.orchestrator.workflow
|
||||||
|
workflow.claim(task_id, step_id, worker_id, session_id)
|
||||||
|
workflow.start(task_id, step_id, worker_id, session_id)
|
||||||
|
self._chain(task_id).append(
|
||||||
|
"STEP_CLAIMED",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"step_id": step_id,
|
||||||
|
"worker_id": worker_id,
|
||||||
|
"session_id": session_id,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"step_id": step_id,
|
||||||
|
"state": "RUNNING",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def heartbeat(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
step_id = str(payload["step_id"])
|
||||||
|
worker_id = str(payload["worker_id"])
|
||||||
|
session_id = str(payload["session_id"])
|
||||||
|
self.orchestrator.workflow.heartbeat(task_id, step_id, worker_id, session_id)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"step_id": step_id,
|
||||||
|
"state": "RUNNING",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def result(self, task_id: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
step_id = str(payload["step_id"])
|
||||||
|
result_digest = str(payload["result_digest"])
|
||||||
|
self.orchestrator.workflow.complete(task_id, step_id, result_digest=result_digest)
|
||||||
|
self._chain(task_id).append(
|
||||||
|
"STEP_RESULT",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"step_id": step_id,
|
||||||
|
"result_digest": result_digest,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"step_id": step_id,
|
||||||
|
"state": "COMPLETED",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def evidence(self, task_id: str) -> dict[str, Any]:
|
||||||
|
chain = self._chain(task_id)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"events": chain.export(),
|
||||||
|
"verified": chain.verify(),
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_release_plan(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
plan = release_plan_from_mapping(payload)
|
||||||
|
self.release = ReleaseController()
|
||||||
|
self.release.attach_plan(plan)
|
||||||
|
self.release.mark_built()
|
||||||
|
return {
|
||||||
|
"plan": release_plan_to_mapping(plan),
|
||||||
|
"release_state": self.release.status().state.value,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def verify_release(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
self.release.verify(
|
||||||
|
artifact_ok=payload.get("artifact_ok") is True,
|
||||||
|
evidence_ok=payload.get("evidence_ok") is True,
|
||||||
|
)
|
||||||
|
self.release.request_owner_approval()
|
||||||
|
return {
|
||||||
|
"release_state": self.release.status().state.value,
|
||||||
|
"production_release": "BLOCKED_EXTERNAL_OWNER_SIGNATURE",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def task_status(self, task_id: str) -> dict[str, Any]:
|
||||||
|
store = self.orchestrator.workflow.store
|
||||||
|
state, revision = store.get_task_state(task_id)
|
||||||
|
steps = store.list_steps(task_id)
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"state": state.value,
|
||||||
|
"revision": revision,
|
||||||
|
"steps": {
|
||||||
|
step_id: {
|
||||||
|
"state": item.state.value,
|
||||||
|
"attempt": item.attempt,
|
||||||
|
"max_attempts": item.max_attempts,
|
||||||
|
"worker_id": item.worker_id,
|
||||||
|
"session_id": item.session_id,
|
||||||
|
}
|
||||||
|
for step_id, item in steps.items()
|
||||||
|
},
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
"""Owner approval verification contracts for candidate releases."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from .errors import ApprovalRequired, StateConflict
|
||||||
|
from .models import ApprovalEnvelope, TaskPlan, canonical_json
|
||||||
|
from .store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureVerifier(Protocol):
|
||||||
|
"""Verify an approval payload without exposing private-key operations."""
|
||||||
|
|
||||||
|
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
|
||||||
|
"""Return ``True`` when the signature is valid for ``key_id``."""
|
||||||
|
|
||||||
|
|
||||||
|
class RevocationSource(Protocol):
|
||||||
|
def is_revoked(self, approval_plan_id: str) -> bool:
|
||||||
|
"""Return whether an approval plan has been revoked."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalVerificationResult:
|
||||||
|
approval_plan_id: str
|
||||||
|
task_id: str
|
||||||
|
signing_key_id: str
|
||||||
|
verified_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalVerifier:
|
||||||
|
"""Verify exact approval bindings and consume nonces before execution."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
store: StateStore,
|
||||||
|
signature_verifier: SignatureVerifier,
|
||||||
|
revocations: RevocationSource,
|
||||||
|
) -> None:
|
||||||
|
self.store = store
|
||||||
|
self.signature_verifier = signature_verifier
|
||||||
|
self.revocations = revocations
|
||||||
|
|
||||||
|
def verify(
|
||||||
|
self,
|
||||||
|
plan: TaskPlan,
|
||||||
|
envelope: ApprovalEnvelope,
|
||||||
|
*,
|
||||||
|
now: datetime | None = None,
|
||||||
|
) -> ApprovalVerificationResult:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
if envelope.task_id != plan.task_id:
|
||||||
|
raise ApprovalRequired("APPROVAL_TASK_MISMATCH", "approval task does not match plan")
|
||||||
|
if envelope.task_plan_sha256 != plan.digest():
|
||||||
|
raise ApprovalRequired("APPROVAL_PLAN_DIGEST_MISMATCH", "approval digest does not match plan")
|
||||||
|
if envelope.binding != plan.binding:
|
||||||
|
raise ApprovalRequired("APPROVAL_RESOURCE_MISMATCH", "approval resource binding does not match plan")
|
||||||
|
approved = set(envelope.capabilities)
|
||||||
|
required = {cap for step in plan.steps for cap in step.required_capabilities}
|
||||||
|
if not required.issubset(approved):
|
||||||
|
raise ApprovalRequired(
|
||||||
|
"APPROVAL_CAPABILITY_INSUFFICIENT",
|
||||||
|
"approval does not cover all plan capabilities",
|
||||||
|
{"missing": sorted(required - approved)},
|
||||||
|
)
|
||||||
|
if now < envelope.not_before or now >= envelope.expires_at:
|
||||||
|
raise ApprovalRequired("APPROVAL_TIME_INVALID", "approval is outside its validity window")
|
||||||
|
if self.revocations.is_revoked(envelope.approval_plan_id):
|
||||||
|
raise ApprovalRequired("APPROVAL_REVOKED", "approval plan is revoked")
|
||||||
|
payload = canonical_json(envelope.payload()).encode("utf-8")
|
||||||
|
try:
|
||||||
|
signature_valid = self.signature_verifier.verify(
|
||||||
|
envelope.signing_key_id, payload, envelope.signature
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
raise ApprovalRequired(
|
||||||
|
"BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE",
|
||||||
|
"trusted Owner approval verifier is unavailable",
|
||||||
|
) from error
|
||||||
|
if not signature_valid:
|
||||||
|
raise ApprovalRequired("APPROVAL_SIGNATURE_INVALID", "approval signature verification failed")
|
||||||
|
try:
|
||||||
|
self.store.consume_nonce(f"approval:{envelope.approval_plan_id}", envelope.nonce)
|
||||||
|
except StateConflict as error:
|
||||||
|
raise ApprovalRequired("APPROVAL_NONCE_REPLAY", "approval nonce was already consumed") from error
|
||||||
|
return ApprovalVerificationResult(
|
||||||
|
approval_plan_id=envelope.approval_plan_id,
|
||||||
|
task_id=envelope.task_id,
|
||||||
|
signing_key_id=envelope.signing_key_id,
|
||||||
|
verified_at=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticRevocationSource:
|
||||||
|
def __init__(self, revoked: set[str] | None = None) -> None:
|
||||||
|
self.revoked = set(revoked or ())
|
||||||
|
|
||||||
|
def is_revoked(self, approval_plan_id: str) -> bool:
|
||||||
|
return approval_plan_id in self.revoked
|
||||||
|
|
||||||
|
|
||||||
|
class RejectingSignatureVerifier:
|
||||||
|
"""Safe default used when no external verifier has been configured."""
|
||||||
|
|
||||||
|
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UnavailableSignatureVerifier:
|
||||||
|
"""Safe runtime default when no Owner HSM/KMS verifier has been supplied."""
|
||||||
|
|
||||||
|
def verify(self, key_id: str, payload: bytes, signature: str) -> bool:
|
||||||
|
raise ConnectionError("Owner verifier unavailable")
|
||||||
+225
@@ -0,0 +1,225 @@
|
|||||||
|
"""Deterministic Candidate artifact and package-closure construction."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
import stat
|
||||||
|
import tarfile
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from .errors import EvidenceIncomplete, ReleaseBlocked
|
||||||
|
from .models import is_git_oid
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_bytes(value: bytes) -> str:
|
||||||
|
return hashlib.sha256(value).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ArtifactFile:
|
||||||
|
path: str
|
||||||
|
content: bytes
|
||||||
|
mode: int = 0o644
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
parsed = PurePosixPath(self.path)
|
||||||
|
if (
|
||||||
|
parsed.is_absolute()
|
||||||
|
or ".." in parsed.parts
|
||||||
|
or not self.path
|
||||||
|
or "\\" in self.path
|
||||||
|
or self.path != self.path.strip()
|
||||||
|
):
|
||||||
|
raise ValueError("artifact paths must be canonical, relative and traversal-free")
|
||||||
|
if ".git" in parsed.parts:
|
||||||
|
raise ValueError(".git content is forbidden")
|
||||||
|
if self.mode not in (0o644, 0o755):
|
||||||
|
raise ValueError("artifact mode must be 0644 or 0755")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ArtifactManifest:
|
||||||
|
source_oid: str
|
||||||
|
tree_oid: str
|
||||||
|
files: Mapping[str, str]
|
||||||
|
artifact_sha256: str
|
||||||
|
package_closure_sha256: str
|
||||||
|
format_version: int = 1
|
||||||
|
secrets_included: bool = False
|
||||||
|
|
||||||
|
def as_json_bytes(self) -> bytes:
|
||||||
|
payload = {
|
||||||
|
"format_version": self.format_version,
|
||||||
|
"source_oid": self.source_oid,
|
||||||
|
"tree_oid": self.tree_oid,
|
||||||
|
"files": dict(sorted(self.files.items())),
|
||||||
|
"artifact_sha256": self.artifact_sha256,
|
||||||
|
"package_closure_sha256": self.package_closure_sha256,
|
||||||
|
"secrets_included": self.secrets_included,
|
||||||
|
}
|
||||||
|
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manifest_sha256(self) -> str:
|
||||||
|
return sha256_bytes(self.as_json_bytes())
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class BuiltArtifact:
|
||||||
|
content: bytes
|
||||||
|
manifest: ArtifactManifest
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactBuilder:
|
||||||
|
MAX_FILE_BYTES = 8 * 1024 * 1024
|
||||||
|
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
|
||||||
|
FORBIDDEN_MARKERS = (
|
||||||
|
b"begin private " + b"key",
|
||||||
|
b"authorization:" + b" bearer ",
|
||||||
|
b"password" + b"=",
|
||||||
|
b"token" + b"=",
|
||||||
|
b"secret" + b"=",
|
||||||
|
b"api_key" + b"=",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_oid(name: str, value: str) -> None:
|
||||||
|
if not is_git_oid(value):
|
||||||
|
raise ValueError(f"{name} must be a full lowercase Git OID")
|
||||||
|
|
||||||
|
def build_from_directory(
|
||||||
|
self, source_oid: str, tree_oid: str, source: str | Path
|
||||||
|
) -> BuiltArtifact:
|
||||||
|
root = Path(source)
|
||||||
|
if root.is_symlink() or not root.is_dir():
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ARTIFACT_SOURCE_LINK_REJECTED", "artifact source must be a real directory"
|
||||||
|
)
|
||||||
|
files: list[ArtifactFile] = []
|
||||||
|
for directory, dirnames, filenames in os.walk(root, followlinks=False):
|
||||||
|
current = Path(directory)
|
||||||
|
for name in tuple(dirnames):
|
||||||
|
item = current / name
|
||||||
|
if item.is_symlink() or name == ".git":
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ARTIFACT_SOURCE_LINK_REJECTED",
|
||||||
|
"artifact source contains a linked or repository directory",
|
||||||
|
)
|
||||||
|
for name in filenames:
|
||||||
|
item = current / name
|
||||||
|
info = item.lstat()
|
||||||
|
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ARTIFACT_SOURCE_LINK_REJECTED",
|
||||||
|
"artifact source contains a link or special file",
|
||||||
|
)
|
||||||
|
if info.st_size > self.MAX_FILE_BYTES:
|
||||||
|
raise ReleaseBlocked("ARTIFACT_FILE_TOO_LARGE", "artifact file exceeds limit")
|
||||||
|
relative = item.relative_to(root).as_posix()
|
||||||
|
mode = 0o755 if info.st_mode & 0o111 else 0o644
|
||||||
|
files.append(ArtifactFile(relative, item.read_bytes(), mode))
|
||||||
|
return self.build(source_oid, tree_oid, tuple(files))
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self, source_oid: str, tree_oid: str, files: tuple[ArtifactFile, ...]
|
||||||
|
) -> BuiltArtifact:
|
||||||
|
self._validate_oid("source_oid", source_oid)
|
||||||
|
self._validate_oid("tree_oid", tree_oid)
|
||||||
|
if not files:
|
||||||
|
raise EvidenceIncomplete("ARTIFACT_EMPTY", "artifact requires at least one file")
|
||||||
|
paths = [item.path for item in files]
|
||||||
|
if len(paths) != len(set(paths)):
|
||||||
|
raise ReleaseBlocked("ARTIFACT_DUPLICATE_PATH", "artifact contains duplicate paths")
|
||||||
|
total = 0
|
||||||
|
for item in files:
|
||||||
|
total += len(item.content)
|
||||||
|
if len(item.content) > self.MAX_FILE_BYTES or total > self.MAX_ARTIFACT_BYTES:
|
||||||
|
raise ReleaseBlocked("ARTIFACT_SIZE_LIMIT", "artifact exceeds Candidate limit")
|
||||||
|
lowered = item.content.lower()
|
||||||
|
if any(marker in lowered for marker in self.FORBIDDEN_MARKERS):
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ARTIFACT_SECRET_MARKER",
|
||||||
|
"artifact contains a forbidden secret marker",
|
||||||
|
{"path": item.path},
|
||||||
|
)
|
||||||
|
output = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=output, mode="w", format=tarfile.PAX_FORMAT) as archive:
|
||||||
|
for item in sorted(files, key=lambda value: value.path):
|
||||||
|
info = tarfile.TarInfo(item.path)
|
||||||
|
info.size = len(item.content)
|
||||||
|
info.mode = item.mode
|
||||||
|
info.mtime = 0
|
||||||
|
info.uid = 0
|
||||||
|
info.gid = 0
|
||||||
|
info.uname = "root"
|
||||||
|
info.gname = "root"
|
||||||
|
archive.addfile(info, io.BytesIO(item.content))
|
||||||
|
content = output.getvalue()
|
||||||
|
file_hashes = {item.path: sha256_bytes(item.content) for item in files}
|
||||||
|
closure_payload = json.dumps(
|
||||||
|
{
|
||||||
|
"source_oid": source_oid,
|
||||||
|
"tree_oid": tree_oid,
|
||||||
|
"files": dict(sorted(file_hashes.items())),
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
manifest = ArtifactManifest(
|
||||||
|
source_oid=source_oid,
|
||||||
|
tree_oid=tree_oid,
|
||||||
|
files=file_hashes,
|
||||||
|
artifact_sha256=sha256_bytes(content),
|
||||||
|
package_closure_sha256=sha256_bytes(closure_payload),
|
||||||
|
secrets_included=False,
|
||||||
|
)
|
||||||
|
return BuiltArtifact(content=content, manifest=manifest)
|
||||||
|
|
||||||
|
def verify(self, built: BuiltArtifact) -> None:
|
||||||
|
if built.manifest.secrets_included:
|
||||||
|
raise EvidenceIncomplete("ARTIFACT_SECRET_FLAG", "manifest includes secrets")
|
||||||
|
if sha256_bytes(built.content) != built.manifest.artifact_sha256:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"ARTIFACT_DIGEST_MISMATCH", "artifact digest does not match manifest"
|
||||||
|
)
|
||||||
|
actual: dict[str, str] = {}
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(built.content), mode="r:") as archive:
|
||||||
|
for member in archive.getmembers():
|
||||||
|
try:
|
||||||
|
ArtifactFile(member.name, b"", member.mode)
|
||||||
|
except ValueError as error:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"ARTIFACT_MEMBER_INVALID", "archive contains an unsafe member"
|
||||||
|
) from error
|
||||||
|
if not member.isfile() or member.name in actual:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"ARTIFACT_MEMBER_INVALID", "archive contains a link, special or duplicate member"
|
||||||
|
)
|
||||||
|
stream = archive.extractfile(member)
|
||||||
|
if stream is None:
|
||||||
|
raise EvidenceIncomplete("ARTIFACT_MEMBER_INVALID", "archive member is unreadable")
|
||||||
|
actual[member.name] = sha256_bytes(stream.read())
|
||||||
|
if actual != dict(built.manifest.files):
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"ARTIFACT_FILE_MISMATCH", "artifact file closure does not match manifest"
|
||||||
|
)
|
||||||
|
closure_payload = json.dumps(
|
||||||
|
{
|
||||||
|
"source_oid": built.manifest.source_oid,
|
||||||
|
"tree_oid": built.manifest.tree_oid,
|
||||||
|
"files": dict(sorted(actual.items())),
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
if sha256_bytes(closure_payload) != built.manifest.package_closure_sha256:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"PACKAGE_CLOSURE_MISMATCH", "package closure digest does not match"
|
||||||
|
)
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"""Candidate inspection CLI; all commands are local and non-production."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .artifact import ArtifactBuilder, ArtifactManifest, BuiltArtifact
|
||||||
|
from .evidence import EvidenceChain
|
||||||
|
from .release import ReleaseController
|
||||||
|
from .serialization import (
|
||||||
|
release_plan_from_mapping,
|
||||||
|
release_plan_to_mapping,
|
||||||
|
task_plan_from_mapping,
|
||||||
|
task_plan_to_mapping,
|
||||||
|
)
|
||||||
|
from .service import CandidateApplication
|
||||||
|
from .store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(prog="aios-autonomous-release")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
plan = sub.add_parser("plan")
|
||||||
|
plan.add_argument("payload")
|
||||||
|
digest = sub.add_parser("digest")
|
||||||
|
digest.add_argument("payload")
|
||||||
|
status = sub.add_parser("status")
|
||||||
|
status.add_argument("--state", required=True, type=Path)
|
||||||
|
status.add_argument("--task-id", required=True)
|
||||||
|
evidence = sub.add_parser("verify-evidence")
|
||||||
|
evidence.add_argument("--state", required=True, type=Path)
|
||||||
|
evidence.add_argument("--task-id", required=True)
|
||||||
|
evidence.add_argument("--require", action="append", default=[])
|
||||||
|
|
||||||
|
task = sub.add_parser("task")
|
||||||
|
task_sub = task.add_subparsers(dest="task_command", required=True)
|
||||||
|
task_create = task_sub.add_parser("create")
|
||||||
|
task_create.add_argument("--state", required=True, type=Path)
|
||||||
|
task_create.add_argument("--payload", required=True, type=Path)
|
||||||
|
for name in ("status", "steps"):
|
||||||
|
item = task_sub.add_parser(name)
|
||||||
|
item.add_argument("--state", required=True, type=Path)
|
||||||
|
item.add_argument("--task-id", required=True)
|
||||||
|
|
||||||
|
evidence_group = sub.add_parser("evidence")
|
||||||
|
evidence_sub = evidence_group.add_subparsers(dest="evidence_command", required=True)
|
||||||
|
evidence_verify = evidence_sub.add_parser("verify")
|
||||||
|
evidence_verify.add_argument("--state", required=True, type=Path)
|
||||||
|
evidence_verify.add_argument("--task-id", required=True)
|
||||||
|
evidence_verify.add_argument("--require", action="append", default=[])
|
||||||
|
|
||||||
|
artifact = sub.add_parser("artifact")
|
||||||
|
artifact_sub = artifact.add_subparsers(dest="artifact_command", required=True)
|
||||||
|
artifact_build = artifact_sub.add_parser("build")
|
||||||
|
artifact_build.add_argument("--source", required=True, type=Path)
|
||||||
|
artifact_build.add_argument("--source-oid", required=True)
|
||||||
|
artifact_build.add_argument("--tree-oid", required=True)
|
||||||
|
artifact_build.add_argument("--output-directory", required=True, type=Path)
|
||||||
|
artifact_verify = artifact_sub.add_parser("verify")
|
||||||
|
artifact_verify.add_argument("--artifact", required=True, type=Path)
|
||||||
|
artifact_verify.add_argument("--manifest", required=True, type=Path)
|
||||||
|
|
||||||
|
release = sub.add_parser("release")
|
||||||
|
release_sub = release.add_subparsers(dest="release_command", required=True)
|
||||||
|
for name in ("plan", "verify"):
|
||||||
|
item = release_sub.add_parser(name)
|
||||||
|
item.add_argument("--payload", required=True, type=Path)
|
||||||
|
|
||||||
|
acceptance = sub.add_parser("acceptance")
|
||||||
|
acceptance.add_argument("--repo-root", required=True, type=Path)
|
||||||
|
acceptance.add_argument("--output", required=True, type=Path)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> dict:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError("JSON payload must be an object")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def exclusive(path: Path, content: bytes) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||||
|
descriptor = os.open(
|
||||||
|
path,
|
||||||
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
|
||||||
|
0o640,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
os.write(descriptor, content)
|
||||||
|
os.fsync(descriptor)
|
||||||
|
finally:
|
||||||
|
os.close(descriptor)
|
||||||
|
|
||||||
|
|
||||||
|
def artifact_from_files(artifact: Path, manifest: Path) -> BuiltArtifact:
|
||||||
|
payload = load_json(manifest)
|
||||||
|
contract = ArtifactManifest(
|
||||||
|
source_oid=str(payload["source_oid"]),
|
||||||
|
tree_oid=str(payload["tree_oid"]),
|
||||||
|
files={str(key): str(value) for key, value in payload["files"].items()},
|
||||||
|
artifact_sha256=str(payload["artifact_sha256"]),
|
||||||
|
package_closure_sha256=str(payload["package_closure_sha256"]),
|
||||||
|
format_version=int(payload.get("format_version", 1)),
|
||||||
|
secrets_included=bool(payload.get("secrets_included", False)),
|
||||||
|
)
|
||||||
|
return BuiltArtifact(artifact.read_bytes(), contract)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
if args.command in {"plan", "digest"}:
|
||||||
|
plan = task_plan_from_mapping(json.loads(args.payload))
|
||||||
|
if args.command == "plan":
|
||||||
|
print(json.dumps(task_plan_to_mapping(plan), sort_keys=True))
|
||||||
|
else:
|
||||||
|
print(plan.digest())
|
||||||
|
return 0
|
||||||
|
if args.command == "task":
|
||||||
|
application = CandidateApplication(args.state)
|
||||||
|
if args.task_command == "create":
|
||||||
|
result = application.api.create_task(load_json(args.payload))
|
||||||
|
elif args.task_command == "steps":
|
||||||
|
result = application.api.task_steps(args.task_id)
|
||||||
|
else:
|
||||||
|
result = application.api.task_status(args.task_id)
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.command == "evidence":
|
||||||
|
chain = EvidenceChain(StateStore(args.state), args.task_id)
|
||||||
|
result = {
|
||||||
|
"task_id": args.task_id,
|
||||||
|
"verified": chain.verify(),
|
||||||
|
"evidence_closure": chain.close(tuple(args.require)),
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.command == "artifact":
|
||||||
|
builder = ArtifactBuilder()
|
||||||
|
if args.artifact_command == "build":
|
||||||
|
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
|
||||||
|
builder.verify(built)
|
||||||
|
artifact_path = args.output_directory / "candidate.tar"
|
||||||
|
manifest_path = args.output_directory / "manifest.json"
|
||||||
|
exclusive(artifact_path, built.content)
|
||||||
|
exclusive(manifest_path, built.manifest.as_json_bytes() + b"\n")
|
||||||
|
result = {
|
||||||
|
"artifact": str(artifact_path),
|
||||||
|
"manifest": str(manifest_path),
|
||||||
|
"artifact_sha256": built.manifest.artifact_sha256,
|
||||||
|
"manifest_sha256": built.manifest.manifest_sha256,
|
||||||
|
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
built = artifact_from_files(args.artifact, args.manifest)
|
||||||
|
builder.verify(built)
|
||||||
|
result = {
|
||||||
|
"verified": True,
|
||||||
|
"artifact_sha256": built.manifest.artifact_sha256,
|
||||||
|
"manifest_sha256": built.manifest.manifest_sha256,
|
||||||
|
"package_closure_sha256": built.manifest.package_closure_sha256,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.command == "release":
|
||||||
|
plan = release_plan_from_mapping(load_json(args.payload))
|
||||||
|
if args.release_command == "plan":
|
||||||
|
result = {
|
||||||
|
"plan": release_plan_to_mapping(plan),
|
||||||
|
"release_state": "CREATED",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
controller = ReleaseController()
|
||||||
|
controller.attach_plan(plan)
|
||||||
|
controller.mark_built()
|
||||||
|
controller.verify(artifact_ok=True, evidence_ok=True)
|
||||||
|
controller.request_owner_approval()
|
||||||
|
result = {
|
||||||
|
"release_state": controller.status().state.value,
|
||||||
|
"production_release": "BLOCKED_EXTERNAL_OWNER_SIGNATURE",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.command == "acceptance":
|
||||||
|
runner = args.repo_root / "scripts" / "autonomous-release" / "run_acceptance.py"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(runner),
|
||||||
|
"--repo-root",
|
||||||
|
str(args.repo_root),
|
||||||
|
"--output",
|
||||||
|
str(args.output),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return completed.returncode
|
||||||
|
store = StateStore(args.state)
|
||||||
|
if args.command == "status":
|
||||||
|
state, revision = store.get_task_state(args.task_id)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"task_id": args.task_id,
|
||||||
|
"state": state.value,
|
||||||
|
"revision": revision,
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
chain = EvidenceChain(store, args.task_id)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"task_id": args.task_id,
|
||||||
|
"evidence_closure": chain.close(tuple(args.require)),
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+307
@@ -0,0 +1,307 @@
|
|||||||
|
"""Task-bound, reference-only Candidate credential lifecycle."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from .errors import CredentialUnavailable, PolicyDenied
|
||||||
|
from .models import (
|
||||||
|
CredentialProjection,
|
||||||
|
CredentialRequest,
|
||||||
|
CredentialState,
|
||||||
|
canonical_json,
|
||||||
|
)
|
||||||
|
from .store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_RE = re.compile(
|
||||||
|
r"(?:systemd-credential:[A-Za-z0-9_.@-]+|secret-ref://candidate/[A-Za-z0-9_./@-]+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialProvider(Protocol):
|
||||||
|
"""External provider contract; values crossing this boundary are opaque references."""
|
||||||
|
|
||||||
|
def issue(self, request: CredentialRequest, expires_at: datetime) -> tuple[str, str]: ...
|
||||||
|
|
||||||
|
def revoke(self, credential_id: str, reference: str) -> None: ...
|
||||||
|
|
||||||
|
def is_usable(self, credential_id: str, reference: str) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CredentialLifecycleResult:
|
||||||
|
credential_id: str
|
||||||
|
reference: str
|
||||||
|
state: CredentialState
|
||||||
|
expires_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialBroker:
|
||||||
|
"""Enforce approval, branch, consumer and operation bindings for projections."""
|
||||||
|
|
||||||
|
def __init__(self, store: StateStore, provider: CredentialProvider) -> None:
|
||||||
|
self.store = store
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_request(request: CredentialRequest, now: datetime) -> None:
|
||||||
|
if request.binding.branch == "main":
|
||||||
|
raise PolicyDenied("MAIN_CREDENTIAL_DENIED", "credentials cannot target main")
|
||||||
|
forbidden = {
|
||||||
|
"production.deploy",
|
||||||
|
"production.rollback",
|
||||||
|
"tag.write",
|
||||||
|
"baseline.write",
|
||||||
|
"registry.write",
|
||||||
|
"secret.export",
|
||||||
|
"secret.read",
|
||||||
|
}
|
||||||
|
overlap = forbidden.intersection(request.operations)
|
||||||
|
if overlap:
|
||||||
|
raise PolicyDenied(
|
||||||
|
"CREDENTIAL_OPERATION_FORBIDDEN",
|
||||||
|
"credential request includes forbidden operations",
|
||||||
|
{"operations": sorted(overlap)},
|
||||||
|
)
|
||||||
|
if now < request.not_before or now >= request.expires_at:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_APPROVAL_WINDOW_INVALID", "credential approval is not currently valid"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _metadata(request: CredentialRequest) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"request_id": request.request_id,
|
||||||
|
"task_id": request.task_id,
|
||||||
|
"session_id": request.session_id,
|
||||||
|
"worker_id": request.worker_id,
|
||||||
|
"audience": request.audience,
|
||||||
|
"consumer_service": request.consumer_service,
|
||||||
|
"provider": request.provider,
|
||||||
|
"runtime_user": request.runtime_user,
|
||||||
|
"approval_plan_id": request.approval_plan_id,
|
||||||
|
"repository": request.binding.repository,
|
||||||
|
"branch": request.binding.branch,
|
||||||
|
"base_oid": request.binding.base_oid,
|
||||||
|
"allowed_paths": list(request.binding.allowed_paths),
|
||||||
|
"operations": list(request.operations),
|
||||||
|
"not_before": request.not_before.astimezone(timezone.utc).isoformat(),
|
||||||
|
"expires_at": request.expires_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
"nonce": request.nonce,
|
||||||
|
"max_uses": request.max_uses,
|
||||||
|
"use_count": 0,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compensate(self, credential_id: str, reference: str) -> None:
|
||||||
|
try:
|
||||||
|
self.provider.revoke(credential_id, reference)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def issue(
|
||||||
|
self, request: CredentialRequest, now: datetime | None = None
|
||||||
|
) -> CredentialProjection:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
self._validate_request(request, now)
|
||||||
|
expires_at = min(request.expires_at, now + timedelta(seconds=request.ttl_seconds))
|
||||||
|
try:
|
||||||
|
credential_id, reference = self.provider.issue(request, expires_at)
|
||||||
|
except Exception as error:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_PROVIDER_UNAVAILABLE", "credential provider issue failed"
|
||||||
|
) from error
|
||||||
|
if not credential_id or not reference or not REFERENCE_RE.fullmatch(reference):
|
||||||
|
if credential_id and reference:
|
||||||
|
self._compensate(credential_id, reference)
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_REFERENCE_REJECTED", "provider did not return an approved opaque reference"
|
||||||
|
)
|
||||||
|
projection = CredentialProjection(
|
||||||
|
credential_id=credential_id,
|
||||||
|
reference=reference,
|
||||||
|
request=request,
|
||||||
|
state=CredentialState.ISSUED,
|
||||||
|
issued_at=now,
|
||||||
|
expires_at=expires_at,
|
||||||
|
plaintext_returned=False,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO credentials(credential_id, task_id, reference, state, metadata_json, "
|
||||||
|
"issued_at, expires_at) VALUES(?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
credential_id,
|
||||||
|
request.task_id,
|
||||||
|
reference,
|
||||||
|
CredentialState.ISSUED.value,
|
||||||
|
canonical_json(self._metadata(request)),
|
||||||
|
now.isoformat(),
|
||||||
|
expires_at.isoformat(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO consumed_nonces(scope, nonce, consumed_at) VALUES(?, ?, ?)",
|
||||||
|
(f"credential:{request.approval_plan_id}", request.nonce, now.isoformat()),
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
self._compensate(credential_id, reference)
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_PERSISTENCE_FAILED",
|
||||||
|
"credential issuance was compensated after durable-state failure",
|
||||||
|
) from error
|
||||||
|
return projection
|
||||||
|
|
||||||
|
def project(self, projection: CredentialProjection) -> CredentialLifecycleResult:
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"UPDATE credentials SET state=? WHERE credential_id=? AND reference=? AND state=?",
|
||||||
|
(
|
||||||
|
CredentialState.PROJECTED.value,
|
||||||
|
projection.credential_id,
|
||||||
|
projection.reference,
|
||||||
|
CredentialState.ISSUED.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if cursor.rowcount != 1:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_STATE_CONFLICT", "only a durably issued credential may be projected"
|
||||||
|
)
|
||||||
|
return CredentialLifecycleResult(
|
||||||
|
projection.credential_id,
|
||||||
|
projection.reference,
|
||||||
|
CredentialState.PROJECTED,
|
||||||
|
projection.expires_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
def authorize(
|
||||||
|
self,
|
||||||
|
projection: CredentialProjection,
|
||||||
|
*,
|
||||||
|
task_id: str,
|
||||||
|
session_id: str,
|
||||||
|
worker_id: str,
|
||||||
|
audience: str,
|
||||||
|
consumer_service: str,
|
||||||
|
operation: str,
|
||||||
|
path: str,
|
||||||
|
now: datetime | None = None,
|
||||||
|
) -> None:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
request = projection.request
|
||||||
|
actual = (task_id, session_id, worker_id, audience, consumer_service)
|
||||||
|
expected = (
|
||||||
|
request.task_id,
|
||||||
|
request.session_id,
|
||||||
|
request.worker_id,
|
||||||
|
request.audience,
|
||||||
|
request.consumer_service,
|
||||||
|
)
|
||||||
|
if actual != expected:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_SCOPE_MISMATCH", "credential scope does not match the caller"
|
||||||
|
)
|
||||||
|
if operation not in request.operations or not request.binding.allows_path(path):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_OPERATION_DENIED", "credential operation or path is not approved"
|
||||||
|
)
|
||||||
|
if now < request.not_before or now >= projection.expires_at:
|
||||||
|
raise CredentialUnavailable("CREDENTIAL_EXPIRED", "credential is outside its valid window")
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT state, metadata_json FROM credentials WHERE credential_id=? AND reference=?",
|
||||||
|
(projection.credential_id, projection.reference),
|
||||||
|
).fetchone()
|
||||||
|
if row is None or row["state"] != CredentialState.PROJECTED.value:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_NOT_PROJECTED", "credential is not in projected state"
|
||||||
|
)
|
||||||
|
metadata = json.loads(row["metadata_json"])
|
||||||
|
count = int(metadata.get("use_count", 0))
|
||||||
|
if count >= request.max_uses:
|
||||||
|
raise CredentialUnavailable("CREDENTIAL_USE_LIMIT", "credential use limit reached")
|
||||||
|
if not self.provider.is_usable(projection.credential_id, projection.reference):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_PROVIDER_REJECTED", "provider no longer accepts the credential"
|
||||||
|
)
|
||||||
|
metadata["use_count"] = count + 1
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE credentials SET metadata_json=? WHERE credential_id=?",
|
||||||
|
(canonical_json(metadata), projection.credential_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def revoke(
|
||||||
|
self, credential_id: str, reference: str, now: datetime | None = None
|
||||||
|
) -> CredentialLifecycleResult:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT state, expires_at FROM credentials WHERE credential_id=? AND reference=?",
|
||||||
|
(credential_id, reference),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError(credential_id)
|
||||||
|
expires_at = datetime.fromisoformat(row["expires_at"])
|
||||||
|
if row["state"] == CredentialState.REVOKED.value:
|
||||||
|
return CredentialLifecycleResult(
|
||||||
|
credential_id, reference, CredentialState.REVOKED, expires_at
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.provider.revoke(credential_id, reference)
|
||||||
|
except Exception as error:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_REVOCATION_FAILED", "upstream credential revocation failed"
|
||||||
|
) from error
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE credentials SET state=?, revoked_at=? WHERE credential_id=? AND reference=?",
|
||||||
|
(CredentialState.REVOKED.value, now.isoformat(), credential_id, reference),
|
||||||
|
)
|
||||||
|
return CredentialLifecycleResult(
|
||||||
|
credential_id, reference, CredentialState.REVOKED, expires_at
|
||||||
|
)
|
||||||
|
|
||||||
|
def verify_revoked(self, credential_id: str, reference: str) -> None:
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT state FROM credentials WHERE credential_id=? AND reference=?",
|
||||||
|
(credential_id, reference),
|
||||||
|
).fetchone()
|
||||||
|
if row is None or row["state"] != CredentialState.REVOKED.value:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_REVOCATION_UNRECORDED", "durable revocation record is missing"
|
||||||
|
)
|
||||||
|
if self.provider.is_usable(credential_id, reference):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"CREDENTIAL_REVOCATION_UNVERIFIED",
|
||||||
|
"provider still accepts revoked credential",
|
||||||
|
{"credential_id": credential_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryCredentialProvider:
|
||||||
|
"""Candidate test provider; never suitable as production identity evidence."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._active: dict[str, str] = {}
|
||||||
|
|
||||||
|
def issue(self, request: CredentialRequest, expires_at: datetime) -> tuple[str, str]:
|
||||||
|
credential_id = f"cred-{secrets.token_hex(8)}"
|
||||||
|
reference = f"secret-ref://candidate/{request.task_id}/{credential_id}"
|
||||||
|
self._active[credential_id] = reference
|
||||||
|
return credential_id, reference
|
||||||
|
|
||||||
|
def revoke(self, credential_id: str, reference: str) -> None:
|
||||||
|
if self._active.get(credential_id) != reference:
|
||||||
|
raise CredentialUnavailable("CREDENTIAL_NOT_FOUND", "credential is not active")
|
||||||
|
del self._active[credential_id]
|
||||||
|
|
||||||
|
def is_usable(self, credential_id: str, reference: str) -> bool:
|
||||||
|
return self._active.get(credential_id) == reference
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
"""Stable error types and codes for the autonomous-release candidate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class AutonomousReleaseError(RuntimeError):
|
||||||
|
"""Base error with a stable machine-readable code."""
|
||||||
|
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
details: Mapping[str, Any] | None = None
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
if not self.details:
|
||||||
|
return f"{self.code}: {self.message}"
|
||||||
|
return f"{self.code}: {self.message} ({dict(self.details)})"
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyDenied(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalRequired(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityMismatch(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialUnavailable(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceIncomplete(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseBlocked(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StateConflict(AutonomousReleaseError):
|
||||||
|
pass
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
"""Durable, replayable and tamper-evident evidence closure."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json
|
||||||
|
from typing import Any, Mapping, TYPE_CHECKING
|
||||||
|
|
||||||
|
from .errors import EvidenceIncomplete
|
||||||
|
from .models import sha256_json
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class EvidenceEvent:
|
||||||
|
event_type: str
|
||||||
|
task_id: str
|
||||||
|
payload: Mapping[str, Any]
|
||||||
|
previous_digest: str | None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
def event_digest(self) -> str:
|
||||||
|
return sha256_json(
|
||||||
|
{
|
||||||
|
"event_type": self.event_type,
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"payload": dict(self.payload),
|
||||||
|
"previous_digest": self.previous_digest,
|
||||||
|
"created_at": self.created_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceChain:
|
||||||
|
"""Append-only chain optionally backed by the Candidate state database."""
|
||||||
|
|
||||||
|
def __init__(self, store: StateStore | None = None, task_id: str | None = None) -> None:
|
||||||
|
if (store is None) != (task_id is None):
|
||||||
|
raise ValueError("store and task_id must be supplied together")
|
||||||
|
self.store = store
|
||||||
|
self.task_id = task_id
|
||||||
|
self._records: list[tuple[EvidenceEvent, str]] = []
|
||||||
|
if store is not None and task_id is not None:
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self) -> None:
|
||||||
|
assert self.store is not None and self.task_id is not None
|
||||||
|
self._records = []
|
||||||
|
for row in self.store.list_events(self.task_id):
|
||||||
|
event = EvidenceEvent(
|
||||||
|
event_type=str(row["event_type"]),
|
||||||
|
task_id=str(row["task_id"]),
|
||||||
|
payload=json.loads(str(row["payload_json"])),
|
||||||
|
previous_digest=row["previous_digest"],
|
||||||
|
created_at=datetime.fromisoformat(str(row["created_at"])),
|
||||||
|
)
|
||||||
|
self._records.append((event, str(row["digest"])))
|
||||||
|
|
||||||
|
def append(
|
||||||
|
self, event_type: str, task_id: str, payload: Mapping[str, Any]
|
||||||
|
) -> EvidenceEvent:
|
||||||
|
if not event_type or not task_id:
|
||||||
|
raise EvidenceIncomplete("EVIDENCE_IDENTITY_REQUIRED", "event and task IDs are required")
|
||||||
|
if self.task_id is not None and task_id != self.task_id:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"EVIDENCE_TASK_MISMATCH", "event task does not match the durable chain"
|
||||||
|
)
|
||||||
|
if payload.get("secrets_included") is not False:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"EVIDENCE_SECRET_MARKER_REQUIRED",
|
||||||
|
"evidence must explicitly assert secrets_included=false",
|
||||||
|
)
|
||||||
|
previous = self._records[-1][1] if self._records else None
|
||||||
|
event = EvidenceEvent(
|
||||||
|
event_type=event_type,
|
||||||
|
task_id=task_id,
|
||||||
|
payload=dict(payload),
|
||||||
|
previous_digest=previous,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
event_digest = event.event_digest()
|
||||||
|
if self.store is not None:
|
||||||
|
self.store.append_event(
|
||||||
|
task_id,
|
||||||
|
event_type,
|
||||||
|
payload,
|
||||||
|
previous,
|
||||||
|
event_digest,
|
||||||
|
event.created_at,
|
||||||
|
)
|
||||||
|
self._records.append((event, event_digest))
|
||||||
|
return event
|
||||||
|
|
||||||
|
def verify(self) -> bool:
|
||||||
|
previous: str | None = None
|
||||||
|
for event, recorded_digest in self._records:
|
||||||
|
if event.previous_digest != previous or event.event_digest() != recorded_digest:
|
||||||
|
return False
|
||||||
|
previous = recorded_digest
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self, required_event_types: tuple[str, ...]) -> str:
|
||||||
|
if not self._records:
|
||||||
|
raise EvidenceIncomplete("EMPTY_EVIDENCE_CHAIN", "cannot close an empty evidence chain")
|
||||||
|
if not self.verify():
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"EVIDENCE_CHAIN_INVALID", "evidence chain verification failed"
|
||||||
|
)
|
||||||
|
found = {event.event_type for event, _ in self._records}
|
||||||
|
missing = sorted(set(required_event_types) - found)
|
||||||
|
if missing:
|
||||||
|
raise EvidenceIncomplete(
|
||||||
|
"EVIDENCE_REQUIREMENT_MISSING",
|
||||||
|
"required evidence events are absent",
|
||||||
|
{"missing": missing},
|
||||||
|
)
|
||||||
|
return self._records[-1][1]
|
||||||
|
|
||||||
|
def closure_digest(self) -> str:
|
||||||
|
return self.close(())
|
||||||
|
|
||||||
|
def export(self) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"event_type": event.event_type,
|
||||||
|
"task_id": event.task_id,
|
||||||
|
"payload": dict(event.payload),
|
||||||
|
"previous_digest": event.previous_digest,
|
||||||
|
"digest": recorded_digest,
|
||||||
|
"created_at": event.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for event, recorded_digest in self._records
|
||||||
|
]
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
"""Native Candidate execution lifecycle with mandatory cleanup evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from .evidence import EvidenceChain
|
||||||
|
from .native_adapters import CandidateNativeAdapter, NativeSession
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class NativeCanaryResult:
|
||||||
|
provider: str
|
||||||
|
broker_session_id: str
|
||||||
|
provider_session_id: str
|
||||||
|
message_id: str
|
||||||
|
receipt: str
|
||||||
|
broker_session_verified: bool
|
||||||
|
openclaw_native_child_session_verified: bool
|
||||||
|
codex_child_session_verified: bool
|
||||||
|
simulation: bool
|
||||||
|
session_closed: bool
|
||||||
|
secrets_included: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionManager:
|
||||||
|
def __init__(self, evidence: EvidenceChain) -> None:
|
||||||
|
self.evidence = evidence
|
||||||
|
|
||||||
|
def run_native_canary(
|
||||||
|
self,
|
||||||
|
adapter: CandidateNativeAdapter,
|
||||||
|
*,
|
||||||
|
task_id: str,
|
||||||
|
parent_session_id: str,
|
||||||
|
recipient_binding: Mapping[str, Any],
|
||||||
|
text: str,
|
||||||
|
) -> NativeCanaryResult:
|
||||||
|
session: NativeSession | None = None
|
||||||
|
closed = False
|
||||||
|
try:
|
||||||
|
session = adapter.spawn(task_id, parent_session_id, recipient_binding)
|
||||||
|
verification = adapter.verify(session)
|
||||||
|
message = adapter.send_text(session, text)
|
||||||
|
adapter.interrupt(session)
|
||||||
|
adapter.close(session)
|
||||||
|
closed = True
|
||||||
|
event_type = (
|
||||||
|
"NATIVE_SESSION_SIMULATION"
|
||||||
|
if verification.simulation
|
||||||
|
else "NATIVE_SESSION_VERIFIED"
|
||||||
|
)
|
||||||
|
self.evidence.append(
|
||||||
|
event_type,
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"provider": session.provider,
|
||||||
|
"broker_session_id": session.broker_session_id,
|
||||||
|
"provider_session_id": session.provider_session_id,
|
||||||
|
"message_id": message.message_id,
|
||||||
|
"receipt": message.receipt,
|
||||||
|
"broker_session_verified": verification.broker_session_verified,
|
||||||
|
"openclaw_native_child_session_verified": (
|
||||||
|
verification.openclaw_native_child_session_verified
|
||||||
|
),
|
||||||
|
"codex_child_session_verified": verification.codex_child_session_verified,
|
||||||
|
"simulation": verification.simulation,
|
||||||
|
"session_closed": True,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return NativeCanaryResult(
|
||||||
|
provider=session.provider,
|
||||||
|
broker_session_id=session.broker_session_id,
|
||||||
|
provider_session_id=session.provider_session_id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
receipt=message.receipt,
|
||||||
|
broker_session_verified=verification.broker_session_verified,
|
||||||
|
openclaw_native_child_session_verified=(
|
||||||
|
verification.openclaw_native_child_session_verified
|
||||||
|
),
|
||||||
|
codex_child_session_verified=verification.codex_child_session_verified,
|
||||||
|
simulation=verification.simulation,
|
||||||
|
session_closed=True,
|
||||||
|
secrets_included=False,
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
if session is not None and not closed:
|
||||||
|
try:
|
||||||
|
adapter.close(session)
|
||||||
|
closed = True
|
||||||
|
except Exception:
|
||||||
|
closed = False
|
||||||
|
self.evidence.append(
|
||||||
|
"NATIVE_SESSION_FAILED",
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"provider": adapter.provider,
|
||||||
|
"error_code": type(error).__name__,
|
||||||
|
"session_closed": closed,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
raise
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
"""API-only Gitea deploy-key lifecycle canary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping, Protocol, Sequence
|
||||||
|
|
||||||
|
from .errors import CredentialUnavailable, StateConflict
|
||||||
|
|
||||||
|
|
||||||
|
GITEA_REFERENCE = "systemd-credential:gitea-admin-api-candidate"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class EphemeralKeyProjection:
|
||||||
|
public_key: str
|
||||||
|
private_key_reference: str
|
||||||
|
|
||||||
|
|
||||||
|
class KeyProjectionProvider(Protocol):
|
||||||
|
def create(self, task_id: str, branch: str) -> EphemeralKeyProjection: ...
|
||||||
|
|
||||||
|
def delete(self, reference: str) -> None: ...
|
||||||
|
|
||||||
|
def is_present(self, reference: str) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaDeployKeyProvider(Protocol):
|
||||||
|
simulation: bool
|
||||||
|
|
||||||
|
def list_deploy_keys(
|
||||||
|
self, repository: str, credential_reference: str
|
||||||
|
) -> Sequence[Mapping[str, Any]]: ...
|
||||||
|
|
||||||
|
def create_deploy_key(
|
||||||
|
self,
|
||||||
|
repository: str,
|
||||||
|
title: str,
|
||||||
|
public_key: str,
|
||||||
|
credential_reference: str,
|
||||||
|
) -> Mapping[str, Any]: ...
|
||||||
|
|
||||||
|
def delete_deploy_key(
|
||||||
|
self, repository: str, key_id: int, credential_reference: str
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class SSHAuthenticationProbe(Protocol):
|
||||||
|
def authenticate(
|
||||||
|
self, repository: str, branch: str, private_key_reference: str
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GiteaCanaryResult:
|
||||||
|
key_created: bool
|
||||||
|
key_deleted: bool
|
||||||
|
api_key_absent_after_delete: bool
|
||||||
|
post_revoke_auth_rejected: bool
|
||||||
|
credential_projection_deleted: bool
|
||||||
|
gitea_admin_api_lifecycle_canary: bool
|
||||||
|
simulation: bool
|
||||||
|
secrets_included: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaDeployKeyCanary:
|
||||||
|
"""Exercise only repository deploy-key list/create/delete provider operations."""
|
||||||
|
|
||||||
|
repository = "aios/aios"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
gitea: GiteaDeployKeyProvider,
|
||||||
|
projections: KeyProjectionProvider,
|
||||||
|
ssh_probe: SSHAuthenticationProbe,
|
||||||
|
) -> None:
|
||||||
|
self.gitea = gitea
|
||||||
|
self.projections = projections
|
||||||
|
self.ssh_probe = ssh_probe
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self, *, task_id: str, branch: str, credential_reference: str
|
||||||
|
) -> GiteaCanaryResult:
|
||||||
|
if credential_reference != GITEA_REFERENCE:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"BLOCKED_GITEA_ADMIN_CREDENTIAL_REQUIRED",
|
||||||
|
"exact Candidate Gitea Admin API reference is required",
|
||||||
|
)
|
||||||
|
if not task_id or not branch.startswith("patch/"):
|
||||||
|
raise StateConflict("GITEA_CANARY_BINDING_INVALID", "task and patch branch are required")
|
||||||
|
projection: EphemeralKeyProjection | None = None
|
||||||
|
key_id: int | None = None
|
||||||
|
key_created = False
|
||||||
|
key_deleted = False
|
||||||
|
api_absent = False
|
||||||
|
auth_rejected = False
|
||||||
|
projection_deleted = False
|
||||||
|
title = f"aios-candidate-{task_id}"
|
||||||
|
try:
|
||||||
|
projection = self.projections.create(task_id, branch)
|
||||||
|
if (
|
||||||
|
not projection.public_key.startswith("ssh-ed25519 ")
|
||||||
|
or not projection.private_key_reference.startswith("systemd-credential:")
|
||||||
|
):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"GITEA_CANARY_KEY_PROJECTION_INVALID",
|
||||||
|
"ephemeral key projection is not reference-only",
|
||||||
|
)
|
||||||
|
before = self.gitea.list_deploy_keys(self.repository, credential_reference)
|
||||||
|
if any(item.get("title") == title for item in before):
|
||||||
|
raise StateConflict("GITEA_CANARY_KEY_COLLISION", "canary key title already exists")
|
||||||
|
created = self.gitea.create_deploy_key(
|
||||||
|
self.repository,
|
||||||
|
title,
|
||||||
|
projection.public_key,
|
||||||
|
credential_reference,
|
||||||
|
)
|
||||||
|
raw_id = created.get("id")
|
||||||
|
if not isinstance(raw_id, int) or created.get("key") != projection.public_key:
|
||||||
|
raise StateConflict("GITEA_CANARY_CREATE_INVALID", "created key response is invalid")
|
||||||
|
key_id = raw_id
|
||||||
|
key_created = True
|
||||||
|
visible = self.gitea.list_deploy_keys(self.repository, credential_reference)
|
||||||
|
if not any(item.get("id") == key_id for item in visible):
|
||||||
|
raise StateConflict("GITEA_CANARY_KEY_NOT_VISIBLE", "created key is not listed")
|
||||||
|
if not self.ssh_probe.authenticate(
|
||||||
|
self.repository, branch, projection.private_key_reference
|
||||||
|
):
|
||||||
|
raise StateConflict("GITEA_CANARY_SSH_AUTH_FAILED", "deploy key could not authenticate")
|
||||||
|
self.gitea.delete_deploy_key(self.repository, key_id, credential_reference)
|
||||||
|
key_deleted = True
|
||||||
|
key_id = None
|
||||||
|
after = self.gitea.list_deploy_keys(self.repository, credential_reference)
|
||||||
|
api_absent = not any(item.get("title") == title for item in after)
|
||||||
|
if not api_absent:
|
||||||
|
raise StateConflict("GITEA_CANARY_DELETE_UNVERIFIED", "deleted key remains visible")
|
||||||
|
self.projections.delete(projection.private_key_reference)
|
||||||
|
projection_deleted = not self.projections.is_present(projection.private_key_reference)
|
||||||
|
auth_rejected = not self.ssh_probe.authenticate(
|
||||||
|
self.repository, branch, projection.private_key_reference
|
||||||
|
)
|
||||||
|
if not projection_deleted or not auth_rejected:
|
||||||
|
raise StateConflict(
|
||||||
|
"GITEA_CANARY_REVOCATION_UNVERIFIED",
|
||||||
|
"projection deletion or post-revocation SSH rejection failed",
|
||||||
|
)
|
||||||
|
simulated = bool(getattr(self.gitea, "simulation", True))
|
||||||
|
return GiteaCanaryResult(
|
||||||
|
key_created=key_created,
|
||||||
|
key_deleted=key_deleted,
|
||||||
|
api_key_absent_after_delete=api_absent,
|
||||||
|
post_revoke_auth_rejected=auth_rejected,
|
||||||
|
credential_projection_deleted=projection_deleted,
|
||||||
|
gitea_admin_api_lifecycle_canary=not simulated,
|
||||||
|
simulation=simulated,
|
||||||
|
secrets_included=False,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if key_id is not None:
|
||||||
|
try:
|
||||||
|
self.gitea.delete_deploy_key(
|
||||||
|
self.repository, key_id, credential_reference
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if projection is not None and self.projections.is_present(
|
||||||
|
projection.private_key_reference
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self.projections.delete(projection.private_key_reference)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
+376
@@ -0,0 +1,376 @@
|
|||||||
|
"""Typed contracts for the dormant autonomous-release candidate.
|
||||||
|
|
||||||
|
This package is dependency-free by design so it can be audited and exercised in
|
||||||
|
an isolated candidate worktree before any production service is installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from enum import Enum
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from typing import Any, Mapping, Sequence
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now() -> datetime:
|
||||||
|
"""Return an aware UTC timestamp."""
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_json(value: Any) -> str:
|
||||||
|
"""Serialize *value* deterministically for signatures and evidence hashes."""
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_json(value: Any) -> str:
|
||||||
|
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def is_sha256(value: str) -> bool:
|
||||||
|
return len(value) == 64 and all(ch in "0123456789abcdef" for ch in value)
|
||||||
|
|
||||||
|
|
||||||
|
def is_git_oid(value: str) -> bool:
|
||||||
|
return len(value) == 40 and all(ch in "0123456789abcdef" for ch in value)
|
||||||
|
|
||||||
|
|
||||||
|
class RiskClass(str, Enum):
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
CRITICAL = "critical"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskState(str, Enum):
|
||||||
|
CREATED = "CREATED"
|
||||||
|
PLANNED = "PLANNED"
|
||||||
|
READY = "READY"
|
||||||
|
RUNNING = "RUNNING"
|
||||||
|
BLOCKED = "BLOCKED"
|
||||||
|
FAILED = "FAILED"
|
||||||
|
COMPLETED = "COMPLETED"
|
||||||
|
READY_FOR_OWNER_APPROVAL = "READY_FOR_OWNER_APPROVAL"
|
||||||
|
|
||||||
|
|
||||||
|
class StepState(str, Enum):
|
||||||
|
PENDING = "PENDING"
|
||||||
|
READY = "READY"
|
||||||
|
CLAIMED = "CLAIMED"
|
||||||
|
RUNNING = "RUNNING"
|
||||||
|
BLOCKED = "BLOCKED"
|
||||||
|
FAILED = "FAILED"
|
||||||
|
COMPLETED = "COMPLETED"
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialState(str, Enum):
|
||||||
|
REQUESTED = "REQUESTED"
|
||||||
|
ISSUED = "ISSUED"
|
||||||
|
PROJECTED = "PROJECTED"
|
||||||
|
REVOKED = "REVOKED"
|
||||||
|
EXPIRED = "EXPIRED"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ResourceBinding:
|
||||||
|
repository: str
|
||||||
|
branch: str
|
||||||
|
base_oid: str
|
||||||
|
allowed_paths: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.repository or "/" not in self.repository:
|
||||||
|
raise ValueError("repository must be owner/name")
|
||||||
|
if self.branch == "main" or not self.branch.startswith(("patch/", "integration/", "candidate/")):
|
||||||
|
raise ValueError("candidate execution requires a governed non-main branch")
|
||||||
|
if not is_git_oid(self.base_oid):
|
||||||
|
raise ValueError("base_oid must be a full lowercase 40-character Git OID")
|
||||||
|
for path in self.allowed_paths:
|
||||||
|
if not path or path.startswith("/") or ".." in path.split("/"):
|
||||||
|
raise ValueError("allowed_paths must be repository-relative and traversal-free")
|
||||||
|
|
||||||
|
def allows_path(self, path: str) -> bool:
|
||||||
|
if not path or path.startswith("/") or ".." in path.split("/"):
|
||||||
|
return False
|
||||||
|
return any(path == prefix or path.startswith(prefix.rstrip("/") + "/") for prefix in self.allowed_paths)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ApprovalEnvelope:
|
||||||
|
approval_plan_id: str
|
||||||
|
task_id: str
|
||||||
|
task_plan_sha256: str
|
||||||
|
binding: ResourceBinding
|
||||||
|
capabilities: tuple[str, ...]
|
||||||
|
risk: RiskClass
|
||||||
|
not_before: datetime
|
||||||
|
expires_at: datetime
|
||||||
|
nonce: str
|
||||||
|
signing_key_id: str
|
||||||
|
signature: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.expires_at <= self.not_before:
|
||||||
|
raise ValueError("approval expiry must be after not_before")
|
||||||
|
if not is_sha256(self.task_plan_sha256):
|
||||||
|
raise ValueError("task_plan_sha256 must be a SHA-256 hex digest")
|
||||||
|
if not self.approval_plan_id or not self.task_id or not self.nonce:
|
||||||
|
raise ValueError("approval identifiers and nonce are required")
|
||||||
|
|
||||||
|
def payload(self) -> Mapping[str, Any]:
|
||||||
|
return {
|
||||||
|
"approval_plan_id": self.approval_plan_id,
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"task_plan_sha256": self.task_plan_sha256,
|
||||||
|
"repository": self.binding.repository,
|
||||||
|
"branch": self.binding.branch,
|
||||||
|
"base_oid": self.binding.base_oid,
|
||||||
|
"allowed_paths": list(self.binding.allowed_paths),
|
||||||
|
"capabilities": list(self.capabilities),
|
||||||
|
"risk": self.risk.value,
|
||||||
|
"not_before": self.not_before.astimezone(timezone.utc).isoformat(),
|
||||||
|
"expires_at": self.expires_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
"nonce": self.nonce,
|
||||||
|
"signing_key_id": self.signing_key_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkflowStep:
|
||||||
|
step_id: str
|
||||||
|
worker_role: str
|
||||||
|
required_capabilities: tuple[str, ...]
|
||||||
|
depends_on: tuple[str, ...] = ()
|
||||||
|
max_attempts: int = 1
|
||||||
|
timeout_seconds: int = 900
|
||||||
|
allowed_paths: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.step_id or not self.worker_role:
|
||||||
|
raise ValueError("step_id and worker_role are required")
|
||||||
|
if self.max_attempts < 1 or self.max_attempts > 5:
|
||||||
|
raise ValueError("max_attempts must be between 1 and 5")
|
||||||
|
if self.timeout_seconds < 1 or self.timeout_seconds > 86_400:
|
||||||
|
raise ValueError("timeout_seconds must be between 1 and 86400")
|
||||||
|
if self.step_id in self.depends_on:
|
||||||
|
raise ValueError("a step cannot depend on itself")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TaskPlan:
|
||||||
|
task_id: str
|
||||||
|
intent: str
|
||||||
|
binding: ResourceBinding
|
||||||
|
risk: RiskClass
|
||||||
|
steps: tuple[WorkflowStep, ...]
|
||||||
|
evidence_requirements: tuple[str, ...]
|
||||||
|
production_side_effects_allowed: bool = False
|
||||||
|
created_at: datetime = field(default_factory=utc_now)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.task_id or not self.intent:
|
||||||
|
raise ValueError("task_id and intent are required")
|
||||||
|
if self.production_side_effects_allowed:
|
||||||
|
raise ValueError("candidate TaskPlan must not allow production side effects")
|
||||||
|
ids = [step.step_id for step in self.steps]
|
||||||
|
if len(ids) != len(set(ids)):
|
||||||
|
raise ValueError("step IDs must be unique")
|
||||||
|
known = set(ids)
|
||||||
|
for step in self.steps:
|
||||||
|
missing = set(step.depends_on) - known
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"step {step.step_id} has unknown dependencies: {sorted(missing)}")
|
||||||
|
self._validate_acyclic()
|
||||||
|
|
||||||
|
def _validate_acyclic(self) -> None:
|
||||||
|
dependencies = {step.step_id: set(step.depends_on) for step in self.steps}
|
||||||
|
pending = set(dependencies)
|
||||||
|
while pending:
|
||||||
|
ready = {step_id for step_id in pending if not (dependencies[step_id] & pending)}
|
||||||
|
if not ready:
|
||||||
|
raise ValueError("workflow contains a dependency cycle")
|
||||||
|
pending -= ready
|
||||||
|
|
||||||
|
def digest(self) -> str:
|
||||||
|
return sha256_json(
|
||||||
|
{
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"intent": self.intent,
|
||||||
|
"repository": self.binding.repository,
|
||||||
|
"branch": self.binding.branch,
|
||||||
|
"base_oid": self.binding.base_oid,
|
||||||
|
"allowed_paths": list(self.binding.allowed_paths),
|
||||||
|
"risk": self.risk.value,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": step.step_id,
|
||||||
|
"worker_role": step.worker_role,
|
||||||
|
"required_capabilities": list(step.required_capabilities),
|
||||||
|
"depends_on": list(step.depends_on),
|
||||||
|
"max_attempts": step.max_attempts,
|
||||||
|
"timeout_seconds": step.timeout_seconds,
|
||||||
|
"allowed_paths": list(step.allowed_paths),
|
||||||
|
}
|
||||||
|
for step in self.steps
|
||||||
|
],
|
||||||
|
"evidence_requirements": list(self.evidence_requirements),
|
||||||
|
"production_side_effects_allowed": self.production_side_effects_allowed,
|
||||||
|
"created_at": self.created_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkerDescriptor:
|
||||||
|
worker_id: str
|
||||||
|
role: str
|
||||||
|
capabilities: frozenset[str]
|
||||||
|
trust_zone: str
|
||||||
|
endpoint_id: str
|
||||||
|
online: bool = True
|
||||||
|
current_load: int = 0
|
||||||
|
max_concurrency: int = 1
|
||||||
|
|
||||||
|
def can_accept(self, required: Sequence[str], trust_zone: str) -> bool:
|
||||||
|
return (
|
||||||
|
self.online
|
||||||
|
and self.trust_zone == trust_zone
|
||||||
|
and self.current_load < self.max_concurrency
|
||||||
|
and set(required).issubset(self.capabilities)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CredentialRequest:
|
||||||
|
request_id: str
|
||||||
|
task_id: str
|
||||||
|
session_id: str
|
||||||
|
worker_id: str
|
||||||
|
audience: str
|
||||||
|
consumer_service: str
|
||||||
|
binding: ResourceBinding
|
||||||
|
operations: tuple[str, ...]
|
||||||
|
provider: str
|
||||||
|
runtime_user: str
|
||||||
|
approval_plan_id: str
|
||||||
|
not_before: datetime
|
||||||
|
expires_at: datetime
|
||||||
|
nonce: str
|
||||||
|
ttl_seconds: int
|
||||||
|
max_uses: int
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.ttl_seconds < 1 or self.ttl_seconds > 3600:
|
||||||
|
raise ValueError("credential TTL must be between 1 and 3600 seconds")
|
||||||
|
if self.max_uses < 1 or self.max_uses > 1000:
|
||||||
|
raise ValueError("credential max_uses must be between 1 and 1000")
|
||||||
|
if not self.operations:
|
||||||
|
raise ValueError("credential operations cannot be empty")
|
||||||
|
required = (
|
||||||
|
self.request_id,
|
||||||
|
self.task_id,
|
||||||
|
self.session_id,
|
||||||
|
self.worker_id,
|
||||||
|
self.audience,
|
||||||
|
self.consumer_service,
|
||||||
|
self.provider,
|
||||||
|
self.runtime_user,
|
||||||
|
self.approval_plan_id,
|
||||||
|
self.nonce,
|
||||||
|
)
|
||||||
|
if any(not item or item != item.strip() for item in required):
|
||||||
|
raise ValueError("credential identity, scope and approval fields are required")
|
||||||
|
if len(set(self.operations)) != len(self.operations):
|
||||||
|
raise ValueError("credential operations must be unique")
|
||||||
|
if self.not_before.tzinfo is None or self.expires_at.tzinfo is None:
|
||||||
|
raise ValueError("credential time bounds must be timezone aware")
|
||||||
|
if self.expires_at <= self.not_before:
|
||||||
|
raise ValueError("credential expires_at must follow not_before")
|
||||||
|
if (self.expires_at - self.not_before).total_seconds() > self.ttl_seconds:
|
||||||
|
raise ValueError("credential approval window exceeds requested TTL")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CredentialProjection:
|
||||||
|
credential_id: str
|
||||||
|
reference: str
|
||||||
|
request: CredentialRequest
|
||||||
|
state: CredentialState
|
||||||
|
issued_at: datetime
|
||||||
|
expires_at: datetime
|
||||||
|
plaintext_returned: bool = False
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.plaintext_returned:
|
||||||
|
raise ValueError("credential plaintext must never be returned")
|
||||||
|
if self.expires_at <= self.issued_at:
|
||||||
|
raise ValueError("credential expiry must be after issuance")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ResultEnvelope:
|
||||||
|
task_id: str
|
||||||
|
step_id: str
|
||||||
|
worker_id: str
|
||||||
|
session_id: str
|
||||||
|
claim_id: str
|
||||||
|
operation_digest: str
|
||||||
|
result_digest: str
|
||||||
|
evidence_digests: tuple[str, ...]
|
||||||
|
completed_at: datetime = field(default_factory=utc_now)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not is_sha256(self.operation_digest) or not is_sha256(self.result_digest):
|
||||||
|
raise ValueError("operation_digest and result_digest must be SHA-256 values")
|
||||||
|
if any(not is_sha256(digest) for digest in self.evidence_digests):
|
||||||
|
raise ValueError("all evidence digests must be SHA-256 values")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReleasePlan:
|
||||||
|
task_id: str
|
||||||
|
repository: str
|
||||||
|
branch: str
|
||||||
|
version: str
|
||||||
|
target_oid: str
|
||||||
|
tree_oid: str
|
||||||
|
artifact_sha256: str
|
||||||
|
manifest_sha256: str
|
||||||
|
package_closure_sha256: str
|
||||||
|
evidence_closure_oid: str
|
||||||
|
approval_plan_id: str
|
||||||
|
rollback_approval_plan_id: str
|
||||||
|
rollback_artifact_sha256: str
|
||||||
|
expected_tag_oid: str
|
||||||
|
expected_target_oid: str
|
||||||
|
production_config_sha256: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
for name in ("target_oid", "tree_oid", "evidence_closure_oid", "expected_target_oid"):
|
||||||
|
if not is_git_oid(getattr(self, name)):
|
||||||
|
raise ValueError(f"{name} must be a full lowercase 40-character Git OID")
|
||||||
|
for name in (
|
||||||
|
"artifact_sha256",
|
||||||
|
"manifest_sha256",
|
||||||
|
"package_closure_sha256",
|
||||||
|
"rollback_artifact_sha256",
|
||||||
|
"production_config_sha256",
|
||||||
|
):
|
||||||
|
if not is_sha256(getattr(self, name)):
|
||||||
|
raise ValueError(f"{name} must be a SHA-256 hex digest")
|
||||||
|
if not is_git_oid(self.expected_tag_oid):
|
||||||
|
raise ValueError("expected_tag_oid must be a full lowercase 40-character Git OID")
|
||||||
|
if (
|
||||||
|
not self.task_id
|
||||||
|
or "/" not in self.repository
|
||||||
|
or not self.branch.startswith("integration/")
|
||||||
|
or not self.version
|
||||||
|
or not self.approval_plan_id
|
||||||
|
or not self.rollback_approval_plan_id
|
||||||
|
):
|
||||||
|
raise ValueError("release identifiers are required")
|
||||||
|
if self.approval_plan_id == self.rollback_approval_plan_id:
|
||||||
|
raise ValueError("deployment and rollback approvals must be distinct")
|
||||||
|
if self.target_oid != self.expected_target_oid:
|
||||||
|
raise ValueError("target_oid must equal expected_target_oid")
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
"""Candidate-only OpenClaw and Codex native session adapter contracts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping, Protocol, Sequence
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from .errors import StateConflict
|
||||||
|
|
||||||
|
|
||||||
|
class NativeProvider(Protocol):
|
||||||
|
simulation: bool
|
||||||
|
|
||||||
|
def spawn(
|
||||||
|
self, request: Mapping[str, Any], credential_reference: str
|
||||||
|
) -> Mapping[str, Any]: ...
|
||||||
|
|
||||||
|
def status(
|
||||||
|
self, provider_session_id: str, credential_reference: str
|
||||||
|
) -> Mapping[str, Any]: ...
|
||||||
|
|
||||||
|
def list_tree(
|
||||||
|
self, parent_session_id: str, credential_reference: str
|
||||||
|
) -> Sequence[Mapping[str, Any]]: ...
|
||||||
|
|
||||||
|
def send_text(
|
||||||
|
self,
|
||||||
|
provider_session_id: str,
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
credential_reference: str,
|
||||||
|
) -> Mapping[str, Any]: ...
|
||||||
|
|
||||||
|
def interrupt(self, provider_session_id: str, credential_reference: str) -> None: ...
|
||||||
|
|
||||||
|
def close(self, provider_session_id: str, credential_reference: str) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class NativeSession:
|
||||||
|
provider: str
|
||||||
|
broker_session_id: str
|
||||||
|
provider_session_id: str
|
||||||
|
task_id: str
|
||||||
|
parent_session_id: str
|
||||||
|
recipient_binding: Mapping[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SessionVerification:
|
||||||
|
broker_session_verified: bool
|
||||||
|
openclaw_native_child_session_verified: bool
|
||||||
|
codex_child_session_verified: bool
|
||||||
|
simulation: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TextResult:
|
||||||
|
content_type: str
|
||||||
|
encoding: str
|
||||||
|
image_only: bool
|
||||||
|
task_id: str
|
||||||
|
message_id: str
|
||||||
|
receipt: str
|
||||||
|
recipient_binding: Mapping[str, str]
|
||||||
|
secrets_included: bool
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateNativeAdapter:
|
||||||
|
provider = ""
|
||||||
|
required_reference = ""
|
||||||
|
|
||||||
|
def __init__(self, client: NativeProvider, credential_reference: str) -> None:
|
||||||
|
if credential_reference != self.required_reference:
|
||||||
|
raise StateConflict(
|
||||||
|
"CANDIDATE_CREDENTIAL_REFERENCE_MISMATCH",
|
||||||
|
"adapter requires its exact Candidate credential reference",
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
self.credential_reference = credential_reference
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _recipient(value: Mapping[str, Any]) -> dict[str, str]:
|
||||||
|
recipient_id = value.get("recipient_id")
|
||||||
|
if (
|
||||||
|
not isinstance(recipient_id, str)
|
||||||
|
or not recipient_id
|
||||||
|
or isinstance(value.get("recipients"), (list, tuple))
|
||||||
|
):
|
||||||
|
raise StateConflict(
|
||||||
|
"RECIPIENT_BINDING_REQUIRED",
|
||||||
|
"one trusted TaskPlan recipient binding is required",
|
||||||
|
)
|
||||||
|
return {str(key): str(item) for key, item in value.items()}
|
||||||
|
|
||||||
|
def spawn(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
parent_session_id: str,
|
||||||
|
recipient_binding: Mapping[str, Any],
|
||||||
|
) -> NativeSession:
|
||||||
|
if not task_id or not parent_session_id:
|
||||||
|
raise StateConflict("NATIVE_SESSION_BINDING_REQUIRED", "task and parent are required")
|
||||||
|
recipient = self._recipient(recipient_binding)
|
||||||
|
response = self.client.spawn(
|
||||||
|
{
|
||||||
|
"task_id": task_id,
|
||||||
|
"parent_session_id": parent_session_id,
|
||||||
|
"recipient_binding": recipient,
|
||||||
|
"candidate_only": True,
|
||||||
|
"production_session_access": False,
|
||||||
|
},
|
||||||
|
self.credential_reference,
|
||||||
|
)
|
||||||
|
provider_session_id = response.get("provider_session_id")
|
||||||
|
if (
|
||||||
|
not isinstance(provider_session_id, str)
|
||||||
|
or not provider_session_id
|
||||||
|
or response.get("task_id") != task_id
|
||||||
|
or response.get("parent_session_id") != parent_session_id
|
||||||
|
or response.get("candidate_only") is not True
|
||||||
|
or response.get("production_session_access") is not False
|
||||||
|
):
|
||||||
|
raise StateConflict(
|
||||||
|
"NATIVE_SESSION_PROVISIONING_INVALID",
|
||||||
|
"provider response is not bound to this Candidate task",
|
||||||
|
)
|
||||||
|
broker_id = f"broker-{uuid.uuid4()}"
|
||||||
|
if broker_id == provider_session_id:
|
||||||
|
raise StateConflict("SESSION_ID_COLLISION", "broker and provider IDs must differ")
|
||||||
|
return NativeSession(
|
||||||
|
provider=self.provider,
|
||||||
|
broker_session_id=broker_id,
|
||||||
|
provider_session_id=provider_session_id,
|
||||||
|
task_id=task_id,
|
||||||
|
parent_session_id=parent_session_id,
|
||||||
|
recipient_binding=recipient,
|
||||||
|
)
|
||||||
|
|
||||||
|
def verify(self, session: NativeSession) -> SessionVerification:
|
||||||
|
self._validate_session(session)
|
||||||
|
status = self.client.status(session.provider_session_id, self.credential_reference)
|
||||||
|
tree = self.client.list_tree(session.parent_session_id, self.credential_reference)
|
||||||
|
status_valid = (
|
||||||
|
status.get("provider_session_id") == session.provider_session_id
|
||||||
|
and status.get("state") in {"PROVISIONED", "ACTIVE", "RUNNING"}
|
||||||
|
)
|
||||||
|
tree_valid = any(
|
||||||
|
item.get("provider_session_id") == session.provider_session_id
|
||||||
|
and item.get("parent_session_id") == session.parent_session_id
|
||||||
|
for item in tree
|
||||||
|
)
|
||||||
|
if not status_valid or not tree_valid:
|
||||||
|
raise StateConflict(
|
||||||
|
"NATIVE_SESSION_VERIFICATION_FAILED",
|
||||||
|
"provider status or parent/child tree is inconsistent",
|
||||||
|
)
|
||||||
|
simulated = bool(getattr(self.client, "simulation", True))
|
||||||
|
return SessionVerification(
|
||||||
|
broker_session_verified=True,
|
||||||
|
openclaw_native_child_session_verified=(
|
||||||
|
self.provider == "openclaw" and not simulated
|
||||||
|
),
|
||||||
|
codex_child_session_verified=(self.provider == "codex" and not simulated),
|
||||||
|
simulation=simulated,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_text(self, session: NativeSession, text: str) -> TextResult:
|
||||||
|
self._validate_session(session)
|
||||||
|
if not isinstance(text, str) or not text:
|
||||||
|
raise StateConflict("TEXT_RESULT_REQUIRED", "non-empty text is required")
|
||||||
|
response = self.client.send_text(
|
||||||
|
session.provider_session_id,
|
||||||
|
{
|
||||||
|
"task_id": session.task_id,
|
||||||
|
"content_type": "text/plain",
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"image_only": False,
|
||||||
|
"text": text,
|
||||||
|
"recipient_binding": dict(session.recipient_binding),
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
self.credential_reference,
|
||||||
|
)
|
||||||
|
required = {
|
||||||
|
"content_type": "text/plain",
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"image_only": False,
|
||||||
|
"task_id": session.task_id,
|
||||||
|
"recipient_binding": dict(session.recipient_binding),
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
if any(response.get(key) != value for key, value in required.items()):
|
||||||
|
raise StateConflict("TEXT_RESULT_CONTRACT_INVALID", "text result binding is invalid")
|
||||||
|
message_id = response.get("message_id")
|
||||||
|
receipt = response.get("receipt")
|
||||||
|
if not isinstance(message_id, str) or not message_id or not isinstance(receipt, str) or not receipt:
|
||||||
|
raise StateConflict("TEXT_RESULT_RECEIPT_REQUIRED", "message and receipt are required")
|
||||||
|
return TextResult(
|
||||||
|
content_type="text/plain",
|
||||||
|
encoding="utf-8",
|
||||||
|
image_only=False,
|
||||||
|
task_id=session.task_id,
|
||||||
|
message_id=message_id,
|
||||||
|
receipt=receipt,
|
||||||
|
recipient_binding=dict(session.recipient_binding),
|
||||||
|
secrets_included=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def status(self, session: NativeSession) -> str:
|
||||||
|
self._validate_session(session)
|
||||||
|
response = self.client.status(session.provider_session_id, self.credential_reference)
|
||||||
|
if response.get("provider_session_id") != session.provider_session_id:
|
||||||
|
raise StateConflict("NATIVE_SESSION_STATUS_INVALID", "provider session ID changed")
|
||||||
|
state = response.get("state")
|
||||||
|
if not isinstance(state, str) or not state:
|
||||||
|
raise StateConflict("NATIVE_SESSION_STATUS_INVALID", "provider state is missing")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def interrupt(self, session: NativeSession) -> None:
|
||||||
|
self._validate_session(session)
|
||||||
|
self.client.interrupt(session.provider_session_id, self.credential_reference)
|
||||||
|
if self.status(session) != "INTERRUPTED":
|
||||||
|
raise StateConflict("NATIVE_SESSION_INTERRUPT_FAILED", "interrupt was not observed")
|
||||||
|
|
||||||
|
def close(self, session: NativeSession) -> None:
|
||||||
|
self._validate_session(session)
|
||||||
|
self.client.close(session.provider_session_id, self.credential_reference)
|
||||||
|
if self.status(session) != "CLOSED":
|
||||||
|
raise StateConflict("NATIVE_SESSION_CLOSE_FAILED", "close was not observed")
|
||||||
|
|
||||||
|
def _validate_session(self, session: NativeSession) -> None:
|
||||||
|
if session.provider != self.provider:
|
||||||
|
raise StateConflict("NATIVE_SESSION_PROVIDER_MISMATCH", "session provider differs")
|
||||||
|
if session.broker_session_id == session.provider_session_id:
|
||||||
|
raise StateConflict("SESSION_ID_COLLISION", "broker and provider IDs must differ")
|
||||||
|
|
||||||
|
|
||||||
|
class OpenClawNativeAdapter(CandidateNativeAdapter):
|
||||||
|
provider = "openclaw"
|
||||||
|
required_reference = "systemd-credential:openclaw-gateway-candidate"
|
||||||
|
|
||||||
|
|
||||||
|
class CodexNativeAdapter(CandidateNativeAdapter):
|
||||||
|
provider = "codex"
|
||||||
|
required_reference = "systemd-credential:codex-app-server-candidate"
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
"""High-level autonomous release orchestration facade."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .evidence import EvidenceChain
|
||||||
|
from .models import TaskPlan
|
||||||
|
from .policy import ReleasePolicy
|
||||||
|
from .workflow import WorkflowEngine
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class OrchestrationResult:
|
||||||
|
task_id: str
|
||||||
|
plan_digest: str
|
||||||
|
evidence_closure: str | None
|
||||||
|
production_side_effects: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AutonomousReleaseOrchestrator:
|
||||||
|
"""Coordinates candidate workflows while keeping production disabled."""
|
||||||
|
|
||||||
|
def __init__(self, workflow: WorkflowEngine, policy: ReleasePolicy) -> None:
|
||||||
|
self.workflow = workflow
|
||||||
|
self.policy = policy
|
||||||
|
self.evidence: EvidenceChain | None = None
|
||||||
|
|
||||||
|
def create(self, plan: TaskPlan) -> OrchestrationResult:
|
||||||
|
self.policy.validate_task(plan)
|
||||||
|
self.workflow.plan(plan)
|
||||||
|
self.evidence = EvidenceChain(self.workflow.store, plan.task_id)
|
||||||
|
self.evidence.append(
|
||||||
|
"TASK_CREATED",
|
||||||
|
plan.task_id,
|
||||||
|
{"plan_digest": plan.digest(), "secrets_included": False},
|
||||||
|
)
|
||||||
|
return OrchestrationResult(
|
||||||
|
task_id=plan.task_id,
|
||||||
|
plan_digest=plan.digest(),
|
||||||
|
evidence_closure=None,
|
||||||
|
production_side_effects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def close_evidence(self, task_id: str) -> OrchestrationResult:
|
||||||
|
if self.evidence is None or self.evidence.task_id != task_id:
|
||||||
|
self.evidence = EvidenceChain(self.workflow.store, task_id)
|
||||||
|
closure = self.evidence.closure_digest()
|
||||||
|
return OrchestrationResult(
|
||||||
|
task_id=task_id,
|
||||||
|
plan_digest="",
|
||||||
|
evidence_closure=closure,
|
||||||
|
production_side_effects=False,
|
||||||
|
)
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
"""Deterministic task planning for autonomous candidate releases."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .models import ResourceBinding, RiskClass, TaskPlan, WorkflowStep
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PlanningRequest:
|
||||||
|
task_id: str
|
||||||
|
intent: str
|
||||||
|
binding: ResourceBinding
|
||||||
|
risk: RiskClass = RiskClass.MEDIUM
|
||||||
|
|
||||||
|
|
||||||
|
class DeterministicPlanner:
|
||||||
|
"""Create a fixed governed DAG without granting production capabilities."""
|
||||||
|
|
||||||
|
def build(self, request: PlanningRequest) -> TaskPlan:
|
||||||
|
steps = (
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="architecture-review",
|
||||||
|
worker_role="hermes-review",
|
||||||
|
required_capabilities=("architecture.review", "risk.analysis"),
|
||||||
|
allowed_paths=request.binding.allowed_paths,
|
||||||
|
),
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="implementation",
|
||||||
|
worker_role="dedicated-writer",
|
||||||
|
required_capabilities=("commit_file.atomic", "git.push.patch_branch"),
|
||||||
|
depends_on=("architecture-review",),
|
||||||
|
max_attempts=2,
|
||||||
|
allowed_paths=request.binding.allowed_paths,
|
||||||
|
),
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="code-review",
|
||||||
|
worker_role="codex-review",
|
||||||
|
required_capabilities=("code.review", "git.diff.read"),
|
||||||
|
depends_on=("implementation",),
|
||||||
|
allowed_paths=request.binding.allowed_paths,
|
||||||
|
),
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="validation",
|
||||||
|
worker_role="openclaw-shadow",
|
||||||
|
required_capabilities=("test.execute", "artifact.verify"),
|
||||||
|
depends_on=("code-review",),
|
||||||
|
max_attempts=2,
|
||||||
|
allowed_paths=request.binding.allowed_paths,
|
||||||
|
),
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="evidence-finalize",
|
||||||
|
worker_role="evidence-controller",
|
||||||
|
required_capabilities=("evidence.finalize",),
|
||||||
|
depends_on=("validation",),
|
||||||
|
allowed_paths=request.binding.allowed_paths,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return TaskPlan(
|
||||||
|
task_id=request.task_id,
|
||||||
|
intent=request.intent,
|
||||||
|
binding=request.binding,
|
||||||
|
risk=request.risk,
|
||||||
|
steps=steps,
|
||||||
|
evidence_requirements=(
|
||||||
|
"git-ref-closure",
|
||||||
|
"test-report",
|
||||||
|
"secret-scan",
|
||||||
|
"artifact-manifest",
|
||||||
|
"credential-revocation",
|
||||||
|
"production-invariants",
|
||||||
|
),
|
||||||
|
production_side_effects_allowed=False,
|
||||||
|
)
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
"""Fail-closed policy engine for autonomous release workflows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from .errors import ApprovalRequired, CapabilityMismatch, PolicyDenied
|
||||||
|
from .models import ApprovalEnvelope, ResourceBinding, RiskClass, TaskPlan, WorkerDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
FORBIDDEN_CAPABILITIES = frozenset({
|
||||||
|
"production.deploy",
|
||||||
|
"production.rollback",
|
||||||
|
"main.write",
|
||||||
|
"tag.write",
|
||||||
|
"baseline.write",
|
||||||
|
"registry.write",
|
||||||
|
"secret.export",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PolicyDecision:
|
||||||
|
allowed: bool
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReleasePolicy:
|
||||||
|
def validate_binding(self, binding: ResourceBinding) -> PolicyDecision:
|
||||||
|
if binding.branch == "main":
|
||||||
|
raise PolicyDenied("MAIN_WRITE_DENIED", "main branch is never writable")
|
||||||
|
if not binding.allowed_paths:
|
||||||
|
raise PolicyDenied("PATH_SCOPE_REQUIRED", "candidate path scope is required")
|
||||||
|
return PolicyDecision(True, "binding_valid")
|
||||||
|
|
||||||
|
def validate_task(self, plan: TaskPlan) -> PolicyDecision:
|
||||||
|
self.validate_binding(plan.binding)
|
||||||
|
if plan.production_side_effects_allowed:
|
||||||
|
raise PolicyDenied("PRODUCTION_SIDE_EFFECT_DENIED", "candidate workflows cannot execute production side effects")
|
||||||
|
return PolicyDecision(True, "task_valid")
|
||||||
|
|
||||||
|
def validate_approval(self, approval: ApprovalEnvelope, now: datetime | None = None) -> PolicyDecision:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
if now < approval.not_before or now >= approval.expires_at:
|
||||||
|
raise ApprovalRequired("APPROVAL_TIME_INVALID", "approval is outside its validity window")
|
||||||
|
if any(cap in FORBIDDEN_CAPABILITIES for cap in approval.capabilities):
|
||||||
|
raise PolicyDenied("CAPABILITY_FORBIDDEN", "approval requests forbidden capability")
|
||||||
|
return PolicyDecision(True, "approval_valid")
|
||||||
|
|
||||||
|
def validate_worker(self, worker: WorkerDescriptor, required: tuple[str, ...], trust_zone: str) -> PolicyDecision:
|
||||||
|
if not worker.can_accept(required, trust_zone):
|
||||||
|
raise CapabilityMismatch("WORKER_CAPABILITY_MISMATCH", "worker cannot satisfy task requirements")
|
||||||
|
return PolicyDecision(True, "worker_valid")
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
"""Governed deployment and rollback state machine."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from .approval import ApprovalVerificationResult
|
||||||
|
from .errors import ReleaseBlocked
|
||||||
|
from .models import ReleasePlan
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseState(str, Enum):
|
||||||
|
CREATED = "CREATED"
|
||||||
|
BUILT = "BUILT"
|
||||||
|
VERIFIED = "VERIFIED"
|
||||||
|
WAITING_OWNER = "WAITING_OWNER"
|
||||||
|
APPROVED = "APPROVED"
|
||||||
|
DEPLOYING = "DEPLOYING"
|
||||||
|
DEPLOYED = "DEPLOYED"
|
||||||
|
FAILED = "FAILED"
|
||||||
|
ROLLBACK_REQUESTED = "ROLLBACK_REQUESTED"
|
||||||
|
ROLLBACK_APPROVED = "ROLLBACK_APPROVED"
|
||||||
|
ROLLING_BACK = "ROLLING_BACK"
|
||||||
|
ROLLED_BACK = "ROLLED_BACK"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReleaseStatus:
|
||||||
|
state: ReleaseState
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseController:
|
||||||
|
"""No transition reaches deployment or rollback without an external receipt."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._state = ReleaseState.CREATED
|
||||||
|
self._plan: ReleasePlan | None = None
|
||||||
|
self._reason: str | None = None
|
||||||
|
|
||||||
|
def _expect(self, *states: ReleaseState) -> None:
|
||||||
|
if self._state not in states:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"RELEASE_STATE_INVALID",
|
||||||
|
"release transition is not allowed from the current state",
|
||||||
|
{"state": self._state.value, "allowed": [item.value for item in states]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def attach_plan(self, plan: ReleasePlan) -> ReleaseStatus:
|
||||||
|
if self._plan is not None:
|
||||||
|
if self._plan == plan:
|
||||||
|
return self.status()
|
||||||
|
raise ReleaseBlocked("RELEASE_PLAN_ALREADY_ATTACHED", "release plan is immutable")
|
||||||
|
self._plan = plan
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def mark_built(self) -> ReleaseStatus:
|
||||||
|
self._require_plan()
|
||||||
|
self._expect(ReleaseState.CREATED)
|
||||||
|
self._state = ReleaseState.BUILT
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def verify(self, *, artifact_ok: bool, evidence_ok: bool) -> ReleaseStatus:
|
||||||
|
self._require_plan()
|
||||||
|
self._expect(ReleaseState.BUILT)
|
||||||
|
if not artifact_ok or not evidence_ok:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"RELEASE_VERIFICATION_FAILED", "artifact or evidence verification failed"
|
||||||
|
)
|
||||||
|
self._state = ReleaseState.VERIFIED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def request_owner_approval(self) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.VERIFIED)
|
||||||
|
self._state = ReleaseState.WAITING_OWNER
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def approve(self, receipt: ApprovalVerificationResult) -> ReleaseStatus:
|
||||||
|
plan = self._require_plan()
|
||||||
|
self._expect(ReleaseState.WAITING_OWNER)
|
||||||
|
if receipt.task_id != plan.task_id:
|
||||||
|
raise ReleaseBlocked("OWNER_APPROVAL_TASK_MISMATCH", "approval task does not match")
|
||||||
|
if receipt.approval_plan_id != plan.approval_plan_id:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"OWNER_APPROVAL_PLAN_MISMATCH", "approval plan does not match release plan"
|
||||||
|
)
|
||||||
|
self._state = ReleaseState.APPROVED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def start_deployment(self) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.APPROVED)
|
||||||
|
self._state = ReleaseState.DEPLOYING
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def mark_deployed(self) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.DEPLOYING)
|
||||||
|
self._state = ReleaseState.DEPLOYED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def mark_failed(self, reason: str) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.DEPLOYING, ReleaseState.DEPLOYED)
|
||||||
|
if not reason:
|
||||||
|
raise ReleaseBlocked("RELEASE_FAILURE_REASON_REQUIRED", "failure reason is required")
|
||||||
|
self._reason = reason
|
||||||
|
self._state = ReleaseState.FAILED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def request_rollback(self, reason: str) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.DEPLOYING, ReleaseState.DEPLOYED, ReleaseState.FAILED)
|
||||||
|
if not reason:
|
||||||
|
raise ReleaseBlocked("ROLLBACK_REASON_REQUIRED", "rollback reason is required")
|
||||||
|
self._reason = reason
|
||||||
|
self._state = ReleaseState.ROLLBACK_REQUESTED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def approve_rollback(self, receipt: ApprovalVerificationResult) -> ReleaseStatus:
|
||||||
|
plan = self._require_plan()
|
||||||
|
self._expect(ReleaseState.ROLLBACK_REQUESTED)
|
||||||
|
if receipt.task_id != plan.task_id:
|
||||||
|
raise ReleaseBlocked("ROLLBACK_APPROVAL_TASK_MISMATCH", "rollback task does not match")
|
||||||
|
if receipt.approval_plan_id != plan.rollback_approval_plan_id:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ROLLBACK_APPROVAL_PLAN_MISMATCH",
|
||||||
|
"rollback approval plan does not match release plan",
|
||||||
|
)
|
||||||
|
self._state = ReleaseState.ROLLBACK_APPROVED
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def start_rollback(self) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.ROLLBACK_APPROVED)
|
||||||
|
self._state = ReleaseState.ROLLING_BACK
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def mark_rolled_back(self) -> ReleaseStatus:
|
||||||
|
self._expect(ReleaseState.ROLLING_BACK)
|
||||||
|
self._state = ReleaseState.ROLLED_BACK
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def rollback(self) -> ReleaseStatus:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"ROLLBACK_APPROVAL_REQUIRED",
|
||||||
|
"use request_rollback, approve_rollback and start_rollback",
|
||||||
|
)
|
||||||
|
|
||||||
|
def status(self) -> ReleaseStatus:
|
||||||
|
return ReleaseStatus(self._state, self._reason)
|
||||||
|
|
||||||
|
def _require_plan(self) -> ReleasePlan:
|
||||||
|
if self._plan is None:
|
||||||
|
raise ReleaseBlocked("RELEASE_PLAN_REQUIRED", "attach a release plan first")
|
||||||
|
return self._plan
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
"""Read-only multi-source release consistency gate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .errors import ReleaseBlocked
|
||||||
|
from .models import is_git_oid, is_sha256
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReleaseEvidence:
|
||||||
|
expected_version: str
|
||||||
|
registry_version: str
|
||||||
|
baseline_version: str
|
||||||
|
local_tag_oid: str
|
||||||
|
remote_tag_oid: str
|
||||||
|
gitea_tag_oid: str
|
||||||
|
expected_tag_oid: str
|
||||||
|
local_peeled_target_oid: str
|
||||||
|
remote_peeled_target_oid: str
|
||||||
|
gitea_peeled_target_oid: str
|
||||||
|
expected_target_oid: str
|
||||||
|
artifact_sha256: str
|
||||||
|
manifest_sha256: str
|
||||||
|
evidence_sha256: str
|
||||||
|
rollback_artifact_sha256: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseGate:
|
||||||
|
"""Require exact tag-object, peeled-target, version and package closure."""
|
||||||
|
|
||||||
|
def verify(self, evidence: ReleaseEvidence) -> None:
|
||||||
|
versions = (
|
||||||
|
evidence.expected_version,
|
||||||
|
evidence.registry_version,
|
||||||
|
evidence.baseline_version,
|
||||||
|
)
|
||||||
|
if not versions[0] or len(set(versions)) != 1:
|
||||||
|
raise ReleaseBlocked("VERSION_MISMATCH", "expected, registry and baseline differ")
|
||||||
|
tag_oids = (
|
||||||
|
evidence.local_tag_oid,
|
||||||
|
evidence.remote_tag_oid,
|
||||||
|
evidence.gitea_tag_oid,
|
||||||
|
evidence.expected_tag_oid,
|
||||||
|
)
|
||||||
|
if any(not is_git_oid(value) for value in tag_oids):
|
||||||
|
raise ReleaseBlocked("TAG_OID_INVALID", "tag object OID is invalid")
|
||||||
|
if evidence.local_tag_oid != evidence.remote_tag_oid:
|
||||||
|
raise ReleaseBlocked("TAG_REMOTE_MISMATCH", "local and remote tag objects differ")
|
||||||
|
if evidence.remote_tag_oid != evidence.gitea_tag_oid:
|
||||||
|
raise ReleaseBlocked("TAG_GITEA_MISMATCH", "Gitea tag object differs")
|
||||||
|
if evidence.gitea_tag_oid != evidence.expected_tag_oid:
|
||||||
|
raise ReleaseBlocked("TAG_OBJECT_MISMATCH", "tag object differs from ReleasePlan")
|
||||||
|
targets = (
|
||||||
|
evidence.local_peeled_target_oid,
|
||||||
|
evidence.remote_peeled_target_oid,
|
||||||
|
evidence.gitea_peeled_target_oid,
|
||||||
|
evidence.expected_target_oid,
|
||||||
|
)
|
||||||
|
if any(not is_git_oid(value) for value in targets):
|
||||||
|
raise ReleaseBlocked("TAG_TARGET_OID_INVALID", "peeled target OID is invalid")
|
||||||
|
if len(set(targets)) != 1:
|
||||||
|
raise ReleaseBlocked("TAG_TARGET_MISMATCH", "peeled tag targets are not identical")
|
||||||
|
digests = (
|
||||||
|
evidence.artifact_sha256,
|
||||||
|
evidence.manifest_sha256,
|
||||||
|
evidence.evidence_sha256,
|
||||||
|
evidence.rollback_artifact_sha256,
|
||||||
|
)
|
||||||
|
if any(not is_sha256(value) for value in digests):
|
||||||
|
raise ReleaseBlocked("ARTIFACT_DIGEST_MISSING", "package closure is incomplete")
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
"""Deterministic worker routing for candidate autonomous release workflows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .errors import CapabilityMismatch
|
||||||
|
from .models import WorkerDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RouteRequest:
|
||||||
|
task_id: str
|
||||||
|
required_capabilities: tuple[str, ...]
|
||||||
|
trust_zone: str
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerRouter:
|
||||||
|
"""Select the least loaded compatible worker without expanding trust."""
|
||||||
|
|
||||||
|
def __init__(self, workers: list[WorkerDescriptor]) -> None:
|
||||||
|
self._workers = list(workers)
|
||||||
|
|
||||||
|
def route(self, request: RouteRequest) -> WorkerDescriptor:
|
||||||
|
candidates = [
|
||||||
|
worker
|
||||||
|
for worker in self._workers
|
||||||
|
if worker.can_accept(request.required_capabilities, request.trust_zone)
|
||||||
|
]
|
||||||
|
if not candidates:
|
||||||
|
raise CapabilityMismatch(
|
||||||
|
"NO_COMPATIBLE_WORKER",
|
||||||
|
"no worker satisfies capability and trust constraints",
|
||||||
|
{"task_id": request.task_id},
|
||||||
|
)
|
||||||
|
return sorted(candidates, key=lambda item: (item.current_load, item.worker_id))[0]
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
"""AIOS Secret Manager V2 reference-only Candidate adapter."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Mapping, Protocol, Sequence
|
||||||
|
|
||||||
|
from .errors import CredentialUnavailable
|
||||||
|
from .models import CredentialRequest
|
||||||
|
|
||||||
|
|
||||||
|
PROVIDER_REFERENCES = {
|
||||||
|
"openclaw": "systemd-credential:openclaw-gateway-candidate",
|
||||||
|
"codex": "systemd-credential:codex-app-server-candidate",
|
||||||
|
"native_adapter_control": "systemd-credential:session-native-adapter-control-candidate",
|
||||||
|
"gitea": "systemd-credential:gitea-admin-api-candidate",
|
||||||
|
}
|
||||||
|
FORBIDDEN_RESPONSE_FIELDS = {
|
||||||
|
"api_key",
|
||||||
|
"token",
|
||||||
|
"password",
|
||||||
|
"cookie",
|
||||||
|
"private_key",
|
||||||
|
"credential_body",
|
||||||
|
"plaintext",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SecretManagerTransport(Protocol):
|
||||||
|
def exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SecretProjectionReceipt:
|
||||||
|
task_id: str
|
||||||
|
credential_id: str
|
||||||
|
reference: str
|
||||||
|
provider: str
|
||||||
|
audience: str
|
||||||
|
consumer_service: str
|
||||||
|
state: str
|
||||||
|
expires_at: datetime
|
||||||
|
plaintext_secret_returned: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class SecretManagerV2:
|
||||||
|
"""Issue, project, inspect and revoke without receiving authentication material."""
|
||||||
|
|
||||||
|
def __init__(self, transport: SecretManagerTransport) -> None:
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _assert_safe(value: Any) -> None:
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
if FORBIDDEN_RESPONSE_FIELDS.intersection(str(key).lower() for key in value):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_PLAINTEXT_REJECTED",
|
||||||
|
"Secret Manager response included a forbidden plaintext field",
|
||||||
|
)
|
||||||
|
for item in value.values():
|
||||||
|
SecretManagerV2._assert_safe(item)
|
||||||
|
elif isinstance(value, (list, tuple)):
|
||||||
|
for item in value:
|
||||||
|
SecretManagerV2._assert_safe(item)
|
||||||
|
|
||||||
|
def _exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||||
|
try:
|
||||||
|
response = self.transport.exchange(operation, payload)
|
||||||
|
except CredentialUnavailable:
|
||||||
|
raise
|
||||||
|
except Exception as error:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_UNAVAILABLE", "Secret Manager V2 operation failed"
|
||||||
|
) from error
|
||||||
|
if not isinstance(response, Mapping):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_RESPONSE_INVALID", "Secret Manager V2 returned a non-object"
|
||||||
|
)
|
||||||
|
self._assert_safe(response)
|
||||||
|
if response.get("plaintext_secret_returned") is not False:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_PLAINTEXT_REJECTED",
|
||||||
|
"Secret Manager did not attest plaintext_secret_returned=false",
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _payload(request: CredentialRequest) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"approval_plan_id": request.approval_plan_id,
|
||||||
|
"task_id": request.task_id,
|
||||||
|
"request_id": request.request_id,
|
||||||
|
"session_id": request.session_id,
|
||||||
|
"worker_id": request.worker_id,
|
||||||
|
"provider": request.provider,
|
||||||
|
"audience": request.audience,
|
||||||
|
"consumer_service": request.consumer_service,
|
||||||
|
"runtime_user": request.runtime_user,
|
||||||
|
"repository": request.binding.repository,
|
||||||
|
"branch": request.binding.branch,
|
||||||
|
"branch_oid": request.binding.base_oid,
|
||||||
|
"allowed_paths": list(request.binding.allowed_paths),
|
||||||
|
"operations": list(request.operations),
|
||||||
|
"not_before": request.not_before.astimezone(timezone.utc).isoformat(),
|
||||||
|
"expires_at": request.expires_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
"nonce": request.nonce,
|
||||||
|
"ttl_seconds": request.ttl_seconds,
|
||||||
|
"max_uses": request.max_uses,
|
||||||
|
}
|
||||||
|
|
||||||
|
def issue(
|
||||||
|
self, request: CredentialRequest, now: datetime | None = None
|
||||||
|
) -> SecretProjectionReceipt:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
if now < request.not_before or now >= request.expires_at:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_APPROVAL_WINDOW_INVALID", "credential request is not currently valid"
|
||||||
|
)
|
||||||
|
expected_reference = PROVIDER_REFERENCES.get(request.provider)
|
||||||
|
if expected_reference is None:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_PROVIDER_DENIED", "provider is not Candidate-approved"
|
||||||
|
)
|
||||||
|
response = self._exchange("issue", self._payload(request))
|
||||||
|
credential_id = response.get("credential_id")
|
||||||
|
if (
|
||||||
|
response.get("task_id") != request.task_id
|
||||||
|
or not isinstance(credential_id, str)
|
||||||
|
or not credential_id
|
||||||
|
or response.get("credential_reference") != expected_reference
|
||||||
|
or response.get("state") != "ISSUED"
|
||||||
|
):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_BINDING_MISMATCH", "issued projection does not match the request"
|
||||||
|
)
|
||||||
|
expires_at = datetime.fromisoformat(str(response.get("expires_at")))
|
||||||
|
if expires_at > request.expires_at or expires_at <= now:
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_EXPIRY_INVALID", "issued projection expiry is outside approval"
|
||||||
|
)
|
||||||
|
return SecretProjectionReceipt(
|
||||||
|
task_id=request.task_id,
|
||||||
|
credential_id=credential_id,
|
||||||
|
reference=expected_reference,
|
||||||
|
provider=request.provider,
|
||||||
|
audience=request.audience,
|
||||||
|
consumer_service=request.consumer_service,
|
||||||
|
state="ISSUED",
|
||||||
|
expires_at=expires_at,
|
||||||
|
plaintext_secret_returned=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _receipt_payload(receipt: SecretProjectionReceipt) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"task_id": receipt.task_id,
|
||||||
|
"credential_id": receipt.credential_id,
|
||||||
|
"credential_reference": receipt.reference,
|
||||||
|
"provider": receipt.provider,
|
||||||
|
"audience": receipt.audience,
|
||||||
|
"consumer_service": receipt.consumer_service,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _state_operation(
|
||||||
|
self, operation: str, receipt: SecretProjectionReceipt, expected_state: str
|
||||||
|
) -> Mapping[str, Any]:
|
||||||
|
response = self._exchange(operation, self._receipt_payload(receipt))
|
||||||
|
if (
|
||||||
|
response.get("task_id") != receipt.task_id
|
||||||
|
or response.get("credential_id") != receipt.credential_id
|
||||||
|
or response.get("state") != expected_state
|
||||||
|
):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_STATE_MISMATCH", f"{operation} response is not task-bound"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def project(self, receipt: SecretProjectionReceipt) -> None:
|
||||||
|
self._state_operation("project", receipt, "PROJECTED")
|
||||||
|
|
||||||
|
def status(self, receipt: SecretProjectionReceipt) -> str:
|
||||||
|
return str(self._state_operation("status", receipt, "PROJECTED")["state"])
|
||||||
|
|
||||||
|
def revoke(self, receipt: SecretProjectionReceipt) -> None:
|
||||||
|
self._state_operation("revoke", receipt, "REVOKED")
|
||||||
|
|
||||||
|
def verify_revocation(self, receipt: SecretProjectionReceipt) -> None:
|
||||||
|
response = self._state_operation("verify_revocation", receipt, "REVOKED")
|
||||||
|
if response.get("post_revocation_auth") != "REJECTED":
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_REVOCATION_UNVERIFIED",
|
||||||
|
"post-revocation authentication was not rejected",
|
||||||
|
)
|
||||||
|
|
||||||
|
def audit(self, receipt: SecretProjectionReceipt) -> Sequence[Mapping[str, Any]]:
|
||||||
|
response = self._exchange("audit", self._receipt_payload(receipt))
|
||||||
|
if (
|
||||||
|
response.get("task_id") != receipt.task_id
|
||||||
|
or response.get("credential_id") != receipt.credential_id
|
||||||
|
):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_AUDIT_MISMATCH", "audit response is not task-bound"
|
||||||
|
)
|
||||||
|
events = response.get("events")
|
||||||
|
if not isinstance(events, list) or any(
|
||||||
|
not isinstance(event, Mapping) or event.get("secrets_included") is not False
|
||||||
|
for event in events
|
||||||
|
):
|
||||||
|
raise CredentialUnavailable(
|
||||||
|
"SECRET_MANAGER_AUDIT_INVALID", "audit events are absent or unsafe"
|
||||||
|
)
|
||||||
|
return events
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
"""JSON serialization helpers for candidate API and CLI boundaries."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from .models import ReleasePlan, ResourceBinding, RiskClass, TaskPlan, WorkflowStep
|
||||||
|
|
||||||
|
|
||||||
|
def task_plan_from_mapping(value: Mapping[str, Any]) -> TaskPlan:
|
||||||
|
binding_value = value["binding"]
|
||||||
|
binding = ResourceBinding(
|
||||||
|
repository=str(binding_value["repository"]),
|
||||||
|
branch=str(binding_value["branch"]),
|
||||||
|
base_oid=str(binding_value["base_oid"]),
|
||||||
|
allowed_paths=tuple(str(item) for item in binding_value.get("allowed_paths", ())),
|
||||||
|
)
|
||||||
|
steps = tuple(
|
||||||
|
WorkflowStep(
|
||||||
|
step_id=str(item["step_id"]),
|
||||||
|
worker_role=str(item["worker_role"]),
|
||||||
|
required_capabilities=tuple(str(cap) for cap in item.get("required_capabilities", ())),
|
||||||
|
depends_on=tuple(str(dep) for dep in item.get("depends_on", ())),
|
||||||
|
max_attempts=int(item.get("max_attempts", 1)),
|
||||||
|
timeout_seconds=int(item.get("timeout_seconds", 900)),
|
||||||
|
allowed_paths=tuple(str(path) for path in item.get("allowed_paths", ())),
|
||||||
|
)
|
||||||
|
for item in value.get("steps", ())
|
||||||
|
)
|
||||||
|
created_at_raw = value.get("created_at")
|
||||||
|
created_at = datetime.fromisoformat(created_at_raw) if created_at_raw else None
|
||||||
|
kwargs: dict[str, Any] = {
|
||||||
|
"task_id": str(value["task_id"]),
|
||||||
|
"intent": str(value["intent"]),
|
||||||
|
"binding": binding,
|
||||||
|
"risk": RiskClass(str(value.get("risk", RiskClass.MEDIUM.value))),
|
||||||
|
"steps": steps,
|
||||||
|
"evidence_requirements": tuple(str(item) for item in value.get("evidence_requirements", ())),
|
||||||
|
"production_side_effects_allowed": bool(value.get("production_side_effects_allowed", False)),
|
||||||
|
}
|
||||||
|
if created_at is not None:
|
||||||
|
kwargs["created_at"] = created_at
|
||||||
|
return TaskPlan(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def task_plan_to_mapping(plan: TaskPlan) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"task_id": plan.task_id,
|
||||||
|
"intent": plan.intent,
|
||||||
|
"binding": {
|
||||||
|
"repository": plan.binding.repository,
|
||||||
|
"branch": plan.binding.branch,
|
||||||
|
"base_oid": plan.binding.base_oid,
|
||||||
|
"allowed_paths": list(plan.binding.allowed_paths),
|
||||||
|
},
|
||||||
|
"risk": plan.risk.value,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": step.step_id,
|
||||||
|
"worker_role": step.worker_role,
|
||||||
|
"required_capabilities": list(step.required_capabilities),
|
||||||
|
"depends_on": list(step.depends_on),
|
||||||
|
"max_attempts": step.max_attempts,
|
||||||
|
"timeout_seconds": step.timeout_seconds,
|
||||||
|
"allowed_paths": list(step.allowed_paths),
|
||||||
|
}
|
||||||
|
for step in plan.steps
|
||||||
|
],
|
||||||
|
"evidence_requirements": list(plan.evidence_requirements),
|
||||||
|
"production_side_effects_allowed": plan.production_side_effects_allowed,
|
||||||
|
"created_at": plan.created_at.isoformat(),
|
||||||
|
"plan_digest": plan.digest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def release_plan_from_mapping(value: Mapping[str, Any]) -> ReleasePlan:
|
||||||
|
return ReleasePlan(
|
||||||
|
task_id=str(value["task_id"]),
|
||||||
|
repository=str(value["repository"]),
|
||||||
|
branch=str(value["branch"]),
|
||||||
|
version=str(value["version"]),
|
||||||
|
target_oid=str(value["target_oid"]),
|
||||||
|
tree_oid=str(value["tree_oid"]),
|
||||||
|
artifact_sha256=str(value["artifact_sha256"]),
|
||||||
|
manifest_sha256=str(value["manifest_sha256"]),
|
||||||
|
package_closure_sha256=str(value["package_closure_sha256"]),
|
||||||
|
evidence_closure_oid=str(value["evidence_closure_oid"]),
|
||||||
|
approval_plan_id=str(value["approval_plan_id"]),
|
||||||
|
rollback_approval_plan_id=str(value["rollback_approval_plan_id"]),
|
||||||
|
rollback_artifact_sha256=str(value["rollback_artifact_sha256"]),
|
||||||
|
expected_tag_oid=str(value["expected_tag_oid"]),
|
||||||
|
expected_target_oid=str(value["expected_target_oid"]),
|
||||||
|
production_config_sha256=str(value["production_config_sha256"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def release_plan_to_mapping(plan: ReleasePlan) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"task_id": plan.task_id,
|
||||||
|
"repository": plan.repository,
|
||||||
|
"branch": plan.branch,
|
||||||
|
"version": plan.version,
|
||||||
|
"target_oid": plan.target_oid,
|
||||||
|
"tree_oid": plan.tree_oid,
|
||||||
|
"artifact_sha256": plan.artifact_sha256,
|
||||||
|
"manifest_sha256": plan.manifest_sha256,
|
||||||
|
"package_closure_sha256": plan.package_closure_sha256,
|
||||||
|
"evidence_closure_oid": plan.evidence_closure_oid,
|
||||||
|
"approval_plan_id": plan.approval_plan_id,
|
||||||
|
"rollback_approval_plan_id": plan.rollback_approval_plan_id,
|
||||||
|
"rollback_artifact_sha256": plan.rollback_artifact_sha256,
|
||||||
|
"expected_tag_oid": plan.expected_tag_oid,
|
||||||
|
"expected_target_oid": plan.expected_target_oid,
|
||||||
|
"production_config_sha256": plan.production_config_sha256,
|
||||||
|
}
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
"""Hardened loopback HTTP boundary for the dormant Candidate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from http import HTTPStatus
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from .api import AutonomousReleaseAPI
|
||||||
|
from .errors import AutonomousReleaseError
|
||||||
|
from .orchestrator import AutonomousReleaseOrchestrator
|
||||||
|
from .policy import ReleasePolicy
|
||||||
|
from .store import StateStore
|
||||||
|
from .workflow import WorkflowEngine
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateApplication:
|
||||||
|
def __init__(self, state_path: str | Path) -> None:
|
||||||
|
self.store = StateStore(state_path)
|
||||||
|
workflow = WorkflowEngine(self.store)
|
||||||
|
self.api = AutonomousReleaseAPI(
|
||||||
|
AutonomousReleaseOrchestrator(workflow, ReleasePolicy())
|
||||||
|
)
|
||||||
|
|
||||||
|
def ready(self) -> bool:
|
||||||
|
with self.store.transaction() as connection:
|
||||||
|
return connection.execute("SELECT 1").fetchone()[0] == 1
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
server_version = "AIOSAutonomousReleaseCandidate/2"
|
||||||
|
sys_version = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def application(self) -> CandidateApplication:
|
||||||
|
return self.server.application # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
def _send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
|
||||||
|
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
self.send_response(status.value)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(encoded)))
|
||||||
|
self.send_header("Cache-Control", "no-store")
|
||||||
|
self.send_header("X-Content-Type-Options", "nosniff")
|
||||||
|
self.send_header("Referrer-Policy", "no-referrer")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(encoded)
|
||||||
|
|
||||||
|
def _read_json(self) -> dict[str, Any]:
|
||||||
|
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
|
||||||
|
if content_type != "application/json":
|
||||||
|
raise UnsupportedMediaType
|
||||||
|
try:
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
except ValueError as error:
|
||||||
|
raise InvalidRequest from error
|
||||||
|
if length <= 0 or length > 1_048_576:
|
||||||
|
raise InvalidRequest
|
||||||
|
try:
|
||||||
|
raw = self.rfile.read(length)
|
||||||
|
value = json.loads(raw.decode("utf-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||||
|
raise InvalidRequest from error
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise InvalidRequest
|
||||||
|
return value
|
||||||
|
|
||||||
|
def do_GET(self) -> None: # noqa: N802
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
if path == "/health":
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.OK,
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"mode": "candidate-shadow-only",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if path == "/ready":
|
||||||
|
if self.application.ready():
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.OK, {"status": "ready", "secrets_included": False}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
|
{"status": "not_ready", "secrets_included": False},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if path.startswith("/v1/tasks/") and path.endswith("/steps"):
|
||||||
|
task_id = path[len("/v1/tasks/") : -len("/steps")].strip("/")
|
||||||
|
if not task_id or "/" in task_id:
|
||||||
|
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._send_json(HTTPStatus.OK, self.application.api.task_steps(task_id))
|
||||||
|
except KeyError:
|
||||||
|
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
|
||||||
|
return
|
||||||
|
if path.startswith("/v1/tasks/") and path.endswith("/evidence"):
|
||||||
|
task_id = path[len("/v1/tasks/") : -len("/evidence")].strip("/")
|
||||||
|
if not task_id or "/" in task_id:
|
||||||
|
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._send_json(HTTPStatus.OK, self.application.api.evidence(task_id))
|
||||||
|
except KeyError:
|
||||||
|
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
|
||||||
|
return
|
||||||
|
if path.startswith("/v1/tasks/"):
|
||||||
|
task_id = path[len("/v1/tasks/") :].strip("/")
|
||||||
|
if not task_id or "/" in task_id:
|
||||||
|
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._send_json(HTTPStatus.OK, self.application.api.task_status(task_id))
|
||||||
|
except KeyError:
|
||||||
|
self._send_json(HTTPStatus.NOT_FOUND, {"error": "task_not_found"})
|
||||||
|
return
|
||||||
|
self._send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"})
|
||||||
|
|
||||||
|
def do_POST(self) -> None: # noqa: N802
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
try:
|
||||||
|
payload = self._read_json()
|
||||||
|
if path == "/v1/tasks":
|
||||||
|
self._send_json(HTTPStatus.CREATED, self.application.api.create_task(payload))
|
||||||
|
return
|
||||||
|
if path == "/v1/release/plan":
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.CREATED, self.application.api.create_release_plan(payload)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if path == "/v1/release/verify":
|
||||||
|
self._send_json(HTTPStatus.OK, self.application.api.verify_release(payload))
|
||||||
|
return
|
||||||
|
if path.startswith("/v1/tasks/") and path.endswith("/evidence/close"):
|
||||||
|
task_id = path[len("/v1/tasks/") : -len("/evidence/close")].strip("/")
|
||||||
|
if not task_id:
|
||||||
|
raise InvalidRequest
|
||||||
|
self._send_json(HTTPStatus.OK, self.application.api.close_evidence(task_id))
|
||||||
|
return
|
||||||
|
for action, handler in (
|
||||||
|
("claim", self.application.api.claim),
|
||||||
|
("heartbeat", self.application.api.heartbeat),
|
||||||
|
("result", self.application.api.result),
|
||||||
|
):
|
||||||
|
suffix = f"/{action}"
|
||||||
|
if path.startswith("/v1/tasks/") and path.endswith(suffix):
|
||||||
|
task_id = path[len("/v1/tasks/") : -len(suffix)].strip("/")
|
||||||
|
if not task_id or "/" in task_id:
|
||||||
|
raise InvalidRequest
|
||||||
|
self._send_json(HTTPStatus.OK, handler(task_id, payload))
|
||||||
|
return
|
||||||
|
self._send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"})
|
||||||
|
except UnsupportedMediaType:
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.UNSUPPORTED_MEDIA_TYPE, {"error": "content_type_required"}
|
||||||
|
)
|
||||||
|
except (InvalidRequest, ValueError, KeyError):
|
||||||
|
self._send_json(HTTPStatus.BAD_REQUEST, {"error": "invalid_request"})
|
||||||
|
except AutonomousReleaseError as error:
|
||||||
|
self._send_json(HTTPStatus.CONFLICT, {"error": error.code})
|
||||||
|
except Exception:
|
||||||
|
self._send_json(
|
||||||
|
HTTPStatus.INTERNAL_SERVER_ERROR, {"error": "candidate_request_failed"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_message(self, _format: str, *_args: Any) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRequest(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedMediaType(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateHTTPServer(ThreadingHTTPServer):
|
||||||
|
daemon_threads = True
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
def __init__(self, address: tuple[str, int], application: CandidateApplication) -> None:
|
||||||
|
try:
|
||||||
|
loopback = ipaddress.ip_address(address[0]).is_loopback
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValueError("candidate service requires a numeric loopback address") from error
|
||||||
|
if not loopback:
|
||||||
|
raise ValueError("candidate service must bind to loopback")
|
||||||
|
self.application = application
|
||||||
|
super().__init__(address, CandidateRequestHandler)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(prog="aios-autonomous-release-service")
|
||||||
|
parser.add_argument("--host", default="127.0.0.1")
|
||||||
|
parser.add_argument("--port", type=int, default=19951)
|
||||||
|
parser.add_argument("--state", required=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
application = CandidateApplication(args.state)
|
||||||
|
server = CandidateHTTPServer((args.host, args.port), application)
|
||||||
|
try:
|
||||||
|
server.serve_forever(poll_interval=0.5)
|
||||||
|
finally:
|
||||||
|
server.server_close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
"""Unified candidate session lifecycle contracts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .errors import StateConflict
|
||||||
|
|
||||||
|
|
||||||
|
class SessionState(str, Enum):
|
||||||
|
CREATED = "CREATED"
|
||||||
|
PROVISIONED = "PROVISIONED"
|
||||||
|
ACTIVE = "ACTIVE"
|
||||||
|
INTERRUPTED = "INTERRUPTED"
|
||||||
|
CLOSED = "CLOSED"
|
||||||
|
REVOKED = "REVOKED"
|
||||||
|
|
||||||
|
|
||||||
|
_ALLOWED_TRANSITIONS: dict[SessionState, frozenset[SessionState]] = {
|
||||||
|
SessionState.CREATED: frozenset({SessionState.PROVISIONED, SessionState.CLOSED, SessionState.REVOKED}),
|
||||||
|
SessionState.PROVISIONED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
|
||||||
|
SessionState.ACTIVE: frozenset({SessionState.INTERRUPTED, SessionState.CLOSED, SessionState.REVOKED}),
|
||||||
|
SessionState.INTERRUPTED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
|
||||||
|
SessionState.CLOSED: frozenset({SessionState.REVOKED}),
|
||||||
|
SessionState.REVOKED: frozenset(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SessionIdentity:
|
||||||
|
session_id: str
|
||||||
|
provider: str
|
||||||
|
provider_session_id: str
|
||||||
|
worker_id: str
|
||||||
|
task_id: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not all((self.session_id, self.provider, self.provider_session_id, self.worker_id, self.task_id)):
|
||||||
|
raise ValueError("all session identity fields are required")
|
||||||
|
if self.session_id == self.provider_session_id:
|
||||||
|
raise ValueError("broker and provider session IDs must be distinct")
|
||||||
|
|
||||||
|
|
||||||
|
class SessionOrchestrator:
|
||||||
|
"""Provider-neutral session lifecycle guard with explicit state transitions."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._sessions: dict[str, tuple[SessionIdentity, SessionState]] = {}
|
||||||
|
|
||||||
|
def create(self, identity: SessionIdentity) -> None:
|
||||||
|
if identity.session_id in self._sessions:
|
||||||
|
raise StateConflict("SESSION_EXISTS", "session already exists")
|
||||||
|
self._sessions[identity.session_id] = (identity, SessionState.CREATED)
|
||||||
|
|
||||||
|
def transition(self, session_id: str, state: SessionState) -> None:
|
||||||
|
current = self._sessions.get(session_id)
|
||||||
|
if current is None:
|
||||||
|
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
|
||||||
|
current_state = current[1]
|
||||||
|
if state not in _ALLOWED_TRANSITIONS[current_state]:
|
||||||
|
raise StateConflict(
|
||||||
|
"SESSION_TRANSITION_INVALID",
|
||||||
|
"session state transition is not allowed",
|
||||||
|
{"session_id": session_id, "from": current_state.value, "to": state.value},
|
||||||
|
)
|
||||||
|
self._sessions[session_id] = (current[0], state)
|
||||||
|
|
||||||
|
def identity(self, session_id: str) -> SessionIdentity:
|
||||||
|
current = self._sessions.get(session_id)
|
||||||
|
if current is None:
|
||||||
|
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
|
||||||
|
return current[0]
|
||||||
|
|
||||||
|
def status(self, session_id: str) -> SessionState:
|
||||||
|
current = self._sessions.get(session_id)
|
||||||
|
if current is None:
|
||||||
|
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
|
||||||
|
return current[1]
|
||||||
|
|
||||||
|
def close(self, session_id: str) -> None:
|
||||||
|
self.transition(session_id, SessionState.CLOSED)
|
||||||
|
|
||||||
|
def revoke(self, session_id: str) -> None:
|
||||||
|
self.transition(session_id, SessionState.REVOKED)
|
||||||
+383
@@ -0,0 +1,383 @@
|
|||||||
|
"""Durable, private SQLite state for the autonomous-release Candidate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
from typing import Any, Iterator, Mapping, Sequence
|
||||||
|
|
||||||
|
from .errors import StateConflict
|
||||||
|
from .models import StepState, TaskState, canonical_json
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class StoredStep:
|
||||||
|
task_id: str
|
||||||
|
step_id: str
|
||||||
|
state: StepState
|
||||||
|
attempt: int
|
||||||
|
max_attempts: int
|
||||||
|
worker_id: str | None
|
||||||
|
session_id: str | None
|
||||||
|
lease_expires_at: datetime | None
|
||||||
|
result_digest: str | None
|
||||||
|
updated_at: datetime
|
||||||
|
last_error: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class StateStore:
|
||||||
|
"""SQLite store with WAL, full sync, schema checks and serialized CAS writes."""
|
||||||
|
|
||||||
|
def __init__(self, path: str | Path) -> None:
|
||||||
|
self.path = Path(path)
|
||||||
|
self._prepare_path()
|
||||||
|
self._initialize()
|
||||||
|
os.chmod(self.path, 0o600)
|
||||||
|
|
||||||
|
def _prepare_path(self) -> None:
|
||||||
|
if self.path.exists() and self.path.is_symlink():
|
||||||
|
raise StateConflict("STATE_PATH_SYMLINK", "state database must not be a symlink")
|
||||||
|
parent = self.path.parent
|
||||||
|
parent.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||||
|
if parent.is_symlink():
|
||||||
|
raise StateConflict("STATE_DIRECTORY_SYMLINK", "state directory must not be a symlink")
|
||||||
|
current_mode = parent.stat().st_mode & 0o777
|
||||||
|
os.chmod(parent, current_mode & ~0o027)
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
if self.path.exists() and self.path.is_symlink():
|
||||||
|
raise StateConflict("STATE_PATH_SYMLINK", "state database must not be a symlink")
|
||||||
|
connection = sqlite3.connect(self.path, timeout=30.0)
|
||||||
|
connection.row_factory = sqlite3.Row
|
||||||
|
connection.execute("PRAGMA foreign_keys=ON")
|
||||||
|
connection.execute("PRAGMA journal_mode=WAL")
|
||||||
|
connection.execute("PRAGMA synchronous=FULL")
|
||||||
|
connection.execute("PRAGMA busy_timeout=30000")
|
||||||
|
return connection
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(self) -> Iterator[sqlite3.Connection]:
|
||||||
|
connection = self._connect()
|
||||||
|
try:
|
||||||
|
connection.execute("BEGIN IMMEDIATE")
|
||||||
|
yield connection
|
||||||
|
connection.commit()
|
||||||
|
except Exception:
|
||||||
|
connection.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def _initialize(self) -> None:
|
||||||
|
with self.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
||||||
|
)
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version'"
|
||||||
|
).fetchone()
|
||||||
|
version = int(row["value"]) if row is not None else None
|
||||||
|
if version not in (None, 1, SCHEMA_VERSION):
|
||||||
|
raise StateConflict(
|
||||||
|
"SCHEMA_VERSION_UNSUPPORTED",
|
||||||
|
"state schema is not supported",
|
||||||
|
{"found": version, "expected": SCHEMA_VERSION},
|
||||||
|
)
|
||||||
|
connection.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
task_id TEXT PRIMARY KEY,
|
||||||
|
plan_digest TEXT NOT NULL,
|
||||||
|
plan_json TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS steps (
|
||||||
|
task_id TEXT NOT NULL,
|
||||||
|
step_id TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
attempt INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_attempts INTEGER NOT NULL DEFAULT 1,
|
||||||
|
worker_id TEXT,
|
||||||
|
session_id TEXT,
|
||||||
|
lease_expires_at TEXT,
|
||||||
|
result_digest TEXT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(task_id, step_id),
|
||||||
|
FOREIGN KEY(task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
task_id TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
payload_json TEXT NOT NULL,
|
||||||
|
previous_digest TEXT,
|
||||||
|
digest TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS consumed_nonces (
|
||||||
|
scope TEXT NOT NULL,
|
||||||
|
nonce TEXT NOT NULL,
|
||||||
|
consumed_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(scope, nonce)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS credentials (
|
||||||
|
credential_id TEXT PRIMARY KEY,
|
||||||
|
task_id TEXT NOT NULL,
|
||||||
|
reference TEXT NOT NULL UNIQUE,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
metadata_json TEXT NOT NULL,
|
||||||
|
issued_at TEXT NOT NULL,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
revoked_at TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS idempotency (
|
||||||
|
namespace TEXT NOT NULL,
|
||||||
|
operation_id TEXT NOT NULL,
|
||||||
|
result_json TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(namespace, operation_id)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if version == 1:
|
||||||
|
existing = {
|
||||||
|
item["name"] for item in connection.execute("PRAGMA table_info(steps)")
|
||||||
|
}
|
||||||
|
additions = {
|
||||||
|
"max_attempts": "INTEGER NOT NULL DEFAULT 1",
|
||||||
|
"lease_expires_at": "TEXT",
|
||||||
|
"result_digest": "TEXT",
|
||||||
|
}
|
||||||
|
for column, declaration in additions.items():
|
||||||
|
if column not in existing:
|
||||||
|
connection.execute(f"ALTER TABLE steps ADD COLUMN {column} {declaration}")
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO metadata(key, value) VALUES('schema_version', ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||||
|
(str(SCHEMA_VERSION),),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
def create_task(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
plan_digest: str,
|
||||||
|
plan: Mapping[str, Any],
|
||||||
|
step_ids: tuple[str, ...],
|
||||||
|
step_limits: Mapping[str, int] | None = None,
|
||||||
|
) -> None:
|
||||||
|
now = self._now()
|
||||||
|
limits = step_limits or {}
|
||||||
|
with self.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO tasks(task_id, plan_digest, plan_json, state, created_at, updated_at) "
|
||||||
|
"VALUES(?, ?, ?, ?, ?, ?)",
|
||||||
|
(task_id, plan_digest, canonical_json(plan), TaskState.PLANNED.value, now, now),
|
||||||
|
)
|
||||||
|
connection.executemany(
|
||||||
|
"INSERT INTO steps(task_id, step_id, state, max_attempts, updated_at) "
|
||||||
|
"VALUES(?, ?, ?, ?, ?)",
|
||||||
|
[
|
||||||
|
(task_id, step_id, StepState.PENDING.value, int(limits.get(step_id, 1)), now)
|
||||||
|
for step_id in step_ids
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_task_state(self, task_id: str) -> tuple[TaskState, int]:
|
||||||
|
with self._connect() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT state, revision FROM tasks WHERE task_id=?", (task_id,)
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError(task_id)
|
||||||
|
return TaskState(row["state"]), int(row["revision"])
|
||||||
|
|
||||||
|
def compare_and_set_task_state(
|
||||||
|
self, task_id: str, expected_revision: int, new_state: TaskState
|
||||||
|
) -> int:
|
||||||
|
with self.transaction() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"UPDATE tasks SET state=?, revision=revision+1, updated_at=? "
|
||||||
|
"WHERE task_id=? AND revision=?",
|
||||||
|
(new_state.value, self._now(), task_id, expected_revision),
|
||||||
|
)
|
||||||
|
if cursor.rowcount != 1:
|
||||||
|
raise StateConflict(
|
||||||
|
"TASK_REVISION_CONFLICT",
|
||||||
|
"task state changed concurrently",
|
||||||
|
{"task_id": task_id, "expected_revision": expected_revision},
|
||||||
|
)
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT revision FROM tasks WHERE task_id=?", (task_id,)
|
||||||
|
).fetchone()
|
||||||
|
return int(row["revision"])
|
||||||
|
|
||||||
|
def transition_step(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
expected: Sequence[StepState],
|
||||||
|
state: StepState,
|
||||||
|
*,
|
||||||
|
worker_id: str | None = None,
|
||||||
|
session_id: str | None = None,
|
||||||
|
lease_expires_at: datetime | None = None,
|
||||||
|
result_digest: str | None = None,
|
||||||
|
last_error: str | None = None,
|
||||||
|
increment_attempt: bool = False,
|
||||||
|
clear_assignment: bool = False,
|
||||||
|
) -> StoredStep:
|
||||||
|
with self.transaction() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT * FROM steps WHERE task_id=? AND step_id=?", (task_id, step_id)
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
raise KeyError(f"{task_id}:{step_id}")
|
||||||
|
if StepState(row["state"]) not in set(expected):
|
||||||
|
raise StateConflict(
|
||||||
|
"STEP_STATE_CONFLICT",
|
||||||
|
"step state changed or transition is invalid",
|
||||||
|
{"task_id": task_id, "step_id": step_id, "state": row["state"]},
|
||||||
|
)
|
||||||
|
if increment_attempt and int(row["attempt"]) >= int(row["max_attempts"]):
|
||||||
|
raise StateConflict(
|
||||||
|
"STEP_ATTEMPTS_EXHAUSTED",
|
||||||
|
"step has no remaining attempts",
|
||||||
|
{"task_id": task_id, "step_id": step_id},
|
||||||
|
)
|
||||||
|
next_worker = None if clear_assignment else (worker_id or row["worker_id"])
|
||||||
|
next_session = None if clear_assignment else (session_id or row["session_id"])
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE steps SET state=?, attempt=attempt+?, worker_id=?, session_id=?, "
|
||||||
|
"lease_expires_at=?, result_digest=COALESCE(?, result_digest), last_error=?, updated_at=? "
|
||||||
|
"WHERE task_id=? AND step_id=?",
|
||||||
|
(
|
||||||
|
state.value,
|
||||||
|
1 if increment_attempt else 0,
|
||||||
|
next_worker,
|
||||||
|
next_session,
|
||||||
|
lease_expires_at.isoformat() if lease_expires_at else None,
|
||||||
|
result_digest,
|
||||||
|
last_error,
|
||||||
|
self._now(),
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
updated = connection.execute(
|
||||||
|
"SELECT * FROM steps WHERE task_id=? AND step_id=?", (task_id, step_id)
|
||||||
|
).fetchone()
|
||||||
|
return self._step_from_row(updated)
|
||||||
|
|
||||||
|
def set_step_state(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
state: StepState,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> StoredStep:
|
||||||
|
current = self.list_steps(task_id)[step_id]
|
||||||
|
return self.transition_step(task_id, step_id, (current.state,), state, **kwargs)
|
||||||
|
|
||||||
|
def list_steps(self, task_id: str) -> dict[str, StoredStep]:
|
||||||
|
with self._connect() as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
"SELECT * FROM steps WHERE task_id=? ORDER BY step_id", (task_id,)
|
||||||
|
).fetchall()
|
||||||
|
return {row["step_id"]: self._step_from_row(row) for row in rows}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _step_from_row(row: sqlite3.Row) -> StoredStep:
|
||||||
|
return StoredStep(
|
||||||
|
task_id=row["task_id"],
|
||||||
|
step_id=row["step_id"],
|
||||||
|
state=StepState(row["state"]),
|
||||||
|
attempt=int(row["attempt"]),
|
||||||
|
max_attempts=int(row["max_attempts"]),
|
||||||
|
worker_id=row["worker_id"],
|
||||||
|
session_id=row["session_id"],
|
||||||
|
lease_expires_at=(
|
||||||
|
datetime.fromisoformat(row["lease_expires_at"])
|
||||||
|
if row["lease_expires_at"]
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
result_digest=row["result_digest"],
|
||||||
|
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||||
|
last_error=row["last_error"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def consume_nonce(self, scope: str, nonce: str) -> None:
|
||||||
|
try:
|
||||||
|
with self.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO consumed_nonces(scope, nonce, consumed_at) VALUES(?, ?, ?)",
|
||||||
|
(scope, nonce, self._now()),
|
||||||
|
)
|
||||||
|
except sqlite3.IntegrityError as error:
|
||||||
|
raise StateConflict(
|
||||||
|
"NONCE_REPLAY", "nonce was already consumed", {"scope": scope, "nonce": nonce}
|
||||||
|
) from error
|
||||||
|
|
||||||
|
def append_event(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
event_type: str,
|
||||||
|
payload: Mapping[str, Any],
|
||||||
|
previous_digest: str | None,
|
||||||
|
event_digest: str,
|
||||||
|
created_at: datetime,
|
||||||
|
) -> int:
|
||||||
|
with self.transaction() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"INSERT INTO events(task_id, event_type, payload_json, previous_digest, digest, created_at) "
|
||||||
|
"VALUES(?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
task_id,
|
||||||
|
event_type,
|
||||||
|
canonical_json(payload),
|
||||||
|
previous_digest,
|
||||||
|
event_digest,
|
||||||
|
created_at.astimezone(timezone.utc).isoformat(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return int(cursor.lastrowid)
|
||||||
|
|
||||||
|
def list_events(self, task_id: str) -> list[Mapping[str, Any]]:
|
||||||
|
with self._connect() as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
"SELECT * FROM events WHERE task_id=? ORDER BY sequence", (task_id,)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def put_idempotent_result(
|
||||||
|
self, namespace: str, operation_id: str, result: Mapping[str, Any]
|
||||||
|
) -> Mapping[str, Any]:
|
||||||
|
encoded = canonical_json(result)
|
||||||
|
with self.transaction() as connection:
|
||||||
|
existing = connection.execute(
|
||||||
|
"SELECT result_json FROM idempotency WHERE namespace=? AND operation_id=?",
|
||||||
|
(namespace, operation_id),
|
||||||
|
).fetchone()
|
||||||
|
if existing is not None:
|
||||||
|
return json.loads(existing["result_json"])
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO idempotency(namespace, operation_id, result_json, created_at) "
|
||||||
|
"VALUES(?, ?, ?, ?)",
|
||||||
|
(namespace, operation_id, encoded, self._now()),
|
||||||
|
)
|
||||||
|
return dict(result)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Read-only annotated and lightweight tag consistency checks."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .errors import ReleaseBlocked
|
||||||
|
from .models import is_git_oid
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TagReference:
|
||||||
|
ref: str
|
||||||
|
object_type: str
|
||||||
|
tag_oid: str
|
||||||
|
peeled_target_oid: str
|
||||||
|
|
||||||
|
|
||||||
|
class TagVerifier:
|
||||||
|
def verify(
|
||||||
|
self,
|
||||||
|
tag: TagReference,
|
||||||
|
*,
|
||||||
|
expected_tag_oid: str,
|
||||||
|
expected_target_oid: str,
|
||||||
|
) -> None:
|
||||||
|
if not tag.ref.startswith("refs/tags/") or tag.ref.endswith("^{}"):
|
||||||
|
raise ReleaseBlocked("TAG_REF_INVALID", "canonical tag ref is required")
|
||||||
|
if tag.object_type not in ("commit", "tag"):
|
||||||
|
raise ReleaseBlocked("TAG_OBJECT_TYPE_INVALID", "unsupported tag object type")
|
||||||
|
values = (tag.tag_oid, tag.peeled_target_oid, expected_tag_oid, expected_target_oid)
|
||||||
|
if any(not is_git_oid(value) for value in values):
|
||||||
|
raise ReleaseBlocked("TAG_OID_INVALID", "tag OIDs must be complete lowercase hex")
|
||||||
|
if tag.tag_oid != expected_tag_oid:
|
||||||
|
raise ReleaseBlocked("TAG_OBJECT_MISMATCH", "tag object does not match expectation")
|
||||||
|
if tag.peeled_target_oid != expected_target_oid:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"TAG_PEELED_TARGET_MISMATCH", "peeled target does not match expected target"
|
||||||
|
)
|
||||||
|
if tag.object_type == "commit" and tag.tag_oid != tag.peeled_target_oid:
|
||||||
|
raise ReleaseBlocked(
|
||||||
|
"LIGHTWEIGHT_TAG_INCONSISTENT",
|
||||||
|
"lightweight tag object and target must be identical",
|
||||||
|
)
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
"""Dormant Candidate worker supervisor.
|
||||||
|
|
||||||
|
Provider clients are injected by the external management plane. This process
|
||||||
|
never converts a missing projection into a simulated native identity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
def write_status(directory: Path) -> None:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||||
|
if directory.is_symlink():
|
||||||
|
raise RuntimeError("worker state directory symlink rejected")
|
||||||
|
payload = {
|
||||||
|
"status": "dormant",
|
||||||
|
"task_plane_execution_enabled": os.environ.get(
|
||||||
|
"AIOS_TASK_PLANE_EXECUTION_ENABLED", "false"
|
||||||
|
).lower()
|
||||||
|
== "true",
|
||||||
|
"gitops_writes_enabled": os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower()
|
||||||
|
== "true",
|
||||||
|
"production_side_effects": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
temporary = directory / f"worker-status.{os.getpid()}.new"
|
||||||
|
target = directory / "worker-status.json"
|
||||||
|
if temporary.is_symlink() or target.is_symlink():
|
||||||
|
raise RuntimeError("worker status symlink rejected")
|
||||||
|
descriptor = os.open(
|
||||||
|
temporary,
|
||||||
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
|
||||||
|
0o640,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
os.write(descriptor, (json.dumps(payload, sort_keys=True) + "\n").encode("utf-8"))
|
||||||
|
os.fsync(descriptor)
|
||||||
|
finally:
|
||||||
|
os.close(descriptor)
|
||||||
|
os.replace(temporary, target)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--state-directory", required=True, type=Path)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
if os.environ.get("AIOS_PRODUCTION_DEPLOYMENT_ENABLED", "false").lower() == "true":
|
||||||
|
raise RuntimeError("Candidate worker refuses production deployment mode")
|
||||||
|
write_status(args.state_directory)
|
||||||
|
stopping = threading.Event()
|
||||||
|
signal.signal(signal.SIGTERM, lambda *_: stopping.set())
|
||||||
|
signal.signal(signal.SIGINT, lambda *_: stopping.set())
|
||||||
|
while not stopping.wait(5):
|
||||||
|
write_status(args.state_directory)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
"""Durable fail-closed workflow state machine."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from .errors import StateConflict
|
||||||
|
from .models import StepState, TaskPlan, TaskState, is_sha256
|
||||||
|
from .store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class WorkflowDecision:
|
||||||
|
task_id: str
|
||||||
|
ready_steps: tuple[str, ...]
|
||||||
|
blocked: bool
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowEngine:
|
||||||
|
def __init__(self, store: StateStore) -> None:
|
||||||
|
self.store = store
|
||||||
|
|
||||||
|
def _task_state(self, task_id: str, state: TaskState) -> None:
|
||||||
|
current, revision = self.store.get_task_state(task_id)
|
||||||
|
if current != state:
|
||||||
|
self.store.compare_and_set_task_state(task_id, revision, state)
|
||||||
|
|
||||||
|
def plan(self, task: TaskPlan) -> None:
|
||||||
|
self.store.create_task(
|
||||||
|
task.task_id,
|
||||||
|
task.digest(),
|
||||||
|
{
|
||||||
|
"task_id": task.task_id,
|
||||||
|
"intent": task.intent,
|
||||||
|
"branch": task.binding.branch,
|
||||||
|
"base_oid": task.binding.base_oid,
|
||||||
|
},
|
||||||
|
tuple(step.step_id for step in task.steps),
|
||||||
|
{step.step_id: step.max_attempts for step in task.steps},
|
||||||
|
)
|
||||||
|
|
||||||
|
def next_steps(self, task: TaskPlan) -> WorkflowDecision:
|
||||||
|
states = self.store.list_steps(task.task_id)
|
||||||
|
by_id = {step.step_id: step for step in task.steps}
|
||||||
|
if any(item.state == StepState.FAILED for item in states.values()):
|
||||||
|
self._task_state(task.task_id, TaskState.FAILED)
|
||||||
|
return WorkflowDecision(task.task_id, (), True, "terminal_step_failure")
|
||||||
|
ready: list[str] = []
|
||||||
|
for step_id, stored in states.items():
|
||||||
|
if stored.state not in (StepState.PENDING, StepState.READY):
|
||||||
|
continue
|
||||||
|
dependencies = by_id[step_id].depends_on
|
||||||
|
dependency_states = tuple(states[item].state for item in dependencies)
|
||||||
|
if any(item in (StepState.FAILED, StepState.BLOCKED) for item in dependency_states):
|
||||||
|
if stored.state != StepState.BLOCKED:
|
||||||
|
self.store.transition_step(
|
||||||
|
task.task_id,
|
||||||
|
step_id,
|
||||||
|
(stored.state,),
|
||||||
|
StepState.BLOCKED,
|
||||||
|
last_error="dependency_failed",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if all(item == StepState.COMPLETED for item in dependency_states):
|
||||||
|
if stored.state == StepState.PENDING:
|
||||||
|
self.store.transition_step(
|
||||||
|
task.task_id, step_id, (StepState.PENDING,), StepState.READY
|
||||||
|
)
|
||||||
|
ready.append(step_id)
|
||||||
|
if states and all(item.state == StepState.COMPLETED for item in states.values()):
|
||||||
|
self._task_state(task.task_id, TaskState.COMPLETED)
|
||||||
|
elif ready:
|
||||||
|
self._task_state(task.task_id, TaskState.READY)
|
||||||
|
return WorkflowDecision(task.task_id, tuple(sorted(ready)), False)
|
||||||
|
|
||||||
|
def claim(self, task_id: str, step_id: str, worker_id: str, session_id: str) -> None:
|
||||||
|
if not worker_id or not session_id:
|
||||||
|
raise StateConflict("STEP_CLAIM_BINDING_REQUIRED", "worker and session are required")
|
||||||
|
self.store.transition_step(
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.READY,),
|
||||||
|
StepState.CLAIMED,
|
||||||
|
worker_id=worker_id,
|
||||||
|
session_id=session_id,
|
||||||
|
increment_attempt=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
worker_id: str,
|
||||||
|
session_id: str,
|
||||||
|
*,
|
||||||
|
lease_seconds: int = 60,
|
||||||
|
now: datetime | None = None,
|
||||||
|
) -> None:
|
||||||
|
current = self.store.list_steps(task_id)[step_id]
|
||||||
|
if current.worker_id != worker_id or current.session_id != session_id:
|
||||||
|
raise StateConflict("STEP_CLAIM_OWNER_MISMATCH", "claim owner does not match")
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
self.store.transition_step(
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.CLAIMED,),
|
||||||
|
StepState.RUNNING,
|
||||||
|
worker_id=worker_id,
|
||||||
|
session_id=session_id,
|
||||||
|
lease_expires_at=now + timedelta(seconds=lease_seconds),
|
||||||
|
)
|
||||||
|
self._task_state(task_id, TaskState.RUNNING)
|
||||||
|
|
||||||
|
def heartbeat(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
worker_id: str,
|
||||||
|
session_id: str,
|
||||||
|
*,
|
||||||
|
lease_seconds: int = 60,
|
||||||
|
now: datetime | None = None,
|
||||||
|
) -> None:
|
||||||
|
current = self.store.list_steps(task_id)[step_id]
|
||||||
|
if current.worker_id != worker_id or current.session_id != session_id:
|
||||||
|
raise StateConflict("STEP_CLAIM_OWNER_MISMATCH", "heartbeat owner does not match")
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
self.store.transition_step(
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.RUNNING,),
|
||||||
|
StepState.RUNNING,
|
||||||
|
lease_expires_at=now + timedelta(seconds=lease_seconds),
|
||||||
|
)
|
||||||
|
|
||||||
|
def complete(self, task_id: str, step_id: str, *, result_digest: str) -> None:
|
||||||
|
if not is_sha256(result_digest):
|
||||||
|
raise StateConflict("RESULT_DIGEST_INVALID", "result digest must be SHA-256")
|
||||||
|
current = self.store.list_steps(task_id)[step_id]
|
||||||
|
if current.state == StepState.COMPLETED and current.result_digest == result_digest:
|
||||||
|
return
|
||||||
|
self.store.transition_step(
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.RUNNING,),
|
||||||
|
StepState.COMPLETED,
|
||||||
|
result_digest=result_digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fail(
|
||||||
|
self,
|
||||||
|
task: TaskPlan,
|
||||||
|
step_id: str,
|
||||||
|
reason: str,
|
||||||
|
*,
|
||||||
|
retryable: bool,
|
||||||
|
) -> StepState:
|
||||||
|
current = self.store.list_steps(task.task_id)[step_id]
|
||||||
|
next_state = (
|
||||||
|
StepState.PENDING
|
||||||
|
if retryable and current.attempt < current.max_attempts
|
||||||
|
else StepState.FAILED
|
||||||
|
)
|
||||||
|
self.store.transition_step(
|
||||||
|
task.task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.CLAIMED, StepState.RUNNING),
|
||||||
|
next_state,
|
||||||
|
last_error=reason,
|
||||||
|
clear_assignment=next_state == StepState.PENDING,
|
||||||
|
)
|
||||||
|
self._task_state(
|
||||||
|
task.task_id, TaskState.BLOCKED if next_state == StepState.PENDING else TaskState.FAILED
|
||||||
|
)
|
||||||
|
return next_state
|
||||||
|
|
||||||
|
def recover_expired(
|
||||||
|
self, task: TaskPlan, *, now: datetime | None = None
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
recovered: list[str] = []
|
||||||
|
for step_id, current in self.store.list_steps(task.task_id).items():
|
||||||
|
if (
|
||||||
|
current.state == StepState.RUNNING
|
||||||
|
and current.lease_expires_at is not None
|
||||||
|
and current.lease_expires_at <= now
|
||||||
|
):
|
||||||
|
self.fail(task, step_id, "lease_expired", retryable=True)
|
||||||
|
recovered.append(step_id)
|
||||||
|
return tuple(recovered)
|
||||||
|
|
||||||
|
def recoverable(self, task_id: str, step_id: str) -> None:
|
||||||
|
self.store.transition_step(
|
||||||
|
task_id,
|
||||||
|
step_id,
|
||||||
|
(StepState.BLOCKED,),
|
||||||
|
StepState.PENDING,
|
||||||
|
clear_assignment=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "aios-autonomous-release-candidate"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Candidate-only governed autonomous release control plane"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
license = { text = "Proprietary" }
|
||||||
|
authors = [{ name = "AIOS" }]
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
aios-autonomous-release = "aios_autonomous_release_candidate.cli:main"
|
||||||
|
aios-autonomous-release-service = "aios_autonomous_release_candidate.service:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["aios_autonomous_release_candidate*"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-ra --strict-config --strict-markers"
|
||||||
|
testpaths = ["../../tests/autonomous_release"]
|
||||||
-238
@@ -1,238 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import sqlite3
|
|
||||||
import subprocess
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
from .task_status_merge_broker import ContractDenied, TaskGrant, canonical, load_signed_task_grant, read_runtime_secret
|
|
||||||
|
|
||||||
|
|
||||||
GITEA_BIN = Path("/data/aios-gitops/runtime/bin/gitea")
|
|
||||||
GITEA_CONFIG = Path("/data/aios-gitops/runtime/gitea-bin/custom/conf/app.ini")
|
|
||||||
GITEA_DB = Path("/data/aios-gitops/runtime/gitea-bin/data/gitea.db")
|
|
||||||
RUNUSER = Path("/usr/sbin/runuser")
|
|
||||||
SYSTEMD_CREDS = Path("/usr/bin/systemd-creds")
|
|
||||||
GIT = Path("/usr/bin/git")
|
|
||||||
GITEA_REPOSITORY = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git")
|
|
||||||
|
|
||||||
|
|
||||||
def _git(*arguments: str, input_bytes: bytes | None = None) -> bytes:
|
|
||||||
return subprocess.run(
|
|
||||||
[str(GIT), f"--git-dir={GITEA_REPOSITORY}", *arguments], input=input_bytes,
|
|
||||||
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
||||||
).stdout
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_local_governance_binding(grant: TaskGrant) -> None:
|
|
||||||
"""Verify the exact open PR and latest Owner review without issuing a token."""
|
|
||||||
connection = sqlite3.connect(f"file:{GITEA_DB}?mode=ro", uri=True)
|
|
||||||
connection.row_factory = sqlite3.Row
|
|
||||||
try:
|
|
||||||
pr = connection.execute(
|
|
||||||
"SELECT pr.head_branch,pr.base_branch,pr.merge_base,pr.has_merged,i.is_closed "
|
|
||||||
"FROM pull_request pr JOIN issue i ON i.id=pr.issue_id JOIN repository repo ON repo.id=i.repo_id "
|
|
||||||
"WHERE repo.owner_name='aios' AND repo.lower_name='aios' AND pr.[index]=?",
|
|
||||||
(grant.raw["pr_number"],),
|
|
||||||
).fetchone()
|
|
||||||
review = connection.execute(
|
|
||||||
"SELECT r.type,r.commit_id,r.official,r.stale,r.dismissed,r.created_unix,u.id,u.lower_name "
|
|
||||||
"FROM review r JOIN user u ON u.id=r.reviewer_id JOIN issue i ON i.id=r.issue_id "
|
|
||||||
"JOIN repository repo ON repo.id=i.repo_id "
|
|
||||||
"WHERE repo.owner_name='aios' AND repo.lower_name='aios' AND i.[index]=? "
|
|
||||||
"AND u.id=2 AND u.lower_name='owner' ORDER BY r.id DESC LIMIT 1",
|
|
||||||
(grant.raw["pr_number"],),
|
|
||||||
).fetchone()
|
|
||||||
finally:
|
|
||||||
connection.close()
|
|
||||||
if pr is None or pr["base_branch"] != "governance/approvals" or pr["merge_base"] != grant.raw["base_oid"] or pr["has_merged"] != 0 or pr["is_closed"] != 0:
|
|
||||||
raise ContractDenied("LOCAL_PR_BINDING_INVALID")
|
|
||||||
head = _git("rev-parse", f"refs/heads/{pr['head_branch']}").decode().strip()
|
|
||||||
base = _git("rev-parse", "refs/heads/governance/approvals").decode().strip()
|
|
||||||
if head != grant.raw["target_commit"] or base != grant.raw["base_oid"]:
|
|
||||||
raise ContractDenied("LOCAL_REF_BINDING_INVALID")
|
|
||||||
if review is None or review["type"] != 1 or review["commit_id"] != head or review["official"] != 1 or review["stale"] != 0 or review["dismissed"] != 0:
|
|
||||||
raise ContractDenied("LOCAL_OWNER_APPROVAL_MISSING")
|
|
||||||
submitted = datetime.fromtimestamp(review["created_unix"], tz=timezone.utc)
|
|
||||||
if submitted < grant.issued_at or submitted >= grant.expires_at:
|
|
||||||
raise ContractDenied("LOCAL_OWNER_APPROVAL_EXPIRED")
|
|
||||||
tree = _git("show", "-s", "--format=%T", head).decode().strip()
|
|
||||||
if tree != grant.raw["approved_tree_oid"]:
|
|
||||||
raise ContractDenied("APPROVED_TREE_MISMATCH")
|
|
||||||
entry = _git("ls-tree", head, "--", grant.raw["manifest_path"]).decode().strip().split()
|
|
||||||
if len(entry) < 3 or entry[1] != "blob" or entry[2] != grant.raw["manifest_blob_oid"]:
|
|
||||||
raise ContractDenied("MANIFEST_BLOB_BINDING_INVALID")
|
|
||||||
manifest = _git("cat-file", "blob", grant.raw["manifest_blob_oid"])
|
|
||||||
if hashlib.sha256(manifest).hexdigest() != grant.raw["manifest_sha256"]:
|
|
||||||
raise ContractDenied("MANIFEST_SHA256_MISMATCH")
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_absolute(path: Path, expected_parent: Path) -> None:
|
|
||||||
if not path.is_absolute() or path.is_symlink() or path.parent != expected_parent:
|
|
||||||
raise ContractDenied("CREDENTIAL_PATH_UNSAFE")
|
|
||||||
|
|
||||||
|
|
||||||
class TaskCredentialBroker:
|
|
||||||
def __init__(self, grant: TaskGrant, audit_dir: Path, audit_key: bytes):
|
|
||||||
self.grant = grant
|
|
||||||
self.audit_key = bytes(audit_key)
|
|
||||||
if not self.audit_key:
|
|
||||||
raise ContractDenied("CREDENTIAL_AUDIT_KEY_EMPTY")
|
|
||||||
self.audit_dir = Path(audit_dir)
|
|
||||||
if not self.audit_dir.is_absolute() or self.audit_dir.is_symlink():
|
|
||||||
raise ContractDenied("AUDIT_DIR_UNSAFE")
|
|
||||||
|
|
||||||
def provision(self, *, identity: str, token_name: str, encrypted_path: Path) -> dict:
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise ContractDenied("ROOT_BROKER_REQUIRED")
|
|
||||||
expected = self.grant.raw["status_identity"] if token_name.endswith("-status") else self.grant.raw["merge_identity"]
|
|
||||||
if identity != expected or identity not in {"codex", "gpt"}:
|
|
||||||
raise ContractDenied("IDENTITY_BINDING_INVALID")
|
|
||||||
prefix = f"pr{self.grant.raw['pr_number']}-{self.grant.raw['target_commit'][:12]}"
|
|
||||||
if token_name not in {f"{prefix}-status", f"{prefix}-merge"}:
|
|
||||||
raise ContractDenied("TOKEN_NAME_INVALID")
|
|
||||||
_safe_absolute(encrypted_path, Path("/run/aios-task-credential-broker"))
|
|
||||||
if encrypted_path.exists():
|
|
||||||
raise ContractDenied("CREDENTIAL_PATH_EXISTS")
|
|
||||||
_verify_local_governance_binding(self.grant)
|
|
||||||
command = [str(RUNUSER), "-u", "aiman", "--", str(GITEA_BIN), "admin", "user", "generate-access-token", "--config", str(GITEA_CONFIG), "--username", identity, "--token-name", token_name, "--scopes", "write:repository", "--raw"]
|
|
||||||
issued = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
|
|
||||||
if len(issued) < 32 or b"\n" in issued or b"\x00" in issued:
|
|
||||||
raise ContractDenied("ISSUED_TOKEN_INVALID")
|
|
||||||
subprocess.run([str(SYSTEMD_CREDS), "encrypt", "--name=gitea-token", "-", str(encrypted_path)], input=issued, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
||||||
os.chmod(encrypted_path, 0o400)
|
|
||||||
connection = sqlite3.connect(f"file:{GITEA_DB}?mode=ro", uri=True)
|
|
||||||
try:
|
|
||||||
rows = connection.execute("SELECT t.id,u.id FROM access_token t JOIN user u ON u.id=t.uid WHERE u.lower_name=? AND t.name=? AND t.scope='write:repository'", (identity, token_name)).fetchall()
|
|
||||||
finally:
|
|
||||||
connection.close()
|
|
||||||
if len(rows) != 1:
|
|
||||||
raise ContractDenied("TOKEN_DATABASE_BINDING_MISSING")
|
|
||||||
row = rows[0]
|
|
||||||
receipt = {"status": "PASS", "operation": "credential_provision", "identity": identity, "token_name": token_name, "token_id": int(row[0]), "token_uid": int(row[1]), "grant_sha256": self.grant.digest, "credential_ref": f"systemd-credential-encrypted://{encrypted_path.name}"}
|
|
||||||
return self._audit(receipt)
|
|
||||||
|
|
||||||
def revoke(self, *, receipt: dict, encrypted_path: Path) -> dict:
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise ContractDenied("ROOT_BROKER_REQUIRED")
|
|
||||||
_safe_absolute(encrypted_path, Path("/run/aios-task-credential-broker"))
|
|
||||||
self.validate_provision_receipt(receipt, encrypted_path)
|
|
||||||
decrypted = subprocess.run([str(SYSTEMD_CREDS), "decrypt", str(encrypted_path), "-"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
|
|
||||||
if len(decrypted) < 32 or b"\n" in decrypted or b"\x00" in decrypted:
|
|
||||||
raise ContractDenied("PROJECTED_TOKEN_INVALID")
|
|
||||||
token_header = {"Authorization": "token " + decrypted.decode("utf-8")}
|
|
||||||
preflight = urllib.request.Request("http://127.0.0.1:3000/api/v1/repos/aios/aios", headers=token_header)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(preflight, timeout=10) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
raise ContractDenied("TOKEN_PREFLIGHT_STATUS_INVALID")
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
raise ContractDenied(f"TOKEN_PREFLIGHT_FAILED:{exc.code}") from exc
|
|
||||||
connection = sqlite3.connect(GITEA_DB)
|
|
||||||
try:
|
|
||||||
connection.execute("BEGIN IMMEDIATE")
|
|
||||||
cursor = connection.execute(
|
|
||||||
"DELETE FROM access_token WHERE id=? AND uid=? AND name=? AND scope='write:repository'",
|
|
||||||
(receipt["token_id"], receipt["token_uid"], receipt["token_name"]),
|
|
||||||
)
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
raise ContractDenied("TOKEN_REVOCATION_MISMATCH")
|
|
||||||
connection.commit()
|
|
||||||
except Exception:
|
|
||||||
connection.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
connection.close()
|
|
||||||
connection = sqlite3.connect(f"file:{GITEA_DB}?mode=ro", uri=True)
|
|
||||||
try:
|
|
||||||
remaining = connection.execute("SELECT COUNT(*) FROM access_token WHERE id=?", (receipt["token_id"],)).fetchone()[0]
|
|
||||||
finally:
|
|
||||||
connection.close()
|
|
||||||
if remaining != 0:
|
|
||||||
raise ContractDenied("TOKEN_REVOCATION_READBACK_MISMATCH")
|
|
||||||
request = urllib.request.Request("http://127.0.0.1:3000/api/v1/repos/aios/aios", headers=token_header)
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(request, timeout=10)
|
|
||||||
raise ContractDenied("REVOKED_TOKEN_STILL_ACCEPTED")
|
|
||||||
except urllib.error.HTTPError as exc:
|
|
||||||
if exc.code != 401:
|
|
||||||
raise ContractDenied("REVOCATION_HTTP_STATUS_INVALID") from exc
|
|
||||||
encrypted_path.unlink(missing_ok=True)
|
|
||||||
result = {"status": "PASS", "operation": "credential_revoke", "identity": receipt["identity"], "token_name": receipt["token_name"], "token_id": receipt["token_id"], "grant_sha256": self.grant.digest, "token_rows_remaining": 0, "credential_projection_remaining": False, "old_token_http_status": 401}
|
|
||||||
return self._audit(result)
|
|
||||||
|
|
||||||
def validate_provision_receipt(self, receipt: dict, encrypted_path: Path) -> None:
|
|
||||||
required = {"status", "operation", "identity", "token_name", "token_id", "token_uid", "grant_sha256", "credential_ref", "audit_sha256", "audit_hmac_signature"}
|
|
||||||
expected_credential_ref = f"systemd-credential-encrypted://{encrypted_path.name}"
|
|
||||||
unsigned_receipt = {key: value for key, value in receipt.items() if key not in {"audit_sha256", "audit_hmac_signature"}}
|
|
||||||
expected_name_prefix = f"pr{self.grant.raw['pr_number']}-{self.grant.raw['target_commit'][:12]}"
|
|
||||||
if (set(receipt) != required or receipt["status"] != "PASS" or receipt["operation"] != "credential_provision"
|
|
||||||
or receipt["grant_sha256"] != self.grant.digest or receipt["identity"] not in {self.grant.raw["status_identity"], self.grant.raw["merge_identity"]}
|
|
||||||
or receipt["token_name"] not in {f"{expected_name_prefix}-status", f"{expected_name_prefix}-merge"}
|
|
||||||
or receipt["credential_ref"] != expected_credential_ref
|
|
||||||
or hashlib.sha256(canonical(unsigned_receipt)).hexdigest() != receipt["audit_sha256"]
|
|
||||||
or not hmac.compare_digest(receipt["audit_hmac_signature"], hmac.new(self.audit_key, canonical(unsigned_receipt), hashlib.sha256).hexdigest())):
|
|
||||||
raise ContractDenied("PROVISION_RECEIPT_INVALID")
|
|
||||||
|
|
||||||
def _audit(self, value: dict) -> dict:
|
|
||||||
raw = canonical(value)
|
|
||||||
result = dict(value)
|
|
||||||
result["audit_sha256"] = hashlib.sha256(raw).hexdigest()
|
|
||||||
result["audit_hmac_signature"] = hmac.new(self.audit_key, raw, hashlib.sha256).hexdigest()
|
|
||||||
self.audit_dir.mkdir(mode=0o750, parents=True, exist_ok=True)
|
|
||||||
destination = self.audit_dir / f"{value['operation']}-{value['token_name']}.json"
|
|
||||||
descriptor = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o640)
|
|
||||||
try:
|
|
||||||
os.write(descriptor, canonical(result) + b"\n")
|
|
||||||
os.fsync(descriptor)
|
|
||||||
finally:
|
|
||||||
os.close(descriptor)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
parser = argparse.ArgumentParser(prog="aios-task-credential-broker")
|
|
||||||
parser.add_argument("--grant", required=True, type=Path)
|
|
||||||
parser.add_argument("--audit-dir", required=True, type=Path)
|
|
||||||
parser.add_argument("--operation", choices=("provision", "revoke"))
|
|
||||||
parser.add_argument("--identity")
|
|
||||||
parser.add_argument("--token-name")
|
|
||||||
parser.add_argument("--encrypted-path", type=Path)
|
|
||||||
parser.add_argument("--receipt", type=Path)
|
|
||||||
parser.add_argument("--request", type=Path)
|
|
||||||
parser.add_argument("--grant-signature", required=True, type=Path)
|
|
||||||
parser.add_argument("--allowed-signers", required=True, type=Path)
|
|
||||||
parser.add_argument("--audit-key", required=True, type=Path)
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
grant = load_signed_task_grant(args.grant, args.grant_signature, args.allowed_signers)
|
|
||||||
broker = TaskCredentialBroker(grant, args.audit_dir, read_runtime_secret(args.audit_key))
|
|
||||||
if args.request is not None:
|
|
||||||
request = json.loads(args.request.read_text(encoding="utf-8"))
|
|
||||||
if not isinstance(request, dict) or set(request) not in ({"operation", "identity", "token_name", "encrypted_path"}, {"operation", "identity", "token_name", "encrypted_path", "receipt"}):
|
|
||||||
raise ContractDenied("CREDENTIAL_REQUEST_INVALID")
|
|
||||||
args.operation = request["operation"]
|
|
||||||
args.identity = request["identity"]
|
|
||||||
args.token_name = request["token_name"]
|
|
||||||
args.encrypted_path = Path(request["encrypted_path"])
|
|
||||||
args.receipt = Path(request["receipt"]) if "receipt" in request else None
|
|
||||||
if args.operation is None or args.identity is None or args.token_name is None or args.encrypted_path is None:
|
|
||||||
raise ContractDenied("CREDENTIAL_REQUEST_INCOMPLETE")
|
|
||||||
if args.operation == "provision":
|
|
||||||
result = broker.provision(identity=args.identity, token_name=args.token_name, encrypted_path=args.encrypted_path)
|
|
||||||
else:
|
|
||||||
if args.receipt is None:
|
|
||||||
raise ContractDenied("RECEIPT_REQUIRED")
|
|
||||||
result = broker.revoke(receipt=json.loads(args.receipt.read_text(encoding="utf-8")), encrypted_path=args.encrypted_path)
|
|
||||||
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import sqlite3
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .task_credential_broker import SYSTEMD_CREDS, TaskCredentialBroker
|
|
||||||
from .task_status_merge_broker import ContractDenied, load_signed_task_grant
|
|
||||||
|
|
||||||
|
|
||||||
RUNTIME_ROOT = Path("/var/lib/aios-gitops/runtime/task-status-merge")
|
|
||||||
CREDENTIAL_ROOT = Path("/run/aios-task-credential-broker")
|
|
||||||
|
|
||||||
|
|
||||||
def _operation_complete(task_dir: Path, token_name: str) -> bool:
|
|
||||||
if token_name.endswith("-status"):
|
|
||||||
journal, operation = task_dir / "status-journal" / "journal.sqlite3", "statuses"
|
|
||||||
elif token_name.endswith("-merge"):
|
|
||||||
journal, operation = task_dir / "merge-journal" / "journal.sqlite3", "merge"
|
|
||||||
else:
|
|
||||||
raise ContractDenied("SWEEPER_TOKEN_NAME_INVALID")
|
|
||||||
if not journal.is_file() or journal.is_symlink():
|
|
||||||
return False
|
|
||||||
connection = sqlite3.connect(f"file:{journal}?mode=ro", uri=True)
|
|
||||||
try:
|
|
||||||
row = connection.execute("SELECT state FROM quota WHERE operation=?", (operation,)).fetchone()
|
|
||||||
finally:
|
|
||||||
connection.close()
|
|
||||||
return row == ("COMMITTED",)
|
|
||||||
|
|
||||||
|
|
||||||
def sweep(runtime_root: Path, allowed_signers: Path, *, now: datetime | None = None) -> list[dict]:
|
|
||||||
if os.geteuid() != 0:
|
|
||||||
raise ContractDenied("ROOT_SWEEPER_REQUIRED")
|
|
||||||
if runtime_root != RUNTIME_ROOT or runtime_root.is_symlink():
|
|
||||||
raise ContractDenied("SWEEPER_ROOT_INVALID")
|
|
||||||
current = now or datetime.now(timezone.utc)
|
|
||||||
results: list[dict] = []
|
|
||||||
for task_dir in sorted(runtime_root.iterdir() if runtime_root.exists() else []):
|
|
||||||
if not task_dir.is_dir() or task_dir.is_symlink():
|
|
||||||
raise ContractDenied("SWEEPER_TASK_DIR_INVALID")
|
|
||||||
grant_path, signature_path = task_dir / "grant.json", task_dir / "grant.sig"
|
|
||||||
if not grant_path.is_file() or not signature_path.is_file():
|
|
||||||
raise ContractDenied("SWEEPER_GRANT_INCOMPLETE")
|
|
||||||
grant = load_signed_task_grant(grant_path, signature_path, allowed_signers, now=current, allow_expired=True)
|
|
||||||
audit_credential = CREDENTIAL_ROOT / f"{task_dir.name}-audit.cred"
|
|
||||||
if not audit_credential.is_file() or audit_credential.is_symlink():
|
|
||||||
raise ContractDenied("SWEEPER_TASK_AUDIT_CREDENTIAL_MISSING")
|
|
||||||
audit_key = subprocess.run(
|
|
||||||
[str(SYSTEMD_CREDS), "decrypt", str(audit_credential), "-"], check=True,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
||||||
).stdout.strip()
|
|
||||||
if not audit_key or b"\n" in audit_key or b"\x00" in audit_key:
|
|
||||||
raise ContractDenied("SWEEPER_TASK_AUDIT_KEY_INVALID")
|
|
||||||
broker = TaskCredentialBroker(grant, task_dir / "audit", audit_key)
|
|
||||||
for receipt_path in sorted((task_dir / "audit").glob("credential_provision-*.json")):
|
|
||||||
if receipt_path.is_symlink():
|
|
||||||
raise ContractDenied("SWEEPER_RECEIPT_UNSAFE")
|
|
||||||
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
|
|
||||||
token_name = str(receipt.get("token_name") or "")
|
|
||||||
credential_ref = str(receipt.get("credential_ref") or "")
|
|
||||||
prefix = "systemd-credential-encrypted://"
|
|
||||||
if not credential_ref.startswith(prefix):
|
|
||||||
raise ContractDenied("SWEEPER_CREDENTIAL_REF_INVALID")
|
|
||||||
encrypted_path = CREDENTIAL_ROOT / credential_ref.removeprefix(prefix)
|
|
||||||
revoked_receipt = task_dir / "audit" / f"credential_revoke-{token_name}.json"
|
|
||||||
if revoked_receipt.exists():
|
|
||||||
if encrypted_path.exists():
|
|
||||||
raise ContractDenied("SWEEPER_REVOKED_PROJECTION_REAPPEARED")
|
|
||||||
continue
|
|
||||||
if current < grant.expires_at and not _operation_complete(task_dir, token_name):
|
|
||||||
continue
|
|
||||||
if not encrypted_path.is_file() or encrypted_path.is_symlink():
|
|
||||||
raise ContractDenied("SWEEPER_CREDENTIAL_PROJECTION_MISSING")
|
|
||||||
results.append(broker.revoke(receipt=receipt, encrypted_path=encrypted_path))
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
parser = argparse.ArgumentParser(prog="aios-task-credential-expiry-sweeper")
|
|
||||||
parser.add_argument("--runtime-root", required=True, type=Path)
|
|
||||||
parser.add_argument("--allowed-signers", required=True, type=Path)
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
result = sweep(args.runtime_root, args.allowed_signers)
|
|
||||||
print(json.dumps({"status": "PASS", "revoked": result}, sort_keys=True, separators=(",", ":")))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
-1090
File diff suppressed because it is too large
Load Diff
-472
@@ -1,472 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import sqlite3
|
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
from typing import Any, Protocol
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_CONTEXTS = frozenset({
|
|
||||||
"aios/branch-protection",
|
|
||||||
"aios/evidence-closure",
|
|
||||||
"aios/secret-scan",
|
|
||||||
"aios/state-machine",
|
|
||||||
"aios/validator",
|
|
||||||
})
|
|
||||||
ALLOWED_IDENTITIES = frozenset({"codex", "gpt"})
|
|
||||||
OID = re.compile(r"^[0-9a-f]{40}$")
|
|
||||||
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
||||||
SECRET_REF = re.compile(r"^systemd-credential://[A-Za-z0-9_.-]+$")
|
|
||||||
SUPERSEDED_PR_NUMBER = 36
|
|
||||||
SUPERSEDED_HEAD_OID = "a8644fc172bd8ebbba17a16e6bf586c93fd1e793"
|
|
||||||
FIELDS = frozenset({
|
|
||||||
"schema_id", "schema_version", "repository", "pr_number", "base_oid",
|
|
||||||
"target_commit", "approved_tree_oid", "manifest_path", "manifest_blob_oid", "manifest_sha256",
|
|
||||||
"payload_sha256", "status_identity", "merge_identity", "status_contexts",
|
|
||||||
"status_write_quota", "merge_quota", "merge_method", "branch_push_allowed",
|
|
||||||
"tag_write_allowed", "main_write_allowed", "registry_write_allowed",
|
|
||||||
"baseline_write_allowed", "production_write_allowed", "issued_at", "expires_at",
|
|
||||||
"audit_key_ref", "grant_signer_identity", "grant_signature_namespace",
|
|
||||||
"status_evidence_sha256", "supersedes_pr_number", "supersedes_head_oid",
|
|
||||||
"renewal_required", "timestamp_contract", "canonical_timezone",
|
|
||||||
"raw_timestamp_preserved", "naive_datetime_allowed",
|
|
||||||
})
|
|
||||||
RFC3339 = re.compile(
|
|
||||||
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContractDenied(RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def canonical(value: Any) -> bytes:
|
|
||||||
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def _time(value: str) -> datetime:
|
|
||||||
if not isinstance(value, str) or value != value.strip() or not RFC3339.fullmatch(value):
|
|
||||||
raise ContractDenied("TIMESTAMP_INVALID")
|
|
||||||
try:
|
|
||||||
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
|
|
||||||
parsed = datetime.fromisoformat(normalized)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ContractDenied("TIMESTAMP_INVALID") from exc
|
|
||||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
||||||
raise ContractDenied("TIMESTAMP_TIMEZONE_REQUIRED")
|
|
||||||
return parsed.astimezone(timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def _utc_timestamp(value: datetime) -> str:
|
|
||||||
normalized = value.astimezone(timezone.utc).isoformat()
|
|
||||||
return normalized[:-6] + "Z" if normalized.endswith("+00:00") else normalized
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class TaskGrant:
|
|
||||||
raw: dict[str, Any]
|
|
||||||
issued_at: datetime
|
|
||||||
expires_at: datetime
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, value: dict[str, Any], *, now: datetime | None = None,
|
|
||||||
allow_expired: bool = False) -> "TaskGrant":
|
|
||||||
if not isinstance(value, dict) or set(value) != FIELDS:
|
|
||||||
raise ContractDenied("GRANT_FIELDS_INVALID")
|
|
||||||
if value["schema_id"] != "AIOS_TASK_STATUS_MERGE_GRANT" or value["schema_version"] != 1:
|
|
||||||
raise ContractDenied("GRANT_SCHEMA_INVALID")
|
|
||||||
if value["repository"] != "aios/aios" or type(value["pr_number"]) is not int or value["pr_number"] <= 0:
|
|
||||||
raise ContractDenied("REPOSITORY_OR_PR_MISMATCH")
|
|
||||||
for field in ("base_oid", "target_commit", "approved_tree_oid", "manifest_blob_oid"):
|
|
||||||
if not isinstance(value[field], str) or not OID.fullmatch(value[field]):
|
|
||||||
raise ContractDenied("OID_INVALID")
|
|
||||||
if not isinstance(value["manifest_path"], str) or not re.fullmatch(r"approvals/[A-Za-z0-9._/-]+\.json", value["manifest_path"]) or ".." in value["manifest_path"].split("/"):
|
|
||||||
raise ContractDenied("MANIFEST_PATH_INVALID")
|
|
||||||
for field in ("manifest_sha256", "payload_sha256"):
|
|
||||||
if not isinstance(value[field], str) or not SHA256.fullmatch(value[field]):
|
|
||||||
raise ContractDenied("SHA256_INVALID")
|
|
||||||
evidence = value["status_evidence_sha256"]
|
|
||||||
if not isinstance(evidence, dict) or set(evidence) != ALLOWED_CONTEXTS or any(not isinstance(item, str) or not SHA256.fullmatch(item) for item in evidence.values()):
|
|
||||||
raise ContractDenied("STATUS_EVIDENCE_BINDING_INVALID")
|
|
||||||
if value["supersedes_pr_number"] != SUPERSEDED_PR_NUMBER or value["pr_number"] == SUPERSEDED_PR_NUMBER:
|
|
||||||
raise ContractDenied("RENEWAL_PR_INVALID")
|
|
||||||
if value["supersedes_head_oid"] != SUPERSEDED_HEAD_OID or value["supersedes_head_oid"] == value["target_commit"] or value["renewal_required"] is not True:
|
|
||||||
raise ContractDenied("RENEWAL_BINDING_INVALID")
|
|
||||||
if value["status_identity"] not in ALLOWED_IDENTITIES or value["merge_identity"] not in ALLOWED_IDENTITIES:
|
|
||||||
raise ContractDenied("IDENTITY_NOT_ALLOWED")
|
|
||||||
if set(value["status_contexts"]) != ALLOWED_CONTEXTS or len(value["status_contexts"]) != 5:
|
|
||||||
raise ContractDenied("STATUS_CONTEXTS_INVALID")
|
|
||||||
if value["status_write_quota"] != 5 or value["merge_quota"] != 1:
|
|
||||||
raise ContractDenied("QUOTA_INVALID")
|
|
||||||
if value["merge_method"] != "fast-forward-only":
|
|
||||||
raise ContractDenied("MERGE_METHOD_INVALID")
|
|
||||||
denied = (
|
|
||||||
"branch_push_allowed", "tag_write_allowed", "main_write_allowed",
|
|
||||||
"registry_write_allowed", "baseline_write_allowed", "production_write_allowed",
|
|
||||||
)
|
|
||||||
if any(value[field] is not False for field in denied):
|
|
||||||
raise ContractDenied("WRITE_SCOPE_EXPANDED")
|
|
||||||
if not isinstance(value["audit_key_ref"], str) or not SECRET_REF.fullmatch(value["audit_key_ref"]):
|
|
||||||
raise ContractDenied("AUDIT_KEY_REFERENCE_INVALID")
|
|
||||||
if value["grant_signer_identity"] != "aios-task-grant-signer" or value["grant_signature_namespace"] != "aios-task-status-merge-grant-v1":
|
|
||||||
raise ContractDenied("GRANT_SIGNATURE_POLICY_INVALID")
|
|
||||||
if (value["timestamp_contract"] != "RFC3339_OFFSET_AWARE_V2"
|
|
||||||
or value["canonical_timezone"] != "UTC"
|
|
||||||
or value["raw_timestamp_preserved"] is not True
|
|
||||||
or value["naive_datetime_allowed"] is not False):
|
|
||||||
raise ContractDenied("TIMESTAMP_CONTRACT_INVALID")
|
|
||||||
issued, expires = _time(value["issued_at"]), _time(value["expires_at"])
|
|
||||||
current = now or datetime.now(timezone.utc)
|
|
||||||
lifetime = (expires - issued).total_seconds()
|
|
||||||
if lifetime <= 0 or lifetime > 12 * 3600 or (not allow_expired and current >= expires):
|
|
||||||
raise ContractDenied("GRANT_EXPIRED_OR_LIFETIME_INVALID")
|
|
||||||
return cls(dict(value), issued, expires)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def digest(self) -> str:
|
|
||||||
return hashlib.sha256(canonical(self.raw)).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def load_signed_task_grant(grant_path: Path, signature_path: Path, allowed_signers: Path,
|
|
||||||
*, now: datetime | None = None, allow_expired: bool = False) -> TaskGrant:
|
|
||||||
for path in (grant_path, signature_path, allowed_signers):
|
|
||||||
if not path.is_absolute() or path.is_symlink():
|
|
||||||
raise ContractDenied("GRANT_SIGNATURE_PATH_UNSAFE")
|
|
||||||
raw = grant_path.read_bytes()
|
|
||||||
if len(raw) > 1024 * 1024:
|
|
||||||
raise ContractDenied("GRANT_TOO_LARGE")
|
|
||||||
try:
|
|
||||||
value = json.loads(raw)
|
|
||||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
||||||
raise ContractDenied("GRANT_JSON_INVALID") from exc
|
|
||||||
if raw != canonical(value) + b"\n":
|
|
||||||
raise ContractDenied("GRANT_NOT_CANONICAL")
|
|
||||||
grant = TaskGrant.from_dict(value, now=now, allow_expired=allow_expired)
|
|
||||||
command = [
|
|
||||||
"/usr/bin/ssh-keygen", "-Y", "verify", "-f", str(allowed_signers),
|
|
||||||
"-I", "aios-task-grant-signer", "-n", "aios-task-status-merge-grant-v1",
|
|
||||||
"-s", str(signature_path),
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
subprocess.run(command, input=raw, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
|
||||||
except (OSError, subprocess.CalledProcessError) as exc:
|
|
||||||
raise ContractDenied("GRANT_SIGNATURE_INVALID") from exc
|
|
||||||
return grant
|
|
||||||
|
|
||||||
|
|
||||||
class API(Protocol):
|
|
||||||
def get(self, path: str) -> Any: ...
|
|
||||||
def post(self, path: str, payload: dict[str, Any]) -> Any: ...
|
|
||||||
|
|
||||||
|
|
||||||
def read_runtime_secret(path: Path) -> bytes:
|
|
||||||
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
||||||
descriptor = os.open(path, flags)
|
|
||||||
try:
|
|
||||||
metadata = os.fstat(descriptor)
|
|
||||||
mode = stat.S_IMODE(metadata.st_mode)
|
|
||||||
credential_directory = os.environ.get("CREDENTIALS_DIRECTORY")
|
|
||||||
systemd_projection = (
|
|
||||||
credential_directory is not None
|
|
||||||
and path.parent == Path(credential_directory)
|
|
||||||
and mode == 0o440
|
|
||||||
and metadata.st_uid == 0
|
|
||||||
and metadata.st_gid == 0
|
|
||||||
)
|
|
||||||
if not stat.S_ISREG(metadata.st_mode) or (mode != 0o400 and not systemd_projection):
|
|
||||||
raise ContractDenied("CREDENTIAL_FILE_UNSAFE")
|
|
||||||
value = os.read(descriptor, 16 * 1024 + 1)
|
|
||||||
finally:
|
|
||||||
os.close(descriptor)
|
|
||||||
if not value or len(value) > 16 * 1024 or b"\x00" in value or b"\n" in value.strip():
|
|
||||||
raise ContractDenied("CREDENTIAL_CONTENT_INVALID")
|
|
||||||
return value.strip()
|
|
||||||
|
|
||||||
|
|
||||||
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
||||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaClient:
|
|
||||||
def __init__(self, token_path: Path, api_base: str = "http://127.0.0.1:3000/api/v1"):
|
|
||||||
parsed = urllib.parse.urlparse(api_base)
|
|
||||||
if parsed.scheme != "http" or parsed.hostname != "127.0.0.1" or parsed.path.rstrip("/") != "/api/v1" or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
||||||
raise ContractDenied("GITEA_API_ORIGIN_INVALID")
|
|
||||||
self.token_path = Path(token_path)
|
|
||||||
self.api_base = api_base.rstrip("/")
|
|
||||||
self.opener = urllib.request.build_opener(_NoRedirect)
|
|
||||||
|
|
||||||
def _request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
|
|
||||||
if not path.startswith("/") or ".." in path.split("/"):
|
|
||||||
raise ContractDenied("GITEA_API_PATH_INVALID")
|
|
||||||
token = read_runtime_secret(self.token_path).decode("utf-8")
|
|
||||||
body = canonical(payload) if payload is not None else None
|
|
||||||
request = urllib.request.Request(self.api_base + path, data=body, method=method, headers={"Authorization": f"token {token}", "Content-Type": "application/json", "Accept": "application/json"})
|
|
||||||
try:
|
|
||||||
with self.opener.open(request, timeout=20) as response:
|
|
||||||
raw = response.read(2 * 1024 * 1024 + 1)
|
|
||||||
except (urllib.error.URLError, OSError) as exc:
|
|
||||||
raise ContractDenied(f"GITEA_API_FAILED:{type(exc).__name__}") from exc
|
|
||||||
if len(raw) > 2 * 1024 * 1024:
|
|
||||||
raise ContractDenied("GITEA_API_RESPONSE_TOO_LARGE")
|
|
||||||
return json.loads(raw.decode("utf-8")) if raw else None
|
|
||||||
|
|
||||||
def get(self, path: str) -> Any:
|
|
||||||
return self._request("GET", path)
|
|
||||||
|
|
||||||
def post(self, path: str, payload: dict[str, Any]) -> Any:
|
|
||||||
return self._request("POST", path, payload)
|
|
||||||
|
|
||||||
|
|
||||||
class QuotaJournal:
|
|
||||||
def __init__(self, path: Path, grant: TaskGrant):
|
|
||||||
self.path = Path(path)
|
|
||||||
self.grant = grant
|
|
||||||
self.connection = sqlite3.connect(self.path, isolation_level=None)
|
|
||||||
journal_mode = self.connection.execute("PRAGMA journal_mode=WAL").fetchone()
|
|
||||||
if journal_mode is None or str(journal_mode[0]).lower() != "wal":
|
|
||||||
raise ContractDenied("JOURNAL_MODE_INVALID")
|
|
||||||
self.connection.execute("PRAGMA synchronous=FULL")
|
|
||||||
synchronous = self.connection.execute("PRAGMA synchronous").fetchone()
|
|
||||||
if synchronous is None or int(synchronous[0]) != 2:
|
|
||||||
raise ContractDenied("JOURNAL_SYNCHRONOUS_INVALID")
|
|
||||||
self.connection.execute("CREATE TABLE IF NOT EXISTS quota(operation TEXT PRIMARY KEY, state TEXT NOT NULL, effect_sha256 TEXT)")
|
|
||||||
self.connection.execute("CREATE TABLE IF NOT EXISTS audit(seq INTEGER PRIMARY KEY AUTOINCREMENT, event BLOB NOT NULL, previous_hash TEXT NOT NULL, event_hash TEXT NOT NULL UNIQUE)")
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.connection.close()
|
|
||||||
|
|
||||||
def reserve(self, operation: str) -> str:
|
|
||||||
limit = {"statuses": 1, "merge": 1}.get(operation)
|
|
||||||
if limit is None:
|
|
||||||
raise ContractDenied("OPERATION_INVALID")
|
|
||||||
self.connection.execute("BEGIN IMMEDIATE")
|
|
||||||
try:
|
|
||||||
row = self.connection.execute("SELECT state FROM quota WHERE operation=?", (operation,)).fetchone()
|
|
||||||
if row and row[0] == "COMMITTED":
|
|
||||||
raise ContractDenied("STATUS_QUOTA_EXHAUSTED" if operation == "statuses" else "MERGE_QUOTA_EXHAUSTED")
|
|
||||||
if row and row[0] != "RESERVED":
|
|
||||||
raise ContractDenied("JOURNAL_STATE_INVALID")
|
|
||||||
if not row:
|
|
||||||
self.connection.execute("INSERT INTO quota(operation,state,effect_sha256) VALUES(?,'RESERVED',NULL)", (operation,))
|
|
||||||
self.connection.execute("COMMIT")
|
|
||||||
return "RESERVED"
|
|
||||||
except Exception:
|
|
||||||
self.connection.execute("ROLLBACK")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def state(self, operation: str) -> str | None:
|
|
||||||
row = self.connection.execute("SELECT state FROM quota WHERE operation=?", (operation,)).fetchone()
|
|
||||||
return str(row[0]) if row else None
|
|
||||||
|
|
||||||
def commit(self, operation: str, effect: dict[str, Any]) -> None:
|
|
||||||
digest = hashlib.sha256(canonical(effect)).hexdigest()
|
|
||||||
self.connection.execute("BEGIN IMMEDIATE")
|
|
||||||
try:
|
|
||||||
row = self.connection.execute("SELECT state,effect_sha256 FROM quota WHERE operation=?", (operation,)).fetchone()
|
|
||||||
if row is None or row[0] not in {"RESERVED", "COMMITTED"}:
|
|
||||||
raise ContractDenied("RESERVATION_MISSING")
|
|
||||||
if row[0] == "COMMITTED" and row[1] != digest:
|
|
||||||
raise ContractDenied("COMMITTED_EFFECT_MISMATCH")
|
|
||||||
self.connection.execute("UPDATE quota SET state='COMMITTED',effect_sha256=? WHERE operation=?", (digest, operation))
|
|
||||||
self.connection.execute("COMMIT")
|
|
||||||
except Exception:
|
|
||||||
self.connection.execute("ROLLBACK")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def append(self, event: dict[str, Any]) -> str:
|
|
||||||
row = self.connection.execute("SELECT event_hash FROM audit ORDER BY seq DESC LIMIT 1").fetchone()
|
|
||||||
previous = row[0] if row else "0" * 64
|
|
||||||
raw = canonical(event)
|
|
||||||
digest = hashlib.sha256(previous.encode() + raw).hexdigest()
|
|
||||||
self.connection.execute("INSERT INTO audit(event,previous_hash,event_hash) VALUES(?,?,?)", (raw, previous, digest))
|
|
||||||
return digest
|
|
||||||
|
|
||||||
def verify_chain(self) -> bool:
|
|
||||||
previous = "0" * 64
|
|
||||||
for raw, observed_previous, observed_hash in self.connection.execute("SELECT event,previous_hash,event_hash FROM audit ORDER BY seq"):
|
|
||||||
if observed_previous != previous or hashlib.sha256(previous.encode() + raw).hexdigest() != observed_hash:
|
|
||||||
return False
|
|
||||||
previous = observed_hash
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _latest_statuses(values: Any) -> dict[str, dict[str, Any]]:
|
|
||||||
if not isinstance(values, list):
|
|
||||||
raise ContractDenied("STATUS_RESPONSE_INVALID")
|
|
||||||
latest: dict[str, tuple[tuple[str, int], dict[str, Any]]] = {}
|
|
||||||
for item in values:
|
|
||||||
if not isinstance(item, dict) or item.get("context") not in ALLOWED_CONTEXTS:
|
|
||||||
continue
|
|
||||||
key = (str(item.get("created_at") or ""), int(item.get("id") or 0))
|
|
||||||
context = item["context"]
|
|
||||||
if context not in latest or key > latest[context][0]:
|
|
||||||
latest[context] = (key, item)
|
|
||||||
return {context: item for context, (_, item) in latest.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_owner_binding(api: API, grant: TaskGrant) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
||||||
pr_number = grant.raw["pr_number"]
|
|
||||||
head, base = grant.raw["target_commit"], grant.raw["base_oid"]
|
|
||||||
pr = api.get(f"/repos/aios/aios/pulls/{pr_number}")
|
|
||||||
reviews = api.get(f"/repos/aios/aios/pulls/{pr_number}/reviews")
|
|
||||||
if not isinstance(pr, dict) or pr.get("state") != "open" or pr.get("mergeable") is not True or (pr.get("head") or {}).get("sha") != head or (pr.get("base") or {}).get("sha") != base or (pr.get("base") or {}).get("ref") != "governance/approvals":
|
|
||||||
raise ContractDenied("PR_BINDING_INVALID")
|
|
||||||
if not isinstance(reviews, list):
|
|
||||||
raise ContractDenied("REVIEW_RESPONSE_INVALID")
|
|
||||||
owners = [item for item in reviews if isinstance(item, dict) and (item.get("user") or {}).get("login") == "owner" and (item.get("user") or {}).get("id") == 2]
|
|
||||||
if not owners:
|
|
||||||
raise ContractDenied("OWNER_EXACT_HEAD_APPROVAL_MISSING")
|
|
||||||
latest = max(owners, key=lambda item: int(item.get("id") or -1))
|
|
||||||
if latest.get("state") != "APPROVED" or latest.get("commit_id") != head or latest.get("official") is not True or latest.get("stale") is not False or latest.get("dismissed") is not False:
|
|
||||||
raise ContractDenied("OWNER_EXACT_HEAD_APPROVAL_MISSING")
|
|
||||||
raw_timestamp = str(latest.get("submitted_at") or "")
|
|
||||||
submitted = _time(raw_timestamp)
|
|
||||||
if submitted < grant.issued_at or submitted >= grant.expires_at:
|
|
||||||
raise ContractDenied("OWNER_APPROVAL_EXPIRED")
|
|
||||||
normalized_review = dict(latest)
|
|
||||||
normalized_review["review_timestamp_raw"] = raw_timestamp
|
|
||||||
normalized_review["review_timestamp_utc"] = _utc_timestamp(submitted)
|
|
||||||
return pr, normalized_review
|
|
||||||
|
|
||||||
|
|
||||||
def _signed_receipt(value: dict[str, Any], key: bytes) -> dict[str, Any]:
|
|
||||||
if not key:
|
|
||||||
raise ContractDenied("AUDIT_KEY_EMPTY")
|
|
||||||
result = dict(value)
|
|
||||||
result["audit_signature"] = hmac.new(key, canonical(value), hashlib.sha256).hexdigest()
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class StatusBroker:
|
|
||||||
def __init__(self, grant: TaskGrant, journal: QuotaJournal, api: API, audit_key: bytes):
|
|
||||||
self.grant, self.journal, self.api, self.audit_key = grant, journal, api, audit_key
|
|
||||||
|
|
||||||
def publish(self, evidence: list[dict[str, Any]]) -> dict[str, Any]:
|
|
||||||
if not isinstance(evidence, list) or len(evidence) != 5:
|
|
||||||
raise ContractDenied("EVIDENCE_SET_INVALID")
|
|
||||||
mapped = {item.get("context"): item for item in evidence if isinstance(item, dict)}
|
|
||||||
if set(mapped) != ALLOWED_CONTEXTS or any(set(item) != {"context", "evidence_sha256"} or not SHA256.fullmatch(str(item["evidence_sha256"])) or item["evidence_sha256"] != self.grant.raw["status_evidence_sha256"].get(context) for context, item in mapped.items()):
|
|
||||||
raise ContractDenied("EVIDENCE_SET_INVALID")
|
|
||||||
_, review = _verify_owner_binding(self.api, self.grant)
|
|
||||||
self.journal.reserve("statuses")
|
|
||||||
published = []
|
|
||||||
head = self.grant.raw["target_commit"]
|
|
||||||
pr_number = self.grant.raw["pr_number"]
|
|
||||||
observed_before = self.api.get(f"/repos/aios/aios/commits/{head}/statuses")
|
|
||||||
already = _latest_statuses(observed_before)
|
|
||||||
already = {context: item for context, item in already.items()
|
|
||||||
if item.get("status", item.get("state")) == "success"
|
|
||||||
and item.get("sha", head) == head
|
|
||||||
and (item.get("creator") or {}).get("login") == self.grant.raw["status_identity"]
|
|
||||||
and str(item.get("description") or "").endswith(mapped[context]["evidence_sha256"])}
|
|
||||||
for context in sorted(ALLOWED_CONTEXTS):
|
|
||||||
digest = mapped[context]["evidence_sha256"]
|
|
||||||
if context in already:
|
|
||||||
published.append({"context": context, "evidence_sha256": digest, "status_id": already[context].get("id"), "reconciled": True})
|
|
||||||
continue
|
|
||||||
response = self.api.post(f"/repos/aios/aios/statuses/{head}", {
|
|
||||||
"state": "success", "context": context,
|
|
||||||
"description": f"task-scoped PASS evidence_sha256={digest}",
|
|
||||||
"target_url": f"https://g.cnnetmkt.top/git/aios/aios/pulls/{pr_number}",
|
|
||||||
})
|
|
||||||
if not isinstance(response, dict) or response.get("context") != context or response.get("status", response.get("state")) != "success":
|
|
||||||
raise ContractDenied("STATUS_PUBLICATION_FAILED")
|
|
||||||
published.append({"context": context, "evidence_sha256": digest, "status_id": response.get("id")})
|
|
||||||
observed = self.api.get(f"/repos/aios/aios/commits/{head}/statuses")
|
|
||||||
latest = _latest_statuses(observed)
|
|
||||||
if set(latest) != ALLOWED_CONTEXTS or any(
|
|
||||||
item.get("status", item.get("state")) != "success"
|
|
||||||
or item.get("sha", head) != head
|
|
||||||
or (item.get("creator") or {}).get("login") != self.grant.raw["status_identity"]
|
|
||||||
or not str(item.get("description") or "").endswith(mapped[context]["evidence_sha256"])
|
|
||||||
for context, item in latest.items()
|
|
||||||
):
|
|
||||||
raise ContractDenied("STATUS_READBACK_INVALID")
|
|
||||||
receipt = {"status": "PASS", "operation": "publish_statuses", "grant_sha256": self.grant.digest, "target_commit": head, "published_count": 5, "published": published, "review_timestamp_raw": review["review_timestamp_raw"], "review_timestamp_utc": review["review_timestamp_utc"]}
|
|
||||||
receipt["audit_signature"] = hmac.new(self.audit_key, canonical(receipt), hashlib.sha256).hexdigest()
|
|
||||||
receipt["audit_chain_hash"] = self.journal.append(receipt)
|
|
||||||
self.journal.commit("statuses", receipt)
|
|
||||||
return receipt
|
|
||||||
|
|
||||||
|
|
||||||
class MergeBroker:
|
|
||||||
def __init__(self, grant: TaskGrant, journal: QuotaJournal, api: API, audit_key: bytes):
|
|
||||||
self.grant, self.journal, self.api, self.audit_key = grant, journal, api, audit_key
|
|
||||||
|
|
||||||
def merge(self) -> dict[str, Any]:
|
|
||||||
head, base = self.grant.raw["target_commit"], self.grant.raw["base_oid"]
|
|
||||||
pr_number = self.grant.raw["pr_number"]
|
|
||||||
pr = self.api.get(f"/repos/aios/aios/pulls/{pr_number}")
|
|
||||||
if isinstance(pr, dict) and pr.get("merged") is True:
|
|
||||||
observed_branch = self.api.get("/repos/aios/aios/branches/governance%2Fapprovals")
|
|
||||||
if pr.get("merge_commit_sha") != head or (observed_branch.get("commit") or {}).get("id") != head or self.journal.state("merge") != "RESERVED":
|
|
||||||
raise ContractDenied("FAST_FORWARD_RESULT_MISMATCH")
|
|
||||||
receipt = {"status": "PASS", "operation": "fast_forward_merge", "grant_sha256": self.grant.digest, "pr_number": pr_number, "governance_oid": head, "merge_method": "fast-forward-only", "reconciled": True}
|
|
||||||
receipt["audit_signature"] = hmac.new(self.audit_key, canonical(receipt), hashlib.sha256).hexdigest()
|
|
||||||
receipt["audit_chain_hash"] = self.journal.append(receipt)
|
|
||||||
self.journal.commit("merge", receipt)
|
|
||||||
return receipt
|
|
||||||
pr, review = _verify_owner_binding(self.api, self.grant)
|
|
||||||
statuses = self.api.get(f"/repos/aios/aios/commits/{head}/statuses")
|
|
||||||
latest = _latest_statuses(statuses)
|
|
||||||
if set(latest) != ALLOWED_CONTEXTS or any(item.get("status", item.get("state")) != "success" or item.get("sha", head) != head or (item.get("creator") or {}).get("login") != self.grant.raw["status_identity"] or not str(item.get("description") or "").endswith(self.grant.raw["status_evidence_sha256"][context]) for context, item in latest.items()):
|
|
||||||
raise ContractDenied("MACHINE_STATUS_INVALID")
|
|
||||||
self.journal.reserve("merge")
|
|
||||||
self.api.post(f"/repos/aios/aios/pulls/{pr_number}/merge", {"Do": "fast-forward-only", "head_commit_id": head, "force_merge": False, "delete_branch_after_merge": False})
|
|
||||||
observed_pr = self.api.get(f"/repos/aios/aios/pulls/{pr_number}")
|
|
||||||
observed_branch = self.api.get("/repos/aios/aios/branches/governance%2Fapprovals")
|
|
||||||
if observed_pr.get("merged") is not True or observed_pr.get("merge_commit_sha") != head or (observed_branch.get("commit") or {}).get("id") != head:
|
|
||||||
raise ContractDenied("FAST_FORWARD_RESULT_MISMATCH")
|
|
||||||
receipt = {"status": "PASS", "operation": "fast_forward_merge", "grant_sha256": self.grant.digest, "pr_number": pr_number, "governance_oid": head, "merge_method": "fast-forward-only", "review_timestamp_raw": review["review_timestamp_raw"], "review_timestamp_utc": review["review_timestamp_utc"]}
|
|
||||||
receipt["audit_signature"] = hmac.new(self.audit_key, canonical(receipt), hashlib.sha256).hexdigest()
|
|
||||||
receipt["audit_chain_hash"] = self.journal.append(receipt)
|
|
||||||
self.journal.commit("merge", receipt)
|
|
||||||
return receipt
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
parser = argparse.ArgumentParser(prog="aios-task-status-merge-broker")
|
|
||||||
parser.add_argument("--mode", required=True, choices=("status", "merge"))
|
|
||||||
parser.add_argument("--grant", required=True, type=Path)
|
|
||||||
parser.add_argument("--token", required=True, type=Path)
|
|
||||||
parser.add_argument("--audit-key", required=True, type=Path)
|
|
||||||
parser.add_argument("--grant-signature", required=True, type=Path)
|
|
||||||
parser.add_argument("--allowed-signers", required=True, type=Path)
|
|
||||||
parser.add_argument("--journal", required=True, type=Path)
|
|
||||||
parser.add_argument("--evidence", type=Path)
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
grant = load_signed_task_grant(args.grant, args.grant_signature, args.allowed_signers)
|
|
||||||
journal = QuotaJournal(args.journal, grant)
|
|
||||||
try:
|
|
||||||
client = GiteaClient(args.token)
|
|
||||||
key = read_runtime_secret(args.audit_key)
|
|
||||||
if args.mode == "status":
|
|
||||||
if args.evidence is None:
|
|
||||||
raise ContractDenied("EVIDENCE_REQUIRED")
|
|
||||||
result = StatusBroker(grant, journal, client, key).publish(json.loads(args.evidence.read_text(encoding="utf-8")))
|
|
||||||
else:
|
|
||||||
if args.evidence is not None:
|
|
||||||
raise ContractDenied("EVIDENCE_FORBIDDEN")
|
|
||||||
result = MergeBroker(grant, journal, client, key).merge()
|
|
||||||
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
|
||||||
return 0
|
|
||||||
finally:
|
|
||||||
journal.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
-323
@@ -1,323 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"$id": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP",
|
|
||||||
"title": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP observed approval envelope",
|
|
||||||
"description": "Versioned fail-closed validation envelope assembled from an immutable request plus observed Owner, Git, machine-gate, runtime, and mirror evidence.",
|
|
||||||
"x-schema-version": 1,
|
|
||||||
"x-schema-status": "CANDIDATE",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"document_kind",
|
|
||||||
"request_id",
|
|
||||||
"schema_id",
|
|
||||||
"schema_version",
|
|
||||||
"request_created_at",
|
|
||||||
"request_expires_at",
|
|
||||||
"requester_identity",
|
|
||||||
"repository_identity",
|
|
||||||
"approval_plan_path",
|
|
||||||
"approval_plan_blob_oid",
|
|
||||||
"approval_plan_sha256",
|
|
||||||
"schema_document_sha256",
|
|
||||||
"validator_policy_sha256",
|
|
||||||
"required_machine_contexts",
|
|
||||||
"governance_base_ref",
|
|
||||||
"governance_base_oid",
|
|
||||||
"request_head_ref",
|
|
||||||
"request_head_oid",
|
|
||||||
"code_base_ref",
|
|
||||||
"code_base_oid",
|
|
||||||
"candidate_commit_oid",
|
|
||||||
"candidate_parent_oid",
|
|
||||||
"candidate_tree_oid",
|
|
||||||
"candidate_diff_sha256",
|
|
||||||
"candidate_files_sha256",
|
|
||||||
"target_ref",
|
|
||||||
"target_initial_state",
|
|
||||||
"provision_publisher",
|
|
||||||
"provision_writer",
|
|
||||||
"provision_credential_projection",
|
|
||||||
"publisher_identity",
|
|
||||||
"writer_identity",
|
|
||||||
"credential_secret_ref",
|
|
||||||
"publisher_scope",
|
|
||||||
"writer_scope",
|
|
||||||
"credential_scope",
|
|
||||||
"observed_preapproval_operations",
|
|
||||||
"allowed_operations",
|
|
||||||
"allowed_operations_sha256",
|
|
||||||
"denied_operations",
|
|
||||||
"maximum_operation_count",
|
|
||||||
"maximum_pr_count",
|
|
||||||
"maximum_branch_count",
|
|
||||||
"maximum_commit_count",
|
|
||||||
"allowed_paths",
|
|
||||||
"allowed_paths_sha256",
|
|
||||||
"approved_at",
|
|
||||||
"expires_at",
|
|
||||||
"maximum_lifetime_seconds",
|
|
||||||
"auto_revoke",
|
|
||||||
"required_human_approvals",
|
|
||||||
"allowed_human_approvers",
|
|
||||||
"approved_by",
|
|
||||||
"owner_review_id",
|
|
||||||
"owner_review_commit_oid",
|
|
||||||
"approval_plan_id",
|
|
||||||
"approval_audit_event_id",
|
|
||||||
"production_runtime_anchor_change",
|
|
||||||
"production_writes_enabled",
|
|
||||||
"tag_operations",
|
|
||||||
"registry_operations",
|
|
||||||
"baseline_operations",
|
|
||||||
"deployment_operations",
|
|
||||||
"force_push_allowed",
|
|
||||||
"tag_rewrite_allowed",
|
|
||||||
"direct_main_push_allowed"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"document_kind": {"const": "ENVELOPE"},
|
|
||||||
"request_id": {"type": "string", "minLength": 3, "maxLength": 192, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]+$"},
|
|
||||||
"schema_id": {"const": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP"},
|
|
||||||
"schema_version": {"const": 1},
|
|
||||||
"request_created_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
|
||||||
"request_expires_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
|
||||||
"requester_identity": {"$ref": "#/$defs/identity"},
|
|
||||||
"repository_identity": {"const": "aios/aios"},
|
|
||||||
"approval_plan_path": {"type": "string", "pattern": "^approvals/governance-bootstrap/[A-Za-z0-9._-]+\\.json$"},
|
|
||||||
"approval_plan_blob_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"approval_plan_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"schema_document_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"validator_policy_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"required_machine_contexts": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 5,
|
|
||||||
"maxItems": 5,
|
|
||||||
"uniqueItems": true,
|
|
||||||
"items": {"enum": [
|
|
||||||
"governance/authority-mirror",
|
|
||||||
"governance/fault-injection",
|
|
||||||
"governance/runtime-isolation",
|
|
||||||
"governance/schema-contract",
|
|
||||||
"governance/validator-security"
|
|
||||||
]}
|
|
||||||
},
|
|
||||||
"governance_base_ref": {"const": "refs/heads/governance/approvals"},
|
|
||||||
"governance_base_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"request_head_ref": {"type": "string", "pattern": "^refs/heads/governance/request/[A-Za-z0-9._/-]+$"},
|
|
||||||
"request_head_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"code_base_ref": {"type": "string", "pattern": "^refs/heads/patch/[A-Za-z0-9._/-]+$"},
|
|
||||||
"code_base_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"candidate_commit_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"candidate_parent_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"candidate_tree_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"candidate_diff_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"candidate_files_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"target_ref": {"type": "string", "pattern": "^refs/heads/patch/[A-Za-z0-9._/-]+$"},
|
|
||||||
"target_initial_state": {"const": "ABSENT"},
|
|
||||||
"provision_publisher": {"const": true},
|
|
||||||
"provision_writer": {"const": true},
|
|
||||||
"provision_credential_projection": {"const": true},
|
|
||||||
"publisher_identity": {"$ref": "#/$defs/identity"},
|
|
||||||
"writer_identity": {"$ref": "#/$defs/identity"},
|
|
||||||
"credential_secret_ref": {"type": "string", "pattern": "^(systemd-credential|secret-ref)://[A-Za-z0-9_.-]+$"},
|
|
||||||
"publisher_scope": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["repository", "local_code_write", "administrator"],
|
|
||||||
"properties": {
|
|
||||||
"repository": {"const": "aios/aios"},
|
|
||||||
"local_code_write": {"const": false},
|
|
||||||
"administrator": {"const": false}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"writer_scope": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["repository", "remote_credentials", "remote_push", "run_as_root"],
|
|
||||||
"properties": {
|
|
||||||
"repository": {"const": "aios/aios"},
|
|
||||||
"remote_credentials": {"const": false},
|
|
||||||
"remote_push": {"const": false},
|
|
||||||
"run_as_root": {"const": false}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"credential_scope": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"provider", "projection", "audience", "writer_visible",
|
|
||||||
"runtime_anchor_visible", "expires_with_request", "revocation_required",
|
|
||||||
"post_revoke_http_status"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"provider": {"const": "systemd"},
|
|
||||||
"projection": {"const": "LoadCredential"},
|
|
||||||
"audience": {"$ref": "#/$defs/identity"},
|
|
||||||
"writer_visible": {"const": false},
|
|
||||||
"runtime_anchor_visible": {"const": false},
|
|
||||||
"expires_with_request": {"const": true},
|
|
||||||
"revocation_required": {"const": true},
|
|
||||||
"post_revoke_http_status": {"const": 401}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"observed_preapproval_operations": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 2,
|
|
||||||
"maxItems": 2,
|
|
||||||
"uniqueItems": true,
|
|
||||||
"items": {"enum": [
|
|
||||||
"create_governance_request_branch",
|
|
||||||
"create_governance_approval_pr"
|
|
||||||
]}
|
|
||||||
},
|
|
||||||
"allowed_operations": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"uniqueItems": true,
|
|
||||||
"items": {"enum": [
|
|
||||||
"provision_task_publisher",
|
|
||||||
"provision_task_writer",
|
|
||||||
"provision_task_credential_projection",
|
|
||||||
"create_approved_code_branch",
|
|
||||||
"push_approved_code_branch",
|
|
||||||
"read_remote_refs",
|
|
||||||
"revoke_task_credentials",
|
|
||||||
"remove_task_identities"
|
|
||||||
]}
|
|
||||||
},
|
|
||||||
"allowed_operations_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"denied_operations": {"type": "array", "minItems": 12, "uniqueItems": true, "items": {"type": "string"}},
|
|
||||||
"maximum_operation_count": {"type": "integer", "minimum": 1, "maximum": 10},
|
|
||||||
"maximum_pr_count": {"const": 1},
|
|
||||||
"maximum_branch_count": {"const": 1},
|
|
||||||
"maximum_commit_count": {"const": 1},
|
|
||||||
"allowed_paths": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
|
||||||
"allowed_paths_sha256": {"$ref": "#/$defs/sha256"},
|
|
||||||
"approved_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
|
||||||
"expires_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
|
||||||
"maximum_lifetime_seconds": {"type": "integer", "minimum": 1, "maximum": 43200},
|
|
||||||
"auto_revoke": {"const": true},
|
|
||||||
"required_human_approvals": {"const": 1},
|
|
||||||
"allowed_human_approvers": {"const": ["owner"]},
|
|
||||||
"approved_by": {"const": "owner"},
|
|
||||||
"owner_review_id": {"type": "integer", "minimum": 1},
|
|
||||||
"owner_review_commit_oid": {"$ref": "#/$defs/oid"},
|
|
||||||
"approval_plan_id": {"type": "string", "minLength": 3, "maxLength": 192},
|
|
||||||
"approval_audit_event_id": {"type": "string", "minLength": 3, "maxLength": 192},
|
|
||||||
"production_runtime_anchor_change": {"const": false},
|
|
||||||
"production_writes_enabled": {"const": false},
|
|
||||||
"tag_operations": {"const": false},
|
|
||||||
"registry_operations": {"const": false},
|
|
||||||
"baseline_operations": {"const": false},
|
|
||||||
"deployment_operations": {"const": false},
|
|
||||||
"force_push_allowed": {"const": false},
|
|
||||||
"tag_rewrite_allowed": {"const": false},
|
|
||||||
"direct_main_push_allowed": {"const": false}
|
|
||||||
},
|
|
||||||
"$defs": {
|
|
||||||
"plan": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"document_kind", "schema_id", "schema_version", "payload_sha256",
|
|
||||||
"payload"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"document_kind": {"const": "PLAN"},
|
|
||||||
"schema_id": {"const": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP_PLAN"},
|
|
||||||
"schema_version": {"const": 1},
|
|
||||||
"payload_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"payload": {"$ref": "#/$defs/planPayload"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"planPayload": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"allowed_human_approvers", "allowed_operations", "allowed_operations_sha256",
|
|
||||||
"allowed_paths", "allowed_paths_sha256", "approval_plan_id", "approval_plan_path",
|
|
||||||
"approved_by", "auto_revoke", "baseline_operations", "candidate_commit_oid",
|
|
||||||
"candidate_diff_sha256", "candidate_files_sha256", "candidate_parent_oid",
|
|
||||||
"candidate_tree_oid", "code_base_oid", "code_base_ref", "credential_scope",
|
|
||||||
"credential_secret_ref", "denied_operations", "deployment_operations",
|
|
||||||
"direct_main_push_allowed", "expires_at", "force_push_allowed",
|
|
||||||
"governance_base_oid", "governance_base_ref", "maximum_branch_count",
|
|
||||||
"maximum_commit_count", "maximum_lifetime_seconds", "maximum_operation_count",
|
|
||||||
"maximum_pr_count", "observed_preapproval_operations",
|
|
||||||
"production_runtime_anchor_change", "production_writes_enabled",
|
|
||||||
"provision_credential_projection", "provision_publisher", "provision_writer",
|
|
||||||
"publisher_identity", "publisher_scope", "registry_operations",
|
|
||||||
"repository_identity", "request_created_at", "request_expires_at",
|
|
||||||
"request_head_ref", "request_id", "requester_identity",
|
|
||||||
"required_human_approvals", "required_machine_contexts",
|
|
||||||
"schema_document_sha256", "schema_id", "schema_version", "tag_operations",
|
|
||||||
"tag_rewrite_allowed", "target_initial_state", "target_ref",
|
|
||||||
"validator_policy_sha256", "writer_identity", "writer_scope"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"allowed_human_approvers": {"$ref": "#/properties/allowed_human_approvers"},
|
|
||||||
"allowed_operations": {"$ref": "#/properties/allowed_operations"},
|
|
||||||
"allowed_operations_sha256": {"$ref": "#/properties/allowed_operations_sha256"},
|
|
||||||
"allowed_paths": {"$ref": "#/properties/allowed_paths"},
|
|
||||||
"allowed_paths_sha256": {"$ref": "#/properties/allowed_paths_sha256"},
|
|
||||||
"approval_plan_id": {"$ref": "#/properties/approval_plan_id"},
|
|
||||||
"approval_plan_path": {"$ref": "#/properties/approval_plan_path"},
|
|
||||||
"approved_by": {"$ref": "#/properties/approved_by"},
|
|
||||||
"auto_revoke": {"$ref": "#/properties/auto_revoke"},
|
|
||||||
"baseline_operations": {"$ref": "#/properties/baseline_operations"},
|
|
||||||
"candidate_commit_oid": {"$ref": "#/properties/candidate_commit_oid"},
|
|
||||||
"candidate_diff_sha256": {"$ref": "#/properties/candidate_diff_sha256"},
|
|
||||||
"candidate_files_sha256": {"$ref": "#/properties/candidate_files_sha256"},
|
|
||||||
"candidate_parent_oid": {"$ref": "#/properties/candidate_parent_oid"},
|
|
||||||
"candidate_tree_oid": {"$ref": "#/properties/candidate_tree_oid"},
|
|
||||||
"code_base_oid": {"$ref": "#/properties/code_base_oid"},
|
|
||||||
"code_base_ref": {"$ref": "#/properties/code_base_ref"},
|
|
||||||
"credential_scope": {"$ref": "#/properties/credential_scope"},
|
|
||||||
"credential_secret_ref": {"$ref": "#/properties/credential_secret_ref"},
|
|
||||||
"denied_operations": {"$ref": "#/properties/denied_operations"},
|
|
||||||
"deployment_operations": {"$ref": "#/properties/deployment_operations"},
|
|
||||||
"direct_main_push_allowed": {"$ref": "#/properties/direct_main_push_allowed"},
|
|
||||||
"expires_at": {"$ref": "#/properties/expires_at"},
|
|
||||||
"force_push_allowed": {"$ref": "#/properties/force_push_allowed"},
|
|
||||||
"governance_base_oid": {"$ref": "#/properties/governance_base_oid"},
|
|
||||||
"governance_base_ref": {"$ref": "#/properties/governance_base_ref"},
|
|
||||||
"maximum_branch_count": {"$ref": "#/properties/maximum_branch_count"},
|
|
||||||
"maximum_commit_count": {"$ref": "#/properties/maximum_commit_count"},
|
|
||||||
"maximum_lifetime_seconds": {"$ref": "#/properties/maximum_lifetime_seconds"},
|
|
||||||
"maximum_operation_count": {"$ref": "#/properties/maximum_operation_count"},
|
|
||||||
"maximum_pr_count": {"$ref": "#/properties/maximum_pr_count"},
|
|
||||||
"observed_preapproval_operations": {"$ref": "#/properties/observed_preapproval_operations"},
|
|
||||||
"production_runtime_anchor_change": {"$ref": "#/properties/production_runtime_anchor_change"},
|
|
||||||
"production_writes_enabled": {"$ref": "#/properties/production_writes_enabled"},
|
|
||||||
"provision_credential_projection": {"$ref": "#/properties/provision_credential_projection"},
|
|
||||||
"provision_publisher": {"$ref": "#/properties/provision_publisher"},
|
|
||||||
"provision_writer": {"$ref": "#/properties/provision_writer"},
|
|
||||||
"publisher_identity": {"$ref": "#/properties/publisher_identity"},
|
|
||||||
"publisher_scope": {"$ref": "#/properties/publisher_scope"},
|
|
||||||
"registry_operations": {"$ref": "#/properties/registry_operations"},
|
|
||||||
"repository_identity": {"$ref": "#/properties/repository_identity"},
|
|
||||||
"request_created_at": {"$ref": "#/properties/request_created_at"},
|
|
||||||
"request_expires_at": {"$ref": "#/properties/request_expires_at"},
|
|
||||||
"request_head_ref": {"$ref": "#/properties/request_head_ref"},
|
|
||||||
"request_id": {"$ref": "#/properties/request_id"},
|
|
||||||
"requester_identity": {"$ref": "#/properties/requester_identity"},
|
|
||||||
"required_human_approvals": {"$ref": "#/properties/required_human_approvals"},
|
|
||||||
"required_machine_contexts": {"$ref": "#/properties/required_machine_contexts"},
|
|
||||||
"schema_document_sha256": {"$ref": "#/properties/schema_document_sha256"},
|
|
||||||
"schema_id": {"$ref": "#/properties/schema_id"},
|
|
||||||
"schema_version": {"$ref": "#/properties/schema_version"},
|
|
||||||
"tag_operations": {"$ref": "#/properties/tag_operations"},
|
|
||||||
"tag_rewrite_allowed": {"$ref": "#/properties/tag_rewrite_allowed"},
|
|
||||||
"target_initial_state": {"$ref": "#/properties/target_initial_state"},
|
|
||||||
"target_ref": {"$ref": "#/properties/target_ref"},
|
|
||||||
"validator_policy_sha256": {"$ref": "#/properties/validator_policy_sha256"},
|
|
||||||
"writer_identity": {"$ref": "#/properties/writer_identity"},
|
|
||||||
"writer_scope": {"$ref": "#/properties/writer_scope"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oid": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
||||||
"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"identity": {"type": "string", "minLength": 3, "maxLength": 128, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]+$"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"$id": "AIOS_TASK_STATUS_MERGE_GRANT_V1",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"schema_id", "schema_version", "repository", "pr_number", "base_oid",
|
|
||||||
"target_commit", "approved_tree_oid", "manifest_path", "manifest_blob_oid", "manifest_sha256",
|
|
||||||
"payload_sha256", "status_identity", "merge_identity", "status_contexts",
|
|
||||||
"status_write_quota", "merge_quota", "merge_method", "branch_push_allowed",
|
|
||||||
"tag_write_allowed", "main_write_allowed", "registry_write_allowed",
|
|
||||||
"baseline_write_allowed", "production_write_allowed", "issued_at", "expires_at",
|
|
||||||
"audit_key_ref", "grant_signer_identity", "grant_signature_namespace",
|
|
||||||
"status_evidence_sha256", "supersedes_pr_number", "supersedes_head_oid",
|
|
||||||
"renewal_required", "timestamp_contract", "canonical_timezone",
|
|
||||||
"raw_timestamp_preserved", "naive_datetime_allowed"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"schema_id": {"const": "AIOS_TASK_STATUS_MERGE_GRANT"},
|
|
||||||
"schema_version": {"const": 1},
|
|
||||||
"repository": {"const": "aios/aios"},
|
|
||||||
"pr_number": {"type": "integer", "minimum": 1},
|
|
||||||
"base_oid": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
||||||
"target_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
||||||
"approved_tree_oid": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
||||||
"manifest_path": {"type": "string", "pattern": "^approvals/[A-Za-z0-9._/-]+\\.json$"},
|
|
||||||
"manifest_blob_oid": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
||||||
"manifest_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"payload_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"status_evidence_sha256": {
|
|
||||||
"type": "object", "additionalProperties": false,
|
|
||||||
"required": ["aios/branch-protection", "aios/evidence-closure", "aios/secret-scan", "aios/state-machine", "aios/validator"],
|
|
||||||
"properties": {
|
|
||||||
"aios/branch-protection": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"aios/evidence-closure": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"aios/secret-scan": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"aios/state-machine": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
|
||||||
"aios/validator": {"type": "string", "pattern": "^[0-9a-f]{64}$"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"supersedes_pr_number": {"const": 36},
|
|
||||||
"supersedes_head_oid": {"const": "a8644fc172bd8ebbba17a16e6bf586c93fd1e793"},
|
|
||||||
"renewal_required": {"const": true},
|
|
||||||
"timestamp_contract": {"const": "RFC3339_OFFSET_AWARE_V2"},
|
|
||||||
"canonical_timezone": {"const": "UTC"},
|
|
||||||
"raw_timestamp_preserved": {"const": true},
|
|
||||||
"naive_datetime_allowed": {"const": false},
|
|
||||||
"status_identity": {"enum": ["codex", "gpt"]},
|
|
||||||
"merge_identity": {"enum": ["codex", "gpt"]},
|
|
||||||
"status_contexts": {
|
|
||||||
"type": "array", "minItems": 5, "maxItems": 5, "uniqueItems": true,
|
|
||||||
"items": {"enum": ["aios/branch-protection", "aios/evidence-closure", "aios/secret-scan", "aios/state-machine", "aios/validator"]}
|
|
||||||
},
|
|
||||||
"status_write_quota": {"const": 5},
|
|
||||||
"merge_quota": {"const": 1},
|
|
||||||
"merge_method": {"const": "fast-forward-only"},
|
|
||||||
"branch_push_allowed": {"const": false},
|
|
||||||
"tag_write_allowed": {"const": false},
|
|
||||||
"main_write_allowed": {"const": false},
|
|
||||||
"registry_write_allowed": {"const": false},
|
|
||||||
"baseline_write_allowed": {"const": false},
|
|
||||||
"production_write_allowed": {"const": false},
|
|
||||||
"issued_at": {"type": "string", "format": "date-time"},
|
|
||||||
"expires_at": {"type": "string", "format": "date-time"},
|
|
||||||
"audit_key_ref": {"type": "string", "pattern": "^systemd-credential://[A-Za-z0-9_.-]+$"},
|
|
||||||
"grant_signer_identity": {"const": "aios-task-grant-signer"},
|
|
||||||
"grant_signature_namespace": {"const": "aios-task-status-merge-grant-v1"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.models import (
|
||||||
|
ResourceBinding,
|
||||||
|
RiskClass,
|
||||||
|
TaskPlan,
|
||||||
|
WorkflowStep,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.policy import ReleasePolicy
|
||||||
|
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||||
|
|
||||||
|
|
||||||
|
OID = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def build_plan():
|
||||||
|
return TaskPlan(
|
||||||
|
task_id="test-task",
|
||||||
|
intent="candidate validation",
|
||||||
|
binding=ResourceBinding(
|
||||||
|
repository="aios/aios",
|
||||||
|
branch="patch/test",
|
||||||
|
base_oid=OID,
|
||||||
|
allowed_paths=("services/test/",),
|
||||||
|
),
|
||||||
|
risk=RiskClass.LOW,
|
||||||
|
steps=(
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="validate",
|
||||||
|
worker_role="tester",
|
||||||
|
required_capabilities=("test.execute",),
|
||||||
|
allowed_paths=("services/test/",),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
evidence_requirements=("tests",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_policy_accepts_candidate_plan():
|
||||||
|
ReleasePolicy().validate_task(build_plan())
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_chain_detects_valid_closure():
|
||||||
|
chain = EvidenceChain()
|
||||||
|
chain.append("START", "test-task", {"ok": True, "secrets_included": False})
|
||||||
|
chain.append("DONE", "test-task", {"ok": True, "secrets_included": False})
|
||||||
|
assert chain.verify()
|
||||||
|
assert len(chain.closure_digest()) == 64
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.artifact import ArtifactBuilder, ArtifactFile, BuiltArtifact
|
||||||
|
from aios_autonomous_release_candidate.errors import EvidenceIncomplete, ReleaseBlocked
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_is_deterministic_and_reverified() -> None:
|
||||||
|
builder = ArtifactBuilder()
|
||||||
|
files = (
|
||||||
|
ArtifactFile("bin/run", b"#!/bin/sh\nexit 0\n", 0o755),
|
||||||
|
ArtifactFile("config/candidate.json", b'{"production":false}\n'),
|
||||||
|
)
|
||||||
|
first = builder.build("a" * 40, "b" * 40, files)
|
||||||
|
second = builder.build("a" * 40, "b" * 40, tuple(reversed(files)))
|
||||||
|
assert first.content == second.content
|
||||||
|
assert first.manifest.manifest_sha256 == second.manifest.manifest_sha256
|
||||||
|
assert first.manifest.secrets_included is False
|
||||||
|
builder.verify(first)
|
||||||
|
with pytest.raises(EvidenceIncomplete, match="ARTIFACT_DIGEST_MISMATCH"):
|
||||||
|
builder.verify(BuiltArtifact(first.content + b"tampered", first.manifest))
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_tree_rejects_symlink_and_hardlink(tmp_path: Path) -> None:
|
||||||
|
root = tmp_path / "source"
|
||||||
|
root.mkdir()
|
||||||
|
target = root / "file.txt"
|
||||||
|
target.write_text("safe", encoding="utf-8")
|
||||||
|
(root / "link.txt").symlink_to(target)
|
||||||
|
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_LINK_REJECTED"):
|
||||||
|
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_rejects_secret_markers() -> None:
|
||||||
|
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SECRET_MARKER"):
|
||||||
|
ArtifactBuilder().build(
|
||||||
|
"a" * 40,
|
||||||
|
"b" * 40,
|
||||||
|
(ArtifactFile("unsafe.env", b"password" + b"=" + b"x" * 16),),
|
||||||
|
)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_artifact_build_and_verify(tmp_path: Path, capsys) -> None:
|
||||||
|
source = tmp_path / "source"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "payload.txt").write_text("candidate payload\n", encoding="utf-8")
|
||||||
|
output = tmp_path / "output"
|
||||||
|
assert main(
|
||||||
|
[
|
||||||
|
"artifact",
|
||||||
|
"build",
|
||||||
|
"--source",
|
||||||
|
str(source),
|
||||||
|
"--source-oid",
|
||||||
|
"a" * 40,
|
||||||
|
"--tree-oid",
|
||||||
|
"b" * 40,
|
||||||
|
"--output-directory",
|
||||||
|
str(output),
|
||||||
|
]
|
||||||
|
) == 0
|
||||||
|
built = json.loads(capsys.readouterr().out)
|
||||||
|
assert built["secrets_included"] is False
|
||||||
|
assert main(
|
||||||
|
[
|
||||||
|
"artifact",
|
||||||
|
"verify",
|
||||||
|
"--artifact",
|
||||||
|
built["artifact"],
|
||||||
|
"--manifest",
|
||||||
|
built["manifest"],
|
||||||
|
]
|
||||||
|
) == 0
|
||||||
|
assert json.loads(capsys.readouterr().out)["verified"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_release_verify_stops_at_owner(tmp_path: Path, capsys) -> None:
|
||||||
|
payload = {
|
||||||
|
"task_id": "cli-release-task",
|
||||||
|
"repository": "aios/aios",
|
||||||
|
"branch": "integration/cli-release",
|
||||||
|
"version": "SHADOW-0.1.0",
|
||||||
|
"target_oid": "a" * 40,
|
||||||
|
"tree_oid": "b" * 40,
|
||||||
|
"artifact_sha256": "c" * 64,
|
||||||
|
"manifest_sha256": "d" * 64,
|
||||||
|
"package_closure_sha256": "e" * 64,
|
||||||
|
"evidence_closure_oid": "f" * 40,
|
||||||
|
"approval_plan_id": "external-owner-release",
|
||||||
|
"rollback_approval_plan_id": "external-owner-rollback",
|
||||||
|
"rollback_artifact_sha256": "1" * 64,
|
||||||
|
"expected_tag_oid": "2" * 40,
|
||||||
|
"expected_target_oid": "a" * 40,
|
||||||
|
"production_config_sha256": "3" * 64,
|
||||||
|
}
|
||||||
|
path = tmp_path / "release-plan.json"
|
||||||
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
assert main(["release", "verify", "--payload", str(path)]) == 0
|
||||||
|
result = json.loads(capsys.readouterr().out)
|
||||||
|
assert result["release_state"] == "WAITING_OWNER"
|
||||||
|
assert result["production_release"] == "BLOCKED_EXTERNAL_OWNER_SIGNATURE"
|
||||||
|
assert result["production_side_effects"] is False
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.models import (
|
||||||
|
ResourceBinding,
|
||||||
|
RiskClass,
|
||||||
|
TaskPlan,
|
||||||
|
WorkflowStep,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.policy import ReleasePolicy
|
||||||
|
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||||
|
|
||||||
|
|
||||||
|
OID = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def build_plan():
|
||||||
|
return TaskPlan(
|
||||||
|
task_id="test-task",
|
||||||
|
intent="candidate validation",
|
||||||
|
binding=ResourceBinding(
|
||||||
|
repository="aios/aios",
|
||||||
|
branch="patch/test",
|
||||||
|
base_oid=OID,
|
||||||
|
allowed_paths=("services/test/",),
|
||||||
|
),
|
||||||
|
risk=RiskClass.LOW,
|
||||||
|
steps=(
|
||||||
|
WorkflowStep(
|
||||||
|
step_id="validate",
|
||||||
|
worker_role="tester",
|
||||||
|
required_capabilities=("test.execute",),
|
||||||
|
allowed_paths=("services/test/",),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
evidence_requirements=("tests",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_policy_accepts_candidate_plan():
|
||||||
|
ReleasePolicy().validate_task(build_plan())
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_chain_detects_valid_closure():
|
||||||
|
chain = EvidenceChain()
|
||||||
|
chain.append("START", "test-task", {"ok": True, "secrets_included": False})
|
||||||
|
chain.append("DONE", "test-task", {"ok": True, "secrets_included": False})
|
||||||
|
assert chain.verify()
|
||||||
|
assert len(chain.closure_digest()) == 64
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.credential import CredentialBroker
|
||||||
|
from aios_autonomous_release_candidate.errors import CredentialUnavailable, EvidenceIncomplete
|
||||||
|
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||||
|
from aios_autonomous_release_candidate.models import CredentialRequest, ResourceBinding
|
||||||
|
from aios_autonomous_release_candidate.store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
class Provider:
|
||||||
|
def __init__(self, reference: str = "systemd-credential:openclaw-gateway-candidate") -> None:
|
||||||
|
self.reference = reference
|
||||||
|
self.active: set[str] = set()
|
||||||
|
self.revoked: list[str] = []
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
def issue(self, request, expires_at):
|
||||||
|
self.count += 1
|
||||||
|
credential_id = f"credential-{self.count}"
|
||||||
|
self.active.add(credential_id)
|
||||||
|
return credential_id, self.reference
|
||||||
|
|
||||||
|
def revoke(self, credential_id, reference):
|
||||||
|
self.active.discard(credential_id)
|
||||||
|
self.revoked.append(credential_id)
|
||||||
|
|
||||||
|
def is_usable(self, credential_id, reference):
|
||||||
|
return credential_id in self.active
|
||||||
|
|
||||||
|
|
||||||
|
def request(request_id: str = "request-1") -> CredentialRequest:
|
||||||
|
return CredentialRequest(
|
||||||
|
request_id=request_id,
|
||||||
|
task_id="task-credential",
|
||||||
|
session_id="session-1",
|
||||||
|
worker_id="worker-1",
|
||||||
|
audience="openclaw-candidate",
|
||||||
|
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||||
|
binding=ResourceBinding("aios/aios", "patch/credential-test", "a" * 40, ("services/test",)),
|
||||||
|
operations=("native_session.spawn",),
|
||||||
|
ttl_seconds=300,
|
||||||
|
max_uses=1,
|
||||||
|
provider="openclaw",
|
||||||
|
runtime_user="aiman",
|
||||||
|
approval_plan_id="owner-stage-b-native-sessions-20260726",
|
||||||
|
not_before=NOW,
|
||||||
|
expires_at=NOW + timedelta(minutes=5),
|
||||||
|
nonce="nonce-credential-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_credential_is_scope_bound_revocable_and_idempotent(tmp_path: Path) -> None:
|
||||||
|
provider = Provider()
|
||||||
|
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
|
||||||
|
projection = broker.issue(request(), NOW)
|
||||||
|
broker.project(projection)
|
||||||
|
broker.authorize(
|
||||||
|
projection,
|
||||||
|
task_id="task-credential",
|
||||||
|
session_id="session-1",
|
||||||
|
worker_id="worker-1",
|
||||||
|
audience="openclaw-candidate",
|
||||||
|
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||||
|
operation="native_session.spawn",
|
||||||
|
path="services/test/file.py",
|
||||||
|
now=NOW,
|
||||||
|
)
|
||||||
|
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_SCOPE_MISMATCH"):
|
||||||
|
broker.authorize(
|
||||||
|
projection,
|
||||||
|
task_id="other-task",
|
||||||
|
session_id="session-1",
|
||||||
|
worker_id="worker-1",
|
||||||
|
audience="openclaw-candidate",
|
||||||
|
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||||
|
operation="native_session.spawn",
|
||||||
|
path="services/test/file.py",
|
||||||
|
now=NOW,
|
||||||
|
)
|
||||||
|
first = broker.revoke(projection.credential_id, projection.reference, NOW)
|
||||||
|
second = broker.revoke(projection.credential_id, projection.reference, NOW)
|
||||||
|
assert first == second
|
||||||
|
broker.verify_revoked(projection.credential_id, projection.reference)
|
||||||
|
|
||||||
|
|
||||||
|
def test_persistence_failure_compensates_upstream_issuance(tmp_path: Path) -> None:
|
||||||
|
provider = Provider()
|
||||||
|
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
|
||||||
|
broker.issue(request(), NOW)
|
||||||
|
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_PERSISTENCE_FAILED"):
|
||||||
|
broker.issue(request("request-2"), NOW)
|
||||||
|
assert "credential-2" in provider.revoked
|
||||||
|
|
||||||
|
|
||||||
|
def test_plaintext_or_untrusted_projection_is_rejected(tmp_path: Path) -> None:
|
||||||
|
provider = Provider("/run/credentials/candidate/raw-value")
|
||||||
|
broker = CredentialBroker(StateStore(tmp_path / "state.sqlite3"), provider)
|
||||||
|
with pytest.raises(CredentialUnavailable, match="CREDENTIAL_REFERENCE_REJECTED"):
|
||||||
|
broker.issue(request(), NOW)
|
||||||
|
assert provider.revoked == ["credential-1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_chain_survives_restart_and_detects_tampering(tmp_path: Path) -> None:
|
||||||
|
store = StateStore(tmp_path / "state.sqlite3")
|
||||||
|
chain = EvidenceChain(store, "task-evidence")
|
||||||
|
chain.append("TASK_STARTED", "task-evidence", {"secrets_included": False})
|
||||||
|
chain.append(
|
||||||
|
"TESTS_PASSED",
|
||||||
|
"task-evidence",
|
||||||
|
{"passed": 42, "skipped": 0, "secrets_included": False},
|
||||||
|
)
|
||||||
|
reopened = EvidenceChain(store, "task-evidence")
|
||||||
|
assert reopened.verify()
|
||||||
|
assert len(reopened.close(("TASK_STARTED", "TESTS_PASSED"))) == 64
|
||||||
|
with pytest.raises(EvidenceIncomplete, match="EVIDENCE_REQUIREMENT_MISSING"):
|
||||||
|
reopened.close(("NATIVE_SESSION_VERIFIED",))
|
||||||
|
|
||||||
|
with store.transaction() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE events SET payload_json=? WHERE task_id=? AND sequence=1",
|
||||||
|
('{"secrets_included":true}', "task-evidence"),
|
||||||
|
)
|
||||||
|
assert EvidenceChain(store, "task-evidence").verify() is False
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.approval import (
|
||||||
|
ApprovalVerifier,
|
||||||
|
StaticRevocationSource,
|
||||||
|
UnavailableSignatureVerifier,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.errors import ApprovalRequired
|
||||||
|
from aios_autonomous_release_candidate.gitea_canary import (
|
||||||
|
EphemeralKeyProjection,
|
||||||
|
GiteaDeployKeyCanary,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.models import (
|
||||||
|
ApprovalEnvelope,
|
||||||
|
ResourceBinding,
|
||||||
|
RiskClass,
|
||||||
|
TaskPlan,
|
||||||
|
WorkflowStep,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.store import StateStore
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
|
||||||
|
OID = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def task_plan() -> TaskPlan:
|
||||||
|
return TaskPlan(
|
||||||
|
"external-gate-task",
|
||||||
|
"external gate verification",
|
||||||
|
ResourceBinding("aios/aios", "patch/external-gate", OID, ("services/test",)),
|
||||||
|
RiskClass.HIGH,
|
||||||
|
(WorkflowStep("canary", "tester", ("native_session.spawn",)),),
|
||||||
|
("external-gates",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_owner_verifier_fails_with_stable_blocker(tmp_path) -> None:
|
||||||
|
plan = task_plan()
|
||||||
|
envelope = ApprovalEnvelope(
|
||||||
|
approval_plan_id="owner-external-gate",
|
||||||
|
task_id=plan.task_id,
|
||||||
|
task_plan_sha256=plan.digest(),
|
||||||
|
binding=plan.binding,
|
||||||
|
capabilities=("native_session.spawn",),
|
||||||
|
risk=RiskClass.HIGH,
|
||||||
|
not_before=NOW - timedelta(minutes=1),
|
||||||
|
expires_at=NOW + timedelta(minutes=5),
|
||||||
|
nonce="nonce-owner-external",
|
||||||
|
signing_key_id="owner-kms-1",
|
||||||
|
signature="opaque-signature",
|
||||||
|
)
|
||||||
|
verifier = ApprovalVerifier(
|
||||||
|
StateStore(tmp_path / "state.sqlite3"),
|
||||||
|
UnavailableSignatureVerifier(),
|
||||||
|
StaticRevocationSource(),
|
||||||
|
)
|
||||||
|
with pytest.raises(
|
||||||
|
ApprovalRequired, match="BLOCKED_OWNER_APPROVAL_VERIFIER_UNAVAILABLE"
|
||||||
|
):
|
||||||
|
verifier.verify(plan, envelope, now=NOW)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyProjection:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.present = False
|
||||||
|
|
||||||
|
def create(self, task_id, branch):
|
||||||
|
self.present = True
|
||||||
|
return EphemeralKeyProjection(
|
||||||
|
public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICandidateCanaryOnly",
|
||||||
|
private_key_reference="systemd-credential:gitea-deploy-key-canary",
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, reference):
|
||||||
|
self.present = False
|
||||||
|
|
||||||
|
def is_present(self, reference):
|
||||||
|
return self.present
|
||||||
|
|
||||||
|
|
||||||
|
class Gitea:
|
||||||
|
simulation = True
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.keys = {}
|
||||||
|
|
||||||
|
def list_deploy_keys(self, repository, credential_reference):
|
||||||
|
return list(self.keys.values())
|
||||||
|
|
||||||
|
def create_deploy_key(self, repository, title, public_key, credential_reference):
|
||||||
|
item = {"id": 1, "title": title, "key": public_key, "read_only": False}
|
||||||
|
self.keys[1] = item
|
||||||
|
return item
|
||||||
|
|
||||||
|
def delete_deploy_key(self, repository, key_id, credential_reference):
|
||||||
|
self.keys.pop(key_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHProbe:
|
||||||
|
def __init__(self, gitea, projection) -> None:
|
||||||
|
self.gitea = gitea
|
||||||
|
self.projection = projection
|
||||||
|
|
||||||
|
def authenticate(self, repository, branch, private_key_reference):
|
||||||
|
return bool(self.gitea.keys) and self.projection.present
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_canary_always_revokes_key_and_projection() -> None:
|
||||||
|
projection = KeyProjection()
|
||||||
|
gitea = Gitea()
|
||||||
|
result = GiteaDeployKeyCanary(gitea, projection, SSHProbe(gitea, projection)).run(
|
||||||
|
task_id="external-gate-task",
|
||||||
|
branch="patch/external-gate",
|
||||||
|
credential_reference="systemd-credential:gitea-admin-api-candidate",
|
||||||
|
)
|
||||||
|
assert result.key_created is True
|
||||||
|
assert result.key_deleted is True
|
||||||
|
assert result.post_revoke_auth_rejected is True
|
||||||
|
assert result.credential_projection_deleted is True
|
||||||
|
assert result.gitea_admin_api_lifecycle_canary is False # simulation is not real evidence
|
||||||
|
assert gitea.keys == {}
|
||||||
|
assert projection.present is False
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.service import CandidateApplication, CandidateHTTPServer
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def running_server(tmp_path: Path):
|
||||||
|
application = CandidateApplication(tmp_path / "state.sqlite3")
|
||||||
|
server = CandidateHTTPServer(("127.0.0.1", 0), application)
|
||||||
|
thread = Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
try:
|
||||||
|
yield application, f"http://127.0.0.1:{server.server_port}"
|
||||||
|
finally:
|
||||||
|
server.shutdown()
|
||||||
|
server.server_close()
|
||||||
|
thread.join(timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
def get_json(url: str):
|
||||||
|
with urllib.request.urlopen(url, timeout=5) as response:
|
||||||
|
return response.status, json.load(response)
|
||||||
|
|
||||||
|
|
||||||
|
def post_json(url: str, payload: dict):
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=5) as response:
|
||||||
|
return response.status, json.load(response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_and_readiness_are_loopback_safe(tmp_path: Path) -> None:
|
||||||
|
with running_server(tmp_path) as (_, base):
|
||||||
|
status, health = get_json(f"{base}/health")
|
||||||
|
assert status == 200
|
||||||
|
assert health["production_side_effects"] is False
|
||||||
|
status, ready = get_json(f"{base}/ready")
|
||||||
|
assert status == 200
|
||||||
|
assert ready == {"status": "ready", "secrets_included": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_content_type_is_required(tmp_path: Path) -> None:
|
||||||
|
with running_server(tmp_path) as (_, base):
|
||||||
|
request = urllib.request.Request(f"{base}/v1/tasks", data=b"{}", method="POST")
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(request, timeout=5)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
assert error.code == 415
|
||||||
|
assert json.load(error)["error"] == "content_type_required"
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_exception_text_is_not_disclosed(tmp_path: Path) -> None:
|
||||||
|
with running_server(tmp_path) as (application, base):
|
||||||
|
def fail(_payload):
|
||||||
|
raise RuntimeError("sensitive internal diagnostic")
|
||||||
|
|
||||||
|
application.api.create_task = fail
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{base}/v1/tasks",
|
||||||
|
data=b"{}",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(request, timeout=5)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
assert error.code == 500
|
||||||
|
body = error.read().decode("utf-8")
|
||||||
|
assert "candidate_request_failed" in body
|
||||||
|
assert "sensitive internal diagnostic" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_lifecycle_evidence_and_release_gate_endpoints(tmp_path: Path) -> None:
|
||||||
|
task_id = "api-lifecycle-task"
|
||||||
|
task = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"intent": "exercise candidate API",
|
||||||
|
"binding": {
|
||||||
|
"repository": "aios/aios",
|
||||||
|
"branch": "patch/api-lifecycle",
|
||||||
|
"base_oid": "a" * 40,
|
||||||
|
"allowed_paths": ["services/test"],
|
||||||
|
},
|
||||||
|
"risk": "medium",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step_id": "test-step",
|
||||||
|
"worker_role": "tester",
|
||||||
|
"required_capabilities": ["test.execute"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence_requirements": ["STEP_RESULT"],
|
||||||
|
"production_side_effects_allowed": False,
|
||||||
|
}
|
||||||
|
with running_server(tmp_path) as (_, base):
|
||||||
|
status, created = post_json(f"{base}/v1/tasks", task)
|
||||||
|
assert status == 201
|
||||||
|
assert created["production_side_effects"] is False
|
||||||
|
status, steps = get_json(f"{base}/v1/tasks/{task_id}/steps")
|
||||||
|
assert status == 200
|
||||||
|
assert steps["steps"]["test-step"]["state"] == "READY"
|
||||||
|
binding = {
|
||||||
|
"step_id": "test-step",
|
||||||
|
"worker_id": "worker-1",
|
||||||
|
"session_id": "session-1",
|
||||||
|
}
|
||||||
|
_, claimed = post_json(f"{base}/v1/tasks/{task_id}/claim", binding)
|
||||||
|
assert claimed["state"] == "RUNNING"
|
||||||
|
_, heartbeat = post_json(f"{base}/v1/tasks/{task_id}/heartbeat", binding)
|
||||||
|
assert heartbeat["state"] == "RUNNING"
|
||||||
|
_, result = post_json(
|
||||||
|
f"{base}/v1/tasks/{task_id}/result",
|
||||||
|
{"step_id": "test-step", "result_digest": hashlib.sha256(b"ok").hexdigest()},
|
||||||
|
)
|
||||||
|
assert result["state"] == "COMPLETED"
|
||||||
|
_, evidence = get_json(f"{base}/v1/tasks/{task_id}/evidence")
|
||||||
|
assert evidence["verified"] is True
|
||||||
|
assert any(item["event_type"] == "STEP_RESULT" for item in evidence["events"])
|
||||||
|
_, closure = post_json(f"{base}/v1/tasks/{task_id}/evidence/close", {})
|
||||||
|
assert len(closure["evidence_closure"]) == 64
|
||||||
|
|
||||||
|
release_plan = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"repository": "aios/aios",
|
||||||
|
"branch": "integration/api-lifecycle",
|
||||||
|
"version": "SHADOW-0.1.0",
|
||||||
|
"target_oid": "b" * 40,
|
||||||
|
"tree_oid": "c" * 40,
|
||||||
|
"artifact_sha256": "d" * 64,
|
||||||
|
"manifest_sha256": "e" * 64,
|
||||||
|
"package_closure_sha256": "f" * 64,
|
||||||
|
"evidence_closure_oid": "1" * 40,
|
||||||
|
"approval_plan_id": "external-owner-release",
|
||||||
|
"rollback_approval_plan_id": "external-owner-rollback",
|
||||||
|
"rollback_artifact_sha256": "2" * 64,
|
||||||
|
"expected_tag_oid": "3" * 40,
|
||||||
|
"expected_target_oid": "b" * 40,
|
||||||
|
"production_config_sha256": "4" * 64,
|
||||||
|
}
|
||||||
|
_, release = post_json(f"{base}/v1/release/plan", release_plan)
|
||||||
|
assert release["release_state"] == "BUILT"
|
||||||
|
_, verified = post_json(
|
||||||
|
f"{base}/v1/release/verify", {"artifact_ok": True, "evidence_ok": True}
|
||||||
|
)
|
||||||
|
assert verified["release_state"] == "WAITING_OWNER"
|
||||||
|
assert verified["production_release"] == "BLOCKED_EXTERNAL_OWNER_SIGNATURE"
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.errors import StateConflict
|
||||||
|
from aios_autonomous_release_candidate.native_adapters import (
|
||||||
|
CodexNativeAdapter,
|
||||||
|
OpenClawNativeAdapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Provider:
|
||||||
|
simulation = True
|
||||||
|
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.state = "ACTIVE"
|
||||||
|
self.parent = "parent-1"
|
||||||
|
|
||||||
|
def spawn(self, request: Mapping[str, Any], credential_reference: str):
|
||||||
|
return {
|
||||||
|
"provider_session_id": f"{self.name}-native-1",
|
||||||
|
"task_id": request["task_id"],
|
||||||
|
"parent_session_id": request["parent_session_id"],
|
||||||
|
"candidate_only": True,
|
||||||
|
"production_session_access": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def status(self, provider_session_id: str, credential_reference: str):
|
||||||
|
return {"provider_session_id": provider_session_id, "state": self.state}
|
||||||
|
|
||||||
|
def list_tree(self, parent_session_id: str, credential_reference: str):
|
||||||
|
return [{"provider_session_id": f"{self.name}-native-1", "parent_session_id": parent_session_id}]
|
||||||
|
|
||||||
|
def send_text(self, provider_session_id, payload, credential_reference):
|
||||||
|
return {
|
||||||
|
"content_type": "text/plain",
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"image_only": False,
|
||||||
|
"task_id": payload["task_id"],
|
||||||
|
"message_id": "message-1",
|
||||||
|
"receipt": "receipt-1",
|
||||||
|
"recipient_binding": payload["recipient_binding"],
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def interrupt(self, provider_session_id: str, credential_reference: str):
|
||||||
|
self.state = "INTERRUPTED"
|
||||||
|
|
||||||
|
def close(self, provider_session_id: str, credential_reference: str):
|
||||||
|
self.state = "CLOSED"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("adapter_type", "provider_name", "reference", "flag"),
|
||||||
|
(
|
||||||
|
(
|
||||||
|
OpenClawNativeAdapter,
|
||||||
|
"openclaw",
|
||||||
|
"systemd-credential:openclaw-gateway-candidate",
|
||||||
|
"openclaw_native_child_session_verified",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
CodexNativeAdapter,
|
||||||
|
"codex",
|
||||||
|
"systemd-credential:codex-app-server-candidate",
|
||||||
|
"codex_child_session_verified",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_native_adapter_preserves_identity_and_text_contract(
|
||||||
|
adapter_type, provider_name, reference, flag
|
||||||
|
) -> None:
|
||||||
|
adapter = adapter_type(Provider(provider_name), reference)
|
||||||
|
session = adapter.spawn("task-native", "parent-1", {"recipient_id": "weixin-owner"})
|
||||||
|
assert session.broker_session_id != session.provider_session_id
|
||||||
|
verification = adapter.verify(session)
|
||||||
|
assert verification.broker_session_verified is True
|
||||||
|
assert getattr(verification, flag) is False # simulated providers are never real canary evidence
|
||||||
|
result = adapter.send_text(session, "hello")
|
||||||
|
assert result.content_type == "text/plain"
|
||||||
|
assert result.secrets_included is False
|
||||||
|
adapter.interrupt(session)
|
||||||
|
assert adapter.status(session) == "INTERRUPTED"
|
||||||
|
adapter.close(session)
|
||||||
|
assert adapter.status(session) == "CLOSED"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipient_binding_must_be_single_and_trusted() -> None:
|
||||||
|
adapter = OpenClawNativeAdapter(
|
||||||
|
Provider("openclaw"), "systemd-credential:openclaw-gateway-candidate"
|
||||||
|
)
|
||||||
|
with pytest.raises(StateConflict, match="RECIPIENT_BINDING_REQUIRED"):
|
||||||
|
adapter.spawn("task-native", "parent-1", {"recipients": ["one", "two"]})
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
from dataclasses import replace
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.approval import ApprovalVerificationResult
|
||||||
|
from aios_autonomous_release_candidate.errors import ReleaseBlocked
|
||||||
|
from aios_autonomous_release_candidate.models import ReleasePlan
|
||||||
|
from aios_autonomous_release_candidate.release import ReleaseController, ReleaseState
|
||||||
|
from aios_autonomous_release_candidate.release_gate import ReleaseEvidence, ReleaseGate
|
||||||
|
from aios_autonomous_release_candidate.tag import TagReference, TagVerifier
|
||||||
|
|
||||||
|
|
||||||
|
OID = "a" * 40
|
||||||
|
TAG_OID = "b" * 40
|
||||||
|
DIGEST = "c" * 64
|
||||||
|
|
||||||
|
|
||||||
|
def plan() -> ReleasePlan:
|
||||||
|
return ReleasePlan(
|
||||||
|
task_id="release-task",
|
||||||
|
repository="aios/aios",
|
||||||
|
branch="integration/autonomous-release",
|
||||||
|
version="V2R1C4",
|
||||||
|
target_oid=OID,
|
||||||
|
tree_oid="d" * 40,
|
||||||
|
artifact_sha256=DIGEST,
|
||||||
|
manifest_sha256="e" * 64,
|
||||||
|
package_closure_sha256="3" * 64,
|
||||||
|
evidence_closure_oid="f" * 40,
|
||||||
|
approval_plan_id="owner-release-v2r1c4",
|
||||||
|
rollback_approval_plan_id="owner-rollback-v2r1c4",
|
||||||
|
rollback_artifact_sha256="1" * 64,
|
||||||
|
expected_tag_oid=TAG_OID,
|
||||||
|
expected_target_oid=OID,
|
||||||
|
production_config_sha256="2" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def approval(plan_id: str) -> ApprovalVerificationResult:
|
||||||
|
return ApprovalVerificationResult(
|
||||||
|
approval_plan_id=plan_id,
|
||||||
|
task_id="release-task",
|
||||||
|
signing_key_id="owner-kms-1",
|
||||||
|
verified_at=datetime(2026, 7, 26, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deploy_and_rollback_require_distinct_verified_approvals() -> None:
|
||||||
|
controller = ReleaseController()
|
||||||
|
controller.attach_plan(plan())
|
||||||
|
controller.mark_built()
|
||||||
|
controller.verify(artifact_ok=True, evidence_ok=True)
|
||||||
|
controller.request_owner_approval()
|
||||||
|
with pytest.raises(ReleaseBlocked, match="OWNER_APPROVAL_PLAN_MISMATCH"):
|
||||||
|
controller.approve(approval("wrong-plan"))
|
||||||
|
controller.approve(approval("owner-release-v2r1c4"))
|
||||||
|
controller.start_deployment()
|
||||||
|
controller.mark_deployed()
|
||||||
|
controller.request_rollback("candidate acceptance regression")
|
||||||
|
with pytest.raises(ReleaseBlocked, match="ROLLBACK_APPROVAL_PLAN_MISMATCH"):
|
||||||
|
controller.approve_rollback(approval("owner-release-v2r1c4"))
|
||||||
|
controller.approve_rollback(approval("owner-rollback-v2r1c4"))
|
||||||
|
controller.start_rollback()
|
||||||
|
assert controller.mark_rolled_back().state == ReleaseState.ROLLED_BACK
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotated_tag_uses_object_and_peeled_oids_separately() -> None:
|
||||||
|
tag = TagReference("refs/tags/V2R1C4", "tag", TAG_OID, OID)
|
||||||
|
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid=OID)
|
||||||
|
with pytest.raises(ReleaseBlocked, match="TAG_PEELED_TARGET_MISMATCH"):
|
||||||
|
TagVerifier().verify(tag, expected_tag_oid=TAG_OID, expected_target_oid="3" * 40)
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_gate_requires_four_source_tag_and_artifact_closure() -> None:
|
||||||
|
evidence = ReleaseEvidence(
|
||||||
|
expected_version="V2R1C4",
|
||||||
|
registry_version="V2R1C4",
|
||||||
|
baseline_version="V2R1C4",
|
||||||
|
local_tag_oid=TAG_OID,
|
||||||
|
remote_tag_oid=TAG_OID,
|
||||||
|
gitea_tag_oid=TAG_OID,
|
||||||
|
expected_tag_oid=TAG_OID,
|
||||||
|
local_peeled_target_oid=OID,
|
||||||
|
remote_peeled_target_oid=OID,
|
||||||
|
gitea_peeled_target_oid=OID,
|
||||||
|
expected_target_oid=OID,
|
||||||
|
artifact_sha256=DIGEST,
|
||||||
|
manifest_sha256="e" * 64,
|
||||||
|
evidence_sha256="4" * 64,
|
||||||
|
rollback_artifact_sha256="1" * 64,
|
||||||
|
)
|
||||||
|
ReleaseGate().verify(evidence)
|
||||||
|
with pytest.raises(ReleaseBlocked, match="TAG_GITEA_MISMATCH"):
|
||||||
|
ReleaseGate().verify(replace(evidence, gitea_tag_oid="5" * 40))
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.errors import CredentialUnavailable
|
||||||
|
from aios_autonomous_release_candidate.evidence import EvidenceChain
|
||||||
|
from aios_autonomous_release_candidate.execution_manager import ExecutionManager
|
||||||
|
from aios_autonomous_release_candidate.models import CredentialRequest, ResourceBinding
|
||||||
|
from aios_autonomous_release_candidate.native_adapters import OpenClawNativeAdapter
|
||||||
|
from aios_autonomous_release_candidate.secret_manager import SecretManagerV2
|
||||||
|
from aios_autonomous_release_candidate.store import StateStore
|
||||||
|
from test_native_adapter_contracts import Provider
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 7, 26, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
class Transport:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def exchange(self, operation, payload):
|
||||||
|
self.calls.append((operation, payload))
|
||||||
|
base = {
|
||||||
|
"task_id": payload["task_id"],
|
||||||
|
"credential_id": payload.get("credential_id", "credential-openclaw-1"),
|
||||||
|
"plaintext_secret_returned": False,
|
||||||
|
}
|
||||||
|
if operation == "issue":
|
||||||
|
return {
|
||||||
|
**base,
|
||||||
|
"credential_reference": "systemd-credential:openclaw-gateway-candidate",
|
||||||
|
"state": "ISSUED",
|
||||||
|
"expires_at": payload["expires_at"],
|
||||||
|
}
|
||||||
|
if operation == "audit":
|
||||||
|
return {**base, "events": [{"operation": "issue", "secrets_included": False}]}
|
||||||
|
if operation in {"project", "status"}:
|
||||||
|
return {**base, "state": "PROJECTED"}
|
||||||
|
if operation == "verify_revocation":
|
||||||
|
return {**base, "state": "REVOKED", "post_revocation_auth": "REJECTED"}
|
||||||
|
return {**base, "state": "REVOKED"}
|
||||||
|
|
||||||
|
|
||||||
|
def credential_request() -> CredentialRequest:
|
||||||
|
return CredentialRequest(
|
||||||
|
request_id="request-1",
|
||||||
|
task_id="task-execution",
|
||||||
|
session_id="session-1",
|
||||||
|
worker_id="worker-1",
|
||||||
|
audience="openclaw-candidate",
|
||||||
|
consumer_service="aios-autonomous-release-worker-candidate.service",
|
||||||
|
binding=ResourceBinding("aios/aios", "patch/execution", "a" * 40, ("services/test",)),
|
||||||
|
operations=("native_session.spawn",),
|
||||||
|
provider="openclaw",
|
||||||
|
runtime_user="aiman",
|
||||||
|
approval_plan_id="owner-stage-b-native-sessions-20260726",
|
||||||
|
not_before=NOW,
|
||||||
|
expires_at=NOW + timedelta(minutes=5),
|
||||||
|
nonce="nonce-execution-1",
|
||||||
|
ttl_seconds=300,
|
||||||
|
max_uses=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_secret_manager_uses_reference_only_protocol() -> None:
|
||||||
|
transport = Transport()
|
||||||
|
manager = SecretManagerV2(transport)
|
||||||
|
receipt = manager.issue(credential_request(), NOW)
|
||||||
|
assert receipt.reference == "systemd-credential:openclaw-gateway-candidate"
|
||||||
|
assert receipt.plaintext_secret_returned is False
|
||||||
|
manager.project(receipt)
|
||||||
|
manager.status(receipt)
|
||||||
|
manager.revoke(receipt)
|
||||||
|
manager.verify_revocation(receipt)
|
||||||
|
events = manager.audit(receipt)
|
||||||
|
assert events[0]["secrets_included"] is False
|
||||||
|
assert [operation for operation, _ in transport.calls] == [
|
||||||
|
"issue",
|
||||||
|
"project",
|
||||||
|
"status",
|
||||||
|
"revoke",
|
||||||
|
"verify_revocation",
|
||||||
|
"audit",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_secret_manager_rejects_plaintext_fields() -> None:
|
||||||
|
transport = Transport()
|
||||||
|
|
||||||
|
def unsafe(operation, payload):
|
||||||
|
return {"task_id": payload["task_id"], "credential_id": "credential-1", "api_key": "redacted"}
|
||||||
|
|
||||||
|
transport.exchange = unsafe
|
||||||
|
with pytest.raises(CredentialUnavailable, match="SECRET_MANAGER_PLAINTEXT_REJECTED"):
|
||||||
|
SecretManagerV2(transport).issue(credential_request(), NOW)
|
||||||
|
|
||||||
|
|
||||||
|
def test_execution_manager_records_simulation_without_native_claim(tmp_path: Path) -> None:
|
||||||
|
evidence = EvidenceChain(StateStore(tmp_path / "state.sqlite3"), "task-execution")
|
||||||
|
adapter = OpenClawNativeAdapter(
|
||||||
|
Provider("openclaw"), "systemd-credential:openclaw-gateway-candidate"
|
||||||
|
)
|
||||||
|
result = ExecutionManager(evidence).run_native_canary(
|
||||||
|
adapter,
|
||||||
|
task_id="task-execution",
|
||||||
|
parent_session_id="parent-1",
|
||||||
|
recipient_binding={"recipient_id": "weixin-owner"},
|
||||||
|
text="candidate canary",
|
||||||
|
)
|
||||||
|
assert result.broker_session_verified is True
|
||||||
|
assert result.openclaw_native_child_session_verified is False
|
||||||
|
assert result.simulation is True
|
||||||
|
assert result.session_closed is True
|
||||||
|
assert evidence.verify()
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from pathlib import Path
|
||||||
|
import stat
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aios_autonomous_release_candidate.errors import StateConflict
|
||||||
|
from aios_autonomous_release_candidate.models import (
|
||||||
|
ResourceBinding,
|
||||||
|
RiskClass,
|
||||||
|
StepState,
|
||||||
|
TaskPlan,
|
||||||
|
WorkflowStep,
|
||||||
|
)
|
||||||
|
from aios_autonomous_release_candidate.store import StateStore
|
||||||
|
from aios_autonomous_release_candidate.workflow import WorkflowEngine
|
||||||
|
|
||||||
|
|
||||||
|
OID = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def plan() -> TaskPlan:
|
||||||
|
return TaskPlan(
|
||||||
|
task_id="workflow-test",
|
||||||
|
intent="exercise durable workflow",
|
||||||
|
binding=ResourceBinding("aios/aios", "patch/workflow-test", OID, ("services/test",)),
|
||||||
|
risk=RiskClass.LOW,
|
||||||
|
steps=(
|
||||||
|
WorkflowStep("build", "builder", ("file.write",), max_attempts=2, timeout_seconds=30),
|
||||||
|
WorkflowStep("test", "tester", ("test.execute",), depends_on=("build",), max_attempts=2),
|
||||||
|
),
|
||||||
|
evidence_requirements=("tests",),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_is_private_and_rejects_schema_drift(tmp_path: Path) -> None:
|
||||||
|
state = tmp_path / "private" / "state.sqlite3"
|
||||||
|
StateStore(state)
|
||||||
|
assert stat.S_IMODE(state.stat().st_mode) == 0o600
|
||||||
|
assert stat.S_IMODE(state.parent.stat().st_mode) & 0o027 == 0
|
||||||
|
|
||||||
|
with StateStore(state).transaction() as connection:
|
||||||
|
connection.execute("UPDATE metadata SET value='999' WHERE key='schema_version'")
|
||||||
|
with pytest.raises(StateConflict, match="SCHEMA_VERSION_UNSUPPORTED"):
|
||||||
|
StateStore(state)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dependency_retry_and_terminal_failure_are_durable(tmp_path: Path) -> None:
|
||||||
|
store = StateStore(tmp_path / "state.sqlite3")
|
||||||
|
engine = WorkflowEngine(store)
|
||||||
|
task = plan()
|
||||||
|
engine.plan(task)
|
||||||
|
|
||||||
|
assert engine.next_steps(task).ready_steps == ("build",)
|
||||||
|
assert store.list_steps(task.task_id)["build"].state == StepState.READY
|
||||||
|
engine.claim(task.task_id, "build", "writer", "session-1")
|
||||||
|
engine.start(task.task_id, "build", "writer", "session-1")
|
||||||
|
engine.complete(task.task_id, "build", result_digest="b" * 64)
|
||||||
|
|
||||||
|
assert engine.next_steps(task).ready_steps == ("test",)
|
||||||
|
engine.claim(task.task_id, "test", "tester", "session-2")
|
||||||
|
engine.start(task.task_id, "test", "tester", "session-2")
|
||||||
|
assert engine.fail(task, "test", "transient", retryable=True) == StepState.PENDING
|
||||||
|
assert engine.next_steps(task).ready_steps == ("test",)
|
||||||
|
engine.claim(task.task_id, "test", "tester", "session-3")
|
||||||
|
engine.start(task.task_id, "test", "tester", "session-3")
|
||||||
|
assert engine.fail(task, "test", "still broken", retryable=True) == StepState.FAILED
|
||||||
|
decision = engine.next_steps(task)
|
||||||
|
assert decision.blocked is True
|
||||||
|
assert decision.reason == "terminal_step_failure"
|
||||||
|
|
||||||
|
|
||||||
|
def test_claim_is_atomic_under_contention(tmp_path: Path) -> None:
|
||||||
|
store = StateStore(tmp_path / "state.sqlite3")
|
||||||
|
engine = WorkflowEngine(store)
|
||||||
|
task = plan()
|
||||||
|
engine.plan(task)
|
||||||
|
engine.next_steps(task)
|
||||||
|
|
||||||
|
def claim(worker: str) -> str:
|
||||||
|
try:
|
||||||
|
engine.claim(task.task_id, "build", worker, f"session-{worker}")
|
||||||
|
return "claimed"
|
||||||
|
except StateConflict:
|
||||||
|
return "rejected"
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||||
|
results = list(pool.map(claim, ("one", "two")))
|
||||||
|
assert sorted(results) == ["claimed", "rejected"]
|
||||||
@@ -1,440 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
sys.path.insert(0, str(ROOT / "services" / "aios-single-owner-governance"))
|
|
||||||
|
|
||||||
from aios_single_owner_governance.task_status_merge_broker import ( # noqa: E402
|
|
||||||
ALLOWED_CONTEXTS,
|
|
||||||
ContractDenied,
|
|
||||||
MergeBroker,
|
|
||||||
QuotaJournal,
|
|
||||||
StatusBroker,
|
|
||||||
TaskGrant,
|
|
||||||
_time,
|
|
||||||
_utc_timestamp,
|
|
||||||
canonical,
|
|
||||||
load_signed_task_grant,
|
|
||||||
read_runtime_secret,
|
|
||||||
)
|
|
||||||
from aios_single_owner_governance.task_credential_broker import TaskCredentialBroker # noqa: E402
|
|
||||||
from aios_single_owner_governance import task_credential_expiry_sweeper as sweeper # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
HEAD = "d" * 40
|
|
||||||
OLD_HEAD = "a8644fc172bd8ebbba17a16e6bf586c93fd1e793"
|
|
||||||
BASE = "8814dff20222625b60454c613b61ccf15fa20e51"
|
|
||||||
|
|
||||||
|
|
||||||
def grant() -> dict:
|
|
||||||
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
||||||
value = {
|
|
||||||
"schema_id": "AIOS_TASK_STATUS_MERGE_GRANT",
|
|
||||||
"schema_version": 1,
|
|
||||||
"repository": "aios/aios",
|
|
||||||
"pr_number": 36,
|
|
||||||
"base_oid": BASE,
|
|
||||||
"target_commit": HEAD,
|
|
||||||
"approved_tree_oid": "3649be2dac8fcea3a79cb1604b2cfb63f6cc0307",
|
|
||||||
"manifest_path": "approvals/release-campaign/AIOS-V2R1C4-PHASE-B-RENEWAL.json",
|
|
||||||
"manifest_blob_oid": "d6e4431992e4315799c7f0487f2b12a20d0e2890",
|
|
||||||
"manifest_sha256": "2555c5e37f77f1d0aac51864733b55ee3e91169ae76bf67e8817de6d1874eea4",
|
|
||||||
"payload_sha256": "4db7d3fd5aaa00a65eabfce7501f275b5f6c8a2587b6153fa8066ec0dafa5d6d",
|
|
||||||
"status_evidence_sha256": {context: "1" * 64 for context in ALLOWED_CONTEXTS},
|
|
||||||
"supersedes_pr_number": 36,
|
|
||||||
"supersedes_head_oid": OLD_HEAD,
|
|
||||||
"renewal_required": True,
|
|
||||||
"timestamp_contract": "RFC3339_OFFSET_AWARE_V2",
|
|
||||||
"canonical_timezone": "UTC",
|
|
||||||
"raw_timestamp_preserved": True,
|
|
||||||
"naive_datetime_allowed": False,
|
|
||||||
"status_identity": "codex",
|
|
||||||
"merge_identity": "codex",
|
|
||||||
"status_contexts": sorted(ALLOWED_CONTEXTS),
|
|
||||||
"status_write_quota": 5,
|
|
||||||
"merge_quota": 1,
|
|
||||||
"merge_method": "fast-forward-only",
|
|
||||||
"branch_push_allowed": False,
|
|
||||||
"tag_write_allowed": False,
|
|
||||||
"main_write_allowed": False,
|
|
||||||
"registry_write_allowed": False,
|
|
||||||
"baseline_write_allowed": False,
|
|
||||||
"production_write_allowed": False,
|
|
||||||
"issued_at": now.isoformat().replace("+00:00", "Z"),
|
|
||||||
"expires_at": (now + timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
|
|
||||||
"audit_key_ref": "systemd-credential://task-audit-hmac",
|
|
||||||
"grant_signer_identity": "aios-task-grant-signer",
|
|
||||||
"grant_signature_namespace": "aios-task-status-merge-grant-v1",
|
|
||||||
}
|
|
||||||
value["pr_number"] = 37
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAPI:
|
|
||||||
def __init__(self, *, with_statuses: bool = False, fail_status_after: int | None = None,
|
|
||||||
fail_after_merge: bool = False, review_timestamp: str | None = None,
|
|
||||||
reviewer: str = "owner", review_commit: str = HEAD):
|
|
||||||
self.posts: list[tuple[str, dict]] = []
|
|
||||||
self.merged = False
|
|
||||||
self.status_rows = [{"id": index + 1, "context": value, "status": "success", "sha": HEAD,
|
|
||||||
"description": "task-scoped PASS evidence_sha256=" + "1" * 64,
|
|
||||||
"creator": {"login": "codex"}} for index, value in enumerate(ALLOWED_CONTEXTS)] if with_statuses else []
|
|
||||||
self.fail_status_after = fail_status_after
|
|
||||||
self.fail_after_merge = fail_after_merge
|
|
||||||
self.base = BASE
|
|
||||||
self.review_timestamp = review_timestamp or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
||||||
self.reviewer = reviewer
|
|
||||||
self.review_commit = review_commit
|
|
||||||
|
|
||||||
def get(self, path: str):
|
|
||||||
if path.endswith("/reviews"):
|
|
||||||
return [{
|
|
||||||
"id": 76,
|
|
||||||
"state": "APPROVED",
|
|
||||||
"commit_id": self.review_commit,
|
|
||||||
"official": True,
|
|
||||||
"stale": False,
|
|
||||||
"dismissed": False,
|
|
||||||
"submitted_at": self.review_timestamp,
|
|
||||||
"user": {"id": 2 if self.reviewer == "owner" else 42, "login": self.reviewer},
|
|
||||||
}]
|
|
||||||
if path.endswith("/statuses"):
|
|
||||||
return list(self.status_rows)
|
|
||||||
if path.endswith("/branches/governance%2Fapprovals"):
|
|
||||||
return {"name": "governance/approvals", "commit": {"id": HEAD if self.merged else BASE}}
|
|
||||||
if path.endswith("/pulls/37"):
|
|
||||||
return {
|
|
||||||
"number": 37,
|
|
||||||
"state": "closed" if self.merged else "open",
|
|
||||||
"merged": self.merged,
|
|
||||||
"mergeable": True,
|
|
||||||
"head": {"sha": HEAD},
|
|
||||||
"base": {"sha": self.base, "ref": "governance/approvals"},
|
|
||||||
"merge_commit_sha": HEAD if self.merged else None,
|
|
||||||
}
|
|
||||||
raise AssertionError(path)
|
|
||||||
|
|
||||||
def post(self, path: str, payload: dict):
|
|
||||||
self.posts.append((path, payload))
|
|
||||||
if path.endswith("/merge"):
|
|
||||||
assert payload == {
|
|
||||||
"Do": "fast-forward-only",
|
|
||||||
"head_commit_id": HEAD,
|
|
||||||
"force_merge": False,
|
|
||||||
"delete_branch_after_merge": False,
|
|
||||||
}
|
|
||||||
self.merged = True
|
|
||||||
if self.fail_after_merge:
|
|
||||||
raise ContractDenied("SIMULATED_POST_MERGE_LOSS")
|
|
||||||
return {"merged": True}
|
|
||||||
if self.fail_status_after is not None and len(self.status_rows) >= self.fail_status_after:
|
|
||||||
raise ContractDenied("SIMULATED_STATUS_FAILURE")
|
|
||||||
row = {"id": len(self.posts), "context": payload["context"], "status": "success", "sha": HEAD,
|
|
||||||
"description": payload["description"],
|
|
||||||
"creator": {"login": "codex"}}
|
|
||||||
self.status_rows.append(row)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def evidence() -> list[dict]:
|
|
||||||
return [{"context": value, "evidence_sha256": "1" * 64} for value in sorted(ALLOWED_CONTEXTS)]
|
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusMergeBrokerTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.temporary = tempfile.TemporaryDirectory()
|
|
||||||
self.root = Path(self.temporary.name)
|
|
||||||
self.journals: list[QuotaJournal] = []
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
for journal in self.journals:
|
|
||||||
journal.close()
|
|
||||||
self.temporary.cleanup()
|
|
||||||
|
|
||||||
def journal(self, item: TaskGrant) -> QuotaJournal:
|
|
||||||
value = QuotaJournal(self.root / "journal.sqlite3", item)
|
|
||||||
self.journals.append(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def test_status_broker_publishes_exact_five_and_quota_is_single_use(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
api = FakeAPI()
|
|
||||||
journal = self.journal(item)
|
|
||||||
receipt = StatusBroker(item, journal, api, b"audit-key").publish(evidence())
|
|
||||||
self.assertEqual(receipt["status"], "PASS")
|
|
||||||
self.assertEqual(receipt["published_count"], 5)
|
|
||||||
self.assertEqual({entry[1]["context"] for entry in api.posts}, ALLOWED_CONTEXTS)
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "STATUS_QUOTA_EXHAUSTED"):
|
|
||||||
StatusBroker(item, journal, api, b"audit-key").publish(evidence())
|
|
||||||
|
|
||||||
def test_rfc3339_offset_parser_normalizes_to_utc(self):
|
|
||||||
cases = {
|
|
||||||
"2026-08-01T02:03:02Z": "2026-08-01T02:03:02Z",
|
|
||||||
"2026-08-01T10:03:02+08:00": "2026-08-01T02:03:02Z",
|
|
||||||
"2026-07-31T18:03:02-08:00": "2026-08-01T02:03:02Z",
|
|
||||||
"2026-08-01T10:03:02.123456+08:00": "2026-08-01T02:03:02.123456Z",
|
|
||||||
}
|
|
||||||
for raw, expected in cases.items():
|
|
||||||
with self.subTest(raw=raw):
|
|
||||||
self.assertEqual(_utc_timestamp(_time(raw)), expected)
|
|
||||||
|
|
||||||
def test_rfc3339_offset_parser_rejects_ambiguous_or_invalid_values(self):
|
|
||||||
invalid = [
|
|
||||||
"2026-08-01T10:03:02", "2026-08-01", "2026-08-01T10:03:02+0800",
|
|
||||||
"2026-08-01T10:03:02+25:00", "2026-08-01T10:03:02z",
|
|
||||||
" 2026-08-01T10:03:02+08:00", "2026-08-01T10:03:02+08:00 ",
|
|
||||||
None, 1, [],
|
|
||||||
]
|
|
||||||
for raw in invalid:
|
|
||||||
with self.subTest(raw=raw):
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "TIMESTAMP_INVALID"):
|
|
||||||
_time(raw) # type: ignore[arg-type]
|
|
||||||
|
|
||||||
def test_offset_review_preserves_raw_and_canonical_utc_in_receipt(self):
|
|
||||||
value = grant()
|
|
||||||
value["issued_at"] = "2026-08-01T00:00:00Z"
|
|
||||||
value["expires_at"] = "2026-08-01T12:00:00Z"
|
|
||||||
item = TaskGrant.from_dict(value, now=datetime(2026, 8, 1, 3, tzinfo=timezone.utc))
|
|
||||||
api = FakeAPI(review_timestamp="2026-08-01T10:03:02+08:00")
|
|
||||||
receipt = StatusBroker(item, self.journal(item), api, b"audit-key").publish(evidence())
|
|
||||||
self.assertEqual(receipt["review_timestamp_raw"], "2026-08-01T10:03:02+08:00")
|
|
||||||
self.assertEqual(receipt["review_timestamp_utc"], "2026-08-01T02:03:02Z")
|
|
||||||
|
|
||||||
def test_review_expiry_boundary_is_fail_closed(self):
|
|
||||||
value = grant()
|
|
||||||
value["issued_at"] = "2026-08-01T00:00:00Z"
|
|
||||||
value["expires_at"] = "2026-08-01T02:03:02Z"
|
|
||||||
item = TaskGrant.from_dict(value, now=datetime(2026, 8, 1, 1, tzinfo=timezone.utc))
|
|
||||||
for raw in ("2026-08-01T02:03:02Z", "2026-08-01T02:03:03Z"):
|
|
||||||
with self.subTest(raw=raw), self.assertRaisesRegex(ContractDenied, "OWNER_APPROVAL_EXPIRED"):
|
|
||||||
StatusBroker(item, self.journal(item), FakeAPI(review_timestamp=raw), b"audit-key").publish(evidence())
|
|
||||||
|
|
||||||
def test_wrong_reviewer_and_reviewed_head_remain_rejected(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
for api in (FakeAPI(reviewer="codex"), FakeAPI(review_commit="e" * 40)):
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "OWNER_EXACT_HEAD_APPROVAL_MISSING"):
|
|
||||||
StatusBroker(item, self.journal(item), api, b"audit-key").publish(evidence())
|
|
||||||
|
|
||||||
def test_merge_broker_uses_fast_forward_only_and_binds_exact_head(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
api = FakeAPI(with_statuses=True)
|
|
||||||
journal = self.journal(item)
|
|
||||||
receipt = MergeBroker(item, journal, api, b"audit-key").merge()
|
|
||||||
self.assertEqual(receipt["status"], "PASS")
|
|
||||||
self.assertEqual(receipt["governance_oid"], HEAD)
|
|
||||||
self.assertTrue(api.merged)
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "MERGE_QUOTA_EXHAUSTED"):
|
|
||||||
MergeBroker(item, journal, FakeAPI(with_statuses=True), b"audit-key").merge()
|
|
||||||
|
|
||||||
def test_contract_rejects_scope_expansion_secret_and_wrong_identity(self):
|
|
||||||
value = grant()
|
|
||||||
value["main_write_allowed"] = True
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "WRITE_SCOPE_EXPANDED"):
|
|
||||||
TaskGrant.from_dict(value)
|
|
||||||
|
|
||||||
def test_unbound_evidence_fails_closed(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
changed = evidence()
|
|
||||||
changed[0]["evidence_sha256"] = "2" * 64
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "EVIDENCE_SET_INVALID"):
|
|
||||||
StatusBroker(item, self.journal(item), FakeAPI(), b"audit-key").publish(changed)
|
|
||||||
|
|
||||||
def test_grant_requires_detached_asymmetric_signature(self):
|
|
||||||
grant_path = self.root / "grant.json"
|
|
||||||
signature_path = self.root / "grant.sig"
|
|
||||||
allowed_signers = self.root / "allowed_signers"
|
|
||||||
raw = canonical(grant()) + b"\n"
|
|
||||||
grant_path.write_bytes(raw)
|
|
||||||
signature_path.write_text("ssh-signature", encoding="utf-8")
|
|
||||||
allowed_signers.write_text("aios-task-grant-signer ssh-ed25519 AAAA", encoding="utf-8")
|
|
||||||
with mock.patch("aios_single_owner_governance.task_status_merge_broker.subprocess.run") as runner:
|
|
||||||
item = load_signed_task_grant(grant_path, signature_path, allowed_signers)
|
|
||||||
self.assertEqual(item.raw["pr_number"], 37)
|
|
||||||
self.assertEqual(runner.call_args.kwargs["input"], raw)
|
|
||||||
with mock.patch(
|
|
||||||
"aios_single_owner_governance.task_status_merge_broker.subprocess.run",
|
|
||||||
side_effect=subprocess.CalledProcessError(255, ["ssh-keygen"]),
|
|
||||||
):
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "GRANT_SIGNATURE_INVALID"):
|
|
||||||
load_signed_task_grant(grant_path, signature_path, allowed_signers)
|
|
||||||
|
|
||||||
def test_base_movement_blocks_status_publication_before_side_effect(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
api = FakeAPI()
|
|
||||||
api.base = "c" * 40
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "PR_BINDING_INVALID"):
|
|
||||||
StatusBroker(item, self.journal(item), api, b"audit-key").publish(evidence())
|
|
||||||
self.assertEqual(api.posts, [])
|
|
||||||
|
|
||||||
def test_credential_provision_rechecks_local_owner_and_refs_before_issuance(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
broker = TaskCredentialBroker(item, self.root / "audit", b"audit-key")
|
|
||||||
with mock.patch("aios_single_owner_governance.task_credential_broker.os.geteuid", return_value=0, create=True), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker._safe_absolute"), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker._verify_local_governance_binding", side_effect=ContractDenied("LOCAL_OWNER_APPROVAL_MISSING")), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.subprocess.run") as runner:
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "LOCAL_OWNER_APPROVAL_MISSING"):
|
|
||||||
broker.provision(
|
|
||||||
identity="codex",
|
|
||||||
token_name="pr37-dddddddddddd-status",
|
|
||||||
encrypted_path=Path("/run/aios-task-credential-broker/status.cred"),
|
|
||||||
)
|
|
||||||
runner.assert_not_called()
|
|
||||||
value = grant()
|
|
||||||
value["status_identity"] = "gitops-bot"
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "IDENTITY_NOT_ALLOWED"):
|
|
||||||
TaskGrant.from_dict(value)
|
|
||||||
|
|
||||||
def test_systemd_credential_projection_mode_and_name_are_supported_exactly(self):
|
|
||||||
metadata = mock.Mock(st_mode=0o100440, st_uid=0, st_gid=0)
|
|
||||||
credential = Path("/run/credentials/unit/gitea-token")
|
|
||||||
with mock.patch.dict("os.environ", {"CREDENTIALS_DIRECTORY": str(credential.parent)}), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_status_merge_broker.os.open", return_value=7), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_status_merge_broker.os.fstat", return_value=metadata), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_status_merge_broker.os.read", return_value=b"x" * 40), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_status_merge_broker.os.close"):
|
|
||||||
self.assertEqual(read_runtime_secret(credential), b"x" * 40)
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
broker = TaskCredentialBroker(item, self.root / "provision-audit", b"audit-key")
|
|
||||||
connection = mock.MagicMock()
|
|
||||||
connection.execute.return_value.fetchall.return_value = [(101, 42)]
|
|
||||||
issued = mock.Mock(stdout=b"x" * 40)
|
|
||||||
encrypted = mock.Mock(stdout=b"")
|
|
||||||
with mock.patch("aios_single_owner_governance.task_credential_broker.os.geteuid", return_value=0, create=True), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker._safe_absolute"), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker._verify_local_governance_binding"), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.os.chmod"), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.subprocess.run", side_effect=[issued, encrypted]) as runner, \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.sqlite3.connect", return_value=connection):
|
|
||||||
broker.provision(identity="codex", token_name="pr37-dddddddddddd-status", encrypted_path=Path("/run/aios-task-credential-broker/status.cred"))
|
|
||||||
self.assertIn("--name=gitea-token", runner.call_args_list[1].args[0])
|
|
||||||
|
|
||||||
def test_expiry_sweeper_revokes_projected_token_after_grant_expiry(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
runtime_root = self.root / "runtime"
|
|
||||||
credential_root = self.root / "credentials"
|
|
||||||
task_dir = runtime_root / "task-37"
|
|
||||||
audit_dir = task_dir / "audit"
|
|
||||||
audit_dir.mkdir(parents=True)
|
|
||||||
credential_root.mkdir()
|
|
||||||
(task_dir / "grant.json").write_text("{}", encoding="utf-8")
|
|
||||||
(task_dir / "grant.sig").write_text("signature", encoding="utf-8")
|
|
||||||
token_name = "pr37-dddddddddddd-status"
|
|
||||||
encrypted_path = credential_root / "status.cred"
|
|
||||||
encrypted_path.write_text("encrypted", encoding="utf-8")
|
|
||||||
(credential_root / "task-37-audit.cred").write_text("encrypted-audit-key", encoding="utf-8")
|
|
||||||
real_broker = TaskCredentialBroker(item, audit_dir, b"audit-key")
|
|
||||||
receipt = real_broker._audit({
|
|
||||||
"status": "PASS", "operation": "credential_provision", "identity": "codex",
|
|
||||||
"token_name": token_name, "token_id": 101, "token_uid": 7,
|
|
||||||
"grant_sha256": item.digest,
|
|
||||||
"credential_ref": "systemd-credential-encrypted://status.cred",
|
|
||||||
})
|
|
||||||
real_broker.validate_provision_receipt(receipt, encrypted_path)
|
|
||||||
tampered = dict(receipt)
|
|
||||||
tampered["token_id"] = 102
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "PROVISION_RECEIPT_INVALID"):
|
|
||||||
real_broker.validate_provision_receipt(tampered, encrypted_path)
|
|
||||||
fake_broker = mock.Mock()
|
|
||||||
fake_broker.revoke.return_value = {"status": "PASS", "old_token_http_status": 401}
|
|
||||||
with mock.patch.object(sweeper, "RUNTIME_ROOT", runtime_root), \
|
|
||||||
mock.patch.object(sweeper, "CREDENTIAL_ROOT", credential_root), \
|
|
||||||
mock.patch.object(sweeper.os, "geteuid", return_value=0, create=True), \
|
|
||||||
mock.patch.object(sweeper, "load_signed_task_grant", return_value=item), \
|
|
||||||
mock.patch.object(sweeper.subprocess, "run", return_value=mock.Mock(stdout=b"audit-key")), \
|
|
||||||
mock.patch.object(sweeper, "TaskCredentialBroker", return_value=fake_broker) as broker_type:
|
|
||||||
results = sweeper.sweep(runtime_root, self.root / "allowed_signers", now=item.expires_at)
|
|
||||||
self.assertEqual(results, [{"status": "PASS", "old_token_http_status": 401}])
|
|
||||||
broker_type.assert_called_once_with(item, audit_dir, b"audit-key")
|
|
||||||
fake_broker.revoke.assert_called_once()
|
|
||||||
|
|
||||||
def test_revocation_is_exact_access_token_row_only_and_proves_http_401(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
broker = TaskCredentialBroker(item, self.root / "revoke-audit", b"audit-key")
|
|
||||||
token_name = "pr37-dddddddddddd-status"
|
|
||||||
receipt = broker._audit({
|
|
||||||
"status": "PASS", "operation": "credential_provision", "identity": "codex",
|
|
||||||
"token_name": token_name, "token_id": 101, "token_uid": 7,
|
|
||||||
"grant_sha256": item.digest,
|
|
||||||
"credential_ref": "systemd-credential-encrypted://status.cred",
|
|
||||||
})
|
|
||||||
response = mock.MagicMock()
|
|
||||||
response.__enter__.return_value.status = 200
|
|
||||||
unauthorized = urllib.error.HTTPError("http://127.0.0.1:3000/api/v1/repos/aios/aios", 401, "Unauthorized", {}, None)
|
|
||||||
write_connection = mock.MagicMock()
|
|
||||||
write_connection.execute.return_value.rowcount = 1
|
|
||||||
read_connection = mock.MagicMock()
|
|
||||||
read_connection.execute.return_value.fetchone.return_value = (0,)
|
|
||||||
with mock.patch("aios_single_owner_governance.task_credential_broker.os.geteuid", return_value=0, create=True), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker._safe_absolute"), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.subprocess.run", return_value=mock.Mock(stdout=b"x" * 40)), \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.sqlite3.connect", side_effect=[write_connection, read_connection]) as connect, \
|
|
||||||
mock.patch("aios_single_owner_governance.task_credential_broker.urllib.request.urlopen", side_effect=[response, unauthorized]) as urlopen:
|
|
||||||
result = broker.revoke(receipt=receipt, encrypted_path=Path("/run/aios-task-credential-broker/status.cred"))
|
|
||||||
self.assertEqual(result["old_token_http_status"], 401)
|
|
||||||
self.assertIn("/repos/aios/aios", urlopen.call_args_list[0].args[0].full_url)
|
|
||||||
delete_sql = write_connection.execute.call_args_list[1].args[0]
|
|
||||||
self.assertIn("DELETE FROM access_token", delete_sql)
|
|
||||||
self.assertNotIn("commit_status", delete_sql)
|
|
||||||
self.assertIn("mode=ro", connect.call_args_list[1].args[0])
|
|
||||||
value = grant()
|
|
||||||
value["token"] = "plaintext"
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "GRANT_FIELDS_INVALID"):
|
|
||||||
TaskGrant.from_dict(value)
|
|
||||||
|
|
||||||
def test_audit_chain_is_content_addressed_and_contains_no_secret(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
journal = self.journal(item)
|
|
||||||
receipt = StatusBroker(item, journal, FakeAPI(), b"audit-key").publish(evidence())
|
|
||||||
serialized = json.dumps(receipt, sort_keys=True)
|
|
||||||
self.assertNotIn("audit-key", serialized)
|
|
||||||
self.assertEqual(len(receipt["audit_signature"]), 64)
|
|
||||||
self.assertTrue(journal.verify_chain())
|
|
||||||
|
|
||||||
def test_credential_broker_is_root_only_and_never_returns_a_token(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
broker = TaskCredentialBroker(item, self.root / "audit", b"audit-key")
|
|
||||||
with mock.patch("aios_single_owner_governance.task_credential_broker.os.geteuid", return_value=1000, create=True):
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "ROOT_BROKER_REQUIRED"):
|
|
||||||
broker.provision(
|
|
||||||
identity="codex",
|
|
||||||
token_name="pr36-a8644fc172bd-status",
|
|
||||||
encrypted_path=Path("/run/aios-task-credential-broker/status.cred"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_partial_status_side_effects_are_reconciled_without_duplicate_writes(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
api = FakeAPI(fail_status_after=2)
|
|
||||||
journal = self.journal(item)
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "SIMULATED_STATUS_FAILURE"):
|
|
||||||
StatusBroker(item, journal, api, b"audit-key").publish(evidence())
|
|
||||||
self.assertEqual(len(api.status_rows), 2)
|
|
||||||
api.fail_status_after = None
|
|
||||||
receipt = StatusBroker(item, journal, api, b"audit-key").publish(evidence())
|
|
||||||
self.assertEqual(receipt["published_count"], 5)
|
|
||||||
self.assertEqual(len(api.status_rows), 5)
|
|
||||||
|
|
||||||
def test_post_merge_response_loss_is_reconciled_from_exact_remote_ref(self):
|
|
||||||
item = TaskGrant.from_dict(grant())
|
|
||||||
api = FakeAPI(with_statuses=True, fail_after_merge=True)
|
|
||||||
journal = self.journal(item)
|
|
||||||
with self.assertRaisesRegex(ContractDenied, "SIMULATED_POST_MERGE_LOSS"):
|
|
||||||
MergeBroker(item, journal, api, b"audit-key").merge()
|
|
||||||
api.fail_after_merge = False
|
|
||||||
receipt = MergeBroker(item, journal, api, b"audit-key").merge()
|
|
||||||
self.assertTrue(receipt["reconciled"])
|
|
||||||
self.assertEqual(receipt["governance_oid"], HEAD)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
|
|
||||||
BACKEND_PATH = Path(__file__).resolve().parents[1] / "scripts" / "wechat_send_backend.py"
|
BACKEND_PATH = Path(__file__).resolve().parents[1] / "scripts" / "wechat_send_backend.py"
|
||||||
@@ -80,6 +89,450 @@ class WechatSendBackendTests(TestCase):
|
|||||||
self.assertEqual(args[1]["content"], "hello")
|
self.assertEqual(args[1]["content"], "hello")
|
||||||
self.assertFalse(args[1]["dry_run"])
|
self.assertFalse(args[1]["dry_run"])
|
||||||
|
|
||||||
|
def test_ucc_auth_is_authorization_header_not_argv_or_output(self):
|
||||||
|
secret = "ucc-super-secret-token"
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
status = 200
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def read(self, _limit=-1):
|
||||||
|
return b'{"ok":true,"status":"sent"}'
|
||||||
|
|
||||||
|
urlopen = Mock(return_value=Response())
|
||||||
|
output = io.StringIO()
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_ADAPTER_TOKEN", secret),
|
||||||
|
patch.object(self.backend, "_open_no_redirect", urlopen),
|
||||||
|
redirect_stdout(output),
|
||||||
|
redirect_stderr(output),
|
||||||
|
):
|
||||||
|
status, payload = self.backend._post_ucc(
|
||||||
|
"/api/internal/wechat/send", {"target": "chat-001", "content": "hello"}
|
||||||
|
)
|
||||||
|
|
||||||
|
request = urlopen.call_args.args[0]
|
||||||
|
self.assertEqual(request.get_header("Authorization"), f"Bearer {secret}")
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload, {"ok": True, "status": "sent"})
|
||||||
|
self.assertNotIn(secret, " ".join(self.backend._openclaw_base_args()))
|
||||||
|
self.assertNotIn(secret, output.getvalue())
|
||||||
|
self.assertFalse({"--gateway", "--token", "--password"} & set(self.backend._openclaw_base_args()))
|
||||||
|
|
||||||
|
def test_ucc_paths_are_canonical_first_unique_and_bounded(self):
|
||||||
|
with patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"):
|
||||||
|
paths = self.backend._ucc_send_paths()
|
||||||
|
|
||||||
|
self.assertEqual(paths[0], "/api/internal/wechat/send")
|
||||||
|
self.assertLessEqual(len(paths), 2)
|
||||||
|
self.assertEqual(len(paths), len(set(paths)))
|
||||||
|
self.assertTrue(all(path.startswith("/") for path in paths))
|
||||||
|
|
||||||
|
def test_ucc_accepts_only_explicit_true_and_sent_when_not_dry_run(self):
|
||||||
|
self.assertTrue(self.backend._ucc_response_sent({"ok": True, "status": "sent"}))
|
||||||
|
for response in (
|
||||||
|
{"ok": 1, "status": "sent"},
|
||||||
|
{"ok": True, "status": "accepted"},
|
||||||
|
{"ok": False, "status": "sent"},
|
||||||
|
{"status": "sent"},
|
||||||
|
):
|
||||||
|
with self.subTest(response=response):
|
||||||
|
self.assertFalse(self.backend._ucc_response_sent(response))
|
||||||
|
|
||||||
|
def test_ambiguous_canonical_failures_never_fallback(self):
|
||||||
|
secret = "raw-upstream-secret"
|
||||||
|
failures = (
|
||||||
|
TimeoutError(secret),
|
||||||
|
ConnectionError(secret),
|
||||||
|
self.backend._BackendProtocolError(secret),
|
||||||
|
RuntimeError(secret),
|
||||||
|
)
|
||||||
|
for failure in failures:
|
||||||
|
with self.subTest(failure=type(failure).__name__):
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"),
|
||||||
|
patch.object(self.backend, "_post_ucc", side_effect=failure) as post,
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", message_id="msg-001"
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["code"], "ucc_adapter_unavailable")
|
||||||
|
self.assertEqual(post.call_count, 1)
|
||||||
|
self.assertNotIn(secret, repr(result))
|
||||||
|
|
||||||
|
def test_only_explicit_http_404_or_405_uses_legacy_fallback(self):
|
||||||
|
for http_status in (404, 405):
|
||||||
|
with self.subTest(http_status=http_status):
|
||||||
|
missing = urllib.error.HTTPError(
|
||||||
|
"http://127.0.0.1/api/internal/wechat/send",
|
||||||
|
http_status,
|
||||||
|
"not found",
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_SEND_PATH", "/api/internal/wechat/send"),
|
||||||
|
patch.object(
|
||||||
|
self.backend,
|
||||||
|
"_post_ucc",
|
||||||
|
side_effect=[missing, (200, {"ok": True, "status": "sent"})],
|
||||||
|
) as post,
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", message_id="msg-001"
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertEqual(result["endpoint"], "/internal/channels/wechat/send")
|
||||||
|
self.assertEqual(post.call_count, 2)
|
||||||
|
|
||||||
|
def test_non_not_found_http_error_never_falls_back(self):
|
||||||
|
denied = urllib.error.HTTPError(
|
||||||
|
"http://127.0.0.1/api/internal/wechat/send", 401, "denied", {}, None
|
||||||
|
)
|
||||||
|
with patch.object(self.backend, "_post_ucc", side_effect=denied) as post:
|
||||||
|
result = self.backend.send_ucc_adapter(target="chat-001", content="hello").to_dict()
|
||||||
|
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["code"], "ucc_adapter_http_error")
|
||||||
|
post.assert_called_once()
|
||||||
|
|
||||||
|
def test_ucc_rejection_does_not_expose_raw_error_or_secret(self):
|
||||||
|
secret = "provider-password=secret-value"
|
||||||
|
with patch.object(
|
||||||
|
self.backend,
|
||||||
|
"_post_ucc",
|
||||||
|
return_value=(200, {"ok": False, "status": "failed", "error": secret}),
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", message_id="msg-001"
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["code"], "ucc_adapter_rejected")
|
||||||
|
self.assertNotIn(secret, repr(result))
|
||||||
|
|
||||||
|
def test_dry_run_requires_dry_run_status_and_never_reports_sent(self):
|
||||||
|
with patch.object(
|
||||||
|
self.backend,
|
||||||
|
"_post_ucc",
|
||||||
|
return_value=(200, {"ok": True, "status": "dry_run"}),
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", dry_run=True
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertEqual(result["status"], "dry_run")
|
||||||
|
self.assertNotEqual(result["status"], "sent")
|
||||||
|
|
||||||
|
for dry_run, upstream_status in ((True, "sent"), (False, "dry_run")):
|
||||||
|
with self.subTest(dry_run=dry_run, upstream_status=upstream_status):
|
||||||
|
with patch.object(
|
||||||
|
self.backend,
|
||||||
|
"_post_ucc",
|
||||||
|
return_value=(200, {"ok": True, "status": upstream_status}),
|
||||||
|
):
|
||||||
|
rejected = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", dry_run=dry_run
|
||||||
|
).to_dict()
|
||||||
|
self.assertFalse(rejected["ok"])
|
||||||
|
self.assertEqual(rejected["status"], "failed")
|
||||||
|
|
||||||
|
def test_invalid_provider_message_id_never_reverses_confirmed_send(self):
|
||||||
|
invalid_values = (
|
||||||
|
"\ud800",
|
||||||
|
"contains\x00control",
|
||||||
|
"x" * (self.backend.MAX_IDENTIFIER_BYTES + 1),
|
||||||
|
{"not": "a string"},
|
||||||
|
)
|
||||||
|
for provider_message_id in invalid_values:
|
||||||
|
with self.subTest(value=repr(provider_message_id)[:40]):
|
||||||
|
with patch.object(
|
||||||
|
self.backend,
|
||||||
|
"_post_ucc",
|
||||||
|
return_value=(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"status": "sent",
|
||||||
|
"provider_message_id": provider_message_id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello"
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertTrue(result["ok"])
|
||||||
|
self.assertEqual(result["status"], "sent")
|
||||||
|
self.assertNotIn("provider_message_id", result)
|
||||||
|
|
||||||
|
def test_openclaw_2026_6_11_commands_have_no_unsupported_global_gateway(self):
|
||||||
|
completed = SimpleNamespace(returncode=0, stdout='{"channels": []}')
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "OPENCLAW_BIN", "/usr/local/bin/openclaw"),
|
||||||
|
patch.object(self.backend, "_run_openclaw", return_value=completed) as run,
|
||||||
|
):
|
||||||
|
self.assertIsNone(self.backend.discover_openclaw_wechat_channel())
|
||||||
|
|
||||||
|
command = run.call_args.args[0]
|
||||||
|
self.assertEqual(command, ["/usr/local/bin/openclaw", "channels", "list", "--json"])
|
||||||
|
self.assertFalse({"--gateway", "--token", "--password"} & set(command))
|
||||||
|
|
||||||
|
def test_openclaw_output_overflow_terminates_while_process_is_running(self):
|
||||||
|
writer = (
|
||||||
|
"import os,time;"
|
||||||
|
f"os.write(1,b'x'*{self.backend.MAX_RESPONSE_BYTES + 4096});"
|
||||||
|
"time.sleep(10)"
|
||||||
|
)
|
||||||
|
started = time.monotonic()
|
||||||
|
completed = self.backend._run_openclaw([sys.executable, "-c", writer], timeout=4)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
|
||||||
|
self.assertLess(elapsed, 2.0)
|
||||||
|
self.assertNotEqual(completed.returncode, 0)
|
||||||
|
self.assertEqual(completed.stdout, "")
|
||||||
|
self.assertEqual(completed.stderr, "")
|
||||||
|
|
||||||
|
def test_openclaw_timeout_returns_failure_and_terminates_promptly(self):
|
||||||
|
started = time.monotonic()
|
||||||
|
completed = self.backend._run_openclaw(
|
||||||
|
[sys.executable, "-c", "import time;time.sleep(10)"], timeout=1
|
||||||
|
)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
|
||||||
|
self.assertLess(elapsed, 3.0)
|
||||||
|
self.assertNotEqual(completed.returncode, 0)
|
||||||
|
self.assertEqual(completed.stdout, "")
|
||||||
|
self.assertEqual(completed.stderr, "")
|
||||||
|
|
||||||
|
def test_timeout_kills_inherited_pipe_descendant_after_parent_exits(self):
|
||||||
|
if os.name != "posix" or not Path("/proc").is_dir():
|
||||||
|
self.skipTest("POSIX process-group and procfs contract")
|
||||||
|
|
||||||
|
def process_is_running(pid):
|
||||||
|
try:
|
||||||
|
state = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8").split()[2]
|
||||||
|
except (FileNotFoundError, IndexError, OSError):
|
||||||
|
return False
|
||||||
|
return state not in {"X", "Z"}
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
pid_path = Path(directory) / "descendant.pid"
|
||||||
|
descendant = "import time;time.sleep(30)"
|
||||||
|
launcher = (
|
||||||
|
"import pathlib,subprocess,sys;"
|
||||||
|
f"child=subprocess.Popen([sys.executable,'-c',{descendant!r}]);"
|
||||||
|
f"pathlib.Path({str(pid_path)!r}).write_text(str(child.pid),encoding='utf-8')"
|
||||||
|
)
|
||||||
|
started = time.monotonic()
|
||||||
|
completed = self.backend._run_openclaw(
|
||||||
|
[sys.executable, "-c", launcher], timeout=1
|
||||||
|
)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
self.assertTrue(pid_path.is_file())
|
||||||
|
descendant_pid = int(pid_path.read_text(encoding="utf-8"))
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + 2
|
||||||
|
while process_is_running(descendant_pid) and time.monotonic() < deadline:
|
||||||
|
time.sleep(0.02)
|
||||||
|
self.assertFalse(process_is_running(descendant_pid))
|
||||||
|
finally:
|
||||||
|
if process_is_running(descendant_pid):
|
||||||
|
os.kill(descendant_pid, signal.SIGKILL)
|
||||||
|
|
||||||
|
self.assertLess(elapsed, 3.0)
|
||||||
|
self.assertNotEqual(completed.returncode, 0)
|
||||||
|
|
||||||
|
def test_process_tree_termination_uses_posix_process_group(self):
|
||||||
|
if os.name != "posix":
|
||||||
|
self.skipTest("POSIX process-group contract")
|
||||||
|
|
||||||
|
class Process:
|
||||||
|
pid = 43210
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.wait_count = 0
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def wait(self, timeout):
|
||||||
|
self.wait_count += 1
|
||||||
|
if self.wait_count == 1:
|
||||||
|
raise subprocess.TimeoutExpired(["openclaw"], timeout)
|
||||||
|
return -signal.SIGKILL
|
||||||
|
|
||||||
|
process = Process()
|
||||||
|
with patch.object(self.backend.os, "killpg") as killpg:
|
||||||
|
self.backend._terminate_process_tree(process)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
killpg.call_args_list,
|
||||||
|
[
|
||||||
|
call(process.pid, signal.SIGTERM),
|
||||||
|
call(process.pid, signal.SIGKILL),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unsupported_and_generic_chat_backends_fail_closed(self):
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "SEND_BACKEND", "openclaw-chat"),
|
||||||
|
patch.object(self.backend, "ucc_adapter_alive", return_value=False),
|
||||||
|
):
|
||||||
|
selected = self.backend.select_backend()
|
||||||
|
result = self.backend.send_message(target="chat-001", content="hello")
|
||||||
|
|
||||||
|
self.assertEqual(selected["backend"], "unavailable")
|
||||||
|
self.assertEqual(selected["reason"], "unsupported_backend")
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["code"], "unsupported_backend")
|
||||||
|
|
||||||
|
def test_invalid_or_oversized_input_is_rejected_before_network(self):
|
||||||
|
with patch.object(self.backend, "_post_ucc") as post:
|
||||||
|
empty = self.backend.send_ucc_adapter(target="", content="hello").to_dict()
|
||||||
|
oversized = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="x" * (self.backend.MAX_CONTENT_BYTES + 1)
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(empty["code"], "invalid_input")
|
||||||
|
self.assertEqual(oversized["code"], "input_too_large")
|
||||||
|
post.assert_not_called()
|
||||||
|
|
||||||
|
def test_unpaired_surrogate_inputs_fail_closed_before_network(self):
|
||||||
|
cases = (
|
||||||
|
{"target": "\ud800", "content": "hello"},
|
||||||
|
{"target": "chat-001", "content": "\ud800"},
|
||||||
|
{"target": "chat-001", "content": "hello", "account_id": "\ud800"},
|
||||||
|
{"target": "chat-001", "content": "hello", "message_id": "\ud800"},
|
||||||
|
{"target": "chat-001", "content": "hello", "metadata": {"bad": "\ud800"}},
|
||||||
|
)
|
||||||
|
with patch.object(self.backend, "_post_ucc") as post:
|
||||||
|
for kwargs in cases:
|
||||||
|
with self.subTest(field=next(reversed(kwargs))):
|
||||||
|
result = self.backend.send_ucc_adapter(**kwargs).to_dict()
|
||||||
|
self.assertEqual(result["code"], "invalid_input")
|
||||||
|
post.assert_not_called()
|
||||||
|
|
||||||
|
def test_deeply_nested_metadata_is_bounded_before_network(self):
|
||||||
|
metadata = {}
|
||||||
|
for _ in range(1200):
|
||||||
|
metadata = {"nested": metadata}
|
||||||
|
|
||||||
|
with patch.object(self.backend, "_post_ucc") as post:
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello", metadata=metadata
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(result["code"], "input_too_large")
|
||||||
|
post.assert_not_called()
|
||||||
|
|
||||||
|
def test_adapter_origin_must_be_allowlisted(self):
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_ADAPTER_URL", "https://evil.example:8443"),
|
||||||
|
patch.object(
|
||||||
|
self.backend,
|
||||||
|
"UCC_ADAPTER_ALLOWED_ORIGINS",
|
||||||
|
("http://127.0.0.1:18800",),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertIsNone(self.backend._adapter_base_url())
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_ADAPTER_URL", "https://adapter.example:8443"),
|
||||||
|
patch.object(
|
||||||
|
self.backend,
|
||||||
|
"UCC_ADAPTER_ALLOWED_ORIGINS",
|
||||||
|
("https://adapter.example:8443",),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(
|
||||||
|
self.backend._adapter_base_url(), "https://adapter.example:8443"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_adapter_origin_rejects_scheme_userinfo_fragment_and_path(self):
|
||||||
|
invalid_origins = (
|
||||||
|
"ftp://127.0.0.1:18800",
|
||||||
|
"http://user:password@127.0.0.1:18800",
|
||||||
|
"http://127.0.0.1:18800#fragment",
|
||||||
|
"http://127.0.0.1:18800/base-path",
|
||||||
|
)
|
||||||
|
for origin in invalid_origins:
|
||||||
|
with self.subTest(origin=origin):
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_ADAPTER_URL", origin),
|
||||||
|
patch.object(
|
||||||
|
self.backend,
|
||||||
|
"UCC_ADAPTER_ALLOWED_ORIGINS",
|
||||||
|
("http://127.0.0.1:18800",),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertIsNone(self.backend._adapter_base_url())
|
||||||
|
|
||||||
|
def test_adapter_path_rejects_decoded_traversal(self):
|
||||||
|
for path in (
|
||||||
|
"/api/%2e%2e/send",
|
||||||
|
"/api/%2E%2E/send",
|
||||||
|
"/api/%2525252e%2525252e/send",
|
||||||
|
"/api/%252525252e%252525252e/send",
|
||||||
|
"/api/../send",
|
||||||
|
"/api/%zz/send",
|
||||||
|
):
|
||||||
|
with self.subTest(path=path):
|
||||||
|
self.assertIsNone(self.backend._safe_path(path))
|
||||||
|
|
||||||
|
def test_adapter_path_keeps_intended_safe_canonical_paths(self):
|
||||||
|
for path in (
|
||||||
|
"/api/internal/wechat/send",
|
||||||
|
"/internal/channels/wechat/send",
|
||||||
|
"/api/%7Ehealth/status",
|
||||||
|
):
|
||||||
|
with self.subTest(path=path):
|
||||||
|
self.assertEqual(self.backend._safe_path(path), path)
|
||||||
|
|
||||||
|
def test_redirects_are_refused_and_authorization_cannot_cross_origin(self):
|
||||||
|
handler = self.backend._NoRedirectHandler()
|
||||||
|
self.assertIsNone(
|
||||||
|
handler.redirect_request(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
302,
|
||||||
|
"redirect",
|
||||||
|
{"Location": "https://evil.example/steal"},
|
||||||
|
"https://evil.example/steal",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
redirected = urllib.error.HTTPError(
|
||||||
|
"http://127.0.0.1:18800/api/internal/wechat/send",
|
||||||
|
302,
|
||||||
|
"redirect",
|
||||||
|
{"Location": "https://evil.example/steal"},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch.object(self.backend, "UCC_ADAPTER_TOKEN", "header-secret"),
|
||||||
|
patch.object(self.backend, "_open_no_redirect", side_effect=redirected) as opened,
|
||||||
|
):
|
||||||
|
result = self.backend.send_ucc_adapter(
|
||||||
|
target="chat-001", content="hello"
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
request = opened.call_args.args[0]
|
||||||
|
self.assertEqual(urllib.parse.urlsplit(request.full_url).netloc, "127.0.0.1:18800")
|
||||||
|
self.assertEqual(request.get_header("Authorization"), "Bearer header-secret")
|
||||||
|
opened.assert_called_once()
|
||||||
|
self.assertFalse(result["ok"])
|
||||||
|
self.assertEqual(result["code"], "ucc_adapter_http_error")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import unittest
|
import unittest
|
||||||
|
|||||||
Reference in New Issue
Block a user