Compare commits

..
Author SHA1 Message Date
owner 9adec87c4b feat(governed): Release Source Admission (AIOS-BASELINE-1.1-RELEASE-SOURCE-ADMISSION)
Make governed release source closure explicit so a pushed governed candidate
can enter the authoritative Git/release closure without manual runtime
overwrite or fixed-recipe bypass.

- Gitea machine transport: add _safe_branch() and exact OID/branch verification.
- release_source_admission.py (new): admit a governed source by verifying remote
  branch -> exact OID, fetching only to refs/aios/release-sources/<OID>,
  validating authoritative main ancestry + tree OID, and writing a policy-bound,
  HMAC-authenticated immutable admission record (source_admission_hmac).
- trusted candidate builder: read-only admission verifier, remains
  PrivateNetwork / read-only; missing admission surfaces SOURCE_ADMISSION_REQUIRED.
- Model API / release saga: consume admission records and return machine-actionable
  SOURCE_ADMISSION_REQUIRED (not NOT_FOUND) when a non-fixed source is unadmitted.
- systemd: add aios-release-source-admission.service (oneshot, root/gitops,
  AIOS_GITOPS_WRITES_ENABLED=false, independent source_admission_hmac credential);
  wire source_admission_hmac into trusted builder; update staging unit.
- Fixed V2R2C2/V2R2C3 recipes are not overridden by admission.
- Tests: new release_source_admission + adjusted builder/model-api/live-executor/
  readiness tests.

Verified: 13-file after_sha256 byte-exact; py_compile PASS;
Linux 326 targeted passed / 0 failed; acceptance 165; regression 1401;
systemd-analyze verify 3/3 PASS.

Base: a320ea3 (agent/baseline-1.1-principal-recovery-20260908).
2026-09-08 11:44:09 +08:00
owner a320ea391e feat(governed): TaskPrincipal auto-recovery (AIOS-BASELINE-1.1-PRODUCTION-CLOSURE)
Integrate the TaskPrincipal auto-recovery patch from the Agent Execution
Handoff 2026-09-08 onto the Baseline 1.1 mainline candidate.

Server-side execution principal recovery:
- TaskAdmissionStore.get_grant_by_id() / get_grant_by_task_id(): read-only
  immutable grant lookup from durable admission grant/receipt state.
- TaskAdmissionPrincipalProvider: recover short-lived execution principal
  from durable admission grant + Owner epoch + authoritative official Task;
  only accepts CLAIMED/RUNNING tasks; re-verifies epoch/capability/task/job/
  repository/branch/generation before every mutation; caller JSON cannot
  supply principal; all miss-scopes fail closed.
- Bridge main() wires the admission runtime verifier back into
  BridgeHandler.principal_verifier; trusted hosts may still inject.
- staging unit: AIOS_P0_TASK_PRINCIPAL_WRITES_ENABLED=true while
  governed_writes_enabled still fail-closed on verifier readiness + legacy
  writes=false.
- OpenAPI contract updated to server-side principal recovery.
- New negative tests + adjusted recovery tests.

Verified before applying: all 8 modified files matched manifest before_sha256
(LF-normalized), so applied after-state bytes directly (no 3-way merge needed).
Index blobs match manifest after_sha256 for all 9 files.
Targeted suite: 81 passed / 1 skipped (POSIX mode-bit, Linux env) on CPython 3.13.

Base: 6ffd7fe (Baseline 1.1 port); authoritative main 0136a534.
2026-09-08 10:07:06 +08:00
owner 6ffd7fe19c feat(governed): port Implementation Baseline 1.1 (57 files)
Apply the approved Implementation Baseline 1.1 architecture revisions onto the
current mainline, verified faithful to the delivered change bundle:

- Add aios_baseline: signed owner-delegation-grant.v1 / operational-epoch.v1 /
  task-principal.v1 auth chain, runtime contract + deployment-truth evaluator,
  declarative operation registry (effect-class + fail-closed dispatch),
  lineage ledger, release/recipe/saga ledger, bounded optimization controller,
  authenticated HTTP transport, resident worker loop and systemd host probe.
- Extend existing Job authority (aios_gitops_job_controller/executor) with
  durable intent/receipt/reconcile and fail-closed admission; harden
  task_plane/store heartbeat CAS and terminal-state fencing; make
  reconciliation_controller fail-closed on incomplete/drift evidence.
- Add isolated-validation profile templates (deploy/baseline-v1.1, systemd
  .example), source change/rollback tooling (tools/baseline_v1_1), the
  AT-01..AT-23 acceptance matrix, M0-M5 runbook, installation/rollback docs and
  the baseline_v1_1 test suite.
- Keep legacy write authorities untouched; isolated-validation profile only.

Staged blobs verified byte-identical to the delivered source-changes bundle.
Cross-platform baseline tests pass; systemd/fcntl-dependent paths require the
declared Linux x86_64 + CPython 3.12 + systemd runtime (site-bound).
2026-09-08 03:51:26 +08:00
AIOS Governed Continuation 0136a53426 feat(governed): make Owner merge authority final
autonomy-smoke-tests phase2-pr76 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests phase2-pr76 PASS evidence_sha256=15bc76da47bdf8218f18fc8a7da65fafd841677d5f754b71a0c9f56e07d84c1b
campaign-approval-gate phase2-pr76 PASS evidence_sha256=ea7ed0516efe6736a8eaee426685e40a2393fbf3ae84104a743f394204c2ec33
git-diff-check phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile phase2-pr76 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract phase2-pr76 PASS evidence_sha256=211444cb0c146f5d4a57e13b780cfe7ffb406e8a8bc2e11c02a3a4728f66b0b2
release-state-machine phase2-pr76 PASS evidence_sha256=6cea88752b3e58ae5cee56ff3b1b13eab34117918d6bad9661194c031e2dc157
secret-scan phase2-pr76 PASS evidence_sha256=7cb68dd604dc6cd4904246e16333072d512dba14c096301e08c5a2b78a45bd55
signer-validator-contract phase2-pr76 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
2026-09-07 23:14:29 +08:00
owner 1f9f65d8c7 Merge pull request 'feat: formal lineage supersession P0' (#75) from agent/singlehost-lineage-supersession-p0-20260905 into main
Reviewed-on: #75
2026-09-07 21:54:16 +08:00
AIOS Governed Continuation 591884bf5f fix(governed): provide collision-safe task storage identity
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=d3ed00952a599a4d17d6cf5e8f9714c233ada76347b3567acab8455fb07c7717
autonomy-unit-tests phase1-p0 PASS evidence_sha256=77c39918bb5883e62fbbae39f1812a1b5b6e19c98ba422d988ab509f84575b35
campaign-approval-gate phase1-p0 PASS evidence_sha256=8f562805faf8d739233301596d3a95d6a128eff7324fd30d663d94a196cf5c13
git-diff-check phase1-p0 PASS evidence_sha256=dc18ae7a546ada66b0bfdd9045464f8413312234baf8d3004eac78387c614134
py-compile phase1-p0 PASS evidence_sha256=ed151cbcc033e00f78a86e86840883d5996b2c7a33ed4520421cc3a6f4423dd3
reconciliation-contract phase1-p0 PASS evidence_sha256=3a4243f272ab2458dc631eb751f267f93c48713d96a59b311ae885e4d0f70728
release-state-machine phase1-p0 PASS evidence_sha256=d24c81a9969c35fc4c599195e3078c10fb06a0b6addfed9b44f4972b228464b9
secret-scan phase1-p0 PASS evidence_sha256=649005c3fef5220b20c9f616658ad283a28d506fe231d8bb6f795de183ad51c5
signer-validator-contract phase1-p0 PASS evidence_sha256=b5461be6646b4347c1ad85cf13889eb03de515ce5e89c041a8f24626fae2ec17
2026-09-07 21:32:46 +08:00
AIOS Governed Continuation 5cf599e1d1 feat: integrate formal lineage supersession P0 2026-09-07 21:28:09 +08:00
owner b3cee6ae48 Merge pull request 'feat(discovery): governed immutable repository discovery P0' (#73) from agent/repository-discovery-p0-20260902 into main
Reviewed-on: #73
2026-09-07 20:43:02 +08:00
AIOS Governed Continuation c8c25fa672 fix(discovery): make P0 contract hermetic
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=13aff2dfe664be536470ca26a6ac3f20ba15fdca37cd4f8e799c0c8c36974be9
autonomy-unit-tests phase1-p0 PASS evidence_sha256=118708029df05d861585179a31e4f4eaf0d7953563f5347b76732f2055785ba0
campaign-approval-gate phase1-p0 PASS evidence_sha256=4e39d586b32ad2d17da8fc38f5d359ace8a239af8687b4f894a338fe9f0bf83c
git-diff-check phase1-p0 PASS evidence_sha256=2638b909fa281012b6a43aaf34e1d611b0dba1fc78bf47f54941739973ee6281
py-compile phase1-p0 PASS evidence_sha256=440cf54c4225f809a575eff9b24238acd361497fd18bffbb359087e88310dcd6
reconciliation-contract phase1-p0 PASS evidence_sha256=131fb1042ff1ceb565dbf1ec7aed1cd3879d728f912745c0589a02b96381baaf
release-state-machine phase1-p0 PASS evidence_sha256=a74b5192b248b6e342afeea01c85ecbb8a02d71891bcf334df2e89d96cc1577d
secret-scan phase1-p0 PASS evidence_sha256=d4789e5c45ba681c651c613427d70d911a4cdd70fbead0772c4e1e376bef41cd
signer-validator-contract phase1-p0 PASS evidence_sha256=a29c7664de7aa9e30e966f8c4a14c0eaf8191fbb92ba726a72ba3c7ec2659eec
2026-09-07 20:33:11 +08:00
AIOS Governed Continuation 1735371e06 feat(discovery): integrate P0 discovery with task admission bootstrap
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=a96e50527a39e969b53df42ce799526faa8e3bcd618b6f5e2ec2e2e13c574f02
autonomy-unit-tests phase1-p0 FAIL evidence_sha256=d61da9d108d6ace7d845f78b1010773b4b1ce315a538209d7e60eb870c8eb953
campaign-approval-gate phase1-p0 PASS evidence_sha256=29d249ebfb389c3137e411c1d87fb88c7f755cdba4fea5118be187259c707295
git-diff-check phase1-p0 FAIL evidence_sha256=a0adbb54c3b220b9ffc82656470f1d67904823e461fb8ffedd713846814acc78
py-compile phase1-p0 PASS evidence_sha256=f504e34aa0a28ad009ba3b3fd3a75448297be43c452f4dc5ba445d3a4d0f7f07
reconciliation-contract phase1-p0 PASS evidence_sha256=8965ee765da46e32e79d046eeb57db023a2e9bd5105f057a27546b42eed470e3
release-state-machine phase1-p0 PASS evidence_sha256=e5a4c127d4b5b6eb27918fd9c54c06ee23d36d613bb816be475006c81b8ff479
secret-scan phase1-p0 PASS evidence_sha256=fc82389ca65008f045aa9357e5f276eeb0ce040ac5511197d65f1027975bc238
signer-validator-contract phase1-p0 PASS evidence_sha256=79a1d0246c73f0485816f77345fbc769c23f659cd177fefba90bb27e842214f3
2026-09-07 20:14:22 +08:00
AIOS Governed Continuation 447163fe82 test(security): allow exact historical idempotency fixture
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=fdb08fc46698d7d38b3981df0e27bc5b5bb1da1c57f63fc756a4f08b649a132c
autonomy-unit-tests phase1-p0 PASS evidence_sha256=a7d74280ec71babbb7ff2824c0a8f1ec29db647478f395457979e02164360f9d
campaign-approval-gate phase1-p0 PASS evidence_sha256=d0d77e65dee6f2cf1303a1030473331754ab91982b830684284a562fe5320f0d
git-diff-check phase1-p0 PASS evidence_sha256=5e9628231a64e6faa04570e4817ff4109d791cea2838541994698847987983fb
py-compile phase1-p0 PASS evidence_sha256=47df225f475589b8d9c843ba6d49908c39ce7920890e8b2f355d24818d0558e0
reconciliation-contract phase1-p0 PASS evidence_sha256=24840ca69508034e002e4b12a5525822042bb01b5a6f73a697ca5fdbea30f8ec
release-state-machine phase1-p0 PASS evidence_sha256=805e5a17e6cfb8a2070a34c36e52c89808b89fd4055800dc1fa9b3d58c6c656b
secret-scan phase1-p0 PASS evidence_sha256=eb3dc75806b3af6c1963f1d284cb8b90971694356fdac63d2a09cd08972d8c74
signer-validator-contract phase1-p0 PASS evidence_sha256=da317f9fc7c8ce90b99417c4ec129ed5565c058ac96bb89e56a1a72ce5ab97b3
2026-09-07 18:00:05 +08:00
AIOS Governed Continuation 35f8d189e6 test(governed): normalize bootstrap contract line endings
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=e174976180ecd717a6d24749c91cc53e35a573273de650004fb4a5b7983ccc23
autonomy-unit-tests phase1-p0 PASS evidence_sha256=45c6e4bd0d53ee3f0c64186f16cadbe13adf07b8d4fa24d4624ed90fc08468b2
campaign-approval-gate phase1-p0 PASS evidence_sha256=b182a5af7b5605da5bda1856d6f69774ec19305666fb563b6b4254b85eea167f
git-diff-check phase1-p0 PASS evidence_sha256=6b50f3b275db390e111297b3780a66a97269aa102317dae7584f9987fbd72981
py-compile phase1-p0 PASS evidence_sha256=c82d4aedfb4c752b67467a25ea9f8b9f75f0bea008553abcbcaa78f03751f164
reconciliation-contract phase1-p0 PASS evidence_sha256=cd0071eb74bc0606654eea7159ab835a6ea087a24ce594657a0d7b286206cb59
release-state-machine phase1-p0 PASS evidence_sha256=d4b8b0d8838fc523eaab64dbb4618971b341148f068c1da2395c3955d6fd6e23
secret-scan phase1-p0 FAIL evidence_sha256=47d13c1b7d3c967277c56ce2c62c12bd2b9f221e1e42cbb38461c44574f0919f
signer-validator-contract phase1-p0 PASS evidence_sha256=cdb8009765558b5acd2c3de15bdf5681814e64b2c1587676a5528be5bbca1e95
2026-09-07 17:40:07 +08:00
AIOS Governed Continuation 6d4f79a8c2 fix(governed): make P0 task admission self-contained 2026-09-07 15:22:20 +08:00
AIOS Governed Continuation 1aee083564 feat: formal lineage supersession P0 2026-09-07 08:27:33 +08:00
AIOS Governed Continuation 1efa7724c1 feat(discovery): governed immutable repository discovery P0 2026-09-04 00:16:46 +08:00
AIOS Governed Continuation a7e835b9d3 feat(governed): close P0 task admission bootstrap 2026-09-03 21:40:32 +08:00
121 changed files with 29765 additions and 255 deletions
+10
View File
@@ -0,0 +1,10 @@
title = "AIOS repository secret scan policy"
[extend]
useDefault = true
[allowlist]
description = "Exact non-secret Task Admission test idempotency fixture in the historical PR commit"
condition = "AND"
paths = ['''tests/test_task_admission_bootstrap_http\.py$''']
regexes = ['''idempotency-1''']
@@ -0,0 +1,122 @@
"""Staging HTTP overlay for the exact Task lineage supersession capability."""
from __future__ import annotations
from http.server import ThreadingHTTPServer
from typing import Any, Callable, Mapping
from urllib.parse import urlparse
from aios_task_lineage_supersession import (
DECISION_SCHEMA,
TaskLineageSupersessionController,
TaskLineageSupersessionError,
)
SUPERSESSION_ROUTE = "/v1/control/task-lineage-supersession"
def _status(error: TaskLineageSupersessionError) -> int:
return error.http_status
class LineageSupersessionEndpoint:
"""Framework-neutral endpoint; authentication must precede all parsing/mutation."""
def __init__(
self,
db_path,
verifier,
authenticate_control: Callable[[Mapping[str, str]], Any],
*,
clock=None,
identity_digest=None,
) -> None:
self.authenticate_control = authenticate_control
self.controller = TaskLineageSupersessionController(
db_path, verifier, clock=clock, identity_digest=identity_digest
)
def post(self, headers: Mapping[str, str], body: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
self.authenticate_control(headers)
try:
result = self.controller.supersede(body)
except TaskLineageSupersessionError as exc:
return _status(exc), {"error": exc.code}
return (200 if result.get("idempotent") else 201), result
def build_runtime():
"""Bind to the already-active recovery API and its formal Owner verifier lazily."""
try:
from aios_agent_task_api import APIConfig
from aios_agent_task_api_p0_failed_lease_recovery import (
P0FailedLeaseRecoveryHandler,
P0FailedLeaseRecoveryTaskApplication,
)
from aios_agent_task_protocol import TaskProtocolError
from aios_owner_approval_verifier import (
GrantRootOwnerApprovalVerifier,
OwnerApprovalVerifierError,
)
except ImportError as exc:
raise RuntimeError("LINEAGE_SUPERSESSION_RUNTIME_OVERLAY_UNAVAILABLE") from exc
# The active entrypoint layers failed- and active-lease recovery on top of
# expired recovery. Inherit that current layer so adding lineage does not
# silently remove either already-deployed recovery route.
class Application(P0FailedLeaseRecoveryTaskApplication):
def __init__(self, config: Any) -> None:
super().__init__(config)
try:
verifier = GrantRootOwnerApprovalVerifier()
except OwnerApprovalVerifierError as exc:
raise TaskProtocolError(str(exc), 503) from exc
self.lineage_supersession = LineageSupersessionEndpoint(
config.db_path, verifier, self.authenticate_control
)
def health(self) -> dict[str, Any]:
value = dict(super().health())
value.update({
"task_lineage_supersession": True,
"task_lineage_supersession_schema": DECISION_SCHEMA,
"task_lineage_supersession_exact_scope": True,
"task_lineage_supersession_exact_cas": True,
"task_lineage_supersession_append_only": True,
"task_lineage_supersession_caller_payload": False,
})
return value
class Handler(P0FailedLeaseRecoveryHandler):
app: Application | None = None
def do_POST(self) -> None:
path = urlparse(self.path).path.rstrip("/") or "/"
if path != SUPERSESSION_ROUTE:
super().do_POST()
return
def action() -> None:
if self.app is None:
raise TaskProtocolError("SERVICE_UNAVAILABLE", 503)
status, result = self.app.lineage_supersession.post(
self._headers(), self._body()
)
if "error" in result:
raise TaskProtocolError(str(result["error"]), status)
self._json(result, status)
self._run(action)
return APIConfig, Application, Handler
def serve(config=None) -> None:
APIConfig, Application, Handler = build_runtime()
config = config or APIConfig.from_env()
application = Application(config)
Handler.app = application
ThreadingHTTPServer((config.host, config.port), Handler).serve_forever()
if __name__ == "__main__":
serve()
@@ -389,6 +389,8 @@ class CanonicalBridgeInitialExecutionResolver:
class InitialExecutionProjectionStore:
"""Apply the exact initial projection through one durable CAS transaction."""
allows_supersession_child = False
def __init__(
self,
db_path: Path,
@@ -406,6 +408,32 @@ class InitialExecutionProjectionStore:
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _validate_projection_entrypoint(
self, connection: sqlite3.Connection, task_id: str
) -> None:
"""Fence formal lineage children from the legacy projection entrypoint.
A supersession child carries additional append-only lineage evidence that
must be committed with its initial projection. The legacy endpoint has
no such contract, so it must not be able to project one accidentally.
"""
tables = {
str(row[0])
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
if "task_lineage_supersessions" not in tables:
return
lineage = connection.execute(
"SELECT 1 FROM task_lineage_supersessions WHERE child_task_id=?",
(task_id,),
).fetchone()
if lineage is not None and not self.allows_supersession_child:
raise InitialExecutionProjectionError(
"SUPERSESSION_INITIAL_PROJECTION_ENDPOINT_REQUIRED", 409
)
@staticmethod
def _columns(connection: sqlite3.Connection, table: str) -> dict[str, sqlite3.Row]:
return {
@@ -622,6 +650,7 @@ class InitialExecutionProjectionStore:
).fetchone()
if task is None:
raise InitialExecutionProjectionError("TASK_NOT_FOUND", 404)
self._validate_projection_entrypoint(connection, task_id)
if (
str(task["status"]) != request.expected_status
or int(task["revision"]) != request.expected_revision
@@ -771,6 +800,29 @@ class InitialExecutionProjectionStore:
"trace_id": str(task["task_id"]),
}
def _projection_event_extension(
self,
connection: sqlite3.Connection,
task_id: str,
request: InitialExecutionProjectionRequest,
execution: Mapping[str, Any],
applied_revision: int,
) -> Mapping[str, Any]:
"""Return server-derived fields for a specialized projection event."""
return {}
def _insert_projection_extension(
self,
connection: sqlite3.Connection,
task_id: str,
request: InitialExecutionProjectionRequest,
execution: Mapping[str, Any],
event_payload: Mapping[str, Any],
applied_revision: int,
) -> None:
"""Persist specialized evidence in the caller's projection transaction."""
return None
def project(
self,
task_id: str,
@@ -782,6 +834,7 @@ class InitialExecutionProjectionStore:
request_sha = _sha(request.public())
with self._connect() as connection:
self._validate_projection_entrypoint(connection, task_id)
self._ensure_receipt_table(connection)
replay = connection.execute(
"SELECT * FROM task_initial_execution_projections "
@@ -915,6 +968,20 @@ class InitialExecutionProjectionStore:
"main_write": False,
"production_write": False,
}
extension = dict(
self._projection_event_extension(
connection,
task_id,
request,
execution,
applied_revision,
)
)
if set(extension).intersection(event_payload):
raise InitialExecutionProjectionError(
"INITIAL_EXECUTION_PROJECTION_EVENT_EXTENSION_CONFLICT", 500
)
event_payload.update(extension)
self._insert_event(connection, task_id, event_payload)
connection.execute(
"""
@@ -938,6 +1005,14 @@ class InitialExecutionProjectionStore:
_now(),
),
)
self._insert_projection_extension(
connection,
task_id,
request,
execution,
event_payload,
applied_revision,
)
connection.commit()
return {
"task_id": task_id,
@@ -0,0 +1,2 @@
"""Implementation Baseline 1.1 integrations for existing AIOS authorities."""
BASELINE_VERSION = '1.1'
File diff suppressed because it is too large Load Diff
+281
View File
@@ -0,0 +1,281 @@
"""Executable validation profile and thin Action client.
This supplied ZIP does not establish the online Task authority. Consequently
the runnable profile creates an explicitly isolated validation authority. Online
activation requires the authority mapping gates documented in this package.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import secrets
import re
import signal
import stat
import threading
import time
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey,Ed25519PublicKey
from task_plane.store import TaskPlaneStore
from aios_gitops_job_controller import JobStore
from .authorization import AuthorizationService,sign_owner_delegation_grant
from .host import SystemdHost
from .lineage import LineageLedger
from .operations import OperationRegistry
from .runtime_contract import validate_contract
from .service import BaselineService,Identity
from .worker_policy import WorkerProfile
def _unique_object(pairs):
result={}
for key,value in pairs:
if key in result: raise ValueError('DUPLICATE_JSON_KEY')
result[key]=value
return result
def read_json(path):
return json.loads(_safe_read(Path(path).absolute()).decode('utf-8'),object_pairs_hook=_unique_object,
parse_constant=lambda _value: (_ for _ in ()).throw(ValueError('NON_FINITE_JSON')))
def _path(value):
if not isinstance(value,str): raise ValueError('PATH_INVALID')
prefix='${CREDENTIALS_DIRECTORY}/'
if value.startswith(prefix):
directory=os.environ.get('CREDENTIALS_DIRECTORY')
name=value[len(prefix):]
if not directory or '/' in name or name in ('','.','..'): raise ValueError('CREDENTIAL_PATH_INVALID')
value=str(Path(directory)/name)
if '$' in value or not Path(value).is_absolute() or '..' in Path(value).parts:
raise ValueError('PATH_MUST_BE_EXPLICIT_ABSOLUTE')
return Path(value)
def _safe_read(path,*,private=False):
path=_path(str(path))
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_CLOEXEC)
try:
info=os.fstat(fd)
forbidden=0o077 if private else 0o022
if not stat.S_ISREG(info.st_mode) or info.st_uid not in {0,os.geteuid()} or info.st_mode & forbidden or info.st_size>65536:
raise ValueError('CONFIG_OR_SECRET_FILE_PERMISSIONS_INVALID')
with os.fdopen(fd,'rb',closefd=False) as stream:
data=stream.read(65537)
if len(data)>65536: raise ValueError('CONFIG_OR_SECRET_FILE_TOO_LARGE')
return data
finally: os.close(fd)
def secret(path):
return _safe_read(path,private=True)
def load_configuration(path):
config=read_json(path)
required={'schema','profile','state_dir','listen','machine_key_id','machine_key_file',
'owner_public_key_files','owner_key_scopes','identities','workers','contracts','operations','host_bindings'}
if type(config) is not dict or set(config)-{'receipt_public_key_files'}!=required or config['schema']!='aios.baseline-server-config.v1':
raise ValueError('SERVER_CONFIG_FIELDS_INVALID')
if config['profile']!='isolated-validation': raise ValueError('PROFILE_REQUIRES_VERIFIED_AUTHORITY_ADAPTER')
listen=config['listen']
if type(listen) is not dict or set(listen)!={'host','port'} or listen['host']!='127.0.0.1' or type(listen['port']) is not int or not 1024<=listen['port']<=65535:
raise ValueError('LOOPBACK_LISTENER_REQUIRED')
state=_path(config['state_dir'])
if state.is_symlink() or state==Path('/') or state.name in {'aios','active-runtime','app'}:
raise ValueError('ISOLATED_STATE_PATH_REQUIRED')
if not isinstance(config['machine_key_id'],str) or not config['machine_key_id']: raise ValueError('MACHINE_ID_REQUIRED')
machine=serialization.load_pem_private_key(secret(config['machine_key_file']),password=None)
if not isinstance(machine,Ed25519PrivateKey): raise ValueError('ED25519_MACHINE_KEY_REQUIRED')
keys={}
for key_id,filename in config['owner_public_key_files'].items():
public=serialization.load_pem_public_key(_safe_read(filename))
if not isinstance(public,Ed25519PublicKey): raise ValueError('ED25519_OWNER_KEY_REQUIRED')
keys[key_id]=public
if not keys: raise ValueError('OWNER_TRUST_ROOT_REQUIRED')
receipt_keys={}
for key_id,filename in config.get('receipt_public_key_files',{}).items():
public=serialization.load_pem_public_key(_safe_read(filename))
if not isinstance(public,Ed25519PublicKey): raise ValueError('ED25519_RECEIPT_KEY_REQUIRED')
receipt_keys[key_id]=public
tokens={}
for entry in config['identities']:
if type(entry) is not dict or set(entry)-{'token_file','tenant_id','subject_id','role','worker_id'}:
raise ValueError('IDENTITY_FIELDS_INVALID')
token=secret(entry['token_file']).decode().strip()
if len(token)<32 or len(token)>512 or any(x.isspace() for x in token) or token in tokens: raise ValueError('TOKEN_INVALID_OR_DUPLICATE')
identity=Identity(**{k:v for k,v in entry.items() if k!='token_file'})
if identity.role not in {'user','owner','worker'} or any(not isinstance(v,str) or not v for v in (identity.tenant_id,identity.subject_id)):
raise ValueError('IDENTITY_INVALID')
if (identity.role=='worker') != bool(identity.worker_id): raise ValueError('WORKER_IDENTITY_INVALID')
tokens[token]=identity
workers={}
for entry in config['workers']:
worker=WorkerProfile.from_dict(entry)
if worker.worker_id in workers: raise ValueError('WORKER_DUPLICATE')
workers[worker.worker_id]=worker
for identity in tokens.values():
if identity.role=='worker':
worker=workers.get(identity.worker_id)
if not worker or worker.identity!=identity.subject_id or identity.tenant_id not in worker.tenant_scope:
raise ValueError('WORKER_IDENTITY_SCOPE_MISMATCH')
contracts=[validate_contract(c) for c in config['contracts']]
operations=OperationRegistry(config['operations'])
host=SystemdHost(config['host_bindings'])
config['_resolved']={'state':state,'machine':machine,'owner_keys':keys,'workers':workers,
'contracts':contracts,'operations':operations,'host':host,'tokens':tokens,'receipt_keys':receipt_keys}
return config,tokens
def build_service(config):
resolved=config['_resolved']
state=resolved['state']
marker=state/'ISOLATED_VALIDATION_AUTHORITY'
if state.exists() and not marker.is_file() and any(state.iterdir()):
raise ValueError('EXISTING_STATE_AUTHORITY_NOT_IDENTIFIED')
if marker.exists() and _safe_read(marker)!=b'aios.baseline.isolated-validation.v1\n':
raise ValueError('STATE_AUTHORITY_MARKER_INVALID')
state.mkdir(parents=True,exist_ok=True,mode=0o700)
if state.stat().st_mode & 0o077: raise ValueError('STATE_DIRECTORY_PERMISSIONS_INVALID')
if not marker.exists(): marker.write_text('aios.baseline.isolated-validation.v1\n')
auth=AuthorizationService(state/'authorization.sqlite3',resolved['owner_keys'],resolved['machine'],config['machine_key_id'],
owner_key_scopes=config['owner_key_scopes'],
control_context_verifier=lambda ctx:isinstance(ctx,Identity) and ctx.role=='owner' and any(ctx is known for known in resolved['tokens'].values()))
store=TaskPlaneStore(str(state/'tasks.sqlite3'),high_assurance=True)
return BaselineService(store,state/'jobs',auth,resolved['contracts'],resolved['operations'],resolved['workers'],
resolved['host'],LineageLedger(state/'lineage.sqlite3',store,JobStore(state/'jobs').read),resolved['machine'],
machine_key_id=config['machine_key_id'],trusted_receipt_keys=resolved['receipt_keys'])
def _write_json(path,value,mode=0o600):
data=(json.dumps(value,ensure_ascii=False,sort_keys=True,indent=2,allow_nan=False)+'\n').encode()
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,mode)
with os.fdopen(fd,'wb') as out: out.write(data)
def initialize_validation(directory,*,unit):
"""Create explicit local-only example identities, not production trust roots."""
directory=Path(directory).absolute()
directory.mkdir(mode=0o700) # Never overwrite an existing installation.
# Validate unit syntax before writing any identity material.
SystemdHost({'validation:staging:sample':unit})
owner=Ed25519PrivateKey.generate()
machine=Ed25519PrivateKey.generate()
for name,key in [('owner-private.pem',owner),('machine-private.pem',machine)]:
path=directory/name
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
with os.fdopen(fd,'wb') as out:
out.write(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()))
(directory/'owner-public.pem').write_bytes(owner.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo))
for name in ('user.token','worker.token'):
fd=os.open(directory/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
with os.fdopen(fd,'w') as out: out.write(secrets.token_urlsafe(48)+'\n')
operation={'operation_id':'service-status','version':1,'effect_class':'READ_ONLY','handler_ref':'host.service_status',
'input_schema':'aios.operation.service-status.input.v1','required_permissions':['service-status'],'timeout_seconds':10,
'readback':{'mode':'NONE','required':False},'retry':{'mode':'SAFE','max_attempts':2},'reconcile':{'mode':'NONE'},
'rollback':{'mode':'NONE'},'owner_boundary':{'mode':'WITHIN_GRANT'}}
contract={'contract_schema':'aios.runtime-contract.v1','contract_revision':1,'component_id':'sample','environment':'staging',
'tenant_scope':'validation','lifecycle':'active','entrypoints':{'status':{'component_id':'sample','environment':'staging','resolver_ref':'service://sample/status'}},
'bindings':{},'resource_refs':{},'credential_refs':{},'operations':['service-status']}
config={'schema':'aios.baseline-server-config.v1','profile':'isolated-validation','state_dir':str(directory/'state'),
'listen':{'host':'127.0.0.1','port':8769},'machine_key_id':'validation-machine','machine_key_file':str(directory/'machine-private.pem'),
'owner_public_key_files':{'validation-owner':str(directory/'owner-public.pem')},
'owner_key_scopes':{'validation-owner':{'tenant_ids':['validation'],'subject_ids':['owner']}},
'identities':[{'token_file':str(directory/'user.token'),'tenant_id':'validation','subject_id':'owner','role':'owner'},
{'token_file':str(directory/'worker.token'),'tenant_id':'validation','subject_id':'machine-worker','role':'worker','worker_id':'diagnostic-worker'}],
'workers':[{'worker_id':'diagnostic-worker','identity':'machine-worker','tenant_scope':['validation'],'subject_scope':['owner'],
'capabilities':['service-status'],'priority':10,'fallback_executor':False,'heartbeat_seconds':15,'max_concurrency':1}],
'contracts':[contract],'operations':[operation],'host_bindings':{'validation:staging:sample':unit}}
_write_json(directory/'server.json',config)
_write_json(directory/'worker.json',{'schema':'aios.baseline-worker-config.v1','base_url':'http://127.0.0.1:8769',
'token_file':str(directory/'worker.token'),'poll_seconds':2})
grant=sign_owner_delegation_grant(owner,'validation-owner',grant_id='validation-grant',generation=1,
tenant_id='validation',subject_id='owner',mission_ids=('validation-mission',),task_ids=('*',),operations=('service-status',),
component_ids=('sample',),environments=('staging',),issued_at=int(time.time()),validity_mode='UNTIL_REVOKED',max_effects=20)
_write_json(directory/'grant.json',grant)
_write_json(directory/'goal.json',{'mission_id':'validation-mission','grant_id':'validation-grant','operation':'service-status',
'component_id':'sample','environment':'staging','inputs':{},'intent':'检查已绑定 staging 服务并生成诊断结果'})
return directory
def main(argv=None):
parser=argparse.ArgumentParser(description=__doc__)
sub=parser.add_subparsers(dest='command',required=True)
init=sub.add_parser('init-validation');init.add_argument('--directory',required=True);init.add_argument('--service-unit',required=True)
for name in ('preflight','server','grant-import','grant-revoke'):
item=sub.add_parser(name);item.add_argument('--config',required=True)
if name=='grant-import': item.add_argument('--grant',required=True)
if name=='grant-revoke':
item.add_argument('--grant',required=True);item.add_argument('--owner-token-file',required=True)
worker=sub.add_parser('worker');worker.add_argument('--config',required=True);worker.add_argument('--once',action='store_true')
for name in ('goal-submit','goal-explain','task-get','task-cancel'):
item=sub.add_parser(name);item.add_argument('--base-url',default='http://127.0.0.1:8769');item.add_argument('--token-file',required=True)
if name=='goal-submit': item.add_argument('--request',required=True);item.add_argument('--idempotency-key',required=True)
elif name=='goal-explain': item.add_argument('--mission-id',required=True)
else: item.add_argument('--task-id',required=True)
args=parser.parse_args(argv)
try:
if args.command=='init-validation':
print(json.dumps({'profile':'isolated-validation','directory':str(initialize_validation(args.directory,unit=args.service_unit))}));return 0
if args.command in {'preflight','server','grant-import','grant-revoke'}:
config,tokens=load_configuration(args.config)
if args.command=='preflight':
print(json.dumps({'status':'CONFIG_VALID','profile':config['profile'],'online_authority':'NOT_VERIFIED',
'production_activation':'BLOCKED','registered_operations':len(config['operations'])}));return 0
service=build_service(config)
if args.command=='grant-import':
grant=service.authorization.register_grant(read_json(args.grant))
print(json.dumps({'grant_id':grant['payload']['grant_id'],'registered':True}));return 0
if args.command=='grant-revoke':
context=tokens.get(secret(args.owner_token_file).decode().strip())
grant=read_json(args.grant)
payload=grant['payload']
if context is None or context.role!='owner' or payload['tenant_id']!=context.tenant_id or payload['subject_id']!=context.subject_id:
raise ValueError('OWNER_SCOPE_DENIED')
# Register verifies signature and ID before a target can be revoked.
service.authorization.register_grant(grant)
generation=service.authorization.revoke_grant(payload['grant_id'],control_context=context)
print(json.dumps({'grant_id':payload['grant_id'],'revocation_generation':generation}));return 0
from .transport import serve
server=serve(service,tokens,**config['listen'])
stop=threading.Event()
def recover_loop():
while not stop.wait(5):
try: service.recover()
except Exception: service.storage_blocked=True
recovery=threading.Thread(target=recover_loop,daemon=True);recovery.start()
def shutdown(_signal,_frame):
stop.set();threading.Thread(target=server.shutdown,daemon=True).start()
signal.signal(signal.SIGTERM,shutdown);signal.signal(signal.SIGINT,shutdown)
print(json.dumps({'status':'LISTENING','host':config['listen']['host'],'port':config['listen']['port'],'profile':config['profile']}),flush=True)
try: server.serve_forever(poll_interval=0.5)
finally: stop.set();server.server_close()
return 0
from .transport import HttpTransport
if args.command=='worker':
from .worker import OperationWorker
config=read_json(args.config)
if type(config) is not dict or set(config)!={'schema','base_url','token_file','poll_seconds'} or config['schema']!='aios.baseline-worker-config.v1': raise ValueError('WORKER_CONFIG_INVALID')
transport=HttpTransport(config['base_url'],secret(config['token_file']).decode().strip(),timeout_seconds=20)
worker=OperationWorker(transport,poll_seconds=config['poll_seconds'])
if args.once: print(json.dumps(worker.run_once(),ensure_ascii=False));return 0
worker.run_forever();return 0
transport=HttpTransport(args.base_url,secret(args.token_file).decode().strip(),timeout_seconds=20)
if args.command=='goal-submit': result=transport.submit_goal(read_json(args.request),idempotency_key=args.idempotency_key)
elif args.command=='goal-explain': result=transport.explain(args.mission_id)
elif args.command=='task-get': result=transport.get_task(args.task_id)
else: result=transport.cancel_task(args.task_id)
print(json.dumps(result,ensure_ascii=False,indent=2,allow_nan=False));return 0
except (Exception,KeyboardInterrupt) as exc:
# Avoid dumping request bodies, credential material or Python tracebacks.
text=str(exc)
code=text if re.fullmatch(r'[A-Z][A-Z0-9_]{2,100}',text) else getattr(exc,'code',type(exc).__name__)
print(json.dumps({'status':'ERROR','error_type':type(exc).__name__,'code':code,'message':'See local configuration and acceptance runbook; no action was reported successful.'}))
return 1
if __name__=='__main__':
raise SystemExit(main())
@@ -0,0 +1,39 @@
"""Canonical, domain-separated machine receipts, distinct from Owner grants."""
import base64
import hashlib
import json
from cryptography.exceptions import InvalidSignature
def canonical(value):
return json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False,allow_nan=False).encode()
def digest(value):
return hashlib.sha256(canonical(value)).hexdigest()
class ReceiptSigner:
def __init__(self, private_key, *, key_id='baseline-machine', trusted_public_keys=None):
self.private_key=private_key
self.public_key=private_key.public_key()
self.key_id=key_id
self.trusted_public_keys=dict(trusted_public_keys or {})
self.trusted_public_keys[key_id]=self.public_key
def sign(self,payload):
raw=canonical({'domain':'aios.operation-receipt.v1','key_id':self.key_id,'payload':payload})
return {'schema':'aios.operation-receipt.v1','key_id':self.key_id,'payload':payload,
'signature':base64.b64encode(self.private_key.sign(raw)).decode()}
def verify(self,envelope):
try:
if type(envelope) is not dict or set(envelope)!={'schema','key_id','payload','signature'} or envelope['schema']!='aios.operation-receipt.v1' or type(envelope['payload']) is not dict:
return False
public=self.trusted_public_keys.get(envelope['key_id'])
if public is None: return False
raw=canonical({'domain':envelope['schema'],'key_id':envelope['key_id'],'payload':envelope['payload']})
public.verify(base64.b64decode(envelope['signature'],validate=True),raw)
return True
except (ValueError,TypeError,InvalidSignature,KeyError):
return False
+103
View File
@@ -0,0 +1,103 @@
"""Small registered host diagnostics; unit bindings come from trusted configuration."""
from __future__ import annotations
import os
from pathlib import Path
import re
import signal
import selectors
import stat
import subprocess
import time
class HostError(RuntimeError):
pass
class SystemdHost:
PROPERTIES = ('Id', 'ActiveState', 'SubState', 'MainPID', 'InvocationID')
def __init__(self, bindings: dict[str, str], *, executable: str = '/usr/bin/systemctl',
timeout_seconds: float = 10):
if type(timeout_seconds) not in (int,float) or not 0 < timeout_seconds <= 60:
raise HostError('HOST_TIMEOUT_INVALID')
self.bindings = dict(bindings)
for binding,unit in self.bindings.items():
if not isinstance(binding,str) or len(binding.split(':'))!=3 or any(not re.fullmatch(r'[A-Za-z0-9_.-]+',part) for part in binding.split(':')):
raise HostError('HOST_TENANT_BINDING_INVALID')
if not isinstance(unit, str) or not re.fullmatch(r'[A-Za-z0-9_.@:-]+\.service', unit):
raise HostError('HOST_UNIT_INVALID')
if not Path(executable).is_absolute() or not Path(executable).is_file() or Path(executable).is_symlink():
raise HostError('HOST_EXECUTABLE_UNAVAILABLE')
info=Path(executable).stat()
if info.st_uid not in {0,os.geteuid()} or info.st_mode & 0o022:
raise HostError('HOST_EXECUTABLE_UNTRUSTED')
self._executable_identity=(info.st_dev,info.st_ino,info.st_size,info.st_mtime_ns)
self.executable = executable
self.timeout_seconds = timeout_seconds
def status(self, component_id: str, environment: str, *, tenant_id: str, deadline=None) -> dict:
if any(not isinstance(part,str) or not re.fullmatch(r'[A-Za-z0-9_.-]+',part) for part in (tenant_id,environment,component_id)):
raise HostError('HOST_SCOPE_INVALID')
unit = self.bindings.get(f'{tenant_id}:{environment}:{component_id}')
if not unit:
raise HostError('HOST_BINDING_NOT_FOUND')
remaining=self.timeout_seconds if deadline is None else min(self.timeout_seconds,deadline-time.time())
if remaining<=0: raise HostError('HOST_OPERATION_TIMEOUT')
stop_at=time.monotonic()+remaining
executable_fd=os.open(self.executable,os.O_RDONLY|os.O_NOFOLLOW|os.O_CLOEXEC)
try:
info=os.fstat(executable_fd)
if not stat.S_ISREG(info.st_mode) or (info.st_dev,info.st_ino,info.st_size,info.st_mtime_ns)!=self._executable_identity:
raise HostError('HOST_EXECUTABLE_IDENTITY_CHANGED')
executable_path=f'/proc/self/fd/{executable_fd}'
if not Path(executable_path).exists(): raise HostError('HOST_EXECUTOR_PLATFORM_UNSUPPORTED')
argv=[executable_path,'show','--no-pager','--property='+','.join(self.PROPERTIES),'--',unit]
process=subprocess.Popen(argv,stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE,
env={'PATH':'/usr/bin:/bin','LANG':'C'},start_new_session=True,pass_fds=(executable_fd,))
finally: os.close(executable_fd)
output=bytearray()
byte_count=0
try:
with selectors.DefaultSelector() as selector:
selector.register(process.stdout,selectors.EVENT_READ,'stdout')
selector.register(process.stderr,selectors.EVENT_READ,'stderr')
while selector.get_map():
remaining=stop_at-time.monotonic()
if remaining<=0: raise HostError('HOST_OPERATION_TIMEOUT')
for event,_mask in selector.select(remaining):
chunk=os.read(event.fileobj.fileno(),4096)
if not chunk:
selector.unregister(event.fileobj)
continue
byte_count+=len(chunk)
if byte_count>65536: raise HostError('HOST_RESPONSE_TOO_LARGE')
if event.data=='stdout': output.extend(chunk)
process.wait(timeout=max(0.001,stop_at-time.monotonic()))
except (HostError,subprocess.TimeoutExpired) as exc:
try: os.killpg(process.pid,signal.SIGKILL)
except ProcessLookupError: pass
process.wait()
if isinstance(exc,HostError): raise
raise HostError('HOST_OPERATION_TIMEOUT') from exc
finally:
process.stdout.close()
process.stderr.close()
if process.returncode != 0:
raise HostError('HOST_STATUS_UNAVAILABLE')
try: out=output.decode('utf-8')
except UnicodeDecodeError as exc: raise HostError('HOST_OBSERVATION_INVALID') from exc
properties = {}
for line in out.splitlines():
key, sep, value = line.partition('=')
if sep and key in self.PROPERTIES:
if key in properties:
raise HostError('HOST_DUPLICATE_PROPERTY')
properties[key] = value
if set(properties) != set(self.PROPERTIES) or properties['Id'] != unit:
raise HostError('HOST_OBSERVATION_INCOMPLETE')
if not properties['MainPID'].isdigit():
raise HostError('HOST_OBSERVATION_INVALID')
return {'operation':'service-status', 'tenant_id':tenant_id, 'component_id':component_id, 'environment':environment,
'unit':unit, 'healthy':properties['ActiveState'] == 'active',
'properties':properties, 'observed_at':time.time(), 'source':'systemd.show'}
+650
View File
@@ -0,0 +1,650 @@
"""Persistent correlation relationships for the AIOS baseline.
This is deliberately a relationship authority. It does not create or advance
Tasks, Jobs, Releases, or Deployments. In particular, a Task is admitted only
after the configured Task authority returns the real Task and its authenticated
tenant/subject binding has been checked.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, is_dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
import sqlite3
from typing import Any, Callable, Mapping
class LineageError(RuntimeError):
"""Base class for persistent lineage failures."""
class LineageConflict(LineageError):
"""An opaque identity was already bound to different lineage."""
class LineageVerificationError(LineageError):
"""An external authority did not verify the requested relationship."""
@dataclass(frozen=True)
class VerifiedAcceptanceEvidence:
"""Typed output of a configured acceptance evidence verifier."""
acceptance_id: str
tenant_id: str
subject_id: str
component_id: str
deployment_id: str
release_id: str
artifact: Any
accepted: bool
evidence_id: str
observed: Mapping[str, Any] | None = None
observed_at: str | None = None
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _required(value: Any, name: str) -> str:
text = str(value or "")
if not text or len(text) > 512 or any(ord(char) < 32 for char in text):
raise ValueError(f"invalid {name}")
return text
def _json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
_SELF_HASH_FIELDS = {
"manifest_sha256",
"manifest_hash",
"outer_manifest_hash",
"outer_manifest_sha256",
"self_hash",
"self_sha256",
}
def canonical_artifact(artifact: Any) -> str:
"""Return an exact artifact identity without permitting outer self-hashes."""
if not isinstance(artifact, (str, Mapping)):
raise ValueError("artifact must be a string or object")
if isinstance(artifact, str):
_required(artifact, "artifact")
else:
if not artifact:
raise ValueError("artifact must not be empty")
if any(not isinstance(key,str) for key in artifact):
raise ValueError("artifact object keys must be strings")
if _SELF_HASH_FIELDS.intersection(key.lower() for key in artifact):
raise ValueError("outer artifact manifest must not contain a self hash")
try:
return _json(artifact)
except (TypeError, ValueError) as exc:
raise ValueError("artifact must be canonical JSON") from exc
def decode_artifact(value: str) -> Any:
return json.loads(value)
def _mapping(value: Any) -> Mapping[str, Any]:
if isinstance(value, Mapping):
return value
if is_dataclass(value):
return asdict(value)
converter = getattr(value, "to_dict", None)
if not callable(converter):
converter = getattr(value, "as_dict", None)
if callable(converter):
try:
converted = converter(redact_payload=False)
except TypeError:
converted = converter()
if isinstance(converted, Mapping):
return converted
raise LineageVerificationError("task_authority returned an invalid Task")
def _task_plan(task: Any) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
raw = _mapping(task)
plan: Any = raw.get("plan") or raw.get("task_plan")
if plan is None and hasattr(task, "task_plan"):
plan = getattr(task, "task_plan")
if plan is None:
plan = raw
plan_map = _mapping(plan)
return raw, plan_map
class LineageLedger:
"""SQLite-backed authority for cross-domain identity relationships."""
def __init__(self, path: str | Path, task_authority: Any, job_authority: Any):
if task_authority is None:
raise ValueError("task_authority is required")
if job_authority is None:
raise ValueError("job_authority is required")
self.path = str(path)
self.task_authority = task_authority
self.job_authority = job_authority
Path(self.path).parent.mkdir(parents=True, exist_ok=True)
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.execute("PRAGMA journal_mode=WAL")
tables={row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if "lineage_tasks" in tables and "lineage_schema_meta" not in tables:
raise LineageConflict("unversioned candidate lineage schema requires explicit migration")
if "lineage_schema_meta" in tables:
version=connection.execute("SELECT version FROM lineage_schema_meta WHERE domain='lineage'").fetchone()
if version is None or version[0] != 2:
raise LineageConflict("unsupported lineage schema version")
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS lineage_schema_meta (
domain TEXT PRIMARY KEY,
version INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS lineage_tasks (
tenant_id TEXT NOT NULL,
task_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
source_task_id TEXT,
metadata_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, task_id),
UNIQUE (tenant_id, task_id, subject_id, mission_id, component_id, grant_id),
FOREIGN KEY (tenant_id, source_task_id)
REFERENCES lineage_tasks(tenant_id, task_id)
);
CREATE TABLE IF NOT EXISTS lineage_effects (
tenant_id TEXT NOT NULL,
effect_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
task_id TEXT NOT NULL,
job_id TEXT NOT NULL,
source_effect_id TEXT,
metadata_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, effect_id),
UNIQUE (tenant_id, job_id),
UNIQUE (tenant_id, effect_id, subject_id, mission_id, component_id),
UNIQUE (tenant_id, effect_id, subject_id, mission_id, component_id, grant_id),
FOREIGN KEY (tenant_id, task_id, subject_id, mission_id, component_id, grant_id)
REFERENCES lineage_tasks(tenant_id, task_id, subject_id, mission_id, component_id, grant_id),
FOREIGN KEY (tenant_id, source_effect_id, subject_id, mission_id, component_id)
REFERENCES lineage_effects(tenant_id, effect_id, subject_id, mission_id, component_id)
);
CREATE TABLE IF NOT EXISTS lineage_releases (
tenant_id TEXT NOT NULL,
release_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
effect_id TEXT NOT NULL,
source_task_id TEXT NOT NULL,
source_effect_id TEXT NOT NULL,
artifact_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, release_id),
UNIQUE (tenant_id, effect_id),
UNIQUE (tenant_id, release_id, subject_id, mission_id, component_id, artifact_json),
FOREIGN KEY (tenant_id, effect_id, subject_id, mission_id, component_id, grant_id)
REFERENCES lineage_effects(tenant_id, effect_id, subject_id, mission_id, component_id, grant_id),
FOREIGN KEY (tenant_id, source_task_id, subject_id, mission_id, component_id, grant_id)
REFERENCES lineage_tasks(tenant_id, task_id, subject_id, mission_id, component_id, grant_id)
);
CREATE TABLE IF NOT EXISTS lineage_deployments (
tenant_id TEXT NOT NULL,
deployment_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
release_id TEXT NOT NULL,
environment TEXT NOT NULL,
target_revision INTEGER NOT NULL,
artifact_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, deployment_id),
UNIQUE (tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json),
FOREIGN KEY (tenant_id, release_id, subject_id, mission_id, component_id, artifact_json)
REFERENCES lineage_releases(tenant_id, release_id, subject_id, mission_id, component_id, artifact_json)
);
CREATE TABLE IF NOT EXISTS lineage_acceptances (
tenant_id TEXT NOT NULL,
acceptance_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
deployment_id TEXT NOT NULL,
release_id TEXT NOT NULL,
artifact_json TEXT NOT NULL,
accepted INTEGER NOT NULL CHECK (accepted IN (0,1)),
evidence_id TEXT NOT NULL,
evidence_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, acceptance_id),
FOREIGN KEY (tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json)
REFERENCES lineage_deployments(
tenant_id, deployment_id, subject_id, mission_id, component_id, release_id, artifact_json
)
);
"""
)
connection.execute("INSERT OR IGNORE INTO lineage_schema_meta(domain,version) VALUES('lineage',2)")
def close(self) -> None:
"""Compatibility hook; connections are transaction-scoped."""
def _authorized_task(self, tenant_id: str, subject_id: str, task_id: str) -> Any:
authority = self.task_authority
try:
if hasattr(authority, "get_task"):
task = authority.get_task(task_id)
elif callable(authority):
try:
task = authority(tenant_id, subject_id, task_id)
except TypeError:
task = authority(task_id)
else:
raise TypeError("authority has no task lookup")
except Exception as exc:
raise LineageVerificationError("task_authority did not confirm task") from exc
raw, plan = _task_plan(task)
returned_id = raw.get("task_id") or plan.get("task_id") or getattr(task, "task_id", None)
if str(returned_id or "") != task_id:
raise LineageVerificationError("task_authority returned wrong task_id")
issuer = plan.get("issuer") if isinstance(plan.get("issuer"), Mapping) else {}
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
tenant_values = [value for value in (issuer.get("tenant_id"), metadata.get("tenant_id"), baseline.get("tenant_id")) if value is not None]
subject_values = [
value for value in (issuer.get("subject_id"), metadata.get("subject_id"), baseline.get("subject_id")) if value is not None
]
# issuer.id is accepted as subject identity only when no explicit subject_id exists.
if not subject_values and issuer.get("id") is not None:
subject_values.append(issuer["id"])
if not tenant_values or any(str(value) != tenant_id for value in tenant_values):
raise LineageVerificationError("task_authority tenant mismatch")
if not subject_values or any(str(value) != subject_id for value in subject_values):
raise LineageVerificationError("task_authority subject mismatch")
return task
def _authorized_job(
self, tenant_id: str, subject_id: str, mission_id: str, grant_id: str,
task_id: str, effect_id: str, job_id: str,
) -> None:
authority=self.job_authority
try:
if hasattr(authority,"get_job"):
job=authority.get_job(job_id)
elif hasattr(authority,"read"):
job=authority.read(job_id)
elif callable(authority):
job=authority(job_id)
else:
raise TypeError("authority has no Job lookup")
except Exception as exc:
raise LineageVerificationError("job_authority did not confirm Job") from exc
raw=_mapping(job)
request=raw.get("request") or raw.get("binding")
if request is None and isinstance(raw.get("request_json"),str):
try:
request=json.loads(raw["request_json"])
except json.JSONDecodeError as exc:
raise LineageVerificationError("job_authority returned invalid request") from exc
if not isinstance(request,Mapping):
request=raw
if isinstance(request.get("binding"),Mapping):
request=request["binding"]
bindings={
"job_id":raw.get("job_id") or request.get("job_id"),
"tenant_id":request.get("tenant_id"),"subject_id":request.get("subject_id"),
"mission_id":request.get("mission_id"),"grant_id":request.get("grant_id"),
"task_id":request.get("task_id"),"effect_id":request.get("effect_id"),
}
expected={"job_id":job_id,"tenant_id":tenant_id,"subject_id":subject_id,
"mission_id":mission_id,"grant_id":grant_id,"task_id":task_id,"effect_id":effect_id}
if any(str(bindings[key] or "") != value for key,value in expected.items()):
raise LineageVerificationError("job_authority binding mismatch")
@staticmethod
def _check_task_context(task: Any, mission_id: str, grant_id: str) -> None:
_, plan = _task_plan(task)
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
mission_values = [value for value in (metadata.get("mission_id"), baseline.get("mission_id")) if value is not None]
grant_values = [value for value in (metadata.get("grant_id"), baseline.get("grant_id")) if value is not None]
if any(str(value) != mission_id for value in mission_values):
raise LineageVerificationError("task_authority mission mismatch")
if any(str(value) != grant_id for value in grant_values):
raise LineageVerificationError("task_authority grant mismatch")
@staticmethod
def _task_component(task: Any, requested: str | None) -> str:
_, plan = _task_plan(task)
metadata = plan.get("metadata") if isinstance(plan.get("metadata"), Mapping) else {}
baseline = metadata.get("baseline") if isinstance(metadata.get("baseline"), Mapping) else {}
values=[value for value in (metadata.get("component_id"),baseline.get("component_id")) if value is not None]
if requested is not None:
requested=_required(requested,"component_id")
if any(str(value) != requested for value in values):
raise LineageVerificationError("task_authority component mismatch")
return requested
if not values or any(str(value) != str(values[0]) for value in values):
raise LineageVerificationError("task_authority component missing or inconsistent")
return _required(values[0],"component_id")
def link_task(
self,
tenant_id: str,
subject_id: str,
mission_id: str,
grant_id: str,
task_id: str,
*,
component_id: str | None = None,
source_task_id: str | None = None,
metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
tenant_id, subject_id, mission_id, grant_id, task_id = (
_required(tenant_id, "tenant_id"), _required(subject_id, "subject_id"),
_required(mission_id, "mission_id"), _required(grant_id, "grant_id"),
_required(task_id, "task_id"),
)
task = self._authorized_task(tenant_id, subject_id, task_id)
self._check_task_context(task, mission_id, grant_id)
component_id=self._task_component(task,component_id)
source_task_id = _required(source_task_id, "source_task_id") if source_task_id else None
raw_task, task_plan = _task_plan(task)
authoritative_source = task_plan.get("parent_task_id") or raw_task.get("parent_task_id")
if authoritative_source is not None:
authoritative_source = _required(authoritative_source, "authoritative source_task_id")
if source_task_id is not None and source_task_id != authoritative_source:
raise LineageVerificationError("source_task_id does not match Task authority")
source_task_id = authoritative_source
metadata_json = _json(dict(metadata or {}))
values = (subject_id, mission_id, component_id, grant_id, source_task_id)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"SELECT * FROM lineage_tasks WHERE tenant_id=? AND task_id=?", (tenant_id, task_id)
).fetchone()
if existing:
if tuple(existing[key] for key in ("subject_id", "mission_id", "component_id", "grant_id", "source_task_id")) != values:
raise LineageConflict("task_id already has different lineage")
return self._task_row(existing)
if source_task_id:
self._require_row(connection, "lineage_tasks", "task_id", tenant_id, source_task_id, subject_id, mission_id, component_id)
connection.execute(
"INSERT INTO lineage_tasks VALUES(?,?,?,?,?,?,?,?,?)",
(tenant_id, task_id, subject_id, mission_id, component_id, grant_id, source_task_id, metadata_json, _now()),
)
row = connection.execute(
"SELECT * FROM lineage_tasks WHERE tenant_id=? AND task_id=?", (tenant_id, task_id)
).fetchone()
return self._task_row(row)
@staticmethod
def _require_row(connection, table, id_column, tenant_id, object_id, subject_id, mission_id, component_id=None):
row = connection.execute(
f"SELECT * FROM {table} WHERE tenant_id=? AND {id_column}=?", (tenant_id, object_id)
).fetchone()
if row is None:
raise LineageConflict(f"{id_column} has no linked authority record")
if row["subject_id"] != subject_id or row["mission_id"] != mission_id:
raise LineageConflict(f"{id_column} belongs to different subject or mission")
if component_id is not None and row["component_id"] != component_id:
raise LineageConflict(f"{id_column} belongs to different component")
return row
def link_effect(
self, tenant_id: str, subject_id: str, mission_id: str, task_id: str,
effect_id: str, job_id: str, *, source_effect_id: str | None = None,
metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
tenant_id, subject_id, mission_id, task_id, effect_id, job_id = map(
lambda pair: _required(pair[0], pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
(task_id,"task_id"),(effect_id,"effect_id"),(job_id,"job_id")),
)
source_effect_id = _required(source_effect_id, "source_effect_id") if source_effect_id else None
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
task=self._require_row(connection, "lineage_tasks", "task_id", tenant_id, task_id, subject_id, mission_id)
component_id=task["component_id"]
grant_id=task["grant_id"]
self._authorized_job(tenant_id,subject_id,mission_id,grant_id,task_id,effect_id,job_id)
existing = connection.execute(
"SELECT * FROM lineage_effects WHERE tenant_id=? AND effect_id=?", (tenant_id, effect_id)
).fetchone()
expected = (subject_id, mission_id, component_id, grant_id, task_id, job_id, source_effect_id)
if existing:
actual = tuple(existing[key] for key in ("subject_id","mission_id","component_id","grant_id","task_id","job_id","source_effect_id"))
if actual != expected:
raise LineageConflict("effect_id already has different task/job identity")
return self._effect_row(existing)
job = connection.execute(
"SELECT effect_id,task_id,subject_id FROM lineage_effects WHERE tenant_id=? AND job_id=?",
(tenant_id, job_id),
).fetchone()
if job:
raise LineageConflict("job_id already belongs to exactly one Task/effect")
if source_effect_id:
self._require_row(connection, "lineage_effects", "effect_id", tenant_id, source_effect_id, subject_id, mission_id, component_id)
connection.execute(
"INSERT INTO lineage_effects VALUES(?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,effect_id,subject_id,mission_id,component_id,grant_id,task_id,job_id,source_effect_id,_json(dict(metadata or {})),_now()),
)
row = connection.execute("SELECT * FROM lineage_effects WHERE tenant_id=? AND effect_id=?",(tenant_id,effect_id)).fetchone()
return self._effect_row(row)
def link_release(
self, tenant_id: str, subject_id: str, mission_id: str, effect_id: str,
release_id: str, artifact: Any, *, metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
tenant_id, subject_id, mission_id, effect_id, release_id = map(
lambda pair: _required(pair[0],pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
(effect_id,"effect_id"),(release_id,"release_id")),
)
artifact_json = canonical_artifact(artifact)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
effect = self._require_row(connection,"lineage_effects","effect_id",tenant_id,effect_id,subject_id,mission_id)
existing = connection.execute("SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=?",(tenant_id,release_id)).fetchone()
expected = (subject_id,mission_id,effect["component_id"],effect["grant_id"],effect_id,effect["task_id"],effect_id,artifact_json)
if existing:
actual = tuple(existing[key] for key in ("subject_id","mission_id","component_id","grant_id","effect_id","source_task_id","source_effect_id","artifact_json"))
if actual != expected:
raise LineageConflict("release_id already has different source lineage")
return self._release_row(existing)
producer = connection.execute("SELECT release_id FROM lineage_releases WHERE tenant_id=? AND effect_id=?",(tenant_id,effect_id)).fetchone()
if producer:
raise LineageConflict("release-producing effect already points to a unique release")
connection.execute(
"INSERT INTO lineage_releases VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,release_id,subject_id,mission_id,effect["component_id"],effect["grant_id"],effect_id,effect["task_id"],effect_id,artifact_json,_json(dict(metadata or {})),_now()),
)
row=connection.execute("SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=?",(tenant_id,release_id)).fetchone()
return self._release_row(row)
def link_deployment(
self, tenant_id: str, subject_id: str, mission_id: str, release_id: str,
deployment_id: str, component_id: str, environment: str, target_revision: int, artifact: Any,
*, metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
tenant_id,subject_id,mission_id,release_id,deployment_id,component_id,environment=map(
lambda pair:_required(pair[0],pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),
(release_id,"release_id"),(deployment_id,"deployment_id"),(component_id,"component_id"),(environment,"environment")),
)
if not isinstance(target_revision,int) or isinstance(target_revision,bool) or target_revision < 1:
raise ValueError("invalid target_revision")
artifact_json=canonical_artifact(artifact)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
release=self._require_row(connection,"lineage_releases","release_id",tenant_id,release_id,subject_id,mission_id,component_id)
if release["artifact_json"] != artifact_json:
raise LineageConflict("deployment artifact does not match release")
existing=connection.execute("SELECT * FROM lineage_deployments WHERE tenant_id=? AND deployment_id=?",(tenant_id,deployment_id)).fetchone()
expected=(subject_id,mission_id,component_id,release_id,environment,target_revision,artifact_json)
if existing:
actual=tuple(existing[key] for key in ("subject_id","mission_id","component_id","release_id","environment","target_revision","artifact_json"))
if actual != expected:
raise LineageConflict("deployment_id already has different release identity")
return self._deployment_row(existing)
connection.execute(
"INSERT INTO lineage_deployments VALUES(?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,deployment_id,subject_id,mission_id,component_id,release_id,environment,target_revision,artifact_json,_json(dict(metadata or {})),_now()),
)
row=connection.execute("SELECT * FROM lineage_deployments WHERE tenant_id=? AND deployment_id=?",(tenant_id,deployment_id)).fetchone()
return self._deployment_row(row)
def record_acceptance(
self, tenant_id: str, subject_id: str, mission_id: str, component_id: str, acceptance_id: str,
deployment_id: str, release_id: str, artifact: Any, accepted: bool, *,
evidence: Any, verifier: Callable[[Any, Mapping[str, Any]], VerifiedAcceptanceEvidence],
) -> dict[str, Any]:
if not callable(verifier):
raise LineageVerificationError("acceptance verifier is required")
tenant_id,subject_id,mission_id,component_id,acceptance_id,deployment_id,release_id=map(
lambda pair:_required(pair[0],pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),(component_id,"component_id"),
(acceptance_id,"acceptance_id"),(deployment_id,"deployment_id"),(release_id,"release_id")),
)
if type(accepted) is not bool:
raise ValueError("accepted must be boolean")
artifact_json=canonical_artifact(artifact)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
deployment=self._require_row(connection,"lineage_deployments","deployment_id",tenant_id,deployment_id,subject_id,mission_id,component_id)
if deployment["release_id"] != release_id:
raise LineageConflict("acceptance release_id does not match deployment")
if deployment["artifact_json"] != artifact_json:
raise LineageConflict("acceptance artifact does not match deployment")
expected={"acceptance_id":acceptance_id,"tenant_id":tenant_id,"subject_id":subject_id,"component_id":component_id,
"deployment_id":deployment_id,"release_id":release_id,"artifact":decode_artifact(artifact_json),
"accepted":accepted}
try:
verified=verifier(evidence,expected)
except Exception as exc:
raise LineageVerificationError("acceptance evidence was not verified") from exc
if not isinstance(verified,VerifiedAcceptanceEvidence):
raise LineageVerificationError("verifier must return VerifiedAcceptanceEvidence")
bindings=(verified.acceptance_id,verified.tenant_id,verified.subject_id,verified.component_id,verified.deployment_id,verified.release_id,canonical_artifact(verified.artifact),verified.accepted)
if bindings != (acceptance_id,tenant_id,subject_id,component_id,deployment_id,release_id,artifact_json,accepted):
raise LineageVerificationError("verified acceptance evidence binding mismatch")
evidence_id=_required(verified.evidence_id,"evidence_id")
evidence_json=_json({
"evidence_id":evidence_id,
"observed":dict(verified.observed or {}),
"observed_at":verified.observed_at,
"signed_envelope":dict(evidence) if isinstance(evidence,Mapping) else None,
})
existing=connection.execute("SELECT * FROM lineage_acceptances WHERE tenant_id=? AND acceptance_id=?",(tenant_id,acceptance_id)).fetchone()
expected_row=(subject_id,mission_id,component_id,deployment_id,release_id,artifact_json,int(accepted),evidence_id,evidence_json)
if existing:
actual=tuple(existing[key] for key in ("subject_id","mission_id","component_id","deployment_id","release_id","artifact_json","accepted","evidence_id","evidence_json"))
if actual != expected_row:
raise LineageConflict("acceptance_id already has different binding")
return self._acceptance_row(existing)
connection.execute(
"INSERT INTO lineage_acceptances VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,acceptance_id,subject_id,mission_id,component_id,deployment_id,release_id,artifact_json,int(accepted),evidence_id,evidence_json,_now()),
)
row=connection.execute("SELECT * FROM lineage_acceptances WHERE tenant_id=? AND acceptance_id=?",(tenant_id,acceptance_id)).fetchone()
return self._acceptance_row(row)
def explain(self, tenant_id: str, subject_id: str, mission_id: str) -> dict[str, Any]:
tenant_id,subject_id,mission_id=(_required(tenant_id,"tenant_id"),_required(subject_id,"subject_id"),_required(mission_id,"mission_id"))
specs=(("tasks","lineage_tasks",self._task_row),("effects","lineage_effects",self._effect_row),
("releases","lineage_releases",self._release_row),("deployments","lineage_deployments",self._deployment_row),
("acceptances","lineage_acceptances",self._acceptance_row))
result={"tenant_id":tenant_id,"subject_id":subject_id,"mission_id":mission_id}
with self._connect() as connection:
for key,table,converter in specs:
rows=connection.execute(f"SELECT * FROM {table} WHERE tenant_id=? AND subject_id=? AND mission_id=? ORDER BY created_at",(tenant_id,subject_id,mission_id)).fetchall()
result[key]=[converter(row) for row in rows]
return result
def get_release(self, tenant_id: str, subject_id: str, release_id: str) -> dict[str, Any]:
"""Resolve an exact tenant/subject release without disclosing other scopes."""
tenant_id, subject_id, release_id = (
_required(tenant_id, "tenant_id"), _required(subject_id, "subject_id"),
_required(release_id, "release_id"),
)
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=? AND subject_id=?",
(tenant_id, release_id, subject_id),
).fetchone()
if row is None:
raise KeyError("release lineage not found")
return self._release_row(row)
def get_release_for_component(self, tenant_id: str, component_id: str, release_id: str) -> dict[str, Any]:
tenant_id,component_id,release_id=(_required(tenant_id,"tenant_id"),_required(component_id,"component_id"),_required(release_id,"release_id"))
with self._connect() as connection:
row=connection.execute(
"SELECT * FROM lineage_releases WHERE tenant_id=? AND release_id=? AND component_id=?",
(tenant_id,release_id,component_id),
).fetchone()
if row is None:
raise KeyError("release component lineage not found")
return self._release_row(row)
@staticmethod
def _common(row):
return {key:row[key] for key in row.keys() if not key.endswith("_json")}
@classmethod
def _task_row(cls,row):
value=cls._common(row); value["metadata"]=json.loads(row["metadata_json"]); return value
@classmethod
def _effect_row(cls,row):
value=cls._common(row); value["metadata"]=json.loads(row["metadata_json"]); return value
@classmethod
def _release_row(cls,row):
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["metadata"]=json.loads(row["metadata_json"]); return value
@classmethod
def _deployment_row(cls,row):
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["metadata"]=json.loads(row["metadata_json"]); return value
@classmethod
def _acceptance_row(cls,row):
value=cls._common(row); value["artifact"]=decode_artifact(row["artifact_json"]); value["accepted"]=bool(row["accepted"]); value["evidence"]=json.loads(row["evidence_json"]); return value
__all__ = [
"LineageConflict", "LineageError", "LineageLedger", "LineageVerificationError",
"VerifiedAcceptanceEvidence", "canonical_artifact", "decode_artifact",
]
@@ -0,0 +1,258 @@
"""Declarative, non-executing operation registry and timeout safety rules."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime, timezone
import math
import re
from typing import Any, Iterable, Mapping, Sequence
EFFECT_CLASSES = frozenset(
{"READ_ONLY", "IDEMPOTENT", "CONDITIONAL", "RECONCILABLE", "IRREVERSIBLE"}
)
_FORBIDDEN_EXECUTION_FIELDS = frozenset({"command", "shell", "argv", "executable", "script"})
_HANDLER_REF = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,126}$")
def _invalid(code: str, field: str = "") -> ValueError:
return ValueError(f"{code}:{field}" if field else code)
def _text(value: object, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise _invalid("field_invalid", field)
return value.strip()
def _policy(value: object, field: str, allowed_modes: set[str]) -> dict[str, Any]:
if not isinstance(value, Mapping):
raise _invalid("policy_invalid", field)
result = deepcopy(dict(value))
mode = _text(result.get("mode"), f"{field}.mode").upper()
if mode not in allowed_modes:
raise _invalid("policy_mode_invalid", f"{field}.mode")
result["mode"] = mode
return result
@dataclass(frozen=True, slots=True)
class OperationDefinition:
operation_id: str
version: int | str
effect_class: str
handler_ref: str
input_schema: str | Mapping[str, Any]
required_permissions: tuple[str, ...]
timeout_seconds: float
readback: Mapping[str, Any]
retry: Mapping[str, Any]
reconcile: Mapping[str, Any]
rollback: Mapping[str, Any]
owner_boundary: Mapping[str, Any]
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> "OperationDefinition":
if not isinstance(data, Mapping):
raise _invalid("operation_definition_invalid")
forbidden = sorted(_FORBIDDEN_EXECUTION_FIELDS.intersection(data))
if forbidden:
raise _invalid("arbitrary_execution_forbidden", forbidden[0])
operation_id = _text(data.get("operation_id"), "operation_id")
version = data.get("version")
if (
isinstance(version, bool)
or not isinstance(version, (int, str))
or (isinstance(version, int) and version < 1)
or (isinstance(version, str) and not version.strip())
):
raise _invalid("field_invalid", "version")
effect_class = _text(data.get("effect_class"), "effect_class").upper()
if effect_class not in EFFECT_CLASSES:
raise _invalid("effect_class_invalid", "effect_class")
handler_ref = _text(data.get("handler_ref", data.get("handler")), "handler_ref")
if not _HANDLER_REF.fullmatch(handler_ref):
raise _invalid("handler_ref_invalid", "handler_ref")
input_schema = data.get("input_schema")
if not isinstance(input_schema, (str, Mapping)) or not input_schema:
raise _invalid("field_invalid", "input_schema")
input_schema = deepcopy(input_schema)
permissions = data.get("required_permissions", data.get("permissions"))
if not isinstance(permissions, Sequence) or isinstance(permissions, (str, bytes, bytearray)):
raise _invalid("field_invalid", "required_permissions")
required_permissions = tuple(
_text(permission, f"required_permissions[{index}]")
for index, permission in enumerate(permissions)
)
if len(set(required_permissions)) != len(required_permissions):
raise _invalid("duplicate_permission", "required_permissions")
timeout_seconds = data.get("timeout_seconds")
if (
isinstance(timeout_seconds, bool)
or not isinstance(timeout_seconds, (int, float))
or not math.isfinite(timeout_seconds)
or timeout_seconds <= 0
):
raise _invalid("field_invalid", "timeout_seconds")
readback = _policy(data.get("readback"), "readback", {"NONE", "STATE", "EXACT"})
if "required" in readback and not isinstance(readback["required"], bool):
raise _invalid("field_invalid", "readback.required")
retry = _policy(
data.get("retry"),
"retry",
{"NEVER", "SAFE", "AFTER_READBACK", "RECONCILE_FIRST"},
)
attempts = retry.get("max_attempts", 1)
if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 1:
raise _invalid("field_invalid", "retry.max_attempts")
retry["max_attempts"] = attempts
reconcile = _policy(
data.get("reconcile"), "reconcile", {"NONE", "EXACT_READBACK", "CUSTOM", "REQUIRED"}
)
rollback = _policy(
data.get("rollback"), "rollback", {"NONE", "OPERATION", "COMPENSATE"}
)
if rollback["mode"] == "OPERATION":
_text(rollback.get("operation_id"), "rollback.operation_id")
owner_boundary = _policy(
data.get("owner_boundary"),
"owner_boundary",
{"NONE", "WITHIN_GRANT", "EXPLICIT_AUTHORIZATION", "CROSS_BOUNDARY_DENIED"},
)
restart_operation = (
"service.restart" in operation_id
or "service-restart" in operation_id
or handler_ref.endswith("service_restart")
)
if restart_operation and (
readback["mode"] != "EXACT"
or not readback.get("required", False)
or reconcile["mode"] == "NONE"
):
raise _invalid("service_restart_exact_readback_required")
return cls(
operation_id=operation_id,
version=version,
effect_class=effect_class,
handler_ref=handler_ref,
input_schema=input_schema,
required_permissions=required_permissions,
timeout_seconds=float(timeout_seconds),
readback=readback,
retry=retry,
reconcile=reconcile,
rollback=rollback,
owner_boundary=owner_boundary,
)
def timeout_action(self, outcome: str, attempts: int = 1) -> str:
return operation_timeout_action(self, outcome=outcome, attempts=attempts)
@property
def permissions(self) -> tuple[str, ...]:
return self.required_permissions
class OperationRegistry:
"""Lookup-only registry; it deliberately has no execute or shell interface."""
def __init__(self, definitions: Iterable[OperationDefinition | Mapping[str, Any]]) -> None:
self._definitions: dict[str, OperationDefinition] = {}
for raw in definitions:
definition = raw if isinstance(raw, OperationDefinition) else OperationDefinition.from_dict(raw)
if definition.operation_id in self._definitions:
raise _invalid("duplicate_operation_id", definition.operation_id)
self._definitions[definition.operation_id] = definition
def get(self, operation_id: str) -> OperationDefinition:
return self._definitions[_text(operation_id, "operation_id")]
@property
def definitions(self) -> tuple[OperationDefinition, ...]:
return tuple(self._definitions.values())
def operation_timeout_action(
definition: OperationDefinition,
outcome: str = "AMBIGUOUS",
attempts: int = 1,
) -> str:
"""Return ``RETRY``, ``RECONCILE`` or ``STOP`` after a timeout.
An unknown response from a mutating sink never authorizes a blind repeat.
Exact readback/reconciliation must first establish whether the effect happened.
"""
if not isinstance(definition, OperationDefinition):
raise _invalid("operation_definition_invalid")
normalized_outcome = _text(outcome, "outcome").upper()
if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 1:
raise _invalid("field_invalid", "attempts")
if definition.effect_class == "IRREVERSIBLE":
return "STOP"
if definition.effect_class != "READ_ONLY" and normalized_outcome in {
"AMBIGUOUS",
"UNKNOWN",
"MAY_HAVE_SUCCEEDED",
}:
if definition.reconcile["mode"] != "NONE" or definition.readback.get("required", False):
return "RECONCILE"
return "STOP"
retry_mode = definition.retry["mode"]
if attempts >= definition.retry["max_attempts"] or retry_mode == "NEVER":
return "STOP"
if retry_mode in {"AFTER_READBACK", "RECONCILE_FIRST"} and normalized_outcome not in {
"NOT_OBSERVED",
"PROVEN_ABSENT",
}:
return "RECONCILE" if definition.reconcile["mode"] != "NONE" else "STOP"
return "RETRY"
def _timestamp(value: object) -> datetime | None:
if not isinstance(value, str):
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None or parsed.utcoffset() is None:
return None
return parsed.astimezone(timezone.utc)
def verify_service_restart(
before: Mapping[str, Any], after: Mapping[str, Any]
) -> dict[str, Any]:
"""Verify a restart with instance identity and start-time evidence.
Merely observing an active unit proves current activity, not that the requested
restart occurred.
"""
if not isinstance(before, Mapping) or not isinstance(after, Mapping):
return {"status": "UNKNOWN", "reasons": ["restart_observation_invalid"]}
if not isinstance(after.get("active"), bool):
return {"status": "UNKNOWN", "reasons": ["service_active_invalid"]}
if after["active"] is False:
return {"status": "DRIFT", "reasons": ["service_not_active"]}
before_id = before.get("instance_id")
after_id = after.get("instance_id")
before_started = _timestamp(before.get("started_at"))
after_started = _timestamp(after.get("started_at"))
if (
not isinstance(before_id, str)
or not before_id
or not isinstance(after_id, str)
or not after_id
or before_started is None
or after_started is None
):
return {"status": "UNKNOWN", "reasons": ["restart_identity_missing"]}
if before_id == after_id or after_started <= before_started:
return {"status": "DRIFT", "reasons": ["restart_not_observed"]}
return {"status": "VERIFIED", "reasons": []}
@@ -0,0 +1,866 @@
"""Bounded, task-only optimization proposals for Baseline 1.1.
This module deliberately has no configuration or policy write adapter. Its only
effect boundary is ``submit_task``, supplied by the authenticated root service.
"""
from __future__ import annotations
import hashlib
import json
import math
from dataclasses import dataclass
from pathlib import Path
import re
import sqlite3
import time
from typing import Any, Callable, Mapping
ENVELOPE_SCHEMA = "aios.optimization-envelope.v1"
PROPOSAL_SCHEMA = "aios.optimization-proposal.v1"
TASK_REQUEST_SCHEMA = "aios.optimization-task-request.v1"
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}")
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}")
# An envelope may narrow these limits but cannot introduce another path or
# expand a range. Every entry describes a low-risk operational tuning knob.
_HARD_PARAMETER_LIMITS: dict[str, tuple[float, float, bool]] = {
"retry.max_attempts": (1, 10, True),
"retry.backoff_seconds": (0, 60, False),
"backoff.initial_seconds": (0, 60, False),
"backoff.maximum_seconds": (0.1, 600, False),
"routing.candidate_weight_percent": (0, 25, False),
"routing.weight_percent": (0, 25, False),
"evidence_reuse.max_age_seconds": (0, 3600, True),
"evidence_reuse.ttl_seconds": (0, 3600, True),
"timeout.operation_seconds": (0.1, 300, False),
"timeout.seconds": (0.1, 300, False),
"poll.interval_seconds": (0.1, 300, False),
"notification_dedup.window_seconds": (0, 3600, True),
"test_selection.optional_sample_percent": (1, 100, False),
"test_selection.optional_percentage": (1, 100, False),
}
_NON_PRODUCTION_ENVIRONMENTS = frozenset({"development", "test", "staging", "canary"})
class OptimizationError(RuntimeError):
"""A fail-closed optimization boundary error with a stable code."""
def __init__(self, code: str):
self.code = code
super().__init__(code)
def canonical_json_bytes(value: Any) -> bytes:
try:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise OptimizationError("OPTIMIZATION_DOCUMENT_NOT_CANONICAL") from exc
def _digest(value: Any) -> str:
return "sha256:" + hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def _number(value: Any, code: str, *, minimum: float | None = None,
maximum: float | None = None) -> int | float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise OptimizationError(code)
if not math.isfinite(float(value)):
raise OptimizationError(code)
if minimum is not None and value < minimum:
raise OptimizationError(code)
if maximum is not None and value > maximum:
raise OptimizationError(code)
return value
def _identifier(value: Any, code: str) -> str:
if not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None:
raise OptimizationError(code)
return value
def _unique_identifiers(value: Any, code: str, *, maximum: int = 256) -> tuple[str, ...]:
if not isinstance(value, (list, tuple)) or len(value) > maximum:
raise OptimizationError(code)
items = tuple(_identifier(item, code) for item in value)
if len(set(items)) != len(items):
raise OptimizationError(code)
return items
@dataclass(frozen=True)
class ParameterRange:
minimum: int | float
maximum: int | float
integer: bool
@dataclass(frozen=True)
class QualityFloor:
direction: str
threshold: int | float
@dataclass(frozen=True)
class PromotionRules:
objective_metric: str
minimum_improvement: int | float
mandatory_tests: tuple[str, ...]
mandatory_quality_gates: tuple[str, ...]
required_receipt_schema: str
@dataclass(frozen=True)
class OptimizationEnvelope:
schema: str
envelope_id: str
tenant_id: str
allowed_parameters: Mapping[str, ParameterRange]
allowed_components: tuple[str, ...]
allowed_environments: tuple[str, ...]
max_change_size: int
max_compute_budget: int | float
quality_floor: Mapping[str, QualityFloor]
rollback_requirement: bool
promotion_rules: PromotionRules
source_config_digest: str
key_id: str
signature: str
@classmethod
def from_dict(
cls,
data: Mapping[str, Any],
*,
verify_envelope: Callable[[str, bytes, str], bool] | None,
) -> "OptimizationEnvelope":
"""Parse a trusted-config envelope after authenticating its exact bytes.
``verify_envelope`` is intentionally mandatory. The caller cannot mark
a document trusted with a boolean or construct an unrestricted envelope.
"""
if verify_envelope is None or not callable(verify_envelope):
raise OptimizationError("OPTIMIZATION_ENVELOPE_VERIFIER_REQUIRED")
if not isinstance(data, Mapping):
raise OptimizationError("OPTIMIZATION_ENVELOPE_INVALID")
required = {
"schema", "envelope_id", "tenant_id", "allowed_parameters",
"allowed_components", "allowed_environments", "max_change_size",
"max_compute_budget", "quality_floor", "rollback_requirement",
"promotion_rules", "source_config_digest", "key_id", "signature",
}
if set(data) != required:
raise OptimizationError("OPTIMIZATION_ENVELOPE_FIELDS_INVALID")
unsigned = dict(data)
signature = unsigned.pop("signature")
key_id = data.get("key_id")
if not isinstance(signature, str) or not signature or not isinstance(key_id, str):
raise OptimizationError("OPTIMIZATION_ENVELOPE_SIGNATURE_INVALID")
try:
verified = verify_envelope(key_id, canonical_json_bytes(unsigned), signature)
except Exception as exc:
raise OptimizationError("OPTIMIZATION_ENVELOPE_SIGNATURE_INVALID") from exc
if verified is not True:
raise OptimizationError("OPTIMIZATION_ENVELOPE_SIGNATURE_INVALID")
if data["schema"] != ENVELOPE_SCHEMA:
raise OptimizationError("OPTIMIZATION_ENVELOPE_SCHEMA_INVALID")
envelope_id = _identifier(data["envelope_id"], "OPTIMIZATION_ENVELOPE_ID_INVALID")
tenant_id = _identifier(data["tenant_id"], "OPTIMIZATION_TENANT_INVALID")
if not _DIGEST.fullmatch(str(data["source_config_digest"])):
raise OptimizationError("OPTIMIZATION_SOURCE_CONFIG_DIGEST_INVALID")
key_id = _identifier(key_id, "OPTIMIZATION_ENVELOPE_KEY_ID_INVALID")
raw_parameters = data["allowed_parameters"]
if not isinstance(raw_parameters, Mapping) or not raw_parameters or len(raw_parameters) > 16:
raise OptimizationError("OPTIMIZATION_ALLOWED_PARAMETERS_INVALID")
parameters: dict[str, ParameterRange] = {}
for path, raw_rule in raw_parameters.items():
if path not in _HARD_PARAMETER_LIMITS:
raise OptimizationError("OPTIMIZATION_PARAMETER_PATH_FORBIDDEN")
if not isinstance(raw_rule, Mapping) or not {"minimum", "maximum"} <= set(raw_rule):
raise OptimizationError("OPTIMIZATION_PARAMETER_RANGE_INVALID")
if set(raw_rule) - {"minimum", "maximum", "integer"}:
raise OptimizationError("OPTIMIZATION_PARAMETER_RANGE_INVALID")
hard_min, hard_max, hard_integer = _HARD_PARAMETER_LIMITS[path]
minimum = _number(raw_rule["minimum"], "OPTIMIZATION_PARAMETER_RANGE_INVALID")
maximum = _number(raw_rule["maximum"], "OPTIMIZATION_PARAMETER_RANGE_INVALID")
integer = raw_rule.get("integer", hard_integer)
if type(integer) is not bool or minimum > maximum:
raise OptimizationError("OPTIMIZATION_PARAMETER_RANGE_INVALID")
if minimum < hard_min or maximum > hard_max or (hard_integer and not integer):
raise OptimizationError("OPTIMIZATION_PARAMETER_RANGE_TOO_BROAD")
if integer and (type(minimum) is not int or type(maximum) is not int):
raise OptimizationError("OPTIMIZATION_PARAMETER_RANGE_INVALID")
parameters[path] = ParameterRange(minimum, maximum, integer)
components = _unique_identifiers(
data["allowed_components"], "OPTIMIZATION_COMPONENTS_INVALID", maximum=32
)
environments = _unique_identifiers(
data["allowed_environments"], "OPTIMIZATION_ENVIRONMENTS_INVALID", maximum=4
)
if not components or not environments or not set(environments) <= _NON_PRODUCTION_ENVIRONMENTS:
raise OptimizationError("OPTIMIZATION_ENVIRONMENTS_INVALID")
max_change_size = _number(
data["max_change_size"], "OPTIMIZATION_CHANGE_SIZE_INVALID", minimum=1, maximum=16
)
if type(max_change_size) is not int:
raise OptimizationError("OPTIMIZATION_CHANGE_SIZE_INVALID")
max_compute_budget = _number(
data["max_compute_budget"],
"OPTIMIZATION_COMPUTE_BUDGET_INVALID",
minimum=0.001,
maximum=10_000,
)
raw_quality = data["quality_floor"]
if not isinstance(raw_quality, Mapping) or not raw_quality or len(raw_quality) > 16:
raise OptimizationError("OPTIMIZATION_QUALITY_FLOOR_INVALID")
quality: dict[str, QualityFloor] = {}
for metric, raw_rule in raw_quality.items():
metric = _identifier(metric, "OPTIMIZATION_QUALITY_METRIC_INVALID")
if not isinstance(raw_rule, Mapping) or set(raw_rule) != {"direction", "threshold"}:
raise OptimizationError("OPTIMIZATION_QUALITY_FLOOR_INVALID")
if raw_rule["direction"] not in {"higher", "lower"}:
raise OptimizationError("OPTIMIZATION_QUALITY_DIRECTION_INVALID")
threshold = _number(
raw_rule["threshold"], "OPTIMIZATION_QUALITY_FLOOR_INVALID",
minimum=-1e12, maximum=1e12,
)
quality[metric] = QualityFloor(raw_rule["direction"], threshold)
if type(data["rollback_requirement"]) is not bool:
raise OptimizationError("OPTIMIZATION_ROLLBACK_REQUIREMENT_INVALID")
raw_promotion = data["promotion_rules"]
promotion_fields = {
"objective_metric", "minimum_improvement", "mandatory_tests",
"mandatory_quality_gates", "required_receipt_schema",
}
if not isinstance(raw_promotion, Mapping) or set(raw_promotion) != promotion_fields:
raise OptimizationError("OPTIMIZATION_PROMOTION_RULES_INVALID")
objective = _identifier(
raw_promotion["objective_metric"], "OPTIMIZATION_OBJECTIVE_METRIC_INVALID"
)
if objective not in quality:
raise OptimizationError("OPTIMIZATION_OBJECTIVE_METRIC_INVALID")
minimum_improvement = _number(
raw_promotion["minimum_improvement"],
"OPTIMIZATION_MINIMUM_IMPROVEMENT_INVALID",
minimum=0,
maximum=1e12,
)
mandatory_tests = _unique_identifiers(
raw_promotion["mandatory_tests"], "OPTIMIZATION_MANDATORY_TESTS_INVALID"
)
mandatory_gates = _unique_identifiers(
raw_promotion["mandatory_quality_gates"], "OPTIMIZATION_MANDATORY_GATES_INVALID"
)
if not mandatory_tests or not mandatory_gates:
raise OptimizationError("OPTIMIZATION_PROMOTION_RULES_INVALID")
receipt_schema = _identifier(
raw_promotion["required_receipt_schema"], "OPTIMIZATION_RECEIPT_SCHEMA_INVALID"
)
if not receipt_schema.startswith("aios."):
raise OptimizationError("OPTIMIZATION_RECEIPT_SCHEMA_INVALID")
promotion = PromotionRules(
objective, minimum_improvement, mandatory_tests, mandatory_gates, receipt_schema
)
return cls(
ENVELOPE_SCHEMA, envelope_id, tenant_id, parameters, components,
environments, max_change_size, max_compute_budget, quality,
data["rollback_requirement"], promotion, data["source_config_digest"],
key_id, signature,
)
_PROPOSAL_REQUIRED = {
"schema", "proposal_id", "envelope_id", "tenant_id", "component",
"environment", "changes", "compute_budget", "tests", "quality_gates",
"baseline_metrics", "rollback_plan",
}
def validate_proposal(envelope: OptimizationEnvelope, proposal: Mapping[str, Any]) -> dict:
"""Return a canonical candidate constrained by ``envelope``."""
if not isinstance(envelope, OptimizationEnvelope):
raise OptimizationError("OPTIMIZATION_ENVELOPE_REQUIRED")
if not isinstance(proposal, Mapping):
raise OptimizationError("OPTIMIZATION_PROPOSAL_INVALID")
allowed_fields = _PROPOSAL_REQUIRED | {"evidence_reuse"}
if set(proposal) - allowed_fields or not _PROPOSAL_REQUIRED <= set(proposal):
raise OptimizationError("OPTIMIZATION_PROPOSAL_FIELDS_INVALID")
if proposal["schema"] != PROPOSAL_SCHEMA:
raise OptimizationError("OPTIMIZATION_PROPOSAL_SCHEMA_INVALID")
proposal_id = _identifier(proposal["proposal_id"], "OPTIMIZATION_PROPOSAL_ID_INVALID")
if proposal["envelope_id"] != envelope.envelope_id:
raise OptimizationError("OPTIMIZATION_ENVELOPE_BINDING_MISMATCH")
if proposal["tenant_id"] != envelope.tenant_id:
raise OptimizationError("OPTIMIZATION_PROPOSAL_TENANT_MISMATCH")
component = _identifier(proposal["component"], "OPTIMIZATION_COMPONENT_INVALID")
if component not in envelope.allowed_components:
raise OptimizationError("OPTIMIZATION_COMPONENT_FORBIDDEN")
environment = _identifier(proposal["environment"], "OPTIMIZATION_ENVIRONMENT_INVALID")
if environment not in envelope.allowed_environments:
raise OptimizationError("OPTIMIZATION_ENVIRONMENT_FORBIDDEN")
raw_changes = proposal["changes"]
if not isinstance(raw_changes, list) or not raw_changes or len(raw_changes) > envelope.max_change_size:
raise OptimizationError("OPTIMIZATION_CHANGE_SIZE_EXCEEDED")
changes: list[dict[str, Any]] = []
paths: set[str] = set()
for change in raw_changes:
if not isinstance(change, Mapping) or set(change) != {"path", "value"}:
raise OptimizationError("OPTIMIZATION_CHANGE_INVALID")
path = change["path"]
if not isinstance(path, str) or path not in envelope.allowed_parameters:
raise OptimizationError("OPTIMIZATION_PARAMETER_PATH_FORBIDDEN")
if path in paths:
raise OptimizationError("OPTIMIZATION_PARAMETER_DUPLICATE")
paths.add(path)
rule = envelope.allowed_parameters[path]
value = _number(change["value"], "OPTIMIZATION_PARAMETER_VALUE_INVALID")
if rule.integer and type(value) is not int:
raise OptimizationError("OPTIMIZATION_PARAMETER_VALUE_INVALID")
if value < rule.minimum or value > rule.maximum:
raise OptimizationError("OPTIMIZATION_PARAMETER_OUT_OF_RANGE")
changes.append({"path": path, "value": value})
compute_budget = _number(
proposal["compute_budget"],
"OPTIMIZATION_COMPUTE_BUDGET_EXCEEDED",
minimum=0.001,
maximum=float(envelope.max_compute_budget),
)
tests = _unique_identifiers(proposal["tests"], "OPTIMIZATION_TESTS_INVALID")
gates = _unique_identifiers(proposal["quality_gates"], "OPTIMIZATION_QUALITY_GATES_INVALID")
for mandatory in envelope.promotion_rules.mandatory_tests:
if mandatory not in tests:
raise OptimizationError("OPTIMIZATION_MANDATORY_TEST_MISSING")
for mandatory in envelope.promotion_rules.mandatory_quality_gates:
if mandatory not in gates:
raise OptimizationError("OPTIMIZATION_MANDATORY_GATE_MISSING")
raw_baseline = proposal["baseline_metrics"]
if not isinstance(raw_baseline, Mapping) or set(raw_baseline) != set(envelope.quality_floor):
raise OptimizationError("OPTIMIZATION_BASELINE_METRICS_INVALID")
baseline = {
metric: _number(raw_baseline[metric], "OPTIMIZATION_BASELINE_METRICS_INVALID")
for metric in sorted(raw_baseline)
}
rollback_plan = proposal["rollback_plan"]
if envelope.rollback_requirement:
if (
not isinstance(rollback_plan, Mapping)
or set(rollback_plan) != {"strategy", "reference"}
or rollback_plan.get("strategy") != "restore_previous_values"
):
raise OptimizationError("OPTIMIZATION_ROLLBACK_PLAN_REQUIRED")
_identifier(rollback_plan.get("reference"), "OPTIMIZATION_ROLLBACK_PLAN_INVALID")
normalized_rollback: dict[str, str] | None = {
"strategy": "restore_previous_values",
"reference": rollback_plan["reference"],
}
else:
if rollback_plan is not None:
raise OptimizationError("OPTIMIZATION_ROLLBACK_PLAN_INVALID")
normalized_rollback = None
normalized_evidence = None
if "evidence_reuse" in proposal:
evidence = proposal["evidence_reuse"]
if not isinstance(evidence, Mapping) or set(evidence) != {"scope", "evidence_ids"}:
raise OptimizationError("OPTIMIZATION_EVIDENCE_REUSE_INVALID")
expected_scope = {
"tenant_id": envelope.tenant_id,
"component": component,
"environment": environment,
}
if evidence["scope"] != expected_scope:
raise OptimizationError("OPTIMIZATION_EVIDENCE_SCOPE_MISMATCH")
evidence_ids = _unique_identifiers(
evidence["evidence_ids"], "OPTIMIZATION_EVIDENCE_REUSE_INVALID", maximum=128
)
normalized_evidence = {"scope": expected_scope, "evidence_ids": list(evidence_ids)}
normalized = {
"schema": PROPOSAL_SCHEMA,
"proposal_id": proposal_id,
"envelope_id": envelope.envelope_id,
"tenant_id": envelope.tenant_id,
"component": component,
"environment": environment,
"changes": changes,
"compute_budget": float(compute_budget),
"tests": list(tests),
"quality_gates": list(gates),
"baseline_metrics": baseline,
"rollback_plan": normalized_rollback,
}
if normalized_evidence is not None:
normalized["evidence_reuse"] = normalized_evidence
if len(canonical_json_bytes(normalized)) > 65_536:
raise OptimizationError("OPTIMIZATION_PROPOSAL_TOO_LARGE")
return normalized
class OptimizationController:
"""Durable proposal state machine whose only external effect is task admission."""
def __init__(
self,
path: str | Path,
envelope: OptimizationEnvelope,
submit_task: Callable[[Mapping[str, Any]], Mapping[str, Any]],
*,
verify_receipt: Callable[[Mapping[str, Any]], bool],
):
if not isinstance(envelope, OptimizationEnvelope):
raise OptimizationError("OPTIMIZATION_ENVELOPE_REQUIRED")
if not callable(submit_task):
raise OptimizationError("OPTIMIZATION_TASK_SUBMITTER_REQUIRED")
if not callable(verify_receipt):
raise OptimizationError("OPTIMIZATION_RECEIPT_VERIFIER_REQUIRED")
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.envelope = envelope
self.submit_task = submit_task
self.verify_receipt = verify_receipt
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.execute("PRAGMA journal_mode=WAL")
connection.execute(
"""
CREATE TABLE IF NOT EXISTS optimization_proposals (
proposal_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
envelope_id TEXT NOT NULL,
proposal_digest TEXT NOT NULL,
proposal_json TEXT NOT NULL,
status TEXT NOT NULL,
evaluation_task_id TEXT,
promotion_task_id TEXT,
baseline_json TEXT NOT NULL,
candidate_json TEXT,
effect_json TEXT,
evaluation_json TEXT,
receipt_json TEXT,
error_code TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
)
"""
)
def _context(self, value: Mapping[str, Any]) -> dict[str, str]:
if not isinstance(value, Mapping):
raise OptimizationError("OPTIMIZATION_AUTHENTICATED_CONTEXT_REQUIRED")
fields = ("tenant_id", "subject_id", "grant_id", "mission_id", "service_identity")
try:
context = {
field: _identifier(value[field], "OPTIMIZATION_AUTHENTICATED_CONTEXT_INVALID")
for field in fields
}
except KeyError as exc:
raise OptimizationError("OPTIMIZATION_AUTHENTICATED_CONTEXT_REQUIRED") from exc
if context["service_identity"] != "aios-root-service":
raise OptimizationError("OPTIMIZATION_ROOT_SERVICE_REQUIRED")
if context["tenant_id"] != self.envelope.tenant_id:
raise OptimizationError("OPTIMIZATION_CONTEXT_TENANT_MISMATCH")
return context
def _row(self, proposal_id: str) -> sqlite3.Row:
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM optimization_proposals WHERE proposal_id=?", (proposal_id,)
).fetchone()
if row is None:
raise OptimizationError("OPTIMIZATION_PROPOSAL_NOT_FOUND")
return row
def _authorized_row(self, proposal_id: str, context: Mapping[str, Any]) -> sqlite3.Row:
checked = self._context(context)
row = self._row(_identifier(proposal_id, "OPTIMIZATION_PROPOSAL_ID_INVALID"))
if row["tenant_id"] != checked["tenant_id"]:
raise OptimizationError("OPTIMIZATION_CONTEXT_TENANT_MISMATCH")
if row["subject_id"] != checked["subject_id"]:
raise OptimizationError("OPTIMIZATION_CONTEXT_SUBJECT_MISMATCH")
if row["grant_id"] != checked["grant_id"]:
raise OptimizationError("OPTIMIZATION_CONTEXT_GRANT_MISMATCH")
if row["mission_id"] != checked["mission_id"]:
raise OptimizationError("OPTIMIZATION_CONTEXT_MISSION_MISMATCH")
return row
@staticmethod
def _decode(value: str | None) -> Any:
return None if value is None else json.loads(value)
def _record(self, row: sqlite3.Row) -> dict:
proposal = self._decode(row["proposal_json"])
result = dict(proposal)
result.update(
{
"proposal_id": row["proposal_id"],
"proposal_digest": row["proposal_digest"],
"status": row["status"],
"evaluation_task_id": row["evaluation_task_id"],
"promotion_task_id": row["promotion_task_id"],
"baseline_metrics": self._decode(row["baseline_json"]),
"candidate_metrics": self._decode(row["candidate_json"]),
"effect_comparison": self._decode(row["effect_json"]),
"error_code": row["error_code"],
}
)
return result
def get_proposal(self, proposal_id: str, *, authenticated_context: Mapping[str, Any]) -> dict:
return self._record(self._authorized_row(proposal_id, authenticated_context))
def _persist_invalid(self, proposal: Mapping[str, Any], context: Mapping[str, str],
error: OptimizationError) -> None:
proposal_id = proposal.get("proposal_id")
tenant_id = proposal.get("tenant_id")
if (
not isinstance(proposal_id, str)
or _IDENTIFIER.fullmatch(proposal_id) is None
or tenant_id != self.envelope.tenant_id
):
return
try:
encoded = canonical_json_bytes(proposal)
except OptimizationError:
return
if len(encoded) > 65_536:
return
digest = "sha256:" + hashlib.sha256(encoded).hexdigest()
baseline = proposal.get("baseline_metrics")
if not isinstance(baseline, Mapping):
baseline = {}
now = time.time()
with self._connect() as connection:
connection.execute(
"""
INSERT OR IGNORE INTO optimization_proposals
(proposal_id,tenant_id,subject_id,grant_id,mission_id,envelope_id,
proposal_digest,proposal_json,status,baseline_json,error_code,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
proposal_id, self.envelope.tenant_id, context["subject_id"],
context["grant_id"], context["mission_id"], self.envelope.envelope_id,
digest, encoded.decode("utf-8"), "STOPPED_INVALID",
canonical_json_bytes(baseline).decode("utf-8"), error.code, now, now,
),
)
def propose(self, proposal: Mapping[str, Any], *, authenticated_context: Mapping[str, Any]) -> dict:
context = self._context(authenticated_context)
try:
normalized = validate_proposal(self.envelope, proposal)
except OptimizationError as exc:
if isinstance(proposal, Mapping):
self._persist_invalid(proposal, context, exc)
raise
proposal_digest = _digest(normalized)
proposal_id = normalized["proposal_id"]
now = time.time()
encoded = canonical_json_bytes(normalized).decode("utf-8")
with self._connect() as connection:
existing = connection.execute(
"SELECT * FROM optimization_proposals WHERE proposal_id=?", (proposal_id,)
).fetchone()
if existing is not None:
if existing["proposal_digest"] != proposal_digest:
raise OptimizationError("OPTIMIZATION_PROPOSAL_ID_CONFLICT")
if (
existing["tenant_id"] != context["tenant_id"]
or existing["subject_id"] != context["subject_id"]
or existing["grant_id"] != context["grant_id"]
or existing["mission_id"] != context["mission_id"]
):
raise OptimizationError("OPTIMIZATION_CONTEXT_BINDING_MISMATCH")
if existing["status"] != "PROPOSAL_RECORDED":
return self._record(existing)
else:
connection.execute(
"""
INSERT INTO optimization_proposals
(proposal_id,tenant_id,subject_id,grant_id,mission_id,envelope_id,
proposal_digest,proposal_json,status,baseline_json,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
proposal_id, context["tenant_id"], context["subject_id"],
context["grant_id"], context["mission_id"], self.envelope.envelope_id,
proposal_digest, encoded, "PROPOSAL_RECORDED",
canonical_json_bytes(normalized["baseline_metrics"]).decode("utf-8"),
now, now,
),
)
requested_task_id = f"optimization-evaluate-{proposal_id}"
task_request = {
"schema": TASK_REQUEST_SCHEMA,
"task_id": requested_task_id,
"action": "evaluate",
"tenant_id": context["tenant_id"],
"subject_id": context["subject_id"],
"grant_id": context["grant_id"],
"mission_id": context["mission_id"],
"proposal_id": proposal_id,
"proposal_digest": proposal_digest,
"component": normalized["component"],
"environment": normalized["environment"],
"compute_budget": normalized["compute_budget"],
}
try:
receipt = self.submit_task(task_request)
if not isinstance(receipt, Mapping):
raise OptimizationError("OPTIMIZATION_TASK_SUBMISSION_INVALID")
task_id = _identifier(
receipt.get("task_id"), "OPTIMIZATION_TASK_SUBMISSION_INVALID"
)
except Exception as exc:
with self._connect() as connection:
connection.execute(
"UPDATE optimization_proposals SET status=?,error_code=?,updated_at=? "
"WHERE proposal_id=? AND status='PROPOSAL_RECORDED'",
("STOPPED_SUBMISSION_FAILED", "OPTIMIZATION_TASK_SUBMISSION_FAILED", time.time(), proposal_id),
)
if isinstance(exc, OptimizationError):
raise
raise OptimizationError("OPTIMIZATION_TASK_SUBMISSION_FAILED") from exc
with self._connect() as connection:
connection.execute(
"UPDATE optimization_proposals SET status=?,evaluation_task_id=?,updated_at=? "
"WHERE proposal_id=? AND status='PROPOSAL_RECORDED'",
("EVALUATION_SUBMITTED", task_id, time.time(), proposal_id),
)
return self.get_proposal(proposal_id, authenticated_context=context)
def _trusted_receipt(self, receipt: Any) -> Mapping[str, Any]:
if not isinstance(receipt, Mapping):
raise OptimizationError("OPTIMIZATION_RECEIPT_INVALID")
try:
trusted = self.verify_receipt(receipt)
except Exception as exc:
raise OptimizationError("OPTIMIZATION_RECEIPT_UNTRUSTED") from exc
if trusted is not True:
raise OptimizationError("OPTIMIZATION_RECEIPT_UNTRUSTED")
return receipt
def record_evaluation(
self,
proposal_id: str,
evaluation: Mapping[str, Any],
*,
authenticated_context: Mapping[str, Any],
) -> dict:
row = self._authorized_row(proposal_id, authenticated_context)
if row["status"] != "EVALUATION_SUBMITTED":
raise OptimizationError("OPTIMIZATION_EVALUATION_STATE_INVALID")
if not isinstance(evaluation, Mapping) or set(evaluation) != {
"candidate_metrics", "compute_used", "tests_passed",
"quality_gates_passed", "receipt",
}:
raise OptimizationError("OPTIMIZATION_EVALUATION_INVALID")
result_body = {key: evaluation[key] for key in evaluation if key != "receipt"}
receipt = self._trusted_receipt(evaluation["receipt"])
required_receipt = {
"schema", "task_id", "proposal_id", "proposal_digest", "action",
"success", "result_digest", "key_id", "signature",
}
if set(receipt) != required_receipt:
raise OptimizationError("OPTIMIZATION_RECEIPT_INVALID")
if receipt["schema"] != self.envelope.promotion_rules.required_receipt_schema:
raise OptimizationError("OPTIMIZATION_RECEIPT_SCHEMA_MISMATCH")
if receipt["task_id"] != row["evaluation_task_id"]:
raise OptimizationError("OPTIMIZATION_RECEIPT_TASK_MISMATCH")
if receipt["proposal_id"] != row["proposal_id"]:
raise OptimizationError("OPTIMIZATION_RECEIPT_PROPOSAL_MISMATCH")
if receipt["proposal_digest"] != row["proposal_digest"]:
raise OptimizationError("OPTIMIZATION_RECEIPT_PROPOSAL_DIGEST_MISMATCH")
if receipt["action"] != "evaluate" or receipt["success"] is not True:
raise OptimizationError("OPTIMIZATION_RECEIPT_RESULT_INVALID")
if receipt["result_digest"] != _digest(result_body):
raise OptimizationError("OPTIMIZATION_RECEIPT_RESULT_DIGEST_MISMATCH")
proposal = self._decode(row["proposal_json"])
compute_used = _number(
evaluation["compute_used"], "OPTIMIZATION_EVALUATION_BUDGET_EXCEEDED", minimum=0
)
if compute_used > proposal["compute_budget"] or compute_used > self.envelope.max_compute_budget:
status = "STOPPED_BUDGET_EXCEEDED"
error_code = "OPTIMIZATION_EVALUATION_BUDGET_EXCEEDED"
return self._store_evaluation(row, evaluation, receipt, {}, status, error_code)
tests_passed = _unique_identifiers(
evaluation["tests_passed"], "OPTIMIZATION_EVALUATION_TESTS_INVALID"
)
gates_passed = _unique_identifiers(
evaluation["quality_gates_passed"], "OPTIMIZATION_EVALUATION_GATES_INVALID"
)
if not set(self.envelope.promotion_rules.mandatory_tests) <= set(tests_passed):
return self._store_evaluation(
row, evaluation, receipt, {}, "STOPPED_MANDATORY_TEST_FAILED",
"OPTIMIZATION_MANDATORY_TEST_FAILED",
)
if not set(self.envelope.promotion_rules.mandatory_quality_gates) <= set(gates_passed):
return self._store_evaluation(
row, evaluation, receipt, {}, "STOPPED_MANDATORY_GATE_FAILED",
"OPTIMIZATION_MANDATORY_GATE_FAILED",
)
raw_candidate = evaluation["candidate_metrics"]
if not isinstance(raw_candidate, Mapping) or set(raw_candidate) != set(self.envelope.quality_floor):
raise OptimizationError("OPTIMIZATION_CANDIDATE_METRICS_INVALID")
candidate = {
metric: _number(raw_candidate[metric], "OPTIMIZATION_CANDIDATE_METRICS_INVALID")
for metric in sorted(raw_candidate)
}
baseline = self._decode(row["baseline_json"])
effect: dict[str, float] = {}
quality_regression = False
for metric in sorted(self.envelope.quality_floor):
rule = self.envelope.quality_floor[metric]
if rule.direction == "higher":
delta = float(candidate[metric]) - float(baseline[metric])
below_floor = candidate[metric] < rule.threshold
else:
delta = float(baseline[metric]) - float(candidate[metric])
below_floor = candidate[metric] > rule.threshold
effect[metric] = round(delta, 12)
quality_regression = quality_regression or below_floor or delta < -1e-12
objective = self.envelope.promotion_rules.objective_metric
if quality_regression:
status, error_code = "STOPPED_QUALITY_REGRESSION", "OPTIMIZATION_QUALITY_REGRESSION"
elif effect[objective] + 1e-12 < self.envelope.promotion_rules.minimum_improvement:
status, error_code = "STOPPED_NO_IMPROVEMENT", "OPTIMIZATION_NO_IMPROVEMENT"
else:
status, error_code = "ELIGIBLE", None
return self._store_evaluation(row, evaluation, receipt, effect, status, error_code, candidate)
def _store_evaluation(
self,
row: sqlite3.Row,
evaluation: Mapping[str, Any],
receipt: Mapping[str, Any],
effect: Mapping[str, Any],
status: str,
error_code: str | None,
candidate: Mapping[str, Any] | None = None,
) -> dict:
candidate = dict(candidate or {})
with self._connect() as connection:
connection.execute(
"""
UPDATE optimization_proposals
SET status=?,candidate_json=?,effect_json=?,evaluation_json=?,receipt_json=?,
error_code=?,updated_at=?
WHERE proposal_id=?
""",
(
status, canonical_json_bytes(candidate).decode("utf-8"),
canonical_json_bytes(effect).decode("utf-8"),
canonical_json_bytes({k: evaluation[k] for k in evaluation if k != "receipt"}).decode("utf-8"),
canonical_json_bytes(receipt).decode("utf-8"), error_code, time.time(),
row["proposal_id"],
),
)
return self._record(self._row(row["proposal_id"]))
def promote(
self,
proposal_id: str,
promotion: Mapping[str, Any],
*,
authenticated_context: Mapping[str, Any],
) -> dict:
context = self._context(authenticated_context)
row = self._authorized_row(proposal_id, context)
if not isinstance(promotion, Mapping) or set(promotion) != {
"evaluation_task_id", "proposal_digest"
}:
raise OptimizationError("OPTIMIZATION_PROMOTION_REQUEST_INVALID")
if promotion["evaluation_task_id"] != row["evaluation_task_id"]:
raise OptimizationError("OPTIMIZATION_PROMOTION_TASK_MISMATCH")
if promotion["proposal_digest"] != row["proposal_digest"]:
raise OptimizationError("OPTIMIZATION_PROMOTION_PROPOSAL_MISMATCH")
if row["status"] == "PROMOTION_SUBMITTED":
return self._record(row)
if row["status"] != "ELIGIBLE":
raise OptimizationError("OPTIMIZATION_NOT_PROMOTABLE")
stored_receipt = self._trusted_receipt(self._decode(row["receipt_json"]))
if (
stored_receipt.get("task_id") != row["evaluation_task_id"]
or stored_receipt.get("proposal_digest") != row["proposal_digest"]
or stored_receipt.get("action") != "evaluate"
or stored_receipt.get("success") is not True
):
raise OptimizationError("OPTIMIZATION_PROMOTION_RECEIPT_BINDING_INVALID")
evaluation = self._decode(row["evaluation_json"])
if stored_receipt.get("result_digest") != _digest(evaluation):
raise OptimizationError("OPTIMIZATION_PROMOTION_QUALITY_RESULT_INVALID")
task_request = {
"schema": TASK_REQUEST_SCHEMA,
"task_id": f"optimization-promote-{proposal_id}",
"action": "promote",
"tenant_id": context["tenant_id"],
"subject_id": context["subject_id"],
"grant_id": context["grant_id"],
"mission_id": context["mission_id"],
"proposal_id": row["proposal_id"],
"proposal_digest": row["proposal_digest"],
"evaluation_task_id": row["evaluation_task_id"],
"evaluation_result_digest": stored_receipt["result_digest"],
}
try:
submission = self.submit_task(task_request)
if not isinstance(submission, Mapping):
raise OptimizationError("OPTIMIZATION_TASK_SUBMISSION_INVALID")
task_id = _identifier(
submission.get("task_id"), "OPTIMIZATION_TASK_SUBMISSION_INVALID"
)
except Exception as exc:
with self._connect() as connection:
connection.execute(
"UPDATE optimization_proposals SET status=?,error_code=?,updated_at=? WHERE proposal_id=?",
("STOPPED_PROMOTION_SUBMISSION_FAILED", "OPTIMIZATION_TASK_SUBMISSION_FAILED", time.time(), proposal_id),
)
if isinstance(exc, OptimizationError):
raise
raise OptimizationError("OPTIMIZATION_TASK_SUBMISSION_FAILED") from exc
with self._connect() as connection:
connection.execute(
"UPDATE optimization_proposals SET status=?,promotion_task_id=?,error_code=NULL,updated_at=? "
"WHERE proposal_id=? AND status='ELIGIBLE'",
("PROMOTION_SUBMITTED", task_id, time.time(), proposal_id),
)
return self.get_proposal(proposal_id, authenticated_context=context)
__all__ = [
"OptimizationController", "OptimizationEnvelope", "OptimizationError",
"canonical_json_bytes", "validate_proposal",
]
+742
View File
@@ -0,0 +1,742 @@
"""Release-owned deployment targets and a receipt-gated persistent saga.
The module persists intent before invoking a governed operation adapter. It
does not contain a shell runner, production signer, or success simulator. A
configured verifier must translate an adapter/sink receipt into a typed,
operation-bound :class:`VerifiedReceipt` before a stage can advance.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import Any, Callable, Mapping, Sequence
from .lineage import (
LineageLedger,
VerifiedAcceptanceEvidence,
canonical_artifact,
decode_artifact,
)
class ReleaseError(RuntimeError):
pass
class ReleaseConflict(ReleaseError):
pass
class ReleaseVerificationError(ReleaseError):
pass
class ReleaseAuthorizationError(ReleaseError):
pass
class ReleaseReconciliationRequired(ReleaseError):
pass
@dataclass(frozen=True)
class VerifiedReceipt:
"""Trusted verifier output, fully bound to one persisted stage effect."""
receipt_id: str
tenant_id: str
subject_id: str
component_id: str
task_id: str
source_task_id: str
source_effect_id: str
grant_id: str
job_id: str
effect_id: str
operation_id: str
deployment_id: str
release_id: str
artifact: Any
observed: Mapping[str, Any]
outcome: str = "SUCCEEDED"
observed_at: str | None = None
@dataclass(frozen=True)
class EnvironmentDeploymentTarget:
tenant_id: str
component_id: str
environment: str
release_id: str
artifact: Any
revision: int
stage: str
source_revision: str | None
observed_main_revision: str | None
previous_release_id: str | None
previous_artifact: Any | None
rollback_release_id: str | None
rollback_artifact: Any | None
rollback_compatible: bool
updated_by_subject_id: str
updated_at: str
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _required(value: Any, name: str) -> str:
text = str(value or "")
if not text or len(text) > 512 or any(ord(char) < 32 for char in text):
raise ValueError(f"invalid {name}")
return text
def _json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
def _recipe_digest(stages: Sequence[Mapping[str, Any]]) -> str:
return hashlib.sha256(_json(list(stages)).encode()).hexdigest()
class ReleaseLedger:
"""The release domain authority for dynamic targets and deployment sagas."""
TARGET_STAGES = {"STAGED", "ACTIVE", "ROLLBACK_STAGED"}
def __init__(
self,
path: str | Path,
*,
lineage: LineageLedger,
receipt_verifier: Callable[[Any, Mapping[str, Any]], VerifiedReceipt],
target_authorizer: Callable[[str,str,str,str,str], bool],
):
if not isinstance(lineage, LineageLedger):
raise ValueError("lineage ledger is required")
if not callable(receipt_verifier):
raise ValueError("receipt_verifier is required")
if not callable(target_authorizer):
raise ValueError("target_authorizer is required")
self.path = str(path)
self.lineage = lineage
self.receipt_verifier = receipt_verifier
self.target_authorizer = target_authorizer
Path(self.path).parent.mkdir(parents=True, exist_ok=True)
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.execute("PRAGMA journal_mode=WAL")
tables={row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if "release_targets" in tables and "release_schema_meta" not in tables:
raise ReleaseConflict("unversioned candidate release schema requires explicit migration")
if "release_schema_meta" in tables:
version=connection.execute("SELECT version FROM release_schema_meta WHERE domain='release'").fetchone()
if version is None or version[0] != 2:
raise ReleaseConflict("unsupported release schema version")
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS release_schema_meta (
domain TEXT PRIMARY KEY,
version INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS release_recipes (
recipe_id TEXT PRIMARY KEY,
recipe_digest TEXT NOT NULL,
stages_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS release_targets (
tenant_id TEXT NOT NULL,
component_id TEXT NOT NULL,
environment TEXT NOT NULL,
release_id TEXT NOT NULL,
artifact_json TEXT NOT NULL,
revision INTEGER NOT NULL,
stage TEXT NOT NULL,
source_revision TEXT,
observed_main_revision TEXT,
previous_release_id TEXT,
previous_artifact_json TEXT,
rollback_release_id TEXT,
rollback_artifact_json TEXT,
rollback_compatible INTEGER NOT NULL CHECK (rollback_compatible IN (0,1)),
updated_by_subject_id TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, component_id, environment)
);
CREATE TABLE IF NOT EXISTS release_sagas (
tenant_id TEXT NOT NULL,
saga_id TEXT NOT NULL,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
source_task_id TEXT NOT NULL,
source_effect_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
release_id TEXT NOT NULL,
deployment_id TEXT NOT NULL,
environment TEXT NOT NULL,
artifact_json TEXT NOT NULL,
recipe_id TEXT NOT NULL,
recipe_digest TEXT NOT NULL,
target_revision INTEGER NOT NULL,
stage_index INTEGER NOT NULL,
state TEXT NOT NULL,
revision INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, saga_id),
UNIQUE (tenant_id, deployment_id),
FOREIGN KEY (recipe_id) REFERENCES release_recipes(recipe_id)
);
CREATE TABLE IF NOT EXISTS release_effect_intents (
tenant_id TEXT NOT NULL,
saga_id TEXT NOT NULL,
stage_index INTEGER NOT NULL,
stage_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
effect_id TEXT NOT NULL,
job_id TEXT,
state TEXT NOT NULL,
receipt_id TEXT,
receipt_json TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, saga_id, stage_index),
UNIQUE (tenant_id, effect_id),
FOREIGN KEY (tenant_id, saga_id)
REFERENCES release_sagas(tenant_id, saga_id)
);
CREATE TABLE IF NOT EXISTS release_deployment_facts (
tenant_id TEXT NOT NULL,
fact_id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id TEXT NOT NULL,
mission_id TEXT NOT NULL,
component_id TEXT NOT NULL,
task_id TEXT NOT NULL,
grant_id TEXT NOT NULL,
job_id TEXT NOT NULL,
deployment_id TEXT NOT NULL,
release_id TEXT NOT NULL,
artifact_json TEXT NOT NULL,
saga_id TEXT NOT NULL,
stage_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
effect_id TEXT NOT NULL,
receipt_id TEXT NOT NULL,
observed_json TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE (tenant_id, effect_id, receipt_id),
FOREIGN KEY (tenant_id, saga_id)
REFERENCES release_sagas(tenant_id, saga_id)
);
"""
)
connection.execute("INSERT OR IGNORE INTO release_schema_meta(domain,version) VALUES('release',2)")
def close(self) -> None:
"""Compatibility hook; connections are transaction-scoped."""
def _authorize(self, tenant_id: str, subject_id: str, component_id: str, environment: str, action: str) -> None:
try:
authorized=self.target_authorizer(tenant_id,subject_id,component_id,environment,action)
except Exception as exc:
raise ReleaseAuthorizationError("deployment target authorization unavailable") from exc
if authorized is not True:
raise ReleaseAuthorizationError("deployment target authorization denied")
def register_recipe(self, recipe_id: str, stages: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
recipe_id = _required(recipe_id, "recipe_id")
if not isinstance(stages, Sequence) or isinstance(stages, (str, bytes)) or not stages:
raise ValueError("recipe stages must be a non-empty sequence")
normalized = []
stage_ids = set()
for item in stages:
if not isinstance(item, Mapping) or set(item) - {"stage_id", "operation_id", "parameters"}:
raise ValueError("invalid recipe stage")
stage_id = _required(item.get("stage_id"), "stage_id")
operation_id = _required(item.get("operation_id"), "operation_id")
if stage_id in stage_ids:
raise ValueError("duplicate recipe stage_id")
stage_ids.add(stage_id)
parameters = item.get("parameters", {})
if not isinstance(parameters, Mapping):
raise ValueError("stage parameters must be an object")
normalized.append({"stage_id": stage_id, "operation_id": operation_id, "parameters": dict(parameters)})
stages_json = _json(normalized)
digest = _recipe_digest(normalized)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute("SELECT * FROM release_recipes WHERE recipe_id=?", (recipe_id,)).fetchone()
if existing:
if existing["recipe_digest"] != digest:
raise ReleaseConflict("recipe_id already has different immutable stages")
else:
connection.execute("INSERT INTO release_recipes VALUES(?,?,?,?)", (recipe_id, digest, stages_json, _now()))
return {"recipe_id": recipe_id, "recipe_digest": digest, "stages": normalized}
def set_target(
self,
tenant_id: str,
subject_id: str,
component_id: str,
environment: str,
release_id: str,
artifact: Any,
*,
expected_revision: int | None,
stage: str = "STAGED",
source_revision: str | None = None,
observed_main_revision: str | None = None,
rollback_release_id: str | None = None,
rollback_artifact: Any | None = None,
rollback_compatible: bool = False,
) -> EnvironmentDeploymentTarget:
tenant_id, subject_id, component_id, environment, release_id = map(
lambda pair: _required(pair[0], pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(component_id,"component_id"),(environment,"environment"),(release_id,"release_id")),
)
self._authorize(tenant_id,subject_id,component_id,environment,"write")
if stage not in self.TARGET_STAGES:
raise ValueError("invalid target stage")
if expected_revision is not None and (not isinstance(expected_revision, int) or isinstance(expected_revision, bool) or expected_revision < 0):
raise ValueError("invalid expected_revision")
if type(rollback_compatible) is not bool:
raise ValueError("rollback_compatible must be boolean")
artifact_json = canonical_artifact(artifact)
try:
linked_release = self.lineage.get_release_for_component(tenant_id, component_id, release_id)
except KeyError as exc:
raise ReleaseConflict("deployment target release has no lineage") from exc
if canonical_artifact(linked_release["artifact"]) != artifact_json:
raise ReleaseConflict("deployment target artifact does not match release")
rollback_artifact_json = canonical_artifact(rollback_artifact) if rollback_artifact is not None else None
source_revision = _required(source_revision, "source_revision") if source_revision else None
observed_main_revision = _required(observed_main_revision, "observed_main_revision") if observed_main_revision else None
rollback_release_id = _required(rollback_release_id, "rollback_release_id") if rollback_release_id else None
if (rollback_release_id is None) != (rollback_artifact_json is None):
raise ValueError("rollback release and artifact must be supplied together")
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
current = connection.execute(
"SELECT * FROM release_targets WHERE tenant_id=? AND component_id=? AND environment=?",
(tenant_id, component_id, environment),
).fetchone()
if current is None:
if expected_revision is not None:
raise ReleaseConflict("expected revision does not match absent target")
if stage == "ROLLBACK_STAGED":
raise ReleaseConflict("rollback requires a current target")
revision = 1
previous_release_id = previous_artifact_json = None
else:
if expected_revision != current["revision"]:
raise ReleaseConflict("expected revision does not match target")
if stage == "ROLLBACK_STAGED":
if not current["rollback_compatible"]:
raise ReleaseConflict("current target has no compatible rollback")
if release_id != current["rollback_release_id"] or artifact_json != current["rollback_artifact_json"]:
raise ReleaseConflict("rollback target does not match compatible rollback binding")
revision = current["revision"] + 1
previous_release_id = current["release_id"]
previous_artifact_json = current["artifact_json"]
if rollback_compatible and rollback_release_id is None:
rollback_release_id = previous_release_id
rollback_artifact_json = previous_artifact_json
if rollback_compatible and current is not None and rollback_release_id is None:
raise ReleaseConflict("compatible rollback requires a previous target")
now = _now()
connection.execute(
"""INSERT INTO release_targets VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(tenant_id,component_id,environment) DO UPDATE SET
release_id=excluded.release_id, artifact_json=excluded.artifact_json,
revision=excluded.revision, stage=excluded.stage,
source_revision=excluded.source_revision,
observed_main_revision=excluded.observed_main_revision,
previous_release_id=excluded.previous_release_id,
previous_artifact_json=excluded.previous_artifact_json,
rollback_release_id=excluded.rollback_release_id,
rollback_artifact_json=excluded.rollback_artifact_json,
rollback_compatible=excluded.rollback_compatible,
updated_by_subject_id=excluded.updated_by_subject_id,
updated_at=excluded.updated_at""",
(tenant_id,component_id,environment,release_id,artifact_json,revision,stage,
source_revision,observed_main_revision,previous_release_id,previous_artifact_json,
rollback_release_id,rollback_artifact_json,int(rollback_compatible),subject_id,now),
)
row = connection.execute(
"SELECT * FROM release_targets WHERE tenant_id=? AND component_id=? AND environment=?",
(tenant_id,component_id,environment),
).fetchone()
return self._target(row)
def get_target(self, tenant_id: str, subject_id: str, component_id: str, environment: str) -> EnvironmentDeploymentTarget:
tenant_id,subject_id,component_id,environment=(
_required(tenant_id,"tenant_id"),_required(subject_id,"subject_id"),
_required(component_id,"component_id"),_required(environment,"environment"),
)
self._authorize(tenant_id,subject_id,component_id,environment,"read")
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM release_targets WHERE tenant_id=? AND component_id=? AND environment=?",
(tenant_id,component_id,environment),
).fetchone()
if row is None:
raise KeyError("deployment target not found")
return self._target(row)
@staticmethod
def _target(row: sqlite3.Row) -> EnvironmentDeploymentTarget:
return EnvironmentDeploymentTarget(
tenant_id=row["tenant_id"], component_id=row["component_id"], environment=row["environment"],
release_id=row["release_id"], artifact=decode_artifact(row["artifact_json"]), revision=row["revision"],
stage=row["stage"], source_revision=row["source_revision"], observed_main_revision=row["observed_main_revision"],
previous_release_id=row["previous_release_id"],
previous_artifact=decode_artifact(row["previous_artifact_json"]) if row["previous_artifact_json"] else None,
rollback_release_id=row["rollback_release_id"],
rollback_artifact=decode_artifact(row["rollback_artifact_json"]) if row["rollback_artifact_json"] else None,
rollback_compatible=bool(row["rollback_compatible"]),
updated_by_subject_id=row["updated_by_subject_id"], updated_at=row["updated_at"],
)
def start_saga(
self, tenant_id: str, subject_id: str, mission_id: str, component_id: str, saga_id: str,
release_id: str, deployment_id: str, environment: str, artifact: Any,
recipe_id: str, *, expected_target_revision: int,
) -> dict[str, Any]:
tenant_id,subject_id,mission_id,component_id,saga_id,release_id,deployment_id,environment,recipe_id=map(
lambda pair:_required(pair[0],pair[1]),
((tenant_id,"tenant_id"),(subject_id,"subject_id"),(mission_id,"mission_id"),(component_id,"component_id"),(saga_id,"saga_id"),
(release_id,"release_id"),(deployment_id,"deployment_id"),(environment,"environment"),(recipe_id,"recipe_id")),
)
if not isinstance(expected_target_revision,int) or isinstance(expected_target_revision,bool) or expected_target_revision < 1:
raise ValueError("invalid expected_target_revision")
self._authorize(tenant_id,subject_id,component_id,environment,"deploy")
artifact_json=canonical_artifact(artifact)
target=self.get_target(tenant_id,subject_id,component_id,environment)
if target.revision != expected_target_revision:
raise ReleaseConflict("expected target revision does not match")
if target.stage not in {"STAGED","ROLLBACK_STAGED"}:
raise ReleaseConflict("deployment target is not staged")
if target.release_id != release_id or canonical_artifact(target.artifact) != artifact_json:
raise ReleaseConflict("saga does not match deployment target")
graph=self.lineage.explain(tenant_id,subject_id,mission_id)
release=next((item for item in graph["releases"] if item["release_id"] == release_id),None)
if release is None or release["component_id"] != component_id or canonical_artifact(release["artifact"]) != artifact_json:
raise ReleaseConflict("release has no exact source lineage")
source_task=next((item for item in graph["tasks"] if item["task_id"] == release["source_task_id"]),None)
if source_task is None or source_task["component_id"] != component_id:
raise ReleaseConflict("release source Task component lineage missing")
with self._connect() as connection:
recipe=connection.execute("SELECT * FROM release_recipes WHERE recipe_id=?",(recipe_id,)).fetchone()
if recipe is None:
raise ReleaseConflict("recipe not found")
# The relationship is durable before saga admission and is idempotent on retry.
self.lineage.link_deployment(
tenant_id,subject_id,mission_id,release_id,deployment_id,component_id,environment,
expected_target_revision,decode_artifact(artifact_json),
metadata={"saga_id":saga_id,"recipe_id":recipe_id,"recipe_digest":recipe["recipe_digest"]},
)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing=connection.execute("SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)).fetchone()
expected=(subject_id,mission_id,component_id,release["source_task_id"],release["source_effect_id"],source_task["grant_id"],release_id,deployment_id,environment,artifact_json,recipe_id,recipe["recipe_digest"],expected_target_revision)
if existing:
actual=tuple(existing[key] for key in ("subject_id","mission_id","component_id","source_task_id","source_effect_id","grant_id","release_id","deployment_id","environment","artifact_json","recipe_id","recipe_digest","target_revision"))
if actual != expected:
raise ReleaseConflict("saga_id already has different immutable binding")
return self._saga(connection,existing)
collision=connection.execute("SELECT saga_id FROM release_sagas WHERE tenant_id=? AND deployment_id=?",(tenant_id,deployment_id)).fetchone()
if collision:
raise ReleaseConflict("deployment_id already has a different saga")
now=_now()
connection.execute(
"INSERT INTO release_sagas VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,saga_id,subject_id,mission_id,component_id,release["source_task_id"],release["source_effect_id"],source_task["grant_id"],release_id,deployment_id,environment,artifact_json,
recipe_id,recipe["recipe_digest"],expected_target_revision,0,"RUNNING",0,now,now),
)
row=connection.execute("SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)).fetchone()
return self._saga(connection,row)
def get_saga(self, tenant_id: str, subject_id: str, saga_id: str) -> dict[str, Any]:
with self._connect() as connection:
row=connection.execute("SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(_required(tenant_id,"tenant_id"),_required(saga_id,"saga_id"))).fetchone()
if row is None:
raise KeyError("release saga not found")
if row["subject_id"] != _required(subject_id,"subject_id"):
raise ReleaseAuthorizationError("release saga subject denied")
return self._saga(connection,row)
@staticmethod
def _stages(connection: sqlite3.Connection, recipe_id: str) -> list[dict[str, Any]]:
row=connection.execute("SELECT stages_json FROM release_recipes WHERE recipe_id=?",(recipe_id,)).fetchone()
if row is None:
raise ReleaseConflict("persisted recipe missing")
return json.loads(row["stages_json"])
def _saga(self, connection: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
stages=self._stages(connection,row["recipe_id"])
index=row["stage_index"]
return {
"tenant_id":row["tenant_id"],"subject_id":row["subject_id"],"mission_id":row["mission_id"],
"component_id":row["component_id"],"source_task_id":row["source_task_id"],
"source_effect_id":row["source_effect_id"],"grant_id":row["grant_id"],
"saga_id":row["saga_id"],"release_id":row["release_id"],"deployment_id":row["deployment_id"],
"environment":row["environment"],"artifact":decode_artifact(row["artifact_json"]),
"recipe_id":row["recipe_id"],"recipe_digest":row["recipe_digest"],
"target_revision":row["target_revision"],"stage_index":index,
"stage_id":stages[index]["stage_id"] if index < len(stages) else None,
"state":row["state"],"revision":row["revision"],"created_at":row["created_at"],"updated_at":row["updated_at"],
}
def advance_saga(
self, tenant_id: str, subject_id: str, saga_id: str, expected_revision: int,
operation_adapter: Any,
) -> dict[str, Any]:
tenant_id,subject_id,saga_id=(
_required(tenant_id,"tenant_id"),_required(subject_id,"subject_id"),_required(saga_id,"saga_id")
)
if not isinstance(expected_revision,int) or isinstance(expected_revision,bool) or expected_revision < 0:
raise ValueError("invalid expected_revision")
prepare=getattr(operation_adapter,"prepare",None)
execute=getattr(operation_adapter,"execute",None)
if not callable(prepare) or not callable(execute):
raise ValueError("governed operation adapter requires prepare and execute")
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
saga=connection.execute(
"SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)
).fetchone()
if saga is None:
raise KeyError("release saga not found")
if saga["subject_id"] != subject_id:
raise ReleaseAuthorizationError("saga belongs to different subject")
self._authorize(tenant_id,subject_id,saga["component_id"],saga["environment"],"deploy")
if saga["revision"] != expected_revision:
raise ReleaseConflict("expected revision does not match saga")
if saga["state"] == "COMPLETED":
return self._saga(connection,saga)
stages=self._stages(connection,saga["recipe_id"])
stage=stages[saga["stage_index"]]
effect_id=f"release:{saga_id}:{saga['stage_index']}:{stage['stage_id']}"
intent=connection.execute(
"SELECT * FROM release_effect_intents WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(tenant_id,saga_id,saga["stage_index"]),
).fetchone()
if intent is None:
now=_now()
connection.execute(
"INSERT INTO release_effect_intents VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
(tenant_id,saga_id,saga["stage_index"],stage["stage_id"],stage["operation_id"],effect_id,
None,"PENDING_JOB",None,None,now,now),
)
elif (intent["effect_id"],intent["stage_id"],intent["operation_id"]) != (
effect_id,stage["stage_id"],stage["operation_id"]
):
raise ReleaseConflict("persisted stage effect identity mismatch")
operation={
"tenant_id":tenant_id,"subject_id":subject_id,"mission_id":saga["mission_id"],
"component_id":saga["component_id"],"task_id":saga["source_task_id"],
"source_task_id":saga["source_task_id"],"source_effect_id":saga["source_effect_id"],
"grant_id":saga["grant_id"],"saga_id":saga_id,
"deployment_id":saga["deployment_id"],"release_id":saga["release_id"],
"environment":saga["environment"],"artifact":decode_artifact(saga["artifact_json"]),
"target_revision":saga["target_revision"],"stage_index":saga["stage_index"],
"stage_id":stage["stage_id"],"operation_id":stage["operation_id"],"effect_id":effect_id,
"parameters":stage.get("parameters",{}),
}
with self._connect() as connection:
intent=connection.execute(
"SELECT * FROM release_effect_intents WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(tenant_id,saga_id,saga["stage_index"]),
).fetchone()
job_id=intent["job_id"]
if job_id is None:
prepared=prepare(dict(operation))
job_id=prepared.get("job_id") if isinstance(prepared,Mapping) else prepared
job_id=_required(job_id,"prepared job_id")
# This is the first point where the relationship graph may name a Job:
# link_effect independently resolves the configured real Job authority.
self.lineage.link_effect(
tenant_id,subject_id,saga["mission_id"],saga["source_task_id"],effect_id,job_id,
source_effect_id=saga["source_effect_id"],
metadata={"deployment_id":saga["deployment_id"],"release_id":saga["release_id"],
"component_id":saga["component_id"],"stage_id":stage["stage_id"]},
)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
current=connection.execute(
"SELECT revision,stage_index FROM release_sagas WHERE tenant_id=? AND saga_id=?",
(tenant_id,saga_id),
).fetchone()
if current is None or current["revision"] != expected_revision or current["stage_index"] != saga["stage_index"]:
raise ReleaseConflict("expected revision changed before Job binding")
bound=connection.execute(
"SELECT job_id FROM release_effect_intents WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(tenant_id,saga_id,saga["stage_index"]),
).fetchone()
if bound["job_id"] not in (None,job_id):
raise ReleaseConflict("stage effect already bound to different Job")
connection.execute(
"UPDATE release_effect_intents SET job_id=?,state='JOB_LINKED',updated_at=? WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(job_id,_now(),tenant_id,saga_id,saga["stage_index"]),
)
operation["job_id"]=job_id
with self._connect() as connection:
intent=connection.execute(
"SELECT * FROM release_effect_intents WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(tenant_id,saga_id,saga["stage_index"]),
).fetchone()
if intent["receipt_json"]:
raw_receipt=json.loads(intent["receipt_json"])
elif intent["state"] == "EFFECT_UNKNOWN":
reconcile=getattr(operation_adapter,"reconcile",None)
if not callable(reconcile):
raise ReleaseReconciliationRequired("RECONCILIATION_REQUIRED: adapter readback unavailable")
raw_receipt=reconcile(dict(operation))
else:
try:
raw_receipt=execute(dict(operation))
except Exception:
with self._connect() as connection:
connection.execute(
"UPDATE release_effect_intents SET state='EFFECT_UNKNOWN',updated_at=? WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(_now(),tenant_id,saga_id,saga["stage_index"]),
)
raise
if not isinstance(raw_receipt,Mapping):
with self._connect() as connection:
connection.execute(
"UPDATE release_effect_intents SET state='EFFECT_UNKNOWN',updated_at=? WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(_now(),tenant_id,saga_id,saga["stage_index"]),
)
raise ReleaseVerificationError("operation adapter receipt must be an object")
try:
unverified_receipt_json=_json(dict(raw_receipt))
except (TypeError,ValueError) as exc:
raise ReleaseVerificationError("operation adapter receipt must be canonical JSON") from exc
if len(unverified_receipt_json.encode("utf-8")) > 262_144:
raise ReleaseVerificationError("operation adapter receipt is too large")
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
current=connection.execute(
"SELECT revision,stage_index FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)
).fetchone()
if current is None or current["revision"] != expected_revision or current["stage_index"] != saga["stage_index"]:
raise ReleaseConflict("expected revision changed before receipt persistence")
connection.execute(
"UPDATE release_effect_intents SET state='RECEIPT_UNVERIFIED',receipt_json=?,updated_at=? WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(unverified_receipt_json,_now(),tenant_id,saga_id,saga["stage_index"]),
)
expected={key:operation[key] for key in (
"tenant_id","subject_id","component_id","task_id","source_task_id","source_effect_id","grant_id",
"job_id","effect_id","operation_id","deployment_id","release_id","artifact"
)}
try:
verified=self.receipt_verifier(raw_receipt,expected)
except Exception as exc:
raise ReleaseVerificationError("configured verifier rejected operation receipt") from exc
if not isinstance(verified,VerifiedReceipt):
raise ReleaseVerificationError("receipt_verifier must return VerifiedReceipt")
if verified.outcome != "SUCCEEDED":
raise ReleaseVerificationError("verified receipt outcome is not SUCCEEDED")
fields=("tenant_id","subject_id","component_id","task_id","source_task_id","source_effect_id","grant_id",
"job_id","effect_id","operation_id","deployment_id","release_id","artifact")
bindings=tuple(canonical_artifact(getattr(verified,name)) if name == "artifact" else getattr(verified,name) for name in fields)
expected_bindings=tuple(canonical_artifact(operation[name]) if name == "artifact" else operation[name] for name in fields)
if bindings != expected_bindings:
wrong=next(name for name,actual,wanted in zip(fields,bindings,expected_bindings) if actual != wanted)
raise ReleaseVerificationError(f"verified receipt {wrong} binding mismatch")
receipt_id=_required(verified.receipt_id,"receipt_id")
if not isinstance(verified.observed,Mapping):
raise ReleaseVerificationError("verified receipt observation must be an object")
observed_json=_json(dict(verified.observed))
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
current=connection.execute("SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)).fetchone()
if current["revision"] != expected_revision or current["stage_index"] != saga["stage_index"]:
raise ReleaseConflict("expected revision changed before receipt commit")
connection.execute(
"UPDATE release_effect_intents SET state='VERIFIED',receipt_id=?,updated_at=? WHERE tenant_id=? AND saga_id=? AND stage_index=?",
(receipt_id,_now(),tenant_id,saga_id,saga["stage_index"]),
)
connection.execute(
"""INSERT OR IGNORE INTO release_deployment_facts
(tenant_id,subject_id,mission_id,component_id,task_id,grant_id,job_id,deployment_id,release_id,artifact_json,saga_id,stage_id,operation_id,effect_id,receipt_id,observed_json,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(tenant_id,subject_id,saga["mission_id"],saga["component_id"],saga["source_task_id"],saga["grant_id"],job_id,
saga["deployment_id"],saga["release_id"],saga["artifact_json"],saga_id,stage["stage_id"],stage["operation_id"],
effect_id,receipt_id,observed_json,_now()),
)
next_index=saga["stage_index"]+1
state="COMPLETED" if next_index == len(stages) else "RUNNING"
updated=connection.execute(
"UPDATE release_sagas SET stage_index=?,state=?,revision=revision+1,updated_at=? WHERE tenant_id=? AND saga_id=? AND revision=?",
(next_index,state,_now(),tenant_id,saga_id,expected_revision),
)
if updated.rowcount != 1:
raise ReleaseConflict("expected revision changed")
row=connection.execute("SELECT * FROM release_sagas WHERE tenant_id=? AND saga_id=?",(tenant_id,saga_id)).fetchone()
return self._saga(connection,row)
def deployment_facts(self, tenant_id: str, subject_id: str, component_id: str, deployment_id: str) -> list[dict[str, Any]]:
tenant_id,subject_id,component_id,deployment_id=(
_required(tenant_id,"tenant_id"),_required(subject_id,"subject_id"),
_required(component_id,"component_id"),_required(deployment_id,"deployment_id"),
)
with self._connect() as connection:
saga=connection.execute(
"SELECT environment,subject_id,component_id FROM release_sagas WHERE tenant_id=? AND deployment_id=?",
(tenant_id,deployment_id),
).fetchone()
if saga is None or saga["subject_id"] != subject_id or saga["component_id"] != component_id:
raise ReleaseAuthorizationError("deployment facts scope denied")
self._authorize(tenant_id,subject_id,component_id,saga["environment"],"read")
rows=connection.execute(
"SELECT * FROM release_deployment_facts WHERE tenant_id=? AND subject_id=? AND component_id=? AND deployment_id=? ORDER BY fact_id",
(tenant_id,subject_id,component_id,deployment_id),
).fetchall()
return [{**{key:row[key] for key in row.keys() if not key.endswith("_json")},
"artifact":decode_artifact(row["artifact_json"]),"observed":json.loads(row["observed_json"])} for row in rows]
def record_acceptance(
self, tenant_id: str, subject_id: str, mission_id: str, component_id: str, acceptance_id: str,
deployment_id: str, release_id: str, artifact: Any, accepted: bool, *,
evidence: Any,
verifier: Callable[[Any, Mapping[str, Any]], VerifiedAcceptanceEvidence],
) -> dict[str, Any]:
with self._connect() as connection:
saga=connection.execute(
"SELECT environment,subject_id,component_id FROM release_sagas WHERE tenant_id=? AND deployment_id=?",
(tenant_id,deployment_id),
).fetchone()
if saga is None or saga["subject_id"] != subject_id or saga["component_id"] != component_id:
raise ReleaseAuthorizationError("acceptance deployment scope denied")
self._authorize(tenant_id,subject_id,component_id,saga["environment"],"accept")
return self.lineage.record_acceptance(
tenant_id,subject_id,mission_id,component_id,acceptance_id,deployment_id,release_id,
artifact,accepted,evidence=evidence,verifier=verifier,
)
__all__ = [
"EnvironmentDeploymentTarget", "ReleaseAuthorizationError", "ReleaseConflict", "ReleaseError", "ReleaseLedger",
"ReleaseReconciliationRequired", "ReleaseVerificationError", "VerifiedReceipt",
]
@@ -0,0 +1,375 @@
"""Pure validation and deployment-truth evaluation for runtime contracts.
Runtime contracts describe stable structure. A release-owned deployment target
is supplied separately so that changing a release does not rewrite structural
configuration or turn a source branch into runtime evidence.
"""
from __future__ import annotations
from copy import deepcopy
from datetime import datetime, timezone
import re
from pathlib import PurePosixPath
from typing import Any, Mapping, Sequence
CONTRACT_SCHEMA = "aios.runtime-contract.v1"
LIFECYCLES = frozenset({"active", "candidate", "legacy", "example"})
_DYNAMIC_FIELDS = frozenset(
{"desired", "desired_commit", "desired_release", "desired_target", "desired_targets"}
)
_SECRET_FIELDS = frozenset(
{"secret", "secret_bytes", "token", "password", "private_key", "credential_value", "value"}
)
_DIGEST = re.compile(r"^(?:sha256:)?[0-9a-fA-F]{64}$")
_SAFE_SYMBOLIC_REF = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[/A-Za-z0-9_.:@+-]+$")
def _error(code: str, path: str = "") -> ValueError:
return ValueError(f"{code}:{path}" if path else code)
def _nonempty_string(value: object, path: str) -> str:
if not isinstance(value, str) or not value.strip():
raise _error("field_invalid", path)
return value.strip()
def _positive_int(value: object, path: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
raise _error("field_invalid", path)
return value
def _reject_dynamic_fields(value: object, path: str = "contract") -> None:
if isinstance(value, Mapping):
for key, child in value.items():
if not isinstance(key, str):
raise _error("field_name_invalid", path)
child_path = f"{path}.{key}"
if key.lower() in _DYNAMIC_FIELDS:
raise _error("dynamic_field_forbidden", child_path)
_reject_dynamic_fields(child, child_path)
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
for index, child in enumerate(value):
_reject_dynamic_fields(child, f"{path}[{index}]")
def _safe_path_ref(value: object, path: str) -> str:
text = _nonempty_string(value, path)
if "\x00" in text or "\\" in text:
raise _error("unsafe_path_ref", path)
candidate = PurePosixPath(text)
if candidate.is_absolute() or any(part in {"", ".", ".."} for part in candidate.parts):
raise _error("unsafe_path_ref", path)
return text
def _symbolic_ref(value: object, path: str) -> str:
text = _nonempty_string(value, path)
if "\x00" in text or ".." in text or not _SAFE_SYMBOLIC_REF.fullmatch(text):
raise _error("resolver_ref_invalid", path)
return text
def _credential_ref(value: object, path: str) -> str:
text = _nonempty_string(value, path)
allowed = (
text.startswith("credential://")
or text.startswith("secret://")
or text.startswith("systemd-credential:")
)
if not allowed or "\x00" in text or ".." in text or text.startswith("/run/credentials/"):
raise _error("credential_ref_invalid", path)
return text
def _validate_binding_group(
group: object,
group_name: str,
component_id: str,
environment: str,
tenant_scope: str,
) -> dict[str, Any]:
if not isinstance(group, Mapping):
raise _error("binding_group_invalid", group_name)
normalized: dict[str, Any] = {}
for name, raw in group.items():
item_path = f"{group_name}.{name}"
_nonempty_string(name, f"{group_name}.name")
if isinstance(raw, str):
reference_field = (
"credential_ref"
if group_name == "credential_refs"
else "path_ref"
if group_name == "resource_refs" and ":" not in raw
else "resolver_ref"
)
item = {
"component_id": component_id,
"environment": environment,
reference_field: raw,
}
elif isinstance(raw, Mapping):
item = deepcopy(dict(raw))
else:
raise _error("binding_invalid", item_path)
if _nonempty_string(item.get("component_id"), f"{item_path}.component_id") != component_id:
raise _error("binding_component_mismatch", item_path)
if _nonempty_string(item.get("environment"), f"{item_path}.environment") != environment:
raise _error("binding_environment_mismatch", item_path)
if "tenant_scope" in item and (
_nonempty_string(item["tenant_scope"], f"{item_path}.tenant_scope") != tenant_scope
):
raise _error("binding_tenant_mismatch", item_path)
reference_fields = [key for key in ("resolver_ref", "path_ref", "credential_ref") if key in item]
if len(reference_fields) != 1:
raise _error("binding_reference_invalid", item_path)
reference_field = reference_fields[0]
reference_path = f"{item_path}.{reference_field}"
if group_name == "credential_refs" or reference_field == "credential_ref":
for key in item:
if key.lower() in _SECRET_FIELDS:
raise _error("secret_material_forbidden", f"{item_path}.{key}")
item[reference_field] = _credential_ref(item[reference_field], reference_path)
elif reference_field == "path_ref":
item[reference_field] = _safe_path_ref(item[reference_field], reference_path)
else:
item[reference_field] = _symbolic_ref(item[reference_field], reference_path)
normalized[str(name)] = item
return normalized
def validate_contract(document: Mapping[str, Any]) -> dict[str, Any]:
"""Validate and copy a stable ``aios.runtime-contract.v1`` document.
The returned mapping owns its nested containers, so callers cannot mutate the
source after validation. Invalid contracts raise ``ValueError`` with a stable
machine-readable code prefix.
"""
if not isinstance(document, Mapping):
raise _error("contract_invalid")
_reject_dynamic_fields(document)
result = deepcopy(dict(document))
if result.get("contract_schema") != CONTRACT_SCHEMA:
raise _error("contract_schema_invalid", "contract_schema")
result["contract_revision"] = _positive_int(result.get("contract_revision"), "contract_revision")
component_id = _nonempty_string(result.get("component_id"), "component_id")
environment = _nonempty_string(result.get("environment"), "environment")
result["component_id"] = component_id
result["environment"] = environment
tenant_scope = _nonempty_string(result.get("tenant_scope"), "tenant_scope")
result["tenant_scope"] = tenant_scope
lifecycle = _nonempty_string(result.get("lifecycle"), "lifecycle").lower()
if lifecycle not in LIFECYCLES:
raise _error("lifecycle_invalid", "lifecycle")
result["lifecycle"] = lifecycle
if "resources" in result and "resource_refs" in result:
raise _error("duplicate_resource_group")
if "resources" in result:
result["resource_refs"] = result.pop("resources")
for group_name in ("entrypoints", "bindings", "resource_refs", "credential_refs"):
result[group_name] = _validate_binding_group(
result.get(group_name), group_name, component_id, environment, tenant_scope
)
operations = result.get("operations")
if (
not isinstance(operations, Sequence)
or isinstance(operations, (str, bytes, bytearray))
or not operations
):
raise _error("operations_invalid", "operations")
normalized_operations = [
_nonempty_string(operation, f"operations[{index}]")
for index, operation in enumerate(operations)
]
if len(set(normalized_operations)) != len(normalized_operations):
raise _error("duplicate_operation", "operations")
result["operations"] = normalized_operations
return result
def _unknown(*reasons: str) -> dict[str, Any]:
return {
"status": "UNKNOWN",
"state": "UNKNOWN",
"verification": "UNKNOWN",
"converged": False,
"reasons": list(dict.fromkeys(reasons)),
}
def _drift(reasons: list[str]) -> dict[str, Any]:
return {
"status": "DRIFT",
"state": "DRIFT",
"verification": "FAILED",
"converged": False,
"reasons": list(dict.fromkeys(reasons)),
}
def _parse_time(value: object) -> datetime | None:
if isinstance(value, datetime):
parsed = value
elif isinstance(value, str):
try:
parsed = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
except ValueError:
return None
else:
return None
if parsed.tzinfo is None or parsed.utcoffset() is None:
return None
return parsed.astimezone(timezone.utc)
def _digest_value(document: Mapping[str, Any], *names: str) -> object:
for name in names:
if name in document:
return document[name]
return None
def _valid_digest(value: object) -> bool:
return isinstance(value, str) and bool(_DIGEST.fullmatch(value))
def evaluate_deployment(
contract: Mapping[str, Any],
target: Mapping[str, Any] | None,
observation: Mapping[str, Any] | None,
provenance: Mapping[str, Any] | None,
now: datetime | str,
) -> dict[str, Any]:
"""Evaluate one target using fresh runtime and provenance evidence.
Missing or malformed evidence is ``UNKNOWN``. ``DRIFT`` is reserved for a
complete evidence set that proves an incompatible target or runtime fact.
Repository HEAD is intentionally not an input to this decision.
"""
stable = validate_contract(contract)
missing: list[str] = []
if target is None:
missing.append("target_missing")
elif not isinstance(target, Mapping):
missing.append("target_invalid")
if observation is None:
missing.append("observation_missing")
elif not isinstance(observation, Mapping):
missing.append("observation_invalid")
if provenance is None:
missing.append("provenance_missing")
elif not isinstance(provenance, Mapping):
missing.append("provenance_invalid")
current_time = _parse_time(now)
if current_time is None:
missing.append("now_invalid")
if missing:
return _unknown(*missing)
assert target is not None and observation is not None and provenance is not None
assert current_time is not None
target_errors: list[str] = []
for field in ("component_id", "environment", "release_id", "deployment_id"):
if not isinstance(target.get(field), str) or not str(target[field]).strip():
target_errors.append(f"target_{field}_invalid")
if "tenant_scope" in target and (
not isinstance(target["tenant_scope"], str) or not target["tenant_scope"].strip()
):
target_errors.append("target_tenant_scope_invalid")
target_artifact = target.get("artifact_digest")
if not _valid_digest(target_artifact):
target_errors.append("target_artifact_digest_invalid")
if isinstance(target.get("revision"), bool) or not isinstance(target.get("revision"), int) or target.get("revision", 0) < 1:
target_errors.append("target_revision_invalid")
observed_at = _parse_time(observation.get("observed_at"))
max_age = observation.get("max_age_seconds", observation.get("max_age"))
observed_artifact = _digest_value(
observation, "actual_artifact_digest", "actual_artifact", "artifact_digest"
)
observation_errors: list[str] = []
if observed_at is None:
observation_errors.append("observation_observed_at_invalid")
if isinstance(max_age, bool) or not isinstance(max_age, (int, float)) or max_age <= 0:
observation_errors.append("observation_max_age_invalid")
if not isinstance(observation.get("source"), str) or not observation.get("source", "").strip():
observation_errors.append("observation_source_invalid")
if not _valid_digest(observed_artifact):
observation_errors.append("observation_actual_artifact_invalid")
if not isinstance(observation.get("healthy"), bool):
observation_errors.append("observation_healthy_invalid")
for field in ("component_id", "environment", "tenant_scope", "deployment_id"):
if field in observation and (
not isinstance(observation[field], str) or not observation[field].strip()
):
observation_errors.append(f"observation_{field}_invalid")
if "revision" in observation and (
isinstance(observation["revision"], bool)
or not isinstance(observation["revision"], int)
or observation["revision"] < 1
):
observation_errors.append("observation_revision_invalid")
artifact_provenance = _digest_value(provenance, "artifact_digest", "artifact_hash")
source_provenance = _digest_value(provenance, "source_digest", "source_hash")
builder_provenance = _digest_value(provenance, "builder_digest", "builder_hash")
provenance_errors: list[str] = []
if not _valid_digest(artifact_provenance):
provenance_errors.append("provenance_artifact_digest_invalid")
if not _valid_digest(source_provenance):
provenance_errors.append("provenance_source_digest_invalid")
if not _valid_digest(builder_provenance):
provenance_errors.append("provenance_builder_digest_invalid")
evidence_errors = target_errors + observation_errors + provenance_errors
if evidence_errors:
return _unknown(*evidence_errors)
assert observed_at is not None and isinstance(max_age, (int, float))
age = (current_time - observed_at).total_seconds()
if age < 0:
return _unknown("observation_from_future")
if age > max_age:
return _unknown("observation_stale")
drift: list[str] = []
if target["component_id"] != stable["component_id"]:
drift.append("target_component_id_mismatch")
if target["environment"] != stable["environment"]:
drift.append("target_environment_mismatch")
if "tenant_scope" in target and target["tenant_scope"] != stable["tenant_scope"]:
drift.append("target_tenant_scope_mismatch")
if "tenant_scope" in observation and observation["tenant_scope"] != stable["tenant_scope"]:
drift.append("observation_tenant_scope_mismatch")
for field in ("component_id", "environment", "deployment_id", "revision"):
if field in observation and observation[field] != target[field]:
drift.append(f"observation_{field}_mismatch")
if observed_artifact != target_artifact:
drift.append("artifact_mismatch")
if artifact_provenance != target_artifact:
drift.append("provenance_artifact_mismatch")
if observation["healthy"] is False:
drift.append("observation_unhealthy")
if drift:
return _drift(drift)
return {
"status": "VERIFIED",
"state": "CONVERGED",
"verification": "VERIFIED",
"converged": True,
"reasons": [],
"component_id": target["component_id"],
"environment": target["environment"],
"release_id": target["release_id"],
"deployment_id": target["deployment_id"],
"artifact_digest": target_artifact,
"revision": target["revision"],
"observed_at": observation["observed_at"],
"observation_source": observation["source"],
}
+450
View File
@@ -0,0 +1,450 @@
"""Thin authenticated application service over existing TaskPlaneStore and JobStore.
This module owns no second Task lifecycle store. Added task runtime columns live
in the supplied Task authority database; the lineage ledger owns only relations.
The built-in host adapter is READ_ONLY. Mutating operations require separately
registered governed adapters with receiver fencing/readback, never arbitrary argv.
"""
from __future__ import annotations
from dataclasses import dataclass
from contextlib import contextmanager
from datetime import datetime,timezone
import base64
import json
import re
import sqlite3
import threading
import time
from pathlib import Path
from typing import Mapping
from task_plane.models import TaskPlan,WorkerRegistration,ProbeEvidence,ProbeOutcome,Budget
from task_plane.errors import TaskPlaneHTTPError
from aios_gitops_job_controller import GitOpsJobController,JobStore,JobState,JobError
from .evidence import ReceiptSigner,canonical,digest
from .runtime_contract import validate_contract
from .worker_policy import eligible
from .authorization import AuthorizationError
from .host import HostError
class ServiceError(RuntimeError):
def __init__(self,code,status=409):
super().__init__(code)
self.code=code
self.status=status
@dataclass(frozen=True)
class Identity:
tenant_id: str
subject_id: str
role: str
worker_id: str | None=None
class BaselineService:
def __init__(self,store,job_root,authorization,contracts,operations,worker_profiles,host,lineage,machine_key,
*,machine_key_id='baseline-machine',trusted_receipt_keys=None):
self.store=store
self.signer=ReceiptSigner(machine_key,key_id=machine_key_id,trusted_public_keys=trusted_receipt_keys)
self.jobs=GitOpsJobController(JobStore(Path(job_root)),generic_receipt_verifier=self.signer.verify)
self.authorization=authorization
self.contracts={}
for entry in contracts:
value=validate_contract(entry)
key=(value['tenant_scope'],value['environment'],value['component_id'])
if key in self.contracts: raise ServiceError('CONTRACT_DUPLICATE')
self.contracts[key]=value
self.operations=operations
self.worker_profiles=dict(worker_profiles)
self.host=host
self.lineage=lineage
self.storage_blocked=False
self._execute_lock=threading.Lock()
with self.store._transaction():
self.store.conn.execute('''CREATE TABLE IF NOT EXISTS baseline_task_runtime(
task_id TEXT PRIMARY KEY REFERENCES tasks(task_id),ready_at REAL,
blocker_code TEXT,job_id TEXT,effect_id TEXT NOT NULL UNIQUE)''')
self.store.conn.execute('''CREATE TABLE IF NOT EXISTS baseline_worker_policy(
worker_id TEXT PRIMARY KEY,policy_json TEXT NOT NULL)''')
self.store.probe_attestation_verifier=self._verify_probe
def _user(self,identity):
if not isinstance(identity,Identity) or identity.role not in {'user','owner'}:
raise ServiceError('USER_IDENTITY_REQUIRED',403)
def _worker(self,identity):
if not isinstance(identity,Identity) or identity.role!='worker':
raise ServiceError('WORKER_IDENTITY_REQUIRED',403)
profile=self.worker_profiles.get(identity.worker_id)
if not profile or profile.identity!=identity.subject_id or identity.tenant_id not in profile.tenant_scope:
raise ServiceError('WORKER_SCOPE_DENIED',403)
return profile
def _task(self,identity,task_id):
task=self.store.get_task(task_id)
meta=task.task_plan.metadata.get('baseline',{})
if meta.get('tenant_id')!=identity.tenant_id or meta.get('subject_id')!=identity.subject_id:
raise ServiceError('TASK_SCOPE_DENIED',403)
return task
def _scope(self,task):
meta=task.task_plan.metadata['baseline']
return {**{key:meta[key] for key in ('tenant_id','subject_id','mission_id','operation','component_id','environment')},'task_id':task.task_id}
def _principal(self,task):
meta=task.task_plan.metadata['baseline']
return self.authorization.issue_principal(meta['grant_id'],ttl_seconds=60,**self._scope(task))
def _fresh_write(self):
if self.storage_blocked: raise ServiceError('STORAGE_RECOVERY_REQUIRED',503)
try:
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET blocker_code=blocker_code WHERE 0')
except (sqlite3.Error,OSError) as exc:
self.storage_blocked=True
raise ServiceError('TASK_STORAGE_UNAVAILABLE',503) from exc
def submit_goal(self,identity,request,*,idempotency_key):
self._user(identity)
allowed={'mission_id','grant_id','operation','component_id','environment','inputs','intent'}
if type(request) is not dict or set(request)!=allowed or not isinstance(idempotency_key,str) or not 1<=len(idempotency_key)<=128:
raise ServiceError('GOAL_SCHEMA_INVALID',400)
if any(not isinstance(request[key],str) or not request[key] or len(request[key])>2048 for key in allowed-{'inputs'}):
raise ServiceError('GOAL_SCHEMA_INVALID',400)
if request['inputs']!={}: raise ServiceError('OPERATION_INPUT_SCHEMA_INVALID',400)
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}',request['mission_id']): raise ServiceError('MISSION_ID_INVALID',400)
operation=self.operations.get(request['operation'])
if operation.handler_ref!='host.service_status' or operation.effect_class!='READ_ONLY' or operation.required_permissions!=(operation.operation_id,):
raise ServiceError('REGISTERED_HANDLER_UNAVAILABLE',503)
contract=self.contracts.get((identity.tenant_id,request['environment'],request['component_id']))
if not contract or contract['lifecycle']!='active' or request['operation'] not in contract['operations']:
raise ServiceError('CONTRACT_OPERATION_UNAVAILABLE',403)
host_key=identity.tenant_id+':'+request['environment']+':'+request['component_id']
if host_key not in self.host.bindings: raise ServiceError('HOST_BINDING_UNAVAILABLE',503)
key=digest({'tenant':identity.tenant_id,'subject':identity.subject_id,'key':idempotency_key})
task_id='task-'+key[:40]
effect_id='effect-'+digest({'tenant':identity.tenant_id,'task':task_id,'request':request})[:40]
meta={**request,'tenant_id':identity.tenant_id,'subject_id':identity.subject_id,'effect_id':effect_id,
'operation_version':operation.version,'contract_revision':contract['contract_revision']}
plan=TaskPlan.from_dict({'task_id':task_id,'task_type':'host.diagnostic','intent':request['intent'],
'issuer':{'id':identity.subject_id,'tenant_id':identity.tenant_id,'subject_id':identity.subject_id},
'required_capabilities':[request['operation']],'risk':'low','approval_policy':'governed-grant',
'correlation_id':request['mission_id'],'metadata':{'baseline':meta},
'budgets':{'attempts':operation.retry['max_attempts'],'time_seconds':30*operation.retry['max_attempts']},'evidence_requirements':['diagnostic']})
# Validate business authorization before creating an executable task.
self.authorization.issue_principal(request['grant_id'],ttl_seconds=60,tenant_id=identity.tenant_id,
subject_id=identity.subject_id,task_id=task_id,**{key:request[key] for key in ('mission_id','operation','component_id','environment')})
self._fresh_write()
try:
with self.store._transaction():
task=self.store.create_task_with_event(plan,idempotency_key=key)
self.store.conn.execute('INSERT OR IGNORE INTO baseline_task_runtime(task_id,ready_at,effect_id) VALUES(?,?,?)',
(task_id,time.time(),effect_id))
self.lineage.link_task(identity.tenant_id,identity.subject_id,request['mission_id'],request['grant_id'],task_id)
return self.get_task(identity,task_id)
except TaskPlaneHTTPError as exc:
raise ServiceError('TASK_CONFLICT') from exc
except (sqlite3.Error,OSError) as exc:
self.storage_blocked=True
raise ServiceError('TASK_STORAGE_UNAVAILABLE',503) from exc
def get_task(self,identity,task_id):
self._user(identity)
task=self._task(identity,task_id)
with self.store._transaction():
row=self.store.conn.execute('SELECT * FROM baseline_task_runtime WHERE task_id=?',(task_id,)).fetchone()
result=task.to_dict()
if row:
result.update({'ready_at':row['ready_at'],'blocker_code':row['blocker_code'],'effect_id':row['effect_id'],'job_id':row['job_id']})
if row['job_id']:
job=self.jobs.store.read(row['job_id'])
result['job_state']=job['state']
result['side_effect_state']=job['side_effect_state']
envelope=json.loads(job['receipt_json']) if job.get('receipt_json') else None
if self._receipt_valid(task,envelope): result['receipt']=envelope['payload']
result['remaining_budget']=self.store.get_task_budget(task_id).to_dict()
return result
def explain(self,identity,mission_id):
self._user(identity)
result=self.lineage.explain(identity.tenant_id,identity.subject_id,mission_id)
result['tasks_runtime']=[self.get_task(identity,t.task_id) for t in self.store.list_tasks()
if t.task_plan.metadata.get('baseline',{}).get('mission_id')==mission_id and
t.task_plan.issuer.get('tenant_id')==identity.tenant_id and t.task_plan.issuer.get('subject_id')==identity.subject_id]
return result
def register_worker(self,identity):
profile=self._worker(identity)
try:
existing=self.store.get_worker(profile.worker_id)
except TaskPlaneHTTPError:
existing=None
now=time.time()
worker=WorkerRegistration.from_dict({'worker_id':profile.worker_id,'worker_type':'baseline-operation-worker',
'identity':profile.identity,'capabilities':list(profile.capabilities),'trust_zone':'governed-host',
'max_concurrency':profile.max_concurrency,'heartbeat_interval_seconds':profile.heartbeat_seconds,
'supported_protocols':['aios-baseline-worker-v1'],'last_heartbeat':now})
self.store.register_worker(worker,actor_identity=profile.identity,is_admin=False,
expected_revision=existing.registration_revision if existing else None)
with self.store._transaction():
self.store.conn.execute('INSERT INTO baseline_worker_policy VALUES(?,?) ON CONFLICT(worker_id) DO UPDATE SET policy_json=excluded.policy_json',
(profile.worker_id,canonical(profile.__dict__).decode()))
return {'worker_id':profile.worker_id,'registered':True,'fallback_executor':profile.fallback_executor}
def heartbeat(self,identity):
profile=self._worker(identity)
worker=self.store.get_worker(profile.worker_id)
self.store.heartbeat_worker(profile.worker_id,actor_identity=profile.identity,expected_revision=worker.registration_revision)
return {'worker_id':profile.worker_id,'alive':True}
def claim_next(self,identity):
profile=self._worker(identity)
worker=self.store.get_worker(profile.worker_id)
if worker.disabled: raise ServiceError('WORKER_DISABLED',403)
self._fresh_write()
for task in sorted(self.store.list_tasks(),key=lambda t:(t.created_at,t.task_id)):
meta=task.task_plan.metadata.get('baseline',{})
if not meta or task.status not in {'CREATED','QUEUED','FAILED_RECOVERABLE'}: continue
with self.store._transaction():
row=self.store.conn.execute('SELECT ready_at FROM baseline_task_runtime WHERE task_id=?',(task.task_id,)).fetchone()
if not row: continue
now=time.time()
task_scope={**meta,'ready_at':row['ready_at']}
if not eligible(profile,task_scope,now=now,last_heartbeat=worker.last_heartbeat): continue
# Priority is configuration on ordinary eligible workers; no special main actor.
candidates=[]
for registered in self.store.list_workers():
peer=self.worker_profiles.get(registered.worker_id)
if not peer or registered.disabled or not eligible(peer,task_scope,now=now,last_heartbeat=registered.last_heartbeat): continue
with self.store._transaction():
active=self.store.conn.execute("SELECT count(*) FROM claims WHERE worker_id=? AND status='ACTIVE' AND lease_until>?",(peer.worker_id,now)).fetchone()[0]
if active<peer.max_concurrency: candidates.append(peer)
if not candidates or sorted(candidates,key=lambda p:(-p.priority,p.worker_id))[0].worker_id!=profile.worker_id: continue
try:
self._principal(task)
with self.store._transaction():
self.store.consume_budget(task.task_id,Budget(attempts=1,time_seconds=30))
claim=self.store.claim_task(task.task_id,profile.worker_id,profile.identity,lease_seconds=30,
expected_revision=task.revision,operation_digest=task.task_plan.operation_digest())
except TaskPlaneHTTPError:
budget=self.store.get_task_budget(task.task_id)
if budget.attempts==0 or budget.time_seconds==0:
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET blocker_code=? WHERE task_id=?',('TASK_BUDGET_EXHAUSTED',task.task_id))
continue
except AuthorizationError:
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET blocker_code=? WHERE task_id=?',('AUTHORIZATION_REQUIRED',task.task_id))
continue
return {'task_id':task.task_id,'claim_id':claim.claim_id,'generation':claim.attempt,
'lease_until':claim.lease_until,'operation':meta['operation']}
return None
def renew(self,identity,task_id,claim_id):
profile=self._worker(identity)
claim=self.store.get_claim(claim_id)
if claim.task_id!=task_id or claim.worker_id!=profile.worker_id or claim.worker_identity!=profile.identity:
raise ServiceError('CLAIM_IDENTITY_MISMATCH',403)
task=self.store.get_task(task_id)
meta=task.task_plan.metadata.get('baseline',{})
if meta.get('tenant_id') not in profile.tenant_scope or meta.get('subject_id') not in profile.subject_scope or task.status=='CANCELLED':
raise ServiceError('CLAIM_SCOPE_OR_STATE_DENIED',403)
self._principal(task)
self.store.heartbeat_claim(task_id,claim_id,30)
return {'task_id':task_id,'claim_id':claim_id,'renewed':True}
def _advance(self,job_id,state,**kwargs):
row=self.jobs.store.read(job_id)
return self.jobs.advance(job_id,state,expected_revision=int(row['revision']),**kwargs)
@contextmanager
def _execution_slot(self,deadline):
remaining=deadline-time.time()
if remaining<=0 or not self._execute_lock.acquire(timeout=remaining):
raise ServiceError('EXECUTION_DEADLINE_EXPIRED',408)
try:
if time.time()>=deadline: raise ServiceError('EXECUTION_DEADLINE_EXPIRED',408)
yield
finally: self._execute_lock.release()
def execute_claim(self,identity,task_id,claim_id,*,deadline=None):
profile=self._worker(identity)
deadline=time.time()+30 if deadline is None else deadline
with self._execution_slot(deadline):
self._fresh_write()
task=self.store.get_task(task_id)
meta=task.task_plan.metadata.get('baseline',{})
claim=self.store.get_claim(claim_id)
if claim.task_id!=task_id or claim.worker_id!=profile.worker_id or claim.worker_identity!=profile.identity:
raise ServiceError('CLAIM_IDENTITY_MISMATCH',403)
if meta.get('tenant_id') not in profile.tenant_scope or meta.get('subject_id') not in profile.subject_scope:
raise ServiceError('WORKER_SCOPE_DENIED',403)
user=Identity(meta['tenant_id'],meta['subject_id'],'user')
if task.status=='COMPLETED': return self.get_task(user,task_id)
if claim.status!='ACTIVE' or claim.lease_until<=time.time() or task.status=='CANCELLED':
raise ServiceError('CLAIM_EXPIRED_OR_CANCELLED')
principal=self._principal(task)
self.authorization.authorize(principal,**self._scope(task))
operation=self.operations.get(meta['operation'])
if operation.handler_ref!='host.service_status' or operation.effect_class!='READ_ONLY':
raise ServiceError('HANDLER_NOT_REGISTERED',503)
self.authorization.assert_effect_bounds(principal,operation,**self._scope(task))
request={'schema':'aios.operation-job.v1','tenant_id':meta['tenant_id'],'subject_id':meta['subject_id'],
'mission_id':meta['mission_id'],'grant_id':meta['grant_id'],'task_id':task_id,'effect_id':meta['effect_id'],
'operation':meta['operation'],'operation_version':str(operation.version),'resource_kind':'systemd-service',
'resource_id':digest({'tenant':meta['tenant_id'],'environment':meta['environment'],'component':meta['component_id']}),'input_digest':digest(meta['inputs'])}
try:
job=self.jobs.submit(request)
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET job_id=? WHERE task_id=?',(job.job_id,task_id))
self.lineage.link_task(meta['tenant_id'],meta['subject_id'],meta['mission_id'],meta['grant_id'],task_id)
self.lineage.link_effect(meta['tenant_id'],meta['subject_id'],meta['mission_id'],task_id,meta['effect_id'],job.job_id)
row=self.jobs.store.read(job.job_id)
if row['state']=='COMPLETED':
self._close_task(task,claim,json.loads(row['receipt_json']))
return self.get_task(user,task_id)
if row['state'] not in {'REQUESTED','AUTHORIZED'}:
raise ServiceError('JOB_RECONCILIATION_REQUIRED')
self.jobs.assert_effect_admission(exclude_job_id=job.job_id)
self.authorization.reserve_effect(meta['grant_id'],meta['effect_id'],principal,**self._scope(task))
if row['state']=='REQUESTED': self._advance(job.job_id,JobState.AUTHORIZED,side_effect_state='AUTHORIZED')
row=self.jobs.store.read(job.job_id)
self.jobs.lease_resource_job(job.job_id,lease_id=claim_id,resource_kind=request['resource_kind'],
resource_id=request['resource_id'],generation=None,
lease_expires_at=datetime.fromtimestamp(min(claim.lease_until,time.time()+30,deadline),timezone.utc).isoformat(),
expected_revision=int(row['revision']))
effect_lease=self.jobs.get_execution_lease(job.job_id)
fence_generation=int(effect_lease['generation'])
self._advance(job.job_id,JobState.EXECUTING,side_effect_state='IN_PROGRESS')
# Last authorization/claim check directly before the registered host boundary.
self.authorization.authorize(principal,**self._scope(task))
if self.store.get_task(task_id).status=='CANCELLED' or self.store.get_claim(claim_id).lease_until<=time.time():
raise ServiceError('CLAIM_EXPIRED_OR_CANCELLED')
self.jobs.assert_execution_lease(job.job_id,generation=fence_generation,lease_id=claim_id)
if time.time()>=deadline: raise ServiceError('EXECUTION_DEADLINE_EXPIRED',408)
observation=self.host.status(meta['component_id'],meta['environment'],tenant_id=meta['tenant_id'],deadline=deadline)
if time.time()>=deadline or self.store.get_claim(claim_id).lease_until<=time.time():
raise ServiceError('EXECUTION_DEADLINE_EXPIRED',408)
envelope=self.signer.sign({'task_id':task_id,'job_id':job.job_id,'effect_id':meta['effect_id'],
'tenant_id':meta['tenant_id'],'subject_id':meta['subject_id'],'mission_id':meta['mission_id'],
'grant_id':meta['grant_id'],'claim_id':claim_id,'generation':fence_generation,
'operation':meta['operation'],'operation_version':str(operation.version),
'input_digest':request['input_digest'],'resource_kind':request['resource_kind'],
'resource_id':request['resource_id'],'observation':observation,'verified':True})
self._advance(job.job_id,JobState.LOCAL_VERIFIED,side_effect_state='READBACK_VERIFIED',receipt=envelope)
self._advance(job.job_id,JobState.COMPLETED,side_effect_state='READBACK_VERIFIED',receipt=envelope)
self._close_task(task,claim,envelope)
return self.get_task(user,task_id)
except (sqlite3.Error,OSError) as exc:
self.storage_blocked=True
raise ServiceError('STORAGE_RECOVERY_REQUIRED',503) from exc
except HostError as exc:
try:
self._advance(job.job_id,JobState.RECONCILIATION_REQUIRED,side_effect_state='UNKNOWN')
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET blocker_code=? WHERE task_id=?',(str(exc),task_id))
except (sqlite3.Error,OSError,JobError): self.storage_blocked=True
raise ServiceError(str(exc),503) from exc
except JobError as exc:
if 'UNAVAILABLE' in str(exc) or 'STORAGE' in str(exc): self.storage_blocked=True
raise ServiceError(str(exc),503 if self.storage_blocked else 409) from exc
def _receipt_valid(self,task,envelope):
meta=task.task_plan.metadata['baseline']
if not self.signer.verify(envelope): return False
payload=envelope['payload']
with self.store._transaction():
runtime=self.store.conn.execute('SELECT job_id FROM baseline_task_runtime WHERE task_id=?',(task.task_id,)).fetchone()
if not runtime or not runtime['job_id']: return False
expected={**{k:meta[k] for k in ('effect_id','tenant_id','subject_id','mission_id','grant_id','operation')},
'task_id':task.task_id,'job_id':runtime['job_id'],'operation_version':str(meta['operation_version']),
'input_digest':digest(meta['inputs']),'resource_kind':'systemd-service',
'resource_id':digest({'tenant':meta['tenant_id'],'environment':meta['environment'],'component':meta['component_id']})}
if any(payload.get(k)!=v for k,v in expected.items()) or payload.get('verified') is not True: return False
try:
lease=self.jobs.get_execution_lease(runtime['job_id'])
source_claim=self.store.get_claim(payload.get('claim_id'))
if payload.get('claim_id')!=lease['lease_id'] or payload.get('generation')!=lease['generation']: return False
if source_claim.task_id!=task.task_id or source_claim.operation_digest!=task.task_plan.operation_digest(): return False
except (JobError,TaskPlaneHTTPError,TypeError): return False
observation=payload.get('observation')
if type(observation) is not dict: return False
for k in ('operation','tenant_id','component_id','environment'):
if observation.get(k)!=meta[k]: return False
binding=meta['tenant_id']+':'+meta['environment']+':'+meta['component_id']
if observation.get('unit')!=self.host.bindings.get(binding) or observation.get('source')!='systemd.show': return False
return True
def _close_task(self,task,claim,envelope):
if not self._receipt_valid(task,envelope): raise ServiceError('RESULT_ATTESTATION_INVALID',403)
current=self.store.get_task(task.task_id)
if current.status=='CANCELLED': raise ServiceError('TASK_CANCELLED')
if current.status=='CLAIMED': self.store.update_task_status(task.task_id,current.revision,'RUNNING')
self.store.complete_claim(task.task_id,claim.claim_id,'COMPLETED',envelope,
evidence_entries=[('diagnostic',envelope,claim.worker_identity)])
def cancel_task(self,identity,task_id):
self._user(identity)
task=self._task(identity,task_id)
self.store.cancel_task_with_event(task_id,task.revision,'cancelled by authenticated subject')
return self.get_task(identity,task_id)
def _verify_probe(self,probe):
try:
self.signer.public_key.verify(base64.b64decode(probe.signature,validate=True),canonical(probe.digest_payload()))
return True
except Exception:
return False
def recover(self):
"""Reconcile expired baseline claims from durable signed receipts; never fake a result."""
with self._execute_lock:
return self._recover_locked()
def _recover_locked(self):
recovered=[]
with self.store._transaction():
rows=self.store.conn.execute("SELECT claim_id FROM claims WHERE status='ACTIVE' AND lease_until<?",(time.time(),)).fetchall()
for row in rows:
claim=self.store.get_claim(row['claim_id'])
task=self.store.get_task(claim.task_id)
meta=task.task_plan.metadata.get('baseline')
if not meta: continue
with self.store._transaction():
rt=self.store.conn.execute('SELECT job_id FROM baseline_task_runtime WHERE task_id=?',(task.task_id,)).fetchone()
outcome=ProbeOutcome.NOT_APPLIED
ref='read-only-no-persistent-effect'
if rt and rt['job_id']:
job=self.jobs.store.read(rt['job_id'])
env=json.loads(job['receipt_json']) if job.get('receipt_json') else None
if job['state']=='COMPLETED':
if self._receipt_valid(task,env):
outcome=ProbeOutcome.APPLIED_VERIFIED
ref=digest(env)
else: outcome=ProbeOutcome.INDETERMINATE
elif self.operations.get(meta['operation']).effect_class!='READ_ONLY':
outcome=ProbeOutcome.INDETERMINATE
now=time.time()
probe=ProbeEvidence.create(task_id=task.task_id,claim_id=claim.claim_id,worker_id=claim.worker_id,
worker_identity=claim.worker_identity,operation_digest=claim.operation_digest,outcome=outcome,
observed_at=now,expires_at=now+30,probe_executor_identity='baseline-reconciler',attestation_ref=ref)
from dataclasses import replace
probe=replace(probe,signature=base64.b64encode(self.signer.private_key.sign(canonical(probe.digest_payload()))).decode())
self.store.recover_claim_with_probe_and_event(task.task_id,claim.claim_id,probe)
if outcome==ProbeOutcome.NOT_APPLIED and rt and rt['job_id']:
job=self.jobs.store.read(rt['job_id'])
if job['state']!='COMPLETED':
self._advance(rt['job_id'],JobState.RECONCILIATION_REQUIRED,side_effect_state='UNKNOWN')
self.jobs.reconcile(rt['job_id'],lambda _:{'state':'REQUESTED','side_effect_state':'READ_ONLY_SAFE_TO_RETRY'})
recovered.append({'task_id':task.task_id,'outcome':outcome.value})
# A successful writable transaction permits recovery processing, not blind effect replay.
with self.store._transaction():
self.store.conn.execute('UPDATE baseline_task_runtime SET blocker_code=blocker_code WHERE 0')
try:
self.jobs.assert_effect_admission()
self.storage_blocked=False
except JobError:
self.storage_blocked=True
return recovered
@@ -0,0 +1,513 @@
"""Authenticated loopback HTTP transport for the Baseline 1.1 service."""
from __future__ import annotations
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import ipaddress
import json
import math
import re
import time
import threading
from typing import Any, Mapping
from urllib.error import HTTPError, URLError
from urllib.parse import quote, unquote, urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from .service import Identity, ServiceError
MAX_REQUEST_BYTES = 65_536
MAX_RESPONSE_BYTES = 1_048_576
MAX_DEADLINE_SECONDS = 30
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}")
_IDENTITY_FIELDS = frozenset({"tenant_id", "subject_id", "role", "worker_id", "identity"})
class TransportError(RuntimeError):
def __init__(self, code: str, status: int | None = None):
self.code = code
self.status = status
super().__init__(code)
class _RequestError(RuntimeError):
def __init__(self, code: str, status: int):
self.code = code
self.status = status
super().__init__(code)
def _safe_identifier(value: Any, code: str = "HTTP_PATH_INVALID") -> str:
if not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None:
raise _RequestError(code, 400)
return value
def _has_identity_field(value: Any) -> bool:
if isinstance(value, Mapping):
if any(key in _IDENTITY_FIELDS for key in value):
return True
return any(_has_identity_field(item) for item in value.values())
if isinstance(value, list):
return any(_has_identity_field(item) for item in value)
return False
def _loopback_host(host: str, code: str) -> str:
if not isinstance(host, str):
raise TransportError(code)
try:
address = ipaddress.ip_address(host)
except ValueError as exc:
raise TransportError(code) from exc
if not address.is_loopback or address.version != 4:
raise TransportError(code)
return str(address)
class _BaselineHTTPServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, address, handler, service, identities):
self.baseline_service = service
self.trusted_identities = identities
self.request_slots=threading.BoundedSemaphore(16)
super().__init__(address, handler)
def get_request(self):
connection, address = super().get_request()
connection.settimeout(MAX_DEADLINE_SECONDS)
return connection, address
def process_request(self,request,client_address):
if not self.request_slots.acquire(blocking=False):
try:
body=b'{"error":{"code":"HTTP_CAPACITY_EXCEEDED"}}'
request.sendall(b'HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: '+str(len(body)).encode()+b'\r\n\r\n'+body)
except OSError: pass
self.shutdown_request(request)
return
try: super().process_request(request,client_address)
except Exception:
self.request_slots.release()
raise
def process_request_thread(self,request,client_address):
try: super().process_request_thread(request,client_address)
finally: self.request_slots.release()
class _Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
server_version = "AIOSBaselineHTTP/1"
sys_version = ""
def log_message(self, _format, *_args):
# Request paths can contain tenant-owned identifiers; do not emit them or
# bearer-related failure details through the default stderr logger.
return
def send_error(self, code, message=None, explain=None):
# BaseHTTPRequestHandler otherwise emits an HTML page which may include
# parser details. Keep pre-dispatch protocol failures in the same closed
# JSON error format.
status = code if isinstance(code, int) and 400 <= code <= 599 else 400
self._send(status, {"error": {"code": "HTTP_PROTOCOL_ERROR"}})
def _send(self, status: int, value: Any) -> None:
try:
body = json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError):
status = 500
body = b'{"error":{"code":"HTTP_RESPONSE_INVALID"}}'
if len(body) > MAX_RESPONSE_BYTES:
status = 500
body = b'{"error":{"code":"HTTP_RESPONSE_TOO_LARGE"}}'
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Connection", "close")
self.end_headers()
try:
self.wfile.write(body)
except OSError:
# A peer can disconnect or let its deadline expire while a response
# is being written. Do not let the server print a handler traceback.
pass
self.close_connection = True
def _error(self, error: _RequestError) -> None:
self._send(error.status, {"error": {"code": error.code}})
def _deadline(self) -> float:
values = self.headers.get_all("X-AIOS-Deadline", [])
if len(values) != 1:
raise _RequestError("HTTP_DEADLINE_REQUIRED", 400)
try:
deadline = float(values[0])
except (TypeError, ValueError) as exc:
raise _RequestError("HTTP_DEADLINE_INVALID", 400) from exc
now = time.time()
if not math.isfinite(deadline) or deadline > now + MAX_DEADLINE_SECONDS:
raise _RequestError("HTTP_DEADLINE_INVALID", 400)
if deadline <= now:
raise _RequestError("HTTP_DEADLINE_EXPIRED", 408)
self.connection.settimeout(deadline - now)
return deadline
def _identity(self) -> Identity:
values = self.headers.get_all("Authorization", [])
if len(values) != 1 or not values[0].startswith("Bearer "):
raise _RequestError("HTTP_UNAUTHORIZED", 401)
token = values[0][7:]
if not token or any(ord(char) < 33 or ord(char) > 126 for char in token):
raise _RequestError("HTTP_UNAUTHORIZED", 401)
identity = self.server.trusted_identities.get(token)
if identity is None:
raise _RequestError("HTTP_UNAUTHORIZED", 401)
return identity
def _json_body(self) -> dict:
if self.headers.get("Transfer-Encoding") is not None:
raise _RequestError("HTTP_TRANSFER_ENCODING_FORBIDDEN", 400)
content_types = self.headers.get_all("Content-Type", [])
if content_types != ["application/json"]:
raise _RequestError("HTTP_CONTENT_TYPE_INVALID", 415)
lengths = self.headers.get_all("Content-Length", [])
if len(lengths) != 1 or not lengths[0].isdigit():
raise _RequestError("HTTP_CONTENT_LENGTH_INVALID", 411)
length = int(lengths[0])
if length > MAX_REQUEST_BYTES:
raise _RequestError("HTTP_BODY_TOO_LARGE", 413)
if length <= 0:
raise _RequestError("HTTP_BODY_REQUIRED", 400)
raw = self.rfile.read(length)
if len(raw) != length:
raise _RequestError("HTTP_BODY_TRUNCATED", 400)
try:
def unique_object(pairs):
result = {}
for key, item in pairs:
if key in result:
raise _RequestError("HTTP_JSON_DUPLICATE_KEY", 400)
result[key] = item
return result
def reject_constant(_value):
raise _RequestError("HTTP_JSON_NON_FINITE", 400)
value = json.loads(
raw.decode("utf-8"), object_pairs_hook=unique_object,
parse_constant=reject_constant,
)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise _RequestError("HTTP_JSON_INVALID", 400) from exc
if type(value) is not dict:
raise _RequestError("HTTP_JSON_OBJECT_REQUIRED", 400)
if _has_identity_field(value):
raise _RequestError("HTTP_IDENTITY_FIELD_FORBIDDEN", 400)
return value
def _path(self) -> list[str]:
parsed = urlsplit(self.path)
if parsed.query or parsed.fragment or not parsed.path.startswith("/"):
raise _RequestError("HTTP_PATH_INVALID", 400)
raw_segments = parsed.path.split("/")[1:]
if any(not segment for segment in raw_segments):
raise _RequestError("HTTP_NOT_FOUND", 404)
segments = [unquote(segment) for segment in raw_segments]
if any("/" in segment or "\\" in segment for segment in segments):
raise _RequestError("HTTP_PATH_INVALID", 400)
return segments
@staticmethod
def _exact(body: Mapping[str, Any], fields: set[str]) -> None:
if set(body) != fields:
raise _RequestError("HTTP_BODY_SCHEMA_INVALID", 400)
def _dispatch(self, method: str, identity: Identity, segments: list[str], body: dict | None):
service = self.server.baseline_service
if method == "POST" and segments == ["v1", "goals", "submit"]:
self._exact(body, {"request", "idempotency_key"})
if type(body["request"]) is not dict:
raise _RequestError("HTTP_BODY_SCHEMA_INVALID", 400)
return 201, service.submit_goal(
identity, body["request"], idempotency_key=body["idempotency_key"]
)
if method == "GET" and len(segments) == 3 and segments[:2] == ["v1", "tasks"]:
return 200, service.get_task(identity, _safe_identifier(segments[2]))
if (
method == "POST" and len(segments) == 4
and segments[:2] == ["v1", "tasks"] and segments[3] == "cancel"
):
self._exact(body, set())
return 200, service.cancel_task(identity, _safe_identifier(segments[2]))
if (
method == "GET" and len(segments) == 4
and segments[:2] == ["v1", "missions"] and segments[3] == "explain"
):
return 200, service.explain(identity, _safe_identifier(segments[2]))
if method == "POST" and segments == ["v1", "workers", "register"]:
self._exact(body, set())
return 200, service.register_worker(identity)
if method == "POST" and segments == ["v1", "workers", "heartbeat"]:
self._exact(body, set())
return 200, service.heartbeat(identity)
if method == "POST" and segments == ["v1", "workers", "claim"]:
self._exact(body, set())
return 200, service.claim_next(identity)
if method == "POST" and segments == ["v1", "workers", "renew"]:
self._exact(body, {"task_id", "claim_id"})
return 200, service.renew(
identity, _safe_identifier(body["task_id"]), _safe_identifier(body["claim_id"])
)
if method == "POST" and segments == ["v1", "workers", "execute"]:
self._exact(body, {"task_id", "claim_id"})
return 200, service.execute_claim(
identity, _safe_identifier(body["task_id"]), _safe_identifier(body["claim_id"]),deadline=self.request_deadline
)
raise _RequestError("HTTP_NOT_FOUND", 404)
def _handle(self, method: str) -> None:
try:
self.request_deadline=self._deadline()
identity = self._identity()
segments = self._path()
body = self._json_body() if method == "POST" else None
if method == "GET":
lengths = self.headers.get_all("Content-Length", [])
if self.headers.get("Transfer-Encoding") is not None:
raise _RequestError("HTTP_GET_BODY_FORBIDDEN", 400)
if len(lengths) > 1 or (lengths and (not lengths[0].isdigit() or int(lengths[0]) != 0)):
raise _RequestError("HTTP_GET_BODY_FORBIDDEN", 400)
status, result = self._dispatch(method, identity, segments, body)
self._send(status, result)
except _RequestError as exc:
self._error(exc)
except ServiceError as exc:
status = exc.status if isinstance(exc.status, int) and 400 <= exc.status <= 599 else 500
self._send(status, {"error": {"code": exc.code}})
except TimeoutError:
self._send(408, {"error": {"code": "HTTP_DEADLINE_EXPIRED"}})
except Exception:
# No exception string, traceback, request headers, or token crosses
# this boundary.
self._send(500, {"error": {"code": "HTTP_INTERNAL_ERROR"}})
def do_GET(self):
self._handle("GET")
def do_POST(self):
self._handle("POST")
def _method_not_allowed(self):
try:
self._deadline()
self._identity()
self._send(405, {"error": {"code": "HTTP_METHOD_NOT_ALLOWED"}})
except _RequestError as exc:
self._error(exc)
do_PUT = _method_not_allowed
do_PATCH = _method_not_allowed
do_DELETE = _method_not_allowed
do_OPTIONS = _method_not_allowed
do_HEAD = _method_not_allowed
do_TRACE = _method_not_allowed
do_CONNECT = _method_not_allowed
def serve(
service: Any,
tokens: Mapping[str, Identity],
host: str = "127.0.0.1",
port: int = 0,
) -> ThreadingHTTPServer:
"""Build an unstarted real HTTP server bound only to an IPv4 loopback."""
host = _loopback_host(host, "HTTP_LOOPBACK_REQUIRED")
if type(port) is not int or not 0 <= port <= 65_535:
raise TransportError("HTTP_PORT_INVALID")
if not isinstance(tokens, Mapping) or not tokens:
raise TransportError("HTTP_TRUSTED_IDENTITIES_REQUIRED")
identities: dict[str, Identity] = {}
for token, identity in tokens.items():
if (
not isinstance(token, str) or not 32<=len(token)<=512
or any(ord(char) < 33 or ord(char) > 126 for char in token)
or not isinstance(identity, Identity)
):
raise TransportError("HTTP_TRUSTED_IDENTITY_INVALID")
identities[token] = identity
return _BaselineHTTPServer((host, port), _Handler, service, identities)
class _NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
class HttpTransport:
"""Small urllib client for the loopback API; redirects and proxies are off."""
def __init__(
self,
base_url: str,
token: str,
*,
timeout_seconds: float = 15,
max_response_bytes: int = MAX_RESPONSE_BYTES,
):
try:
parsed = urlsplit(base_url)
parsed_port = parsed.port
except ValueError as exc:
raise TransportError("TRANSPORT_BASE_URL_INVALID") from exc
if (
parsed.scheme != "http" or parsed.username is not None or parsed.password is not None
or parsed.query or parsed.fragment or parsed.path not in {"", "/"}
or parsed.hostname is None or parsed_port is None
):
raise TransportError("TRANSPORT_BASE_URL_INVALID")
_loopback_host(parsed.hostname, "TRANSPORT_LOOPBACK_REQUIRED")
if not isinstance(token, str) or not 32<=len(token)<=512 or any(
ord(char) < 33 or ord(char) > 126 for char in token
):
raise TransportError("TRANSPORT_TOKEN_INVALID")
if (
isinstance(timeout_seconds, bool)
or not isinstance(timeout_seconds, (int, float))
or not math.isfinite(float(timeout_seconds))
or not 0.1 <= timeout_seconds <= MAX_DEADLINE_SECONDS
):
raise TransportError("TRANSPORT_TIMEOUT_INVALID")
if type(max_response_bytes) is not int or not 1 <= max_response_bytes <= 4 * 1024 * 1024:
raise TransportError("TRANSPORT_RESPONSE_LIMIT_INVALID")
self.base_url = f"http://{parsed.hostname}:{parsed_port}"
self.token = token
self.timeout_seconds = float(timeout_seconds)
self.max_response_bytes = max_response_bytes
self._opener = build_opener(ProxyHandler({}), _NoRedirect())
def _decode(self, response) -> Any:
content_type = response.headers.get("Content-Type", "").split(";", 1)[0]
if content_type != "application/json":
raise TransportError("TRANSPORT_RESPONSE_TYPE_INVALID")
declared = response.headers.get("Content-Length")
if declared is None or not declared.isdigit() or int(declared) > self.max_response_bytes:
raise TransportError("TRANSPORT_RESPONSE_TOO_LARGE")
raw = response.read(self.max_response_bytes + 1)
if len(raw) > self.max_response_bytes or len(raw) != int(declared):
raise TransportError("TRANSPORT_RESPONSE_SIZE_INVALID")
try:
def unique_object(pairs):
result = {}
for key, item in pairs:
if key in result:
raise ValueError("duplicate")
result[key] = item
return result
return json.loads(
raw.decode("utf-8"), object_pairs_hook=unique_object,
parse_constant=lambda _value: (_ for _ in ()).throw(ValueError("non-finite")),
)
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
raise TransportError("TRANSPORT_RESPONSE_JSON_INVALID") from exc
def _request(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> Any:
if not isinstance(path, str) or not path.startswith("/") or "?" in path or "#" in path:
raise TransportError("TRANSPORT_PATH_INVALID")
data = None
headers = {
"Authorization": "Bearer " + self.token,
"Accept": "application/json",
"X-AIOS-Deadline": str(time.time() + self.timeout_seconds),
}
if method == "POST":
if not isinstance(body, Mapping):
raise TransportError("TRANSPORT_BODY_INVALID")
try:
data = json.dumps(
body, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise TransportError("TRANSPORT_BODY_INVALID") from exc
if not data or len(data) > MAX_REQUEST_BYTES:
raise TransportError("TRANSPORT_BODY_TOO_LARGE")
headers["Content-Type"] = "application/json"
request = Request(self.base_url + path, data=data, headers=headers, method=method)
try:
with self._opener.open(request, timeout=self.timeout_seconds) as response:
return self._decode(response)
except HTTPError as exc:
if 300 <= exc.code < 400:
exc.close()
raise TransportError("TRANSPORT_REDIRECT_REJECTED", exc.code) from None
try:
payload = self._decode(exc)
except TransportError:
payload = None
except (TimeoutError, OSError):
raise TransportError("TRANSPORT_UNAVAILABLE") from None
finally:
exc.close()
code = None
if isinstance(payload, Mapping) and isinstance(payload.get("error"), Mapping):
code = payload["error"].get("code")
if not isinstance(code, str) or _IDENTIFIER.fullmatch(code) is None:
code = "TRANSPORT_HTTP_ERROR"
raise TransportError(code, exc.code) from None
except (URLError, TimeoutError, OSError):
raise TransportError("TRANSPORT_UNAVAILABLE") from None
def submit_goal(self, request: Mapping[str, Any], *, idempotency_key: str) -> dict:
return self._request(
"POST", "/v1/goals/submit",
{"request": request, "idempotency_key": idempotency_key},
)
def get_task(self, task_id: str) -> dict:
return self._request("GET", "/v1/tasks/" + quote(task_id, safe=""))
def cancel_task(self, task_id: str) -> dict:
return self._request("POST", "/v1/tasks/" + quote(task_id, safe="") + "/cancel", {})
def explain(self, mission_id: str) -> dict:
return self._request("GET", "/v1/missions/" + quote(mission_id, safe="") + "/explain")
def register_worker(self) -> dict:
return self._request("POST", "/v1/workers/register", {})
def heartbeat(self) -> dict:
return self._request("POST", "/v1/workers/heartbeat", {})
def claim_next(self) -> dict | None:
return self._request("POST", "/v1/workers/claim", {})
def renew(self, task_id: str, claim_id: str) -> dict:
return self._request(
"POST", "/v1/workers/renew", {"task_id": task_id, "claim_id": claim_id}
)
def execute_claim(self, task_id: str, claim_id: str) -> dict:
return self._request(
"POST", "/v1/workers/execute", {"task_id": task_id, "claim_id": claim_id}
)
# Goal-oriented names used by thin clients.
submit = submit_goal
query = get_task
cancel = cancel_task
Transport = HttpTransport
__all__ = ["HttpTransport", "Transport", "TransportError", "serve"]
@@ -0,0 +1,204 @@
"""Verification-only Ed25519 boundaries for release and acceptance evidence."""
from __future__ import annotations
import base64
import json
import re
from types import MappingProxyType
from typing import Any, Callable, Mapping
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from .lineage import VerifiedAcceptanceEvidence, canonical_artifact
from .release import VerifiedReceipt
RELEASE_RECEIPT_DOMAIN = b"aios.release-operation-receipt.v1"
ACCEPTANCE_EVIDENCE_DOMAIN = b"aios.release-acceptance-evidence.v1"
_ENVELOPE_FIELDS = {"schema", "key_id", "payload", "signature"}
_RECEIPT_FIELDS = {
"receipt_id", "tenant_id", "subject_id", "component_id", "task_id",
"source_task_id", "source_effect_id", "grant_id", "job_id", "effect_id", "operation_id",
"deployment_id", "release_id", "artifact", "outcome", "observed", "observed_at",
}
_ACCEPTANCE_FIELDS = {
"acceptance_id", "tenant_id", "subject_id", "component_id", "deployment_id", "release_id",
"artifact", "accepted", "evidence_id", "observed", "observed_at",
}
_B64URL = re.compile(r"[A-Za-z0-9_-]+")
class SignatureVerificationError(ValueError):
"""A configured trust root did not authenticate an exact evidence payload."""
def _canonical(value: Any) -> bytes:
try:
return json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise SignatureVerificationError("signed payload is not canonical JSON") from exc
def _decode_urlsafe(value: Any, name: str, expected_size: int) -> bytes:
if not isinstance(value, str) or not value or not _B64URL.fullmatch(value) or "=" in value:
raise SignatureVerificationError(f"invalid {name} encoding")
try:
raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
except (ValueError, TypeError) as exc:
raise SignatureVerificationError(f"invalid {name} encoding") from exc
if len(raw) != expected_size:
raise SignatureVerificationError(f"invalid {name} size")
return raw
def _public_key(value: Any) -> Ed25519PublicKey:
if isinstance(value, Ed25519PublicKey):
return value
if isinstance(value, bytes):
raw = value
elif isinstance(value, str):
raw = _decode_urlsafe(value, "trusted public key", 32)
else:
raise ValueError("trusted public key must be Ed25519PublicKey, raw bytes, or base64url")
if len(raw) != 32:
raise ValueError("trusted Ed25519 public key must be 32 bytes")
return Ed25519PublicKey.from_public_bytes(raw)
def _trusted_keys(values: Mapping[str, Any]) -> Mapping[str, Ed25519PublicKey]:
if not isinstance(values, Mapping) or not values:
raise ValueError("trusted_public_keys must be a non-empty mapping")
keys: dict[str, Ed25519PublicKey] = {}
for key_id, value in values.items():
if not isinstance(key_id, str) or not key_id or len(key_id) > 128:
raise ValueError("invalid trusted key_id")
keys[key_id] = _public_key(value)
return MappingProxyType(keys)
def _verified_payload(
envelope: Any,
keys: Mapping[str, Ed25519PublicKey],
*,
schema: str,
domain: bytes,
payload_fields: set[str],
) -> Mapping[str, Any]:
if not isinstance(envelope, Mapping) or set(envelope) != _ENVELOPE_FIELDS:
raise SignatureVerificationError("signed envelope fields invalid")
if envelope.get("schema") != schema:
raise SignatureVerificationError("signed envelope schema mismatch")
key_id = envelope.get("key_id")
if not isinstance(key_id, str) or key_id not in keys:
raise SignatureVerificationError("untrusted signing key_id")
payload = envelope.get("payload")
if not isinstance(payload, Mapping) or set(payload) != payload_fields:
raise SignatureVerificationError("signed payload fields invalid")
signature = _decode_urlsafe(envelope.get("signature"), "signature", 64)
try:
keys[key_id].verify(signature, domain + b"\x00" + _canonical(dict(payload)))
except InvalidSignature as exc:
raise SignatureVerificationError("Ed25519 signature invalid") from exc
return payload
def _text(payload: Mapping[str, Any], name: str) -> str:
value = payload.get(name)
if not isinstance(value, str) or not value or len(value) > 512:
raise SignatureVerificationError(f"signed {name} invalid")
return value
def _expected(payload: Mapping[str, Any], expected: Mapping[str, Any], fields: tuple[str, ...]) -> None:
for name in fields:
if name not in expected:
raise SignatureVerificationError(f"verifier expected {name} missing")
actual = canonical_artifact(payload[name]) if name == "artifact" else payload[name]
wanted = canonical_artifact(expected[name]) if name == "artifact" else expected[name]
if actual != wanted:
raise SignatureVerificationError(f"signed {name} binding mismatch")
def ed25519_release_receipt_verifier(
trusted_public_keys: Mapping[str, Any],
) -> Callable[[Any, Mapping[str, Any]], VerifiedReceipt]:
"""Build a receipt verifier from local/configured public keys only."""
keys = _trusted_keys(trusted_public_keys)
def verify(envelope: Any, expected: Mapping[str, Any]) -> VerifiedReceipt:
payload = _verified_payload(
envelope, keys,
schema="aios.release-operation-receipt-envelope.v1",
domain=RELEASE_RECEIPT_DOMAIN,
payload_fields=_RECEIPT_FIELDS,
)
_expected(
payload, expected,
("tenant_id", "subject_id", "component_id", "task_id", "source_task_id", "source_effect_id",
"grant_id", "job_id", "effect_id", "operation_id", "deployment_id", "release_id", "artifact"),
)
if payload.get("outcome") != "SUCCEEDED":
raise SignatureVerificationError("signed receipt outcome is not SUCCEEDED")
if not isinstance(payload.get("observed"), Mapping):
raise SignatureVerificationError("signed receipt observation invalid")
return VerifiedReceipt(
receipt_id=_text(payload, "receipt_id"), tenant_id=_text(payload, "tenant_id"),
subject_id=_text(payload, "subject_id"), effect_id=_text(payload, "effect_id"),
component_id=_text(payload,"component_id"), task_id=_text(payload,"task_id"),
source_task_id=_text(payload,"source_task_id"), source_effect_id=_text(payload,"source_effect_id"),
grant_id=_text(payload,"grant_id"), job_id=_text(payload,"job_id"),
operation_id=_text(payload, "operation_id"), deployment_id=_text(payload, "deployment_id"),
release_id=_text(payload, "release_id"), artifact=payload["artifact"],
observed=dict(payload["observed"]), outcome="SUCCEEDED",
observed_at=_text(payload, "observed_at"),
)
return verify
def ed25519_acceptance_evidence_verifier(
trusted_public_keys: Mapping[str, Any],
) -> Callable[[Any, Mapping[str, Any]], VerifiedAcceptanceEvidence]:
"""Build an acceptance verifier in a domain separate from stage receipts."""
keys = _trusted_keys(trusted_public_keys)
def verify(envelope: Any, expected: Mapping[str, Any]) -> VerifiedAcceptanceEvidence:
payload = _verified_payload(
envelope, keys,
schema="aios.release-acceptance-evidence-envelope.v1",
domain=ACCEPTANCE_EVIDENCE_DOMAIN,
payload_fields=_ACCEPTANCE_FIELDS,
)
_expected(
payload, expected,
("acceptance_id", "tenant_id", "subject_id", "component_id", "deployment_id", "release_id", "artifact", "accepted"),
)
if type(payload.get("accepted")) is not bool:
raise SignatureVerificationError("signed accepted decision invalid")
if not isinstance(payload.get("observed"), Mapping):
raise SignatureVerificationError("signed acceptance observation invalid")
return VerifiedAcceptanceEvidence(
acceptance_id=_text(payload, "acceptance_id"), tenant_id=_text(payload, "tenant_id"),
subject_id=_text(payload, "subject_id"), deployment_id=_text(payload, "deployment_id"),
component_id=_text(payload,"component_id"),
release_id=_text(payload, "release_id"), artifact=payload["artifact"],
accepted=payload["accepted"], evidence_id=_text(payload, "evidence_id"),
observed=dict(payload["observed"]), observed_at=_text(payload, "observed_at"),
)
return verify
__all__ = [
"ACCEPTANCE_EVIDENCE_DOMAIN", "RELEASE_RECEIPT_DOMAIN",
"SignatureVerificationError", "ed25519_acceptance_evidence_verifier",
"ed25519_release_receipt_verifier",
]
+121
View File
@@ -0,0 +1,121 @@
"""Thin resident worker loop over the authenticated HTTP transport.
Worker policy, operation handlers, effects, receipts, and completion remain in
the trusted server. This process only maintains its registered transport
identity and asks the service to run an offered claim.
"""
from __future__ import annotations
import math
import threading
import time
from typing import Any
from .transport import TransportError
class WorkerError(RuntimeError):
pass
class OperationWorker:
HEARTBEAT_INTERVAL_SECONDS = 15.0
def __init__(self, transport: Any, poll_seconds: float = 1.0, stop_event=None):
required = (
"register_worker", "heartbeat", "claim_next", "renew", "execute_claim"
)
if transport is None or any(not callable(getattr(transport, name, None)) for name in required):
raise WorkerError("WORKER_TRANSPORT_INVALID")
if (
isinstance(poll_seconds, bool)
or not isinstance(poll_seconds, (int, float))
or not math.isfinite(float(poll_seconds))
or not 0.001 <= poll_seconds <= 60
):
raise WorkerError("WORKER_POLL_INTERVAL_INVALID")
if stop_event is not None and (
not callable(getattr(stop_event, "is_set", None))
or not callable(getattr(stop_event, "wait", None))
):
raise WorkerError("WORKER_STOP_EVENT_INVALID")
self.transport = transport
self.poll_seconds = float(poll_seconds)
self.stop_event = stop_event if stop_event is not None else threading.Event()
self.registered = False
self.worker_id: str | None = None
self._last_heartbeat = 0.0
self.last_error: str | None = None
def _register(self) -> None:
result = self.transport.register_worker()
if (
not isinstance(result, dict) or result.get("registered") is not True
or not isinstance(result.get("worker_id"), str) or not result["worker_id"]
):
raise WorkerError("WORKER_REGISTRATION_RESULT_INVALID")
self.worker_id = result["worker_id"]
self.registered = True
def _heartbeat(self, *, force: bool = False) -> None:
now = time.monotonic()
if not force and now < self._last_heartbeat + self.HEARTBEAT_INTERVAL_SECONDS:
return
result = self.transport.heartbeat()
if (
not isinstance(result, dict) or result.get("alive") is not True
or result.get("worker_id") != self.worker_id
):
raise WorkerError("WORKER_HEARTBEAT_RESULT_INVALID")
self._last_heartbeat = time.monotonic()
def run_once(self):
"""Perform one register/heartbeat/claim/renew/execute iteration."""
if self.stop_event.is_set():
return None
just_registered = False
if not self.registered:
self._register()
just_registered = True
self._heartbeat(force=just_registered)
claim = self.transport.claim_next()
if claim is None:
self.last_error = None
return None
required = {"task_id", "claim_id", "generation", "lease_until", "operation"}
if not isinstance(claim, dict) or set(claim) != required:
raise WorkerError("WORKER_CLAIM_RESULT_INVALID")
task_id = claim["task_id"]
claim_id = claim["claim_id"]
if not isinstance(task_id, str) or not task_id or not isinstance(claim_id, str) or not claim_id:
raise WorkerError("WORKER_CLAIM_RESULT_INVALID")
renewed = self.transport.renew(task_id, claim_id)
if (
not isinstance(renewed, dict) or renewed.get("renewed") is not True
or renewed.get("task_id") != task_id or renewed.get("claim_id") != claim_id
):
raise WorkerError("WORKER_RENEW_RESULT_INVALID")
result = self.transport.execute_claim(task_id, claim_id)
if not isinstance(result, dict) or result.get("task_id") != task_id:
raise WorkerError("WORKER_EXECUTION_RESULT_INVALID")
self.last_error = None
return result
def run_forever(self) -> None:
"""Run until the supplied stop event is set, using interruptible waits."""
while not self.stop_event.is_set():
try:
self.run_once()
except TransportError as exc:
# Transport failures are observable and retried on the next
# bounded poll. Re-register after a server restart; programming
# and schema errors still terminate.
self.last_error = exc.code
self.registered = False
self.stop_event.wait(self.poll_seconds)
Worker = OperationWorker
__all__ = ["OperationWorker", "Worker", "WorkerError"]
@@ -0,0 +1,51 @@
"""Registered worker eligibility; fallback is deployment policy, never a special actor."""
from dataclasses import dataclass
import math
from typing import Mapping
@dataclass(frozen=True)
class WorkerProfile:
worker_id: str
identity: str
tenant_scope: tuple[str, ...]
subject_scope: tuple[str, ...]
capabilities: tuple[str, ...]
priority: int
fallback_executor: bool
heartbeat_seconds: int
max_concurrency: int
@classmethod
def from_dict(cls, value: Mapping):
if set(value) != set(cls.__dataclass_fields__):
raise ValueError('WORKER_PROFILE_FIELDS_INVALID')
data = dict(value)
for name in ('worker_id', 'identity'):
if not isinstance(data[name], str) or not data[name] or len(data[name]) > 128:
raise ValueError('WORKER_ID_INVALID')
for name in ('tenant_scope', 'subject_scope', 'capabilities'):
items = data[name]
if not isinstance(items, (list, tuple)) or not items or any(not isinstance(x, str) or not x or '*' in x for x in items):
raise ValueError('WORKER_SCOPE_INVALID')
data[name] = tuple(sorted(set(items)))
if type(data['fallback_executor']) is not bool:
raise ValueError('WORKER_FALLBACK_INVALID')
for name, lower, upper in (('priority', 0, 1000), ('heartbeat_seconds', 1, 300), ('max_concurrency', 1, 32)):
if type(data[name]) is not int or not lower <= data[name] <= upper:
raise ValueError('WORKER_LIMIT_INVALID')
return cls(**data)
def eligible(worker: WorkerProfile, task: Mapping, *, now: float, last_heartbeat: float,
fallback_seconds: int = 600) -> bool:
ready_at = task.get('ready_at')
if any(type(value) not in (int,float) or not math.isfinite(value) for value in (ready_at,now,last_heartbeat)) or ready_at > now:
return False
if last_heartbeat > now or now > last_heartbeat + worker.heartbeat_seconds * 3:
return False
if task.get('tenant_id') not in worker.tenant_scope or task.get('subject_id') not in worker.subject_scope:
return False
if task.get('operation') not in worker.capabilities:
return False
return not worker.fallback_executor or now >= ready_at + fallback_seconds
+102 -3
View File
@@ -24,7 +24,13 @@ from gitea_tag_publication import (
validate_version,
validate_gitea_api_url,
)
from orchestrator.action_monitor import ActionMonitor
try:
from orchestrator.action_monitor import ActionMonitor
except ModuleNotFoundError:
# The deployed service normally supplies runtime/agent-os on sys.path.
# Keep direct, isolated contract execution bound to the repository copy.
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "runtime" / "agent-os"))
from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import (
ApprovalKeyRing,
ApprovalRevocationStore,
@@ -64,6 +70,11 @@ except ModuleNotFoundError:
repository_snapshot,
run_authoritative_git,
)
try:
from aios_repository_discovery import RepositoryDiscovery, RepositoryDiscoveryError
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from aios_repository_discovery import RepositoryDiscovery, RepositoryDiscoveryError
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
@@ -109,6 +120,8 @@ READ_ONLY_GET_PATHS = frozenset({
'/admin/ai-evolution/api/data',
'/api/release-consistency', '/api/tag-publication-status',
'/api/release-acceptance',
'/api/repository/tree', '/api/repository/search',
'/api/repository/diff-index', '/api/repository/file',
'/getReleaseCandidate', '/getReleasePlan', '/getReleaseAcceptance',
'/getApprovalEnvelope', '/getReleaseSaga', '/getCapabilityStatus',
'/getRecoveryStatus',
@@ -342,6 +355,77 @@ def required_authoritative_git_dir() -> Path:
return Path(str(snapshot["authoritative_git_dir"]))
_DISCOVERY_ACTIONS = {
'/api/repository/tree': (
'list_repository_tree',
{'repository', 'ref', 'limit'},
{'prefix', 'depth', 'cursor'},
),
'/api/repository/search': (
'search_repository_source',
{'repository', 'ref', 'query', 'max_matches', 'max_bytes'},
{'path_globs', 'cursor'},
),
'/api/repository/diff-index': (
'get_diff_index',
{'repository', 'from', 'to', 'limit'},
{'path_prefix', 'cursor'},
),
'/api/repository/file': (
'read_repository_file',
{'repository', 'ref', 'path'},
{'max_bytes'},
),
}
def repository_discovery_request(
path: str, query: dict[str, list[str]]
) -> dict[str, Any]:
operation, required, optional = _DISCOVERY_ACTIONS[path]
if set(query) - required - optional:
raise ValueError('unknown repository discovery query field')
request: dict[str, Any] = {}
for key, values in query.items():
if key == 'path_globs':
if not values:
raise ValueError('empty repository discovery path glob')
request[key] = list(values)
continue
if len(values) != 1:
raise ValueError('duplicate repository discovery query field')
value: Any = values[0]
if key in {'depth', 'limit', 'max_matches', 'max_bytes'}:
try:
value = int(value, 10)
except (TypeError, ValueError) as exc:
raise ValueError('invalid repository discovery integer') from exc
request[key] = value
if required - set(request):
raise ValueError('missing repository discovery query field')
if not REPOSITORY_IDENTITY:
raise RepositoryDiscoveryError('REPOSITORY_DISCOVERY_UNAVAILABLE')
try:
discovery = RepositoryDiscovery(
allowed_repositories=(REPOSITORY_IDENTITY,),
git_dir=required_authoritative_git_dir(),
cursor_secret=os.getenv('AIOS_REPOSITORY_DISCOVERY_CURSOR_SECRET'),
)
except RuntimeError as exc:
if isinstance(exc, RepositoryDiscoveryError):
raise
raise RepositoryDiscoveryError('REPOSITORY_DISCOVERY_UNAVAILABLE') from exc
public_to_internal = {
'ref': 'revision',
'path': 'file_path',
'path_prefix': 'prefix',
}
internal_request = {
public_to_internal.get(key, key): value for key, value in request.items()
}
return dict(getattr(discovery, operation)(internal_request))
def publication_publisher() -> TagPublisher:
authoritative_repo = required_authoritative_git_dir()
publisher = TagPublisher(
@@ -441,6 +525,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path in _DISCOVERY_ACTIONS:
return self.sendj({'error': 'method_not_allowed'},405)
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
@@ -456,12 +542,21 @@ class H(BaseHTTPRequestHandler):
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
q=urllib.parse.parse_qs(u.query)
if len(self.path) > 8192:
return self.sendj({'error':'query_too_large'},413)
try:
q=urllib.parse.parse_qs(
u.query, keep_blank_values=True, max_num_fields=64
)
except ValueError:
return self.sendj({'error':'query_invalid'},400)
try:
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
if path in ('/api','/api/status'): return self.sendj(api_spec())
if path in _DISCOVERY_ACTIONS:
return self.sendj(repository_discovery_request(path, q))
if path in ('/api/base','/api/baseline','/base'):
data=jload(WS/'baseline'/'current.json', {})
data.setdefault('authoritative_repository', repository_snapshot(
@@ -648,6 +743,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':'not_found','path':path},404)
except FileNotFoundError:
return self.sendj({'error':'release candidate not found'},404)
except RepositoryDiscoveryError as e:
return self.sendj({'error': redact(e)}, getattr(e, 'status_code', 400))
except ValueError as e:
error=redact(e)
monitor.record_failure('do_GET', error, {'path':path})
@@ -664,6 +761,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if path in _DISCOVERY_ACTIONS:
return self.sendj({'error':'method_not_allowed'},405)
if reject_non_read_http(self, 'POST', path): return
data=self.body()
try:
@@ -960,7 +1059,7 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/repository/tree','GET /api/repository/search','GET /api/repository/diff-index','GET /api/repository/file','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
def main():
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
+122 -16
View File
@@ -6,6 +6,12 @@ import hashlib
from pathlib import Path
from typing import Any, Callable, Mapping
from aios_task_admission_bridge_contract import (
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_ROUTES,
)
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
BRIDGE_OPERATIONS = (
@@ -27,17 +33,23 @@ BRIDGE_OPERATIONS = (
"getReleaseConsistency",
"runReleaseAcceptance",
"getControlPlaneSnapshot",
"listRepositoryTree",
"searchRepositorySource",
"getDiffIndex",
"readRepositoryFile",
)
TASK_ADMISSION_OPERATIONS = tuple(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS)
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"projectOnlineAgentExecution": ("task_id", "required_capabilities"),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
"resumeGitOpsJob": ("job_id", "expected_revision"),
"reconcileGitOpsJob": ("job_id", "expected_revision"),
"acquireWriterLease": ("schema", "task_id", "job_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "lease_seconds"),
"commitFileGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("task_id", "job_id"),
"resumeGitOpsJob": ("task_id", "job_id", "expected_revision"),
"reconcileGitOpsJob": ("task_id", "job_id", "expected_revision"),
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
"getTagPublicationStatus": ("version",),
@@ -46,8 +58,32 @@ OPERATION_REQUIRED_FIELDS = {
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"getControlPlaneSnapshot": (),
**TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
"listRepositoryTree": ("repository", "ref", "limit"),
"searchRepositorySource": ("repository", "ref", "query", "max_matches", "max_bytes"),
"getDiffIndex": ("repository", "from", "to", "limit"),
"readRepositoryFile": ("repository", "ref", "path"),
}
OPERATION_OPTIONAL_FIELDS = {
"listRepositoryTree": ("prefix", "depth", "cursor"),
"searchRepositorySource": ("path_globs", "cursor"),
"getDiffIndex": ("path_prefix", "cursor"),
"readRepositoryFile": ("max_bytes",),
}
READ_ONLY_OPERATIONS = frozenset(
{
"getRuntimeStatus",
"getGovernedWriteCapabilities",
"getControlPlaneSnapshot",
"listRepositoryTree",
"searchRepositorySource",
"getDiffIndex",
"readRepositoryFile",
}
)
_OPERATION_PATHS = {
"createGovernedTask": "/v2/governed-task",
"projectOnlineAgentExecution": "/v2/execution-envelope",
@@ -60,19 +96,54 @@ _OPERATION_PATHS = {
"getTagPublicationStatus": "/v2/tag/status",
"getReleaseConsistency": "/v2/release/consistency",
"runReleaseAcceptance": "/v2/release/acceptance",
**{operation: path for path, operation in TASK_ADMISSION_ROUTES.items()},
"listRepositoryTree": "/v2/repository/tree",
"searchRepositorySource": "/v2/repository/search",
"getDiffIndex": "/v2/repository/diff-index",
"readRepositoryFile": "/v2/repository/file",
}
def operation_contracts() -> dict[str, dict[str, object]]:
def _has_complete_task_admission_handlers(
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None,
) -> bool:
return handlers is not None and all(
callable(handlers.get(operation)) for operation in TASK_ADMISSION_OPERATIONS
)
def operation_contracts(
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
) -> dict[str, dict[str, object]]:
"""Return the runtime contract consumed by adapters and schema tooling."""
return {
contracts = {
operation: {
"method": "POST",
"method": "GET" if operation in OPERATION_OPTIONAL_FIELDS else "POST",
"path": _OPERATION_PATHS.get(operation),
"required_fields": list(fields),
"optional_fields": list(OPERATION_OPTIONAL_FIELDS.get(operation, ())),
"read_only": operation in READ_ONLY_OPERATIONS,
}
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
if operation not in TASK_ADMISSION_OPERATIONS
}
if _has_complete_task_admission_handlers(handlers):
contracts.update(
{
operation: {
"method": "GET" if operation == "getTaskAdmissionStatus" else "POST",
"path": _OPERATION_PATHS[operation],
"required_fields": list(
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation]
),
"optional_fields": list(
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
),
}
for operation in TASK_ADMISSION_OPERATIONS
}
)
return contracts
def render_openapi_contract_fragment() -> str:
@@ -85,6 +156,8 @@ def render_openapi_contract_fragment() -> str:
" post:\n"
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" security: [{taskPrincipalAuth: []}]\n"
" x-aios-task-principal: Bridge resolves a fresh server-side TaskPrincipal from the authoritative admitted+claimed Task and re-verifies its Owner epoch, then checks exact task/job/repository/branch/generation/capability binding; caller JSON cannot provide a principal and missing verification is denied. An explicitly injected legacy bearer verifier may still be used by a trusted host.\n"
" requestBody:\n"
" required: true\n"
" content:\n"
@@ -116,8 +189,12 @@ def render_openapi_contract_fragment() -> str:
" message: {type: string, minLength: 1, maxLength: 200}\n"
" responses:\n"
" '200': {description: Governed commit job and verified branch result}\n"
" '202': {description: Existing external Job alias requires authoritative reconciliation; no replacement Job is created}\n"
" '401': {description: No verified Task principal is available}\n"
" '403': {description: Verified Task principal lacks exact task, external Job, or capability scope}\n"
" '400': {description: Contract or policy rejection}\n"
" '409': {description: Branch CAS or idempotency conflict}\n"
" '423': {description: Narrow P0 Task-principal mutation gate is frozen; assertion is not consumed}\n"
)
@@ -183,20 +260,38 @@ class ControlPlaneReleaseManifest:
class BridgeV2Adapter:
"""Thin operation adapter; all mutating handlers are injected seams."""
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self.runtime_status = dict(runtime_status)
def __init__(self, *, runtime_status: Mapping[str, Any] | Callable[[], Mapping[str, Any]], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self._runtime_status_source = runtime_status if callable(runtime_status) else dict(runtime_status)
self.handlers = dict(handlers or {})
def _runtime_status(self) -> dict[str, Any]:
value = self._runtime_status_source() if callable(self._runtime_status_source) else self._runtime_status_source
if not isinstance(value, Mapping):
raise BridgeV2Error("RUNTIME_STATUS_INVALID")
return dict(value)
def getRuntimeStatus(self) -> dict[str, Any]:
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self._runtime_status()}
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
runtime_status = self._runtime_status()
governed_writes_enabled = runtime_status.get("governed_writes_enabled") is True
return {
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": operation_contracts(),
"governed_writes_enabled": governed_writes_enabled,
"governed_write_blockers": list(
runtime_status.get("governed_write_blockers") or []
),
"operations": [
*BRIDGE_OPERATIONS,
*(
TASK_ADMISSION_OPERATIONS
if _has_complete_task_admission_handlers(self.handlers)
else ()
),
],
"operation_contracts": operation_contracts(self.handlers),
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -213,12 +308,16 @@ class BridgeV2Adapter:
@staticmethod
def legacy_write_disabled() -> dict[str, object]:
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": True}
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": False}
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
required = OPERATION_REQUIRED_FIELDS[operation]
if any(key not in request for key in required):
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_MISSING")
if operation in OPERATION_OPTIONAL_FIELDS:
allowed = set(required) | set(OPERATION_OPTIONAL_FIELDS[operation])
if set(request) - allowed:
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_UNKNOWN")
handler = self.handlers.get(operation)
if handler is None:
raise BridgeV2Error("GOVERNED_HANDLER_UNAVAILABLE")
@@ -239,5 +338,12 @@ class BridgeV2Adapter:
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
def getTaskAdmissionEnvelope(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionEnvelope", request)
def submitTaskAdmissionAuthorization(self, request: Mapping[str, Any]) -> Any: return self._call("submitTaskAdmissionAuthorization", request)
def getTaskAdmissionStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionStatus", request)
def listRepositoryTree(self, request: Mapping[str, Any]) -> Any: return self._call("listRepositoryTree", request)
def searchRepositorySource(self, request: Mapping[str, Any]) -> Any: return self._call("searchRepositorySource", request)
def getDiffIndex(self, request: Mapping[str, Any]) -> Any: return self._call("getDiffIndex", request)
def readRepositoryFile(self, request: Mapping[str, Any]) -> Any: return self._call("readRepositoryFile", request)
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
@@ -11,19 +11,40 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import re
import sqlite3
from typing import Any, Mapping
import stat
from typing import Any, Callable, Mapping
from urllib.parse import parse_qsl, urlsplit
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2 import (
BridgeV2Adapter,
BridgeV2Error,
OPERATION_OPTIONAL_FIELDS,
OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_OPERATIONS,
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
TASK_ADMISSION_ROUTES,
)
from aios_repository_discovery import RepositoryDiscovery, RepositoryDiscoveryError
from aios_task_admission_bootstrap import TaskAdmissionError
from gitea_tag_publication import validate_gitea_api_url
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore, _RECOVERABLE, sha256_json
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from aios_governed_task_bootstrap import (
BootstrapError,
GovernedTaskBinding,
GovernedTaskBootstrap,
)
from governed_production_release import build_executor_config
from aios_baseline.runtime_contract import evaluate_deployment
MAX_BODY = 2 * 1024 * 1024
TaskPrincipalVerifier = Callable[
[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any] | None
]
_PRIVATE_PRINCIPAL = "_verified_task_principal"
ROUTES = {
"/v2/runtime": "getRuntimeStatus",
"/v2/capabilities": "getGovernedWriteCapabilities",
@@ -42,8 +63,27 @@ ROUTES = {
"/v2/release/execute": "executeApprovedProductionRelease",
"/v2/release/consistency": "getReleaseConsistency",
"/v2/release/acceptance": "runReleaseAcceptance",
"/v2/task-admission/envelope": "getTaskAdmissionEnvelope",
"/v2/task-admission/authorize": "submitTaskAdmissionAuthorization",
"/v2/task-admission/status": "getTaskAdmissionStatus",
"/v2/repository/tree": "listRepositoryTree",
"/v2/repository/search": "searchRepositorySource",
"/v2/repository/diff-index": "getDiffIndex",
"/v2/repository/file": "readRepositoryFile",
}
# Admission is deliberately separate from a TaskPrincipal. A successful
# admission can create only its durable, Owner-authorized Task record; every
# Git-mutating operation still requires a verified TaskPrincipal and this
# independently evaluated runtime gate.
_TASK_PRINCIPAL_MUTATING_OPERATIONS = frozenset({
"acquireWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"resumeGitOpsJob",
"reconcileGitOpsJob",
})
@dataclass(frozen=True, slots=True)
class RuntimeConfig:
@@ -63,12 +103,84 @@ class RuntimeConfig:
baseline_path: Path = Path("/var/lib/aios-gitops-runtime-anchor/baseline/current.json")
health_url: str = "http://127.0.0.1:18927/health"
release_plan_path: Path | None = None
repository_discovery_cursor_secret: str | bytes | None = None
def _safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
def _unknown_production() -> dict[str, Any]:
return {
"baseline_version": None,
"target_oid": None,
"deployment_state": "UNKNOWN",
}
def load_production_observation(
path: Path,
now: datetime | str | None = None,
) -> dict[str, Any]:
"""Project production fields only from a fresh validated observation bundle."""
try:
metadata = path.lstat()
if (
stat.S_ISLNK(metadata.st_mode)
or not stat.S_ISREG(metadata.st_mode)
or metadata.st_mode & 0o022
or metadata.st_size < 2
or metadata.st_size > 1024 * 1024
):
return _unknown_production()
def strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError("PRODUCTION_OBSERVATION_DUPLICATE_FIELD")
result[key] = value
return result
bundle = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=strict_object)
except (OSError, UnicodeError, ValueError, TypeError):
return _unknown_production()
if not isinstance(bundle, dict) or set(bundle) != {
"contract",
"target",
"observation",
"provenance",
}:
return _unknown_production()
try:
result = evaluate_deployment(
bundle["contract"],
bundle["target"],
bundle["observation"],
bundle["provenance"],
now or datetime.now(timezone.utc),
)
except (TypeError, ValueError):
return _unknown_production()
target = bundle["target"]
release_id = target.get("release_id") if isinstance(target, Mapping) else None
target_oid = target.get("target_oid") if isinstance(target, Mapping) else None
if (
result.get("status") != "VERIFIED"
or not isinstance(release_id, str)
or not release_id.strip()
or not isinstance(target_oid, str)
or re.fullmatch(r"[0-9a-f]{40}", target_oid) is None
):
return _unknown_production()
return {
"baseline_version": release_id,
"target_oid": target_oid,
"deployment_state": "ACTIVE",
}
class GovernedRuntime:
def __init__(self, config: RuntimeConfig) -> None:
self.config = config
@@ -79,6 +191,7 @@ class GovernedRuntime:
self.controllers: dict[str, GitOpsJobController] = {}
self.bindings: dict[str, dict[str, Any]] = {}
self.release = build_executor_config(config)
self._repository_discovery_instance: RepositoryDiscovery | None = None
metadata_root = config.state_root / "task-bindings"
if metadata_root.is_dir():
for metadata in metadata_root.glob("*.json"):
@@ -95,6 +208,34 @@ class GovernedRuntime:
os.environ.setdefault("GIT_ASKPASS", str(askpass)); os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force"); os.environ.setdefault("GIT_TERMINAL_PROMPT", "0"); os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
def runtime_status(self) -> dict[str, Any]:
verifier = BridgeHandler.principal_verifier
readiness_reader = getattr(verifier, "readiness", None)
if callable(readiness_reader):
try:
principal_readiness = dict(readiness_reader())
except Exception:
principal_readiness = {
"ready": False,
"error": "TASK_PRINCIPAL_SEALED_READINESS_UNAVAILABLE",
}
else:
principal_readiness = {
"ready": False,
"error": "TASK_PRINCIPAL_SEALED_READINESS_UNAVAILABLE",
}
legacy_global_writes_enabled = os.environ.get(
"AIOS_GITOPS_WRITES_ENABLED", "false"
).strip().lower() in {"1", "true", "yes", "on"}
task_principal_writes_enabled = os.environ.get(
"AIOS_P0_TASK_PRINCIPAL_WRITES_ENABLED", "false"
).strip().lower() in {"1", "true", "yes", "on"}
blockers: list[str] = []
if principal_readiness.get("ready") is not True:
blockers.append("TASK_PRINCIPAL_VERIFIER_NOT_READY")
if legacy_global_writes_enabled:
blockers.append("LEGACY_GLOBAL_WRITES_MUST_REMAIN_DISABLED")
if not task_principal_writes_enabled:
blockers.append("TASK_PRINCIPAL_WRITES_DISABLED")
return {
"status": "active",
"health": 200,
@@ -109,9 +250,100 @@ class GovernedRuntime:
"repository": self.config.repository,
"repository_identity": self.config.repository,
"authoritative_git_dir": str(self.config.authoritative_git_dir),
"legacy_global_writes_enabled": False,
"legacy_global_writes_enabled": legacy_global_writes_enabled,
"task_principal_writes_enabled": task_principal_writes_enabled,
"principal_verifier_configured": verifier is not None,
"task_principal_readiness": principal_readiness,
"governed_writes_enabled": not blockers,
"governed_write_blockers": blockers,
}
def project_admitted_task(
self,
grant: Any,
task_result: Mapping[str, Any],
) -> Mapping[str, Any]:
"""Durably bind one verified admission to its exact local Job input.
This hook is called only after the Agent Task API returns an exact
readback. It writes a local binding, not a Job or a lease; later
TaskPrincipal-gated continuation remains responsible for every Git
side effect.
"""
task_id = str(getattr(grant, "task_id", ""))
repository = str(getattr(grant, "repository", ""))
base_oid = str(getattr(grant, "authority_base_oid", ""))
target_branch = str(getattr(grant, "target_branch", ""))
risk = str(getattr(grant, "risk", ""))
generation = int(getattr(grant, "generation", 0))
external_job_id = str(task_result.get("external_job_id") or "")
if (
not task_id
or repository != self.config.repository
or not base_oid
or not target_branch
or generation < 1
or re.fullmatch(r"job-[0-9a-f]{32}", external_job_id) is None
):
raise RuntimeError("TASK_ADMISSION_PROJECTION_COORDINATES_INVALID")
projection_key = "task-admission:" + task_id
description = (
"P0 governed execution projection for officially admitted Agent Task."
)
try:
existing = self.bootstrap.get_binding(task_id)
except BootstrapError as exc:
if str(exc) != "TASK_EXECUTION_PROJECTION_MISSING":
raise
existing = None
if existing is not None:
if (
existing.repository != repository
or existing.base_oid != base_oid
or existing.target_branch != target_branch
or existing.risk != risk
or existing.description != description
or existing.idempotency_key != projection_key
or existing.generation != generation
or existing.job_id != external_job_id
):
raise RuntimeError("TASK_ADMISSION_PROJECTION_BINDING_CONFLICT")
result = existing.response(idempotent=True)
else:
if not self.authority.commit_exists(base_oid):
raise RuntimeError("TASK_ADMISSION_PROJECTION_BASE_NOT_FOUND")
current_branch_oid = self.authority.read_ref(
"refs/heads/" + target_branch
)
if current_branch_oid is None:
branch_mode = "CREATE_NEW"
expected_branch_oid = base_oid
elif current_branch_oid == base_oid:
branch_mode = "CONTINUE_EXISTING"
expected_branch_oid = current_branch_oid
else:
raise RuntimeError("TASK_ADMISSION_PROJECTION_BRANCH_CONFLICT")
binding = GovernedTaskBinding(
task_id=task_id,
repository=repository,
base_oid=base_oid,
target_branch=target_branch,
risk=risk,
description=description,
idempotency_key=projection_key,
expected_branch_oid=expected_branch_oid,
branch_mode=branch_mode,
generation=generation,
job_id=external_job_id,
)
self.bootstrap._write(binding)
readback = self.bootstrap.get_binding(task_id)
if readback != binding:
raise RuntimeError("TASK_ADMISSION_PROJECTION_READBACK_MISMATCH")
result = readback.response()
self.bindings[task_id] = dict(result)
return result
def _job_root(self, task_id: str) -> Path:
return self.config.state_root / "jobs" / _safe(task_id)
@@ -175,6 +407,32 @@ class GovernedRuntime:
)
result = self.bootstrap.create(request); self.bindings[result["task_id"]] = result; return result
def _repository_discovery(self) -> RepositoryDiscovery:
if self._repository_discovery_instance is None:
self._repository_discovery_instance = RepositoryDiscovery(
allowed_repositories=(self.config.repository,),
git_dir=self.config.authoritative_git_dir,
cursor_secret=(
self.config.repository_discovery_cursor_secret
if self.config.repository_discovery_cursor_secret is not None
else os.environ.get("AIOS_REPOSITORY_DISCOVERY_CURSOR_SECRET")
),
)
return self._repository_discovery_instance
def repository_discovery(
self, operation: str, request: Mapping[str, Any]
) -> dict[str, Any]:
method = {
"listRepositoryTree": "list_repository_tree",
"searchRepositorySource": "search_repository_source",
"getDiffIndex": "get_diff_index",
"readRepositoryFile": "read_repository_file",
}.get(operation)
if method is None:
raise ValueError("REPOSITORY_DISCOVERY_OPERATION_INVALID")
return getattr(self._repository_discovery(), method)(request)
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
replay = self._durable_completed_replay_lease(request)
if replay is not None:
@@ -732,7 +990,11 @@ class GovernedRuntime:
def snapshot(self) -> dict[str, Any]:
task = self._snapshot_task()
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
runtime = self.runtime_status()
production = load_production_observation(
self.config.state_root / "production-observation.json"
)
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=runtime, repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production=production, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": runtime["governed_writes_enabled"], "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
def handlers(self) -> dict[str, Any]:
return {
@@ -745,6 +1007,18 @@ class GovernedRuntime:
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
"getControlPlaneSnapshot": lambda request: self.snapshot(),
"listRepositoryTree": lambda request: self.repository_discovery(
"listRepositoryTree", request
),
"searchRepositorySource": lambda request: self.repository_discovery(
"searchRepositorySource", request
),
"getDiffIndex": lambda request: self.repository_discovery(
"getDiffIndex", request
),
"readRepositoryFile": lambda request: self.repository_discovery(
"readRepositoryFile", request
),
"createTagApprovalRequest": self.release.approval_request,
"requestOrCreateTag": self.release.publish_tag,
"getTagPublicationStatus": self.release.tag_status,
@@ -760,47 +1034,284 @@ def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -
return GovernedRuntime(runtime_config).handlers()
def build_bridge_adapter(
runtime: GovernedRuntime,
*,
task_admission_handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
) -> BridgeV2Adapter:
handlers = dict(runtime.handlers())
if task_admission_handlers is not None:
if (
set(task_admission_handlers) != set(TASK_ADMISSION_OPERATIONS)
or any(
not callable(task_admission_handlers.get(operation))
for operation in TASK_ADMISSION_OPERATIONS
)
):
raise ValueError("TASK_ADMISSION_HANDLERS_INCOMPLETE")
handlers.update(task_admission_handlers)
return BridgeV2Adapter(runtime_status=runtime.runtime_status, handlers=handlers)
class _HTTPError(RuntimeError): pass
class BridgeHandler(BaseHTTPRequestHandler):
adapter: BridgeV2Adapter | None = None
principal_verifier: TaskPrincipalVerifier | None = None
def log_message(self, *_args: Any) -> None: return
def _json(self, value: Any, status: int = 200) -> None:
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode(); self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
@staticmethod
def _result_status(value: Any) -> int:
status = value.get("http_status") if isinstance(value, Mapping) else None
return status if isinstance(status, int) and 100 <= status <= 599 else 200
def _body(self) -> dict[str, Any]:
try: length = int(self.headers.get("Content-Length", "-1"))
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
value = json.loads(self.rfile.read(length).decode("utf-8"))
def strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise _HTTPError("JSON_DUPLICATE_FIELD")
value[key] = item
return value
value = json.loads(
self.rfile.read(length).decode("utf-8"),
object_pairs_hook=strict_object,
)
if not isinstance(value, dict): raise _HTTPError("JSON_OBJECT_REQUIRED")
return value
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode", "source_lease_epoch"}
if operation in TASK_ADMISSION_OPERATIONS:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | set(
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
)
elif operation in OPERATION_OPTIONAL_FIELDS:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | set(
OPERATION_OPTIONAL_FIELDS[operation]
)
else:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "expected_revision", "generation", "branch_mode", "source_lease_epoch"}
if operation == "projectOnlineAgentExecution": allowed.add("execution")
if operation == "commitFilesGoverned": allowed.add("files")
allowed.update({"owner_approval", "execute", "job_id", "historical_lease_head_oid"})
if operation not in TASK_ADMISSION_OPERATIONS and operation not in OPERATION_OPTIONAL_FIELDS:
allowed.update({"owner_approval", "execute", "job_id", "historical_lease_head_oid"})
if "principal" in body or _PRIVATE_PRINCIPAL in body:
raise _HTTPError("CALLER_PRINCIPAL_FORBIDDEN")
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
def _verified_request(self, operation: str, body: Mapping[str, Any]) -> dict[str, Any]:
protected = {
"createGovernedTask", "projectOnlineAgentExecution",
"acquireWriterLease", "commitFileGoverned", "commitFilesGoverned",
"getGitOpsJob", "resumeGitOpsJob", "reconcileGitOpsJob",
}
if operation not in protected:
return dict(body)
if operation in _TASK_PRINCIPAL_MUTATING_OPERATIONS:
adapter = self.adapter
if adapter is None:
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED")
try:
runtime = adapter.getRuntimeStatus()
except Exception as exc:
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED") from exc
if (
not isinstance(runtime, Mapping)
or runtime.get("governed_writes_enabled") is not True
or runtime.get("legacy_global_writes_enabled") is not False
or runtime.get("task_principal_writes_enabled") is not True
):
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED")
verifier = type(self).principal_verifier
if verifier is None:
raise _HTTPError("TASK_PRINCIPAL_VERIFIER_UNAVAILABLE")
get_all = getattr(self.headers, "get_all", None)
if callable(get_all):
authorization_values = get_all("Authorization") or []
else:
authorization = self.headers.get("Authorization")
authorization_values = [] if authorization is None else [authorization]
transport = {
"operation": operation,
"client_address": self.client_address[0] if self.client_address else None,
"headers": {
"authorization": authorization_values[0] if len(authorization_values) == 1 else None,
"x-request-id": self.headers.get("X-Request-Id"),
},
}
principal = verifier(transport, dict(body))
if not isinstance(principal, Mapping):
raise _HTTPError("TASK_PRINCIPAL_REJECTED")
return {**body, _PRIVATE_PRINCIPAL: dict(principal)}
def _query_body(self, operation: str) -> dict[str, Any]:
parsed = urlsplit(self.path)
if len(self.path) > 8192:
raise _HTTPError("QUERY_TOO_LARGE")
try:
pairs = parse_qsl(
parsed.query,
keep_blank_values=True,
strict_parsing=False,
max_num_fields=64,
)
except ValueError as exc:
raise _HTTPError("QUERY_INVALID") from exc
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | set(
OPERATION_OPTIONAL_FIELDS[operation]
)
result: dict[str, Any] = {}
for key, value in pairs:
if key not in allowed:
raise _HTTPError("UNKNOWN_FIELDS")
if key == "path_globs":
result.setdefault(key, []).append(value)
continue
if key in result:
raise _HTTPError("DUPLICATE_FIELD")
if key in {"depth", "limit", "max_matches", "max_bytes"}:
try:
value = int(value, 10)
except (TypeError, ValueError) as exc:
raise _HTTPError("INVALID_INTEGER") from exc
result[key] = value
if set(OPERATION_REQUIRED_FIELDS[operation]) - set(result):
raise _HTTPError("MISSING_FIELDS")
return result
def do_GET(self) -> None:
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
parsed = urlsplit(self.path)
if parsed.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
if parsed.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
if parsed.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
if parsed.path == "/v2/task-admission/status":
if not callable(self.adapter.handlers.get("getTaskAdmissionStatus")):
self._json({"error": "NOT_FOUND"}, 404)
return
try:
parameters = parse_qsl(parsed.query, keep_blank_values=True)
if (
len(parameters) != 1
or parameters[0][0] != "envelope_id"
or not parameters[0][1]
):
raise _HTTPError("TASK_ADMISSION_QUERY_INVALID")
result = self.adapter.getTaskAdmissionStatus(
{"envelope_id": parameters[0][1]}
)
self._json(result, self._result_status(result))
except TaskAdmissionError as exc:
self._json({"error": str(exc)}, exc.status)
except _HTTPError as exc:
self._json({"error": str(exc)}, 400)
except (ValueError, BridgeV2Error) as exc:
self._json({"error": str(exc)}, 400)
except Exception as exc:
self._json({"error": str(exc) or type(exc).__name__}, 503)
return
operation = ROUTES.get(parsed.path)
if operation in OPERATION_OPTIONAL_FIELDS:
try:
body = self._query_body(operation)
self._allowed(operation, body)
result = getattr(self.adapter, operation)(body)
self._json(result, self._result_status(result))
except _HTTPError as exc:
status = 413 if str(exc) == "QUERY_TOO_LARGE" else 400
self._json({"error": str(exc)}, status)
except RepositoryDiscoveryError as exc:
self._json({"error": exc.code}, exc.status_code)
except (BridgeV2Error, ValueError) as exc:
self._json({"error": str(exc)}, 400)
except Exception as exc:
self._json({"error": str(exc) or type(exc).__name__}, 503)
return
self._json({"error": "NOT_FOUND"}, 404)
def do_POST(self) -> None:
operation = ROUTES.get(self.path)
operation = ROUTES.get(urlsplit(self.path).path)
if operation in OPERATION_OPTIONAL_FIELDS:
self._json({"error": "METHOD_NOT_ALLOWED"}, 405)
return
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities") or self.adapter is None: self._json({"error": "NOT_FOUND"}, 404); return
if operation in TASK_ADMISSION_OPERATIONS and not callable(self.adapter.handlers.get(operation)):
self._json({"error": "NOT_FOUND"}, 404)
return
try:
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
body = self._body(); self._allowed(operation, body)
result = getattr(self.adapter, operation)(self._verified_request(operation, body))
self._json(result, self._result_status(result))
except _HTTPError as exc:
error = str(exc)
if error == "REQUEST_TOO_LARGE":
status = 413
elif error == "TASK_PRINCIPAL_WRITES_DISABLED":
status = 423
else:
status = 401 if error.startswith("TASK_PRINCIPAL_") else 400
self._json({"error": error}, status)
except TaskAdmissionError as exc: self._json({"error": str(exc)}, exc.status)
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
except Exception as exc:
error = str(exc) or type(exc).__name__
if error.startswith("TASK_PRINCIPAL_"):
status = 403
elif any(token in error for token in ("CONFLICT", "EXPECTATION", "MONOTONIC", "REVISION_CAS")):
status = 409
else:
status = 503
self._json({"error": error}, status)
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
def main() -> None:
def main(
*,
task_admission_handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
task_principal_verifier: TaskPrincipalVerifier | None = None,
) -> None:
activation_raw = os.environ.get(
"AIOS_TASK_ADMISSION_ACTIVATION_FILE",
"/var/lib/aios-gitops/bridge-v2-staging/task-admission-activation.json",
).strip()
discovery_secret_file = os.environ.get(
"AIOS_REPOSITORY_DISCOVERY_CURSOR_SECRET_FILE", ""
).strip()
if discovery_secret_file:
secret_path = Path(discovery_secret_file)
try:
secret_metadata = secret_path.lstat()
if (
stat.S_ISLNK(secret_metadata.st_mode)
or not stat.S_ISREG(secret_metadata.st_mode)
or secret_metadata.st_mode & 0o022
or secret_metadata.st_size > 4096
):
raise RuntimeError("REPOSITORY_DISCOVERY_CURSOR_SECRET_INVALID")
discovery_cursor_secret: str | bytes | None = secret_path.read_bytes()
except (OSError, RuntimeError) as exc:
if str(exc) == "REPOSITORY_DISCOVERY_CURSOR_SECRET_INVALID":
raise
raise RuntimeError(
"REPOSITORY_DISCOVERY_CURSOR_SECRET_UNAVAILABLE"
) from exc
if not discovery_cursor_secret:
raise RuntimeError("REPOSITORY_DISCOVERY_CURSOR_SECRET_EMPTY")
else:
discovery_cursor_secret = os.environ.get(
"AIOS_REPOSITORY_DISCOVERY_CURSOR_SECRET"
)
state_root = Path("/var/lib/aios-gitops/bridge-v2-staging")
token_path = Path(os.environ.get("AIOS_GITEA_TOKEN_FILE", "/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token"))
ssh_key_path = Path(os.environ.get("AIOS_GITEA_SSH_KEY_PATH", "/home/aiman/.ssh/id_ed25519_codex_tmp"))
@@ -820,10 +1331,41 @@ def main() -> None:
"AIOS_GITOPS_BASELINE_AUTHORITY",
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
)),
repository_discovery_cursor_secret=discovery_cursor_secret,
)
runtime = GovernedRuntime(config)
# Prefer a server-side TaskPrincipal provider recovered from the same
# verified Task-admission runtime. This removes the caller-side credential
# bootstrap loop while keeping every mutation dependent on a currently
# claimed Task and a freshly re-verified Owner epoch. Hosts may still
# inject an independently authenticated verifier explicitly.
BridgeHandler.principal_verifier = task_principal_verifier
if task_admission_handlers is None:
admission_runtime = None
if os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_environment,
)
admission_runtime = build_task_admission_runtime_from_environment(
post_admission=runtime.project_admitted_task,
)
elif activation_raw and Path(activation_raw).is_file():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_activation_file,
)
admission_runtime = build_task_admission_runtime_from_activation_file(
Path(activation_raw),
post_admission=runtime.project_admitted_task,
)
if admission_runtime is not None:
task_admission_handlers = admission_runtime.handlers
if BridgeHandler.principal_verifier is None:
BridgeHandler.principal_verifier = admission_runtime.principal_verifier
serve(
BridgeV2Adapter(runtime_status=runtime.runtime_status(), handlers=runtime.handlers()),
build_bridge_adapter(
runtime,
task_admission_handlers=task_admission_handlers,
),
port=int(os.environ.get("AIOS_BRIDGE_V2_PORT", "18892")),
)
@@ -24,6 +24,25 @@ def _safe_oid(value: str, field: str) -> str:
return value
def _safe_branch(value: str) -> str:
branch = str(value or "")
if (
not branch
or len(branch) > 240
or branch != branch.strip()
or branch.startswith(("/", "-", "."))
or branch.endswith(("/", ".", ".lock"))
or ".." in branch
or "//" in branch
or "@{" in branch
or "\\" in branch
or any(part in {"", ".", ".."} for part in branch.split("/"))
or any(ch not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._/-" for ch in branch)
):
raise GiteaTransportError("BRANCH_INVALID")
return branch
@dataclass(slots=True)
class GiteaMachineTransport:
api_base: str
@@ -91,6 +110,7 @@ class GiteaMachineTransport:
return self.repository_info()
def read_branch(self, branch: str) -> str | None:
branch = _safe_branch(branch)
owner, name = self.repository_id.split("/", 1)
try:
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/branches/{quote(branch, safe='/')}")[1]
@@ -136,6 +156,99 @@ class GiteaMachineTransport:
finally:
askpass.unlink(missing_ok=True)
def fetch_branch_exact(self, branch: str, expected_oid: str) -> str:
"""Fetch one exact remote branch tip into a quarantine ref.
This never updates ``main`` or any caller-visible branch. The remote
ref is read before and after the fetch so a concurrent branch move
cannot be silently admitted.
"""
branch = _safe_branch(branch)
expected_oid = _safe_oid(expected_oid, "expected_oid")
remote_before = self.read_branch(branch)
if remote_before != expected_oid:
raise GiteaTransportError("GITEA_SOURCE_REF_MISMATCH")
target_ref = f"refs/aios/release-sources/{expected_oid}"
current = subprocess.run(
[
"git",
f"--git-dir={self.authority_git_dir}",
"rev-parse",
"--verify",
f"{target_ref}^{{commit}}",
],
text=True,
capture_output=True,
check=False,
timeout=self.timeout,
)
if current.returncode == 0 and current.stdout.strip().lower() == expected_oid:
remote_after = self.read_branch(branch)
if remote_after != expected_oid:
raise GiteaTransportError("GITEA_SOURCE_REF_MOVED")
return expected_oid
askpass, additions = self._askpass()
environment = os.environ.copy()
environment.update(additions)
try:
result = subprocess.run(
[
"git",
f"--git-dir={self.authority_git_dir}",
"fetch",
"--no-tags",
"--no-write-fetch-head",
self.remote_url,
f"refs/heads/{branch}:{target_ref}",
],
text=True,
capture_output=True,
check=False,
env=environment,
timeout=self.timeout,
)
if result.returncode != 0:
raise GiteaTransportError("GITEA_SOURCE_FETCH_FAILED")
finally:
askpass.unlink(missing_ok=True)
local = subprocess.run(
[
"git",
f"--git-dir={self.authority_git_dir}",
"rev-parse",
"--verify",
f"{target_ref}^{{commit}}",
],
text=True,
capture_output=True,
check=False,
timeout=self.timeout,
)
local_oid = local.stdout.strip().lower()
if local.returncode != 0 or local_oid != expected_oid:
subprocess.run(
[
"git",
f"--git-dir={self.authority_git_dir}",
"update-ref",
"-d",
target_ref,
],
text=True,
capture_output=True,
check=False,
timeout=self.timeout,
)
raise GiteaTransportError("GITEA_SOURCE_FETCH_MISMATCH")
remote_after = self.read_branch(branch)
if remote_after != expected_oid:
raise GiteaTransportError("GITEA_SOURCE_REF_MOVED")
return expected_oid
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str:
_safe_oid(expected_oid, "expected_oid")
_safe_oid(new_oid, "new_oid")
+581 -21
View File
@@ -9,13 +9,14 @@ from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import base64
import hashlib
import json
import os
from pathlib import Path
import sqlite3
import tempfile
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any, Callable, Mapping
@@ -61,18 +62,52 @@ _ORDER = {
JobState.COMPLETED: 8,
}
_RECOVERABLE = {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}
_RECONCILABLE = _RECOVERABLE | {
JobState.RECONCILING,
JobState.EXECUTING,
JobState.LOCAL_VERIFIED,
JobState.REMOTE_SYNCING,
JobState.REMOTE_VERIFIED,
}
_TERMINAL = {
JobState.COMPLETED,
JobState.FAILED,
JobState.CANCELLED,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
}
_GENERIC_TRANSITIONS = {
JobState.REQUESTED: {JobState.AUTHORIZED, JobState.CANCELLED},
JobState.AUTHORIZED: {JobState.LEASED, JobState.CANCELLED},
JobState.LEASED: {JobState.WORKTREE_READY, JobState.EXECUTING, JobState.CANCELLED},
JobState.WORKTREE_READY: {JobState.EXECUTING, JobState.CANCELLED},
JobState.EXECUTING: {JobState.LOCAL_VERIFIED},
JobState.LOCAL_VERIFIED: {JobState.COMPLETED},
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _utc_datetime(value: str | datetime) -> datetime:
if isinstance(value, datetime):
parsed = value
elif isinstance(value, str):
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise JobError("TIMESTAMP_INVALID") from exc
else:
raise JobError("TIMESTAMP_INVALID")
if parsed.tzinfo is None:
raise JobError("TIMESTAMP_TIMEZONE_REQUIRED")
return parsed.astimezone(timezone.utc)
def _iso_utc(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
@@ -273,6 +308,27 @@ class JobStore:
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS resource_leases (
lease_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
resource_kind TEXT NOT NULL, resource_id TEXT NOT NULL,
generation INTEGER NOT NULL, lease_expires_at TEXT NOT NULL,
state TEXT NOT NULL, revision INTEGER NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS resource_fences (
resource_kind TEXT NOT NULL, resource_id TEXT NOT NULL,
generation INTEGER NOT NULL, job_id TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY(resource_kind,resource_id),
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS reconciliation_claims (
job_id TEXT PRIMARY KEY, claim_id TEXT NOT NULL UNIQUE,
reconcile_epoch INTEGER NOT NULL, base_revision INTEGER NOT NULL,
lease_expires_at TEXT NOT NULL, state TEXT NOT NULL,
revision INTEGER NOT NULL, updated_at TEXT NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS completed_replay_reconciliations (
reconciliation_id TEXT PRIMARY KEY, job_id TEXT NOT NULL,
identity_digest TEXT NOT NULL UNIQUE,
@@ -579,11 +635,62 @@ class JobStore:
class GitOpsJobController:
def __init__(self, store: JobStore) -> None:
def __init__(
self,
store: JobStore,
*,
clock: Callable[[], datetime] | None = None,
max_lease_seconds: int = 300,
reconcile_lease_seconds: int = 30,
generic_receipt_verifier: Callable[[Mapping[str, Any]], bool] | None = None,
) -> None:
if max_lease_seconds < 1 or reconcile_lease_seconds < 1:
raise JobError("LEASE_CONFIGURATION_INVALID")
self.store = store
self._clock = clock or (lambda: datetime.now(timezone.utc))
self.max_lease_seconds = max_lease_seconds
self.reconcile_lease_seconds = reconcile_lease_seconds
self.generic_receipt_verifier = generic_receipt_verifier
def _now(self) -> datetime:
return _utc_datetime(self._clock())
def _validated_deadline(self, value: str) -> str:
deadline = _utc_datetime(value)
now = self._now()
if deadline <= now:
raise JobError("LEASE_EXPIRED")
if deadline > now + timedelta(seconds=self.max_lease_seconds):
raise JobError("LEASE_DURATION_EXCEEDED")
return _iso_utc(deadline)
@staticmethod
def _binding(request: Mapping[str, Any]) -> dict[str, str]:
if request.get("schema") == "aios.operation-job.v1":
fields = (
"schema", "tenant_id", "subject_id", "mission_id", "grant_id",
"task_id", "effect_id", "operation", "operation_version",
"resource_kind", "resource_id", "input_digest",
)
if any(not isinstance(request.get(field), str) or not request[field].strip() for field in fields):
raise JobError("OPERATION_JOB_BINDING_INVALID")
digest = str(request["input_digest"])
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise JobError("INPUT_DIGEST_INVALID")
binding = {field: str(request[field]).strip() for field in fields}
expected = request.get("expected_resource_revision")
if expected is not None:
if isinstance(expected, bool) or not isinstance(expected, (str, int)) or not str(expected).strip():
raise JobError("EXPECTED_RESOURCE_REVISION_INVALID")
binding["expected_resource_revision"] = str(expected).strip()
idempotency_key = request.get("idempotency_key")
if idempotency_key is not None:
if not isinstance(idempotency_key, str) or not idempotency_key.strip():
raise JobError("IDEMPOTENCY_KEY_INVALID")
binding["idempotency_key"] = idempotency_key.strip()
if any(len(value) > 512 for value in binding.values()):
raise JobError("OPERATION_JOB_BINDING_INVALID")
return binding
fields = ("campaign_id", "repository", "operation", "branch", "expected_branch_oid", "payload_digest")
if any(not isinstance(request.get(field), str) or not request[field].strip() for field in fields):
raise JobError("JOB_BINDING_INVALID")
@@ -634,20 +741,29 @@ class GitOpsJobController:
def submit(self, request: Mapping[str, Any], *, payload: bytes | None = None) -> JobReceipt:
binding = self._binding(request)
bound_digest = binding.get("payload_digest", binding.get("input_digest"))
assert bound_digest is not None
payload_ref = request.get("payload_ref")
payload_size = request.get("payload_size")
if payload is not None:
payload_ref, payload_size = self.store.payloads.put(payload, binding["payload_digest"])
if payload_ref is not None and payload_ref != f"sha256/{binding['payload_digest']}":
payload_ref, payload_size = self.store.payloads.put(payload, bound_digest)
if payload_ref is not None and payload_ref != f"sha256/{bound_digest}":
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
raise JobError("PAYLOAD_SIZE_INVALID")
client_idempotency_key = binding.get("idempotency_key")
if client_idempotency_key is not None:
if len(client_idempotency_key) > 128 or not all(
character.isalnum() or character in "._:-" for character in client_idempotency_key
):
raise JobError("IDEMPOTENCY_KEY_INVALID")
if client_idempotency_key is not None and (
len(client_idempotency_key) > 128
or not all(character.isalnum() or character in "._:-" for character in client_idempotency_key)
):
raise JobError("IDEMPOTENCY_KEY_INVALID")
if binding.get("schema") == "aios.operation-job.v1":
key = sha256_json({
"schema": binding["schema"],
"tenant_id": binding["tenant_id"],
"effect_id": binding["effect_id"],
})
elif client_idempotency_key is not None:
key = sha256_json({"client_idempotency_key": client_idempotency_key})
else:
key = sha256_json(binding)
@@ -666,9 +782,15 @@ class GitOpsJobController:
row = dict(existing)
connection.commit()
return self.store._receipt(row, duplicate=True)
generic = binding.get("schema") == "aios.operation-job.v1"
storage_campaign = binding["mission_id"] if generic else binding["campaign_id"]
storage_repository = "__operation_job__" if generic else binding["repository"]
storage_branch = "__operation_job__" if generic else binding["branch"]
storage_oid = "0" * 40 if generic else binding["expected_branch_oid"]
storage_digest = binding["input_digest"] if generic else binding["payload_digest"]
connection.execute(
"INSERT INTO jobs(job_id,idempotency_key,campaign_id,repository,operation,branch,expected_branch_oid,payload_digest,payload_ref,payload_size,state,side_effect_state,revision,request_json,receipt_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(job_id, key, binding["campaign_id"], binding["repository"], binding["operation"], binding["branch"], binding["expected_branch_oid"], binding["payload_digest"], payload_ref, payload_size, JobState.REQUESTED.value, "NONE", 0, json.dumps({"binding": binding, **durable_request}, sort_keys=True), None, now, now),
(job_id, key, storage_campaign, storage_repository, binding["operation"], storage_branch, storage_oid, storage_digest, payload_ref, payload_size, JobState.REQUESTED.value, "NONE", 0, json.dumps({"binding": binding, **durable_request}, sort_keys=True), None, now, now),
)
connection.execute("INSERT INTO idempotency_keys(key,job_id,binding_json,created_at) VALUES(?,?,?,?)", (key, job_id, json.dumps(binding, sort_keys=True), now))
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, None, JobState.REQUESTED.value, 0, "NONE", now))
@@ -697,8 +819,12 @@ class GitOpsJobController:
expected_revision: int,
) -> JobReceipt:
"""Atomically create a branch lease and move its Job to LEASED."""
if generation < 1 or any(len(oid) != 40 for oid in (lineage_anchor_oid, current_head_oid)):
if isinstance(generation, bool) or generation < 1 or any(
len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid)
for oid in (lineage_anchor_oid, current_head_oid)
):
raise JobError("LEASE_BINDING_INVALID")
deadline = self._validated_deadline(lease_expires_at)
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
@@ -707,10 +833,23 @@ class GitOpsJobController:
raise JobError("JOB_NOT_FOUND")
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if JobState(row["state"]) != JobState.AUTHORIZED:
raise JobError("LEASE_JOB_STATE_INVALID")
if row["branch"] in {"main", "master"}:
raise JobError("PROTECTED_BRANCH_REJECTED")
now = utc_now()
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, lease_expires_at, "ACTIVE", 0))
existing_lease = connection.execute(
"SELECT * FROM branch_leases WHERE job_id=?", (job_id,)
).fetchone()
if existing_lease is None:
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, deadline, "ACTIVE", 0))
elif existing_lease["state"] == "ACTIVE" and _utc_datetime(existing_lease["lease_expires_at"]) > self._now():
raise JobError("LEASE_CONFLICT")
else:
connection.execute(
"UPDATE branch_leases SET lease_id=?,lineage_anchor_oid=?,current_head_oid=?,generation=?,lease_expires_at=?,state='ACTIVE',revision=revision+1 WHERE job_id=?",
(lease_id, lineage_anchor_oid, current_head_oid, generation, deadline, job_id),
)
next_revision = expected_revision + 1
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?", (JobState.LEASED.value, "LEASED", next_revision, now, job_id, expected_revision))
if updated.rowcount != 1:
@@ -734,6 +873,13 @@ class GitOpsJobController:
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
lease = connection.execute("SELECT * FROM branch_leases WHERE lease_id=?", (lease_id,)).fetchone()
if lease is None:
raise JobError("LEASE_NOT_FOUND")
if lease["state"] != "ACTIVE" or _utc_datetime(lease["lease_expires_at"]) <= self._now():
raise JobError("LEASE_EXPIRED")
if int(lease["generation"]) != generation:
raise JobError("LEASE_FENCE_REJECTED")
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
@@ -743,6 +889,182 @@ class GitOpsJobController:
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def lease_resource_job(
self,
job_id: str,
*,
lease_id: str,
resource_kind: str,
resource_id: str,
generation: int | None = None,
lease_expires_at: str,
expected_revision: int,
) -> JobReceipt:
"""Lease a scoped non-Git resource without pretending it is a branch."""
if not lease_id or not resource_kind or not resource_id or isinstance(generation, bool):
raise JobError("LEASE_BINDING_INVALID")
deadline = self._validated_deadline(lease_expires_at)
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
request = json.loads(row["request_json"])
binding = request.get("binding", request)
if binding.get("schema") != "aios.operation-job.v1":
raise JobError("RESOURCE_LEASE_PROFILE_REQUIRED")
if binding.get("resource_kind") != resource_kind or binding.get("resource_id") != resource_id:
raise JobError("RESOURCE_LEASE_BINDING_MISMATCH")
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if JobState(row["state"]) != JobState.AUTHORIZED:
raise JobError("LEASE_JOB_STATE_INVALID")
active = connection.execute(
"SELECT 1 FROM resource_leases WHERE resource_kind=? AND resource_id=? AND state='ACTIVE' AND lease_expires_at>?",
(resource_kind, resource_id, _iso_utc(self._now())),
).fetchone()
if active is not None:
raise JobError("RESOURCE_LEASE_ACTIVE")
fence = connection.execute(
"SELECT generation FROM resource_fences WHERE resource_kind=? AND resource_id=?",
(resource_kind, resource_id),
).fetchone()
prior_generation = 0 if fence is None else int(fence["generation"])
assigned_generation = prior_generation + 1 if generation is None else generation
if assigned_generation <= prior_generation:
raise JobError("LEASE_FENCE_REJECTED")
old_job_lease = connection.execute(
"SELECT * FROM resource_leases WHERE job_id=?", (job_id,)
).fetchone()
if old_job_lease is None:
connection.execute(
"INSERT INTO resource_leases(lease_id,job_id,resource_kind,resource_id,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,0)",
(lease_id, job_id, resource_kind, resource_id, assigned_generation, deadline, "ACTIVE"),
)
else:
connection.execute(
"UPDATE resource_leases SET lease_id=?,resource_kind=?,resource_id=?,generation=?,lease_expires_at=?,state='ACTIVE',revision=revision+1 WHERE job_id=?",
(lease_id, resource_kind, resource_id, assigned_generation, deadline, job_id),
)
connection.execute(
"INSERT INTO resource_fences(resource_kind,resource_id,generation,job_id,updated_at) VALUES(?,?,?,?,?) ON CONFLICT(resource_kind,resource_id) DO UPDATE SET generation=excluded.generation,job_id=excluded.job_id,updated_at=excluded.updated_at",
(resource_kind, resource_id, assigned_generation, job_id, utc_now()),
)
next_revision = expected_revision + 1
now = utc_now()
updated = connection.execute(
"UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?",
(JobState.LEASED.value, "LEASED", next_revision, now, job_id, expected_revision),
)
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute(
"INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)",
(job_id, row["state"], JobState.LEASED.value, next_revision, "LEASED", now),
)
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except sqlite3.IntegrityError as exc:
raise JobError("LEASE_CONFLICT") from exc
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def assert_execution_lease(
self, job_id: str, *, generation: int, lease_id: str | None = None
) -> Mapping[str, Any]:
"""Fence a receiver-side effect by the active lease and generation."""
try:
with self.store._connect() as connection:
job = connection.execute("SELECT request_json,state FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if job is None:
raise JobError("JOB_NOT_FOUND")
if JobState(job["state"]) not in {
JobState.LEASED, JobState.WORKTREE_READY, JobState.EXECUTING,
JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING,
}:
raise JobError("LEASE_JOB_STATE_INVALID")
binding = json.loads(job["request_json"]).get("binding", {})
table = "resource_leases" if binding.get("schema") == "aios.operation-job.v1" else "branch_leases"
lease = connection.execute(f"SELECT * FROM {table} WHERE job_id=?", (job_id,)).fetchone()
if lease is None:
raise JobError("LEASE_NOT_FOUND")
if (
lease["state"] != "ACTIVE"
or _utc_datetime(lease["lease_expires_at"]) <= self._now()
or int(lease["generation"]) != generation
or (lease_id is not None and lease["lease_id"] != lease_id)
):
raise JobError("LEASE_FENCE_REJECTED")
if table == "resource_leases":
fence = connection.execute(
"SELECT generation,job_id FROM resource_fences WHERE resource_kind=? AND resource_id=?",
(lease["resource_kind"], lease["resource_id"]),
).fetchone()
if (
fence is None
or int(fence["generation"]) != int(lease["generation"])
or fence["job_id"] != job_id
):
raise JobError("LEASE_FENCE_REJECTED")
return dict(lease)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def get_execution_lease(self, job_id: str) -> Mapping[str, Any]:
"""Return the current typed lease so callers can pass its fence token."""
try:
with self.store._connect() as connection:
request_row = connection.execute(
"SELECT request_json FROM jobs WHERE job_id=?", (job_id,)
).fetchone()
if request_row is None:
raise JobError("JOB_NOT_FOUND")
binding = json.loads(request_row["request_json"]).get("binding", {})
table = "resource_leases" if binding.get("schema") == "aios.operation-job.v1" else "branch_leases"
lease = connection.execute(f"SELECT * FROM {table} WHERE job_id=?", (job_id,)).fetchone()
if lease is None:
raise JobError("LEASE_NOT_FOUND")
return dict(lease)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def assert_effect_admission(self, *, exclude_job_id: str | None = None) -> None:
"""Stop new effects while an earlier durable intent needs reconciliation."""
in_flight = (
JobState.EXECUTING, JobState.LOCAL_VERIFIED,
JobState.REMOTE_SYNCING, JobState.REMOTE_VERIFIED,
JobState.RECONCILING,
)
placeholders = ",".join("?" for _ in in_flight)
values: list[Any] = [state.value for state in in_flight]
query = (
f"SELECT job_id FROM jobs WHERE (state IN ({placeholders}) "
"OR (state=? AND side_effect_state='UNKNOWN'))"
)
values.append(JobState.RECONCILIATION_REQUIRED.value)
if exclude_job_id is not None:
query += " AND job_id<>?"
values.append(exclude_job_id)
query += " LIMIT 1"
try:
with self.store._connect() as connection:
if connection.execute(query, values).fetchone() is not None:
raise JobError("EFFECT_ADMISSION_LATCHED")
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
@@ -752,6 +1074,75 @@ class GitOpsJobController:
expected_revision=expected_revision,
)
def _validate_generic_receipt(
self,
connection: sqlite3.Connection,
row: Mapping[str, Any],
receipt: Mapping[str, Any] | None,
*,
recovery: bool,
) -> None:
if self.generic_receipt_verifier is None:
raise JobError("GENERIC_RECEIPT_VERIFIER_REQUIRED")
if not isinstance(receipt, Mapping) or set(receipt) != {"schema", "key_id", "payload", "signature"}:
raise JobError("GENERIC_RECEIPT_INVALID")
if receipt.get("schema") != "aios.operation-receipt.v1":
raise JobError("GENERIC_RECEIPT_INVALID")
if not isinstance(receipt.get("key_id"), str) or not receipt["key_id"].strip():
raise JobError("GENERIC_RECEIPT_INVALID")
signature = receipt.get("signature")
try:
decoded_signature = base64.b64decode(signature, validate=True) if isinstance(signature, str) else b""
except (ValueError, TypeError):
decoded_signature = b""
if len(decoded_signature) != 64:
raise JobError("GENERIC_RECEIPT_INVALID")
payload = receipt.get("payload")
if not isinstance(payload, Mapping) or not isinstance(payload.get("observation"), Mapping):
raise JobError("GENERIC_RECEIPT_INVALID")
request = json.loads(row["request_json"])
binding = request.get("binding", request)
required_bindings = (
"tenant_id", "subject_id", "mission_id", "grant_id", "task_id",
"effect_id", "operation", "operation_version", "resource_kind",
"resource_id", "input_digest",
)
if any(payload.get(field) != binding.get(field) for field in required_bindings):
raise JobError("GENERIC_RECEIPT_BINDING_MISMATCH")
if payload.get("job_id") != row["job_id"]:
raise JobError("GENERIC_RECEIPT_BINDING_MISMATCH")
if "expected_resource_revision" in binding and payload.get("expected_resource_revision") != binding["expected_resource_revision"]:
raise JobError("GENERIC_RECEIPT_BINDING_MISMATCH")
lease = connection.execute(
"SELECT * FROM resource_leases WHERE job_id=?", (row["job_id"],)
).fetchone()
if (
lease is None
or payload.get("claim_id") != lease["lease_id"]
or isinstance(payload.get("generation"), bool)
or payload.get("generation") != int(lease["generation"])
):
raise JobError("GENERIC_RECEIPT_LEASE_MISMATCH")
if not recovery:
fence = connection.execute(
"SELECT generation,job_id FROM resource_fences WHERE resource_kind=? AND resource_id=?",
(lease["resource_kind"], lease["resource_id"]),
).fetchone()
if (
lease["state"] != "ACTIVE"
or _utc_datetime(lease["lease_expires_at"]) <= self._now()
or fence is None
or int(fence["generation"]) != int(lease["generation"])
or fence["job_id"] != row["job_id"]
):
raise JobError("GENERIC_RECEIPT_LEASE_MISMATCH")
try:
verified = self.generic_receipt_verifier(receipt)
except Exception as exc:
raise JobError("GENERIC_RECEIPT_SIGNATURE_INVALID") from exc
if verified is not True:
raise JobError("GENERIC_RECEIPT_SIGNATURE_INVALID")
def _advance(
self,
job_id: str,
@@ -779,7 +1170,17 @@ class GitOpsJobController:
)
if previous in _TERMINAL and state != previous and not recovery_transition:
raise JobError("JOB_TERMINAL")
if state not in _RECOVERABLE | {JobState.RECONCILING, JobState.MANUAL_OWNER_REVIEW_REQUIRED}:
durable_request = json.loads(row["request_json"])
generic = durable_request.get("binding", durable_request).get("schema") == "aios.operation-job.v1"
if generic:
allowed = _GENERIC_TRANSITIONS.get(previous, set()) | _RECOVERABLE | {
JobState.RECONCILIATION_REQUIRED, JobState.MANUAL_OWNER_REVIEW_REQUIRED,
}
if state not in allowed:
raise JobError("GENERIC_JOB_TRANSITION_INVALID")
if state in {JobState.LOCAL_VERIFIED, JobState.COMPLETED}:
self._validate_generic_receipt(connection, row, receipt, recovery=False)
if not generic and state not in _RECOVERABLE | {JobState.RECONCILING, JobState.MANUAL_OWNER_REVIEW_REQUIRED}:
if previous in _RECOVERABLE:
raise JobError("RECONCILE_REQUIRED")
if previous != JobState.RECONCILING and _ORDER.get(state, -1) < _ORDER.get(previous, -1):
@@ -790,6 +1191,9 @@ class GitOpsJobController:
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,receipt_json=?,updated_at=? WHERE job_id=? AND revision=?", (state.value, side_effect_state, next_revision, receipt_json, now, job_id, revision))
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
if state in _TERMINAL:
connection.execute("UPDATE branch_leases SET state='USED' WHERE job_id=? AND state='ACTIVE'", (job_id,))
connection.execute("UPDATE resource_leases SET state='USED' WHERE job_id=? AND state='ACTIVE'", (job_id,))
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, previous.value, state.value, next_revision, side_effect_state, now))
if receipt is not None:
digest = hashlib.sha256(canonical_json(receipt)).hexdigest()
@@ -897,19 +1301,175 @@ class GitOpsJobController:
_allow_verified_external_recovery=True,
)
def reconcile(self, job_id: str, observer: Callable[[Mapping[str, Any]], Mapping[str, Any]]) -> JobReceipt:
current = self.store.read(job_id)
if JobState(current["state"]) not in _RECOVERABLE:
return self.store._receipt(current, duplicate=True)
self.advance(job_id, JobState.RECONCILING, side_effect_state="IN_PROGRESS")
def _claim_reconciliation(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]] | None:
now = self._now()
deadline = _iso_utc(now + timedelta(seconds=self.reconcile_lease_seconds))
try:
observed = dict(observer(current))
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
state = JobState(row["state"])
if state not in _RECONCILABLE:
connection.commit()
return None
existing = connection.execute(
"SELECT * FROM reconciliation_claims WHERE job_id=?", (job_id,)
).fetchone()
if (
state == JobState.RECONCILING
and existing is not None
and existing["state"] == "ACTIVE"
and _utc_datetime(existing["lease_expires_at"]) > now
):
raise JobError("RECONCILIATION_CLAIM_ACTIVE")
epoch = 1 if existing is None else int(existing["reconcile_epoch"]) + 1
claim_id = f"reconcile-{job_id}-{epoch}"
revision = int(row["revision"])
claimed_revision = revision + 1
updated = connection.execute(
"UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?",
(JobState.RECONCILING.value, "IN_PROGRESS", claimed_revision, utc_now(), job_id, revision),
)
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute(
"INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)",
(job_id, state.value, JobState.RECONCILING.value, claimed_revision, "IN_PROGRESS", utc_now()),
)
if existing is None:
connection.execute(
"INSERT INTO reconciliation_claims(job_id,claim_id,reconcile_epoch,base_revision,lease_expires_at,state,revision,updated_at) VALUES(?,?,?,?,?,'ACTIVE',0,?)",
(job_id, claim_id, epoch, claimed_revision, deadline, utc_now()),
)
else:
connection.execute(
"UPDATE reconciliation_claims SET claim_id=?,reconcile_epoch=?,base_revision=?,lease_expires_at=?,state='ACTIVE',revision=revision+1,updated_at=? WHERE job_id=?",
(claim_id, epoch, claimed_revision, deadline, utc_now(), job_id),
)
current = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
claim = {
"claim_id": claim_id,
"reconcile_epoch": epoch,
"base_revision": claimed_revision,
"effect_id": json.loads(current["request_json"]).get("binding", {}).get("effect_id", job_id),
"observed_at": _iso_utc(now),
"lease_expires_at": deadline,
}
return current, claim
def _finish_reconciliation(
self,
job_id: str,
claim: Mapping[str, Any],
state: JobState,
*,
side_effect_state: str,
receipt: Mapping[str, Any] | None,
) -> JobReceipt:
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
current_claim = connection.execute(
"SELECT * FROM reconciliation_claims WHERE job_id=?", (job_id,)
).fetchone()
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
if (
current_claim is None
or current_claim["state"] != "ACTIVE"
or current_claim["claim_id"] != claim["claim_id"]
or int(current_claim["reconcile_epoch"]) != int(claim["reconcile_epoch"])
or int(current_claim["base_revision"]) != int(row["revision"])
or _utc_datetime(current_claim["lease_expires_at"]) <= self._now()
):
raise JobError("RECONCILIATION_CLAIM_LOST")
durable_request = json.loads(row["request_json"])
generic = durable_request.get("binding", durable_request).get("schema") == "aios.operation-job.v1"
if generic and state == JobState.COMPLETED:
self._validate_generic_receipt(connection, row, receipt, recovery=True)
revision = int(row["revision"])
next_revision = revision + 1
receipt_payload = None
if receipt is not None:
receipt_payload = dict(receipt)
receipt_payload["reconciliation_claim"] = dict(claim)
receipt_json = json.dumps(receipt_payload, sort_keys=True) if receipt_payload is not None else row["receipt_json"]
now = utc_now()
updated = connection.execute(
"UPDATE jobs SET state=?,side_effect_state=?,revision=?,receipt_json=?,updated_at=? WHERE job_id=? AND revision=?",
(state.value, side_effect_state, next_revision, receipt_json, now, job_id, revision),
)
if updated.rowcount != 1:
raise JobError("RECONCILIATION_CLAIM_LOST")
connection.execute("UPDATE branch_leases SET state='RECONCILED' WHERE job_id=? AND state='ACTIVE'", (job_id,))
connection.execute("UPDATE resource_leases SET state='RECONCILED' WHERE job_id=? AND state='ACTIVE'", (job_id,))
connection.execute(
"UPDATE reconciliation_claims SET state='COMPLETED',revision=revision+1,updated_at=? WHERE job_id=? AND claim_id=?",
(now, job_id, claim["claim_id"]),
)
connection.execute(
"INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)",
(job_id, row["state"], state.value, next_revision, side_effect_state, now),
)
if receipt_payload is not None:
digest = hashlib.sha256(canonical_json(receipt_payload)).hexdigest()
connection.execute(
"INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)",
(f"receipt-{job_id}-{next_revision}", job_id, f"RECONCILE:{state.value}", digest, json.dumps(receipt_payload, sort_keys=True), now),
)
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def reconcile(self, job_id: str, observer: Callable[[Mapping[str, Any]], Mapping[str, Any]]) -> JobReceipt:
claimed = self._claim_reconciliation(job_id)
if claimed is None:
current = self.store.read(job_id)
return self.store._receipt(current, duplicate=True)
current, claim = claimed
observed_input = dict(current)
observed_input["reconciliation_claim"] = dict(claim)
try:
observed = dict(observer(observed_input))
stable = JobState(observed.get("state", JobState.REQUESTED.value))
if stable in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILING}:
raise JobError("RECONCILIATION_UNSTABLE")
return self.advance(job_id, stable, side_effect_state=str(observed.get("side_effect_state", "NONE")), receipt=observed.get("receipt"))
return self._finish_reconciliation(
job_id,
claim,
stable,
side_effect_state=str(observed.get("side_effect_state", "NONE")),
receipt=observed.get("receipt"),
)
except Exception as exc:
self.advance(job_id, JobState.RECONCILIATION_REQUIRED, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
if isinstance(exc, JobError) and str(exc) == "RECONCILIATION_CLAIM_LOST":
raise
try:
self._finish_reconciliation(
job_id,
claim,
JobState.RECONCILIATION_REQUIRED,
side_effect_state="UNKNOWN",
receipt={"error_code": type(exc).__name__},
)
except JobError as finish_exc:
if str(finish_exc) == "RECONCILIATION_CLAIM_LOST":
raise finish_exc from exc
raise
raise JobError("RECONCILIATION_REQUIRED") from exc
def resume(self, job_id: str) -> JobReceipt:
+363 -124
View File
@@ -4,15 +4,16 @@ from __future__ import annotations
import argparse
import base64
from dataclasses import dataclass, replace
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta, timezone
import json
from pathlib import Path
from typing import Any, Mapping
from typing import Any, Callable, Mapping
from aios_authority_git_executor import AuthorityGitExecutor, AuthorityGitError
from aios_campaign_recovery import RecoveryEvidenceError, load_recovered_authorization
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineBrokerError
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore, ServiceUnavailable
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
@@ -34,6 +35,17 @@ class DurableJobExecutor:
authority: AuthorityGitExecutor
broker: GiteaMachineBroker
recovery_path: Path
clock: Callable[[], datetime] = field(default=lambda: datetime.now(timezone.utc))
lease_seconds: int = 60
_persistence_halted: bool = field(default=False, init=False, repr=False)
def __post_init__(self) -> None:
try:
self.controller.assert_effect_admission()
except JobError as exc:
if str(exc) != "EFFECT_ADMISSION_LATCHED":
raise
self._persistence_halted = True
def _request(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
row = self.controller.store.read(job_id)
@@ -41,7 +53,14 @@ class DurableJobExecutor:
request = json.loads(row["request_json"])
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise JobExecutionError("JOB_REQUEST_INVALID") from exc
return row, dict(request.get("binding") or {})
binding = dict(request.get("binding") or {})
# Payload location is derived from and protected by payload_digest, but
# is intentionally outside the idempotency binding. Execution still
# needs it after a durable round trip.
for field_name in ("payload_ref", "payload_size"):
if field_name in request:
binding[field_name] = request[field_name]
return row, binding
def _validate_campaign(self, request: Mapping[str, Any]) -> str:
operation = str(request.get("operation", ""))
@@ -89,151 +108,372 @@ class DurableJobExecutor:
raise JobExecutionError("NEW_OID_NOT_DESCENDANT")
if not self.authority.ancestry_check(EXPECTED_BRANCH_OID, new_oid):
raise JobExecutionError("NEW_OID_OUTSIDE_AUTHORIZED_LINEAGE")
if operation == "RECONCILE_AUTHORITY_MAIN":
old_main = request.get("expected_main_oid")
if (
not isinstance(old_main, str)
or len(old_main) != 40
or any(ch not in "0123456789abcdef" for ch in old_main)
):
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
if not self.authority.ancestry_check(old_main, new_oid):
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
identity = self.broker.verify_identity()
if identity.get("repository") != REPOSITORY or identity.get("login") == "aiman":
raise JobExecutionError("MACHINE_IDENTITY_SCOPE_INVALID")
return new_oid
def dry_run(self, job_id: str) -> dict[str, Any]:
_row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
remote = self.broker.readback()
if remote is None:
action = "CREATE_REQUIRED"
elif remote == new_oid:
action = "ALREADY_IN_SYNC"
elif remote == request["expected_branch_oid"]:
action = "FAST_FORWARD_REQUIRED"
else:
action = "MANUAL_OWNER_REVIEW_REQUIRED"
if action == "MANUAL_OWNER_REVIEW_REQUIRED":
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
return {
row, request = self._request(job_id)
operation = str(request.get("operation") or "")
if operation not in {
"ADVANCE_EXISTING_CAMPAIGN_BRANCH", "COMMIT_FILE",
"SYNC_TASK_BRANCH_TO_GITEA", "CREATE_PR", "PUBLISH_CHECK",
"PROTECTED_MERGE", "RECONCILE_AUTHORITY_MAIN",
}:
raise JobExecutionError("OPERATION_NOT_ALLOWED")
self._validate_campaign(request)
if operation == "COMMIT_FILE":
self._commit_file_spec(row, request)
observation = self._observe_operation(request)
result = {
"mode": "DRY_RUN",
"operation": operation,
"decision": "SAFE_TO_ADVANCE",
"authority_action": "CAS_REQUIRED",
"gitea_action": action,
"previous_branch_oid": request["expected_branch_oid"],
"final_branch_oid": new_oid,
"force_push": False,
"effect_performed": False,
"readback": observation,
}
def _recover_requested(self, job_id: str) -> dict[str, Any]:
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.resume(job_id)
row = self.controller.store.read(job_id)
return row
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
result.update({
"authority_action": "CAS_REQUIRED",
"gitea_action": "ALREADY_IN_SYNC" if observation["verification"] == "VERIFIED" else "FAST_FORWARD_REQUIRED",
"previous_branch_oid": request["expected_branch_oid"],
"final_branch_oid": request["new_branch_oid"],
"force_push": False,
})
return result
def execute(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
if dry_run:
return self.dry_run(job_id)
if self._persistence_halted:
raise JobExecutionError("PERSISTENCE_FAILURE_LATCHED")
try:
return self._execute_unprotected(job_id, dry_run=dry_run)
return self._execute_unprotected(job_id)
except ServiceUnavailable:
self._persistence_halted = True
raise
except Exception as exc:
try:
row = self.controller.store.read(job_id)
state = JobState(row["state"])
if state not in {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED, JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.advance(job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
except Exception:
pass
if state in {
JobState.EXECUTING, JobState.LOCAL_VERIFIED,
JobState.REMOTE_SYNCING, JobState.REMOTE_VERIFIED,
}:
self.controller.advance(
job_id,
JobState.RECONCILIATION_REQUIRED,
side_effect_state="UNKNOWN",
receipt={"error_code": type(exc).__name__, "verification": "UNKNOWN"},
)
elif state not in {
JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED,
JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED,
}:
self.controller.advance(
job_id, JobState.RECOVERABLE_FAILURE,
side_effect_state="NONE",
receipt={"error_code": type(exc).__name__, "effect_performed": False},
)
except ServiceUnavailable:
self._persistence_halted = True
raise
def _execute_unprotected(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
def _execute_unprotected(self, job_id: str) -> dict[str, Any]:
row, request = self._request(job_id)
operation = request.get("operation")
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
if dry_run:
return self.dry_run(job_id)
return self._advance_campaign(job_id)
if operation == "COMMIT_FILE":
return self._commit_file(job_id, request)
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
new_oid = self._validate_campaign(request)
final = self.broker.push_branch(new_oid)
return {"operation": operation, "remote_verified": final == new_oid, "final_network_oid": final}
if operation == "CREATE_PR":
self._validate_campaign(request)
return dict(self.broker.create_pr("AIOS autonomous release control plane", "Durable campaign continuation; exact head and recovery evidence are bound in the Job."))
if operation == "PUBLISH_CHECK":
self._validate_campaign(request)
context = request.get("check_context", "aios/durable-release")
state = request.get("check_state", "success")
description = request.get("check_description", "Durable campaign control-plane check passed")
return dict(self.broker.status(str(state), str(description), str(context)))
if operation == "PROTECTED_MERGE":
self._validate_campaign(request)
number = int(request.get("pr_number", "0"))
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
expected_main = request.get("expected_main_oid")
if expected_main is not None:
self.broker.transport.expected_merge_base = expected_main
return dict(self.broker.merge(number))
if operation == "RECONCILE_AUTHORITY_MAIN":
self._validate_campaign(request)
old = request.get("expected_main_oid")
new = request.get("new_branch_oid")
if not isinstance(old, str) or not isinstance(new, str):
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
gitea_main = self.broker.transport.read_branch("main")
if gitea_main != new:
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
if not self.authority.ancestry_check(old, new):
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new)}
raise JobExecutionError("OPERATION_NOT_ALLOWED")
state = JobState(row["state"])
if state == JobState.COMPLETED:
return self.controller.store._receipt(row, duplicate=True).as_dict()
if state in {
JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED,
JobState.RECONCILING, JobState.EXECUTING, JobState.LOCAL_VERIFIED,
JobState.REMOTE_SYNCING, JobState.REMOTE_VERIFIED,
}:
return self.reconcile(job_id)
def _advance_campaign(self, job_id: str) -> dict[str, Any]:
self._recover_requested(job_id)
row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
expected_oid = request["expected_branch_oid"]
operation = str(request.get("operation") or "")
if operation not in {
"ADVANCE_EXISTING_CAMPAIGN_BRANCH", "COMMIT_FILE",
"SYNC_TASK_BRANCH_TO_GITEA", "CREATE_PR", "PUBLISH_CHECK",
"PROTECTED_MERGE", "RECONCILE_AUTHORITY_MAIN",
}:
raise JobExecutionError("OPERATION_NOT_ALLOWED")
try:
self.controller.assert_effect_admission(exclude_job_id=job_id)
except JobError as exc:
if str(exc) == "EFFECT_ADMISSION_LATCHED":
self._persistence_halted = True
raise JobExecutionError("PERSISTENCE_FAILURE_LATCHED") from exc
raise
self._validate_campaign(request)
if operation == "COMMIT_FILE":
self._commit_file_spec(row, request)
row = self._prepare_intent(job_id, row, request)
generation = int(request.get("generation", "1"))
self.controller.assert_execution_lease(job_id, generation=generation)
before = self._observe_operation(request)
if before["verification"] == "VERIFIED":
return self._complete(job_id, request, {"reconciled_before_effect": True}, before)
if before["verification"] != "ABSENT":
return self._record_unknown(job_id, request, before)
effect = self._perform_effect(job_id, row, request, generation)
after = self._observe_operation(request)
if after["verification"] != "VERIFIED":
return self._record_unknown(job_id, request, after, effect=effect)
return self._complete(job_id, request, effect, after)
def _prepare_intent(
self, job_id: str, row: Mapping[str, Any], request: Mapping[str, Any]
) -> dict[str, Any]:
state = JobState(row["state"])
if state == JobState.REQUESTED:
receipt = self.controller.advance(job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID})
self.controller.advance(
job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED",
receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
self.controller.lease_job(job_id, lease_id=f"lease-{job_id}", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=expected_oid, generation=1, lease_expires_at="2099-01-01T00:00:00Z", expected_revision=int(row["revision"]))
state = JobState(row["state"])
if state == JobState.AUTHORIZED:
deadline = self.clock().astimezone(timezone.utc) + timedelta(seconds=self.lease_seconds)
self.controller.lease_job(
job_id,
lease_id=f"lease-{job_id}",
lineage_anchor_oid=LINEAGE_ANCHOR,
current_head_oid=str(request["expected_branch_oid"]),
generation=int(request.get("generation", "1")),
lease_expires_at=deadline.isoformat(),
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.LEASED:
self.controller.advance(job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY", receipt={"branch": BRANCH, "expected_oid": expected_oid})
state = JobState(row["state"])
if state == JobState.LEASED:
self.controller.advance(
job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY",
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.WORKTREE_READY:
self.controller.advance(job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
state = JobState(row["state"])
if state == JobState.WORKTREE_READY:
self.controller.advance(
job_id, JobState.EXECUTING, side_effect_state="INTENT_COMMITTED",
receipt={
"effect_id": job_id,
"operation": request["operation"],
"input_digest": row["payload_digest"],
"intent_committed": True,
},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(job_id)
authority_before = self.authority.read_ref(f"refs/heads/{BRANCH}")
if authority_before == expected_oid:
authority_oid = self.authority.advance_task_branch_CAS(f"refs/heads/{BRANCH}", expected_oid, new_oid)
elif authority_before == new_oid:
authority_oid = new_oid
else:
raise JobExecutionError("AUTHORITY_BRANCH_CONFLICT")
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.EXECUTING:
self.controller.advance(job_id, JobState.LOCAL_VERIFIED, side_effect_state="LOCAL_VERIFIED", receipt={"authority": {"branch": BRANCH, "previous_oid": expected_oid, "new_oid": new_oid, "CAS_success": True, "readback_oid": authority_oid}})
row = self.controller.store.read(job_id)
remote_before = self.broker.readback()
if remote_before != new_oid:
if remote_before not in {None, expected_oid}:
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
if JobState(row["state"]) == JobState.LOCAL_VERIFIED:
if hasattr(self.broker, "receipt"):
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected_oid)
self.controller.advance(job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS", receipt={"gitea": {"previous_oid": remote_before, "desired_oid": new_oid, "force_push": False}})
final_network = self.broker.push_branch(new_oid)
else:
final_network = new_oid
final_api = self.broker.readback()
if final_api != new_oid:
raise JobExecutionError("GITEA_BRANCH_READBACK_MISMATCH")
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING}:
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt={"gitea": {"machine_actor": "aios-autonomy-agent", "previous_oid": remote_before, "desired_oid": new_oid, "final_network_oid": final_network, "final_API_oid": final_api, "force_push": False}})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.REMOTE_VERIFIED:
self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt={"authority_oid": authority_oid, "network_oid": final_network, "gitea_api_oid": final_api, "force_push": False})
return self.controller.store._receipt(self.controller.store.read(job_id), duplicate=True).as_dict()
if JobState(row["state"]) != JobState.EXECUTING:
raise JobExecutionError("JOB_EXECUTION_STATE_INVALID")
return dict(row)
def _commit_file(self, job_id: str, request: Mapping[str, Any]) -> dict[str, Any]:
def _perform_effect(
self,
job_id: str,
row: Mapping[str, Any],
request: Mapping[str, Any],
generation: int,
) -> dict[str, Any]:
operation = str(request["operation"])
new_oid = str(request["new_branch_oid"])
self.controller.assert_execution_lease(job_id, generation=generation)
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
expected = str(request["expected_branch_oid"])
current = self.authority.read_ref(f"refs/heads/{BRANCH}")
authority_oid = new_oid if current == new_oid else self.authority.advance_task_branch_CAS(
f"refs/heads/{BRANCH}", expected, new_oid
)
self.controller.assert_execution_lease(job_id, generation=generation)
remote = self.broker.readback()
if remote != new_oid:
if hasattr(self.broker, "receipt"):
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected)
self.broker.push_branch(new_oid)
return {"authority_oid": authority_oid, "network_oid": new_oid, "force_push": False}
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
return {"final_network_oid": self.broker.push_branch(new_oid), "force_push": False}
if operation == "CREATE_PR":
return dict(self.broker.create_pr(
"AIOS autonomous release control plane",
"Durable campaign continuation; exact head and recovery evidence are bound in the Job.",
))
if operation == "PUBLISH_CHECK":
return dict(self.broker.status(
str(request.get("check_state", "success")),
str(request.get("check_description", "Durable campaign control-plane check passed")),
str(request.get("check_context", "aios/durable-release")),
))
if operation == "PROTECTED_MERGE":
number = int(request.get("pr_number", "0"))
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
if request.get("expected_main_oid") is not None:
self.broker.transport.expected_merge_base = request["expected_main_oid"]
return dict(self.broker.merge(number))
if operation == "RECONCILE_AUTHORITY_MAIN":
old = request.get("expected_main_oid")
if not isinstance(old, str):
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
if self.broker.transport.read_branch("main") != new_oid:
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new_oid)}
if operation == "COMMIT_FILE":
path, content = self._commit_file_spec(row, request)
return {
"new_oid": self.authority.commit_file_in_isolated_worktree(
base_oid=row["expected_branch_oid"], files={path: content},
message="chore(gitops): durable commit-file", worktree_name=job_id,
)
}
raise JobExecutionError("OPERATION_NOT_ALLOWED")
def _observe_operation(self, request: Mapping[str, Any]) -> dict[str, Any]:
operation = str(request["operation"])
new_oid = str(request["new_branch_oid"])
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
authority = self.authority.read_ref(f"refs/heads/{BRANCH}")
remote = self.broker.readback()
if authority == new_oid and remote == new_oid:
return {"verification": "VERIFIED", "authority_oid": authority, "remote_oid": remote}
expected = request["expected_branch_oid"]
if authority == expected and remote in {None, expected}:
return {"verification": "ABSENT", "authority_oid": authority, "remote_oid": remote}
return {"verification": "UNKNOWN", "blocker": "PARTIAL_OR_CONFLICTING_BRANCH_EFFECT", "authority_oid": authority, "remote_oid": remote}
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
remote = self.broker.readback()
if remote == new_oid:
return {"verification": "VERIFIED", "remote_oid": remote}
if remote in {None, EXPECTED_BRANCH_OID}:
return {"verification": "ABSENT", "remote_oid": remote}
return {"verification": "UNKNOWN", "blocker": "REMOTE_BRANCH_CONFLICT", "remote_oid": remote}
if operation == "RECONCILE_AUTHORITY_MAIN":
current = self.authority.read_ref("refs/heads/main")
if current == new_oid:
return {"verification": "VERIFIED", "authority_main": current}
if current == request.get("expected_main_oid"):
return {"verification": "ABSENT", "authority_main": current}
return {"verification": "UNKNOWN", "blocker": "AUTHORITY_MAIN_CONFLICT", "authority_main": current}
if operation == "CREATE_PR" and hasattr(self.broker, "read_pr"):
value = self.broker.read_pr(BRANCH, "main", new_oid)
return {"verification": "VERIFIED" if value else "ABSENT", "pr": value}
if operation == "PUBLISH_CHECK" and hasattr(self.broker, "read_status"):
value = self.broker.read_status(
new_oid, str(request.get("check_context", "aios/durable-release"))
)
if value is None:
return {"verification": "ABSENT"}
expected_state = str(request.get("check_state", "success"))
return {"verification": "VERIFIED" if value.get("status") == expected_state else "UNKNOWN", "status": dict(value), **({} if value.get("status") == expected_state else {"blocker": "STATUS_READBACK_MISMATCH"})}
if operation == "PROTECTED_MERGE" and hasattr(self.broker, "read_merge"):
value = self.broker.read_merge(int(request.get("pr_number", "0")))
if value is None:
return {"verification": "ABSENT"}
if value.get("merged") is True and value.get("head_oid") == request["expected_branch_oid"]:
return {"verification": "VERIFIED", "merge": dict(value)}
if value.get("merged") is False:
return {"verification": "ABSENT", "merge": dict(value)}
return {"verification": "UNKNOWN", "blocker": "MERGE_READBACK_MISMATCH", "merge": dict(value)}
return {"verification": "UNKNOWN", "blocker": "EXACT_READBACK_UNSUPPORTED"}
def _complete(
self, job_id: str, request: Mapping[str, Any], effect: Mapping[str, Any], observation: Mapping[str, Any]
) -> dict[str, Any]:
row = self.controller.store.read(job_id)
receipt = {
"effect_id": job_id,
"operation": request["operation"],
"effect": dict(effect),
"verification": "VERIFIED",
"readback": dict(observation),
}
completed = self.controller.advance(
job_id, JobState.COMPLETED, side_effect_state="VERIFIED",
receipt=receipt, expected_revision=int(row["revision"]),
)
return completed.as_dict()
def _record_unknown(
self,
job_id: str,
request: Mapping[str, Any],
observation: Mapping[str, Any],
*,
effect: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
row = self.controller.store.read(job_id)
receipt = {
"effect_id": job_id,
"operation": request["operation"],
"effect": dict(effect) if effect is not None else None,
"verification": "UNKNOWN",
"blocker": observation.get("blocker", "READBACK_INCONCLUSIVE"),
"readback": dict(observation),
}
pending = self.controller.advance(
job_id, JobState.RECONCILIATION_REQUIRED, side_effect_state="UNKNOWN",
receipt=receipt, expected_revision=int(row["revision"]),
)
return pending.as_dict()
def reconcile(self, job_id: str) -> dict[str, Any]:
def observer(_current: Mapping[str, Any]) -> Mapping[str, Any]:
_row, request = self._request(job_id)
self._validate_campaign(request)
observation = self._observe_operation(request)
if observation["verification"] == "VERIFIED":
return {
"state": JobState.COMPLETED.value,
"side_effect_state": "VERIFIED",
"receipt": {
"effect_id": job_id, "operation": request["operation"],
"effect": None, "verification": "VERIFIED",
"readback": observation, "reconciled": True,
},
}
if observation["verification"] == "ABSENT":
return {
"state": JobState.REQUESTED.value,
"side_effect_state": "RECONCILED_ABSENT",
"receipt": {
"effect_id": job_id, "operation": request["operation"],
"verification": "ABSENT", "readback": observation,
"reconciled": True,
},
}
return {
"state": JobState.RECONCILIATION_REQUIRED.value,
"side_effect_state": "UNKNOWN",
"receipt": {
"effect_id": job_id, "operation": request["operation"],
"verification": "UNKNOWN",
"blocker": observation.get("blocker", "READBACK_INCONCLUSIVE"),
"readback": observation, "reconciled": True,
},
}
result = self.controller.reconcile(job_id, observer).as_dict()
if result["state"] in {JobState.COMPLETED.value, JobState.REQUESTED.value}:
self._persistence_halted = False
return result
def _commit_file_spec(
self, row: Mapping[str, Any], request: Mapping[str, Any]
) -> tuple[str, bytes]:
payload_ref = request.get("payload_ref")
if not payload_ref:
raise JobExecutionError("PAYLOAD_REQUIRED")
@@ -244,8 +484,7 @@ class DurableJobExecutor:
content = base64.b64decode(spec["content_base64"], validate=True)
except Exception as exc:
raise JobExecutionError("COMMIT_FILE_PAYLOAD_INVALID") from exc
new_oid = self.authority.commit_file_in_isolated_worktree(base_oid=row["expected_branch_oid"], files={path: content}, message="chore(gitops): durable commit-file", worktree_name=job_id)
return {"operation": "COMMIT_FILE", "new_oid": new_oid}
return path, content
def build_from_environment(job_root: Path) -> DurableJobExecutor:
@@ -0,0 +1,32 @@
"""Collision-resistant storage keys for durable governed Task state."""
from __future__ import annotations
import hashlib
import re
class JobIdentityError(RuntimeError):
pass
def legacy_task_storage_key(task_id: str) -> str:
"""Return the historical lossy key solely to detect migration conflicts."""
if not isinstance(task_id, str) or not task_id:
raise JobIdentityError("TASK_ID_INVALID")
return re.sub(r"[^A-Za-z0-9_.-]+", "-", task_id).strip("-")[:120] or "task"
def task_storage_key(task_id: str) -> str:
"""Return a canonical collision-resistant key for every Task state sink."""
if (
not isinstance(task_id, str)
or not task_id
or task_id != task_id.strip()
or len(task_id) > 512
):
raise JobIdentityError("TASK_ID_INVALID")
readable = re.sub(r"[^A-Za-z0-9_.-]+", "-", task_id).strip("-")[:48] or "task"
digest = hashlib.sha256(
("aios-task-storage-v1\0" + task_id).encode("utf-8")
).hexdigest()
return f"{readable}-{digest}"
@@ -4,13 +4,22 @@ from __future__ import annotations
from dataclasses import dataclass, asdict
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import tempfile
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore
from aios_gitops_job_identity import legacy_task_storage_key, task_storage_key
from aios_online_agent_execution import GovernedExecutionBinding, project_execution_envelope
try: # pragma: no cover - exercised on the Linux activation host
import fcntl
except ImportError: # pragma: no cover - non-POSIX execution stays fail closed
fcntl = None # type: ignore[assignment]
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
@@ -19,6 +28,29 @@ class BootstrapError(RuntimeError):
pass
def _lexists(path: Path) -> bool:
try:
path.lstat()
return True
except FileNotFoundError:
return False
def _require_runtime_directory(path: Path) -> None:
try:
metadata = path.lstat()
except OSError as exc:
raise BootstrapError("TASK_STORAGE_PARENT_INVALID") from exc
expected_uid = getattr(os, "geteuid", lambda: metadata.st_uid)()
if (
stat.S_ISLNK(metadata.st_mode)
or not stat.S_ISDIR(metadata.st_mode)
or metadata.st_mode & 0o022
or metadata.st_uid != expected_uid
):
raise BootstrapError("TASK_STORAGE_PARENT_INVALID")
@dataclass(frozen=True, slots=True)
class GovernedTaskBinding:
task_id: str
@@ -37,25 +69,95 @@ class GovernedTaskBinding:
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
def _safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
# ===== AIOS Formal Lineage producer-native reservation v12 =====
import re as _fl_re
_FL_PRODUCER_KEY = _fl_re.compile(r"^[0-9a-f]{64}$")
def _formal_lineage_reservation_receipt(
value,
*,
request_idempotency_key: str,
expected_side_effect_state: str,
):
if callable(getattr(value, "as_dict", None)):
raw=value.as_dict()
elif isinstance(value, dict):
raw=dict(value)
else:
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_TYPE_INVALID")
producer_key=raw.get("idempotency_key")
job_id=raw.get("job_id")
if not isinstance(job_id,str) or not job_id:
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_JOB_ID_INVALID")
if raw.get("state")!="REQUESTED":
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_STATE_INVALID")
if raw.get("side_effect_state")!=expected_side_effect_state:
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_SIDE_EFFECT_STATE_INVALID")
if not isinstance(producer_key,str) or not _FL_PRODUCER_KEY.fullmatch(producer_key):
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_PRODUCER_IDEMPOTENCY_INVALID")
if job_id!="job-"+producer_key[:32]:
raise RuntimeError("GOVERNED_TASK_SUBMIT_RECEIPT_JOB_PRODUCER_KEY_CONFLICT")
if not isinstance(request_idempotency_key,str) or not request_idempotency_key:
raise RuntimeError("GOVERNED_TASK_REQUEST_IDEMPOTENCY_INVALID")
return {
"job_id":job_id,
"state":"REQUESTED",
"side_effect_state":raw["side_effect_state"],
"producer_idempotency_key":producer_key,
"request_idempotency_key":request_idempotency_key,
}
def _formal_lineage_attach_receipt(response, receipt):
if not isinstance(response, dict):
raise RuntimeError("GOVERNED_TASK_BINDING_RESPONSE_INVALID")
if response.get("state")!="BOUND":
raise RuntimeError("GOVERNED_TASK_BINDING_STATE_INVALID")
if response.get("job_id")!=receipt.get("job_id"):
raise RuntimeError("GOVERNED_TASK_BINDING_RECEIPT_JOB_CONFLICT")
result=dict(response)
result["reservation_receipt"]=dict(receipt)
return result
class GovernedTaskBootstrap:
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.lock_root = self.root / "task-binding-locks"; self.task_meta_root.mkdir(parents=True, exist_ok=True, mode=0o700); self.job_root.mkdir(parents=True, exist_ok=True, mode=0o700); self.lock_root.mkdir(parents=True, exist_ok=True, mode=0o700); _require_runtime_directory(self.task_meta_root); _require_runtime_directory(self.job_root); _require_runtime_directory(self.lock_root)
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
root = self.job_root / task_storage_key(task_id)
legacy = self.job_root / legacy_task_storage_key(task_id)
if legacy != root and _lexists(legacy):
if _lexists(root):
raise BootstrapError("TASK_STORAGE_LEGACY_CONFLICT")
raise BootstrapError("TASK_STORAGE_LEGACY_MIGRATION_REQUIRED")
store = JobStore(root, payload_root=root / "payloads")
return store, GitOpsJobController(store)
def _metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_safe(task_id) + ".json")
return self.task_meta_root / (task_storage_key(task_id) + ".json")
def _legacy_metadata(self, task_id: str) -> Path:
return self.task_meta_root / (legacy_task_storage_key(task_id) + ".json")
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
path = self._metadata(task_id)
if not path.is_file() or path.is_symlink(): return None
legacy = self._legacy_metadata(task_id)
if legacy != path and _lexists(legacy):
if _lexists(path):
raise BootstrapError("TASK_BINDING_LEGACY_CONFLICT")
raise BootstrapError("TASK_BINDING_LEGACY_MIGRATION_REQUIRED")
if not _lexists(path):
return None
if not path.is_file() or path.is_symlink():
raise BootstrapError("TASK_BINDING_PATH_INVALID")
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
binding = GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
if binding.task_id != task_id:
raise BootstrapError("TASK_BINDING_IDENTITY_CONFLICT")
return binding
def get_binding(self, task_id: str) -> GovernedTaskBinding:
"""Read the exact durable binding; never synthesize missing lineage."""
binding = self._read_existing(str(task_id))
@@ -96,29 +198,106 @@ class GovernedTaskBootstrap:
)
def _write(self, binding: GovernedTaskBinding) -> None:
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
path = self._metadata(binding.task_id)
descriptor, temporary_name = tempfile.mkstemp(
prefix=".task-binding-", dir=self.task_meta_root,
)
temporary = Path(temporary_name)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
stream.write(json.dumps(asdict(binding), sort_keys=True) + "\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
os.chmod(path, 0o600)
parent = os.open(self.task_meta_root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(parent)
finally:
os.close(parent)
except BaseException:
temporary.unlink(missing_ok=True)
raise
def _task_lock(self, task_id: str) -> int:
if fcntl is None:
raise BootstrapError("TASK_BINDING_LOCK_UNAVAILABLE")
path = self.lock_root / (task_storage_key(task_id) + ".lock")
descriptor = os.open(
path,
os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0),
0o600,
)
metadata = os.fstat(descriptor)
expected_uid = getattr(os, "geteuid", lambda: metadata.st_uid)()
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_nlink != 1
or metadata.st_uid != expected_uid
):
os.close(descriptor)
raise BootstrapError("TASK_BINDING_LOCK_INVALID")
os.fchmod(descriptor, 0o600)
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as exc:
os.close(descriptor)
raise BootstrapError("TASK_BINDING_BUSY") from exc
return descriptor
@staticmethod
def _unlock_task(descriptor: int) -> None:
if fcntl is not None:
fcntl.flock(descriptor, fcntl.LOCK_UN)
os.close(descriptor)
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
return existing.response(idempotent=True)
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
expected = str(request.get("expected_branch_oid") or values["base_oid"])
if current is None:
mode, generation = "CREATE_NEW", 1
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
else:
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
if generation < 1: raise BootstrapError("GENERATION_INVALID")
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
_, controller = self._store(values["task_id"])
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
self._write(binding)
return binding.response()
descriptor = self._task_lock(values["task_id"])
try:
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")): raise BootstrapError("TASK_BINDING_CONFLICT")
expected = str(request.get('expected_branch_oid') or values['base_oid'])
payload = json.dumps(values, sort_keys=True, separators=(',', ':')).encode()
_, controller = self._store(values['task_id'])
_fl_submit_receipt = controller.submit({'idempotency_key': values['idempotency_key'], 'campaign_id': values['task_id'], 'task_id': values['task_id'], 'repository': values['repository'], 'operation': 'GOVERNED_TASK_CREATE', 'branch': values['target_branch'], 'expected_branch_oid': expected, 'payload_digest': hashlib.sha256(payload).hexdigest(), 'lineage_anchor_oid': values['base_oid']}, payload=payload)
_fl_receipt = _formal_lineage_reservation_receipt(
_fl_submit_receipt, request_idempotency_key=request["idempotency_key"],
expected_side_effect_state='SAME_JOB'
)
_fl_response = existing.response(idempotent=True)
return _formal_lineage_attach_receipt(_fl_response,_fl_receipt)
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
expected = str(request.get("expected_branch_oid") or values["base_oid"])
if current is None:
mode, generation = "CREATE_NEW", 1
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
else:
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
if generation < 1: raise BootstrapError("GENERATION_INVALID")
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
_, controller = self._store(values["task_id"])
expected_job_id = controller.internal_job_id_for_idempotency_key(values["idempotency_key"])
with controller.store._connect() as connection:
bootstrap_jobs = connection.execute(
"SELECT job_id FROM jobs WHERE operation='GOVERNED_TASK_CREATE' ORDER BY job_id"
).fetchall()
if bootstrap_jobs and [str(row[0]) for row in bootstrap_jobs] != [expected_job_id]:
raise BootstrapError("TASK_BINDING_RECOVERY_CONFLICT")
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
self._write(binding)
_fl_receipt = _formal_lineage_reservation_receipt(
receipt, request_idempotency_key=request["idempotency_key"],
expected_side_effect_state="QUEUED"
)
_fl_response = binding.response()
return _formal_lineage_attach_receipt(_fl_response,_fl_receipt)
finally:
self._unlock_task(descriptor)
@@ -2,6 +2,7 @@
from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from dataclasses import replace
from pathlib import Path
from baseline_authority import BaselineAuthority, BaselineAuthorityError
from silent_release_controller import ReleaseCandidate
@@ -114,6 +115,9 @@ TRUSTED_BUILDER_SOCKET = Path(os.getenv(
TRUSTED_BUILDER_PUBLIC_KEYRING = Path(os.getenv(
'AIOS_TRUSTED_CANDIDATE_BUILDER_PUBLIC_KEYRING', ''
))
TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT = str(os.getenv(
'AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT', ''
)).strip()
TRUSTED_RELEASE_POLICY = Path(os.getenv(
'AIOS_SILENT_RELEASE_TRUSTED_POLICY',
str(WS / 'config' / 'gitops' / 'silent-release-policy.json'),
@@ -253,6 +257,14 @@ class _HostTrustedCandidateBuilderAdapter:
nonce=secrets.token_hex(32),
)
except BuilderError as exc:
code = str(exc)
if code in {'SOURCE_ADMISSION_REQUIRED', 'RECIPE_UNKNOWN'} or (
code == 'REQUEST_RELEASE_BINDING_INVALID'
and not intent.get('version')
):
raise CapabilityAuthorizationError(
'release source admission required'
) from exc
raise TrustedCandidateBuilderUnavailable(
'trusted builder request failed'
) from exc
@@ -290,6 +302,7 @@ def _build_silent_release_service() -> SilentReleaseActionService:
builder_attestation_verifier = None
trusted_policy = None
builder_readiness_probe = None
source_admission_resolver = None
if TRUSTED_BUILDER_ENABLED:
# Every path is opt-in. In particular, this API receives its own
# client HMAC credential, never the builder's signing private key.
@@ -317,6 +330,31 @@ def _build_silent_release_service() -> SilentReleaseActionService:
client_hmac = (
TRUSTED_BUILDER_CREDENTIALS_DIRECTORY / 'builder_hmac'
).read_bytes().strip()
if TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT:
from release_source_admission import ReleaseSourceAdmissionStore
try:
source_admission_hmac = (
TRUSTED_BUILDER_CREDENTIALS_DIRECTORY
/ 'source_admission_hmac'
).read_bytes().strip()
except OSError as exc:
raise CapabilityAuthorizationError(
'release source admission credential is unavailable'
) from exc
source_store = ReleaseSourceAdmissionStore(
Path(TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT),
repository=builder_config.repository,
policy_sha256=builder_config.policy_sha256,
hmac_key=source_admission_hmac,
expected_owner_uid=0,
writable=False,
)
builder_config = replace(
builder_config,
source_admission_resolver=source_store.resolve_recipe,
)
source_admission_resolver = source_store.lookup_source
client_keyring = BuilderKeyring(builder_hmac=client_hmac)
client = UnixTrustedCandidateBuilderClient(
builder_config,
@@ -365,6 +403,7 @@ def _build_silent_release_service() -> SilentReleaseActionService:
builder_attestation_verifier=builder_attestation_verifier,
trusted_policy=trusted_policy,
builder_readiness_probe=builder_readiness_probe,
source_admission_resolver=source_admission_resolver,
allow_legacy_candidate_input=False,
expected_repository=REPOSITORY_IDENTITY,
policy_sha256=policy_sha,
@@ -0,0 +1,527 @@
"""Owner-final, structured authorization for one governed pull-request merge.
The Owner approval is the policy decision. Signing and public verification
prove that decision but cannot change its repository, pull request, CAS base,
technical-gate contract, or lifetime. This module deliberately exposes no
generic sign-bytes API and does not publish or alter technical check results.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import os
from pathlib import Path
import re
import secrets
import sqlite3
import time
from typing import Any, Callable, Mapping, Protocol
from aios_single_host_authority import (
OWNER_NAMESPACE,
LocalFileSigner,
SignedArtifact,
canonical_json_bytes,
sha256_json,
)
POLICY_SCHEMA = "aios.owner-merge-approval-policy.v2"
APPROVAL_SCHEMA = "aios.owner-merge-approval.v2"
RECEIPT_SCHEMA = "aios.owner-merge-admission-receipt.v2"
OPERATION = "merge.pull-request"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
REPOSITORY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,255}$")
CHECK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
TOKEN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{11,255}$")
class OwnerMergeApprovalError(RuntimeError):
"""Stable fail-closed error for the Owner merge approval contract."""
def __init__(self, code: str):
super().__init__(code)
self.code = code
def _fail(code: str) -> None:
raise OwnerMergeApprovalError(code)
def _require_oid(value: object, code: str) -> str:
if not isinstance(value, str) or OID_RE.fullmatch(value) is None:
_fail(code)
return value
def _require_digest(value: object, code: str) -> str:
if not isinstance(value, str) or DIGEST_RE.fullmatch(value) is None:
_fail(code)
return value
@dataclass(frozen=True, slots=True)
class OwnerMergeApprovalPolicy:
"""Immutable policy input for one exact protected merge approval."""
repository: str
pr_number: int
pr_head_oid: str
expected_main_oid: str
operation: str
required_check_contract_digest: str
required_checks: tuple[str, ...]
owner_identity: str
audit_lineage_digest: str
maximum_ttl_seconds: int
policy_version: str = "v2"
schema: str = POLICY_SCHEMA
def validate(self) -> None:
if (
self.schema != POLICY_SCHEMA
or self.policy_version != "v2"
or not isinstance(self.repository, str)
or REPOSITORY_RE.fullmatch(self.repository) is None
or type(self.pr_number) is not int
or not 1 <= self.pr_number < 2**31
or self.operation != OPERATION
or not isinstance(self.owner_identity, str)
or IDENTITY_RE.fullmatch(self.owner_identity) is None
or type(self.maximum_ttl_seconds) is not int
or not 60 <= self.maximum_ttl_seconds <= 900
):
_fail("OWNER_MERGE_POLICY_INVALID")
_require_oid(self.pr_head_oid, "OWNER_MERGE_POLICY_HEAD_INVALID")
_require_oid(self.expected_main_oid, "OWNER_MERGE_POLICY_BASE_INVALID")
_require_digest(
self.required_check_contract_digest,
"OWNER_MERGE_POLICY_CHECK_CONTRACT_INVALID",
)
_require_digest(self.audit_lineage_digest, "OWNER_MERGE_POLICY_LINEAGE_INVALID")
if (
not isinstance(self.required_checks, tuple)
or not self.required_checks
or len(set(self.required_checks)) != len(self.required_checks)
or any(not isinstance(item, str) or CHECK_RE.fullmatch(item) is None for item in self.required_checks)
):
_fail("OWNER_MERGE_POLICY_REQUIRED_CHECKS_INVALID")
def as_dict(self) -> dict[str, Any]:
self.validate()
value = asdict(self)
value["required_checks"] = list(self.required_checks)
return value
@property
def digest(self) -> str:
return sha256_json(self.as_dict())
@dataclass(frozen=True, slots=True)
class OwnerMergeLiveState:
"""Read-only live evidence; this object has no status publication method."""
repository: str
pr_number: int
pr_head_oid: str
main_oid: str
required_check_contract_digest: str
check_statuses: Mapping[str, bool]
@dataclass(frozen=True, slots=True)
class OwnerMergeOidReadback:
authority_main_oid: str
gitea_main_oid: str
api_main_oid: str
@dataclass(frozen=True, slots=True)
class OwnerMergeAdmissionReceipt:
approval_id: str
nonce: str
owner_identity: str
policy_digest: str
proof_path: str
merge_oid: str
schema: str = RECEIPT_SCHEMA
class ApprovalProofVerifier(Protocol):
"""Public proof verifier; it is intentionally unable to issue approvals."""
def verify(
self,
*,
namespace: str,
payload: bytes,
signature: str,
key_id: str,
) -> bool:
...
class GovernedMergeExecutor(Protocol):
def execute(self, approval: Mapping[str, object]) -> str:
...
class MergeOidReconciler(Protocol):
def readback(self, merge_oid: str) -> OwnerMergeOidReadback:
...
APPROVAL_FIELDS = frozenset(
{
"schema",
"approval_id",
"nonce",
"repository",
"pr_number",
"pr_head_oid",
"expected_main_oid",
"operation",
"required_check_contract_digest",
"required_checks",
"owner_identity",
"audit_lineage_digest",
"policy_version",
"policy_digest",
"issued_at",
"not_before",
"expires_at",
"signature_namespace",
"signature_key_id",
"canonical_payload_digest",
}
)
class OwnerMergeApprovalAuthority:
"""Structured Owner signer for an immutable merge policy.
``issue`` has no caller-controlled payload parameter. The immutable policy
selects the complete authorization scope; only approval IDs and nonces are
generated inside the authority.
"""
def __init__(
self,
key_path: Path,
policy: OwnerMergeApprovalPolicy,
*,
require_root_owner: bool = True,
clock: Callable[[], float] = time.time,
token_urlsafe: Callable[[int], str] = secrets.token_urlsafe,
) -> None:
policy.validate()
self.policy = policy
self.signer = LocalFileSigner(
Path(key_path),
signer_id="local-owner-authority",
namespace=OWNER_NAMESPACE,
require_root_owner=require_root_owner,
)
self.clock = clock
self.token_urlsafe = token_urlsafe
def _token(self, prefix: str) -> str:
value = prefix + self.token_urlsafe(24)
if TOKEN_RE.fullmatch(value) is None:
_fail("OWNER_MERGE_AUTHORITY_GENERATED_TOKEN_INVALID")
return value
def issue(self) -> SignedArtifact:
now = int(self.clock())
approval_id = self._token("owner-merge-approval-")
nonce = self._token("owner-merge-nonce-")
policy = self.policy
payload: dict[str, Any] = {
"schema": APPROVAL_SCHEMA,
"approval_id": approval_id,
"nonce": nonce,
"repository": policy.repository,
"pr_number": policy.pr_number,
"pr_head_oid": policy.pr_head_oid,
"expected_main_oid": policy.expected_main_oid,
"operation": policy.operation,
"required_check_contract_digest": policy.required_check_contract_digest,
"required_checks": list(policy.required_checks),
"owner_identity": policy.owner_identity,
"audit_lineage_digest": policy.audit_lineage_digest,
"policy_version": policy.policy_version,
"policy_digest": policy.digest,
"issued_at": now,
"not_before": max(0, now - 5),
"expires_at": now + policy.maximum_ttl_seconds,
"signature_namespace": OWNER_NAMESPACE,
"signature_key_id": self.signer.fingerprint,
}
payload["canonical_payload_digest"] = sha256_json(payload)
return self.signer._sign_canonical(payload)
class OwnerMergeApprovalVerifier:
"""Verify Owner proof, preferring normal verification then recovery proof.
The recovery verifier validates a pre-existing approval with the registered
public Owner root. It cannot issue an approval and is therefore not a
second policy principal.
"""
def __init__(
self,
*,
primary: ApprovalProofVerifier,
recovery: ApprovalProofVerifier | None = None,
clock: Callable[[], float] = time.time,
) -> None:
self.primary = primary
self.recovery = recovery
self.clock = clock
@staticmethod
def _artifact(artifact: SignedArtifact | Mapping[str, Any]) -> tuple[dict[str, Any], str, str, str]:
if isinstance(artifact, SignedArtifact):
return dict(artifact.payload), artifact.signature, artifact.key_id, artifact.namespace
if not isinstance(artifact, Mapping) or set(artifact) != {"payload", "signature", "key_id", "namespace"}:
_fail("OWNER_MERGE_APPROVAL_ARTIFACT_INVALID")
payload = artifact.get("payload")
signature = artifact.get("signature")
key_id = artifact.get("key_id")
namespace = artifact.get("namespace")
if not isinstance(payload, Mapping) or not all(isinstance(item, str) for item in (signature, key_id, namespace)):
_fail("OWNER_MERGE_APPROVAL_ARTIFACT_INVALID")
return dict(payload), signature, key_id, namespace
@staticmethod
def _validate_scope(payload: Mapping[str, Any], policy: OwnerMergeApprovalPolicy, *, now: int, key_id: str, namespace: str) -> None:
if set(payload) != APPROVAL_FIELDS:
_fail("OWNER_MERGE_APPROVAL_FIELDS_INVALID")
expected = {
"schema": APPROVAL_SCHEMA,
"repository": policy.repository,
"pr_number": policy.pr_number,
"pr_head_oid": policy.pr_head_oid,
"expected_main_oid": policy.expected_main_oid,
"operation": policy.operation,
"required_check_contract_digest": policy.required_check_contract_digest,
"required_checks": list(policy.required_checks),
"owner_identity": policy.owner_identity,
"audit_lineage_digest": policy.audit_lineage_digest,
"policy_version": policy.policy_version,
"policy_digest": policy.digest,
"signature_namespace": OWNER_NAMESPACE,
"signature_key_id": key_id,
}
if payload.get("pr_head_oid") != policy.pr_head_oid:
_fail("OWNER_MERGE_APPROVAL_HEAD_BINDING_INVALID")
if payload.get("expected_main_oid") != policy.expected_main_oid:
_fail("OWNER_MERGE_APPROVAL_BASE_BINDING_INVALID")
if payload.get("policy_digest") != policy.digest:
_fail("OWNER_MERGE_POLICY_DIGEST_DRIFT")
if namespace != OWNER_NAMESPACE or any(payload.get(key) != value for key, value in expected.items()):
_fail("OWNER_MERGE_APPROVAL_SCOPE_INVALID")
for field in ("approval_id", "nonce"):
if not isinstance(payload.get(field), str) or TOKEN_RE.fullmatch(payload[field]) is None:
_fail("OWNER_MERGE_APPROVAL_TOKEN_INVALID")
for field in ("issued_at", "not_before", "expires_at"):
if type(payload.get(field)) is not int:
_fail("OWNER_MERGE_APPROVAL_TIME_INVALID")
if not payload["not_before"] <= now < payload["expires_at"]:
_fail("OWNER_MERGE_APPROVAL_EXPIRED")
if payload["issued_at"] > now + 5 or payload["expires_at"] - payload["issued_at"] != policy.maximum_ttl_seconds:
_fail("OWNER_MERGE_APPROVAL_TIME_INVALID")
supplied = payload.get("canonical_payload_digest")
body = dict(payload)
body.pop("canonical_payload_digest", None)
if supplied != sha256_json(body):
_fail("OWNER_MERGE_APPROVAL_CANONICAL_DIGEST_INVALID")
def verify(
self,
artifact: SignedArtifact | Mapping[str, Any],
policy: OwnerMergeApprovalPolicy,
) -> tuple[dict[str, Any], str]:
policy.validate()
payload, signature, key_id, namespace = self._artifact(artifact)
canonical = canonical_json_bytes(payload)
proof_path: str | None = None
for path, verifier in (("primary", self.primary), ("recovery", self.recovery)):
if verifier is None:
continue
try:
verified = verifier.verify(
namespace=namespace,
payload=canonical,
signature=signature,
key_id=key_id,
)
except Exception:
verified = False
if verified is True:
proof_path = path
break
if proof_path is None:
_fail("OWNER_MERGE_APPROVAL_SIGNATURE_INVALID")
self._validate_scope(payload, policy, now=int(self.clock()), key_id=key_id, namespace=namespace)
return payload, proof_path
class SQLiteOwnerMergeNonceLedger:
"""Durable one-shot fence. Unknown effects are intentionally not retried."""
def __init__(self, path: Path) -> None:
self.path = Path(path)
def _connection(self) -> sqlite3.Connection:
if self.path.is_symlink() or self.path.parent.is_symlink():
_fail("OWNER_MERGE_NONCE_LEDGER_PATH_INVALID")
self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
connection = sqlite3.connect(self.path, timeout=5, isolation_level=None)
connection.execute(
"CREATE TABLE IF NOT EXISTS owner_merge_nonce ("
"nonce TEXT PRIMARY KEY, approval_digest TEXT NOT NULL, state TEXT NOT NULL, claimed_at INTEGER NOT NULL)"
)
return connection
def claim(self, *, nonce: str, approval_digest: str, now: int) -> None:
connection = self._connection()
try:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"SELECT state FROM owner_merge_nonce WHERE nonce=?", (nonce,)
).fetchone()
if existing is not None:
connection.rollback()
_fail("OWNER_MERGE_NONCE_REPLAY")
connection.execute(
"INSERT INTO owner_merge_nonce(nonce,approval_digest,state,claimed_at) VALUES(?,?,?,?)",
(nonce, approval_digest, "PENDING", now),
)
connection.commit()
except OwnerMergeApprovalError:
raise
except sqlite3.Error as exc:
try:
connection.rollback()
except sqlite3.Error:
pass
raise OwnerMergeApprovalError("OWNER_MERGE_NONCE_LEDGER_UNAVAILABLE") from exc
finally:
connection.close()
def mark(self, *, nonce: str, state: str) -> None:
if state not in {"COMPLETED", "UNKNOWN"}:
_fail("OWNER_MERGE_NONCE_STATE_INVALID")
connection = self._connection()
try:
connection.execute("BEGIN IMMEDIATE")
updated = connection.execute(
"UPDATE owner_merge_nonce SET state=? WHERE nonce=? AND state='PENDING'",
(state, nonce),
).rowcount
if updated != 1:
connection.rollback()
_fail("OWNER_MERGE_NONCE_STATE_CONFLICT")
connection.commit()
except OwnerMergeApprovalError:
raise
except sqlite3.Error as exc:
try:
connection.rollback()
except sqlite3.Error:
pass
raise OwnerMergeApprovalError("OWNER_MERGE_NONCE_LEDGER_UNAVAILABLE") from exc
finally:
connection.close()
class GovernedOwnerMergeAdmission:
"""Admit one protected merge without owning any policy or technical gate."""
def __init__(
self,
*,
policy: OwnerMergeApprovalPolicy,
verifier: OwnerMergeApprovalVerifier,
live_state_reader: Callable[[], OwnerMergeLiveState],
executor: GovernedMergeExecutor,
reconciler: MergeOidReconciler,
nonce_ledger: SQLiteOwnerMergeNonceLedger,
clock: Callable[[], float] = time.time,
) -> None:
policy.validate()
self.policy = policy
self.verifier = verifier
self.live_state_reader = live_state_reader
self.executor = executor
self.reconciler = reconciler
self.nonce_ledger = nonce_ledger
self.clock = clock
def _validate_live_state(self, live: OwnerMergeLiveState) -> None:
if not isinstance(live, OwnerMergeLiveState):
_fail("OWNER_MERGE_LIVE_STATE_INVALID")
policy = self.policy
if live.repository != policy.repository:
_fail("OWNER_MERGE_REPOSITORY_DRIFT")
if live.pr_number != policy.pr_number:
_fail("OWNER_MERGE_PR_BINDING_DRIFT")
if live.pr_head_oid != policy.pr_head_oid:
_fail("OWNER_MERGE_HEAD_DRIFT")
if live.main_oid != policy.expected_main_oid:
_fail("OWNER_MERGE_BASE_DRIFT")
if live.required_check_contract_digest != policy.required_check_contract_digest:
_fail("OWNER_MERGE_REQUIRED_CHECK_CONTRACT_DRIFT")
if not isinstance(live.check_statuses, Mapping) or set(live.check_statuses) != set(policy.required_checks):
_fail("OWNER_MERGE_REQUIRED_CHECK_CONTRACT_DRIFT")
if any(value is not True for value in live.check_statuses.values()):
_fail("OWNER_MERGE_REQUIRED_CHECKS_FAILED")
def admit(self, artifact: SignedArtifact | Mapping[str, Any]) -> OwnerMergeAdmissionReceipt:
approval, proof_path = self.verifier.verify(artifact, self.policy)
self._validate_live_state(self.live_state_reader())
nonce = str(approval["nonce"])
approval_digest = hashlib.sha256(canonical_json_bytes(approval)).hexdigest()
self.nonce_ledger.claim(nonce=nonce, approval_digest=approval_digest, now=int(self.clock()))
try:
merge_oid = self.executor.execute(dict(approval))
_require_oid(merge_oid, "OWNER_MERGE_EXECUTOR_RESULT_INVALID")
readback = self.reconciler.readback(merge_oid)
if (
not isinstance(readback, OwnerMergeOidReadback)
or readback.authority_main_oid != merge_oid
or readback.gitea_main_oid != merge_oid
or readback.api_main_oid != merge_oid
):
self.nonce_ledger.mark(nonce=nonce, state="UNKNOWN")
_fail("OWNER_MERGE_OID_RECONCILIATION_REQUIRED")
except OwnerMergeApprovalError:
# Every failure after a durable claim represents an operation that
# is either known failed or needs reconciliation. Preserve the
# one-shot fence rather than exposing a retryable PENDING state.
try:
self.nonce_ledger.mark(nonce=nonce, state="UNKNOWN")
except OwnerMergeApprovalError:
pass
raise
except Exception as exc:
try:
self.nonce_ledger.mark(nonce=nonce, state="UNKNOWN")
except OwnerMergeApprovalError:
pass
raise OwnerMergeApprovalError("OWNER_MERGE_EXECUTION_UNKNOWN") from exc
self.nonce_ledger.mark(nonce=nonce, state="COMPLETED")
return OwnerMergeAdmissionReceipt(
approval_id=str(approval["approval_id"]),
nonce=nonce,
owner_identity=str(approval["owner_identity"]),
policy_digest=str(approval["policy_digest"]),
proof_path=proof_path,
merge_oid=merge_oid,
)
@@ -0,0 +1,287 @@
"""Restricted issuance service for ``aios.owner-merge-approval.v2``.
The service has one Owner-only operation. It accepts an empty request payload,
loads one immutable policy, generates approval identifiers and nonces itself,
and stores one exact issuance response. It deliberately does not expose a
generic signing, payload, or policy-selection API.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import sqlite3
import stat
import time
from typing import Any, Callable, Mapping
from aios_owner_merge_authorization import (
OwnerMergeApprovalAuthority,
OwnerMergeApprovalError,
OwnerMergeApprovalPolicy,
OwnerMergeApprovalVerifier,
)
from aios_single_host_authority import RegistryOwnerApprovalVerifier, SignedArtifact
ISSUE_OPERATION = "issue_owner_merge_approval"
def _fail(code: str) -> None:
raise OwnerMergeApprovalError(code)
def _secure_regular(path: Path, *, require_root_owner: bool) -> None:
try:
metadata = path.lstat()
except OSError as exc:
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_FILE_MISSING") from exc
if path.is_symlink() or not stat.S_ISREG(metadata.st_mode):
_fail("OWNER_MERGE_ISSUER_FILE_INVALID")
if stat.S_IMODE(metadata.st_mode) & 0o022:
_fail("OWNER_MERGE_ISSUER_FILE_WRITABLE")
if require_root_owner and metadata.st_uid != 0:
_fail("OWNER_MERGE_ISSUER_FILE_NOT_ROOT_OWNED")
def _read_policy(path: Path, *, require_root_owner: bool) -> OwnerMergeApprovalPolicy:
_secure_regular(path, require_root_owner=require_root_owner)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_POLICY_INVALID") from exc
if not isinstance(value, Mapping):
_fail("OWNER_MERGE_ISSUER_POLICY_INVALID")
expected = set(OwnerMergeApprovalPolicy.__dataclass_fields__)
if set(value) != expected or not isinstance(value.get("required_checks"), list):
_fail("OWNER_MERGE_ISSUER_POLICY_INVALID")
try:
policy = OwnerMergeApprovalPolicy(
repository=value["repository"],
pr_number=value["pr_number"],
pr_head_oid=value["pr_head_oid"],
expected_main_oid=value["expected_main_oid"],
operation=value["operation"],
required_check_contract_digest=value["required_check_contract_digest"],
required_checks=tuple(value["required_checks"]),
owner_identity=value["owner_identity"],
audit_lineage_digest=value["audit_lineage_digest"],
maximum_ttl_seconds=value["maximum_ttl_seconds"],
policy_version=value["policy_version"],
schema=value["schema"],
)
policy.validate()
except (KeyError, TypeError, OwnerMergeApprovalError) as exc:
if isinstance(exc, OwnerMergeApprovalError):
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_POLICY_INVALID") from exc
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_POLICY_INVALID") from exc
return policy
def _artifact_dict(artifact: SignedArtifact) -> dict[str, Any]:
return {
"payload": dict(artifact.payload),
"signature": artifact.signature,
"key_id": artifact.key_id,
"namespace": artifact.namespace,
}
class SQLiteOwnerMergeIssuanceLedger:
"""Durably replay a current approval and retain expired issuance history."""
def __init__(self, path: Path) -> None:
self.path = Path(path)
def _connect(self) -> sqlite3.Connection:
if self.path.is_symlink() or self.path.parent.is_symlink():
_fail("OWNER_MERGE_ISSUER_LEDGER_PATH_INVALID")
self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
connection = sqlite3.connect(self.path, timeout=5, isolation_level=None)
connection.execute(
"CREATE TABLE IF NOT EXISTS owner_merge_issuance ("
"nonce TEXT PRIMARY KEY, policy_digest TEXT NOT NULL, response_json TEXT NOT NULL, "
"issued_at INTEGER NOT NULL, expires_at INTEGER NOT NULL)"
)
connection.execute(
"CREATE INDEX IF NOT EXISTS owner_merge_issuance_policy_expiry "
"ON owner_merge_issuance(policy_digest, expires_at)"
)
return connection
def issue_once(
self,
*,
policy_digest: str,
now: int,
issuer: Callable[[], dict[str, Any]],
validate_current: Callable[[dict[str, Any]], None],
expiry_of: Callable[[dict[str, Any]], int],
) -> tuple[dict[str, Any], bool]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT response_json, expires_at FROM owner_merge_issuance "
"WHERE policy_digest=? AND expires_at>? ORDER BY issued_at DESC",
(policy_digest, now),
).fetchall()
if len(rows) > 1:
connection.rollback()
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
if rows:
try:
response = json.loads(rows[0][0])
except (TypeError, json.JSONDecodeError) as exc:
connection.rollback()
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_LEDGER_INVALID") from exc
if not isinstance(response, dict):
connection.rollback()
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
if type(rows[0][1]) is not int or expiry_of(response) != rows[0][1]:
connection.rollback()
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
validate_current(response)
connection.rollback()
return response, True
response = issuer()
expires_at = expiry_of(response)
if expires_at <= now:
connection.rollback()
_fail("OWNER_MERGE_ISSUER_RESPONSE_EXPIRED")
artifact = response.get("artifact")
if not isinstance(artifact, Mapping):
connection.rollback()
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
payload = artifact.get("payload")
if not isinstance(payload, Mapping) or not isinstance(payload.get("nonce"), str):
connection.rollback()
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
encoded = json.dumps(
response,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
)
connection.execute(
"INSERT INTO owner_merge_issuance(nonce,policy_digest,response_json,issued_at,expires_at) "
"VALUES(?,?,?,?,?)",
(payload["nonce"], policy_digest, encoded, now, expires_at),
)
connection.commit()
return response, False
except OwnerMergeApprovalError:
raise
except sqlite3.IntegrityError as exc:
try:
connection.rollback()
except sqlite3.Error:
pass
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_NONCE_CONFLICT") from exc
except sqlite3.Error as exc:
try:
connection.rollback()
except sqlite3.Error:
pass
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_LEDGER_UNAVAILABLE") from exc
finally:
connection.close()
class OwnerMergeApprovalService:
"""A formal structured adapter, not an arbitrary signing oracle."""
def __init__(
self,
*,
policy_path: Path,
registry_path: Path,
owner_key_path: Path,
issuance_ledger_path: Path,
allowed_uids: tuple[int, ...],
require_root_owner: bool = True,
clock: Callable[[], float] = time.time,
token_urlsafe: Callable[[int], str] | None = None,
) -> None:
if not allowed_uids or any(type(uid) is not int or uid < 0 for uid in allowed_uids):
_fail("OWNER_MERGE_ISSUER_ALLOWED_UIDS_INVALID")
self.policy_path = Path(policy_path)
self.registry_path = Path(registry_path)
self.owner_key_path = Path(owner_key_path)
self.allowed_uids = tuple(allowed_uids)
self.require_root_owner = require_root_owner
self.clock = clock
self.token_urlsafe = token_urlsafe
self.ledger = SQLiteOwnerMergeIssuanceLedger(issuance_ledger_path)
def handle(self, request: Mapping[str, Any], *, peer_uid: int) -> dict[str, Any]:
if peer_uid not in self.allowed_uids:
_fail("OWNER_MERGE_ISSUER_PEER_UID_FORBIDDEN")
if not isinstance(request, Mapping) or set(request) != {"operation", "payload"}:
_fail("OWNER_MERGE_ISSUER_REQUEST_INVALID")
if request.get("operation") != ISSUE_OPERATION:
_fail("OWNER_MERGE_ISSUER_OPERATION_FORBIDDEN")
if request.get("payload") != {}:
_fail("OWNER_MERGE_ISSUER_REQUEST_FIELDS_INVALID")
policy = _read_policy(
self.policy_path,
require_root_owner=self.require_root_owner,
)
_secure_regular(
self.owner_key_path,
require_root_owner=self.require_root_owner,
)
primary = RegistryOwnerApprovalVerifier(
self.registry_path,
require_root_owner=self.require_root_owner,
)
verifier = OwnerMergeApprovalVerifier(primary=primary, clock=self.clock)
def expiry_of(response: dict[str, Any]) -> int:
if set(response) != {"status", "artifact"} or response.get("status") != "PASS":
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
artifact = response.get("artifact")
if not isinstance(artifact, Mapping):
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
payload = artifact.get("payload")
if not isinstance(payload, Mapping) or type(payload.get("expires_at")) is not int:
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
return payload["expires_at"]
def validate_current(response: dict[str, Any]) -> None:
expiry_of(response)
artifact = response["artifact"]
if not isinstance(artifact, Mapping):
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
try:
verifier.verify(artifact, policy)
except OwnerMergeApprovalError as exc:
raise OwnerMergeApprovalError("OWNER_MERGE_ISSUER_LEDGER_INVALID") from exc
def issue() -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if self.token_urlsafe is not None:
kwargs["token_urlsafe"] = self.token_urlsafe
artifact = OwnerMergeApprovalAuthority(
self.owner_key_path,
policy,
require_root_owner=self.require_root_owner,
clock=self.clock,
**kwargs,
).issue()
value = _artifact_dict(artifact)
verifier.verify(value, policy)
return {"status": "PASS", "artifact": value}
response, idempotent = self.ledger.issue_once(
policy_digest=policy.digest,
now=int(self.clock()),
issuer=issue,
validate_current=validate_current,
expiry_of=expiry_of,
)
if set(response) != {"status", "artifact"} or response.get("status") != "PASS":
_fail("OWNER_MERGE_ISSUER_LEDGER_INVALID")
return {**response, "idempotent": idempotent}
@@ -0,0 +1,967 @@
"""Bounded, read-only discovery over a verified immutable Git repository.
The module is deliberately independent from Task, lease, credential, worktree,
and write code. Every public operation verifies the exact repository identity,
resolves a floating ref once to a full commit OID, and then reads only that
immutable snapshot.
"""
from __future__ import annotations
import base64
from collections.abc import Iterable, Iterator, Mapping
import fnmatch
import hashlib
import hmac
import json
import os
from pathlib import Path
import re
import subprocess
import time
from authoritative_git import AuthoritativeGitError, verify_repository
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,239}$")
_IDENTITY_RE = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
)
_READ_ONLY_COMMANDS = frozenset({"cat-file", "diff", "ls-tree", "rev-parse"})
_MAX_COMMAND_OUTPUT = 1_048_576
_MAX_RECORD_BYTES = 1_048_576
_MAX_LIMIT = 1_000
_MAX_DEPTH = 32
_MAX_BYTES = 1_048_576
_MAX_GLOBS = 16
_MAX_QUERY_LENGTH = 512
_MAX_CURSOR_LENGTH = 4_096
_GIT_TIMEOUT_SECONDS = 8.0
class RepositoryDiscoveryError(RuntimeError):
"""A fail-closed repository discovery request error."""
def __init__(self, code: str, *, status_code: int | None = None) -> None:
self.code = str(code)
self.status_code = (
status_code if status_code is not None else self._status_for(self.code)
)
super().__init__(self.code)
@staticmethod
def _status_for(code: str) -> int:
if "OUTPUT_LIMIT" in code:
return 413
if code.endswith("_NOT_FOUND"):
return 404
if code in {"REPOSITORY_NOT_ALLOWED", "REPOSITORY_IDENTITY_MISMATCH"}:
return 403
if code in {
"REPOSITORY_DISCOVERY_UNAVAILABLE",
"REPOSITORY_DISCOVERY_GIT_FAILED",
"REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID",
"REPOSITORY_DISCOVERY_CURSOR_SECRET_REQUIRED",
}:
return 503
return 400
class RepositoryDiscoveryNotFound(RepositoryDiscoveryError):
"""A validated immutable object or path was not found."""
class RepositoryDiscoveryOutputTooLarge(RepositoryDiscoveryError):
"""A fixed read-only Git query exceeded its collection boundary."""
def _error(code: str) -> RepositoryDiscoveryError:
return RepositoryDiscoveryError(code)
def _request_mapping(request: Mapping[str, object]) -> Mapping[str, object]:
if not isinstance(request, Mapping):
raise _error("REPOSITORY_DISCOVERY_REQUEST_INVALID")
return request
def _required_request_value(request: Mapping[str, object], field: str) -> object:
value = request.get(field)
if value is None:
raise _error("REPOSITORY_DISCOVERY_REQUEST_INVALID")
return value
def _canonical(value: Mapping[str, object]) -> bytes:
return json.dumps(
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode("ascii")
def _positive_int(value: object, field: str, maximum: int) -> int:
if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum:
raise _error(f"REPOSITORY_DISCOVERY_INVALID_{field.upper()}")
return value
def _safe_relative_path(
value: object,
field: str,
*,
allow_empty: bool,
allow_glob: bool = False,
) -> str:
if not isinstance(value, str) or value != value.strip() or "\x00" in value:
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
if "\\" in value:
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
if value in ("", "."):
if allow_empty:
return ""
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
if value.startswith(("/", "-")) or "//" in value or ":" in value:
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
if not allow_glob and any(character in value for character in "*?["):
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
parts = value.split("/")
if any(part in ("", ".", "..") for part in parts):
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
if len(value) > 512:
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
return value
def _safe_glob(value: object) -> str:
return _safe_relative_path(value, "path_glob", allow_empty=False, allow_glob=True)
def _safe_ref(value: str) -> bool:
return bool(
_REF_RE.fullmatch(value)
and ".." not in value
and "//" not in value
and "@{" not in value
and not value.endswith(("/", ".", ".lock"))
and not any(part in ("", ".", "..") for part in value.split("/"))
)
def _parse_tree_record(record: bytes) -> dict[str, str] | None:
try:
header, raw_path = record.split(b"\t", 1)
mode, kind, oid = header.decode("ascii").split(" ", 2)
path = raw_path.decode("utf-8")
except (UnicodeDecodeError, ValueError) as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
if kind not in {"blob", "tree", "commit"} or not _OID_RE.fullmatch(oid.lower()):
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
try:
_safe_relative_path(path, "tree_path", allow_empty=False)
except RepositoryDiscoveryError:
# Git permits filenames outside the public POSIX-relative contract.
# They are not exposed as usable discovery paths.
return None
return {"mode": mode, "type": kind, "object_oid": oid.lower(), "path": path}
def _decode_output_path(raw: bytes) -> str:
try:
value = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
return _safe_relative_path(value, "git_output_path", allow_empty=False)
class RepositoryDiscovery:
"""Repository-scoped immutable Git discovery with signed cursors."""
def __init__(
self,
*,
allowed_repositories: Iterable[str] | None = None,
git_dir: str | Path | None = None,
cursor_secret: str | bytes | None = None,
) -> None:
configured = allowed_repositories
if configured is None:
configured = (os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", ""),)
identities = tuple(configured)
if not identities or any(
not isinstance(item, str) or not _IDENTITY_RE.fullmatch(item)
for item in identities
):
raise _error("REPOSITORY_DISCOVERY_ALLOWLIST_INVALID")
self._allowed_repositories = frozenset(identities)
self._git_dir = git_dir
secret = (
cursor_secret
if cursor_secret is not None
else os.getenv("AIOS_REPOSITORY_DISCOVERY_CURSOR_SECRET", "")
)
if isinstance(secret, str):
secret = secret.encode("utf-8")
if not isinstance(secret, bytes) or len(secret) < 16:
raise _error("REPOSITORY_DISCOVERY_CURSOR_SECRET_REQUIRED")
self._cursor_secret = secret
def _repository_path(self, repository: object) -> Path:
if not isinstance(repository, str) or repository not in self._allowed_repositories:
raise _error("REPOSITORY_NOT_ALLOWED")
try:
path, actual_identity = verify_repository(
git_dir=self._git_dir,
repository_identity=repository,
)
except AuthoritativeGitError as exc:
code = (
"REPOSITORY_IDENTITY_MISMATCH"
if "REPOSITORY_IDENTITY_MISMATCH" in str(exc)
else "REPOSITORY_DISCOVERY_UNAVAILABLE"
)
raise _error(code) from exc
if actual_identity != repository:
raise _error("REPOSITORY_IDENTITY_MISMATCH")
return path
@staticmethod
def _git_env() -> dict[str, str]:
return {
**os.environ,
"GIT_OPTIONAL_LOCKS": "0",
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_TERMINAL_PROMPT": "0",
}
@staticmethod
def _run(
path: Path,
args: list[str],
*,
max_output: int = _MAX_COMMAND_OUTPUT,
) -> bytes:
if not args or args[0] not in _READ_ONLY_COMMANDS:
raise _error("REPOSITORY_DISCOVERY_GIT_COMMAND_REJECTED")
output_limit = min(max_output, _MAX_COMMAND_OUTPUT)
process: subprocess.Popen[bytes] | None = None
try:
process = subprocess.Popen(
["git", "--git-dir", str(path), *args],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=RepositoryDiscovery._git_env(),
)
assert process.stdout is not None
output = process.stdout.read(output_limit + 1)
if len(output) > output_limit:
process.kill()
process.wait(timeout=1)
raise RepositoryDiscoveryOutputTooLarge(
"REPOSITORY_DISCOVERY_OUTPUT_LIMIT"
)
returncode = process.wait(timeout=_GIT_TIMEOUT_SECONDS)
except RepositoryDiscoveryError:
raise
except (OSError, subprocess.TimeoutExpired) as exc:
if process is not None:
try:
process.kill()
process.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
pass
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED") from exc
if returncode != 0:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED")
return output
@staticmethod
def _read_blob_prefix(path: Path, oid: str, maximum: int) -> tuple[bytes, bool]:
"""Read at most maximum bytes plus one sentinel byte."""
process: subprocess.Popen[bytes] | None = None
try:
process = subprocess.Popen(
["git", "--git-dir", str(path), "cat-file", "blob", oid],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=RepositoryDiscovery._git_env(),
)
assert process.stdout is not None
content = process.stdout.read(maximum + 1)
truncated = len(content) > maximum
if truncated:
process.kill()
process.wait(timeout=1)
return content[:maximum], True
if process.wait(timeout=_GIT_TIMEOUT_SECONDS) != 0:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED")
return content, False
except RepositoryDiscoveryError:
raise
except (OSError, subprocess.TimeoutExpired) as exc:
if process is not None:
try:
process.kill()
process.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
pass
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED") from exc
def _iter_nul_records(self, path: Path, args: list[str]) -> Iterator[bytes]:
if not args or args[0] not in _READ_ONLY_COMMANDS:
raise _error("REPOSITORY_DISCOVERY_GIT_COMMAND_REJECTED")
process: subprocess.Popen[bytes] | None = None
buffer = b""
started = time.monotonic()
try:
process = subprocess.Popen(
["git", "--git-dir", str(path), *args],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=self._git_env(),
)
assert process.stdout is not None
while True:
if time.monotonic() - started > _GIT_TIMEOUT_SECONDS:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED")
chunk = process.stdout.read(64 * 1024)
if not chunk:
break
buffer += chunk
if len(buffer) > _MAX_RECORD_BYTES and b"\0" not in buffer:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
while b"\0" in buffer:
record, buffer = buffer.split(b"\0", 1)
if len(record) > _MAX_RECORD_BYTES:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
yield record
if buffer:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
if process.wait(
timeout=max(0.1, _GIT_TIMEOUT_SECONDS - (time.monotonic() - started))
) != 0:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED")
except RepositoryDiscoveryError:
raise
except (OSError, subprocess.TimeoutExpired) as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED") from exc
finally:
if process is not None and process.poll() is None:
try:
process.kill()
process.wait(timeout=1)
except (OSError, subprocess.TimeoutExpired):
pass
def _resolve_at(self, path: Path, revision: object, field: str) -> str:
if (
not isinstance(revision, str)
or revision != revision.strip()
or not revision
or revision.startswith("-")
):
raise _error("REPOSITORY_DISCOVERY_INVALID_REVISION")
if _OID_RE.fullmatch(revision.lower()):
candidates = [revision.lower()]
elif revision == "HEAD":
candidates = ["HEAD"]
elif _safe_ref(revision) and not any(
token in revision for token in ("^", "~", ":", "\\")
):
if revision.startswith("refs/"):
if not revision.startswith(("refs/heads/", "refs/tags/")):
raise _error("REPOSITORY_DISCOVERY_INVALID_REVISION")
candidates = [revision]
else:
candidates = [f"refs/heads/{revision}", f"refs/tags/{revision}"]
else:
raise _error("REPOSITORY_DISCOVERY_INVALID_REVISION")
resolved: list[str] = []
for candidate in candidates:
try:
output = self._run(
path,
["rev-parse", "--verify", f"{candidate}^{{commit}}"],
max_output=256,
)
except RepositoryDiscoveryError:
continue
oid = output.decode("ascii", errors="ignore").strip().lower()
if _OID_RE.fullmatch(oid):
resolved.append(oid)
if not resolved:
raise RepositoryDiscoveryNotFound(
f"REPOSITORY_DISCOVERY_{field.upper()}_NOT_FOUND"
)
if len(set(resolved)) != 1:
raise _error("REPOSITORY_DISCOVERY_AMBIGUOUS_REVISION")
return resolved[0]
def resolve_revision(self, repository: str, revision: str) -> str:
return self._resolve_at(self._repository_path(repository), revision, "revision")
def _cursor_offset(
self,
cursor: str | None,
*,
operation: str,
repository: str,
resolved_oid: str,
query: Mapping[str, object],
) -> int:
if cursor is None:
return 0
if (
not isinstance(cursor, str)
or not cursor
or len(cursor) > _MAX_CURSOR_LENGTH
or "." not in cursor
):
raise _error("REPOSITORY_DISCOVERY_CURSOR_INVALID")
encoded, supplied_signature = cursor.split(".", 1)
try:
padded = encoded + "=" * (-len(encoded) % 4)
payload = json.loads(
base64.urlsafe_b64decode(padded.encode("ascii")).decode("ascii")
)
except (UnicodeError, ValueError, json.JSONDecodeError) as exc:
raise _error("REPOSITORY_DISCOVERY_CURSOR_INVALID") from exc
expected_signature = hmac.new(
self._cursor_secret, encoded.encode("ascii"), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(supplied_signature, expected_signature):
raise _error("REPOSITORY_DISCOVERY_CURSOR_INVALID")
expected = {
"operation": operation,
"repository": repository,
"resolved_oid": resolved_oid,
"query": dict(query),
}
if (
not isinstance(payload, dict)
or payload.get("v") != 1
or any(payload.get(key) != value for key, value in expected.items())
):
raise _error("REPOSITORY_DISCOVERY_CURSOR_INVALID")
offset = payload.get("offset")
if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
raise _error("REPOSITORY_DISCOVERY_CURSOR_INVALID")
return offset
def _next_cursor(
self,
*,
operation: str,
repository: str,
resolved_oid: str,
query: Mapping[str, object],
offset: int,
) -> str:
payload = {
"v": 1,
"operation": operation,
"repository": repository,
"resolved_oid": resolved_oid,
"query": dict(query),
"offset": offset,
}
encoded = base64.urlsafe_b64encode(_canonical(payload)).decode("ascii").rstrip("=")
signature = hmac.new(
self._cursor_secret, encoded.encode("ascii"), hashlib.sha256
).hexdigest()
return f"{encoded}.{signature}"
def _iter_tree_entries(
self,
path: Path,
oid: str,
prefix: str,
*,
include_trees: bool,
) -> Iterator[dict[str, str]]:
args = ["ls-tree", "-z", "-r", "--full-tree"]
if include_trees:
args.append("-t")
args.append(oid)
if prefix:
args.extend(["--", prefix])
for record in self._iter_nul_records(path, args):
entry = _parse_tree_record(record)
if entry is None:
continue
if not prefix or entry["path"] == prefix or entry["path"].startswith(prefix + "/"):
yield entry
def list_tree(
self,
*,
repository: str,
revision: str,
prefix: str = "",
depth: int = 8,
limit: int = 100,
cursor: str | None = None,
) -> dict[str, object]:
prefix = _safe_relative_path(prefix, "prefix", allow_empty=True)
depth = _positive_int(depth, "depth", _MAX_DEPTH)
limit = _positive_int(limit, "limit", _MAX_LIMIT)
path = self._repository_path(repository)
resolved_oid = self._resolve_at(path, revision, "revision")
query = {"prefix": prefix, "depth": depth, "limit": limit}
offset = self._cursor_offset(
cursor,
operation="list_tree",
repository=repository,
resolved_oid=resolved_oid,
query=query,
)
page: list[dict[str, str]] = []
matched = 0
for entry in self._iter_tree_entries(path, resolved_oid, prefix, include_trees=True):
relative = entry["path"][len(prefix):].lstrip("/") if prefix else entry["path"]
if relative.count("/") + 1 > depth:
continue
if matched >= offset and len(page) < limit:
page.append(entry)
matched += 1
has_more = offset + len(page) < matched
return {
"repository": repository,
"resolved_oid": resolved_oid,
"entries": page,
"truncated": has_more,
"next_cursor": (
self._next_cursor(
operation="list_tree",
repository=repository,
resolved_oid=resolved_oid,
query=query,
offset=offset + len(page),
)
if has_more
else None
),
}
def list_repository_tree(self, request: Mapping[str, object]) -> dict[str, object]:
request = _request_mapping(request)
return self.list_tree(
repository=_required_request_value(request, "repository"), # type: ignore[arg-type]
revision=request.get("revision", request.get("ref", "HEAD")), # type: ignore[arg-type]
prefix=request.get("prefix", ""), # type: ignore[arg-type]
depth=request.get("depth", 8), # type: ignore[arg-type]
limit=request.get("limit", 100), # type: ignore[arg-type]
cursor=request.get("cursor"), # type: ignore[arg-type]
)
def _blob_size(self, path: Path, oid: str) -> int:
output = self._run(path, ["cat-file", "-s", oid], max_output=128)
try:
size = int(output.decode("ascii").strip())
except (UnicodeDecodeError, ValueError) as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
if size < 0:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
return size
def search_source(
self,
*,
repository: str,
revision: str,
query: str,
path_globs: Iterable[str] | None = None,
max_matches: int = 100,
max_bytes: int = 262_144,
cursor: str | None = None,
) -> dict[str, object]:
if (
not isinstance(query, str)
or not query
or "\x00" in query
or len(query) > _MAX_QUERY_LENGTH
):
raise _error("REPOSITORY_DISCOVERY_QUERY_INVALID")
max_matches = _positive_int(max_matches, "max_matches", _MAX_LIMIT)
max_bytes = _positive_int(max_bytes, "max_bytes", _MAX_BYTES)
if path_globs is None:
globs = ("*",)
else:
if isinstance(path_globs, (str, bytes)) or not isinstance(path_globs, Iterable):
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
globs = tuple(_safe_glob(item) for item in path_globs)
if not globs or len(globs) > _MAX_GLOBS:
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
path = self._repository_path(repository)
resolved_oid = self._resolve_at(path, revision, "revision")
query_binding = {
"query": query,
"path_globs": list(globs),
"max_matches": max_matches,
"max_bytes": max_bytes,
}
offset = self._cursor_offset(
cursor,
operation="search_source",
repository=repository,
resolved_oid=resolved_oid,
query=query_binding,
)
scanned_bytes = 0
skipped_for_bound = False
binary_blobs = 0
matched = 0
matches: list[dict[str, object]] = []
has_more = False
for entry in self._iter_tree_entries(path, resolved_oid, "", include_trees=False):
if entry["type"] != "blob" or not any(
fnmatch.fnmatchcase(entry["path"], pattern) for pattern in globs
):
continue
blob_size = self._blob_size(path, entry["object_oid"])
remaining = max_bytes - scanned_bytes
if blob_size > remaining:
skipped_for_bound = True
continue
raw, clipped = self._read_blob_prefix(path, entry["object_oid"], blob_size)
if clipped:
raise _error("REPOSITORY_DISCOVERY_GIT_FAILED")
scanned_bytes += len(raw)
if b"\0" in raw:
binary_blobs += 1
continue
text = raw.decode("utf-8", errors="replace")
for number, line in enumerate(text.splitlines(), start=1):
if query not in line:
continue
if matched < offset:
matched += 1
continue
if len(matches) >= max_matches:
has_more = True
break
matches.append(
{
"path": entry["path"],
"line_start": number,
"line_end": number,
"blob_oid": entry["object_oid"],
"snippet": line[:512],
}
)
matched += 1
if has_more:
break
return {
"repository": repository,
"resolved_oid": resolved_oid,
"matches": matches,
"scanned_bytes": scanned_bytes,
"max_bytes": max_bytes,
"binary_blobs_skipped": binary_blobs,
"truncated": bool(has_more or skipped_for_bound),
"next_cursor": (
self._next_cursor(
operation="search_source",
repository=repository,
resolved_oid=resolved_oid,
query=query_binding,
offset=offset + len(matches),
)
if has_more
else None
),
}
def search_repository_source(self, request: Mapping[str, object]) -> dict[str, object]:
request = _request_mapping(request)
path_globs = request.get("path_globs")
if path_globs is not None and (
isinstance(path_globs, (str, bytes)) or not isinstance(path_globs, Iterable)
):
raise _error("REPOSITORY_DISCOVERY_PATH_INVALID")
return self.search_source(
repository=_required_request_value(request, "repository"), # type: ignore[arg-type]
revision=request.get("revision", request.get("ref", "HEAD")), # type: ignore[arg-type]
query=_required_request_value(request, "query"), # type: ignore[arg-type]
path_globs=path_globs, # type: ignore[arg-type]
max_matches=request.get("max_matches", 100), # type: ignore[arg-type]
max_bytes=request.get("max_bytes", 262_144), # type: ignore[arg-type]
cursor=request.get("cursor"), # type: ignore[arg-type]
)
def _iter_diff_name_entries(
self,
path: Path,
from_oid: str,
to_oid: str,
prefix: str,
) -> Iterator[dict[str, object]]:
suffix = ["--", prefix] if prefix else []
records = self._iter_nul_records(
path,
[
"diff",
"--no-ext-diff",
"--no-textconv",
"--find-renames=50%",
"--name-status",
"-z",
from_oid,
to_oid,
*suffix,
],
)
iterator = iter(records)
while True:
try:
status_raw = next(iterator)
except StopIteration:
return
try:
status = status_raw.decode("ascii")
except UnicodeDecodeError as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
if not status or status[0] not in {"A", "C", "D", "M", "R", "T", "U", "X"}:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
try:
if status[0] in {"R", "C"}:
old_path = _decode_output_path(next(iterator))
new_path = _decode_output_path(next(iterator))
yield {
"status": status,
"path": new_path,
"old_path": old_path,
}
else:
yield {
"status": status,
"path": _decode_output_path(next(iterator)),
"old_path": None,
}
except StopIteration as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
@staticmethod
def _parse_numstat_count(value: bytes) -> int | None:
if value == b"-":
return None
try:
parsed = int(value.decode("ascii"), 10)
except (UnicodeDecodeError, ValueError) as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
if parsed < 0:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
return parsed
def _diff_stats(
self,
path: Path,
from_oid: str,
to_oid: str,
prefix: str,
wanted_paths: set[str],
) -> dict[str, tuple[int | None, int | None]]:
suffix = ["--", prefix] if prefix else []
records = self._iter_nul_records(
path,
[
"diff",
"--no-ext-diff",
"--no-textconv",
"--find-renames=50%",
"--numstat",
"-z",
from_oid,
to_oid,
*suffix,
],
)
iterator = iter(records)
stats: dict[str, tuple[int | None, int | None]] = {}
for row in iterator:
if not row:
continue
fields = row.split(b"\t", 2)
if len(fields) != 3:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID")
additions = self._parse_numstat_count(fields[0])
deletions = self._parse_numstat_count(fields[1])
if fields[2]:
stat_path = _decode_output_path(fields[2])
else:
try:
next(iterator) # old path
stat_path = _decode_output_path(next(iterator))
except StopIteration as exc:
raise _error("REPOSITORY_DISCOVERY_GIT_OUTPUT_INVALID") from exc
if stat_path in wanted_paths:
stats[stat_path] = (additions, deletions)
if wanted_paths <= set(stats):
break
return stats
def diff_index(
self,
*,
repository: str,
from_revision: str,
to_revision: str,
prefix: str = "",
limit: int = 100,
cursor: str | None = None,
) -> dict[str, object]:
prefix = _safe_relative_path(prefix, "prefix", allow_empty=True)
limit = _positive_int(limit, "limit", _MAX_LIMIT)
path = self._repository_path(repository)
from_oid = self._resolve_at(path, from_revision, "from")
to_oid = self._resolve_at(path, to_revision, "to")
query = {
"from_oid": from_oid,
"to_oid": to_oid,
"prefix": prefix,
"limit": limit,
}
offset = self._cursor_offset(
cursor,
operation="diff_index",
repository=repository,
resolved_oid=to_oid,
query=query,
)
page: list[dict[str, object]] = []
total = 0
for entry in self._iter_diff_name_entries(path, from_oid, to_oid, prefix):
if total >= offset and len(page) < limit:
page.append(entry)
total += 1
wanted_paths = {str(entry["path"]) for entry in page}
stat_by_path = (
self._diff_stats(path, from_oid, to_oid, prefix, wanted_paths)
if page
else {}
)
for entry in page:
additions, deletions = stat_by_path.get(
str(entry["path"]), (None, None)
)
entry["additions"] = additions
entry["deletions"] = deletions
has_more = offset + len(page) < total
return {
"repository": repository,
"resolved_from_oid": from_oid,
"resolved_to_oid": to_oid,
"entries": page,
"truncated": has_more,
"next_cursor": (
self._next_cursor(
operation="diff_index",
repository=repository,
resolved_oid=to_oid,
query=query,
offset=offset + len(page),
)
if has_more
else None
),
}
def get_diff_index(self, request: Mapping[str, object]) -> dict[str, object]:
request = _request_mapping(request)
from_revision = request.get("from_revision", request.get("from"))
to_revision = request.get("to_revision", request.get("to"))
prefix = request.get("prefix", request.get("path_prefix", ""))
if from_revision is None or to_revision is None:
raise _error("REPOSITORY_DISCOVERY_REQUEST_INVALID")
return self.diff_index(
repository=_required_request_value(request, "repository"), # type: ignore[arg-type]
from_revision=from_revision, # type: ignore[arg-type]
to_revision=to_revision, # type: ignore[arg-type]
prefix=prefix, # type: ignore[arg-type]
limit=request.get("limit", 100), # type: ignore[arg-type]
cursor=request.get("cursor"), # type: ignore[arg-type]
)
def read_file(
self,
*,
repository: str,
revision: str,
file_path: str,
max_bytes: int = 262_144,
) -> dict[str, object]:
file_path = _safe_relative_path(file_path, "file_path", allow_empty=False)
max_bytes = _positive_int(max_bytes, "max_bytes", _MAX_BYTES)
path = self._repository_path(repository)
resolved_oid = self._resolve_at(path, revision, "revision")
exact: list[dict[str, str]] = []
for entry in self._iter_tree_entries(
path, resolved_oid, file_path, include_trees=False
):
if entry["path"] == file_path and entry["type"] == "blob":
exact.append(entry)
if len(exact) != 1:
raise RepositoryDiscoveryNotFound("REPOSITORY_DISCOVERY_FILE_NOT_FOUND")
blob_oid = exact[0]["object_oid"]
content, truncated = self._read_blob_prefix(path, blob_oid, max_bytes)
return {
"repository": repository,
"resolved_oid": resolved_oid,
"blob_oid": blob_oid,
"path": file_path,
"content": content.decode("utf-8", errors="replace"),
"content_base64": base64.b64encode(content).decode("ascii"),
"content_bytes": len(content),
"truncated": truncated,
}
def read_repository_file(self, request: Mapping[str, object]) -> dict[str, object]:
request = _request_mapping(request)
file_path = request.get("file_path", request.get("path"))
if file_path is None:
raise _error("REPOSITORY_DISCOVERY_REQUEST_INVALID")
return self.read_file(
repository=_required_request_value(request, "repository"), # type: ignore[arg-type]
revision=request.get("revision", request.get("ref", "HEAD")), # type: ignore[arg-type]
file_path=file_path, # type: ignore[arg-type]
max_bytes=request.get("max_bytes", 262_144), # type: ignore[arg-type]
)
def create_repository_discovery(
*,
allowed_repositories: Iterable[str] | None = None,
git_dir: str | Path | None = None,
cursor_secret: str | bytes | None = None,
) -> RepositoryDiscovery:
return RepositoryDiscovery(
allowed_repositories=allowed_repositories,
git_dir=git_dir,
cursor_secret=cursor_secret,
)
def list_repository_tree(
request: Mapping[str, object], *, discovery: RepositoryDiscovery | None = None
) -> dict[str, object]:
return (discovery or create_repository_discovery()).list_repository_tree(request)
def search_repository_source(
request: Mapping[str, object], *, discovery: RepositoryDiscovery | None = None
) -> dict[str, object]:
return (discovery or create_repository_discovery()).search_repository_source(request)
def get_diff_index(
request: Mapping[str, object], *, discovery: RepositoryDiscovery | None = None
) -> dict[str, object]:
return (discovery or create_repository_discovery()).get_diff_index(request)
def read_repository_file(
request: Mapping[str, object], *, discovery: RepositoryDiscovery | None = None
) -> dict[str, object]:
return (discovery or create_repository_discovery()).read_repository_file(request)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,707 @@
"""Single-host operational delegation for AIOS migration.
This module is deliberately narrow:
* Owner signs only OperationalDelegation and ExpiredTaskRecoveryDecision.
* Machine signer signs only WorkerRuntimeBinding and WorkerRuntimeProof.
* Every object is bound to one immutable Task identity and a narrow capability set.
* There is no arbitrary sign-bytes API.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import json
from pathlib import Path
import re
import time
from typing import Any, Mapping, Sequence
from aios_single_host_authority import (
DEFAULT_MACHINE_SIGNER_ID,
DEFAULT_OWNER_SIGNER_ID,
MACHINE_NAMESPACE,
OWNER_IDENTITY,
OWNER_NAMESPACE,
LocalFileSigner,
RegistryOwnerApprovalVerifier,
SignerRegistry,
SingleHostAuthorityError,
canonical_json_bytes,
sha256_json,
verify_sshsig,
)
OPERATIONAL_POLICY_SCHEMA = "aios.single-host-operational-policy.v1"
OPERATIONAL_DELEGATION_SCHEMA = "aios.owner-operational-delegation.v1"
RECOVERY_DECISION_SCHEMA = "aios.expired-task-recovery-decision.v1"
WORKER_BINDING_SCHEMA = "aios.worker-runtime-binding.v1"
WORKER_PROOF_SCHEMA = "aios.worker-runtime-proof.v1"
REVOCATION_SCHEMA = "aios.operational-revocations.v1"
ALLOWED_OPERATIONAL_CAPABILITIES = frozenset({
"task.recovery",
"worker.runtime.bind",
"worker.runtime.proof",
"task.claim.guard",
})
ALLOWED_EXPECTED_STATE = "EXPIRED"
HEX40 = re.compile(r"^[0-9a-f]{40}$")
HEX64 = re.compile(r"^[0-9a-f]{64}$")
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
CAP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
class OperationalAuthorityError(RuntimeError):
def __init__(self, code: str):
super().__init__(code)
self.code = code
def fail(code: str) -> None:
raise OperationalAuthorityError(code)
def _exact_fields(value: Mapping[str, Any], fields: set[str], code: str) -> None:
if not isinstance(value, Mapping) or set(value) != fields:
fail(code)
def _safe_id(value: str, code: str) -> str:
if not isinstance(value, str) or not SAFE_ID.fullmatch(value):
fail(code)
return value
def _hex(value: str, n: int, code: str) -> str:
regex = HEX40 if n == 40 else HEX64
if not isinstance(value, str) or not regex.fullmatch(value):
fail(code)
return value
def _caps(value: Sequence[str], code: str) -> tuple[str, ...]:
if not isinstance(value, (list, tuple)) or not value:
fail(code)
out = tuple(sorted(set(value)))
if len(out) != len(value) or any(not isinstance(x, str) or not CAP.fullmatch(x) for x in out):
fail(code)
return out
def task_identity_sha256(task: Mapping[str, Any]) -> str:
"""Digest only immutable recovery/claim identity fields.
Revision, state, attempt and deadline are intentionally excluded.
"""
fields = (
"task_id", "repository", "target_branch", "base_oid", "generation",
"execution_plan_sha256", "external_job_identity",
)
try:
value = {name: task[name] for name in fields}
except KeyError as exc:
raise OperationalAuthorityError("TASK_IDENTITY_FIELD_MISSING") from exc
_safe_id(str(value["task_id"]), "TASK_ID_INVALID")
_safe_id(str(value["repository"]), "REPOSITORY_INVALID")
_safe_id(str(value["target_branch"]), "TARGET_BRANCH_INVALID")
_hex(str(value["base_oid"]), 40, "BASE_OID_INVALID")
if type(value["generation"]) is not int or value["generation"] < 1:
fail("GENERATION_INVALID")
_hex(str(value["execution_plan_sha256"]), 64, "EXECUTION_PLAN_DIGEST_INVALID")
_safe_id(str(value["external_job_identity"]), "EXTERNAL_JOB_IDENTITY_INVALID")
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
@dataclass(frozen=True, slots=True)
class OperationalPolicy:
repository: str
task_id: str
task_identity_sha256: str
target_branch: str
base_oid: str
generation: int
execution_plan_sha256: str
external_job_identity: str
allowed_capabilities: tuple[str, ...]
allowed_worker_ids: tuple[str, ...]
maximum_delegation_ttl_seconds: int
maximum_recovery_decision_ttl_seconds: int
maximum_binding_ttl_seconds: int
maximum_proof_ttl_seconds: int
maximum_heartbeat_age_seconds: int
maximum_task_deadline_extension_seconds: int
schema: str = OPERATIONAL_POLICY_SCHEMA
def validate(self) -> None:
if self.schema != OPERATIONAL_POLICY_SCHEMA:
fail("OPERATIONAL_POLICY_SCHEMA_INVALID")
_safe_id(self.repository, "OPERATIONAL_POLICY_REPOSITORY_INVALID")
_safe_id(self.task_id, "OPERATIONAL_POLICY_TASK_ID_INVALID")
_hex(self.task_identity_sha256, 64, "OPERATIONAL_POLICY_TASK_IDENTITY_INVALID")
_safe_id(self.target_branch, "OPERATIONAL_POLICY_BRANCH_INVALID")
_hex(self.base_oid, 40, "OPERATIONAL_POLICY_BASE_OID_INVALID")
if type(self.generation) is not int or self.generation < 1:
fail("OPERATIONAL_POLICY_GENERATION_INVALID")
_hex(self.execution_plan_sha256, 64, "OPERATIONAL_POLICY_PLAN_INVALID")
_safe_id(self.external_job_identity, "OPERATIONAL_POLICY_JOB_INVALID")
caps = _caps(self.allowed_capabilities, "OPERATIONAL_POLICY_CAPABILITIES_INVALID")
if not set(caps).issubset(ALLOWED_OPERATIONAL_CAPABILITIES):
fail("OPERATIONAL_POLICY_CAPABILITY_WIDENING")
if not self.allowed_worker_ids or any(not SAFE_ID.fullmatch(x) for x in self.allowed_worker_ids):
fail("OPERATIONAL_POLICY_WORKERS_INVALID")
bounds = (
(self.maximum_delegation_ttl_seconds, 300, 7 * 24 * 3600),
(self.maximum_recovery_decision_ttl_seconds, 30, 900),
(self.maximum_binding_ttl_seconds, 60, 24 * 3600),
(self.maximum_proof_ttl_seconds, 15, 900),
(self.maximum_heartbeat_age_seconds, 5, 300),
(self.maximum_task_deadline_extension_seconds, 300, 24 * 3600),
)
if any(type(v) is not int or not lo <= v <= hi for v, lo, hi in bounds):
fail("OPERATIONAL_POLICY_TTL_INVALID")
@property
def digest(self) -> str:
self.validate()
return sha256_json(asdict(self))
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "OperationalPolicy":
fields = set(cls.__dataclass_fields__)
_exact_fields(value, fields, "OPERATIONAL_POLICY_FIELDS_INVALID")
try:
policy = cls(
repository=value["repository"],
task_id=value["task_id"],
task_identity_sha256=value["task_identity_sha256"],
target_branch=value["target_branch"],
base_oid=value["base_oid"],
generation=value["generation"],
execution_plan_sha256=value["execution_plan_sha256"],
external_job_identity=value["external_job_identity"],
allowed_capabilities=tuple(value["allowed_capabilities"]),
allowed_worker_ids=tuple(value["allowed_worker_ids"]),
maximum_delegation_ttl_seconds=value["maximum_delegation_ttl_seconds"],
maximum_recovery_decision_ttl_seconds=value["maximum_recovery_decision_ttl_seconds"],
maximum_binding_ttl_seconds=value["maximum_binding_ttl_seconds"],
maximum_proof_ttl_seconds=value["maximum_proof_ttl_seconds"],
maximum_heartbeat_age_seconds=value["maximum_heartbeat_age_seconds"],
maximum_task_deadline_extension_seconds=value["maximum_task_deadline_extension_seconds"],
schema=value["schema"],
)
except Exception as exc:
raise OperationalAuthorityError("OPERATIONAL_POLICY_FIELDS_INVALID") from exc
policy.validate()
return policy
def build_exact_policy(
task: Mapping[str, Any],
*,
worker_id: str,
allowed_capabilities: Sequence[str] = tuple(sorted(ALLOWED_OPERATIONAL_CAPABILITIES)),
) -> OperationalPolicy:
identity = task_identity_sha256(task)
policy = OperationalPolicy(
repository=str(task["repository"]),
task_id=str(task["task_id"]),
task_identity_sha256=identity,
target_branch=str(task["target_branch"]),
base_oid=str(task["base_oid"]),
generation=int(task["generation"]),
execution_plan_sha256=str(task["execution_plan_sha256"]),
external_job_identity=str(task["external_job_identity"]),
allowed_capabilities=tuple(sorted(allowed_capabilities)),
allowed_worker_ids=(worker_id,),
maximum_delegation_ttl_seconds=24 * 3600,
maximum_recovery_decision_ttl_seconds=300,
maximum_binding_ttl_seconds=4 * 3600,
maximum_proof_ttl_seconds=300,
maximum_heartbeat_age_seconds=30,
maximum_task_deadline_extension_seconds=4 * 3600,
)
policy.validate()
return policy
def _signed_payload_dict(payload: Mapping[str, Any], *, key_id: str, namespace: str) -> dict[str, Any]:
value = dict(payload)
value["signature_namespace"] = namespace
value["signature_key_id"] = key_id
value["canonical_payload_digest"] = sha256_json(value)
return value
def _verify_digest_field(payload: Mapping[str, Any]) -> bool:
if not isinstance(payload, Mapping) or "canonical_payload_digest" not in payload:
return False
value = dict(payload)
supplied = value.pop("canonical_payload_digest", None)
return isinstance(supplied, str) and supplied == sha256_json(value)
@dataclass(frozen=True, slots=True)
class SignedOperationalArtifact:
payload: dict[str, Any]
signature: str
key_id: str
namespace: str
@property
def canonical_payload(self) -> bytes:
return canonical_json_bytes(self.payload)
@property
def payload_sha256(self) -> str:
return hashlib.sha256(self.canonical_payload).hexdigest()
def to_dict(self) -> dict[str, Any]:
return {
"payload": dict(self.payload),
"signature": self.signature,
"key_id": self.key_id,
"namespace": self.namespace,
}
class OperationalOwnerAuthority:
def __init__(self, key_path: Path, policy: OperationalPolicy, *, require_root_owner: bool = True, clock: Any = time.time):
policy.validate()
self.policy = policy
self.signer = LocalFileSigner(
Path(key_path),
signer_id=DEFAULT_OWNER_SIGNER_ID,
namespace=OWNER_NAMESPACE,
require_root_owner=require_root_owner,
)
self.clock = clock
@property
def fingerprint(self) -> str:
return self.signer.fingerprint
def _sign(self, payload: Mapping[str, Any]) -> SignedOperationalArtifact:
full = _signed_payload_dict(payload, key_id=self.fingerprint, namespace=OWNER_NAMESPACE)
art = self.signer._sign_canonical(full)
return SignedOperationalArtifact(dict(art.payload), art.signature, art.key_id, art.namespace)
def issue_operational_delegation(self, *, delegation_id: str, nonce: str, ttl_seconds: int) -> SignedOperationalArtifact:
now = int(self.clock())
p = self.policy
if not SAFE_ID.fullmatch(delegation_id) or not SAFE_ID.fullmatch(nonce):
fail("OPERATIONAL_DELEGATION_ID_INVALID")
if type(ttl_seconds) is not int or not 60 <= ttl_seconds <= p.maximum_delegation_ttl_seconds:
fail("OPERATIONAL_DELEGATION_TTL_INVALID")
payload = {
"schema": OPERATIONAL_DELEGATION_SCHEMA,
"delegation_id": delegation_id,
"nonce": nonce,
"owner_identity": OWNER_IDENTITY,
"repository": p.repository,
"task_id": p.task_id,
"task_identity_sha256": p.task_identity_sha256,
"target_branch": p.target_branch,
"base_oid": p.base_oid,
"generation": p.generation,
"execution_plan_sha256": p.execution_plan_sha256,
"external_job_identity": p.external_job_identity,
"allowed_capabilities": list(p.allowed_capabilities),
"allowed_worker_ids": list(p.allowed_worker_ids),
"policy_digest": p.digest,
"issued_at": now,
"not_before": max(0, now - 5),
"expires_at": now + ttl_seconds,
"revocation_epoch": 0,
"status": "ACTIVE",
}
return self._sign(payload)
def issue_recovery_decision(
self,
delegation: Mapping[str, Any],
*,
decision_id: str,
nonce: str,
expected_revision: int,
expected_attempt: int,
original_deadline: int,
new_deadline: int,
ttl_seconds: int,
) -> SignedOperationalArtifact:
now = int(self.clock())
verify_delegation_scope(delegation, self.policy, now=now)
if "task.recovery" not in delegation["allowed_capabilities"]:
fail("RECOVERY_CAPABILITY_NOT_DELEGATED")
if not SAFE_ID.fullmatch(decision_id) or not SAFE_ID.fullmatch(nonce):
fail("RECOVERY_DECISION_ID_INVALID")
if type(expected_revision) is not int or expected_revision < 0 or type(expected_attempt) is not int or expected_attempt < 0:
fail("RECOVERY_CAS_INVALID")
if type(original_deadline) is not int or type(new_deadline) is not int:
fail("RECOVERY_DEADLINE_INVALID")
if new_deadline <= now or new_deadline <= original_deadline:
fail("RECOVERY_DEADLINE_INVALID")
if new_deadline - now > self.policy.maximum_task_deadline_extension_seconds:
fail("RECOVERY_DEADLINE_WIDENING")
if type(ttl_seconds) is not int or not 15 <= ttl_seconds <= self.policy.maximum_recovery_decision_ttl_seconds:
fail("RECOVERY_DECISION_TTL_INVALID")
payload = {
"schema": RECOVERY_DECISION_SCHEMA,
"decision_id": decision_id,
"nonce": nonce,
"parent_delegation_id": delegation["delegation_id"],
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
"task_id": self.policy.task_id,
"task_identity_sha256": self.policy.task_identity_sha256,
"expected_state": ALLOWED_EXPECTED_STATE,
"expected_revision": expected_revision,
"expected_attempt": expected_attempt,
"original_deadline": original_deadline,
"new_deadline": new_deadline,
"repository": self.policy.repository,
"target_branch": self.policy.target_branch,
"base_oid": self.policy.base_oid,
"generation": self.policy.generation,
"execution_plan_sha256": self.policy.execution_plan_sha256,
"external_job_identity": self.policy.external_job_identity,
"policy_digest": self.policy.digest,
"one_time_use": True,
"issued_at": now,
"not_before": max(0, now - 5),
"expires_at": now + ttl_seconds,
}
return self._sign(payload)
class OperationalMachineAuthority:
def __init__(self, key_path: Path, policy: OperationalPolicy, *, require_root_owner: bool = True, clock: Any = time.time):
policy.validate()
self.policy = policy
self.signer = LocalFileSigner(
Path(key_path),
signer_id=DEFAULT_MACHINE_SIGNER_ID,
namespace=MACHINE_NAMESPACE,
require_root_owner=require_root_owner,
)
self.clock = clock
@property
def fingerprint(self) -> str:
return self.signer.fingerprint
def _sign(self, payload: Mapping[str, Any]) -> SignedOperationalArtifact:
full = _signed_payload_dict(payload, key_id=self.fingerprint, namespace=MACHINE_NAMESPACE)
art = self.signer._sign_canonical(full)
return SignedOperationalArtifact(dict(art.payload), art.signature, art.key_id, art.namespace)
def issue_runtime_binding(
self,
delegation: Mapping[str, Any],
*,
binding_id: str,
nonce: str,
worker_id: str,
runtime_id: str,
runtime_sha256: str,
task_contract_version: str,
supported_contract_versions: Sequence[str],
required_capabilities: Sequence[str],
worker_capabilities: Sequence[str],
max_concurrency: int,
ttl_seconds: int,
) -> SignedOperationalArtifact:
now = int(self.clock())
p = self.policy
verify_delegation_scope(delegation, p, now=now)
if "worker.runtime.bind" not in delegation["allowed_capabilities"]:
fail("RUNTIME_BIND_CAPABILITY_NOT_DELEGATED")
if worker_id not in p.allowed_worker_ids:
fail("WORKER_ID_NOT_DELEGATED")
for val, code in [(binding_id, "BINDING_ID_INVALID"), (nonce, "BINDING_NONCE_INVALID"), (worker_id, "WORKER_ID_INVALID"), (runtime_id, "RUNTIME_ID_INVALID"), (task_contract_version, "CONTRACT_VERSION_INVALID")]:
_safe_id(val, code)
_hex(runtime_sha256, 64, "RUNTIME_SHA256_INVALID")
supported = tuple(sorted(set(supported_contract_versions)))
if task_contract_version not in supported or not supported or any(not SAFE_ID.fullmatch(x) for x in supported):
fail("CONTRACT_INCOMPATIBLE")
required = _caps(required_capabilities, "REQUIRED_CAPABILITIES_INVALID")
worker_caps = _caps(worker_capabilities, "WORKER_CAPABILITIES_INVALID")
if not set(required).issubset(set(worker_caps)):
fail("CAPABILITY_MISMATCH")
if type(max_concurrency) is not int or not 1 <= max_concurrency <= 64:
fail("MAX_CONCURRENCY_INVALID")
if type(ttl_seconds) is not int or not 30 <= ttl_seconds <= p.maximum_binding_ttl_seconds:
fail("BINDING_TTL_INVALID")
if now + ttl_seconds > int(delegation["expires_at"]):
fail("BINDING_EXCEEDS_PARENT")
payload = {
"schema": WORKER_BINDING_SCHEMA,
"binding_id": binding_id,
"nonce": nonce,
"parent_delegation_id": delegation["delegation_id"],
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
"repository": p.repository,
"task_id": p.task_id,
"task_identity_sha256": p.task_identity_sha256,
"target_branch": p.target_branch,
"base_oid": p.base_oid,
"generation": p.generation,
"execution_plan_sha256": p.execution_plan_sha256,
"external_job_identity": p.external_job_identity,
"worker_id": worker_id,
"runtime_id": runtime_id,
"runtime_sha256": runtime_sha256,
"task_contract_version": task_contract_version,
"supported_contract_versions": list(supported),
"required_capabilities": list(required),
"worker_capabilities": list(worker_caps),
"max_concurrency": max_concurrency,
"policy_digest": p.digest,
"issued_at": now,
"not_before": max(0, now - 5),
"expires_at": now + ttl_seconds,
}
return self._sign(payload)
def issue_runtime_proof(
self,
delegation: Mapping[str, Any],
binding: Mapping[str, Any],
*,
proof_id: str,
nonce: str,
heartbeat_at: int,
adapter_readiness: bool,
resource_readiness: bool,
active_claim_count: int,
ttl_seconds: int,
) -> SignedOperationalArtifact:
now = int(self.clock())
p = self.policy
verify_delegation_scope(delegation, p, now=now)
verify_binding_scope(binding, delegation, p, now=now)
if "worker.runtime.proof" not in delegation["allowed_capabilities"]:
fail("RUNTIME_PROOF_CAPABILITY_NOT_DELEGATED")
_safe_id(proof_id, "PROOF_ID_INVALID")
_safe_id(nonce, "PROOF_NONCE_INVALID")
if type(heartbeat_at) is not int or heartbeat_at > now + 5 or now - heartbeat_at > p.maximum_heartbeat_age_seconds:
fail("HEARTBEAT_STALE")
if adapter_readiness is not True or resource_readiness is not True:
fail("RUNTIME_NOT_READY")
if type(active_claim_count) is not int or active_claim_count < 0 or active_claim_count >= int(binding["max_concurrency"]):
fail("MAX_CONCURRENCY_REACHED")
if type(ttl_seconds) is not int or not 10 <= ttl_seconds <= p.maximum_proof_ttl_seconds:
fail("PROOF_TTL_INVALID")
if now + ttl_seconds > min(int(binding["expires_at"]), int(delegation["expires_at"])):
fail("PROOF_EXCEEDS_PARENT")
payload = {
"schema": WORKER_PROOF_SCHEMA,
"proof_id": proof_id,
"nonce": nonce,
"parent_delegation_id": delegation["delegation_id"],
"parent_delegation_digest": hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest(),
"binding_id": binding["binding_id"],
"binding_digest": hashlib.sha256(canonical_json_bytes(dict(binding))).hexdigest(),
"repository": p.repository,
"task_id": p.task_id,
"task_identity_sha256": p.task_identity_sha256,
"generation": p.generation,
"execution_plan_sha256": p.execution_plan_sha256,
"worker_id": binding["worker_id"],
"runtime_id": binding["runtime_id"],
"runtime_sha256": binding["runtime_sha256"],
"heartbeat_at": heartbeat_at,
"adapter_readiness": True,
"resource_readiness": True,
"active_claim_count": active_claim_count,
"max_concurrency": binding["max_concurrency"],
"policy_digest": p.digest,
"issued_at": now,
"expires_at": now + ttl_seconds,
}
return self._sign(payload)
DELEGATION_FIELDS = {
"schema","delegation_id","nonce","owner_identity","repository","task_id",
"task_identity_sha256","target_branch","base_oid","generation","execution_plan_sha256",
"external_job_identity","allowed_capabilities","allowed_worker_ids","policy_digest",
"issued_at","not_before","expires_at","revocation_epoch","status",
"signature_namespace","signature_key_id","canonical_payload_digest",
}
BINDING_FIELDS = {
"schema","binding_id","nonce","parent_delegation_id","parent_delegation_digest",
"repository","task_id","task_identity_sha256","target_branch","base_oid","generation",
"execution_plan_sha256","external_job_identity","worker_id","runtime_id","runtime_sha256",
"task_contract_version","supported_contract_versions","required_capabilities","worker_capabilities",
"max_concurrency","policy_digest","issued_at","not_before","expires_at",
"signature_namespace","signature_key_id","canonical_payload_digest",
}
PROOF_FIELDS = {
"schema","proof_id","nonce","parent_delegation_id","parent_delegation_digest",
"binding_id","binding_digest","repository","task_id","task_identity_sha256","generation",
"execution_plan_sha256","worker_id","runtime_id","runtime_sha256","heartbeat_at",
"adapter_readiness","resource_readiness","active_claim_count","max_concurrency",
"policy_digest","issued_at","expires_at","signature_namespace","signature_key_id",
"canonical_payload_digest",
}
RECOVERY_FIELDS = {
"schema","decision_id","nonce","parent_delegation_id","parent_delegation_digest",
"task_id","task_identity_sha256","expected_state","expected_revision","expected_attempt",
"original_deadline","new_deadline","repository","target_branch","base_oid","generation",
"execution_plan_sha256","external_job_identity","policy_digest","one_time_use",
"issued_at","not_before","expires_at","signature_namespace","signature_key_id",
"canonical_payload_digest",
}
def verify_delegation_scope(value: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
policy.validate()
_exact_fields(value, DELEGATION_FIELDS, "OPERATIONAL_DELEGATION_FIELDS_INVALID")
now = int(time.time()) if now is None else int(now)
if (
value["schema"] != OPERATIONAL_DELEGATION_SCHEMA
or value["status"] != "ACTIVE"
or value["revocation_epoch"] != 0
or value["owner_identity"] != OWNER_IDENTITY
or value["repository"] != policy.repository
or value["task_id"] != policy.task_id
or value["task_identity_sha256"] != policy.task_identity_sha256
or value["target_branch"] != policy.target_branch
or value["base_oid"] != policy.base_oid
or value["generation"] != policy.generation
or value["execution_plan_sha256"] != policy.execution_plan_sha256
or value["external_job_identity"] != policy.external_job_identity
or value["policy_digest"] != policy.digest
or not set(value["allowed_capabilities"]).issubset(set(policy.allowed_capabilities))
or not set(value["allowed_worker_ids"]).issubset(set(policy.allowed_worker_ids))
or value["signature_namespace"] != OWNER_NAMESPACE
or not _verify_digest_field(value)
or not int(value["not_before"]) <= now < int(value["expires_at"])
):
fail("OPERATIONAL_DELEGATION_SCOPE_INVALID")
def verify_binding_scope(binding: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
_exact_fields(binding, BINDING_FIELDS, "WORKER_BINDING_FIELDS_INVALID")
now = int(time.time()) if now is None else int(now)
expected_parent = hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
if (
binding["schema"] != WORKER_BINDING_SCHEMA
or binding["parent_delegation_id"] != delegation["delegation_id"]
or binding["parent_delegation_digest"] != expected_parent
or binding["repository"] != policy.repository
or binding["task_id"] != policy.task_id
or binding["task_identity_sha256"] != policy.task_identity_sha256
or binding["target_branch"] != policy.target_branch
or binding["base_oid"] != policy.base_oid
or binding["generation"] != policy.generation
or binding["execution_plan_sha256"] != policy.execution_plan_sha256
or binding["external_job_identity"] != policy.external_job_identity
or binding["worker_id"] not in policy.allowed_worker_ids
or binding["policy_digest"] != policy.digest
or binding["signature_namespace"] != MACHINE_NAMESPACE
or not _verify_digest_field(binding)
or not int(binding["not_before"]) <= now < int(binding["expires_at"])
):
fail("WORKER_BINDING_SCOPE_INVALID")
required = set(binding["required_capabilities"])
worker = set(binding["worker_capabilities"])
if not required.issubset(worker) or binding["task_contract_version"] not in binding["supported_contract_versions"]:
fail("WORKER_BINDING_COMPATIBILITY_INVALID")
def verify_proof_scope(proof: Mapping[str, Any], binding: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
_exact_fields(proof, PROOF_FIELDS, "WORKER_PROOF_FIELDS_INVALID")
now = int(time.time()) if now is None else int(now)
if (
proof["schema"] != WORKER_PROOF_SCHEMA
or proof["parent_delegation_id"] != delegation["delegation_id"]
or proof["parent_delegation_digest"] != hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
or proof["binding_id"] != binding["binding_id"]
or proof["binding_digest"] != hashlib.sha256(canonical_json_bytes(dict(binding))).hexdigest()
or proof["repository"] != policy.repository
or proof["task_id"] != policy.task_id
or proof["task_identity_sha256"] != policy.task_identity_sha256
or proof["generation"] != policy.generation
or proof["execution_plan_sha256"] != policy.execution_plan_sha256
or proof["worker_id"] != binding["worker_id"]
or proof["runtime_id"] != binding["runtime_id"]
or proof["runtime_sha256"] != binding["runtime_sha256"]
or proof["max_concurrency"] != binding["max_concurrency"]
or proof["policy_digest"] != policy.digest
or proof["signature_namespace"] != MACHINE_NAMESPACE
or proof["adapter_readiness"] is not True
or proof["resource_readiness"] is not True
or not _verify_digest_field(proof)
or not now < int(proof["expires_at"])
or now - int(proof["heartbeat_at"]) > policy.maximum_heartbeat_age_seconds
or int(proof["active_claim_count"]) >= int(proof["max_concurrency"])
):
fail("WORKER_PROOF_SCOPE_INVALID")
def verify_recovery_scope(decision: Mapping[str, Any], delegation: Mapping[str, Any], policy: OperationalPolicy, *, now: int | None = None) -> None:
_exact_fields(decision, RECOVERY_FIELDS, "RECOVERY_DECISION_FIELDS_INVALID")
now = int(time.time()) if now is None else int(now)
if (
decision["schema"] != RECOVERY_DECISION_SCHEMA
or decision["parent_delegation_id"] != delegation["delegation_id"]
or decision["parent_delegation_digest"] != hashlib.sha256(canonical_json_bytes(dict(delegation))).hexdigest()
or decision["task_id"] != policy.task_id
or decision["task_identity_sha256"] != policy.task_identity_sha256
or decision["expected_state"] != "EXPIRED"
or decision["repository"] != policy.repository
or decision["target_branch"] != policy.target_branch
or decision["base_oid"] != policy.base_oid
or decision["generation"] != policy.generation
or decision["execution_plan_sha256"] != policy.execution_plan_sha256
or decision["external_job_identity"] != policy.external_job_identity
or decision["policy_digest"] != policy.digest
or decision["one_time_use"] is not True
or decision["signature_namespace"] != OWNER_NAMESPACE
or not _verify_digest_field(decision)
or not int(decision["not_before"]) <= now < int(decision["expires_at"])
or int(decision["new_deadline"]) <= now
):
fail("RECOVERY_DECISION_SCOPE_INVALID")
class OperationalVerifier:
"""Verify signatures through the public root-owned SignerRegistry."""
def __init__(self, registry_path: Path, *, require_root_owner: bool = True):
self.registry_path = Path(registry_path)
self.require_root_owner = require_root_owner
def _verify(self, artifact: SignedOperationalArtifact | Mapping[str, Any], *, role: str, signer_id: str, namespace: str) -> dict[str, Any]:
if isinstance(artifact, SignedOperationalArtifact):
payload, signature, key_id, artifact_namespace = artifact.payload, artifact.signature, artifact.key_id, artifact.namespace
else:
_exact_fields(artifact, {"payload","signature","key_id","namespace"}, "SIGNED_ARTIFACT_FIELDS_INVALID")
payload, signature, key_id, artifact_namespace = artifact["payload"], artifact["signature"], artifact["key_id"], artifact["namespace"]
if artifact_namespace != namespace:
fail("SIGNED_ARTIFACT_NAMESPACE_INVALID")
registry = SignerRegistry(self.registry_path, require_root_owner=self.require_root_owner)
rec = registry.require(signer_id, role=role)
if rec.fingerprint != key_id:
fail("SIGNED_ARTIFACT_KEY_ID_INVALID")
if role == "MACHINE_OPERATIONAL":
owner_rec = registry.require(DEFAULT_OWNER_SIGNER_ID, role="OWNER_AUTHORITY")
if rec.parent_signer_id != owner_rec.signer_id or rec.parent_fingerprint != owner_rec.fingerprint:
fail("MACHINE_PARENT_BINDING_INVALID")
# RegistryOwnerApprovalVerifier handles Owner namespace; generic machine uses registry public key.
if role == "OWNER_AUTHORITY":
verifier = RegistryOwnerApprovalVerifier(self.registry_path, require_root_owner=self.require_root_owner, signer_id=signer_id)
ok = verifier.verify(namespace=namespace, payload=canonical_json_bytes(payload), signature=signature, key_id=key_id)
else:
from aios_single_host_authority import _load_public_key_line
pub = _load_public_key_line(rec.public_key)
ok = verify_sshsig(pub, canonical_json_bytes(payload), signature, namespace=namespace, expected_key_id=key_id)
if not ok:
fail("SIGNED_ARTIFACT_SIGNATURE_INVALID")
return dict(payload)
def owner(self, artifact: SignedOperationalArtifact | Mapping[str, Any]) -> dict[str, Any]:
return self._verify(artifact, role="OWNER_AUTHORITY", signer_id=DEFAULT_OWNER_SIGNER_ID, namespace=OWNER_NAMESPACE)
def machine(self, artifact: SignedOperationalArtifact | Mapping[str, Any]) -> dict[str, Any]:
return self._verify(artifact, role="MACHINE_OPERATIONAL", signer_id=DEFAULT_MACHINE_SIGNER_ID, namespace=MACHINE_NAMESPACE)
@@ -0,0 +1,410 @@
"""Unix-domain fixed-schema service for operational migration authority."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
from pathlib import Path
import socket
import sqlite3
import struct
import stat
import sys
import time
from typing import Any, Mapping
from aios_single_host_authority import (
DEFAULT_REGISTRY_PATH,
OWNER_NAMESPACE,
MACHINE_NAMESPACE,
SingleHostAuthorityError,
)
from aios_single_host_operational_authority import (
OperationalAuthorityError,
OperationalMachineAuthority,
OperationalOwnerAuthority,
OperationalPolicy,
OperationalVerifier,
SignedOperationalArtifact,
verify_binding_scope,
verify_delegation_scope,
)
DEFAULT_POLICY = Path("/etc/aios/single-host-operational-policy.json")
DEFAULT_OWNER_KEY = Path("/var/lib/aios-owner-authority/owner-authority.key")
DEFAULT_MACHINE_KEY = Path("/var/lib/aios-machine-signer/machine-operational.key")
DEFAULT_OWNER_SOCKET = Path("/run/aios/owner-authority.sock")
DEFAULT_MACHINE_SOCKET = Path("/run/aios/machine-signer.sock")
DEFAULT_LEDGER = Path("/var/lib/aios-operational-authority/issuance-ledger.sqlite3")
MAX_REQUEST = 128 * 1024
READINESS_SCHEMA = "aios.signer-readiness-challenge.v1"
class OperationalServiceError(RuntimeError):
def __init__(self, code: str):
super().__init__(code)
self.code = code
def fail(code: str) -> None:
raise OperationalServiceError(code)
def _read_policy(path: Path, *, require_root_owner: bool = True) -> OperationalPolicy:
path=Path(path)
if not path.is_absolute():
fail("OPERATIONAL_POLICY_PATH_INVALID")
flags=os.O_RDONLY
if hasattr(os,"O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
fd=os.open(path,flags)
except OSError as exc:
raise OperationalServiceError("OPERATIONAL_POLICY_UNAVAILABLE") from exc
try:
st=os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
fail("OPERATIONAL_POLICY_NOT_REGULAR")
mode=stat.S_IMODE(st.st_mode)
if mode & 0o022:
fail("OPERATIONAL_POLICY_WRITABLE_BY_GROUP_OR_OTHER")
if require_root_owner and st.st_uid != 0:
fail("OPERATIONAL_POLICY_NOT_ROOT_OWNED")
raw=bytearray()
while len(raw) <= MAX_REQUEST:
chunk=os.read(fd,min(65536,MAX_REQUEST+1-len(raw)))
if not chunk: break
raw.extend(chunk)
if not raw or len(raw)>MAX_REQUEST:
fail("OPERATIONAL_POLICY_UNAVAILABLE")
finally:
os.close(fd)
try:
obj=json.loads(bytes(raw).decode("utf-8"))
except Exception as exc:
raise OperationalServiceError("OPERATIONAL_POLICY_INVALID") from exc
return OperationalPolicy.from_mapping(obj)
def _artifact(value: Mapping[str, Any]) -> SignedOperationalArtifact:
if not isinstance(value, Mapping) or set(value) != {"payload","signature","key_id","namespace"}:
fail("SIGNED_ARTIFACT_INVALID")
if not isinstance(value["payload"], Mapping):
fail("SIGNED_ARTIFACT_INVALID")
return SignedOperationalArtifact(
payload=dict(value["payload"]),
signature=str(value["signature"]),
key_id=str(value["key_id"]),
namespace=str(value["namespace"]),
)
def _peer_uid(conn: socket.socket) -> int:
if not hasattr(socket, "SO_PEERCRED"):
fail("PEER_CREDENTIALS_UNAVAILABLE")
pid, uid, gid = struct.unpack("3i", conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")))
return uid
class DurableIssuanceLedger:
"""Root-owned exact-idempotent structured signing ledger."""
def __init__(self, path: Path):
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
if os.name == "posix": os.chmod(self.path.parent, 0o700)
if self.path.exists() and self.path.is_symlink():
fail("ISSUANCE_LEDGER_SYMLINK_FORBIDDEN")
conn = sqlite3.connect(self.path)
try:
conn.execute("PRAGMA journal_mode=DELETE")
conn.execute("""
CREATE TABLE IF NOT EXISTS issuance (
role TEXT NOT NULL,
operation TEXT NOT NULL,
nonce TEXT NOT NULL,
request_sha256 TEXT NOT NULL,
response_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY(role, operation, nonce)
)
""")
conn.commit()
finally:
conn.close()
os.chmod(self.path, 0o600)
def issue(self, *, role: str, operation: str, nonce: str, request: Mapping[str, Any], issuer: Any) -> dict[str, Any]:
request_sha = hashlib.sha256(
json.dumps(request, sort_keys=True, separators=(",",":"), ensure_ascii=False).encode("utf-8")
).hexdigest()
conn = sqlite3.connect(self.path, timeout=5, isolation_level=None)
try:
conn.execute("BEGIN IMMEDIATE")
row = conn.execute(
"SELECT request_sha256,response_json FROM issuance WHERE role=? AND operation=? AND nonce=?",
(role, operation, nonce),
).fetchone()
if row is not None:
if row[0] != request_sha:
conn.rollback()
fail("NONCE_REUSE_WITH_DIFFERENT_REQUEST")
result = json.loads(row[1])
conn.commit()
return result
result = issuer()
encoded = json.dumps(result, sort_keys=True, separators=(",",":"), ensure_ascii=False)
conn.execute(
"INSERT INTO issuance(role,operation,nonce,request_sha256,response_json,created_at) VALUES(?,?,?,?,?,?)",
(role, operation, nonce, request_sha, encoded, int(time.time())),
)
conn.commit()
return result
except sqlite3.IntegrityError:
conn.rollback()
fail("ISSUANCE_LEDGER_RACE")
finally:
conn.close()
class OperationalAuthorityService:
def __init__(
self,
*,
role: str,
policy_path: Path = DEFAULT_POLICY,
registry_path: Path = DEFAULT_REGISTRY_PATH,
owner_key_path: Path = DEFAULT_OWNER_KEY,
machine_key_path: Path = DEFAULT_MACHINE_KEY,
socket_path: Path | None = None,
ledger_path: Path = DEFAULT_LEDGER,
allowed_uids: tuple[int, ...] = (0,),
clock: Any = time.time,
require_root_owner: bool = True,
):
if role not in {"owner","machine"}:
fail("ROLE_INVALID")
self.role = role
self.policy_path = Path(policy_path)
self.registry_path = Path(registry_path)
self.owner_key_path = Path(owner_key_path)
self.machine_key_path = Path(machine_key_path)
self.socket_path = Path(socket_path or (DEFAULT_OWNER_SOCKET if role == "owner" else DEFAULT_MACHINE_SOCKET))
self.allowed_uids = tuple(allowed_uids)
self.ledger = DurableIssuanceLedger(ledger_path)
self.clock = clock
self.require_root_owner = require_root_owner
def _components(self):
policy = _read_policy(self.policy_path, require_root_owner=self.require_root_owner)
verifier = OperationalVerifier(self.registry_path, require_root_owner=self.require_root_owner)
if self.role == "owner":
signer = OperationalOwnerAuthority(self.owner_key_path, policy, require_root_owner=self.require_root_owner, clock=self.clock)
else:
signer = OperationalMachineAuthority(self.machine_key_path, policy, require_root_owner=self.require_root_owner, clock=self.clock)
return policy, verifier, signer
def handle(self, request: Mapping[str, Any], *, peer_uid: int) -> dict[str, Any]:
if peer_uid not in self.allowed_uids:
fail("PEER_UID_FORBIDDEN")
if not isinstance(request, Mapping) or set(request) != {"operation","payload"} or not isinstance(request["payload"], Mapping):
fail("REQUEST_INVALID")
op = request["operation"]
p = request["payload"]
policy, verifier, signer = self._components()
if op == "identity":
return {
"status":"PASS",
"role":self.role,
"policy_digest":policy.digest,
"key_id":signer.fingerprint,
"socket":str(self.socket_path),
"arbitrary_signing":False,
}
if op == "readiness_challenge":
if set(p) != {"challenge_id","challenge_sha256"}:
fail("REQUEST_FIELDS_INVALID")
challenge_id = str(p["challenge_id"])
challenge_sha256 = str(p["challenge_sha256"])
if not challenge_id or len(challenge_id) > 160:
fail("READINESS_CHALLENGE_ID_INVALID")
if not isinstance(challenge_sha256, str) or not re.fullmatch(r"[0-9a-f]{64}", challenge_sha256):
fail("READINESS_CHALLENGE_SHA256_INVALID")
namespace = OWNER_NAMESPACE if self.role == "owner" else MACHINE_NAMESPACE
def issue():
art = signer._sign({
"schema": READINESS_SCHEMA,
"role": self.role,
"challenge_id": challenge_id,
"challenge_sha256": challenge_sha256,
"policy_digest": policy.digest,
"issued_at": int(self.clock()),
})
return {"status":"PASS","artifact":art.to_dict()}
return self.ledger.issue(
role=self.role,
operation=op,
nonce=challenge_id,
request=p,
issuer=issue,
)
if self.role == "owner":
if op == "issue_operational_delegation":
if set(p) != {"delegation_id","nonce","ttl_seconds"}:
fail("REQUEST_FIELDS_INVALID")
def issue():
art = signer.issue_operational_delegation(
delegation_id=str(p["delegation_id"]),
nonce=str(p["nonce"]),
ttl_seconds=p["ttl_seconds"],
)
return {"status":"PASS","artifact":art.to_dict()}
return self.ledger.issue(role=self.role, operation=op, nonce=str(p["nonce"]), request=p, issuer=issue)
if op == "issue_expired_task_recovery":
if set(p) != {"delegation","decision_id","nonce","expected_revision","expected_attempt","original_deadline","new_deadline","ttl_seconds"}:
fail("REQUEST_FIELDS_INVALID")
parent_art = _artifact(p["delegation"])
parent = verifier.owner(parent_art)
verify_delegation_scope(parent, policy, now=int(self.clock()))
def issue():
art = signer.issue_recovery_decision(
parent,
decision_id=str(p["decision_id"]),
nonce=str(p["nonce"]),
expected_revision=p["expected_revision"],
expected_attempt=p["expected_attempt"],
original_deadline=p["original_deadline"],
new_deadline=p["new_deadline"],
ttl_seconds=p["ttl_seconds"],
)
return {"status":"PASS","artifact":art.to_dict()}
return self.ledger.issue(role=self.role, operation=op, nonce=str(p["nonce"]), request=p, issuer=issue)
fail("OWNER_OPERATION_FORBIDDEN")
if op == "issue_worker_runtime_binding":
if set(p) != {
"delegation","binding_id","nonce","worker_id","runtime_id","runtime_sha256",
"task_contract_version","supported_contract_versions","required_capabilities",
"worker_capabilities","max_concurrency","ttl_seconds"
}:
fail("REQUEST_FIELDS_INVALID")
parent_art = _artifact(p["delegation"])
parent = verifier.owner(parent_art)
verify_delegation_scope(parent, policy, now=int(self.clock()))
def issue():
art = signer.issue_runtime_binding(
parent,
binding_id=str(p["binding_id"]), nonce=str(p["nonce"]),
worker_id=str(p["worker_id"]), runtime_id=str(p["runtime_id"]),
runtime_sha256=str(p["runtime_sha256"]),
task_contract_version=str(p["task_contract_version"]),
supported_contract_versions=tuple(p["supported_contract_versions"]),
required_capabilities=tuple(p["required_capabilities"]),
worker_capabilities=tuple(p["worker_capabilities"]),
max_concurrency=p["max_concurrency"], ttl_seconds=p["ttl_seconds"],
)
return {"status":"PASS","artifact":art.to_dict()}
return self.ledger.issue(role=self.role, operation=op, nonce=str(p["nonce"]), request=p, issuer=issue)
if op == "issue_worker_runtime_proof":
if set(p) != {
"delegation","binding","proof_id","nonce","heartbeat_at","adapter_readiness",
"resource_readiness","active_claim_count","ttl_seconds"
}:
fail("REQUEST_FIELDS_INVALID")
parent_art = _artifact(p["delegation"])
binding_art = _artifact(p["binding"])
parent = verifier.owner(parent_art)
binding = verifier.machine(binding_art)
verify_delegation_scope(parent, policy, now=int(self.clock()))
verify_binding_scope(binding, parent, policy, now=int(self.clock()))
def issue():
art = signer.issue_runtime_proof(
parent, binding,
proof_id=str(p["proof_id"]), nonce=str(p["nonce"]),
heartbeat_at=p["heartbeat_at"],
adapter_readiness=p["adapter_readiness"],
resource_readiness=p["resource_readiness"],
active_claim_count=p["active_claim_count"],
ttl_seconds=p["ttl_seconds"],
)
return {"status":"PASS","artifact":art.to_dict()}
return self.ledger.issue(role=self.role, operation=op, nonce=str(p["nonce"]), request=p, issuer=issue)
fail("MACHINE_OPERATION_FORBIDDEN")
def serve_forever(self) -> None:
path = self.socket_path
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() or path.is_symlink():
if path.is_socket():
path.unlink()
else:
fail("SOCKET_PATH_OCCUPIED")
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.bind(str(path))
os.chmod(path, 0o600)
sock.listen(16)
while True:
conn, _ = sock.accept()
with conn:
try:
uid = _peer_uid(conn)
chunks = []
total = 0
while True:
data = conn.recv(65536)
if not data:
break
total += len(data)
if total > MAX_REQUEST:
fail("REQUEST_TOO_LARGE")
chunks.append(data)
if b"\n" in data:
break
raw = b"".join(chunks)
line = raw.split(b"\n",1)[0]
req = json.loads(line.decode("utf-8"))
result = self.handle(req, peer_uid=uid)
out = {"ok":True,"result":result}
except (OperationalServiceError, OperationalAuthorityError, SingleHostAuthorityError) as exc:
out = {"ok":False,"error":getattr(exc,"code",str(exc))}
except Exception:
out = {"ok":False,"error":"INTERNAL_FAIL_CLOSED"}
conn.sendall(json.dumps(out, sort_keys=True, separators=(",",":")).encode("utf-8")+b"\n")
finally:
sock.close()
try:
path.unlink()
except FileNotFoundError:
pass
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--role", choices=("owner","machine"), required=True)
ap.add_argument("--policy", default=str(DEFAULT_POLICY))
ap.add_argument("--registry", default=str(DEFAULT_REGISTRY_PATH))
ap.add_argument("--owner-key", default=str(DEFAULT_OWNER_KEY))
ap.add_argument("--machine-key", default=str(DEFAULT_MACHINE_KEY))
ap.add_argument("--socket")
ns = ap.parse_args()
service = OperationalAuthorityService(
role=ns.role,
policy_path=Path(ns.policy),
registry_path=Path(ns.registry),
owner_key_path=Path(ns.owner_key),
machine_key_path=Path(ns.machine_key),
socket_path=Path(ns.socket) if ns.socket else None,
)
service.serve_forever()
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
"""Bridge contract surface for Task Admission Bootstrap.
This module is intentionally transport-agnostic. The live Bridge may merge
these routes/operations into its existing authenticated/HTTPS surface, but the
authorization proof for the authorize operation is the verified delegation
epoch or Owner signature carried by the request itself.
No handler here can issue TaskPrincipal assertions or WriterLeases. Those remain
strictly downstream of a real admitted/claimed Agent Task.
"""
from __future__ import annotations
from typing import Any, Mapping
from aios_task_admission_bootstrap import TaskAdmissionBootstrapService
TASK_ADMISSION_ROUTES = {
"/v2/task-admission/envelope": "getTaskAdmissionEnvelope",
"/v2/task-admission/authorize": "submitTaskAdmissionAuthorization",
"/v2/task-admission/status": "getTaskAdmissionStatus",
}
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS = {
"getTaskAdmissionEnvelope": (
"idempotency_key",
"task_id",
"repository",
"authority_base_oid",
"target_branch",
"generation",
"execution_digest",
"requested_capabilities",
"file_allowlist",
"risk",
),
"submitTaskAdmissionAuthorization": (
"envelope_id",
"authorization_kind",
),
"getTaskAdmissionStatus": ("envelope_id",),
}
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS = {
"getTaskAdmissionEnvelope": ("expires_in_seconds",),
"submitTaskAdmissionAuthorization": (
"delegation_epoch_id",
"owner_key_id",
"signature_base64",
),
"getTaskAdmissionStatus": (),
}
class TaskAdmissionBridgeContract:
def __init__(self, service: TaskAdmissionBootstrapService) -> None:
self.service = service
@staticmethod
def _validate_fields(operation: str, request: Mapping[str, Any]) -> None:
required = set(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation])
allowed = required | set(TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation])
if not required.issubset(request):
raise ValueError("TASK_ADMISSION_BRIDGE_REQUIRED_FIELDS_MISSING")
if set(request) - allowed:
raise ValueError("TASK_ADMISSION_BRIDGE_UNKNOWN_FIELDS")
def dispatch(self, operation: str, request: Mapping[str, Any]) -> dict[str, Any]:
if operation not in TASK_ADMISSION_OPERATION_REQUIRED_FIELDS:
raise ValueError("TASK_ADMISSION_BRIDGE_OPERATION_NOT_FOUND")
self._validate_fields(operation, request)
if operation == "getTaskAdmissionEnvelope":
return self.service.create_envelope(request)
if operation == "submitTaskAdmissionAuthorization":
return self.service.authorize_and_admit(request)
if operation == "getTaskAdmissionStatus":
return self.service.status(str(request["envelope_id"]))
raise ValueError("TASK_ADMISSION_BRIDGE_OPERATION_NOT_FOUND")
def handlers(self) -> dict[str, Any]:
return {
operation: (
lambda request, operation=operation: self.dispatch(operation, request)
)
for operation in TASK_ADMISSION_OPERATION_REQUIRED_FIELDS
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,394 @@
"""Append-only ClaimGuard binding for the superseded Single-Host child Task."""
from __future__ import annotations
from datetime import datetime, timezone
import hashlib
import json
import re
import sqlite3
from typing import Any, Callable, Mapping
from aios_task_lineage_supersession_initial_projection import (
PROJECTION_RECEIPT_SCHEMA,
PROJECTION_TABLE,
SOURCE_BUSINESS_EXECUTION_DIGEST,
)
SOURCE_TASK_ID = "TASK-SINGLEHOST-TRUSTROOT-P0-20260904"
CHILD_TASK_ID = SOURCE_TASK_ID + "-S1"
WORKER_ID = "p0-task-principal-v15-20260830"
REPOSITORY = "aios/aios"
TARGET_BRANCH = "agent/single-host-trust-root-p0-20260904"
GENERATION = 1
EXTERNAL_JOB_ID = "job-4e10f92a6a5742518a9f1d8745c845ab"
BINDING_SCHEMA = "aios.task-claim-guard-binding.v2"
RECEIPT_SCHEMA = "aios.task-claim-guard-binding-receipt.v2"
HEX64 = re.compile(r"^[0-9a-f]{64}$")
class ClaimGuardBindingError(RuntimeError):
def __init__(self, code: str, http_status: int = 409):
super().__init__(code)
self.code = code
self.http_status = int(http_status)
def _fail(code: str, status: int = 409) -> None:
raise ClaimGuardBindingError(code, status)
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
def _sha_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _sha_json(value: Any) -> str:
return _sha_text(canonical_json(value))
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _row_dict(row: Any) -> dict[str, Any]:
if row is None:
return {}
if hasattr(row, "keys"):
return {str(key): row[key] for key in row.keys()}
_fail("TASK_ROW_MAPPING_UNAVAILABLE", 500)
def _json(raw: Any, code: str) -> Any:
if not isinstance(raw, str):
_fail(code)
try:
return json.loads(raw)
except (TypeError, ValueError) as exc:
raise ClaimGuardBindingError(code) from exc
def _default_policy_factory(value: Mapping[str, Any]):
try:
from aios_single_host_operational_authority import OperationalPolicy
except ImportError as exc:
raise ClaimGuardBindingError("OPERATIONAL_POLICY_RUNTIME_UNAVAILABLE", 503) from exc
return OperationalPolicy.from_mapping(value)
def _policy_value(policy: Any, name: str) -> Any:
try:
return getattr(policy, name)
except AttributeError as exc:
raise ClaimGuardBindingError("OPERATIONAL_POLICY_FIELDS_INVALID") from exc
def _load_projection_evidence(connection, relation) -> dict[str, Any]:
try:
row = connection.execute(
f"SELECT * FROM {PROJECTION_TABLE} WHERE task_id=? AND source_task_id=? "
"AND supersession_ordinal=?",
(CHILD_TASK_ID, SOURCE_TASK_ID, 1),
).fetchone()
except sqlite3.OperationalError as exc:
raise ClaimGuardBindingError(
"FORMAL_CHILD_INITIAL_PROJECTION_SCHEMA_UNAVAILABLE", 503
) from exc
if row is None:
_fail("FORMAL_CHILD_INITIAL_PROJECTION_REQUIRED")
value = _row_dict(row)
receipt = _json(value.get("receipt_json"), "CHILD_PROJECTION_RECEIPT_INVALID")
canonical_digest = value.get("canonical_child_execution_digest")
source_digest = value.get("source_business_execution_digest")
exact = {
"task_id": CHILD_TASK_ID,
"source_task_id": SOURCE_TASK_ID,
"supersession_ordinal": 1,
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"external_job_id": EXTERNAL_JOB_ID,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"owner_decision_sha256": relation["owner_decision_sha256"],
}
if any(value.get(key) != expected for key, expected in exact.items()):
_fail("CHILD_PROJECTION_LINEAGE_MISMATCH")
if (
not isinstance(canonical_digest, str)
or HEX64.fullmatch(canonical_digest) is None
or canonical_digest == SOURCE_BUSINESS_EXECUTION_DIGEST
or source_digest != SOURCE_BUSINESS_EXECUTION_DIGEST
or _sha_text(value["receipt_json"]) != value.get("receipt_sha256")
or not isinstance(receipt, Mapping)
or receipt.get("schema") != PROJECTION_RECEIPT_SCHEMA
or receipt.get("canonical_child_execution_digest") != canonical_digest
or receipt.get("source_business_execution_digest") != source_digest
or receipt.get("owner_supersession_decision_sha256")
!= relation["owner_decision_sha256"]
):
_fail("CHILD_PROJECTION_RECEIPT_MISMATCH")
return {
**value,
"canonical_child_execution_digest": canonical_digest,
"source_business_execution_digest": source_digest,
}
def install_child_binding(
connection,
*,
policy: Mapping[str, Any],
runtime_id: str,
runtime_observation_digest: str,
task_contract_version: str,
policy_factory: Callable[[Mapping[str, Any]], Any] = _default_policy_factory,
now: str | None = None,
) -> dict[str, Any]:
"""Generate and persist the only permitted child binding from current DB state."""
if not isinstance(policy, Mapping):
_fail("OPERATIONAL_POLICY_INVALID")
if not isinstance(runtime_id, str) or not runtime_id or len(runtime_id) > 512:
_fail("RUNTIME_ID_INVALID")
if not isinstance(runtime_observation_digest, str) or HEX64.fullmatch(runtime_observation_digest) is None:
_fail("RUNTIME_OBSERVATION_DIGEST_INVALID")
if not isinstance(task_contract_version, str) or not task_contract_version or len(task_contract_version) > 128:
_fail("TASK_CONTRACT_VERSION_INVALID")
relation = connection.execute(
"SELECT * FROM task_lineage_supersessions WHERE source_task_id=? AND supersession_ordinal=1 AND child_task_id=?",
(SOURCE_TASK_ID, CHILD_TASK_ID),
).fetchone()
if relation is None:
_fail("FORMAL_SUPERSESSION_REQUIRED")
projection = _load_projection_evidence(connection, relation)
canonical_execution_digest = projection["canonical_child_execution_digest"]
task_row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (CHILD_TASK_ID,)).fetchone()
if task_row is None:
_fail("CHILD_TASK_NOT_FOUND", 404)
task = _row_dict(task_row)
if (
task.get("status") != "QUEUED" or int(task.get("revision", -1)) < 2
or int(task.get("attempt", -1)) != 0 or task.get("claimed_by") is not None
or task.get("lease_token_hash") is not None or task.get("lease_expires_at") is not None
or task.get("result_json") is not None or task.get("verification_json") is not None
):
_fail("CHILD_PROJECTED_CLAIMABLE_STATE_REQUIRED")
event_rows = connection.execute(
"SELECT event_type,payload_json FROM events WHERE task_id=? ORDER BY event_id",
(CHILD_TASK_ID,),
).fetchall()
if [row[0] for row in event_rows] != [
"TASK_CREATED",
"TASK_INITIAL_EXECUTION_PROJECTION_AUTHORIZED",
]:
_fail("CANONICAL_INITIAL_PROJECTION_REQUIRED")
projection_event = _json(
event_rows[-1][1], "CANONICAL_INITIAL_PROJECTION_EVENT_INVALID"
)
if (
projection_event.get("source_business_execution_digest")
!= SOURCE_BUSINESS_EXECUTION_DIGEST
or projection_event.get("canonical_child_execution_digest")
!= canonical_execution_digest
or projection_event.get("owner_supersession_decision_sha256")
!= relation["owner_decision_sha256"]
or _sha_json(projection_event) != projection["projection_event_sha256"]
):
_fail("CANONICAL_INITIAL_PROJECTION_EVENT_MISMATCH")
metadata_raw = task.get("metadata_json")
capabilities_raw = task.get("required_capabilities_json")
metadata = _json(metadata_raw, "CHILD_METADATA_INVALID")
capabilities = _json(capabilities_raw, "CHILD_CAPABILITIES_INVALID")
if (
_sha_text(metadata_raw) != projection["metadata_sha256"]
or _sha_text(capabilities_raw)
!= projection["required_capabilities_sha256"]
):
_fail("CHILD_PROJECTION_TASK_DIGEST_MISMATCH")
if not isinstance(metadata, Mapping) or not isinstance(capabilities, list) or not capabilities:
_fail("CHILD_PROJECTION_INVALID")
if any(not isinstance(value, str) or not value for value in capabilities) or len(set(capabilities)) != len(capabilities):
_fail("CHILD_CAPABILITIES_INVALID")
execution = metadata.get("execution")
if not isinstance(execution, Mapping):
_fail("CHILD_EXECUTION_PROJECTION_MISSING")
exact_execution = {
"task_id": CHILD_TASK_ID, "repository": REPOSITORY,
"target_branch": TARGET_BRANCH, "generation": GENERATION,
"execution_digest": canonical_execution_digest, "job_id": EXTERNAL_JOB_ID,
}
if any(execution.get(key) != value for key, value in exact_execution.items()):
_fail("CHILD_EXECUTION_BINDING_MISMATCH")
parsed_policy = policy_factory(dict(policy))
if hasattr(parsed_policy, "validate"):
parsed_policy.validate()
policy_exact = {
"task_id": CHILD_TASK_ID, "repository": REPOSITORY,
"target_branch": TARGET_BRANCH, "generation": GENERATION,
"execution_plan_sha256": canonical_execution_digest,
"external_job_identity": EXTERNAL_JOB_ID,
}
if any(_policy_value(parsed_policy, key) != value for key, value in policy_exact.items()):
_fail("OPERATIONAL_POLICY_SCOPE_MISMATCH")
if tuple(_policy_value(parsed_policy, "allowed_worker_ids")) != (WORKER_ID,):
_fail("OPERATIONAL_POLICY_WORKER_MISMATCH")
expected_oid = execution.get("expected_branch_oid", execution.get("base_oid"))
if expected_oid is not None and _policy_value(parsed_policy, "base_oid") != expected_oid:
_fail("OPERATIONAL_POLICY_BASE_OID_MISMATCH")
policy_json = canonical_json(dict(policy))
policy_sha = _sha_text(policy_json)
policy_digest = str(_policy_value(parsed_policy, "digest"))
if policy_digest != policy_sha:
_fail("OPERATIONAL_POLICY_DIGEST_MISMATCH")
binding = {
"schema": BINDING_SCHEMA, "task_id": CHILD_TASK_ID,
"source_task_id": SOURCE_TASK_ID, "supersession_ordinal": 1,
"expected_revision": int(task["revision"]),
"metadata_sha256": _sha_text(metadata_raw),
"required_capabilities_sha256": _sha_json(sorted(capabilities)),
"worker_id": WORKER_ID,
"execution_digest": canonical_execution_digest,
"canonical_child_execution_digest": canonical_execution_digest,
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"repository": REPOSITORY, "target_branch": TARGET_BRANCH, "generation": GENERATION,
"external_job_id": EXTERNAL_JOB_ID, "policy_sha256": policy_sha,
"policy_digest": policy_digest, "runtime_id": runtime_id,
"runtime_observation_digest": runtime_observation_digest,
"task_contract_version": task_contract_version,
"owner_supersession_decision_sha256": relation["owner_decision_sha256"],
}
binding_json = canonical_json(binding)
binding_sha = _sha_text(binding_json)
binding_id = "sha256:" + binding_sha
existing = connection.execute(
"SELECT binding_sha256 FROM task_claim_guard_bindings_v2 WHERE task_id=?", (CHILD_TASK_ID,)
).fetchone()
if existing is not None:
if existing[0] != binding_sha:
_fail("CLAIM_GUARD_BINDING_ALREADY_EXISTS")
receipt = connection.execute(
"SELECT receipt_json FROM task_claim_guard_binding_receipts_v2 WHERE binding_id=?", (binding_id,)
).fetchone()
if receipt is None:
_fail("CLAIM_GUARD_BINDING_RECEIPT_MISSING", 500)
return {**json.loads(receipt[0]), "idempotent": True}
created_at = now or _now()
connection.execute(
"""INSERT INTO task_claim_guard_bindings_v2(
binding_id,task_id,source_task_id,supersession_ordinal,expected_revision,
metadata_sha256,required_capabilities_sha256,worker_id,execution_digest,
repository,target_branch,generation,policy_json,policy_sha256,binding_json,
binding_sha256,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(binding_id, CHILD_TASK_ID, SOURCE_TASK_ID, 1, binding["expected_revision"],
binding["metadata_sha256"], binding["required_capabilities_sha256"], WORKER_ID,
canonical_execution_digest, REPOSITORY, TARGET_BRANCH, GENERATION, policy_json, policy_sha,
binding_json, binding_sha, created_at),
)
receipt = {
"schema": RECEIPT_SCHEMA, "status": "BOUND", "binding_id": binding_id,
"binding_sha256": binding_sha, "task_id": CHILD_TASK_ID,
"expected_revision": binding["expected_revision"],
"metadata_sha256": binding["metadata_sha256"], "worker_id": WORKER_ID,
"execution_digest": canonical_execution_digest,
"canonical_child_execution_digest": canonical_execution_digest,
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH, "generation": GENERATION,
"owner_supersession_decision_sha256": relation["owner_decision_sha256"],
}
receipt_sha = _sha_json(receipt)
receipt_id = "sha256:" + receipt_sha
connection.execute(
"INSERT INTO task_claim_guard_binding_receipts_v2 VALUES(?,?,?,?,?,?)",
(receipt_id, binding_id, CHILD_TASK_ID, canonical_json(receipt), receipt_sha, created_at),
)
return {**receipt, "receipt_id": receipt_id, "receipt_sha256": receipt_sha, "idempotent": False}
def _load_child_binding(connection) -> tuple[dict[str, Any], dict[str, Any]]:
row = connection.execute(
"SELECT * FROM task_claim_guard_bindings_v2 WHERE task_id=?", (CHILD_TASK_ID,)
).fetchone()
if row is None:
_fail("CHILD_CLAIM_GUARD_BINDING_MISSING")
value = _row_dict(row)
binding = _json(value["binding_json"], "CLAIM_GUARD_BINDING_INVALID")
policy = _json(value["policy_json"], "OPERATIONAL_POLICY_INVALID")
relation = connection.execute(
"SELECT * FROM task_lineage_supersessions WHERE source_task_id=? "
"AND supersession_ordinal=1 AND child_task_id=?",
(SOURCE_TASK_ID, CHILD_TASK_ID),
).fetchone()
if relation is None:
_fail("FORMAL_SUPERSESSION_REQUIRED")
projection = _load_projection_evidence(connection, relation)
canonical_execution_digest = projection["canonical_child_execution_digest"]
if (
_sha_text(value["binding_json"]) != value["binding_sha256"]
or binding.get("schema") != BINDING_SCHEMA
or value.get("execution_digest") != canonical_execution_digest
or binding.get("execution_digest") != canonical_execution_digest
or binding.get("canonical_child_execution_digest")
!= canonical_execution_digest
or binding.get("source_business_execution_digest")
!= SOURCE_BUSINESS_EXECUTION_DIGEST
or binding.get("owner_supersession_decision_sha256")
!= relation["owner_decision_sha256"]
or policy.get("execution_plan_sha256") != canonical_execution_digest
):
_fail("CLAIM_GUARD_BINDING_DIGEST_MISMATCH")
task = connection.execute("SELECT * FROM tasks WHERE task_id=?", (CHILD_TASK_ID,)).fetchone()
task = _row_dict(task)
if (
task.get("status") != "QUEUED" or int(task.get("revision", -1)) != int(binding["expected_revision"])
or int(task.get("attempt", -1)) != 0 or task.get("claimed_by") is not None
or task.get("lease_token_hash") is not None or task.get("lease_expires_at") is not None
or _sha_text(task.get("metadata_json", "")) != binding["metadata_sha256"]
or task.get("result_json") is not None or task.get("verification_json") is not None
):
_fail("CHILD_CLAIM_GUARD_TASK_CAS_MISMATCH")
return binding, policy
def _run_child_guard(connection, binding, policy_value, agent_id):
try:
from aios_single_host_operational_authority import OperationalPolicy, OperationalVerifier
from aios_singlehost_claim_guard_stage1 import _LiveReader, REGISTRY_PATH
from aios_task_claim_guard import ClaimGuard, ClaimGuardError as CoreError
except ImportError as exc:
raise ClaimGuardBindingError("CLAIM_GUARD_RUNTIME_UNAVAILABLE", 503) from exc
policy = OperationalPolicy.from_mapping(policy_value)
cfg = {
"task_id": CHILD_TASK_ID, "expected_claim_revision": binding["expected_revision"],
"task_metadata_sha256": binding["metadata_sha256"],
"required_capabilities_sha256": binding["required_capabilities_sha256"],
"task_contract_version": binding["task_contract_version"], "worker_id": WORKER_ID,
"policy_digest": binding["policy_digest"], "runtime_id": binding["runtime_id"],
"runtime_observation_digest": binding["runtime_observation_digest"],
}
reader = _LiveReader(connection, policy, cfg, now=int(datetime.now(timezone.utc).timestamp()))
try:
ClaimGuard(policy, OperationalVerifier(REGISTRY_PATH, require_root_owner=True)).validate_in_transaction(
reader, task_id=CHILD_TASK_ID,
expected_revision=int(binding["expected_revision"]), worker_id=agent_id,
)
except CoreError as exc:
raise ClaimGuardBindingError(exc.code) from exc
def enforce_claim_guard(connection, *, task_id: str, agent_id: str) -> None:
"""Exact two-lineage dispatcher; unrelated Tasks retain their prior behavior."""
if task_id == SOURCE_TASK_ID:
try:
from aios_singlehost_claim_guard_stage1 import enforce_claim_guard as frozen_guard
except ImportError as exc:
raise ClaimGuardBindingError("FROZEN_SOURCE_CLAIM_GUARD_UNAVAILABLE", 503) from exc
frozen_guard(connection, task_id=task_id, agent_id=agent_id)
return
if task_id != CHILD_TASK_ID:
return
if agent_id != WORKER_ID:
_fail("WORKER_ID_MISMATCH", 403)
binding, policy = _load_child_binding(connection)
_run_child_guard(connection, binding, policy, agent_id)
@@ -0,0 +1,339 @@
"""One-time, Owner-signed, append-only supersession for one exact Task lineage."""
from __future__ import annotations
from contextlib import closing
from datetime import datetime, timedelta, timezone
import hashlib
import json
from pathlib import Path
import re
import sqlite3
from typing import Any, Callable, Mapping, Protocol
DECISION_SCHEMA = "aios.task-lineage-supersession-decision.v1"
RECEIPT_SCHEMA = "aios.task-lineage-supersession-receipt.v1"
OWNER_NAMESPACE = "aios-owner-approval-v1"
OWNER_PRINCIPAL = "aios-task-grant-signer"
OWNER_KEY_ID = "SHA256:rPqpxfPHtaZPM3zSWojX8Q7VxzCknsYqOokyo/aAM3A"
SOURCE_TASK_ID = "TASK-SINGLEHOST-TRUSTROOT-P0-20260904"
CHILD_TASK_ID = SOURCE_TASK_ID + "-S1"
REPOSITORY = "aios/aios"
TARGET_BRANCH = "agent/single-host-trust-root-p0-20260904"
GENERATION = 1
EXECUTION_DIGEST = "e99bda6a251832eb3865271eb5687af7eea44dd261144cdcdfa57104c0780536"
EXTERNAL_JOB_ID = "job-4e10f92a6a5742518a9f1d8745c845ab"
SUPERSESSION_ORDINAL = 1
CANONICAL_PROFILE = "aios.task-initial-execution.v1"
CANONICAL_PROFILE_VERSION = 1
SUPERSESSION_REASON = "formal-lineage-supersession-after-expired-unclaimed-source"
MAX_TTL = timedelta(minutes=5)
HEX64 = re.compile(r"^[0-9a-f]{64}$")
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
class TaskLineageSupersessionError(RuntimeError):
def __init__(self, code: str, http_status: int = 409):
super().__init__(code)
self.code = code
self.http_status = int(http_status)
class SignatureVerifier(Protocol):
def verify(self, *, namespace: str, payload: bytes, signature: str, key_id: str) -> bool: ...
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
def sha256_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _fail(code: str, status: int = 409) -> None:
raise TaskLineageSupersessionError(code, status)
def _utc(value: Any, field: str) -> datetime:
if not isinstance(value, str) or not value or len(value) > 80:
_fail(field.upper() + "_INVALID", 400)
try:
result = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise TaskLineageSupersessionError(field.upper() + "_INVALID", 400) from exc
if result.tzinfo is None:
_fail(field.upper() + "_INVALID", 400)
return result.astimezone(timezone.utc)
def _text(value: Any, field: str, *, maximum: int = 1024) -> str:
if not isinstance(value, str) or not value or len(value) > maximum:
_fail(field.upper() + "_INVALID", 400)
return value
def decision_payload(raw: Mapping[str, Any]) -> dict[str, Any]:
return {key: raw[key] for key in raw if key != "signature"}
DECISION_FIELDS = {
"schema", "decision_id", "nonce", "source_task_id",
"source_task_identity_sha256", "source_metadata_sha256",
"source_expected_status", "source_expected_revision", "source_expected_attempt",
"source_expected_claimed_by", "source_expected_lease_token_hash",
"source_expected_lease_expires_at", "repository", "target_branch", "generation",
"execution_digest", "external_job_id", "child_task_id", "supersession_ordinal",
"canonical_projection_profile", "canonical_projection_version", "supersession_reason",
"owner_identity", "signature_namespace", "signature_principal", "key_id",
"issued_at", "not_before", "expires_at", "one_time_use", "signature",
}
def validate_decision(raw: Mapping[str, Any], *, now: datetime) -> dict[str, Any]:
if not isinstance(raw, Mapping) or set(raw) != DECISION_FIELDS:
_fail("DECISION_FIELDS_INVALID", 400)
exact = {
"schema": DECISION_SCHEMA,
"source_task_id": SOURCE_TASK_ID,
"source_expected_status": "EXPIRED",
"source_expected_attempt": 0,
"source_expected_claimed_by": None,
"source_expected_lease_token_hash": None,
"source_expected_lease_expires_at": None,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"execution_digest": EXECUTION_DIGEST,
"external_job_id": EXTERNAL_JOB_ID,
"child_task_id": CHILD_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
"supersession_reason": SUPERSESSION_REASON,
"owner_identity": OWNER_PRINCIPAL,
"signature_namespace": OWNER_NAMESPACE,
"signature_principal": OWNER_PRINCIPAL,
"key_id": OWNER_KEY_ID,
"one_time_use": True,
}
for field, expected in exact.items():
if raw[field] != expected:
_fail(field.upper() + "_MISMATCH", 403 if field.startswith(("owner", "signature", "key")) else 409)
for field in ("decision_id", "nonce"):
value = _text(raw[field], field, maximum=256)
if SAFE_ID.fullmatch(value) is None:
_fail(field.upper() + "_INVALID", 400)
for field in ("source_task_identity_sha256", "source_metadata_sha256"):
if not isinstance(raw[field], str) or HEX64.fullmatch(raw[field]) is None:
_fail(field.upper() + "_INVALID", 400)
if type(raw["source_expected_revision"]) is not int or raw["source_expected_revision"] < 1:
_fail("SOURCE_EXPECTED_REVISION_INVALID", 400)
_text(raw["signature"], "signature", maximum=32768)
issued_at = _utc(raw["issued_at"], "issued_at")
not_before = _utc(raw["not_before"], "not_before")
expires_at = _utc(raw["expires_at"], "expires_at")
if issued_at > expires_at or not_before > expires_at:
_fail("DECISION_TIME_ORDER_INVALID", 403)
if expires_at - issued_at > MAX_TTL or expires_at <= issued_at:
_fail("DECISION_TTL_INVALID", 403)
now = now.astimezone(timezone.utc)
if now < not_before or now >= expires_at:
_fail("DECISION_EXPIRED_OR_NOT_YET_VALID", 403)
if issued_at > now + timedelta(seconds=5) or now - issued_at > MAX_TTL:
_fail("DECISION_ISSUED_AT_INVALID", 403)
return {**dict(raw), "issued_at_dt": issued_at, "not_before_dt": not_before, "expires_at_dt": expires_at}
def _default_identity_digest(row: sqlite3.Row) -> str:
try:
from aios_expired_task_recovery import task_identity_digest
except ImportError as exc:
raise TaskLineageSupersessionError("FORMAL_TASK_IDENTITY_HELPER_UNAVAILABLE", 503) from exc
return str(task_identity_digest(row))
def _json_object(row: sqlite3.Row, field: str) -> dict[str, Any]:
try:
value = json.loads(row[field])
except (TypeError, ValueError, KeyError) as exc:
raise TaskLineageSupersessionError("SOURCE_TASK_JSON_INVALID", 409) from exc
if not isinstance(value, Mapping):
_fail("SOURCE_TASK_JSON_INVALID")
return dict(value)
def _verify_source(row: sqlite3.Row, d: Mapping[str, Any], identity_digest: Callable[[sqlite3.Row], str]) -> None:
if row["task_id"] != SOURCE_TASK_ID:
_fail("SOURCE_TASK_ID_MISMATCH")
cas = (
row["status"] == "EXPIRED"
and int(row["revision"]) == d["source_expected_revision"]
and int(row["attempt"]) == 0
and row["claimed_by"] is None
and row["lease_token_hash"] is None
and row["lease_expires_at"] is None
)
if not cas:
_fail("SOURCE_TASK_EXACT_CAS_MISMATCH")
metadata_raw = row["metadata_json"]
if not isinstance(metadata_raw, str) or sha256_text(metadata_raw) != d["source_metadata_sha256"]:
_fail("SOURCE_METADATA_DIGEST_MISMATCH")
if identity_digest(row) != d["source_task_identity_sha256"]:
_fail("SOURCE_TASK_IDENTITY_DIGEST_MISMATCH")
metadata = _json_object(row, "metadata_json")
execution = metadata.get("execution")
constraints = _json_object(row, "constraints_json")
if not isinstance(execution, Mapping):
_fail("SOURCE_EXECUTION_BINDING_INVALID")
expected = {
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"execution_digest": EXECUTION_DIGEST,
"external_job_id": EXTERNAL_JOB_ID,
"task_id": SOURCE_TASK_ID,
}
if any(execution.get(key) != value for key, value in expected.items()):
_fail("SOURCE_EXECUTION_BINDING_MISMATCH")
if constraints.get("generation") != GENERATION:
_fail("SOURCE_GENERATION_CONSTRAINT_MISMATCH")
class TaskLineageSupersessionController:
def __init__(self, db_path: str | Path, verifier: SignatureVerifier, *, clock=None, identity_digest=None):
self.db_path = Path(db_path)
self.verifier = verifier
self.clock = clock or (lambda: datetime.now(timezone.utc))
self.identity_digest = identity_digest or _default_identity_digest
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.db_path, timeout=30, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _now(self) -> datetime:
value = self.clock()
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
@staticmethod
def _assert_schema(connection: sqlite3.Connection) -> None:
required = {"tasks", "events", "task_lineage_supersessions", "task_lineage_supersession_receipts"}
actual = {row[0] for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if not required.issubset(actual):
_fail("SUPERSESSION_SCHEMA_UNAVAILABLE", 503)
columns = {row[1] for row in connection.execute("PRAGMA table_info(tasks)")}
task_columns = {
"task_id", "goal", "instructions", "success_criteria_json",
"required_capabilities_json", "constraints_json", "metadata_json", "deadline",
"priority", "max_attempts", "attempt", "status", "claimed_by",
"lease_token_hash", "lease_expires_at", "result_json", "result_digest",
"result_idempotency_key", "verification_json", "revision", "created_at", "updated_at",
}
if not task_columns.issubset(columns):
_fail("TASK_SCHEMA_UNSUPPORTED", 503)
def supersede(self, raw: Mapping[str, Any]) -> dict[str, Any]:
now = self._now()
decision = validate_decision(raw, now=now)
payload_bytes = canonical_json(decision_payload(raw)).encode("utf-8")
if not self.verifier.verify(
namespace=OWNER_NAMESPACE, payload=payload_bytes,
signature=decision["signature"], key_id=decision["key_id"],
):
_fail("DECISION_SIGNATURE_INVALID", 403)
decision_sha = hashlib.sha256(payload_bytes).hexdigest()
now_text = now.isoformat()
with closing(self._connect()) as connection:
connection.execute("BEGIN IMMEDIATE")
try:
self._assert_schema(connection)
replay = connection.execute(
"SELECT owner_decision_sha256,receipt_json FROM task_lineage_supersession_receipts WHERE decision_id=? OR nonce=?",
(decision["decision_id"], decision["nonce"]),
).fetchone()
if replay is not None:
if replay["owner_decision_sha256"] != decision_sha:
_fail("DECISION_REPLAY_MISMATCH")
result = json.loads(replay["receipt_json"])
connection.commit()
return {**result, "idempotent": True}
source = connection.execute("SELECT * FROM tasks WHERE task_id=?", (SOURCE_TASK_ID,)).fetchone()
if source is None:
_fail("SOURCE_TASK_NOT_FOUND", 404)
_verify_source(source, decision, self.identity_digest)
if connection.execute(
"SELECT 1 FROM task_lineage_supersessions WHERE source_task_id=? AND supersession_ordinal=?",
(SOURCE_TASK_ID, SUPERSESSION_ORDINAL),
).fetchone():
_fail("SUPERSESSION_ALREADY_EXISTS")
if connection.execute("SELECT 1 FROM tasks WHERE task_id=?", (CHILD_TASK_ID,)).fetchone():
_fail("CHILD_TASK_ALREADY_EXISTS")
connection.execute(
"""INSERT INTO tasks(
task_id,goal,instructions,success_criteria_json,required_capabilities_json,
constraints_json,metadata_json,deadline,priority,max_attempts,attempt,status,
claimed_by,lease_token_hash,lease_expires_at,result_json,result_digest,
result_idempotency_key,verification_json,revision,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
CHILD_TASK_ID, source["goal"], source["instructions"], source["success_criteria_json"],
"[]", source["constraints_json"], "{}", None, source["priority"], source["max_attempts"],
0, "QUEUED", None, None, None, None, None, None, None, 1, now_text, now_text,
),
)
connection.execute(
"""INSERT INTO task_lineage_supersessions(
source_task_id,supersession_ordinal,child_task_id,source_task_identity_sha256,
source_metadata_sha256,owner_decision_sha256,decision_id,created_at)
VALUES(?,?,?,?,?,?,?,?)""",
(SOURCE_TASK_ID, SUPERSESSION_ORDINAL, CHILD_TASK_ID,
decision["source_task_identity_sha256"], decision["source_metadata_sha256"],
decision_sha, decision["decision_id"], now_text),
)
event_payload = {
"task_id": CHILD_TASK_ID, "supersedes_task_id": SOURCE_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"source_task_identity_sha256": decision["source_task_identity_sha256"],
"source_metadata_sha256": decision["source_metadata_sha256"],
"owner_supersession_decision_sha256": decision_sha,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
}
connection.execute(
"INSERT INTO events(task_id,agent_id,event_type,payload_json,created_at) VALUES(?,?,?,?,?)",
(CHILD_TASK_ID, None, "TASK_CREATED", canonical_json(event_payload), now_text),
)
receipt = {
"schema": RECEIPT_SCHEMA, "status": "SUPERSEDED",
"decision_id": decision["decision_id"], "owner_decision_sha256": decision_sha,
"source_task_id": SOURCE_TASK_ID, "source_task_identity_sha256": decision["source_task_identity_sha256"],
"source_metadata_sha256": decision["source_metadata_sha256"],
"source_status": "EXPIRED", "source_revision": decision["source_expected_revision"],
"source_attempt": 0, "source_history_preserved": True,
"child_task_id": CHILD_TASK_ID, "supersession_ordinal": SUPERSESSION_ORDINAL,
"child_status": "QUEUED", "child_revision": 1, "child_attempt": 0,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
"exact_identity_cas": True, "one_time_use": True,
"owner_signature_namespace": OWNER_NAMESPACE,
"owner_signature_principal": OWNER_PRINCIPAL,
"owner_signature_key_id": OWNER_KEY_ID,
"owner_signature_sha256": sha256_text(decision["signature"]),
}
connection.execute(
"""INSERT INTO task_lineage_supersession_receipts(
decision_id,nonce,source_task_id,supersession_ordinal,child_task_id,
owner_decision_sha256,decision_json,receipt_json,consumed_at)
VALUES(?,?,?,?,?,?,?,?,?)""",
(decision["decision_id"], decision["nonce"], SOURCE_TASK_ID, SUPERSESSION_ORDINAL,
CHILD_TASK_ID, decision_sha, canonical_json(dict(raw)), canonical_json(receipt), now_text),
)
connection.commit()
return {**receipt, "idempotent": False}
except Exception:
connection.rollback()
raise
@@ -0,0 +1,548 @@
"""Canonical initial projection for the exact Single-Host supersession child.
The source business execution digest is lineage evidence. It is deliberately
not used as the child execution-envelope digest: the canonical envelope binds
the child Task id and therefore has its own server-derived digest.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import sqlite3
from typing import Any, Mapping
from aios_agent_task_initial_execution_projection import (
InitialExecutionProjectionError,
InitialExecutionProjectionRequest,
InitialExecutionProjectionStore,
RUNTIME_CAPABILITY_RE,
_now,
_require_string,
_required_authorization_id,
)
from aios_online_agent_execution import (
ExecutionProjectionError,
GovernedExecutionBinding,
validate_execution_envelope,
)
from aios_task_lineage_supersession import (
CANONICAL_PROFILE,
CANONICAL_PROFILE_VERSION,
CHILD_TASK_ID,
DECISION_SCHEMA,
EXECUTION_DIGEST as SOURCE_BUSINESS_EXECUTION_DIGEST,
EXTERNAL_JOB_ID,
GENERATION,
OWNER_KEY_ID,
OWNER_NAMESPACE,
OWNER_PRINCIPAL,
RECEIPT_SCHEMA as SUPERSESSION_RECEIPT_SCHEMA,
REPOSITORY,
SOURCE_TASK_ID,
SUPERSESSION_ORDINAL,
TARGET_BRANCH,
canonical_json,
decision_payload,
)
PROJECTION_INTENT_SCHEMA = "aios.task-lineage-supersession-initial-projection-intent.v1"
PROJECTION_RECEIPT_SCHEMA = "aios.task-lineage-supersession-initial-projection-receipt.v1"
PROJECTION_IDEMPOTENCY_KEY = "task-lineage-supersession-s1-initial-projection-v1"
PROJECTION_REASON = "canonical-initial-projection-for-formal-supersession-child"
PROJECTION_TABLE = "task_lineage_supersession_initial_projections"
def _sha_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _sha_json(value: Any) -> str:
return _sha_text(canonical_json(value))
def _json_object(raw: Any, code: str) -> dict[str, Any]:
if not isinstance(raw, str):
raise InitialExecutionProjectionError(code, 409)
try:
value = json.loads(raw)
except (TypeError, ValueError) as exc:
raise InitialExecutionProjectionError(code, 409) from exc
if not isinstance(value, Mapping):
raise InitialExecutionProjectionError(code, 409)
return dict(value)
@dataclass(frozen=True)
class SupersessionChildProjectionIntent:
"""Caller-selectable portion of the exact child projection.
Task identity, CAS values, Job identity, projection profile, reason and
idempotency are intentionally absent from the request and server-owned.
"""
adapter: str
required_capability: str
authorization_id: str
@classmethod
def from_mapping(
cls, value: Mapping[str, Any]
) -> "SupersessionChildProjectionIntent":
if not isinstance(value, Mapping) or set(value) != {
"adapter",
"required_capability",
"authorization_id",
}:
raise InitialExecutionProjectionError(
"SUPERSESSION_INITIAL_PROJECTION_FIELDS_INVALID", 400
)
adapter = _require_string(value["adapter"], "RUNTIME_ADAPTER_INVALID")
capability = _require_string(
value["required_capability"],
"RUNTIME_CAPABILITY_INVALID",
RUNTIME_CAPABILITY_RE,
)
if adapter not in {"hermes", "openclaw"} or capability != f"agent.runtime.{adapter}":
raise InitialExecutionProjectionError("RUNTIME_CAPABILITY_MISMATCH", 400)
authorization_id = _require_string(
value["authorization_id"], "AUTHORIZATION_ID_INVALID"
)
return cls(adapter, capability, authorization_id)
def public(self) -> dict[str, Any]:
return {
"schema": PROJECTION_INTENT_SCHEMA,
"source_task_id": SOURCE_TASK_ID,
"child_task_id": CHILD_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
"expected_status": "QUEUED",
"expected_revision": 1,
"expected_attempt": 0,
"expected_metadata_sha256": _sha_json({}),
"expected_required_capabilities_sha256": _sha_json([]),
"expected_job_id": EXTERNAL_JOB_ID,
"adapter": self.adapter,
"required_capability": self.required_capability,
"authorization_id": self.authorization_id,
"idempotency_key": PROJECTION_IDEMPOTENCY_KEY,
"reason": PROJECTION_REASON,
}
def initial_request(self, canonical_execution_digest: str) -> InitialExecutionProjectionRequest:
return InitialExecutionProjectionRequest.from_mapping(
{
"expected_status": "QUEUED",
"expected_revision": 1,
"expected_attempt": 0,
"expected_metadata_sha256": _sha_json({}),
"expected_required_capabilities_sha256": _sha_json([]),
"expected_job_id": EXTERNAL_JOB_ID,
"expected_execution_digest": canonical_execution_digest,
"required_capability": self.required_capability,
"adapter": self.adapter,
"authorization_id": self.authorization_id,
"idempotency_key": PROJECTION_IDEMPOTENCY_KEY,
"reason": PROJECTION_REASON,
}
)
def _table_names(connection: sqlite3.Connection) -> set[str]:
return {
str(row[0])
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
def _load_lineage_context(
connection: sqlite3.Connection, *, expect_projected: bool
) -> dict[str, Any]:
required_tables = {
"tasks",
"events",
"task_lineage_supersessions",
"task_lineage_supersession_receipts",
PROJECTION_TABLE,
}
if not required_tables.issubset(_table_names(connection)):
raise InitialExecutionProjectionError(
"SUPERSESSION_INITIAL_PROJECTION_SCHEMA_UNAVAILABLE", 503
)
relation = connection.execute(
"SELECT * FROM task_lineage_supersessions "
"WHERE source_task_id=? AND supersession_ordinal=? AND child_task_id=?",
(SOURCE_TASK_ID, SUPERSESSION_ORDINAL, CHILD_TASK_ID),
).fetchone()
if relation is None:
raise InitialExecutionProjectionError("FORMAL_SUPERSESSION_REQUIRED", 409)
receipt_row = connection.execute(
"SELECT * FROM task_lineage_supersession_receipts "
"WHERE decision_id=? AND source_task_id=? AND supersession_ordinal=? "
"AND child_task_id=?",
(
relation["decision_id"],
SOURCE_TASK_ID,
SUPERSESSION_ORDINAL,
CHILD_TASK_ID,
),
).fetchone()
if receipt_row is None:
raise InitialExecutionProjectionError("SUPERSESSION_RECEIPT_REQUIRED", 409)
decision = _json_object(receipt_row["decision_json"], "SUPERSESSION_DECISION_INVALID")
receipt = _json_object(receipt_row["receipt_json"], "SUPERSESSION_RECEIPT_INVALID")
decision_sha256 = _sha_text(canonical_json(decision_payload(decision)))
exact_decision = {
"schema": DECISION_SCHEMA,
"source_task_id": SOURCE_TASK_ID,
"child_task_id": CHILD_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"external_job_id": EXTERNAL_JOB_ID,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
"owner_identity": OWNER_PRINCIPAL,
"signature_namespace": OWNER_NAMESPACE,
"signature_principal": OWNER_PRINCIPAL,
"key_id": OWNER_KEY_ID,
"one_time_use": True,
}
if any(decision.get(key) != expected for key, expected in exact_decision.items()):
raise InitialExecutionProjectionError(
"SUPERSESSION_DECISION_SCOPE_MISMATCH", 409
)
if (
decision_sha256 != relation["owner_decision_sha256"]
or decision_sha256 != receipt_row["owner_decision_sha256"]
or decision.get("source_task_identity_sha256")
!= relation["source_task_identity_sha256"]
or decision.get("source_metadata_sha256") != relation["source_metadata_sha256"]
or receipt.get("schema") != SUPERSESSION_RECEIPT_SCHEMA
or receipt.get("owner_decision_sha256") != decision_sha256
or receipt.get("source_task_id") != SOURCE_TASK_ID
or receipt.get("child_task_id") != CHILD_TASK_ID
or receipt.get("supersession_ordinal") != SUPERSESSION_ORDINAL
):
raise InitialExecutionProjectionError(
"SUPERSESSION_LINEAGE_EVIDENCE_MISMATCH", 409
)
events = connection.execute(
"SELECT event_type,payload_json FROM events WHERE task_id=? ORDER BY event_id",
(CHILD_TASK_ID,),
).fetchall()
expected_events = ["TASK_CREATED"]
if expect_projected:
expected_events.append("TASK_INITIAL_EXECUTION_PROJECTION_AUTHORIZED")
if [str(row["event_type"]) for row in events] != expected_events:
raise InitialExecutionProjectionError(
"SUPERSESSION_CHILD_EVENT_CHAIN_INVALID", 409
)
created = _json_object(events[0]["payload_json"], "SUPERSESSION_CREATED_EVENT_INVALID")
exact_created = {
"task_id": CHILD_TASK_ID,
"supersedes_task_id": SOURCE_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"source_task_identity_sha256": relation["source_task_identity_sha256"],
"source_metadata_sha256": relation["source_metadata_sha256"],
"owner_supersession_decision_sha256": decision_sha256,
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
}
if any(created.get(key) != expected for key, expected in exact_created.items()):
raise InitialExecutionProjectionError(
"SUPERSESSION_CREATED_EVENT_SCOPE_MISMATCH", 409
)
return {
"source_task_identity_sha256": relation["source_task_identity_sha256"],
"source_metadata_sha256": relation["source_metadata_sha256"],
"owner_decision_sha256": decision_sha256,
}
def _validate_canonical_child_execution(
resolved: Mapping[str, Any],
*,
intent: SupersessionChildProjectionIntent,
runtime_input: Mapping[str, Any],
) -> dict[str, Any]:
try:
binding = GovernedExecutionBinding.from_mapping(resolved)
execution = validate_execution_envelope(resolved, binding=binding)
except ExecutionProjectionError as exc:
raise InitialExecutionProjectionError(
"SUPERSESSION_CANONICAL_EXECUTION_INVALID", 409
) from exc
exact_binding = {
"task_id": CHILD_TASK_ID,
"job_id": EXTERNAL_JOB_ID,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
}
if any(execution.get(key) != expected for key, expected in exact_binding.items()):
raise InitialExecutionProjectionError(
"SUPERSESSION_CANONICAL_EXECUTION_BINDING_MISMATCH", 409
)
if (
execution.get("action") != "agent_runtime"
or execution.get("adapter") != intent.adapter
or execution.get("capability_scope") != [intent.required_capability]
or execution.get("runtime_input") != dict(runtime_input)
or execution.get("change_set") != []
or execution.get("verification") != []
or execution.get("governance", {}).get("writer_lease") is not False
):
raise InitialExecutionProjectionError(
"SUPERSESSION_AGENT_RUNTIME_ENVELOPE_INVALID", 409
)
if execution["execution_digest"] == SOURCE_BUSINESS_EXECUTION_DIGEST:
raise InitialExecutionProjectionError(
"CHILD_CANONICAL_DIGEST_COLLIDES_WITH_SOURCE_BUSINESS_DIGEST", 409
)
return execution
class _SupersessionProjectionTransactionStore(InitialExecutionProjectionStore):
"""Per-invocation store that adds immutable lineage evidence atomically."""
allows_supersession_child = True
def __init__(self, db_path, resolved: Mapping[str, Any], intent):
self._resolved = dict(resolved)
self.intent = intent
super().__init__(db_path, self._resolve_once)
def _resolve_once(self, *_args):
return dict(self._resolved)
def _projection_event_extension(
self, connection, task_id, request, execution, applied_revision
):
if task_id != CHILD_TASK_ID:
raise InitialExecutionProjectionError(
"SUPERSESSION_CHILD_TASK_ID_MISMATCH", 409
)
context = _load_lineage_context(connection, expect_projected=False)
return {
"source_task_id": SOURCE_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"source_task_identity_sha256": context["source_task_identity_sha256"],
"source_metadata_sha256": context["source_metadata_sha256"],
"owner_supersession_decision_sha256": context["owner_decision_sha256"],
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"canonical_child_execution_digest": execution["execution_digest"],
"canonical_projection_profile": CANONICAL_PROFILE,
"canonical_projection_version": CANONICAL_PROFILE_VERSION,
}
def _insert_projection_extension(
self,
connection,
task_id,
request,
execution,
event_payload,
applied_revision,
):
context = _load_lineage_context(connection, expect_projected=True)
task = connection.execute(
"SELECT metadata_json,required_capabilities_json FROM tasks WHERE task_id=?",
(CHILD_TASK_ID,),
).fetchone()
if task is None:
raise InitialExecutionProjectionError("TASK_NOT_FOUND", 404)
intent_json = canonical_json(self.intent.public())
intent_sha256 = _sha_text(intent_json)
receipt = {
"schema": PROJECTION_RECEIPT_SCHEMA,
"projection_id": "sha256:" + intent_sha256,
"task_id": CHILD_TASK_ID,
"source_task_id": SOURCE_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"source_task_identity_sha256": context["source_task_identity_sha256"],
"source_metadata_sha256": context["source_metadata_sha256"],
"owner_supersession_decision_sha256": context["owner_decision_sha256"],
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"canonical_child_execution_digest": execution["execution_digest"],
"external_job_id": EXTERNAL_JOB_ID,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"adapter": self.intent.adapter,
"required_capability": self.intent.required_capability,
"expected_revision": 1,
"applied_revision": applied_revision,
"attempt": 0,
"metadata_sha256": _sha_text(task["metadata_json"]),
"required_capabilities_sha256": _sha_text(
task["required_capabilities_json"]
),
"projection_event_sha256": _sha_json(dict(event_payload)),
}
receipt_json = canonical_json(receipt)
receipt_sha256 = _sha_text(receipt_json)
connection.execute(
f"""INSERT INTO {PROJECTION_TABLE}(
projection_id,task_id,source_task_id,supersession_ordinal,
intent_sha256,intent_json,owner_decision_sha256,
source_task_identity_sha256,source_metadata_sha256,
source_business_execution_digest,canonical_child_execution_digest,
external_job_id,repository,target_branch,generation,adapter,
required_capability,expected_revision,applied_revision,metadata_sha256,
required_capabilities_sha256,projection_event_sha256,receipt_json,
receipt_sha256,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
receipt["projection_id"],
CHILD_TASK_ID,
SOURCE_TASK_ID,
SUPERSESSION_ORDINAL,
intent_sha256,
intent_json,
context["owner_decision_sha256"],
context["source_task_identity_sha256"],
context["source_metadata_sha256"],
SOURCE_BUSINESS_EXECUTION_DIGEST,
execution["execution_digest"],
EXTERNAL_JOB_ID,
REPOSITORY,
TARGET_BRANCH,
GENERATION,
self.intent.adapter,
self.intent.required_capability,
1,
applied_revision,
receipt["metadata_sha256"],
receipt["required_capabilities_sha256"],
receipt["projection_event_sha256"],
receipt_json,
receipt_sha256,
_now(),
),
)
class SupersessionChildInitialProjectionStore:
"""Server-owned adapter from exact supersession lineage to current ABI."""
def __init__(self, db_path, resolver):
self.db_path = db_path
self.resolver = resolver
def _connect(self):
connection = sqlite3.connect(self.db_path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@staticmethod
def _replay(connection, intent: SupersessionChildProjectionIntent):
if PROJECTION_TABLE not in _table_names(connection):
raise InitialExecutionProjectionError(
"SUPERSESSION_INITIAL_PROJECTION_SCHEMA_UNAVAILABLE", 503
)
row = connection.execute(
f"SELECT * FROM {PROJECTION_TABLE} WHERE task_id=?", (CHILD_TASK_ID,)
).fetchone()
if row is None:
tables = _table_names(connection)
if "task_initial_execution_projections" in tables:
generic = connection.execute(
"SELECT 1 FROM task_initial_execution_projections "
"WHERE task_id=? AND idempotency_key=?",
(CHILD_TASK_ID, PROJECTION_IDEMPOTENCY_KEY),
).fetchone()
if generic is not None:
raise InitialExecutionProjectionError(
"SUPERSESSION_PROJECTION_RECEIPT_MISSING", 500
)
return None
intent_sha256 = _sha_json(intent.public())
if row["intent_sha256"] != intent_sha256:
raise InitialExecutionProjectionError(
"SUPERSESSION_INITIAL_PROJECTION_INTENT_CONFLICT", 409
)
receipt = _json_object(
row["receipt_json"], "SUPERSESSION_PROJECTION_RECEIPT_INVALID"
)
if (
_sha_text(row["receipt_json"]) != row["receipt_sha256"]
or receipt.get("schema") != PROJECTION_RECEIPT_SCHEMA
or receipt.get("canonical_child_execution_digest")
!= row["canonical_child_execution_digest"]
):
raise InitialExecutionProjectionError(
"SUPERSESSION_PROJECTION_RECEIPT_DIGEST_MISMATCH", 500
)
_load_lineage_context(connection, expect_projected=True)
task = connection.execute(
"SELECT * FROM tasks WHERE task_id=?", (CHILD_TASK_ID,)
).fetchone()
if (
task is None
or task["status"] != "QUEUED"
or int(task["revision"]) != int(row["applied_revision"])
or int(task["attempt"]) != 0
or task["claimed_by"] is not None
or task["lease_token_hash"] is not None
or task["lease_expires_at"] is not None
or _sha_text(task["metadata_json"]) != row["metadata_sha256"]
or _sha_text(task["required_capabilities_json"])
!= row["required_capabilities_sha256"]
):
raise InitialExecutionProjectionError(
"SUPERSESSION_PROJECTED_TASK_CAS_MISMATCH", 409
)
return {**receipt, "idempotent": True}
def project(self, value: Mapping[str, Any]) -> dict[str, Any]:
intent = SupersessionChildProjectionIntent.from_mapping(value)
if intent.authorization_id != _required_authorization_id():
raise InitialExecutionProjectionError("AUTHORIZATION_ID_MISMATCH", 403)
with self._connect() as connection:
replay = self._replay(connection, intent)
if replay is not None:
return replay
probe_request = intent.initial_request("0" * 64)
probe_store = _SupersessionProjectionTransactionStore(
self.db_path, {}, intent
)
task, _, _, _ = probe_store._load_and_validate(
connection, CHILD_TASK_ID, probe_request
)
_load_lineage_context(connection, expect_projected=False)
runtime_input = probe_store._runtime_input(task)
resolved = dict(
self.resolver(
CHILD_TASK_ID,
EXTERNAL_JOB_ID,
intent.required_capability,
intent.adapter,
runtime_input,
)
)
execution = _validate_canonical_child_execution(
resolved, intent=intent, runtime_input=runtime_input
)
store = _SupersessionProjectionTransactionStore(
self.db_path, resolved, intent
)
result = store.project(
CHILD_TASK_ID, intent.initial_request(execution["execution_digest"])
)
with self._connect() as connection:
receipt = self._replay(connection, intent)
if receipt is None:
raise InitialExecutionProjectionError(
"SUPERSESSION_PROJECTION_RECEIPT_MISSING", 500
)
return {**receipt, "idempotent": bool(result["idempotent"])}
@@ -0,0 +1,221 @@
"""Fixed Owner operation for one exact Task-lineage supersession.
This staging overlay deliberately extends the installed single-host operational
authority instead of exposing a generic signing primitive. The caller can
only provide the three live CAS values which cannot be known at build time.
Every authority, lineage and execution field is constructed server-side.
"""
from __future__ import annotations
import re
import secrets
from typing import Any, Callable, Mapping
from aios_single_host_authority import OWNER_IDENTITY, OWNER_NAMESPACE, sha256_json
from aios_single_host_operational_authority import (
OperationalAuthorityError,
OperationalOwnerAuthority,
OperationalPolicy,
SignedOperationalArtifact,
)
TASK_LINEAGE_SUPERSESSION_OPERATION = "issue_task_lineage_supersession"
TASK_LINEAGE_SUPERSESSION_SCHEMA = "aios.task-lineage-supersession-decision.v1"
SOURCE_TASK_ID = "TASK-SINGLEHOST-TRUSTROOT-P0-20260904"
CHILD_TASK_ID = SOURCE_TASK_ID + "-S1"
SUPERSESSION_ORDINAL = 1
REPOSITORY = "aios/aios"
TARGET_BRANCH = "agent/single-host-trust-root-p0-20260904"
GENERATION = 1
SOURCE_BUSINESS_EXECUTION_DIGEST = (
"e99bda6a251832eb3865271eb5687af7eea44dd261144cdcdfa57104c0780536"
)
EXTERNAL_JOB_IDENTITY = "job-4e10f92a6a5742518a9f1d8745c845ab"
CANONICAL_PROJECTION_PROFILE = "aios.task-initial-execution.v1"
CANONICAL_PROJECTION_VERSION = 1
SUPERSESSION_REASON = "formal-lineage-supersession-after-expired-unclaimed-source"
DECISION_TTL_SECONDS = 300
HEX64 = re.compile(r"^[0-9a-f]{64}$")
SAFE_TOKEN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
def _fail(code: str) -> None:
raise OperationalAuthorityError(code)
def _assert_fixed_policy(policy: OperationalPolicy) -> None:
"""Refuse issuance if the live policy is not the approved source scope."""
policy.validate()
exact = {
"task_id": SOURCE_TASK_ID,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"execution_plan_sha256": SOURCE_BUSINESS_EXECUTION_DIGEST,
"external_job_identity": EXTERNAL_JOB_IDENTITY,
}
if any(getattr(policy, field) != expected for field, expected in exact.items()):
_fail("TASK_LINEAGE_SUPERSESSION_POLICY_MISMATCH")
if policy.maximum_recovery_decision_ttl_seconds < DECISION_TTL_SECONDS:
_fail("TASK_LINEAGE_SUPERSESSION_POLICY_TTL_TOO_NARROW")
TASK_LINEAGE_SUPERSESSION_FIELDS = {
"schema",
"decision_id",
"nonce",
"source_task_id",
"source_task_identity_sha256",
"source_metadata_sha256",
"source_expected_status",
"source_expected_revision",
"source_expected_attempt",
"source_expected_claimed_by",
"source_expected_lease_token_hash",
"source_expected_lease_expires_at",
"repository",
"target_branch",
"generation",
"source_business_execution_digest",
"external_job_identity",
"child_task_id",
"supersession_ordinal",
"canonical_projection_profile",
"canonical_projection_version",
"supersession_reason",
"owner_identity",
"policy_digest",
"one_time_use",
"issued_at",
"not_before",
"expires_at",
"signature_namespace",
"signature_key_id",
"canonical_payload_digest",
}
def verify_task_lineage_supersession_scope(
decision: Mapping[str, Any],
policy: OperationalPolicy,
*,
now: int,
) -> None:
"""Validate a signature-verified decision against the fixed source scope."""
_assert_fixed_policy(policy)
if not isinstance(decision, Mapping) or set(decision) != TASK_LINEAGE_SUPERSESSION_FIELDS:
_fail("TASK_LINEAGE_SUPERSESSION_FIELDS_INVALID")
exact = {
"schema": TASK_LINEAGE_SUPERSESSION_SCHEMA,
"source_task_id": SOURCE_TASK_ID,
"source_expected_status": "EXPIRED",
"source_expected_attempt": 0,
"source_expected_claimed_by": None,
"source_expected_lease_token_hash": None,
"source_expected_lease_expires_at": None,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"external_job_identity": EXTERNAL_JOB_IDENTITY,
"child_task_id": CHILD_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"canonical_projection_profile": CANONICAL_PROJECTION_PROFILE,
"canonical_projection_version": CANONICAL_PROJECTION_VERSION,
"supersession_reason": SUPERSESSION_REASON,
"owner_identity": OWNER_IDENTITY,
"policy_digest": policy.digest,
"one_time_use": True,
"signature_namespace": OWNER_NAMESPACE,
}
if any(decision.get(field) != expected for field, expected in exact.items()):
_fail("TASK_LINEAGE_SUPERSESSION_SCOPE_INVALID")
if decision.get("source_task_identity_sha256") != policy.task_identity_sha256:
_fail("TASK_LINEAGE_SUPERSESSION_SOURCE_IDENTITY_INVALID")
for field in ("source_task_identity_sha256", "source_metadata_sha256"):
if not isinstance(decision.get(field), str) or HEX64.fullmatch(decision[field]) is None:
_fail("TASK_LINEAGE_SUPERSESSION_DIGEST_INVALID")
if type(decision.get("source_expected_revision")) is not int or not 1 <= decision["source_expected_revision"] < 2**63:
_fail("TASK_LINEAGE_SUPERSESSION_REVISION_INVALID")
for field in ("decision_id", "nonce"):
if not isinstance(decision.get(field), str) or SAFE_TOKEN.fullmatch(decision[field]) is None:
_fail("TASK_LINEAGE_SUPERSESSION_GENERATED_ID_INVALID")
for field in ("issued_at", "not_before", "expires_at"):
if type(decision.get(field)) is not int:
_fail("TASK_LINEAGE_SUPERSESSION_TIME_INVALID")
if not decision["not_before"] <= now < decision["expires_at"]:
_fail("TASK_LINEAGE_SUPERSESSION_TIME_INVALID")
if decision["issued_at"] > now + 5 or now - decision["issued_at"] > DECISION_TTL_SECONDS:
_fail("TASK_LINEAGE_SUPERSESSION_TIME_INVALID")
if decision["expires_at"] - decision["issued_at"] != DECISION_TTL_SECONDS:
_fail("TASK_LINEAGE_SUPERSESSION_TIME_INVALID")
digest_value = dict(decision)
supplied_digest = digest_value.pop("canonical_payload_digest", None)
if supplied_digest != sha256_json(digest_value):
_fail("TASK_LINEAGE_SUPERSESSION_CANONICAL_DIGEST_INVALID")
class TaskLineageSupersessionOwnerAuthority(OperationalOwnerAuthority):
"""Owner signer with one bounded constructor and no caller-defined message."""
def __init__(
self,
*args: Any,
token_hex: Callable[[int], str] = secrets.token_hex,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self._token_hex = token_hex
def issue_task_lineage_supersession(
self,
*,
source_expected_revision: int,
source_task_identity_sha256: str,
source_metadata_sha256: str,
) -> SignedOperationalArtifact:
_assert_fixed_policy(self.policy)
if type(source_expected_revision) is not int or not 1 <= source_expected_revision < 2**63:
_fail("TASK_LINEAGE_SUPERSESSION_REVISION_INVALID")
if source_task_identity_sha256 != self.policy.task_identity_sha256:
_fail("TASK_LINEAGE_SUPERSESSION_SOURCE_IDENTITY_INVALID")
if not isinstance(source_metadata_sha256, str) or HEX64.fullmatch(source_metadata_sha256) is None:
_fail("TASK_LINEAGE_SUPERSESSION_METADATA_DIGEST_INVALID")
decision_id = "task-lineage-supersession-" + self._token_hex(16)
nonce = "task-lineage-supersession-nonce-" + self._token_hex(16)
if SAFE_TOKEN.fullmatch(decision_id) is None or SAFE_TOKEN.fullmatch(nonce) is None:
_fail("TASK_LINEAGE_SUPERSESSION_GENERATED_ID_INVALID")
now = int(self.clock())
payload = {
"schema": TASK_LINEAGE_SUPERSESSION_SCHEMA,
"decision_id": decision_id,
"nonce": nonce,
"source_task_id": SOURCE_TASK_ID,
"source_task_identity_sha256": source_task_identity_sha256,
"source_metadata_sha256": source_metadata_sha256,
"source_expected_status": "EXPIRED",
"source_expected_revision": source_expected_revision,
"source_expected_attempt": 0,
"source_expected_claimed_by": None,
"source_expected_lease_token_hash": None,
"source_expected_lease_expires_at": None,
"repository": REPOSITORY,
"target_branch": TARGET_BRANCH,
"generation": GENERATION,
"source_business_execution_digest": SOURCE_BUSINESS_EXECUTION_DIGEST,
"external_job_identity": EXTERNAL_JOB_IDENTITY,
"child_task_id": CHILD_TASK_ID,
"supersession_ordinal": SUPERSESSION_ORDINAL,
"canonical_projection_profile": CANONICAL_PROJECTION_PROFILE,
"canonical_projection_version": CANONICAL_PROJECTION_VERSION,
"supersession_reason": SUPERSESSION_REASON,
"owner_identity": OWNER_IDENTITY,
"policy_digest": self.policy.digest,
"one_time_use": True,
"issued_at": now,
"not_before": max(0, now - 5),
"expires_at": now + DECISION_TTL_SECONDS,
}
return self._sign(payload)
@@ -0,0 +1,190 @@
"""Staging-only server overlay for the fixed lineage Owner operation."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import sqlite3
import time
from typing import Any, Callable, Mapping
from aios_single_host_authority import DEFAULT_REGISTRY_PATH
from aios_single_host_operational_authority import (
OperationalMachineAuthority,
OperationalVerifier,
)
from aios_single_host_operational_authority_server import (
DEFAULT_LEDGER,
DEFAULT_MACHINE_KEY,
DEFAULT_OWNER_KEY,
DEFAULT_POLICY,
DurableIssuanceLedger,
OperationalAuthorityService as BaseOperationalAuthorityService,
_read_policy,
fail,
)
from aios_task_lineage_supersession_owner_authority import (
TASK_LINEAGE_SUPERSESSION_OPERATION,
TaskLineageSupersessionOwnerAuthority,
)
REQUEST_FIELDS = {
"source_expected_revision",
"source_task_identity_sha256",
"source_metadata_sha256",
}
class TaskLineageSupersessionIssuanceLedger(DurableIssuanceLedger):
"""Operation-scoped one-time fence layered on the existing durable ledger."""
def issue_once(
self,
*,
role: str,
operation: str,
request: Mapping[str, Any],
issuer: Callable[[], dict[str, Any]],
) -> dict[str, Any]:
request_sha = hashlib.sha256(
json.dumps(
request,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
).hexdigest()
connection = sqlite3.connect(self.path, timeout=5, isolation_level=None)
try:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"SELECT 1 FROM issuance WHERE role=? AND operation=? LIMIT 1",
(role, operation),
).fetchone()
if existing is not None:
connection.rollback()
fail("TASK_LINEAGE_SUPERSESSION_ALREADY_ISSUED")
result = issuer()
artifact = result.get("artifact")
nonce = artifact.get("payload", {}).get("nonce") if isinstance(artifact, Mapping) else None
if not isinstance(nonce, str) or not nonce:
connection.rollback()
fail("TASK_LINEAGE_SUPERSESSION_ISSUER_RESULT_INVALID")
encoded = json.dumps(
result,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
)
connection.execute(
"INSERT INTO issuance(role,operation,nonce,request_sha256,response_json,created_at) "
"VALUES(?,?,?,?,?,?)",
(role, operation, nonce, request_sha, encoded, int(time.time())),
)
connection.commit()
return result
except sqlite3.IntegrityError:
connection.rollback()
fail("ISSUANCE_LEDGER_RACE")
except Exception:
connection.rollback()
raise
finally:
connection.close()
class OperationalAuthorityService(BaseOperationalAuthorityService):
"""Preserve all existing operations and add one exact Owner-only operation."""
def __init__(self, *args: Any, token_hex=None, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.ledger = TaskLineageSupersessionIssuanceLedger(self.ledger.path)
self._token_hex = token_hex
def _components(self):
policy = _read_policy(
self.policy_path,
require_root_owner=self.require_root_owner,
)
verifier = OperationalVerifier(
self.registry_path,
require_root_owner=self.require_root_owner,
)
if self.role == "owner":
kwargs = {}
if self._token_hex is not None:
kwargs["token_hex"] = self._token_hex
signer = TaskLineageSupersessionOwnerAuthority(
self.owner_key_path,
policy,
require_root_owner=self.require_root_owner,
clock=self.clock,
**kwargs,
)
else:
signer = OperationalMachineAuthority(
self.machine_key_path,
policy,
require_root_owner=self.require_root_owner,
clock=self.clock,
)
return policy, verifier, signer
def handle(self, request: Mapping[str, Any], *, peer_uid: int) -> dict[str, Any]:
if peer_uid not in self.allowed_uids:
fail("PEER_UID_FORBIDDEN")
if not isinstance(request, Mapping) or set(request) != {"operation", "payload"}:
fail("REQUEST_INVALID")
if request.get("operation") != TASK_LINEAGE_SUPERSESSION_OPERATION:
return super().handle(request, peer_uid=peer_uid)
if self.role != "owner":
fail("MACHINE_OPERATION_FORBIDDEN")
payload = request.get("payload")
if not isinstance(payload, Mapping) or set(payload) != REQUEST_FIELDS:
fail("REQUEST_FIELDS_INVALID")
_policy, _verifier, signer = self._components()
def issue() -> dict[str, Any]:
artifact = signer.issue_task_lineage_supersession(
source_expected_revision=payload["source_expected_revision"],
source_task_identity_sha256=payload["source_task_identity_sha256"],
source_metadata_sha256=payload["source_metadata_sha256"],
)
return {"status": "PASS", "artifact": artifact.to_dict()}
return self.ledger.issue_once(
role=self.role,
operation=TASK_LINEAGE_SUPERSESSION_OPERATION,
request=payload,
issuer=issue,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--role", choices=("owner", "machine"), required=True)
parser.add_argument("--policy", default=str(DEFAULT_POLICY))
parser.add_argument("--registry", default=str(DEFAULT_REGISTRY_PATH))
parser.add_argument("--owner-key", default=str(DEFAULT_OWNER_KEY))
parser.add_argument("--machine-key", default=str(DEFAULT_MACHINE_KEY))
parser.add_argument("--socket")
parser.add_argument("--ledger", default=str(DEFAULT_LEDGER))
arguments = parser.parse_args()
service = OperationalAuthorityService(
role=arguments.role,
policy_path=Path(arguments.policy),
registry_path=Path(arguments.registry),
owner_key_path=Path(arguments.owner_key),
machine_key_path=Path(arguments.machine_key),
socket_path=Path(arguments.socket) if arguments.socket else None,
ledger_path=Path(arguments.ledger),
)
service.serve_forever()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -15,7 +15,7 @@ import struct
import subprocess
import time
from datetime import datetime, timezone
from dataclasses import dataclass
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any, Callable, Mapping, Protocol
@@ -133,6 +133,7 @@ class TrustedCandidateBuilderConfig:
recipes: Mapping[str, Mapping[str, str]]
governed_recipes: Mapping[str, Mapping[str, str]]
policy_sha256: str
source_admission_resolver: Callable[[str, str | None, str], Mapping[str, str]] | None = None
@classmethod
def from_policy(cls, policy: Mapping[str, Any]) -> "TrustedCandidateBuilderConfig":
@@ -227,14 +228,63 @@ class TrustedCandidateBuilderConfig:
return recipe
def resolve_recipe(self, version: str, candidate_id: str | None, source_commit: str) -> Mapping[str, str]:
recipe = self.fixed_recipe(version)
# Intent-only callers do not choose a candidate identity. The
# policy-owned recipe supplies it; explicit worker requests are
# still checked against the same immutable recipe.
effective_candidate_id = str(candidate_id or recipe.get("candidate_id") or "")
if {"version": recipe.get("version"), "candidate_id": recipe.get("candidate_id"), "source_commit": recipe.get("source_commit")} != {"version": version, "candidate_id": effective_candidate_id, "source_commit": source_commit}:
recipe = self.recipes.get(version)
if recipe is not None:
resolved = self.fixed_recipe(version)
# Fixed/governed-derived policy recipes remain immutable. A
# source admission can never override one of these identities.
effective_candidate_id = str(candidate_id or resolved.get("candidate_id") or "")
if {
"version": resolved.get("version"),
"candidate_id": resolved.get("candidate_id"),
"source_commit": resolved.get("source_commit"),
} != {
"version": version,
"candidate_id": effective_candidate_id,
"source_commit": source_commit,
}:
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
return resolved
resolver = self.source_admission_resolver
if resolver is None:
raise BuilderError("RECIPE_UNKNOWN")
try:
admitted = dict(resolver(version, candidate_id, source_commit))
except Exception as exc:
raise BuilderError("SOURCE_ADMISSION_REQUIRED") from exc
required = {
"mode",
"task_id",
"candidate_id",
"base_oid",
"source_commit",
"version",
"purpose",
}
if (
set(admitted) != required
or admitted.get("mode") != "governed-source-admission"
or admitted.get("version") != version
or admitted.get("source_commit") != source_commit
or not isinstance(admitted.get("candidate_id"), str)
or not admitted.get("candidate_id")
):
raise BuilderError("SOURCE_ADMISSION_INVALID")
effective_candidate_id = str(candidate_id or admitted["candidate_id"])
if effective_candidate_id != admitted["candidate_id"]:
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
return recipe
for name in ("base_oid", "source_commit"):
value = admitted.get(name)
if (
not isinstance(value, str)
or len(value) != 40
or any(char not in "0123456789abcdef" for char in value.lower())
):
raise BuilderError("SOURCE_ADMISSION_INVALID")
if admitted.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
raise BuilderError("SOURCE_ADMISSION_INVALID")
return admitted
def _invalid_governed_recipes() -> Mapping[str, Mapping[str, str]]:
@@ -776,6 +826,30 @@ def _main() -> None:
if not credentials:
raise SystemExit("CREDENTIALS_DIRECTORY is required")
keyring = BuilderKeyring.from_credentials_directory(Path(credentials))
source_admission_root = os.environ.get(
"AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT", ""
).strip()
if source_admission_root:
from release_source_admission import ReleaseSourceAdmissionStore
try:
source_admission_hmac = (
Path(credentials) / "source_admission_hmac"
).read_bytes().strip()
except OSError as exc:
raise BuilderError("SOURCE_ADMISSION_HMAC_UNAVAILABLE") from exc
source_store = ReleaseSourceAdmissionStore(
Path(source_admission_root),
repository=config.repository,
policy_sha256=config.policy_sha256,
hmac_key=source_admission_hmac,
expected_owner_uid=0,
writable=False,
)
config = replace(
config,
source_admission_resolver=source_store.resolve_recipe,
)
signer = CredentialPathEd25519Signer(
keyring.signing_private_key_path,
key_id=os.environ["AIOS_TRUSTED_BUILDER_KEY_ID"],
@@ -25,8 +25,17 @@ class ReconciliationController:
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):
left_value = actual.get(left)
right_value = actual.get(right)
expected_type = bool if left == "declared_production_state" else str
complete = (
left in actual
and right in actual
and isinstance(left_value, expected_type)
and isinstance(right_value, expected_type)
and (expected_type is bool or bool(left_value.strip()) and bool(right_value.strip()))
)
if not complete or left_value != right_value:
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)
@@ -0,0 +1,561 @@
#!/usr/bin/env python3
"""Governed admission of an exact remote Git commit into the release source closure.
The admitter is intentionally separate from the trusted candidate builder:
it may use a narrowly-scoped Gitea read/fetch credential, while the builder
remains PrivateNetwork=true and reads only the authoritative Git object store
plus immutable admission records.
An admission never moves ``main`` and never treats an arbitrary caller-supplied
OID as trusted. It binds:
remote branch ref -> exact full commit OID -> authoritative Git object/tree
-> current authoritative main ancestry -> immutable release recipe metadata.
"""
from __future__ import annotations
import argparse
import hashlib
import hmac
import json
import os
import re
import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping, Protocol
from authoritative_git import (
AuthoritativeGitError,
authoritative_head_oid,
authoritative_lineage,
run_authoritative_git,
)
from aios_gitea_machine_transport import GiteaMachineTransport, GiteaTransportError
from release_version import validate_release_version
SCHEMA = "aios.release-source-admission.v1"
RECIPE_MODE = "governed-source-admission"
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,199}$")
_BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,239}$")
_REF_PREFIX = "refs/heads/"
class ReleaseSourceAdmissionError(RuntimeError):
"""A stable fail-closed source-admission error code."""
def _canonical(value: Mapping[str, Any]) -> bytes:
return json.dumps(
dict(value),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
).encode("utf-8")
def _oid(value: object, field: str) -> str:
text = str(value or "").strip().lower()
if not _OID_RE.fullmatch(text):
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
return text
def _digest(value: object, field: str) -> str:
text = str(value or "").strip().lower()
if not _DIGEST_RE.fullmatch(text):
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
return text
def _identifier(value: object, field: str) -> str:
text = str(value or "").strip()
if not _IDENTIFIER_RE.fullmatch(text) or ".." in text or "\x00" in text:
raise ReleaseSourceAdmissionError(f"{field.upper()}_INVALID")
return text
def _source_ref(value: object) -> str:
text = str(value or "").strip()
if not text.startswith(_REF_PREFIX):
raise ReleaseSourceAdmissionError("SOURCE_REF_INVALID")
branch = text[len(_REF_PREFIX):]
if (
not _BRANCH_RE.fullmatch(branch)
or ".." in branch
or "//" in branch
or "@{" in branch
or branch.endswith(("/", ".", ".lock"))
or any(part in {"", ".", ".."} for part in branch.split("/"))
):
raise ReleaseSourceAdmissionError("SOURCE_REF_INVALID")
return text
def _version(value: object) -> str:
try:
return validate_release_version(value)
except (TypeError, ValueError) as exc:
raise ReleaseSourceAdmissionError("VERSION_INVALID") from exc
def _version_slug(version: str) -> str:
value = re.sub(r"[^A-Za-z0-9]+", "-", version.lower()).strip("-")
if not value:
raise ReleaseSourceAdmissionError("VERSION_INVALID")
return value[:80]
def _now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _secure_stat(path: Path, *, expected_owner_uid: int | None, directory: bool) -> None:
try:
info = path.lstat()
except OSError as exc:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNAVAILABLE") from exc
if path.is_symlink():
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
if directory and not path.is_dir():
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
if not directory and not path.is_file():
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNSAFE")
if expected_owner_uid is not None and info.st_uid != expected_owner_uid:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_OWNER_MISMATCH")
# Group/other write would allow an untrusted process to forge release input.
if info.st_mode & 0o022:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_PERMISSIONS_INVALID")
def _validate_record(
value: Mapping[str, Any],
*,
repository: str,
policy_sha256: str,
hmac_key: bytes,
) -> dict[str, Any]:
if not isinstance(value, Mapping):
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
required = {
"schema",
"admission_id",
"repository",
"task_id",
"version",
"candidate_id",
"base_oid",
"source_ref",
"source_commit",
"remote_ref_oid",
"tree_oid",
"purpose",
"policy_sha256",
"admitted_at",
"mac",
}
if set(value) != required or value.get("schema") != SCHEMA:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
record = dict(value)
if _identifier(record["repository"], "repository") != repository:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_REPOSITORY_MISMATCH")
_identifier(record["task_id"], "task_id")
record["version"] = _version(record["version"])
_identifier(record["candidate_id"], "candidate_id")
_identifier(record["purpose"], "purpose")
_identifier(record["admission_id"], "admission_id")
record["base_oid"] = _oid(record["base_oid"], "base_oid")
record["source_commit"] = _oid(record["source_commit"], "source_commit")
record["remote_ref_oid"] = _oid(record["remote_ref_oid"], "remote_ref_oid")
record["tree_oid"] = _oid(record["tree_oid"], "tree_oid")
record["source_ref"] = _source_ref(record["source_ref"])
record["policy_sha256"] = _digest(record["policy_sha256"], "policy_sha256")
if record["policy_sha256"] != policy_sha256:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_POLICY_MISMATCH")
if record["remote_ref_oid"] != record["source_commit"]:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_REMOTE_MISMATCH")
admitted_at = record.get("admitted_at")
if not isinstance(admitted_at, str) or not admitted_at.endswith("Z") or len(admitted_at) < 20:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_TIME_INVALID")
supplied_mac = record.get("mac")
if (
not isinstance(supplied_mac, str)
or not re.fullmatch(r"[0-9a-f]{64}", supplied_mac)
):
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_MAC_INVALID")
unsigned = {key: value for key, value in record.items() if key != "mac"}
expected_mac = hmac.new(hmac_key, _canonical(unsigned), hashlib.sha256).hexdigest()
if not hmac.compare_digest(supplied_mac, expected_mac):
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_MAC_INVALID")
return record
class ReleaseSourceBackend(Protocol):
def authoritative_main_oid(self) -> str: ...
def remote_ref_oid(self, source_ref: str) -> str: ...
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str: ...
def tree_oid(self, source_commit: str) -> str: ...
def require_lineage(self, base_oid: str, source_commit: str) -> None: ...
class ReleaseSourceAdmissionStore:
"""Durable immutable admission records keyed by exact source commit."""
def __init__(
self,
root: str | os.PathLike[str],
*,
repository: str,
policy_sha256: str,
hmac_key: bytes,
expected_owner_uid: int | None = None,
writable: bool = False,
) -> None:
self.root = Path(root)
self.repository = _identifier(repository, "repository")
self.policy_sha256 = _digest(policy_sha256, "policy_sha256")
if not isinstance(hmac_key, (bytes, bytearray)) or len(hmac_key) < 32:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_HMAC_INVALID")
self.hmac_key = bytes(hmac_key)
self.expected_owner_uid = expected_owner_uid
self.writable = bool(writable)
if self.writable and not self.root.exists():
self.root.mkdir(parents=True, mode=0o750)
if self.root.exists():
_secure_stat(
self.root,
expected_owner_uid=self.expected_owner_uid,
directory=True,
)
def _path(self, source_commit: str) -> Path:
return self.root / f"{_oid(source_commit, 'source_commit')}.json"
def register(self, record: Mapping[str, Any]) -> dict[str, Any]:
if not self.writable:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_READ_ONLY")
supplied = dict(record)
if "mac" not in supplied:
supplied["mac"] = hmac.new(
self.hmac_key,
_canonical(supplied),
hashlib.sha256,
).hexdigest()
checked = _validate_record(
supplied,
repository=self.repository,
policy_sha256=self.policy_sha256,
hmac_key=self.hmac_key,
)
path = self._path(checked["source_commit"])
encoded = _canonical(checked) + b"\n"
if path.exists():
_secure_stat(
path,
expected_owner_uid=self.expected_owner_uid,
directory=False,
)
try:
existing = path.read_bytes()
except OSError as exc:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_UNAVAILABLE") from exc
if existing == encoded:
return dict(checked)
try:
previous = _validate_record(
json.loads(existing),
repository=self.repository,
policy_sha256=self.policy_sha256,
hmac_key=self.hmac_key,
)
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID") from exc
comparable_previous = {
key: value
for key, value in previous.items()
if key not in {"admitted_at", "mac"}
}
comparable_checked = {
key: value
for key, value in checked.items()
if key not in {"admitted_at", "mac"}
}
if comparable_previous == comparable_checked:
return dict(previous)
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CONFLICT")
self.root.mkdir(parents=True, exist_ok=True, mode=0o750)
_secure_stat(
self.root,
expected_owner_uid=self.expected_owner_uid,
directory=True,
)
fd, raw_name = tempfile.mkstemp(
prefix=f".{checked['source_commit']}.",
suffix=".tmp",
dir=self.root,
)
temp_path = Path(raw_name)
try:
os.fchmod(fd, 0o640)
with os.fdopen(fd, "wb", closefd=True) as handle:
handle.write(encoded)
handle.flush()
os.fsync(handle.fileno())
os.replace(temp_path, path)
os.chmod(path, 0o640)
directory_fd = os.open(self.root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
except Exception:
try:
os.close(fd)
except OSError:
pass
temp_path.unlink(missing_ok=True)
raise
_secure_stat(
path,
expected_owner_uid=self.expected_owner_uid,
directory=False,
)
return dict(checked)
def lookup_source(self, source_commit: str) -> dict[str, Any] | None:
path = self._path(source_commit)
if not path.exists():
return None
_secure_stat(
path,
expected_owner_uid=self.expected_owner_uid,
directory=False,
)
try:
raw = path.read_bytes()
if len(raw) > 64 * 1024:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID")
decoded = json.loads(raw)
except ReleaseSourceAdmissionError:
raise
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_RECORD_INVALID") from exc
return _validate_record(
decoded,
repository=self.repository,
policy_sha256=self.policy_sha256,
hmac_key=self.hmac_key,
)
def resolve_recipe(
self,
version: str,
candidate_id: str | None,
source_commit: str,
) -> dict[str, str]:
record = self.lookup_source(source_commit)
if record is None:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_NOT_FOUND")
if record["version"] != version:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_VERSION_MISMATCH")
supplied_candidate = str(candidate_id or "")
if supplied_candidate and supplied_candidate != record["candidate_id"]:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CANDIDATE_MISMATCH")
return {
"mode": RECIPE_MODE,
"task_id": record["task_id"],
"candidate_id": record["candidate_id"],
"base_oid": record["base_oid"],
"source_commit": record["source_commit"],
"version": record["version"],
"purpose": record["purpose"],
}
@dataclass(slots=True)
class GiteaReleaseSourceBackend:
transport: GiteaMachineTransport
git_dir: Path
def authoritative_main_oid(self) -> str:
return authoritative_head_oid(git_dir=self.git_dir)
def remote_ref_oid(self, source_ref: str) -> str:
ref = _source_ref(source_ref)
branch = ref[len(_REF_PREFIX):]
value = self.transport.read_branch(branch)
if value is None:
raise ReleaseSourceAdmissionError("SOURCE_REF_NOT_FOUND")
return _oid(value, "remote_ref_oid")
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str:
ref = _source_ref(source_ref)
branch = ref[len(_REF_PREFIX):]
try:
value = self.transport.fetch_branch_exact(
branch,
_oid(source_commit, "source_commit"),
)
except GiteaTransportError as exc:
raise ReleaseSourceAdmissionError("SOURCE_FETCH_FAILED") from exc
return _oid(value, "fetched_source_commit")
def tree_oid(self, source_commit: str) -> str:
source = _oid(source_commit, "source_commit")
try:
result = run_authoritative_git(
["rev-parse", "--verify", f"{source}^{{tree}}"],
git_dir=self.git_dir,
)
except AuthoritativeGitError as exc:
raise ReleaseSourceAdmissionError("SOURCE_COMMIT_NOT_FOUND") from exc
return _oid(result.stdout.strip().lower(), "tree_oid")
def require_lineage(self, base_oid: str, source_commit: str) -> None:
try:
authoritative_lineage(
_oid(base_oid, "base_oid"),
_oid(source_commit, "source_commit"),
git_dir=self.git_dir,
)
except AuthoritativeGitError as exc:
raise ReleaseSourceAdmissionError("SOURCE_LINEAGE_MISMATCH") from exc
class ReleaseSourceAdmitter:
def __init__(
self,
store: ReleaseSourceAdmissionStore,
backend: ReleaseSourceBackend,
*,
purpose: str = "AIOS_TRUSTED_RELEASE_BUILDER",
) -> None:
if not store.writable:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_STORE_READ_ONLY")
self.store = store
self.backend = backend
self.purpose = _identifier(purpose, "purpose")
def admit(
self,
*,
version: str,
task_id: str,
source_ref: str,
source_commit: str,
) -> dict[str, Any]:
version_value = _version(version)
task_value = _identifier(task_id, "task_id")
ref_value = _source_ref(source_ref)
source_value = _oid(source_commit, "source_commit")
remote_before = self.backend.remote_ref_oid(ref_value)
if remote_before != source_value:
raise ReleaseSourceAdmissionError("SOURCE_REF_MOVED")
base_oid = _oid(self.backend.authoritative_main_oid(), "base_oid")
fetched = _oid(
self.backend.fetch_exact_ref(ref_value, source_value),
"fetched_source_commit",
)
if fetched != source_value:
raise ReleaseSourceAdmissionError("SOURCE_FETCH_MISMATCH")
remote_after = self.backend.remote_ref_oid(ref_value)
if remote_after != source_value:
raise ReleaseSourceAdmissionError("SOURCE_REF_MOVED")
self.backend.require_lineage(base_oid, source_value)
tree_oid = _oid(self.backend.tree_oid(source_value), "tree_oid")
candidate_id = f"rc-{_version_slug(version_value)}-{source_value[:12]}"
admission_id = "source-admission-" + hashlib.sha256(
f"{self.store.repository}\0{version_value}\0{source_value}\0{ref_value}".encode("utf-8")
).hexdigest()[:24]
record = {
"schema": SCHEMA,
"admission_id": admission_id,
"repository": self.store.repository,
"task_id": task_value,
"version": version_value,
"candidate_id": candidate_id,
"base_oid": base_oid,
"source_ref": ref_value,
"source_commit": source_value,
"remote_ref_oid": remote_after,
"tree_oid": tree_oid,
"purpose": self.purpose,
"policy_sha256": self.store.policy_sha256,
"admitted_at": _now(),
}
return self.store.register(record)
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
admit = subparsers.add_parser("admit")
admit.add_argument("--version", required=True)
admit.add_argument("--task-id", required=True)
admit.add_argument("--source-ref", required=True)
admit.add_argument("--source-commit", required=True)
admit.add_argument("--policy", required=True, type=Path)
admit.add_argument("--store-root", required=True, type=Path)
arguments = parser.parse_args()
if arguments.command != "admit":
raise SystemExit("unsupported command")
repository = os.environ.get("AIOS_GITOPS_REPOSITORY_IDENTITY", "")
api_base = os.environ.get("AIOS_GITEA_API_BASE", "")
remote_url = os.environ.get("AIOS_GITEA_REMOTE_URL", "")
raw_git_dir = os.environ.get("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "")
if not repository or not api_base or not remote_url or not raw_git_dir:
raise SystemExit("release source admission environment is incomplete")
git_dir = Path(raw_git_dir)
transport = GiteaMachineTransport(
api_base=api_base,
repository_id=repository,
remote_url=remote_url,
authority_git_dir=git_dir,
credential_name=os.environ.get(
"AIOS_GITEA_MACHINE_CREDENTIAL_NAME",
"gitea-machine-token",
),
)
credentials = os.environ.get("CREDENTIALS_DIRECTORY", "")
if not credentials:
raise SystemExit("CREDENTIALS_DIRECTORY is required")
try:
admission_hmac = (Path(credentials) / "source_admission_hmac").read_bytes().strip()
except OSError as exc:
raise SystemExit("source_admission_hmac credential is required") from exc
store = ReleaseSourceAdmissionStore(
arguments.store_root,
repository=repository,
policy_sha256=_sha256_file(arguments.policy),
hmac_key=admission_hmac,
expected_owner_uid=os.geteuid() if hasattr(os, "geteuid") else None,
writable=True,
)
result = ReleaseSourceAdmitter(
store,
GiteaReleaseSourceBackend(transport=transport, git_dir=git_dir),
).admit(
version=arguments.version,
task_id=arguments.task_id,
source_ref=arguments.source_ref,
source_commit=arguments.source_commit,
)
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
if __name__ == "__main__":
_main()
+57 -8
View File
@@ -3531,6 +3531,7 @@ class SilentReleaseActionService:
trusted_policy: TrustedReleasePolicy | None = None,
orchestrator: ReleaseOrchestrator | None = None,
builder_readiness_probe: Callable[[], Mapping[str, Any]] | None = None,
source_admission_resolver: Callable[[str], Mapping[str, Any] | None] | None = None,
allow_legacy_candidate_input: bool = False,
expected_repository: str = "",
policy_sha256: str = "",
@@ -3547,6 +3548,7 @@ class SilentReleaseActionService:
self.trusted_policy = trusted_policy
self.orchestrator = orchestrator
self.builder_readiness_probe = builder_readiness_probe
self.source_admission_resolver = source_admission_resolver
self.allow_legacy_candidate_input = bool(
allow_legacy_candidate_input
)
@@ -3638,6 +3640,40 @@ class SilentReleaseActionService:
version = str(payload.get("version") or "")
if version:
_validate_identifier("version", version)
source_admission: dict[str, Any] | None = None
resolver = self.source_admission_resolver
if resolver is not None:
try:
raw_admission = resolver(source_commit)
except Exception as exc:
raise CapabilityAuthorizationError(
"release source admission is invalid"
) from exc
if raw_admission is not None:
source_admission = dict(raw_admission)
admitted_source = str(
source_admission.get("source_commit") or ""
)
admitted_version = str(source_admission.get("version") or "")
admission_id = str(
source_admission.get("admission_id") or ""
)
if (
admitted_source != source_commit
or not admitted_version
or not admission_id
):
raise CapabilityAuthorizationError(
"release source admission is invalid"
)
_validate_identifier("version", admitted_version)
_validate_identifier("admission_id", admission_id)
if version and version != admitted_version:
raise CapabilityAuthorizationError(
"release source admission version mismatch"
)
version = admitted_version
intent_id = "intent-" + hashlib.sha256(
f"{event_id}\0{source_commit}".encode("utf-8")
).hexdigest()[:24]
@@ -3657,12 +3693,16 @@ class SilentReleaseActionService:
}
if version:
record["version"] = version
if source_admission is not None:
record["source_admission_id"] = source_admission["admission_id"]
if was_existing:
existing = json.loads(path.read_text(encoding="utf-8"))
if (
existing.get("event_id") != event_id
or existing.get("source_commit") != source_commit
or str(existing.get("version") or "") != version
or str(existing.get("source_admission_id") or "")
!= str(record.get("source_admission_id") or "")
):
raise SagaConflict(
"release intent id is bound to different evidence"
@@ -3742,14 +3782,23 @@ class SilentReleaseActionService:
)
_durable_json(path, record)
return self._intent_response(record, idempotent=False)
except CapabilityAuthorizationError:
record.update(
{
"failure_code": "TRUSTED_BUILDER_REJECTED",
"retryable": False,
"status": "BUILDER_TERMINAL_FAILURE",
}
)
except CapabilityAuthorizationError as exc:
if str(exc) == "release source admission required":
record.update(
{
"failure_code": "SOURCE_ADMISSION_REQUIRED",
"retryable": False,
"status": "SOURCE_ADMISSION_REQUIRED",
}
)
else:
record.update(
{
"failure_code": "TRUSTED_BUILDER_REJECTED",
"retryable": False,
"status": "BUILDER_TERMINAL_FAILURE",
}
)
_durable_json(path, record)
return self._intent_response(record, idempotent=False)
except (ConnectionError, OSError, TimeoutError):
+29 -16
View File
@@ -940,23 +940,29 @@ class TaskPlaneStore:
def heartbeat_claim(self, task_id: str, claim_id: str, lease_seconds: int) -> WorkerClaim:
now = _now()
lease_until = now + max(1, lease_seconds)
row = self.conn.execute(
"SELECT * FROM claims WHERE claim_id=? AND task_id=?",
(claim_id, task_id),
).fetchone()
if row is None:
raise NotFoundError("claim")
claim = self._row_to_claim(row)
if claim.status != "ACTIVE":
raise ConflictError("claim not active")
if claim.lease_until < now:
raise LockedError("lease expired")
with self._transaction():
self.conn.execute(
"UPDATE claims SET lease_until=? WHERE claim_id=?",
(lease_until, claim_id),
row = self.conn.execute(
"SELECT * FROM claims WHERE claim_id=? AND task_id=?",
(claim_id, task_id),
).fetchone()
if row is None:
raise NotFoundError("claim")
claim = self._row_to_claim(row)
if claim.status != "ACTIVE":
raise ConflictError("claim not active")
if claim.lease_until <= now:
raise LockedError("lease expired")
if self.get_task(task_id).status in {"CANCELLED", "COMPLETED", "FAILED_FINAL"}:
raise ConflictError("task is terminal")
updated = self.conn.execute(
"""
UPDATE claims SET lease_until=?
WHERE claim_id=? AND task_id=? AND status='ACTIVE' AND lease_until>?
""",
(lease_until, claim_id, task_id, now),
)
if updated.rowcount != 1:
raise LockedError("claim changed during heartbeat")
self.append_event(task_id, "TASK_PROGRESS", {"claim_id": claim_id, "stage": "heartbeat"}, subject=f"task/{task_id}")
return self.get_claim(claim_id)
@@ -1064,7 +1070,14 @@ class TaskPlaneStore:
),
)
self._fault("recover.after_probe_before_state")
if probe.outcome == ProbeOutcome.APPLIED_VERIFIED:
current_task = self.get_task(task_id)
if current_task.status in {"CANCELLED", "COMPLETED", "FAILED_FINAL"}:
# Recovery records what happened; it cannot undo a terminal Owner
# decision or reopen a finished task. This check shares the state
# transaction with the claim close and the appended probe event.
task_status = current_task.status
claim_status = "CANCELLED" if current_task.status == "CANCELLED" else "CLOSED_BY_RECOVERY"
elif probe.outcome == ProbeOutcome.APPLIED_VERIFIED:
task_status = "COMPLETED"
claim_status = "COMPLETED"
elif probe.outcome == ProbeOutcome.CONFLICT:
@@ -0,0 +1,92 @@
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS task_lineage_supersessions (
source_task_id TEXT NOT NULL,
supersession_ordinal INTEGER NOT NULL CHECK (supersession_ordinal = 1),
child_task_id TEXT NOT NULL UNIQUE,
source_task_identity_sha256 TEXT NOT NULL,
source_metadata_sha256 TEXT NOT NULL,
owner_decision_sha256 TEXT NOT NULL UNIQUE,
decision_id TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
PRIMARY KEY (source_task_id, supersession_ordinal)
);
CREATE TABLE IF NOT EXISTS task_lineage_supersession_receipts (
decision_id TEXT PRIMARY KEY,
nonce TEXT NOT NULL UNIQUE,
source_task_id TEXT NOT NULL,
supersession_ordinal INTEGER NOT NULL,
child_task_id TEXT NOT NULL UNIQUE,
owner_decision_sha256 TEXT NOT NULL UNIQUE,
decision_json TEXT NOT NULL,
receipt_json TEXT NOT NULL,
consumed_at TEXT NOT NULL,
FOREIGN KEY (source_task_id, supersession_ordinal)
REFERENCES task_lineage_supersessions(source_task_id, supersession_ordinal)
);
CREATE TABLE IF NOT EXISTS task_claim_guard_bindings_v2 (
binding_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL UNIQUE,
source_task_id TEXT NOT NULL,
supersession_ordinal INTEGER NOT NULL,
expected_revision INTEGER NOT NULL CHECK (expected_revision >= 1),
metadata_sha256 TEXT NOT NULL,
required_capabilities_sha256 TEXT NOT NULL,
worker_id TEXT NOT NULL,
execution_digest TEXT NOT NULL,
repository TEXT NOT NULL,
target_branch TEXT NOT NULL,
generation INTEGER NOT NULL,
policy_json TEXT NOT NULL,
policy_sha256 TEXT NOT NULL,
binding_json TEXT NOT NULL,
binding_sha256 TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
FOREIGN KEY (source_task_id, supersession_ordinal)
REFERENCES task_lineage_supersessions(source_task_id, supersession_ordinal)
);
CREATE TABLE IF NOT EXISTS task_claim_guard_binding_receipts_v2 (
receipt_id TEXT PRIMARY KEY,
binding_id TEXT NOT NULL UNIQUE,
task_id TEXT NOT NULL UNIQUE,
receipt_json TEXT NOT NULL,
receipt_sha256 TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
FOREIGN KEY (binding_id) REFERENCES task_claim_guard_bindings_v2(binding_id)
);
CREATE TRIGGER IF NOT EXISTS task_lineage_supersessions_no_update
BEFORE UPDATE ON task_lineage_supersessions BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_lineage_supersessions_no_delete
BEFORE DELETE ON task_lineage_supersessions BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_lineage_supersession_receipts_no_update
BEFORE UPDATE ON task_lineage_supersession_receipts BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_RECEIPT_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_lineage_supersession_receipts_no_delete
BEFORE DELETE ON task_lineage_supersession_receipts BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_RECEIPT_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_claim_guard_bindings_v2_no_update
BEFORE UPDATE ON task_claim_guard_bindings_v2 BEGIN
SELECT RAISE(ABORT, 'TASK_CLAIM_GUARD_BINDING_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_claim_guard_bindings_v2_no_delete
BEFORE DELETE ON task_claim_guard_bindings_v2 BEGIN
SELECT RAISE(ABORT, 'TASK_CLAIM_GUARD_BINDING_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_claim_guard_binding_receipts_v2_no_update
BEFORE UPDATE ON task_claim_guard_binding_receipts_v2 BEGIN
SELECT RAISE(ABORT, 'TASK_CLAIM_GUARD_BINDING_RECEIPT_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_claim_guard_binding_receipts_v2_no_delete
BEFORE DELETE ON task_claim_guard_binding_receipts_v2 BEGIN
SELECT RAISE(ABORT, 'TASK_CLAIM_GUARD_BINDING_RECEIPT_APPEND_ONLY');
END;
@@ -0,0 +1,41 @@
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS task_lineage_supersession_initial_projections (
projection_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL UNIQUE,
source_task_id TEXT NOT NULL,
supersession_ordinal INTEGER NOT NULL CHECK (supersession_ordinal = 1),
intent_sha256 TEXT NOT NULL UNIQUE,
intent_json TEXT NOT NULL,
owner_decision_sha256 TEXT NOT NULL UNIQUE,
source_task_identity_sha256 TEXT NOT NULL,
source_metadata_sha256 TEXT NOT NULL,
source_business_execution_digest TEXT NOT NULL,
canonical_child_execution_digest TEXT NOT NULL UNIQUE,
external_job_id TEXT NOT NULL,
repository TEXT NOT NULL,
target_branch TEXT NOT NULL,
generation INTEGER NOT NULL,
adapter TEXT NOT NULL,
required_capability TEXT NOT NULL,
expected_revision INTEGER NOT NULL CHECK (expected_revision = 1),
applied_revision INTEGER NOT NULL CHECK (applied_revision = 2),
metadata_sha256 TEXT NOT NULL,
required_capabilities_sha256 TEXT NOT NULL,
projection_event_sha256 TEXT NOT NULL,
receipt_json TEXT NOT NULL,
receipt_sha256 TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
FOREIGN KEY (source_task_id, supersession_ordinal)
REFERENCES task_lineage_supersessions(source_task_id, supersession_ordinal)
);
CREATE TRIGGER IF NOT EXISTS task_lineage_supersession_initial_projections_no_update
BEFORE UPDATE ON task_lineage_supersession_initial_projections BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_INITIAL_PROJECTION_APPEND_ONLY');
END;
CREATE TRIGGER IF NOT EXISTS task_lineage_supersession_initial_projections_no_delete
BEFORE DELETE ON task_lineage_supersession_initial_projections BEGIN
SELECT RAISE(ABORT, 'TASK_LINEAGE_SUPERSESSION_INITIAL_PROJECTION_APPEND_ONLY');
END;
@@ -0,0 +1,17 @@
{
"schema": "aios.activation-checklist.v1",
"profile": "isolated-validation",
"production_activation_supported": false,
"status": "NOT_RUN",
"checks": [
{"id": "identity", "required": "Replace @RELEASE_ROOT@; record source, artifact and configuration digests separately; verify package bytes.", "status": "NOT_RUN"},
{"id": "authority", "required": "Bind an explicitly isolated TaskPlaneStore; record existing online Task/Job/Release write authorities; do not route online admission into this profile.", "status": "NOT_RUN"},
{"id": "delegation", "required": "Use installed trusted Owner public keys and an explicitly authorized signed Grant. Never generate a production trust root to satisfy a missing credential.", "status": "NOT_RUN"},
{"id": "credentials", "required": "Project encrypted credentials directly into each consuming unit; preserve encrypted credential names; check permissions and independent restart/rotation.", "status": "NOT_RUN"},
{"id": "privileges", "required": "Create separate service and worker users; grant only read-only status access to the registered systemd service; no SSH, root shell or global write.", "status": "NOT_RUN"},
{"id": "configuration", "required": "Use loopback server URL, trusted fixed operation/component bindings and unique state directories. Client request bodies may not select tenant or credential paths.", "status": "NOT_RUN"},
{"id": "storage", "required": "Check directory owner, free space, SQLite WAL commits, durable receipts, recovery permissions and restore procedure on the actual storage device.", "status": "NOT_RUN"},
{"id": "nonrelease_loop", "required": "Record a real registered host service diagnostic through Action, Task claim, operation, receipt and result; unhealthy diagnostics remain observable failures of health.", "status": "NOT_RUN"},
{"id": "online_cutover", "required": "Separate later change: bind verified official Task authority adapter, release acceptance verifier, credential identities, approved installation and single-writer evidence before enabling online admission.", "status": "NOT_SUPPORTED_BY_THIS_PROFILE"}
]
}
+20
View File
@@ -0,0 +1,20 @@
# Validation 实例的凭证边界
本目录的 `.service.example` 是候选验证实例模板。它们不自动安装,不替换现有 Task 权威,也不支持 production profile。先按 `docs/implementation-baseline-1.1/INSTALLATION.md` 完成离线验证,并由安装者将 `@RELEASE_ROOT@` 替换为确定的、只读的发行目录,并将 `@PYTHON_EXECUTABLE@` 绑定到已验收的独立虚拟环境 Python 绝对路径。
服务端账户为 `aios-baseline`;Worker 账户为 `aios-baseline-worker`。Worker 不获得机器签名私钥、Owner 私钥、服务端 token 清单或服务端数据库读取权。Worker 的 transport token 仅映射到已注册 Worker 身份;它不能自行声明租户、用户或权限。
| 消费者 | 凭证名 | 用途 |
| --- | --- | --- |
| validation API | `aios-baseline-machine-key` | 已安装的短期执行证明和 operation receipt 签名身份 |
| validation API | `aios-baseline-user-token` | 验证配置中一位用户的 transport token;实际多用户要逐个绑定可信身份 |
| validation API | `aios-baseline-worker-token` | 验证 Worker 的 transport token |
| validation Worker | `aios-baseline-worker-token` | 仅用于连接本机 API |
每个 unit 使用自己的 `LoadCredentialEncrypted=` 从稳定的 `/etc/credstore.encrypted/` 来源投影。两个服务可以获得同一个受治理 token 的独立投影,但任何服务都不读取另一服务的 `/run/credentials/<unit>/`。加密凭证名称与 `LoadCredentialEncrypted` 左侧名称必须一致,避免更名导致解密身份校验失败。
JSON 中仅允许以 `${CREDENTIALS_DIRECTORY}/<name>` 引用自己的投影目录;不要写入 token、签名私钥或跨服务路径。Owner 公钥和 Runtime Contract 是受治理配置;Owner 私钥留在既有签发边界,不交给 API 或 Worker。凭证缺失时服务启动或操作准入必须失败,不能临时新建信任根或借用其他服务的运行目录。
本包不提供生成生产信任根的脚本。测试在临时目录生成测试专用密钥;它们不能用于真实部署。实际安装前还要确认当前主机 systemd 支持这些加固项和 encrypted credential,并对已渲染 unit 运行 `systemd-analyze verify`。本地模板静态检查不能代替实际服务重启、token 轮换及读取权限验收。
`service-status` 使用固定 `/usr/bin/systemctl show` 参数和可信 unit 映射,只读取注册组件状态。若目标主机限制状态读取,应对该命名 operation 配置最小读取权限;不要改成以 root 运行 API,也不要开放任意命令或 SSH。
@@ -0,0 +1,54 @@
[Unit]
Description=AIOS Baseline 1.1 isolated validation API (no production authority cutover)
After=local-fs.target
ConditionPathExists=@PYTHON_EXECUTABLE@
ConditionPathExists=/etc/aios-baseline/server.json
ConditionPathExists=@RELEASE_ROOT@/active-runtime/app/aios_baseline/cli.py
[Service]
Type=simple
User=aios-baseline
Group=aios-baseline
WorkingDirectory=@RELEASE_ROOT@
Environment=PYTHONPATH=@RELEASE_ROOT@/active-runtime/app
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=PATH=/usr/bin:/bin
LoadCredentialEncrypted=aios-baseline-machine-key:/etc/credstore.encrypted/aios-baseline-machine-key
LoadCredentialEncrypted=aios-baseline-user-token:/etc/credstore.encrypted/aios-baseline-user-token
LoadCredentialEncrypted=aios-baseline-worker-token:/etc/credstore.encrypted/aios-baseline-worker-token
ExecStart=@PYTHON_EXECUTABLE@ -B -m aios_baseline.cli server --config /etc/aios-baseline/server.json
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
KillMode=control-group
UMask=0077
StateDirectory=aios-baseline
StateDirectoryMode=0700
ReadWritePaths=/var/lib/aios-baseline
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
RestrictSUIDSGID=yes
LockPersonality=yes
SystemCallArchitectures=native
RestrictRealtime=yes
TasksMax=64
MemoryMax=512M
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,54 @@
[Unit]
Description=AIOS Baseline 1.1 isolated validation worker (registered transport identity)
After=aios-baseline-validation.service
Requires=aios-baseline-validation.service
ConditionPathExists=@PYTHON_EXECUTABLE@
ConditionPathExists=/etc/aios-baseline/worker.json
ConditionPathExists=@RELEASE_ROOT@/active-runtime/app/aios_baseline/cli.py
[Service]
Type=simple
User=aios-baseline-worker
Group=aios-baseline-worker
WorkingDirectory=@RELEASE_ROOT@
Environment=PYTHONPATH=@RELEASE_ROOT@/active-runtime/app
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=PATH=/usr/bin:/bin
LoadCredentialEncrypted=aios-baseline-worker-token:/etc/credstore.encrypted/aios-baseline-worker-token
ExecStart=@PYTHON_EXECUTABLE@ -B -m aios_baseline.cli worker --config /etc/aios-baseline/worker.json
Restart=on-failure
RestartSec=5s
TimeoutStopSec=15s
KillMode=control-group
UMask=0077
StateDirectory=aios-baseline-worker
StateDirectoryMode=0700
ReadWritePaths=/var/lib/aios-baseline-worker
InaccessiblePaths=-/var/lib/aios-baseline -/etc/aios-baseline/server.json
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=localhost
RestrictSUIDSGID=yes
LockPersonality=yes
SystemCallArchitectures=native
RestrictRealtime=yes
TasksMax=32
MemoryMax=256M
[Install]
WantedBy=multi-user.target
+115
View File
@@ -0,0 +1,115 @@
{
"contracts": [
{
"bindings": {},
"component_id": "sample",
"contract_revision": 1,
"contract_schema": "aios.runtime-contract.v1",
"credential_refs": {},
"entrypoints": {
"status": {
"component_id": "sample",
"environment": "staging",
"resolver_ref": "service://sample/status"
}
},
"environment": "staging",
"lifecycle": "active",
"operations": [
"service-status"
],
"resource_refs": {},
"tenant_scope": "validation"
}
],
"host_bindings": {
"validation:staging:sample": "REPLACE_WITH_EXISTING_SERVICE.service"
},
"identities": [
{
"role": "owner",
"subject_id": "owner",
"tenant_id": "validation",
"token_file": "${CREDENTIALS_DIRECTORY}/aios-baseline-user-token"
},
{
"role": "worker",
"subject_id": "machine-worker",
"tenant_id": "validation",
"token_file": "${CREDENTIALS_DIRECTORY}/aios-baseline-worker-token",
"worker_id": "diagnostic-worker"
}
],
"listen": {
"host": "127.0.0.1",
"port": 8769
},
"machine_key_file": "${CREDENTIALS_DIRECTORY}/aios-baseline-machine-key",
"machine_key_id": "validation-machine",
"operations": [
{
"effect_class": "READ_ONLY",
"handler_ref": "host.service_status",
"input_schema": "aios.operation.service-status.input.v1",
"operation_id": "service-status",
"owner_boundary": {
"mode": "WITHIN_GRANT"
},
"readback": {
"mode": "NONE",
"required": false
},
"reconcile": {
"mode": "NONE"
},
"required_permissions": [
"service-status"
],
"retry": {
"max_attempts": 2,
"mode": "SAFE"
},
"rollback": {
"mode": "NONE"
},
"timeout_seconds": 10,
"version": 1
}
],
"owner_key_scopes": {
"validation-owner": {
"subject_ids": [
"owner"
],
"tenant_ids": [
"validation"
]
}
},
"owner_public_key_files": {
"validation-owner": "/etc/aios-baseline/owner-public.pem"
},
"profile": "isolated-validation",
"schema": "aios.baseline-server-config.v1",
"state_dir": "/var/lib/aios-baseline",
"workers": [
{
"capabilities": [
"service-status"
],
"fallback_executor": false,
"heartbeat_seconds": 15,
"identity": "machine-worker",
"max_concurrency": 1,
"priority": 10,
"subject_scope": [
"owner"
],
"tenant_scope": [
"validation"
],
"worker_id": "diagnostic-worker"
}
],
"receipt_public_key_files": {}
}
+6
View File
@@ -0,0 +1,6 @@
{
"base_url": "http://127.0.0.1:8769",
"poll_seconds": 2,
"schema": "aios.baseline-worker-config.v1",
"token_file": "${CREDENTIALS_DIRECTORY}/aios-baseline-worker-token"
}
@@ -11,9 +11,10 @@ WorkingDirectory=@RUNTIME_ROOT@
Environment=AIOS_BRIDGE_SCHEMA_VERSION=aios.chatgpt-gitops-bridge.v2
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
Environment=AIOS_P0_TASK_PRINCIPAL_WRITES_ENABLED=true
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITEA_TOKEN_FILE=/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token
Environment=AIOS_GITEA_TOKEN_FILE=%d/gitea-governance-executor-token
Environment=AIOS_GITEA_REMOTE=gitea-http
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops/model-api-staging/baseline/current.json
Environment=AIOS_CONTROL_PLANE_TARGET_VERSION=V2R2C1
@@ -21,7 +22,7 @@ Environment=AIOS_CONTROL_PLANE_TARGET_OID=@SOURCE_OID@
Environment=AIOS_CONTROL_PLANE_ARTIFACT_SHA256=@ARTIFACT_SHA256@
Environment=PYTHONPATH=@RUNTIME_ROOT@:@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
Environment=AIOS_DEPLOYMENT_STATE=STAGING
LoadCredential=gitea_token:/run/credentials/aios-governance-executor.service/gitea-governance-executor-token
LoadCredentialEncrypted=gitea-governance-executor-token:/etc/credstore.encrypted/gitea-governance-executor-token
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/bridge-v2-staging
ExecStartPre=/usr/bin/setfacl -m u:aiman:--x /usr/local/libexec
ExecStart=/usr/bin/python3 -B @RUNTIME_ROOT@/app/aios_chatgpt_bridge_v2_server.py
@@ -7,8 +7,10 @@ Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_SOCKET=/run/aios-trusted-candidate-bu
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_POLICY=/opt/aios/gitops-model-api/<VERIFIED_PACKAGE_OID>/runtime/config/gitops/trusted-candidate-builder-policy.json
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_PUBLIC_KEYRING=/etc/aios-gitops/trusted-builder/public-keyring.json
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_CREDENTIALS_DIRECTORY=%d
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
Environment=AIOS_SILENT_RELEASE_TRUSTED_POLICY=/opt/aios/gitops-model-api/<VERIFIED_PACKAGE_OID>/runtime/config/gitops/trusted-release-policy.json
LoadCredential=builder_hmac:/etc/aios-gitops/credential-references/trusted-candidate-builder-hmac
LoadCredential=source_admission_hmac:/etc/aios-gitops/credential-references/release-source-admission-hmac
# The credential source above is a reference only; no secret value belongs here.
# Keep execution shadow-disabled and do not add Owner/deploy/tag/baseline switches.
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
@@ -27,6 +27,7 @@ Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18893
@@ -44,7 +45,7 @@ ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline -/var/lib/aios-release-source-admission
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
[Install]
@@ -0,0 +1,41 @@
[Unit]
Description=AIOS governed release source admission
After=network-online.target aios-gitea.service
Wants=network-online.target
[Service]
Type=oneshot
User=root
Group=gitops
UMask=0027
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITEA_REMOTE_URL=https://g.cnnetmkt.top/git/aios/aios.git
Environment=AIOS_GITEA_MACHINE_CREDENTIAL_NAME=gitea-machine-token
EnvironmentFile=/run/aios-release-source-admission/request.env
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
ExecStart=/usr/bin/python3 -B /opt/aios/release-source-admission/current/app/release_source_admission.py admit --version ${AIOS_RELEASE_VERSION} --task-id ${AIOS_RELEASE_TASK_ID} --source-ref ${AIOS_RELEASE_SOURCE_REF} --source-commit ${AIOS_RELEASE_SOURCE_COMMIT} --policy /opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json --store-root /var/lib/aios-release-source-admission
ExecStartPost=/usr/bin/rm -f /run/aios-release-source-admission/request.env
RuntimeDirectory=aios-release-source-admission
RuntimeDirectoryMode=0750
StateDirectory=aios-release-source-admission
StateDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=
ReadOnlyPaths=/opt/aios/release-source-admission /opt/aios/trusted-candidate-builder /etc/aios/credentials /etc/credstore.encrypted
ReadWritePaths=/data/aios-gitops/repo.git /var/lib/aios-release-source-admission /run/aios-release-source-admission
@@ -10,10 +10,12 @@ Group=root
UMask=0077
LoadCredentialEncrypted=signing_private_key:/etc/credstore.encrypted/aios-trusted-candidate-builder/signing_private_key
LoadCredentialEncrypted=builder_hmac:/etc/credstore.encrypted/aios-trusted-candidate-builder/builder_hmac
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
Environment=AIOS_TRUSTED_BUILDER_POLICY=/opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json
Environment=AIOS_TRUSTED_BUILDER_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_TRUSTED_BUILDER_EVIDENCE_ROOT=/var/lib/aios-trusted-candidate-builder/evidence
Environment=AIOS_TRUSTED_BUILDER_NONCE_LEDGER=/var/lib/aios-trusted-candidate-builder/nonce-ledger.json
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
Environment=AIOS_TRUSTED_BUILDER_KEY_ID=builder-attestation-ed25519-g1
Environment=AIOS_TRUSTED_BUILDER_KEY_GENERATION=1
Environment=PYTHONDONTWRITEBYTECODE=1
@@ -37,7 +39,7 @@ MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
CapabilityBoundingSet=
ReadOnlyPaths=/opt/aios/trusted-candidate-builder /data/aios-gitops/repo.git /var/lib/aios-trusted-candidate-builder/evidence
ReadOnlyPaths=/opt/aios/trusted-candidate-builder /data/aios-gitops/repo.git /var/lib/aios-trusted-candidate-builder/evidence -/var/lib/aios-release-source-admission
ReadWritePaths=/var/lib/aios-trusted-candidate-builder
[Install]
@@ -0,0 +1,76 @@
# ADR: Owner-final governed merge authorization v2
Date: 2026-09-07
Status: Accepted for the P0 Owner-final authorization repair
## Context
AIOS is a single-Owner system. A signer, verifier, required check, branch
protection rule, or merge executor may prove or execute an Owner decision, but
must not become an independent policy authority that can permanently override
an exact, valid Owner authorization.
The legacy approval seam also names the proof implementation as the Owner
identity. That conflates the policy subject with a key or adapter and makes a
default signer outage look like a higher-level veto.
## Decision
Introduce the versioned `aios.owner-merge-approval.v2` artifact and make it
the policy input to a governed merge admission. Its canonical payload binds:
- repository, pull-request number and exact pull-request head OID;
- the expected `main` OID used as the CAS base;
- `merge.pull-request` as the only operation;
- required-check contract digest, policy version/digest and audit lineage
digest;
- generated approval ID and nonce, issued/not-before/expiry times; and
- a distinct Owner policy identity plus the separate proof key identity.
The structured Owner authority creates the complete canonical payload from an
immutable merge policy. It accepts no arbitrary caller bytes and has no
generic signing operation. Changing PR head, CAS base, policy, repository,
operation, or approval expiry invalidates the approval; a new policy-bound
approval is required.
An issuance ledger replays the same still-valid artifact for idempotent client
delivery, retains expired artifacts for audit, and issues a fresh approval
with a new nonce after expiry. It must never turn an expired approval into a
permanent signing deadlock.
Proof verification is public-key based. A recovery verifier validates an
already-issued v2 approval against the registered Owner root without calling
the default signing service. It is not a second policy principal and cannot
mint, broaden, or replace an Owner approval. Absence of every trusted proof
fails closed.
## Governing merge admission
The admission service consumes a valid approval only after independent live
technical evidence satisfies the exact required-check contract. It then
durably claims the approval nonce before requesting the protected merge. A
duplicate or uncertain nonce is never retried as another side effect. The
executor remains responsible for branch protection and CAS; its result is
accepted only after Authority, Gitea, and repository API OID reconciliation.
Owner approval never writes a check result and never converts failed CI,
security, provenance, repository-boundary, or OID-integrity evidence into
PASS. Failure of those gates remains fail-closed. A post-side-effect
reconciliation failure is recorded as unknown/reconciliation-required, not
reported as a successful merge or blindly retried.
## Consequences
The model distinguishes three roles:
```text
Owner approval decides whether this exact merge is allowed
signer/verifier proves that exact Owner decision and its binding
technical gates/CAS establish whether the merge can safely execute
merge executor performs the protected operation and records outcome
```
The v2 contract is additive and versioned. It does not reinterpret legacy
approvals or relax existing branch protection, CI, security, provenance,
tenant-boundary, nonce, expiry, audit, or reconciliation controls.
@@ -0,0 +1,79 @@
# ADR: Bounded Immutable Repository Discovery
- Status: Implemented locally; pending governed publication and live runtime
deployment
- Date: 2026-09-02
- Scope: AIOS Repository Discovery P0
## Context
Repository discovery was needed to inspect unknown paths and compare exact
repository snapshots, while the ordinary SSH route was not an authenticated
execution plane. The discovery surface must not turn a readable runtime into a
general-purpose shell, grep, checkout, or Git writer. It also must remain safe
when a branch or tag moves between requests and when a diff is large.
## Decision
Implement a repository-scoped, read-only module behind the formal Bridge and
Actions read routes:
1. Verify the exact `aios/aios` identity before every request.
2. Resolve `HEAD`, branch, tag, or full commit input to one full commit OID;
reject ambiguous short names and revision expressions.
3. Invoke only fixed read-only Git subcommands (`rev-parse`, `ls-tree`,
`cat-file`, and `diff`) with `shell=False`, disabled prompts, and bounded
output. Stream NUL-delimited tree and diff records rather than collecting a
complete tree or patch.
4. Enforce normalized relative paths, repository allowlisting, depth/page/
match/byte limits, binary-source exclusion, and no patch response.
5. Sign pagination cursors with an HMAC over the operation, repository,
resolved OID, complete normalized query, and offset. A cursor for another
snapshot or query is invalid.
6. Return resolved commit identity for every read and blob identity for file
reads. Keep the four operations read-only and non-consequential at the
Bridge contract boundary.
7. Keep the mutating implementation workflow separate: Task and TaskPrincipal
admission, claim/heartbeat, WriterLease, CAS, and `commitFilesGoverned` are
still mandatory for source changes.
The canonical runtime paths are `/v2/repository/*`; the Actions adapter maps
the same contract to `/api/repository/*`. The OpenAPI document records the
mapping without copying a Path Item or duplicating operation IDs.
## Alternatives rejected
- Retrying arbitrary SSH credentials or inventing a new network profile would
bypass the supported execution-plane boundary.
- User-controlled `grep`, shell commands, checkout paths, or direct bare-repo
reads would make the repository and host boundary non-deterministic.
- Reusing the write route would introduce unnecessary Task/lease/credential
authority into a read-only request and make a read endpoint a write probe.
- Returning a full patch or collecting all Git output before pagination would
violate the bounded-output requirement for large repositories.
## Consequences
The API callers must carry the returned OID/cursor and must not assume that a
floating ref remains unchanged. Search may return fewer results when its byte
bound is exhausted, and a diff is an index rather than a patch. Cursor secret
configuration becomes a required runtime dependency for paginated operations.
The isolated implementation is testable against a disposable bare repository.
Windows can compile and run the portable discovery tests, but importing the
full Bridge/Actions runtime requires POSIX `fcntl`; Linux HTTP and locking
integration remains a separate verification item.
At the time of this decision, live HTTPS authority/runtime readback was
available, but the live OpenAPI/capability surface did not advertise these four
discovery operations and no authorized P0 Task envelope was present in the
execution context. Therefore this ADR does not claim a governed commit,
remote publication, or deployed runtime.
## Verification gate
This decision is complete for local implementation only after Python compile,
discovery unit/large-diff tests, and Bridge/OpenAPI/Actions drift tests pass.
It becomes live only after a formal Task, TaskPrincipal, lease, governed
commit, and independent authority/remote/API OID readback prove the exact same
commit, with all frozen write and staging invariants unchanged.
@@ -0,0 +1,102 @@
# ADR: P0-0 Task Admission Bootstrap without weakening TaskPrincipal
Date: 2026-09-03
Status: Candidate for semantic port
## Context
The live control plane can expose governed writer operations while still being
unable to create the Agent Task/TaskLease required by TaskPrincipal. This is a
bootstrap deadlock, not a reason to enable global repository writes.
The safety invariant is:
```text
Owner / durable delegation
-> Task admission grant
-> real Agent Task
-> claim + heartbeat + TaskLease
-> TaskPrincipal
-> WriterLease
-> governed commit
```
`GovernedTaskBootstrap.create()` may create a GitOps binding/Job. It is not, by
itself, evidence of an Agent Task claim or TaskLease.
## Decision
Introduce a narrow Task Admission Bootstrap substrate with three operations:
- `getTaskAdmissionEnvelope`
- `submitTaskAdmissionAuthorization`
- `getTaskAdmissionStatus`
The envelope normalizes and binds repository, source OID, target branch,
generation, execution digest, capabilities, file allowlist, risk, policy digest,
expiry and nonce.
Authorization is accepted only from:
1. an already-verified `OwnerDelegationEpoch`; or
2. an Owner signature over the exact canonical envelope.
There is deliberately no permissive default authorization backend.
The derived `TaskAdmissionGrant` can only narrow the parent authorization.
Production, tag, baseline, main, global-write, force-push, runtime-anchor and
arbitrary-shell capabilities are explicitly rejected.
The grant is passed to the existing Agent Task control plane via an injected
`TaskAdmissionPort`. This component never writes the Task database.
The production adapter accepts only a service-managed official Task transport;
it never returns its control credential. It first reconciles the exact task
identity, then POSTs the official `/v1/tasks` contract only if absent, and
reconciles an ambiguous response before reporting `UNKNOWN`. Task receipts use
actual `QUEUED`, `CLAIMED`, or `RUNNING` state, never a synthetic ready state.
Durable epochs are resolved through an injected provider that receives the
authenticated caller identity from trusted middleware. A body-supplied epoch
identifier is only a lookup key. The existing durable preparation authorization
does not become file-write or TaskPrincipal authority.
TaskPrincipal is not used to bootstrap the Task. It remains downstream of a
real claim/TaskLease.
## Crash / replay semantics
Envelope creation is idempotent by exact request digest.
Authorization/admission is durable and exact replay returns the same Task
receipt. A SQLite `BEGIN IMMEDIATE` claim persists the immutable grant and its
authorization-proof digest before the Task side effect. Cross-instance callers
observe the durable envelope winner; a different proof cannot replace the
stored grant. The live Task API retains the second exact-identity boundary.
A crash after authorization but before Task API admission must resume the same
envelope/grant identity. It must not create a replacement task or widen scope.
## Integration rule for current live authority
The source snapshot used for this candidate is not proven byte-identical to the
2026-09-03 live authority. Therefore the code is a semantic-port candidate.
The live integration must:
- preserve existing TaskPrincipal verification;
- adapt `TaskAdmissionPort` to the actual Agent Task API;
- adapt `OwnerAuthorizationBackend` to the actual trusted Owner/delegation
verifier;
- expose admission routes outside the TaskPrincipal gate, because the
cryptographic/delegation proof is the bootstrap authority;
- never turn the runtime anchor into a writer;
- never use Gitea/SSH/downstream credentials as Owner authority;
- publish OpenAPI/capability metadata only when handlers are actually wired.
## Consequences
The control plane can become unattended after one durable Owner delegation
epoch, while retaining fail-closed boundaries. If no valid delegation exists,
the only legitimate stop is an already-generated canonical signing envelope
awaiting the Owner's cryptographic signature.
@@ -0,0 +1,38 @@
# ADR: Owner Scoped Task Lineage Supersession P0
## 决策
`TASK-SINGLEHOST-LINEAGE-SUPERSESSION-P0-20260905` 新增一个只服务于
`TASK-SINGLEHOST-TRUSTROOT-P0-20260904` 到
`TASK-SINGLEHOST-TRUSTROOT-P0-20260904-S1` 的控制面 capability。
它创建 append-only child lineage;绝不重写 source Task、metadata、revision、attempt、claim 或 lease。
## 安全边界
请求只携带 Owner decision 和 source 身份断言。控制面复用正式 Task identity helper
与现有 Owner verifier,严格校验 namespace、principal、当前 Owner key、短 TTL、nonce、
decision ID、exact-CAS 字段以及唯一 child ID/ordinal。未知字段、任意 child payload、通配
Task/repository/branch、旧 signer 或 replay 均 fail-closed。
child payload 由 canonical initial-execution profile 在服务端构造:`QUEUED`、revision 1、
attempt 0、无 claim/lease、空初始 capabilities 和 pristine metadata。child 普通 event 链
只允许 `TASK_CREATED`;lineage/decision/receipt 审计必须写入独立 append-only 表。
## 一致性和重放
单一 `BEGIN IMMEDIATE` 事务重读 source exact-CAS、检查 source+ordinal/child/decision/nonce
唯一性、插入 child、lineage、decision 和 receipt,并整体提交。任一步失败必须回滚,不能
留下半创建 child。只有 decision digest 完全相同的请求可返回同一 receipt;相同唯一键而
digest 不同必须拒绝。append-only 表禁止 UPDATE 和 DELETE。
## ClaimGuard
source 的 frozen ClaimGuard artifact 永不修改。child 的 Task identity 和 projected metadata
identity 必然不同,必须使用新建的版本化、append-only child binding/receipt,精确绑定
child task、projected revision/metadata/capabilities、worker、execution digest、repository、
branch、generation 和 external job。dispatch 只识别 source 与 exact child;缺失或重复 binding
一律拒绝。不得使用 latest、通配符或 caller 指定路径。
## 验收
live invocation 前必须在 isolated SQLite 完成 source exact state 到 canonical projection、
+171
View File
@@ -240,11 +240,177 @@ components:
additionalProperties: false
properties:
candidate_id: {type: string}
RepositoryTreeEntry:
type: object
required: [path, type, object_oid]
properties:
path: {type: string, minLength: 1, maxLength: 512}
type: {type: string, enum: [blob, tree, commit]}
object_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
mode: {type: string}
RepositoryTreeResponse:
type: object
required: [repository, resolved_oid, entries, truncated, next_cursor]
properties:
repository: {type: string, const: aios/aios}
resolved_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
entries: {type: array, items: {$ref: '#/components/schemas/RepositoryTreeEntry'}}
truncated: {type: boolean}
next_cursor: {type: string, nullable: true, maxLength: 4096}
RepositorySearchMatch:
type: object
required: [path, line_start, line_end, blob_oid, snippet]
properties:
path: {type: string, minLength: 1, maxLength: 512}
line_start: {type: integer, minimum: 1}
line_end: {type: integer, minimum: 1}
blob_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
snippet: {type: string, maxLength: 512}
RepositorySearchResponse:
type: object
required: [repository, resolved_oid, matches, scanned_bytes, max_bytes, truncated, next_cursor]
properties:
repository: {type: string, const: aios/aios}
resolved_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
matches: {type: array, items: {$ref: '#/components/schemas/RepositorySearchMatch'}}
scanned_bytes: {type: integer, minimum: 0}
max_bytes: {type: integer, minimum: 1, maximum: 1048576}
binary_blobs_skipped: {type: integer, minimum: 0}
truncated: {type: boolean}
next_cursor: {type: string, nullable: true, maxLength: 4096}
RepositoryDiffEntry:
type: object
required: [path, status, additions, deletions, old_path]
properties:
path: {type: string, minLength: 1, maxLength: 512}
status: {type: string, pattern: '^[ACDMRTUX][0-9]*$'}
additions: {type: integer, nullable: true, minimum: 0}
deletions: {type: integer, nullable: true, minimum: 0}
old_path: {type: string, nullable: true, maxLength: 512}
RepositoryDiffIndexResponse:
type: object
required: [repository, resolved_from_oid, resolved_to_oid, entries, truncated, next_cursor]
properties:
repository: {type: string, const: aios/aios}
resolved_from_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
resolved_to_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
entries: {type: array, items: {$ref: '#/components/schemas/RepositoryDiffEntry'}}
truncated: {type: boolean}
next_cursor: {type: string, nullable: true, maxLength: 4096}
RepositoryFileResponse:
type: object
required: [repository, resolved_oid, blob_oid, path, content, content_base64, content_bytes, truncated]
properties:
repository: {type: string, const: aios/aios}
resolved_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
blob_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
path: {type: string, minLength: 1, maxLength: 512}
content: {type: string}
content_base64: {type: string, format: byte}
content_bytes: {type: integer, minimum: 0}
truncated: {type: boolean}
paths:
/v2/repository/tree:
get:
operationId: listRepositoryTree
summary: List one bounded page of an immutable repository tree
security: []
x-openai-isConsequential: false
x-aios-read-only: true
x-aios-bounded: true
x-aios-repository-scoped: true
x-aios-no-shell: true
x-aios-legacy-path: /api/repository/tree
parameters:
- {in: query, name: repository, required: true, schema: {type: string, const: aios/aios}}
- {in: query, name: ref, required: true, schema: {type: string, minLength: 1, maxLength: 240}}
- {in: query, name: prefix, schema: {type: string, maxLength: 512}}
- {in: query, name: depth, schema: {type: integer, minimum: 1, maximum: 32, default: 8}}
- {in: query, name: limit, required: true, schema: {type: integer, minimum: 1, maximum: 1000}}
- {in: query, name: cursor, schema: {type: string, maxLength: 4096}}
responses:
'200': {description: Immutable tree page, content: {application/json: {schema: {$ref: '#/components/schemas/RepositoryTreeResponse'}}}}
'400': {description: Invalid repository, ref, path, bounds, or cursor}
'403': {description: Repository identity is not allowed}
'404': {description: Repository ref or path not found}
'503': {description: Authoritative repository or discovery configuration unavailable}
/v2/repository/search:
get:
operationId: searchRepositorySource
summary: Search bounded source blobs at one immutable repository revision
security: []
x-openai-isConsequential: false
x-aios-read-only: true
x-aios-bounded: true
x-aios-repository-scoped: true
x-aios-no-shell: true
x-aios-legacy-path: /api/repository/search
parameters:
- {in: query, name: repository, required: true, schema: {type: string, const: aios/aios}}
- {in: query, name: ref, required: true, schema: {type: string, minLength: 1, maxLength: 240}}
- {in: query, name: query, required: true, schema: {type: string, minLength: 1, maxLength: 512}}
- {in: query, name: path_globs, style: form, explode: true, schema: {type: array, maxItems: 16, items: {type: string, maxLength: 512}}}
- {in: query, name: max_matches, required: true, schema: {type: integer, minimum: 1, maximum: 1000}}
- {in: query, name: max_bytes, required: true, schema: {type: integer, minimum: 1, maximum: 1048576}}
- {in: query, name: cursor, schema: {type: string, maxLength: 4096}}
responses:
'200': {description: Bounded source matches with blob identity, content: {application/json: {schema: {$ref: '#/components/schemas/RepositorySearchResponse'}}}}
'400': {description: Invalid repository, ref, query, path glob, bounds, or cursor}
'403': {description: Repository identity is not allowed}
'404': {description: Repository ref not found}
'503': {description: Authoritative repository or discovery configuration unavailable}
/v2/repository/diff-index:
get:
operationId: getDiffIndex
summary: List a bounded immutable diff index without returning a patch
security: []
x-openai-isConsequential: false
x-aios-read-only: true
x-aios-bounded: true
x-aios-repository-scoped: true
x-aios-no-shell: true
x-aios-legacy-path: /api/repository/diff-index
parameters:
- {in: query, name: repository, required: true, schema: {type: string, const: aios/aios}}
- {in: query, name: from, required: true, schema: {type: string, minLength: 1, maxLength: 240}}
- {in: query, name: to, required: true, schema: {type: string, minLength: 1, maxLength: 240}}
- {in: query, name: path_prefix, schema: {type: string, maxLength: 512}}
- {in: query, name: limit, required: true, schema: {type: integer, minimum: 1, maximum: 1000}}
- {in: query, name: cursor, schema: {type: string, maxLength: 4096}}
responses:
'200': {description: Bounded diff index with immutable endpoints, content: {application/json: {schema: {$ref: '#/components/schemas/RepositoryDiffIndexResponse'}}}}
'400': {description: Invalid repository, revision, path prefix, bounds, or cursor}
'403': {description: Repository identity is not allowed}
'404': {description: Repository revision not found}
'503': {description: Authoritative repository or discovery configuration unavailable}
/v2/repository/file:
get:
operationId: readRepositoryFile
summary: Read one bounded file from an immutable repository revision
security: []
x-openai-isConsequential: false
x-aios-read-only: true
x-aios-bounded: true
x-aios-repository-scoped: true
x-aios-no-shell: true
x-aios-legacy-path: /api/repository/file
parameters:
- {in: query, name: repository, required: true, schema: {type: string, const: aios/aios}}
- {in: query, name: ref, required: true, schema: {type: string, minLength: 1, maxLength: 240}}
- {in: query, name: path, required: true, schema: {type: string, minLength: 1, maxLength: 512}}
- {in: query, name: max_bytes, schema: {type: integer, minimum: 1, maximum: 1048576, default: 262144}}
responses:
'200': {description: Bounded file content with resolved commit and blob identity, content: {application/json: {schema: {$ref: '#/components/schemas/RepositoryFileResponse'}}}}
'400': {description: Invalid repository, ref, path, or bounds}
'403': {description: Repository identity is not allowed}
'404': {description: Repository ref or file not found}
'503': {description: Authoritative repository or discovery configuration unavailable}
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Submit one atomic governed commit containing multiple files
security: [{taskPrincipalAuth: []}]
x-aios-task-principal: Bridge resolves a fresh server-side TaskPrincipal from the authoritative admitted+claimed Task and re-verifies its Owner epoch, then checks exact task/job/repository/branch/generation/capability binding; caller JSON cannot provide a principal and missing verification is denied. An explicitly injected legacy bearer verifier may still be used by a trusted host.
requestBody:
required: true
content:
@@ -254,6 +420,7 @@ paths:
additionalProperties: false
required:
- task_id
- job_id
- repository
- expected_branch_oid
- target_branch
@@ -283,8 +450,12 @@ paths:
message: {type: string, minLength: 1, maxLength: 200}
responses:
'200': {description: Governed commit job and verified branch result}
'202': {description: Existing external Job alias requires authoritative reconciliation; no replacement Job is created}
'401': {description: No verified Task principal is available}
'403': {description: Verified Task principal lacks exact task, external Job, or capability scope}
'400': {description: Contract or policy rejection}
'409': {description: Branch CAS or idempotency conflict}
'423': {description: Narrow P0 Task-principal mutation gate is frozen; assertion is not consumed}
/createReleaseIntent:
post:
operationId: createReleaseIntent
@@ -0,0 +1,109 @@
# AIOS Task Admission Bootstrap OpenAPI fragment.
# Integrate into the canonical Bridge OpenAPI only when the corresponding live
# handlers and authorization backend are configured. This fragment does not
# enable legacy repository mutation or change TaskPrincipal/WriterLease gates.
components:
schemas:
TaskAdmissionEnvelopeRequest:
type: object
additionalProperties: false
required:
- idempotency_key
- task_id
- repository
- authority_base_oid
- target_branch
- generation
- execution_digest
- requested_capabilities
- file_allowlist
- risk
properties:
idempotency_key: {type: string, minLength: 8, maxLength: 256}
task_id: {type: string, minLength: 3, maxLength: 180}
repository: {type: string, enum: [aios/aios]}
authority_base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/.+'}
generation: {type: integer, minimum: 1}
execution_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
requested_capabilities:
type: array
minItems: 1
uniqueItems: true
items: {type: string}
file_allowlist:
type: array
minItems: 1
uniqueItems: true
items: {type: string}
risk: {type: string, enum: [low, medium]}
expires_in_seconds:
type: integer
minimum: 60
maximum: 900
TaskAdmissionAuthorizationRequest:
type: object
additionalProperties: false
required: [envelope_id, authorization_kind]
properties:
envelope_id: {type: string}
authorization_kind:
type: string
enum: [delegation_epoch, owner_signature]
delegation_epoch_id: {type: string}
owner_key_id: {type: string}
signature_base64: {type: string}
paths:
/v2/task-admission/envelope:
post:
operationId: getTaskAdmissionEnvelope
summary: Build and persist a canonical narrow Task admission envelope
description: >
No Git/Task mutation occurs. The server normalizes repository, branch,
capabilities, file allowlist, risk, policy digest, expiry and nonce.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TaskAdmissionEnvelopeRequest'}
responses:
'200': {description: Canonical envelope and signing payload}
'403': {description: Requested scope exceeds bootstrap policy}
'409': {description: Idempotency key conflicts with a different request}
/v2/task-admission/authorize:
post:
operationId: submitTaskAdmissionAuthorization
summary: Verify durable delegation or Owner signature and admit one exact Task
description: >
This operation does not accept a general-purpose bearer as authority.
Authorization is proven by a verified OwnerDelegationEpoch or by the
Owner signature over the exact canonical envelope. The resulting grant
can only narrow scope and is handed to the existing Agent Task API.
A delegation identifier in caller JSON is insufficient: the backend
binds the verified parent epoch to the authenticated service caller.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TaskAdmissionAuthorizationRequest'}
responses:
'200': {description: Exact QUEUED, CLAIMED, or RUNNING Task receipt or idempotent replay}
'403': {description: Invalid, expired, revoked or widened authorization}
'409': {description: Conflicting idempotent admission}
/v2/task-admission/status:
get:
operationId: getTaskAdmissionStatus
summary: Read durable Task-admission state
parameters:
- in: query
name: envelope_id
required: true
schema: {type: string}
responses:
'200': {description: Pending or admitted receipt}
'404': {description: Envelope not found}
@@ -0,0 +1,117 @@
# Repository Discovery P0 Runbook
This runbook covers the read-only repository discovery surface and the
governed workflow used to publish a change to it. It does not authorize a
production deployment, a protected-ref update, a tag operation, or a runtime
mode change.
## Safety invariants
Before any operation, record the repository identity and the exact authority
OID. The only repository identity in this contract is `aios/aios`. A floating
ref is resolved once; all subsequent reads in the response are bound to the
returned full `resolved_oid` (or `resolved_from_oid` and `resolved_to_oid` for
a diff) and, for files, the returned `blob_oid`.
The following invariants remain frozen for this P0:
```text
AIOS_GITOPS_WRITES_ENABLED=false
legacy_global_writes_enabled=false
18893 runtime_mode=IMMUTABLE_STAGING
direct bare-repository writes/pushes=forbidden
direct Task DB writes=forbidden
```
Discovery is a read-only operation. It does not acquire a WriterLease and it
does not create a Task. Any implementation change still has to use the formal
Task → TaskPrincipal → TaskLease/claim/heartbeat → WriterLease →
`commitFilesGoverned` path. A request to discovery must never be used as a
write-capability probe.
## Execution-plane preflight
1. Prefer the formal HTTPS control plane at the configured public API/Bridge
endpoint. If raw SSH fails before authentication, preserve that evidence
and do not retry arbitrary usernames, keys, ports, or shell commands.
2. Read runtime, capability, snapshot, and OpenAPI facts. Confirm that the
authority identity and OID are the expected ones, and record whether the
discovery routes are actually deployed on that plane.
3. Classify the available plane as A (network-enabled Codex profile), B
(attested online worker), C (formal HTTPS control plane), or D (existing
executor/coordinator). Select only a supported plane; do not invent a new
SSH or credential path.
4. Treat a readable runtime or a candidate branch as evidence of availability,
not as authorization to write.
The durable discovery record is
`execution-plane-discovery.json`. It must not be included in the governed
source change unless the task explicitly allowlists it.
## Read operations
The canonical Bridge routes are:
| Operation | Method | Required query fields | Optional query fields |
| --- | --- | --- | --- |
| `listRepositoryTree` | `GET /v2/repository/tree` | `repository`, `ref`, `limit` | `prefix`, `depth`, `cursor` |
| `searchRepositorySource` | `GET /v2/repository/search` | `repository`, `ref`, `query`, `max_matches`, `max_bytes` | `path_globs`, `cursor` |
| `getDiffIndex` | `GET /v2/repository/diff-index` | `repository`, `from`, `to`, `limit` | `path_prefix`, `cursor` |
| `readRepositoryFile` | `GET /v2/repository/file` | `repository`, `ref`, `path` | `max_bytes` |
The Actions adapter exposes the corresponding read-only legacy paths under
`/api/repository/*`; the OpenAPI document records each mapping with
`x-aios-legacy-path`.
Every request is bounded. Repository identity is allowlisted, refs are
limited to a branch/tag/full commit form, paths are normalized POSIX-relative
paths, and traversal, option-like paths, shell metacharacters, and ambiguous
refs fail closed. Search skips binary blobs and enforces its cumulative byte
bound. Tree, search, and diff responses are paginated. A cursor is an HMAC-
bound record containing the operation, repository, resolved OID, complete
query, and offset; never edit or manually construct one.
The diff endpoint returns an index only: status, path, optional old path, and
bounded additions/deletions. It never returns a patch body. The file endpoint
returns bounded text plus `content_base64`, `resolved_oid`, and `blob_oid`.
## Publishing an implementation change
1. Preserve the existing dirty worktree. Create a new isolated worktree at
the exact authority OID and use a task-scoped branch.
2. Run discovery unit, pagination, cursor, identity, traversal, binary,
max-byte, immutable-head, and large-diff tests. Run the Bridge/OpenAPI/
Actions drift tests and compile checks.
3. Obtain a formal governed Task and keep its exact task, job, generation,
idempotency, branch, and base-OID identities. Claim and heartbeat through
the supported control plane; do not edit the Task DB or copy a credential.
4. Acquire the Task-bound principal and WriterLease. Submit the allowlisted
source files through `commitFilesGoverned`, then reconcile the Job if the
result is uncertain.
5. Independently read back the authority Git ref, the remote Git API ref, and
the control-plane projection. Completion requires three-way equality to the
governed commit OID, plus the required deployment/acceptance/restart/
official-verification evidence for any release claim.
If the discovery routes are absent from the live OpenAPI/runtime surface, stop
at local validation. Report the implementation and tests as local evidence and
mark governed Task, WriterLease, governed commit, and post-commit readback as
`NOT_VERIFIED`.
## Failure and recovery
- `REPOSITORY_NOT_ALLOWED`, identity mismatch, invalid path/ref, invalid
cursor, or ambiguous ref is a client or security rejection; do not broaden
the allowlist.
- An unavailable authority repository or missing cursor secret is a runtime
configuration failure; do not disable cursor signing or fall back to a
workspace checkout.
- A timeout, oversized output, or large diff is bounded failure. Narrow the
prefix/page or repair the existing supported runtime, never return an
unbounded patch.
- On Windows, a missing `fcntl` means POSIX integration is unverified. Keep
the Python compile and portable unit evidence, then rerun the HTTP and file
locking probes on the supported Linux runtime.
No recovery step in this runbook resets, cleans, overwrites, force-pushes, or
auto-commits a user worktree.
@@ -0,0 +1,102 @@
# Task Admission Bootstrap P0 Runbook
## Goal
Repair the Task-admission bootstrap chain without enabling legacy/global writes.
## Preconditions
Re-read live authority, runtime mode, OpenAPI, capabilities and current Task
API. Do not assume the package source paths are current.
Required invariants:
```text
AIOS_GITOPS_WRITES_ENABLED=false
legacy_global_writes_enabled=false
18893=IMMUTABLE_STAGING
main/tag/baseline/production writes=false
```
## Semantic-port sequence
1. Locate the live Agent Task create/admission implementation.
2. Locate the current Owner/delegation signature verifier.
3. Port `aios_task_admission_bootstrap.py` as the common substrate.
4. Implement `TaskAdmissionPort.admit()` with the official Agent Task API.
5. Implement `OwnerAuthorizationBackend` with the current trusted verifier.
6. Wire the three task-admission operations into the existing Bridge.
7. Keep TaskPrincipal issuance downstream of real Task claim/TaskLease.
8. Merge the OpenAPI fragment only after the live handlers are wired.
9. Run impacted Linux/POSIX, TaskPrincipal, Bridge and writer regression.
10. Create the bootstrap-repair governed commit through the existing sanctioned
recovery path.
11. Use the repaired admission path to create the Repository Discovery P0 Task.
12. Claim/heartbeat -> TaskLease -> TaskPrincipal -> WriterLease.
13. Commit only the existing Repository Discovery allowlist through
`commitFilesGoverned`.
14. Reconcile ambiguous outcomes before any retry.
Admission uses a per-envelope, crash-releasing SQLite lock under the
admission state path's `.locks` directory. It persists the immutable
grant before an effect, then the official Task API GET reconciles any
crashed or ambiguous effect before a POST is permitted.
15. Replays revalidate the supplied proof, policy, expiry and revocation before
reading a receipt. A different otherwise-valid proof conflicts with the
persisted authorization digest; it never becomes a harmless receipt replay.
16. The Task API adapter must receive a server-resolved trusted execution plan
for the signed execution digest. It supplies `metadata.execution` with
the exact repository, branch, generation, digest and nonempty external job
identifier. Do not derive a job identity from request JSON or generate one.
17. Require Authority / remote Git / API OID equality and idempotent replay.
18. Publish immutable staging artifact and run Repository Discovery live E2E.
## Never do
- direct Task DB inserts/updates;
- direct bare-repo/ref changes;
- raw Git push as a substitute for governed publication;
- borrow SSH/Gitea/service credentials as Owner authorization;
- disable TaskPrincipal verification;
- issue TaskPrincipal without a live TaskLease;
- set global writes true;
- turn 18893 into a writer;
- include production/tag/baseline/main permissions in a task grant.
## Acceptance
Bootstrap repair:
```text
ENVELOPE_EXACT_REPLAY=PASS
SCOPE_NARROWING=PASS
DELEGATION_REVOKE_EXPIRY=PASS
OWNER_SIGNATURE_VERIFY=PASS
CONCURRENT_ADMISSION_SINGLE_TASK=PASS
TASK_DB_DIRECT_WRITE=false
TASKPRINCIPAL_BYPASS=false
GLOBAL_WRITES=false
CRASH_AFTER_EFFECT_REPLAY=PASS
CRASH_BEFORE_EFFECT_REPLAY=PASS
CHANGED_VALID_PROOF_CONFLICT=PASS
TASK_API_EXACT_IDENTITY=PASS
```
These are implementation and test acceptance conditions only. They are not
evidence of a live claim, governed publication, remote OID equality, or staging
E2E execution; obtain those independently through the sanctioned live runbook.
Repository Discovery publication:
```text
TASK_CREATED=PASS
TASK_CLAIMED=PASS
TASK_HEARTBEAT=PASS
TASK_LEASE=PASS
TASK_PRINCIPAL=PASS
WRITER_LEASE=PASS
COMMIT_FILES_GOVERNED=PASS
AUTHORITY_REMOTE_API_OID_EQUAL=PASS
IDEMPOTENCY_REPLAY=PASS
STAGING_LIVE_E2E=PASS
TASK_VERIFIED=SUCCEEDED
```
@@ -0,0 +1,32 @@
# Task Lineage Supersession P0 Runbook
## 范围锁定
只允许 source `TASK-SINGLEHOST-TRUSTROOT-P0-20260904`、ordinal `1` 与 child
`TASK-SINGLEHOST-TRUSTROOT-P0-20260904-S1`。每次 live 操作前从 Task API/DB 的只读正式路径
重读 source:必须是 `EXPIRED`、attempt 0、无 claim/lease,并重新计算正式 identity digest。
同时确认 repository、branch、generation、execution digest、external job 与 source metadata
digest 均为 decision 的 exact 值,确认当前 Local Owner Authority 和 global/legacy writes 状态。
## 实施和测试
1. 在专用 staging worktree 完成 controller、API wrapper、migration、child ClaimGuard binding、
ADR/runbook 和 isolated SQLite tests。
2. 覆盖错误 signer/namespace、旧 signer、所有 source CAS 字段漂移、nonce/replay、TTL、
child/payload injection、existing child/lineage、projection/ClaimGuard compatibility 的失败路径。
3. 验证失败后 source 的字节等价快照、child 不存在、没有 lineage/receipt/event 残留。
4. 在 backup/isolated DB 跑 complete dry-run:supersession -> child rev1 -> Initial Projection ->
需要时 Supervisor Projection -> immutable child ClaimGuard binding -> guarded claim attempt 1。
## Staging 激活与 live 操作
仅在测试与 dry-run 全部 PASS 后,将新 API overlay 放在 Task API `PYTHONPATH` 最前并切换到
lineage wrapper;daemon-reload/restart 仅限非生产 Task API/projection/ClaimGuard 服务。验证
health 及固定 supersession route 后,再次重读 live source 并通过当前 Owner authority 签发一份
短 TTL、one-time decision。只调用一次正式 endpoint;不得用 sqlite、maintenance shell、
direct Git ref 或任意签名接口替代。
## 后续和停止条件
成功后只沿 child lineage:Initial Projection、必要的 Supervisor Projection、新 immutable
ClaimGuard binding、worker `p0-task-principal-v15-20260830` 的 guarded claim、governed execution。
@@ -0,0 +1,48 @@
# AIOS Baseline 1.1 验收矩阵
本矩阵定义 AT-01~AT-23 的验收标准和证据边界。它不以“测试文件存在”冒充通过:本次本地自动化的实际命令、退出码和数量以包内测试结果文件为准。下表标注的是覆盖入口及尚需现场核验的范围,单项 AT 只有在全部必要证据齐备后才能标记验收通过。
状态约定:`LOCAL_AUTOMATED` 表示已有可执行本地测试入口;`LOCAL_PARTIAL` 表示只能覆盖该场景的一部分;`SITE_REQUIRED` 表示必须在准确绑定的现场环境补充验收。上述标签不是 PASS。未执行、证据过期、身份不符或无法访问目标均为 `NOT_VERIFIED/UNKNOWN`,不能按健康放行。
| ID | 场景与必须满足的结果 | 本地代码/测试入口 | 当前证据边界与现场补充 |
| --- | --- | --- | --- |
| AT-01 | 缺失/过期 observation、错误任务身份、main 与 runtime 不同;UNKNOWN fail-closed;drift 比较 DeploymentTarget 与 observation,不拿 main HEAD 当 desired | `test_contracts.py`、`test_loop.py`;Runtime Contract / deployment evaluator | LOCAL_PARTIAL;需现场核对 Resolver、正式入口、canonical ref 和 artifact identity |
| AT-02 | 支持包缺文件、改一字节、额外未声明内容必须校验失败;生成身份与内容可追溯 | `test_package_tools.py`;`bundle.py verify/stage` | LOCAL_AUTOMATED(源码包边界);线上运行镜像与 overlay 构建仍须独立验证 |
| AT-03 | 每个允许执行的 operation 在 dry-run/shadow 下都不调用副作用 writer,不伪造已执行或发布成功 | `test_job_safety.py`;既有 Job executor dry-run 分支 | LOCAL_PARTIAL;新 adapter 加入注册表时必须补相同负面测试,不能沿用其他 adapter 结论 |
| AT-04 | 同一有效 Grant 下短期证明过期/服务重启仍能完成后续授权范围内任务;不重复业务审批、不扩大权限 | `test_authorization.py`、`test_loop.py` | LOCAL_AUTOMATED(授权/本地任务);完整生产发布链仍需 AT-16 |
| AT-05 | 父 Grant 撤销、过期、签名错误、跨 tenant/subject/scope 重放、伪造 Mapping 在派生与消费两端均拒绝 | `test_authorization.py`、`test_transport.py` | LOCAL_AUTOMATED;真实密钥轮换和撤销传播需部署证据 |
| AT-06 | 过期 lease、终态 Job 再领 lease、旧 generation 延迟写均拒绝;新 claim 保持独占 | `test_job_safety.py`、`test_worker_host.py`;TaskPlaneStore 原有回归 | LOCAL_PARTIAL;每个实际外部写接收端须证明 fencing 生效,不能只在发送端检查 |
| AT-07 | 外部成功而 receipt 丢失、对账中止、双 observer 乱序;复用 effect、精确 readback、旧结果不能覆盖新事实 | `test_job_safety.py`、`test_release_lineage.py` | LOCAL_AUTOMATED(可控 effect sink);实际远端 readback 身份契约需验收 |
| AT-08 | ready+600 秒后扩大 eligible Worker;普通与 fallback Worker 竞争仍只有一个 CAS claim,不抢有效 lease | `test_worker_host.py`、`test_loop.py`;WorkerProfile/TaskPlaneStore | LOCAL_PARTIAL;需常驻实例 heartbeat/transport 与竞争运行证据;聊天 GPT 不算 Worker |
| AT-09 | blocked 任务未进入 ready 时不静默无限等待;显示 blocker、持续时间、修复和升级边界 | 服务端状态与错误返回;本包实现范围以源码报告为准 | SITE_REQUIRED;完整 Doctor 自动修复/升级场景不能用普通任务成功代替 |
| AT-10 | Worker/子进程退出、取消、超时、重启;有限终止、服务端恢复,同 effect 不因缓存丢失重复 | `test_worker_host.py`、`test_loop.py`、`test_transport.py`、`test_job_safety.py` | LOCAL_PARTIAL;实际 systemd kill/restart、取消竞争和主机启动顺序需演练 |
| AT-11 | 重复提交/事件/回执、提交后投递器退出;outbox 持久、去重、终态不倒退 | `test_loop.py`;TaskPlaneStore outbox 与幂等接口既有测试 | LOCAL_PARTIAL;通知投递器/inbox 的端到端故障仍需专门证据 |
| AT-12 | 跨 tenant/subject 查询、取消、执行和关联被拒;Worker 不能读服务端私钥和其他工作目录 | `test_authorization.py`、`test_loop.py`、`test_transport.py`、`test_release_lineage.py`;独立 UID unit 模板 | LOCAL_PARTIAL;artifact 下载、cache key、文件权限及真实子进程边界需现场覆盖 |
| AT-13 | Bridge/provider 独立重启或 token 轮换后使用自身 credential projection,不依赖另一服务临时目录 | 修订现有 staging unit;`deploy/baseline-v1.1/` 凭证模板 | SITE_REQUIRED;静态 unit 不等同于解密、轮换和重启已验证 |
| AT-14 | 子任务、重复诊断和连续失败不能用新 task ID 重置 Grant 预算;超限有界停止 | `test_authorization.py`;持久 effect 预算;`test_optimization.py` | LOCAL_PARTIAL;目标级总成本、墙钟、模型预算的完整现场计费来源仍须接线 |
| AT-15 | 支持 profile 可在干净目录执行;错 source/config 的证据不能放行;未绑定线上权威明确拒绝 | `test_package_tools.py`、`test_cli.py`、`test_contracts.py` | LOCAL_AUTOMATED(隔离 profile);目标 OS、Python、systemd 版本与现场配置仍需记录 |
| AT-16 | 同一 Grant:patch→tests→受保护 merge→build→staging→业务验收→production→生产业务验收;全程有唯一关联 | `test_release_lineage.py`;ReleaseLedger/recipe/receipt verifier 接口 | SITE_REQUIRED;本包的状态机和模拟 adapter 测试不等于真实发布链已完成 |
| AT-17 | 编码 Worker 自报成功、伪造 receipt、降低验收规则或读签名私钥不能放行 | `test_authorization.py`、`test_release_lineage.py`、`test_optimization.py`;API/Worker 独立 UID | LOCAL_PARTIAL;生产 verifier 独立职责、规则冻结和私钥访问权限需现场验证 |
| AT-18 | 优化无改善或越过质量底线不晋升;优化提案只生成受治理任务,不直接写 policy DB | `test_optimization.py`;签名 OptimizationEnvelope | LOCAL_AUTOMATED(提案与质量约束);真实实验数据来源、回滚及模型成本路由需后续验收 |
| AT-19 | 旧 schema/任务与新版本并存时保留原签名字节;main 合法前进不等于 runtime drift;旧 handler 明确处理 | `test_authorization.py`、`test_contracts.py`、`test_release_lineage.py` | LOCAL_PARTIAL;线上旧任务排空、writer 停用、迁移/路由策略需现场清单 |
| AT-20 | 坏候选、控制面不能启动、数据迁移不可逆时只允许兼容回退或前向修复;无万能 root | `test_release_lineage.py`、`test_package_tools.py`;源码回滚、Release 回退约束 | LOCAL_PARTIAL;数据库迁移/外部副作用不由源码 rollback 撤销;独立最小恢复流程需现场演练 |
| AT-21 | SQLite/Git/receipt 一致备份在隔离环境恢复,保留身份和未决 effect;实测 RPO/RTO | 各 ledger 重启测试是局部依据;安装文档给出一致备份边界 | SITE_REQUIRED;本包没有以“可重开 SQLite”宣称整机备份恢复验收通过 |
| AT-22 | persist intent→外部 effect 成功→主机在 receipt 落盘前失去执行能力;恢复同 effect→RECONCILIATION_REQUIRED→readback→补 receipt;外部效果次数仍为 1 | `test_job_safety.py`;子进程退出/独立持久 effect sink 故障注入 | LOCAL_PARTIAL;SIGKILL/进程崩溃不能证明磁盘缓存断电持久性,物理/虚拟机硬掉电需现场验证 |
| AT-23 | intent/lease 无法持久化时不得开始 effect;effect 后 receipt 无法提交则停止新 effect 并恢复对账;磁盘满/WAL/receipt unavailable 不冒充完成 | `test_job_safety.py`;SQLite/receipt 写失败注入及持久停止边界 | LOCAL_PARTIAL;实际 ENOSPC、WAL checkpoint、目录权限/介质损坏需隔离卷或现场故障试验 |
## 每次验收必须保存的记录
每条记录至少包含:AT 编号、完整命令、开始/结束时间、执行环境、源码/构建/配置身份、Grant/Task/effect/job/Release/deployment/acceptance 关联(适用时)、注入点、实际 observation、receipt 引用、退出码、明确结论和剩余缺口。密钥/token 只保存 CredentialReference,不能复制到报告。
`UNKNOWN`、`RECONCILIATION_REQUIRED`、预算耗尽导致的有界停止都可能是正确故障处理结果;它们不是业务成功。只有可信且当前的业务 acceptance,准确绑定 deployment+release+artifact,才能关闭相应发布验收。
## M0–M5 的放行顺序
| 实施包 | 放行条件 | 不可用来代替放行的证据 |
| --- | --- | --- |
| M0 Truth & Identity | 静态 Contract、动态 target、observation、provenance 分离;Resolver 与 CredentialReference 对应真实目标;UNKNOWN fail-closed | 目录存在、进程健康、main 与 runtime 碰巧相同 |
| M1 Authorization & Effect Safety | Grant→短期证明→Principal→实际 lease/fencing;意图先持久化;未知结果精确对账;AT-22/23 本地故障通过 | 仅延长 TTL、发送端自称 fenced、effect 成功但未写 receipt |
| M2 Real Autonomous Loop | 至少一条真实非发布 Action→Task→常驻 Worker→已注册 operation→receipt→result;tenant/subject 在服务端派生 | 仅 scheduler 单元测试、伪造 worker 或直接函数调用绕过 admission |
| M3 Release Closure | 同一 Grant 完成真实 patch 至 production business acceptance,至少一次 ambiguous publication/worker crash 自动恢复 | 本地 Release 状态机、模拟 adapter、部署命令返回 0 |
| M4 Surface Reduction | 每类事实 active write authority=1;旧入口停止接收新任务;历史任务可追溯;恢复演练通过 | 新模块存在但旧 writers 仍在写,或把全系统静态函数数当活跃写者数 |
| M5 Optimization | 签名 Envelope 限定范围;真实效果和质量底线可核验;只经受治理任务晋升并能回退 | optimizer 自报改善、直接改 policy DB、放宽验收阈值 |
@@ -0,0 +1,20 @@
# 交付统一验收结果
本次验收在新建虚拟环境中,仅使用包内锁定离线 wheel 安装依赖。没有连接生产系统。
| 范围 | 实际结果 |
| --- | --- |
| 新增 Baseline 1.1 | 165 passed;0 skipped |
| 全量源码回归 | 1212 passed;154 subtests passed;3 skipped |
| 全量退出码 | 0 |
| Python | 3.12.13 |
| 平台 | Linux-6.18.35-x86_64-with-glibc2.39 |
| 验收完成时间(UTC) | 2026-09-07T19:07:27.816913+00:00 |
JUnit tests=1369 包含子测试,不能再把这个数字与 1212 相加。3 项跳过分别是 live Gitea tag publication、opt-in live signer integration,以及离线验收主动隔离的 deployed signer 检查。它们保持现场未验收状态,没有改成通过。
实际命令为完整包 `run_acceptance.py --profile all --output ...`,该工具在一次性源码副本中执行,显式向子进程传递源码 PYTHONPATH,使进程崩溃/重启测试不依赖编辑器环境,也避免历史测试的 runtime 输出污染发行目录。副本保留原始 runtime/agent-os 源码;只隔离生成输出,不删除历史实现。首次干净环境运行发现并修正了这个验收启动器的子进程导入问题;修正后完整重跑通过。
完整日志、JUnit、依赖版本、开始/结束时间、命令、退出码和逐源码文件 SHA 在完整包 `acceptance/full/`;新增基线证据在 `acceptance/baseline/`。源码改动后应重新生成相应证据,不能沿用本结果放行。
AT-22 的进程故障注入通过;真实主机硬掉电仍需现场演练。AT-23 的 intent/receipt 持久化失败与停止/恢复通过;真实介质 ENOSPC、WAL checkpoint、备份 RPO/RTO 仍需现场验收。生产发布、正式 Task API 接入和旧写权威清退不在本次执行范围。
@@ -0,0 +1,107 @@
# AIOS 最终架构修改建议:Implementation Baseline 1.1
本基线吸收七项最终修订,作为后续编码、集成与验收的共同约束。目标是一台机器、一人维护、多个常驻自治 Worker:Owner 给出目标和真实业务边界,系统在既有授权内自动发现对象、领任务、执行、验证、恢复并产生后续优化任务。
本文件依据本次明确要求、上传源码和可见交互/评审材料;相关材料反映的持续诉求是少反复授权、少人工补路径、可恢复、低维护成本和严格身份隔离。它不声称已逐条读取全部三个月历史聊天,也不将材料里的现场状态当作本次在线测量。
## 1. 最高原则与冻结决策
**AIOS 的自动化程度不得高于其可观测、可恢复和可归因程度。**
不能精确识别对象、持久化 intent、证明当前授权或围栏旧执行者,就不开始副作用。结果未知时先对账;不能验证业务结果时不宣称完成;无法安全恢复时有界停止并给出最小决策材料。正常续期、已授权恢复、同一范围内重试不重新索要 Owner 业务审批。
继续使用 SQLite、Git、systemd;不引入 Kubernetes、消息中间件、Backstage 或新 Secrets 平台。不打开 legacy global write,不增加万能 SSH/root,不把 GPT 对话包装成执行身份。Task、Job、Release 保持各自事实域,Action 只负责入口。自动优化使用与普通开发相同的授权、执行、验证和发布边界。
“单一写权威”按事实及资源范围判定,不等于全系统共用一张状态表或一个 daemon。允许同一进程承载多个模块,但模块不能直接修改另一模块的权威表。
## 2. 事实域及写权威
| 事实域 | 唯一写入口 | 权威内容 | 其他模块的使用方式 |
| --- | --- | --- | --- |
| Authorization | `AuthorizationService` | 已验签 Grant、当前 generation、撤销、effect 预算预留 | 请求派生证明,在执行边界重新验证 |
| Task | 既有正式 Task authority;隔离 profile 使用既有 `TaskPlaneStore` | 准入、任务状态、claim、Worker 登记、任务 outbox | 通过正式 API/适配器调用,不能另写 Task 状态 |
| Job / Effect | 既有 `JobController` 与受治理 executor | intent、attempt、资源 lease/fence、receipt、reconciliation | 持久化请求后执行;由权威 receipt 解释结果 |
| Release / DeploymentTarget | 正式 Release authority;候选实现为 `ReleaseLedger` | 发布 recipe/saga、动态目标、部署事实、验收收口 | CAS 更新目标;消费经可信 verifier 验证的 receipt |
| Runtime Contract | 受版本治理的配置入口 | 组件结构、入口、能力、资源及凭证引用 | Resolver 解析;不保存动态 desired |
| Runtime Observation | 对应组件的可信观测适配器 | 实际部署身份、时间、来源、健康事实 | 只作为带新鲜度和来源的证据,不反写目标 |
| 关联图 | `LineageLedger` | 不可改绑的身份关系 | `goal-explain` 查询;不拥有 Task/Job 状态机 |
| Optimization | `OptimizationController` | 提案、实验证据、停止或晋升申请 | 只能请求受治理 Task,不能写政策或部署库 |
新增库是明确事实域的持久层,不是新的线上写权威自动生效。当前 CLI 只支持 `isolated-validation`,复用上传源码中的 TaskPlaneStore;连接真实 Task API 前必须证明其正式入口、身份与兼容性,不能因为本地新库可启动就切换线上 admission。
## 3. 运行结构、期望目标和实际观测分离
| 对象 | 回答的问题 | 关键身份 |
| --- | --- | --- |
| `RuntimeContract` | 组件是什么、正式入口和允许能力是什么 | component、environment、tenant scope、contract revision、引用 |
| `EnvironmentDeploymentTarget` | 此部署资源现在应运行哪个 Release | 部署资源范围、release、artifact、target revision |
| `RuntimeObservation` | 实际运行什么,证据多新 | component、deployment、actual artifact、observed time、source |
| `ReleaseProvenance` | 制品从哪里产生 | artifact/source/builder 身份与构建证据 |
drift 比较同一部署资源的 desired 与 observed。main HEAD、staging source、production Release 不同本身不是故障;缺目标、缺来源、观测过期或身份绑定不完整应为 UNKNOWN。用户报告的 `0136a… / 91f… / 998d…` 以及缺 `gitea_token` 的 UNKNOWN,只构成待核验输入,不作为本包生产运行结论。
M0 只做薄合同和可信解析:回答操作哪个组件、入口、desired/observed、制品来源、凭证引用和证据新鲜度,不做大目录迁移。当前实现提供合同校验、动态目标评价和受信固定绑定;完整在线 Resolver/Doctor 仍需适配实际 authority 与服务清单。
目标的唯一性固定为“tenant + component/deployment scope + environment”,subject 用于授权及审计。`ReleaseLedger` 必须按资源主键实施 CAS,不能让不同用户为同一物理组件建立互相冲突的 desired。主键迁移必须检测已有目标冲突,不能静默合并或用当前观测覆盖期望目标。
## 4. 持续业务授权与短期执行权
授权链固定为 `OwnerDelegationGrant → OperationalEpoch → TaskPrincipal`,执行时另行获得正式 `ClaimLease / Fencing`。Grant 保存目标/任务范围、tenant、subject、环境、操作能力、破坏性上限、预算、明确期限或 UNTIL_REVOKED,以及撤销代次。
新增长期 schema `aios.owner-delegation-grant.v1` 和短期 `aios.operational-epoch.v1`;历史 `owner-delegation-epoch.v1/v2` 不改名、不改签名字节,也不重解释为长期 Grant。派生证明保存根授权 digest/generation;代码字段为 `root_grant_digest`、`root_grant_generation`,承担提议中 `parent_grant_digest` 的根绑定语义。Principal 另绑定其父 Epoch digest。
签名采用显式受信公钥验证,调用者不能附一份 Mapping 或公钥就自称受信。每次实际副作用检查根授权当前状态、精确 scope、破坏性上限、预算及真实 lease。短期证明过期可在有效 Grant 内自动更新;撤销、到期、跨环境或越过破坏性上限不能靠续期绕过。租约来自 Task/Job 权威,Principal 不伪造 claim。
当前实现提供持久 `max_effects` 预留:同一稳定 effect 不重复扣额,换 task ID 不重置同一 Grant generation 的限额。总模型费用、计算资源和整个 mission 的综合预算仍需真实计量接入;不能把 effect 计数声称为完整成本控制。
## 5. 不可改绑的业务关联与执行恢复
各对象共享 `mission_id`、授权身份和 tenant/subject,但不共享状态机:
```mermaid
flowchart TD
G["Mission / Grant"] --> T["Task 权威"]
T --> J["Effect / Job 权威"]
J --> R["Release 权威"]
R --> D["Deployment"]
D --> A["Business Acceptance"]
T -. 身份关系 .-> L["Lineage / goal-explain"]
J -. 身份关系 .-> L
A -. 验证结果 .-> L
```
每个新 operation Job 必须绑定一个真实 Task;effect/Job 不能换绑另一任务。计划中的 job_id 必须传到正式 Job adapter,并在权威确认后才能显示为已存在的 Job。产生 Release 的 effect 只能指向唯一 release 身份,Release 保存来源链;Acceptance 必须绑定 deployment、release、artifact。非发布诊断任务只生成真实存在的 Task/Job/receipt,不能为补齐图形虚构 Release。
effect 的稳定身份先于执行产生,attempt 可以变化。正常路径为持久 intent、有效 claim/lease/fence、执行、精确 readback、receipt、业务结果;结果不确定或跨域投递失败时进入 reconciliation。没有 readback/reconcile 能力时明确停止,不退回 execute。已有 Task outbox 及持久阶段记录支持恢复,但跨事实域不声称存在一个 SQLite 分布式事务;消费必须幂等,断点必须保留,关联缺口由受控重放补齐。
旧执行者的 fencing 必须由副作用接收端再次检查;只在 scheduler 端检查不能阻止延迟写。AT-22 覆盖外部成功而 receipt 未提交后的同 effect 对账;AT-23 要求 intent/lease 无法持久化时 effect 不开始,receipt 提交失败后阻止新 effect。进程崩溃测试和 SQLite 错误注入已作为本地入口;真实掉电、ENOSPC、WAL checkpoint 和介质故障仍须现场验证。
## 6. Operation Registry 与无人值守 Worker
`OperationDefinition` 声明版本、输入 schema、所需能力、可信 handler 引用、期限、effect class,以及独立的 retry/readback/reconcile/rollback/owner-boundary 策略。Registry 是受治理查表入口,不是动态 shell 或任意 Python 加载入口。
| 主 effect class | 未知结果的处理原则 |
| --- | --- |
| READ_ONLY | 可在预算内重新观察;诊断执行成功不代表对象健康 |
| IDEMPOTENT | 仅在幂等键、权限、围栏和重试策略仍有效时重试 |
| CONDITIONAL | 重新读 revision,保留 expected revision/CAS,不能删掉条件重试 |
| RECONCILABLE | 先按稳定 effect 精确 readback,再决定补 receipt 或后续动作 |
| IRREVERSIBLE | 不盲重试;遵守显式 Owner 边界和恢复能力 |
这些属性会重叠,因此 class 负责主分派,幂等性、可观察性和可逆性另作约束。service-restart 不能仅凭 `active` 宣称本次重启已发生,需调用实例/事件等精确关联;当前 validation 内置仅为固定参数 `service-status`。旧 COMMIT_FILE adapter 缺乏 intent 可定位的未引用 commit readback 时明确停止,不为打通演示而制造成功。
Worker 登记包含身份、capabilities、tenant/subject scope、priority、fallback_executor、heartbeat 和并发限制。正常选择 eligible Worker;从 `ready_at` 起 600 秒后扩大 fallback eligibility,仍使用同一 CAS/lease/fencing。blocked 时间不冒充 ready 时间,现存有效 claim 不被强抢。“主 Agent”只是部署策略;聊天 GPT 不拥有 Worker 的常驻进程身份。
## 7. Tenant-first 与优化边界
核心记录从第一天携带 tenant、subject、scope,身份从受信服务端认证上下文取得。查询、取消、执行、关联以及制品/cache/工作目录/credential 都必须在各自边界验证;不能只在 UI 隐藏对象。首版不做租户 UI、billing、独立租户管理服务或复杂公平调度。当前独立 UID 模板与 API 隔离不是所有未来 artifact/cache 实现已验收的证明。
签名 `OptimizationEnvelope` 限定参数、组件、非生产环境、变更量、计算预算、质量下限、回退要求和晋升条件。初期仅开放有限 retry/backoff、worker routing、evidence reuse、timeout、poll interval、通知去重和非强制测试选择。授权政策、信任根、租户隔离、验收阈值、破坏性能力、Grant 语义和生产迁移规则禁止修改。
优化流程固定为观察、提案、受治理 Task、实验、验证、晋升申请;优化器不能直接修改 policy DB 或运行配置。质量评价区分越高越好/越低越好,无改善或低于门槛就停止。当前模块已实现这些约束和持久提案,真实实验执行器、费用来源、运行参数晋升与回退仍需通过同一治理链接线。
## 8. 交付状态和实施顺序
本包包含实际代码、负面测试、可运行的 loopback 非发布闭环、安全源码 stage/rollback 和 AT-01~AT-23 标准。M0/M1 的本地模块与 M2 隔离纵向闭环已交付;M3 的关联/目标/saga/验签是候选基础,未宣称真实生产发布通过;M4 的旧写者排空和生产 imports 清理未执行;M5 是有界提案实现,未自动优化线上系统。
上线顺序为 M0 Truth & Identity、M1 Authorization & Effect Safety、M2 Real Autonomous Loop、M3 Release Closure、M4 Surface Reduction、M5 Optimization。每阶段以完整输入、实际 adapter 与退出标准放行,详见 `M0_M5_IMPLEMENTATION_RUNBOOK.md`。不使用“架构方案正确”替代接收端围栏、正式权威绑定、可信业务验收或备份恢复证据。
@@ -0,0 +1,210 @@
# Baseline 1.1 signed authorization chain
## Result
`aios_baseline.authorization` implements a new authorization chain without
changing the frozen historical owner-delegation wire formats:
1. an Owner signs `aios.owner-delegation-grant.v1` with a supplied Ed25519 key;
2. the local authorization service verifies and durably registers that grant;
3. the service issues a short, signed `aios.operational-epoch.v1`;
4. the service embeds that epoch in a signed `aios.task-principal.v1` and binds
the principal to it by digest;
5. every authorization and effect reservation rereads SQLite and checks the
current root grant digest, generation, revocation state, expiry, signature,
tenant, subject and narrowed scope.
The TaskPrincipal is a short authorization credential. It does not manufacture
a Task claim or ClaimLease. The task authority and executor must independently
enforce their authoritative claim lease, heartbeat and fencing token.
## Public API
```python
AuthorizationService(
db_path,
owner_public_keys, # key_id -> Ed25519PublicKey
machine_private_key,
machine_key_id,
*,
owner_key_scopes, # key_id -> exact tenant_ids/subject_ids lists
clock=time.time,
control_context_verifier=None,
trusted_machine_public_keys=None,
)
sign_owner_delegation_grant(
owner_private_key,
owner_key_id,
*,
grant_id,
generation,
tenant_id,
subject_id,
mission_ids,
task_ids,
operations,
component_ids,
environments,
issued_at,
validity_mode, # required: UNTIL_REVOKED or EXPIRES_AT
expires_at=None,
max_effects=None,
destructive_ceiling="NONE",
)
service.register_grant(signed_envelope) -> dict
service.issue_principal(grant_id, *, exact_scope, ttl_seconds) -> dict
service.authorize(principal, *, exact_scope) -> dict
service.assert_effect_bounds(principal, operation_definition, *, exact_scope) -> dict
service.reserve_effect(grant_id, effect_id, principal, *, exact_scope) -> bool
service.revoke_grant(grant_id, *, control_context) -> int
```
`register_grant` and `authorize` return JSON-safe copies of the signed grant.
`reserve_effect` returns `True` for the first reservation and `False` for an
idempotent retry of the same stable effect ID. It reauthorizes and reserves in
one `BEGIN IMMEDIATE` transaction. A configured `max_effects` applies per grant
generation. An omitted `max_effects` leaves mission-level budget accounting to
the calling service.
The grant signing helper requires an explicit validity mode. It never creates a
key and does not accept an unsigned grant payload as authority. `EXPIRES_AT`
requires an integer expiry after issuance. `UNTIL_REVOKED` forbids an expiry.
The signed `destructive_ceiling` defaults to `NONE` and is strictly one of
`NONE`, `REVERSIBLE`, or `IRREVERSIBLE`. `assert_effect_bounds` accepts only a
trusted `OperationDefinition`: `NONE` permits only `READ_ONLY`; `REVERSIBLE`
also requires rollback, required state/exact readback, and reconciliation; and
`IRREVERSIBLE` is an explicit maximum. A ceiling never replaces Job
authorization, durable intent, claim lease, receiver fencing, readback, or
reconciliation.
Owner public-key trust is separately constrained by the required constructor
argument `owner_key_scopes`. Its key set must exactly equal `owner_public_keys`;
each key has exact nonempty `tenant_ids` and `subject_ids` lists. A wildcard is
effective only when the configuration explicitly supplies the sole list item
`"*"`. Registration and every later durable grant load check the signing key's
scope, so placing keys for several tenants in one process does not let one
tenant's Owner key sign another tenant's grant.
## Exact wire objects
Every signed object uses an exact `signing` object:
```json
{"algorithm":"Ed25519","key_id":"...","signature":"base64-64-byte-signature"}
```
The signature covers the schema, every payload field, the signing algorithm and
key ID, and, for a TaskPrincipal, the complete nested signed epoch. The message
starts with the `AIOS-AUTHORIZATION` domain, a NUL separator, the schema, and a
second NUL separator before canonical JSON.
An OwnerDelegationGrant has exact outer keys `schema`, `payload`, `signing`.
Its payload has:
```json
{
"grant_id": "...",
"generation": 1,
"tenant_id": "...",
"subject_id": "...",
"scope": {
"mission_ids": ["..."],
"task_ids": ["..."],
"operations": ["..."],
"component_ids": ["..."],
"environments": ["..."]
},
"issued_at": 2000000000,
"validity": {"mode": "UNTIL_REVOKED"},
"limits": {},
"destructive_ceiling": "NONE"
}
```
The alternative validity is
`{"mode":"EXPIRES_AT","expires_at":<integer>}`. `limits` is either empty or
contains one positive integer `max_effects`. Each scope list is nonempty and
may contain explicit values or the sole wildcard `"*"`.
An OperationalEpoch has exact outer keys `schema`, `payload`, `signing`. Its
payload has `epoch_id`, `root_grant_id`, `root_grant_digest`,
`root_grant_generation`, all seven exact binding fields (`tenant_id`,
`subject_id`, `mission_id`, `task_id`, `operation`, `component_id`,
`environment`), `issued_at` and `expires_at`.
A TaskPrincipal has exact outer keys `schema`, `payload`, `operational_epoch`,
`signing`. Its payload has `credential_id`, `nonce`, `parent_epoch_digest`, the
same three root fields, the same seven binding fields, `issued_at` and
`expires_at`. Epoch and principal validity is capped at 300 seconds and at an
explicit root-grant expiry.
## Durable state and revocation
SQLite stores the canonical signed grant, its digest, its current durable
generation and revocation flag. Revocation increments the durable generation in
an immediate transaction, records a revocation row, and leaves old proofs
invalid even when their short expiry has not elapsed. Reopening the service on
the same database retains that decision. The same signed generation cannot
clear revocation. Reactivation requires a newly Owner-signed generation greater
than the durable revocation generation.
`revoke_grant` fails closed unless `control_context_verifier` returns the exact
boolean `True` for the caller's authenticated local control-service context.
The authorization module does not authenticate network requests; the hosting
service must construct that trusted context from its local authenticated API
boundary. Authorization never relies on the verifier or a cache for freshness;
it reads current durable state on every use.
The active machine signing key is always trusted for newly issued objects.
`trusted_machine_public_keys` can retain previous public keys for the remainder
of already-issued proof lifetimes during rotation. Owner trust keys and machine
trust keys remain separate.
## Verification
The initial failure-first run stopped during collection because
`aios_baseline.authorization` did not yet exist. After implementation, a focused
run found and fixed strict nonce validation and clarified that substituting the
nested epoch invalidates the outer signature before the parent-digest check.
The final focused commands were:
```console
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app \
python3 -m pytest -q \
tests/baseline_v1_1/test_authorization.py \
tests/baseline_v1_1/test_claim_heartbeat.py
```
Result before the ceiling extension: `25 passed in 0.80s`. The post-extension
focused run reported `28 passed in 0.35s`. After the Owner trust-root and claim
heartbeat hardening, the current focused result is `35 passed`.
The tests cover exact-scope success, all three signature layers, unknown Owner
keys, unsigned payload refusal, fake Mapping parents, nested-parent
substitution, every cross-scope dimension, grant narrowing, live and persisted
revocation, proof renewal without Owner reapproval, explicit root expiry,
Owner-signed generation reactivation, strict types and signature encoding,
explicit helper validity, durable idempotent effect budgets, unbounded budget
delegation, machine-key rotation, refusal of untrusted retired keys, NaN and
empty-identity rejection, and the revocation-generation boundary around durable
effect budgets. Revocation cannot reset a budget or revive the immutable old
signed generation; only a newer Owner-signed generation can reactivate it.
The extension tests also cover signed ceiling tampering, conservative default
denial, rollback/readback requirements, and explicit irreversible authority.
They now also cover exact Owner-key scope configuration, cross-tenant and
cross-subject Owner-key refusal, explicitly configured global trust, cancellation
before heartbeat, and the exact lease-expiry boundary.
## Security review disposition
| Severity | Status | Boundary | Disposition |
| --- | --- | --- | --- |
| High | Closed | Owner trust roots | Each trusted Owner key now requires explicit tenant and subject scope; no implicit cross-tenant authority remains. |
| High | Closed | Revocation and effect budget | Every use rereads root generation and revocation state in SQLite. Revocation cannot revive or reset the old generation's reservations. |
| High | Closed | Receipt and recovery integration | Generic Jobs require the exact transition graph and trusted, fully bound signed receipts; Task recovery preserves terminal Owner cancellation. |
| Medium | Closed | Claim heartbeat | Heartbeat now checks terminal Task state and performs the live-lease update with `status='ACTIVE'` and `lease_until>now` under one immediate transaction. |
| High | Pending M3 | Mutating receiver fence | The supplied handler remains read-only. Any future mutating receiver must atomically enforce the authoritative fence at the sink and recheck authorization, claim and deadline around receiver execution. |
| Medium | Accepted for initial single-host profile | Admission scope | The durable unknown-effect admission latch intentionally stops new effects across the shared Job store. This conservative availability policy is documented in SERVICE_LOOP_REPORT.md; later tenant/resource partitioning must retain a global storage-integrity gate. |
@@ -0,0 +1,220 @@
# Truth and Operation Contracts Report
## Scope
Implemented Task 3 and the adjacent C01 trusted-snapshot correction without live calls or production access.
Files changed for this work:
- `active-runtime/app/aios_baseline/runtime_contract.py`
- `active-runtime/app/aios_baseline/operations.py`
- `active-runtime/app/reconciliation_controller.py`
- `active-runtime/app/aios_chatgpt_bridge_v2_server.py`
- `deploy/systemd/aios-chatgpt-bridge-v2-staging.service`
- `tests/baseline_v1_1/test_contracts.py`
- `tests/test_completed_replay_reconciliation_v7.py`
- `tests/test_isolated_writer_exact_path_policy.py`
- `tests/test_r7_atomic_recovery_and_bridge_sandbox.py`
- `docs/implementation-baseline-1.1/CONTRACTS_REPORT.md`
No package `__init__.py` was changed by Task 3. No arbitrary command executor, production trust root, live call, deployment, or Git commit was created.
## Public functions and payload schemas
### `validate_contract(document: Mapping[str, Any]) -> dict[str, Any]`
Required stable contract fields:
```json
{
"contract_schema": "aios.runtime-contract.v1",
"contract_revision": 1,
"component_id": "sample",
"environment": "staging",
"tenant_scope": "tenant-a",
"lifecycle": "active",
"entrypoints": {
"host": {
"component_id": "sample",
"environment": "staging",
"resolver_ref": "service://sample/status"
}
},
"bindings": {},
"resource_refs": {},
"credential_refs": {},
"operations": ["service-status"]
}
```
`lifecycle` is one of `active`, `candidate`, `legacy`, or `example`. Reference groups are mappings and may be empty. A reference value may be a symbolic string, which is normalized with the contract's component and environment, or an object containing matching `component_id` and `environment` plus exactly one of `resolver_ref`, `path_ref`, or `credential_ref`. If a reference object supplies `tenant_scope`, it must match the contract.
`path_ref` must be relative and cannot contain traversal, backslashes, or NUL. Credential references accept `credential://`, `secret://`, and `systemd-credential:` references; raw `/run/credentials/...` paths and inline secret fields are rejected. The keys `desired`, `desired_commit`, `desired_release`, `desired_target`, and `desired_targets` are rejected at every depth because release targets are separate objects.
### `evaluate_deployment(contract, target, observation, provenance, now) -> dict[str, Any]`
Target payload:
```json
{
"component_id": "sample",
"environment": "staging",
"release_id": "release-17",
"deployment_id": "deployment-22",
"artifact_digest": "sha256:<64 lowercase or uppercase hex characters>",
"revision": 4
}
```
Observation payload:
```json
{
"observed_at": "2026-09-07T11:59:30Z",
"max_age_seconds": 60,
"source": "systemd-readback.v1",
"actual_artifact_digest": "sha256:<64 hex characters>",
"healthy": true,
"component_id": "sample",
"environment": "staging",
"deployment_id": "deployment-22",
"revision": 4
}
```
`max_age` is accepted as an alias for `max_age_seconds`; `actual_artifact` and `artifact_digest` are accepted aliases for `actual_artifact_digest`. The final four binding fields are optional, but when present they must be well typed and match the target. `healthy` is nullable at the source boundary, but null is evaluated as `UNKNOWN`, never healthy.
Provenance payload:
```json
{
"artifact_digest": "sha256:<64 hex characters>",
"source_digest": "sha256:<64 hex characters>",
"builder_digest": "sha256:<64 hex characters>"
}
```
`artifact_hash`, `source_hash`, and `builder_hash` are accepted aliases. `now` accepts a timezone-aware `datetime` or ISO-8601 string and is injected by tests. Missing, malformed, future, stale, or nullable evidence returns `status/state/verification = UNKNOWN`. A complete incompatible target returns `DRIFT`. Matching contract, target, observation, and provenance returns `status = VERIFIED`, `state = CONVERGED`, and `converged = true`. A repository's current HEAD is deliberately ignored.
### `OperationDefinition.from_dict(data)`
Operation definition payload:
```json
{
"operation_id": "service-status",
"version": 1,
"effect_class": "READ_ONLY",
"handler_ref": "host.service_status",
"input_schema": "aios.operation.service-status.input.v1",
"required_permissions": ["service-status"],
"timeout_seconds": 10,
"readback": {"mode": "NONE", "required": false},
"retry": {"mode": "SAFE", "max_attempts": 2},
"reconcile": {"mode": "NONE"},
"rollback": {"mode": "NONE"},
"owner_boundary": {"mode": "WITHIN_GRANT"}
}
```
`permissions` is accepted as an alias for `required_permissions`. Primary effect classes are `READ_ONLY`, `IDEMPOTENT`, `CONDITIONAL`, `RECONCILABLE`, and `IRREVERSIBLE`. Retry modes are `NEVER`, `SAFE`, `AFTER_READBACK`, and `RECONCILE_FIRST`. Readback, retry, reconcile, rollback, and owner-boundary data remain separate mappings rather than being inferred from the effect class. A service restart must declare required `EXACT` readback and reconciliation. Top-level `command`, `shell`, `argv`, `executable`, and `script` fields are rejected, and handler references cannot contain shell syntax.
### Registry and operation helpers
- `OperationRegistry(definitions).get(operation_id) -> OperationDefinition` performs lookup only and raises `KeyError` for an unregistered ID.
- `OperationRegistry.definitions -> tuple[OperationDefinition, ...]` exposes immutable registration order.
- `operation_timeout_action(definition, outcome="AMBIGUOUS", attempts=1) -> str` returns `RETRY`, `RECONCILE`, or `STOP`. It never retries an irreversible effect and requires reconciliation for an ambiguous mutating outcome when reconciliation/readback exists.
- `verify_service_restart(before, after) -> dict` requires a changed instance identity and a later start timestamp. An active state without restart identity returns `UNKNOWN`.
- `load_production_observation(path, now=None) -> dict` reads a bounded, regular, non-group/world-writable `production-observation.json` bundle with exact keys `contract`, `target`, `observation`, and `provenance`. It projects `ACTIVE` only after `evaluate_deployment` returns `VERIFIED`; otherwise it projects `UNKNOWN` without a baseline version or target OID.
## Reconciliation and snapshot corrections
`ReconciliationController.reconcile` now treats absent, null, wrong-type, or empty evidence as drift requiring reconciliation. This fixes the root cause where every missing pair compared as `None == None` and an empty mapping returned healthy.
`GovernedRuntime.snapshot` no longer embeds `V2R1C7`, `c7adece1cc8beffe7c718d53c775898b45d6c7f8`, or `ACTIVE`. It reads `<state_root>/production-observation.json`; absent or unverified evidence retains the existing three-field production response shape with `deployment_state: UNKNOWN`.
The staging unit now projects the existing encrypted credential ID directly for its own service:
```ini
LoadCredentialEncrypted=gitea-governance-executor-token:/etc/credstore.encrypted/gitea-governance-executor-token
Environment=AIOS_GITEA_TOKEN_FILE=%d/gitea-governance-executor-token
```
The legacy bridge sandbox test now parses both directives and asserts that the projected credential name, encrypted-file basename, and consumer `%d` basename are identical. It also rejects any reference to the governance executor's runtime credential directory.
## Failure-first and verification evidence
Initial Task 3 collection before implementation:
```text
ERROR tests/baseline_v1_1/test_contracts.py
ModuleNotFoundError: No module named 'aios_baseline'
1 error in 0.15s
```
The empty optional groups and permissions-alias tests failed before their implementation:
```text
FAILED test_structural_contract_allows_empty_optional_reference_groups
FAILED test_read_only_definition_accepts_permissions_alias_and_registry_lists_definitions
2 failed, 39 passed in 0.20s
```
The trusted snapshot tests failed before C01 implementation:
```text
ImportError: cannot import name 'load_production_observation'
1 error in 0.28s
```
Focused Task 3, C01, and legacy reconciliation command:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/baseline_v1_1/test_contracts.py tests/test_autonomy_v1_components.py
.................................................................... [100%]
68 passed in 0.30s
```
Existing Bridge/control-plane compatibility command:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/test_action_contract_regressions.py tests/test_bridge_v2_governed_http_bootstrap.py tests/test_control_plane_convergence_contract.py tests/test_canonical_task_projection_recovery.py
............................................ [100%]
44 passed in 0.99s
```
The standalone completed-replay HTTP test originally returned `423` with body error `TASK_PRINCIPAL_WRITES_DISABLED`. Its mutating-operation adapter exposed no `getRuntimeStatus`, and the fixture provided neither the required TaskPrincipal verifier nor an authorization header. The fixture now supplies the exact ready gate facts and authenticated verifier; no production gate was relaxed:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/test_completed_replay_reconciliation_v7.py
........... [100%]
11 passed in 0.60s
```
Fresh final scoped verification across Task 3, C01, Bridge/control-plane compatibility, completed replay, and isolated-writer fixtures:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/baseline_v1_1/test_contracts.py tests/test_autonomy_v1_components.py tests/test_action_contract_regressions.py tests/test_bridge_v2_governed_http_bootstrap.py tests/test_control_plane_convergence_contract.py tests/test_canonical_task_projection_recovery.py tests/test_completed_replay_reconciliation_v7.py tests/test_isolated_writer_exact_path_policy.py tests/test_isolated_writer_policy.py
........................................................................ [ 47%]
................................................................................ [100%]
152 passed, 64 subtests passed in 1.87s
```
`git diff --check` completed with no output for all files in this report.
The isolated-writer legacy subset initially had two pristine-baseline negative-fixture failures. The audience and branch tests did not establish the required `gitops` group lookup, so the unchanged production guard correctly stopped at `isolated_writer_path_policy_group_rejected` before either intended assertion. The fixtures now supply only that valid prerequisite; production guard order and strictness are unchanged:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/test_isolated_writer_exact_path_policy.py tests/test_isolated_writer_policy.py
........................ [ 82%]
..... [100%]
29 passed, 64 subtests passed in 0.40s
```
Focused legacy systemd assertion:
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app python3 -m pytest -q tests/test_r7_atomic_recovery_and_bridge_sandbox.py
.. [100%]
2 passed in 0.04s
```
@@ -0,0 +1,49 @@
# AIOS Implementation Baseline 1.1 Implementation Plan
> For agentic workers: use the approved user architecture and the task interfaces below. Implement continuously, with failure-first tests and scoped review. No production access is part of this package-build task.
Goal: deliver source changes, runnable integration/acceptance, safe migration/package tools and precise deployment instructions for the seven final architecture revisions.
Architecture: extend the existing Task/Job/Release authorities through explicit adapters. RuntimeContract describes stable structure; deployment targets are dynamic Release-owned records. Historical authorization wire formats remain frozen. New Grant and OperationalEpoch are separate objects. Generic operation execution persists intent before effects and uses receiver fencing and reconciliation.
Tech stack: Python 3.11+, SQLite/Git/systemd, existing cryptography and pytest; no new online services or middleware.
Spec: ARCHITECTURE_BASELINE_1_1.md (generated from the approved baseline and current user's seven revisions).
Global constraints:
- The imported ZIP SHA256 is fd28f8f065873c5600f17de0a536cae6f4bc18567687fd3f178d67ad8c1f931a; local snapshot commit is an import identity, not remote main.
- Legacy global writes remain false. No arbitrary remote shell, generated production trust roots, direct production policy DB writes or implicit authority cutover.
- Tenant and subject come from authenticated service context. All durable identity links are tenant-scoped. Existing TaskPlaneStore remains the task authority for the explicit standalone profile; existing official Task API integrations use adapters and activation evidence, not silent replacement.
- Every code task: write behavioral tests, observe expected failure, implement, run covering regression, record output. Package inspection and deployment preflight are read-only by default.
## Task 1: Existing Job reliability
Files: aios_gitops_job_controller.py, aios_gitops_job_executor.py, tests/baseline_v1_1/test_job_safety.py and affected existing fixtures.
Deliver: finite leases/state checks, recoverable reconcile claims/epoch CAS, universal dry-run, complete operation receipt/readback. Unknown effects must not blindly repeat; DB failure latches stop further effects until explicit successful reconciliation. Preserve old campaign authorization limits.
Tests: dry-run mutator never called; expired and terminal lease refused; observer crash/reordering; duplicate execution; intent commit failure; external success followed by receipt storage failure.
## Task 2: Signed authorization chain
Files: aios_baseline/authorization.py; tests/baseline_v1_1/test_authorization.py.
Deliver: signed owner-delegation-grant.v1, new operational-epoch.v1 and task-principal.v1 with root grant digest/generation, persistent grant/revocation state, expiry/UNTIL_REVOKED, narrowing, Ed25519 verified trusted keys. Preserve old v1/v2 modules.
Tests: signature tampering, fake parent Mapping, cross-tenant/subject/task/scope, revoked generation, expired proof auto-refresh under active grant, restart persistence.
## Task 3: Truth and operation contracts
Files: aios_baseline/runtime_contract.py, aios_baseline/operations.py, reconciliation_controller.py; tests/baseline_v1_1/test_contracts.py.
Public interfaces: validate_contract(document)->dict; evaluate_deployment(contract,target,observation,provenance,now)->dict; OperationDefinition.from_dict(data); OperationRegistry(definitions).get(operation_id).
Deliver: structural contract rejects dynamic desired; dynamic target/observation/provenance validation and UNKNOWN, effect class plus orthogonal readback/retry/reversibility policy. No arbitrary command execution in registry.
## Task 4: Task loop and identity linkage
Files: aios_baseline/lineage.py, service.py, worker.py, http_api.py, host.py; existing task_plane through adapters; tests/baseline_v1_1/test_loop.py.
Deliver: use TaskPlaneStore for tasks/claims; authenticate tenant-scoped thin Action; persistent correlation graph; registry-based fallback eligibility at ready_at+600; bounded subprocess host-status operation; cancellation and evidence; no special main-agent branch. Worker heartbeat/transport are real.
Tests: authenticated HTTP submit->claim->real local diagnostic->receipt->result; tenant denial; duplicate submission; restart and fallback; lineage referential constraints.
## Task 5: Release and optimization
Files: aios_baseline/release.py, optimization.py; tests/baseline_v1_1/test_release_optimization.py.
Deliver: Release-owned targets, deployment/acceptance IDs and source lineage, recipe/receipt adapters to governed operations; bounded OptimizationEnvelope, proposal/task-only execution, objective quality floors. No direct policy mutation.
Tests: acceptance wrong deployment rejected; restartable saga; stale target CAS; forbidden optimization; quality regression stops promotion.
## Task 6: Packaging, activation and acceptance
Files: scripts/baseline_v1_1/*, deploy/baseline_v1_1/*, docs/implementation-baseline-1.1/*; tests/baseline_v1_1/test_package_tools.py.
Deliver: manifest with before/after hashes, patch/full source packages, conflict-safe stage/apply/rollback, authority and credential preflight, sanitized config examples, Linux systemd profile, full AT-01..23 matrix with actual statuses. AT-22 uses subprocess crash plus durable separate effect sink; physical power failure is marked online-required. AT-23 exercises SQLite and receipt failures with fail-closed admission.
Tests: clean staging, wrong baseline/tamper refusal, repeated apply, rollback conflict refusal, credential path validation; run integrated regression and public-interface suites.
Rulings:
- Effect class is primary dispatch metadata, with independent retry/readback/rollback properties, since categories overlap.
- Historical owner-delegation-epoch.v2 is not relabeled or reinterpreted. New proof schema refers to the new Grant; migration verifies original bytes.
- Live activation requires observed official authority bindings and valid installed signing identities. Packaging does not claim production activation.
@@ -0,0 +1,146 @@
# AIOS Implementation Baseline 1.1:安装、验证与回滚
本包交付架构修订、可执行代码、隔离验证闭环和源码变更工具。默认运行 `isolated-validation` profile;线上 Task/Job/Release 的实际写权威绑定、真实发布和生产业务验收仍须通过现场门禁。源码包安装完成不能作为生产发布完成的证据。
## 1. 先确认包身份和运行前提
提供的源码来自用户上传 ZIP,原始 ZIP SHA-256 为 `fd28f8f065873c5600f17de0a536cae6f4bc18567687fd3f178d67ad8c1f931a`。本地导入基线为 `f5d015249604ce5bdacfd707608f83e8a3875f60`;这是离线快照身份,不是远端 main 或当前 runtime 的证明。
最终包的顶层路径约定:
| 路径 | 用途 |
| --- | --- |
| `source/aios/` | 完整修改后源码;首先阅读根 README 和本文件 |
| `source-baseline.git.bundle` | 可离线恢复原始导入 commit 的 Git 对象 |
| `source-changes.zip` | 每文件 before/after hash、旧/新内容及 manifest |
| 根目录校验清单与验收结果 | 本次实际生成的包身份及本地测试证据;以根 README 中的实际文件名为准 |
需要 Python 3.11+、Git,以及源码依赖清单中固定版本的 `cryptography`。运行测试需要 `pytest`。真实服务诊断需要 Linux/systemd 以及可读取状态的现存 `.service`。源码包工具只依赖 Python 标准库和 Git。使用包内依赖说明准备独立虚拟环境,不自动升级或覆盖正在运行的 AIOS Python 环境。
SHA-256 校验只证明字节完整性;它不证明 Owner 业务授权。使用可信的交付来源,并保留实际下载包的校验值。不要将测试生成的密钥、token 或 Grant 移入生产信任域。
## 2. 源码隔离部署:先 dry-run,再 stage
以下命令从已解压完整包的顶层执行。路径变量只用于这次安装;所有目标目录必须全新,父目录须存在。
```bash
export AIOS_PACKAGE_ROOT="$PWD"
export AIOS_BASE_REPO="$AIOS_PACKAGE_ROOT/clean-baseline"
export AIOS_STAGE="$AIOS_PACKAGE_ROOT/staged-baseline-1.1"
python3 "$AIOS_PACKAGE_ROOT/source/aios/tools/baseline_v1_1/bundle.py" verify \
"$AIOS_PACKAGE_ROOT/source-changes.zip"
git init "$AIOS_BASE_REPO"
git -C "$AIOS_BASE_REPO" fetch "$AIOS_PACKAGE_ROOT/source-baseline.git.bundle" HEAD
git -C "$AIOS_BASE_REPO" checkout --detach FETCH_HEAD
python3 "$AIOS_PACKAGE_ROOT/source/aios/tools/baseline_v1_1/bundle.py" stage \
"$AIOS_PACKAGE_ROOT/source-changes.zip" \
--source "$AIOS_BASE_REPO" --destination "$AIOS_STAGE" --dry-run
python3 "$AIOS_PACKAGE_ROOT/source/aios/tools/baseline_v1_1/bundle.py" stage \
"$AIOS_PACKAGE_ROOT/source-changes.zip" \
--source "$AIOS_BASE_REPO" --destination "$AIOS_STAGE"
```
`stage` 不修改 `--source`。它校验源仓库 clean、HEAD、所有基线受跟踪文件的 hash/mode,再创建独立副本;应用 manifest 后保留基线 HEAD 和可审查的工作区差异。它拒绝既有目标、未知目标覆盖、符号链接、路径穿越、ZIP 未声明成员、缺失文件、错基线及字节篡改。不会启动 systemd、写线上权威库、启用 legacy global write 或运行仓库脚本。
已有真实仓库与导入基线不一致时,`stage` 拒绝是预期行为。不要使用 `--force`、重写线上 HEAD 或伪造 before hash。将包内变化在真实仓库的独立 worktree 中审查/移植,重新建立该仓库的源码与验收身份,再走原有变更治理链。本工具没有绕过开关。
Git 忽略文件不属于基线内容,不会复制到候选目录。不要在源目录依赖未声明的 overlay、运行态数据库或私有凭证。工具当前以普通文件和可执行普通文件为交付对象,遇到符号链接、Git submodule、异常文件布局或超限包会安全拒绝。
## 3. 执行本地验收
在已准备依赖的独立 Python 环境中运行:
```bash
cd "$AIOS_STAGE"
export PYTHONPATH="$AIOS_STAGE/active-runtime/app"
export PYTHONDONTWRITEBYTECODE=1
python3 -m pytest -q tests/baseline_v1_1
```
本地测试包含真实 SQLite、Ed25519 签名、临时 Git 仓库、HTTP transport 和受限子进程等行为测试;其中可控替身用于模拟远端系统和故障。测试通过不代表已连接用户主机、不代表主机断电与磁盘耗尽已现场复现,也不代表生产业务验收通过。具体 AT-01~AT-23 范围见 `ACCEPTANCE_MATRIX.md` 和本次实际测试结果。
保存测试命令、Python/依赖版本、源码身份、配置摘要和退出码;不要把 token 或私钥放进验收报告。任何源代码或运行配置变化后,旧证据不能无条件用于放行新候选。
## 4. 跑第一条真实非发布闭环
选择当前 Linux 主机中一个已存在、允许当前用户只读查询的服务,并将下方 `YOUR_EXISTING_SERVICE.service` 替换成准确的 unit 名。此步骤只调用固定的 `systemctl show` 状态读取;不重启服务。
```bash
export AIOS_VALIDATION_DIR="$AIOS_PACKAGE_ROOT/validation-run"
python3 -m aios_baseline.cli init-validation \
--directory "$AIOS_VALIDATION_DIR" \
--service-unit YOUR_EXISTING_SERVICE.service
python3 -m aios_baseline.cli preflight --config "$AIOS_VALIDATION_DIR/server.json"
python3 -m aios_baseline.cli grant-import \
--config "$AIOS_VALIDATION_DIR/server.json" --grant "$AIOS_VALIDATION_DIR/grant.json"
```
`init-validation` 仅为新建的本地验证实例生成测试身份和一个最多 20 个 effect、限定 `service-status/sample/staging` 的 Grant。它不会覆盖既有目录。实际部署使用已安装的信任身份和明确签署的业务授权,不复用这里生成的密钥。`preflight` 返回 `CONFIG_VALID` 仅表示配置检查通过,同时会明确 `online_authority=NOT_VERIFIED`、`production_activation=BLOCKED`。
在终端 A 启动服务:
```bash
python3 -m aios_baseline.cli server --config "$AIOS_VALIDATION_DIR/server.json"
```
在已设置相同路径变量和 `PYTHONPATH` 的终端 B 提交目标并运行真实 Worker:
```bash
python3 -m aios_baseline.cli goal-submit \
--token-file "$AIOS_VALIDATION_DIR/user.token" \
--request "$AIOS_VALIDATION_DIR/goal.json" \
--idempotency-key first-service-diagnostic
python3 -m aios_baseline.cli worker \
--config "$AIOS_VALIDATION_DIR/worker.json" --once
python3 -m aios_baseline.cli goal-explain \
--token-file "$AIOS_VALIDATION_DIR/user.token" \
--mission-id validation-mission
```
关联链应包含 Task、effect/job 和经验证的诊断 receipt;这是非发布任务,不应虚构 Release/Deployment/Acceptance。进程完成“诊断”与被诊断服务 `healthy=true` 是两个不同事实。服务不健康时仍须返回真实 observation,不能将诊断执行成功冒充组件健康。
常驻 Worker 使用相同命令去掉 `--once`。聊天中的 GPT 没有此 Worker 的常驻身份、heartbeat 或 transport,不是 fallback worker。将受信 Worker 配置的 `fallback_executor` 设为 true 后,调度按 readiness 时间扩大其 eligibility,仍走同一 CAS、lease 和 fencing。
## 5. systemd 验证实例与现场门禁
候选 unit 位于 `deploy/baseline-v1.1/`,均以 `.example` 结尾。API 与 Worker 使用独立 UID、各自 credential projection、固定 loopback transport 和有限资源。替换 `@RELEASE_ROOT@` 和 `@PYTHON_EXECUTABLE@`(已验收的独立 Python 环境)、配置只读发行目录、既有信任身份与独立状态目录之后,先执行 `systemd-analyze verify` 检查已渲染 unit。详细凭证名称及隔离规则见该目录的 `CREDENTIALS.md`。
本包不会自动创建系统用户、复制凭证、安装或启用 unit。上述操作由实际安装角色在已批准的主机范围内完成。它们是配置绑定与部署行为,不要求重新签署仍有效的业务 Grant;若实际组件、环境、破坏性级别或预算超出授权,才进入真实 Owner 边界。
将 `ACTIVATION_GATES.json` 复制为本次验收记录并逐项填入证据引用。不得把 `NOT_RUN` 批量改成 PASS。生产接入需要另外完成官方 Task authority adapter、Release recipe 的实际受治理 operation adapter、可信业务验收 verifier、单一活跃写权威切换和恢复演练。本地 validation profile 明确拒绝 production 配置。
## 6. 回滚与存储恢复
仅回滚隔离候选源码:
```bash
python3 "$AIOS_PACKAGE_ROOT/source/aios/tools/baseline_v1_1/bundle.py" rollback \
--directory "$AIOS_STAGE" --dry-run
python3 "$AIOS_PACKAGE_ROOT/source/aios/tools/baseline_v1_1/bundle.py" rollback \
--directory "$AIOS_STAGE"
```
工具在首次写入前检查所有受影响文件当前仍为已部署 after hash/mode,并要求 HEAD 未产生后续提交;发现本地修改、符号链接或对象冲突即拒绝,不覆盖后改内容。回滚只恢复包内源码,不撤销远端副作用、不回退数据库、不修改 systemd。执行回滚前停止使用该候选目录的验证进程;对运行中的发行目录不使用本工具原地回滚。
源码回退与业务补偿是不同操作。Release 是否允许回退由目标 artifact、数据兼容性和已验证 rollback operation 决定。存储恢复需先停止新 effect 准入,使用 SQLite backup API 或停止写者后的完整一致副本,同时恢复 Git/receipt/关联身份;只复制活跃 SQLite 主 DB 文件不构成可靠备份。恢复后先对账未决 effect,再恢复调度,记录实测 RPO/RTO。无法可靠记录 intent/lease/receipt 时,不得继续产生新副作用。
## 7. 重新生成变更包
由维护者在受审查的修改后源码仓库中执行;输出必须位于源码目录之外,且不能覆盖既有文件:
```bash
python3 tools/baseline_v1_1/bundle.py create \
--repo "$AIOS_STAGE" \
--base f5d015249604ce5bdacfd707608f83e8a3875f60 \
--output "$AIOS_PACKAGE_ROOT/reviewed-source-changes.zip"
```
输出 manifest 保存每个 added/modified/deleted 文件的 before/after SHA-256 与 mode,并包含精确的旧、新内容。重新生成后须重新验收和记录新包身份;不能沿用旧包的验收结论。
@@ -0,0 +1,91 @@
# Job safety implementation report
## Scope
Task 1 hardens the existing SQLite Job controller and campaign executor. Legacy
campaign, repository, branch, authorization-digest, protected-branch and
machine-identity restrictions remain in force.
## Delivered behavior
- Branch and typed resource leases require an `AUTHORIZED` Job, a timezone-aware
future deadline no more than 300 seconds away, and revision CAS. Effect
receivers recheck the active lease ID, expiry and generation.
- Generic registered operations can persist an `aios.operation-job.v1` binding
in the existing JobStore. Resource generations are allocated monotonically per
`(resource_kind, resource_id)` and are available through
`get_execution_lease()`.
- Generic Jobs use an explicit transition graph. `LOCAL_VERIFIED` and
`COMPLETED` require a request-bound, lease-bound operation receipt accepted by
the mandatory `generic_receipt_verifier`; direct `REQUESTED` to `COMPLETED`
transitions and self-asserted booleans are rejected.
- Reconciliation uses a durable claim ID, incrementing epoch, base revision and
short deadline. An expired `RECONCILING` claim can be taken over; late observer
results fail with `RECONCILIATION_CLAIM_LOST`.
- Every campaign executor operation passes through the same dry-run boundary.
Dry-run performs validation/read-only planning and leaves the Job in
`REQUESTED`; no mutating adapter is called.
- Real execution persists `EXECUTING / INTENT_COMMITTED` before an effect,
checks the lease at the effect boundary, performs exact readback and commits a
receipt with effect ID, operation and observation. An adapter without exact
readback yields `RECONCILIATION_REQUIRED`, `UNKNOWN` and the explicit
`EXACT_READBACK_UNSUPPORTED` blocker; it does not fabricate completion.
- A persistence failure latches the executor against further effects. Durable
in-flight/UNKNOWN Jobs also block new-effect admission after restart through
`assert_effect_admission()`. Explicit reconciliation is the recovery path.
- COMMIT_FILE now restores its content-addressed `payload_ref` and `payload_size`
from the durable request. Its current Authority adapter cannot identify an
unreferenced commit from intent alone, so execution reports the explicit exact
readback blocker instead of risking a duplicate commit.
Campaign readback support is explicit: branch advance reads Authority and Gitea,
branch sync reads Gitea, and main reconciliation reads Authority. PR, status and
merge operations require adapter methods `read_pr`, `read_status` and
`read_merge`; absent methods yield the blocker above before an effect. This is a
safe integration requirement for the current adapters, not a success claim.
## Integration API
For a registered non-Git operation:
1. `submit()` with schema `aios.operation-job.v1` and tenant, subject, mission,
grant, task, effect, operation/version, resource kind/ID and input digest.
2. `advance(job_id, AUTHORIZED, ..., expected_revision=...)`.
3. `lease_resource_job(..., generation=None, lease_expires_at=..., expected_revision=...)`.
4. Read the assigned fence through `get_execution_lease(job_id)`.
5. Call `assert_effect_admission(exclude_job_id=job_id)` before admitting a new
effect and `assert_execution_lease(job_id, lease_id=..., generation=...)`
immediately before the handler.
6. Persist `EXECUTING` intent before the handler and a verified receipt plus
`COMPLETED` state after exact readback. On ambiguity call `reconcile()` with a
read-only observer; do not repeat the handler.
Construct the controller with
`generic_receipt_verifier=<trusted Ed25519 envelope verifier>`. Generic operation
completion is unavailable when this verifier is absent.
## Verification evidence
Failure-first run before implementation: `13 failed` in the new safety suite,
all at the missing clock/reconciliation/executor boundaries.
Latest focused run before this report:
```text
................................................ [100%]
48 passed in 1.13s
```
This combines 16 Task 1 safety tests (including AT-22 subprocess crash and
AT-23 persistence fault injection) with the pre-existing selected 32 Job tests.
No network or production system was used.
The separate completed-replay regression suite reports `11 passed in 1.16s`.
The generic operation loop integration reports `6 passed in 0.55s`.
Final combined run:
```text
................................................................. [100%]
65 passed in 1.71s
```
@@ -0,0 +1,77 @@
# M0–M5 实施手册与退出标准
本手册区分已交付本地代码、尚待接线的现场能力和明确剩余实现。先按 `INSTALLATION.md` 校验包、建立独立 stage 并执行实际测试命令;所有阶段保留源码、配置、授权及证据身份。线上部署仍沿用既有批准的变更边界,不能把 validation profile 当作正式 authority adapter。
当前使用者可立即执行本地 CLI 诊断闭环、测试和源码包工具。标为“剩余实现”的项目必须完成代码及针对性测试;标为“现场绑定”的项目必须提交真实目标证据,不能仅填写配置后标为完成。同一有效 Grant 内的机器证明续期和恢复不重复业务审批。
## M0 — Truth & Identity
**已交付代码:** `runtime_contract.py` 校验静态合同并拒绝动态 desired;目标/观测/provenance 评价;缺失 reconciliation 观测不再默认健康;运行 snapshot 的可信状态修正;消费服务自身 encrypted credential projection;源码 before/after hash 工具。
**输入:** 原始源码/制品身份、组件清单、正式入口、权威角色、命名资源和凭证引用、受信配置来源。
**执行:** 先只读采集 authority map;分别建立 Contract、DeploymentTarget、Observation 和 provenance。核对目标资源一致及新鲜度,显式保留 UNKNOWN。对已渲染配置执行 preflight;检查每个服务自身的凭证投影,按组件登记缺失项。
**剩余实现/现场绑定:** 正式在线 Resolver、按操作 Doctor 和自动修复目录;真实 package/build identity 与运行内容对照;现场已有目标映射到 tenant/component/environment 后的冲突检测。当前固定绑定不是通用 Resolver 已完成。
**退出标准:** 执行者能机器回答对象、入口、desired、observed、artifact/source、CredentialReference 和证据年龄;AT-01/02/13/15 对应范围有证据。main 前进不误判 drift,UNKNOWN 不放行。**回滚:** 撤销候选解析/配置切换,保留旧权威读取;不批量迁移目录,不重写动态 target 来迎合旧观测。
## M1 — Authorization & Effect Safety
**已交付代码:** 新 Grant/OperationalEpoch/Principal 签名链、撤销和 effect 预算;真实资源 lease 及单调 fencing;Job intent/receipt、可接管对账;统一 dry-run;持久未决 effect 阻止新 effect 的恢复门禁。
**输入:** 已安装 Owner 公钥、机器签名身份、明确有效 Grant、OperationDefinition、精确资源及读回接口。
**执行:** 注册已验签 Grant;自动派生收窄 Principal;在同一 intent 下申请真实 lease;执行前再次检查根授权/预算/接收端 fence。验证签名伪造、撤销、过期、scope 重放、CAS 竞争及 dry-run 无副作用;注入成功后 receipt 丢失、存储失败和对账 observer 乱序。
**剩余实现/现场绑定:** 每个真实写 adapter 的 effect-keyed readback 与接收端 fencing;COMMIT_FILE 未引用 commit 的可定位恢复;实际密钥轮换、掉电、ENOSPC/WAL 故障。缺能力时维持显式 blocker,不能自动退回旧 endpoint。
**退出标准:** AT-03~07、17、22、23 的适用项通过;intent 无法提交不执行,未知结果不盲重试。**回滚:** 停止新 effect 准入,保留 Grant 撤销记录、intent 和 receipt,先对账再恢复;代码回退不能删掉未决 Job 或重新解释旧签名。
## M2 — Real Autonomous Loop
**已交付代码:** 认证 HTTP 薄入口、既有 TaskPlaneStore 适配、Worker transport/heartbeat、能力与 fallback 策略、固定参数 Host status executor、签名诊断 receipt、Task 结果和 `goal-explain`。
**输入:** M0/M1 基线、现存可读 `.service`、隔离状态目录、用户/Worker 固定认证身份、显式声明的 Worker scopes。
**执行:** 按安装手册执行 `init-validation → preflight → grant-import`;启动 server,提交 goal,运行 Worker,查询任务及 mission。尽早通过这条真实非发布闭环接线;之后测试重复请求、跨租户访问、取消、超时、重启以及 ready+600 秒 fallback 竞争。
**剩余实现/现场绑定:** 正式线上 Task API adapter;完整 blocked Doctor/修复/升级;目标级总计算和模型费用计量;artifact/cache 隔离;通知投递器/inbox 的故障闭环。现有 effect 数和任务 attempt 预算不是全部成本预算。
**退出标准:** 真实常驻 Worker 经正式 admission/claim 执行已注册诊断并返回可归因 receipt;无伪造 Release;AT-08~12/14 的实施范围清晰。**回滚:** 停止候选 admission/Worker,恢复未决 claim/Job;已有 Task 不因切换丢失,不建立第二套线上 Task 表。
## M3 — Release Closure
**已交付代码:** `LineageLedger` 不可改绑关系;`ReleaseLedger` 动态 target CAS、不可变 recipe、稳定阶段 effect、saga 恢复;独立签名 receipt/acceptance verifier。ReleaseEpoch 语义由 release 身份及发布记录承载,不新增通用超级状态表。
**输入:** 真实 Task/Job/Release 权威绑定、组件部署资源键、受保护 merge/build/deploy 操作、已验签 receipt、业务验收 verifier,以及同一 Grant 覆盖的准确阶段范围。
**执行:** 先核验资源主键并补齐正式适配器。将每个 recipe 阶段接到正式 Job 权威,显式传递 task/grant/effect/job 身份,先持久 effect 后调用;验证 readback、权威 Job 和签名绑定,再推进 saga。执行同 Grant 的 patch→tests→受保护 merge→build→staging→业务验收→production→生产业务验收;至少注入一次 ambiguous publication 或 Worker crash 并恢复。
**剩余实现/现场绑定:** 以上真实 mutation receiver、精确 publication readback、受信验收来源与完整发布链。本包不将历史 HTTP 写端点包成绕过新授权的 adapter;模拟 callback 成功仅证明状态机约束。
**退出标准:** AT-16/17/19/20 有真实链路证据;acceptance 精确绑定 deployment+release+artifact。当前状态为生产发布 **NOT_VERIFIED**。**回滚:** 仅使用预先声明兼容的 rollback release/artifact 与受治理补偿操作;不可逆迁移走前向修复,保留失败部署事实。
## M4 — Surface Reduction
**已交付代码:** 不可改绑身份模型、源码 manifest、隔离 stage/冲突保护 rollback、部署门禁清单。它们支持切换审查,不自动停用线上写者。
**输入:** 实际活跃 writer 清单、旧任务/schema/签名、迁移映射、接入路由、备份和最小恢复入口。
**执行:** 按事实域和资源范围列出当前写入口;先验证新正式入口,再停止旧入口接受新任务,保留旧任务 handler 或进行明确迁移。逐一删除旁路调用、跨服务凭证路径和 production legacy imports。使用 SQLite backup API 或停止写者后的完整一致副本,在隔离环境恢复 SQLite/Git/receipt 并对账未决效果。
**剩余实现/现场绑定:** 实际旧 writers 排空和路由切换、历史任务映射、数据库兼容迁移、整机一致备份与恢复脚本/演练。不能伪造历史缺失的 Task 或 receipt。
**退出标准:** 每类事实 active write authority=1;旧 Task/Release writers 和 legacy 新任务入口均为 0;cross-service credential path、production legacy imports 为 0;AT-19~21 完成并记录实测 RPO/RTO。当前未宣称达标。**回滚:** 仅按已验证迁移/handler 兼容计划恢复单一入口;不同时重开新旧 writers,不将只复制 SQLite 主文件当一致恢复。
## M5 — Optimization
**已交付代码:** 签名 Envelope 校验、封闭低风险参数目录、提案持久化、可信实验 receipt、质量方向/底线判断、无改善停止,以及只生成受治理晋升 Task 的接口。
**输入:** 已稳定通过前述闭环的运行证据、显式 Envelope、基线/候选指标、计算预算、强制测试/质量门、回退计划。
**执行:** 首批选 retry/backoff、有限 Worker routing、证据复用、timeout、poll、通知去重等可测参数。记录提案,提交实验 Task,消费可信结果;只有满足质量底线、预算和最小改善才提交晋升 Task,再按普通治理链落地。
**剩余实现/现场绑定:** 真实实验执行、费用计量、运行参数应用和回退 adapter;模型成本路由及经验适用范围管理。首版不自动重写架构核心代码或生产迁移规则。
**退出标准:** AT-18 通过;授权政策、信任根、隔离、验收标准和 Grant 语义始终不在可优化范围;提案不能直接写 policy/runtime DB。**回滚:** 停止新提案与晋升,按受治理 Task 恢复已记录旧值;保留失败实验和停止原因,不降低验收标准换取成功。
@@ -0,0 +1,105 @@
# Bounded Optimization Implementation Report
## Scope
Baseline 1.1 adds a proposal-only optimization controller. It can record a
candidate and ask the authenticated task service to evaluate or promote it. It
has no adapter for policy, authorization, trust-root, tenant-isolation,
acceptance, destructive-operation, OwnerGrant, production-migration, or runtime
configuration writes.
The implemented public interfaces are:
- `OptimizationEnvelope.from_dict(data, *, verify_envelope)`
- `validate_proposal(envelope, proposal)`
- `OptimizationController(path, envelope, submit_task, *, verify_receipt)`
- `OptimizationController.propose(proposal, *, authenticated_context)`
- `OptimizationController.record_evaluation(proposal_id, evaluation, *, authenticated_context)`
- `OptimizationController.promote(proposal_id, promotion, *, authenticated_context)`
- `OptimizationController.get_proposal(proposal_id, *, authenticated_context)`
## Trust and authority boundaries
Envelope parsing requires a verifier callback. The callback receives the key
ID, canonical unsigned JSON bytes, and signature. A missing verifier, a
verification exception, a non-`True` result, or any post-signature change is
rejected. Semantic validation happens after signature verification and limits a
signed envelope to a built-in set of low-risk parameter paths and hard numeric
ranges. A trusted signer therefore cannot accidentally turn the envelope into a
generic patch or broad capability mechanism.
Task receipts are authenticated through a separate required verifier. The
controller then checks the receipt schema, evaluation task ID, proposal ID,
proposal digest, action, success result, and digest of the complete quality
result. Promotion re-verifies the stored receipt and its quality-result binding.
The authenticated root-service context supplies tenant, subject, grant, and
mission IDs. All reads and state transitions require the same bindings that
created the proposal. The controller includes those IDs in each task request.
The task callback is the only outward effect.
## Bounded behavior
The closed parameter registry covers finite retry, backoff, limited candidate
routing weight, tenant-scoped evidence reuse TTL, operation timeout, poll
interval, notification-deduplication window, and optional-test sampling. Exact
paths are required. Unknown paths and generic code-patch paths are rejected.
Each proposal must remain within its envelope's component, non-production
environment, change-count, compute-budget, and per-parameter bounds. Mandatory
tests and quality gates must be present in both the proposed evaluation and the
verified result. Evidence reuse, when supplied, must match the exact tenant,
component, and environment; it cannot request a global or cross-tenant cache.
A required rollback plan can only reference restoration of previous values and
cannot embed a command.
Direction-aware quality floors support both higher-is-better and
lower-is-better metrics. The SQLite record contains the baseline, candidate,
and normalized effect comparison. A floor violation, regression, missing
mandatory check, budget overrun, or insufficient objective improvement moves
the proposal to a terminal `STOPPED_*` state. Only an `ELIGIBLE` proposal can
submit a promotion task. Promotion still does not apply the candidate directly.
## Durable states
The SQLite workflow uses these states:
1. `PROPOSAL_RECORDED`
2. `EVALUATION_SUBMITTED`
3. `ELIGIBLE` or terminal `STOPPED_*`
4. `PROMOTION_SUBMITTED`
Invalid proposals with a safe, tenant-matching proposal ID are retained as
`STOPPED_INVALID` without calling the task submitter. Proposal IDs are
digest-bound, so reuse with different content is rejected. Evaluation and stop
states cannot be overwritten by later evaluation submissions.
## Test evidence
The failure-first run was:
```text
python3 -m pytest -q tests/baseline_v1_1/test_optimization.py
ERROR tests/baseline_v1_1/test_optimization.py
ModuleNotFoundError: No module named 'aios_baseline.optimization'
```
After implementation and the terminal-state regression fix, the focused run
was:
```text
19 passed in 0.13s
```
The tests cover signature tampering and missing verification, forbidden and
generic paths, numeric limits, mandatory tests and gates, evidence-isolation
scope, identity-bound task submission, invalid-candidate stopping, cross-tenant
and cross-subject denial, untrusted and wrong-task receipts, quality regression,
no improvement, terminal stop behavior, task-only promotion, and restart
persistence of baseline/candidate/effect data.
The separately assigned loopback transport suite also completed with 7 passed.
It covers the real submit-to-receipt HTTP flow, cancel/query/explain, server-side
token identity, tenant-spoof rejection, strict body/deadline/JSON handling,
sanitized failures, redirect and non-loopback rejection, and the interruptible
resident worker loop.
@@ -0,0 +1,228 @@
# Release and Lineage Implementation Report
## Delivered authority boundaries
`aios_baseline.lineage.LineageLedger` is a SQLite relationship authority. It
does not own Task or Job state. Its constructor requires Task and Job authorities, and
every `link_task` resolves the real Task before writing. The returned Task must
carry the requested tenant and subject in its issuer or baseline metadata;
mission, component, and Grant metadata must also match. Every `link_effect`
resolves the durable Job and verifies its tenant, subject, mission, Grant,
Task, and effect binding before recording the relationship.
Opaque Task, effect, Job, Release, deployment, and acceptance IDs are unique
within a tenant. The same opaque ID may exist in another tenant. Composite
foreign keys also bind subject, mission, component, Grant, Release artifact, and deployment at
the SQLite layer. The API treats an exact replay as idempotent and rejects any
attempt to rebind:
- a Task to another subject, mission, Grant, or source Task;
- an effect to another Task or Job;
- a Job to another effect/Task;
- a release-producing effect to a second Release;
- a Deployment to another Release, artifact, target revision, or environment;
- an Acceptance to another Deployment, Release, artifact, tenant, or subject.
`explain(tenant_id, subject_id, mission_id)` uses equality predicates for all
three scope fields. It returns Tasks, effects/Jobs, Releases, Deployments, and
Acceptances only for that exact mission.
`aios_baseline.release.ReleaseLedger` owns dynamic
`EnvironmentDeploymentTarget` records. A target is never written into a
`RuntimeContract`. One target row exists per `(tenant, component, environment)`;
the subject is the authenticated actor recorded for audit. Every target action
passes through a configured resolver with tenant, subject, component,
environment, and action. Multiple authorized subjects therefore share one CAS
revision instead of creating conflicting desired state. `set_target` uses an expected-revision CAS; `None` creates
revision 1 and later writes require the exact current revision. A `STAGED`
target records source and observed-main revisions independently, so main being
ahead of the staged source is valid. A `ROLLBACK_STAGED` target is admitted only
when the current record has an explicit compatible rollback Release and exact
artifact binding. The outer artifact object rejects recursive self-hash fields.
Recipes and their canonical SHA-256 identities are immutable. A saga persists
its recipe identity, component and source Task/effect/Grant, stable per-stage effect ID, effect intent, raw receiver
receipt state, typed verified receipt binding, deployment facts, target
revision, and CAS revision. The adapter must first create or resolve a real
Job via `prepare`; only then does the lineage authority link the effect. An
operation exception leaves `EFFECT_UNKNOWN`. Recovery calls adapter readback,
and missing readback fails with `RECONCILIATION_REQUIRED` without repeating the
effect. A returned but unverified receipt is persisted
as `RECEIPT_UNVERIFIED`; it is reverified rather than causing the effect to be
issued again. Only an injected verifier returning `VerifiedReceipt` with the
exact component, Task, source effect, Grant, Job, effect, operation, tenant,
subject, Deployment, Release, and artifact can
advance a stage. Acceptance follows the same pattern through
`VerifiedAcceptanceEvidence` and includes an explicit `acceptance_id`.
## Public interfaces
```python
LineageLedger(path, task_authority, job_authority)
ledger.link_task(tenant_id, subject_id, mission_id, grant_id, task_id,
source_task_id=None, metadata=None)
ledger.link_effect(tenant_id, subject_id, mission_id, task_id, effect_id,
job_id, source_effect_id=None, metadata=None)
ledger.link_release(tenant_id, subject_id, mission_id, effect_id, release_id,
artifact, metadata=None)
ledger.link_deployment(tenant_id, subject_id, mission_id, release_id,
deployment_id, component_id, environment, target_revision, artifact,
metadata=None)
ledger.record_acceptance(tenant_id, subject_id, mission_id, component_id, acceptance_id,
deployment_id, release_id, artifact, accepted,
evidence=raw_evidence, verifier=trusted_verifier)
ledger.explain(tenant_id, subject_id, mission_id)
ReleaseLedger(path, lineage=ledger, receipt_verifier=trusted_verifier,
target_authorizer=authorization_resolver)
releases.register_recipe(recipe_id, stages)
releases.set_target(tenant_id, subject_id, component_id, environment, release_id, artifact,
expected_revision=revision_or_none, stage="STAGED", ...)
releases.start_saga(tenant_id, subject_id, mission_id, component_id, saga_id, release_id,
deployment_id, environment, artifact, recipe_id,
expected_target_revision=revision)
releases.advance_saga(tenant_id, subject_id, saga_id, expected_revision,
governed_operation_adapter)
releases.get_saga(tenant_id, subject_id, saga_id)
releases.deployment_facts(tenant_id, subject_id, component_id, deployment_id)
```
An artifact argument may be a non-empty string or canonical JSON object. The
object is compared by canonical JSON identity; outer self-hash fields such as
`manifest_sha256` are forbidden.
## Existing Bridge and governance audit
The existing loopback Bridge exposes `/v2/release/artifact`,
`/v2/release/execute`, `/v2/release/consistency`, and
`/v2/release/acceptance`. These endpoints use the historical static tuple
`version`, `target_oid`, `evidence_closure_oid`, and `approval_plan_id` (plus
`execute` where applicable). Static source review found that this protocol is
not a conforming ReleaseLedger operation receiver:
1. The Bridge allowlist rejects the new tenant, subject, mission, component, Grant, Task,
Job, effect, Deployment, target-revision, and idempotency fields.
2. Release routes are outside the Bridge's TaskPrincipal-protected mutating
operation set. They therefore cannot demonstrate the new Grant/Task/Job
authorization chain.
3. Historical release responses are unsigned and are not bound to the stable
saga effect ID, Deployment ID, Release ID, subject, or canonical artifact.
A `status`, `accepted`, or `ACTIVE_VERIFIED` field is not verifier authority.
4. The trusted-plan executor deliberately rejects production execution with
`PRODUCTION_EXECUTION_REQUIRES_APPROVED_RELEASE_EXECUTOR`. The legacy path
invokes its pre-existing privileged production adapter, but it is not routed
through the new Grant/Job/effect protocol and must not be repurposed as that
protocol.
5. The Bridge has no effect-keyed readback endpoint that can reconcile an
unknown ReleaseLedger stage after response loss.
6. Historical acceptance is not bound to an explicit acceptance and
Deployment identity and does not return verifier-authenticated evidence for
the new ledger.
## Configured Ed25519 verification
`aios_baseline.verifiers` provides verification-only factories. Trusted public
keys are supplied by local configuration when the factory is built and copied
into an immutable map. A request cannot add or replace a key. Values may be an
`Ed25519PublicKey`, 32 raw public-key bytes, or an unpadded base64url public key.
```python
from aios_baseline.release import ReleaseLedger
from aios_baseline.verifiers import (
ed25519_acceptance_evidence_verifier,
ed25519_release_receipt_verifier,
)
trusted_release_receivers = {
"staging-receiver-2026": configured_ed25519_public_key,
}
trusted_acceptance_observers = {
"staging-observer-2026": configured_acceptance_public_key,
}
releases = ReleaseLedger(
state_path,
lineage=lineage,
receipt_verifier=ed25519_release_receipt_verifier(
trusted_release_receivers
),
target_authorizer=configured_component_target_authorizer,
)
acceptance_verifier = ed25519_acceptance_evidence_verifier(
trusted_acceptance_observers
)
```
Operation receipts use envelope schema
`aios.release-operation-receipt-envelope.v1` and signature domain
`aios.release-operation-receipt.v1`. Acceptance evidence uses envelope schema
`aios.release-acceptance-evidence-envelope.v1` and the separate signature domain
`aios.release-acceptance-evidence.v1`. The signature input is the ASCII domain,
a zero byte, and strict canonical JSON for the payload. The verifier rejects
unknown envelope/payload fields, unknown key IDs, caller-provided public-key
fields, cross-domain signatures, a non-`SUCCEEDED` operation outcome, a
non-Boolean acceptance decision, and any component/Task/Grant/Job/effect/
Deployment/Release/artifact binding mismatch. This key
set and both domains are separate from the diagnostic `evidence.ReceiptSigner`.
For that reason, this package does not add a loopback HTTP writer around the
historical endpoints. Such a wrapper would create a governance bypass while
appearing to satisfy the new contract. The currently safe, observation-only
projection is:
```json
{
"transport": "loopback-http",
"base_url": "http://127.0.0.1:18892",
"operations": {
"release.consistency.observe": {
"method": "POST",
"path": "/v2/release/consistency",
"request_projection": [
"version",
"target_oid",
"evidence_closure_oid",
"approval_plan_id"
],
"receipt_use": "diagnostic-only-unverified"
}
},
"mutating_operations": {},
"verified_receipt_operations": {}
}
```
A future conforming receiver must admit a new OperationalEpoch/TaskPrincipal,
create or reuse the authoritative Job before the effect, fence idempotency by
`effect_id`, expose effect-keyed readback, and return a signature-verified
receipt over all ReleaseLedger binding fields. That receiver belongs at the
existing Job and approved-release authority boundary, not in this relationship
ledger.
## Verification and honest status
Failure-first execution initially failed at import because the modules did not
exist. After implementation:
- `test_release_lineage.py`: **15 passed**. This uses a local SQLite file and
reopens both ledgers across simulated process boundaries. Task authority and
receipt verifiers are controlled callback fixtures; the operation callback
is a fake configured sink used only to test fencing and verification. Two
additional tests use real Ed25519 keys and cryptographic verification for
valid signatures, signature tampering, cross-domain replay, caller key
injection, decision mismatch, and Deployment mismatch. The valid signed
acceptance is also persisted through the real SQLite ledger path.
- Candidate-schema tests verify independent targets for two components in one
environment, one shared CAS across authorized subjects, denial of an
unauthorized subject, cross-component receipt rejection, refusal to repeat
an unknown effect without readback, and fail-closed rejection of an
unversioned candidate database. These schemas have not been released, so no
implicit legacy migration is provided.
- A targeted local loop test passed with the real `TaskPlaneStore` and
`JobStore.read` authorities, including parsing the durable Job's
`request_json.binding` and checking tenant/subject/mission/Grant/Task/effect.
No Bridge server, remote Git authority, production executor, production health
endpoint, or installed signing identity was exercised. Remote deployment and
acceptance are **NOT_VERIFIED**. No production success or live activation is
claimed.
@@ -0,0 +1,44 @@
# 实际任务闭环、集成边界与审查修正
## 可执行入口
`python -m aios_baseline.cli` 提供 `init-validation`、`preflight`、`grant-import`、`grant-revoke`、`server`、`worker`、`goal-submit`、`goal-explain`、`task-get`、`task-cancel`。真实 transport 是 IPv4 loopback HTTP;身份来自服务端持有的 token 映射。Worker 进程登记、heartbeat、claim、renew、execute,不能提交自行声称成功的 receipt,也不能从请求指定 tenant/subject 或任意命令。
Task/claim/outbox 使用既有 `TaskPlaneStore`;新增 runtime 信息在同一 Task 数据库中,仅含 readiness、blocker、effect/job 关联。Generic Job 使用既有 JobController/JobStore,独立资源围栏按 tenant/component/environment 分配单调 generation。Lineage 查询真实 Task 和 Job 权威后才保存关系;它不创建这些事实。
配置文件、私钥、token 和公钥均通过 no-follow 打开并对实际文件描述符检查所有者、权限、大小;已有非空状态目录必须带精确的隔离权威标识。Owner key 显式绑定 tenant/subject scope。业务 Grant 与机器密钥分离;机器 receipt key 轮换通过受信历史公钥保留旧证据,不能因签名密钥变化就重新执行已完成 effect。
## 第一个纵向闭环的真实范围
`goal-submit → Task admission → HTTP Worker claim → Job intent → Grant/lease/fencing → 受限 systemctl show 子进程 → 签名 receipt → Task completion → goal-explain`。
`service-status` 是内置 READ_ONLY operation,组件绑定必须精确到 `tenant:environment:component`,不会尝试路径、扫描所有凭证或访问 SSH。测试启动真实 HTTP server、独立子进程、SQLite 和 Ed25519;可控 systemctl 程序用于构造稳定观测。现场运行使用 `/usr/bin/systemctl` 与已登记 unit,实际目标健康、systemd 权限及凭证解密仍需现场证据。
“完成诊断”不等于“组件健康”。receipt 保留实际 `healthy` 和 systemd 观测;没有有效观测不会生成成功 receipt。任务查询同时返回 Job state、side-effect state、blocker 和剩余 Task budget。
当前 server 对 host operation 采用有界串行执行,HTTP 并发连接最多 16 个,token 长度统一为 32~512 字符。HTTP deadline 限制执行锁等待并传到 receiver;host 子进程固定 argv、无继承凭证环境、固定可执行文件身份、累计 stdout/stderr 上限 64 KiB、超时杀进程组。单机首次实现采用全 Job 域的保守停止准入策略:未决效果会阻止新的效果;物理存储故障保持全局停止。更细的租户/资源并行度属于后续可测优化,不以牺牲恢复判断换吞吐。
## 本次审查发现并已修复的具体缺陷
| 缺陷 | 修正 | 行为测试 |
| --- | --- | --- |
| 每任务 attempt 从 1 开始,被误用于全资源围栏 | Job 权威按资源分配 generation;旧持有者被拒绝 | 连续两任务围栏代数为 1、2 |
| Generic Job 可直接 REQUESTED→COMPLETED | 显式状态转换、已持久 lease、强制可信 receipt verifier | 越级完成、缺 verifier、改绑均拒绝 |
| 签名正确但 job/mission/grant 等关联错误仍能完成 Task | 对照实际 Job request/lease/Task 逐项验证身份、输入摘要和 observation | 多字段签名错绑拒绝 |
| 取消后恢复可将任务重新完成或置为可重试 | Task 权威在恢复事务内保留终态;heartbeat 也拒绝终态和到期 lease | 已取消 expired claim 不复活 |
| 跨租户安装的 Owner key 自动获得全租户签发权 | 显式 owner_key_scopes,并在每次持久读取验证 | 跨 tenant/subject、重启收窄 trust 均拒绝 |
| 已完成 receipt 的密钥轮换被误判成 effect 未发生 | receipt 带 key_id,显式旧公钥信任;无法验签进入未知而非重做 | 旧证据仅在明确受信时可用 |
| 排队请求可越过客户端 deadline 开始执行 | 有期限的执行锁及 receiver deadline、claim/fence 二次核验 | 过期排队请求不启动 host |
| 子进程输出在读完后才限额 | 增量有界读取,超限立即终止进程组 | 输出洪泛与 binary 被替换时拒绝 |
| 生产 snapshot 固定 ACTIVE 和旧版本 | 验证动态 observation/target/provenance;缺失或过期为 UNKNOWN | 新鲜、错绑、缺失、过期观测 |
| staging 消费另一服务临时 credential 路径 | 每消费者直接投影同名 encrypted credential,使用自身 `%d` | unit 投影身份与独立路径断言 |
| DeploymentTarget 按 subject 分裂、缺组件维度 | tenant/component/environment 资源主键、统一 CAS 与独立授权 | 两组件目标、同组件多操作者和拒权 |
| Release 把计划 Job ID 当事实,未知时可退回 execute | adapter prepare 后查询真实 Job;UNKNOWN 只走 reconcile | 虚假 Job、无 reconcile、错组件验收拒绝 |
## 尚未接入现场的部分
正式 Task API adapter、完整 Doctor 修复/升级、整条真实发布 recipe、独立业务 acceptance executor、目标级模型/费用计量、artifact/cache/工作目录的全部生产路径、旧 writer 排空及整机一致备份恢复,都需要具体现场绑定或剩余适配代码。对应 M0–M5 runbook 与 Release 报告已列出输入、职责和门禁。本包不会通过扩大角色权限、伪造远端回执或启用 legacy global write 来掩盖缺口。
新 Lineage/Release 候选数据库带 schema version;未知或早期无版本候选库明确拒绝。包从新隔离目录启动,不静默修改线上数据库。历史 epoch 签名格式和受保护分支政策保留原语义。
实际统一验收的命令、版本、源码逐文件 SHA、JUnit 和退出码在完整包 `acceptance/`;各模块报告中的早期数量只用于记录开发过程,以最后统一结果为准。
+543
View File
@@ -0,0 +1,543 @@
"""Read-only Phase 2 runtime introspection with a bounded SSH transport.
The wrapper is deliberately sent as ``subprocess.run(input=payload)``. No
JSON, ABI data, or base64 representation is placed in the Windows SSH argv;
the default argv is the fixed, short command documented by
``DEFAULT_REMOTE_COMMAND``.
The remote wrapper reads the current issuer unit's MainPID, WorkingDirectory,
and PYTHONPATH, then resolves only the named importable modules. It does not
walk release trees, ``/opt``, historical worktrees, SOUL, or MEMORY, and it
does not call a database, signer, Bridge, Task API, or mutation endpoint.
This module is locally testable, but the module import and systemd/POSIX
behaviour still require external Linux live validation. Phase 2 status is
always explicit about all mutation classes being false.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence
DEFAULT_SSH = "ssh.exe"
DEFAULT_HOST = "hw"
DEFAULT_REMOTE_COMMAND = 'test "$(hostname)" = cn-network-mkt && sudo -n python3 -'
DEFAULT_TIMEOUT_SECONDS = 60
DEFAULT_ISSUER_UNIT = "aios-task-principal-issuer.service"
DEFAULT_ADMISSION_MODULE = "aios_task_principal_admission"
DEFAULT_MIGRATION_MODULE = "aios_task_principal_storage_migration"
DEFAULT_REQUIRED_ADMISSION_FUNCTIONS = (
"offline_migrate_task_principal_schema",
"supersede_p0_scope",
)
DEFAULT_MIGRATION_VERIFIER = "SSHOwnerBootstrapVerifier"
MAX_ARGV_BYTES = 512
MAX_REQUEST_BYTES = 32 * 1024
MUTATION_FLAGS = (
"task_db_mutation",
"scope_mutation",
"git_mutation",
"service_mutation",
)
class RemoteTransportError(RuntimeError):
"""A safe, non-secret description of an SSH transport failure."""
class Phase2StatusError(RuntimeError):
"""The remote result was not a valid fail-closed Phase2 status."""
def build_ssh_argv(
ssh: str = DEFAULT_SSH,
host: str = DEFAULT_HOST,
) -> list[str]:
"""Return the only SSH argv shape permitted by the Phase2 transport."""
argv = [
ssh,
"-T",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=12",
host,
DEFAULT_REMOTE_COMMAND,
]
argv_bytes = sum(len(part.encode("utf-8")) + 1 for part in argv)
if argv_bytes > MAX_ARGV_BYTES:
raise ValueError("SSH argv exceeds the Phase2 hard limit")
return argv
def run_remote_python(
payload: bytes,
*,
ssh: str = DEFAULT_SSH,
host: str = DEFAULT_HOST,
timeout: float = DEFAULT_TIMEOUT_SECONDS,
runner: Callable[..., Any] = subprocess.run,
) -> bytes:
"""Send *payload* as stdin to the fixed remote Python command.
Error messages intentionally contain no stderr text. SSH stderr can
include command output, environment values, or credentials emitted by a
misconfigured remote service.
"""
if not isinstance(payload, bytes):
raise TypeError("payload must be bytes")
if timeout <= 0:
raise ValueError("timeout must be positive")
argv = build_ssh_argv(ssh=ssh, host=host)
try:
completed = runner(
argv,
input=payload,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise RemoteTransportError(
f"remote SSH timed out after {timeout:g} seconds"
) from exc
except OSError as exc:
raise RemoteTransportError("remote SSH could not be started") from exc
return_code = getattr(completed, "returncode", None)
if return_code != 0:
raise RemoteTransportError(f"remote SSH exited with status {return_code}")
output = getattr(completed, "stdout", b"")
if isinstance(output, str):
output = output.encode("utf-8")
if not isinstance(output, bytes):
raise RemoteTransportError("remote SSH returned a non-byte stdout value")
return output
def module_sha256(path: str | Path) -> str:
"""Hash one resolved module file without searching neighbouring files."""
digest = hashlib.sha256()
with Path(path).open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def validate_public_function_ast(
source: str,
required_functions: Sequence[str] = DEFAULT_REQUIRED_ADMISSION_FUNCTIONS,
) -> dict[str, Any]:
"""Validate public symbols and the source-level export mechanism.
The live admission overlay intentionally re-exports the legacy public API
through a small ``globals()[name] = value`` loop. Requiring every public
function to be a direct ``def`` would therefore reject the exact deployed
module even though the runtime symbol and signature are present. The AST
check still proves that the module has either a direct/imported binding or
an explicit dynamic re-export mechanism; the caller separately verifies
the runtime callable and its signature.
"""
try:
tree = ast.parse(source)
except SyntaxError as exc:
return {
"ok": False,
"missing": list(required_functions),
"error": f"AST_PARSE_ERROR:{exc.msg}",
}
top_level = {
node.name: node
for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and not node.name.startswith("_")
}
imported = {
alias.asname or alias.name.split(".", 1)[0]
for node in tree.body
if isinstance(node, (ast.Import, ast.ImportFrom))
for alias in node.names
if alias.name != "*"
}
dynamic_reexport = any(
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "globals"
for node in ast.walk(tree)
)
bound = set(top_level) | imported
missing = [
name
for name in required_functions
if name not in bound and not dynamic_reexport
]
return {
"ok": not missing,
"missing": missing,
"functions": sorted(top_level),
"imported": sorted(imported),
"dynamic_reexport": dynamic_reexport,
}
def make_phase2_status(
status: str,
errors: Sequence[str] = (),
**details: Any,
) -> dict[str, Any]:
"""Create a status object whose mutation flags are always false."""
result: dict[str, Any] = {
"kind": "phase2-status",
"version": 1,
"status": status,
"read_only": True,
"fail_closed": status != "PASS",
}
result.update({flag: False for flag in MUTATION_FLAGS})
if errors:
result["errors"] = list(errors)
result.update(details)
return result
def validate_phase2_status(status: Mapping[str, Any]) -> dict[str, Any]:
"""Validate the result contract and reject any mutation claim."""
if status.get("kind") != "phase2-status":
raise Phase2StatusError("remote result is not phase2-status")
for flag in MUTATION_FLAGS:
if status.get(flag) is not False:
raise Phase2StatusError(f"remote result has invalid {flag}")
if status.get("status") not in {"PASS", "FAIL"}:
raise Phase2StatusError("remote result has invalid status")
return dict(status)
def _json_request(request: Mapping[str, Any]) -> str:
"""Serialize only a small request into the stdin-delivered wrapper."""
encoded = json.dumps(dict(request), ensure_ascii=True, separators=(",", ":"))
if len(encoded.encode("utf-8")) > MAX_REQUEST_BYTES:
raise ValueError("Phase2 request is unexpectedly large")
return encoded
REMOTE_SCRIPT_PREFIX = r'''# Phase2 read-only runtime wrapper. Do not add mutations here.
import ast
import hashlib
import importlib
import importlib.util
import inspect
import json
import os
import shlex
import subprocess
import sys
from pathlib import Path
sys.dont_write_bytecode = True
REQUEST = json.loads('''
REMOTE_SCRIPT_SUFFIX = r''')
MUTATION_FLAGS = ("task_db_mutation", "scope_mutation", "git_mutation", "service_mutation")
def status(status_name, errors=(), **details):
result = {
"kind": "phase2-status",
"version": 1,
"status": status_name,
"read_only": True,
"fail_closed": status_name != "PASS",
}
result.update({flag: False for flag in MUTATION_FLAGS})
if errors:
result["errors"] = list(errors)
result.update(details)
return result
def systemd_value(unit, property_name):
try:
completed = subprocess.run(
["systemctl", "show", unit, "--property=" + property_name, "--value", "--no-pager"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
timeout=12,
)
except (OSError, subprocess.TimeoutExpired):
raise RuntimeError("SYSTEMD_QUERY_FAILED")
if completed.returncode != 0:
raise RuntimeError("SYSTEMD_QUERY_FAILED")
return completed.stdout.decode("utf-8", "strict").strip()
def parse_pythonpath(environment_value):
for token in shlex.split(environment_value):
if token.startswith("PYTHONPATH="):
return token[len("PYTHONPATH="):].split(os.pathsep)
return []
def runtime_identity():
unit = REQUEST.get("issuer_unit", "aios-task-principal-issuer.service")
pid = REQUEST.get("issuer_pid")
working_directory = REQUEST.get("working_directory")
pythonpath = REQUEST.get("pythonpath")
# Regression callers may pin one identity component while obtaining the
# remaining components from the live unit. In particular, never replace
# an explicitly supplied PID with a newly queried MainPID.
if pid is None:
pid = int(systemd_value(unit, "MainPID"))
if working_directory is None:
working_directory = systemd_value(unit, "WorkingDirectory")
if pythonpath is None:
pythonpath = parse_pythonpath(systemd_value(unit, "Environment"))
if not pid or not working_directory or working_directory == "-":
raise RuntimeError("ISSUER_RUNTIME_IDENTITY_INCOMPLETE")
if isinstance(pythonpath, str):
pythonpath = pythonpath.split(os.pathsep)
return {
"unit": unit,
"main_pid": int(pid),
"working_directory": working_directory,
"pythonpath": [item for item in pythonpath if item],
}
def setup_import_path(identity):
path = [identity["working_directory"]]
path.extend(identity["pythonpath"])
path.extend(item for item in sys.path if item and item not in path)
sys.path[:] = path
def importable_module(module_name):
try:
spec = importlib.util.find_spec(module_name)
if spec is None or not spec.origin or spec.origin in {"built-in", "frozen"}:
raise RuntimeError("MODULE_NOT_IMPORTABLE:" + module_name)
module = importlib.import_module(module_name)
except Exception as exc:
if isinstance(exc, RuntimeError):
raise
raise RuntimeError("MODULE_IMPORT_FAILED:" + module_name) from exc
module_path = getattr(module, "__file__", None)
if not module_path:
raise RuntimeError("MODULE_HAS_NO_FILE:" + module_name)
return module, Path(module_path).resolve()
def hash_file(path):
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 inspect_admission(module, path, required, expected_hash):
source = path.read_text(encoding="utf-8")
tree = ast.parse(source)
nodes = {
node.name: node for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and not node.name.startswith("_")
}
imported = {
alias.asname or alias.name.split(".", 1)[0]
for node in tree.body
if isinstance(node, (ast.Import, ast.ImportFrom))
for alias in node.names
if alias.name != "*"
}
dynamic_reexport = any(
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "globals"
for node in ast.walk(tree)
)
bound = set(nodes) | imported
missing = [
name
for name in required
if name not in bound and not dynamic_reexport
]
if missing:
raise RuntimeError("PUBLIC_SYMBOL_MISSING:" + ",".join(missing))
signatures = {}
for name in required:
value = getattr(module, name, None)
if value is None:
raise RuntimeError("PUBLIC_SYMBOL_NOT_IMPORTABLE:" + name)
try:
signatures[name] = str(inspect.signature(value))
except (TypeError, ValueError):
raise RuntimeError("SIGNATURE_UNAVAILABLE:" + name)
actual_hash = hash_file(path)
if expected_hash and actual_hash != expected_hash:
raise RuntimeError("MODULE_HASH_MISMATCH:admission")
return {
"name": module.__name__,
"path": str(path),
"sha256": actual_hash,
"signatures": signatures,
"ast_functions": sorted(nodes),
"ast_imported": sorted(imported),
"ast_dynamic_reexport": dynamic_reexport,
}
def inspect_migration(module, path, verifier_name, expected_hash):
verifier = getattr(module, verifier_name, None)
if verifier is None:
raise RuntimeError("MIGRATION_VERIFIER_MISSING:" + verifier_name)
try:
signature = str(inspect.signature(verifier))
except (TypeError, ValueError):
raise RuntimeError("MIGRATION_VERIFIER_SIGNATURE_UNAVAILABLE")
actual_hash = hash_file(path)
if expected_hash and actual_hash != expected_hash:
raise RuntimeError("MODULE_HASH_MISMATCH:migration")
return {
"name": module.__name__,
"path": str(path),
"sha256": actual_hash,
"verifier": verifier_name,
"signature": signature,
}
def main():
try:
identity = runtime_identity()
setup_import_path(identity)
admission_name = REQUEST.get("admission_module", "aios_task_principal_admission")
migration_name = REQUEST.get("migration_module", "aios_task_principal_storage_migration")
if migration_name == admission_name:
raise RuntimeError("MIGRATION_MODULE_NOT_DISTINCT")
required = tuple(REQUEST.get("required_admission_functions", (
"offline_migrate_task_principal_schema", "supersede_p0_scope")))
admission, admission_path = importable_module(admission_name)
migration, migration_path = importable_module(migration_name)
admission_info = inspect_admission(
admission,
admission_path,
required,
REQUEST.get("expected_admission_sha256"),
)
migration_info = inspect_migration(
migration,
migration_path,
REQUEST.get("migration_verifier", "SSHOwnerBootstrapVerifier"),
REQUEST.get("expected_migration_sha256"),
)
print(json.dumps(status(
"PASS",
issuer=identity,
modules={"admission": admission_info, "migration": migration_info},
ast_public_functions=list(required),
), sort_keys=True, separators=(",", ":")))
except Exception as exc:
# Emit only stable error classes and never exception text from imports.
error = str(exc).split(":", 1)[0] or "PHASE2_INTROSPECTION_FAILED"
print(json.dumps(status("FAIL", errors=[error]), sort_keys=True, separators=(",", ":")))
if __name__ == "__main__":
main()
'''
def build_remote_payload(request: Mapping[str, Any] | None = None) -> bytes:
"""Build the stdin wrapper; request data never enters SSH argv."""
request = {} if request is None else request
# repr() makes the embedded JSON a valid Python string for every request
# value, including apostrophes and backslashes; it still remains stdin.
request_literal = repr(_json_request(request))
return (REMOTE_SCRIPT_PREFIX + request_literal + REMOTE_SCRIPT_SUFFIX).encode("utf-8")
def _load_request(args: argparse.Namespace) -> dict[str, Any]:
request: dict[str, Any] = {}
if args.request_json:
loaded = json.loads(args.request_json)
if not isinstance(loaded, dict):
raise ValueError("request JSON must be an object")
request.update(loaded)
if args.request_file:
loaded = json.loads(Path(args.request_file).read_text(encoding="utf-8"))
if not isinstance(loaded, dict):
raise ValueError("request JSON must be an object")
request.update(loaded)
overrides = {
"issuer_unit": args.issuer_unit,
"issuer_pid": args.issuer_pid,
"working_directory": args.working_directory,
"pythonpath": args.pythonpath,
"admission_module": args.admission_module,
"migration_module": args.migration_module,
"expected_admission_sha256": args.expected_admission_sha256,
"expected_migration_sha256": args.expected_migration_sha256,
}
request.update({key: value for key, value in overrides.items() if value is not None})
return request
def _write_status(path: str | Path, status: Mapping[str, Any]) -> None:
destination = Path(path)
destination.write_text(
json.dumps(dict(status), ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--request-json")
parser.add_argument("--request-file")
parser.add_argument("--output", default="phase2-status.json")
parser.add_argument("--ssh", default=DEFAULT_SSH)
parser.add_argument("--host", default=DEFAULT_HOST)
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SECONDS)
parser.add_argument("--issuer-unit")
parser.add_argument("--issuer-pid", type=int)
parser.add_argument("--working-directory")
parser.add_argument("--pythonpath", nargs="*")
parser.add_argument("--admission-module")
parser.add_argument("--migration-module")
parser.add_argument("--expected-admission-sha256")
parser.add_argument("--expected-migration-sha256")
args = parser.parse_args(argv)
try:
request = _load_request(args)
payload = build_remote_payload(request)
stdout = run_remote_python(
payload,
ssh=args.ssh,
host=args.host,
timeout=args.timeout,
)
remote_status = json.loads(stdout.decode("utf-8"))
status = validate_phase2_status(remote_status)
except (OSError, ValueError, json.JSONDecodeError, RemoteTransportError, Phase2StatusError) as exc:
status = make_phase2_status("FAIL", errors=[str(exc).split(":", 1)[0]])
_write_status(args.output, status)
return 1
_write_status(args.output, status)
return 0 if status["status"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
+666
View File
@@ -0,0 +1,666 @@
from __future__ import annotations
import base64
import copy
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from aios_baseline.authorization import (
AuthorizationError,
AuthorizationService,
sign_owner_delegation_grant,
)
from aios_baseline.operations import OperationDefinition
class Clock:
def __init__(self, value: int = 2_000_000_000) -> None:
self.value = value
def __call__(self) -> int:
return self.value
def grant(
owner_key: Ed25519PrivateKey,
clock: Clock,
*,
grant_id: str = "grant-0001",
generation: int = 1,
tenant_id: str = "tenant-a",
subject_id: str = "owner-a",
task_ids: tuple[str, ...] = ("task-a",),
operations: tuple[str, ...] = ("deploy",),
expires_at: int | None = None,
max_effects: int | None = None,
destructive_ceiling: str = "NONE",
) -> dict:
return sign_owner_delegation_grant(
owner_key,
"owner-key-1",
grant_id=grant_id,
generation=generation,
tenant_id=tenant_id,
subject_id=subject_id,
mission_ids=("mission-a",),
task_ids=task_ids,
operations=operations,
component_ids=("component-a",),
environments=("staging",),
issued_at=clock(),
validity_mode="EXPIRES_AT" if expires_at is not None else "UNTIL_REVOKED",
expires_at=expires_at,
max_effects=max_effects,
destructive_ceiling=destructive_ceiling,
)
def service(tmp_path, owner_key: Ed25519PrivateKey, machine_key: Ed25519PrivateKey, clock: Clock, **kwargs):
return AuthorizationService(
tmp_path / "authorization.sqlite3",
{"owner-key-1": owner_key.public_key()},
machine_key,
"machine-key-1",
owner_key_scopes={
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": ["owner-a"],
}
},
clock=clock,
**kwargs,
)
SCOPE = {
"tenant_id": "tenant-a",
"subject_id": "owner-a",
"mission_id": "mission-a",
"task_id": "task-a",
"operation": "deploy",
"component_id": "component-a",
"environment": "staging",
}
def test_signed_chain_authorizes_exact_narrowed_scope(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
signed_grant = grant(owner_key, clock)
registered = auth.register_grant(signed_grant)
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert registered == signed_grant
assert principal["schema"] == "aios.task-principal.v1"
assert principal["operational_epoch"]["schema"] == "aios.operational-epoch.v1"
assert principal["payload"]["root_grant_digest"] == auth.grant_digest(signed_grant)
assert principal["payload"]["root_grant_generation"] == 1
assert isinstance(principal["payload"]["credential_id"], str)
assert isinstance(principal["payload"]["nonce"], str)
assert auth.authorize(principal, **SCOPE) == signed_grant
@pytest.mark.parametrize(
("target", "field", "replacement"),
[
("grant", "tenant_id", "tenant-b"),
("epoch", "task_id", "task-b"),
("principal", "operation", "delete"),
],
)
def test_any_signature_tampering_is_rejected(tmp_path, target, field, replacement):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
signed_grant = grant(owner_key, clock)
if target == "grant":
tampered = copy.deepcopy(signed_grant)
tampered["payload"][field] = replacement
with pytest.raises(AuthorizationError, match="signature"):
auth.register_grant(tampered)
return
auth.register_grant(signed_grant)
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
tampered = copy.deepcopy(principal)
if target == "epoch":
tampered["operational_epoch"]["payload"][field] = replacement
else:
tampered["payload"][field] = replacement
with pytest.raises(AuthorizationError, match="signature|parent"):
auth.authorize(tampered, **SCOPE)
def test_unknown_owner_key_and_unsigned_grant_are_rejected(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
unknown = grant(owner_key, clock)
unknown["signing"]["key_id"] = "attacker-key"
with pytest.raises(AuthorizationError, match="trusted owner key"):
auth.register_grant(unknown)
with pytest.raises(AuthorizationError, match="fields"):
auth.register_grant(grant(owner_key, clock)["payload"])
def test_fake_mapping_parent_and_parent_digest_substitution_are_rejected(tmp_path):
class FakeParent(dict):
pass
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
fake = copy.deepcopy(principal)
fake["operational_epoch"] = FakeParent(fake["operational_epoch"])
with pytest.raises(AuthorizationError, match="plain object"):
auth.authorize(fake, **SCOPE)
other = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
substituted = copy.deepcopy(principal)
substituted["operational_epoch"] = other["operational_epoch"]
with pytest.raises(AuthorizationError, match="parent|signature"):
auth.authorize(substituted, **SCOPE)
@pytest.mark.parametrize(
("field", "wrong"),
[
("tenant_id", "tenant-b"),
("subject_id", "owner-b"),
("mission_id", "mission-b"),
("task_id", "task-b"),
("operation", "delete"),
("component_id", "component-b"),
("environment", "production"),
],
)
def test_authorize_denies_cross_scope_use(tmp_path, field, wrong):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
attempted = dict(SCOPE)
attempted[field] = wrong
with pytest.raises(AuthorizationError, match="scope"):
auth.authorize(principal, **attempted)
def test_principal_issuance_must_narrow_root_scope(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock, task_ids=("task-a", "task-b")))
denied = dict(SCOPE)
denied["task_id"] = "task-c"
with pytest.raises(AuthorizationError, match="grant scope"):
auth.issue_principal("grant-0001", ttl_seconds=30, **denied)
narrowed = dict(SCOPE)
narrowed["task_id"] = "task-b"
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **narrowed)
assert auth.authorize(principal, **narrowed)["payload"]["grant_id"] == "grant-0001"
def test_revocation_is_durable_and_checked_during_proof_lifetime(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
trusted_context = object()
verifier = lambda value: value is trusted_context
auth = service(
tmp_path,
owner_key,
machine_key,
clock,
control_context_verifier=verifier,
)
auth.register_grant(grant(owner_key, clock))
principal = auth.issue_principal("grant-0001", ttl_seconds=60, **SCOPE)
with pytest.raises(AuthorizationError, match="trusted control context"):
auth.revoke_grant("grant-0001", control_context=object())
assert auth.revoke_grant("grant-0001", control_context=trusted_context) == 2
with pytest.raises(AuthorizationError, match="generation|revoked"):
auth.authorize(principal, **SCOPE)
restarted = service(
tmp_path,
owner_key,
machine_key,
clock,
control_context_verifier=verifier,
)
with pytest.raises(AuthorizationError, match="generation|revoked"):
restarted.authorize(principal, **SCOPE)
def test_expired_proof_refreshes_without_owner_reapproval_and_survives_restart(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
signed_grant = grant(owner_key, clock)
auth.register_grant(signed_grant)
expired = auth.issue_principal("grant-0001", ttl_seconds=5, **SCOPE)
clock.value += 5
with pytest.raises(AuthorizationError, match="expired"):
auth.authorize(expired, **SCOPE)
restarted = service(tmp_path, owner_key, machine_key, clock)
refreshed = restarted.issue_principal("grant-0001", ttl_seconds=5, **SCOPE)
assert refreshed != expired
assert restarted.authorize(refreshed, **SCOPE) == signed_grant
def test_explicit_grant_expiry_caps_child_lease_and_cannot_be_refreshed(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock, expires_at=clock() + 7))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert principal["payload"]["expires_at"] == clock() + 7
assert principal["operational_epoch"]["payload"]["expires_at"] == clock() + 7
clock.value += 7
with pytest.raises(AuthorizationError, match="expired"):
auth.authorize(principal, **SCOPE)
with pytest.raises(AuthorizationError, match="expired"):
auth.issue_principal("grant-0001", ttl_seconds=5, **SCOPE)
def test_higher_owner_signed_generation_reactivates_but_stale_proofs_stay_invalid(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
control = object()
auth = service(
tmp_path,
owner_key,
machine_key,
clock,
control_context_verifier=lambda value: value is control,
)
auth.register_grant(grant(owner_key, clock))
old = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
auth.revoke_grant("grant-0001", control_context=control)
new_grant = grant(owner_key, clock, generation=3)
auth.register_grant(new_grant)
fresh = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
with pytest.raises(AuthorizationError, match="generation"):
auth.authorize(old, **SCOPE)
assert auth.authorize(fresh, **SCOPE) == new_grant
def test_strict_schema_types_and_signature_encoding(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
malformed = grant(owner_key, clock)
malformed["payload"]["generation"] = True
with pytest.raises(AuthorizationError, match="generation"):
auth.register_grant(malformed)
malformed = grant(owner_key, clock)
malformed["signing"]["signature"] = base64.b64encode(b"short").decode("ascii")
with pytest.raises(AuthorizationError, match="signature"):
auth.register_grant(malformed)
with pytest.raises(AuthorizationError, match="ttl_seconds"):
auth.register_grant(grant(owner_key, clock))
auth.issue_principal("grant-0001", ttl_seconds=True, **SCOPE)
def test_grant_helper_requires_an_explicit_validity_mode():
owner_key = Ed25519PrivateKey.generate()
clock = Clock()
arguments = {
"grant_id": "grant-0001",
"generation": 1,
"tenant_id": "tenant-a",
"subject_id": "owner-a",
"mission_ids": ("mission-a",),
"task_ids": ("task-a",),
"operations": ("deploy",),
"component_ids": ("component-a",),
"environments": ("staging",),
"issued_at": clock(),
}
with pytest.raises(TypeError):
sign_owner_delegation_grant(owner_key, "owner-key-1", **arguments)
with pytest.raises(AuthorizationError, match="expires_at"):
sign_owner_delegation_grant(
owner_key,
"owner-key-1",
validity_mode="EXPIRES_AT",
expires_at=None,
**arguments,
)
string_scope = dict(arguments)
string_scope["mission_ids"] = "abc"
with pytest.raises(AuthorizationError, match="mission_ids.*list or tuple"):
sign_owner_delegation_grant(
owner_key,
"owner-key-1",
validity_mode="UNTIL_REVOKED",
**string_scope,
)
def test_effect_budget_reserves_each_stable_effect_id_once_and_persists(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock, max_effects=1))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert auth.reserve_effect("grant-0001", "effect-0001", principal, **SCOPE) is True
assert auth.reserve_effect("grant-0001", "effect-0001", principal, **SCOPE) is False
restarted = service(tmp_path, owner_key, machine_key, clock)
with pytest.raises(AuthorizationError, match="effect budget"):
restarted.reserve_effect("grant-0001", "effect-0002", principal, **SCOPE)
def test_unbounded_grant_leaves_mission_budget_to_caller(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert auth.reserve_effect("grant-0001", "effect-0001", principal, **SCOPE) is True
assert auth.reserve_effect("grant-0001", "effect-0002", principal, **SCOPE) is True
def test_old_machine_proof_can_be_verified_during_machine_key_rotation(tmp_path):
owner_key = Ed25519PrivateKey.generate()
old_machine_key = Ed25519PrivateKey.generate()
new_machine_key = Ed25519PrivateKey.generate()
clock = Clock()
original = service(tmp_path, owner_key, old_machine_key, clock)
signed_grant = grant(owner_key, clock)
original.register_grant(signed_grant)
old_principal = original.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
rotated = AuthorizationService(
tmp_path / "authorization.sqlite3",
{"owner-key-1": owner_key.public_key()},
new_machine_key,
"machine-key-2",
owner_key_scopes={
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": ["owner-a"],
}
},
clock=clock,
trusted_machine_public_keys={
"machine-key-1": old_machine_key.public_key(),
"machine-key-2": new_machine_key.public_key(),
},
)
assert rotated.authorize(old_principal, **SCOPE) == signed_grant
def test_revocation_generation_cannot_reset_budget_or_revive_immutable_grant(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
control = object()
auth = service(
tmp_path,
owner_key,
machine_key,
clock,
control_context_verifier=lambda value: value is control,
)
signed_grant = grant(owner_key, clock, max_effects=1)
auth.register_grant(signed_grant)
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert auth.reserve_effect("grant-0001", "effect-0001", principal, **SCOPE) is True
assert auth.revoke_grant("grant-0001", control_context=control) == 2
with pytest.raises(AuthorizationError, match="stale"):
auth.register_grant(signed_grant)
with pytest.raises(AuthorizationError, match="revoked|generation"):
auth.reserve_effect("grant-0001", "effect-0002", principal, **SCOPE)
with pytest.raises(AuthorizationError, match="revoked"):
auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
def test_rejects_nan_empty_identity_and_untrusted_retired_machine_key(tmp_path):
owner_key = Ed25519PrivateKey.generate()
old_machine_key = Ed25519PrivateKey.generate()
new_machine_key = Ed25519PrivateKey.generate()
clock = Clock()
original = service(tmp_path, owner_key, old_machine_key, clock)
signed_grant = grant(owner_key, clock)
original.register_grant(signed_grant)
old_principal = original.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
malformed = grant(owner_key, clock)
malformed["payload"]["issued_at"] = float("nan")
with pytest.raises(AuthorizationError, match="issued_at"):
original.register_grant(malformed)
malformed = grant(owner_key, clock)
malformed["payload"]["tenant_id"] = ""
with pytest.raises(AuthorizationError, match="tenant_id"):
original.register_grant(malformed)
rotated_without_old_trust = AuthorizationService(
tmp_path / "authorization.sqlite3",
{"owner-key-1": owner_key.public_key()},
new_machine_key,
"machine-key-2",
owner_key_scopes={
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": ["owner-a"],
}
},
clock=clock,
)
with pytest.raises(AuthorizationError, match="trusted machine key"):
rotated_without_old_trust.authorize(old_principal, **SCOPE)
def operation_definition(effect_class: str, *, reversible: bool = False) -> OperationDefinition:
return OperationDefinition.from_dict(
{
"operation_id": "deploy",
"version": 1,
"effect_class": effect_class,
"handler_ref": "trusted.deploy",
"input_schema": "aios.deploy.input.v1",
"required_permissions": ["deploy"],
"timeout_seconds": 10,
"readback": {"mode": "EXACT", "required": True},
"retry": {"mode": "AFTER_READBACK", "max_attempts": 1},
"reconcile": {"mode": "EXACT_READBACK"},
"rollback": {"mode": "OPERATION", "operation_id": "deploy.rollback"}
if reversible
else {"mode": "NONE"},
"owner_boundary": {"mode": "WITHIN_GRANT"},
}
)
def test_signed_destructive_ceiling_defaults_none_and_denies_mutating_definition(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
signed_grant = grant(owner_key, clock)
assert signed_grant["payload"]["destructive_ceiling"] == "NONE"
auth.register_grant(signed_grant)
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert auth.assert_effect_bounds(
principal, operation_definition("READ_ONLY"), **SCOPE
) == signed_grant
with pytest.raises(AuthorizationError, match="destructive ceiling"):
auth.assert_effect_bounds(
principal, operation_definition("IDEMPOTENT"), **SCOPE
)
def test_reversible_ceiling_requires_rollback_and_irreversible_ceiling_is_explicit(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
auth.register_grant(grant(owner_key, clock, destructive_ceiling="REVERSIBLE"))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
with pytest.raises(AuthorizationError, match="rollback"):
auth.assert_effect_bounds(
principal, operation_definition("RECONCILABLE", reversible=False), **SCOPE
)
assert auth.assert_effect_bounds(
principal, operation_definition("RECONCILABLE", reversible=True), **SCOPE
)["payload"]["destructive_ceiling"] == "REVERSIBLE"
with pytest.raises(AuthorizationError, match="irreversible"):
auth.assert_effect_bounds(
principal, operation_definition("IRREVERSIBLE", reversible=False), **SCOPE
)
owner_key_2 = Ed25519PrivateKey.generate()
auth_2 = AuthorizationService(
tmp_path / "authorization-2.sqlite3",
{"owner-key-1": owner_key_2.public_key()},
machine_key,
"machine-key-1",
owner_key_scopes={
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": ["owner-a"],
}
},
clock=clock,
)
auth_2.register_grant(grant(owner_key_2, clock, destructive_ceiling="IRREVERSIBLE"))
principal_2 = auth_2.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
assert auth_2.assert_effect_bounds(
principal_2, operation_definition("IRREVERSIBLE"), **SCOPE
)["payload"]["destructive_ceiling"] == "IRREVERSIBLE"
def test_destructive_ceiling_is_signed_and_strictly_enumerated(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
auth = service(tmp_path, owner_key, machine_key, clock)
tampered = grant(owner_key, clock)
tampered["payload"]["destructive_ceiling"] = "IRREVERSIBLE"
with pytest.raises(AuthorizationError, match="signature"):
auth.register_grant(tampered)
with pytest.raises(AuthorizationError, match="destructive_ceiling"):
grant(owner_key, clock, destructive_ceiling="UNBOUNDED")
def test_owner_key_trust_is_explicitly_bound_to_tenant_and_subject(tmp_path):
owner_key = Ed25519PrivateKey.generate()
machine_key = Ed25519PrivateKey.generate()
clock = Clock()
with pytest.raises(TypeError, match="owner_key_scopes"):
AuthorizationService(
tmp_path / "missing-scopes.sqlite3",
{"owner-key-1": owner_key.public_key()},
machine_key,
"machine-key-1",
clock=clock,
)
auth = service(tmp_path, owner_key, machine_key, clock)
with pytest.raises(AuthorizationError, match="Owner key scope"):
auth.register_grant(grant(owner_key, clock, tenant_id="tenant-b"))
with pytest.raises(AuthorizationError, match="Owner key scope"):
auth.register_grant(grant(owner_key, clock, subject_id="owner-b"))
auth.register_grant(grant(owner_key, clock))
principal = auth.issue_principal("grant-0001", ttl_seconds=30, **SCOPE)
reopened_with_narrower_trust = AuthorizationService(
tmp_path / "authorization.sqlite3",
{"owner-key-1": owner_key.public_key()},
machine_key,
"machine-key-1",
owner_key_scopes={
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": ["owner-b"],
}
},
clock=clock,
)
with pytest.raises(AuthorizationError, match="Owner key scope"):
reopened_with_narrower_trust.authorize(principal, **SCOPE)
globally_scoped = AuthorizationService(
tmp_path / "explicit-global.sqlite3",
{"owner-key-1": owner_key.public_key()},
machine_key,
"machine-key-1",
owner_key_scopes={
"owner-key-1": {"tenant_ids": ["*"], "subject_ids": ["*"]}
},
clock=clock,
)
assert globally_scoped.register_grant(
grant(owner_key, clock, tenant_id="tenant-b", subject_id="owner-b")
)["payload"]["tenant_id"] == "tenant-b"
@pytest.mark.parametrize(
"scopes",
[
{},
{"owner-key-1": {"tenant_ids": ["tenant-a"]}},
{
"owner-key-1": {
"tenant_ids": ["*", "tenant-a"],
"subject_ids": ["owner-a"],
}
},
{
"owner-key-1": {
"tenant_ids": ["tenant-a"],
"subject_ids": [],
}
},
],
)
def test_owner_key_scope_configuration_is_exact_and_fail_closed(tmp_path, scopes):
with pytest.raises(AuthorizationError, match="owner_key_scopes"):
AuthorizationService(
tmp_path / "authorization.sqlite3",
{"owner-key-1": Ed25519PrivateKey.generate().public_key()},
Ed25519PrivateKey.generate(),
"machine-key-1",
owner_key_scopes=scopes,
)
@@ -0,0 +1,73 @@
from __future__ import annotations
from unittest import mock
import pytest
from task_plane.api import TaskPlaneService
from task_plane.errors import ConflictError, LockedError
from task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
from task_plane.store import TaskPlaneStore
IDENTITY = "spiffe://aios/agent/heartbeat"
WORKER_ID = "heartbeat-worker"
def claimed_task():
store = TaskPlaneStore(":memory:")
service = TaskPlaneService(
store,
policy=PolicyEngine(
execution_enabled=True,
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)},
),
)
service.register_worker(
{
"worker_id": WORKER_ID,
"worker_type": "test",
"identity": IDENTITY,
"capabilities": ["code.read"],
"trust_zone": "test",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
task = service.create_task(
{"task_id": "task-heartbeat", "required_capabilities": ["code.read"]}
)
claim = service.claim(
task["task_id"],
{
"worker_id": WORKER_ID,
"actor_identity": IDENTITY,
"lease_seconds": 30,
},
)
return store, task, claim
def test_cancelled_task_cannot_renew_a_still_active_claim():
store, task, claim = claimed_task()
original_lease = store.get_claim(claim["claim_id"]).lease_until
current = store.get_task(task["task_id"])
store.cancel_task_with_event(task["task_id"], current.revision, "owner cancelled")
with pytest.raises(ConflictError, match="task is terminal"):
store.heartbeat_claim(task["task_id"], claim["claim_id"], 60)
assert store.get_claim(claim["claim_id"]).lease_until == original_lease
assert store.get_task(task["task_id"]).status == "CANCELLED"
def test_lease_at_current_time_is_expired_and_cannot_be_renewed():
store, task, claim = claimed_task()
boundary = store.get_claim(claim["claim_id"]).lease_until
with mock.patch("task_plane.store._now", return_value=boundary):
with pytest.raises(LockedError, match="lease expired"):
store.heartbeat_claim(task["task_id"], claim["claim_id"], 30)
assert store.get_claim(claim["claim_id"]).lease_until == boundary
+43
View File
@@ -0,0 +1,43 @@
import json
from pathlib import Path
import pytest
from aios_baseline.cli import initialize_validation, load_configuration, build_service
def test_generated_validation_configuration_is_usable_and_not_production(tmp_path):
root=tmp_path/'validation'
initialize_validation(root,unit='sample.service')
config,tokens=load_configuration(root/'server.json')
assert config['profile']=='isolated-validation'
assert len(tokens)==2
assert not (root/'state').exists() # preflight is read only
service=build_service(config)
assert service.jobs.store is not None
assert (root/'owner-private.pem').stat().st_mode & 0o077 == 0
raw=json.loads((root/'server.json').read_text())
raw['profile']='production'
(root/'server.json').write_text(json.dumps(raw))
with pytest.raises(ValueError,match='PROFILE'): load_configuration(root/'server.json')
def test_config_cannot_register_duplicate_tokens_or_expand_secret_variables(tmp_path,monkeypatch):
root=tmp_path/'validation'
initialize_validation(root,unit='sample.service')
raw=json.loads((root/'server.json').read_text())
raw['identities'][1]['token_file']=raw['identities'][0]['token_file']
(root/'server.json').write_text(json.dumps(raw))
with pytest.raises(ValueError,match='TOKEN'): load_configuration(root/'server.json')
raw['identities'][1]['token_file']='$HOME/token'
(root/'server.json').write_text(json.dumps(raw))
with pytest.raises(ValueError,match='PATH'): load_configuration(root/'server.json')
def test_config_rejects_symlink_public_config_and_untrusted_state_marker(tmp_path):
root=tmp_path/'validation'
initialize_validation(root,unit='sample.service')
alias=tmp_path/'alias.json';alias.symlink_to(root/'server.json')
with pytest.raises(OSError): load_configuration(alias)
config,_=load_configuration(root/'server.json')
state=root/'state';state.mkdir(mode=0o700)
(state/'ISOLATED_VALIDATION_AUTHORITY').write_text('different-authority\n')
with pytest.raises(ValueError,match='MARKER'): build_service(config)
+460
View File
@@ -0,0 +1,460 @@
from __future__ import annotations
from copy import deepcopy
from datetime import datetime, timezone
import pytest
from aios_baseline.operations import (
OperationDefinition,
OperationRegistry,
operation_timeout_action,
verify_service_restart,
)
from aios_baseline.runtime_contract import evaluate_deployment, validate_contract
from aios_chatgpt_bridge_v2_server import (
GovernedRuntime,
RuntimeConfig,
load_production_observation,
)
from reconciliation_controller import ReconciliationController
NOW = datetime(2026, 9, 7, 12, 0, tzinfo=timezone.utc)
DIGEST = "sha256:" + "a" * 64
SOURCE_DIGEST = "sha256:" + "b" * 64
BUILDER_DIGEST = "sha256:" + "c" * 64
def runtime_contract() -> dict:
return {
"contract_schema": "aios.runtime-contract.v1",
"contract_revision": 3,
"component_id": "task-plane",
"environment": "staging",
"tenant_scope": "tenant-a",
"lifecycle": "active",
"entrypoints": {
"task_api": {
"component_id": "task-plane",
"environment": "staging",
"resolver_ref": "service://task-plane/task-api",
}
},
"bindings": {
"service": {
"component_id": "task-plane",
"environment": "staging",
"resolver_ref": "service://task-plane/worker",
}
},
"resource_refs": {
"state_root": {
"component_id": "task-plane",
"environment": "staging",
"path_ref": "runtime/state/task-plane",
}
},
"credential_refs": {
"task_api": {
"component_id": "task-plane",
"environment": "staging",
"credential_ref": "systemd-credential:task-plane-api",
}
},
"operations": ["host.status.v1", "task.claim.v1"],
}
def deployment_target() -> dict:
return {
"component_id": "task-plane",
"environment": "staging",
"release_id": "release-17",
"deployment_id": "deployment-22",
"artifact_digest": DIGEST,
"revision": 4,
}
def observation() -> dict:
return {
"component_id": "task-plane",
"environment": "staging",
"deployment_id": "deployment-22",
"observed_at": "2026-09-07T11:59:30Z",
"max_age_seconds": 60,
"source": "systemd-readback.v1",
"actual_artifact_digest": DIGEST,
"revision": 4,
"healthy": True,
}
def provenance() -> dict:
return {
"artifact_digest": DIGEST,
"source_digest": SOURCE_DIGEST,
"builder_digest": BUILDER_DIGEST,
}
def operation(**updates: object) -> dict:
data = {
"operation_id": "service.restart.v1",
"version": 1,
"effect_class": "RECONCILABLE",
"handler_ref": "host.service_restart",
"input_schema": "aios.operation.service-restart.input.v1",
"required_permissions": ["host:service:restart"],
"timeout_seconds": 30,
"readback": {"mode": "EXACT", "required": True},
"retry": {"mode": "AFTER_READBACK", "max_attempts": 2},
"reconcile": {"mode": "EXACT_READBACK"},
"rollback": {"mode": "OPERATION", "operation_id": "service.restore.v1"},
"owner_boundary": {"mode": "WITHIN_GRANT"},
}
data.update(updates)
return data
def test_structural_contract_is_normalized_without_dynamic_target() -> None:
result = validate_contract(runtime_contract())
assert result["component_id"] == "task-plane"
assert result["contract_revision"] == 3
assert result["operations"] == ["host.status.v1", "task.claim.v1"]
def test_structural_contract_allows_empty_optional_reference_groups() -> None:
document = runtime_contract()
document["bindings"] = {}
document["resource_refs"] = {}
document["credential_refs"] = {}
assert validate_contract(document)["credential_refs"] == {}
@pytest.mark.parametrize(
"field",
["desired", "desired_commit", "desired_release", "desired_target", "desired_targets"],
)
def test_structural_contract_rejects_dynamic_desired_fields_at_any_depth(field: str) -> None:
document = runtime_contract()
document["bindings"]["service"][field] = "must-live-in-release-target"
with pytest.raises(ValueError, match="dynamic_field_forbidden"):
validate_contract(document)
def test_contract_resolver_binding_must_match_component_and_environment() -> None:
document = runtime_contract()
document["resource_refs"]["state_root"]["environment"] = "production"
with pytest.raises(ValueError, match="binding_environment_mismatch"):
validate_contract(document)
def test_contract_rejects_cross_tenant_resolver_binding() -> None:
document = runtime_contract()
document["entrypoints"]["task_api"]["tenant_scope"] = "tenant-b"
with pytest.raises(ValueError, match="binding_tenant_mismatch"):
validate_contract(document)
@pytest.mark.parametrize("path_ref", ["/etc/aios/state", "../other/state", "runtime/../secret", "bad\x00ref"])
def test_contract_rejects_unsafe_path_references(path_ref: str) -> None:
document = runtime_contract()
document["resource_refs"]["state_root"]["path_ref"] = path_ref
with pytest.raises(ValueError, match="unsafe_path_ref"):
validate_contract(document)
@pytest.mark.parametrize(
"credential_ref",
[
"/run/credentials/other-service/admin-token",
"plain-secret-bytes",
],
)
def test_contract_rejects_cross_service_or_inline_credentials(credential_ref: str) -> None:
document = runtime_contract()
document["credential_refs"]["task_api"]["credential_ref"] = credential_ref
with pytest.raises(ValueError, match="credential_ref"):
validate_contract(document)
def test_contract_rejects_secret_value_fields_even_when_reference_is_present() -> None:
document = runtime_contract()
document["credential_refs"]["task_api"]["token"] = "secret-bytes"
with pytest.raises(ValueError, match="secret_material_forbidden"):
validate_contract(document)
@pytest.mark.parametrize(
("target", "observed", "proof", "reason"),
[
(None, observation(), provenance(), "target_missing"),
(deployment_target(), None, provenance(), "observation_missing"),
(deployment_target(), observation(), None, "provenance_missing"),
],
)
def test_missing_evidence_is_unknown(target, observed, proof, reason: str) -> None:
result = evaluate_deployment(runtime_contract(), target, observed, proof, NOW)
assert result["status"] == "UNKNOWN"
assert result["state"] == "UNKNOWN"
assert reason in result["reasons"]
def test_stale_observation_is_unknown_with_injected_clock() -> None:
observed = observation()
observed["observed_at"] = "2026-09-07T11:58:59Z"
result = evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)
assert result["status"] == "UNKNOWN"
assert "observation_stale" in result["reasons"]
def test_nullable_health_and_wrong_types_are_unknown() -> None:
observed = observation()
observed["healthy"] = None
assert evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)["status"] == "UNKNOWN"
observed["healthy"] = "true"
result = evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)
assert result["status"] == "UNKNOWN"
assert "observation_healthy_invalid" in result["reasons"]
def test_complete_incompatible_target_is_drift() -> None:
target = deployment_target()
target["environment"] = "production"
observed = observation()
observed["environment"] = "production"
result = evaluate_deployment(runtime_contract(), target, observed, provenance(), NOW)
assert result["status"] == "DRIFT"
assert "target_environment_mismatch" in result["reasons"]
def test_complete_cross_tenant_target_is_drift() -> None:
target = deployment_target()
target["tenant_scope"] = "tenant-b"
result = evaluate_deployment(runtime_contract(), target, observation(), provenance(), NOW)
assert result["status"] == "DRIFT"
assert "target_tenant_scope_mismatch" in result["reasons"]
def test_complete_cross_tenant_observation_is_drift_without_target_tenant_field() -> None:
observed = observation()
observed["tenant_scope"] = "tenant-b"
result = evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)
assert result["status"] == "DRIFT"
assert "observation_tenant_scope_mismatch" in result["reasons"]
def test_artifact_mismatch_is_drift_only_when_evidence_is_complete() -> None:
observed = observation()
observed["actual_artifact_digest"] = "sha256:" + "d" * 64
result = evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)
assert result["status"] == "DRIFT"
assert "artifact_mismatch" in result["reasons"]
def test_matching_bindings_are_verified_and_converged_despite_unrelated_main_head() -> None:
observed = observation()
observed["main_head"] = "different-from-release-source"
result = evaluate_deployment(runtime_contract(), deployment_target(), observed, provenance(), NOW)
assert result["status"] == "VERIFIED"
assert result["state"] == "CONVERGED"
assert result["verification"] == "VERIFIED"
assert result["converged"] is True
def test_operation_definition_keeps_effect_and_safety_policies_orthogonal() -> None:
definition = OperationDefinition.from_dict(operation())
assert definition.effect_class == "RECONCILABLE"
assert definition.retry["mode"] == "AFTER_READBACK"
assert definition.rollback["mode"] == "OPERATION"
assert definition.owner_boundary["mode"] == "WITHIN_GRANT"
def test_read_only_definition_accepts_permissions_alias_and_registry_lists_definitions() -> None:
data = {
"operation_id": "service.status.v1",
"version": 1,
"effect_class": "READ_ONLY",
"handler_ref": "host.service_status",
"input_schema": "aios.operation.service-status.input.v1",
"permissions": ["service-status"],
"timeout_seconds": 10,
"readback": {"mode": "NONE", "required": False},
"retry": {"mode": "SAFE", "max_attempts": 2},
"reconcile": {"mode": "NONE"},
"rollback": {"mode": "NONE"},
"owner_boundary": {"mode": "WITHIN_GRANT"},
}
definition = OperationDefinition.from_dict(data)
registry = OperationRegistry([definition])
assert definition.required_permissions == ("service-status",)
assert definition.permissions == ("service-status",)
assert registry.definitions == (definition,)
@pytest.mark.parametrize("effect_class", ["READ_ONLY", "IDEMPOTENT", "CONDITIONAL", "RECONCILABLE", "IRREVERSIBLE"])
def test_registry_accepts_all_primary_effect_classes(effect_class: str) -> None:
data = operation(
operation_id=f"effect.{effect_class.lower()}.v1",
effect_class=effect_class,
retry={"mode": "NEVER", "max_attempts": 1},
rollback={"mode": "NONE"},
)
if effect_class == "IRREVERSIBLE":
data["owner_boundary"] = {"mode": "EXPLICIT_AUTHORIZATION"}
definition = OperationDefinition.from_dict(data)
registry = OperationRegistry([definition])
assert registry.get(data["operation_id"]) is definition
@pytest.mark.parametrize("forbidden", ["command", "shell", "argv", "executable", "script"])
def test_registry_definitions_cannot_embed_arbitrary_execution(forbidden: str) -> None:
data = operation()
data[forbidden] = "sh -c whoami"
with pytest.raises(ValueError, match="arbitrary_execution_forbidden"):
OperationDefinition.from_dict(data)
def test_registry_rejects_duplicate_operation_ids() -> None:
definition = OperationDefinition.from_dict(operation())
with pytest.raises(ValueError, match="duplicate_operation_id"):
OperationRegistry([definition, definition])
def test_timeout_never_blindly_retries_irreversible_or_ambiguous_effects() -> None:
irreversible = OperationDefinition.from_dict(
operation(
effect_class="IRREVERSIBLE",
retry={"mode": "SAFE", "max_attempts": 4},
rollback={"mode": "NONE"},
owner_boundary={"mode": "EXPLICIT_AUTHORIZATION"},
)
)
assert operation_timeout_action(irreversible, outcome="NOT_OBSERVED") == "STOP"
assert operation_timeout_action(OperationDefinition.from_dict(operation()), outcome="AMBIGUOUS") == "RECONCILE"
def test_service_active_state_alone_cannot_prove_a_restart() -> None:
result = verify_service_restart(
{"active": True, "instance_id": "before", "started_at": "2026-09-07T11:00:00Z"},
{"active": True},
)
assert result["status"] == "UNKNOWN"
assert "restart_identity_missing" in result["reasons"]
def test_service_restart_is_verified_by_changed_instance_and_fresh_start_time() -> None:
result = verify_service_restart(
{"active": True, "instance_id": "before", "started_at": "2026-09-07T11:00:00Z"},
{"active": True, "instance_id": "after", "started_at": "2026-09-07T11:59:00Z"},
)
assert result == {"status": "VERIFIED", "reasons": []}
def production_observation_bundle() -> dict:
contract = runtime_contract()
contract["environment"] = "production"
for group in ("entrypoints", "bindings", "resource_refs", "credential_refs"):
for binding in contract[group].values():
binding["environment"] = "production"
target = deployment_target()
target.update(
environment="production",
release_id="V2R1C9",
target_oid="d" * 40,
)
observed = observation()
observed.update(environment="production", observed_at="2026-09-07T11:59:45Z")
return {
"contract": contract,
"target": target,
"observation": observed,
"provenance": provenance(),
}
def test_missing_production_observation_is_unknown(tmp_path) -> None:
assert load_production_observation(tmp_path / "missing.json", NOW) == {
"baseline_version": None,
"target_oid": None,
"deployment_state": "UNKNOWN",
}
def test_stale_production_observation_cannot_claim_active(tmp_path) -> None:
bundle = production_observation_bundle()
bundle["observation"]["observed_at"] = "2026-09-07T11:50:00Z"
path = tmp_path / "production-observation.json"
path.write_text(__import__("json").dumps(bundle), encoding="utf-8")
assert load_production_observation(path, NOW)["deployment_state"] == "UNKNOWN"
def test_verified_production_observation_projects_compatible_snapshot_fields(tmp_path) -> None:
path = tmp_path / "production-observation.json"
path.write_text(__import__("json").dumps(production_observation_bundle()), encoding="utf-8")
assert load_production_observation(path, NOW) == {
"baseline_version": "V2R1C9",
"target_oid": "d" * 40,
"deployment_state": "ACTIVE",
}
def test_bridge_snapshot_defaults_production_to_unknown(tmp_path) -> None:
runtime = object.__new__(GovernedRuntime)
runtime.config = RuntimeConfig(
target_oid="e" * 40,
authoritative_git_dir=tmp_path / "repo.git",
state_root=tmp_path,
v2_job_root=tmp_path / "jobs",
token_path=tmp_path / "token",
ssh_key_path=tmp_path / "ssh",
)
runtime.authority = type("Authority", (), {"read_ref": lambda self, ref: "f" * 40})()
runtime._snapshot_task = lambda: {}
runtime.runtime_status = lambda: {
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
"control_plane_release_version": "V2R2C1",
"runtime_release_commit": "e" * 40,
"runtime_path": str(tmp_path),
"artifact_sha256": None,
"drift_status": "PASS",
"governed_writes_enabled": False,
}
assert runtime.snapshot()["production"] == {
"baseline_version": None,
"target_oid": None,
"deployment_state": "UNKNOWN",
}
def test_staging_unit_uses_its_own_encrypted_credential_projection() -> None:
unit = (
__import__("pathlib").Path(__file__).resolve().parents[2]
/ "deploy/systemd/aios-chatgpt-bridge-v2-staging.service"
).read_text(encoding="utf-8")
assert "LoadCredentialEncrypted=gitea-governance-executor-token:/etc/credstore.encrypted/gitea-governance-executor-token" in unit
assert "Environment=AIOS_GITEA_TOKEN_FILE=%d/gitea-governance-executor-token" in unit
assert "/run/credentials/aios-governance-executor.service/" not in unit
def test_empty_or_nullable_reconciliation_evidence_is_not_healthy() -> None:
controller = ReconciliationController()
empty = controller.reconcile({})
assert empty.healthy is False
assert set(empty.drift) == {label for _, _, label in controller.CHECKS}
nullable = controller.reconcile(
{
key: None
for pair in controller.CHECKS
for key in pair[:2]
}
)
assert nullable.healthy is False
def test_body_claiming_production_state_is_not_blanket_truth() -> None:
report = ReconciliationController().reconcile({"body": {"production_state": "ACTIVE"}})
assert report.healthy is False
+582
View File
@@ -0,0 +1,582 @@
from __future__ import annotations
import base64
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import textwrap
import pytest
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import aios_gitops_job_executor as executor_module
from aios_gitops_job_controller import (
GitOpsJobController,
JobError,
JobState,
JobStore,
ServiceUnavailable,
)
OLD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
NEW = "3fbbc18fb2131bbbd9e511004a59ee95cf3e86b7"
class Clock:
def __init__(self) -> None:
self.value = datetime(2026, 9, 8, tzinfo=timezone.utc)
def __call__(self) -> datetime:
return self.value
class Authority:
def __init__(self) -> None:
self.branch = OLD
self.main = OLD
self.mutations = 0
def read_ref(self, ref: str):
return self.main if ref == "refs/heads/main" else self.branch
def ancestry_check(self, older: str, newer: str) -> bool:
return older == newer or (older, newer) == (OLD, NEW)
def advance_task_branch_CAS(self, _branch: str, expected: str, new: str) -> str:
assert self.branch == expected
self.mutations += 1
self.branch = new
return new
def ff_CAS_main_after_verified_protected_merge(self, expected: str, new: str) -> str:
assert self.main == expected
self.mutations += 1
self.main = new
return new
def commit_file_in_isolated_worktree(self, **_kwargs) -> str:
self.mutations += 1
return NEW
class Broker:
def __init__(self) -> None:
self.remote = OLD
self.mutations = 0
def verify_identity(self):
return {"login": "aios-autonomy-agent", "repository": executor_module.REPOSITORY}
def readback(self):
return self.remote
def push_branch(self, new: str) -> str:
self.mutations += 1
self.remote = new
return new
def create_pr(self, *_args):
self.mutations += 1
return {"number": 1}
def status(self, *_args):
self.mutations += 1
return {"status": "success"}
def merge(self, *_args):
self.mutations += 1
return {"merged": True}
def campaign_request(operation: str = "SYNC_TASK_BRANCH_TO_GITEA", **updates):
request = {
"campaign_id": executor_module.CAMPAIGN_ID,
"repository": executor_module.REPOSITORY,
"operation": operation,
"branch": executor_module.BRANCH,
"expected_branch_oid": OLD if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH" else NEW,
"payload_digest": "0" * 64,
"task_id": executor_module.TASK_ID,
"new_branch_oid": NEW,
"authorization_recovery_digest": executor_module.AUTHORIZATION_DIGEST,
}
if operation == "RECONCILE_AUTHORITY_MAIN":
request["expected_main_oid"] = OLD
request.update(updates)
return request
def recovery():
return {
"digest": executor_module.AUTHORIZATION_DIGEST,
"campaign_id": executor_module.CAMPAIGN_ID,
"task_id": executor_module.TASK_ID,
"repository": executor_module.REPOSITORY,
"branch": executor_module.BRANCH,
"expected_branch_oid": OLD,
}
def generic_request(**updates):
request = {
"schema": "aios.operation-job.v1",
"tenant_id": "tenant-a",
"subject_id": "subject-a",
"mission_id": "mission-a",
"grant_id": "grant-a",
"task_id": "task-a",
"effect_id": "effect-a",
"operation": "host.status.v1",
"operation_version": "1",
"resource_kind": "host",
"resource_id": "tenant-a/staging/local-host",
"input_digest": hashlib.sha256(b"{}").hexdigest(),
}
request.update(updates)
return request
def receipt_signer():
private = Ed25519PrivateKey.generate()
def sign(payload):
raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return {
"schema": "aios.operation-receipt.v1",
"key_id": "test-machine",
"payload": payload,
"signature": base64.b64encode(private.sign(raw)).decode(),
}
def verify(envelope):
try:
raw = json.dumps(envelope["payload"], sort_keys=True, separators=(",", ":")).encode()
private.public_key().verify(base64.b64decode(envelope["signature"], validate=True), raw)
return envelope.get("key_id") == "test-machine"
except (InvalidSignature, KeyError, TypeError, ValueError):
return False
return sign, verify
def signed_generic_receipt(sign, request, job_id, lease):
return sign({
**{key: request[key] for key in (
"tenant_id", "subject_id", "mission_id", "grant_id", "task_id",
"effect_id", "operation", "operation_version", "resource_kind",
"resource_id", "input_digest",
)},
"job_id": job_id,
"claim_id": lease["lease_id"],
"generation": int(lease["generation"]),
"observation": {"active": True},
"verified": True,
})
def make_executor(tmp_path: Path, monkeypatch, operation="SYNC_TASK_BRANCH_TO_GITEA"):
monkeypatch.setattr(executor_module, "load_recovered_authorization", lambda _path: recovery())
clock = Clock()
controller = GitOpsJobController(JobStore(tmp_path / "jobs"), clock=clock)
job = controller.submit(campaign_request(operation))
authority, broker = Authority(), Broker()
if operation != "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
authority.branch = NEW
executor = executor_module.DurableJobExecutor(
controller, authority, broker, tmp_path / "recovery.json", clock=clock
)
return clock, controller, job, authority, broker, executor
def test_lease_requires_authorized_live_job_and_bounded_future_deadline(tmp_path):
clock = Clock()
controller = GitOpsJobController(JobStore(tmp_path / "jobs"), clock=clock)
job = controller.submit(campaign_request("ADVANCE_EXISTING_CAMPAIGN_BRANCH"))
kwargs = dict(
lease_id="lease-1",
lineage_anchor_oid=OLD,
current_head_oid=OLD,
generation=1,
expected_revision=job.revision,
)
with pytest.raises(JobError, match="LEASE_JOB_STATE_INVALID"):
controller.lease_job(job.job_id, lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(), **kwargs)
authorized = controller.advance(job.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
kwargs["expected_revision"] = authorized.revision
with pytest.raises(JobError, match="LEASE_EXPIRED"):
controller.lease_job(job.job_id, lease_expires_at=(clock.value - timedelta(seconds=1)).isoformat(), **kwargs)
with pytest.raises(JobError, match="LEASE_DURATION_EXCEEDED"):
controller.lease_job(job.job_id, lease_expires_at=(clock.value + timedelta(hours=1)).isoformat(), **kwargs)
terminal_controller = GitOpsJobController(JobStore(tmp_path / "terminal"), clock=clock)
terminal = terminal_controller.submit(campaign_request("ADVANCE_EXISTING_CAMPAIGN_BRANCH"))
terminal = terminal_controller.advance(
terminal.job_id,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
side_effect_state="MANUAL_OWNER_REVIEW_REQUIRED",
)
with pytest.raises(JobError, match="LEASE_JOB_STATE_INVALID"):
terminal_controller.lease_job(
terminal.job_id,
lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(),
**{**kwargs, "expected_revision": terminal.revision},
)
def test_resource_job_profile_and_lease_are_typed_and_fenced(tmp_path):
clock = Clock()
controller = GitOpsJobController(JobStore(tmp_path / "jobs"), clock=clock)
request = {
"schema": "aios.operation-job.v1",
"tenant_id": "tenant-a",
"subject_id": "subject-a",
"mission_id": "mission-a",
"grant_id": "grant-a",
"task_id": "task-a",
"effect_id": "effect-a",
"operation": "host.status.v1",
"operation_version": "1",
"resource_kind": "host",
"resource_id": "local-host",
"input_digest": hashlib.sha256(b"{}").hexdigest(),
}
submitted = controller.submit(request)
authorized = controller.advance(submitted.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
leased = controller.lease_resource_job(
submitted.job_id,
lease_id="resource-lease-1",
resource_kind="host",
resource_id="local-host",
generation=None,
lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(),
expected_revision=authorized.revision,
)
assert leased.state is JobState.LEASED
first_lease = controller.get_execution_lease(submitted.job_id)
assert first_lease["generation"] == 1
controller.assert_execution_lease(submitted.job_id, lease_id="resource-lease-1", generation=1)
with pytest.raises(JobError, match="LEASE_FENCE_REJECTED"):
controller.assert_execution_lease(submitted.job_id, lease_id="resource-lease-1", generation=0)
clock.value += timedelta(seconds=31)
second_request = {**request, "task_id": "task-b", "effect_id": "effect-b"}
second = controller.submit(second_request)
second = controller.advance(second.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
controller.lease_resource_job(
second.job_id,
lease_id="resource-lease-2",
resource_kind="host",
resource_id="local-host",
generation=None,
lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(),
expected_revision=second.revision,
)
assert controller.get_execution_lease(second.job_id)["generation"] == 2
with pytest.raises(JobError, match="LEASE_FENCE_REJECTED"):
controller.assert_execution_lease(submitted.job_id, lease_id="resource-lease-1", generation=1)
def test_generic_job_cannot_skip_intent_readback_or_trusted_receipt(tmp_path):
clock = Clock()
sign, verify = receipt_signer()
controller = GitOpsJobController(
JobStore(tmp_path / "jobs"), clock=clock, generic_receipt_verifier=verify
)
request = generic_request()
submitted = controller.submit(request)
invented = sign({"verified": True})
with pytest.raises(JobError, match="GENERIC_JOB_TRANSITION_INVALID"):
controller.advance(
submitted.job_id, JobState.COMPLETED,
side_effect_state="VERIFIED", receipt=invented,
)
assert controller.store.read(submitted.job_id)["state"] == JobState.REQUESTED.value
authorized = controller.advance(
submitted.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED"
)
leased = controller.lease_resource_job(
submitted.job_id, lease_id="claim-a", resource_kind=request["resource_kind"],
resource_id=request["resource_id"], generation=None,
lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(),
expected_revision=authorized.revision,
)
controller.advance(
submitted.job_id, JobState.EXECUTING,
side_effect_state="INTENT_COMMITTED", expected_revision=leased.revision,
)
lease = controller.get_execution_lease(submitted.job_id)
valid_receipt = signed_generic_receipt(sign, request, submitted.job_id, lease)
tampered = {**valid_receipt, "payload": {**valid_receipt["payload"], "input_digest": "f" * 64}}
with pytest.raises(JobError, match="GENERIC_RECEIPT_BINDING_MISMATCH"):
controller.advance(
submitted.job_id, JobState.LOCAL_VERIFIED,
side_effect_state="READBACK_VERIFIED", receipt=tampered,
)
local = controller.advance(
submitted.job_id, JobState.LOCAL_VERIFIED,
side_effect_state="READBACK_VERIFIED", receipt=valid_receipt,
)
completed = controller.advance(
submitted.job_id, JobState.COMPLETED,
side_effect_state="READBACK_VERIFIED", receipt=valid_receipt,
expected_revision=local.revision,
)
assert completed.state is JobState.COMPLETED
def test_generic_complete_requires_configured_verifier_and_recovery_accepts_bound_receipt(tmp_path):
clock = Clock()
sign, verify = receipt_signer()
request = generic_request()
controller = GitOpsJobController(
JobStore(tmp_path / "jobs"), clock=clock, generic_receipt_verifier=verify,
)
submitted = controller.submit(request)
authorized = controller.advance(submitted.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
leased = controller.lease_resource_job(
submitted.job_id, lease_id="claim-a", resource_kind=request["resource_kind"],
resource_id=request["resource_id"], generation=None,
lease_expires_at=(clock.value + timedelta(seconds=30)).isoformat(),
expected_revision=authorized.revision,
)
executing = controller.advance(
submitted.job_id, JobState.EXECUTING,
side_effect_state="INTENT_COMMITTED", expected_revision=leased.revision,
)
receipt = signed_generic_receipt(
sign, request, submitted.job_id, controller.get_execution_lease(submitted.job_id)
)
pending = controller.advance(
submitted.job_id, JobState.RECONCILIATION_REQUIRED,
side_effect_state="UNKNOWN", expected_revision=executing.revision,
)
clock.value += timedelta(seconds=31)
completed = controller.reconcile(
submitted.job_id,
lambda _row: {
"state": JobState.COMPLETED.value,
"side_effect_state": "READBACK_VERIFIED",
"receipt": receipt,
},
)
assert completed.state is JobState.COMPLETED
untrusted = GitOpsJobController(JobStore(tmp_path / "untrusted"), clock=Clock())
other_request = generic_request(effect_id="effect-b", task_id="task-b")
other = untrusted.submit(other_request)
other = untrusted.advance(other.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
other = untrusted.lease_resource_job(
other.job_id, lease_id="claim-b", resource_kind=other_request["resource_kind"],
resource_id=other_request["resource_id"], generation=None,
lease_expires_at=(Clock().value + timedelta(seconds=30)).isoformat(),
expected_revision=other.revision,
)
untrusted.advance(other.job_id, JobState.EXECUTING, side_effect_state="INTENT_COMMITTED")
other_receipt = signed_generic_receipt(
sign, other_request, other.job_id, untrusted.get_execution_lease(other.job_id)
)
with pytest.raises(JobError, match="GENERIC_RECEIPT_VERIFIER_REQUIRED"):
untrusted.advance(
other.job_id, JobState.LOCAL_VERIFIED,
side_effect_state="READBACK_VERIFIED", receipt=other_receipt,
)
def test_reconcile_crash_can_be_taken_over_and_stale_observer_cannot_commit(tmp_path):
clock = Clock()
controller = GitOpsJobController(
JobStore(tmp_path / "jobs"), clock=clock, reconcile_lease_seconds=10
)
job = controller.submit(campaign_request("ADVANCE_EXISTING_CAMPAIGN_BRANCH"))
controller.advance(job.job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN")
with pytest.raises(KeyboardInterrupt):
controller.reconcile(job.job_id, lambda _row: (_ for _ in ()).throw(KeyboardInterrupt()))
assert controller.store.read(job.job_id)["state"] == JobState.RECONCILING.value
with pytest.raises(JobError, match="RECONCILIATION_CLAIM_ACTIVE"):
controller.reconcile(job.job_id, lambda _row: {"state": "REQUESTED"})
clock.value += timedelta(seconds=11)
def stale_observer(_row):
clock.value += timedelta(seconds=11)
winner = controller.reconcile(
job.job_id,
lambda _new: {"state": "REQUESTED", "side_effect_state": "RECONCILED_ABSENT"},
)
assert winner.state is JobState.REQUESTED
return {"state": "COMPLETED", "side_effect_state": "VERIFIED"}
with pytest.raises(JobError, match="RECONCILIATION_CLAIM_LOST"):
controller.reconcile(job.job_id, stale_observer)
assert controller.store.read(job.job_id)["state"] == JobState.REQUESTED.value
@pytest.mark.parametrize(
"operation",
[
"ADVANCE_EXISTING_CAMPAIGN_BRANCH",
"SYNC_TASK_BRANCH_TO_GITEA",
"CREATE_PR",
"PUBLISH_CHECK",
"PROTECTED_MERGE",
"RECONCILE_AUTHORITY_MAIN",
"COMMIT_FILE",
],
)
def test_dry_run_never_calls_a_mutating_adapter(tmp_path, monkeypatch, operation):
_clock, controller, job, authority, broker, executor = make_executor(
tmp_path, monkeypatch, operation
)
if operation == "COMMIT_FILE":
payload = b'{"path":"x.txt","content_base64":"eA=="}'
controller = GitOpsJobController(JobStore(tmp_path / "commit-jobs"), clock=Clock())
job = controller.submit(
campaign_request(operation, payload_digest=hashlib.sha256(payload).hexdigest()),
payload=payload,
)
executor = executor_module.DurableJobExecutor(
controller, authority, broker, tmp_path / "recovery.json", clock=Clock()
)
result = executor.execute(job.job_id, dry_run=True)
assert result["mode"] == "DRY_RUN"
assert authority.mutations == broker.mutations == 0
assert controller.store.read(job.job_id)["state"] == JobState.REQUESTED.value
def test_execution_does_not_call_effect_when_intent_commit_is_refused(tmp_path, monkeypatch):
_clock, controller, job, authority, broker, executor = make_executor(tmp_path, monkeypatch)
original = controller.advance
def refuse(job_id, state, **kwargs):
if state is JobState.EXECUTING:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE")
return original(job_id, state, **kwargs)
monkeypatch.setattr(controller, "advance", refuse)
with pytest.raises(ServiceUnavailable, match="JOB_DB_UNAVAILABLE"):
executor.execute(job.job_id)
assert authority.mutations == broker.mutations == 0
with pytest.raises(executor_module.JobExecutionError, match="PERSISTENCE_FAILURE_LATCHED"):
executor.execute(job.job_id)
def test_external_success_receipt_failure_latches_then_readback_recovers_without_repeat(tmp_path, monkeypatch):
_clock, controller, job, _authority, broker, executor = make_executor(tmp_path, monkeypatch)
original = controller.advance
fail = {"once": True}
def refuse_receipt(job_id, state, **kwargs):
if state is JobState.COMPLETED and fail["once"]:
fail["once"] = False
raise ServiceUnavailable("JOB_DB_UNAVAILABLE")
return original(job_id, state, **kwargs)
monkeypatch.setattr(controller, "advance", refuse_receipt)
with pytest.raises(ServiceUnavailable, match="JOB_DB_UNAVAILABLE"):
executor.execute(job.job_id)
assert broker.mutations == 1
with pytest.raises(executor_module.JobExecutionError, match="PERSISTENCE_FAILURE_LATCHED"):
executor.execute(job.job_id)
next_job = controller.submit(campaign_request("CREATE_PR", idempotency_key="after-receipt-failure"))
with pytest.raises(executor_module.JobExecutionError, match="PERSISTENCE_FAILURE_LATCHED"):
executor.execute(next_job.job_id)
assert broker.mutations == 1
restarted = executor_module.DurableJobExecutor(
controller, executor.authority, broker, tmp_path / "recovery.json", clock=executor.clock
)
with pytest.raises(executor_module.JobExecutionError, match="PERSISTENCE_FAILURE_LATCHED"):
restarted.execute(job.job_id)
recovered = executor.reconcile(job.job_id)
assert recovered["state"] == JobState.COMPLETED.value
assert broker.mutations == 1
assert executor.execute(job.job_id)["state"] == JobState.COMPLETED.value
def test_unsupported_exact_readback_reports_unknown_without_fake_success_or_effect(tmp_path, monkeypatch):
_clock, controller, job, _authority, broker, executor = make_executor(
tmp_path, monkeypatch, "PUBLISH_CHECK"
)
result = executor.execute(job.job_id)
assert result["state"] == JobState.RECONCILIATION_REQUIRED.value
assert result["receipt"]["verification"] == "UNKNOWN"
assert result["receipt"]["blocker"] == "EXACT_READBACK_UNSUPPORTED"
assert broker.mutations == 0
again = executor.execute(job.job_id)
assert again["state"] == JobState.RECONCILIATION_REQUIRED.value
assert broker.mutations == 0
def test_subprocess_crash_after_durable_sink_success_recovers_without_repeat(tmp_path):
"""AT-22: process death after effect durability cannot repeat the effect."""
job_root = tmp_path / "jobs"
sink = tmp_path / "effect-sink.json"
recovery_path = tmp_path / "recovery.json"
script = textwrap.dedent(
f"""
import json, os
from pathlib import Path
import aios_gitops_job_executor as m
from aios_gitops_job_controller import GitOpsJobController, JobStore
OLD = {OLD!r}; NEW = {NEW!r}
sink = Path({str(sink)!r})
root = Path({str(job_root)!r})
m.load_recovered_authorization = lambda _path: {{
'digest': m.AUTHORIZATION_DIGEST, 'campaign_id': m.CAMPAIGN_ID,
'task_id': m.TASK_ID, 'repository': m.REPOSITORY,
'branch': m.BRANCH, 'expected_branch_oid': OLD,
}}
class Authority:
def read_ref(self, _ref): return NEW
def ancestry_check(self, older, newer): return older == newer or (older == OLD and newer == NEW)
class Broker:
def verify_identity(self): return {{'login':'aios-autonomy-agent','repository':m.REPOSITORY}}
def readback(self):
if sink.exists():
if os.environ.get('CRASH_AFTER_EFFECT') == '1': os._exit(77)
return json.loads(sink.read_text())['oid']
return OLD
def push_branch(self, oid):
previous = json.loads(sink.read_text()) if sink.exists() else {{'count': 0}}
payload = {{'count': previous['count'] + 1, 'oid': oid}}
with sink.open('w') as stream:
json.dump(payload, stream); stream.flush(); os.fsync(stream.fileno())
return oid
controller = GitOpsJobController(JobStore(root))
request = {campaign_request()!r}
job = controller.submit(request)
executor = m.DurableJobExecutor(controller, Authority(), Broker(), Path({str(recovery_path)!r}))
if os.environ['ACTION'] == 'execute':
executor.execute(job.job_id)
else:
print(json.dumps(executor.reconcile(job.job_id), sort_keys=True))
"""
)
environment = os.environ.copy()
environment["PYTHONDONTWRITEBYTECODE"] = "1"
environment["ACTION"] = "execute"
environment["CRASH_AFTER_EFFECT"] = "1"
crashed = subprocess.run([sys.executable, "-c", script], env=environment, check=False)
assert crashed.returncode == 77
assert json.loads(sink.read_text()) == {"count": 1, "oid": NEW}
environment["ACTION"] = "recover"
environment["CRASH_AFTER_EFFECT"] = "0"
recovered = subprocess.run(
[sys.executable, "-c", script], env=environment, check=True,
capture_output=True, text=True,
)
assert json.loads(recovered.stdout)["state"] == JobState.COMPLETED.value
assert json.loads(sink.read_text()) == {"count": 1, "oid": NEW}
+190
View File
@@ -0,0 +1,190 @@
from __future__ import annotations
import sys
import time
from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from task_plane.store import TaskPlaneStore
from aios_gitops_job_controller import JobStore
from aios_baseline.authorization import AuthorizationService, sign_owner_delegation_grant
from aios_baseline.host import SystemdHost
from aios_baseline.lineage import LineageLedger
from aios_baseline.operations import OperationRegistry
from aios_baseline.service import BaselineService, Identity, ServiceError
from aios_baseline.worker_policy import WorkerProfile
def fixture_service(tmp_path):
owner = Ed25519PrivateKey.generate()
machine = Ed25519PrivateKey.generate()
auth = AuthorizationService(tmp_path/'auth.db', {'owner':owner.public_key()}, machine, 'machine',
owner_key_scopes={'owner':{'tenant_ids':['tenant-a'],'subject_ids':['user-a']}})
auth.register_grant(sign_owner_delegation_grant(owner,'owner',grant_id='grant-a',generation=1,
tenant_id='tenant-a',subject_id='user-a',mission_ids=('mission-a',),task_ids=('*',),
operations=('service-status',),component_ids=('sample',),environments=('staging',),
issued_at=int(time.time())-1,validity_mode='UNTIL_REVOKED',max_effects=10))
store=TaskPlaneStore(str(tmp_path/'tasks.db'),high_assurance=True)
worker=WorkerProfile.from_dict(dict(worker_id='worker-a',identity='machine-a',tenant_scope=['tenant-a'],
subject_scope=['user-a'],capabilities=['service-status'],priority=10,fallback_executor=False,
heartbeat_seconds=15,max_concurrency=1))
program=tmp_path/'systemctl-fixture'
program.write_text('#!'+sys.executable+'\nprint("Id=sample.service\\nActiveState=active\\nSubState=running\\nMainPID=123\\nInvocationID=abc")\n')
program.chmod(0o700)
host=SystemdHost({'tenant-a:staging:sample':'sample.service'},executable=str(program))
operation=dict(operation_id='service-status',version=1,effect_class='READ_ONLY',handler_ref='host.service_status',
input_schema='aios.operation.service-status.input.v1',required_permissions=['service-status'],timeout_seconds=10,
readback={'mode':'NONE','required':False},retry={'mode':'SAFE','max_attempts':2},
reconcile={'mode':'NONE'},rollback={'mode':'NONE'},owner_boundary={'mode':'WITHIN_GRANT'})
contract=dict(contract_schema='aios.runtime-contract.v1',contract_revision=1,component_id='sample',
environment='staging',tenant_scope='tenant-a',lifecycle='active',
entrypoints={'host':{'component_id':'sample','environment':'staging','resolver_ref':'service://sample/status'}},
bindings={},resource_refs={},credential_refs={},operations=['service-status'])
service=BaselineService(store, tmp_path/'jobs', auth, [contract], OperationRegistry([operation]),
{'worker-a':worker},host,LineageLedger(tmp_path/'lineage.db',store,JobStore(tmp_path/'jobs').read),machine)
return service
USER=Identity('tenant-a','user-a','user')
WORKER=Identity('tenant-a','machine-a','worker','worker-a')
REQUEST=dict(mission_id='mission-a',grant_id='grant-a',operation='service-status',component_id='sample',
environment='staging',inputs={},intent='检查 staging 服务并形成诊断结果')
def test_real_local_task_goes_through_claim_job_receipt_and_lineage(tmp_path):
service=fixture_service(tmp_path)
created=service.submit_goal(USER,REQUEST,idempotency_key='request-a')
duplicate=service.submit_goal(USER,REQUEST,idempotency_key='request-a')
assert duplicate['task_id']==created['task_id']
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
assert claim['task_id']==created['task_id']
result=service.execute_claim(WORKER,claim['task_id'],claim['claim_id'])
assert result['status']=='COMPLETED'
assert result['receipt']['observation']['properties']['MainPID']=='123'
assert service.get_task(USER,created['task_id'])['status']=='COMPLETED'
assert service.explain(USER,'mission-a')['mission_id']=='mission-a'
assert service.claim_next(WORKER) is None
def test_spoofed_tenant_cross_subject_and_worker_results_are_denied(tmp_path):
service=fixture_service(tmp_path)
with pytest.raises(ServiceError): service.submit_goal(USER,{**REQUEST,'tenant_id':'tenant-b'},idempotency_key='a')
created=service.submit_goal(USER,REQUEST,idempotency_key='a')
for outsider in (Identity('tenant-b','user-a','user'),Identity('tenant-a','user-b','user')):
with pytest.raises(ServiceError): service.get_task(outsider,created['task_id'])
with pytest.raises(ServiceError): service.cancel_task(outsider,created['task_id'])
with pytest.raises(ServiceError): service.register_worker(Identity('tenant-b','machine-a','worker','worker-a'))
with pytest.raises(ServiceError): service.execute_claim(USER,created['task_id'],'invented')
def test_changed_payload_with_same_request_key_cannot_change_task(tmp_path):
service=fixture_service(tmp_path)
service.submit_goal(USER,REQUEST,idempotency_key='a')
with pytest.raises(ServiceError): service.submit_goal(USER,{**REQUEST,'intent':'different goal'},idempotency_key='a')
def test_cancelled_expired_claim_is_not_resurrected_by_recovery(tmp_path):
service=fixture_service(tmp_path)
created=service.submit_goal(USER,REQUEST,idempotency_key='cancel')
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
service.cancel_task(USER,created['task_id'])
with service.store._transaction():
service.store.conn.execute('UPDATE claims SET lease_until=? WHERE claim_id=?',(time.time()-1,claim['claim_id']))
service.recover()
assert service.get_task(USER,created['task_id'])['status']=='CANCELLED'
assert service.claim_next(WORKER) is None
def test_multiple_tasks_share_monotonic_resource_fence_without_sharing_state(tmp_path):
service=fixture_service(tmp_path)
service.register_worker(WORKER)
generations=[]
for request_key in ('first','second'):
service.submit_goal(USER,REQUEST,idempotency_key=request_key)
claim=service.claim_next(WORKER)
result=service.execute_claim(WORKER,claim['task_id'],claim['claim_id'])
assert result['status']=='COMPLETED'
generations.append(result['receipt']['generation'])
assert generations==[1,2]
def test_persisted_receipt_closes_expired_task_without_repeating_host_diagnostic(tmp_path,monkeypatch):
service=fixture_service(tmp_path)
service.submit_goal(USER,REQUEST,idempotency_key='recover')
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
original=service._close_task
def lose_power(*args): raise OSError('simulated process exit before Task acknowledgment')
monkeypatch.setattr(service,'_close_task',lose_power)
with pytest.raises(ServiceError): service.execute_claim(WORKER,claim['task_id'],claim['claim_id'])
with service.store._transaction():
service.store.conn.execute('UPDATE claims SET lease_until=? WHERE claim_id=?',(time.time()-1,claim['claim_id']))
monkeypatch.setattr(service,'_close_task',original)
def must_not_execute(*args,**kwargs): raise AssertionError('must use durable receipt')
monkeypatch.setattr(service.host,'status',must_not_execute)
service.recover()
assert service.get_task(USER,claim['task_id'])['status']=='COMPLETED'
def test_wrong_signed_lineage_cannot_be_attached_to_real_task(tmp_path):
service=fixture_service(tmp_path)
service.submit_goal(USER,REQUEST,idempotency_key='scope')
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
result=service.execute_claim(WORKER,claim['task_id'],claim['claim_id'])
task=service.store.get_task(claim['task_id'])
assert service._receipt_valid(task,service.signer.sign(result['receipt']))
for field in ('job_id','mission_id','grant_id','claim_id','generation','operation','input_digest','resource_id'):
forged=service.signer.sign({**result['receipt'],field:'wrong'})
assert not service._receipt_valid(task,forged),field
def test_receipt_key_rotation_preserves_old_evidence_only_with_explicit_trust(tmp_path):
from aios_baseline.evidence import ReceiptSigner
service=fixture_service(tmp_path)
service.submit_goal(USER,REQUEST,idempotency_key='rotation')
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
result=service.execute_claim(WORKER,claim['task_id'],claim['claim_id'])
old=service.signer
envelope=old.sign(result['receipt'])
new_key=Ed25519PrivateKey.generate()
assert not ReceiptSigner(new_key,key_id='next').verify(envelope)
rotated=ReceiptSigner(new_key,key_id='next',trusted_public_keys={old.key_id:old.public_key})
service.signer=rotated
assert service._receipt_valid(service.store.get_task(claim['task_id']),envelope)
def test_fallback_is_normal_worker_eligibility_with_priority_and_same_claim_protocol(tmp_path):
service=fixture_service(tmp_path)
task=service.submit_goal(USER,REQUEST,idempotency_key='fallback')
service.register_worker(WORKER)
profile=service.worker_profiles['worker-a']
fallback=WorkerProfile.from_dict({**profile.__dict__,'worker_id':'worker-fallback','identity':'machine-fallback',
'fallback_executor':True,'priority':100})
service.worker_profiles[fallback.worker_id]=fallback
identity=Identity('tenant-a','machine-fallback','worker','worker-fallback')
service.register_worker(identity)
assert service.claim_next(identity) is None
with service.store._transaction():
service.store.conn.execute('UPDATE baseline_task_runtime SET ready_at=? WHERE task_id=?',(time.time()-601,task['task_id']))
assert service.claim_next(WORKER) is None
claim=service.claim_next(identity)
assert claim['task_id']==task['task_id']
assert service.execute_claim(identity,claim['task_id'],claim['claim_id'])['status']=='COMPLETED'
def test_deadline_expiring_while_waiting_for_executor_never_starts_host(tmp_path,monkeypatch):
service=fixture_service(tmp_path)
service.submit_goal(USER,REQUEST,idempotency_key='deadline')
service.register_worker(WORKER)
claim=service.claim_next(WORKER)
def unexpected(*args,**kwargs): raise AssertionError('expired call executed')
monkeypatch.setattr(service.host,'status',unexpected)
service._execute_lock.acquire()
try:
with pytest.raises(ServiceError,match='DEADLINE'):
service.execute_claim(WORKER,claim['task_id'],claim['claim_id'],deadline=time.time()+0.05)
finally: service._execute_lock.release()
assert service.get_task(USER,claim['task_id'])['job_id'] is None
+435
View File
@@ -0,0 +1,435 @@
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from aios_baseline.optimization import (
OptimizationController,
OptimizationEnvelope,
OptimizationError,
canonical_json_bytes,
validate_proposal,
)
def _signed_envelope(**overrides):
private_key = Ed25519PrivateKey.generate()
document = {
"schema": "aios.optimization-envelope.v1",
"envelope_id": "opt-envelope-1",
"tenant_id": "tenant-a",
"allowed_parameters": {
"retry.max_attempts": {"minimum": 1, "maximum": 5, "integer": True},
"timeout.operation_seconds": {"minimum": 5, "maximum": 60},
"evidence_reuse.max_age_seconds": {
"minimum": 0,
"maximum": 600,
"integer": True,
},
},
"allowed_components": ["task-worker"],
"allowed_environments": ["staging"],
"max_change_size": 2,
"max_compute_budget": 20,
"quality_floor": {
"success_rate": {"direction": "higher", "threshold": 0.95},
"error_rate": {"direction": "lower", "threshold": 0.05},
},
"rollback_requirement": True,
"promotion_rules": {
"objective_metric": "success_rate",
"minimum_improvement": 0.01,
"mandatory_tests": ["test_worker_safety"],
"mandatory_quality_gates": ["no_tenant_leak"],
"required_receipt_schema": "aios.governed-task-receipt.v1",
},
"source_config_digest": "sha256:" + "a" * 64,
"key_id": "optimization-config-key-1",
}
document.update(overrides)
signature = private_key.sign(canonical_json_bytes(document))
document["signature"] = base64.b64encode(signature).decode("ascii")
def verify(key_id, payload, encoded_signature):
assert key_id == "optimization-config-key-1"
try:
private_key.public_key().verify(base64.b64decode(encoded_signature), payload)
except Exception:
return False
return True
return document, verify
def _envelope():
document, verifier = _signed_envelope()
return OptimizationEnvelope.from_dict(document, verify_envelope=verifier)
def _context(**overrides):
value = {
"tenant_id": "tenant-a",
"subject_id": "optimizer-service",
"grant_id": "grant-123",
"mission_id": "mission-456",
"service_identity": "aios-root-service",
}
value.update(overrides)
return value
def _proposal(**overrides):
value = {
"schema": "aios.optimization-proposal.v1",
"proposal_id": "proposal-1",
"envelope_id": "opt-envelope-1",
"tenant_id": "tenant-a",
"component": "task-worker",
"environment": "staging",
"changes": [
{"path": "retry.max_attempts", "value": 4},
{"path": "timeout.operation_seconds", "value": 30},
],
"compute_budget": 10,
"tests": ["test_worker_safety", "test_candidate_latency"],
"quality_gates": ["no_tenant_leak"],
"baseline_metrics": {"success_rate": 0.95, "error_rate": 0.04},
"rollback_plan": {
"strategy": "restore_previous_values",
"reference": "baseline-config-17",
},
}
value.update(overrides)
return value
def _signed_receipt_verifier():
private_key = Ed25519PrivateKey.generate()
def sign(receipt):
signed = dict(receipt)
signed["key_id"] = "governed-receipt-key-1"
signed["signature"] = base64.b64encode(
private_key.sign(canonical_json_bytes(signed))
).decode("ascii")
return signed
def verify(receipt):
unsigned = dict(receipt)
signature = unsigned.pop("signature", "")
if unsigned.get("key_id") != "governed-receipt-key-1":
return False
try:
private_key.public_key().verify(
base64.b64decode(signature), canonical_json_bytes(unsigned)
)
except Exception:
return False
return True
return sign, verify
def _controller(tmp_path: Path):
submitted = []
sign, verify = _signed_receipt_verifier()
def submit_task(task):
submitted.append(task)
return {"task_id": "evaluation-task-1", "state": "QUEUED"}
controller = OptimizationController(
tmp_path / "optimization.sqlite3",
_envelope(),
submit_task,
verify_receipt=verify,
)
return controller, submitted, sign
def _evaluation(proposal_record, sign, **overrides):
result = {
"candidate_metrics": {"success_rate": 0.97, "error_rate": 0.03},
"compute_used": 8,
"tests_passed": ["test_worker_safety", "test_candidate_latency"],
"quality_gates_passed": ["no_tenant_leak"],
}
result.update(overrides)
receipt = sign(
{
"schema": "aios.governed-task-receipt.v1",
"task_id": proposal_record["evaluation_task_id"],
"proposal_id": proposal_record["proposal_id"],
"proposal_digest": proposal_record["proposal_digest"],
"action": "evaluate",
"success": True,
"result_digest": "sha256:"
+ hashlib.sha256(canonical_json_bytes(result)).hexdigest(),
}
)
return {**result, "receipt": receipt}
def test_envelope_requires_a_valid_signature_over_the_exact_payload():
document, verifier = _signed_envelope()
document["max_compute_budget"] = 10_000
with pytest.raises(OptimizationError, match="OPTIMIZATION_ENVELOPE_SIGNATURE_INVALID"):
OptimizationEnvelope.from_dict(document, verify_envelope=verifier)
with pytest.raises(OptimizationError, match="OPTIMIZATION_ENVELOPE_VERIFIER_REQUIRED"):
OptimizationEnvelope.from_dict(document, verify_envelope=None)
@pytest.mark.parametrize(
"path",
[
"authorization.accept_without_grant",
"trust_root.key",
"tenant_isolation.enabled",
"acceptance.threshold",
"destructive_capability.enabled",
"owner_grant.semantics",
"production_migration.auto_apply",
"code.patch",
],
)
def test_envelope_rejects_forbidden_or_generic_parameter_paths(path):
document, verifier = _signed_envelope(
allowed_parameters={path: {"minimum": 0, "maximum": 1}}
)
# Re-sign the changed trusted document so this reaches semantic validation.
private_key = Ed25519PrivateKey.generate()
unsigned = dict(document)
unsigned.pop("signature")
document["signature"] = base64.b64encode(
private_key.sign(canonical_json_bytes(unsigned))
).decode("ascii")
def changed_verifier(_key_id, payload, signature):
try:
private_key.public_key().verify(base64.b64decode(signature), payload)
except Exception:
return False
return True
with pytest.raises(OptimizationError, match="OPTIMIZATION_PARAMETER_PATH_FORBIDDEN"):
OptimizationEnvelope.from_dict(document, verify_envelope=changed_verifier)
def test_proposal_is_bounded_and_cannot_remove_mandatory_checks():
envelope = _envelope()
normalized = validate_proposal(envelope, _proposal())
assert normalized["changes"][0] == {"path": "retry.max_attempts", "value": 4}
with pytest.raises(OptimizationError, match="OPTIMIZATION_PARAMETER_OUT_OF_RANGE"):
validate_proposal(
envelope,
_proposal(changes=[{"path": "retry.max_attempts", "value": 99}]),
)
with pytest.raises(OptimizationError, match="OPTIMIZATION_MANDATORY_TEST_MISSING"):
validate_proposal(envelope, _proposal(tests=["test_candidate_latency"]))
with pytest.raises(OptimizationError, match="OPTIMIZATION_MANDATORY_GATE_MISSING"):
validate_proposal(envelope, _proposal(quality_gates=[]))
def test_evidence_reuse_cannot_widen_tenant_component_or_environment_scope():
envelope = _envelope()
proposal = _proposal(
changes=[{"path": "evidence_reuse.max_age_seconds", "value": 300}],
evidence_reuse={
"scope": {
"tenant_id": "tenant-b",
"component": "task-worker",
"environment": "staging",
},
"evidence_ids": ["evidence-1"],
},
)
with pytest.raises(OptimizationError, match="OPTIMIZATION_EVIDENCE_SCOPE_MISMATCH"):
validate_proposal(envelope, proposal)
def test_propose_only_persists_a_record_and_submits_an_identity_bound_task(tmp_path):
controller, submitted, _ = _controller(tmp_path)
record = controller.propose(_proposal(), authenticated_context=_context())
assert record["status"] == "EVALUATION_SUBMITTED"
assert record["evaluation_task_id"] == "evaluation-task-1"
assert submitted == [
{
"schema": "aios.optimization-task-request.v1",
"task_id": "optimization-evaluate-proposal-1",
"action": "evaluate",
"tenant_id": "tenant-a",
"subject_id": "optimizer-service",
"grant_id": "grant-123",
"mission_id": "mission-456",
"proposal_id": "proposal-1",
"proposal_digest": record["proposal_digest"],
"component": "task-worker",
"environment": "staging",
"compute_budget": 10.0,
}
]
def test_invalid_candidate_is_stopped_without_task_submission(tmp_path):
controller, submitted, _ = _controller(tmp_path)
with pytest.raises(OptimizationError, match="OPTIMIZATION_COMPONENT_FORBIDDEN"):
controller.propose(
_proposal(component="authorization-service"),
authenticated_context=_context(),
)
stopped = controller.get_proposal("proposal-1", authenticated_context=_context())
assert stopped["status"] == "STOPPED_INVALID"
assert submitted == []
def test_cross_tenant_or_subject_access_is_denied(tmp_path):
controller, _, _ = _controller(tmp_path)
controller.propose(_proposal(), authenticated_context=_context())
with pytest.raises(OptimizationError, match="OPTIMIZATION_CONTEXT_TENANT_MISMATCH"):
controller.get_proposal(
"proposal-1", authenticated_context=_context(tenant_id="tenant-b")
)
with pytest.raises(OptimizationError, match="OPTIMIZATION_CONTEXT_SUBJECT_MISMATCH"):
controller.get_proposal(
"proposal-1", authenticated_context=_context(subject_id="other-service")
)
def test_forged_or_wrong_task_receipt_is_rejected(tmp_path):
controller, _, sign = _controller(tmp_path)
proposal = controller.propose(_proposal(), authenticated_context=_context())
evaluation = _evaluation(proposal, sign)
evaluation["receipt"]["task_id"] = "some-other-task"
with pytest.raises(OptimizationError, match="OPTIMIZATION_RECEIPT_UNTRUSTED"):
controller.record_evaluation(
"proposal-1", evaluation, authenticated_context=_context()
)
validly_signed_wrong_binding = _evaluation(proposal, sign)
unsigned = dict(validly_signed_wrong_binding["receipt"])
unsigned["task_id"] = "some-other-task"
unsigned.pop("signature")
validly_signed_wrong_binding["receipt"] = sign(unsigned)
with pytest.raises(OptimizationError, match="OPTIMIZATION_RECEIPT_TASK_MISMATCH"):
controller.record_evaluation(
"proposal-1", validly_signed_wrong_binding, authenticated_context=_context()
)
def test_no_improvement_stops_promotion(tmp_path):
controller, submitted, sign = _controller(tmp_path)
proposal = controller.propose(_proposal(), authenticated_context=_context())
evaluation = _evaluation(
proposal,
sign,
candidate_metrics={"success_rate": 0.95, "error_rate": 0.04},
)
result = controller.record_evaluation(
"proposal-1", evaluation, authenticated_context=_context()
)
assert result["status"] == "STOPPED_NO_IMPROVEMENT"
assert result["effect_comparison"] == {
"error_rate": 0.0,
"success_rate": 0.0,
}
with pytest.raises(OptimizationError, match="OPTIMIZATION_NOT_PROMOTABLE"):
controller.promote(
"proposal-1",
{
"evaluation_task_id": "evaluation-task-1",
"proposal_digest": proposal["proposal_digest"],
},
authenticated_context=_context(),
)
assert len(submitted) == 1
def test_quality_floor_or_baseline_regression_stops_promotion(tmp_path):
controller, _, sign = _controller(tmp_path)
proposal = controller.propose(_proposal(), authenticated_context=_context())
evaluation = _evaluation(
proposal,
sign,
candidate_metrics={"success_rate": 0.97, "error_rate": 0.05},
)
result = controller.record_evaluation(
"proposal-1", evaluation, authenticated_context=_context()
)
assert result["status"] == "STOPPED_QUALITY_REGRESSION"
assert result["effect_comparison"]["error_rate"] == pytest.approx(-0.01)
improved = _evaluation(proposal, sign)
with pytest.raises(OptimizationError, match="OPTIMIZATION_EVALUATION_STATE_INVALID"):
controller.record_evaluation(
"proposal-1", improved, authenticated_context=_context()
)
assert controller.get_proposal(
"proposal-1", authenticated_context=_context()
)["status"] == "STOPPED_QUALITY_REGRESSION"
def test_eligible_candidate_promotion_is_another_governed_task_only(tmp_path):
controller, submitted, sign = _controller(tmp_path)
proposal = controller.propose(_proposal(), authenticated_context=_context())
result = controller.record_evaluation(
"proposal-1",
_evaluation(proposal, sign),
authenticated_context=_context(),
)
assert result["status"] == "ELIGIBLE"
promoted = controller.promote(
"proposal-1",
{
"evaluation_task_id": "evaluation-task-1",
"proposal_digest": proposal["proposal_digest"],
},
authenticated_context=_context(),
)
assert promoted["status"] == "PROMOTION_SUBMITTED"
assert len(submitted) == 2
promotion_task = submitted[-1]
assert promotion_task["action"] == "promote"
assert promotion_task["tenant_id"] == "tenant-a"
assert promotion_task["grant_id"] == "grant-123"
assert promotion_task["mission_id"] == "mission-456"
assert "changes" not in promotion_task
def test_restart_reads_persisted_baseline_candidate_and_effect(tmp_path):
path = tmp_path / "optimization.sqlite3"
submitted = []
sign, verify = _signed_receipt_verifier()
def submit_task(task):
submitted.append(task)
return {"task_id": "evaluation-task-1"}
first = OptimizationController(path, _envelope(), submit_task, verify_receipt=verify)
proposal = first.propose(_proposal(), authenticated_context=_context())
first.record_evaluation(
"proposal-1", _evaluation(proposal, sign), authenticated_context=_context()
)
restarted = OptimizationController(path, _envelope(), submit_task, verify_receipt=verify)
restored = restarted.get_proposal("proposal-1", authenticated_context=_context())
assert restored["baseline_metrics"] == {"error_rate": 0.04, "success_rate": 0.95}
assert restored["candidate_metrics"] == {"error_rate": 0.03, "success_rate": 0.97}
assert restored["effect_comparison"] == {
"error_rate": pytest.approx(0.01),
"success_rate": pytest.approx(0.02),
}
+141
View File
@@ -0,0 +1,141 @@
"""Exercise package installation against real temporary Git repositories."""
from pathlib import Path
import importlib.util
import json
import subprocess
import zipfile
import pytest
ROOT = Path(__file__).resolve().parents[2]
SPEC = importlib.util.spec_from_file_location("baseline_bundle", ROOT / "tools/baseline_v1_1/bundle.py")
bundle = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(bundle)
def git(root, *args):
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()
@pytest.fixture
def repos(tmp_path):
source = tmp_path / "source"
source.mkdir()
git(source, "init", "--quiet")
(source / "app.py").write_text("print('before')\n")
(source / "retired.txt").write_text("historical\n")
git(source, "add", ".")
git(source, "-c", "user.name=Test", "-c", "user.email=test@localhost", "commit", "--quiet", "-m", "base")
revision = git(source, "rev-parse", "HEAD")
work = tmp_path / "work"
subprocess.run(["git", "clone", "--quiet", str(source), str(work)], check=True)
(work / "app.py").write_text("print('after')\n")
(work / "retired.txt").unlink()
(work / "new").mkdir()
(work / "new/tool.py").write_text("print('new')\n")
artifact = tmp_path / "change.zip"
bundle.create_bundle(work, revision, artifact)
return source, work, revision, artifact
def test_stage_dry_run_install_and_conflict_safe_rollback(repos, tmp_path):
source, work, revision, artifact = repos
target = tmp_path / "staged"
result = bundle.stage_bundle(artifact, source, target, dry_run=True)
assert result["status"] == "DRY_RUN"
assert not target.exists()
assert (source / "app.py").read_text() == "print('before')\n"
result = bundle.stage_bundle(artifact, source, target)
assert result["status"] == "STAGED"
assert (target / "app.py").read_text() == "print('after')\n"
assert not (target / "retired.txt").exists()
assert (target / "new/tool.py").is_file()
assert git(target, "rev-parse", "HEAD") == revision
with pytest.raises(bundle.PackageError, match="exists"):
bundle.stage_bundle(artifact, source, target)
(target / "app.py").write_text("owner edited\n")
with pytest.raises(bundle.PackageError, match="changed"):
bundle.rollback(target)
assert (target / "app.py").read_text() == "owner edited\n"
assert (target / "new/tool.py").exists()
(target / "app.py").write_text("print('after')\n")
assert bundle.rollback(target, dry_run=True)["status"] == "DRY_RUN"
assert bundle.rollback(target)["status"] == "ROLLED_BACK"
assert not (target / "new/tool.py").exists()
assert (target / "retired.txt").read_text() == "historical\n"
assert not git(target, "status", "--porcelain")
def test_dirty_wrong_base_and_unknown_destination_refused(repos, tmp_path):
source, work, revision, artifact = repos
(source / "local.txt").write_text("do not overwrite")
with pytest.raises(bundle.PackageError, match="clean"):
bundle.stage_bundle(artifact, source, tmp_path / "one")
(source / "local.txt").unlink()
(source / "app.py").write_text("another base")
git(source, "add", ".")
git(source, "-c", "user.name=Test", "-c", "user.email=test@localhost", "commit", "--quiet", "-m", "different")
with pytest.raises(bundle.PackageError, match="baseline"):
bundle.stage_bundle(artifact, source, tmp_path / "two")
assert not (tmp_path / "two").exists()
def test_tamper_traversal_extra_entries_and_symlink_refused(repos, tmp_path):
source, work, revision, artifact = repos
with zipfile.ZipFile(artifact) as z:
entries = {name: z.read(name) for name in z.namelist()}
for index, change in enumerate((
{"new/app.py": b"tampered"},
{"../escape": b"bad"},
{"unlisted.py": b"bad"},
)):
bad = tmp_path / f"bad-{index}.zip"
with zipfile.ZipFile(bad, "w") as z:
for name, body in (entries | change).items():
z.writestr(name, body)
with pytest.raises(bundle.PackageError):
bundle.verify_bundle(bad)
(work / "escape.py").symlink_to(source / "app.py")
with pytest.raises(bundle.PackageError, match="symlink"):
bundle.create_bundle(work, revision, tmp_path / "links.zip")
def test_rollback_refuses_added_file_symlink_without_touching_referent(repos, tmp_path):
source, work, revision, artifact = repos
target = tmp_path / "staged"
bundle.stage_bundle(artifact, source, target)
outside = tmp_path / "outside"
outside.write_text("owner data")
(target / "new/tool.py").unlink()
(target / "new/tool.py").symlink_to(outside)
with pytest.raises(bundle.PackageError, match="symlink"):
bundle.rollback(target)
assert outside.read_text() == "owner data"
assert (target / "app.py").read_text() == "print('after')\n"
def test_manifest_semantic_tampering_is_rejected(repos, tmp_path):
source, work, revision, artifact = repos
with zipfile.ZipFile(artifact) as z:
entries = {name: z.read(name) for name in z.namelist()}
document = json.loads(entries["manifest.json"])
document["changes"][0]["before_sha256"] = "0" * 64
entries["manifest.json"] = json.dumps(document).encode()
bad = tmp_path / "bad-manifest.zip"
with zipfile.ZipFile(bad, "w") as z:
for name, body in entries.items():
z.writestr(name, body)
with pytest.raises(bundle.PackageError, match="before|base"):
bundle.verify_bundle(bad)
def test_rollback_preserves_user_staged_changes(repos, tmp_path):
source, work, revision, artifact = repos
target = tmp_path / "staged"
bundle.stage_bundle(artifact, source, target)
git(target, "add", "app.py")
with pytest.raises(bundle.PackageError, match="index changed"):
bundle.rollback(target)
assert (target / "app.py").read_text() == "print('after')\n"
assert git(target, "diff", "--cached", "--name-only") == "app.py"
+583
View File
@@ -0,0 +1,583 @@
from __future__ import annotations
import base64
import json
import sqlite3
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from aios_baseline.lineage import (
LineageConflict,
LineageLedger,
LineageVerificationError,
VerifiedAcceptanceEvidence,
)
from aios_baseline.release import (
ReleaseAuthorizationError,
ReleaseConflict,
ReleaseLedger,
ReleaseReconciliationRequired,
ReleaseVerificationError,
VerifiedReceipt,
)
from aios_baseline.verifiers import (
ACCEPTANCE_EVIDENCE_DOMAIN,
RELEASE_RECEIPT_DOMAIN,
SignatureVerificationError,
ed25519_acceptance_evidence_verifier,
ed25519_release_receipt_verifier,
)
class TaskAuthority:
def __init__(self):
self.tasks = {}
self.jobs = {}
def add(self, task_id, tenant_id="tenant-a", subject_id="user-a", **metadata):
metadata.setdefault("component_id", "component-a")
self.tasks[task_id] = {
"task_id": task_id,
"plan": {
"issuer": {"tenant_id": tenant_id, "subject_id": subject_id},
"metadata": metadata,
},
}
def get_task(self, task_id):
return self.tasks[task_id]
def add_job(
self, job_id, task_id, effect_id, tenant_id="tenant-a", subject_id="user-a",
mission_id="mission-1", grant_id="grant-1",
):
self.jobs[job_id] = {
"job_id": job_id,
"request": {
"tenant_id": tenant_id, "subject_id": subject_id,
"mission_id": mission_id, "grant_id": grant_id,
"task_id": task_id, "effect_id": effect_id,
},
}
def read(self, job_id):
return self.jobs[job_id]
@pytest.fixture
def authority():
value = TaskAuthority()
value.add("task-1", mission_id="mission-1", grant_id="grant-1")
value.add("task-2", mission_id="mission-1", grant_id="grant-1")
value.add_job("job-1", "task-1", "effect-1")
value.add_job("job-2", "task-2", "effect-1")
return value
def build_lineage(path, authority):
ledger = LineageLedger(path, authority, authority)
ledger.link_task("tenant-a", "user-a", "mission-1", "grant-1", "task-1")
ledger.link_effect(
"tenant-a", "user-a", "mission-1", "task-1", "effect-1", "job-1"
)
ledger.link_release(
"tenant-a", "user-a", "mission-1", "effect-1", "release-1", {"sha256": "abc"}
)
return ledger
def test_lineage_is_durable_and_explains_only_exact_mission(tmp_path, authority):
db = tmp_path / "domain.sqlite"
ledger = build_lineage(db, authority)
ledger.link_deployment(
"tenant-a", "user-a", "mission-1", "release-1", "deployment-1",
"component-a", "staging", 1, {"sha256": "abc"},
)
ledger.close()
reopened = LineageLedger(db, authority, authority)
graph = reopened.explain("tenant-a", "user-a", "mission-1")
assert [item["task_id"] for item in graph["tasks"]] == ["task-1"]
assert graph["effects"][0]["job_id"] == "job-1"
assert graph["releases"][0]["source_task_id"] == "task-1"
assert graph["deployments"][0]["release_id"] == "release-1"
assert reopened.explain("tenant-a", "user-a", "mission-10")["tasks"] == []
def test_task_authority_and_global_within_tenant_identity_are_enforced(tmp_path, authority):
ledger = LineageLedger(tmp_path / "lineage.sqlite", authority, authority)
with pytest.raises(LineageVerificationError, match="task_authority"):
ledger.link_task("tenant-a", "intruder", "mission-1", "grant-1", "task-1")
ledger.link_task("tenant-a", "user-a", "mission-1", "grant-1", "task-1")
with pytest.raises((LineageConflict, LineageVerificationError)):
ledger.link_task("tenant-a", "other-user", "other", "other", "task-1")
# A tenant boundary permits the same opaque ID without leaking either graph.
authority.add("shared", tenant_id="tenant-a", subject_id="user-a")
ledger.link_task("tenant-a", "user-a", "m-a", "g-a", "shared")
authority.add("shared", tenant_id="tenant-b", subject_id="user-b")
ledger.link_task("tenant-b", "user-b", "m-b", "g-b", "shared")
assert len(ledger.explain("tenant-a", "user-a", "m-a")["tasks"]) == 1
assert len(ledger.explain("tenant-b", "user-b", "m-b")["tasks"]) == 1
def test_job_effect_and_release_bindings_are_immutable(tmp_path, authority):
ledger = build_lineage(tmp_path / "lineage.sqlite", authority)
ledger.link_task("tenant-a", "user-a", "mission-1", "grant-1", "task-2")
with pytest.raises((LineageConflict, LineageVerificationError)):
ledger.link_effect(
"tenant-a", "user-a", "mission-1", "task-2", "effect-2", "job-1"
)
with pytest.raises(LineageConflict, match="release-producing effect"):
ledger.link_release(
"tenant-a", "user-a", "mission-1", "effect-1", "release-2", {"sha256": "def"}
)
with pytest.raises(LineageConflict, match="effect_id"):
ledger.link_effect(
"tenant-a", "user-a", "mission-1", "task-2", "effect-1", "job-2"
)
authority.add("task-other-subject", tenant_id="tenant-a", subject_id="user-b")
ledger.link_task("tenant-a", "user-b", "mission-b", "grant-b", "task-other-subject")
with pytest.raises((LineageConflict, LineageVerificationError)):
ledger.link_effect(
"tenant-a", "user-b", "mission-b", "task-other-subject", "effect-b", "job-1"
)
def _acceptance_verifier(raw, expected):
if raw != {"proof": "trusted"}:
raise ValueError("untrusted")
return VerifiedAcceptanceEvidence(
acceptance_id=expected["acceptance_id"],
tenant_id=expected["tenant_id"],
subject_id=expected["subject_id"],
deployment_id=expected["deployment_id"],
release_id=expected["release_id"],
component_id=expected["component_id"],
artifact=expected["artifact"],
accepted=True,
evidence_id="evidence-1",
)
def test_acceptance_rejects_wrong_deployment_release_or_artifact(tmp_path, authority):
ledger = build_lineage(tmp_path / "lineage.sqlite", authority)
ledger.link_deployment(
"tenant-a", "user-a", "mission-1", "release-1", "deployment-1",
"component-a", "staging", 1, {"sha256": "abc"},
)
for deployment, release, artifact in (
("deployment-x", "release-1", {"sha256": "abc"}),
("deployment-1", "release-x", {"sha256": "abc"}),
("deployment-1", "release-1", {"sha256": "wrong"}),
):
with pytest.raises((LineageConflict, LineageVerificationError)):
ledger.record_acceptance(
"tenant-a", "user-a", "mission-1", "component-a", "acceptance-x",
deployment, release, artifact, True,
evidence={"proof": "trusted"}, verifier=_acceptance_verifier,
)
accepted = ledger.record_acceptance(
"tenant-a", "user-a", "mission-1", "component-a", "acceptance-1",
"deployment-1", "release-1", {"sha256": "abc"}, True,
evidence={"proof": "trusted"}, verifier=_acceptance_verifier,
)
assert accepted["acceptance_id"] == "acceptance-1"
with pytest.raises(LineageConflict, match="component"):
ledger.record_acceptance(
"tenant-a", "user-a", "mission-1", "component-b", "acceptance-component",
"deployment-1", "release-1", {"sha256": "abc"}, True,
evidence={"proof": "trusted"}, verifier=_acceptance_verifier,
)
def wrong_binding(raw, expected):
verified = _acceptance_verifier(raw, expected)
return VerifiedAcceptanceEvidence(
**{**verified.__dict__, "deployment_id": "deployment-other"}
)
with pytest.raises(LineageVerificationError, match="binding"):
ledger.record_acceptance(
"tenant-a", "user-a", "mission-1", "component-a", "acceptance-2",
"deployment-1", "release-1", {"sha256": "abc"}, True,
evidence={"proof": "trusted"}, verifier=wrong_binding,
)
def _receipt_verifier(raw, expected):
if raw.get("signed_by") != "configured-sink":
raise ValueError("untrusted receipt")
return VerifiedReceipt(
receipt_id=raw["receipt_id"],
tenant_id=expected["tenant_id"],
subject_id=expected["subject_id"],
component_id=expected["component_id"],
task_id=expected["task_id"],
source_task_id=expected["source_task_id"],
source_effect_id=expected["source_effect_id"],
grant_id=expected["grant_id"],
job_id=expected["job_id"],
effect_id=raw["effect_id"],
operation_id=expected["operation_id"],
deployment_id=expected["deployment_id"],
release_id=expected["release_id"],
artifact=expected["artifact"],
observed={"healthy": True},
)
def _target_authorizer(tenant_id, subject_id, component_id, environment, action):
return (
tenant_id == "tenant-a"
and subject_id in {"user-a", "operator-a"}
and component_id in {"component-a", "component-b"}
and environment == "staging"
and action in {"read", "write", "deploy", "accept"}
)
class GovernedAdapter:
def __init__(self, authority, execute, reconcile=None):
self.authority = authority
self._execute = execute
if reconcile is not None:
self.reconcile = reconcile
def prepare(self, operation):
job_id = f"real-job:{operation['saga_id']}:{operation['stage_index']}"
self.authority.add_job(
job_id, operation["task_id"], operation["effect_id"],
tenant_id=operation["tenant_id"], subject_id=operation["subject_id"],
mission_id=operation["mission_id"], grant_id=operation["grant_id"],
)
return {"job_id": job_id}
def execute(self, operation):
return self._execute(operation)
def _release_ledger(path, lineage):
release = ReleaseLedger(
path, lineage=lineage, receipt_verifier=_receipt_verifier,
target_authorizer=_target_authorizer,
)
release.register_recipe(
"recipe-1",
[
{"stage_id": "install", "operation_id": "deploy.install"},
{"stage_id": "health", "operation_id": "deploy.health"},
],
)
release.set_target(
"tenant-a", "user-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=None, stage="STAGED", source_revision="source-5",
observed_main_revision="main-9", rollback_compatible=True,
)
release.start_saga(
"tenant-a", "user-a", "mission-1", "component-a", "saga-1", "release-1",
"deployment-1", "staging", {"sha256": "abc"}, "recipe-1",
expected_target_revision=1,
)
return release
def test_release_target_is_persistent_cas_and_allows_main_ahead(tmp_path, authority):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
release = _release_ledger(db, lineage)
target = release.get_target("tenant-a", "user-a", "component-a", "staging")
assert target.revision == 1
assert target.source_revision == "source-5"
assert target.observed_main_revision == "main-9" # main may be ahead while staged
with pytest.raises(ReleaseConflict, match="expected revision"):
release.set_target(
"tenant-a", "user-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=0,
)
reopened = ReleaseLedger(
db, lineage=lineage, receipt_verifier=_receipt_verifier,
target_authorizer=_target_authorizer,
)
assert reopened.get_target("tenant-a", "user-a", "component-a", "staging").revision == 1
assert reopened.get_saga("tenant-a", "user-a", "saga-1")["stage_id"] == "install"
with pytest.raises(ReleaseAuthorizationError):
reopened.get_saga("tenant-a", "intruder", "saga-1")
def test_targets_are_component_scoped_and_subject_is_authorized_actor(tmp_path, authority):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
authority.add("task-b", component_id="component-b", mission_id="mission-b", grant_id="grant-b")
authority.add_job("job-b", "task-b", "effect-b", mission_id="mission-b", grant_id="grant-b")
lineage.link_task("tenant-a", "user-a", "mission-b", "grant-b", "task-b")
lineage.link_effect("tenant-a", "user-a", "mission-b", "task-b", "effect-b", "job-b")
lineage.link_release(
"tenant-a", "user-a", "mission-b", "effect-b", "release-b", {"sha256": "bbb"}
)
release = _release_ledger(db, lineage)
second = release.set_target(
"tenant-a", "user-a", "component-b", "staging", "release-b", {"sha256": "bbb"},
expected_revision=None,
)
assert second.revision == 1
assert release.get_target("tenant-a", "user-a", "component-a", "staging").release_id == "release-1"
assert release.get_target("tenant-a", "user-a", "component-b", "staging").release_id == "release-b"
shared = release.set_target(
"tenant-a", "operator-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=1,
)
assert shared.revision == 2
assert shared.updated_by_subject_id == "operator-a"
with pytest.raises(ReleaseConflict, match="expected revision"):
release.set_target(
"tenant-a", "user-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=1,
)
with pytest.raises(ReleaseAuthorizationError):
release.get_target("tenant-a", "intruder", "component-a", "staging")
with pytest.raises(ReleaseAuthorizationError):
release.set_target(
"tenant-a", "intruder", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=2,
)
def test_unversioned_candidate_release_schema_is_rejected(tmp_path, authority):
db = tmp_path / "legacy-candidate.sqlite"
lineage = build_lineage(db, authority)
with sqlite3.connect(db) as connection:
connection.execute("CREATE TABLE release_targets(tenant_id TEXT, environment TEXT)")
with pytest.raises(ReleaseConflict, match="unversioned"):
ReleaseLedger(
db, lineage=lineage, receipt_verifier=_receipt_verifier,
target_authorizer=_target_authorizer,
)
def test_target_rollback_requires_the_persisted_compatible_binding(tmp_path, authority):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
release = _release_ledger(db, lineage)
with pytest.raises(ReleaseConflict, match="rollback target"):
release.set_target(
"tenant-a", "user-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=1, stage="ROLLBACK_STAGED",
)
def test_outer_artifact_manifest_must_not_hash_itself(tmp_path, authority):
ledger = LineageLedger(tmp_path / "lineage.sqlite", authority, authority)
ledger.link_task("tenant-a", "user-a", "mission-1", "grant-1", "task-1")
ledger.link_effect(
"tenant-a", "user-a", "mission-1", "task-1", "effect-1", "job-1"
)
with pytest.raises(ValueError, match="self hash"):
ledger.link_release(
"tenant-a", "user-a", "mission-1", "effect-1", "release-1",
{"files": [], "manifest_sha256": "recursive-impossibility"},
)
def test_release_saga_reuses_effect_after_crash_and_advances_only_verified_receipt(
tmp_path, authority
):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
release = _release_ledger(db, lineage)
seen = []
def crash(operation):
seen.append(operation["effect_id"])
raise RuntimeError("process crashed after durable external sink write")
with pytest.raises(RuntimeError, match="crashed"):
release.advance_saga("tenant-a", "user-a", "saga-1", 0, GovernedAdapter(authority, crash))
assert release.get_saga("tenant-a", "user-a", "saga-1")["revision"] == 0
# A new process/object observes the intent and keeps its stable effect fence.
release = ReleaseLedger(
db, lineage=lineage, receipt_verifier=_receipt_verifier,
target_authorizer=_target_authorizer,
)
with pytest.raises(ReleaseReconciliationRequired, match="RECONCILIATION_REQUIRED"):
release.advance_saga(
"tenant-a", "user-a", "saga-1", 0,
GovernedAdapter(authority, lambda _operation: pytest.fail("unknown effect repeated")),
)
def resume(operation):
seen.append(operation["effect_id"])
return {
"signed_by": "configured-sink",
"receipt_id": "receipt-install",
"effect_id": operation["effect_id"],
}
adapter=GovernedAdapter(authority, lambda _operation: pytest.fail("unknown effect repeated"), reconcile=resume)
advanced = release.advance_saga("tenant-a", "user-a", "saga-1", 0, adapter)
assert seen[0] == seen[1]
assert advanced["stage_id"] == "health"
assert advanced["revision"] == 1
with pytest.raises(ReleaseConflict, match="expected revision"):
release.advance_saga("tenant-a", "user-a", "saga-1", 0, adapter)
def test_release_rejects_receipt_for_wrong_effect(tmp_path, authority):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
release = _release_ledger(db, lineage)
calls = []
def wrong(operation):
calls.append(operation["effect_id"])
return {
"signed_by": "configured-sink",
"receipt_id": "receipt-wrong",
"effect_id": "different-effect",
}
with pytest.raises(ReleaseVerificationError, match="effect_id"):
release.advance_saga("tenant-a", "user-a", "saga-1", 0, GovernedAdapter(authority, wrong))
with pytest.raises(ReleaseVerificationError, match="effect_id"):
release.advance_saga("tenant-a", "user-a", "saga-1", 0, GovernedAdapter(authority, wrong))
assert len(calls) == 1 # the durable unverified receipt prevents effect replay
assert release.get_saga("tenant-a", "user-a", "saga-1")["revision"] == 0
def test_release_rejects_verified_receipt_for_other_component(tmp_path, authority):
db = tmp_path / "domain.sqlite"
lineage = build_lineage(db, authority)
def wrong_component(raw, expected):
return VerifiedReceipt(
receipt_id="receipt-wrong-component", tenant_id=expected["tenant_id"],
subject_id=expected["subject_id"], component_id="component-b",
task_id=expected["task_id"],source_task_id=expected["source_task_id"],
source_effect_id=expected["source_effect_id"],grant_id=expected["grant_id"],job_id=expected["job_id"],
effect_id=expected["effect_id"], operation_id=expected["operation_id"],
deployment_id=expected["deployment_id"], release_id=expected["release_id"],
artifact=expected["artifact"], observed={"healthy": True},
)
release = ReleaseLedger(
db, lineage=lineage, receipt_verifier=wrong_component,
target_authorizer=_target_authorizer,
)
release.register_recipe("recipe", [{"stage_id": "install", "operation_id": "deploy.install"}])
release.set_target(
"tenant-a", "user-a", "component-a", "staging", "release-1", {"sha256": "abc"},
expected_revision=None,
)
release.start_saga(
"tenant-a", "user-a", "mission-1", "component-a", "saga", "release-1",
"deployment", "staging", {"sha256": "abc"}, "recipe", expected_target_revision=1,
)
with pytest.raises(ReleaseVerificationError, match="component_id"):
release.advance_saga(
"tenant-a", "user-a", "saga", 0,
GovernedAdapter(authority, lambda operation: {"effect_id": operation["effect_id"]}),
)
def test_outer_artifact_manifest_cannot_hash_itself(tmp_path, authority):
ledger = LineageLedger(tmp_path / "lineage.sqlite", authority, authority)
ledger.link_task("tenant-a", "user-a", "mission-1", "grant-1", "task-1")
ledger.link_effect("tenant-a", "user-a", "mission-1", "task-1", "effect-1", "job-1")
with pytest.raises(ValueError, match="self hash"):
ledger.link_release(
"tenant-a", "user-a", "mission-1", "effect-1", "release-1",
{"sha256": "abc", "manifest_sha256": "self"},
)
def _signed_envelope(private_key, domain, schema, payload, key_id="configured-key"):
canonical = json.dumps(
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
).encode()
signature = private_key.sign(domain + b"\x00" + canonical)
return {
"schema": schema,
"key_id": key_id,
"payload": payload,
"signature": base64.urlsafe_b64encode(signature).rstrip(b"=").decode(),
}
def test_ed25519_release_verifier_authenticates_domain_and_every_binding():
private_key = Ed25519PrivateKey.generate()
verifier = ed25519_release_receipt_verifier(
{"configured-key": private_key.public_key()}
)
expected = {
"tenant_id": "tenant-a", "subject_id": "user-a", "effect_id": "effect-a",
"component_id": "component-a", "task_id": "task-a", "source_task_id": "task-a",
"source_effect_id": "source-effect-a", "grant_id": "grant-a", "job_id": "job-a",
"operation_id": "deploy.install", "deployment_id": "deployment-a",
"release_id": "release-a", "component_id": "component-a",
"artifact": {"sha256": "abc"},
}
payload = {
**expected, "receipt_id": "receipt-a", "outcome": "SUCCEEDED",
"observed": {"installed": True}, "observed_at": "2026-09-07T20:00:00Z",
}
envelope = _signed_envelope(
private_key, RELEASE_RECEIPT_DOMAIN,
"aios.release-operation-receipt-envelope.v1", payload,
)
assert verifier(envelope, expected).effect_id == "effect-a"
tampered = {**envelope, "payload": {**payload, "deployment_id": "other"}}
with pytest.raises(SignatureVerificationError, match="signature"):
verifier(tampered, expected)
cross_domain = _signed_envelope(
private_key, ACCEPTANCE_EVIDENCE_DOMAIN,
"aios.release-operation-receipt-envelope.v1", payload,
)
with pytest.raises(SignatureVerificationError, match="signature"):
verifier(cross_domain, expected)
with pytest.raises(SignatureVerificationError, match="fields"):
verifier({**envelope, "public_key": "caller-controlled"}, expected)
def test_ed25519_acceptance_verifier_binds_decision_and_deployment(tmp_path, authority):
private_key = Ed25519PrivateKey.generate()
verifier = ed25519_acceptance_evidence_verifier(
{"acceptance-key": private_key.public_key()}
)
expected = {
"acceptance_id": "acceptance-crypto", "tenant_id": "tenant-a",
"subject_id": "user-a", "deployment_id": "deployment-1",
"release_id": "release-1", "component_id": "component-a",
"artifact": {"sha256": "abc"}, "accepted": True,
}
payload = {
**expected, "evidence_id": "evidence-a", "observed": {"healthy": True},
"observed_at": "2026-09-07T20:00:00Z",
}
envelope = _signed_envelope(
private_key, ACCEPTANCE_EVIDENCE_DOMAIN,
"aios.release-acceptance-evidence-envelope.v1", payload, key_id="acceptance-key",
)
assert verifier(envelope, expected).accepted is True
ledger = build_lineage(tmp_path / "signed-acceptance.sqlite", authority)
ledger.link_deployment(
"tenant-a", "user-a", "mission-1", "release-1", "deployment-1",
"component-a", "staging", 1, {"sha256": "abc"},
)
stored = ledger.record_acceptance(
"tenant-a", "user-a", "mission-1", "component-a", "acceptance-crypto",
"deployment-1", "release-1", {"sha256": "abc"}, True,
evidence=envelope, verifier=verifier,
)
assert stored["evidence_id"] == "evidence-a"
with pytest.raises(SignatureVerificationError, match="accepted"):
verifier(envelope, {**expected, "accepted": False})
with pytest.raises(SignatureVerificationError, match="deployment_id"):
verifier(envelope, {**expected, "deployment_id": "deployment-other"})
with pytest.raises(SignatureVerificationError, match="component_id"):
verifier(envelope, {**expected, "component_id": "component-b"})
+269
View File
@@ -0,0 +1,269 @@
from __future__ import annotations
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import threading
import time
import pytest
from aios_baseline.service import Identity
from aios_baseline.transport import HttpTransport, TransportError, serve
from aios_baseline.worker import OperationWorker
from tests.baseline_v1_1.test_loop import REQUEST, USER, WORKER, fixture_service
def _running_server(service):
server = serve(
service,
{"user-token-012345678901234567890123456789": USER, "worker-token-012345678901234567890123456789": WORKER},
host="127.0.0.1",
port=0,
)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server, thread
def _stop(server, thread):
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_real_http_goal_worker_receipt_query_and_explain(tmp_path):
service = fixture_service(tmp_path)
server, thread = _running_server(service)
base_url = f"http://127.0.0.1:{server.server_port}"
try:
user = HttpTransport(base_url, "user-token-012345678901234567890123456789")
worker_transport = HttpTransport(base_url, "worker-token-012345678901234567890123456789")
created = user.submit_goal(REQUEST, idempotency_key="transport-a")
worker = OperationWorker(worker_transport, poll_seconds=0.01)
completed = worker.run_once()
assert completed["task_id"] == created["task_id"]
assert completed["status"] == "COMPLETED"
assert completed["receipt"]["observation"]["properties"]["MainPID"] == "123"
assert user.get_task(created["task_id"])["status"] == "COMPLETED"
explained = user.explain("mission-a")
assert explained["mission_id"] == "mission-a"
assert [item["task_id"] for item in explained["tasks_runtime"]] == [created["task_id"]]
finally:
_stop(server, thread)
def test_cancel_and_worker_claim_have_no_client_completion_or_receipt_route(tmp_path):
service = fixture_service(tmp_path)
server, thread = _running_server(service)
base_url = f"http://127.0.0.1:{server.server_port}"
try:
user = HttpTransport(base_url, "user-token-012345678901234567890123456789")
worker = HttpTransport(base_url, "worker-token-012345678901234567890123456789")
created = user.submit_goal(REQUEST, idempotency_key="transport-cancel")
cancelled = user.cancel_task(created["task_id"])
worker.register_worker()
worker.heartbeat()
assert cancelled["status"] == "CANCELLED"
assert worker.claim_next() is None
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
body = json.dumps({"task_id": created["task_id"], "receipt": {"success": True}})
connection.request(
"POST",
"/v1/workers/complete",
body=body,
headers={
"Authorization": "Bearer worker-token-012345678901234567890123456789",
"Content-Type": "application/json",
"Content-Length": str(len(body.encode())),
"X-AIOS-Deadline": str(time.time() + 3),
},
)
response = connection.getresponse()
response.read()
connection.close()
assert response.status == 404
assert user.get_task(created["task_id"])["status"] == "CANCELLED"
finally:
_stop(server, thread)
def test_server_uses_token_identity_and_rejects_body_scope_claims(tmp_path):
service = fixture_service(tmp_path)
server, thread = _running_server(service)
base_url = f"http://127.0.0.1:{server.server_port}"
try:
user = HttpTransport(base_url, "user-token-012345678901234567890123456789")
with pytest.raises(TransportError, match="HTTP_IDENTITY_FIELD_FORBIDDEN") as exc:
user.submit_goal({**REQUEST, "tenant_id": "tenant-b"}, idempotency_key="spoof")
assert exc.value.status == 400
worker = HttpTransport(base_url, "worker-token-012345678901234567890123456789")
with pytest.raises(TransportError, match="HTTP_IDENTITY_FIELD_FORBIDDEN"):
worker._request("POST", "/v1/workers/register", {"tenant_id": "tenant-b"})
outsider = HttpTransport(base_url, "not-a-token-012345678901234567890123456789")
with pytest.raises(TransportError, match="HTTP_UNAUTHORIZED") as unauthorized:
outsider.submit_goal(REQUEST, idempotency_key="unauthorized")
assert unauthorized.value.status == 401
finally:
_stop(server, thread)
def test_http_boundary_rejects_large_malformed_or_expired_requests(tmp_path):
service = fixture_service(tmp_path)
server, thread = _running_server(service)
try:
def post(raw, *, deadline):
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
connection.request(
"POST",
"/v1/goals/submit",
body=raw,
headers={
"Authorization": "Bearer user-token-012345678901234567890123456789",
"Content-Type": "application/json",
"Content-Length": str(len(raw)),
"X-AIOS-Deadline": deadline,
},
)
response = connection.getresponse()
payload = response.read()
connection.close()
return response.status, payload
status, payload = post(b"{" + b"x" * 70_000, deadline=str(time.time() + 3))
assert status == 413
assert b"HTTP_BODY_TOO_LARGE" in payload
status, payload = post(b"not-json", deadline=str(time.time() + 3))
assert status == 400
assert b"HTTP_JSON_INVALID" in payload
status, payload = post(
b'{"request":{},"request":{},"idempotency_key":"duplicate"}',
deadline=str(time.time() + 3),
)
assert status == 400
assert b"HTTP_JSON_DUPLICATE_KEY" in payload
status, payload = post(
b'{"request":{},"idempotency_key":NaN}', deadline=str(time.time() + 3)
)
assert status == 400
assert b"HTTP_JSON_NON_FINITE" in payload
status, payload = post(b"{}", deadline=str(time.time() - 1))
assert status == 408
assert b"HTTP_DEADLINE_EXPIRED" in payload
finally:
_stop(server, thread)
def test_internal_errors_do_not_return_tracebacks_exception_text_or_tokens(tmp_path):
service = fixture_service(tmp_path)
original = service.get_task
def fail(_identity, _task_id):
raise RuntimeError("user-token-012345678901234567890123456789 super-secret traceback bait")
service.get_task = fail
server, thread = _running_server(service)
try:
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=3)
connection.request(
"GET",
"/v1/tasks/task-example",
headers={
"Authorization": "Bearer user-token-012345678901234567890123456789",
"X-AIOS-Deadline": str(time.time() + 3),
},
)
response = connection.getresponse()
payload = response.read().decode()
connection.close()
assert response.status == 500
assert json.loads(payload) == {"error": {"code": "HTTP_INTERNAL_ERROR"}}
assert "secret" not in payload.lower()
assert "traceback" not in payload.lower()
assert "user-token-012345678901234567890123456789" not in payload
finally:
service.get_task = original
_stop(server, thread)
def test_transport_is_loopback_only_and_rejects_redirects():
with pytest.raises(TransportError, match="TRANSPORT_LOOPBACK_REQUIRED"):
HttpTransport("http://example.com:8080", "token-012345678901234567890123456789")
aios_server = serve(object(), {"worker-token-012345678901234567890123456789": WORKER}, port=0)
aios_thread = threading.Thread(target=aios_server.serve_forever, daemon=True)
aios_thread.start()
try:
connection = HTTPConnection("127.0.0.1", aios_server.server_port, timeout=3)
connection.request(
"PUT",
"/v1/workers/execute",
body=b"{}",
headers={
"Authorization": "Bearer worker-token-012345678901234567890123456789",
"Content-Type": "application/json",
"X-AIOS-Deadline": str(time.time() + 3),
},
)
response = connection.getresponse()
payload = response.read()
connection.close()
assert response.status == 405
assert json.loads(payload) == {"error": {"code": "HTTP_METHOD_NOT_ALLOWED"}}
assert response.headers["Content-Type"] == "application/json"
finally:
_stop(aios_server, aios_thread)
class Redirect(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", "/elsewhere")
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, *_args):
return
redirector = ThreadingHTTPServer(("127.0.0.1", 0), Redirect)
thread = threading.Thread(target=redirector.serve_forever, daemon=True)
thread.start()
try:
transport = HttpTransport(f"http://127.0.0.1:{redirector.server_port}", "token-012345678901234567890123456789")
with pytest.raises(TransportError, match="TRANSPORT_REDIRECT_REJECTED"):
transport.get_task("task-a")
finally:
_stop(redirector, thread)
def test_worker_run_forever_uses_stop_event_wait_and_stops(tmp_path):
service = fixture_service(tmp_path)
server, server_thread = _running_server(service)
stop = threading.Event()
worker = OperationWorker(
HttpTransport(f"http://127.0.0.1:{server.server_port}", "worker-token-012345678901234567890123456789"),
poll_seconds=0.01,
stop_event=stop,
)
thread = threading.Thread(target=worker.run_forever, daemon=True)
try:
thread.start()
deadline = time.time() + 2
while not worker.registered and time.time() < deadline:
time.sleep(0.01)
assert worker.registered is True
stop.set()
thread.join(timeout=1)
assert not thread.is_alive()
finally:
stop.set()
thread.join(timeout=1)
_stop(server, server_thread)
+71
View File
@@ -0,0 +1,71 @@
from pathlib import Path
import os
import sys
import time
import pytest
from aios_baseline.worker_policy import WorkerProfile, eligible
from aios_baseline.host import SystemdHost, HostError
def profile(**changes):
value = dict(worker_id='worker-a', identity='machine-a', tenant_scope=['tenant-a'],
subject_scope=['user-a'], capabilities=['service-status'], priority=10,
fallback_executor=True, heartbeat_seconds=15, max_concurrency=1)
value.update(changes)
return WorkerProfile.from_dict(value)
def test_fallback_expands_only_after_ready_deadline():
task = dict(tenant_id='tenant-a', subject_id='user-a', operation='service-status', ready_at=100)
assert not eligible(profile(), task, now=699, last_heartbeat=699)
assert eligible(profile(), task, now=700, last_heartbeat=700)
assert eligible(profile(fallback_executor=False), task, now=101, last_heartbeat=101)
assert not eligible(profile(), task, now=700, last_heartbeat=640)
def test_fallback_cannot_expand_tenant_subject_or_capability():
task = dict(tenant_id='tenant-a', subject_id='user-b', operation='service-status', ready_at=100)
assert not eligible(profile(), task, now=800, last_heartbeat=800)
assert not eligible(profile(), {**task, 'subject_id':'user-a','tenant_id':'tenant-b'}, now=800, last_heartbeat=800)
assert not eligible(profile(), {**task, 'subject_id':'user-a','operation':'service-restart'}, now=800, last_heartbeat=800)
def systemctl(tmp_path, body):
path = tmp_path / 'systemctl-fixture'
path.write_text('#!' + sys.executable + '\n' + body)
path.chmod(0o700)
return str(path)
def test_host_status_uses_real_bounded_process_and_no_ambient_credentials(tmp_path, monkeypatch):
executable = systemctl(tmp_path, "import os,sys\nassert 'AIOS_ADMIN_TOKEN' not in os.environ\nassert sys.argv[-1] == 'sample.service'\nprint('Id=sample.service\\nActiveState=active\\nSubState=running\\nMainPID=123\\nInvocationID=abc')\n")
monkeypatch.setenv('AIOS_ADMIN_TOKEN','must-not-leak')
host = SystemdHost({'tenant-a:staging:sample':'sample.service'}, executable=executable)
result = host.status('sample', 'staging',tenant_id='tenant-a')
assert result['healthy'] is True
assert result['properties']['MainPID'] == '123'
assert result['unit'] == 'sample.service'
assert 'must-not-leak' not in str(result)
with pytest.raises(HostError): host.status('sample; rm -rf /', 'staging',tenant_id='tenant-a')
def test_host_nonzero_missing_properties_and_timeout_never_healthy(tmp_path):
executable = systemctl(tmp_path, "import sys\nprint('Id=sample.service')\nsys.exit(1)\n")
host = SystemdHost({'tenant-a:staging:sample':'sample.service'}, executable=executable)
with pytest.raises(HostError): host.status('sample','staging',tenant_id='tenant-a')
executable = systemctl(tmp_path, "print('Id=sample.service')\n")
with pytest.raises(HostError): host.status('sample','staging',tenant_id='tenant-a')
executable = systemctl(tmp_path, "import time\ntime.sleep(5)\n")
host = SystemdHost({'tenant-a:staging:sample':'sample.service'}, executable=executable, timeout_seconds=0.1)
start = time.monotonic()
with pytest.raises(HostError, match='TIMEOUT'): host.status('sample','staging',tenant_id='tenant-a')
assert time.monotonic()-start < 2
def test_host_kills_output_flood_and_rejects_changed_executable(tmp_path):
executable=systemctl(tmp_path,"import sys\nsys.stdout.write('X'*200000)\n")
host=SystemdHost({'tenant-a:staging:sample':'sample.service'},executable=executable)
with pytest.raises(HostError,match='TOO_LARGE'): host.status('sample','staging',tenant_id='tenant-a')
Path(executable).write_text('#!'+sys.executable+'\nraise Exception("new unexpected program")\n')
with pytest.raises(HostError,match='IDENTITY_CHANGED'): host.status('sample','staging',tenant_id='tenant-a')
@@ -0,0 +1,78 @@
from __future__ import annotations
from pathlib import Path
import sys
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
import aios_agent_task_api_lineage_supersession as api
class FakeController:
def __init__(self, db_path, verifier, **kwargs):
self.db_path = db_path
def supersede(self, body):
if body.get("fail"):
raise api.TaskLineageSupersessionError("SOURCE_TASK_EXACT_CAS_MISMATCH", 409)
return {"status": "SUPERSEDED", "idempotent": bool(body.get("replay"))}
def test_endpoint_authenticates_before_controller(monkeypatch):
calls = []
monkeypatch.setattr(api, "TaskLineageSupersessionController", FakeController)
endpoint = api.LineageSupersessionEndpoint(
"unused.sqlite3", object(), lambda headers: calls.append(dict(headers))
)
status, result = endpoint.post({"Authorization": "Bearer control"}, {})
assert status == 201
assert result["status"] == "SUPERSEDED"
assert calls == [{"Authorization": "Bearer control"}]
assert endpoint.post({}, {"replay": True})[0] == 200
assert endpoint.post({}, {"fail": True}) == (
409, {"error": "SOURCE_TASK_EXACT_CAS_MISMATCH"}
)
def test_authentication_failure_prevents_controller(monkeypatch):
called = False
class MustNotRun(FakeController):
def supersede(self, body):
nonlocal called
called = True
monkeypatch.setattr(api, "TaskLineageSupersessionController", MustNotRun)
def deny(_headers):
raise PermissionError("denied")
endpoint = api.LineageSupersessionEndpoint("unused", object(), deny)
with pytest.raises(PermissionError, match="denied"):
endpoint.post({}, {})
assert called is False
def test_runtime_binding_is_lazy_and_fails_closed_when_overlay_missing(monkeypatch):
for name in (
"aios_agent_task_api", "aios_agent_task_api_p0_expired_recovery",
"aios_agent_task_protocol", "aios_owner_approval_verifier",
):
monkeypatch.delitem(sys.modules, name, raising=False)
original = __import__
def blocked(name, *args, **kwargs):
if name == "aios_agent_task_api_p0_expired_recovery":
raise ImportError(name)
return original(name, *args, **kwargs)
monkeypatch.setattr("builtins.__import__", blocked)
with pytest.raises(RuntimeError, match="RUNTIME_OVERLAY_UNAVAILABLE"):
api.build_runtime()
def test_error_status_is_not_inferred_from_untrusted_text():
assert api._status(api.TaskLineageSupersessionError("anything", 403)) == 403
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
from pathlib import Path
import sys
import pytest
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_gitops_job_identity import ( # noqa: E402
JobIdentityError,
legacy_task_storage_key,
task_storage_key,
)
def test_collision_resistant_key_separates_historical_lossy_collision() -> None:
first = "TASK/A"
second = "TASK:A"
assert legacy_task_storage_key(first) == legacy_task_storage_key(second) == "TASK-A"
assert task_storage_key(first).startswith("TASK-A-")
assert task_storage_key(second).startswith("TASK-A-")
assert task_storage_key(first) != task_storage_key(second)
@pytest.mark.parametrize("value", ["", " task", "task ", "x" * 513, None])
def test_task_storage_key_rejects_noncanonical_task_identity(value: object) -> None:
with pytest.raises(JobIdentityError, match="TASK_ID_INVALID"):
task_storage_key(value) # type: ignore[arg-type]
@@ -102,6 +102,13 @@ def test_http_route_contract_is_complete():
"/v2/release/execute",
"/v2/release/consistency",
"/v2/release/acceptance",
"/v2/repository/tree",
"/v2/repository/search",
"/v2/repository/diff-index",
"/v2/repository/file",
"/v2/task-admission/envelope",
"/v2/task-admission/authorize",
"/v2/task-admission/status",
}
from aios_chatgpt_bridge_v2_server import ROUTES
@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from types import SimpleNamespace
@@ -20,7 +21,9 @@ COMMIT = "1" * 40
def make_store(tmp_path: Path) -> tuple[GitOpsJobController, str, dict]:
store = JobStore(tmp_path / "job-root")
controller = GitOpsJobController(store)
controller = GitOpsJobController(
store, clock=lambda: datetime(2026, 9, 8, tzinfo=timezone.utc)
)
request = {
"idempotency_key": "v7-replay",
"campaign_id": TASK,
@@ -50,7 +53,7 @@ def make_store(tmp_path: Path) -> tuple[GitOpsJobController, str, dict]:
lineage_anchor_oid=BASE,
current_head_oid=BASE,
generation=2,
lease_expires_at="2099-01-01T00:00:00Z",
lease_expires_at="2026-09-08T00:01:00Z",
expected_revision=row["revision"],
)
with store._connect() as connection:
@@ -295,12 +298,25 @@ def test_real_http_post_carries_external_replay_identity(tmp_path: Path):
observed = {}
class Adapter:
def getRuntimeStatus(self):
return {
"governed_writes_enabled": True,
"legacy_global_writes_enabled": False,
"task_principal_writes_enabled": True,
}
def commitFilesGoverned(self, body):
observed.update(body)
return {"job_id": body["job_id"], "current_head_oid": COMMIT}
def verify_principal(transport, request):
assert transport["operation"] == "commitFilesGoverned"
assert transport["headers"]["authorization"] == "Bearer fixture-principal"
return {"task_id": request["task_id"], "tenant_id": "fixture"}
server = HTTPServer(("127.0.0.1", 0), BridgeHandler)
BridgeHandler.adapter = Adapter()
BridgeHandler.principal_verifier = verify_principal
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
body = {
@@ -317,7 +333,15 @@ def test_real_http_post_carries_external_replay_identity(tmp_path: Path):
}
try:
connection = HTTPConnection("127.0.0.1", server.server_port, timeout=5)
connection.request("POST", "/v2/commit-files", body=json.dumps(body), headers={"Content-Type": "application/json"})
connection.request(
"POST",
"/v2/commit-files",
body=json.dumps(body),
headers={
"Content-Type": "application/json",
"Authorization": "Bearer fixture-principal",
},
)
response = connection.getresponse()
payload = json.loads(response.read().decode())
connection.close()
@@ -326,6 +350,7 @@ def test_real_http_post_carries_external_replay_identity(tmp_path: Path):
server.server_close()
thread.join(timeout=5)
BridgeHandler.adapter = None
BridgeHandler.principal_verifier = None
assert response.status == 200
assert observed["job_id"] == EXTERNAL
assert observed["source_lease_epoch"] == 3
@@ -218,10 +218,14 @@ def test_revision_cas_and_receipt_are_transactional():
def test_job_lease_mutation_is_transactional_and_revision_bound():
with tempfile.TemporaryDirectory() as temp:
controller = GitOpsJobController(JobStore(temp))
controller = GitOpsJobController(
JobStore(temp),
clock=lambda: datetime(2026, 8, 7, 23, 59, tzinfo=timezone.utc),
)
req = {"campaign_id": "campaign", "repository": "aios/aios", "operation": "commit-file", "branch": request().target_branch, "expected_branch_oid": CAMPAIGN_HEAD, "payload_digest": "b" * 64}
job = controller.submit(req)
leased = controller.lease_job(job.job_id, lease_id="lease-1", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=CAMPAIGN_HEAD, generation=1, lease_expires_at="2026-08-08T00:00:00Z", expected_revision=0)
authorized = controller.advance(job.job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED")
leased = controller.lease_job(job.job_id, lease_id="lease-1", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=CAMPAIGN_HEAD, generation=1, lease_expires_at="2026-08-08T00:00:00Z", expected_revision=authorized.revision)
assert leased.state == JobState.LEASED
controller.update_lease("lease-1", expected_revision=0, current_head_oid="c" * 40, generation=1)
with pytest.raises(JobError, match="LEASE_REVISION_CAS_CONFLICT"):
@@ -229,6 +229,10 @@ class ExactPathPolicyTests(unittest.TestCase):
os.environ["AIOS_GITOPS_SERVICE_AUDIENCE"] = "other.service"
path_policy.clear_path_policy_cache()
with mock.patch.object(
path_policy.grp,
"getgrnam",
return_value=mock.Mock(gr_gid=os.getgid()),
), mock.patch.object(
path_policy, "load_path_policy", return_value=loaded
), self.assertRaisesRegex(
path_policy.PathPolicyError,
@@ -242,6 +246,10 @@ class ExactPathPolicyTests(unittest.TestCase):
os.environ["AIOS_GITOPS_ALLOWED_BRANCH"] = "patch/other"
path_policy.clear_path_policy_cache()
with mock.patch.object(
path_policy.grp,
"getgrnam",
return_value=mock.Mock(gr_gid=os.getgid()),
), mock.patch.object(
path_policy, "load_path_policy", return_value=loaded
), self.assertRaisesRegex(
path_policy.PathPolicyError,
+76
View File
@@ -1,12 +1,14 @@
from __future__ import annotations
import tempfile
from types import SimpleNamespace
from pathlib import Path
import pytest
import aios_gitops_job_executor as executor_module
from aios_authority_git_executor import AuthorityGitError, AuthorityGitExecutor
import aios_gitea_machine_transport as gitea_transport_module
from aios_gitea_machine_transport import GiteaMachineTransport
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineCredentialProjection
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore, JobState
@@ -162,3 +164,77 @@ def test_machine_transport_branch_cas_is_not_force_push(monkeypatch):
monkeypatch.setattr(GiteaMachineTransport, "_push_branch_fast_forward", lambda _self, branch, new: calls.append((branch, new)))
assert transport.compare_and_swap_branch(executor_module.BRANCH, OLD, NEW) == NEW
assert calls == [(executor_module.BRANCH, NEW)]
def test_machine_transport_fetches_exact_branch_tip_into_quarantine_ref(monkeypatch, tmp_path):
transport = GiteaMachineTransport(
"http://127.0.0.1:3000/api/v1",
"aios/aios",
"https://g.cnnetmkt.top/git/aios/aios.git",
tmp_path / "repo.git",
)
reads = iter([NEW, NEW])
monkeypatch.setattr(
GiteaMachineTransport,
"read_branch",
lambda _self, _branch: next(reads),
)
askpass = tmp_path / "askpass"
askpass.write_text("#!/bin/sh\n", encoding="utf-8")
monkeypatch.setattr(
GiteaMachineTransport,
"_askpass",
lambda _self: (askpass, {}),
)
calls = []
def run(argv, **kwargs):
calls.append(list(argv))
if "fetch" in argv:
return SimpleNamespace(returncode=0, stdout="", stderr="")
if "rev-parse" in argv:
# Before fetch the quarantine ref is absent; after fetch it resolves.
seen_fetch = any("fetch" in call for call in calls[:-1])
return SimpleNamespace(
returncode=0 if seen_fetch else 1,
stdout=(NEW + "\n") if seen_fetch else "",
stderr="",
)
if "update-ref" in argv:
return SimpleNamespace(returncode=0, stdout="", stderr="")
raise AssertionError(argv)
monkeypatch.setattr(gitea_transport_module.subprocess, "run", run)
assert transport.fetch_branch_exact(executor_module.BRANCH, NEW) == NEW
fetch = next(call for call in calls if "fetch" in call)
assert "--no-tags" in fetch
assert "--no-write-fetch-head" in fetch
assert f"refs/heads/{executor_module.BRANCH}:refs/aios/release-sources/{NEW}" in fetch
assert not any("--force" in item or item == "-f" for item in fetch)
def test_machine_transport_does_not_fetch_when_remote_tip_moved(monkeypatch, tmp_path):
transport = GiteaMachineTransport(
"http://127.0.0.1:3000/api/v1",
"aios/aios",
"https://g.cnnetmkt.top/git/aios/aios.git",
tmp_path / "repo.git",
)
monkeypatch.setattr(
GiteaMachineTransport,
"read_branch",
lambda _self, _branch: OLD,
)
monkeypatch.setattr(
gitea_transport_module.subprocess,
"run",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("git must not run after remote mismatch")
),
)
with pytest.raises(
gitea_transport_module.GiteaTransportError,
match="GITEA_SOURCE_REF_MISMATCH",
):
transport.fetch_branch_exact(executor_module.BRANCH, NEW)
@@ -0,0 +1,176 @@
from __future__ import annotations
import json
import os
from pathlib import Path
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from aios_owner_merge_authorization import (
OwnerMergeApprovalError,
OwnerMergeApprovalPolicy,
OwnerMergeApprovalVerifier,
)
from aios_owner_merge_authorization_server import OwnerMergeApprovalService
from aios_single_host_authority import (
RegistryOwnerApprovalVerifier,
ssh_fingerprint,
ssh_public_line,
)
NOW = 2_000_000_000
def _write_key(path: Path, key: Ed25519PrivateKey) -> None:
path.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
)
os.chmod(path, 0o600)
@pytest.fixture
def service_env(tmp_path: Path):
owner_key = Ed25519PrivateKey.generate()
owner_path = tmp_path / "owner.key"
_write_key(owner_path, owner_key)
registry_path = tmp_path / "registry.json"
registry_path.write_text(
json.dumps(
{
"schema": "aios.single-host-signer-registry.v1",
"authority_generation": 1,
"signers": [
{
"signer_id": "local-owner-authority",
"role": "OWNER_AUTHORITY",
"provider": "local_unix_service",
"fingerprint": ssh_fingerprint(owner_key.public_key()),
"public_key": ssh_public_line(owner_key.public_key(), "owner"),
"socket": "/run/aios/owner-authority.sock",
"secret_ref": "aios-secret://owner",
"status": "ACTIVE",
"generation": 1,
"parent_signer_id": "",
"parent_fingerprint": "",
}
],
},
sort_keys=True,
)
+ "\n"
)
os.chmod(registry_path, 0o644)
policy = OwnerMergeApprovalPolicy(
repository="aios/aios",
pr_number=75,
pr_head_oid="a" * 40,
expected_main_oid="b" * 40,
operation="merge.pull-request",
required_check_contract_digest="c" * 64,
required_checks=("contract", "security"),
owner_identity="owner:aios-single-host",
audit_lineage_digest="d" * 64,
maximum_ttl_seconds=300,
)
policy_path = tmp_path / "policy.json"
policy_path.write_text(json.dumps(policy.as_dict(), sort_keys=True) + "\n")
os.chmod(policy_path, 0o600)
return {
"policy": policy,
"policy_path": policy_path,
"registry_path": registry_path,
"owner_path": owner_path,
"ledger_path": tmp_path / "issuance.sqlite3",
}
def _service(env):
return OwnerMergeApprovalService(
policy_path=env["policy_path"],
registry_path=env["registry_path"],
owner_key_path=env["owner_path"],
issuance_ledger_path=env["ledger_path"],
allowed_uids=(1000,),
require_root_owner=False,
clock=lambda: NOW,
token_urlsafe=lambda _size: "server-generated-nonce",
)
def test_service_issues_exact_owner_approval_and_exact_replay_is_audit_idempotent(service_env):
service = _service(service_env)
request = {"operation": "issue_owner_merge_approval", "payload": {}}
first = service.handle(request, peer_uid=1000)
second = _service(service_env).handle(request, peer_uid=1000)
assert first["status"] == "PASS"
assert first["idempotent"] is False
assert second["idempotent"] is True
assert second["artifact"] == first["artifact"]
verifier = OwnerMergeApprovalVerifier(
primary=RegistryOwnerApprovalVerifier(
service_env["registry_path"], require_root_owner=False
),
clock=lambda: NOW,
)
payload, proof_path = verifier.verify(first["artifact"], service_env["policy"])
assert proof_path == "primary"
assert payload["repository"] == "aios/aios"
assert payload["pr_number"] == 75
def test_expired_approval_is_retained_for_audit_but_reissued_with_a_new_nonce(service_env):
clock = [NOW]
token_values = iter(("approval-one", "nonce-one", "approval-two", "nonce-two"))
service = OwnerMergeApprovalService(
policy_path=service_env["policy_path"],
registry_path=service_env["registry_path"],
owner_key_path=service_env["owner_path"],
issuance_ledger_path=service_env["ledger_path"],
allowed_uids=(1000,),
require_root_owner=False,
clock=lambda: clock[0],
token_urlsafe=lambda _size: next(token_values),
)
request = {"operation": "issue_owner_merge_approval", "payload": {}}
first = service.handle(request, peer_uid=1000)
clock[0] += service_env["policy"].maximum_ttl_seconds
second = service.handle(request, peer_uid=1000)
assert first["idempotent"] is False
assert second["idempotent"] is False
assert second["artifact"]["payload"]["nonce"] != first["artifact"]["payload"]["nonce"]
assert second["artifact"]["payload"]["issued_at"] == clock[0]
@pytest.mark.parametrize(
"raw_request",
[
{"operation": "issue_owner_merge_approval", "payload": {"nonce": "caller-nonce"}},
{"operation": "issue_owner_merge_approval", "payload": {"pr_head_oid": "f" * 40}},
{"operation": "sign", "payload": {}},
{"operation": "sign_bytes", "payload": {}},
],
)
def test_service_rejects_caller_selected_scope_and_generic_signing(service_env, raw_request):
service = _service(service_env)
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_ISSUER_"):
service.handle(raw_request, peer_uid=1000)
def test_service_rejects_untrusted_peer_before_issuing_anything(service_env):
with pytest.raises(OwnerMergeApprovalError, match="OWNER_MERGE_ISSUER_PEER_UID_FORBIDDEN"):
_service(service_env).handle(
{"operation": "issue_owner_merge_approval", "payload": {}},
peer_uid=2000,
)

Some files were not shown because too many files have changed in this diff Show More