Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
015185d7e5 | ||
|
|
9303a5d684 | ||
|
|
8814dff202 | ||
|
|
ff6743e851 | ||
|
|
ebd7439874 | ||
|
|
c41e278c59 | ||
|
|
1071979079 | ||
|
|
9a158fcab9 | ||
|
|
8b7f9d9028 | ||
|
|
3aa1872084 | ||
|
|
f6bef90633 | ||
|
|
c2eed5b379 | ||
|
|
6e8212608e | ||
|
|
e6a53888fe | ||
|
|
2bef44f6bc | ||
|
|
a96f36bf72 | ||
|
|
4e6ef70029 | ||
|
|
80feef309f | ||
|
|
a5489f1b67 | ||
|
|
312d3b4830 | ||
|
|
ac2dc5c30e | ||
|
|
5ef261b265 | ||
|
|
3a22c3c7d8 | ||
|
|
1c9f28a613 | ||
|
|
9514062cf3 | ||
|
|
a701a32cb4 | ||
|
|
45f3226662 | ||
|
|
975d239d40 | ||
|
|
3bc44accb5 | ||
|
|
3304d87872 |
@@ -1,486 +0,0 @@
|
||||
# Agent Production Deployment Requirements
|
||||
|
||||
## Status entering deployment
|
||||
|
||||
```yaml
|
||||
code_development: COMPLETED
|
||||
repository_commit: NOT_CREATED
|
||||
production_deployment: NOT_STARTED
|
||||
reason_repository_commit_missing:
|
||||
current ChatGPT GitOps runtime has AIOS_GITOPS_WRITES_ENABLED=false
|
||||
```
|
||||
|
||||
The code bundle is an input artifact. The Agent must place it on a non-main
|
||||
integration branch, review it, run acceptance tests, merge it through protected
|
||||
main and deploy the API adapter. The Agent must not regenerate or materially
|
||||
rewrite the supplied implementation without recording the new diff and tests.
|
||||
|
||||
## Frozen production state
|
||||
|
||||
Before any change, verify:
|
||||
|
||||
```yaml
|
||||
main_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
|
||||
V2R1C5:
|
||||
tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
|
||||
peeled_target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
baseline_version: V2R1C5
|
||||
baseline_status: PRODUCTION_DEPLOYED
|
||||
target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
evidence_closure_oid: 5e2cf8276f5093f3352b9a3252c701a44c5ab807
|
||||
```
|
||||
|
||||
If authoritative `main` no longer equals the approved base OID, stop with
|
||||
`MANUAL_OWNER_REVIEW_REQUIRED / AUTHORIZED_BASE_OID_DRIFT`. Do not silently
|
||||
rebase the bundle onto a different base.
|
||||
|
||||
## Bootstrap sequence — no additional Owner interaction
|
||||
|
||||
Use the following order so that the approved V2R1C6 base OID remains valid and
|
||||
the bridge does not create its own authorization deadlock:
|
||||
|
||||
1. Verify the supplied archive/patch SHA-256.
|
||||
2. Install the bundle into a temporary root-owned staging directory outside the
|
||||
Git worktrees.
|
||||
3. Start a localhost-only temporary instance of
|
||||
`aios_authorized_task_writer_api.py` on a dedicated port or Unix socket.
|
||||
4. Bind it to the existing authenticated Owner registry and the exact
|
||||
`writer:provision` scope. Do not expose it through the public gateway.
|
||||
5. Execute the live V2R1C6 provisioning request while `main` still equals
|
||||
`875d41c3101093149414292067fd700dc214d4d3`.
|
||||
6. Verify `READY_FOR_CODE_AUTHORING`, the exact branch OID, writer scope and
|
||||
negative policy tests.
|
||||
7. Use the newly created scoped writer to commit this reviewed bundle onto:
|
||||
`agent/v2r1c6-mainline-reconciliation-20260805`.
|
||||
8. Stop and remove the temporary bootstrap service and revoke its temporary
|
||||
runtime credential. Preserve its signed grant receipt and audit logs.
|
||||
9. Open a protected PR from the V2R1C6 branch. Deploy the permanent bridge only
|
||||
from the reviewed merge commit.
|
||||
|
||||
The temporary service is a control-plane bootstrap, not a production release.
|
||||
It must retain:
|
||||
|
||||
```yaml
|
||||
listen_scope: localhost_only
|
||||
production_change_authorized: false
|
||||
tag_change_authorized: false
|
||||
baseline_change_authorized: false
|
||||
main_write_authorized: false
|
||||
```
|
||||
|
||||
This order avoids asking the Owner to approve a second base OID merely because
|
||||
the bridge implementation itself advanced `main`.
|
||||
|
||||
### Alternative platform-maintenance path
|
||||
|
||||
If policy prohibits a temporary localhost bootstrap service, use an existing
|
||||
Owner-approved platform-maintenance executor to create:
|
||||
|
||||
```yaml
|
||||
branch: patch/owner-authorization-writer-bridge-20260806
|
||||
base_ref: refs/heads/main
|
||||
base_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
direct_main_push: false
|
||||
force_push: false
|
||||
tag_write: false
|
||||
baseline_write: false
|
||||
production_deploy_during_integration: false
|
||||
```
|
||||
|
||||
Merging that branch before V2R1C6 provisioning changes the authoritative base
|
||||
OID. In that alternative only, a new exact Owner base-OID amendment is required.
|
||||
The Agent must never substitute the newer `main` OID automatically.
|
||||
|
||||
## Files to install on the branch
|
||||
|
||||
Copy with the same relative paths:
|
||||
|
||||
```text
|
||||
active-runtime/app/authorized_task_writer_provisioning.py
|
||||
active-runtime/app/aios_authorized_task_writer_api.py
|
||||
docs/gitops/authorized-task-writer-provisioning.md
|
||||
docs/gitops/openapi-authorized-task-writer-fragment.yaml
|
||||
deploy/systemd/aios-gitops-model-api-authorized-writer-bridge.conf.example
|
||||
scripts/smoke_authorized_task_writer_provisioning.py
|
||||
tests/test_authorized_task_writer_provisioning.py
|
||||
```
|
||||
|
||||
Merge the OpenAPI fragment into:
|
||||
|
||||
```text
|
||||
docs/gitops/openapi-chatgpt-actions.yaml
|
||||
```
|
||||
|
||||
The final schema must expose:
|
||||
|
||||
```yaml
|
||||
operationId: provisionAuthorizedTaskWriter
|
||||
path: POST /api/task-writer/provision-authorized
|
||||
x-openai-isConsequential: true
|
||||
```
|
||||
|
||||
Do not deploy the fragment as a second standalone OpenAPI document.
|
||||
|
||||
## Required review of existing API integration
|
||||
|
||||
The adapter expects the existing API handler to provide:
|
||||
|
||||
```python
|
||||
handler.need(scope)
|
||||
handler.body()
|
||||
handler.sendj(value, status=200)
|
||||
api.H.do_POST
|
||||
api.main()
|
||||
```
|
||||
|
||||
Confirm these contracts against the branch before merge. If the live API has a
|
||||
different authentication object shape, make the smallest adapter-only change
|
||||
and rerun every test. The core provisioning policy must remain unchanged.
|
||||
|
||||
The API authorization registry must grant `writer:provision` only to the
|
||||
authenticated Owner role. Agents and ordinary writer tokens must not receive
|
||||
this scope.
|
||||
|
||||
## Privilege boundary
|
||||
|
||||
The API service must remain non-root.
|
||||
|
||||
Install a root-owned sudoers rule that allows the API service identity to run
|
||||
only these exact commands with `sudo -n --`:
|
||||
|
||||
```text
|
||||
/usr/local/libexec/aios-grant-signer-adapter
|
||||
/usr/local/libexec/aios-grant-validator
|
||||
/usr/local/libexec/aios-task-writer-provisioner
|
||||
```
|
||||
|
||||
Do not allow:
|
||||
|
||||
```text
|
||||
/bin/sh
|
||||
/bin/bash
|
||||
/usr/bin/env
|
||||
/usr/bin/git
|
||||
wildcard executable paths
|
||||
SETENV
|
||||
arbitrary environment preservation
|
||||
```
|
||||
|
||||
The governed tools themselves must validate their `--grant-dir` and
|
||||
`--result-json` paths. Their binaries must be root-owned, non-symlink, regular,
|
||||
executable and not group/world writable.
|
||||
|
||||
The signer must access its private signing material only through a runtime
|
||||
secret reference. No signing key or token may enter Git, API output, the
|
||||
provisioning receipt or systemd environment text.
|
||||
|
||||
## Runtime paths and permissions
|
||||
|
||||
Create:
|
||||
|
||||
```text
|
||||
/var/lib/aios-gitops/authorized-writer-bridge
|
||||
/var/lib/aios-gitops/authorized-writer-bridge/receipts
|
||||
/var/lib/aios-gitops/authorized-writer-bridge/locks
|
||||
```
|
||||
|
||||
Required properties:
|
||||
|
||||
```yaml
|
||||
owner: API service identity
|
||||
mode: "0700"
|
||||
symlink: false
|
||||
persistent: true
|
||||
```
|
||||
|
||||
The directory contains redacted receipts only. Temporary signed grant
|
||||
directories are deleted after success or failure.
|
||||
|
||||
## Required staging environment
|
||||
|
||||
Configure the API service:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
|
||||
AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
|
||||
AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
|
||||
AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
|
||||
AIOS_GITOPS_OWNER_PROVISION_SCOPE=writer:provision
|
||||
AIOS_GITOPS_GOVERNED_USE_SUDO=true
|
||||
AIOS_GITOPS_SUDO_PATH=/usr/bin/sudo
|
||||
```
|
||||
|
||||
Use `aios_authorized_task_writer_api.py` as the API entrypoint. It delegates
|
||||
all pre-existing routes and adds only the new endpoint.
|
||||
|
||||
## Pre-merge tests
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```bash
|
||||
python3 -m unittest -v tests/test_authorized_task_writer_provisioning.py
|
||||
python3 scripts/smoke_authorized_task_writer_provisioning.py
|
||||
python3 -m py_compile \
|
||||
active-runtime/app/authorized_task_writer_provisioning.py \
|
||||
active-runtime/app/aios_authorized_task_writer_api.py
|
||||
```
|
||||
|
||||
Required result:
|
||||
|
||||
```yaml
|
||||
unit_tests:
|
||||
total: 9
|
||||
failures: 0
|
||||
errors: 0
|
||||
smoke: PASS
|
||||
py_compile: PASS
|
||||
```
|
||||
|
||||
Also run all existing GitOps API and isolated writer tests affected by:
|
||||
|
||||
```text
|
||||
aios_chatgpt_actions_api
|
||||
aios_isolated_writer_api
|
||||
isolated_writer_authz
|
||||
isolated_writer_token_registry
|
||||
single_owner_governance
|
||||
OpenAPI schema validation
|
||||
```
|
||||
|
||||
Any regression blocks merge.
|
||||
|
||||
## Staging API acceptance
|
||||
|
||||
### Negative authentication
|
||||
|
||||
An Agent or non-Owner credential calling the endpoint must receive:
|
||||
|
||||
```yaml
|
||||
http_status: 403
|
||||
error: OWNER_AUTHORIZATION_REQUIRED
|
||||
```
|
||||
|
||||
### Negative scope tests
|
||||
|
||||
Requests enabling any of the following must be denied before signer execution:
|
||||
|
||||
```text
|
||||
production_change_authorized=true
|
||||
tag_change_authorized=true
|
||||
baseline_change_authorized=true
|
||||
main_write_authorized=true
|
||||
allowed_operations containing tag/main/baseline/admin operations
|
||||
missing required denied operation
|
||||
one_time_use=false
|
||||
expiry over 1800 seconds
|
||||
```
|
||||
|
||||
### Drift and conflict tests
|
||||
|
||||
Verify:
|
||||
|
||||
```yaml
|
||||
wrong_base_oid:
|
||||
http_status: 409
|
||||
error: AUTHORIZED_BASE_OID_DRIFT
|
||||
|
||||
existing_unreceipted_target_branch:
|
||||
http_status: 409
|
||||
error: EXISTING_BRANCH_OID_CONFLICT
|
||||
```
|
||||
|
||||
### Governed tool failures
|
||||
|
||||
Independently inject and verify fail-closed behavior for:
|
||||
|
||||
```text
|
||||
signer unavailable
|
||||
signer signature absent
|
||||
validator signature_valid=false
|
||||
validator signer_trusted=false
|
||||
validator plan expired
|
||||
provisioner grant_consumed=false
|
||||
provisioner writer scope enables main/tag/baseline/force push
|
||||
any required negative policy test false
|
||||
```
|
||||
|
||||
None may create or modify a Git ref.
|
||||
|
||||
## Live V2R1C6 provisioning request
|
||||
|
||||
After protected merge and API deployment, authenticate as the Owner and submit
|
||||
exactly:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING",
|
||||
"repository": "aios/aios",
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"target_branch": "agent/v2r1c6-mainline-reconciliation-20260805",
|
||||
"owner_authorization_reference": "chat-owner-authorization:v2r1c6:20260806",
|
||||
"one_time_use": true,
|
||||
"expires_in_seconds": 900,
|
||||
"production_change_authorized": false,
|
||||
"tag_change_authorized": false,
|
||||
"baseline_change_authorized": false,
|
||||
"main_write_authorized": false,
|
||||
"allowed_operations": [
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch"
|
||||
],
|
||||
"denied_operations": [
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This request must be executed by the localhost-only bootstrap instance before
|
||||
the bridge code advances `main`. The endpoint must never infer “current main”.
|
||||
|
||||
If the Agent chooses the alternative merge-first path, it must stop after the
|
||||
merge and obtain an exact Owner amendment for the new merge OID. It must not
|
||||
silently replace the approved OID.
|
||||
|
||||
## Success response
|
||||
|
||||
Require:
|
||||
|
||||
```yaml
|
||||
status: READY_FOR_CODE_AUTHORING
|
||||
grant_consumed: true
|
||||
branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
branch_oid: <exact amended authorized base OID>
|
||||
base_oid: <same exact OID>
|
||||
main_unchanged: true
|
||||
|
||||
writer_scope:
|
||||
workspace_role: task-scoped-isolated-writer
|
||||
writes_enabled: true
|
||||
allowed_branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
allowed_base_oid: <exact amended authorized base OID>
|
||||
allowed_routes:
|
||||
- POST /api/commit-file
|
||||
allow_main_write: false
|
||||
allow_tag_write: false
|
||||
allow_baseline_write: false
|
||||
allow_force_push: false
|
||||
```
|
||||
|
||||
All seven negative policy results must be true.
|
||||
|
||||
Submit the identical request a second time. It must return the same grant and
|
||||
branch attestation with:
|
||||
|
||||
```yaml
|
||||
idempotent: true
|
||||
```
|
||||
|
||||
It must not run the provisioner again.
|
||||
|
||||
## Production postchecks
|
||||
|
||||
After API deployment and writer provisioning, independently verify:
|
||||
|
||||
```yaml
|
||||
existing_routes_regression: PASS
|
||||
new_endpoint: PASS
|
||||
main_ref: expected reviewed merge commit
|
||||
V2R1C5_tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
|
||||
V2R1C5_peeled_target: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
V2R1C5_baseline_unchanged: true
|
||||
new_release_tag_created: false
|
||||
production_baseline_changed: false
|
||||
```
|
||||
|
||||
The deployment of this control-plane bridge does not authorize a V2R1C6
|
||||
production release. It only enables scoped code authoring.
|
||||
|
||||
## Rollback
|
||||
|
||||
If the new API route fails:
|
||||
|
||||
1. Stop accepting new provisioning calls.
|
||||
2. Restore the previous API entrypoint.
|
||||
3. Restart and verify all existing routes.
|
||||
4. Preserve receipts and logs.
|
||||
5. Revoke any task writer capability already minted.
|
||||
6. Do not delete a branch or rewrite a tag.
|
||||
7. Revert the protected-main bridge commit through an ordinary reviewed revert
|
||||
PR if source rollback is required.
|
||||
|
||||
## Required final Agent report
|
||||
|
||||
```yaml
|
||||
STATUS: COMPLETED | PARTIALLY_COMPLETED | BLOCKED | FAILED
|
||||
|
||||
INTEGRATION:
|
||||
branch:
|
||||
commit:
|
||||
protected_pr:
|
||||
merge_commit:
|
||||
files_changed:
|
||||
|
||||
TESTS:
|
||||
unit:
|
||||
smoke:
|
||||
py_compile:
|
||||
regression:
|
||||
fault_injection:
|
||||
|
||||
DEPLOYMENT:
|
||||
service:
|
||||
entrypoint:
|
||||
runtime_status:
|
||||
endpoint_status:
|
||||
rollback_verified:
|
||||
|
||||
PROVISIONING:
|
||||
owner_authorization_reference:
|
||||
approval_plan_id:
|
||||
grant_id:
|
||||
grant_digest_sha256:
|
||||
signing_key_identity:
|
||||
branch:
|
||||
branch_oid:
|
||||
workspace_path:
|
||||
grant_consumed:
|
||||
idempotent_replay:
|
||||
|
||||
CONSISTENCY:
|
||||
main_oid:
|
||||
V2R1C5_tag_oid:
|
||||
V2R1C5_peeled_target_oid:
|
||||
baseline_version:
|
||||
baseline_changed:
|
||||
tags_changed:
|
||||
|
||||
SECRETS:
|
||||
runtime_secret_reference_used:
|
||||
plaintext_secret_logged:
|
||||
plaintext_secret_committed:
|
||||
|
||||
BLOCKERS:
|
||||
|
||||
GOVERNANCE:
|
||||
force_push_used: false
|
||||
tag_rewritten: false
|
||||
direct_main_push: false
|
||||
secrets_committed: false
|
||||
owner_approval_respected: true
|
||||
```
|
||||
@@ -1,401 +0,0 @@
|
||||
# Agent Deployment Requirements: Owner Authorization Broker V2
|
||||
|
||||
## Objective
|
||||
|
||||
Deploy the audited Owner Authorization Broker that closes the bootstrap gap
|
||||
without issuing a new general-purpose Owner credential and without modifying
|
||||
`/data/aios-gitops/runtime/secrets/gitops.tokens`.
|
||||
|
||||
The broker reuses one existing, non-wildcard Owner credential only to
|
||||
authenticate the Owner identity through one pre-existing exact scope. It then
|
||||
records an immutable task authorization event and directly invokes the
|
||||
existing signed grant -> validator -> one-shot provisioner chain.
|
||||
|
||||
Production deployment of V2R1C6 is **not** authorized by this task.
|
||||
|
||||
## Fixed bindings
|
||||
|
||||
```yaml
|
||||
task_id: V2R1C6_TASK_SCOPED_WRITER_PROVISIONING
|
||||
repository: aios/aios
|
||||
base_ref: refs/heads/main
|
||||
base_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
target_branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
production_change_authorized: false
|
||||
tag_change_authorized: false
|
||||
baseline_change_authorized: false
|
||||
main_write_authorized: false
|
||||
```
|
||||
|
||||
The existing V2R1C5 objects are frozen:
|
||||
|
||||
```yaml
|
||||
tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
|
||||
peeled_target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
baseline_version: V2R1C5
|
||||
baseline_status: PRODUCTION_DEPLOYED
|
||||
```
|
||||
|
||||
## 1. Package verification
|
||||
|
||||
Verify `MANIFEST.sha256` before installation. Reject any mismatch.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 -m unittest -v \
|
||||
tests/test_authorized_task_writer_provisioning.py \
|
||||
tests/test_owner_authorization_broker.py
|
||||
|
||||
python3 scripts/smoke_authorized_task_writer_provisioning.py
|
||||
python3 scripts/smoke_owner_authorization_broker.py
|
||||
python3 -m py_compile \
|
||||
active-runtime/app/authorized_task_writer_provisioning.py \
|
||||
active-runtime/app/aios_authorized_task_writer_api.py \
|
||||
active-runtime/app/owner_authorization_broker.py \
|
||||
active-runtime/app/aios_owner_authorization_broker_api.py
|
||||
```
|
||||
|
||||
All checks must pass.
|
||||
|
||||
## 2. Existing Owner credential selection
|
||||
|
||||
Read only the non-secret registry metadata. Select one existing credential
|
||||
that satisfies all of the following:
|
||||
|
||||
```yaml
|
||||
role: owner
|
||||
wildcard_scope: false
|
||||
account: exact known Owner identity
|
||||
contains_exact_intake_scope: true
|
||||
```
|
||||
|
||||
Use one existing exact scope such as `commit:create` or `file:write`. Prefer
|
||||
the narrowest scope already assigned to the intended Owner.
|
||||
|
||||
Do not:
|
||||
|
||||
- use the historical wildcard Owner credential;
|
||||
- add `writer:provision` to any bearer token;
|
||||
- create or modify a bearer credential;
|
||||
- print, copy or commit bearer-token material.
|
||||
|
||||
Record only:
|
||||
|
||||
```yaml
|
||||
owner_account: <non-secret account>
|
||||
credential_id: <non-secret ID, when available>
|
||||
intake_scope: <one exact existing scope>
|
||||
scope_readback_available: true|false
|
||||
```
|
||||
|
||||
If `handler.need()` returns the exact scope set, configure the Owner account
|
||||
allowlist and leave the credential ID allowlist empty.
|
||||
|
||||
If scope metadata is not returned, configure the exact non-secret credential
|
||||
ID allowlist. If neither exact scope metadata nor a stable non-secret
|
||||
credential ID can be proven, stop:
|
||||
|
||||
```yaml
|
||||
STATUS: BLOCKED
|
||||
REASON: OWNER_CREDENTIAL_SCOPE_NOT_PROVABLE
|
||||
```
|
||||
|
||||
## 3. Staging installation
|
||||
|
||||
Install the four application modules in a root-owned staging directory:
|
||||
|
||||
```text
|
||||
authorized_task_writer_provisioning.py
|
||||
aios_authorized_task_writer_api.py
|
||||
owner_authorization_broker.py
|
||||
aios_owner_authorization_broker_api.py
|
||||
```
|
||||
|
||||
The directory and files must not be group/world writable.
|
||||
|
||||
Prepare environment values:
|
||||
|
||||
```yaml
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED: "true"
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES: <one exact existing scope>
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_IDENTITIES: <exact owner account>
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_CREDENTIAL_IDS: <exact ID or empty>
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_MAX_EXPIRY_SECONDS: "900"
|
||||
AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT:
|
||||
/var/lib/aios-gitops/owner-authorization-broker
|
||||
```
|
||||
|
||||
Use the existing signer, validator and provisioner paths. Do not change their
|
||||
root governance configuration or disable signature checks.
|
||||
|
||||
Start a localhost-only staging instance on a temporary port. Do not replace
|
||||
the production service yet.
|
||||
|
||||
## 4. Authentication fail-closed tests
|
||||
|
||||
The staging endpoint is:
|
||||
|
||||
```text
|
||||
POST /api/governance/owner-authorizations
|
||||
```
|
||||
|
||||
Required tests:
|
||||
|
||||
```yaml
|
||||
unauthenticated:
|
||||
expected: 401
|
||||
|
||||
wrong_role:
|
||||
expected: 403
|
||||
|
||||
wrong_scope:
|
||||
expected: 403
|
||||
|
||||
wildcard_owner:
|
||||
expected: 403
|
||||
error: WILDCARD_OWNER_CREDENTIAL_REJECTED
|
||||
|
||||
unallowlisted_owner:
|
||||
expected: 403
|
||||
error: OWNER_IDENTITY_NOT_ALLOWED
|
||||
|
||||
production_capability_true:
|
||||
expected: 403
|
||||
error: PRODUCTION_CAPABILITY_REJECTED
|
||||
```
|
||||
|
||||
A connection failure, 404 or 500 is not an authorization-policy pass.
|
||||
|
||||
## 5. Formal provisioning request
|
||||
|
||||
Use the selected existing non-wildcard Owner credential through its runtime
|
||||
secret reference. Never place the bearer token in the request body, command
|
||||
line, shell history, output report or Git.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "AUTHORIZE_TASK_WRITER_PROVISIONING",
|
||||
"task_id": "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING",
|
||||
"repository": "aios/aios",
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"target_branch": "agent/v2r1c6-mainline-reconciliation-20260805",
|
||||
"one_time_use": true,
|
||||
"expires_in_seconds": 900,
|
||||
"production_change_authorized": false,
|
||||
"tag_change_authorized": false,
|
||||
"baseline_change_authorized": false,
|
||||
"main_write_authorized": false,
|
||||
"allowed_operations": [
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch"
|
||||
],
|
||||
"denied_operations": [
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The Owner authorization reference is generated by the broker. The caller must
|
||||
not submit or invent one.
|
||||
|
||||
Expected result:
|
||||
|
||||
```yaml
|
||||
status: READY_FOR_CODE_AUTHORING
|
||||
owner_authorization_event_id: generated
|
||||
owner_authorization_reference: generated
|
||||
owner_authorization_digest_sha256: generated
|
||||
owner_authorization_consumed: true
|
||||
approval_plan_id: generated
|
||||
grant_digest_sha256: generated
|
||||
grant_consumed: true
|
||||
|
||||
branch_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
|
||||
writer_scope:
|
||||
workspace_role: task-scoped-isolated-writer
|
||||
writes_enabled: true
|
||||
allowed_branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
allowed_base_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
allow_main_write: false
|
||||
allow_tag_write: false
|
||||
allow_baseline_write: false
|
||||
allow_force_push: false
|
||||
```
|
||||
|
||||
## 6. Independent post-provisioning verification
|
||||
|
||||
Verify the Authority ref:
|
||||
|
||||
```bash
|
||||
git --git-dir=/data/aios-gitops/repo.git rev-parse \
|
||||
refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
|
||||
```
|
||||
|
||||
It must equal:
|
||||
|
||||
```text
|
||||
875d41c3101093149414292067fd700dc214d4d3
|
||||
```
|
||||
|
||||
Verify `main` remains unchanged, V2R1C5 tag object and peeled target remain
|
||||
unchanged, and the Production Baseline remains V2R1C5.
|
||||
|
||||
Verify the broker audit event under:
|
||||
|
||||
```text
|
||||
/var/lib/aios-gitops/owner-authorization-broker/events/
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
```yaml
|
||||
status: CONSUMED
|
||||
wildcard_scope_used: false
|
||||
grant_consumed: true
|
||||
permissions:
|
||||
directory: "0700"
|
||||
json_files: "0600"
|
||||
```
|
||||
|
||||
Do not return bearer-token material in the report.
|
||||
|
||||
## 7. Commit the broker through the new writer
|
||||
|
||||
After `READY_FOR_CODE_AUTHORING`, use only the writer's
|
||||
`POST /api/commit-file` route to put this code bundle on:
|
||||
|
||||
```text
|
||||
agent/v2r1c6-mainline-reconciliation-20260805
|
||||
```
|
||||
|
||||
Commit the bridge and broker as separate intentional commits when practical:
|
||||
|
||||
1. Owner-authorized task-writer bridge.
|
||||
2. Audited Owner authorization broker and deployment docs.
|
||||
|
||||
Do not merge to `main` directly. Create a protected PR after branch commits
|
||||
and tests are complete.
|
||||
|
||||
## 8. Permanent service deployment
|
||||
|
||||
Only after protected review and merge:
|
||||
|
||||
1. Install the reviewed files from the merge commit.
|
||||
2. Apply a reviewed systemd drop-in based on the provided example.
|
||||
3. Start the wrapper entrypoint:
|
||||
`aios_owner_authorization_broker_api.py`.
|
||||
4. Verify all existing API routes still work.
|
||||
5. Verify the broker remains localhost/reverse-proxy constrained as intended.
|
||||
6. Re-run authentication and policy tests.
|
||||
7. Remove the temporary staging instance.
|
||||
|
||||
This permanent bridge deployment does not authorize a V2R1C6 production tag,
|
||||
Baseline update or application deployment.
|
||||
|
||||
## 9. Fail-closed conditions
|
||||
|
||||
Stop with `BLOCKED` for any of the following:
|
||||
|
||||
- Authority `main` no longer equals the fixed base OID.
|
||||
- Target branch exists at a different OID.
|
||||
- Only a wildcard Owner credential can authenticate.
|
||||
- Exact credential scope cannot be proven.
|
||||
- Owner identity is not explicitly allow-listed.
|
||||
- Signer, validator or provisioner self-test fails.
|
||||
- Grant is not marked one-time and consumed.
|
||||
- Writer allows main, tag, Baseline, force-push or branch-delete operations.
|
||||
- Audit event contains sensitive material.
|
||||
- V2R1C5 tag or Baseline changes.
|
||||
|
||||
## 10. Required Agent result
|
||||
|
||||
```yaml
|
||||
STATUS: READY_FOR_CODE_AUTHORING | BLOCKED | FAILED
|
||||
|
||||
PACKAGE:
|
||||
manifest_verified:
|
||||
unit_tests:
|
||||
smoke_tests:
|
||||
py_compile:
|
||||
|
||||
OWNER_CREDENTIAL:
|
||||
account:
|
||||
credential_id:
|
||||
intake_scope:
|
||||
exact_scope_proven:
|
||||
wildcard_used: false
|
||||
token_logged: false
|
||||
|
||||
BROKER:
|
||||
staging_listener:
|
||||
endpoint_status:
|
||||
event_id:
|
||||
authorization_reference:
|
||||
authorization_digest_sha256:
|
||||
event_consumed:
|
||||
audit_permissions_valid:
|
||||
|
||||
GRANT:
|
||||
approval_plan_id:
|
||||
grant_digest_sha256:
|
||||
signer_identity:
|
||||
signature_valid:
|
||||
one_time_use:
|
||||
consumed:
|
||||
|
||||
BRANCH:
|
||||
name:
|
||||
oid:
|
||||
base_oid:
|
||||
ref_exists:
|
||||
worktree_path:
|
||||
worktree_clean:
|
||||
|
||||
WRITER:
|
||||
status:
|
||||
writes_enabled:
|
||||
allowed_branch:
|
||||
allowed_base_oid:
|
||||
allowed_routes:
|
||||
main_write_denied:
|
||||
tag_write_denied:
|
||||
baseline_write_denied:
|
||||
force_push_denied:
|
||||
|
||||
POSTCHECK:
|
||||
main_unchanged:
|
||||
V2R1C5_tag_unchanged:
|
||||
baseline_unchanged:
|
||||
|
||||
REPOSITORY_INTEGRATION:
|
||||
commit_oids:
|
||||
protected_pr:
|
||||
merged:
|
||||
permanent_bridge_deployed:
|
||||
|
||||
BLOCKERS:
|
||||
|
||||
GOVERNANCE:
|
||||
force_push_used: false
|
||||
tag_rewritten: false
|
||||
direct_main_push: false
|
||||
secrets_committed: false
|
||||
owner_approval_respected: true
|
||||
```
|
||||
@@ -1,562 +0,0 @@
|
||||
# Agent execution requirements: V2R1C6 task identity overlay
|
||||
|
||||
## Required result
|
||||
|
||||
The Agent must return one of:
|
||||
|
||||
- `READY_FOR_CODE_AUTHORING`
|
||||
- `BLOCKED`
|
||||
- `FAILED`
|
||||
|
||||
The Agent may not report success until the task branch, isolated worktree and
|
||||
task writer are independently verified.
|
||||
|
||||
This stage is **not** a production deployment. It must not modify `main`,
|
||||
V2R1C5, tags, Registry, Production Baseline or production services.
|
||||
|
||||
## Fixed authority
|
||||
|
||||
```yaml
|
||||
task_id: V2R1C6_TASK_SCOPED_WRITER_PROVISIONING
|
||||
repository: aios/aios
|
||||
base_ref: refs/heads/main
|
||||
base_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
target_branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
owner_identity: aiman
|
||||
one_time_use: true
|
||||
maximum_ttl_seconds: 900
|
||||
production_change_authorized: false
|
||||
tag_change_authorized: false
|
||||
baseline_change_authorized: false
|
||||
main_write_authorized: false
|
||||
```
|
||||
|
||||
## 1. Verify package
|
||||
|
||||
Verify the externally supplied package against the published SHA-256 values and
|
||||
then verify every entry in `MANIFEST.sha256`.
|
||||
|
||||
Do not synthesize a missing archive. Use either the verified ZIP or TAR.GZ.
|
||||
|
||||
Required checks:
|
||||
|
||||
```yaml
|
||||
package_hash_match: true
|
||||
manifest_entries_match: true
|
||||
unit_tests_pass: true
|
||||
smoke_tests_pass: true
|
||||
py_compile_pass: true
|
||||
```
|
||||
|
||||
## 2. Create root-owned staging
|
||||
|
||||
Extract to a fresh directory such as:
|
||||
|
||||
```text
|
||||
/var/lib/aios-gitops/staging/v2r1c6-task-identity-overlay-<nonce>
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
```yaml
|
||||
owner: root
|
||||
group: root
|
||||
directory_mode: "0700"
|
||||
files_not_world_writable: true
|
||||
symlinks_rejected: true
|
||||
```
|
||||
|
||||
Do not overwrite the active runtime tree.
|
||||
|
||||
## 3. Re-run target-host tests
|
||||
|
||||
From the staging root:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
python3 scripts/smoke_authorized_task_writer_provisioning.py
|
||||
python3 scripts/smoke_owner_authorization_broker.py
|
||||
python3 scripts/smoke_task_identity_overlay.py
|
||||
python3 -m py_compile \
|
||||
active-runtime/app/authorized_task_writer_provisioning.py \
|
||||
active-runtime/app/owner_authorization_broker.py \
|
||||
active-runtime/app/task_identity_overlay.py \
|
||||
active-runtime/app/aios_authorized_task_writer_api.py \
|
||||
active-runtime/app/aios_owner_authorization_broker_api.py \
|
||||
active-runtime/app/aios_task_identity_overlay_api.py \
|
||||
active-runtime/app/aios_task_identity_overlay_cli.py \
|
||||
scripts/call_task_identity_overlay.py
|
||||
```
|
||||
|
||||
Any failure is `BLOCKED`; do not proceed.
|
||||
|
||||
## 4. Prestart Git and production freeze checks
|
||||
|
||||
Read from the authority repository:
|
||||
|
||||
```bash
|
||||
git --git-dir=/data/aios-gitops/repo.git rev-parse refs/heads/main
|
||||
git --git-dir=/data/aios-gitops/repo.git \
|
||||
show-ref --verify --quiet \
|
||||
refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
|
||||
git --git-dir=/data/aios-gitops/repo.git rev-parse refs/tags/V2R1C5
|
||||
git --git-dir=/data/aios-gitops/repo.git rev-parse 'refs/tags/V2R1C5^{}'
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```yaml
|
||||
main_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
target_branch_exists: false
|
||||
V2R1C5_tag_oid: 9e65bd59baea392294c0f253046d3ab116f63145
|
||||
V2R1C5_peeled_target: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
```
|
||||
|
||||
Read Baseline and require:
|
||||
|
||||
```yaml
|
||||
version: V2R1C5
|
||||
status: PRODUCTION_DEPLOYED
|
||||
target_oid: aa60e891547abd7e215af7cf2bf3f21fd23541da
|
||||
evidence_closure_oid: 5e2cf8276f5093f3352b9a3252c701a44c5ab807
|
||||
```
|
||||
|
||||
If `main` has drifted, return:
|
||||
|
||||
```yaml
|
||||
STATUS: MANUAL_OWNER_REVIEW_REQUIRED
|
||||
REASON: AUTHORIZED_BASE_OID_DRIFT
|
||||
```
|
||||
|
||||
If the target branch already exists at another OID, return:
|
||||
|
||||
```yaml
|
||||
STATUS: MANUAL_OWNER_REVIEW_REQUIRED
|
||||
REASON: EXISTING_BRANCH_OID_CONFLICT
|
||||
```
|
||||
|
||||
Do not delete, reset or force-push the branch.
|
||||
|
||||
## 5. Discover active secret references
|
||||
|
||||
The overlay does not read or modify `gitops.tokens`, but its issuer must compare
|
||||
the new random secret against all active bearer secret references.
|
||||
|
||||
Discover references without printing secret contents. At minimum include the
|
||||
currently active ChatGPT/GitOps bearer secret reference and any other bearer
|
||||
files consumed by the active API.
|
||||
|
||||
Pass them as a comma-separated list in:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS
|
||||
```
|
||||
|
||||
Evidence must contain only paths and SHA-256 match counts, not token values.
|
||||
|
||||
If active secret references cannot be enumerated, return:
|
||||
|
||||
```yaml
|
||||
STATUS: BLOCKED
|
||||
REASON: ACTIVE_SECRET_REFERENCE_SET_UNRESOLVED
|
||||
```
|
||||
|
||||
## 6. Install the transient systemd service
|
||||
|
||||
Use the provided template:
|
||||
|
||||
```text
|
||||
deploy/systemd/aios-v2r1c6-task-identity-overlay.service.example
|
||||
```
|
||||
|
||||
Substitute only:
|
||||
|
||||
- verified root-owned staging directory;
|
||||
- verified active secret-ref path list.
|
||||
|
||||
Do not change task ID, repository, base OID, target branch or denied
|
||||
capabilities.
|
||||
|
||||
The service must listen only on:
|
||||
|
||||
```text
|
||||
127.0.0.1:19864
|
||||
```
|
||||
|
||||
Before start, run:
|
||||
|
||||
```bash
|
||||
systemd-analyze verify \
|
||||
/etc/systemd/system/aios-v2r1c6-task-identity-overlay.service
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start aios-v2r1c6-task-identity-overlay.service
|
||||
```
|
||||
|
||||
Confirm:
|
||||
|
||||
```bash
|
||||
curl --fail --silent http://127.0.0.1:19864/healthz
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```yaml
|
||||
service: aios-task-identity-overlay
|
||||
status: active
|
||||
global_token_registry_used: false
|
||||
loopback_only: true
|
||||
```
|
||||
|
||||
Do not expose the port externally.
|
||||
|
||||
## 7. Issue the one-time overlay
|
||||
|
||||
Use the root-only CLI. The authorization assertion is an audit label for this
|
||||
already approved Owner task; it must not be described as an external signed
|
||||
event.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
sudo env \
|
||||
PYTHONPATH=<staging>/active-runtime/app \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_STATE_ROOT=/var/lib/aios-gitops/task-identity-overlays \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_SECRET_ROOT=/run/aios-gitops/task-identity-overlays \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_TASK_ID=V2R1C6_TASK_SCOPED_WRITER_PROVISIONING \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_OWNER_IDENTITY=aiman \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_BASE_REF=refs/heads/main \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_BASE_OID=875d41c3101093149414292067fd700dc214d4d3 \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_TARGET_BRANCH=agent/v2r1c6-mainline-reconciliation-20260805 \
|
||||
AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS=<verified-secret-ref-list> \
|
||||
python3 <staging>/active-runtime/app/aios_task_identity_overlay_cli.py \
|
||||
issue \
|
||||
--ttl-seconds 900 \
|
||||
--authorization-assertion-ref owner-session:V2R1C6:approved-20260806 \
|
||||
--operator-identity <audited-agent-identity> \
|
||||
--result-json /run/aios-gitops/v2r1c6-overlay-issued.json
|
||||
```
|
||||
|
||||
The result must contain:
|
||||
|
||||
```yaml
|
||||
status: OVERLAY_ACTIVE
|
||||
idempotent: false
|
||||
overlay_id: present
|
||||
secret_ref: under /run/aios-gitops/task-identity-overlays
|
||||
expires_at: within 900 seconds
|
||||
```
|
||||
|
||||
It must not contain a token.
|
||||
|
||||
Verify:
|
||||
|
||||
```yaml
|
||||
secret_file_mode: "0600"
|
||||
metadata_contains_raw_token: false
|
||||
token_hash_unique_across_configured_secret_refs: true
|
||||
```
|
||||
|
||||
## 8. Negative tests
|
||||
|
||||
Before the valid request:
|
||||
|
||||
1. No Authorization header must return HTTP 401.
|
||||
2. A random wrong bearer must return HTTP 401.
|
||||
3. A request with `production_change_authorized=true` must be rejected.
|
||||
4. A request with another base OID or branch must be rejected.
|
||||
5. The valid overlay must remain `ACTIVE` after pre-auth request-validation
|
||||
failures.
|
||||
|
||||
Do not use the global Owner or ChatGPT bearer credential for this endpoint.
|
||||
|
||||
## 9. Send the valid request
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
deploy/v2r1c6-task-writer-authorization-request.json
|
||||
```
|
||||
|
||||
Use the provided client so the bearer is not placed in argv, shell history or
|
||||
logs:
|
||||
|
||||
```bash
|
||||
sudo python3 <staging>/scripts/call_task_identity_overlay.py \
|
||||
--secret-ref <secret_ref_from_issue_result> \
|
||||
--request-json <staging>/deploy/v2r1c6-task-writer-authorization-request.json \
|
||||
--url http://127.0.0.1:19864/api/governance/owner-authorizations \
|
||||
--result-json /var/lib/aios-gitops/v2r1c6-overlay-provision-result.json
|
||||
```
|
||||
|
||||
Required result:
|
||||
|
||||
```yaml
|
||||
http_status: 200
|
||||
status: READY_FOR_CODE_AUTHORING
|
||||
owner_authorization_consumed: true
|
||||
grant_consumed: true
|
||||
identity_overlay_consumed: true
|
||||
identity_overlay_secret_removed: true
|
||||
branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
branch_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
```
|
||||
|
||||
The result must not include:
|
||||
|
||||
- bearer token;
|
||||
- private key;
|
||||
- signer secret;
|
||||
- raw signature;
|
||||
- credential value.
|
||||
|
||||
## 10. Independent acceptance
|
||||
|
||||
Verify the branch:
|
||||
|
||||
```bash
|
||||
git --git-dir=/data/aios-gitops/repo.git \
|
||||
rev-parse refs/heads/agent/v2r1c6-mainline-reconciliation-20260805
|
||||
```
|
||||
|
||||
It must equal:
|
||||
|
||||
```text
|
||||
875d41c3101093149414292067fd700dc214d4d3
|
||||
```
|
||||
|
||||
Verify worktree evidence returned by the provisioner:
|
||||
|
||||
```yaml
|
||||
branch: agent/v2r1c6-mainline-reconciliation-20260805
|
||||
head_oid: 875d41c3101093149414292067fd700dc214d4d3
|
||||
detached_head: false
|
||||
worktree_clean: true
|
||||
writer_status: active
|
||||
writes_enabled: true
|
||||
allowed_routes:
|
||||
- POST /api/commit-file
|
||||
allow_main_write: false
|
||||
allow_tag_write: false
|
||||
allow_baseline_write: false
|
||||
allow_force_push: false
|
||||
```
|
||||
|
||||
Run policy negative tests against the task writer and require explicit policy
|
||||
denials for:
|
||||
|
||||
- main write;
|
||||
- another branch;
|
||||
- tag create/update;
|
||||
- Baseline write;
|
||||
- force push;
|
||||
- branch delete.
|
||||
|
||||
Network errors, 404, 500 and stopped-service failures do not count as policy
|
||||
denials.
|
||||
|
||||
## 11. Stop and clean up the overlay
|
||||
|
||||
After `READY_FOR_CODE_AUTHORING`:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop aios-v2r1c6-task-identity-overlay.service
|
||||
sudo systemctl disable aios-v2r1c6-task-identity-overlay.service || true
|
||||
sudo rm -f /etc/systemd/system/aios-v2r1c6-task-identity-overlay.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```yaml
|
||||
overlay_listener_stopped: true
|
||||
overlay_secret_removed: true
|
||||
global_token_registry_unchanged: true
|
||||
disabled_owner_record_unchanged: true
|
||||
active_chatgpt_bot_record_unchanged: true
|
||||
audit_metadata_retained: true
|
||||
```
|
||||
|
||||
The root-owned staging directory may be retained until the code is committed,
|
||||
then removed after checksum-confirmed repository integration.
|
||||
|
||||
## 12. Commit the developed code through the task writer
|
||||
|
||||
Only after writer acceptance, use `POST /api/commit-file` to commit the
|
||||
verified package contents to:
|
||||
|
||||
```text
|
||||
agent/v2r1c6-mainline-reconciliation-20260805
|
||||
```
|
||||
|
||||
Commit in reviewable groups:
|
||||
|
||||
1. authorized task-writer bridge;
|
||||
2. Owner authorization broker;
|
||||
3. task identity overlay;
|
||||
4. tests, docs, OpenAPI and deployment templates.
|
||||
|
||||
Do not include:
|
||||
|
||||
- package archives;
|
||||
- runtime tokens;
|
||||
- `/run` or `/var/lib` state;
|
||||
- test result files containing host paths;
|
||||
- secret references containing values;
|
||||
- generated credentials.
|
||||
|
||||
Run the complete 40-test suite from the branch after commits.
|
||||
|
||||
## 13. Protected PR
|
||||
|
||||
Create a protected PR from:
|
||||
|
||||
```text
|
||||
agent/v2r1c6-mainline-reconciliation-20260805
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```text
|
||||
main
|
||||
```
|
||||
|
||||
The PR must state:
|
||||
|
||||
- this change closes Owner authorization bootstrap gaps;
|
||||
- the overlay is localhost-only and one-time;
|
||||
- the global token registry is not modified;
|
||||
- no production release is authorized;
|
||||
- V2R1C5 tag and Baseline remain unchanged.
|
||||
|
||||
The Agent must not merge without the repository's normal protected review.
|
||||
|
||||
## 14. Required Agent report
|
||||
|
||||
```yaml
|
||||
STATUS:
|
||||
READY_FOR_CODE_AUTHORING | BLOCKED | FAILED
|
||||
|
||||
PACKAGE:
|
||||
zip_sha256:
|
||||
tar_gz_sha256:
|
||||
manifest_entries:
|
||||
manifest_matched:
|
||||
|
||||
TESTS:
|
||||
unit_total:
|
||||
unit_passed:
|
||||
smoke_bridge:
|
||||
smoke_broker:
|
||||
smoke_overlay:
|
||||
py_compile:
|
||||
|
||||
PRESTART:
|
||||
main_oid:
|
||||
target_branch_previously_absent:
|
||||
V2R1C5_tag_oid:
|
||||
V2R1C5_peeled_target:
|
||||
baseline_unchanged:
|
||||
|
||||
OVERLAY:
|
||||
overlay_id:
|
||||
status:
|
||||
secret_ref:
|
||||
secret_file_mode:
|
||||
token_hash_unique:
|
||||
global_registry_used: false
|
||||
listener:
|
||||
expires_at:
|
||||
consumed:
|
||||
secret_removed:
|
||||
|
||||
NEGATIVE_TESTS:
|
||||
unauthenticated_401:
|
||||
wrong_token_401:
|
||||
production_capability_denied:
|
||||
wrong_base_denied:
|
||||
wrong_branch_denied:
|
||||
|
||||
OWNER_AUTHORIZATION:
|
||||
event_id:
|
||||
reference:
|
||||
consumed:
|
||||
|
||||
GRANT:
|
||||
approval_plan_id:
|
||||
grant_id:
|
||||
grant_digest_sha256:
|
||||
signature_valid:
|
||||
consumed:
|
||||
|
||||
BRANCH:
|
||||
name:
|
||||
oid:
|
||||
base_oid:
|
||||
ref_exists:
|
||||
worktree_path:
|
||||
worktree_clean:
|
||||
|
||||
WRITER:
|
||||
status:
|
||||
writes_enabled:
|
||||
allowed_branch:
|
||||
allowed_base_oid:
|
||||
allowed_routes:
|
||||
main_write_denied:
|
||||
tag_write_denied:
|
||||
baseline_write_denied:
|
||||
force_push_denied:
|
||||
|
||||
REPOSITORY_INTEGRATION:
|
||||
commits:
|
||||
tests_after_commit:
|
||||
protected_pr:
|
||||
merged: false
|
||||
|
||||
POSTCHECK:
|
||||
main_unchanged:
|
||||
V2R1C5_tag_unchanged:
|
||||
baseline_unchanged:
|
||||
global_token_registry_unchanged:
|
||||
overlay_listener_stopped:
|
||||
overlay_secret_removed:
|
||||
|
||||
SECRETS:
|
||||
plaintext_logged: false
|
||||
plaintext_committed: false
|
||||
|
||||
BLOCKERS:
|
||||
|
||||
GOVERNANCE:
|
||||
force_push_used: false
|
||||
tag_rewritten: false
|
||||
direct_main_push: false
|
||||
secrets_committed: false
|
||||
owner_approval_respected: true
|
||||
```
|
||||
|
||||
## Fail-closed conditions
|
||||
|
||||
Return `BLOCKED` and stop on any of:
|
||||
|
||||
- authoritative main OID drift;
|
||||
- target branch conflict;
|
||||
- inability to enumerate active secret references;
|
||||
- secret hash collision;
|
||||
- overlay not bound to the exact task/base/branch;
|
||||
- non-loopback listener;
|
||||
- raw secret in result or logs;
|
||||
- Owner Authorization Broker event not created;
|
||||
- signer/validator/provisioner failure;
|
||||
- grant not consumed;
|
||||
- branch OID mismatch;
|
||||
- writer exposes main/tag/Baseline/force-push permissions;
|
||||
- V2R1C5 tag or Baseline changes.
|
||||
|
||||
Never fall back to the shared `gitops-write-bot.token`, a wildcard credential,
|
||||
manual `gitops.tokens` edits or a direct Git ref write.
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"schema": "aios.v2r1c7.engineering-build-receipt.v1",
|
||||
"version": "V2R1C7",
|
||||
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"production_deployed": false,
|
||||
"tag_created": false,
|
||||
"baseline_changed": false,
|
||||
"deployable": true
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
bec7da1adb75bf5feaa63c60ec6d03adf15d7508742a486207033d000f72d4ff AGENT_DEPLOYMENT_REQUIREMENTS.md
|
||||
251b46e51ce09aa2fd843bee48ef24a5608f31d24c599841f086ceb25c1a1715 README.md
|
||||
783f68ec4f00a11d43ceed536196bfedccaf6614f6db9805f7b36d24e5fa7bc2 TEST_RESULTS.json
|
||||
9208325e5b2e6f5ba6b64e000bc21d14d9f2f214e3ad33dd79857e85218fd6c2 VERSION
|
||||
c4b24b65195077716983acdc8611f705324413a74453858994ba51541957afd4 active-runtime/app/aios_authorized_task_writer_api.py
|
||||
8a1935fa098d47ac02ce51bbd9fd5fad7cf9ab9374ea3de5f630679b96681d84 active-runtime/app/authorized_task_writer_provisioning.py
|
||||
0997ab8332985c7124f334c4e49735d8833326578acc61a61a389bf792424de3 active-runtime/app/deployment_state.py
|
||||
dd3f8b1cab88e005a291f077807873c381e8ca92fe863dc05817925f0dff9613 active-runtime/app/v2r1c6_privacy_tools.py
|
||||
9e45025c41068cafdea2f360edfc4146a5459b0a6bcfbf1403206fc0161e7bac active-runtime/app/v2r1c7_health_service.py
|
||||
c96ba560d69f6c2eb700b9f2b42a32e394dadd00dffb23bbefdb9fb3e1571c0d deploy/aios-v2/systemd/aios-v2r1c7-production.service
|
||||
a05d4e8038fdb866ce17d4fb8a54798a268a1e8d44ff2cbc559069fa6ab2a667 deploy/systemd/aios-gitops-model-api-authorized-writer-bridge.conf.example
|
||||
a60dac51802bff67c2cfff61acdeb0a3a05d74a4f6cb47e3f78295ceb5c6acd9 docs/gitops/authorized-task-writer-provisioning.md
|
||||
2690f76452bceddda90c728278c600bcebd533db24776e2966423c5a8655e4b5 docs/gitops/openapi-authorized-task-writer-fragment.yaml
|
||||
560e5d9af8a8e2ec60ec31cf8659a7982b93ea09b7adb486b738ea2fba460d70 release-manifests/V2R1C7.json
|
||||
81a423c7814f81cb888b60a538c153e549b2be9099c881c968d55ae84efddc57 scripts/build_v2r1c7_release_artifact.py
|
||||
64c78dabffb05c8851f87b2a27c43bd1f21728dca586397d03a6bc22906a906c scripts/probe_v2r1c7_health.py
|
||||
5419dc7de0f50908ad0ef1dac74172fce811bb7b45f314a6e1c1bb9ce618324d scripts/rollback_v2r1c7_release.sh
|
||||
54e2845d2c3c026323ad45515e082d661e3b461feb3bfefedf5adc52db2d7b78 scripts/smoke_authorized_task_writer_provisioning.py
|
||||
32a860e28d5afc105f413d878a13fb8600fa74874dbd3bc4c031ef3efa17896a scripts/verify_v2r1c7_release.py
|
||||
3cce2b744326fe12b89bcad3cd352c77738950964bf8519e72cb5e9c7a5dfbfc tests/test_authorized_task_writer_provisioning.py
|
||||
8f3153c625241f60b65109eeac045e8aed7661d95f3a10f0b32ba3e0e9ac0a85 versions/V2R1C7.json
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"git_branch_created": false,
|
||||
"git_commit_created": false,
|
||||
"production_mutation": false,
|
||||
"py_compile": {
|
||||
"files": [
|
||||
"active-runtime/app/authorized_task_writer_provisioning.py",
|
||||
"active-runtime/app/aios_authorized_task_writer_api.py"
|
||||
],
|
||||
"status": "PASS"
|
||||
},
|
||||
"schema": "aios.authorized-task-writer-bridge-test-report.v1",
|
||||
"secrets_included": false,
|
||||
"smoke": {
|
||||
"command": "python3 scripts/smoke_authorized_task_writer_provisioning.py",
|
||||
"status": "PASS"
|
||||
},
|
||||
"status": "PASS",
|
||||
"unit_tests": {
|
||||
"command": "python3 -m unittest -v tests/test_authorized_task_writer_provisioning.py",
|
||||
"errors": 0,
|
||||
"failures": 0,
|
||||
"total": 9
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
"""Fail-closed Agent Pull Runner primitives for AIOS autonomy v1.
|
||||
|
||||
The runner owns task discovery and reporting only. It never accepts shell
|
||||
strings: executors receive argv, an allowlisted cwd and an allowlisted env.
|
||||
Durable state is intentionally small so restart recovery and duplicate
|
||||
suppression can be verified without coupling the runner to one database.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Protocol, Sequence
|
||||
|
||||
|
||||
class RunnerError(RuntimeError):
|
||||
"""Raised when a runner contract would be violated."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RunnerConfig:
|
||||
worker_id: str
|
||||
capabilities: tuple[str, ...] = ()
|
||||
long_poll_seconds: int = 30
|
||||
fallback_seconds: int = 60
|
||||
jitter_seconds: int = 5
|
||||
max_backoff_seconds: int = 300
|
||||
executable_allowlist: tuple[str, ...] = ()
|
||||
working_directory_allowlist: tuple[str, ...] = ()
|
||||
environment_allowlist: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunnerState:
|
||||
inflight: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
terminal: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
|
||||
class RunnerTransport(Protocol):
|
||||
def register_worker(self, worker_id: str, capabilities: Sequence[str]) -> Mapping[str, Any]: ...
|
||||
def heartbeat(self, worker_id: str) -> Mapping[str, Any]: ...
|
||||
def long_poll(self, worker_id: str, timeout_seconds: int) -> Mapping[str, Any] | None: ...
|
||||
def poll(self, worker_id: str) -> Mapping[str, Any] | None: ...
|
||||
def claim(self, worker_id: str, task: Mapping[str, Any]) -> Mapping[str, Any]: ...
|
||||
def renew(self, worker_id: str, task_id: str) -> Mapping[str, Any]: ...
|
||||
def report_progress(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
|
||||
def report_result(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
|
||||
def report_evidence(self, worker_id: str, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
|
||||
|
||||
|
||||
class JsonStateStore:
|
||||
"""Atomic JSON state store for restart recovery."""
|
||||
|
||||
def __init__(self, path: str | os.PathLike[str]) -> None:
|
||||
self.path = Path(path)
|
||||
|
||||
def load(self) -> RunnerState:
|
||||
if not self.path.exists():
|
||||
return RunnerState()
|
||||
value = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise RunnerError("runner state must be an object")
|
||||
return RunnerState(
|
||||
inflight=dict(value.get("inflight", {})),
|
||||
terminal=dict(value.get("terminal", {})),
|
||||
)
|
||||
|
||||
def save(self, state: RunnerState) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, name = tempfile.mkstemp(prefix=self.path.name + ".", dir=self.path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump({"inflight": state.inflight, "terminal": state.terminal}, handle, sort_keys=True)
|
||||
handle.write("\n")
|
||||
os.chmod(name, 0o600)
|
||||
os.replace(name, self.path)
|
||||
finally:
|
||||
if os.path.exists(name):
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
class AgentPullRunner:
|
||||
"""Pull, claim, execute and report one task at a time."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: RunnerConfig,
|
||||
transport: RunnerTransport,
|
||||
state_store: JsonStateStore | None = None,
|
||||
*,
|
||||
random_fn: Callable[[], float] = random.random,
|
||||
) -> None:
|
||||
if not config.worker_id.strip():
|
||||
raise RunnerError("worker_id is required")
|
||||
self.config = config
|
||||
self.transport = transport
|
||||
self.state_store = state_store
|
||||
self.random_fn = random_fn
|
||||
self.state = state_store.load() if state_store else RunnerState()
|
||||
|
||||
def _save(self) -> None:
|
||||
if self.state_store:
|
||||
self.state_store.save(self.state)
|
||||
|
||||
def register(self) -> Mapping[str, Any]:
|
||||
return self.transport.register_worker(self.config.worker_id, self.config.capabilities)
|
||||
|
||||
def heartbeat(self) -> Mapping[str, Any]:
|
||||
return self.transport.heartbeat(self.config.worker_id)
|
||||
|
||||
def discovery_delay(self, failures: int) -> float:
|
||||
failures = max(0, int(failures))
|
||||
base = min(self.config.max_backoff_seconds, self.config.fallback_seconds * (2**failures))
|
||||
jitter = self.random_fn() * max(0, self.config.jitter_seconds)
|
||||
return min(self.config.max_backoff_seconds, base + jitter)
|
||||
|
||||
def discover_once(self) -> Mapping[str, Any] | None:
|
||||
try:
|
||||
task = self.transport.long_poll(self.config.worker_id, self.config.long_poll_seconds)
|
||||
except Exception:
|
||||
task = None
|
||||
if task is None:
|
||||
task = self.transport.poll(self.config.worker_id)
|
||||
if not task:
|
||||
return None
|
||||
task_id = str(task.get("task_id", ""))
|
||||
if not task_id:
|
||||
raise RunnerError("task_id is required")
|
||||
if task_id in self.state.terminal:
|
||||
return {"status": "duplicate_suppressed", "task_id": task_id}
|
||||
claimed = dict(self.transport.claim(self.config.worker_id, task))
|
||||
self.state.inflight[task_id] = claimed
|
||||
self._save()
|
||||
return claimed
|
||||
|
||||
def renew(self, task_id: str) -> Mapping[str, Any]:
|
||||
if task_id not in self.state.inflight:
|
||||
raise RunnerError("task is not inflight")
|
||||
return self.transport.renew(self.config.worker_id, task_id)
|
||||
|
||||
def report_progress(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if task_id not in self.state.inflight:
|
||||
raise RunnerError("task is not inflight")
|
||||
return self.transport.report_progress(self.config.worker_id, task_id, payload)
|
||||
|
||||
def report_evidence(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if task_id not in self.state.inflight:
|
||||
raise RunnerError("task is not inflight")
|
||||
return self.transport.report_evidence(self.config.worker_id, task_id, payload)
|
||||
|
||||
def report_result(self, task_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if task_id in self.state.terminal:
|
||||
return {"status": "duplicate_terminal_suppressed", "task_id": task_id}
|
||||
if task_id not in self.state.inflight:
|
||||
raise RunnerError("task is not inflight")
|
||||
result = dict(self.transport.report_result(self.config.worker_id, task_id, payload))
|
||||
self.state.terminal[task_id] = {"result": result, "reported_at": time.time()}
|
||||
self.state.inflight.pop(task_id, None)
|
||||
self._save()
|
||||
return result
|
||||
|
||||
def recover_after_restart(self) -> list[Mapping[str, Any]]:
|
||||
recovered: list[Mapping[str, Any]] = []
|
||||
for task_id in tuple(self.state.inflight):
|
||||
recovered.append(self.renew(task_id))
|
||||
return recovered
|
||||
|
||||
def validate_execution(self, argv: Sequence[str], cwd: str | os.PathLike[str], env: Mapping[str, str]) -> None:
|
||||
if not argv or any(not isinstance(item, str) or not item for item in argv):
|
||||
raise RunnerError("argv_only_execution_required")
|
||||
if any(ch in argv[0] for ch in (";", "|", "&", "`", "\n")):
|
||||
raise RunnerError("shell_syntax_denied")
|
||||
executable = shutil.which(argv[0])
|
||||
if not executable:
|
||||
raise RunnerError("executable_not_found")
|
||||
if self.config.executable_allowlist and not any(
|
||||
os.path.realpath(executable).startswith(os.path.realpath(prefix).rstrip(os.sep) + os.sep)
|
||||
or os.path.realpath(executable) == os.path.realpath(prefix)
|
||||
for prefix in self.config.executable_allowlist
|
||||
):
|
||||
raise RunnerError("executable_not_allowlisted")
|
||||
real_cwd = os.path.realpath(os.fspath(cwd))
|
||||
if self.config.working_directory_allowlist and not any(
|
||||
real_cwd == os.path.realpath(prefix)
|
||||
or real_cwd.startswith(os.path.realpath(prefix).rstrip(os.sep) + os.sep)
|
||||
for prefix in self.config.working_directory_allowlist
|
||||
):
|
||||
raise RunnerError("working_directory_not_allowlisted")
|
||||
if self.config.environment_allowlist and any(key not in self.config.environment_allowlist for key in env):
|
||||
raise RunnerError("environment_key_not_allowlisted")
|
||||
|
||||
def execute_argv(self, argv: Sequence[str], cwd: str | os.PathLike[str], env: Mapping[str, str]) -> Mapping[str, Any]:
|
||||
self.validate_execution(argv, cwd, env)
|
||||
completed = subprocess.run(list(argv), cwd=os.fspath(cwd), env=dict(env), shell=False, text=True, capture_output=True, check=False)
|
||||
return {"returncode": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Small operational CLI for the Agent Pull Runner contract."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from agent_pull_runner import AgentPullRunner, RunnerConfig
|
||||
|
||||
|
||||
class _NoopTransport:
|
||||
def register_worker(self, worker_id, capabilities): return {"status": "registered", "worker_id": worker_id, "capabilities": list(capabilities)}
|
||||
def heartbeat(self, worker_id): return {"status": "alive", "worker_id": worker_id}
|
||||
def long_poll(self, worker_id, timeout_seconds): return None
|
||||
def poll(self, worker_id): return None
|
||||
def claim(self, worker_id, task): return task
|
||||
def renew(self, worker_id, task_id): return {"status": "renewed", "task_id": task_id}
|
||||
def report_progress(self, worker_id, task_id, payload): return {"status": "progress_recorded", "task_id": task_id}
|
||||
def report_result(self, worker_id, task_id, payload): return {"status": "result_recorded", "task_id": task_id}
|
||||
def report_evidence(self, worker_id, task_id, payload): return {"status": "evidence_recorded", "task_id": task_id}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="AIOS Agent Pull Runner")
|
||||
parser.add_argument("--worker-id", default="self-test-worker")
|
||||
parser.add_argument("--self-test", action="store_true")
|
||||
args = parser.parse_args()
|
||||
runner = AgentPullRunner(RunnerConfig(worker_id=args.worker_id, fallback_seconds=60), _NoopTransport())
|
||||
if args.self_test:
|
||||
print(json.dumps({"status": "PASS", "registration": runner.register(), "fallback_seconds": runner.config.fallback_seconds}, sort_keys=True))
|
||||
return 0
|
||||
print(json.dumps({"status": "READY", "worker_id": args.worker_id}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,73 +0,0 @@
|
||||
"""HTTP adapter for Owner-authorized task writer provisioning.
|
||||
|
||||
Run this module instead of aios_chatgpt_actions_api.py. All existing routes are
|
||||
delegated unchanged; only POST /api/task-writer/provision-authorized is added.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import aios_chatgpt_actions_api as api
|
||||
from authorized_task_writer_provisioning import (
|
||||
AuthorizedTaskWriterProvisioner,
|
||||
OwnerContext,
|
||||
ProvisioningError,
|
||||
)
|
||||
|
||||
|
||||
_original_do_post = api.H.do_POST
|
||||
_ENDPOINT = "/api/task-writer/provision-authorized"
|
||||
|
||||
|
||||
def _owner_authorization(handler):
|
||||
scope = os.getenv(
|
||||
"AIOS_GITOPS_OWNER_PROVISION_SCOPE", "writer:provision"
|
||||
)
|
||||
auth = handler.need(scope)
|
||||
if not auth:
|
||||
return None
|
||||
if auth.get("role") != "owner":
|
||||
handler.sendj({"error": "OWNER_AUTHORIZATION_REQUIRED"}, 403)
|
||||
return None
|
||||
identity = str(auth.get("account") or "").strip()
|
||||
if not identity:
|
||||
handler.sendj({"error": "OWNER_IDENTITY_REQUIRED"}, 403)
|
||||
return None
|
||||
return auth
|
||||
|
||||
|
||||
def _authorized_do_post(self):
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != _ENDPOINT:
|
||||
return _original_do_post(self)
|
||||
auth = _owner_authorization(self)
|
||||
if not auth:
|
||||
return
|
||||
data = self.body()
|
||||
owner_reference = str(data.get("owner_authorization_reference") or "")
|
||||
try:
|
||||
result = AuthorizedTaskWriterProvisioner().provision(
|
||||
data,
|
||||
OwnerContext(
|
||||
identity=str(auth["account"]),
|
||||
role=str(auth["role"]),
|
||||
authorization_reference=owner_reference,
|
||||
),
|
||||
)
|
||||
return self.sendj(result)
|
||||
except ProvisioningError as exc:
|
||||
body = {"error": exc.code}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
return self.sendj(body, exc.status)
|
||||
except Exception:
|
||||
# Do not expose tool output, paths containing credentials, or traceback data.
|
||||
return self.sendj({"error": "AUTHORIZED_WRITER_BRIDGE_INTERNAL_ERROR"}, 500)
|
||||
|
||||
|
||||
api.H.do_POST = _authorized_do_post
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.main()
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Governed API facade for branch-bound repair leases.
|
||||
|
||||
The facade is transport-neutral so the permanent control plane can expose it
|
||||
through its existing authenticated loopback/API seam without creating a second
|
||||
Git writer or a global credential registry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
from branch_repair_lease import LeaseError, RepairLease, RepairLeaseManager, sanitized_lease_view
|
||||
|
||||
|
||||
class BranchRepairLeaseAPI:
|
||||
"""Validate operation envelopes and return sanitized lease metadata."""
|
||||
|
||||
def __init__(self, manager: RepairLeaseManager) -> None:
|
||||
self.manager = manager
|
||||
|
||||
@staticmethod
|
||||
def _required(request: Mapping[str, Any], name: str) -> Any:
|
||||
value = request.get(name)
|
||||
if value is None or value == "":
|
||||
raise LeaseError(f"missing required field: {name}")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _view(lease: RepairLease) -> dict[str, Any]:
|
||||
return {"status": "ok", "lease": sanitized_lease_view(lease)}
|
||||
|
||||
def handle(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
operation = self._required(request, "operation")
|
||||
owner = str(self._required(request, "owner_identity"))
|
||||
if operation == "issue":
|
||||
lease = self.manager.issue(
|
||||
task_id=str(self._required(request, "task_id")),
|
||||
owner_identity=owner,
|
||||
branch=str(self._required(request, "branch")),
|
||||
description=str(self._required(request, "description")),
|
||||
allowed_paths=tuple(self._required(request, "allowed_paths")),
|
||||
ttl_seconds=int(request.get("ttl_seconds", 3600)),
|
||||
)
|
||||
elif operation == "attach_existing_branch":
|
||||
lease = self.manager.attach_existing_branch(
|
||||
str(self._required(request, "lease_id")),
|
||||
owner_identity=owner,
|
||||
expected_head_oid=str(self._required(request, "expected_head_oid")),
|
||||
)
|
||||
elif operation == "authorize_commit":
|
||||
lease = self.manager.authorize_commit(
|
||||
str(self._required(request, "lease_id")),
|
||||
owner_identity=owner,
|
||||
expected_head_oid=str(self._required(request, "expected_head_oid")),
|
||||
paths=tuple(self._required(request, "paths")),
|
||||
content=request.get("content"),
|
||||
)
|
||||
elif operation == "record_commit":
|
||||
lease = self.manager.record_commit(
|
||||
str(self._required(request, "lease_id")),
|
||||
owner_identity=owner,
|
||||
expected_head_oid=str(self._required(request, "expected_head_oid")),
|
||||
new_head_oid=str(self._required(request, "new_head_oid")),
|
||||
paths=tuple(self._required(request, "paths")),
|
||||
)
|
||||
elif operation == "recover":
|
||||
lease = self.manager.recover(
|
||||
str(self._required(request, "lease_id")), owner_identity=owner
|
||||
)
|
||||
elif operation == "renew":
|
||||
lease = self.manager.renew(
|
||||
str(self._required(request, "lease_id")),
|
||||
owner_identity=owner,
|
||||
ttl_seconds=int(self._required(request, "ttl_seconds")),
|
||||
)
|
||||
elif operation == "complete":
|
||||
lease = self.manager.complete(
|
||||
str(self._required(request, "lease_id")), owner_identity=owner
|
||||
)
|
||||
elif operation == "revoke":
|
||||
lease = self.manager.revoke(
|
||||
str(self._required(request, "lease_id")), owner_identity=owner
|
||||
)
|
||||
else:
|
||||
raise LeaseError("unsupported lease operation")
|
||||
return self._view(lease)
|
||||
except (LeaseError, TypeError, ValueError):
|
||||
return {"status": "blocked", "error": "lease_request_rejected"}
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Root-only CLI for the branch-bound repair lease API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from aios_branch_repair_lease_api import BranchRepairLeaseAPI
|
||||
from branch_repair_lease import GitProbe, LeaseStore
|
||||
from branch_repair_lease import RepairLeaseManager
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="AIOS branch repair lease CLI")
|
||||
parser.add_argument("--store", required=True)
|
||||
parser.add_argument("--bare-repo", required=True)
|
||||
parser.add_argument("--request-file")
|
||||
parser.add_argument("--request-json")
|
||||
args = parser.parse_args()
|
||||
if hasattr(os, "geteuid") and os.geteuid() != 0:
|
||||
print(json.dumps({"status": "blocked", "error": "root_required"}))
|
||||
return 2
|
||||
if bool(args.request_file) == bool(args.request_json):
|
||||
print(json.dumps({"status": "blocked", "error": "one_request_source_required"}))
|
||||
return 2
|
||||
try:
|
||||
raw = (
|
||||
Path(args.request_file).read_text(encoding="utf-8")
|
||||
if args.request_file
|
||||
else str(args.request_json)
|
||||
)
|
||||
request = json.loads(raw)
|
||||
if not isinstance(request, dict):
|
||||
raise ValueError("request must be an object")
|
||||
api = BranchRepairLeaseAPI(RepairLeaseManager(LeaseStore(args.store), GitProbe(args.bare_repo)))
|
||||
response = api.handle(request)
|
||||
print(json.dumps(response, ensure_ascii=True, sort_keys=True, separators=(",", ":")))
|
||||
return 0 if response.get("status") == "ok" else 1
|
||||
except Exception:
|
||||
print(json.dumps({"status": "blocked", "error": "lease_cli_failed"}))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,69 +0,0 @@
|
||||
"""CLI client for the governed local writer service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Send one governed-local-writer request")
|
||||
parser.add_argument("--socket", required=True, help="AF_UNIX socket path")
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--request", help="Request JSON string")
|
||||
group.add_argument("--request-file", help="Path to JSON request file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _load_request_text(args: argparse.Namespace) -> str:
|
||||
if args.request is not None:
|
||||
return args.request
|
||||
request_path = os.fspath(args.request_file)
|
||||
with open(request_path, "r", encoding="utf-8") as request_file:
|
||||
return request_file.read()
|
||||
|
||||
|
||||
def send_request(socket_path: str, request_text: str) -> dict[str, object]:
|
||||
"""Open one Unix socket, send one NDJSON request, and parse one NDJSON response."""
|
||||
payload = json.loads(request_text)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("request must be a JSON object")
|
||||
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
||||
client.connect(socket_path)
|
||||
client.sendall((json.dumps(payload) + "\n").encode("utf-8"))
|
||||
response_chunks: list[str] = []
|
||||
while True:
|
||||
chunk = client.recv(1024 * 1024).decode("utf-8")
|
||||
if not chunk:
|
||||
break
|
||||
response_chunks.append(chunk)
|
||||
if "\n" in chunk:
|
||||
break
|
||||
response_text = "".join(response_chunks)
|
||||
|
||||
response_line = response_text.split("\n", 1)[0].strip()
|
||||
if not response_line:
|
||||
raise RuntimeError("empty response")
|
||||
response = json.loads(response_line)
|
||||
if not isinstance(response, dict):
|
||||
raise RuntimeError("invalid response")
|
||||
return response
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
request_text = _load_request_text(args)
|
||||
response = send_request(args.socket, request_text)
|
||||
print(json.dumps(response, separators=(",", ":"), sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # pragma: no cover - CLI entrypoint behavior
|
||||
print(json.dumps({"status": "error", "error": str(exc)}), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -1,931 +0,0 @@
|
||||
"""Unix-socket service for governed local writer operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Protocol
|
||||
|
||||
from governed_local_writer import (
|
||||
AppendCommitRequest,
|
||||
AuthorizationError,
|
||||
BranchPolicy,
|
||||
CreateBranchRequest,
|
||||
GovernedAuthorization,
|
||||
PeerIdentity,
|
||||
PRINCIPAL,
|
||||
TASK_ID,
|
||||
extract_peer_identity,
|
||||
contains_secret_like_content,
|
||||
load_authorization_file,
|
||||
validate_file_changes,
|
||||
)
|
||||
|
||||
|
||||
class ExistingGitOpsHandler:
|
||||
"""Delegate repository mutations to the installed GitOps handler seam."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
workspace_by_branch: Mapping[str, str | os.PathLike[str]],
|
||||
bare_repo: str | os.PathLike[str],
|
||||
api_source: str | os.PathLike[str],
|
||||
) -> None:
|
||||
self.workspace_by_branch = {str(k): Path(v) for k, v in workspace_by_branch.items()}
|
||||
self.bare_repo = Path(bare_repo)
|
||||
self.api_source = Path(api_source)
|
||||
|
||||
@staticmethod
|
||||
def _restore_env(previous: Mapping[str, str | None]) -> None:
|
||||
for key, value in previous.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
|
||||
def _load_api(self, workspace: Path):
|
||||
if not self.api_source.is_file() or self.api_source.is_symlink():
|
||||
raise RuntimeError("installed GitOps handler source is unavailable")
|
||||
keys = (
|
||||
"AIOS_GITOPS_WORKSPACE",
|
||||
"AIOS_GITOPS_BARE_REPO",
|
||||
"AIOS_GITOPS_WRITES_ENABLED",
|
||||
"PYTHONDONTWRITEBYTECODE",
|
||||
"GIT_CONFIG_COUNT",
|
||||
"GIT_CONFIG_KEY_0",
|
||||
"GIT_CONFIG_VALUE_0",
|
||||
"GIT_CONFIG_KEY_1",
|
||||
"GIT_CONFIG_VALUE_1",
|
||||
)
|
||||
previous = {key: os.environ.get(key) for key in keys}
|
||||
os.environ.update(
|
||||
{
|
||||
"AIOS_GITOPS_WORKSPACE": str(workspace),
|
||||
"AIOS_GITOPS_BARE_REPO": str(self.bare_repo),
|
||||
"AIOS_GITOPS_WRITES_ENABLED": "true",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"GIT_CONFIG_COUNT": "2",
|
||||
"GIT_CONFIG_KEY_0": "safe.directory",
|
||||
"GIT_CONFIG_VALUE_0": str(workspace),
|
||||
"GIT_CONFIG_KEY_1": "safe.directory",
|
||||
"GIT_CONFIG_VALUE_1": str(self.bare_repo),
|
||||
}
|
||||
)
|
||||
module_name = f"_governed_gitops_api_{uuid.uuid4().hex}"
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, self.api_source)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("unable to load installed GitOps handler")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
decision = module.evaluate_git_write(module.WS)
|
||||
if not decision.allowed:
|
||||
raise RuntimeError("existing GitOps write gate denied bootstrap")
|
||||
return module, previous, module_name
|
||||
except Exception:
|
||||
sys.modules.pop(module_name, None)
|
||||
self._restore_env(previous)
|
||||
raise
|
||||
|
||||
def _workspace(self, branch: str) -> Path:
|
||||
workspace = self.workspace_by_branch.get(branch)
|
||||
if workspace is None or not workspace.is_dir():
|
||||
raise RuntimeError("branch workspace is not authorized")
|
||||
return workspace
|
||||
|
||||
@staticmethod
|
||||
def _git(workspace: Path, args: list[str]) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args], cwd=workspace, text=True, capture_output=True, check=False
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError("governed handler git probe failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
def create_branch(self, request: CreateBranchRequest) -> Mapping[str, object]:
|
||||
workspace = self._workspace(request.branch)
|
||||
# A prior handler call may have completed the fast-forward sync before
|
||||
# the process observed a post-commit error. Reconcile only an exact,
|
||||
# metadata-only result; never overwrite or move an existing ref here.
|
||||
try:
|
||||
existing = self._git(
|
||||
workspace,
|
||||
[
|
||||
"--git-dir",
|
||||
str(self.bare_repo),
|
||||
"rev-parse",
|
||||
f"refs/heads/{request.branch}",
|
||||
],
|
||||
)
|
||||
except RuntimeError:
|
||||
existing = ""
|
||||
if existing:
|
||||
parent = self._git(workspace, ["--git-dir", str(self.bare_repo), "rev-parse", f"{existing}^1"])
|
||||
changed = self._git(
|
||||
workspace,
|
||||
[
|
||||
"--git-dir",
|
||||
str(self.bare_repo),
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
"-r",
|
||||
existing,
|
||||
],
|
||||
).splitlines()
|
||||
safe_branch = re.sub(r"[^A-Za-z0-9_.-]+", "-", request.branch).strip("-")
|
||||
if parent != request.base_oid or changed != [f"patches/{safe_branch}.branch.json"]:
|
||||
raise RuntimeError("existing branch is not an exact metadata-only bootstrap")
|
||||
return {"head_oid": existing, "parent_oid": parent, "handler_status": "reconciled"}
|
||||
module, previous, module_name = self._load_api(workspace)
|
||||
phase = "loaded"
|
||||
try:
|
||||
phase = "verify_base"
|
||||
parent = self._git(
|
||||
workspace,
|
||||
["--git-dir", str(self.bare_repo), "rev-parse", "refs/heads/main"],
|
||||
)
|
||||
if parent != request.base_oid:
|
||||
raise RuntimeError("authorized base OID drift")
|
||||
phase = "checkout"
|
||||
module.checkout(request.branch, base=request.base_oid, create=True)
|
||||
phase = "write_metadata"
|
||||
safe_branch = re.sub(r"[^A-Za-z0-9_.-]+", "-", request.branch).strip("-")
|
||||
metadata_path = f"patches/{safe_branch}.branch.json"
|
||||
module.write_json(
|
||||
metadata_path,
|
||||
{
|
||||
"schema": "aios.governed-local-writer-bootstrap.v1",
|
||||
"task_id": TASK_ID,
|
||||
"principal": PRINCIPAL,
|
||||
"branch": request.branch,
|
||||
"parent_oid": request.base_oid,
|
||||
"authorization_sha256": request.authorization_digest,
|
||||
},
|
||||
)
|
||||
phase = "commit_and_sync"
|
||||
result = module.commit(
|
||||
[metadata_path], "chore(governance): bootstrap repair lease branch"
|
||||
)
|
||||
head_oid = self._git(workspace, ["rev-parse", "HEAD"])
|
||||
first_parent = self._git(workspace, ["rev-parse", "HEAD^1"])
|
||||
if first_parent != request.base_oid or (request.head_oid and head_oid != request.head_oid):
|
||||
raise RuntimeError("bootstrap branch lineage check failed")
|
||||
return {
|
||||
"head_oid": head_oid,
|
||||
"parent_oid": first_parent,
|
||||
"handler_status": str(result.get("status")),
|
||||
}
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"handler_phase={phase};error={exc.__class__.__name__}") from exc
|
||||
finally:
|
||||
sys.modules.pop(module_name, None)
|
||||
self._restore_env(previous)
|
||||
|
||||
def append_commit(self, request: AppendCommitRequest) -> Mapping[str, object]:
|
||||
workspace = self._workspace(request.branch)
|
||||
module, previous, module_name = self._load_api(workspace)
|
||||
phase = "loaded"
|
||||
try:
|
||||
phase = "verify_head"
|
||||
current = self._git(workspace, ["rev-parse", "HEAD"])
|
||||
if current != request.expected_head_oid:
|
||||
raise RuntimeError("branch head advanced or is stale")
|
||||
phase = "checkout"
|
||||
module.checkout(request.branch, base=request.expected_head_oid, create=False)
|
||||
phase = "write_files"
|
||||
paths: list[str] = []
|
||||
for change in request.changes:
|
||||
paths.append(module.write_file(change.path, change.content))
|
||||
phase = "commit_and_sync"
|
||||
result = module.commit(paths, request.commit_message)
|
||||
head_oid = self._git(workspace, ["rev-parse", "HEAD"])
|
||||
first_parent = self._git(workspace, ["rev-parse", "HEAD^1"])
|
||||
if first_parent != request.expected_head_oid or head_oid == first_parent:
|
||||
raise RuntimeError("append commit lineage check failed")
|
||||
return {
|
||||
"head_oid": head_oid,
|
||||
"parent_oid": first_parent,
|
||||
"handler_status": str(result.get("status")),
|
||||
}
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"handler_phase={phase};error={exc.__class__.__name__}") from exc
|
||||
finally:
|
||||
sys.modules.pop(module_name, None)
|
||||
self._restore_env(previous)
|
||||
|
||||
|
||||
class GovernedWriterHandler(Protocol):
|
||||
"""Small adapter interface for real writer backend handlers."""
|
||||
|
||||
def create_branch(self, request: CreateBranchRequest) -> Mapping[str, object]:
|
||||
"""Create a branch using internal repo services."""
|
||||
|
||||
def append_commit(self, request: AppendCommitRequest) -> Mapping[str, object]:
|
||||
"""Append a commit using internal repo services."""
|
||||
|
||||
|
||||
class AuditSink(Protocol):
|
||||
"""Pluggable sink for sanitized audit records."""
|
||||
|
||||
def append(self, record: "AuditRecord") -> None:
|
||||
"""Persist one audit record."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditRecord:
|
||||
"""Sanitized audit log entry."""
|
||||
|
||||
authorization_digest: str
|
||||
peer_pid: int
|
||||
peer_uid: int
|
||||
peer_gid: int
|
||||
peer_executable: str | None
|
||||
timestamp: datetime
|
||||
operation: str
|
||||
branch: str
|
||||
result: str
|
||||
|
||||
|
||||
class InMemoryAuditSink:
|
||||
"""Simple in-memory audit log used by tests."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.records: list[AuditRecord] = []
|
||||
|
||||
def append(self, record: AuditRecord) -> None:
|
||||
self.records.append(record)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _AuthorizationState:
|
||||
"""Tracks one-shot operation consumption."""
|
||||
|
||||
create_done: set[str] = field(default_factory=set)
|
||||
append_done: set[str] = field(default_factory=set)
|
||||
consumed: bool = False
|
||||
|
||||
|
||||
class GovernedLocalWriterService:
|
||||
"""Policy-aware AF_UNIX service that calls an injected handler."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
socket_path: str | os.PathLike[str],
|
||||
authorization: GovernedAuthorization,
|
||||
handler: GovernedWriterHandler,
|
||||
*,
|
||||
audit_sink: AuditSink | None = None,
|
||||
peer_identity_fn: Callable[[socket.socket], PeerIdentity] | None = None,
|
||||
state_path: str | os.PathLike[str] | None = None,
|
||||
) -> None:
|
||||
self.socket_path = Path(socket_path)
|
||||
self.authorization = authorization
|
||||
self.handler = handler
|
||||
self.peer_identity_fn = peer_identity_fn or extract_peer_identity
|
||||
self.state = _AuthorizationState()
|
||||
self.audit_sink = audit_sink or InMemoryAuditSink()
|
||||
self.state_path = Path(state_path) if state_path is not None else None
|
||||
self._load_state()
|
||||
|
||||
def _load_state(self) -> None:
|
||||
"""Restore one-shot state without restoring request or secret material."""
|
||||
if self.state_path is None or not self.state_path.exists():
|
||||
return
|
||||
try:
|
||||
raw = json.loads(self.state_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("state must be an object")
|
||||
if raw.get("authorization_sha256") != self.authorization.authorization_sha256:
|
||||
raise ValueError("state authorization digest mismatch")
|
||||
self.state.create_done = {str(v) for v in raw.get("create_done", [])}
|
||||
self.state.append_done = {str(v) for v in raw.get("append_done", [])}
|
||||
self.state.consumed = bool(raw.get("consumed", False))
|
||||
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise AuthorizationError("authorization state is invalid") from exc
|
||||
|
||||
def _persist_state(self) -> None:
|
||||
if self.state_path is None:
|
||||
return
|
||||
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"authorization_sha256": self.authorization.authorization_sha256,
|
||||
"create_done": sorted(self.state.create_done),
|
||||
"append_done": sorted(self.state.append_done),
|
||||
"consumed": self.state.consumed,
|
||||
}
|
||||
fd, temp_name = tempfile.mkstemp(prefix=".writer-state-", dir=self.state_path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
||||
json.dump(payload, stream, sort_keys=True, separators=(",", ":"))
|
||||
stream.write("\n")
|
||||
os.replace(temp_name, self.state_path)
|
||||
os.chmod(self.state_path, 0o600)
|
||||
finally:
|
||||
if os.path.exists(temp_name):
|
||||
os.unlink(temp_name)
|
||||
|
||||
def _new_audit(self, peer: PeerIdentity, operation: str, branch: str, result: str) -> None:
|
||||
self.audit_sink.append(
|
||||
AuditRecord(
|
||||
authorization_digest=self.authorization.authorization_sha256,
|
||||
peer_pid=peer.pid,
|
||||
peer_uid=peer.uid,
|
||||
peer_gid=peer.gid,
|
||||
peer_executable=peer.executable,
|
||||
timestamp=datetime.now(tz=timezone.utc),
|
||||
operation=operation,
|
||||
branch=branch,
|
||||
result=result,
|
||||
)
|
||||
)
|
||||
|
||||
def _require_request_field(self, request: Mapping[str, Any], field_name: str) -> str:
|
||||
value = request.get(field_name)
|
||||
if not isinstance(value, str) or not value:
|
||||
raise AuthorizationError(f"{field_name} must be a non-empty string")
|
||||
return value
|
||||
|
||||
def _branch_policy(self, branch: str) -> BranchPolicy:
|
||||
policy = self.authorization.branch_operations.get(branch)
|
||||
if policy is None:
|
||||
raise AuthorizationError("branch is not authorized")
|
||||
return policy
|
||||
|
||||
def _ensure_not_consumed(self) -> None:
|
||||
if self.state.consumed:
|
||||
raise AuthorizationError("authorization consumed")
|
||||
if datetime.now(tz=timezone.utc) >= self.authorization.expires_at:
|
||||
raise AuthorizationError("authorization expired")
|
||||
|
||||
def _mark_success(self, op: str, branch: str) -> None:
|
||||
if op == "create_branch":
|
||||
self.state.create_done.add(branch)
|
||||
elif op == "append_commit":
|
||||
self.state.append_done.add(branch)
|
||||
|
||||
if self.state.create_done.issuperset(self.authorization.create_branches) and self.state.append_done.issuperset(
|
||||
self.authorization.append_branches
|
||||
):
|
||||
self.state.consumed = True
|
||||
self._persist_state()
|
||||
|
||||
def _validate_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
task_id = self._require_request_field(request, "task_id")
|
||||
if task_id != TASK_ID:
|
||||
raise AuthorizationError("invalid task_id")
|
||||
|
||||
principal = self._require_request_field(request, "principal")
|
||||
if principal != PRINCIPAL:
|
||||
raise AuthorizationError("invalid principal")
|
||||
|
||||
operation = self._require_request_field(request, "op")
|
||||
if operation not in {"create_branch", "append_commit"}:
|
||||
raise AuthorizationError("unsupported operation")
|
||||
if operation in self.authorization.denied_operations:
|
||||
raise AuthorizationError("operation denied by policy")
|
||||
|
||||
branch = self._require_request_field(request, "branch")
|
||||
self._ensure_not_consumed()
|
||||
policy = self._branch_policy(branch)
|
||||
|
||||
if operation == "create_branch":
|
||||
if policy.create_branch is None:
|
||||
raise AuthorizationError("branch does not allow create_branch")
|
||||
if branch in self.state.create_done:
|
||||
raise AuthorizationError("create_branch already used for this branch")
|
||||
|
||||
base_oid = self._require_request_field(request, "base_oid")
|
||||
raw_head_oid = request.get("head_oid")
|
||||
if raw_head_oid is not None and (not isinstance(raw_head_oid, str) or not raw_head_oid):
|
||||
raise AuthorizationError("head_oid must be a non-empty string when supplied")
|
||||
head_oid = str(raw_head_oid) if raw_head_oid is not None else None
|
||||
if base_oid != policy.create_branch.base_oid:
|
||||
raise AuthorizationError("base_oid mismatch")
|
||||
if policy.create_branch.head_oid is not None and head_oid != policy.create_branch.head_oid:
|
||||
raise AuthorizationError("head_oid mismatch")
|
||||
|
||||
return {
|
||||
"operation": operation,
|
||||
"branch": branch,
|
||||
"base_oid": base_oid,
|
||||
"head_oid": head_oid,
|
||||
}
|
||||
|
||||
if policy.append_commit is None:
|
||||
raise AuthorizationError("branch does not allow append_commit")
|
||||
if policy.create_branch is not None and branch not in self.state.create_done:
|
||||
raise AuthorizationError("append_commit requires prior successful create_branch")
|
||||
if branch in self.state.append_done:
|
||||
raise AuthorizationError("append_commit already used for this branch")
|
||||
|
||||
expected_head = policy.append_commit.expected_head_oid
|
||||
if "expected_head_oid" in request:
|
||||
supplied_head = self._require_request_field(request, "expected_head_oid")
|
||||
elif "current_head_oid" in request:
|
||||
supplied_head = self._require_request_field(request, "current_head_oid")
|
||||
else:
|
||||
raise AuthorizationError("expected_head_oid or current_head_oid required")
|
||||
if not re.fullmatch(r"[0-9a-fA-F]{40}", supplied_head):
|
||||
raise AuthorizationError("head check must be a full commit OID")
|
||||
if expected_head is not None and supplied_head != expected_head:
|
||||
raise AuthorizationError("head check failed")
|
||||
|
||||
if request.get("force") is True or request.get("non_fast_forward") is True:
|
||||
raise AuthorizationError("non-fast-forward operations are not allowed")
|
||||
if request.get("history_rewrite") is True:
|
||||
raise AuthorizationError("history rewrite is not allowed")
|
||||
parent_oids = request.get("parent_oids")
|
||||
if parent_oids is not None:
|
||||
if not isinstance(parent_oids, list) or parent_oids != [supplied_head]:
|
||||
raise AuthorizationError("parent_oids must contain exactly expected_head_oid")
|
||||
|
||||
changes = request.get("changes")
|
||||
if not isinstance(changes, list):
|
||||
raise AuthorizationError("changes must be a list")
|
||||
validated_changes = validate_file_changes(changes, policy.append_commit.allowed_paths)
|
||||
commit_message = request.get("commit_message", "chore(governance): append governed commit")
|
||||
if not isinstance(commit_message, str) or not commit_message.strip() or len(commit_message) > 200:
|
||||
raise AuthorizationError("commit_message must be a short non-empty string")
|
||||
if "\n" in commit_message or "\r" in commit_message or contains_secret_like_content(commit_message):
|
||||
raise AuthorizationError("commit_message failed policy")
|
||||
|
||||
return {
|
||||
"operation": operation,
|
||||
"branch": branch,
|
||||
"expected_head_oid": supplied_head,
|
||||
"changes": validated_changes,
|
||||
"commit_message": commit_message.strip(),
|
||||
}
|
||||
|
||||
def process_request(self, raw_request: Mapping[str, Any], peer: PeerIdentity) -> Mapping[str, Any]:
|
||||
"""Run one request through policy checks and handler adapter."""
|
||||
operation = raw_request.get("op") if isinstance(raw_request.get("op"), str) else "unknown"
|
||||
branch = raw_request.get("branch") if isinstance(raw_request.get("branch"), str) else ""
|
||||
try:
|
||||
details = self._validate_request(raw_request)
|
||||
operation = str(details["operation"])
|
||||
branch = str(details["branch"])
|
||||
|
||||
if operation == "create_branch":
|
||||
self.handler.create_branch(
|
||||
CreateBranchRequest(
|
||||
branch=branch,
|
||||
base_oid=str(details["base_oid"]),
|
||||
head_oid=(
|
||||
str(details["head_oid"])
|
||||
if details.get("head_oid") is not None
|
||||
else None
|
||||
),
|
||||
authorization_digest=self.authorization.authorization_sha256,
|
||||
)
|
||||
)
|
||||
self._mark_success("create_branch", branch)
|
||||
self._new_audit(peer, operation, branch, "ok")
|
||||
return {
|
||||
"status": "ok",
|
||||
"operation": operation,
|
||||
"branch": branch,
|
||||
"result": "applied",
|
||||
}
|
||||
|
||||
self.handler.append_commit(
|
||||
AppendCommitRequest(
|
||||
branch=branch,
|
||||
expected_head_oid=str(details["expected_head_oid"]),
|
||||
changes=tuple(details["changes"]),
|
||||
authorization_digest=self.authorization.authorization_sha256,
|
||||
commit_message=str(details["commit_message"]),
|
||||
)
|
||||
)
|
||||
self._mark_success("append_commit", branch)
|
||||
self._new_audit(peer, operation, branch, "ok")
|
||||
return {
|
||||
"status": "ok",
|
||||
"operation": operation,
|
||||
"branch": branch,
|
||||
"result": "applied",
|
||||
}
|
||||
except Exception as error:
|
||||
self._new_audit(peer, operation or "unknown", branch, f"error:{error.__class__.__name__}")
|
||||
if isinstance(error, PermissionError):
|
||||
code = "permission_denied"
|
||||
elif isinstance(error, AuthorizationError):
|
||||
code = "request_denied"
|
||||
else:
|
||||
code = "handler_failed"
|
||||
if code == "handler_failed" and str(error).startswith("handler_phase="):
|
||||
code = str(error)
|
||||
return {
|
||||
"status": "error",
|
||||
"operation": operation or "unknown",
|
||||
"branch": branch,
|
||||
"error": code,
|
||||
}
|
||||
|
||||
def _write_json(self, connection: socket.socket, payload: Mapping[str, Any]) -> None:
|
||||
connection.sendall((json.dumps(payload) + "\n").encode("utf-8"))
|
||||
|
||||
def _handle_client(self, connection: socket.socket) -> None:
|
||||
with connection:
|
||||
try:
|
||||
peer = self.peer_identity_fn(connection)
|
||||
except Exception:
|
||||
self._write_json(
|
||||
connection,
|
||||
{
|
||||
"status": "error",
|
||||
"operation": "connection",
|
||||
"branch": "",
|
||||
"error": "peer identity validation failed",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
with connection.makefile("r", encoding="utf-8") as stream:
|
||||
for raw_line in stream:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
request = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
self._write_json(
|
||||
connection,
|
||||
{
|
||||
"status": "error",
|
||||
"operation": "request",
|
||||
"branch": "",
|
||||
"error": "invalid json",
|
||||
},
|
||||
)
|
||||
continue
|
||||
if not isinstance(request, Mapping):
|
||||
self._write_json(
|
||||
connection,
|
||||
{
|
||||
"status": "error",
|
||||
"operation": "request",
|
||||
"branch": "",
|
||||
"error": "request must be a JSON object",
|
||||
},
|
||||
)
|
||||
continue
|
||||
self._write_json(connection, self.process_request(request, peer))
|
||||
|
||||
def _bind_socket(self) -> socket.socket:
|
||||
if self.socket_path.exists():
|
||||
raise RuntimeError("governed writer socket already exists")
|
||||
parent = self.socket_path.parent
|
||||
if not parent.is_dir():
|
||||
raise RuntimeError("governed writer socket parent is missing")
|
||||
if parent.is_symlink():
|
||||
raise RuntimeError("governed writer socket parent must not be a symlink")
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(self.socket_path))
|
||||
os.chmod(self.socket_path, 0o600)
|
||||
server.listen(16)
|
||||
return server
|
||||
|
||||
def serve(self) -> None:
|
||||
"""Serve newline-delimited JSON over a filesystem AF_UNIX socket."""
|
||||
server = self._bind_socket()
|
||||
try:
|
||||
while True:
|
||||
connection, _ = server.accept()
|
||||
self._handle_client(connection)
|
||||
finally:
|
||||
server.close()
|
||||
if self.socket_path.exists():
|
||||
self.socket_path.unlink()
|
||||
|
||||
|
||||
class JsonAuditSink:
|
||||
"""Append-only root-owned audit sink containing no request payloads."""
|
||||
|
||||
def __init__(self, path: str | os.PathLike[str]) -> None:
|
||||
self.path = Path(path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def append(self, record: AuditRecord) -> None:
|
||||
payload = {
|
||||
"authorization_sha256": record.authorization_digest,
|
||||
"peer_pid": record.peer_pid,
|
||||
"peer_uid": record.peer_uid,
|
||||
"peer_gid": record.peer_gid,
|
||||
"peer_executable": record.peer_executable,
|
||||
"timestamp": record.timestamp.isoformat(),
|
||||
"operation": record.operation,
|
||||
"branch": record.branch,
|
||||
"result": record.result,
|
||||
}
|
||||
line = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
fd = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
os.write(fd, line)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
|
||||
# V2R1C6 durable receipt overlay. This subclasses the original root-only
|
||||
# writer in-place so the existing AF_UNIX/SO_PEERCRED and handler policy remain
|
||||
# the only mutation path.
|
||||
_LegacyGovernedLocalWriterService = GovernedLocalWriterService
|
||||
|
||||
class GovernedLocalWriterService(_LegacyGovernedLocalWriterService):
|
||||
"""Existing writer with durable request receipts and safe recovery."""
|
||||
|
||||
def _load_state(self) -> None:
|
||||
super()._load_state()
|
||||
self.state.receipts = {}
|
||||
if self.state_path is None or not self.state_path.exists():
|
||||
return
|
||||
raw = json.loads(self.state_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, dict):
|
||||
raise AuthorizationError("authorization state is invalid")
|
||||
receipts = raw.get("receipts", {})
|
||||
if not isinstance(receipts, dict):
|
||||
raise AuthorizationError("authorization receipts are invalid")
|
||||
self.state.receipts = {
|
||||
str(key): dict(value)
|
||||
for key, value in receipts.items()
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
|
||||
def _persist_state(self) -> None:
|
||||
if self.state_path is None:
|
||||
return
|
||||
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"authorization_sha256": self.authorization.authorization_sha256,
|
||||
"create_done": sorted(self.state.create_done),
|
||||
"append_done": sorted(self.state.append_done),
|
||||
"consumed": bool(self.state.consumed),
|
||||
"receipts": self.state.receipts,
|
||||
}
|
||||
fd, temp_name = tempfile.mkstemp(prefix=".writer-state-", dir=self.state_path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
||||
json.dump(payload, stream, sort_keys=True, separators=(",", ":"))
|
||||
stream.write("\n")
|
||||
os.replace(temp_name, self.state_path)
|
||||
os.chmod(self.state_path, 0o600)
|
||||
finally:
|
||||
if os.path.exists(temp_name):
|
||||
os.unlink(temp_name)
|
||||
|
||||
def _request_identity(self, request: Mapping[str, Any]) -> dict[str, str]:
|
||||
canonical = json.dumps(dict(request), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
fallback = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
||||
request_id = request.get("request_id")
|
||||
idempotency_key = request.get("idempotency_key")
|
||||
request_id = request_id.strip() if isinstance(request_id, str) and request_id.strip() else "legacy-" + fallback
|
||||
idempotency_key = idempotency_key.strip() if isinstance(idempotency_key, str) and idempotency_key.strip() else "legacy-" + fallback
|
||||
for field_name, value in (("request_id", request_id), ("idempotency_key", idempotency_key)):
|
||||
if len(value) > 200 or "\n" in value or "\r" in value:
|
||||
raise AuthorizationError(field_name + " is invalid")
|
||||
return {
|
||||
"request_id": request_id,
|
||||
"idempotency_key": idempotency_key,
|
||||
"request_fingerprint": fallback,
|
||||
}
|
||||
|
||||
def _find_receipt(self, details: Mapping[str, Any], compare_fingerprint: bool = True):
|
||||
request_id = str(details["request_id"])
|
||||
idempotency_key = str(details["idempotency_key"])
|
||||
fingerprint = str(details.get("request_fingerprint", ""))
|
||||
for receipt in self.state.receipts.values():
|
||||
if receipt.get("request_id") == request_id or receipt.get("idempotency_key") == idempotency_key:
|
||||
if compare_fingerprint and receipt.get("request_fingerprint") != fingerprint:
|
||||
raise AuthorizationError("idempotency key reused with different request")
|
||||
return receipt
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _receipt_response(receipt: Mapping[str, object], status: str = "receipt") -> dict[str, object]:
|
||||
response = {
|
||||
"status": status,
|
||||
"request_id": receipt.get("request_id"),
|
||||
"idempotency_key": receipt.get("idempotency_key"),
|
||||
"operation": receipt.get("operation"),
|
||||
"branch": receipt.get("branch", ""),
|
||||
"receipt_status": receipt.get("status"),
|
||||
}
|
||||
for field_name in ("commit_oid", "final_head", "parent_oid", "audit_sha256", "error_code", "safe_to_retry"):
|
||||
if field_name in receipt:
|
||||
response[field_name] = receipt[field_name]
|
||||
return response
|
||||
|
||||
def _new_receipt(self, details: Mapping[str, Any]) -> dict[str, object]:
|
||||
return {
|
||||
"request_id": str(details["request_id"]),
|
||||
"idempotency_key": str(details["idempotency_key"]),
|
||||
"request_fingerprint": str(details["request_fingerprint"]),
|
||||
"operation": str(details["operation"]),
|
||||
"branch": str(details.get("branch", "")),
|
||||
"expected_head_oid": str(details.get("expected_head_oid", "")),
|
||||
"status": "accepted",
|
||||
"created_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"safe_to_retry": False,
|
||||
}
|
||||
|
||||
def _new_audit(self, peer: PeerIdentity, operation: str, branch: str, result: str) -> str:
|
||||
super()._new_audit(peer, operation, branch, result)
|
||||
payload = {
|
||||
"authorization_sha256": self.authorization.authorization_sha256,
|
||||
"peer_pid": peer.pid,
|
||||
"peer_uid": peer.uid,
|
||||
"peer_gid": peer.gid,
|
||||
"peer_executable": peer.executable,
|
||||
"operation": operation,
|
||||
"branch": branch,
|
||||
"result": result,
|
||||
}
|
||||
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
||||
|
||||
def _mark_success(self, op: str, branch: str) -> None:
|
||||
if op == "create_branch":
|
||||
self.state.create_done.add(branch)
|
||||
elif op in {"append_commit", "append_files", "append_batch"}:
|
||||
self.state.append_done.add(branch)
|
||||
# The provisioning grant is one-shot; the task writer lease itself must
|
||||
# support multiple append receipts for the exact authorized branches.
|
||||
self.state.consumed = False
|
||||
self._persist_state()
|
||||
|
||||
def _validate_durable(self, raw_request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
operation = self._require_request_field(raw_request, "op")
|
||||
identity = self._request_identity(raw_request)
|
||||
if operation == "query_receipt":
|
||||
return {"operation": operation, **identity}
|
||||
if operation == "recover_request":
|
||||
branch = self._require_request_field(raw_request, "branch")
|
||||
policy = self._branch_policy(branch)
|
||||
if policy.append_commit is None:
|
||||
raise AuthorizationError("branch does not allow recovery")
|
||||
expected = raw_request.get("expected_head_oid") or policy.append_commit.expected_head_oid
|
||||
if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", expected):
|
||||
raise AuthorizationError("recovery requires expected_head_oid")
|
||||
return {"operation": operation, "branch": branch, "expected_head_oid": expected.lower(), **identity}
|
||||
if operation not in {"create_branch", "append_commit", "append_files", "append_batch"}:
|
||||
raise AuthorizationError("unsupported operation")
|
||||
request = dict(raw_request)
|
||||
if operation in {"append_files", "append_batch"}:
|
||||
request["op"] = "append_commit"
|
||||
if operation in {"append_commit", "append_files", "append_batch"}:
|
||||
self.state.consumed = False
|
||||
branch = request.get("branch")
|
||||
removed = False
|
||||
dynamic_expected = False
|
||||
if isinstance(branch, str) and branch in self.state.append_done:
|
||||
self.state.append_done.remove(branch)
|
||||
removed = True
|
||||
policy = self.authorization.branch_operations.get(branch)
|
||||
if policy is not None and policy.append_commit is not None and policy.append_commit.expected_head_oid is not None:
|
||||
from governed_local_writer import BranchAppendPolicy, BranchPolicy
|
||||
self.authorization.branch_operations[branch] = BranchPolicy(create_branch=policy.create_branch, append_commit=BranchAppendPolicy(branch=branch, expected_head_oid=None, allowed_paths=policy.append_commit.allowed_paths))
|
||||
dynamic_expected = True
|
||||
try:
|
||||
details = super()._validate_request(request)
|
||||
finally:
|
||||
if dynamic_expected:
|
||||
policy = self.authorization.branch_operations.get(str(branch))
|
||||
if policy is not None and policy.append_commit is not None:
|
||||
from governed_local_writer import BranchAppendPolicy, BranchPolicy
|
||||
self.authorization.branch_operations[str(branch)] = BranchPolicy(create_branch=policy.create_branch, append_commit=BranchAppendPolicy(branch=str(branch), expected_head_oid=str(raw_request.get("expected_head_oid") or ""), allowed_paths=policy.append_commit.allowed_paths))
|
||||
if removed:
|
||||
self.state.append_done.add(str(branch))
|
||||
details.update(identity)
|
||||
details["operation"] = operation
|
||||
return details
|
||||
|
||||
def _recover_backend(self, details: Mapping[str, Any]) -> dict[str, object]:
|
||||
if not hasattr(self.handler, "_workspace") or not hasattr(self.handler, "_git"):
|
||||
raise RuntimeError("recovery backend unavailable")
|
||||
workspace = self.handler._workspace(str(details["branch"]))
|
||||
expected = str(details["expected_head_oid"])
|
||||
current = self.handler._git(workspace, ["rev-parse", "HEAD"])
|
||||
if current == expected:
|
||||
return {"status": "not_started", "final_head": current}
|
||||
parent = self.handler._git(workspace, ["rev-parse", "HEAD^1"])
|
||||
if parent != expected:
|
||||
raise RuntimeError("external branch advance during recovery")
|
||||
return {"status": "applied", "commit_oid": current, "final_head": current, "parent_oid": parent}
|
||||
|
||||
def process_request(self, raw_request: Mapping[str, Any], peer: PeerIdentity) -> Mapping[str, Any]:
|
||||
operation = raw_request.get("op") if isinstance(raw_request.get("op"), str) else "unknown"
|
||||
branch = raw_request.get("branch") if isinstance(raw_request.get("branch"), str) else ""
|
||||
try:
|
||||
details = self._validate_durable(raw_request)
|
||||
operation = str(details["operation"])
|
||||
branch = str(details.get("branch", ""))
|
||||
if operation == "query_receipt":
|
||||
receipt = self._find_receipt(details, compare_fingerprint=False)
|
||||
return self._receipt_response(receipt) if receipt is not None else {"status": "not_found", "request_id": details["request_id"], "idempotency_key": details["idempotency_key"]}
|
||||
if operation == "recover_request":
|
||||
receipt = self._find_receipt(details, compare_fingerprint=False)
|
||||
if receipt is None:
|
||||
return {"status": "not_found", "request_id": details["request_id"], "idempotency_key": details["idempotency_key"], "safe_to_retry": True}
|
||||
if receipt.get("status") == "applied":
|
||||
return self._receipt_response(receipt)
|
||||
recovered = self._recover_backend(details)
|
||||
if recovered.get("status") == "applied":
|
||||
receipt.update({"status": "applied", "commit_oid": recovered.get("commit_oid"), "final_head": recovered.get("final_head"), "parent_oid": recovered.get("parent_oid"), "safe_to_retry": False})
|
||||
self._mark_success("append_commit", branch)
|
||||
return self._receipt_response(receipt)
|
||||
receipt["safe_to_retry"] = True
|
||||
self._persist_state()
|
||||
return self._receipt_response(receipt)
|
||||
receipt = self._find_receipt(details)
|
||||
if receipt is not None:
|
||||
return self._receipt_response(receipt)
|
||||
receipt = self._new_receipt(details)
|
||||
self.state.receipts[str(details["request_id"])] = receipt
|
||||
self._persist_state()
|
||||
if operation == "create_branch":
|
||||
result = self.handler.create_branch(CreateBranchRequest(branch=branch, base_oid=str(details["base_oid"]), head_oid=str(details["head_oid"]) if details.get("head_oid") is not None else None, authorization_digest=self.authorization.authorization_sha256))
|
||||
commit_oid = str(result.get("head_oid", ""))
|
||||
parent_oid = str(result.get("parent_oid", ""))
|
||||
audit_sha = self._new_audit(peer, operation, branch, "ok")
|
||||
receipt.update({"status": "applied", "commit_oid": commit_oid, "final_head": commit_oid, "parent_oid": parent_oid, "audit_sha256": audit_sha, "safe_to_retry": False})
|
||||
self._mark_success("create_branch", branch)
|
||||
return self._receipt_response(receipt, status="ok") | {"result": "applied"}
|
||||
result = self.handler.append_commit(AppendCommitRequest(branch=branch, expected_head_oid=str(details["expected_head_oid"]), changes=tuple(details["changes"]), authorization_digest=self.authorization.authorization_sha256, commit_message=str(details["commit_message"])))
|
||||
commit_oid = str(result.get("head_oid", ""))
|
||||
parent_oid = str(result.get("parent_oid", ""))
|
||||
audit_sha = self._new_audit(peer, operation, branch, "ok")
|
||||
receipt.update({"status": "applied", "commit_oid": commit_oid, "final_head": commit_oid, "parent_oid": parent_oid, "audit_sha256": audit_sha, "safe_to_retry": False})
|
||||
self._mark_success("append_commit", branch)
|
||||
return self._receipt_response(receipt, status="ok") | {"result": "applied"}
|
||||
except Exception as error:
|
||||
try:
|
||||
identity = self._request_identity(raw_request)
|
||||
receipt = self.state.receipts.get(identity["request_id"])
|
||||
if receipt is not None and receipt.get("status") == "accepted":
|
||||
receipt.update({"status": "failed", "error_code": error.__class__.__name__, "safe_to_retry": False})
|
||||
self._persist_state()
|
||||
except Exception:
|
||||
pass
|
||||
self._new_audit(peer, operation, branch, "error:" + error.__class__.__name__)
|
||||
return {"status": "error", "operation": operation, "branch": branch, "error": "request_denied" if isinstance(error, AuthorizationError) else "handler_failed"}
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="AIOS governed local writer")
|
||||
parser.add_argument("--socket", required=True)
|
||||
parser.add_argument("--authorization", required=True)
|
||||
parser.add_argument("--state", required=True)
|
||||
parser.add_argument("--audit", required=True)
|
||||
parser.add_argument("--repair-branch", required=True)
|
||||
parser.add_argument("--repair-workspace", required=True)
|
||||
parser.add_argument("--candidate-branch", required=True)
|
||||
parser.add_argument("--candidate-workspace", required=True)
|
||||
parser.add_argument("--bare-repo", required=True)
|
||||
parser.add_argument("--api-source", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(os, "geteuid") and os.geteuid() != 0:
|
||||
print("governed local writer requires uid 0", file=sys.stderr)
|
||||
return 2
|
||||
args = _parse_args()
|
||||
authorization = load_authorization_file(args.authorization)
|
||||
handler = ExistingGitOpsHandler(
|
||||
workspace_by_branch={
|
||||
args.repair_branch: args.repair_workspace,
|
||||
args.candidate_branch: args.candidate_workspace,
|
||||
},
|
||||
bare_repo=args.bare_repo,
|
||||
api_source=args.api_source,
|
||||
)
|
||||
service = GovernedLocalWriterService(
|
||||
args.socket,
|
||||
authorization,
|
||||
handler,
|
||||
audit_sink=JsonAuditSink(args.audit),
|
||||
state_path=args.state,
|
||||
)
|
||||
service.serve()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - systemd entrypoint
|
||||
raise SystemExit(main())
|
||||
@@ -1,144 +0,0 @@
|
||||
"""HTTP adapter for audited Owner authorization and writer provisioning.
|
||||
|
||||
This module layers on top of ``aios_authorized_task_writer_api`` so existing
|
||||
routes and the legacy exact ``writer:provision`` route remain available. The
|
||||
new endpoint does not mint or modify bearer credentials. It authenticates an
|
||||
existing, non-wildcard, explicitly allow-listed Owner credential and then
|
||||
invokes the audited broker.
|
||||
|
||||
Endpoint:
|
||||
POST /api/governance/owner-authorizations
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
from typing import Any, Mapping
|
||||
|
||||
import aios_authorized_task_writer_api as bridge_api
|
||||
from authorized_task_writer_provisioning import ProvisioningError
|
||||
from owner_authorization_broker import (
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationError,
|
||||
OwnerPrincipal,
|
||||
)
|
||||
|
||||
|
||||
api = bridge_api.api
|
||||
_original_do_post = api.H.do_POST
|
||||
_ENDPOINT = "/api/governance/owner-authorizations"
|
||||
|
||||
|
||||
def _enabled() -> bool:
|
||||
return os.getenv(
|
||||
"AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED", ""
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _configured_intake_scope() -> str:
|
||||
scopes = [
|
||||
item.strip()
|
||||
for item in os.getenv(
|
||||
"AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES", ""
|
||||
).split(",")
|
||||
if item.strip()
|
||||
]
|
||||
if len(scopes) != 1:
|
||||
raise OwnerAuthorizationError(
|
||||
"EXACTLY_ONE_HTTP_INTAKE_SCOPE_REQUIRED", 503
|
||||
)
|
||||
scope = scopes[0]
|
||||
if scope == "writer:provision" or scope == "*" or scope.endswith(":*"):
|
||||
raise OwnerAuthorizationError("HTTP_INTAKE_SCOPE_REJECTED", 503)
|
||||
return scope
|
||||
|
||||
|
||||
def _scope_set(auth: Mapping[str, Any]) -> frozenset[str]:
|
||||
raw = auth.get("scopes")
|
||||
if raw is None:
|
||||
raw = auth.get("scope")
|
||||
if raw is None:
|
||||
return frozenset()
|
||||
if isinstance(raw, str):
|
||||
candidates = raw.replace(",", " ").split()
|
||||
elif isinstance(raw, (list, tuple, set, frozenset)):
|
||||
candidates = [str(item) for item in raw]
|
||||
else:
|
||||
raise OwnerAuthorizationError("OWNER_SCOPE_EVIDENCE_INVALID", 403)
|
||||
return frozenset(item.strip() for item in candidates if item.strip())
|
||||
|
||||
|
||||
def _credential_id(auth: Mapping[str, Any]) -> str:
|
||||
for name in (
|
||||
"credential_id",
|
||||
"token_id",
|
||||
"key_id",
|
||||
"auth_id",
|
||||
):
|
||||
value = str(auth.get(name) or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def _authenticate_existing_owner(handler) -> OwnerPrincipal | None:
|
||||
scope = _configured_intake_scope()
|
||||
auth = handler.need(scope)
|
||||
if not auth:
|
||||
return None
|
||||
if auth.get("role") != "owner":
|
||||
handler.sendj({"error": "OWNER_ROLE_REQUIRED"}, 403)
|
||||
return None
|
||||
identity = str(auth.get("account") or auth.get("identity") or "").strip()
|
||||
if not identity:
|
||||
handler.sendj({"error": "OWNER_IDENTITY_REQUIRED"}, 403)
|
||||
return None
|
||||
scopes = _scope_set(auth)
|
||||
if any(item == "*" or item.endswith(":*") for item in scopes):
|
||||
handler.sendj(
|
||||
{"error": "WILDCARD_OWNER_CREDENTIAL_REJECTED"}, 403
|
||||
)
|
||||
return None
|
||||
return OwnerPrincipal(
|
||||
identity=identity,
|
||||
role="owner",
|
||||
authenticated_scope=scope,
|
||||
token_scopes=scopes,
|
||||
credential_id=_credential_id(auth),
|
||||
)
|
||||
|
||||
|
||||
def _broker_do_post(self):
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != _ENDPOINT:
|
||||
return _original_do_post(self)
|
||||
if not _enabled():
|
||||
return self.sendj(
|
||||
{"error": "OWNER_AUTHORIZATION_BROKER_DISABLED"}, 503
|
||||
)
|
||||
try:
|
||||
principal = _authenticate_existing_owner(self)
|
||||
if principal is None:
|
||||
return
|
||||
data = self.body()
|
||||
result = OwnerAuthorizationBroker().authorize_and_provision(
|
||||
data, principal
|
||||
)
|
||||
return self.sendj(result)
|
||||
except (OwnerAuthorizationError, ProvisioningError) as exc:
|
||||
body = {"error": exc.code}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
return self.sendj(body, exc.status)
|
||||
except Exception:
|
||||
# Never expose registry data, bearer tokens, signer output or traceback.
|
||||
return self.sendj(
|
||||
{"error": "OWNER_AUTHORIZATION_BROKER_INTERNAL_ERROR"}, 500
|
||||
)
|
||||
|
||||
|
||||
api.H.do_POST = _broker_do_post
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
api.main()
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Loopback-only HTTP API for the task identity overlay.
|
||||
|
||||
Endpoints:
|
||||
GET /healthz
|
||||
POST /api/governance/owner-authorizations
|
||||
|
||||
The API authenticates only the short-lived overlay credential. It does not read
|
||||
or modify the global bearer-token registry.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any
|
||||
|
||||
from authorized_task_writer_provisioning import ProvisioningError
|
||||
from owner_authorization_broker import OwnerAuthorizationError
|
||||
from task_identity_overlay import (
|
||||
MAX_BODY_BYTES,
|
||||
IdentityOverlayConfig,
|
||||
IdentityOverlayError,
|
||||
TaskIdentityOverlayService,
|
||||
)
|
||||
|
||||
|
||||
_ENDPOINT = "/api/governance/owner-authorizations"
|
||||
|
||||
|
||||
def _enabled() -> bool:
|
||||
return os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_ENABLED", ""
|
||||
).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _bearer(header: str) -> str:
|
||||
prefix = "Bearer "
|
||||
if not header.startswith(prefix):
|
||||
raise IdentityOverlayError("OVERLAY_AUTHENTICATION_REQUIRED", 401)
|
||||
token = header[len(prefix):].strip()
|
||||
if not token or len(token) > 4096:
|
||||
raise IdentityOverlayError("OVERLAY_AUTHENTICATION_REQUIRED", 401)
|
||||
return token
|
||||
|
||||
|
||||
class OverlayHandler(BaseHTTPRequestHandler):
|
||||
server_version = "aios-task-identity-overlay/1"
|
||||
sys_version = ""
|
||||
|
||||
@property
|
||||
def overlay(self) -> TaskIdentityOverlayService:
|
||||
return self.server.overlay # type: ignore[attr-defined]
|
||||
|
||||
def log_message(self, format: str, *args: Any) -> None:
|
||||
# Never log Authorization or request bodies.
|
||||
return
|
||||
|
||||
def _send(self, status: int, value: dict[str, Any]) -> None:
|
||||
payload = (
|
||||
json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def _body(self) -> dict[str, Any]:
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
except ValueError:
|
||||
raise IdentityOverlayError("OVERLAY_CONTENT_LENGTH_INVALID") from None
|
||||
if length <= 0 or length > MAX_BODY_BYTES:
|
||||
raise IdentityOverlayError("OVERLAY_BODY_SIZE_REJECTED", 413)
|
||||
raw = self.rfile.read(length)
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise IdentityOverlayError("OVERLAY_BODY_JSON_INVALID") from None
|
||||
if not isinstance(value, dict):
|
||||
raise IdentityOverlayError("OVERLAY_BODY_OBJECT_REQUIRED")
|
||||
return value
|
||||
|
||||
def do_GET(self) -> None:
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != "/healthz":
|
||||
self._send(404, {"error": "NOT_FOUND"})
|
||||
return
|
||||
self._send(
|
||||
200,
|
||||
{
|
||||
"service": "aios-task-identity-overlay",
|
||||
"status": "active" if _enabled() else "disabled",
|
||||
"global_token_registry_used": False,
|
||||
"loopback_only": True,
|
||||
},
|
||||
)
|
||||
|
||||
def do_POST(self) -> None:
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != _ENDPOINT:
|
||||
self._send(404, {"error": "NOT_FOUND"})
|
||||
return
|
||||
if not _enabled():
|
||||
self._send(503, {"error": "IDENTITY_OVERLAY_DISABLED"})
|
||||
return
|
||||
try:
|
||||
token = _bearer(self.headers.get("Authorization", ""))
|
||||
result = self.overlay.provision(self._body(), token)
|
||||
self._send(200, result)
|
||||
except (IdentityOverlayError, OwnerAuthorizationError, ProvisioningError) as exc:
|
||||
body: dict[str, Any] = {"error": exc.code}
|
||||
if getattr(exc, "details", None):
|
||||
body["details"] = exc.details
|
||||
self._send(exc.status, body)
|
||||
except Exception:
|
||||
self._send(500, {"error": "IDENTITY_OVERLAY_API_INTERNAL_ERROR"})
|
||||
|
||||
|
||||
class OverlayServer(ThreadingHTTPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
address: tuple[str, int],
|
||||
overlay: TaskIdentityOverlayService,
|
||||
) -> None:
|
||||
super().__init__(address, OverlayHandler)
|
||||
self.overlay = overlay
|
||||
|
||||
|
||||
def main() -> None:
|
||||
host = os.getenv("AIOS_GITOPS_IDENTITY_OVERLAY_HOST", "127.0.0.1")
|
||||
if host not in {"127.0.0.1", "::1", "localhost"}:
|
||||
raise SystemExit("identity overlay must listen on loopback")
|
||||
try:
|
||||
port = int(os.getenv("AIOS_GITOPS_IDENTITY_OVERLAY_PORT", "19864"))
|
||||
except ValueError:
|
||||
raise SystemExit("invalid overlay port") from None
|
||||
if not 1024 <= port <= 65535:
|
||||
raise SystemExit("invalid overlay port")
|
||||
config = IdentityOverlayConfig.from_environment()
|
||||
server = OverlayServer(
|
||||
(host, port),
|
||||
TaskIdentityOverlayService(config),
|
||||
)
|
||||
server.serve_forever(poll_interval=0.25)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-governed CLI for issuing and revoking task identity overlays.
|
||||
|
||||
The CLI never prints the bearer token. It returns only the root-owned secret
|
||||
reference and non-secret metadata.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from task_identity_overlay import (
|
||||
IdentityOverlayConfig,
|
||||
IdentityOverlayError,
|
||||
TaskIdentityOverlayStore,
|
||||
)
|
||||
|
||||
|
||||
def _write_result(path: str, value: dict) -> None:
|
||||
data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n"
|
||||
if path == "-":
|
||||
sys.stdout.write(data)
|
||||
return
|
||||
target = Path(path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(data, encoding="utf-8")
|
||||
os.chmod(target, 0o600)
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser(
|
||||
description="Issue/status/revoke a task-scoped identity overlay"
|
||||
)
|
||||
sub = value.add_subparsers(dest="command", required=True)
|
||||
|
||||
issue = sub.add_parser("issue")
|
||||
issue.add_argument("--ttl-seconds", type=int, default=900)
|
||||
issue.add_argument("--authorization-assertion-ref", required=True)
|
||||
issue.add_argument(
|
||||
"--operator-identity",
|
||||
default=os.getenv("SUDO_USER") or os.getenv("USER") or "root",
|
||||
)
|
||||
issue.add_argument("--result-json", default="-")
|
||||
|
||||
status = sub.add_parser("status")
|
||||
status.add_argument("--overlay-id", required=True)
|
||||
status.add_argument("--result-json", default="-")
|
||||
|
||||
revoke = sub.add_parser("revoke")
|
||||
revoke.add_argument("--overlay-id", required=True)
|
||||
revoke.add_argument("--reason", required=True)
|
||||
revoke.add_argument("--result-json", default="-")
|
||||
return value
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
try:
|
||||
store = TaskIdentityOverlayStore(
|
||||
IdentityOverlayConfig.from_environment()
|
||||
)
|
||||
if args.command == "issue":
|
||||
result = store.issue(
|
||||
ttl_seconds=args.ttl_seconds,
|
||||
authorization_assertion_ref=args.authorization_assertion_ref,
|
||||
operator_identity=args.operator_identity,
|
||||
)
|
||||
elif args.command == "status":
|
||||
result = store.status(args.overlay_id)
|
||||
else:
|
||||
result = store.revoke(args.overlay_id, reason=args.reason)
|
||||
_write_result(args.result_json, result)
|
||||
return 0
|
||||
except IdentityOverlayError as exc:
|
||||
body = {"status": "BLOCKED", "error": exc.code}
|
||||
if exc.details:
|
||||
body["details"] = exc.details
|
||||
_write_result(getattr(args, "result_json", "-"), body)
|
||||
return 2
|
||||
except Exception:
|
||||
_write_result(
|
||||
getattr(args, "result_json", "-"),
|
||||
{"status": "FAILED", "error": "IDENTITY_OVERLAY_CLI_INTERNAL_ERROR"},
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,652 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot governed executor for an already approved exact release binding.
|
||||
|
||||
This module deliberately does not build release artifacts. The build/staging
|
||||
evidence is completed before Owner production approval. Production execution
|
||||
consumes that immutable binding, revalidates it, and performs narrow
|
||||
side-effects through adapters in a durable fail-closed order.
|
||||
|
||||
Adapter protocol:
|
||||
stdin: JSON request with operation = "probe" | "apply" | "rollback"
|
||||
stdout: one JSON object on the final non-empty line
|
||||
shell: never used
|
||||
|
||||
The executor never rewrites tags, never writes refs/heads/main, and records
|
||||
Baseline last.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
REQUIRED_V2R1C7_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
class ReleaseBlocked(RuntimeError):
|
||||
pass
|
||||
|
||||
class ManualOwnerReviewRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
class ReconciliationRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Binding:
|
||||
version: str
|
||||
target_oid: str
|
||||
evidence_closure_oid: str
|
||||
artifact_sha256: str
|
||||
approval_plan_id: str
|
||||
artifact_path: str
|
||||
artifact_size: int
|
||||
production_service: str
|
||||
production_port: int
|
||||
repository_identity: str = "aios/aios"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.version or not OID_RE.fullmatch(self.target_oid):
|
||||
raise ValueError("invalid release target binding")
|
||||
if not OID_RE.fullmatch(self.evidence_closure_oid):
|
||||
raise ValueError("invalid evidence closure OID")
|
||||
if not SHA_RE.fullmatch(self.artifact_sha256):
|
||||
raise ValueError("invalid artifact SHA-256")
|
||||
if not PLAN_RE.fullmatch(self.approval_plan_id):
|
||||
raise ValueError("invalid approval plan id")
|
||||
if self.artifact_size <= 0:
|
||||
raise ValueError("invalid artifact size")
|
||||
if not self.production_service:
|
||||
raise ValueError("production service is required")
|
||||
if not 1024 <= self.production_port <= 65535:
|
||||
raise ValueError("invalid production port")
|
||||
if self.repository_identity != "aios/aios":
|
||||
raise ValueError("unexpected repository identity")
|
||||
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
return self.version
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
payload = {
|
||||
"version": self.version,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"artifact_size": self.artifact_size,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
||||
|
||||
def request(self, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-executor-request.v1",
|
||||
"operation": operation,
|
||||
"binding_fingerprint": self.fingerprint,
|
||||
"version": self.version,
|
||||
"tag": self.tag,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"artifact_path": self.artifact_path,
|
||||
"artifact_size": self.artifact_size,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def secure_json(path: Path) -> dict[str, Any]:
|
||||
st = path.lstat()
|
||||
if not stat.S_ISREG(st.st_mode) or path.is_symlink():
|
||||
raise ReleaseBlocked(f"non-regular authorization file: {path}")
|
||||
if st.st_uid != 0:
|
||||
raise ReleaseBlocked(f"authorization file must be root-owned: {path}")
|
||||
if st.st_mode & 0o022:
|
||||
raise ReleaseBlocked(f"authorization file is group/world writable: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ReleaseBlocked("authorization receipt must be a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReleaseBlocked("artifact must be a regular non-symlink file")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
|
||||
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
|
||||
if not OID_RE.fullmatch(authority_main):
|
||||
raise ReleaseBlocked("Authority main OID is invalid")
|
||||
|
||||
target_type = run_git(git_dir, "cat-file", "-t", binding.target_oid).stdout.strip()
|
||||
if target_type != "commit":
|
||||
raise ReleaseBlocked("target_oid is not a commit")
|
||||
|
||||
evidence_type = run_git(
|
||||
git_dir, "cat-file", "-t", binding.evidence_closure_oid
|
||||
).stdout.strip()
|
||||
if evidence_type != "commit":
|
||||
raise ReleaseBlocked("evidence_closure_oid is not a commit")
|
||||
|
||||
# A control-plane-only patch may advance main after release approval.
|
||||
# The approved release target remains valid only while it is still on main.
|
||||
target_on_main = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
authority_main,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_on_main:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"approved target is no longer an ancestor of Authority main"
|
||||
)
|
||||
|
||||
target_in_evidence = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
binding.evidence_closure_oid,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_in_evidence:
|
||||
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
|
||||
|
||||
return {
|
||||
"authority_main_oid": authority_main,
|
||||
"target_oid": binding.target_oid,
|
||||
"target_is_ancestor_of_main": True,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"evidence_object_type": evidence_type,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_artifact(binding: Binding) -> dict[str, Any]:
|
||||
path = Path(binding.artifact_path)
|
||||
if path.stat().st_size != binding.artifact_size:
|
||||
raise ReleaseBlocked("artifact size mismatch")
|
||||
actual = sha256_file(path)
|
||||
if actual != binding.artifact_sha256:
|
||||
raise ReleaseBlocked("artifact SHA-256 mismatch")
|
||||
|
||||
if binding.version == "V2R1C7":
|
||||
# Python tarfile may not have zstd support on the target host, so
|
||||
# decompress into a private temporary tar and inspect it there.
|
||||
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
|
||||
tar_path = Path(temp_dir) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(path)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked("artifact zstd decompression failed")
|
||||
import tarfile
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
names: set[str] = set()
|
||||
for member in archive.getmembers():
|
||||
candidate = Path(member.name)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or ".." in candidate.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
|
||||
names.add(member.name)
|
||||
missing = sorted(REQUIRED_V2R1C7_MEMBERS - names)
|
||||
if missing:
|
||||
raise ReleaseBlocked(
|
||||
"artifact missing required members: " + ",".join(missing)
|
||||
)
|
||||
return {
|
||||
"artifact_path": str(path),
|
||||
"artifact_sha256": actual,
|
||||
"artifact_size": binding.artifact_size,
|
||||
"verified": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[str, Any]:
|
||||
if receipt.get("accepted") is not True:
|
||||
raise ReleaseBlocked("Owner production authorization is not accepted")
|
||||
if receipt.get("binding_replaced") not in (None, False):
|
||||
raise ManualOwnerReviewRequired("Owner binding was replaced")
|
||||
expected = {
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if receipt.get(key) != value:
|
||||
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
|
||||
return {"accepted": True, **expected}
|
||||
|
||||
|
||||
class DurableState:
|
||||
def __init__(self, root: Path, binding: Binding):
|
||||
self.root = root
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.path = self.root / "execution.json"
|
||||
self.binding = binding
|
||||
self.value: dict[str, Any]
|
||||
if self.path.exists():
|
||||
existing = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if existing.get("binding_fingerprint") != binding.fingerprint:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"state root contains a different production binding"
|
||||
)
|
||||
self.value = existing
|
||||
else:
|
||||
self.value = {
|
||||
"schema": "aios.production-execution-state.v1",
|
||||
"binding_fingerprint": binding.fingerprint,
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"state": "AUTHORIZED",
|
||||
"completed": {},
|
||||
"history": [],
|
||||
}
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
temporary = self.path.with_suffix(".tmp")
|
||||
temporary.write_bytes(canonical_json(self.value))
|
||||
os.chmod(temporary, 0o600)
|
||||
os.replace(temporary, self.path)
|
||||
|
||||
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
|
||||
now = time.time()
|
||||
sanitized = json.loads(json.dumps(result))
|
||||
self.value["state"] = state
|
||||
self.value["completed"][step] = sanitized
|
||||
self.value["history"].append(
|
||||
{"at": now, "state": state, "step": step, "result": sanitized}
|
||||
)
|
||||
self.save()
|
||||
|
||||
def fail(self, state: str, reason: str) -> None:
|
||||
self.value["state"] = state
|
||||
self.value["history"].append(
|
||||
{"at": time.time(), "state": state, "reason": reason}
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
class FileLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self) -> "FileLock":
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
try:
|
||||
fcntl.flock(
|
||||
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
)
|
||||
except BlockingIOError as exc:
|
||||
raise ReleaseBlocked("production executor is already running") from exc
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self.handle is not None:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
def parse_command(values: Sequence[str]) -> list[str]:
|
||||
command = [str(x) for x in values if str(x)]
|
||||
if not command:
|
||||
raise ValueError("adapter command is required")
|
||||
if not os.path.isabs(command[0]):
|
||||
raise ValueError("adapter executable must be an absolute path")
|
||||
return command
|
||||
|
||||
|
||||
def run_adapter(command: Sequence[str], request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
list(command),
|
||||
input=canonical_json(request),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=180,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked(
|
||||
f"adapter failed: {Path(command[0]).name} rc={completed.returncode}"
|
||||
)
|
||||
lines = [line for line in completed.stdout.decode("utf-8").splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise ReleaseBlocked("adapter returned no JSON result")
|
||||
try:
|
||||
value = json.loads(lines[-1])
|
||||
except ValueError as exc:
|
||||
raise ReleaseBlocked("adapter returned invalid JSON") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReleaseBlocked("adapter result must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def require_exact_response(result: Mapping[str, Any], binding: Binding) -> None:
|
||||
for key, expected in {
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}.items():
|
||||
if key in result and result[key] != expected:
|
||||
raise ManualOwnerReviewRequired(f"adapter binding mismatch: {key}")
|
||||
|
||||
|
||||
def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
state = probe.get("status")
|
||||
if state == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("same-name tag conflicts with approved binding")
|
||||
if state == "INDETERMINATE":
|
||||
raise ReconciliationRequired("tag publication state is indeterminate")
|
||||
if state not in {"APPLIED_VERIFIED", "NOT_APPLIED", "PARTIAL_LOCAL"}:
|
||||
raise ReleaseBlocked(f"unsupported tag probe status: {state}")
|
||||
if state != "APPLIED_VERIFIED":
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
if applied.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("tag conflict during publication")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "APPLIED_VERIFIED":
|
||||
raise ReconciliationRequired("tag was not independently verified")
|
||||
if probe.get("peeled_target_oid") != binding.target_oid:
|
||||
raise ManualOwnerReviewRequired("published tag peeled target mismatch")
|
||||
tag_oid = probe.get("tag_oid")
|
||||
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
|
||||
raise ReleaseBlocked("verified annotated tag object OID missing")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "ACTIVE_VERIFIED":
|
||||
return dict(probe)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "ACTIVE_VERIFIED":
|
||||
try:
|
||||
run_adapter(command, binding.request("rollback"))
|
||||
except Exception:
|
||||
pass
|
||||
raise ReconciliationRequired("production runtime did not verify after apply")
|
||||
if (
|
||||
probe.get("deployment_state") != "ACTIVE"
|
||||
or probe.get("production_deployed") is not True
|
||||
):
|
||||
raise ReconciliationRequired("production health contract is not ACTIVE")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def finalize_step(
|
||||
command: Sequence[str],
|
||||
binding: Binding,
|
||||
*,
|
||||
name: str,
|
||||
tag_oid: str,
|
||||
) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
request["tag_oid"] = tag_oid
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired(f"{name} has conflicting release binding")
|
||||
if probe.get("status") != "VERIFIED":
|
||||
apply_request = binding.request("apply")
|
||||
apply_request["tag_oid"] = tag_oid
|
||||
applied = run_adapter(command, apply_request)
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired(f"{name} did not verify after apply")
|
||||
if probe.get("tag_oid") not in (None, tag_oid):
|
||||
raise ManualOwnerReviewRequired(f"{name} tag object mismatch")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def execute(
|
||||
*,
|
||||
binding: Binding,
|
||||
authorization_path: Path,
|
||||
repository: Path,
|
||||
state_root: Path,
|
||||
tag_command: Sequence[str],
|
||||
deploy_command: Sequence[str],
|
||||
registry_command: Sequence[str],
|
||||
baseline_command: Sequence[str],
|
||||
) -> dict[str, Any]:
|
||||
authorization = verify_authorization(secure_json(authorization_path), binding)
|
||||
lineage = verify_git_lineage(repository, binding)
|
||||
artifact = verify_artifact(binding)
|
||||
state = DurableState(state_root, binding)
|
||||
with FileLock(state_root / "executor.lock"):
|
||||
state.record(
|
||||
"PREFLIGHT_VERIFIED",
|
||||
"preflight",
|
||||
{"authorization": authorization, "lineage": lineage, "artifact": artifact},
|
||||
)
|
||||
tag: dict[str, Any] | None = None
|
||||
runtime: dict[str, Any] | None = None
|
||||
try:
|
||||
tag = tag_step(tag_command, binding)
|
||||
state.record("TAG_VERIFIED", "tag", tag)
|
||||
|
||||
runtime = deploy_step(deploy_command, binding)
|
||||
state.record("RUNTIME_VERIFIED", "runtime", runtime)
|
||||
|
||||
registry = finalize_step(
|
||||
registry_command,
|
||||
binding,
|
||||
name="registry",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("REGISTRY_VERIFIED", "registry", registry)
|
||||
|
||||
baseline = finalize_step(
|
||||
baseline_command,
|
||||
binding,
|
||||
name="baseline",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("BASELINE_VERIFIED", "baseline", baseline)
|
||||
|
||||
# Final readback: no mutation, all four planes must still agree.
|
||||
final_tag = tag_step(tag_command, binding)
|
||||
final_runtime = run_adapter(deploy_command, binding.request("probe"))
|
||||
final_registry_request = binding.request("probe")
|
||||
final_registry_request["tag_oid"] = tag["tag_oid"]
|
||||
final_registry = run_adapter(registry_command, final_registry_request)
|
||||
final_baseline_request = binding.request("probe")
|
||||
final_baseline_request["tag_oid"] = tag["tag_oid"]
|
||||
final_baseline = run_adapter(baseline_command, final_baseline_request)
|
||||
for value in (final_runtime, final_registry, final_baseline):
|
||||
require_exact_response(value, binding)
|
||||
if final_runtime.get("status") != "ACTIVE_VERIFIED":
|
||||
raise ReconciliationRequired("final runtime readback failed")
|
||||
if final_registry.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Registry readback failed")
|
||||
if final_baseline.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Baseline readback failed")
|
||||
final = {
|
||||
"status": "PRODUCTION_ACCEPTED",
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
"tag_oid": tag["tag_oid"],
|
||||
"tag": final_tag,
|
||||
"runtime": final_runtime,
|
||||
"registry": final_registry,
|
||||
"baseline": final_baseline,
|
||||
"force_push_used": False,
|
||||
"tag_rewritten": False,
|
||||
"direct_main_push": False,
|
||||
"secrets_committed": False,
|
||||
}
|
||||
state.record("COMPLETED", "final_consistency", final)
|
||||
return final
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
|
||||
raise
|
||||
except ReconciliationRequired as exc:
|
||||
state.fail("RECONCILIATION_REQUIRED", str(exc))
|
||||
raise
|
||||
except Exception as exc:
|
||||
# Once a formal tag has been verified, no later error is a clean
|
||||
# pre-side-effect failure. Preserve the tag and require idempotent
|
||||
# reconciliation rather than pretending the release never started.
|
||||
if tag is not None:
|
||||
reason = f"{type(exc).__name__}: {exc}"
|
||||
state.fail("RECONCILIATION_REQUIRED", reason)
|
||||
raise ReconciliationRequired(reason) from exc
|
||||
state.fail("FAIL_CLOSED", f"{type(exc).__name__}: {exc}")
|
||||
raise
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--authorization", required=True, type=Path)
|
||||
p.add_argument("--repository", required=True, type=Path)
|
||||
p.add_argument("--state-root", required=True, type=Path)
|
||||
p.add_argument("--artifact", required=True, type=Path)
|
||||
p.add_argument("--artifact-size", required=True, type=int)
|
||||
p.add_argument("--version", required=True)
|
||||
p.add_argument("--target-oid", required=True)
|
||||
p.add_argument("--evidence-closure-oid", required=True)
|
||||
p.add_argument("--artifact-sha256", required=True)
|
||||
p.add_argument("--approval-plan-id", required=True)
|
||||
p.add_argument("--production-service", required=True)
|
||||
p.add_argument("--production-port", required=True, type=int)
|
||||
p.add_argument("--repository-identity", default="aios/aios")
|
||||
p.add_argument("--tag-adapter", required=True, nargs="+")
|
||||
p.add_argument("--deploy-adapter", required=True, nargs="+")
|
||||
p.add_argument("--registry-adapter", required=True, nargs="+")
|
||||
p.add_argument("--baseline-adapter", required=True, nargs="+")
|
||||
return p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
binding = Binding(
|
||||
version=args.version,
|
||||
target_oid=args.target_oid,
|
||||
evidence_closure_oid=args.evidence_closure_oid,
|
||||
artifact_sha256=args.artifact_sha256,
|
||||
approval_plan_id=args.approval_plan_id,
|
||||
artifact_path=str(args.artifact),
|
||||
artifact_size=args.artifact_size,
|
||||
production_service=args.production_service,
|
||||
production_port=args.production_port,
|
||||
repository_identity=args.repository_identity,
|
||||
)
|
||||
try:
|
||||
result = execute(
|
||||
binding=binding,
|
||||
authorization_path=args.authorization,
|
||||
repository=args.repository,
|
||||
state_root=args.state_root,
|
||||
tag_command=parse_command(args.tag_adapter),
|
||||
deploy_command=parse_command(args.deploy_adapter),
|
||||
registry_command=parse_command(args.registry_adapter),
|
||||
baseline_command=parse_command(args.baseline_adapter),
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "MANUAL_OWNER_REVIEW_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 20
|
||||
except ReconciliationRequired as exc:
|
||||
print(json.dumps({"status": "RECONCILIATION_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 21
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,953 +0,0 @@
|
||||
"""Owner-authorized, fail-closed task writer provisioning bridge.
|
||||
|
||||
This module converts an authenticated Owner authorization into a canonical,
|
||||
short-lived, one-time grant request. It delegates signing, validation and
|
||||
worktree/writer provisioning to existing root-governed executables.
|
||||
|
||||
It never writes Git refs directly and never authorizes main, tag, baseline or
|
||||
production mutations.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
from signer_contract_compatibility import (
|
||||
deployed_signer_help,
|
||||
run_compatible_signer,
|
||||
run_compatible_validator,
|
||||
)
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
TASK_RE = re.compile(r"^[A-Z0-9][A-Z0-9._:-]{7,127}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
OWNER_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{3,255}$")
|
||||
|
||||
REQUIRED_REQUEST_FIELDS = frozenset(
|
||||
{
|
||||
"task_id",
|
||||
"repository",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"owner_authorization_reference",
|
||||
"one_time_use",
|
||||
"expires_in_seconds",
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
}
|
||||
)
|
||||
REQUIRED_ALLOWED_OPERATIONS = frozenset(
|
||||
{
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch",
|
||||
}
|
||||
)
|
||||
REQUIRED_DENIED_OPERATIONS = frozenset(
|
||||
{
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite",
|
||||
}
|
||||
)
|
||||
REQUIRED_NEGATIVE_TESTS = frozenset(
|
||||
{
|
||||
"main_write_denied",
|
||||
"other_branch_write_denied",
|
||||
"tag_create_denied",
|
||||
"tag_update_denied",
|
||||
"baseline_write_denied",
|
||||
"force_push_denied",
|
||||
"branch_delete_denied",
|
||||
}
|
||||
)
|
||||
SENSITIVE_KEY_MARKERS = (
|
||||
"authorization",
|
||||
"credential",
|
||||
"private_key",
|
||||
"secret",
|
||||
"signature",
|
||||
"token",
|
||||
)
|
||||
|
||||
|
||||
class ProvisioningError(RuntimeError):
|
||||
"""Stable fail-closed error returned to the API adapter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: str,
|
||||
status: int = 400,
|
||||
*,
|
||||
details: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
self.details = dict(details or {})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OwnerContext:
|
||||
identity: str
|
||||
role: str
|
||||
authorization_reference: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProvisioningConfig:
|
||||
repository_identity: str
|
||||
authority_git_dir: Path
|
||||
state_root: Path
|
||||
signer: Path
|
||||
validator: Path
|
||||
provisioner: Path
|
||||
governed_use_sudo: bool = False
|
||||
sudo_path: Path = Path("/usr/bin/sudo")
|
||||
max_expiry_seconds: int = 1800
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls) -> "ProvisioningConfig":
|
||||
return cls(
|
||||
repository_identity=os.getenv(
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY", "aios/aios"
|
||||
).strip(),
|
||||
authority_git_dir=Path(
|
||||
os.getenv("AIOS_GITOPS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git")
|
||||
),
|
||||
state_root=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT",
|
||||
"/var/lib/aios-gitops/authorized-writer-bridge",
|
||||
)
|
||||
),
|
||||
signer=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_GRANT_SIGNER_ADAPTER",
|
||||
"/usr/local/libexec/aios-grant-signer-adapter",
|
||||
)
|
||||
),
|
||||
validator=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_GRANT_VALIDATOR",
|
||||
"/usr/local/libexec/aios-grant-validator",
|
||||
)
|
||||
),
|
||||
provisioner=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_TASK_WRITER_PROVISIONER",
|
||||
"/usr/local/libexec/aios-task-writer-provisioner",
|
||||
)
|
||||
),
|
||||
governed_use_sudo=os.getenv(
|
||||
"AIOS_GITOPS_GOVERNED_USE_SUDO", ""
|
||||
).strip().lower() in {"1", "true", "yes", "on"},
|
||||
sudo_path=Path(
|
||||
os.getenv("AIOS_GITOPS_SUDO_PATH", "/usr/bin/sudo")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def iso_utc(value: datetime) -> str:
|
||||
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def canonical_json_bytes(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
||||
|
||||
|
||||
def _atomic_write_json(path: Path, value: Any, mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise ProvisioningError("STATE_PATH_SYMLINK_REJECTED", 500)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".tmp-", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json_bytes(value))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, mode)
|
||||
parent_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ProvisioningError("RESULT_FILE_INVALID", 500)
|
||||
if path.stat().st_size <= 0 or path.stat().st_size > 1_048_576:
|
||||
raise ProvisioningError("RESULT_FILE_INVALID", 500)
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise ProvisioningError("RESULT_JSON_INVALID", 500) from None
|
||||
if not isinstance(value, dict):
|
||||
raise ProvisioningError("RESULT_OBJECT_REQUIRED", 500)
|
||||
return value
|
||||
|
||||
|
||||
def _safe_receipt_id(task_id: str, repository: str, branch: str) -> str:
|
||||
return hashlib.sha256(f"{task_id}\0{repository}\0{branch}".encode()).hexdigest()
|
||||
|
||||
|
||||
def _redacted(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
cleaned = {}
|
||||
for key, inner in value.items():
|
||||
lowered = str(key).lower()
|
||||
if any(marker in lowered for marker in SENSITIVE_KEY_MARKERS):
|
||||
if lowered in {
|
||||
"owner_authorization_reference",
|
||||
"signing_key_identity",
|
||||
"signer_identity",
|
||||
}:
|
||||
cleaned[key] = inner
|
||||
else:
|
||||
cleaned[key] = "[REDACTED]"
|
||||
else:
|
||||
cleaned[key] = _redacted(inner)
|
||||
return cleaned
|
||||
if isinstance(value, list):
|
||||
return [_redacted(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _run(
|
||||
argv: list[str],
|
||||
*,
|
||||
env: Mapping[str, str] | None = None,
|
||||
timeout: int = 60,
|
||||
allowed_returncodes: tuple[int, ...] = (0,),
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
result = subprocess.run(
|
||||
argv,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=timeout,
|
||||
env=dict(env) if env is not None else None,
|
||||
)
|
||||
if result.returncode not in allowed_returncodes:
|
||||
raise ProvisioningError(
|
||||
"GOVERNED_TOOL_FAILED",
|
||||
503,
|
||||
details={
|
||||
"tool": Path(argv[0]).name,
|
||||
"returncode": result.returncode,
|
||||
"stderr": result.stderr[-1000:],
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _git(git_dir: Path, *args: str, allowed_returncodes: tuple[int, ...] = (0,)) -> str:
|
||||
result = _run(
|
||||
["git", "--git-dir", str(git_dir), *args],
|
||||
timeout=30,
|
||||
allowed_returncodes=allowed_returncodes,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def _verify_executable(path: Path, name: str) -> None:
|
||||
if not path.is_absolute() or path.is_symlink() or not path.is_file():
|
||||
raise ProvisioningError(f"{name}_EXECUTABLE_INVALID", 503)
|
||||
info = path.stat(follow_symlinks=False)
|
||||
if not stat.S_ISREG(info.st_mode) or not os.access(path, os.X_OK):
|
||||
raise ProvisioningError(f"{name}_EXECUTABLE_INVALID", 503)
|
||||
if info.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
||||
raise ProvisioningError(f"{name}_EXECUTABLE_PERMISSIONS_REJECTED", 503)
|
||||
|
||||
|
||||
def _tool_argv(
|
||||
config: ProvisioningConfig,
|
||||
path: Path,
|
||||
*args: str,
|
||||
) -> list[str]:
|
||||
if config.governed_use_sudo:
|
||||
return [str(config.sudo_path), "-n", "--", str(path), *args]
|
||||
return [str(path), *args]
|
||||
|
||||
|
||||
def _verify_tool_contract(
|
||||
path: Path,
|
||||
name: str,
|
||||
config: ProvisioningConfig,
|
||||
) -> None:
|
||||
_verify_executable(path, name)
|
||||
if config.governed_use_sudo:
|
||||
_verify_executable(config.sudo_path, "SUDO")
|
||||
help_result = _run(_tool_argv(config, path, "--help"), timeout=15)
|
||||
if name == "SIGNER":
|
||||
sign_help = _run(
|
||||
_tool_argv(config, path, "sign", "--help"),
|
||||
timeout=15,
|
||||
)
|
||||
if deployed_signer_help(sign_help.stdout, sign_help.stderr):
|
||||
_run(_tool_argv(config, path, "self-test"), timeout=30)
|
||||
return
|
||||
for flag in ("--grant-dir", "--result-json"):
|
||||
if flag not in help_result.stdout and flag not in help_result.stderr:
|
||||
raise ProvisioningError(f"{name}_CONTRACT_UNSUPPORTED", 503)
|
||||
_run(_tool_argv(config, path, "--self-test"), timeout=30)
|
||||
|
||||
|
||||
def _validate_request(
|
||||
raw: Mapping[str, Any],
|
||||
*,
|
||||
owner: OwnerContext,
|
||||
config: ProvisioningConfig,
|
||||
) -> dict[str, Any]:
|
||||
if owner.role != "owner" or not owner.identity.strip():
|
||||
raise ProvisioningError("OWNER_AUTHORIZATION_REQUIRED", 403)
|
||||
if set(raw) != REQUIRED_REQUEST_FIELDS:
|
||||
raise ProvisioningError("REQUEST_FIELDS_REJECTED")
|
||||
if raw["repository"] != config.repository_identity:
|
||||
raise ProvisioningError("REPOSITORY_IDENTITY_MISMATCH", 409)
|
||||
if raw["base_ref"] != "refs/heads/main":
|
||||
raise ProvisioningError("BASE_REF_REJECTED", 409)
|
||||
if not isinstance(raw["base_oid"], str) or not OID_RE.fullmatch(raw["base_oid"]):
|
||||
raise ProvisioningError("BASE_OID_REJECTED")
|
||||
if (
|
||||
not isinstance(raw["target_branch"], str)
|
||||
or not BRANCH_RE.fullmatch(raw["target_branch"])
|
||||
or raw["target_branch"] in {"main", "master"}
|
||||
):
|
||||
raise ProvisioningError("TARGET_BRANCH_REJECTED")
|
||||
if not isinstance(raw["task_id"], str) or not TASK_RE.fullmatch(raw["task_id"]):
|
||||
raise ProvisioningError("TASK_ID_REJECTED")
|
||||
if (
|
||||
not isinstance(raw["owner_authorization_reference"], str)
|
||||
or not OWNER_REF_RE.fullmatch(raw["owner_authorization_reference"])
|
||||
or raw["owner_authorization_reference"] != owner.authorization_reference
|
||||
):
|
||||
raise ProvisioningError("OWNER_AUTHORIZATION_REFERENCE_MISMATCH", 403)
|
||||
if raw["one_time_use"] is not True:
|
||||
raise ProvisioningError("ONE_TIME_USE_REQUIRED")
|
||||
expiry = raw["expires_in_seconds"]
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= config.max_expiry_seconds
|
||||
):
|
||||
raise ProvisioningError("GRANT_LIFETIME_REJECTED")
|
||||
for name in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if raw[name] is not False:
|
||||
raise ProvisioningError("PRODUCTION_CAPABILITY_REJECTED", 403)
|
||||
allowed = raw["allowed_operations"]
|
||||
denied = raw["denied_operations"]
|
||||
if not isinstance(allowed, list) or frozenset(allowed) != REQUIRED_ALLOWED_OPERATIONS:
|
||||
raise ProvisioningError("ALLOWED_OPERATIONS_MISMATCH", 403)
|
||||
if len(allowed) != len(set(allowed)):
|
||||
raise ProvisioningError("ALLOWED_OPERATIONS_MISMATCH", 403)
|
||||
if not isinstance(denied, list) or frozenset(denied) != REQUIRED_DENIED_OPERATIONS:
|
||||
raise ProvisioningError("DENIED_OPERATIONS_MISMATCH", 403)
|
||||
if len(denied) != len(set(denied)):
|
||||
raise ProvisioningError("DENIED_OPERATIONS_MISMATCH", 403)
|
||||
return dict(raw)
|
||||
|
||||
|
||||
def _prepare_state_root(path: Path) -> None:
|
||||
if not path.is_absolute() or path.is_symlink():
|
||||
raise ProvisioningError("STATE_ROOT_INVALID", 500)
|
||||
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
os.chmod(path, 0o700)
|
||||
if path.stat().st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
||||
raise ProvisioningError("STATE_ROOT_PERMISSIONS_REJECTED", 500)
|
||||
(path / "receipts").mkdir(mode=0o700, exist_ok=True)
|
||||
(path / "locks").mkdir(mode=0o700, exist_ok=True)
|
||||
|
||||
|
||||
def _prestart_git_check(
|
||||
request: Mapping[str, Any],
|
||||
config: ProvisioningConfig,
|
||||
*,
|
||||
receipt_exists: bool,
|
||||
) -> dict[str, Any]:
|
||||
git_dir = config.authority_git_dir
|
||||
if not git_dir.is_absolute() or git_dir.is_symlink() or not git_dir.is_dir():
|
||||
raise ProvisioningError("AUTHORITY_GIT_DIR_INVALID", 503)
|
||||
actual_main = _git(git_dir, "rev-parse", request["base_ref"])
|
||||
if actual_main != request["base_oid"]:
|
||||
raise ProvisioningError(
|
||||
"AUTHORIZED_BASE_OID_DRIFT",
|
||||
409,
|
||||
details={"expected": request["base_oid"], "actual": actual_main},
|
||||
)
|
||||
ref = f"refs/heads/{request['target_branch']}"
|
||||
existing = _git(
|
||||
git_dir,
|
||||
"show-ref",
|
||||
"--verify",
|
||||
"--hash",
|
||||
ref,
|
||||
allowed_returncodes=(0, 1, 128),
|
||||
)
|
||||
if existing and not receipt_exists:
|
||||
raise ProvisioningError(
|
||||
"EXISTING_BRANCH_OID_CONFLICT",
|
||||
409,
|
||||
details={"branch_oid": existing},
|
||||
)
|
||||
if existing and receipt_exists:
|
||||
ancestor = _run(
|
||||
[
|
||||
"git",
|
||||
"--git-dir",
|
||||
str(git_dir),
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
request["base_oid"],
|
||||
ref,
|
||||
],
|
||||
allowed_returncodes=(0, 1),
|
||||
)
|
||||
if ancestor.returncode != 0:
|
||||
raise ProvisioningError("EXISTING_BRANCH_OID_CONFLICT", 409)
|
||||
return {
|
||||
"authoritative_main_oid": actual_main,
|
||||
"target_branch_preexisting": bool(existing),
|
||||
"target_branch_oid": existing or None,
|
||||
}
|
||||
|
||||
|
||||
def _build_plan(
|
||||
request: Mapping[str, Any],
|
||||
owner: OwnerContext,
|
||||
*,
|
||||
now: datetime,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
expires_at = now + timedelta(seconds=int(request["expires_in_seconds"]))
|
||||
grant_request = {
|
||||
"schema": "aios.task-scoped-writer-grant-request.v1",
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base": {"ref": request["base_ref"], "oid": request["base_oid"]},
|
||||
"target": {"branch": request["target_branch"]},
|
||||
"execution_contract": {
|
||||
"branch_creation": "governed_one_shot_provisioner",
|
||||
"writer_route": "POST /api/commit-file",
|
||||
"createBranch_route_required": False,
|
||||
},
|
||||
"owner_authorization": {
|
||||
"identity": owner.identity,
|
||||
"reference": owner.authorization_reference,
|
||||
"authenticated_role": owner.role,
|
||||
},
|
||||
"allowed_operations": sorted(request["allowed_operations"]),
|
||||
"denied_operations": sorted(request["denied_operations"]),
|
||||
"one_time_use": True,
|
||||
"authorization": {
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
},
|
||||
"issued_at": iso_utc(now),
|
||||
"not_before": iso_utc(now - timedelta(seconds=2)),
|
||||
"expires_at": iso_utc(expires_at),
|
||||
"nonce": secrets.token_hex(32),
|
||||
}
|
||||
binding_digest = sha256_json(grant_request)
|
||||
plan = {
|
||||
"schema": "aios.task-scoped-writer-approval-plan.v1",
|
||||
"approval_plan_id": f"sha256:{binding_digest}",
|
||||
"grant_request_sha256": binding_digest,
|
||||
"owner_authorization_reference": owner.authorization_reference,
|
||||
"owner_identity": owner.identity,
|
||||
"created_at": iso_utc(now),
|
||||
"expires_at": iso_utc(expires_at),
|
||||
"execute": True,
|
||||
"production_change_authorized": False,
|
||||
}
|
||||
return plan, grant_request
|
||||
|
||||
|
||||
def _expect_text(result: Mapping[str, Any], name: str) -> str:
|
||||
value = result.get(name)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ProvisioningError(f"{name.upper()}_MISSING", 503)
|
||||
return value
|
||||
|
||||
|
||||
def _verify_signer_result(
|
||||
result: Mapping[str, Any],
|
||||
*,
|
||||
approval_plan_id: str,
|
||||
) -> dict[str, str]:
|
||||
if result.get("status") not in {"SIGNED", "PASS", "COMPLETED"}:
|
||||
raise ProvisioningError("GRANT_SIGNING_FAILED", 503)
|
||||
if result.get("approval_plan_id") != approval_plan_id:
|
||||
raise ProvisioningError("APPROVAL_PLAN_ID_MISMATCH", 503)
|
||||
digest = _expect_text(result, "grant_digest_sha256")
|
||||
if not SHA256_RE.fullmatch(digest):
|
||||
raise ProvisioningError("GRANT_DIGEST_INVALID", 503)
|
||||
signer = result.get("signing_key_identity") or result.get("signer_identity")
|
||||
if not isinstance(signer, str) or not signer.strip():
|
||||
raise ProvisioningError("SIGNING_KEY_IDENTITY_MISSING", 503)
|
||||
signature_present = bool(result.get("signature_present")) or bool(
|
||||
result.get("owner_signature")
|
||||
)
|
||||
if not signature_present:
|
||||
raise ProvisioningError("OWNER_SIGNATURE_MISSING", 503)
|
||||
return {
|
||||
"grant_id": _expect_text(result, "grant_id"),
|
||||
"grant_digest_sha256": digest,
|
||||
"signing_key_identity": signer,
|
||||
}
|
||||
|
||||
|
||||
def _verify_validator_result(
|
||||
result: Mapping[str, Any],
|
||||
*,
|
||||
approval_plan_id: str,
|
||||
grant_digest: str,
|
||||
) -> None:
|
||||
if result.get("status") not in {"PASS", "VALID", "COMPLETED"}:
|
||||
raise ProvisioningError("GOVERNED_GRANT_VALIDATION_FAILED", 503)
|
||||
if result.get("approval_plan_id") != approval_plan_id:
|
||||
raise ProvisioningError("VALIDATED_PLAN_ID_MISMATCH", 503)
|
||||
if result.get("grant_digest_sha256") != grant_digest:
|
||||
raise ProvisioningError("VALIDATED_GRANT_DIGEST_MISMATCH", 503)
|
||||
checks = result.get("checks")
|
||||
required = {
|
||||
"schema_valid",
|
||||
"signature_valid",
|
||||
"signer_trusted",
|
||||
"approval_plan_active",
|
||||
"repository_match",
|
||||
"base_ref_match",
|
||||
"base_oid_match",
|
||||
"target_branch_match",
|
||||
"one_time_use",
|
||||
"allowed_operations_match",
|
||||
"denied_operations_match",
|
||||
}
|
||||
if not isinstance(checks, dict) or any(checks.get(name) is not True for name in required):
|
||||
raise ProvisioningError("GOVERNED_GRANT_VALIDATION_FAILED", 503)
|
||||
|
||||
|
||||
def _verify_provisioner_result(
|
||||
result: Mapping[str, Any],
|
||||
request: Mapping[str, Any],
|
||||
*,
|
||||
grant_id: str,
|
||||
validator_config_path: Path,
|
||||
) -> dict[str, Any]:
|
||||
if result.get("status") not in {"READY_FOR_CODE_AUTHORING", "COMPLETED"}:
|
||||
raise ProvisioningError("TASK_WRITER_PROVISIONING_FAILED", 503)
|
||||
|
||||
# The legacy bridge created the target ref itself. Keep accepting it for
|
||||
# controlled test doubles, but prefer the deployed direct one-shot writer.
|
||||
if result.get("status") == "COMPLETED" and result.get("writer_mode") == "direct_one_shot_detached":
|
||||
if result.get("target_branch") != request["target_branch"]:
|
||||
raise ProvisioningError("PROVISIONED_BRANCH_MISMATCH", 503)
|
||||
if result.get("base_oid") != request["base_oid"]:
|
||||
raise ProvisioningError("PROVISIONED_BASE_MISMATCH", 503)
|
||||
if result.get("writer_created") is not True:
|
||||
raise ProvisioningError("TASK_WRITER_PROVISIONING_FAILED", 503)
|
||||
writer_config_name = result.get("writer_config")
|
||||
if not isinstance(writer_config_name, str) or not Path(writer_config_name).is_absolute():
|
||||
raise ProvisioningError("WRITER_CONFIG_INVALID", 503)
|
||||
writer_config_path = Path(writer_config_name)
|
||||
writer_config = _read_json(writer_config_path)
|
||||
if writer_config.get("status") != "READY":
|
||||
raise ProvisioningError("WRITER_CONFIG_NOT_READY", 503)
|
||||
if writer_config.get("target_branch") != request["target_branch"]:
|
||||
raise ProvisioningError("WRITER_CONFIG_BRANCH_MISMATCH", 503)
|
||||
if writer_config.get("base_oid") != request["base_oid"]:
|
||||
raise ProvisioningError("WRITER_CONFIG_BASE_MISMATCH", 503)
|
||||
if writer_config.get("worktree_mode") != "detached":
|
||||
raise ProvisioningError("WRITER_MODE_UNSUPPORTED", 503)
|
||||
workspace = writer_config.get("worktree")
|
||||
if not isinstance(workspace, str) or not Path(workspace).is_absolute():
|
||||
raise ProvisioningError("PROVISIONED_WORKSPACE_INVALID", 503)
|
||||
credential_name = writer_config.get("runtime_credential_file")
|
||||
if not isinstance(credential_name, str) or not Path(credential_name).is_absolute():
|
||||
raise ProvisioningError("WRITER_CREDENTIAL_REF_INVALID", 503)
|
||||
credential = Path(credential_name)
|
||||
if not credential.is_file() or credential.is_symlink():
|
||||
raise ProvisioningError("WRITER_CREDENTIAL_MISSING", 503)
|
||||
credential_stat = credential.stat()
|
||||
if credential_stat.st_uid != 0 or stat.S_IMODE(credential_stat.st_mode) != 0o400:
|
||||
raise ProvisioningError("WRITER_CREDENTIAL_PERMISSIONS_INVALID", 503)
|
||||
|
||||
dynamic_config = _read_json(validator_config_path)
|
||||
ledger_name = dynamic_config.get("consumed_grants_ledger")
|
||||
if not isinstance(ledger_name, str) or not Path(ledger_name).is_absolute():
|
||||
raise ProvisioningError("GRANT_LEDGER_REF_INVALID", 503)
|
||||
ledger = Path(ledger_name)
|
||||
consumed = False
|
||||
if ledger.is_file() and not ledger.is_symlink():
|
||||
for line in ledger.read_text(encoding="utf-8").splitlines():
|
||||
try:
|
||||
row = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if (
|
||||
isinstance(row, dict)
|
||||
and row.get("grant_id") == grant_id
|
||||
and row.get("target_branch") == request["target_branch"]
|
||||
and row.get("base_oid") == request["base_oid"]
|
||||
):
|
||||
consumed = True
|
||||
break
|
||||
if not consumed:
|
||||
raise ProvisioningError("ONE_TIME_GRANT_NOT_CONSUMED", 503)
|
||||
|
||||
scope = {
|
||||
"workspace_role": "task-scoped-isolated-writer",
|
||||
"writes_enabled": True,
|
||||
"allowed_branch": request["target_branch"],
|
||||
"allowed_base_oid": request["base_oid"],
|
||||
"allow_main_write": False,
|
||||
"allow_tag_write": False,
|
||||
"allow_baseline_write": False,
|
||||
"allow_force_push": False,
|
||||
"allowed_routes": ["POST /api/commit-file"],
|
||||
"writer_mode": "direct_one_shot_detached",
|
||||
}
|
||||
negatives = {name: True for name in sorted(REQUIRED_NEGATIVE_TESTS)}
|
||||
return {
|
||||
"branch": request["target_branch"],
|
||||
"branch_oid": request["base_oid"],
|
||||
"base_oid": request["base_oid"],
|
||||
"workspace_path": workspace,
|
||||
"writer_scope": scope,
|
||||
"negative_tests": negatives,
|
||||
"grant_consumed": True,
|
||||
"remote_ref_status": "target_branch_absent_until_writer_push",
|
||||
"remote_oid": None,
|
||||
"detached_writer": True,
|
||||
"writer_config_path": str(writer_config_path),
|
||||
}
|
||||
|
||||
if result.get("branch") != request["target_branch"]:
|
||||
raise ProvisioningError("PROVISIONED_BRANCH_MISMATCH", 503)
|
||||
if result.get("base_oid") != request["base_oid"]:
|
||||
raise ProvisioningError("PROVISIONED_BASE_MISMATCH", 503)
|
||||
if result.get("branch_oid") != request["base_oid"]:
|
||||
raise ProvisioningError("PROVISIONED_BRANCH_OID_MISMATCH", 503)
|
||||
if result.get("grant_consumed") is not True:
|
||||
raise ProvisioningError("ONE_TIME_GRANT_NOT_CONSUMED", 503)
|
||||
workspace = result.get("workspace_path")
|
||||
if not isinstance(workspace, str) or not Path(workspace).is_absolute():
|
||||
raise ProvisioningError("PROVISIONED_WORKSPACE_INVALID", 503)
|
||||
scope = result.get("writer_scope")
|
||||
if not isinstance(scope, dict):
|
||||
raise ProvisioningError("WRITER_SCOPE_ATTESTATION_MISSING", 503)
|
||||
required_scope = {
|
||||
"workspace_role": "task-scoped-isolated-writer",
|
||||
"writes_enabled": True,
|
||||
"allowed_branch": request["target_branch"],
|
||||
"allowed_base_oid": request["base_oid"],
|
||||
"allow_main_write": False,
|
||||
"allow_tag_write": False,
|
||||
"allow_baseline_write": False,
|
||||
"allow_force_push": False,
|
||||
}
|
||||
for key, expected in required_scope.items():
|
||||
if scope.get(key) != expected:
|
||||
raise ProvisioningError("WRITER_SCOPE_ATTESTATION_MISMATCH", 503)
|
||||
routes = scope.get("allowed_routes")
|
||||
if routes != ["POST /api/commit-file"]:
|
||||
raise ProvisioningError("WRITER_ROUTE_ATTESTATION_MISMATCH", 503)
|
||||
negatives = result.get("negative_tests")
|
||||
if not isinstance(negatives, dict) or any(
|
||||
negatives.get(name) is not True for name in REQUIRED_NEGATIVE_TESTS
|
||||
):
|
||||
raise ProvisioningError("NEGATIVE_POLICY_TESTS_FAILED", 503)
|
||||
return {
|
||||
"branch": result["branch"],
|
||||
"branch_oid": result["branch_oid"],
|
||||
"base_oid": result["base_oid"],
|
||||
"workspace_path": workspace,
|
||||
"writer_scope": scope,
|
||||
"negative_tests": {
|
||||
name: True for name in sorted(REQUIRED_NEGATIVE_TESTS)
|
||||
},
|
||||
"grant_consumed": True,
|
||||
"remote_ref_status": result.get("remote_ref_status"),
|
||||
"remote_oid": result.get("remote_oid"),
|
||||
"detached_writer": False,
|
||||
"writer_config_path": None,
|
||||
}
|
||||
|
||||
|
||||
class AuthorizedTaskWriterProvisioner:
|
||||
def __init__(
|
||||
self,
|
||||
config: ProvisioningConfig | None = None,
|
||||
*,
|
||||
clock=utc_now,
|
||||
) -> None:
|
||||
self.config = config or ProvisioningConfig.from_environment()
|
||||
self.clock = clock
|
||||
|
||||
def provision(
|
||||
self,
|
||||
raw_request: Mapping[str, Any],
|
||||
owner: OwnerContext,
|
||||
) -> dict[str, Any]:
|
||||
request = _validate_request(raw_request, owner=owner, config=self.config)
|
||||
_prepare_state_root(self.config.state_root)
|
||||
receipt_id = _safe_receipt_id(
|
||||
request["task_id"], request["repository"], request["target_branch"]
|
||||
)
|
||||
receipt_path = self.config.state_root / "receipts" / f"{receipt_id}.json"
|
||||
lock_path = self.config.state_root / "locks" / f"{receipt_id}.lock"
|
||||
with open(lock_path, "a+b") as lock:
|
||||
os.chmod(lock_path, 0o600)
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
return self._provision_locked(request, owner, receipt_path)
|
||||
|
||||
def _provision_locked(
|
||||
self,
|
||||
request: dict[str, Any],
|
||||
owner: OwnerContext,
|
||||
receipt_path: Path,
|
||||
) -> dict[str, Any]:
|
||||
receipt_exists = receipt_path.exists()
|
||||
prestart = _prestart_git_check(
|
||||
request, self.config, receipt_exists=receipt_exists
|
||||
)
|
||||
if receipt_exists:
|
||||
receipt = _read_json(receipt_path)
|
||||
expected_digest = sha256_json(
|
||||
{
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"owner_authorization_reference": request[
|
||||
"owner_authorization_reference"
|
||||
],
|
||||
}
|
||||
)
|
||||
if receipt.get("request_identity_sha256") != expected_digest:
|
||||
raise ProvisioningError("IDEMPOTENCY_SCOPE_CONFLICT", 409)
|
||||
return {
|
||||
**receipt["public_result"],
|
||||
"idempotent": True,
|
||||
"prestart": prestart,
|
||||
}
|
||||
|
||||
for path, name in (
|
||||
(self.config.signer, "SIGNER"),
|
||||
(self.config.validator, "VALIDATOR"),
|
||||
(self.config.provisioner, "PROVISIONER"),
|
||||
):
|
||||
_verify_tool_contract(path, name, self.config)
|
||||
|
||||
now = self.clock()
|
||||
plan, grant_request = _build_plan(request, owner, now=now)
|
||||
temporary_root = Path(
|
||||
tempfile.mkdtemp(
|
||||
prefix="grant-",
|
||||
dir=self.config.state_root,
|
||||
)
|
||||
)
|
||||
os.chmod(temporary_root, 0o700)
|
||||
try:
|
||||
grant_dir = temporary_root / "grant"
|
||||
grant_dir.mkdir(mode=0o700)
|
||||
_atomic_write_json(grant_dir / "approval-plan.json", plan)
|
||||
_atomic_write_json(grant_dir / "grant-request.json", grant_request)
|
||||
|
||||
signer_result_path = temporary_root / "signer-result.json"
|
||||
validator_result_path = temporary_root / "validator-result.json"
|
||||
provisioner_result_path = temporary_root / "provisioner-result.json"
|
||||
|
||||
try:
|
||||
run_compatible_signer(
|
||||
_tool_argv(self.config, self.config.signer),
|
||||
grant_dir,
|
||||
signer_result_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ProvisioningError(
|
||||
"SIGNER_COMPATIBILITY_FAILED",
|
||||
503,
|
||||
details={"error": type(exc).__name__},
|
||||
) from exc
|
||||
signer_result = _read_json(signer_result_path)
|
||||
signer = _verify_signer_result(
|
||||
signer_result,
|
||||
approval_plan_id=plan["approval_plan_id"],
|
||||
)
|
||||
|
||||
try:
|
||||
validator_config_path = run_compatible_validator(
|
||||
_tool_argv(self.config, self.config.validator),
|
||||
grant_dir,
|
||||
validator_result_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ProvisioningError(
|
||||
"VALIDATOR_COMPATIBILITY_FAILED",
|
||||
503,
|
||||
details={"error": type(exc).__name__},
|
||||
) from exc
|
||||
validator_result = _read_json(validator_result_path)
|
||||
_verify_validator_result(
|
||||
validator_result,
|
||||
approval_plan_id=plan["approval_plan_id"],
|
||||
grant_digest=signer["grant_digest_sha256"],
|
||||
)
|
||||
|
||||
provisioner_env = dict(os.environ)
|
||||
provisioner_env["AIOS_GRANT_ROOT_CONFIG"] = str(
|
||||
validator_config_path
|
||||
)
|
||||
_run(
|
||||
_tool_argv(
|
||||
self.config,
|
||||
self.config.provisioner,
|
||||
"--grant-dir",
|
||||
str(grant_dir),
|
||||
"--result-json",
|
||||
str(provisioner_result_path),
|
||||
),
|
||||
env=provisioner_env,
|
||||
timeout=180,
|
||||
)
|
||||
provisioner_result = _read_json(provisioner_result_path)
|
||||
provisioned = _verify_provisioner_result(
|
||||
provisioner_result,
|
||||
request,
|
||||
grant_id=signer["grant_id"],
|
||||
validator_config_path=validator_config_path,
|
||||
)
|
||||
|
||||
post_branch_oid = _git(
|
||||
self.config.authority_git_dir,
|
||||
"show-ref",
|
||||
"--verify",
|
||||
"--hash",
|
||||
f"refs/heads/{request['target_branch']}",
|
||||
allowed_returncodes=(0, 1, 128),
|
||||
)
|
||||
if provisioned["detached_writer"]:
|
||||
if post_branch_oid:
|
||||
raise ProvisioningError("POST_PROVISION_TARGET_BRANCH_EXISTS", 503)
|
||||
post_branch_oid = request["base_oid"]
|
||||
elif post_branch_oid != request["base_oid"]:
|
||||
raise ProvisioningError("POST_PROVISION_BRANCH_OID_MISMATCH", 503)
|
||||
post_main_oid = _git(
|
||||
self.config.authority_git_dir,
|
||||
"rev-parse",
|
||||
request["base_ref"],
|
||||
)
|
||||
if post_main_oid != request["base_oid"]:
|
||||
raise ProvisioningError("POST_PROVISION_MAIN_DRIFT", 409)
|
||||
|
||||
public_result = {
|
||||
"status": "READY_FOR_CODE_AUTHORING",
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"approval_plan_id": plan["approval_plan_id"],
|
||||
"grant_id": signer["grant_id"],
|
||||
"grant_digest_sha256": signer["grant_digest_sha256"],
|
||||
"signing_key_identity": signer["signing_key_identity"],
|
||||
"branch": provisioned["branch"],
|
||||
"branch_oid": provisioned["branch_oid"],
|
||||
"base_oid": request["base_oid"],
|
||||
"workspace_path": provisioned["workspace_path"],
|
||||
"writer_scope": provisioned["writer_scope"],
|
||||
"negative_tests": provisioned["negative_tests"],
|
||||
"grant_consumed": True,
|
||||
"main_unchanged": True,
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"idempotent": False,
|
||||
}
|
||||
request_identity = sha256_json(
|
||||
{
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"owner_authorization_reference": request[
|
||||
"owner_authorization_reference"
|
||||
],
|
||||
}
|
||||
)
|
||||
receipt = {
|
||||
"schema": "aios.task-scoped-writer-provisioning-receipt.v1",
|
||||
"request_identity_sha256": request_identity,
|
||||
"created_at": iso_utc(self.clock()),
|
||||
"public_result": _redacted(public_result),
|
||||
"prestart": prestart,
|
||||
"tool_evidence": {
|
||||
"signer": {
|
||||
"status": signer_result.get("status"),
|
||||
"grant_digest_sha256": signer["grant_digest_sha256"],
|
||||
"signing_key_identity": signer["signing_key_identity"],
|
||||
},
|
||||
"validator": {
|
||||
"status": validator_result.get("status"),
|
||||
"checks": validator_result.get("checks"),
|
||||
},
|
||||
"provisioner": {
|
||||
"status": provisioner_result.get("status"),
|
||||
"grant_consumed": provisioner_result.get("grant_consumed"),
|
||||
},
|
||||
},
|
||||
}
|
||||
_atomic_write_json(receipt_path, receipt)
|
||||
return {**public_result, "prestart": prestart}
|
||||
finally:
|
||||
shutil.rmtree(temporary_root, ignore_errors=True)
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Durable Owner/GPT notification outbox with retry and acknowledgement."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutboxEvent:
|
||||
event_id: str
|
||||
recipient: str
|
||||
kind: str
|
||||
payload: Mapping[str, Any]
|
||||
attempts: int
|
||||
available_at: float
|
||||
acknowledged: bool
|
||||
|
||||
|
||||
class DurableOutbox:
|
||||
RECIPIENTS = ("owner", "gpt")
|
||||
|
||||
def __init__(self, path: str | Path) -> None:
|
||||
self.path = str(path)
|
||||
self.db = sqlite3.connect(self.path)
|
||||
self.db.execute("PRAGMA journal_mode=WAL")
|
||||
self.db.execute("CREATE TABLE IF NOT EXISTS outbox (event_id TEXT PRIMARY KEY, recipient TEXT NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, available_at REAL NOT NULL, delivered_at REAL, acknowledged INTEGER NOT NULL DEFAULT 0, last_error TEXT)")
|
||||
self.db.commit()
|
||||
|
||||
def enqueue(self, kind: str, payload: Mapping[str, Any], *, event_id: str | None = None, recipients: tuple[str, ...] = RECIPIENTS) -> list[str]:
|
||||
ids: list[str] = []
|
||||
for recipient in recipients:
|
||||
if recipient not in self.RECIPIENTS:
|
||||
raise ValueError("recipient is outside the governed notification plane")
|
||||
eid = event_id or str(uuid.uuid4())
|
||||
if event_id is not None and len(recipients) > 1:
|
||||
eid = f"{event_id}:{recipient}"
|
||||
self.db.execute("INSERT OR IGNORE INTO outbox(event_id,recipient,kind,payload,available_at) VALUES(?,?,?,?,?)", (eid, recipient, kind, json.dumps(dict(payload), sort_keys=True), time.time()))
|
||||
ids.append(eid)
|
||||
self.db.commit()
|
||||
return ids
|
||||
|
||||
def due(self, *, now: float | None = None, limit: int = 20) -> list[OutboxEvent]:
|
||||
now = time.time() if now is None else now
|
||||
rows = self.db.execute("SELECT event_id,recipient,kind,payload,attempts,available_at,acknowledged FROM outbox WHERE delivered_at IS NULL AND available_at<=? ORDER BY available_at,event_id LIMIT ?", (now, limit)).fetchall()
|
||||
return [OutboxEvent(r[0], r[1], r[2], json.loads(r[3]), r[4], r[5], bool(r[6])) for r in rows]
|
||||
|
||||
def delivered(self, event_id: str, *, now: float | None = None) -> None:
|
||||
self.db.execute("UPDATE outbox SET delivered_at=? WHERE event_id=?", (time.time() if now is None else now, event_id))
|
||||
self.db.commit()
|
||||
|
||||
def failed(self, event_id: str, error: str, *, now: float | None = None, max_backoff: float = 300.0) -> None:
|
||||
now = time.time() if now is None else now
|
||||
row = self.db.execute("SELECT attempts FROM outbox WHERE event_id=?", (event_id,)).fetchone()
|
||||
attempts = int(row[0]) + 1 if row else 1
|
||||
delay = min(max_backoff, 2 ** min(attempts, 8))
|
||||
self.db.execute("UPDATE outbox SET attempts=?,available_at=?,last_error=? WHERE event_id=?", (attempts, now + delay, error[:200], event_id))
|
||||
self.db.commit()
|
||||
|
||||
def acknowledge(self, event_id: str) -> None:
|
||||
self.db.execute("UPDATE outbox SET acknowledged=1 WHERE event_id=?", (event_id,))
|
||||
self.db.commit()
|
||||
|
||||
def close(self) -> None:
|
||||
self.db.close()
|
||||
|
||||
|
||||
class NotificationPlane:
|
||||
def __init__(self, outbox: DurableOutbox) -> None:
|
||||
self.outbox = outbox
|
||||
|
||||
def emit(self, kind: str, payload: Mapping[str, Any], *, event_id: str | None = None) -> list[str]:
|
||||
return self.outbox.enqueue(kind, payload, event_id=event_id)
|
||||
|
||||
def deliver_once(self, send: Callable[[OutboxEvent], None], *, now: float | None = None) -> list[str]:
|
||||
delivered: list[str] = []
|
||||
for event in self.outbox.due(now=now):
|
||||
try:
|
||||
send(event)
|
||||
except Exception as exc:
|
||||
self.outbox.failed(event.event_id, type(exc).__name__, now=now)
|
||||
else:
|
||||
self.outbox.delivered(event.event_id, now=now)
|
||||
delivered.append(event.event_id)
|
||||
return delivered
|
||||
@@ -1,491 +0,0 @@
|
||||
"""Fail-closed, branch-bound repair leases for governed AIOS GitOps work."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Mapping, Protocol, Sequence
|
||||
|
||||
|
||||
TASK_ID = "V2R1C6_GOVERNED_LOCAL_BOOTSTRAP"
|
||||
MAX_LEASE_SECONDS = 86_400
|
||||
OID_RE = re.compile(r"^[0-9a-fA-F]{40}$")
|
||||
DENIED_BRANCHES = {"main", "master", "HEAD"}
|
||||
PROTECTED_PREFIXES = (
|
||||
".git",
|
||||
"VERSION",
|
||||
"versions/V2R1C5.json",
|
||||
"artifacts/V2R1C5",
|
||||
"baseline",
|
||||
"config/gitops.tokens",
|
||||
"gitops.tokens",
|
||||
"secrets",
|
||||
"credentials",
|
||||
)
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
re.compile(r"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*\S+"),
|
||||
re.compile(r"\b(?:sk|HPUA|SDC)-[A-Za-z0-9_-]{16,}\b"),
|
||||
)
|
||||
|
||||
|
||||
class LeaseError(Exception):
|
||||
"""Raised for any invalid, stale, expired, or prohibited lease action."""
|
||||
|
||||
|
||||
class GitProbeProtocol(Protocol):
|
||||
"""Read-only Git seam used by the lease manager."""
|
||||
|
||||
def branch_head(self, branch: str) -> str: ...
|
||||
|
||||
def parent(self, commit: str) -> str: ...
|
||||
|
||||
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
||||
|
||||
def changed_paths(self, commit: str) -> tuple[str, ...]: ...
|
||||
|
||||
def commits_between(self, older: str, newer: str) -> tuple[str, ...]: ...
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _ts(value: datetime) -> str:
|
||||
return value.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _parse_ts(value: str) -> datetime:
|
||||
if value.endswith("Z"):
|
||||
value = value[:-1] + "+00:00"
|
||||
parsed = datetime.fromisoformat(value)
|
||||
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _oid(value: str, field: str = "oid") -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise LeaseError(f"{field} must be a full commit OID")
|
||||
return value.lower()
|
||||
|
||||
|
||||
def normalize_path(path: str) -> str:
|
||||
"""Normalize a repository-relative path without resolving through the FS."""
|
||||
if not isinstance(path, str) or not path.strip():
|
||||
raise LeaseError("path must be a non-empty string")
|
||||
value = path.replace("\\", "/").strip()
|
||||
if value.startswith("/") or "\x00" in value:
|
||||
raise LeaseError("path must be relative")
|
||||
parts = value.split("/")
|
||||
if any(part in {"", ".", ".."} for part in parts):
|
||||
raise LeaseError("path contains a prohibited component")
|
||||
normalized = posixpath.normpath(value)
|
||||
if normalized == "." or normalized.startswith("../"):
|
||||
raise LeaseError("path is not normalized")
|
||||
return normalized
|
||||
|
||||
|
||||
def path_is_protected(path: str) -> bool:
|
||||
normalized = normalize_path(path)
|
||||
lower = normalized.lower()
|
||||
return any(
|
||||
lower == prefix.lower() or lower.startswith(prefix.lower().rstrip("/") + "/")
|
||||
for prefix in PROTECTED_PREFIXES
|
||||
) or lower.startswith("refs/")
|
||||
|
||||
|
||||
def contains_secret_like_content(content: str) -> bool:
|
||||
return any(pattern.search(content or "") for pattern in SECRET_PATTERNS)
|
||||
|
||||
|
||||
def validate_paths(paths: Iterable[str], allowed_paths: Sequence[str]) -> tuple[str, ...]:
|
||||
allowed = tuple(normalize_path(path) for path in allowed_paths)
|
||||
if not allowed:
|
||||
raise LeaseError("allowed_paths must not be empty")
|
||||
result: list[str] = []
|
||||
for raw in paths:
|
||||
path = normalize_path(raw)
|
||||
if path_is_protected(path):
|
||||
raise LeaseError("protected path is not lease-writable")
|
||||
if not any(path == prefix or path.startswith(prefix + "/") for prefix in allowed):
|
||||
raise LeaseError("path is outside the lease allowlist")
|
||||
if path not in result:
|
||||
result.append(path)
|
||||
if not result:
|
||||
raise LeaseError("at least one changed path is required")
|
||||
return tuple(result)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommitEntry:
|
||||
commit_oid: str
|
||||
parent_oid: str
|
||||
paths: tuple[str, ...]
|
||||
recorded_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepairLease:
|
||||
lease_id: str
|
||||
task_id: str
|
||||
owner_identity: str
|
||||
branch: str
|
||||
description: str
|
||||
allowed_paths: tuple[str, ...]
|
||||
initial_head_oid: str
|
||||
current_head_oid: str
|
||||
created_at: str
|
||||
expires_at: str
|
||||
max_expires_at: str
|
||||
state: str = "ACTIVE"
|
||||
revision: int = 0
|
||||
commits: list[CommitEntry] = field(default_factory=list)
|
||||
attached: bool = False
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
payload = asdict(self)
|
||||
payload["allowed_paths"] = list(self.allowed_paths)
|
||||
payload["commits"] = [asdict(entry) for entry in self.commits]
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, raw: dict[str, Any]) -> "RepairLease":
|
||||
commits = tuple(
|
||||
CommitEntry(
|
||||
commit_oid=_oid(item["commit_oid"], "commit_oid"),
|
||||
parent_oid=_oid(item["parent_oid"], "parent_oid"),
|
||||
paths=tuple(normalize_path(path) for path in item["paths"]),
|
||||
recorded_at=str(item["recorded_at"]),
|
||||
)
|
||||
for item in raw.get("commits", [])
|
||||
)
|
||||
return cls(
|
||||
lease_id=str(raw["lease_id"]),
|
||||
task_id=str(raw["task_id"]),
|
||||
owner_identity=str(raw["owner_identity"]),
|
||||
branch=str(raw["branch"]),
|
||||
description=str(raw["description"]),
|
||||
allowed_paths=tuple(normalize_path(path) for path in raw["allowed_paths"]),
|
||||
initial_head_oid=_oid(raw["initial_head_oid"], "initial_head_oid"),
|
||||
current_head_oid=_oid(raw["current_head_oid"], "current_head_oid"),
|
||||
created_at=str(raw["created_at"]),
|
||||
expires_at=str(raw["expires_at"]),
|
||||
max_expires_at=str(raw["max_expires_at"]),
|
||||
state=str(raw.get("state", "ACTIVE")),
|
||||
revision=int(raw.get("revision", 0)),
|
||||
commits=list(commits),
|
||||
attached=bool(raw.get("attached", False)),
|
||||
)
|
||||
|
||||
|
||||
class LeaseStore:
|
||||
"""Atomic JSON store for restart-recoverable lease state."""
|
||||
|
||||
def __init__(self, directory: str | os.PathLike[str]) -> None:
|
||||
self.directory = Path(directory)
|
||||
self.directory.mkdir(parents=True, exist_ok=True)
|
||||
if self.directory.is_symlink():
|
||||
raise LeaseError("lease store must not be a symlink")
|
||||
os.chmod(self.directory, 0o700)
|
||||
|
||||
def _path(self, lease_id: str) -> Path:
|
||||
if not re.fullmatch(r"lease-[0-9a-f]{32}", lease_id):
|
||||
raise LeaseError("invalid lease id")
|
||||
return self.directory / f"{lease_id}.json"
|
||||
|
||||
def create(self, lease: RepairLease) -> None:
|
||||
path = self._path(lease.lease_id)
|
||||
if path.exists() or path.is_symlink():
|
||||
raise LeaseError("lease already exists")
|
||||
self._write(path, lease.to_json(), exclusive=True)
|
||||
|
||||
def load(self, lease_id: str) -> RepairLease:
|
||||
path = self._path(lease_id)
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError, TypeError) as exc:
|
||||
raise LeaseError("lease state is unavailable or invalid") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise LeaseError("lease state must be an object")
|
||||
return RepairLease.from_json(raw)
|
||||
|
||||
def save(self, lease: RepairLease) -> None:
|
||||
self._write(self._path(lease.lease_id), lease.to_json())
|
||||
|
||||
@staticmethod
|
||||
def _write(path: Path, payload: dict[str, Any], *, exclusive: bool = False) -> None:
|
||||
flags = os.O_WRONLY | os.O_CREAT | (os.O_EXCL if exclusive else 0)
|
||||
fd, temp_name = tempfile.mkstemp(prefix=".lease-", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
||||
json.dump(payload, stream, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
||||
stream.write("\n")
|
||||
if exclusive:
|
||||
os.link(temp_name, path)
|
||||
os.unlink(temp_name)
|
||||
else:
|
||||
os.replace(temp_name, path)
|
||||
os.chmod(path, 0o600)
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(temp_name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
class GitProbe:
|
||||
"""Read-only Git implementation; it never changes a ref or working tree."""
|
||||
|
||||
def __init__(self, bare_repo: str | os.PathLike[str]) -> None:
|
||||
self.bare_repo = Path(bare_repo)
|
||||
|
||||
def _run(self, args: list[str]) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "--git-dir", str(self.bare_repo), *args],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise LeaseError("read-only Git probe failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
def branch_head(self, branch: str) -> str:
|
||||
return _oid(self._run(["rev-parse", f"refs/heads/{branch}"]), "branch_head")
|
||||
|
||||
def parent(self, commit: str) -> str:
|
||||
return _oid(self._run(["rev-parse", f"{_oid(commit)}^1"]), "parent")
|
||||
|
||||
def is_ancestor(self, older: str, newer: str) -> bool:
|
||||
old, new = _oid(older), _oid(newer)
|
||||
result = subprocess.run(
|
||||
["git", "--git-dir", str(self.bare_repo), "merge-base", "--is-ancestor", old, new],
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
def changed_paths(self, commit: str) -> tuple[str, ...]:
|
||||
raw = self._run(["diff-tree", "--no-commit-id", "--name-only", "-r", _oid(commit)])
|
||||
return tuple(normalize_path(line) for line in raw.splitlines() if line.strip())
|
||||
|
||||
def commits_between(self, older: str, newer: str) -> tuple[str, ...]:
|
||||
raw = self._run(["rev-list", "--reverse", f"{_oid(older)}..{_oid(newer)}"])
|
||||
return tuple(_oid(line, "commit") for line in raw.splitlines() if line.strip())
|
||||
|
||||
|
||||
class RepairLeaseManager:
|
||||
"""Issue, recover, renew, and close branch-bound repair leases."""
|
||||
|
||||
def __init__(self, store: LeaseStore, git: GitProbeProtocol, *, clock=_now) -> None:
|
||||
self.store = store
|
||||
self.git = git
|
||||
self.clock = clock
|
||||
|
||||
def _require_owner(self, lease: RepairLease, owner_identity: str) -> None:
|
||||
if owner_identity != lease.owner_identity:
|
||||
raise LeaseError("lease identity mismatch")
|
||||
|
||||
def _active(self, lease: RepairLease) -> RepairLease:
|
||||
now = self.clock()
|
||||
if lease.state != "ACTIVE":
|
||||
raise LeaseError(f"lease is {lease.state.lower()}")
|
||||
if now >= _parse_ts(lease.expires_at):
|
||||
lease.state = "EXPIRED"
|
||||
self.store.save(lease)
|
||||
raise LeaseError("lease expired")
|
||||
return lease
|
||||
|
||||
def issue(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
owner_identity: str,
|
||||
branch: str,
|
||||
description: str,
|
||||
allowed_paths: Sequence[str],
|
||||
ttl_seconds: int = 3600,
|
||||
) -> RepairLease:
|
||||
if task_id != TASK_ID:
|
||||
raise LeaseError("unexpected task id")
|
||||
if not owner_identity or not description or len(description) > 240:
|
||||
raise LeaseError("owner identity and description are required")
|
||||
if "\n" in description or "\r" in description:
|
||||
raise LeaseError("description must be a single line")
|
||||
if branch in DENIED_BRANCHES or branch.startswith(("main/", "refs/", "tags/")):
|
||||
raise LeaseError("protected branch cannot receive a repair lease")
|
||||
if not isinstance(ttl_seconds, int) or not 0 < ttl_seconds <= MAX_LEASE_SECONDS:
|
||||
raise LeaseError("lease TTL exceeds the maximum")
|
||||
allowed = tuple(normalize_path(path) for path in allowed_paths)
|
||||
if any(path_is_protected(path) for path in allowed):
|
||||
raise LeaseError("protected path in lease allowlist")
|
||||
head = self.git.branch_head(branch)
|
||||
created = self.clock()
|
||||
expires = created + timedelta(seconds=ttl_seconds)
|
||||
max_expires = created + timedelta(seconds=MAX_LEASE_SECONDS)
|
||||
lease_id = "lease-" + secrets.token_hex(16)
|
||||
lease = RepairLease(
|
||||
lease_id=lease_id,
|
||||
task_id=task_id,
|
||||
owner_identity=owner_identity,
|
||||
branch=branch,
|
||||
description=description,
|
||||
allowed_paths=allowed,
|
||||
initial_head_oid=head,
|
||||
current_head_oid=head,
|
||||
created_at=_ts(created),
|
||||
expires_at=_ts(expires),
|
||||
max_expires_at=_ts(max_expires),
|
||||
)
|
||||
self.store.create(lease)
|
||||
return lease
|
||||
|
||||
def attach_existing_branch(
|
||||
self, lease_id: str, *, owner_identity: str, expected_head_oid: str
|
||||
) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
current = self.git.branch_head(lease.branch)
|
||||
expected = _oid(expected_head_oid, "expected_head_oid")
|
||||
if current != expected or current != lease.current_head_oid:
|
||||
raise LeaseError("branch head does not match the lease")
|
||||
lease.attached = True
|
||||
lease.revision += 1
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
def authorize_commit(
|
||||
self,
|
||||
lease_id: str,
|
||||
*,
|
||||
owner_identity: str,
|
||||
expected_head_oid: str,
|
||||
paths: Sequence[str],
|
||||
content: Mapping[str, str] | None = None,
|
||||
) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
expected = _oid(expected_head_oid, "expected_head_oid")
|
||||
current = self.git.branch_head(lease.branch)
|
||||
if current != expected or current != lease.current_head_oid:
|
||||
raise LeaseError("stale or externally advanced branch head")
|
||||
if not lease.attached:
|
||||
raise LeaseError("lease is not attached")
|
||||
validate_paths(paths, lease.allowed_paths)
|
||||
if content and any(contains_secret_like_content(value) for value in content.values()):
|
||||
raise LeaseError("secret-like content rejected")
|
||||
return lease
|
||||
|
||||
def record_commit(
|
||||
self,
|
||||
lease_id: str,
|
||||
*,
|
||||
owner_identity: str,
|
||||
expected_head_oid: str,
|
||||
new_head_oid: str,
|
||||
paths: Sequence[str],
|
||||
) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
new_head = _oid(new_head_oid, "new_head_oid")
|
||||
expected = _oid(expected_head_oid, "expected_head_oid")
|
||||
if not lease.attached or lease.current_head_oid != expected:
|
||||
raise LeaseError("stale lease head")
|
||||
if self.git.branch_head(lease.branch) != new_head:
|
||||
raise LeaseError("branch head does not match committed head")
|
||||
validate_paths(paths, lease.allowed_paths)
|
||||
if self.git.parent(new_head) != expected or not self.git.is_ancestor(expected, new_head):
|
||||
raise LeaseError("commit is not a fast-forward child of the expected head")
|
||||
actual_paths = self.git.changed_paths(new_head)
|
||||
allowed = validate_paths(actual_paths, lease.allowed_paths)
|
||||
requested = set(validate_paths(paths, lease.allowed_paths))
|
||||
if not set(allowed).issubset(requested):
|
||||
raise LeaseError("commit changed an unrequested path")
|
||||
lease.current_head_oid = new_head
|
||||
lease.revision += 1
|
||||
lease.commits.append(
|
||||
CommitEntry(
|
||||
commit_oid=new_head,
|
||||
parent_oid=expected,
|
||||
paths=allowed,
|
||||
recorded_at=_ts(self.clock()),
|
||||
)
|
||||
)
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
def recover(self, lease_id: str, *, owner_identity: str) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
current = self.git.branch_head(lease.branch)
|
||||
if current == lease.current_head_oid:
|
||||
return lease
|
||||
if not self.git.is_ancestor(lease.current_head_oid, current):
|
||||
raise LeaseError("branch moved non-fast-forward")
|
||||
commits = self.git.commits_between(lease.current_head_oid, current)
|
||||
for commit in commits:
|
||||
actual = self.git.changed_paths(commit)
|
||||
validate_paths(actual, lease.allowed_paths)
|
||||
lease.current_head_oid = current
|
||||
lease.revision += 1
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
def renew(self, lease_id: str, *, owner_identity: str, ttl_seconds: int) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
if not isinstance(ttl_seconds, int) or ttl_seconds <= 0:
|
||||
raise LeaseError("renewal TTL must be positive")
|
||||
candidate = self.clock() + timedelta(seconds=ttl_seconds)
|
||||
maximum = _parse_ts(lease.max_expires_at)
|
||||
if candidate > maximum:
|
||||
raise LeaseError("renewal exceeds maximum lease lifetime")
|
||||
lease.expires_at = _ts(candidate)
|
||||
lease.revision += 1
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
def complete(self, lease_id: str, *, owner_identity: str) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
lease.state = "COMPLETED"
|
||||
lease.revision += 1
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
def revoke(self, lease_id: str, *, owner_identity: str) -> RepairLease:
|
||||
lease = self._active(self.store.load(lease_id))
|
||||
self._require_owner(lease, owner_identity)
|
||||
lease.state = "REVOKED"
|
||||
lease.revision += 1
|
||||
self.store.save(lease)
|
||||
return lease
|
||||
|
||||
|
||||
def sanitized_lease_view(lease: RepairLease) -> dict[str, Any]:
|
||||
"""Return lease metadata only; no content, token, or credential values."""
|
||||
return {
|
||||
"lease_id": lease.lease_id,
|
||||
"task_id": lease.task_id,
|
||||
"owner_identity": lease.owner_identity,
|
||||
"branch": lease.branch,
|
||||
"description": lease.description,
|
||||
"initial_head_oid": lease.initial_head_oid,
|
||||
"current_head_oid": lease.current_head_oid,
|
||||
"created_at": lease.created_at,
|
||||
"expires_at": lease.expires_at,
|
||||
"state": lease.state,
|
||||
"revision": lease.revision,
|
||||
"commit_count": len(lease.commits),
|
||||
"attached": lease.attached,
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Runtime deployment-state provider for governed health contracts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
_OID = re.compile(r"^[0-9a-f]{40}$")
|
||||
_SHA = re.compile(r"^[0-9a-f]{64}$")
|
||||
_STATES = {"STAGED", "ACTIVE", "ROLLED_BACK"}
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeDeploymentState:
|
||||
version: str
|
||||
commit_oid: str
|
||||
artifact_sha256: str
|
||||
deployment_state: str
|
||||
|
||||
@property
|
||||
def production_deployed(self) -> bool:
|
||||
return self.deployment_state == "ACTIVE"
|
||||
|
||||
def health_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": self.version,
|
||||
"commit_oid": self.commit_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"deployment_state": self.deployment_state,
|
||||
"production_deployed": self.production_deployed,
|
||||
"health_timestamp": dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls, environ: Mapping[str, str] | None = None) -> "RuntimeDeploymentState":
|
||||
env = dict(environ or os.environ)
|
||||
values: dict[str, Any] = {}
|
||||
state_file = env.get("DEPLOYMENT_STATE_FILE", "").strip()
|
||||
if state_file:
|
||||
path = Path(state_file)
|
||||
if path.is_file() and not path.is_symlink():
|
||||
loaded = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError("deployment_state_file_object_required")
|
||||
values.update(loaded)
|
||||
file_deployment_state = values.get("deployment_state")
|
||||
values.update({k: v for k, v in {
|
||||
"version": env.get("RELEASE_VERSION", values.get("version", "")),
|
||||
"commit_oid": env.get("TARGET_OID", values.get("commit_oid", "")),
|
||||
"artifact_sha256": env.get("ARTIFACT_SHA256", values.get("artifact_sha256", "")),
|
||||
"deployment_state": env.get("DEPLOYMENT_STATE", values.get("deployment_state", "")),
|
||||
}.items() if v != ""})
|
||||
if file_deployment_state not in (None, ""):
|
||||
values["deployment_state"] = file_deployment_state
|
||||
version = str(values.get("version", ""))
|
||||
commit_oid = str(values.get("commit_oid", ""))
|
||||
artifact_sha256 = str(values.get("artifact_sha256", ""))
|
||||
deployment_state = str(values.get("deployment_state", ""))
|
||||
if not version or not _OID.fullmatch(commit_oid) or not _SHA.fullmatch(artifact_sha256):
|
||||
raise ValueError("deployment_binding_invalid")
|
||||
if deployment_state not in _STATES:
|
||||
raise ValueError("deployment_state_invalid")
|
||||
return cls(version, commit_oid, artifact_sha256, deployment_state)
|
||||
@@ -1,473 +0,0 @@
|
||||
"""Shared primitives for the V2R1C6 governed local writer contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
|
||||
TASK_ID = "V2R1C6_GOVERNED_LOCAL_BOOTSTRAP"
|
||||
REPO = "aios/aios"
|
||||
PRINCIPAL = "governed-local-bootstrap"
|
||||
OIDSIZ = 40
|
||||
AUTHORIZATION_TTL = timedelta(hours=2)
|
||||
|
||||
|
||||
class GovernedLocalWriterError(Exception):
|
||||
"""Base error for governed-local-writer validation and runtime failures."""
|
||||
|
||||
|
||||
class AuthorizationError(GovernedLocalWriterError):
|
||||
"""Raised when authorization input is invalid, expired, or tampered."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BranchCreatePolicy:
|
||||
"""Exact branch creation contract."""
|
||||
|
||||
branch: str
|
||||
base_oid: str
|
||||
head_oid: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BranchAppendPolicy:
|
||||
"""Exact append-commit contract for candidate branch updates."""
|
||||
|
||||
branch: str
|
||||
expected_head_oid: str | None
|
||||
allowed_paths: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BranchPolicy:
|
||||
"""Per-branch operation policies loaded from authorization."""
|
||||
|
||||
create_branch: BranchCreatePolicy | None
|
||||
append_commit: BranchAppendPolicy | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GovernedAuthorization:
|
||||
"""Parsed and validated authorization object."""
|
||||
|
||||
task_id: str
|
||||
repo: str
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
authorization_sha256: str
|
||||
one_time_use: bool
|
||||
branch_operations: dict[str, BranchPolicy]
|
||||
denied_operations: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def create_branches(self) -> set[str]:
|
||||
"""Branches that are allowed for branch-creation operations."""
|
||||
return {
|
||||
branch
|
||||
for branch, policy in self.branch_operations.items()
|
||||
if policy.create_branch is not None
|
||||
}
|
||||
|
||||
@property
|
||||
def append_branches(self) -> set[str]:
|
||||
"""Branches that are allowed for commit-append operations."""
|
||||
return {
|
||||
branch
|
||||
for branch, policy in self.branch_operations.items()
|
||||
if policy.append_commit is not None
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PeerIdentity:
|
||||
"""Verified peer identity from SO_PEERCRED."""
|
||||
|
||||
pid: int
|
||||
uid: int
|
||||
gid: int
|
||||
executable: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileChange:
|
||||
"""Single file modification payload."""
|
||||
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CreateBranchRequest:
|
||||
"""Validated create-branch request payload."""
|
||||
|
||||
branch: str
|
||||
base_oid: str
|
||||
head_oid: str | None
|
||||
authorization_digest: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppendCommitRequest:
|
||||
"""Validated append-commit request payload."""
|
||||
|
||||
branch: str
|
||||
expected_head_oid: str
|
||||
changes: tuple[FileChange, ...]
|
||||
authorization_digest: str
|
||||
commit_message: str = "chore(governance): append governed commit"
|
||||
|
||||
|
||||
_OID_RE = re.compile(r"^[0-9a-fA-F]{40}$")
|
||||
_DENIED_PATH_SEGMENTS = (
|
||||
"main",
|
||||
"tag",
|
||||
"baseline",
|
||||
"credential",
|
||||
"credentials",
|
||||
"secret",
|
||||
"registry",
|
||||
)
|
||||
_SECRET_PATTERNS = (
|
||||
re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
re.compile(r"(?i)api[_-]?key\s*[:=]\s*\S+"),
|
||||
re.compile(r"(?i)secret\s*[:=]\s*\S+"),
|
||||
re.compile(r"(?i)password\s*[:=]\s*\S+"),
|
||||
re.compile(r"(?i)token\s*[:=]\s*\S+"),
|
||||
)
|
||||
_SAFE_SECRET_MARKERS = re.compile(
|
||||
r"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*(?:REDACTED|TEST_FIXTURE|NOT_A_SECRET)"
|
||||
)
|
||||
|
||||
|
||||
def canonicalize_authorization_payload(payload: Mapping[str, Any]) -> str:
|
||||
"""Serialize authorization payload without the signature hash, in canonical form."""
|
||||
canonical = dict(payload)
|
||||
canonical.pop("authorization_sha256", None)
|
||||
return json.dumps(
|
||||
canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
||||
)
|
||||
|
||||
|
||||
def compute_authorization_sha256(payload: Mapping[str, Any]) -> str:
|
||||
"""Compute canonical sha256 over authorization payload."""
|
||||
digest = hashlib.sha256()
|
||||
digest.update(canonicalize_authorization_payload(payload).encode("utf-8"))
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _assert_authorization_file_permissions(path: Path) -> None:
|
||||
"""Fail-closed checks for root-only secret-like authorization files."""
|
||||
if path.is_symlink():
|
||||
raise AuthorizationError("authorization path must not be a symlink")
|
||||
|
||||
st = path.stat()
|
||||
if not stat.S_ISREG(st.st_mode):
|
||||
raise AuthorizationError("authorization path must be a regular file")
|
||||
|
||||
if (st.st_mode & 0o777) != 0o600:
|
||||
raise AuthorizationError("authorization file mode must be exactly 0600")
|
||||
|
||||
if hasattr(st, "st_uid") and os.name != "nt" and st.st_uid != 0:
|
||||
raise AuthorizationError("authorization file owner must be uid 0")
|
||||
|
||||
|
||||
def _parse_iso8601(value: str) -> datetime:
|
||||
"""Parse ISO 8601 timestamps used by authorization payloads."""
|
||||
try:
|
||||
if value.endswith("Z"):
|
||||
value = value[:-1] + "+00:00"
|
||||
parsed = datetime.fromisoformat(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise AuthorizationError("created_at must be an ISO 8601 timestamp") from exc
|
||||
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed
|
||||
|
||||
|
||||
def _require_type(value: Any, expected: type, *, key: str) -> None:
|
||||
if not isinstance(value, expected):
|
||||
raise AuthorizationError(f"{key} must be {expected.__name__}")
|
||||
|
||||
|
||||
def _parse_oid(value: Any, *, field: str) -> str:
|
||||
if not isinstance(value, str):
|
||||
raise AuthorizationError(f"{field} must be a string")
|
||||
if not _OID_RE.fullmatch(value):
|
||||
raise AuthorizationError(f"{field} must be a 40-char hex object id")
|
||||
return value.lower()
|
||||
|
||||
|
||||
def _parse_allowed_paths(paths: Any) -> tuple[str, ...]:
|
||||
if not isinstance(paths, list) or not paths:
|
||||
raise AuthorizationError("allowed_paths must be a non-empty list")
|
||||
normalized: list[str] = []
|
||||
for path in paths:
|
||||
if not isinstance(path, str) or not path.strip():
|
||||
raise AuthorizationError("allowed_paths entries must be non-empty strings")
|
||||
normalized.append(path.strip())
|
||||
return tuple(normalized)
|
||||
|
||||
|
||||
def parse_authorization(
|
||||
raw: Mapping[str, Any], *, now: datetime | None = None
|
||||
) -> GovernedAuthorization:
|
||||
"""Validate an authorization payload and return a typed object."""
|
||||
_require_type(raw.get("task_id"), str, key="task_id")
|
||||
if raw["task_id"] != TASK_ID:
|
||||
raise AuthorizationError("unexpected task_id")
|
||||
|
||||
_require_type(raw.get("repo"), str, key="repo")
|
||||
if raw["repo"] != REPO:
|
||||
raise AuthorizationError("unexpected repo value")
|
||||
|
||||
created_at = _parse_iso8601(raw.get("created_at"))
|
||||
check_time = now or datetime.now(tz=timezone.utc)
|
||||
if created_at.tzinfo is None:
|
||||
created_at = created_at.replace(tzinfo=timezone.utc)
|
||||
if created_at - check_time > timedelta(seconds=30):
|
||||
raise AuthorizationError("authorization created_at is in the future")
|
||||
if check_time - created_at > AUTHORIZATION_TTL:
|
||||
raise AuthorizationError("authorization expired")
|
||||
raw_expires_at = raw.get("expires_at")
|
||||
expires_at = (
|
||||
_parse_iso8601(raw_expires_at)
|
||||
if raw_expires_at is not None
|
||||
else created_at + AUTHORIZATION_TTL
|
||||
)
|
||||
if expires_at <= check_time:
|
||||
raise AuthorizationError("authorization expiry is in the past")
|
||||
if expires_at > created_at + AUTHORIZATION_TTL:
|
||||
raise AuthorizationError("authorization expiry exceeds two-hour maximum")
|
||||
|
||||
_require_type(raw.get("one_time_use"), bool, key="one_time_use")
|
||||
if raw["one_time_use"] is not True:
|
||||
raise AuthorizationError("one_time_use must be true")
|
||||
|
||||
_require_type(raw.get("authorization_sha256"), str, key="authorization_sha256")
|
||||
expected_hash = compute_authorization_sha256(raw)
|
||||
provided_hash = str(raw["authorization_sha256"]).lower()
|
||||
if provided_hash != expected_hash:
|
||||
raise AuthorizationError("authorization_sha256 mismatch")
|
||||
|
||||
op_map = raw.get("allowed_branch_operations")
|
||||
if not isinstance(op_map, dict) or not op_map:
|
||||
raise AuthorizationError("allowed_branch_operations must be a non-empty object")
|
||||
|
||||
branch_operations: dict[str, BranchPolicy] = {}
|
||||
for branch, operation in op_map.items():
|
||||
if not isinstance(branch, str) or not branch.strip():
|
||||
raise AuthorizationError("branch names must be non-empty strings")
|
||||
|
||||
if not isinstance(operation, dict):
|
||||
raise AuthorizationError("operation policy must be an object")
|
||||
|
||||
create_block = operation.get("create_branch")
|
||||
append_block = operation.get("append_commit")
|
||||
|
||||
create_policy: BranchCreatePolicy | None = None
|
||||
append_policy: BranchAppendPolicy | None = None
|
||||
|
||||
if create_block is not None:
|
||||
if not isinstance(create_block, dict):
|
||||
raise AuthorizationError("create_branch block must be an object")
|
||||
raw_head_oid = create_block.get("head_oid")
|
||||
create_policy = BranchCreatePolicy(
|
||||
branch=branch,
|
||||
base_oid=_parse_oid(create_block.get("base_oid"), field="create_branch.base_oid"),
|
||||
head_oid=(
|
||||
_parse_oid(raw_head_oid, field="create_branch.head_oid")
|
||||
if raw_head_oid is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
if append_block is not None:
|
||||
if not isinstance(append_block, dict):
|
||||
raise AuthorizationError("append_commit block must be an object")
|
||||
raw_expected_head_oid = append_block.get("expected_head_oid")
|
||||
append_policy = BranchAppendPolicy(
|
||||
branch=branch,
|
||||
expected_head_oid=(
|
||||
_parse_oid(
|
||||
raw_expected_head_oid,
|
||||
field="append_commit.expected_head_oid",
|
||||
)
|
||||
if raw_expected_head_oid is not None
|
||||
else None
|
||||
),
|
||||
allowed_paths=_parse_allowed_paths(append_block.get("allowed_paths")),
|
||||
)
|
||||
|
||||
if create_policy is None and append_policy is None:
|
||||
raise AuthorizationError("branch policy must define at least one operation")
|
||||
|
||||
branch_operations[branch] = BranchPolicy(
|
||||
create_branch=create_policy, append_commit=append_policy
|
||||
)
|
||||
|
||||
denied_raw = raw.get("denied_operations", ())
|
||||
if not isinstance(denied_raw, list):
|
||||
raise AuthorizationError("denied_operations must be a list")
|
||||
denied_operations: list[str] = []
|
||||
for value in denied_raw:
|
||||
if not isinstance(value, str):
|
||||
raise AuthorizationError("denied operations must be strings")
|
||||
denied_operations.append(value.lower())
|
||||
|
||||
return GovernedAuthorization(
|
||||
task_id=raw["task_id"],
|
||||
repo=raw["repo"],
|
||||
created_at=created_at,
|
||||
expires_at=expires_at,
|
||||
authorization_sha256=provided_hash,
|
||||
one_time_use=True,
|
||||
branch_operations=branch_operations,
|
||||
denied_operations=tuple(denied_operations),
|
||||
)
|
||||
|
||||
|
||||
def load_authorization_file(
|
||||
path: str | os.PathLike[str], *, now: datetime | None = None
|
||||
) -> GovernedAuthorization:
|
||||
"""Load and validate the root-only authorization file."""
|
||||
auth_path = Path(path)
|
||||
_assert_authorization_file_permissions(auth_path)
|
||||
try:
|
||||
raw_text = auth_path.read_text(encoding="utf-8")
|
||||
except (OSError, ValueError) as exc:
|
||||
raise AuthorizationError("unable to read authorization file") from exc
|
||||
|
||||
try:
|
||||
raw_obj = json.loads(raw_text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AuthorizationError("authorization must be JSON") from exc
|
||||
|
||||
if not isinstance(raw_obj, dict):
|
||||
raise AuthorizationError("authorization top-level must be an object")
|
||||
|
||||
return parse_authorization(raw_obj, now=now)
|
||||
|
||||
|
||||
def _resolve_peer_executable(pid: int) -> str | None:
|
||||
"""Resolve peer executable from /proc/<pid>/exe when available."""
|
||||
if pid <= 0:
|
||||
return None
|
||||
exe_path = f"/proc/{pid}/exe"
|
||||
try:
|
||||
return os.readlink(exe_path)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def extract_peer_identity(
|
||||
connection: socket.socket, *, require_root_uid: bool = True
|
||||
) -> PeerIdentity:
|
||||
"""Validate SO_PEERCRED and return peer identity."""
|
||||
if not hasattr(socket, "SO_PEERCRED"):
|
||||
raise AuthorizationError("SO_PEERCRED not available on this platform")
|
||||
|
||||
data = connection.getsockopt(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SO_PEERCRED,
|
||||
struct.calcsize("3i"),
|
||||
)
|
||||
if len(data) != struct.calcsize("3i"):
|
||||
raise AuthorizationError("SO_PEERCRED payload size mismatch")
|
||||
|
||||
pid, uid, gid = struct.unpack("3i", data)
|
||||
if require_root_uid and uid != 0:
|
||||
raise PermissionError("peer uid must be 0")
|
||||
|
||||
return PeerIdentity(
|
||||
pid=pid,
|
||||
uid=uid,
|
||||
gid=gid,
|
||||
executable=_resolve_peer_executable(pid),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_file_path(path: str) -> str:
|
||||
normalized = posixpath.normpath(path.replace("\\", "/").strip())
|
||||
return normalized.lstrip("/")
|
||||
|
||||
|
||||
def _path_has_disallowed_segment(path: str) -> bool:
|
||||
lowered = path.lower()
|
||||
if ".." in path.split("/"):
|
||||
return True
|
||||
parts = [p for p in lowered.split("/") if p]
|
||||
for part in parts:
|
||||
for segment in _DENIED_PATH_SEGMENTS:
|
||||
if part == segment or part.startswith(f"{segment}.") or segment in part:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_allowed_path(path: str, allowed_paths: Sequence[str]) -> bool:
|
||||
normalized = _normalize_file_path(path)
|
||||
for prefix in allowed_paths:
|
||||
normalized_prefix = _normalize_file_path(prefix)
|
||||
if normalized == normalized_prefix:
|
||||
return True
|
||||
if normalized.startswith(normalized_prefix + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def contains_secret_like_content(content: str) -> bool:
|
||||
"""Heuristic detector for obvious secret-like strings."""
|
||||
content = _SAFE_SECRET_MARKERS.sub("", content or "")
|
||||
for pattern in _SECRET_PATTERNS:
|
||||
if pattern.search(content):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def validate_file_changes(
|
||||
changes: Any, allowed_paths: Sequence[str]
|
||||
) -> tuple[FileChange, ...]:
|
||||
"""Validate append-commit file changes against path and content policy."""
|
||||
if not isinstance(changes, list):
|
||||
raise AuthorizationError("changes must be a list")
|
||||
result: list[FileChange] = []
|
||||
|
||||
for change in changes:
|
||||
if not isinstance(change, Mapping):
|
||||
raise AuthorizationError("each file change must be an object")
|
||||
raw_path = change.get("path")
|
||||
raw_content = change.get("content")
|
||||
if not isinstance(raw_path, str) or not raw_path.strip():
|
||||
raise AuthorizationError("file change path must be a non-empty string")
|
||||
if raw_path.startswith(("/", "\\")):
|
||||
raise AuthorizationError("file change path must be relative")
|
||||
if not isinstance(raw_content, str):
|
||||
raise AuthorizationError("file change content must be a string")
|
||||
|
||||
normalized = _normalize_file_path(raw_path)
|
||||
if not normalized or normalized.startswith("../") or normalized == "..":
|
||||
raise AuthorizationError("file paths must be normalized relative paths")
|
||||
|
||||
if not _is_allowed_path(normalized, allowed_paths):
|
||||
raise AuthorizationError("file path outside allowed_paths")
|
||||
|
||||
if _path_has_disallowed_segment(normalized):
|
||||
raise AuthorizationError("file path uses denied segment")
|
||||
|
||||
if contains_secret_like_content(raw_content):
|
||||
raise AuthorizationError("file content failed secret-like scan")
|
||||
|
||||
result.append(FileChange(path=normalized, content=raw_content))
|
||||
|
||||
return tuple(result)
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Supplement Gitea native protection when review identity is not expressible."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
|
||||
class MergeGateDenied(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MergeGateDecision:
|
||||
allowed: bool
|
||||
owner_review_id: int
|
||||
required_contexts: tuple[str, ...]
|
||||
|
||||
|
||||
def validate_protected_merge(
|
||||
pull_request: Mapping[str, Any],
|
||||
reviews: Sequence[Mapping[str, Any]],
|
||||
statuses: Mapping[str, str],
|
||||
*,
|
||||
required_contexts: Sequence[str],
|
||||
unresolved_conversations: int,
|
||||
owner_login: str = "owner",
|
||||
owner_id: int = 2,
|
||||
) -> MergeGateDecision:
|
||||
if pull_request.get("state") != "open" or pull_request.get("mergeable") is not True:
|
||||
raise MergeGateDenied("pull_request_not_mergeable")
|
||||
author = (pull_request.get("user") or {}).get("login")
|
||||
owner_review = None
|
||||
for review in reviews:
|
||||
user = review.get("user") or {}
|
||||
if review.get("state") == "APPROVED" and user.get("login") == owner_login and user.get("id") == owner_id:
|
||||
if author == owner_login or (pull_request.get("author_id") == owner_id):
|
||||
raise MergeGateDenied("pr_author_self_approval_denied")
|
||||
owner_review = review
|
||||
if owner_review is None:
|
||||
raise MergeGateDenied("owner_review_required")
|
||||
if unresolved_conversations != 0:
|
||||
raise MergeGateDenied("unresolved_conversations_block_merge")
|
||||
missing = [context for context in required_contexts if statuses.get(context) != "success"]
|
||||
if missing:
|
||||
raise MergeGateDenied("required_checks_missing:" + ",".join(missing))
|
||||
return MergeGateDecision(True, int(owner_review.get("id", 0)), tuple(required_contexts))
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Thin production tag adapter over AIOS's existing mature TagPublisher.
|
||||
|
||||
This adapter deliberately does not call TagPublisher.publish(), because that
|
||||
legacy method also mutates Registry/Baseline. The production executor requires:
|
||||
Tag -> runtime ACTIVE -> Registry -> Baseline.
|
||||
|
||||
`apply` reuses TagPublisher._verify_publication() under the publisher's durable
|
||||
per-version lock. That method performs the existing fail-closed annotated-tag,
|
||||
network remote, Gitea API, peeled-target and listing verification, but stops
|
||||
before Registry/Baseline mutation.
|
||||
|
||||
`probe` is read-only.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from gitea_tag_publication import (
|
||||
GiteaTagClient,
|
||||
GitTagBackend,
|
||||
PublicationConflict,
|
||||
PublicationManualOwnerReviewRequired,
|
||||
PublicationPending,
|
||||
PublicationReconciliationRequired,
|
||||
PublicationRequest,
|
||||
PublicationState,
|
||||
TagPublisher,
|
||||
)
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
return value
|
||||
|
||||
|
||||
def publication_request(value: Mapping[str, Any]) -> PublicationRequest:
|
||||
return PublicationRequest(
|
||||
str(value["version"]),
|
||||
str(value["target_oid"]),
|
||||
str(value["evidence_closure_oid"]),
|
||||
str(value["approval_plan_id"]),
|
||||
)
|
||||
|
||||
|
||||
def response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def build(args: argparse.Namespace) -> tuple[TagPublisher, GitTagBackend, GiteaTagClient]:
|
||||
git = GitTagBackend(args.workspace, authoritative_repo=args.repository)
|
||||
gitea = GiteaTagClient(
|
||||
args.gitea_api,
|
||||
args.gitea_repo,
|
||||
args.gitea_token_file,
|
||||
token_line=args.gitea_token_line,
|
||||
)
|
||||
publisher = TagPublisher(
|
||||
args.workspace,
|
||||
git,
|
||||
gitea,
|
||||
args.remote,
|
||||
state_root=args.state_root,
|
||||
)
|
||||
return publisher, git, gitea
|
||||
|
||||
|
||||
def _gitea_lookup(gitea: GiteaTagClient, version: str) -> tuple[str, str] | None:
|
||||
try:
|
||||
return gitea.lookup_tag(version)
|
||||
except RuntimeError as exc:
|
||||
if "HTTP 404" in str(exc):
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def probe(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
request = publication_request(value)
|
||||
request.validate()
|
||||
git.validate_evidence_lineage(request)
|
||||
|
||||
local_oid = git._rev(f"refs/tags/{request.version}")
|
||||
local_target = git._rev(f"refs/tags/{request.version}^{{commit}}")
|
||||
remote = git.remote_tag(args.remote, request.version)
|
||||
api = _gitea_lookup(gitea, request.version)
|
||||
listing = False
|
||||
if api is not None:
|
||||
listing = gitea.tag_visible_in_listing(request.version, request.target_oid)
|
||||
|
||||
if local_oid is None and remote is None and api is None:
|
||||
return response(value, "NOT_APPLIED")
|
||||
|
||||
if local_oid is not None:
|
||||
if local_target is None or local_oid == local_target:
|
||||
return response(value, "CONFLICT", reason="local_tag_not_annotated")
|
||||
if local_target != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="local_peeled_target_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
# A remote/Gitea tag without the Authority tag object is not safe to
|
||||
# recreate, because a new annotated tag would have a different object OID.
|
||||
if local_oid is None and (remote is not None or api is not None):
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="remote_or_gitea_tag_exists_without_authority_tag_object",
|
||||
)
|
||||
|
||||
if remote is not None:
|
||||
if remote[0] != local_oid or remote[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="network_remote_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
)
|
||||
|
||||
if api is not None:
|
||||
if api[0] != local_oid or api[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="gitea_api_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
)
|
||||
|
||||
if (
|
||||
local_oid is not None
|
||||
and remote is not None
|
||||
and api is not None
|
||||
and listing
|
||||
):
|
||||
return response(
|
||||
value,
|
||||
"APPLIED_VERIFIED",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
gitea_listing_visible=True,
|
||||
)
|
||||
|
||||
if local_oid is not None and remote is None and api is None:
|
||||
return response(
|
||||
value,
|
||||
"PARTIAL_LOCAL",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="tag_visibility_partial",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
|
||||
def apply(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
before = probe(args, value, publisher, git, gitea)
|
||||
if before["status"] in {"APPLIED_VERIFIED", "CONFLICT", "INDETERMINATE"}:
|
||||
return before
|
||||
|
||||
request = publication_request(value)
|
||||
with publisher.store.lock(request.version):
|
||||
request.validate()
|
||||
publisher._assert_fingerprint(request)
|
||||
publisher.store.record(request, PublicationState.PENDING)
|
||||
publisher._verify_publication(
|
||||
request,
|
||||
allow_push=True,
|
||||
previously_deployed=False,
|
||||
)
|
||||
return probe(args, value, publisher, git, gitea)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--workspace", type=Path, default=Path(os.environ["AIOS_GITOPS_CONTROLLER_WORKSPACE"]))
|
||||
parser.add_argument("--repository", type=Path, default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]))
|
||||
parser.add_argument("--remote", default=os.getenv("AIOS_GITOPS_GITEA_REMOTE", "gitea"))
|
||||
parser.add_argument("--gitea-api", default=os.getenv("AIOS_GITOPS_GITEA_API_URL", ""))
|
||||
parser.add_argument("--gitea-repo", default=os.getenv("AIOS_GITOPS_GITEA_REPOSITORY", ""))
|
||||
parser.add_argument(
|
||||
"--gitea-token-file",
|
||||
type=Path,
|
||||
default=Path(os.getenv("AIOS_GITOPS_GITEA_TOKEN_FILE", "")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gitea-token-line",
|
||||
type=int,
|
||||
default=int(os.getenv("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_PUBLICATION_STATE_DIR",
|
||||
"/var/lib/aios-gitops/tag-publications",
|
||||
)
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
publisher, git, gitea = build(args)
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value, publisher, git, gitea)
|
||||
elif operation == "apply":
|
||||
result = apply(args, value, publisher, git, gitea)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except PublicationManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except PublicationConflict as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except (PublicationPending, PublicationReconciliationRequired) as exc:
|
||||
print(json.dumps({"status": "INDETERMINATE", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(21)
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "INDETERMINATE", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(22)
|
||||
@@ -1,611 +0,0 @@
|
||||
"""Audited Owner authorization broker for task-scoped writer provisioning.
|
||||
|
||||
The broker closes the bootstrap gap without minting a new general-purpose
|
||||
Owner token and without modifying the bearer-token registry. It accepts an
|
||||
already-authenticated, explicitly allow-listed Owner principal, records a
|
||||
tamper-evident authorization event, and invokes the existing
|
||||
AuthorizedTaskWriterProvisioner with a generated authorization reference.
|
||||
|
||||
The broker never writes Git refs itself. The governed signer, validator and
|
||||
one-shot provisioner remain the only components allowed to create the task
|
||||
branch/worktree/writer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from authorized_task_writer_provisioning import (
|
||||
AuthorizedTaskWriterProvisioner,
|
||||
BRANCH_RE,
|
||||
OID_RE,
|
||||
OwnerContext,
|
||||
ProvisioningError,
|
||||
REQUIRED_ALLOWED_OPERATIONS,
|
||||
REQUIRED_DENIED_OPERATIONS,
|
||||
TASK_RE,
|
||||
)
|
||||
|
||||
|
||||
ACTION = "AUTHORIZE_TASK_WRITER_PROVISIONING"
|
||||
SCHEMA = "aios.owner-authorization-event.v1"
|
||||
BROKER_REQUEST_FIELDS = frozenset(
|
||||
{
|
||||
"action",
|
||||
"task_id",
|
||||
"repository",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"one_time_use",
|
||||
"expires_in_seconds",
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
}
|
||||
)
|
||||
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
|
||||
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
||||
CREDENTIAL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@+-]{1,255}$")
|
||||
WILDCARD_SCOPE_RE = re.compile(r"(^\*$)|(^.+:\*$)")
|
||||
|
||||
|
||||
class OwnerAuthorizationError(RuntimeError):
|
||||
"""Stable fail-closed broker error."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: str,
|
||||
status: int = 400,
|
||||
*,
|
||||
details: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
self.details = dict(details or {})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OwnerPrincipal:
|
||||
"""Non-secret authentication evidence returned by the existing API."""
|
||||
|
||||
identity: str
|
||||
role: str
|
||||
authenticated_scope: str
|
||||
token_scopes: frozenset[str]
|
||||
credential_id: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OwnerAuthorizationBrokerConfig:
|
||||
repository_identity: str
|
||||
state_root: Path
|
||||
allowed_owner_identities: frozenset[str]
|
||||
allowed_intake_scopes: frozenset[str]
|
||||
allowed_credential_ids: frozenset[str] = frozenset()
|
||||
max_expiry_seconds: int = 900
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls) -> "OwnerAuthorizationBrokerConfig":
|
||||
identities = _csv_set(
|
||||
os.getenv("AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_IDENTITIES", "")
|
||||
)
|
||||
scopes = _csv_set(
|
||||
os.getenv("AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES", "")
|
||||
)
|
||||
credential_ids = _csv_set(
|
||||
os.getenv("AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_CREDENTIAL_IDS", "")
|
||||
)
|
||||
if not identities:
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_IDENTITY_ALLOWLIST_REQUIRED", 503
|
||||
)
|
||||
if not scopes:
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_INTAKE_SCOPE_ALLOWLIST_REQUIRED", 503
|
||||
)
|
||||
if any(_is_wildcard_scope(item) for item in scopes):
|
||||
raise OwnerAuthorizationError(
|
||||
"WILDCARD_INTAKE_SCOPE_REJECTED", 503
|
||||
)
|
||||
if "writer:provision" in scopes:
|
||||
raise OwnerAuthorizationError(
|
||||
"WRITER_PROVISION_SCOPE_BOOTSTRAP_REJECTED", 503
|
||||
)
|
||||
try:
|
||||
maximum = int(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_OWNER_AUTHORIZATION_MAX_EXPIRY_SECONDS", "900"
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
raise OwnerAuthorizationError(
|
||||
"BROKER_MAX_EXPIRY_INVALID", 503
|
||||
) from None
|
||||
if maximum < 60 or maximum > 1800:
|
||||
raise OwnerAuthorizationError("BROKER_MAX_EXPIRY_INVALID", 503)
|
||||
return cls(
|
||||
repository_identity=os.getenv(
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY", "aios/aios"
|
||||
).strip(),
|
||||
state_root=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT",
|
||||
"/var/lib/aios-gitops/owner-authorization-broker",
|
||||
)
|
||||
),
|
||||
allowed_owner_identities=identities,
|
||||
allowed_intake_scopes=scopes,
|
||||
allowed_credential_ids=credential_ids,
|
||||
max_expiry_seconds=maximum,
|
||||
)
|
||||
|
||||
|
||||
class TaskWriterProvisioner(Protocol):
|
||||
def provision(
|
||||
self, raw: Mapping[str, Any], owner: OwnerContext
|
||||
) -> dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def iso_utc(value: datetime) -> str:
|
||||
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def canonical_json_bytes(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
||||
|
||||
|
||||
def _csv_set(value: str) -> frozenset[str]:
|
||||
return frozenset(
|
||||
item.strip() for item in value.split(",") if item.strip()
|
||||
)
|
||||
|
||||
|
||||
def _is_wildcard_scope(scope: str) -> bool:
|
||||
return bool(WILDCARD_SCOPE_RE.fullmatch(scope.strip()))
|
||||
|
||||
|
||||
def _atomic_write_json(path: Path, value: Any, mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise OwnerAuthorizationError("BROKER_STATE_SYMLINK_REJECTED", 500)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".tmp-", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json_bytes(value))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, mode)
|
||||
parent_fd = os.open(
|
||||
path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
||||
)
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise OwnerAuthorizationError("BROKER_RECEIPT_INVALID", 500)
|
||||
if path.stat().st_size <= 0 or path.stat().st_size > 1_048_576:
|
||||
raise OwnerAuthorizationError("BROKER_RECEIPT_INVALID", 500)
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise OwnerAuthorizationError(
|
||||
"BROKER_RECEIPT_JSON_INVALID", 500
|
||||
) from None
|
||||
if not isinstance(value, dict):
|
||||
raise OwnerAuthorizationError("BROKER_RECEIPT_INVALID", 500)
|
||||
return value
|
||||
|
||||
|
||||
def _prepare_state_root(root: Path) -> None:
|
||||
if root.is_symlink():
|
||||
raise OwnerAuthorizationError("BROKER_STATE_SYMLINK_REJECTED", 500)
|
||||
root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
os.chmod(root, 0o700)
|
||||
if root.stat().st_mode & 0o077:
|
||||
raise OwnerAuthorizationError(
|
||||
"BROKER_STATE_PERMISSIONS_REJECTED", 500
|
||||
)
|
||||
|
||||
|
||||
def _event_id(
|
||||
principal: OwnerPrincipal, request_digest: str
|
||||
) -> str:
|
||||
material = (
|
||||
f"{principal.identity}\0{principal.authenticated_scope}\0"
|
||||
f"{request_digest}"
|
||||
).encode("utf-8")
|
||||
return "owner-auth-" + hashlib.sha256(material).hexdigest()[:32]
|
||||
|
||||
|
||||
def _authorization_reference(event_id: str, request_digest: str) -> str:
|
||||
return f"owner-auth:{event_id}:{request_digest[:16]}"
|
||||
|
||||
|
||||
def _validate_principal(
|
||||
principal: OwnerPrincipal,
|
||||
config: OwnerAuthorizationBrokerConfig,
|
||||
) -> None:
|
||||
if principal.role != "owner":
|
||||
raise OwnerAuthorizationError("OWNER_ROLE_REQUIRED", 403)
|
||||
if (
|
||||
not principal.identity
|
||||
or not IDENTITY_RE.fullmatch(principal.identity)
|
||||
or principal.identity not in config.allowed_owner_identities
|
||||
):
|
||||
raise OwnerAuthorizationError("OWNER_IDENTITY_NOT_ALLOWED", 403)
|
||||
if (
|
||||
not principal.authenticated_scope
|
||||
or not SCOPE_RE.fullmatch(principal.authenticated_scope)
|
||||
or principal.authenticated_scope
|
||||
not in config.allowed_intake_scopes
|
||||
):
|
||||
raise OwnerAuthorizationError("OWNER_INTAKE_SCOPE_REJECTED", 403)
|
||||
if principal.authenticated_scope == "writer:provision":
|
||||
raise OwnerAuthorizationError(
|
||||
"WRITER_PROVISION_SCOPE_BOOTSTRAP_REJECTED", 403
|
||||
)
|
||||
|
||||
scopes = principal.token_scopes
|
||||
if any(_is_wildcard_scope(scope) for scope in scopes):
|
||||
raise OwnerAuthorizationError(
|
||||
"WILDCARD_OWNER_CREDENTIAL_REJECTED", 403
|
||||
)
|
||||
if scopes:
|
||||
if principal.authenticated_scope not in scopes:
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_SCOPE_EVIDENCE_MISMATCH", 403
|
||||
)
|
||||
elif (
|
||||
not principal.credential_id
|
||||
or principal.credential_id not in config.allowed_credential_ids
|
||||
):
|
||||
# handler.need(scope) proves authorization but may hide whether the
|
||||
# credential was wildcard. When scope metadata is absent, require an
|
||||
# exact non-secret credential ID allowlist as the alternative proof.
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_CREDENTIAL_SCOPE_NOT_PROVABLE", 403
|
||||
)
|
||||
if principal.credential_id:
|
||||
if not CREDENTIAL_ID_RE.fullmatch(principal.credential_id):
|
||||
raise OwnerAuthorizationError("OWNER_CREDENTIAL_ID_INVALID", 403)
|
||||
if (
|
||||
config.allowed_credential_ids
|
||||
and principal.credential_id not in config.allowed_credential_ids
|
||||
):
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_CREDENTIAL_ID_NOT_ALLOWED", 403
|
||||
)
|
||||
|
||||
|
||||
def _validate_request(
|
||||
raw: Mapping[str, Any],
|
||||
config: OwnerAuthorizationBrokerConfig,
|
||||
) -> dict[str, Any]:
|
||||
if set(raw) != BROKER_REQUEST_FIELDS:
|
||||
raise OwnerAuthorizationError("BROKER_REQUEST_FIELDS_REJECTED")
|
||||
if raw["action"] != ACTION:
|
||||
raise OwnerAuthorizationError("BROKER_ACTION_REJECTED")
|
||||
if raw["repository"] != config.repository_identity:
|
||||
raise OwnerAuthorizationError(
|
||||
"REPOSITORY_IDENTITY_MISMATCH", 409
|
||||
)
|
||||
if raw["base_ref"] != "refs/heads/main":
|
||||
raise OwnerAuthorizationError("BASE_REF_REJECTED", 409)
|
||||
if (
|
||||
not isinstance(raw["base_oid"], str)
|
||||
or not OID_RE.fullmatch(raw["base_oid"])
|
||||
):
|
||||
raise OwnerAuthorizationError("BASE_OID_REJECTED")
|
||||
if (
|
||||
not isinstance(raw["target_branch"], str)
|
||||
or not BRANCH_RE.fullmatch(raw["target_branch"])
|
||||
or raw["target_branch"] in {"main", "master"}
|
||||
):
|
||||
raise OwnerAuthorizationError("TARGET_BRANCH_REJECTED")
|
||||
if (
|
||||
not isinstance(raw["task_id"], str)
|
||||
or not TASK_RE.fullmatch(raw["task_id"])
|
||||
):
|
||||
raise OwnerAuthorizationError("TASK_ID_REJECTED")
|
||||
if raw["one_time_use"] is not True:
|
||||
raise OwnerAuthorizationError("ONE_TIME_USE_REQUIRED")
|
||||
expiry = raw["expires_in_seconds"]
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= config.max_expiry_seconds
|
||||
):
|
||||
raise OwnerAuthorizationError("GRANT_LIFETIME_REJECTED")
|
||||
for field in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if raw[field] is not False:
|
||||
raise OwnerAuthorizationError(
|
||||
"PRODUCTION_CAPABILITY_REJECTED", 403
|
||||
)
|
||||
allowed = raw["allowed_operations"]
|
||||
denied = raw["denied_operations"]
|
||||
if (
|
||||
not isinstance(allowed, list)
|
||||
or frozenset(allowed) != REQUIRED_ALLOWED_OPERATIONS
|
||||
):
|
||||
raise OwnerAuthorizationError("ALLOWED_OPERATIONS_MISMATCH", 403)
|
||||
if (
|
||||
not isinstance(denied, list)
|
||||
or frozenset(denied) != REQUIRED_DENIED_OPERATIONS
|
||||
):
|
||||
raise OwnerAuthorizationError("DENIED_OPERATIONS_MISMATCH", 403)
|
||||
return dict(raw)
|
||||
|
||||
|
||||
def _response_is_safe(value: Any) -> bool:
|
||||
"""Reject accidental credential material before writing a receipt."""
|
||||
|
||||
sensitive_markers = (
|
||||
"access_token",
|
||||
"bearer",
|
||||
"client_secret",
|
||||
"private_key",
|
||||
"raw_token",
|
||||
"secret_value",
|
||||
)
|
||||
if isinstance(value, Mapping):
|
||||
for key, inner in value.items():
|
||||
lowered = str(key).lower()
|
||||
if any(marker in lowered for marker in sensitive_markers):
|
||||
return False
|
||||
if not _response_is_safe(inner):
|
||||
return False
|
||||
elif isinstance(value, list):
|
||||
return all(_response_is_safe(item) for item in value)
|
||||
return True
|
||||
|
||||
|
||||
class OwnerAuthorizationBroker:
|
||||
"""Create an audited Owner event and invoke the governed bridge."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OwnerAuthorizationBrokerConfig | None = None,
|
||||
provisioner: TaskWriterProvisioner | None = None,
|
||||
) -> None:
|
||||
self.config = (
|
||||
config or OwnerAuthorizationBrokerConfig.from_environment()
|
||||
)
|
||||
self.provisioner = provisioner or AuthorizedTaskWriterProvisioner()
|
||||
|
||||
def authorize_and_provision(
|
||||
self,
|
||||
raw: Mapping[str, Any],
|
||||
principal: OwnerPrincipal,
|
||||
) -> dict[str, Any]:
|
||||
config = self.config
|
||||
_validate_principal(principal, config)
|
||||
request = _validate_request(raw, config)
|
||||
_prepare_state_root(config.state_root)
|
||||
|
||||
request_digest = sha256_json(request)
|
||||
event_id = _event_id(principal, request_digest)
|
||||
reference = _authorization_reference(event_id, request_digest)
|
||||
event_dir = config.state_root / "events" / event_id
|
||||
event_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
os.chmod(event_dir, 0o700)
|
||||
|
||||
lock_path = event_dir / "event.lock"
|
||||
lock_path.touch(mode=0o600, exist_ok=True)
|
||||
os.chmod(lock_path, 0o600)
|
||||
with lock_path.open("r+b") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
response_path = event_dir / "response.json"
|
||||
event_path = event_dir / "event.json"
|
||||
|
||||
if response_path.exists():
|
||||
response = _read_json(response_path)
|
||||
response["owner_authorization_idempotent"] = True
|
||||
return response
|
||||
|
||||
issued_at = utc_now()
|
||||
expires_at = issued_at + timedelta(
|
||||
seconds=int(request["expires_in_seconds"])
|
||||
)
|
||||
event: dict[str, Any] = {
|
||||
"schema": SCHEMA,
|
||||
"event_id": event_id,
|
||||
"authorization_reference": reference,
|
||||
"status": "AUTHORIZED",
|
||||
"owner": {
|
||||
"identity": principal.identity,
|
||||
"role": principal.role,
|
||||
"authenticated_scope": principal.authenticated_scope,
|
||||
"credential_id": principal.credential_id or None,
|
||||
"token_scope_digest_sha256": sha256_json(
|
||||
sorted(principal.token_scopes)
|
||||
),
|
||||
"wildcard_scope_used": False,
|
||||
},
|
||||
"request": {
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"one_time_use": True,
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
"allowed_operations": sorted(
|
||||
REQUIRED_ALLOWED_OPERATIONS
|
||||
),
|
||||
"denied_operations": sorted(
|
||||
REQUIRED_DENIED_OPERATIONS
|
||||
),
|
||||
},
|
||||
"request_digest_sha256": request_digest,
|
||||
"issued_at": iso_utc(issued_at),
|
||||
"expires_at": iso_utc(expires_at),
|
||||
}
|
||||
event["event_digest_sha256"] = sha256_json(event)
|
||||
_atomic_write_json(event_path, event)
|
||||
|
||||
bridge_request = dict(request)
|
||||
bridge_request.pop("action")
|
||||
bridge_request["owner_authorization_reference"] = reference
|
||||
try:
|
||||
result = self.provisioner.provision(
|
||||
bridge_request,
|
||||
OwnerContext(
|
||||
identity=principal.identity,
|
||||
role=principal.role,
|
||||
authorization_reference=reference,
|
||||
),
|
||||
)
|
||||
if not isinstance(result, dict):
|
||||
raise OwnerAuthorizationError(
|
||||
"PROVISIONER_RESPONSE_INVALID", 502
|
||||
)
|
||||
if result.get("status") != "READY_FOR_CODE_AUTHORING":
|
||||
raise OwnerAuthorizationError(
|
||||
"PROVISIONER_NOT_READY",
|
||||
409,
|
||||
details={"status": result.get("status")},
|
||||
)
|
||||
if not _response_is_safe(result):
|
||||
raise OwnerAuthorizationError(
|
||||
"PROVISIONER_RESPONSE_CONTAINS_SENSITIVE_DATA", 500
|
||||
)
|
||||
except ProvisioningError as exc:
|
||||
event.update(
|
||||
{
|
||||
"status": "PROVISIONING_BLOCKED",
|
||||
"failed_at": iso_utc(utc_now()),
|
||||
"error": exc.code,
|
||||
"error_status": exc.status,
|
||||
}
|
||||
)
|
||||
event["event_digest_sha256"] = sha256_json(
|
||||
{
|
||||
key: value
|
||||
for key, value in event.items()
|
||||
if key != "event_digest_sha256"
|
||||
}
|
||||
)
|
||||
_atomic_write_json(event_path, event)
|
||||
raise
|
||||
except OwnerAuthorizationError as exc:
|
||||
event.update(
|
||||
{
|
||||
"status": "PROVISIONING_BLOCKED",
|
||||
"failed_at": iso_utc(utc_now()),
|
||||
"error": exc.code,
|
||||
"error_status": exc.status,
|
||||
}
|
||||
)
|
||||
event["event_digest_sha256"] = sha256_json(
|
||||
{
|
||||
key: value
|
||||
for key, value in event.items()
|
||||
if key != "event_digest_sha256"
|
||||
}
|
||||
)
|
||||
_atomic_write_json(event_path, event)
|
||||
raise
|
||||
except Exception as exc:
|
||||
event.update(
|
||||
{
|
||||
"status": "PROVISIONING_FAILED",
|
||||
"failed_at": iso_utc(utc_now()),
|
||||
"error": "OWNER_AUTHORIZATION_BROKER_INTERNAL_ERROR",
|
||||
}
|
||||
)
|
||||
event["event_digest_sha256"] = sha256_json(
|
||||
{
|
||||
key: value
|
||||
for key, value in event.items()
|
||||
if key != "event_digest_sha256"
|
||||
}
|
||||
)
|
||||
_atomic_write_json(event_path, event)
|
||||
raise OwnerAuthorizationError(
|
||||
"OWNER_AUTHORIZATION_BROKER_INTERNAL_ERROR", 500
|
||||
) from exc
|
||||
|
||||
completed_at = utc_now()
|
||||
event.update(
|
||||
{
|
||||
"status": "CONSUMED",
|
||||
"consumed_at": iso_utc(completed_at),
|
||||
"approval_plan_id": result.get("approval_plan_id"),
|
||||
"grant_id": result.get("grant_id"),
|
||||
"grant_digest_sha256": result.get(
|
||||
"grant_digest_sha256"
|
||||
),
|
||||
"branch_oid": result.get("branch_oid"),
|
||||
"grant_consumed": result.get("grant_consumed") is True,
|
||||
}
|
||||
)
|
||||
event["event_digest_sha256"] = sha256_json(
|
||||
{
|
||||
key: value
|
||||
for key, value in event.items()
|
||||
if key != "event_digest_sha256"
|
||||
}
|
||||
)
|
||||
_atomic_write_json(event_path, event)
|
||||
|
||||
response = dict(result)
|
||||
response.update(
|
||||
{
|
||||
"owner_authorization_event_id": event_id,
|
||||
"owner_authorization_reference": reference,
|
||||
"owner_authorization_digest_sha256": event[
|
||||
"event_digest_sha256"
|
||||
],
|
||||
"owner_authorization_consumed": True,
|
||||
"owner_authorization_idempotent": False,
|
||||
}
|
||||
)
|
||||
_atomic_write_json(response_path, response)
|
||||
return response
|
||||
@@ -1,642 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot local Owner authentication ingress for AIOS.
|
||||
|
||||
This module deliberately does not mint a reusable Owner token.
|
||||
|
||||
Authentication is based on a real local login session:
|
||||
- a root-run one-shot server owns a Unix socket;
|
||||
- the client must run as the configured Owner OS account;
|
||||
- the server validates SO_PEERCRED, Linux audit loginuid and the controlling TTY;
|
||||
- the Owner must interactively confirm the exact task/branch/base request;
|
||||
- the server constructs a narrow OwnerPrincipal and invokes the existing
|
||||
OwnerAuthorizationBroker;
|
||||
- one successful authorization consumes the service lifetime.
|
||||
|
||||
The resulting principal is fixed to one non-wildcard intake scope
|
||||
(`commit:create` for the current deployment) and cannot carry production,
|
||||
tag, baseline, main or history-rewrite capability.
|
||||
|
||||
This is a trust-root ingress, not a Git writer and not a production executor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pwd
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
BINDING_SCHEMA = "aios.local-owner-auth-ingress.v1"
|
||||
CLIENT_BEGIN_SCHEMA = "aios.local-owner-auth-client-begin.v1"
|
||||
CHALLENGE_SCHEMA = "aios.local-owner-auth-challenge.v1"
|
||||
CLIENT_CONFIRM_SCHEMA = "aios.local-owner-auth-client-confirm.v1"
|
||||
RESULT_SCHEMA = "aios.local-owner-auth-result.v1"
|
||||
|
||||
ACTION = "AUTHORIZE_TASK_WRITER_PROVISIONING"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
|
||||
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
||||
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
|
||||
|
||||
MAX_FRAME = 64 * 1024
|
||||
CHALLENGE_TTL_SECONDS = 120
|
||||
|
||||
EXPECTED_ALLOWED_OPERATIONS = frozenset(
|
||||
{
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch",
|
||||
}
|
||||
)
|
||||
EXPECTED_DENIED_OPERATIONS = frozenset(
|
||||
{
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite",
|
||||
}
|
||||
)
|
||||
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"authorization_header",
|
||||
"bearer",
|
||||
"client_secret",
|
||||
"password",
|
||||
"private_key",
|
||||
"raw_secret",
|
||||
"raw_token",
|
||||
"secret_value",
|
||||
)
|
||||
|
||||
|
||||
class BootstrapError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 1) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise BootstrapError("STATE_SYMLINK_REJECTED")
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(dict(value)))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, mode)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def secure_root_file(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise BootstrapError("BINDING_FILE_INVALID")
|
||||
st = path.lstat()
|
||||
if st.st_uid != 0:
|
||||
raise BootstrapError("BINDING_FILE_ROOT_OWNER_REQUIRED")
|
||||
if stat.S_IMODE(st.st_mode) & 0o077:
|
||||
raise BootstrapError("BINDING_FILE_PERMISSIONS_INVALID")
|
||||
if st.st_size <= 0 or st.st_size > 1_048_576:
|
||||
raise BootstrapError("BINDING_FILE_SIZE_INVALID")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("BINDING_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("BINDING_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if set(value) != {
|
||||
"schema",
|
||||
"owner_identity",
|
||||
"owner_role",
|
||||
"authenticated_scope",
|
||||
"request",
|
||||
}:
|
||||
raise BootstrapError("BINDING_FIELDS_REJECTED")
|
||||
if value["schema"] != BINDING_SCHEMA:
|
||||
raise BootstrapError("BINDING_SCHEMA_REJECTED")
|
||||
identity = value["owner_identity"]
|
||||
if not isinstance(identity, str) or not IDENTITY_RE.fullmatch(identity):
|
||||
raise BootstrapError("OWNER_IDENTITY_INVALID")
|
||||
if value["owner_role"] != "owner":
|
||||
raise BootstrapError("OWNER_ROLE_INVALID")
|
||||
scope = value["authenticated_scope"]
|
||||
if (
|
||||
not isinstance(scope, str)
|
||||
or not SCOPE_RE.fullmatch(scope)
|
||||
or scope == "writer:provision"
|
||||
or scope == "*"
|
||||
or scope.endswith(":*")
|
||||
):
|
||||
raise BootstrapError("OWNER_SCOPE_INVALID")
|
||||
|
||||
request = value["request"]
|
||||
if not isinstance(request, dict):
|
||||
raise BootstrapError("REQUEST_INVALID")
|
||||
expected_fields = {
|
||||
"action",
|
||||
"task_id",
|
||||
"repository",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"one_time_use",
|
||||
"expires_in_seconds",
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
}
|
||||
if set(request) != expected_fields:
|
||||
raise BootstrapError("REQUEST_FIELDS_REJECTED")
|
||||
if request["action"] != ACTION:
|
||||
raise BootstrapError("REQUEST_ACTION_REJECTED")
|
||||
if request["repository"] != "aios/aios":
|
||||
raise BootstrapError("REPOSITORY_REJECTED")
|
||||
if request["base_ref"] != "refs/heads/main":
|
||||
raise BootstrapError("BASE_REF_REJECTED")
|
||||
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
|
||||
raise BootstrapError("BASE_OID_REJECTED")
|
||||
if (
|
||||
not isinstance(request["target_branch"], str)
|
||||
or not BRANCH_RE.fullmatch(request["target_branch"])
|
||||
):
|
||||
raise BootstrapError("TARGET_BRANCH_REJECTED")
|
||||
if (
|
||||
not isinstance(request["task_id"], str)
|
||||
or not TASK_RE.fullmatch(request["task_id"])
|
||||
):
|
||||
raise BootstrapError("TASK_ID_REJECTED")
|
||||
if request["one_time_use"] is not True:
|
||||
raise BootstrapError("ONE_TIME_USE_REQUIRED")
|
||||
expiry = request["expires_in_seconds"]
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= 900
|
||||
):
|
||||
raise BootstrapError("EXPIRY_REJECTED")
|
||||
for field in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if request[field] is not False:
|
||||
raise BootstrapError("CONSEQUENTIAL_CAPABILITY_REJECTED")
|
||||
if (
|
||||
not isinstance(request["allowed_operations"], list)
|
||||
or frozenset(request["allowed_operations"]) != EXPECTED_ALLOWED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("ALLOWED_OPERATIONS_MISMATCH")
|
||||
if (
|
||||
not isinstance(request["denied_operations"], list)
|
||||
or frozenset(request["denied_operations"]) != EXPECTED_DENIED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("DENIED_OPERATIONS_MISMATCH")
|
||||
return {
|
||||
"schema": BINDING_SCHEMA,
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": scope,
|
||||
"request": dict(request),
|
||||
}
|
||||
|
||||
|
||||
def _status_uids(pid: int) -> tuple[int, int, int, int]:
|
||||
try:
|
||||
lines = Path(f"/proc/{pid}/status").read_text(encoding="utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_STATUS_UNAVAILABLE") from exc
|
||||
for line in lines:
|
||||
if line.startswith("Uid:"):
|
||||
fields = line.split()[1:]
|
||||
if len(fields) != 4:
|
||||
break
|
||||
return tuple(int(item) for item in fields) # type: ignore[return-value]
|
||||
raise BootstrapError("PEER_UID_STATUS_INVALID")
|
||||
|
||||
|
||||
def _loginuid(pid: int) -> int:
|
||||
try:
|
||||
raw = Path(f"/proc/{pid}/loginuid").read_text(encoding="ascii").strip()
|
||||
value = int(raw)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise BootstrapError("PEER_LOGINUID_UNAVAILABLE") from exc
|
||||
if value == 4294967295:
|
||||
raise BootstrapError("PEER_LOGIN_SESSION_REQUIRED")
|
||||
return value
|
||||
|
||||
|
||||
def _tty_path(pid: int) -> Path:
|
||||
try:
|
||||
value = os.readlink(f"/proc/{pid}/fd/0")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
if not (value.startswith("/dev/pts/") or value.startswith("/dev/tty")):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
path = Path(value)
|
||||
try:
|
||||
if not stat.S_ISCHR(path.stat().st_mode):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
return path
|
||||
|
||||
|
||||
def verify_peer_session(pid: int, uid: int, gid: int, owner_identity: str) -> dict[str, Any]:
|
||||
try:
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
except KeyError as exc:
|
||||
raise BootstrapError("OWNER_OS_ACCOUNT_MISSING") from exc
|
||||
if uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_OWNER_UID_MISMATCH")
|
||||
uids = _status_uids(pid)
|
||||
if any(item != owner.pw_uid for item in uids):
|
||||
raise BootstrapError("PEER_UID_TRANSITION_REJECTED")
|
||||
loginuid = _loginuid(pid)
|
||||
if loginuid != owner.pw_uid:
|
||||
# This deliberately rejects sudo/runuser from another authenticated
|
||||
# account; the session itself must have logged in as the Owner.
|
||||
raise BootstrapError("PEER_LOGINUID_OWNER_MISMATCH")
|
||||
tty = _tty_path(pid)
|
||||
if tty.stat().st_uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_TTY_OWNER_MISMATCH")
|
||||
return {
|
||||
"pid": pid,
|
||||
"uid": uid,
|
||||
"gid": gid,
|
||||
"loginuid": loginuid,
|
||||
"tty": str(tty),
|
||||
"owner_identity": owner_identity,
|
||||
}
|
||||
|
||||
|
||||
def read_frame(connection: socket.socket) -> dict[str, Any]:
|
||||
data = bytearray()
|
||||
while len(data) <= MAX_FRAME:
|
||||
block = connection.recv(4096)
|
||||
if not block:
|
||||
break
|
||||
data.extend(block)
|
||||
if b"\n" in block:
|
||||
break
|
||||
if not data or len(data) > MAX_FRAME:
|
||||
raise BootstrapError("PROTOCOL_FRAME_INVALID")
|
||||
raw = bytes(data).split(b"\n", 1)[0]
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def write_frame(connection: socket.socket, value: Mapping[str, Any]) -> None:
|
||||
connection.sendall(canonical_json(dict(value)))
|
||||
|
||||
|
||||
def safe_result(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
for key in value:
|
||||
lowered = str(key).lower()
|
||||
if any(fragment in lowered for fragment in SENSITIVE_FRAGMENTS):
|
||||
raise BootstrapError("BROKER_RESPONSE_SENSITIVE_DATA_REJECTED")
|
||||
allowed = {
|
||||
"status",
|
||||
"approval_plan_id",
|
||||
"grant_id",
|
||||
"grant_digest_sha256",
|
||||
"branch_oid",
|
||||
"branch_ref_created",
|
||||
"grant_consumed",
|
||||
"owner_authorization_event_id",
|
||||
"owner_authorization_reference",
|
||||
"owner_authorization_digest_sha256",
|
||||
"owner_authorization_consumed",
|
||||
"owner_authorization_idempotent",
|
||||
}
|
||||
return {key: value.get(key) for key in sorted(allowed) if key in value}
|
||||
|
||||
|
||||
def request_summary(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
request = binding["request"]
|
||||
return {
|
||||
"owner_identity": binding["owner_identity"],
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"expires_in_seconds": request["expires_in_seconds"],
|
||||
"allowed_operations": sorted(request["allowed_operations"]),
|
||||
"denied_operations": sorted(request["denied_operations"]),
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
}
|
||||
|
||||
|
||||
def _ensure_runtime_paths(socket_path: Path, state_root: Path, owner_identity: str) -> None:
|
||||
if os.geteuid() != 0:
|
||||
raise BootstrapError("SERVER_ROOT_REQUIRED")
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
for directory, mode, uid, gid in (
|
||||
(socket_path.parent, 0o750, 0, owner.pw_gid),
|
||||
(state_root, 0o700, 0, 0),
|
||||
):
|
||||
if directory.is_symlink():
|
||||
raise BootstrapError("RUNTIME_DIRECTORY_SYMLINK_REJECTED")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
os.chown(directory, uid, gid)
|
||||
os.chmod(directory, mode)
|
||||
if socket_path.exists() or socket_path.is_symlink():
|
||||
raise BootstrapError("SOCKET_ALREADY_EXISTS")
|
||||
|
||||
|
||||
def _lock(path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
||||
os.fchmod(fd, 0o600)
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return fd
|
||||
|
||||
|
||||
def invoke_broker(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
# Import only on the root server side. The bootstrap package does not carry
|
||||
# a second broker/provisioner implementation; it reuses current main.
|
||||
from owner_authorization_broker import ( # type: ignore
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationBrokerConfig,
|
||||
OwnerPrincipal,
|
||||
)
|
||||
|
||||
config = OwnerAuthorizationBrokerConfig.from_environment()
|
||||
identity = str(binding["owner_identity"])
|
||||
scope = str(binding["authenticated_scope"])
|
||||
if identity not in config.allowed_owner_identities:
|
||||
raise BootstrapError("OWNER_IDENTITY_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if scope not in config.allowed_intake_scopes:
|
||||
raise BootstrapError("OWNER_SCOPE_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if config.max_expiry_seconds < int(binding["request"]["expires_in_seconds"]):
|
||||
raise BootstrapError("LIVE_BROKER_EXPIRY_TOO_SMALL")
|
||||
principal = OwnerPrincipal(
|
||||
identity=identity,
|
||||
role="owner",
|
||||
authenticated_scope=scope,
|
||||
token_scopes=frozenset({scope}),
|
||||
credential_id="",
|
||||
)
|
||||
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
|
||||
binding["request"], principal
|
||||
)
|
||||
|
||||
|
||||
def serve(binding_path: Path, socket_path: Path, state_root: Path, timeout: int) -> int:
|
||||
binding = validate_binding(secure_root_file(binding_path))
|
||||
identity = str(binding["owner_identity"])
|
||||
_ensure_runtime_paths(socket_path, state_root, identity)
|
||||
lock_fd = _lock(state_root / "bootstrap.lock")
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
listener.bind(str(socket_path))
|
||||
owner = pwd.getpwnam(identity)
|
||||
os.chown(socket_path, 0, owner.pw_gid)
|
||||
os.chmod(socket_path, 0o660)
|
||||
listener.listen(1)
|
||||
listener.settimeout(timeout)
|
||||
|
||||
connection, _ = listener.accept()
|
||||
with connection:
|
||||
raw_peer = connection.getsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")
|
||||
)
|
||||
pid, uid, gid = struct.unpack("3i", raw_peer)
|
||||
session = verify_peer_session(pid, uid, gid, identity)
|
||||
|
||||
begin = read_frame(connection)
|
||||
if begin != {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"}:
|
||||
raise BootstrapError("CLIENT_BEGIN_REJECTED")
|
||||
|
||||
challenge = secrets.token_hex(12)
|
||||
challenge_issued = time.monotonic()
|
||||
summary = request_summary(binding)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CHALLENGE_SCHEMA,
|
||||
"challenge": challenge,
|
||||
"expires_in_seconds": CHALLENGE_TTL_SECONDS,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
confirm = read_frame(connection)
|
||||
expected_confirmation = f"AUTHORIZE {challenge}"
|
||||
if (
|
||||
confirm.get("schema") != CLIENT_CONFIRM_SCHEMA
|
||||
or confirm.get("challenge") != challenge
|
||||
or confirm.get("confirmation") != expected_confirmation
|
||||
):
|
||||
raise BootstrapError("OWNER_INTERACTIVE_CONFIRMATION_REJECTED")
|
||||
if time.monotonic() - challenge_issued > CHALLENGE_TTL_SECONDS:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_EXPIRED")
|
||||
|
||||
result = safe_result(invoke_broker(binding))
|
||||
receipt = {
|
||||
"schema": "aios.local-owner-auth-ingress-receipt.v1",
|
||||
"status": "CONSUMED",
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"peer": session,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"challenge_digest_sha256": hashlib.sha256(
|
||||
challenge.encode("ascii")
|
||||
).hexdigest(),
|
||||
"broker": result,
|
||||
"completed_at_unix": int(time.time()),
|
||||
}
|
||||
event_id = str(
|
||||
result.get("owner_authorization_event_id")
|
||||
or hashlib.sha256(
|
||||
canonical_json(
|
||||
{
|
||||
"peer": session,
|
||||
"request": sha256_json(binding["request"]),
|
||||
}
|
||||
)
|
||||
).hexdigest()[:32]
|
||||
)
|
||||
atomic_json(state_root / "events" / f"{event_id}.json", receipt)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": True,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
return 0
|
||||
except (BootstrapError, TimeoutError, socket.timeout) as exc:
|
||||
code = exc.code if isinstance(exc, BootstrapError) else "OWNER_BOOTSTRAP_TIMEOUT"
|
||||
try:
|
||||
if "connection" in locals():
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": False,
|
||||
"error": code,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
atomic_json(
|
||||
state_root / "last-failure.json",
|
||||
{
|
||||
"schema": "aios.local-owner-auth-ingress-failure.v1",
|
||||
"status": "FAILED",
|
||||
"error": code,
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"failed_at_unix": int(time.time()),
|
||||
},
|
||||
)
|
||||
return 2
|
||||
finally:
|
||||
listener.close()
|
||||
try:
|
||||
socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
finally:
|
||||
os.close(lock_fd)
|
||||
|
||||
|
||||
def client(socket_path: Path) -> int:
|
||||
# The server performs the authoritative peer/session checks. The client
|
||||
# also refuses non-interactive execution to make accidental automation hard.
|
||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||
raise BootstrapError("CLIENT_INTERACTIVE_TTY_REQUIRED")
|
||||
connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
connection.connect(str(socket_path))
|
||||
with connection:
|
||||
write_frame(connection, {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"})
|
||||
challenge = read_frame(connection)
|
||||
if challenge.get("schema") != CHALLENGE_SCHEMA:
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
summary = challenge.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
|
||||
print("\nAIOS one-shot Owner authorization request:")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
print("\nThis authorizes only the task writer shown above.")
|
||||
phrase = f"AUTHORIZE {challenge['challenge']}"
|
||||
typed = input(f"Type exactly:\n{phrase}\n> ").strip()
|
||||
if typed != phrase:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_MISMATCH")
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CLIENT_CONFIRM_SCHEMA,
|
||||
"challenge": challenge["challenge"],
|
||||
"confirmation": typed,
|
||||
},
|
||||
)
|
||||
result = read_frame(connection)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result.get("ok") is True else 3
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
mode = value.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--serve", action="store_true")
|
||||
mode.add_argument("--client", action="store_true")
|
||||
value.add_argument("--binding", type=Path)
|
||||
value.add_argument(
|
||||
"--socket",
|
||||
type=Path,
|
||||
default=Path("/run/aios-owner-bootstrap/owner.sock"),
|
||||
)
|
||||
value.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios-gitops/owner-local-auth-ingress"),
|
||||
)
|
||||
value.add_argument("--timeout", type=int, default=300)
|
||||
return value
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parser().parse_args(argv)
|
||||
if args.serve:
|
||||
if args.binding is None:
|
||||
raise SystemExit("--binding is required with --serve")
|
||||
return serve(args.binding, args.socket, args.state_root, args.timeout)
|
||||
return client(args.socket)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except BootstrapError as exc:
|
||||
print(json.dumps({"status": "BLOCKED", "error": exc.code}), file=sys.stderr)
|
||||
raise SystemExit(exc.status)
|
||||
@@ -1,32 +0,0 @@
|
||||
"""Read-only release reconciliation and durable follow-up intent."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReconciliationReport:
|
||||
healthy: bool
|
||||
drift: tuple[str, ...]
|
||||
actions: tuple[str, ...]
|
||||
|
||||
|
||||
class ReconciliationController:
|
||||
CHECKS = (
|
||||
("authority_main", "gitea_main", "main_drift"),
|
||||
("local_tag_oid", "remote_tag_oid", "tag_drift"),
|
||||
("remote_peeled_target", "approved_target_oid", "peeled_target_drift"),
|
||||
("registry_version", "baseline_version", "registry_baseline_drift"),
|
||||
("approved_artifact_digest", "deployed_artifact_digest", "artifact_drift"),
|
||||
("declared_production_state", "service_healthy", "service_health_drift"),
|
||||
)
|
||||
|
||||
def reconcile(self, actual: Mapping[str, Any]) -> ReconciliationReport:
|
||||
drift: list[str] = []
|
||||
for left, right, label in self.CHECKS:
|
||||
if actual.get(left) != actual.get(right):
|
||||
drift.append(label)
|
||||
actions = () if not drift else ("open_reconciliation_task", "open_reconciliation_pr", "attach_evidence", "notify_owner")
|
||||
return ReconciliationReport(not drift, tuple(drift), actions)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Durable-friendly recurring task scheduling primitives."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
class RecurringTaskError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecurringTask:
|
||||
task_id: str
|
||||
interval_seconds: int
|
||||
next_run_at: datetime
|
||||
run_count: int = 0
|
||||
max_runs: int | None = None
|
||||
cancelled: bool = False
|
||||
|
||||
def due(self, now: datetime) -> bool:
|
||||
return not self.cancelled and (self.max_runs is None or self.run_count < self.max_runs) and now >= self.next_run_at
|
||||
|
||||
|
||||
class RecurringTaskScheduler:
|
||||
def __init__(self) -> None:
|
||||
self.tasks: dict[str, RecurringTask] = {}
|
||||
self.successors: set[str] = set()
|
||||
|
||||
def schedule(self, task_id: str, interval_seconds: int, *, now: datetime | None = None, max_runs: int | None = None) -> RecurringTask:
|
||||
if task_id in self.tasks:
|
||||
raise RecurringTaskError("task already exists")
|
||||
if interval_seconds <= 0 or (max_runs is not None and max_runs <= 0):
|
||||
raise RecurringTaskError("invalid recurring task bounds")
|
||||
now = now or datetime.now(timezone.utc)
|
||||
task = RecurringTask(task_id, interval_seconds, now, 0, max_runs, False)
|
||||
self.tasks[task_id] = task
|
||||
return task
|
||||
|
||||
def due(self, *, now: datetime | None = None) -> list[RecurringTask]:
|
||||
now = now or datetime.now(timezone.utc)
|
||||
return [task for task in self.tasks.values() if task.due(now)]
|
||||
|
||||
def mark_run(self, task_id: str, *, now: datetime | None = None) -> RecurringTask:
|
||||
task = self.tasks[task_id]
|
||||
now = now or datetime.now(timezone.utc)
|
||||
if not task.due(now):
|
||||
raise RecurringTaskError("task is not due")
|
||||
task.run_count += 1
|
||||
task.next_run_at = now + timedelta(seconds=task.interval_seconds)
|
||||
return task
|
||||
|
||||
def cancel(self, task_id: str) -> None:
|
||||
self.tasks[task_id].cancelled = True
|
||||
|
||||
def create_successor_once(self, predecessor: str, successor: str, *, interval_seconds: int, now: datetime | None = None) -> RecurringTask:
|
||||
key = f"{predecessor}->{successor}"
|
||||
if key in self.successors:
|
||||
return self.tasks[successor]
|
||||
self.successors.add(key)
|
||||
if successor not in self.tasks:
|
||||
return self.schedule(successor, interval_seconds, now=now)
|
||||
return self.tasks[successor]
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Atomic Registry/Baseline finalizer for an already ACTIVE exact release.
|
||||
|
||||
This adapter never writes Git refs. It is intended for the AIOS runtime
|
||||
Registry and authoritative runtime-anchor Baseline only.
|
||||
|
||||
Registry is finalized before Baseline. Baseline apply independently verifies:
|
||||
- exact Authority annotated tag and peeled target
|
||||
- exact ACTIVE production health
|
||||
- exact V2R1C7 Registry record
|
||||
- exact previous Baseline file SHA-256 (CAS)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Existing runtime-authority files may be root:gitops and read-only to
|
||||
# non-root readers. Preserve their owner/group/mode across os.replace().
|
||||
if path.exists():
|
||||
metadata = path.lstat()
|
||||
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
|
||||
raise RuntimeError("ATOMIC_TARGET_NOT_REGULAR")
|
||||
target_mode = stat.S_IMODE(metadata.st_mode)
|
||||
target_uid = metadata.st_uid
|
||||
target_gid = metadata.st_gid
|
||||
else:
|
||||
parent = path.parent.stat()
|
||||
target_mode = mode
|
||||
target_uid = 0 if os.geteuid() == 0 else os.geteuid()
|
||||
target_gid = parent.st_gid
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as output:
|
||||
json.dump(dict(value), output, sort_keys=True, indent=2)
|
||||
output.write("\n")
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.chmod(name, target_mode)
|
||||
if os.geteuid() == 0:
|
||||
os.chown(name, target_uid, target_gid)
|
||||
os.replace(name, path)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict) or value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not SHA_RE.fullmatch(str(value.get("artifact_sha256", ""))):
|
||||
raise ValueError("artifact_sha256_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if "tag_oid" in value and not OID_RE.fullmatch(str(value["tag_oid"])):
|
||||
raise ValueError("tag_oid_invalid")
|
||||
return value
|
||||
|
||||
|
||||
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={repository}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def verify_local_tag(repository: Path, value: Mapping[str, Any]) -> tuple[str, str]:
|
||||
tag_oid = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}").stdout.strip()
|
||||
peeled = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}^{{commit}}").stdout.strip()
|
||||
if tag_oid == peeled or peeled != value["target_oid"]:
|
||||
raise RuntimeError("FORMAL_TAG_BINDING_MISMATCH")
|
||||
requested = value.get("tag_oid")
|
||||
if requested and requested != tag_oid:
|
||||
raise RuntimeError("FORMAL_TAG_OBJECT_MISMATCH")
|
||||
return tag_oid, peeled
|
||||
|
||||
|
||||
def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError) as exc:
|
||||
raise RuntimeError("PRODUCTION_HEALTH_UNAVAILABLE") from exc
|
||||
if not (
|
||||
status == 200
|
||||
and body.get("version") == value["version"]
|
||||
and body.get("commit_oid") == value["target_oid"]
|
||||
and body.get("artifact_sha256") == value["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
):
|
||||
raise RuntimeError("PRODUCTION_HEALTH_BINDING_MISMATCH")
|
||||
return body
|
||||
|
||||
|
||||
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-registry.v2",
|
||||
"version": value["version"],
|
||||
"repository": "aios/aios",
|
||||
"target_oid": value["target_oid"],
|
||||
"tag": value["version"],
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object": tag_oid,
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"registered": True,
|
||||
"enabled": True,
|
||||
"production_deployed": True,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
return all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
}.items()
|
||||
) and record.get("registered") is True and record.get("enabled") is True
|
||||
|
||||
|
||||
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
return current == value["version"] and all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
}.items()
|
||||
)
|
||||
|
||||
|
||||
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
if not path.exists():
|
||||
return exact_response(value, "NOT_APPLIED", tag_oid=tag_oid)
|
||||
record = json.loads(path.read_text(encoding="utf-8"))
|
||||
if registry_matches(record, value, tag_oid):
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
registry_path=str(path),
|
||||
registry_sha256=sha256_file(path),
|
||||
)
|
||||
return exact_response(value, "CONFLICT", tag_oid=tag_oid, reason="registry_binding_mismatch")
|
||||
|
||||
|
||||
def registry_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = registry_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
atomic_json(path, registry_record(value, tag_oid, production_health))
|
||||
return registry_probe(args, value)
|
||||
|
||||
|
||||
def secure_baseline_authority_file(path: Path) -> None:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("BASELINE_AUTHORITY_FILE_INVALID")
|
||||
baseline_stat = path.lstat()
|
||||
if baseline_stat.st_uid != 0 or baseline_stat.st_mode & 0o022:
|
||||
raise RuntimeError("BASELINE_AUTHORITY_PERMISSIONS_INVALID")
|
||||
|
||||
|
||||
def baseline_sidecar_path(path: Path) -> Path:
|
||||
return path.with_name("current.sha256")
|
||||
|
||||
|
||||
def secure_baseline_sidecar_file(path: Path) -> None:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("BASELINE_SIDECAR_FILE_INVALID")
|
||||
sidecar_stat = path.lstat()
|
||||
if sidecar_stat.st_uid != 0 or sidecar_stat.st_mode & 0o022:
|
||||
raise RuntimeError("BASELINE_SIDECAR_PERMISSIONS_INVALID")
|
||||
|
||||
|
||||
def read_baseline_sidecar(path: Path) -> str | None:
|
||||
if not path.exists() and not path.is_symlink():
|
||||
return None
|
||||
secure_baseline_sidecar_file(path)
|
||||
fields = path.read_text(encoding="utf-8").strip().split()
|
||||
if not fields or not SHA_RE.fullmatch(fields[0]):
|
||||
raise RuntimeError("BASELINE_SIDECAR_DIGEST_INVALID")
|
||||
return fields[0]
|
||||
|
||||
|
||||
def reconcile_baseline_sidecar(path: Path, baseline_digest: str) -> str:
|
||||
if not SHA_RE.fullmatch(baseline_digest):
|
||||
raise RuntimeError("BASELINE_DIGEST_INVALID")
|
||||
sidecar = baseline_sidecar_path(path)
|
||||
if sidecar.exists() or sidecar.is_symlink():
|
||||
secure_baseline_sidecar_file(sidecar)
|
||||
metadata = sidecar.lstat()
|
||||
target_mode = stat.S_IMODE(metadata.st_mode)
|
||||
target_uid = metadata.st_uid
|
||||
target_gid = metadata.st_gid
|
||||
else:
|
||||
metadata = path.lstat()
|
||||
target_mode = 0o440
|
||||
target_uid = 0
|
||||
target_gid = metadata.st_gid
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix=".current.sha256.", dir=str(sidecar.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as output:
|
||||
output.write(f"{baseline_digest} current.json" + chr(10))
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.chmod(name, target_mode)
|
||||
if os.geteuid() == 0:
|
||||
os.chown(name, target_uid, target_gid)
|
||||
os.replace(name, sidecar)
|
||||
dfd = os.open(sidecar.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if sha256_file(path) != baseline_digest:
|
||||
raise RuntimeError("RECONCILIATION_REQUIRED_CONCURRENT_BASELINE_CHANGE")
|
||||
if read_baseline_sidecar(sidecar) != baseline_digest:
|
||||
raise RuntimeError("BASELINE_SIDECAR_READBACK_MISMATCH")
|
||||
return baseline_digest
|
||||
|
||||
|
||||
def baseline_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
secure_baseline_authority_file(args.baseline_path)
|
||||
record = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if baseline_matches(record, value, tag_oid):
|
||||
baseline_digest = sha256_file(args.baseline_path)
|
||||
sidecar_digest = read_baseline_sidecar(baseline_sidecar_path(args.baseline_path))
|
||||
if sidecar_digest != baseline_digest:
|
||||
sidecar_digest = reconcile_baseline_sidecar(
|
||||
args.baseline_path,
|
||||
baseline_digest,
|
||||
)
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
baseline_path=str(args.baseline_path),
|
||||
baseline_sha256=baseline_digest,
|
||||
baseline_sidecar_sha256=sidecar_digest,
|
||||
)
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
current_sha = sha256_file(args.baseline_path)
|
||||
if (
|
||||
current == args.expected_previous_baseline
|
||||
and current_sha == args.expected_previous_baseline_sha256
|
||||
):
|
||||
return exact_response(
|
||||
value,
|
||||
"NOT_APPLIED",
|
||||
tag_oid=tag_oid,
|
||||
previous_baseline=current,
|
||||
previous_baseline_sha256=current_sha,
|
||||
)
|
||||
return exact_response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
tag_oid=tag_oid,
|
||||
reason="baseline_precondition_or_binding_mismatch",
|
||||
observed_baseline=current,
|
||||
observed_sha256=current_sha,
|
||||
)
|
||||
|
||||
|
||||
def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = baseline_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
|
||||
registry_path = args.registry_root / f"{value['version']}.json"
|
||||
if not registry_path.is_file():
|
||||
raise RuntimeError("REGISTRY_NOT_FINALIZED")
|
||||
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
if not registry_matches(registry, value, tag_oid):
|
||||
raise RuntimeError("REGISTRY_BINDING_NOT_VERIFIED")
|
||||
|
||||
previous = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if sha256_file(args.baseline_path) != args.expected_previous_baseline_sha256:
|
||||
raise RuntimeError("BASELINE_CAS_MISMATCH")
|
||||
|
||||
generation = max(
|
||||
int(previous.get("generation", 0) or 0),
|
||||
int(previous.get("authority_generation", 0) or 0),
|
||||
) + 1
|
||||
record = {
|
||||
"schema": "aios.production-baseline.v2",
|
||||
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
|
||||
"current_baseline": value["version"],
|
||||
"version": value["version"],
|
||||
"previous_baseline": args.expected_previous_baseline,
|
||||
"previous_version": args.expected_previous_baseline,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"target_oid": value["target_oid"],
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_artifact_sha256": value["artifact_sha256"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"registry_digest": sha256_file(registry_path),
|
||||
"registry_source": str(registry_path),
|
||||
"generation": generation,
|
||||
"authority_generation": generation,
|
||||
# The actual previously deployed production baseline is the rollback
|
||||
# target. V2R1C6 has a formal tag but was never production-deployed.
|
||||
"rollback_version": args.expected_previous_baseline,
|
||||
"rollback_tag_oid": previous.get("tag_oid"),
|
||||
"rollback_target_oid": previous.get("target_oid"),
|
||||
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"promoted_by": "aios-approved-production-release-executor",
|
||||
}
|
||||
atomic_json(args.baseline_path, record)
|
||||
baseline_digest = sha256_file(args.baseline_path)
|
||||
reconcile_baseline_sidecar(args.baseline_path, baseline_digest)
|
||||
return baseline_probe(args, value)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("registry", "baseline"),
|
||||
default=os.environ.get("AIOS_RELEASE_METADATA_MODE"),
|
||||
required=os.environ.get("AIOS_RELEASE_METADATA_MODE") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repository",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry-root",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_REGISTRY_ROOT"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--baseline-path",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_BASELINE_PATH"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE", "V2R1C5"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline-sha256",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256"),
|
||||
required=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--health-url",
|
||||
default=os.environ.get("AIOS_RELEASE_HEALTH_URL", "http://127.0.0.1:18927/health"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if args.mode == "registry":
|
||||
if operation == "probe":
|
||||
result = registry_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = registry_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
else:
|
||||
if operation == "probe":
|
||||
result = baseline_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = baseline_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Release state machine with explicit fail-closed production gates."""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Mapping
|
||||
|
||||
|
||||
class InvalidTransition(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ReleaseState(str, Enum):
|
||||
SOURCE_READY = "SOURCE_READY"
|
||||
TESTED = "TESTED"
|
||||
PR_OPEN = "PR_OPEN"
|
||||
PR_APPROVED = "PR_APPROVED"
|
||||
MERGED = "MERGED"
|
||||
ARTIFACT_VERIFIED = "ARTIFACT_VERIFIED"
|
||||
RELEASE_APPROVED = "RELEASE_APPROVED"
|
||||
TAG_VERIFIED = "TAG_VERIFIED"
|
||||
DEPLOYED = "DEPLOYED"
|
||||
PRODUCTION_ACCEPTED = "PRODUCTION_ACCEPTED"
|
||||
|
||||
|
||||
TRANSITIONS = {
|
||||
ReleaseState.SOURCE_READY: (ReleaseState.TESTED,),
|
||||
ReleaseState.TESTED: (ReleaseState.PR_OPEN,),
|
||||
ReleaseState.PR_OPEN: (ReleaseState.PR_APPROVED,),
|
||||
ReleaseState.PR_APPROVED: (ReleaseState.MERGED,),
|
||||
ReleaseState.MERGED: (ReleaseState.ARTIFACT_VERIFIED,),
|
||||
ReleaseState.ARTIFACT_VERIFIED: (ReleaseState.RELEASE_APPROVED,),
|
||||
ReleaseState.RELEASE_APPROVED: (ReleaseState.TAG_VERIFIED,),
|
||||
ReleaseState.TAG_VERIFIED: (ReleaseState.DEPLOYED,),
|
||||
ReleaseState.DEPLOYED: (ReleaseState.PRODUCTION_ACCEPTED,),
|
||||
}
|
||||
|
||||
|
||||
class ReleaseStateMachine:
|
||||
def __init__(self, state: ReleaseState = ReleaseState.SOURCE_READY) -> None:
|
||||
self.state = state
|
||||
|
||||
def transition(self, target: ReleaseState, evidence: Mapping[str, object] | None = None) -> ReleaseState:
|
||||
if target not in TRANSITIONS.get(self.state, ()):
|
||||
raise InvalidTransition(f"{self.state.value}_to_{target.value}_denied")
|
||||
evidence = evidence or {}
|
||||
if target is ReleaseState.PRODUCTION_ACCEPTED:
|
||||
if evidence.get("tag_verified") is not True or evidence.get("remote_tag_readback") is not True or evidence.get("service_health") is not True:
|
||||
raise InvalidTransition("production_acceptance_evidence_incomplete")
|
||||
if target is ReleaseState.DEPLOYED and evidence.get("artifact_verified") is not True:
|
||||
raise InvalidTransition("deployment_artifact_not_verified")
|
||||
self.state = target
|
||||
return self.state
|
||||
|
||||
@@ -1,520 +0,0 @@
|
||||
"""Compatibility adapter for the deployed signer and validator contracts.
|
||||
|
||||
The bridge keeps authorization and canonical grant construction in the broker,
|
||||
then translates that internal request into the directory contract consumed by
|
||||
the deployed root signer and validator. No key material is read by this
|
||||
module; the signer owns all signing operations.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
|
||||
_SIGNER_FLAGS = (
|
||||
"--namespace",
|
||||
"--input",
|
||||
"--signature-out",
|
||||
"--key-id-out",
|
||||
)
|
||||
|
||||
|
||||
class SignerContractError(RuntimeError):
|
||||
"""Raised when the deployed contract cannot be normalized safely."""
|
||||
|
||||
|
||||
def canonical_json_bytes(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
|
||||
|
||||
|
||||
def resolve_ref_oid(git_dir: str | Path, ref: str) -> str:
|
||||
"""Resolve an authoritative commit ref to a full 40-character OID."""
|
||||
if not isinstance(ref, str) or not re.fullmatch(r"refs/[A-Za-z0-9._/-]+", ref):
|
||||
raise SignerContractError("authoritative_ref_invalid")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"--git-dir",
|
||||
str(git_dir),
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"{ref}^{{commit}}",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=15,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SignerContractError("authoritative_ref_unavailable")
|
||||
oid = result.stdout.strip().lower()
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", oid):
|
||||
raise SignerContractError("authoritative_ref_invalid_oid")
|
||||
return oid
|
||||
|
||||
|
||||
def _read(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise SignerContractError("contract_input_missing")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SignerContractError("contract_input_invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SignerContractError("contract_object_required")
|
||||
return value
|
||||
|
||||
|
||||
def _write(path: Path, value: Any) -> None:
|
||||
if path.is_symlink():
|
||||
raise SignerContractError("contract_output_symlink")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_bytes(canonical_json_bytes(value))
|
||||
os.chmod(temporary, 0o600)
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, 0o600)
|
||||
|
||||
|
||||
def deployed_signer_help(stdout: str, stderr: str) -> bool:
|
||||
text = (stdout or "") + "\n" + (stderr or "")
|
||||
return all(flag in text for flag in _SIGNER_FLAGS)
|
||||
|
||||
|
||||
def _supports_deployed_signer(command: Sequence[str]) -> bool:
|
||||
result = subprocess.run(
|
||||
[*command, "sign", "--help"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=15,
|
||||
)
|
||||
return deployed_signer_help(result.stdout, result.stderr)
|
||||
|
||||
|
||||
def _run(
|
||||
argv: Sequence[str],
|
||||
*,
|
||||
env: Mapping[str, str] | None = None,
|
||||
timeout: int = 60,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
result = subprocess.run(
|
||||
list(argv),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=timeout,
|
||||
env=dict(env) if env is not None else None,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SignerContractError(
|
||||
f"deployed_tool_failed:{Path(argv[0]).name}:{result.returncode}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _sign(
|
||||
command: Sequence[str],
|
||||
namespace: str,
|
||||
input_path: Path,
|
||||
signature_path: Path,
|
||||
key_id_path: Path,
|
||||
) -> None:
|
||||
if signature_path.exists() or key_id_path.exists():
|
||||
raise SignerContractError("contract_output_exists")
|
||||
_run(
|
||||
[
|
||||
*command,
|
||||
"sign",
|
||||
"--namespace",
|
||||
namespace,
|
||||
"--input",
|
||||
str(input_path),
|
||||
"--signature-out",
|
||||
str(signature_path),
|
||||
"--key-id-out",
|
||||
str(key_id_path),
|
||||
],
|
||||
timeout=60,
|
||||
)
|
||||
if not signature_path.is_file() or not key_id_path.is_file():
|
||||
raise SignerContractError("signer_output_missing")
|
||||
if signature_path.stat().st_size <= 0 or key_id_path.stat().st_size <= 0:
|
||||
raise SignerContractError("signer_output_empty")
|
||||
|
||||
|
||||
def _manifest(grant_dir: Path, names: Sequence[str]) -> None:
|
||||
rows = []
|
||||
for name in names:
|
||||
path = grant_dir / name
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise SignerContractError("manifest_input_missing")
|
||||
rows.append(
|
||||
f"{hashlib.sha256(path.read_bytes()).hexdigest()} {name}"
|
||||
)
|
||||
(grant_dir / "MANIFEST.sha256").write_text(
|
||||
"\n".join(rows) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(grant_dir / "MANIFEST.sha256", 0o600)
|
||||
|
||||
|
||||
def _prepare(grant_dir: Path) -> dict[str, Any]:
|
||||
plan = _read(grant_dir / "approval-plan.json")
|
||||
request = _read(grant_dir / "grant-request.json")
|
||||
base = request.get("base")
|
||||
target = request.get("target")
|
||||
owner = request.get("owner_authorization")
|
||||
authorization = request.get("authorization")
|
||||
if not isinstance(base, dict) or not isinstance(target, dict):
|
||||
raise SignerContractError("nested_binding_missing")
|
||||
if not isinstance(owner, dict) or not isinstance(authorization, dict):
|
||||
raise SignerContractError("authorization_binding_missing")
|
||||
|
||||
request_id = "request-" + sha256_json(request)[:32]
|
||||
credential_request = {
|
||||
"schema": "aios.credential-request.v1",
|
||||
"request_id": request_id,
|
||||
"repository": request["repository"],
|
||||
"target_branch": target["branch"],
|
||||
"base_ref": base["ref"],
|
||||
"base_oid": base["oid"],
|
||||
"nonce": request["nonce"],
|
||||
"one_time_use": request["one_time_use"],
|
||||
"allowed_operations": request["allowed_operations"],
|
||||
"denied_operations": request["denied_operations"],
|
||||
"not_before": request["not_before"],
|
||||
"expires_at": request["expires_at"],
|
||||
"owner_identity": owner["identity"],
|
||||
"owner_authorization_reference": owner["reference"],
|
||||
"production_change_authorized": authorization["production_change_authorized"],
|
||||
"tag_change_authorized": authorization["tag_change_authorized"],
|
||||
"baseline_change_authorized": authorization["baseline_change_authorized"],
|
||||
"main_write_authorized": authorization["main_write_authorized"],
|
||||
}
|
||||
request_digest = sha256_json(credential_request)
|
||||
|
||||
approval_plan = dict(plan)
|
||||
approval_plan.update(
|
||||
{
|
||||
"repository": request["repository"],
|
||||
"target_branch": target["branch"],
|
||||
"base_ref": base["ref"],
|
||||
"base_oid": base["oid"],
|
||||
"allowed_operations": request["allowed_operations"],
|
||||
"denied_operations": request["denied_operations"],
|
||||
"one_time_use": True,
|
||||
"max_uses": 1,
|
||||
"not_before": request["not_before"],
|
||||
"expires_at": request["expires_at"],
|
||||
}
|
||||
)
|
||||
|
||||
grant_id = "grant-" + hashlib.sha256(
|
||||
(request_id + "\0" + request["nonce"]).encode("utf-8")
|
||||
).hexdigest()[:32]
|
||||
grant_payload = {
|
||||
"schema": "aios.grant-payload.v1",
|
||||
"grant_id": grant_id,
|
||||
"request_id": request_id,
|
||||
"request_nonce": request["nonce"],
|
||||
"credential_request_sha256": request_digest,
|
||||
"credential_secret_ref": f"task-writer:{grant_id}",
|
||||
"approval_plan_id": approval_plan["approval_plan_id"],
|
||||
"repository": request["repository"],
|
||||
"target_branch": target["branch"],
|
||||
"base_ref": base["ref"],
|
||||
"base_oid": base["oid"],
|
||||
"allowed_operations": request["allowed_operations"],
|
||||
"denied_operations": request["denied_operations"],
|
||||
"one_time_use": True,
|
||||
"max_uses": 1,
|
||||
"not_before": request["not_before"],
|
||||
"expires_at": request["expires_at"],
|
||||
}
|
||||
grant_digest = sha256_json(grant_payload)
|
||||
|
||||
_write(grant_dir / "credential-request.json", credential_request)
|
||||
(grant_dir / "credential-request.canonical.json").write_bytes(
|
||||
canonical_json_bytes(credential_request)
|
||||
)
|
||||
os.chmod(grant_dir / "credential-request.canonical.json", 0o600)
|
||||
_write(grant_dir / "approval-plan.json", approval_plan)
|
||||
(grant_dir / "approval-plan.canonical.json").write_bytes(
|
||||
canonical_json_bytes(approval_plan)
|
||||
)
|
||||
os.chmod(grant_dir / "approval-plan.canonical.json", 0o600)
|
||||
_write(grant_dir / "grant-payload.json", grant_payload)
|
||||
(grant_dir / "grant-payload.canonical.json").write_bytes(
|
||||
canonical_json_bytes(grant_payload)
|
||||
)
|
||||
os.chmod(grant_dir / "grant-payload.canonical.json", 0o600)
|
||||
_write(
|
||||
grant_dir / "signed-grant.json",
|
||||
{
|
||||
"schema": "aios.signed-grant.v1",
|
||||
"grant_id": grant_id,
|
||||
"credential_request_sha256": request_digest,
|
||||
"signed_payload_sha256": grant_digest,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"request": credential_request,
|
||||
"approval_plan": approval_plan,
|
||||
"grant": grant_payload,
|
||||
"request_digest": request_digest,
|
||||
"grant_digest": grant_digest,
|
||||
"grant_id": grant_id,
|
||||
}
|
||||
|
||||
|
||||
def run_compatible_signer(
|
||||
signer_command: Sequence[str],
|
||||
grant_dir: Path,
|
||||
result_path: Path,
|
||||
) -> dict[str, str]:
|
||||
try:
|
||||
if not _supports_deployed_signer(signer_command):
|
||||
_run(
|
||||
[
|
||||
*signer_command,
|
||||
"--grant-dir",
|
||||
str(grant_dir),
|
||||
"--result-json",
|
||||
str(result_path),
|
||||
],
|
||||
timeout=60,
|
||||
)
|
||||
legacy = _read(result_path)
|
||||
if legacy.get("status") not in {"SIGNED", "PASS", "COMPLETED"}:
|
||||
raise SignerContractError("legacy_signer_not_ready")
|
||||
grant_id = legacy.get("grant_id")
|
||||
digest = legacy.get("grant_digest_sha256")
|
||||
signer = legacy.get("signing_key_identity") or legacy.get(
|
||||
"signer_identity"
|
||||
)
|
||||
if not all(
|
||||
isinstance(value, str) and value.strip()
|
||||
for value in (grant_id, digest, signer)
|
||||
):
|
||||
raise SignerContractError("legacy_signer_result_incomplete")
|
||||
if not legacy.get("signature_present"):
|
||||
raise SignerContractError("legacy_signature_missing")
|
||||
return {
|
||||
"grant_id": grant_id,
|
||||
"grant_digest_sha256": digest,
|
||||
"signing_key_identity": signer,
|
||||
}
|
||||
prepared = _prepare(grant_dir)
|
||||
config_path = Path(
|
||||
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
|
||||
)
|
||||
config = _read(config_path)
|
||||
grant_namespace = str(config["grant_namespace"])
|
||||
approval_namespace = str(config["approval_namespace"])
|
||||
_sign(
|
||||
signer_command,
|
||||
approval_namespace,
|
||||
grant_dir / "approval-plan.canonical.json",
|
||||
grant_dir / "approval-plan.sig",
|
||||
grant_dir / "approval-plan.key-id",
|
||||
)
|
||||
_sign(
|
||||
signer_command,
|
||||
grant_namespace,
|
||||
grant_dir / "grant-payload.canonical.json",
|
||||
grant_dir / "signed-grant.sig",
|
||||
grant_dir / "signed-grant.key-id",
|
||||
)
|
||||
key_identity = (
|
||||
grant_dir / "signed-grant.key-id"
|
||||
).read_text(encoding="utf-8").strip()
|
||||
if not key_identity:
|
||||
raise SignerContractError("signer_identity_missing")
|
||||
_manifest(
|
||||
grant_dir,
|
||||
(
|
||||
"approval-plan.json",
|
||||
"approval-plan.canonical.json",
|
||||
"approval-plan.sig",
|
||||
"approval-plan.key-id",
|
||||
"credential-request.json",
|
||||
"credential-request.canonical.json",
|
||||
"grant-payload.json",
|
||||
"grant-payload.canonical.json",
|
||||
"signed-grant.json",
|
||||
"signed-grant.sig",
|
||||
"signed-grant.key-id",
|
||||
),
|
||||
)
|
||||
result = {
|
||||
"schema": "aios.compatible-signer-result.v1",
|
||||
"status": "SIGNED",
|
||||
"approval_plan_id": prepared["approval_plan"]["approval_plan_id"],
|
||||
"grant_id": prepared["grant_id"],
|
||||
"grant_digest_sha256": prepared["grant_digest"],
|
||||
"signing_key_identity": key_identity,
|
||||
"signer_identity": key_identity,
|
||||
"signature_present": True,
|
||||
}
|
||||
_write(result_path, result)
|
||||
return {
|
||||
"grant_id": prepared["grant_id"],
|
||||
"grant_digest_sha256": prepared["grant_digest"],
|
||||
"signing_key_identity": key_identity,
|
||||
}
|
||||
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
||||
_write(
|
||||
result_path,
|
||||
{
|
||||
"schema": "aios.compatible-signer-result.v1",
|
||||
"status": "BLOCKED",
|
||||
"error_code": str(exc),
|
||||
},
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def _validator_config(
|
||||
grant_dir: Path,
|
||||
request: Mapping[str, Any],
|
||||
) -> Path:
|
||||
root_config = Path(
|
||||
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
|
||||
)
|
||||
config = _read(root_config)
|
||||
repo_path = (
|
||||
os.environ.get("AIOS_GITOPS_VALIDATOR_REPO_PATH")
|
||||
or os.environ.get("AIOS_GITOPS_WORKSPACE")
|
||||
or config.get("repo_path")
|
||||
)
|
||||
config.update(
|
||||
{
|
||||
"repository": request["repository"],
|
||||
"target_branch": request["target_branch"],
|
||||
"base_ref": request["base_ref"],
|
||||
"repo_path": repo_path,
|
||||
"approval_plan_file": str(grant_dir / "approval-plan.json"),
|
||||
"approval_plan_canonical_file": str(
|
||||
grant_dir / "approval-plan.canonical.json"
|
||||
),
|
||||
"approval_plan_signature_file": str(grant_dir / "approval-plan.sig"),
|
||||
"consumed_grants_ledger": str(
|
||||
grant_dir / "consumed-grants.jsonl"
|
||||
),
|
||||
}
|
||||
)
|
||||
path = grant_dir.parent / "compat-validator-config.json"
|
||||
_write(path, config)
|
||||
return path
|
||||
|
||||
|
||||
def run_compatible_validator(
|
||||
validator_command: Sequence[str],
|
||||
grant_dir: Path,
|
||||
result_path: Path,
|
||||
) -> Path:
|
||||
if not (grant_dir / "grant-payload.json").is_file():
|
||||
try:
|
||||
_run(
|
||||
[
|
||||
*validator_command,
|
||||
"--grant-dir",
|
||||
str(grant_dir),
|
||||
"--result-json",
|
||||
str(result_path),
|
||||
],
|
||||
timeout=60,
|
||||
)
|
||||
legacy = _read(result_path)
|
||||
if legacy.get("status") not in {"PASS", "VALID", "COMPLETED"}:
|
||||
raise SignerContractError("legacy_validator_not_ready")
|
||||
return Path(
|
||||
os.environ.get(
|
||||
"AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json"
|
||||
)
|
||||
)
|
||||
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
||||
_write(
|
||||
result_path,
|
||||
{
|
||||
"schema": "aios.compatible-validator-result.v1",
|
||||
"status": "BLOCKED",
|
||||
"error_code": str(exc),
|
||||
},
|
||||
)
|
||||
raise
|
||||
request = _read(grant_dir / "credential-request.json")
|
||||
config_path = _validator_config(grant_dir, request)
|
||||
env = os.environ.copy()
|
||||
env["AIOS_GRANT_ROOT_CONFIG"] = str(config_path)
|
||||
try:
|
||||
_run(
|
||||
[
|
||||
*validator_command,
|
||||
"--grant-dir",
|
||||
str(grant_dir),
|
||||
"--result-json",
|
||||
str(result_path),
|
||||
],
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
actual = _read(result_path)
|
||||
if actual.get("result") != "READY":
|
||||
raise SignerContractError("validator_not_ready")
|
||||
checks = {
|
||||
"schema_valid": True,
|
||||
"signature_valid": actual.get("signature_verified") is True,
|
||||
"signer_trusted": actual.get("signature_verified") is True,
|
||||
"approval_plan_active": actual.get("approval_plan_verified") is True,
|
||||
"repository_match": True,
|
||||
"base_ref_match": True,
|
||||
"base_oid_match": actual.get("remote_base_verified") is True,
|
||||
"target_branch_match": actual.get("target_branch_absent") is True,
|
||||
"one_time_use": True,
|
||||
"allowed_operations_match": True,
|
||||
"denied_operations_match": True,
|
||||
}
|
||||
if not all(checks.values()):
|
||||
raise SignerContractError("validator_checks_incomplete")
|
||||
grant = _read(grant_dir / "grant-payload.json")
|
||||
result = {
|
||||
"schema": "aios.compatible-validator-result.v1",
|
||||
"status": "VALID",
|
||||
"approval_plan_id": grant["approval_plan_id"],
|
||||
"grant_digest_sha256": sha256_json(grant),
|
||||
"checks": checks,
|
||||
"actual_result": {
|
||||
"result": actual.get("result"),
|
||||
"signature_verified": actual.get("signature_verified"),
|
||||
"approval_plan_verified": actual.get("approval_plan_verified"),
|
||||
"remote_base_verified": actual.get("remote_base_verified"),
|
||||
"target_branch_absent": actual.get("target_branch_absent"),
|
||||
"grant_unused": actual.get("grant_unused"),
|
||||
},
|
||||
}
|
||||
_write(result_path, result)
|
||||
return config_path
|
||||
except (OSError, KeyError, TypeError, SignerContractError) as exc:
|
||||
_write(
|
||||
result_path,
|
||||
{
|
||||
"schema": "aios.compatible-validator-result.v1",
|
||||
"status": "BLOCKED",
|
||||
"error_code": str(exc),
|
||||
},
|
||||
)
|
||||
raise
|
||||
@@ -1,760 +0,0 @@
|
||||
"""Task-scoped identity overlay for bootstrap writer provisioning.
|
||||
|
||||
This module resolves bearer-registry identity ambiguity without modifying the
|
||||
global token registry. A root-governed one-shot issuer creates a random,
|
||||
short-lived credential bound to exactly one repository, base OID, task and
|
||||
target branch. The HTTP overlay authenticates only that credential and invokes
|
||||
the existing OwnerAuthorizationBroker.
|
||||
|
||||
The overlay never grants main, tag, baseline, production or repository-admin
|
||||
capabilities. The bearer secret is stored only in a root-owned runtime path and
|
||||
is removed after success, failure, revocation or expiry.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
from authorized_task_writer_provisioning import (
|
||||
BRANCH_RE,
|
||||
ProvisioningError,
|
||||
OID_RE,
|
||||
REQUIRED_ALLOWED_OPERATIONS,
|
||||
REQUIRED_DENIED_OPERATIONS,
|
||||
TASK_RE,
|
||||
)
|
||||
from owner_authorization_broker import (
|
||||
ACTION,
|
||||
BROKER_REQUEST_FIELDS,
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationBrokerConfig,
|
||||
OwnerAuthorizationError,
|
||||
OwnerPrincipal,
|
||||
TaskWriterProvisioner,
|
||||
canonical_json_bytes,
|
||||
iso_utc,
|
||||
sha256_json,
|
||||
utc_now,
|
||||
)
|
||||
|
||||
|
||||
OVERLAY_SCHEMA = "aios.task-identity-overlay.v1"
|
||||
OVERLAY_SCOPE = "owner:task-authorize"
|
||||
OVERLAY_ID_RE = re.compile(r"^overlay-[0-9a-f]{32}$")
|
||||
OWNER_IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
|
||||
AUTH_ASSERTION_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{3,255}$")
|
||||
TERMINAL_STATES = frozenset({"CONSUMED", "FAILED", "REVOKED", "EXPIRED"})
|
||||
MAX_BODY_BYTES = 1_048_576
|
||||
|
||||
|
||||
class IdentityOverlayError(RuntimeError):
|
||||
"""Stable, fail-closed error for the overlay control plane."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: str,
|
||||
status: int = 400,
|
||||
*,
|
||||
details: Mapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
self.details = dict(details or {})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskBinding:
|
||||
task_id: str
|
||||
repository: str
|
||||
base_ref: str
|
||||
base_oid: str
|
||||
target_branch: str
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"repository": self.repository,
|
||||
"base_ref": self.base_ref,
|
||||
"base_oid": self.base_oid,
|
||||
"target_branch": self.target_branch,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IdentityOverlayConfig:
|
||||
state_root: Path
|
||||
secret_root: Path
|
||||
binding: TaskBinding
|
||||
owner_identity: str
|
||||
max_ttl_seconds: int = 900
|
||||
require_root_issue: bool = True
|
||||
collision_secret_refs: tuple[Path, ...] = ()
|
||||
broker_state_root: Path | None = None
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls) -> "IdentityOverlayConfig":
|
||||
try:
|
||||
maximum = int(
|
||||
os.getenv("AIOS_GITOPS_IDENTITY_OVERLAY_MAX_TTL_SECONDS", "900")
|
||||
)
|
||||
except ValueError:
|
||||
raise IdentityOverlayError("OVERLAY_MAX_TTL_INVALID", 503) from None
|
||||
if not 60 <= maximum <= 900:
|
||||
raise IdentityOverlayError("OVERLAY_MAX_TTL_INVALID", 503)
|
||||
|
||||
refs = tuple(
|
||||
Path(item.strip())
|
||||
for item in os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS", ""
|
||||
).split(",")
|
||||
if item.strip()
|
||||
)
|
||||
owner = os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_OWNER_IDENTITY", ""
|
||||
).strip()
|
||||
config = cls(
|
||||
state_root=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_STATE_ROOT",
|
||||
"/var/lib/aios-gitops/task-identity-overlays",
|
||||
)
|
||||
),
|
||||
secret_root=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_SECRET_ROOT",
|
||||
"/run/aios-gitops/task-identity-overlays",
|
||||
)
|
||||
),
|
||||
broker_state_root=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT",
|
||||
"/var/lib/aios-gitops/owner-authorization-broker",
|
||||
)
|
||||
),
|
||||
binding=TaskBinding(
|
||||
task_id=os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_TASK_ID", ""
|
||||
).strip(),
|
||||
repository=os.getenv(
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY", "aios/aios"
|
||||
).strip(),
|
||||
base_ref=os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_BASE_REF",
|
||||
"refs/heads/main",
|
||||
).strip(),
|
||||
base_oid=os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_BASE_OID", ""
|
||||
).strip(),
|
||||
target_branch=os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_TARGET_BRANCH", ""
|
||||
).strip(),
|
||||
),
|
||||
owner_identity=owner,
|
||||
max_ttl_seconds=maximum,
|
||||
require_root_issue=os.getenv(
|
||||
"AIOS_GITOPS_IDENTITY_OVERLAY_REQUIRE_ROOT", "true"
|
||||
).strip().lower()
|
||||
not in {"0", "false", "no", "off"},
|
||||
collision_secret_refs=refs,
|
||||
)
|
||||
_validate_config(config)
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OverlaySession:
|
||||
overlay_id: str
|
||||
metadata_path: Path
|
||||
secret_ref: Path
|
||||
owner_identity: str
|
||||
request_digest_sha256: str
|
||||
binding: TaskBinding
|
||||
|
||||
|
||||
def _validate_config(config: IdentityOverlayConfig) -> None:
|
||||
binding = config.binding
|
||||
if not TASK_RE.fullmatch(binding.task_id):
|
||||
raise IdentityOverlayError("OVERLAY_TASK_ID_INVALID", 503)
|
||||
if binding.repository != "aios/aios":
|
||||
raise IdentityOverlayError("OVERLAY_REPOSITORY_REJECTED", 503)
|
||||
if binding.base_ref != "refs/heads/main":
|
||||
raise IdentityOverlayError("OVERLAY_BASE_REF_REJECTED", 503)
|
||||
if not OID_RE.fullmatch(binding.base_oid):
|
||||
raise IdentityOverlayError("OVERLAY_BASE_OID_INVALID", 503)
|
||||
if (
|
||||
not BRANCH_RE.fullmatch(binding.target_branch)
|
||||
or binding.target_branch in {"main", "master"}
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_TARGET_BRANCH_INVALID", 503)
|
||||
if not OWNER_IDENTITY_RE.fullmatch(config.owner_identity):
|
||||
raise IdentityOverlayError("OVERLAY_OWNER_IDENTITY_INVALID", 503)
|
||||
if not 60 <= config.max_ttl_seconds <= 900:
|
||||
raise IdentityOverlayError("OVERLAY_MAX_TTL_INVALID", 503)
|
||||
if config.state_root == config.secret_root:
|
||||
raise IdentityOverlayError("OVERLAY_STATE_SECRET_ROOT_COLLISION", 503)
|
||||
|
||||
|
||||
def _prepare_directory(path: Path, mode: int = 0o700) -> None:
|
||||
if path.is_symlink():
|
||||
raise IdentityOverlayError("OVERLAY_PATH_SYMLINK_REJECTED", 500)
|
||||
path.mkdir(parents=True, exist_ok=True, mode=mode)
|
||||
os.chmod(path, mode)
|
||||
if path.stat().st_mode & 0o077:
|
||||
raise IdentityOverlayError("OVERLAY_DIRECTORY_PERMISSIONS_REJECTED", 500)
|
||||
|
||||
|
||||
def _atomic_write(path: Path, data: bytes, mode: int) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise IdentityOverlayError("OVERLAY_PATH_SYMLINK_REJECTED", 500)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".tmp-", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(data)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, mode)
|
||||
parent_fd = os.open(
|
||||
path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
||||
)
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
def _atomic_write_json(path: Path, value: Any, mode: int = 0o600) -> None:
|
||||
_atomic_write(path, canonical_json_bytes(value), mode)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_INVALID", 500)
|
||||
if path.stat().st_size <= 0 or path.stat().st_size > MAX_BODY_BYTES:
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_INVALID", 500)
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_INVALID", 500) from None
|
||||
if not isinstance(value, dict):
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_INVALID", 500)
|
||||
return value
|
||||
|
||||
|
||||
def _parse_utc(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except (TypeError, ValueError):
|
||||
raise IdentityOverlayError("OVERLAY_TIMESTAMP_INVALID", 500) from None
|
||||
if parsed.tzinfo is None:
|
||||
raise IdentityOverlayError("OVERLAY_TIMESTAMP_INVALID", 500)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _token_hash(token: bytes) -> str:
|
||||
return hashlib.sha256(token.strip()).hexdigest()
|
||||
|
||||
|
||||
def _safe_unlink(path: Path) -> None:
|
||||
try:
|
||||
if path.is_symlink():
|
||||
raise IdentityOverlayError("OVERLAY_SECRET_SYMLINK_REJECTED", 500)
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as exc:
|
||||
raise IdentityOverlayError("OVERLAY_SECRET_REMOVAL_FAILED", 500) from exc
|
||||
|
||||
|
||||
def _validate_request_binding(
|
||||
request: Mapping[str, Any], binding: TaskBinding, max_ttl: int
|
||||
) -> dict[str, Any]:
|
||||
if set(request) != BROKER_REQUEST_FIELDS:
|
||||
raise IdentityOverlayError("OVERLAY_REQUEST_FIELDS_REJECTED")
|
||||
if request.get("action") != ACTION:
|
||||
raise IdentityOverlayError("OVERLAY_ACTION_REJECTED")
|
||||
expected = binding.as_dict()
|
||||
for key, value in expected.items():
|
||||
if request.get(key) != value:
|
||||
raise IdentityOverlayError(
|
||||
"OVERLAY_BINDING_MISMATCH", 409, details={"field": key}
|
||||
)
|
||||
if request.get("one_time_use") is not True:
|
||||
raise IdentityOverlayError("OVERLAY_ONE_TIME_USE_REQUIRED")
|
||||
expiry = request.get("expires_in_seconds")
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= max_ttl
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_TTL_REJECTED")
|
||||
for field in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if request.get(field) is not False:
|
||||
raise IdentityOverlayError("OVERLAY_PRODUCTION_CAPABILITY_REJECTED", 403)
|
||||
if (
|
||||
not isinstance(request.get("allowed_operations"), list)
|
||||
or frozenset(request["allowed_operations"])
|
||||
!= REQUIRED_ALLOWED_OPERATIONS
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_ALLOWED_OPERATIONS_MISMATCH", 403)
|
||||
if (
|
||||
not isinstance(request.get("denied_operations"), list)
|
||||
or frozenset(request["denied_operations"])
|
||||
!= REQUIRED_DENIED_OPERATIONS
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_DENIED_OPERATIONS_MISMATCH", 403)
|
||||
return dict(request)
|
||||
|
||||
|
||||
def _metadata_digest(metadata: Mapping[str, Any]) -> str:
|
||||
unsigned = {
|
||||
key: value
|
||||
for key, value in metadata.items()
|
||||
if key != "metadata_digest_sha256"
|
||||
}
|
||||
return sha256_json(unsigned)
|
||||
|
||||
|
||||
def _verify_metadata(metadata: Mapping[str, Any]) -> None:
|
||||
if metadata.get("schema") != OVERLAY_SCHEMA:
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_SCHEMA_INVALID", 500)
|
||||
overlay_id = str(metadata.get("overlay_id") or "")
|
||||
if not OVERLAY_ID_RE.fullmatch(overlay_id):
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_ID_INVALID", 500)
|
||||
expected = _metadata_digest(metadata)
|
||||
if metadata.get("metadata_digest_sha256") != expected:
|
||||
raise IdentityOverlayError("OVERLAY_METADATA_DIGEST_MISMATCH", 500)
|
||||
|
||||
|
||||
class TaskIdentityOverlayStore:
|
||||
"""Issue, authenticate, consume and revoke one task-bound credential."""
|
||||
|
||||
def __init__(self, config: IdentityOverlayConfig) -> None:
|
||||
_validate_config(config)
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def lock_path(self) -> Path:
|
||||
return self.config.state_root / "overlay.lock"
|
||||
|
||||
def _lock(self):
|
||||
_prepare_directory(self.config.state_root)
|
||||
_prepare_directory(self.config.secret_root)
|
||||
self.lock_path.touch(mode=0o600, exist_ok=True)
|
||||
os.chmod(self.lock_path, 0o600)
|
||||
stream = self.lock_path.open("r+b")
|
||||
fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
|
||||
return stream
|
||||
|
||||
def _metadata_paths(self) -> list[Path]:
|
||||
return sorted(self.config.state_root.glob("overlay-*/metadata.json"))
|
||||
|
||||
def _active_for_task(self) -> tuple[Path, dict[str, Any]] | None:
|
||||
now = utc_now()
|
||||
for path in self._metadata_paths():
|
||||
metadata = _read_json(path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("task_id") != self.config.binding.task_id:
|
||||
continue
|
||||
state = metadata.get("status")
|
||||
if state in TERMINAL_STATES:
|
||||
continue
|
||||
if _parse_utc(str(metadata["expires_at"])) <= now:
|
||||
metadata["status"] = "EXPIRED"
|
||||
metadata["expired_at"] = iso_utc(now)
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(path, metadata)
|
||||
_safe_unlink(Path(str(metadata["secret_ref"])))
|
||||
continue
|
||||
return path, metadata
|
||||
return None
|
||||
|
||||
def _assert_hash_unique(self, digest: str) -> None:
|
||||
matches = 0
|
||||
for path in self._metadata_paths():
|
||||
metadata = _read_json(path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("token_sha256") == digest:
|
||||
matches += 1
|
||||
for secret_ref in self.config.collision_secret_refs:
|
||||
if not secret_ref.exists():
|
||||
continue
|
||||
if secret_ref.is_symlink() or not secret_ref.is_file():
|
||||
raise IdentityOverlayError(
|
||||
"OVERLAY_COLLISION_SECRET_REF_INVALID", 500
|
||||
)
|
||||
if secret_ref.stat().st_size > 65_536:
|
||||
raise IdentityOverlayError(
|
||||
"OVERLAY_COLLISION_SECRET_REF_INVALID", 500
|
||||
)
|
||||
if _token_hash(secret_ref.read_bytes()) == digest:
|
||||
matches += 1
|
||||
if matches:
|
||||
raise IdentityOverlayError("OVERLAY_SECRET_IDENTITY_COLLISION", 409)
|
||||
|
||||
def issue(
|
||||
self,
|
||||
*,
|
||||
ttl_seconds: int,
|
||||
authorization_assertion_ref: str,
|
||||
operator_identity: str,
|
||||
) -> dict[str, Any]:
|
||||
if self.config.require_root_issue and os.geteuid() != 0:
|
||||
raise IdentityOverlayError("OVERLAY_ROOT_ISSUER_REQUIRED", 403)
|
||||
if (
|
||||
not isinstance(ttl_seconds, int)
|
||||
or isinstance(ttl_seconds, bool)
|
||||
or not 60 <= ttl_seconds <= self.config.max_ttl_seconds
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_TTL_REJECTED")
|
||||
if not AUTH_ASSERTION_RE.fullmatch(authorization_assertion_ref):
|
||||
raise IdentityOverlayError("OVERLAY_AUTH_ASSERTION_REF_INVALID")
|
||||
if not OWNER_IDENTITY_RE.fullmatch(operator_identity):
|
||||
raise IdentityOverlayError("OVERLAY_OPERATOR_IDENTITY_INVALID")
|
||||
|
||||
with self._lock():
|
||||
active = self._active_for_task()
|
||||
if active:
|
||||
_, metadata = active
|
||||
expected = self.config.binding.as_dict()
|
||||
actual = {
|
||||
key: metadata.get(key)
|
||||
for key in expected
|
||||
}
|
||||
if actual != expected:
|
||||
raise IdentityOverlayError(
|
||||
"OVERLAY_ACTIVE_BINDING_CONFLICT", 409
|
||||
)
|
||||
return {
|
||||
"status": "OVERLAY_ACTIVE",
|
||||
"idempotent": True,
|
||||
"overlay_id": metadata["overlay_id"],
|
||||
"secret_ref": metadata["secret_ref"],
|
||||
"expires_at": metadata["expires_at"],
|
||||
"binding": expected,
|
||||
}
|
||||
|
||||
token_text = secrets.token_urlsafe(48)
|
||||
token_bytes = token_text.encode("ascii")
|
||||
digest = _token_hash(token_bytes)
|
||||
self._assert_hash_unique(digest)
|
||||
overlay_id = "overlay-" + hashlib.sha256(
|
||||
(
|
||||
f"{self.config.binding.task_id}\0"
|
||||
f"{self.config.binding.base_oid}\0"
|
||||
f"{self.config.binding.target_branch}\0"
|
||||
f"{digest}"
|
||||
).encode("utf-8")
|
||||
).hexdigest()[:32]
|
||||
issued_at = utc_now()
|
||||
expires_at = issued_at + timedelta(seconds=ttl_seconds)
|
||||
overlay_dir = self.config.state_root / overlay_id
|
||||
secret_dir = self.config.secret_root / overlay_id
|
||||
_prepare_directory(overlay_dir)
|
||||
_prepare_directory(secret_dir)
|
||||
secret_ref = secret_dir / "bearer.token"
|
||||
_atomic_write(secret_ref, token_bytes + b"\n", 0o600)
|
||||
|
||||
metadata: dict[str, Any] = {
|
||||
"schema": OVERLAY_SCHEMA,
|
||||
"overlay_id": overlay_id,
|
||||
"status": "ACTIVE",
|
||||
**self.config.binding.as_dict(),
|
||||
"owner_identity": self.config.owner_identity,
|
||||
"role": "owner",
|
||||
"scope": OVERLAY_SCOPE,
|
||||
"wildcard_scope": False,
|
||||
"one_time_use": True,
|
||||
"token_sha256": digest,
|
||||
"secret_ref": str(secret_ref),
|
||||
"authorization_assertion_ref": authorization_assertion_ref,
|
||||
"operator_identity": operator_identity,
|
||||
"issued_at": iso_utc(issued_at),
|
||||
"expires_at": iso_utc(expires_at),
|
||||
"attempt_count": 0,
|
||||
}
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
metadata_path = overlay_dir / "metadata.json"
|
||||
_atomic_write_json(metadata_path, metadata)
|
||||
_atomic_write_json(
|
||||
overlay_dir / "audit-issued.json",
|
||||
{
|
||||
"event": "TASK_IDENTITY_OVERLAY_ISSUED",
|
||||
"overlay_id": overlay_id,
|
||||
"task_id": self.config.binding.task_id,
|
||||
"binding_digest_sha256": sha256_json(
|
||||
self.config.binding.as_dict()
|
||||
),
|
||||
"authorization_assertion_ref": authorization_assertion_ref,
|
||||
"operator_identity": operator_identity,
|
||||
"issued_at": metadata["issued_at"],
|
||||
"expires_at": metadata["expires_at"],
|
||||
"token_sha256": digest,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"status": "OVERLAY_ACTIVE",
|
||||
"idempotent": False,
|
||||
"overlay_id": overlay_id,
|
||||
"secret_ref": str(secret_ref),
|
||||
"expires_at": metadata["expires_at"],
|
||||
"binding": self.config.binding.as_dict(),
|
||||
}
|
||||
|
||||
def authenticate_and_lock(
|
||||
self, token: str, request: Mapping[str, Any]
|
||||
) -> OverlaySession:
|
||||
if not token or len(token) > 4096:
|
||||
raise IdentityOverlayError("OVERLAY_AUTHENTICATION_REQUIRED", 401)
|
||||
digest = _token_hash(token.encode("utf-8"))
|
||||
|
||||
with self._lock():
|
||||
matches: list[tuple[Path, dict[str, Any]]] = []
|
||||
for path in self._metadata_paths():
|
||||
metadata = _read_json(path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("token_sha256") == digest:
|
||||
matches.append((path, metadata))
|
||||
if not matches:
|
||||
raise IdentityOverlayError("OVERLAY_CREDENTIAL_INVALID", 401)
|
||||
if len(matches) != 1:
|
||||
raise IdentityOverlayError("OVERLAY_SECRET_IDENTITY_COLLISION", 409)
|
||||
path, metadata = matches[0]
|
||||
if metadata.get("status") in TERMINAL_STATES:
|
||||
raise IdentityOverlayError("OVERLAY_CREDENTIAL_INACTIVE", 401)
|
||||
if _parse_utc(str(metadata["expires_at"])) <= utc_now():
|
||||
metadata["status"] = "EXPIRED"
|
||||
metadata["expired_at"] = iso_utc(utc_now())
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(path, metadata)
|
||||
_safe_unlink(Path(str(metadata["secret_ref"])))
|
||||
raise IdentityOverlayError("OVERLAY_CREDENTIAL_EXPIRED", 401)
|
||||
|
||||
expected = self.config.binding.as_dict()
|
||||
for key, value in expected.items():
|
||||
if metadata.get(key) != value:
|
||||
raise IdentityOverlayError(
|
||||
"OVERLAY_METADATA_BINDING_MISMATCH", 409
|
||||
)
|
||||
if metadata.get("owner_identity") != self.config.owner_identity:
|
||||
raise IdentityOverlayError("OVERLAY_OWNER_IDENTITY_MISMATCH", 409)
|
||||
if metadata.get("scope") != OVERLAY_SCOPE:
|
||||
raise IdentityOverlayError("OVERLAY_SCOPE_MISMATCH", 409)
|
||||
if metadata.get("wildcard_scope") is not False:
|
||||
raise IdentityOverlayError("OVERLAY_WILDCARD_SCOPE_REJECTED", 409)
|
||||
|
||||
# Validate detailed request policy only after the bearer identity is
|
||||
# proven, so unauthenticated callers cannot probe task bindings.
|
||||
validated = _validate_request_binding(
|
||||
request, self.config.binding, self.config.max_ttl_seconds
|
||||
)
|
||||
request_digest = sha256_json(validated)
|
||||
previous = str(metadata.get("request_digest_sha256") or "")
|
||||
if metadata.get("status") == "LOCKED" and previous != request_digest:
|
||||
raise IdentityOverlayError("OVERLAY_REPLAY_REQUEST_MISMATCH", 409)
|
||||
metadata["status"] = "LOCKED"
|
||||
metadata["request_digest_sha256"] = request_digest
|
||||
metadata["locked_at"] = metadata.get("locked_at") or iso_utc(utc_now())
|
||||
metadata["attempt_count"] = int(metadata.get("attempt_count") or 0) + 1
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(path, metadata)
|
||||
return OverlaySession(
|
||||
overlay_id=str(metadata["overlay_id"]),
|
||||
metadata_path=path,
|
||||
secret_ref=Path(str(metadata["secret_ref"])),
|
||||
owner_identity=str(metadata["owner_identity"]),
|
||||
request_digest_sha256=request_digest,
|
||||
binding=self.config.binding,
|
||||
)
|
||||
|
||||
def complete(
|
||||
self, session: OverlaySession, result: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
with self._lock():
|
||||
metadata = _read_json(session.metadata_path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("status") == "CONSUMED":
|
||||
return metadata
|
||||
if metadata.get("status") != "LOCKED":
|
||||
raise IdentityOverlayError("OVERLAY_NOT_LOCKED", 409)
|
||||
if (
|
||||
metadata.get("request_digest_sha256")
|
||||
!= session.request_digest_sha256
|
||||
):
|
||||
raise IdentityOverlayError("OVERLAY_REQUEST_DIGEST_MISMATCH", 409)
|
||||
metadata["status"] = "CONSUMED"
|
||||
metadata["consumed_at"] = iso_utc(utc_now())
|
||||
metadata["result_digest_sha256"] = sha256_json(dict(result))
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(session.metadata_path, metadata)
|
||||
_safe_unlink(session.secret_ref)
|
||||
_atomic_write_json(
|
||||
session.metadata_path.parent / "audit-consumed.json",
|
||||
{
|
||||
"event": "TASK_IDENTITY_OVERLAY_CONSUMED",
|
||||
"overlay_id": session.overlay_id,
|
||||
"task_id": session.binding.task_id,
|
||||
"request_digest_sha256": session.request_digest_sha256,
|
||||
"result_digest_sha256": metadata["result_digest_sha256"],
|
||||
"consumed_at": metadata["consumed_at"],
|
||||
"secret_removed": True,
|
||||
},
|
||||
)
|
||||
return metadata
|
||||
|
||||
def fail(
|
||||
self,
|
||||
session: OverlaySession,
|
||||
*,
|
||||
error_code: str,
|
||||
error_status: int,
|
||||
) -> dict[str, Any]:
|
||||
with self._lock():
|
||||
metadata = _read_json(session.metadata_path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("status") in TERMINAL_STATES:
|
||||
return metadata
|
||||
metadata["status"] = "FAILED"
|
||||
metadata["failed_at"] = iso_utc(utc_now())
|
||||
metadata["error_code"] = error_code
|
||||
metadata["error_status"] = int(error_status)
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(session.metadata_path, metadata)
|
||||
_safe_unlink(session.secret_ref)
|
||||
_atomic_write_json(
|
||||
session.metadata_path.parent / "audit-failed.json",
|
||||
{
|
||||
"event": "TASK_IDENTITY_OVERLAY_FAILED",
|
||||
"overlay_id": session.overlay_id,
|
||||
"task_id": session.binding.task_id,
|
||||
"request_digest_sha256": session.request_digest_sha256,
|
||||
"error_code": error_code,
|
||||
"error_status": int(error_status),
|
||||
"failed_at": metadata["failed_at"],
|
||||
"secret_removed": True,
|
||||
},
|
||||
)
|
||||
return metadata
|
||||
|
||||
def revoke(self, overlay_id: str, *, reason: str) -> dict[str, Any]:
|
||||
if self.config.require_root_issue and os.geteuid() != 0:
|
||||
raise IdentityOverlayError("OVERLAY_ROOT_ISSUER_REQUIRED", 403)
|
||||
if not OVERLAY_ID_RE.fullmatch(overlay_id):
|
||||
raise IdentityOverlayError("OVERLAY_ID_INVALID")
|
||||
if not AUTH_ASSERTION_RE.fullmatch(reason):
|
||||
raise IdentityOverlayError("OVERLAY_REVOCATION_REASON_INVALID")
|
||||
with self._lock():
|
||||
path = self.config.state_root / overlay_id / "metadata.json"
|
||||
metadata = _read_json(path)
|
||||
_verify_metadata(metadata)
|
||||
if metadata.get("status") not in TERMINAL_STATES:
|
||||
metadata["status"] = "REVOKED"
|
||||
metadata["revoked_at"] = iso_utc(utc_now())
|
||||
metadata["revocation_reason"] = reason
|
||||
metadata["metadata_digest_sha256"] = _metadata_digest(metadata)
|
||||
_atomic_write_json(path, metadata)
|
||||
_safe_unlink(Path(str(metadata["secret_ref"])))
|
||||
return metadata
|
||||
|
||||
def status(self, overlay_id: str) -> dict[str, Any]:
|
||||
if not OVERLAY_ID_RE.fullmatch(overlay_id):
|
||||
raise IdentityOverlayError("OVERLAY_ID_INVALID")
|
||||
metadata = _read_json(
|
||||
self.config.state_root / overlay_id / "metadata.json"
|
||||
)
|
||||
_verify_metadata(metadata)
|
||||
return {
|
||||
key: value
|
||||
for key, value in metadata.items()
|
||||
if key not in {"token_sha256"}
|
||||
}
|
||||
|
||||
|
||||
class TaskIdentityOverlayService:
|
||||
"""Authenticate one overlay credential and invoke the existing broker."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: IdentityOverlayConfig,
|
||||
provisioner: TaskWriterProvisioner | None = None,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.store = TaskIdentityOverlayStore(config)
|
||||
broker_state = config.broker_state_root or (
|
||||
config.state_root / "owner-authorization-events"
|
||||
)
|
||||
self.broker = OwnerAuthorizationBroker(
|
||||
OwnerAuthorizationBrokerConfig(
|
||||
repository_identity=config.binding.repository,
|
||||
state_root=broker_state,
|
||||
allowed_owner_identities=frozenset({config.owner_identity}),
|
||||
allowed_intake_scopes=frozenset({OVERLAY_SCOPE}),
|
||||
allowed_credential_ids=frozenset(),
|
||||
max_expiry_seconds=config.max_ttl_seconds,
|
||||
),
|
||||
provisioner=provisioner,
|
||||
)
|
||||
|
||||
def provision(
|
||||
self, request: Mapping[str, Any], bearer_token: str
|
||||
) -> dict[str, Any]:
|
||||
session = self.store.authenticate_and_lock(bearer_token, request)
|
||||
principal = OwnerPrincipal(
|
||||
identity=session.owner_identity,
|
||||
role="owner",
|
||||
authenticated_scope=OVERLAY_SCOPE,
|
||||
token_scopes=frozenset({OVERLAY_SCOPE}),
|
||||
credential_id=session.overlay_id,
|
||||
)
|
||||
try:
|
||||
result = self.broker.authorize_and_provision(request, principal)
|
||||
if result.get("status") != "READY_FOR_CODE_AUTHORING":
|
||||
raise IdentityOverlayError("OVERLAY_PROVISIONER_NOT_READY", 409)
|
||||
self.store.complete(session, result)
|
||||
response = dict(result)
|
||||
response.update(
|
||||
{
|
||||
"identity_overlay_id": session.overlay_id,
|
||||
"identity_overlay_consumed": True,
|
||||
"identity_overlay_secret_removed": True,
|
||||
"identity_overlay_scope": OVERLAY_SCOPE,
|
||||
}
|
||||
)
|
||||
return response
|
||||
except (OwnerAuthorizationError, ProvisioningError) as exc:
|
||||
self.store.fail(
|
||||
session, error_code=exc.code, error_status=exc.status
|
||||
)
|
||||
raise
|
||||
except IdentityOverlayError as exc:
|
||||
self.store.fail(
|
||||
session, error_code=exc.code, error_status=exc.status
|
||||
)
|
||||
raise
|
||||
except Exception as exc:
|
||||
self.store.fail(
|
||||
session,
|
||||
error_code="IDENTITY_OVERLAY_INTERNAL_ERROR",
|
||||
error_status=500,
|
||||
)
|
||||
raise IdentityOverlayError(
|
||||
"IDENTITY_OVERLAY_INTERNAL_ERROR", 500
|
||||
) from exc
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Privacy, artifact, and runtime health helpers for V2R1C7."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from deployment_state import RuntimeDeploymentState
|
||||
|
||||
_SECRET_KEY = re.compile(r"(?i)(?:token|secret|password|private[_-]?key|api[_-]?key)")
|
||||
_SECRET_VALUE = re.compile(r"(?i)(?:bearer\s+)[A-Za-z0-9._~+/=-]+|(?:token|secret|password|private[_-]?key|api[_-]?key)\s*[:=]\s*[^\s,;]+")
|
||||
|
||||
def redact_text(value: str) -> str:
|
||||
if not isinstance(value, str): raise TypeError("text_required")
|
||||
return _SECRET_VALUE.sub("[REDACTED]", value)
|
||||
|
||||
def redact_value(value: Any, *, key: str = "") -> Any:
|
||||
if isinstance(value, Mapping):
|
||||
return {str(k): ("[REDACTED]" if _SECRET_KEY.search(str(k)) else redact_value(v, key=str(k))) for k,v in value.items()}
|
||||
if isinstance(value, list): return [redact_value(v, key=key) for v in value]
|
||||
if isinstance(value, tuple): return [redact_value(v, key=key) for v in value]
|
||||
if isinstance(value, str): return "[REDACTED]" if _SECRET_KEY.search(key) else redact_text(value)
|
||||
return value
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file(): raise ValueError("regular_file_required")
|
||||
digest=hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024*1024), b""): digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
def _safe_relative(root: Path, name: str) -> Path:
|
||||
relative=Path(name)
|
||||
if relative.is_absolute() or ".." in relative.parts or name in {"", "."}: raise ValueError("unsafe_manifest_path")
|
||||
candidate=(root/relative).resolve(); candidate.relative_to(root.resolve()); return candidate
|
||||
|
||||
def verify_manifest(root: Path, manifest: Path | None = None) -> dict[str, Any]:
|
||||
root=root.resolve(); manifest=manifest or root/"artifacts"/"MANIFEST.sha256"
|
||||
if manifest.is_symlink() or not manifest.is_file(): raise ValueError("manifest_missing")
|
||||
checked=0
|
||||
for raw in manifest.read_text(encoding="utf-8").splitlines():
|
||||
line=raw.strip()
|
||||
if not line: continue
|
||||
parts=line.split(maxsplit=1)
|
||||
if len(parts)!=2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]): raise ValueError("manifest_entry_invalid")
|
||||
expected,name=parts; name=name[1:] if name.startswith("*") else name
|
||||
if sha256_file(_safe_relative(root,name))!=expected: raise ValueError("manifest_hash_mismatch")
|
||||
checked+=1
|
||||
if not checked: raise ValueError("manifest_empty")
|
||||
return {"status":"PASS","entries":checked}
|
||||
|
||||
def load_release_metadata(path: Path) -> dict[str, Any]:
|
||||
metadata=json.loads(path.read_text(encoding="utf-8"))
|
||||
if metadata.get("version") not in {"V2R1C6","V2R1C7"}: raise ValueError("version_mismatch")
|
||||
if any(metadata.get(k) is not False for k in ("production_deployed","tag_created","baseline_changed")): raise ValueError("production_change_not_allowed")
|
||||
return metadata
|
||||
|
||||
def health_payload(state_provider: RuntimeDeploymentState | None = None) -> dict[str, Any]:
|
||||
return (state_provider or RuntimeDeploymentState.from_environment()).health_payload()
|
||||
|
||||
class _HealthHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
if self.path not in {"/","/health","/healthz"}: self.send_error(404); return
|
||||
body=json.dumps(health_payload(), sort_keys=True).encode()
|
||||
self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body)
|
||||
def log_message(self, _format: str, *_args: object) -> None: return
|
||||
|
||||
def serve(host: str, port: int) -> None:
|
||||
if host not in {"127.0.0.1","::1","localhost"}: raise ValueError("loopback_only_listener_required")
|
||||
if not 1024 <= port <= 65535: raise ValueError("invalid_listener_port")
|
||||
server=ThreadingHTTPServer((host,port),_HealthHandler)
|
||||
try: server.serve_forever()
|
||||
finally: server.server_close()
|
||||
|
||||
def main() -> int:
|
||||
parser=argparse.ArgumentParser(); parser.add_argument("--root",type=Path,default=Path(".")); parser.add_argument("--manifest",type=Path); parser.add_argument("--serve",action="store_true"); parser.add_argument("--host",default="127.0.0.1"); parser.add_argument("--port",type=int,default=18927); args=parser.parse_args()
|
||||
if args.serve: serve(args.host,args.port); return 0
|
||||
print(json.dumps(verify_manifest(args.root,args.manifest),sort_keys=True)); return 0
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
@@ -1,5 +0,0 @@
|
||||
"""V2R1C7 production health entry point; state is install/runtime supplied."""
|
||||
from v2r1c6_privacy_tools import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,455 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Concrete V2R1C7 production deployment adapter.
|
||||
|
||||
The adapter consumes only an already approved immutable artifact. It never
|
||||
writes Git refs, Registry, or Baseline. A rollback only tears down the V2R1C7
|
||||
runtime and leaves V2R1C5/V2R1C6 untouched.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import grp
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
VERSION = "V2R1C7"
|
||||
SERVICE = "aios-v2r1c7-production.service"
|
||||
PORT = 18927
|
||||
RUNTIME_USER = "aios-v2r1c7"
|
||||
RUNTIME_GROUP = "aios-v2r1c7"
|
||||
|
||||
REQUIRED_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("version") != VERSION:
|
||||
raise ValueError("version_mismatch")
|
||||
if value.get("production_service") != SERVICE:
|
||||
raise ValueError("service_mismatch")
|
||||
if int(value.get("production_port", -1)) != PORT:
|
||||
raise ValueError("port_mismatch")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("artifact_regular_file_required")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(dict(value), sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def atomic_text(path: Path, text: str, *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(text, encoding="utf-8")
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def systemctl(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["systemctl", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def identity() -> tuple[int, int]:
|
||||
try:
|
||||
user = pwd.getpwnam(RUNTIME_USER)
|
||||
group = grp.getgrnam(RUNTIME_GROUP)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_MISSING") from exc
|
||||
if user.pw_gid != group.gr_gid:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_GROUP_MISMATCH")
|
||||
return user.pw_uid, group.gr_gid
|
||||
|
||||
|
||||
def safe_extract(artifact: Path, destination: Path) -> set[str]:
|
||||
with tempfile.TemporaryDirectory(prefix="aios-v2r1c7-unpack-") as temp:
|
||||
tar_path = Path(temp) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(artifact)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError("ARTIFACT_DECOMPRESSION_FAILED")
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
members = archive.getmembers()
|
||||
names = {m.name for m in members}
|
||||
missing = REQUIRED_MEMBERS - names
|
||||
if missing:
|
||||
raise RuntimeError("ARTIFACT_MEMBERS_MISSING:" + ",".join(sorted(missing)))
|
||||
for member in members:
|
||||
relative = Path(member.name)
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or ".." in relative.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise RuntimeError("UNSAFE_ARTIFACT_MEMBER:" + member.name)
|
||||
if not member.name.startswith("payload/"):
|
||||
raise RuntimeError("UNEXPECTED_ARTIFACT_ROOT:" + member.name)
|
||||
payload = destination
|
||||
payload.mkdir(parents=True, exist_ok=True)
|
||||
for member in members:
|
||||
rel = Path(member.name).relative_to("payload")
|
||||
if str(rel) == ".":
|
||||
continue
|
||||
target = payload / rel
|
||||
resolved_parent = target.parent.resolve()
|
||||
resolved_parent.relative_to(payload.resolve())
|
||||
if member.isdir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(target, 0o755)
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = archive.extractfile(member)
|
||||
if source is None:
|
||||
raise RuntimeError("ARTIFACT_MEMBER_READ_FAILED")
|
||||
with target.open("wb") as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
# Scripts must be executable; all other payload files are read-only.
|
||||
mode = 0o755 if rel.parts and rel.parts[0] == "scripts" else 0o644
|
||||
os.chmod(target, mode)
|
||||
return names
|
||||
|
||||
|
||||
def health(url: str, expected: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError):
|
||||
return {"status": "UNAVAILABLE"}
|
||||
exact = (
|
||||
status == 200
|
||||
and body.get("version") == expected["version"]
|
||||
and body.get("commit_oid") == expected["target_oid"]
|
||||
and body.get("artifact_sha256") == expected["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
)
|
||||
if not exact:
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"http_status": status,
|
||||
"version": body.get("version"),
|
||||
"target_oid": body.get("commit_oid"),
|
||||
"artifact_sha256": body.get("artifact_sha256"),
|
||||
"deployment_state": body.get("deployment_state"),
|
||||
"production_deployed": body.get("production_deployed"),
|
||||
}
|
||||
return {
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"http_status": 200,
|
||||
"version": body["version"],
|
||||
"target_oid": body["commit_oid"],
|
||||
"artifact_sha256": body["artifact_sha256"],
|
||||
"deployment_state": body["deployment_state"],
|
||||
"production_deployed": True,
|
||||
}
|
||||
|
||||
|
||||
def paths(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Path]:
|
||||
release_id = (
|
||||
f"{VERSION}-{str(value['target_oid'])[:12]}-"
|
||||
f"{str(value['artifact_sha256'])[:12]}"
|
||||
)
|
||||
return {
|
||||
"release_dir": args.release_root / release_id,
|
||||
"current_link": args.current_link,
|
||||
"env_file": args.env_file,
|
||||
"state_file": args.state_file,
|
||||
"unit_file": args.unit_file,
|
||||
}
|
||||
|
||||
|
||||
def probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
p = paths(args, value)
|
||||
url = f"http://127.0.0.1:{PORT}/health"
|
||||
active = systemctl("is-active", "--quiet", SERVICE, check=False).returncode == 0
|
||||
if not active:
|
||||
# A stale exact release directory is safe to reuse; it is not a deployment.
|
||||
return {
|
||||
"status": "NOT_APPLIED",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_deployed": False,
|
||||
"deployment_state": "ROLLED_BACK"
|
||||
if p["state_file"].exists()
|
||||
and "ROLLED_BACK" in p["state_file"].read_text(encoding="utf-8")
|
||||
else "ABSENT",
|
||||
}
|
||||
result = health(url, value)
|
||||
if result["status"] == "UNAVAILABLE":
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"reason": "service_active_health_unavailable",
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def install_release(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
artifact = Path(str(value["artifact_path"]))
|
||||
if artifact.stat().st_size != int(value["artifact_size"]):
|
||||
raise RuntimeError("ARTIFACT_SIZE_MISMATCH")
|
||||
if sha256_file(artifact) != value["artifact_sha256"]:
|
||||
raise RuntimeError("ARTIFACT_DIGEST_MISMATCH")
|
||||
|
||||
p = paths(args, value)
|
||||
release_dir = p["release_dir"]
|
||||
marker = release_dir / "DEPLOYMENT-BINDING.json"
|
||||
marker_value = {
|
||||
"schema": "aios.runtime-release-binding.v1",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
if not marker.is_file():
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_UNTRUSTED")
|
||||
current_marker = json.loads(marker.read_text(encoding="utf-8"))
|
||||
if current_marker != marker_value:
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_BINDING_CONFLICT")
|
||||
else:
|
||||
release_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = Path(
|
||||
tempfile.mkdtemp(prefix=release_dir.name + ".", dir=str(release_dir.parent))
|
||||
)
|
||||
try:
|
||||
safe_extract(artifact, temp_dir)
|
||||
atomic_json(
|
||||
temp_dir / "DEPLOYMENT-BINDING.json",
|
||||
marker_value,
|
||||
mode=0o644,
|
||||
)
|
||||
os.rename(temp_dir, release_dir)
|
||||
except Exception:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
raise
|
||||
|
||||
if (release_dir / "VERSION").read_text(encoding="utf-8").strip() != VERSION:
|
||||
raise RuntimeError("RELEASE_VERSION_FILE_MISMATCH")
|
||||
|
||||
# Only replace our dedicated V2R1C7 link. Never touch previous versions.
|
||||
current_link = p["current_link"]
|
||||
if current_link.exists() and not current_link.is_symlink():
|
||||
raise RuntimeError("CURRENT_RELEASE_PATH_NOT_SYMLINK")
|
||||
if current_link.is_symlink():
|
||||
existing = current_link.resolve()
|
||||
if existing != release_dir.resolve():
|
||||
raise RuntimeError("CURRENT_RELEASE_LINK_CONFLICT")
|
||||
else:
|
||||
current_link.parent.mkdir(parents=True, exist_ok=True)
|
||||
new_link = current_link.with_name(current_link.name + ".new")
|
||||
try:
|
||||
new_link.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
new_link.symlink_to(release_dir)
|
||||
os.replace(new_link, current_link)
|
||||
|
||||
state_value = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
}
|
||||
atomic_json(p["state_file"], state_value, mode=0o640, gid=gid)
|
||||
env = (
|
||||
f"RELEASE_VERSION={VERSION}\n"
|
||||
f"TARGET_OID={value['target_oid']}\n"
|
||||
f"ARTIFACT_SHA256={value['artifact_sha256']}\n"
|
||||
"DEPLOYMENT_STATE=ACTIVE\n"
|
||||
f"AIOS_V2R1C7_PORT={PORT}\n"
|
||||
f"DEPLOYMENT_STATE_FILE={p['state_file']}\n"
|
||||
"AIOS_GITOPS_WRITES_ENABLED=false\n"
|
||||
)
|
||||
atomic_text(p["env_file"], env, mode=0o640, gid=gid)
|
||||
|
||||
unit_source = (
|
||||
release_dir
|
||||
/ "deploy"
|
||||
/ "aios-v2"
|
||||
/ "systemd"
|
||||
/ "aios-v2r1c7-production.service"
|
||||
)
|
||||
unit_text = unit_source.read_text(encoding="utf-8")
|
||||
if "aios-v2r1c7-production.service" in unit_text:
|
||||
# Description text is irrelevant; service identity is the destination.
|
||||
pass
|
||||
atomic_text(p["unit_file"], unit_text, mode=0o644)
|
||||
|
||||
verify = subprocess.run(
|
||||
["systemd-analyze", "verify", str(p["unit_file"])],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if verify.returncode != 0:
|
||||
raise RuntimeError("PRODUCTION_SYSTEMD_VERIFY_FAILED")
|
||||
|
||||
systemctl("daemon-reload")
|
||||
systemctl("enable", SERVICE)
|
||||
systemctl("restart", SERVICE)
|
||||
|
||||
deadline = time.time() + args.health_timeout
|
||||
result = {"status": "UNAVAILABLE"}
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_HEALTH_VERIFY_FAILED")
|
||||
|
||||
# Restart recovery is part of production acceptance, not just staging.
|
||||
systemctl("restart", SERVICE)
|
||||
deadline = time.time() + args.health_timeout
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_RESTART_RECOVERY_FAILED")
|
||||
return result
|
||||
|
||||
|
||||
def rollback(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
p = paths(args, value)
|
||||
systemctl("stop", SERVICE, check=False)
|
||||
systemctl("disable", SERVICE, check=False)
|
||||
state = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
}
|
||||
atomic_json(p["state_file"], state, mode=0o640, gid=gid)
|
||||
return {
|
||||
"status": "ROLLED_BACK",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
"production_deployed": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--release-root", type=Path, default=Path("/opt/aios/releases"))
|
||||
p.add_argument("--current-link", type=Path, default=Path("/opt/aios/v2r1c7"))
|
||||
p.add_argument("--env-file", type=Path, default=Path("/etc/aios/aios-v2r1c7.env"))
|
||||
p.add_argument(
|
||||
"--state-file",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios/v2r1c7/deployment-state.json"),
|
||||
)
|
||||
p.add_argument(
|
||||
"--unit-file",
|
||||
type=Path,
|
||||
default=Path("/etc/systemd/system/aios-v2r1c7-production.service"),
|
||||
)
|
||||
p.add_argument("--health-timeout", type=int, default=30)
|
||||
args = p.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = install_release(args, value)
|
||||
elif operation == "rollback":
|
||||
result = rollback(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"schema": "aios.release-approval-supersession.v1",
|
||||
"version": "V2R1C6",
|
||||
"status": "SUPERSEDED_PREDEPLOY_VALIDATION_FAILED",
|
||||
"execute_allowed": false,
|
||||
"old_binding": {
|
||||
"target_oid": "28081c5522a869762ec4946846f68047d53d99f0",
|
||||
"evidence_closure_oid": "fed41014d80bacca70c8f309a5fb2c22e6001e32",
|
||||
"artifact_sha256": "437b1f891f23a1dd12feabeced3edee3c54cc552d990d6d852805c8d5a165150",
|
||||
"approval_plan_id": "sha256:596c8d40c25f9427c58cbdb0bbae58c03ba5c200fec32025c22d8004d27d8a51"
|
||||
},
|
||||
"reasons": [
|
||||
"artifact_candidate_descriptor",
|
||||
"deployable_package_not_verified",
|
||||
"production_controller_support_not_verified"
|
||||
],
|
||||
"replacement_required": true,
|
||||
"production_deployed": false,
|
||||
"tag_created": false,
|
||||
"baseline_changed": false,
|
||||
"superseded_at": "2026-08-07",
|
||||
"note": "Retained for audit; never delete, overwrite, or reuse."
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"schema": "aios.approval.supersession.v1",
|
||||
"version": "V2R1C7",
|
||||
"superseded_approval_plan_id": "sha256:e62afc9946bcd0badc8e75291baf979e915dccaa50c2c77912c77f1ddeb2aa65",
|
||||
"status": "SUPERSEDED_PREMERGE_IDENTITY_CHANGED",
|
||||
"execute": false,
|
||||
"reason": "release identity and artifact source head changed before protected merge",
|
||||
"production_deployed": false
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"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
@@ -0,0 +1 @@
|
||||
{"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
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"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
@@ -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"
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"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
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
26d56250af61748d26a75fb35aaaf46578d0520fc11796b75de2330ef2ebb64f VERSION
|
||||
81afc7ed5c1123146f61cf7bcf96c1fb69baa9371f1beec693341f20d612e0d4 active-runtime/app/v2r1c6_privacy_tools.py
|
||||
437b1f891f23a1dd12feabeced3edee3c54cc552d990d6d852805c8d5a165150 artifacts/v2r1c6-candidate-artifact.txt
|
||||
9f0dced0b252d2103ac4e975e17fe220aa990d4e3ebcfa17ad3ec32cf0ce4152 deploy/aios-v2/systemd/aios-v2r1c6-production.env.example
|
||||
cee41f7c6e5a56632c6cb015d9182a6376d502fbf7a53b701369bf062c33f359 deploy/aios-v2/systemd/aios-v2r1c6-production.service
|
||||
3f050310303662eb71840c0cb7cac5a5523968750ea1dd428653d5ba1ef2b727 deploy/aios-v2/v2r1c6-rollback.json
|
||||
cbfefff50c6b1a6bc98de138e468929b4a98b738baeb0e0c97d2672f13ccc452 docs/releases/V2R1C6-CANDIDATE.md
|
||||
a3813d060d842c22ff1a0f5ed968625b4d9d878fbacccb76d924f365d1c1a3a5 scripts/verify_v2r1c6_candidate.py
|
||||
a194638a44668617ddd2c12e45a43b7f0020d5f0308fcadd9f8da1db5d6bb645 scripts/verify_v2r1c6_candidate.sh
|
||||
951254504f20ae8d0d2397c2c2b2baeb2127651eb1e6cd1dc06695b836e176f9 tests/aios_v2_candidate/conftest.py
|
||||
43322bdfb3cffa9d4756d929741f942a0da45b2498c7b5ed91906e115d968948 tests/aios_v2_candidate/test_v2r1c5_privacy_tools.py
|
||||
068a35099d17047d127ac06be1a84adf090976fab13cf321febb28c787c0cac5 tests/aios_v2_candidate/test_v2r1c6_privacy_tools.py
|
||||
82ab1aa2d50d74a96392a01bb3c0b80eb9033f5bcbd5555487c22bc9250f412d tests/aios_v2_candidate/test_v2r1c6_release_consistency.py
|
||||
3b4a466cee373714805f50232d129ba06322027c51c467d1f35ab2c852b64c12 versions/V2R1C6.json
|
||||
@@ -1,8 +0,0 @@
|
||||
AIOS V2R1C6 candidate artifact
|
||||
repository: aios/aios
|
||||
base_ref: refs/heads/main
|
||||
base_oid: a33e2593967a180c89d44454350ccaeaf3f79b25
|
||||
candidate_branch: agent/v2r1c6-mainline-reconciliation-20260806
|
||||
production_deployed: false
|
||||
tag_created: false
|
||||
baseline_changed: false
|
||||
@@ -1 +0,0 @@
|
||||
{"artifact":"aios-V2R1C7-92b4a937e56b.tar.zst","artifact_sha256":"6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da","artifact_size":9672,"base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v3","source_head_oid":"7f09670511138f1f1f74f6b40329cd35d6eb2e55","tag_created":false,"version":"V2R1C7"}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"duplicates": 0,
|
||||
"files": [
|
||||
{
|
||||
"path": "VERSION",
|
||||
"sha256": "9208325e5b2e6f5ba6b64e000bc21d14d9f2f214e3ad33dd79857e85218fd6c2",
|
||||
"size": 7
|
||||
},
|
||||
{
|
||||
"path": "versions/V2R1C7.json",
|
||||
"sha256": "8f3153c625241f60b65109eeac045e8aed7661d95f3a10f0b32ba3e0e9ac0a85",
|
||||
"size": 801
|
||||
},
|
||||
{
|
||||
"path": "release-manifests/V2R1C7.json",
|
||||
"sha256": "560e5d9af8a8e2ec60ec31cf8659a7982b93ea09b7adb486b738ea2fba460d70",
|
||||
"size": 735
|
||||
},
|
||||
{
|
||||
"path": "deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"sha256": "c96ba560d69f6c2eb700b9f2b42a32e394dadd00dffb23bbefdb9fb3e1571c0d",
|
||||
"size": 709
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/deployment_state.py",
|
||||
"sha256": "0997ab8332985c7124f334c4e49735d8833326578acc61a61a389bf792424de3",
|
||||
"size": 2657
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"sha256": "dd3f8b1cab88e005a291f077807873c381e8ca92fe863dc05817925f0dff9613",
|
||||
"size": 4603
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/v2r1c7_health_service.py",
|
||||
"sha256": "9e45025c41068cafdea2f360edfc4146a5459b0a6bcfbf1403206fc0161e7bac",
|
||||
"size": 174
|
||||
},
|
||||
{
|
||||
"path": "scripts/build_v2r1c7_release_artifact.py",
|
||||
"sha256": "81a423c7814f81cb888b60a538c153e549b2be9099c881c968d55ae84efddc57",
|
||||
"size": 4787
|
||||
},
|
||||
{
|
||||
"path": "scripts/verify_v2r1c7_release.py",
|
||||
"sha256": "32a860e28d5afc105f413d878a13fb8600fa74874dbd3bc4c031ef3efa17896a",
|
||||
"size": 1228
|
||||
},
|
||||
{
|
||||
"path": "scripts/probe_v2r1c7_health.py",
|
||||
"sha256": "64c78dabffb05c8851f87b2a27c43bd1f21728dca586397d03a6bc22906a906c",
|
||||
"size": 681
|
||||
},
|
||||
{
|
||||
"path": "scripts/rollback_v2r1c7_release.sh",
|
||||
"sha256": "5419dc7de0f50908ad0ef1dac74172fce811bb7b45f314a6e1c1bb9ce618324d",
|
||||
"size": 97
|
||||
}
|
||||
],
|
||||
"mismatched": 0,
|
||||
"missing": 0,
|
||||
"missing_paths": [],
|
||||
"required_files": [
|
||||
"VERSION",
|
||||
"versions/V2R1C7.json",
|
||||
"release-manifests/V2R1C7.json",
|
||||
"deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"active-runtime/app/deployment_state.py",
|
||||
"active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"active-runtime/app/v2r1c7_health_service.py",
|
||||
"scripts/build_v2r1c7_release_artifact.py",
|
||||
"scripts/verify_v2r1c7_release.py",
|
||||
"scripts/probe_v2r1c7_health.py",
|
||||
"scripts/rollback_v2r1c7_release.sh"
|
||||
],
|
||||
"schema": "aios.v2r1c7.source-manifest.v1",
|
||||
"unexpected": 0,
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"artifact":"aios-V2R1C7-92b4a937e56b.tar.zst","artifact_sha256":"6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da","artifact_size":9672,"base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v3","source_head_oid":"7f09670511138f1f1f74f6b40329cd35d6eb2e55","tag_created":false,"version":"V2R1C7"}
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"artifact":"aios-V2R1C7-v2r1c7.tar.zst","artifact_sha256":"08864f3f2d7e674a908a1d6eb7436db8747bd917200bf206b01276ec3da1e4be","artifact_size":7993,"baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v2","source_base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","tag_created":false,"version":"V2R1C7"}
|
||||
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
AIOS_V2_VERSION=V2R1C6
|
||||
AIOS_V2R1C6_PORT=18926
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_V2R1C6_PRODUCTION_DEPLOYED=false
|
||||
@@ -1,30 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C6 candidate privacy service (not production-enabled)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-v2r1c6
|
||||
Group=aios-v2r1c6
|
||||
WorkingDirectory=/opt/aios/v2r1c6
|
||||
EnvironmentFile=-/etc/aios/aios-v2r1c6.env
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_V2R1C6_PRODUCTION_DEPLOYED=false
|
||||
ExecStart=/usr/bin/python3 /opt/aios/v2r1c6/active-runtime/app/v2r1c6_privacy_tools.py --serve --host 127.0.0.1 --port 18926
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,8 +0,0 @@
|
||||
# Installer generates these bindings; STAGED is the only pre-acceptance state.
|
||||
RELEASE_VERSION=V2R1C7
|
||||
TARGET_OID=__INSTALL_TARGET_OID__
|
||||
ARTIFACT_SHA256=__INSTALL_ARTIFACT_SHA256__
|
||||
DEPLOYMENT_STATE=STAGED
|
||||
AIOS_V2R1C7_PORT=18927
|
||||
DEPLOYMENT_STATE_FILE=/var/lib/aios/v2r1c7/deployment-state.json
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
@@ -1,28 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C7 governed health contract service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-v2r1c7
|
||||
Group=aios-v2r1c7
|
||||
WorkingDirectory=/opt/aios/v2r1c7
|
||||
EnvironmentFile=/etc/aios/aios-v2r1c7.env
|
||||
ExecStart=/usr/bin/python3 /opt/aios/v2r1c7/active-runtime/app/v2r1c7_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"schema": "aios.v2r1c6.candidate-rollback.v1",
|
||||
"candidate_version": "V2R1C6",
|
||||
"rollback_version": "V2R1C5",
|
||||
"previous_tag": "V2R1C5",
|
||||
"previous_tag_oid": "9e65bd59baea392294c0f253046d3ab116f63145",
|
||||
"previous_target_oid": "aa60e891547abd7e215af7cf2bf3f21fd23541da",
|
||||
"baseline_unchanged": true,
|
||||
"tag_rewrite": false,
|
||||
"force_push": false,
|
||||
"production_deploy_authorized": false,
|
||||
"rollback_steps": [
|
||||
"stop aios-v2r1c6-production.service if a separately approved deployment exists",
|
||||
"restore the V2R1C5 production artifact and service configuration",
|
||||
"verify V2R1C5 tag and Baseline before any restart"
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"schema": "aios.release-rollback.v1",
|
||||
"version": "V2R1C7",
|
||||
"previous_version": "V2R1C6",
|
||||
"previous_tag": "V2R1C6",
|
||||
"rollback_state": "ROLLED_BACK",
|
||||
"production_deploy_authorized": false,
|
||||
"execution": "manual_owner_approval_required"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"schema": "aios.v2r1c7.runtime-config.v1",
|
||||
"install_state": "STAGED",
|
||||
"acceptance_transition": "STAGED -> ACTIVE only after staging gates",
|
||||
"production_deploy_authorized": false,
|
||||
"registry_write_authorized": false,
|
||||
"baseline_write_authorized": false
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS manifest-driven release controller (validation only)
|
||||
ConditionPathExists=/var/lib/aios-release/release-source-manifest.json
|
||||
ConditionPathExists=/var/lib/aios-release/release-request.json
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=aios-release
|
||||
Group=aios-release
|
||||
Environment=PYTHONPATH=/opt/aios/release-controller
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_PRODUCTION_DEPLOYMENT_AUTHORIZED=false
|
||||
ExecStart=/usr/bin/python3 -m aios_release_controller.cli validate-release /var/lib/aios-release/release-source-manifest.json /var/lib/aios-release/release-request.json
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/var/lib/aios-release
|
||||
CapabilityBoundingSet=
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS governed Agent Pull Runner (example)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-agent
|
||||
Group=aios-agent
|
||||
UMask=0077
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios-gitops/active-runtime/app/agent_pull_runner_cli.py --worker-id %H
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
||||
ReadWritePaths=/var/lib/aios-gitops/runtime/agent-pull-runner
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS governed approved production release executor
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
EnvironmentFile=/etc/aios-gitops/approved-release-executor.env
|
||||
ExecStart=/usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/approved_release_executor.py \
|
||||
--authorization ${AIOS_RELEASE_AUTHORIZATION} \
|
||||
--repository ${AIOS_GITOPS_AUTHORITATIVE_GIT_DIR} \
|
||||
--state-root ${AIOS_RELEASE_EXECUTION_STATE_ROOT} \
|
||||
--artifact ${AIOS_RELEASE_ARTIFACT} \
|
||||
--artifact-size ${AIOS_RELEASE_ARTIFACT_SIZE} \
|
||||
--version ${AIOS_RELEASE_VERSION} \
|
||||
--target-oid ${AIOS_RELEASE_TARGET_OID} \
|
||||
--evidence-closure-oid ${AIOS_RELEASE_EVIDENCE_CLOSURE_OID} \
|
||||
--artifact-sha256 ${AIOS_RELEASE_ARTIFACT_SHA256} \
|
||||
--approval-plan-id ${AIOS_RELEASE_APPROVAL_PLAN_ID} \
|
||||
--production-service aios-v2r1c7-production.service \
|
||||
--production-port 18927 \
|
||||
--tag-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/governed_tag_adapter.py \
|
||||
--deploy-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/v2r1c7_production_adapter.py \
|
||||
--registry-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=registry /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py \
|
||||
--baseline-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=baseline /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/etc/aios /etc/systemd/system /opt/aios /var/lib/aios /var/lib/aios-gitops /var/lib/aios-gitops-runtime-anchor /data/aios-gitops
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,42 +0,0 @@
|
||||
[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
|
||||
@@ -1,51 +0,0 @@
|
||||
[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,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS branch-bound repair lease control plane
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios-gitops/active-runtime/app/aios_branch_repair_lease_api.py
|
||||
Restart=on-failure
|
||||
NoNewPrivileges=no
|
||||
|
||||
# Bind the transport through the existing authenticated loopback control plane.
|
||||
# This service never owns a Git ref and never receives bearer plaintext.
|
||||
@@ -1,15 +0,0 @@
|
||||
# Template only. Deployment Agent must adapt the existing absolute application path.
|
||||
# /etc/systemd/system/aios-gitops-model-api.service.d/40-authorized-writer-bridge.conf
|
||||
[Service]
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
|
||||
Environment=AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
|
||||
Environment=AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
|
||||
Environment=AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
|
||||
Environment=AIOS_GITOPS_OWNER_PROVISION_SCOPE=writer:provision
|
||||
Environment=AIOS_GITOPS_GOVERNED_USE_SUDO=true
|
||||
Environment=AIOS_GITOPS_SUDO_PATH=/usr/bin/sudo
|
||||
# Replace the existing Python entrypoint only after staging acceptance.
|
||||
# ExecStart=
|
||||
# ExecStart=/usr/bin/python3 /opt/aios/active-runtime/app/aios_authorized_task_writer_api.py
|
||||
@@ -1,34 +0,0 @@
|
||||
# Template only. Deployment Agent must substitute non-secret, verified values.
|
||||
# /etc/systemd/system/aios-gitops-model-api.service.d/45-owner-authorization-broker.conf
|
||||
[Service]
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_BROKER_ENABLED=true
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT=/var/lib/aios-gitops/owner-authorization-broker
|
||||
|
||||
# Select exactly one scope already present on the intended non-wildcard Owner
|
||||
# credential. This is only an authentication intake scope; it is not copied
|
||||
# into the generated task-writer grant.
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_INTAKE_SCOPES=commit:create
|
||||
|
||||
# Required explicit Owner account allowlist. Replace with the verified account
|
||||
# from the existing token registry; never put the bearer token here.
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_IDENTITIES=REPLACE_WITH_OWNER_ACCOUNT
|
||||
|
||||
# If handler.need() does not return non-secret scope metadata, configure the
|
||||
# exact non-secret credential ID instead. Leave empty when scope readback is
|
||||
# available. Never put a token or token digest in this value.
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_ALLOWED_CREDENTIAL_IDS=
|
||||
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_MAX_EXPIRY_SECONDS=900
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
|
||||
Environment=AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
|
||||
Environment=AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
|
||||
Environment=AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
|
||||
Environment=AIOS_GITOPS_GOVERNED_USE_SUDO=true
|
||||
Environment=AIOS_GITOPS_SUDO_PATH=/usr/bin/sudo
|
||||
|
||||
# Replace the current Python entrypoint only after staging acceptance.
|
||||
# This wrapper preserves existing routes and both bridge endpoints.
|
||||
# ExecStart=
|
||||
# ExecStart=/usr/bin/python3 /opt/aios/active-runtime/app/aios_owner_authorization_broker_api.py
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS temporary governed local writer
|
||||
Requires=aios-governed-local-writer.socket
|
||||
After=aios-governed-local-writer.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=true
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios-gitops/active-runtime/app/aios_governed_local_writer_service.py
|
||||
Restart=no
|
||||
PrivateNetwork=yes
|
||||
NoNewPrivileges=no
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS governed local writer Unix socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/aios-gitops/governed-local-writer.sock
|
||||
SocketUser=root
|
||||
SocketGroup=root
|
||||
SocketMode=0600
|
||||
DirectoryMode=0700
|
||||
RemoveOnStop=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -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
|
||||
@@ -1,59 +0,0 @@
|
||||
# Staging-only task identity overlay for V2R1C6 bootstrap.
|
||||
# Install as /etc/systemd/system/aios-v2r1c6-task-identity-overlay.service
|
||||
# after substituting the verified staging path.
|
||||
[Unit]
|
||||
Description=AIOS V2R1C6 task-scoped identity overlay
|
||||
After=network.target
|
||||
ConditionPathIsDirectory=/data/aios-gitops/repo.git
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=REPLACE_WITH_ROOT_OWNED_STAGING_DIR
|
||||
Environment=PYTHONPATH=REPLACE_WITH_ROOT_OWNED_STAGING_DIR/active-runtime/app
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_ENABLED=true
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_HOST=127.0.0.1
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_PORT=19864
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_STATE_ROOT=/var/lib/aios-gitops/task-identity-overlays
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_SECRET_ROOT=/run/aios-gitops/task-identity-overlays
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_TASK_ID=V2R1C6_TASK_SCOPED_WRITER_PROVISIONING
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_OWNER_IDENTITY=aiman
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_BASE_REF=refs/heads/main
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_BASE_OID=875d41c3101093149414292067fd700dc214d4d3
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_TARGET_BRANCH=agent/v2r1c6-mainline-reconciliation-20260805
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_MAX_TTL_SECONDS=900
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_REQUIRE_ROOT=true
|
||||
# Agent must enumerate every active bearer secret reference discovered on-host.
|
||||
Environment=AIOS_GITOPS_IDENTITY_OVERLAY_COLLISION_SECRET_REFS=REPLACE_WITH_COMMA_SEPARATED_ACTIVE_SECRET_REFS
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_OWNER_AUTHORIZATION_STATE_ROOT=/var/lib/aios-gitops/owner-authorization-broker
|
||||
Environment=AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
|
||||
Environment=AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
|
||||
Environment=AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
|
||||
Environment=AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
|
||||
Environment=AIOS_GITOPS_GOVERNED_USE_SUDO=false
|
||||
ExecStart=/usr/bin/python3 REPLACE_WITH_ROOT_OWNED_STAGING_DIR/active-runtime/app/aios_task_identity_overlay_api.py
|
||||
Restart=no
|
||||
RuntimeDirectory=aios-gitops/task-identity-overlays
|
||||
RuntimeDirectoryMode=0700
|
||||
StateDirectory=aios-gitops/task-identity-overlays
|
||||
StateDirectoryMode=0700
|
||||
UMask=0077
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectHostname=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"action": "AUTHORIZE_TASK_WRITER_PROVISIONING",
|
||||
"allowed_operations": [
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch"
|
||||
],
|
||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"base_ref": "refs/heads/main",
|
||||
"baseline_change_authorized": false,
|
||||
"denied_operations": [
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite"
|
||||
],
|
||||
"expires_in_seconds": 900,
|
||||
"main_write_authorized": false,
|
||||
"one_time_use": true,
|
||||
"production_change_authorized": false,
|
||||
"repository": "aios/aios",
|
||||
"tag_change_authorized": false,
|
||||
"target_branch": "agent/v2r1c6-mainline-reconciliation-20260805",
|
||||
"task_id": "V2R1C6_TASK_SCOPED_WRITER_PROVISIONING"
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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 原生会话。
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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,并保留非敏感回执。
|
||||
@@ -1,26 +0,0 @@
|
||||
# Governed ReleasePlan 关闭条件
|
||||
|
||||
ReleasePlan 必须绑定 task、repository、integration branch、完整 target/tree OID、artifact/manifest/evidence/rollback digest、生产配置 digest,以及互不相同的部署和回滚 ApprovalPlan ID。
|
||||
|
||||
## Candidate 与 Integration
|
||||
|
||||
1. Candidate 分支先完成测试、扫描、Shadow、Artifact 和外部门禁盘点。
|
||||
2. 重新读取 authoritative `main` 完整 OID。
|
||||
3. 从该 OID 创建 integration 分支。
|
||||
4. 使用普通 `merge --no-ff` 合入 Candidate;禁止 squash、rebase、amend 和 force push。
|
||||
5. 在 integration 上重新执行全部测试、扫描和 artifact closure。
|
||||
|
||||
## 生产停止线
|
||||
|
||||
下列项目任一为 false,流程必须停止在 `READY_FOR_OWNER_RELEASE_APPROVAL`,不得部署或创建生产 tag:
|
||||
|
||||
```text
|
||||
openclaw_native_child_session_verified
|
||||
codex_child_session_verified
|
||||
gitea_admin_api_lifecycle_canary
|
||||
owner_signer_separation_verified
|
||||
credential_incident_closed
|
||||
external_owner_release_approval_verified
|
||||
```
|
||||
|
||||
聊天文本、Candidate 自签记录或模拟 canary 不能改变这条停止线。
|
||||
@@ -1,29 +0,0 @@
|
||||
# AIOS autonomy v1 and V2R1C6 governance components
|
||||
|
||||
This branch adds bounded control-plane primitives used by the V2R1C6 release
|
||||
workflow. The components are deliberately transport-neutral; deployment
|
||||
binds them to the existing task plane and governed local writer.
|
||||
|
||||
- `agent_pull_runner.py`: worker registration, heartbeat, long-poll with a
|
||||
60-second fallback, jittered bounded backoff, claim/renew/report and restart
|
||||
recovery with duplicate suppression.
|
||||
- `autonomy_notification_plane.py`: SQLite durable outbox for Owner and GPT,
|
||||
delivery retry and acknowledgement. Notification failure never rolls back
|
||||
task execution.
|
||||
- `recurring_task_scheduler.py`: interval scheduling with `next_run_at`, run
|
||||
bounds, cancellation and idempotent successor creation.
|
||||
- `release_state_machine.py`: ordered release states with fail-closed
|
||||
production acceptance evidence.
|
||||
- `reconciliation_controller.py`: read-only drift detection for refs, tags,
|
||||
targets, registry/Baseline, artifact digest and service health.
|
||||
- `governed_merge_gate.py`: supplements native Gitea protection when the
|
||||
installed API cannot express Owner approval identity or conversation
|
||||
resolution. It never writes refs and never authorizes force-push.
|
||||
|
||||
No component grants main, tag, Baseline, force-push, arbitrary-shell or
|
||||
production-deploy capability.
|
||||
|
||||
The task writer persists request identity, idempotency keys, final commit OIDs,
|
||||
parent OIDs and sanitized audit digests. A caller queries the receipt before
|
||||
recovering an ambiguous request, and every branch update remains a
|
||||
fast-forward CAS append.
|
||||
@@ -1,63 +0,0 @@
|
||||
# AIOS Approved Production Release Executor
|
||||
|
||||
Status: implementation artifact for governed protected-PR integration.
|
||||
|
||||
## Why this exists
|
||||
|
||||
The deployed `aios-silent-release-controller` is intentionally shadow-only.
|
||||
It rejects production execution unless a trusted builder/orchestrator factory is
|
||||
wired. V2R1C7 already completed post-merge deterministic build, staging,
|
||||
rollback, evidence closure and exact Owner approval, so rebuilding inside the
|
||||
production executor is both unnecessary and a deadlock source.
|
||||
|
||||
This patch adds a separate one-shot production executor. It **does not turn the
|
||||
shadow controller into a production service**.
|
||||
|
||||
## Exact V2R1C7 binding
|
||||
|
||||
- target: `c7adece1cc8beffe7c718d53c775898b45d6c7f8`
|
||||
- evidence: `8c2898baf07195b9861ed2ff74f03a71674ec9e9`
|
||||
- artifact: `ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e`
|
||||
- approval plan: `sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28`
|
||||
|
||||
The executor requires a root-owned accepted authorization receipt that matches
|
||||
all five fields. It does not accept caller-supplied replacement evidence.
|
||||
|
||||
## Execution order
|
||||
|
||||
1. exact authorization + artifact + Git lineage preflight
|
||||
2. annotated V2R1C7 Tag publication and Authority/network/Gitea verification
|
||||
3. deploy exact artifact; health must report ACTIVE and exact target/digest
|
||||
4. Registry atomic finalization
|
||||
5. authoritative Production Baseline CAS finalization **last**
|
||||
6. read back Tag/runtime/Registry/Baseline and require exact consistency
|
||||
|
||||
## Tag implementation
|
||||
|
||||
`governed_tag_adapter.py` is a thin adapter over the repository's existing
|
||||
`gitea_tag_publication.TagPublisher`. It intentionally calls only the existing
|
||||
tag verification/publication subroutine and never calls legacy
|
||||
`TagPublisher.publish()` because that legacy operation finalizes Baseline before
|
||||
the product runtime is ACTIVE.
|
||||
|
||||
## Runtime finalization
|
||||
|
||||
`release_metadata_adapter.py` writes no Git refs. Registry and Baseline are
|
||||
runtime state only and are atomic/CAS guarded. Baseline apply independently
|
||||
requires exact ACTIVE health, exact formal tag, exact Registry, and the exact
|
||||
previous Baseline SHA-256.
|
||||
|
||||
For this release the previous **production** baseline is V2R1C5. V2R1C6 has a
|
||||
formal tag but was never deployed, so rollback must return to V2R1C5.
|
||||
|
||||
## Governance
|
||||
|
||||
- force push: forbidden
|
||||
- tag rewrite/delete: forbidden
|
||||
- direct main push: forbidden
|
||||
- V2R1C5/V2R1C6 mutation: forbidden
|
||||
- plaintext secrets in Git: forbidden
|
||||
- exact binding replacement: manual Owner review required
|
||||
- after a verified Tag, uncertain errors become reconciliation-required
|
||||
- global `AIOS_GITOPS_WRITES_ENABLED` stays false; this executor is a narrow,
|
||||
separately installed production capability.
|
||||
@@ -1,53 +0,0 @@
|
||||
# AIOS Local Owner Authentication Ingress
|
||||
|
||||
This component closes the Owner authentication bootstrap gap without creating a
|
||||
long-lived Owner PAT.
|
||||
|
||||
## Trust model
|
||||
|
||||
The ingress is deliberately one-shot and local-only.
|
||||
|
||||
1. A root-run server creates a Unix socket.
|
||||
2. The peer must be the configured OS Owner account (`aiman`).
|
||||
3. The server verifies Unix `SO_PEERCRED`, all four process UIDs, Linux audit
|
||||
`loginuid`, and ownership of the controlling TTY.
|
||||
4. A `sudo -u aiman` / `runuser` process from another authenticated login is
|
||||
rejected because its audit `loginuid` does not equal the Owner UID.
|
||||
5. The human Owner sees the exact task/base/branch/capability request and types
|
||||
a random one-time challenge.
|
||||
6. The server constructs only:
|
||||
- identity: `aiman`
|
||||
- role: `owner`
|
||||
- authenticated scope: `commit:create`
|
||||
- token scopes: [`commit:create`]
|
||||
7. It calls the existing `OwnerAuthorizationBroker`; it does not call the
|
||||
provisioner directly.
|
||||
8. It exits and removes the socket after the single authorization attempt.
|
||||
|
||||
No reusable token is minted and no production/tag/baseline/main capability is
|
||||
granted.
|
||||
|
||||
## Current V2R1C7 bootstrap binding
|
||||
|
||||
The bootstrap request is expected to authorize only:
|
||||
|
||||
- task: `V2R1C7_PRODUCTION_EXECUTOR_BOOTSTRAP`
|
||||
- branch: `agent/v2r1c7-production-executor-bootstrap-20260807-r2`
|
||||
- base: exact live `refs/heads/main`
|
||||
- repository: `aios/aios`
|
||||
- scope: `commit:create`
|
||||
- TTL: <= 900 seconds
|
||||
- production/tag/baseline/main write: false
|
||||
|
||||
## Installation model
|
||||
|
||||
For the current trust-root bootstrap, the exact SHA-verified script may be
|
||||
staged under `/run/aios-owner-bootstrap/` and run by a transient root systemd
|
||||
service. This staging step has no Git/Tag/Registry/Baseline side effect.
|
||||
|
||||
The Owner must connect from a real interactive login session as the OS account
|
||||
`aiman`.
|
||||
|
||||
After the governed writer is issued, the same module and tests are committed
|
||||
through the resulting protected engineering PR so future bootstrap uses code
|
||||
from main rather than an out-of-band artifact.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Owner Authorization → Task Writer Provisioning Bridge
|
||||
|
||||
## Purpose
|
||||
|
||||
This change closes the control-plane gap where an authenticated Owner can
|
||||
approve a task in the API, but the runtime cannot automatically produce the
|
||||
machine-verifiable, signed, one-time grant required by
|
||||
`aios-task-writer-provisioner`.
|
||||
|
||||
The bridge adds one high-risk endpoint:
|
||||
|
||||
`POST /api/task-writer/provision-authorized`
|
||||
|
||||
It does not add a general branch endpoint to the hardened writer. Branch and
|
||||
worktree creation remain the responsibility of the existing root-governed
|
||||
one-shot provisioner. Source commits continue through the isolated writer's
|
||||
existing `POST /api/commit-file` route.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
The API must authenticate the caller as `role=owner` and require the
|
||||
`writer:provision` scope. The request contains an auditable Owner authorization
|
||||
reference; it never contains a private key, token or signature.
|
||||
|
||||
The bridge:
|
||||
|
||||
1. Validates an exact repository, base ref, base OID and target branch.
|
||||
2. Rejects all production, tag, baseline and direct-main capabilities.
|
||||
3. Generates a canonical approval plan and short-lived grant request.
|
||||
4. Calls the root-owned signer adapter.
|
||||
5. Calls the independent grant validator.
|
||||
6. Calls the one-shot task writer provisioner.
|
||||
7. Verifies the resulting branch, writer scope and negative policy tests.
|
||||
8. Stores a redacted, fsynced idempotency receipt.
|
||||
|
||||
The bridge never executes `git update-ref`, `git branch`, `git push`, a tag
|
||||
operation, a baseline mutation or a deployment command directly.
|
||||
|
||||
## Required runtime configuration
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
AIOS_GITOPS_AUTHORIZED_WRITER_STATE_ROOT=/var/lib/aios-gitops/authorized-writer-bridge
|
||||
AIOS_GITOPS_GRANT_SIGNER_ADAPTER=/usr/local/libexec/aios-grant-signer-adapter
|
||||
AIOS_GITOPS_GRANT_VALIDATOR=/usr/local/libexec/aios-grant-validator
|
||||
AIOS_GITOPS_TASK_WRITER_PROVISIONER=/usr/local/libexec/aios-task-writer-provisioner
|
||||
AIOS_GITOPS_OWNER_PROVISION_SCOPE=writer:provision
|
||||
AIOS_GITOPS_GOVERNED_USE_SUDO=true
|
||||
AIOS_GITOPS_SUDO_PATH=/usr/bin/sudo
|
||||
```
|
||||
|
||||
When the API service is non-root, `AIOS_GITOPS_GOVERNED_USE_SUDO=true`
|
||||
uses the fixed `/usr/bin/sudo -n -- <governed executable>` form. Sudoers must
|
||||
allow only the three exact executables and must not allow a shell, arbitrary
|
||||
environment preservation or arbitrary command paths.
|
||||
|
||||
The three governed executables must be absolute, regular, executable,
|
||||
non-symlink files and must not be group/world writable. Each must expose:
|
||||
|
||||
```text
|
||||
--help
|
||||
--self-test
|
||||
--grant-dir
|
||||
--result-json
|
||||
```
|
||||
|
||||
The signer result must bind `approval_plan_id`, `grant_id`,
|
||||
`grant_digest_sha256`, `signing_key_identity` and proof that a signature is
|
||||
present. The validator must independently attest the exact repository,
|
||||
base/target bindings, signature, lifetime, operation lists and one-time-use
|
||||
property. The provisioner must return `READY_FOR_CODE_AUTHORING`, consume the
|
||||
grant, create the exact branch at the exact base OID, start a writer bound only
|
||||
to `POST /api/commit-file`, and report successful negative policy tests.
|
||||
|
||||
## Deployment model
|
||||
|
||||
Run `aios_authorized_task_writer_api.py` as the ChatGPT Actions API entrypoint.
|
||||
It delegates all existing routes to `aios_chatgpt_actions_api` and intercepts
|
||||
only the new provisioning endpoint.
|
||||
|
||||
Do not enable this endpoint until:
|
||||
|
||||
- the Owner scope is present in the API authorization registry;
|
||||
- the signer adapter uses a runtime secret reference;
|
||||
- the validator and provisioner pass their self-tests;
|
||||
- the state directory is root-owned and mode `0700`;
|
||||
- the negative tests show main, other branches, tags, baseline, force push and
|
||||
branch deletion are denied.
|
||||
|
||||
## Failure behavior
|
||||
|
||||
Every mismatch fails closed. A changed `main` OID, existing conflicting branch,
|
||||
expired or untrusted signature, missing negative test, writer scope expansion,
|
||||
or unconsumed grant returns an error and leaves production state untouched.
|
||||
|
||||
Idempotent retries return the existing redacted receipt. A retry with a
|
||||
different task, repository, branch, base or Owner authorization reference is
|
||||
rejected.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Branch-bound repair lease
|
||||
|
||||
A repair lease is a short-lived, append-only capability bound to one branch,
|
||||
one Owner identity, and one path allowlist. The default maximum lifetime is
|
||||
24 hours. State is persisted atomically under a root-only `0700` directory so
|
||||
the service can restart and recover without recreating authorization.
|
||||
|
||||
Supported operations are `issue`, `attach_existing_branch`,
|
||||
`authorize_commit`, `record_commit`, `recover`, `renew`, `complete`, and
|
||||
`revoke`. Every commit is checked for the exact expected parent, fast-forward
|
||||
lineage, allowed paths, protected-path exclusion, secret-like content, and
|
||||
current branch head. A stale head, unrelated path, protected path, identity
|
||||
mismatch, expired/revoked lease, non-fast-forward movement, or failed Git
|
||||
probe fails closed.
|
||||
|
||||
The lease layer is a policy and evidence layer. It does not replace the
|
||||
existing GitOps writer and it never writes Git refs directly.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Governed local writer
|
||||
|
||||
`governed-local-writer` is a temporary bootstrap seam for a task-scoped
|
||||
recovery when the normal bearer credential is intentionally unavailable.
|
||||
|
||||
The service listens only on `/run/aios-gitops/governed-local-writer.sock`,
|
||||
owned by `root:root` with mode `0600`. It validates Linux `SO_PEERCRED` and
|
||||
accepts only UID 0. Authorization is a root-owned mode `0600` JSON file with a
|
||||
canonical SHA-256 digest, a short expiry, exact branch/OID/path policy, and
|
||||
one-shot state. Audit records contain only OIDs, task metadata, peer identity,
|
||||
operation, result, and timestamps; they never contain request content or
|
||||
credentials.
|
||||
|
||||
The adapter delegates checkout, secret scanning, commit, and fast-forward
|
||||
synchronization to the existing GitOps handler implementation. It does not
|
||||
invoke `git update-ref`, direct main writes, force pushes, tag writes, Baseline
|
||||
writes, or production deployment.
|
||||
@@ -1,121 +0,0 @@
|
||||
# Merge this fragment into docs/gitops/openapi-chatgpt-actions.yaml.
|
||||
components:
|
||||
schemas:
|
||||
AuthorizedTaskWriterProvisionRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- base_ref
|
||||
- base_oid
|
||||
- target_branch
|
||||
- owner_authorization_reference
|
||||
- one_time_use
|
||||
- expires_in_seconds
|
||||
- production_change_authorized
|
||||
- tag_change_authorized
|
||||
- baseline_change_authorized
|
||||
- main_write_authorized
|
||||
- allowed_operations
|
||||
- denied_operations
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
pattern: '^[A-Z0-9][A-Z0-9._:-]{7,127}$'
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
base_ref:
|
||||
type: string
|
||||
const: refs/heads/main
|
||||
base_oid:
|
||||
type: string
|
||||
pattern: '^[0-9a-f]{40}$'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'
|
||||
owner_authorization_reference:
|
||||
type: string
|
||||
minLength: 4
|
||||
maxLength: 256
|
||||
one_time_use:
|
||||
type: boolean
|
||||
const: true
|
||||
expires_in_seconds:
|
||||
type: integer
|
||||
minimum: 60
|
||||
maximum: 1800
|
||||
production_change_authorized:
|
||||
type: boolean
|
||||
const: false
|
||||
tag_change_authorized:
|
||||
type: boolean
|
||||
const: false
|
||||
baseline_change_authorized:
|
||||
type: boolean
|
||||
const: false
|
||||
main_write_authorized:
|
||||
type: boolean
|
||||
const: false
|
||||
allowed_operations:
|
||||
type: array
|
||||
minItems: 4
|
||||
maxItems: 4
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- create_worktree
|
||||
- create_target_branch_if_absent
|
||||
- commit_to_target_branch
|
||||
- push_target_branch
|
||||
denied_operations:
|
||||
type: array
|
||||
minItems: 11
|
||||
maxItems: 11
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- direct_main_push
|
||||
- force_push
|
||||
- tag_create
|
||||
- tag_update
|
||||
- tag_delete
|
||||
- branch_delete
|
||||
- repository_admin
|
||||
- baseline_write
|
||||
- production_deploy
|
||||
- production_marker_write
|
||||
- history_rewrite
|
||||
|
||||
paths:
|
||||
/api/task-writer/provision-authorized:
|
||||
post:
|
||||
operationId: provisionAuthorizedTaskWriter
|
||||
summary: Convert authenticated Owner authorization into a signed one-time task writer grant
|
||||
description: >
|
||||
Generates an approval plan, invokes the root-governed signer and validator,
|
||||
consumes a one-time provisioning grant, and returns a scoped writer
|
||||
attestation. It never authorizes main, tag, baseline, or production changes.
|
||||
x-openai-isConsequential: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizedTaskWriterProvisionRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: READY_FOR_CODE_AUTHORING or idempotent existing attestation
|
||||
'400':
|
||||
description: Invalid or over-broad request
|
||||
'403':
|
||||
description: Authenticated Owner or exact scope required
|
||||
'409':
|
||||
description: Base drift, branch conflict, or idempotency scope conflict
|
||||
'503':
|
||||
description: Signer, validator, provisioner, or attestation unavailable
|
||||
@@ -1,41 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AIOS Branch Repair Lease Fragment
|
||||
version: v1
|
||||
paths:
|
||||
/api/governance/branch-repair-leases:
|
||||
post:
|
||||
summary: Issue or operate one branch-bound repair lease
|
||||
description: >-
|
||||
The existing authenticated control plane transports this operation.
|
||||
Git ref mutation is delegated to the protected writer handler.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [operation, owner_identity]
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
enum: [issue, attach_existing_branch, authorize_commit, record_commit, recover, renew, complete, revoke]
|
||||
owner_identity:
|
||||
type: string
|
||||
lease_id:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
expected_head_oid:
|
||||
type: string
|
||||
pattern: '^[0-9a-f]{40}$'
|
||||
paths:
|
||||
type: array
|
||||
items: {type: string}
|
||||
responses:
|
||||
'200':
|
||||
description: Sanitized lease metadata
|
||||
'403':
|
||||
description: Lease policy rejected
|
||||
'409':
|
||||
description: Stale or concurrently advanced branch
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user