Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a60a3c41 | ||
|
|
9303a5d684 |
+185
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
|
||||
[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
|
||||
@@ -0,0 +1,23 @@
|
||||
[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
|
||||
@@ -0,0 +1,12 @@
|
||||
[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
|
||||
@@ -0,0 +1,26 @@
|
||||
[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
|
||||
@@ -0,0 +1,26 @@
|
||||
[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,28 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
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
@@ -0,0 +1,94 @@
|
||||
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())
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
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())
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"$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,440 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user