Compare commits

...
Author SHA1 Message Date
owner 2852c2a403 feat(governed): Release Migration Plan (AIOS-BASELINE-1.1-RELEASE-MIGRATION-PLAN) 2026-09-08 13:09:40 +08:00
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
81 changed files with 16228 additions and 254 deletions
@@ -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
+1 -1
View File
@@ -157,7 +157,7 @@ def render_openapi_contract_fragment() -> str:
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" security: [{taskPrincipalAuth: []}]\n"
" x-aios-task-principal: Bridge verifies a fresh aios.task-principal-assertion.v1 and exact task/job/repository/branch/generation/revision/attempt/execution-digest/capability binding; caller JSON cannot provide a principal and missing verification is denied.\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"
@@ -38,6 +38,7 @@ from aios_governed_task_bootstrap import (
GovernedTaskBootstrap,
)
from governed_production_release import build_executor_config
from aios_baseline.runtime_contract import evaluate_deployment
MAX_BODY = 2 * 1024 * 1024
TaskPrincipalVerifier = Callable[
@@ -109,6 +110,77 @@ 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
@@ -919,7 +991,10 @@ class GovernedRuntime:
def snapshot(self) -> dict[str, Any]:
task = self._snapshot_task()
runtime = self.runtime_status()
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={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, 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"})
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 {
@@ -1204,6 +1279,7 @@ def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892)
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",
@@ -1258,27 +1334,33 @@ def main(
repository_discovery_cursor_secret=discovery_cursor_secret,
)
runtime = GovernedRuntime(config)
# A TaskPrincipal verifier is deliberately supplied only by the hosting
# service. Without that independently authenticated verifier, all Git
# mutation requests fail closed while the admission-only routes remain
# available for a configured Owner-approved epoch.
BridgeHandler.principal_verifier = None
# 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_handlers_from_environment,
build_task_admission_runtime_from_environment,
)
task_admission_handlers = build_task_admission_handlers_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_handlers_from_activation_file,
build_task_admission_runtime_from_activation_file,
)
task_admission_handlers = build_task_admission_handlers_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(
build_bridge_adapter(
runtime,
@@ -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:
@@ -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,22 @@ 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
if code == 'MIGRATION_PLAN_REQUIRED':
raise CapabilityAuthorizationError(
'migration plan required'
) from exc
if code == 'MIGRATION_PLAN_INVALID':
raise CapabilityAuthorizationError(
'migration plan invalid'
) from exc
raise TrustedCandidateBuilderUnavailable(
'trusted builder request failed'
) from exc
@@ -290,6 +310,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 +338,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 +411,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,
@@ -438,6 +438,71 @@ class TaskAdmissionStore:
).fetchone()
return dict(row) if row else None
def get_grant_by_id(self, grant_id: str) -> TaskAdmissionGrant | None:
"""Return one immutable admitted grant by its stable grant identity.
This is a read-only lookup used by the server-side TaskPrincipal
recovery path. It never trusts caller-supplied grant content.
"""
if not isinstance(grant_id, str) or not grant_id:
raise TaskAdmissionError("TASK_ADMISSION_GRANT_ID_INVALID", 400)
with self._connection() as con:
row = con.execute(
"SELECT grant_json FROM task_admission_grants WHERE grant_id=?",
(grant_id,),
).fetchone()
if row is None:
return None
try:
raw = json.loads(row["grant_json"])
return TaskAdmissionGrant(
**{
**raw,
"capabilities": tuple(raw["capabilities"]),
"file_allowlist": tuple(raw["file_allowlist"]),
}
)
except (TypeError, ValueError, KeyError, json.JSONDecodeError) as exc:
raise TaskAdmissionError(
"TASK_ADMISSION_DURABLE_GRANT_INVALID", 503
) from exc
def get_grant_by_task_id(self, task_id: str) -> TaskAdmissionGrant | None:
"""Return the immutable admitted grant bound to an authoritative Task."""
if not isinstance(task_id, str) or not task_id:
raise TaskAdmissionError("TASK_ADMISSION_TASK_ID_INVALID", 400)
with self._connection() as con:
row = con.execute(
"""
SELECT g.grant_json
FROM task_admission_grants AS g
JOIN task_admission_receipts AS r
ON r.envelope_id = g.envelope_id
WHERE r.task_id = ?
""",
(task_id,),
).fetchone()
if row is None:
return None
try:
raw = json.loads(row["grant_json"])
grant = TaskAdmissionGrant(
**{
**raw,
"capabilities": tuple(raw["capabilities"]),
"file_allowlist": tuple(raw["file_allowlist"]),
}
)
except (TypeError, ValueError, KeyError, json.JSONDecodeError) as exc:
raise TaskAdmissionError(
"TASK_ADMISSION_DURABLE_GRANT_INVALID", 503
) from exc
if grant.task_id != task_id:
raise TaskAdmissionError(
"TASK_ADMISSION_DURABLE_GRANT_TASK_MISMATCH", 503
)
return grant
def save_envelope(
self,
*,
@@ -257,6 +257,28 @@ class AgentTaskAdmissionPort:
raise TaskAdmissionError("TASK_ADMISSION_TASK_STATE_UNKNOWN", 503)
return recovered
def verify_active_task(
self, grant: TaskAdmissionGrant
) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
"""Re-read and validate the authoritative Task for execution use.
Admission may complete while the Task is still QUEUED. A TaskPrincipal
may be recovered only after the Task authority reports CLAIMED/RUNNING.
The exact immutable metadata is revalidated on every call.
"""
execution = self._execution_plan(grant)
task = self._read(grant, execution)
if task is None:
raise TaskAdmissionError("TASK_PRINCIPAL_TASK_NOT_FOUND", 403)
state = str(task.get("state") or task.get("status") or "")
if state not in {"CLAIMED", "RUNNING"}:
raise TaskAdmissionError(
"TASK_PRINCIPAL_TASK_NOT_CLAIMED",
409,
details={"state": state},
)
return task, execution
def projection_result(
self,
grant: TaskAdmissionGrant,
@@ -286,6 +308,152 @@ class AgentTaskAdmissionPort:
"external_job_id": external_job_id,
}
_BRIDGE_PRINCIPAL_CAPABILITIES = {
"acquireWriterLease": "writer.lease",
"commitFileGoverned": "governed.write.file",
"commitFilesGoverned": "governed.write.files",
"getGitOpsJob": "writer.lease",
"resumeGitOpsJob": "writer.lease",
"reconcileGitOpsJob": "writer.lease",
}
@dataclass(frozen=True, slots=True)
class TaskAdmissionPrincipalProvider:
"""Recover a short-lived execution principal from durable admission state.
This provider is server-side only. It never accepts an Owner grant,
TaskPrincipal, or capability from caller JSON. Every use re-reads the
admitted grant, re-verifies its parent Owner epoch (including revocation),
re-reads the authoritative Task, and requires CLAIMED/RUNNING before a
mutating Bridge operation can continue.
"""
store: Any
authorization: Any
task_port: AgentTaskAdmissionPort
epoch_id: str
clock: Any = time.time
def readiness(self) -> Mapping[str, Any]:
try:
epoch = self.authorization.verify_delegation_epoch(self.epoch_id)
except Exception as exc:
return {
"ready": False,
"mode": "server-side-task-admission",
"error": str(exc) or type(exc).__name__,
}
return {
"ready": True,
"mode": "server-side-task-admission",
"epoch_id": epoch.epoch_id,
"repository": epoch.repository,
}
@staticmethod
def _request_value(
request: Mapping[str, Any], name: str, expected: object
) -> None:
if name in request and request[name] != expected:
raise TaskAdmissionError(
"TASK_PRINCIPAL_REQUEST_BINDING_MISMATCH",
403,
details={"field": name},
)
def __call__(
self,
transport: Mapping[str, Any],
request: Mapping[str, Any],
) -> Mapping[str, Any]:
operation = str(transport.get("operation") or "")
capability = _BRIDGE_PRINCIPAL_CAPABILITIES.get(operation)
if capability is None:
raise TaskAdmissionError(
"TASK_PRINCIPAL_BOOTSTRAP_ROUTE_REQUIRED", 403
)
task_id = request.get("task_id")
if not isinstance(task_id, str) or not task_id:
raise TaskAdmissionError("TASK_PRINCIPAL_TASK_ID_INVALID", 403)
grant = self.store.get_grant_by_task_id(task_id)
if grant is None:
raise TaskAdmissionError(
"TASK_PRINCIPAL_AUTHORIZATION_REQUIRED", 403
)
if grant.parent_epoch_id is None:
raise TaskAdmissionError(
"TASK_PRINCIPAL_PARENT_EPOCH_REQUIRED", 403
)
# Re-verify the exact parent on every use so revocation/expiry cannot be
# bypassed by a previously admitted Task.
epoch = self.authorization.verify_delegation_epoch(grant.parent_epoch_id)
if (
epoch.epoch_id != self.epoch_id
or epoch.repository != grant.repository
or epoch.trust_domain != grant.trust_domain
or not set(grant.capabilities).issubset(epoch.allowed_capabilities)
or not any(
grant.target_branch.startswith(prefix)
for prefix in epoch.allowed_branch_prefixes
)
):
raise TaskAdmissionError(
"TASK_PRINCIPAL_PARENT_EPOCH_SCOPE_MISMATCH", 403
)
now = int(self.clock())
if not grant.issued_at <= now < grant.expires_at:
raise TaskAdmissionError("TASK_PRINCIPAL_GRANT_EXPIRED", 403)
if (
"task.principal.issue" not in grant.capabilities
or capability not in grant.capabilities
):
raise TaskAdmissionError(
"TASK_PRINCIPAL_CAPABILITY_DENIED", 403
)
task, execution = self.task_port.verify_active_task(grant)
external_job_id = execution.get("external_job_id")
if not isinstance(external_job_id, str) or not external_job_id:
raise TaskAdmissionError(
"TASK_PRINCIPAL_EXECUTION_ID_INVALID", 503
)
self._request_value(request, "task_id", grant.task_id)
self._request_value(request, "job_id", external_job_id)
self._request_value(request, "repository", grant.repository)
self._request_value(request, "target_branch", grant.target_branch)
self._request_value(request, "generation", grant.generation)
if operation == "acquireWriterLease":
self._request_value(
request, "base_oid", grant.authority_base_oid
)
return {
"schema": "aios.task-principal-server-attestation.v1",
"task_id": grant.task_id,
"grant_id": grant.grant_id,
"parent_epoch_id": grant.parent_epoch_id,
"repository": grant.repository,
"target_branch": grant.target_branch,
"generation": grant.generation,
"job_id": external_job_id,
"capability": capability,
"task_state": str(task.get("state") or task.get("status") or ""),
"request_digest": sha256_json(dict(request)),
"verified_at": now,
}
@dataclass(frozen=True, slots=True)
class TaskAdmissionRuntimeBundle:
handlers: Mapping[str, Any]
principal_verifier: TaskAdmissionPrincipalProvider
class TaskAdmissionHostError(RuntimeError):
"""Stable fail-closed host-composition error."""
@@ -670,7 +838,7 @@ def task_admission_policy_from_epoch(
)
def _build_task_admission_handlers(
def _build_task_admission_runtime(
*,
caller_service: str,
epoch_root: Path,
@@ -682,8 +850,8 @@ def _build_task_admission_handlers(
task_control_token_file: Path,
owner_verifier_module: Path,
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Compose handlers from already-trusted server-side coordinates."""
) -> TaskAdmissionRuntimeBundle:
"""Compose admission handlers plus automatic execution-principal recovery."""
from aios_task_admission_bootstrap import (
TaskAdmissionBootstrapService,
TaskAdmissionStore,
@@ -708,34 +876,49 @@ def _build_task_admission_handlers(
verified_epoch_provider=provider,
signature_verifier=verifier,
)
transport = LocalAgentTaskHTTPTransport(
base_url=task_api_url,
token_file=task_control_token_file,
require_root_owner=True,
)
task_port = AgentTaskAdmissionPort(
LocalAgentTaskHTTPTransport(
base_url=task_api_url,
token_file=task_control_token_file,
require_root_owner=True,
),
transport,
execution_plan_provider=ExecutionPlanDirectoryProvider(
root=execution_plan_dir,
require_root_owner=True,
),
)
store = TaskAdmissionStore(state_db)
service = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(state_db),
store=store,
authorization=authorization,
task_port=task_port,
policy=task_admission_policy_from_epoch(epoch),
post_admission=post_admission,
)
return TaskAdmissionBridgeContract(service).handlers()
return TaskAdmissionRuntimeBundle(
handlers=TaskAdmissionBridgeContract(service).handlers(),
principal_verifier=TaskAdmissionPrincipalProvider(
store=store,
authorization=authorization,
task_port=task_port,
epoch_id=epoch.epoch_id,
),
)
def build_task_admission_handlers_from_activation_file(
def _build_task_admission_handlers(**kwargs: Any) -> Mapping[str, Any]:
"""Compatibility wrapper for callers that only need admission routes."""
return _build_task_admission_runtime(**kwargs).handlers
def build_task_admission_runtime_from_activation_file(
activation_file: Path,
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Load one root-owned, fail-closed staging activation descriptor."""
) -> TaskAdmissionRuntimeBundle:
"""Load admission handlers plus server-side principal recovery."""
_host_require_regular(activation_file, root_owned=True)
value = _host_read_json_object(activation_file)
required = {
@@ -777,7 +960,7 @@ def build_task_admission_handlers_from_activation_file(
if task_api_url != "http://127.0.0.1:18940":
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_TASK_API_INVALID")
return _build_task_admission_handlers(
return _build_task_admission_runtime(
caller_service=caller_service,
epoch_root=paths["epoch_root"],
epoch_id=epoch_id,
@@ -791,12 +974,25 @@ def build_task_admission_handlers_from_activation_file(
)
def build_task_admission_handlers_from_environment(
def build_task_admission_handlers_from_activation_file(
activation_file: Path,
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Compose all three handlers from server-side trusted environment only."""
return build_task_admission_runtime_from_activation_file(
activation_file,
caller_service=caller_service,
post_admission=post_admission,
).handlers
def build_task_admission_runtime_from_environment(
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> TaskAdmissionRuntimeBundle:
"""Compose admission plus automatic TaskPrincipal recovery from trusted environment."""
required = (
"AIOS_TASK_ADMISSION_EPOCH_ROOT",
"AIOS_TASK_ADMISSION_EPOCH_ID",
@@ -813,7 +1009,7 @@ def build_task_admission_handlers_from_environment(
"TASK_ADMISSION_HOST_ENV_MISSING:"
+ ",".join(sorted(missing))
)
return _build_task_admission_handlers(
return _build_task_admission_runtime(
caller_service=caller_service,
epoch_root=Path(os.environ["AIOS_TASK_ADMISSION_EPOCH_ROOT"]),
epoch_id=os.environ["AIOS_TASK_ADMISSION_EPOCH_ID"],
@@ -840,6 +1036,17 @@ def build_task_admission_handlers_from_environment(
post_admission=post_admission,
)
def build_task_admission_handlers_from_environment(
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
return build_task_admission_runtime_from_environment(
caller_service=caller_service,
post_admission=post_admission,
).handlers
# ---- producer phase core v12r4 (hash-pinned sibling source) ----
import hashlib
import json
@@ -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,8 @@ 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
migration_plan_resolver: Callable[[str, str, str], Any] | None = None
@classmethod
def from_policy(cls, policy: Mapping[str, Any]) -> "TrustedCandidateBuilderConfig":
@@ -227,14 +229,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]]:
@@ -464,11 +515,32 @@ class DeterministicEvidenceCollector:
try:
from silent_release_controller import ReleaseCandidate
from silent_release_hardening import ReleasePlan, TrustedCandidateBundle
from release_migration_plan import (
MigrationPlanError,
MigrationPlanRequired,
)
candidate = ReleaseCandidate.from_dict(dict(bundle["candidate"]))
release_plan = ReleasePlan.from_dict(dict(bundle["release_plan"]))
release_plan_payload = dict(bundle["release_plan"])
migration_resolver = self._config.migration_plan_resolver
if migration_resolver is not None:
try:
migration = migration_resolver(
recipe_name,
recipe["candidate_id"],
recipe["source_commit"],
)
except MigrationPlanRequired as exc:
raise BuilderError("MIGRATION_PLAN_REQUIRED") from exc
except MigrationPlanError as exc:
raise BuilderError("MIGRATION_PLAN_INVALID") from exc
if migration is not None:
release_plan_payload["migration"] = migration.to_dict()
release_plan = ReleasePlan.from_dict(release_plan_payload)
checks = dict(raw_checks)
TrustedCandidateBundle(candidate, release_plan, checks).validate()
except BuilderError:
raise
except (ImportError, KeyError, TypeError, ValueError, RuntimeError) as exc:
raise BuilderError("COLLECTOR_EVIDENCE_INVALID") from exc
if candidate.source_commit != recipe["source_commit"] or candidate.candidate_id != recipe["candidate_id"]:
@@ -776,6 +848,50 @@ 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,
)
migration_plan_root = os.environ.get(
"AIOS_TRUSTED_BUILDER_MIGRATION_PLAN_ROOT", ""
).strip()
migration_profile_root = os.environ.get(
"AIOS_TRUSTED_BUILDER_MIGRATION_PROFILE_ROOT", ""
).strip()
if bool(migration_plan_root) != bool(migration_profile_root):
raise BuilderError("MIGRATION_PLAN_CONFIGURATION_INVALID")
if migration_plan_root:
from release_migration_plan import FileMigrationPlanResolver
migration_resolver = FileMigrationPlanResolver(
Path(migration_plan_root),
profile_root=Path(migration_profile_root),
expected_owner_uid=0,
)
config = replace(
config,
migration_plan_resolver=migration_resolver.resolve,
)
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,997 @@
#!/usr/bin/env python3
"""Fail-closed cross-code-line release migration planning.
The migration plan is data, not a shell script. It binds an admitted source to
the exact currently observed staging code lines, the compatible state contract,
and the rollback/verification contract. The plan can then be embedded in the
existing signed ReleasePlan so Owner approval covers migration semantics without
creating a second release authority.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import sqlite3
import stat
import subprocess
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
IDENT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/-]{2,255}$")
PLAN_SCHEMA = "aios.release-migration-plan.v1"
REQUIREMENT_SCHEMA = "aios.release-migration-requirement.v1"
PROFILE_SCHEMA = "aios.release-migration-profile.v1"
ALLOWED_STATE_MODES = frozenset({"REUSE_IN_PLACE_WITH_SNAPSHOT", "PRESERVE_UNUSED"})
REQUIRED_PRECONDITIONS = (
"writer_disabled",
"task_quiescent",
"state_snapshot_verified",
"credentials_ready",
"rollback_ready",
)
REQUIRED_VERIFICATION_CONTRACTS = (
"runtime_identity_exact",
"task_principal_no_bearer_e2e",
"restart_recovery",
"credential_projection",
"release_consistency_no_unknown",
)
class MigrationPlanError(RuntimeError):
"""Migration evidence is missing, malformed, stale, or unsafe."""
class MigrationPlanRequired(MigrationPlanError):
"""A cross-code-line release cannot proceed without an exact migration plan."""
def _canonical_json(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def _sha(value: object, label: str) -> str:
text = str(value or "").lower()
if not SHA_RE.fullmatch(text):
raise MigrationPlanError(f"{label}_invalid")
return text
def _oid(value: object, label: str) -> str:
text = str(value or "").lower()
if not OID_RE.fullmatch(text):
raise MigrationPlanError(f"{label}_invalid")
return text
def _ident(value: object, label: str) -> str:
text = str(value or "")
if not IDENT_RE.fullmatch(text):
raise MigrationPlanError(f"{label}_invalid")
return text
def _secure_json(path: Path, *, expected_owner_uid: int | None = 0) -> dict[str, Any]:
try:
metadata = path.lstat()
except OSError as exc:
raise MigrationPlanError(f"secure_json_unavailable:{path}") from exc
if path.is_symlink() or not stat.S_ISREG(metadata.st_mode):
raise MigrationPlanError("secure_json_regular_file_required")
if expected_owner_uid is not None and os.name != "nt" and metadata.st_uid != expected_owner_uid:
raise MigrationPlanError("secure_json_owner_invalid")
if os.name != "nt" and metadata.st_mode & 0o022:
raise MigrationPlanError("secure_json_mode_invalid")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise MigrationPlanError("secure_json_invalid") from exc
if not isinstance(value, dict):
raise MigrationPlanError("secure_json_object_required")
return value
def _durable_json(path: Path, value: Mapping[str, Any], *, mode: int = 0o640) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
encoded = _canonical_json(dict(value))
if path.exists():
current = path.read_bytes()
if current != encoded:
raise MigrationPlanError("migration_record_conflict")
return
temporary = path.with_name(path.name + ".tmp")
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(encoded)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
if os.name != "nt":
os.chmod(path, mode)
directory_fd = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
finally:
try:
temporary.unlink()
except FileNotFoundError:
pass
def _hash_files(root: Path, files: Sequence[str]) -> str:
payload: dict[str, str] = {}
resolved_root = root.resolve(strict=True)
for relative in files:
rel = Path(relative)
if rel.is_absolute() or ".." in rel.parts:
raise MigrationPlanError("interface_path_invalid")
path = (resolved_root / rel).resolve(strict=True)
try:
path.relative_to(resolved_root)
except ValueError as exc:
raise MigrationPlanError("interface_path_escape") from exc
if path.is_symlink() or not path.is_file():
raise MigrationPlanError("interface_file_invalid")
payload[relative] = hashlib.sha256(path.read_bytes()).hexdigest()
return hashlib.sha256(_canonical_json(payload)).hexdigest()
def _git_blob(git_dir: Path, source_oid: str, relative: str) -> bytes:
rel = Path(relative)
if rel.is_absolute() or ".." in rel.parts:
raise MigrationPlanError("target_interface_path_invalid")
completed = subprocess.run(
["git", f"--git-dir={git_dir}", "show", f"{source_oid}:{relative}"],
check=False,
capture_output=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise MigrationPlanError("target_interface_unavailable")
return bytes(completed.stdout)
def _hash_git_files(git_dir: Path, source_oid: str, files: Sequence[str]) -> str:
payload = {
relative: hashlib.sha256(_git_blob(git_dir, source_oid, relative)).hexdigest()
for relative in files
}
return hashlib.sha256(_canonical_json(payload)).hexdigest()
def _systemd_properties(service: str) -> dict[str, Any]:
_ident(service, "service")
completed = subprocess.run(
[
"systemctl",
"show",
service,
"--property=WorkingDirectory",
"--property=Environment",
"--property=FragmentPath",
"--property=ActiveState",
],
check=False,
capture_output=True,
text=True,
timeout=20,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise MigrationPlanError("systemd_observation_unavailable")
result: dict[str, Any] = {}
for line in completed.stdout.splitlines():
if "=" in line:
key, value = line.split("=", 1)
result[key] = value
if result.get("ActiveState") != "active":
raise MigrationPlanError("component_not_active")
environment: dict[str, str] = {}
for token in str(result.get("Environment") or "").split():
if "=" in token:
key, value = token.split("=", 1)
environment[key] = value.strip('"')
result["environment"] = environment
return result
def _sqlite_schema(connection: sqlite3.Connection) -> dict[str, Any]:
tables = [
row[0]
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)
]
result: dict[str, Any] = {}
for table in tables:
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", table):
raise MigrationPlanError("sqlite_table_name_invalid")
columns = []
for row in connection.execute(f"PRAGMA table_info({table})"):
columns.append(
{
"cid": int(row[0]),
"name": str(row[1]),
"type": str(row[2]),
"notnull": int(row[3]),
"default": row[4],
"pk": int(row[5]),
}
)
result[table] = {
"columns": columns,
"row_count": int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]),
}
return result
def _schema_digest(schema: Mapping[str, Any]) -> str:
shape = {
table: {"columns": value["columns"]}
for table, value in sorted(schema.items())
}
return hashlib.sha256(_canonical_json(shape)).hexdigest()
def _assert_additive_sqlite(before: Mapping[str, Any], after: Mapping[str, Any]) -> None:
for table, old in before.items():
if table not in after:
raise MigrationPlanError("sqlite_table_removed")
old_columns = {column["name"]: column for column in old["columns"]}
new_columns = {column["name"]: column for column in after[table]["columns"]}
for name, column in old_columns.items():
if new_columns.get(name) != column:
raise MigrationPlanError("sqlite_column_changed")
for name, column in new_columns.items():
if name in old_columns:
continue
# A new NOT NULL column is backward compatible only with a default.
if column["notnull"] and column["default"] is None:
raise MigrationPlanError("sqlite_new_column_unsafe")
if int(after[table]["row_count"]) != int(old["row_count"]):
raise MigrationPlanError("sqlite_row_count_changed_during_probe")
@dataclass(frozen=True)
class RuntimeComponentBinding:
component_id: str
service: str
code_line_id: str
source_oid: str
artifact_sha256: str | None
interface_sha256: str
def __post_init__(self) -> None:
_ident(self.component_id, "component_id")
_ident(self.service, "service")
_ident(self.code_line_id, "code_line_id")
_oid(self.source_oid, "source_oid")
if self.artifact_sha256 is not None:
_sha(self.artifact_sha256, "artifact_sha256")
_sha(self.interface_sha256, "interface_sha256")
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "RuntimeComponentBinding":
return cls(**dict(value))
@dataclass(frozen=True)
class StateMigrationContract:
state_id: str
state_ref: str
mode: str
before_schema_sha256: str
after_schema_sha256: str
snapshot_name: str
snapshot_sha256: str
snapshot_required: bool
rollback_compatible: bool
def __post_init__(self) -> None:
_ident(self.state_id, "state_id")
_ident(self.state_ref, "state_ref")
if self.mode not in ALLOWED_STATE_MODES:
raise MigrationPlanError("state_mode_invalid")
_sha(self.before_schema_sha256, "before_schema_sha256")
_sha(self.after_schema_sha256, "after_schema_sha256")
_sha(self.snapshot_sha256, "snapshot_sha256")
snapshot = Path(self.snapshot_name)
if (
snapshot.is_absolute()
or ".." in snapshot.parts
or not snapshot.parts
or str(snapshot).startswith(".")
):
raise MigrationPlanError("snapshot_name_invalid")
if self.snapshot_required is not True or self.rollback_compatible is not True:
raise MigrationPlanError("state_recovery_contract_invalid")
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "StateMigrationContract":
return cls(**dict(value))
@dataclass(frozen=True)
class MigrationPlan:
migration_id: str
version: str
candidate_id: str
repository_identity: str
source_admission_id: str
source_commit: str
tree_oid: str
profile_id: str
profile_sha256: str
from_components: tuple[RuntimeComponentBinding, ...]
to_components: tuple[RuntimeComponentBinding, ...]
state_contracts: tuple[StateMigrationContract, ...]
activation_schema: str
activation_sha256: str
rollback_window_seconds: int
preconditions: tuple[str, ...] = REQUIRED_PRECONDITIONS
verification_contracts: tuple[str, ...] = REQUIRED_VERIFICATION_CONTRACTS
runtime_switch_capability: str = "runtime_switch"
rollback_contract: str = "restore_previous_runtime_and_state_snapshot.v1"
forward_recovery_contract: str = "forward_recovery_after_verified_rollback_block.v1"
writer_expected: bool = False
direct_db_allowed: bool = False
direct_runtime_overwrite_allowed: bool = False
def __post_init__(self) -> None:
_ident(self.migration_id, "migration_id")
_ident(self.version, "version")
_ident(self.candidate_id, "candidate_id")
if self.repository_identity != "aios/aios":
raise MigrationPlanError("repository_identity_invalid")
_ident(self.source_admission_id, "source_admission_id")
_oid(self.source_commit, "source_commit")
_oid(self.tree_oid, "tree_oid")
_ident(self.profile_id, "profile_id")
_sha(self.profile_sha256, "profile_sha256")
_ident(self.activation_schema, "activation_schema")
_sha(self.activation_sha256, "activation_sha256")
if not 60 <= int(self.rollback_window_seconds) <= 86400:
raise MigrationPlanError("rollback_window_invalid")
if tuple(self.preconditions) != REQUIRED_PRECONDITIONS:
raise MigrationPlanError("migration_preconditions_invalid")
if tuple(self.verification_contracts) != REQUIRED_VERIFICATION_CONTRACTS:
raise MigrationPlanError("migration_verification_contracts_invalid")
if self.runtime_switch_capability != "runtime_switch":
raise MigrationPlanError("migration_capability_invalid")
if self.writer_expected or self.direct_db_allowed or self.direct_runtime_overwrite_allowed:
raise MigrationPlanError("migration_unsafe_authority")
if not self.from_components or not self.to_components or not self.state_contracts:
raise MigrationPlanError("migration_contract_incomplete")
from_ids = {item.component_id for item in self.from_components}
to_ids = {item.component_id for item in self.to_components}
if from_ids != to_ids or len(from_ids) != len(self.from_components) or len(to_ids) != len(self.to_components):
raise MigrationPlanError("migration_component_binding_invalid")
if all(
next(item.code_line_id for item in self.from_components if item.component_id == target.component_id)
== target.code_line_id
for target in self.to_components
):
raise MigrationPlanError("migration_not_required")
@property
def canonical_payload(self) -> dict[str, Any]:
return {
"schema": PLAN_SCHEMA,
"migration_id": self.migration_id,
"version": self.version,
"candidate_id": self.candidate_id,
"repository_identity": self.repository_identity,
"source_admission_id": self.source_admission_id,
"source_commit": self.source_commit,
"tree_oid": self.tree_oid,
"profile_id": self.profile_id,
"profile_sha256": self.profile_sha256,
"from_components": [item.to_dict() for item in self.from_components],
"to_components": [item.to_dict() for item in self.to_components],
"state_contracts": [item.to_dict() for item in self.state_contracts],
"activation_schema": self.activation_schema,
"activation_sha256": self.activation_sha256,
"rollback_window_seconds": self.rollback_window_seconds,
"preconditions": list(self.preconditions),
"verification_contracts": list(self.verification_contracts),
"runtime_switch_capability": self.runtime_switch_capability,
"rollback_contract": self.rollback_contract,
"forward_recovery_contract": self.forward_recovery_contract,
"writer_expected": self.writer_expected,
"direct_db_allowed": self.direct_db_allowed,
"direct_runtime_overwrite_allowed": self.direct_runtime_overwrite_allowed,
}
@property
def sha256(self) -> str:
return hashlib.sha256(_canonical_json(self.canonical_payload)).hexdigest()
def to_dict(self) -> dict[str, Any]:
return {**self.canonical_payload, "sha256": self.sha256}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "MigrationPlan":
raw = dict(value)
if raw.pop("schema", None) != PLAN_SCHEMA:
raise MigrationPlanError("migration_plan_schema_invalid")
claimed = raw.pop("sha256", None)
raw["from_components"] = tuple(RuntimeComponentBinding.from_dict(item) for item in raw["from_components"])
raw["to_components"] = tuple(RuntimeComponentBinding.from_dict(item) for item in raw["to_components"])
raw["state_contracts"] = tuple(StateMigrationContract.from_dict(item) for item in raw["state_contracts"])
raw["preconditions"] = tuple(raw.get("preconditions") or ())
raw["verification_contracts"] = tuple(raw.get("verification_contracts") or ())
plan = cls(**raw)
if claimed is not None and claimed != plan.sha256:
raise MigrationPlanError("migration_plan_sha256_mismatch")
return plan
def assert_release_binding(
self,
*,
version: str,
candidate_id: str,
source_commit: str,
tree_oid: str,
repository_identity: str,
) -> None:
expected = (
self.version,
self.candidate_id,
self.source_commit,
self.tree_oid,
self.repository_identity,
)
actual = (version, candidate_id, source_commit, tree_oid, repository_identity)
if actual != expected:
raise MigrationPlanError("migration_release_binding_mismatch")
@dataclass(frozen=True)
class MigrationRequirement:
version: str
candidate_id: str
source_commit: str
repository_identity: str
required: bool
reason_code: str
profile_id: str
profile_sha256: str
migration_plan_sha256: str | None
@property
def canonical_payload(self) -> dict[str, Any]:
return {
"schema": REQUIREMENT_SCHEMA,
**asdict(self),
}
@property
def sha256(self) -> str:
return hashlib.sha256(_canonical_json(self.canonical_payload)).hexdigest()
def to_dict(self) -> dict[str, Any]:
return {**self.canonical_payload, "sha256": self.sha256}
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "MigrationRequirement":
raw = dict(value)
if raw.pop("schema", None) != REQUIREMENT_SCHEMA:
raise MigrationPlanError("migration_requirement_schema_invalid")
claimed = raw.pop("sha256", None)
requirement = cls(**raw)
_ident(requirement.version, "version")
_ident(requirement.candidate_id, "candidate_id")
_oid(requirement.source_commit, "source_commit")
if requirement.repository_identity != "aios/aios":
raise MigrationPlanError("migration_requirement_repository_invalid")
_ident(requirement.reason_code, "reason_code")
_ident(requirement.profile_id, "profile_id")
_sha(requirement.profile_sha256, "profile_sha256")
if requirement.required:
_sha(requirement.migration_plan_sha256, "migration_plan_sha256")
elif requirement.migration_plan_sha256 is not None:
raise MigrationPlanError("unexpected_migration_plan")
if claimed is not None and claimed != requirement.sha256:
raise MigrationPlanError("migration_requirement_sha256_mismatch")
return requirement
class FileMigrationPlanResolver:
"""Read-only resolver used by the trusted builder.
A profile declares that a release version is cross-code-line. Once a
version is declared, absence of requirement/plan state is fail-closed.
"""
def __init__(
self,
root: Path,
*,
profile_root: Path,
expected_owner_uid: int | None = 0,
):
self.root = Path(root)
self.profile_root = Path(profile_root)
self.expected_owner_uid = expected_owner_uid
def _profile_for_version(self, version: str) -> dict[str, Any] | None:
matches = []
if not self.profile_root.is_dir():
return None
for path in sorted(self.profile_root.glob("*.json")):
profile = _secure_json(path, expected_owner_uid=self.expected_owner_uid)
if profile.get("schema") != PROFILE_SCHEMA:
raise MigrationPlanError("migration_profile_schema_invalid")
if profile.get("version") == version:
matches.append(profile)
if len(matches) > 1:
raise MigrationPlanError("migration_profile_ambiguous")
return matches[0] if matches else None
def resolve(self, version: str, candidate_id: str, source_commit: str) -> MigrationPlan | None:
profile = self._profile_for_version(version)
if profile is None:
return None
base = self.root / source_commit
requirement_path = base / "requirement.json"
plan_path = base / "plan.json"
if not requirement_path.is_file():
raise MigrationPlanRequired("MIGRATION_PLAN_REQUIRED")
requirement = MigrationRequirement.from_dict(
_secure_json(requirement_path, expected_owner_uid=self.expected_owner_uid)
)
expected = (version, candidate_id, source_commit, "aios/aios")
actual = (
requirement.version,
requirement.candidate_id,
requirement.source_commit,
requirement.repository_identity,
)
if actual != expected:
raise MigrationPlanError("migration_requirement_binding_mismatch")
if requirement.profile_id != profile.get("profile_id"):
raise MigrationPlanError("migration_profile_binding_mismatch")
profile_sha = hashlib.sha256(_canonical_json(profile)).hexdigest()
if requirement.profile_sha256 != profile_sha:
raise MigrationPlanError("migration_profile_digest_mismatch")
if not requirement.required:
return None
if not plan_path.is_file():
raise MigrationPlanRequired("MIGRATION_PLAN_REQUIRED")
plan = MigrationPlan.from_dict(
_secure_json(plan_path, expected_owner_uid=self.expected_owner_uid)
)
plan.assert_release_binding(
version=version,
candidate_id=candidate_id,
source_commit=source_commit,
tree_oid=plan.tree_oid,
repository_identity="aios/aios",
)
if requirement.migration_plan_sha256 != plan.sha256:
raise MigrationPlanError("migration_requirement_plan_mismatch")
return plan
class MigrationPreSwitchValidator:
"""Validates a root-owned live proof against the signed migration plan."""
def __init__(self, proof_resolver: Callable[[MigrationPlan], Mapping[str, Any]]):
self.proof_resolver = proof_resolver
def validate(self, migration_plan: MigrationPlan | None) -> dict[str, Any]:
if migration_plan is None:
return {"required": False, "verified": True}
proof = dict(self.proof_resolver(migration_plan))
snapshot_digests = sorted(
contract.snapshot_sha256 for contract in migration_plan.state_contracts
)
required = {
"migration_plan_sha256": migration_plan.sha256,
"migration_id": migration_plan.migration_id,
"source_commit": migration_plan.source_commit,
"tree_oid": migration_plan.tree_oid,
"activation_sha256": migration_plan.activation_sha256,
"state_snapshot_sha256s": snapshot_digests,
"writer_disabled": True,
"task_quiescent": True,
"state_snapshot_verified": True,
"credentials_ready": True,
"rollback_ready": True,
}
failed = [key for key, expected in required.items() if proof.get(key) != expected]
if failed:
raise MigrationPlanError(
"MIGRATION_PRE_SWITCH_PROOF_INVALID:" + ",".join(sorted(failed))
)
return {"required": True, "verified": True, **proof}
class HostMigrationCompiler:
def __init__(
self,
*,
authority_git_dir: Path,
profile_path: Path,
output_root: Path,
expected_owner_uid: int | None = 0,
):
self.authority_git_dir = Path(authority_git_dir)
self.profile_path = Path(profile_path)
self.output_root = Path(output_root)
self.expected_owner_uid = expected_owner_uid
def _profile(self) -> dict[str, Any]:
profile = _secure_json(self.profile_path, expected_owner_uid=self.expected_owner_uid)
required = {
"schema",
"profile_id",
"version",
"repository",
"current_components",
"target_components",
"activation_file",
"state_ref",
"state_db",
"state_mode",
"rollback_window_seconds",
}
if set(profile) != required or profile.get("schema") != PROFILE_SCHEMA:
raise MigrationPlanError("migration_profile_fields_invalid")
if profile.get("repository") != "aios/aios":
raise MigrationPlanError("migration_profile_repository_invalid")
_ident(profile.get("profile_id"), "profile_id")
_ident(profile.get("version"), "version")
if profile.get("state_mode") not in ALLOWED_STATE_MODES:
raise MigrationPlanError("migration_profile_state_mode_invalid")
return profile
@staticmethod
def _component_from_live(spec: Mapping[str, Any]) -> RuntimeComponentBinding:
required = {
"component_id",
"service",
"code_line_id",
"expected_source_oid",
"source_oid_env",
"artifact_sha_env",
"interface_files",
"marker_files",
}
if set(spec) != required:
raise MigrationPlanError("current_component_profile_invalid")
properties = _systemd_properties(str(spec["service"]))
environment = properties["environment"]
source_oid = _oid(environment.get(str(spec["source_oid_env"])), "current_source_oid")
expected_oid = _oid(spec["expected_source_oid"], "expected_source_oid")
if source_oid != expected_oid:
raise MigrationPlanError("CURRENT_RUNTIME_DRIFT")
working = Path(str(properties.get("WorkingDirectory") or ""))
if not working.is_absolute() or not working.is_dir():
raise MigrationPlanError("current_working_directory_invalid")
for marker in spec["marker_files"]:
if not (working / str(marker)).is_file():
raise MigrationPlanError("current_code_line_marker_missing")
interface = _hash_files(working, tuple(str(item) for item in spec["interface_files"]))
raw_artifact = environment.get(str(spec["artifact_sha_env"]))
artifact = str(raw_artifact or "").lower()
artifact_value = artifact if SHA_RE.fullmatch(artifact) else None
return RuntimeComponentBinding(
component_id=str(spec["component_id"]),
service=str(spec["service"]),
code_line_id=str(spec["code_line_id"]),
source_oid=source_oid,
artifact_sha256=artifact_value,
interface_sha256=interface,
)
def _component_from_target(
self, spec: Mapping[str, Any], *, source_commit: str
) -> RuntimeComponentBinding:
required = {"component_id", "service", "code_line_id", "interface_files"}
if set(spec) != required:
raise MigrationPlanError("target_component_profile_invalid")
interface_files = tuple(str(item) for item in spec["interface_files"])
interface = _hash_git_files(self.authority_git_dir, source_commit, interface_files)
return RuntimeComponentBinding(
component_id=str(spec["component_id"]),
service=str(spec["service"]),
code_line_id=str(spec["code_line_id"]),
source_oid=source_commit,
artifact_sha256=None,
interface_sha256=interface,
)
@staticmethod
def _state_compatibility(
state_db: Path,
state_ref: str,
*,
snapshot_path: Path,
snapshot_name: str,
) -> StateMigrationContract:
from aios_task_admission_bootstrap import TaskAdmissionStore
if state_db.is_symlink() or not state_db.is_file():
raise MigrationPlanError("state_db_invalid")
snapshot_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="aios-migration-probe-") as temp_dir:
temp_root = Path(temp_dir)
consistent_snapshot = temp_root / "pre-migration.sqlite3"
probe_path = temp_root / "schema-probe.sqlite3"
with sqlite3.connect(
f"file:{state_db}?mode=ro", uri=True
) as live, sqlite3.connect(consistent_snapshot) as copy:
live.backup(copy)
with sqlite3.connect(consistent_snapshot) as frozen:
before = _sqlite_schema(frozen)
# Probe target initialization only on a second private copy. The
# rollback snapshot remains byte-for-byte pre-migration state.
with sqlite3.connect(consistent_snapshot) as source, sqlite3.connect(
probe_path
) as probe:
source.backup(probe)
TaskAdmissionStore(probe_path)
with sqlite3.connect(probe_path) as migrated:
after = _sqlite_schema(migrated)
required_tables = {
"task_admission_envelopes",
"task_admission_grants",
"task_admission_receipts",
}
if not required_tables.issubset(after):
raise MigrationPlanError("INCOMPATIBLE_STATE_SCHEMA")
_assert_additive_sqlite(before, after)
snapshot_bytes = consistent_snapshot.read_bytes()
snapshot_sha = hashlib.sha256(snapshot_bytes).hexdigest()
if snapshot_path.exists():
if snapshot_path.is_symlink() or not snapshot_path.is_file():
raise MigrationPlanError("migration_snapshot_invalid")
if hashlib.sha256(snapshot_path.read_bytes()).hexdigest() != snapshot_sha:
raise MigrationPlanError("migration_snapshot_conflict")
else:
temporary = snapshot_path.with_name(snapshot_path.name + ".tmp")
descriptor = os.open(
temporary,
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
0o640,
)
try:
with os.fdopen(descriptor, "wb") as handle:
handle.write(snapshot_bytes)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, snapshot_path)
if os.name != "nt":
os.chmod(snapshot_path, 0o640)
directory_fd = os.open(snapshot_path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
finally:
try:
temporary.unlink()
except FileNotFoundError:
pass
return StateMigrationContract(
state_id="task-admission",
state_ref=state_ref,
mode="REUSE_IN_PLACE_WITH_SNAPSHOT",
before_schema_sha256=_schema_digest(before),
after_schema_sha256=_schema_digest(after),
snapshot_name=snapshot_name,
snapshot_sha256=snapshot_sha,
snapshot_required=True,
rollback_compatible=True,
)
def compile(
self,
*,
source_admission: Mapping[str, Any],
candidate_id: str,
) -> tuple[MigrationRequirement, MigrationPlan]:
profile = self._profile()
version = str(source_admission.get("version") or "")
source_commit = _oid(source_admission.get("source_commit"), "source_commit")
tree_oid = _oid(source_admission.get("tree_oid"), "tree_oid")
admission_id = _ident(source_admission.get("admission_id"), "source_admission_id")
if (
version != profile["version"]
or source_admission.get("repository") != "aios/aios"
):
raise MigrationPlanError("source_admission_profile_mismatch")
_ident(candidate_id, "candidate_id")
profile_sha = hashlib.sha256(_canonical_json(profile)).hexdigest()
from_components = tuple(
self._component_from_live(item) for item in profile["current_components"]
)
to_components = tuple(
self._component_from_target(item, source_commit=source_commit)
for item in profile["target_components"]
)
if {item.component_id for item in from_components} != {item.component_id for item in to_components}:
raise MigrationPlanError("migration_component_profile_mismatch")
required = any(
next(old.code_line_id for old in from_components if old.component_id == new.component_id)
!= new.code_line_id
for new in to_components
)
if not required:
raise MigrationPlanError("migration_profile_does_not_require_transition")
activation_path = Path(str(profile["activation_file"]))
activation = _secure_json(activation_path, expected_owner_uid=self.expected_owner_uid)
if activation.get("schema") != "aios.task-admission-host-activation.v1":
raise MigrationPlanError("activation_schema_invalid")
state_path = Path(str(profile["state_db"]))
if str(activation.get("state_db") or "") != str(state_path):
raise MigrationPlanError("activation_state_db_mismatch")
root = self.output_root / source_commit
snapshot_name = "snapshots/task-admission.sqlite3"
state_contract = self._state_compatibility(
state_path,
str(profile["state_ref"]),
snapshot_path=root / snapshot_name,
snapshot_name=snapshot_name,
)
migration_id = (
"migration-"
+ hashlib.sha256(
f"{version}\0{candidate_id}\0{source_commit}\0{profile_sha}".encode("utf-8")
).hexdigest()[:24]
)
plan = MigrationPlan(
migration_id=migration_id,
version=version,
candidate_id=candidate_id,
repository_identity="aios/aios",
source_admission_id=admission_id,
source_commit=source_commit,
tree_oid=tree_oid,
profile_id=str(profile["profile_id"]),
profile_sha256=profile_sha,
from_components=from_components,
to_components=to_components,
state_contracts=(state_contract,),
activation_schema=str(activation["schema"]),
activation_sha256=hashlib.sha256(_canonical_json(activation)).hexdigest(),
rollback_window_seconds=int(profile["rollback_window_seconds"]),
)
requirement = MigrationRequirement(
version=version,
candidate_id=candidate_id,
source_commit=source_commit,
repository_identity="aios/aios",
required=True,
reason_code="CROSS_CODE_LINE",
profile_id=str(profile["profile_id"]),
profile_sha256=profile_sha,
migration_plan_sha256=plan.sha256,
)
_durable_json(root / "requirement.json", requirement.to_dict())
_durable_json(root / "plan.json", plan.to_dict())
return requirement, plan
def _policy_sha256(path: Path) -> str:
try:
raw = path.read_bytes()
except OSError as exc:
raise MigrationPlanError("builder_policy_unavailable") from exc
return hashlib.sha256(raw).hexdigest()
def _verified_source_admission(
*,
source_admission_root: Path,
source_commit: str,
builder_policy: Path,
credential_name: str = "source_admission_hmac",
) -> dict[str, Any]:
"""Resolve one source admission through its existing HMAC verifier.
The migration planner never trusts a caller-selected admission JSON path.
It consumes only a root-owned admission store and the independent
``source_admission_hmac`` credential projected by systemd.
"""
from release_source_admission import ReleaseSourceAdmissionStore
credentials = os.environ.get("CREDENTIALS_DIRECTORY", "").strip()
if not credentials:
raise MigrationPlanError("source_admission_credentials_unavailable")
credential_path = Path(credentials) / credential_name
try:
hmac_key = credential_path.read_bytes().strip()
except OSError as exc:
raise MigrationPlanError("source_admission_hmac_unavailable") from exc
try:
store = ReleaseSourceAdmissionStore(
source_admission_root,
repository="aios/aios",
policy_sha256=_policy_sha256(builder_policy),
hmac_key=hmac_key,
expected_owner_uid=0 if os.name != "nt" else None,
writable=False,
)
record = store.lookup_source(source_commit)
except Exception as exc:
raise MigrationPlanError("source_admission_verification_failed") from exc
if record is None:
raise MigrationPlanRequired("SOURCE_ADMISSION_REQUIRED")
return dict(record)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--authority-git-dir", required=True, type=Path)
parser.add_argument("--profile", required=True, type=Path)
parser.add_argument("--output-root", required=True, type=Path)
parser.add_argument("--source-admission-root", required=True, type=Path)
parser.add_argument("--builder-policy", required=True, type=Path)
parser.add_argument("--source-commit", required=True)
parser.add_argument("--candidate-id", required=True)
args = parser.parse_args()
source_commit = _oid(args.source_commit, "source_commit")
compiler = HostMigrationCompiler(
authority_git_dir=args.authority_git_dir,
profile_path=args.profile,
output_root=args.output_root,
expected_owner_uid=0 if os.name != "nt" else None,
)
requirement, plan = compiler.compile(
source_admission=_verified_source_admission(
source_admission_root=args.source_admission_root,
source_commit=source_commit,
builder_policy=args.builder_policy,
),
candidate_id=args.candidate_id,
)
print(
json.dumps(
{
"status": "MIGRATION_PLAN_READY",
"migration_plan_sha256": plan.sha256,
"requirement_sha256": requirement.sha256,
"source_commit": plan.source_commit,
"version": plan.version,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(
json.dumps(
{
"status": "FAIL_CLOSED",
"error": type(exc).__name__,
"reason": str(exc),
},
sort_keys=True,
)
)
raise SystemExit(2)
@@ -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()
+185 -39
View File
@@ -25,6 +25,12 @@ from typing import Any, Callable, Iterable, Mapping, Protocol
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from release_migration_plan import (
MigrationPlan,
MigrationPlanError,
MigrationPreSwitchValidator,
)
from silent_release_controller import (
CANDIDATE_ID_RE,
OID_RE,
@@ -562,6 +568,7 @@ class ReleasePlan:
tag: TagPlan
finalization: FinalizationPlan
policy_sha256: str
migration: MigrationPlan | None = None
def __post_init__(self) -> None:
_validate_sha("policy_sha256", self.policy_sha256)
@@ -588,16 +595,32 @@ class ReleasePlan:
!= self.finalization.expected_previous_baseline_sha256
):
raise ValueError("previous baseline is inconsistent")
if self.migration is not None:
migration = self.migration
if (
migration.version != self.tag.version
or migration.source_commit != self.deployment.source_commit
or migration.tree_oid != self.deployment.tree_oid
or migration.repository_identity
!= self.deployment.repository_identity
):
raise ValueError("migration plan release binding is inconsistent")
return self
@property
def canonical_payload(self) -> dict[str, Any]:
return {
# Legacy release-plan bytes/hashes remain unchanged when migration is
# not required. A cross-code-line migration is an optional signed
# sub-contract and is therefore present only when required.
payload = {
"deployment": self.deployment.canonical_payload,
"finalization": self.finalization.canonical_payload,
"policy_sha256": self.policy_sha256,
"tag": self.tag.canonical_payload,
}
if self.migration is not None:
payload["migration"] = self.migration.to_dict()
return payload
@property
def sha256(self) -> str:
@@ -612,6 +635,7 @@ class ReleasePlan:
def from_dict(cls, payload: Mapping[str, Any]) -> "ReleasePlan":
values = dict(payload)
values.pop("sha256", None)
migration_value = values.get("migration")
return cls(
deployment=DeploymentPlan.from_dict(values["deployment"]),
tag=TagPlan.from_dict(values["tag"]),
@@ -619,6 +643,11 @@ class ReleasePlan:
values["finalization"]
),
policy_sha256=values["policy_sha256"],
migration=(
MigrationPlan.from_dict(migration_value)
if isinstance(migration_value, Mapping)
else None
),
)
@@ -1215,6 +1244,19 @@ class TrustedCandidateBundle:
raise CapabilityAuthorizationError(
"trusted candidate and release plan are inconsistent"
)
if plan.migration is not None:
try:
plan.migration.assert_release_binding(
version=plan.tag.version,
candidate_id=candidate.candidate_id,
source_commit=candidate.source_commit,
tree_oid=candidate.tree_oid,
repository_identity=plan.deployment.repository_identity,
)
except MigrationPlanError as exc:
raise CapabilityAuthorizationError(
"trusted candidate migration binding is invalid"
) from exc
if attestation_verifier is not None:
if self.attestation is None:
raise CapabilityAuthorizationError(
@@ -3090,6 +3132,7 @@ class ReleaseOrchestrator:
runtime_rollback: Callable[..., Mapping[str, Any]],
tag_publish: HardenedTagController,
finalizer: GovernedFinalizationCoordinator,
migration_pre_switch_validator: MigrationPreSwitchValidator | None = None,
):
self.candidates = candidates
self.plans = plans
@@ -3104,6 +3147,7 @@ class ReleaseOrchestrator:
self.runtime_rollback = runtime_rollback
self.tag_publish = tag_publish
self.finalizer = finalizer
self.migration_pre_switch_validator = migration_pre_switch_validator
def _transition(
self,
@@ -3304,6 +3348,14 @@ class ReleaseOrchestrator:
plan.deployment.repository_identity
),
)
if plan.migration is not None:
validator = self.migration_pre_switch_validator
if validator is None:
raise StaleApprovalError(
"MIGRATION_PLAN_REQUIRED: "
"migration pre-switch validator is unavailable"
)
validator.validate(plan.migration)
saga = self._transition(
saga,
SagaState.SWITCH_PENDING,
@@ -3531,6 +3583,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 +3600,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 +3692,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,47 +3745,83 @@ 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"))
prerequisite_states = {
"SOURCE_ADMISSION_REQUIRED",
"MIGRATION_PLAN_REQUIRED",
}
existing_status = str(existing.get("status") or "")
existing_version = str(existing.get("version") or "")
existing_admission = str(existing.get("source_admission_id") or "")
incoming_admission = str(record.get("source_admission_id") or "")
if (
existing.get("event_id") != event_id
or existing.get("source_commit") != source_commit
or str(existing.get("version") or "") != version
or (existing_version and existing_version != version)
or (existing_admission and existing_admission != incoming_admission)
):
raise SagaConflict(
"release intent id is bound to different evidence"
)
if existing.get("status") == "BUILDING":
try:
started_at = _parse_utc(
str(existing.get("attempt_started_at") or "")
)
elapsed = (
datetime.now(timezone.utc) - started_at
).total_seconds()
except (TypeError, ValueError):
elapsed = self.builder_attempt_timeout_seconds + 1
if elapsed <= self.builder_attempt_timeout_seconds:
return self._intent_response(existing, idempotent=True)
existing.update(
{
"failure_code": "TRUSTED_BUILDER_CRASH_RECOVERED",
"last_recovery_code": "TRUSTED_BUILDER_CRASH_RECOVERED",
"recovery_count": int(existing.get("recovery_count") or 0) + 1,
"retryable": int(existing.get("attempt_count") or 0)
< self.max_builder_attempts,
"status": "BUILDER_RETRYABLE_FAILURE",
"updated_at": utc_now(),
}
)
_durable_json(path, existing)
# A source-admission prerequisite may initially know only the
# event/source identity. Once the verified admission becomes
# available, bind the previously missing version/admission ID to
# the same durable intent. Existing non-empty evidence can never
# be replaced.
if (
existing.get("status") != "BUILDER_RETRYABLE_FAILURE"
or int(existing.get("attempt_count") or 0)
>= self.max_builder_attempts
(not existing_version and version)
or (not existing_admission and incoming_admission)
):
return self._intent_response(existing, idempotent=True)
record = dict(existing)
if existing_status != "SOURCE_ADMISSION_REQUIRED":
raise SagaConflict(
"release intent prerequisite binding changed"
)
if version:
existing["version"] = version
if incoming_admission:
existing["source_admission_id"] = incoming_admission
existing["updated_at"] = utc_now()
_durable_json(path, existing)
if existing_status in prerequisite_states:
# Prerequisite blockers are not builder attempts. Replaying
# the same intent after Source Admission or Migration Plan
# material appears is the expected autonomous recovery path.
record = dict(existing)
else:
if existing.get("status") == "BUILDING":
try:
started_at = _parse_utc(
str(existing.get("attempt_started_at") or "")
)
elapsed = (
datetime.now(timezone.utc) - started_at
).total_seconds()
except (TypeError, ValueError):
elapsed = self.builder_attempt_timeout_seconds + 1
if elapsed <= self.builder_attempt_timeout_seconds:
return self._intent_response(existing, idempotent=True)
existing.update(
{
"failure_code": "TRUSTED_BUILDER_CRASH_RECOVERED",
"last_recovery_code": "TRUSTED_BUILDER_CRASH_RECOVERED",
"recovery_count": int(existing.get("recovery_count") or 0) + 1,
"retryable": int(existing.get("attempt_count") or 0)
< self.max_builder_attempts,
"status": "BUILDER_RETRYABLE_FAILURE",
"updated_at": utc_now(),
}
)
_durable_json(path, existing)
if (
existing.get("status") != "BUILDER_RETRYABLE_FAILURE"
or int(existing.get("attempt_count") or 0)
>= self.max_builder_attempts
):
return self._intent_response(existing, idempotent=True)
record = dict(existing)
if self.candidate_builder is None:
record.update(
{
@@ -3742,14 +3866,36 @@ 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:
prerequisite_code = {
"release source admission required": "SOURCE_ADMISSION_REQUIRED",
"migration plan required": "MIGRATION_PLAN_REQUIRED",
}.get(str(exc))
if prerequisite_code is not None:
record.update(
{
# Prerequisite discovery is a control-plane retry, not
# a business/builder execution attempt.
"attempt_count": max(
0, int(record.get("attempt_count") or 0) - 1
),
"failure_code": prerequisite_code,
"retryable": True,
"status": prerequisite_code,
}
)
else:
record.update(
{
"failure_code": (
"MIGRATION_PLAN_INVALID"
if str(exc) == "migration plan invalid"
else "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 @@
{"activation_file":"/var/lib/aios-gitops/bridge-v2-staging/task-admission-activation.json","current_components":[{"artifact_sha_env":"AIOS_GITOPS_ARTIFACT_SHA256","code_line_id":"legacy-governed-main-v15","component_id":"gitops-model-api-staging","expected_source_oid":"91f104ef62a3b688a9259b96e2ca247e0363b564","interface_files":["app/aios_model_api_staging.py","app/silent_release_hardening.py"],"marker_files":["app/aios_model_api_staging.py"],"service":"aios-gitops-model-api-v2r2c1-staging.service","source_oid_env":"AIOS_GITOPS_RUNTIME_OID"},{"artifact_sha_env":"AIOS_CONTROL_PLANE_ARTIFACT_SHA256","code_line_id":"legacy-p0-task-principal","component_id":"chatgpt-bridge-v2-staging","expected_source_oid":"91f104ef62a3b688a9259b96e2ca247e0363b564","interface_files":["app/aios_chatgpt_bridge_v2_server.py","app/aios_task_principal_admission.py"],"marker_files":["app/aios_task_principal_admission.py"],"service":"aios-chatgpt-bridge-v2-staging.service","source_oid_env":"AIOS_CONTROL_PLANE_TARGET_OID"}],"profile_id":"baseline11-principal-recovery-cross-code-line-v1","repository":"aios/aios","rollback_window_seconds":3600,"schema":"aios.release-migration-profile.v1","state_db":"/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3","state_mode":"REUSE_IN_PLACE_WITH_SNAPSHOT","state_ref":"state:task-admission","target_components":[{"code_line_id":"baseline11-release-source-admission","component_id":"gitops-model-api-staging","interface_files":["active-runtime/app/aios_model_api_staging.py","active-runtime/app/silent_release_hardening.py","active-runtime/app/aios_trusted_candidate_builder.py","active-runtime/app/release_migration_plan.py"],"service":"aios-gitops-model-api-v2r2c1-staging.service"},{"code_line_id":"baseline11-task-admission","component_id":"chatgpt-bridge-v2-staging","interface_files":["active-runtime/app/aios_chatgpt_bridge_v2_server.py","active-runtime/app/aios_task_admission_bootstrap.py","active-runtime/app/aios_task_admission_live_adapters.py"],"service":"aios-chatgpt-bridge-v2-staging.service"}],"version":"V2R2C2-baseline11-principal-recovery"}
@@ -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 cross-code-line release migration planner
After=aios-release-source-admission.service
ConditionPathExists=/var/lib/aios-release-source-admission
[Service]
Type=oneshot
User=root
Group=gitops
UMask=0027
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_RELEASE_MIGRATION_PROFILE=/etc/aios-gitops/release-migration-profiles/V2R2C2-baseline11-principal-recovery.json
EnvironmentFile=-/run/aios-release-migration-planner/request.env
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/usr/bin/python3 -B /opt/aios/release-migration/current/app/release_migration_plan.py --authority-git-dir /data/aios-gitops/repo.git --profile ${AIOS_RELEASE_MIGRATION_PROFILE} --output-root /var/lib/aios-release-migration --source-admission-root /var/lib/aios-release-source-admission --builder-policy /opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json --source-commit ${AIOS_RELEASE_SOURCE_COMMIT} --candidate-id ${AIOS_RELEASE_CANDIDATE_ID}
ExecStartPost=/usr/bin/rm -f /run/aios-release-migration-planner/request.env
StateDirectory=aios-release-migration
StateDirectoryMode=0750
RuntimeDirectory=aios-release-migration-planner
RuntimeDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
PrivateNetwork=true
ProtectSystem=strict
ProtectHome=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
RestrictAddressFamilies=AF_UNIX
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
CapabilityBoundingSet=
ReadOnlyPaths=/opt/aios/release-migration /data/aios-gitops/repo.git /var/lib/aios-release-source-admission /opt/aios/trusted-candidate-builder/current/config/gitops/trusted-candidate-builder-policy.json /etc/aios-gitops/release-migration-profiles /var/lib/aios-gitops/bridge-v2-staging
ReadWritePaths=/var/lib/aios-release-migration /run/aios-release-migration-planner
[Install]
WantedBy=multi-user.target
@@ -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,14 @@ 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_MIGRATION_PLAN_ROOT=/var/lib/aios-release-migration
Environment=AIOS_TRUSTED_BUILDER_MIGRATION_PROFILE_ROOT=/etc/aios-gitops/release-migration-profiles
Environment=AIOS_TRUSTED_BUILDER_KEY_ID=builder-attestation-ed25519-g1
Environment=AIOS_TRUSTED_BUILDER_KEY_GENERATION=1
Environment=PYTHONDONTWRITEBYTECODE=1
@@ -37,7 +41,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 -/var/lib/aios-release-migration -/etc/aios-gitops/release-migration-profiles
ReadWritePaths=/var/lib/aios-trusted-candidate-builder
[Install]
+1 -1
View File
@@ -410,7 +410,7 @@ paths:
operationId: commitFilesGoverned
summary: Submit one atomic governed commit containing multiple files
security: [{taskPrincipalAuth: []}]
x-aios-task-principal: Bridge verifies a fresh aios.task-principal-assertion.v1 and exact task/job/repository/branch/generation/revision/attempt/execution-digest/capability binding; caller JSON cannot provide a principal and missing verification is denied.
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:
@@ -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/`;各模块报告中的早期数量只用于记录开发过程,以最后统一结果为准。
+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')
@@ -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)
@@ -504,3 +504,24 @@ def test_host_adapter_accepts_intent_only_without_caller_candidate_id():
assert calls[0][1] == ''
assert calls[0][2] == COMMIT
assert len(calls[0][3]) >= 32
def test_host_adapter_maps_migration_plan_prerequisite_to_typed_blocker():
from aios_trusted_candidate_builder import BuilderError
api = importlib.import_module("aios_model_api_staging")
class MigrationRequiredClient:
def build(self, version, candidate_id, source_commit, *, nonce):
raise BuilderError("MIGRATION_PLAN_REQUIRED")
with pytest.raises(
CapabilityAuthorizationError, match="migration plan required"
):
api._HostTrustedCandidateBuilderAdapter(MigrationRequiredClient()).build(
{
"event_id": "event-migration-required",
"source_commit": COMMIT,
"version": "V2R2C2-baseline11-principal-recovery",
}
)
@@ -1,5 +1,6 @@
from pathlib import Path
import ast
import re
ROOT = Path(__file__).resolve().parents[1]
@@ -26,7 +27,19 @@ def test_bridge_unit_keeps_strict_sandbox_and_required_projection():
).read_text()
assert "ProtectSystem=strict" in text
assert "ProtectHome=true" in text
assert (
"LoadCredential=gitea_token:"
) in text
assert "Environment=AIOS_P0_TASK_PRINCIPAL_WRITES_ENABLED=true" in text
projection = re.search(
r"^LoadCredentialEncrypted=([^:]+):/etc/credstore\.encrypted/([^\n]+)$",
text,
re.MULTILINE,
)
consumer = re.search(
r"^Environment=AIOS_GITEA_TOKEN_FILE=%d/([^\n]+)$",
text,
re.MULTILINE,
)
assert projection is not None
assert consumer is not None
assert projection.group(1) == projection.group(2) == consumer.group(1)
assert "/run/credentials/aios-governance-executor.service/" not in text
assert "/data/aios-gitops/repo.git/worktrees" in text
+501
View File
@@ -0,0 +1,501 @@
from __future__ import annotations
import hashlib
import json
import os
import sqlite3
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
for import_root in (ROOT / "active-runtime" / "app", ROOT):
if str(import_root) not in sys.path:
sys.path.insert(0, str(import_root))
import release_migration_plan as migration_runtime # noqa: E402
from release_migration_plan import ( # noqa: E402
FileMigrationPlanResolver,
HostMigrationCompiler,
MigrationPlan,
MigrationPlanError,
MigrationPlanRequired,
MigrationPreSwitchValidator,
MigrationRequirement,
RuntimeComponentBinding,
StateMigrationContract,
)
from silent_release_hardening import ( # noqa: E402
DeploymentPlan,
FinalizationPlan,
ReleasePlan,
TagPlan,
)
COMMIT = "1" * 40
TREE = "2" * 40
SHA = "3" * 64
POLICY = "4" * 64
VERSION = "V2R2C2-baseline11-principal-recovery"
CANDIDATE = "rc-v2r2c2-baseline11-principal-recovery-111111111111"
def migration_plan() -> MigrationPlan:
return MigrationPlan(
migration_id="migration-test-0000000000000001",
version=VERSION,
candidate_id=CANDIDATE,
repository_identity="aios/aios",
source_admission_id="source-admission-test-0000000001",
source_commit=COMMIT,
tree_oid=TREE,
profile_id="baseline11-principal-recovery-cross-code-line-v1",
profile_sha256="5" * 64,
from_components=(
RuntimeComponentBinding(
component_id="gitops-model-api-staging",
service="aios-gitops-model-api-v2r2c1-staging.service",
code_line_id="legacy-governed-main-v15",
source_oid="6" * 40,
artifact_sha256="7" * 64,
interface_sha256="8" * 64,
),
RuntimeComponentBinding(
component_id="chatgpt-bridge-v2-staging",
service="aios-chatgpt-bridge-v2-staging.service",
code_line_id="legacy-p0-task-principal",
source_oid="6" * 40,
artifact_sha256="9" * 64,
interface_sha256="a" * 64,
),
),
to_components=(
RuntimeComponentBinding(
component_id="gitops-model-api-staging",
service="aios-gitops-model-api-v2r2c1-staging.service",
code_line_id="baseline11-release-source-admission",
source_oid=COMMIT,
artifact_sha256=None,
interface_sha256="b" * 64,
),
RuntimeComponentBinding(
component_id="chatgpt-bridge-v2-staging",
service="aios-chatgpt-bridge-v2-staging.service",
code_line_id="baseline11-task-admission",
source_oid=COMMIT,
artifact_sha256=None,
interface_sha256="c" * 64,
),
),
state_contracts=(
StateMigrationContract(
state_id="task-admission",
state_ref="state:task-admission",
mode="REUSE_IN_PLACE_WITH_SNAPSHOT",
before_schema_sha256="d" * 64,
after_schema_sha256="e" * 64,
snapshot_name="snapshots/task-admission.sqlite3",
snapshot_sha256="f" * 64,
snapshot_required=True,
rollback_compatible=True,
),
),
activation_schema="aios.task-admission-host-activation.v1",
activation_sha256="0" * 64,
rollback_window_seconds=3600,
)
def release_plan(*, migration: MigrationPlan | None = None) -> ReleasePlan:
deployment = DeploymentPlan(
service="aios-gitops-model-api-v2r2c1-staging.service",
release_path="/opt/aios/releases/" + COMMIT,
release_root="/opt/aios/releases",
cutover_link="/opt/aios/current",
dropin_path="/opt/aios/releases/" + COMMIT + "/deployment/production.conf",
dropin_sha256="a" * 64,
systemd_unit_sha256="b" * 64,
rollback_path="/opt/aios/releases/rollback",
rollback_sha256="c" * 64,
source_commit=COMMIT,
tree_oid=TREE,
artifact_sha256=SHA,
manifest_sha256="6" * 64,
package_closure_sha256="7" * 64,
authoritative_git_dir="/data/aios-gitops/repo.git",
repository_identity="aios/aios",
baseline_before_sha256="8" * 64,
registry_before_sha256="9" * 64,
production_tag_refset_before_sha256="a" * 64,
health_contract_version="runtime/v1",
timeout_policy={"seconds": 10},
retry_policy={"attempts": 1},
policy_sha256=POLICY,
health_contract={"checked": True},
)
tag = TagPlan(
version=VERSION,
tag_type="annotated",
expected_tag_oid="b" * 40,
expected_target_oid=COMMIT,
evidence_closure_oid="c" * 40,
approval_plan_id="owner-plan-migration-test",
repository_identity="aios/aios",
gitea_owner="aios",
gitea_repository="aios",
current_remote_tag_oid=None,
remote_absent_evidence_sha256="d" * 64,
verification_contract="migration/v1",
)
finalization = FinalizationPlan(
version=VERSION,
expected_tag_oid="b" * 40,
expected_target_oid=COMMIT,
expected_previous_baseline_sha256="8" * 64,
registry_expected_record={"version": VERSION},
baseline_expected_record={"version": VERSION},
final_consistency_contract="migration/v1",
)
return ReleasePlan(
deployment=deployment,
tag=tag,
finalization=finalization,
policy_sha256=POLICY,
migration=migration,
)
def test_release_plan_legacy_bytes_are_unchanged_without_migration() -> None:
plan = release_plan()
legacy_payload = {
"deployment": plan.deployment.canonical_payload,
"finalization": plan.finalization.canonical_payload,
"policy_sha256": plan.policy_sha256,
"tag": plan.tag.canonical_payload,
}
expected = hashlib.sha256(
json.dumps(legacy_payload, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
assert "migration" not in plan.canonical_payload
assert plan.sha256 == expected
assert ReleasePlan.from_dict(plan.to_dict()).sha256 == expected
def test_migration_is_part_of_release_plan_hash_and_candidate_binding() -> None:
migration = migration_plan()
plan = release_plan(migration=migration)
assert plan.validate().migration == migration
assert ReleasePlan.from_dict(plan.to_dict()).sha256 == plan.sha256
assert release_plan().sha256 != plan.sha256
migration.assert_release_binding(
version=VERSION,
candidate_id=CANDIDATE,
source_commit=COMMIT,
tree_oid=TREE,
repository_identity="aios/aios",
)
with pytest.raises(MigrationPlanError, match="binding"):
migration.assert_release_binding(
version=VERSION,
candidate_id="rc-different",
source_commit=COMMIT,
tree_oid=TREE,
repository_identity="aios/aios",
)
def test_file_resolver_requires_plan_for_profile_and_rejects_tamper(tmp_path: Path) -> None:
profile_root = tmp_path / "profiles"
plan_root = tmp_path / "plans"
profile_root.mkdir()
profile = {
"schema": "aios.release-migration-profile.v1",
"profile_id": "baseline11-principal-recovery-cross-code-line-v1",
"version": VERSION,
}
profile_path = profile_root / "profile.json"
profile_path.write_text(
json.dumps(profile, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
resolver = FileMigrationPlanResolver(
plan_root, profile_root=profile_root, expected_owner_uid=None
)
with pytest.raises(MigrationPlanRequired, match="MIGRATION_PLAN_REQUIRED"):
resolver.resolve(VERSION, CANDIDATE, COMMIT)
migration = migration_plan()
requirement = MigrationRequirement(
version=VERSION,
candidate_id=CANDIDATE,
source_commit=COMMIT,
repository_identity="aios/aios",
required=True,
reason_code="CROSS_CODE_LINE",
profile_id=profile["profile_id"],
profile_sha256=hashlib.sha256(
(json.dumps(profile, sort_keys=True, separators=(",", ":")) + "\n").encode()
).hexdigest(),
migration_plan_sha256=migration.sha256,
)
source_root = plan_root / COMMIT
source_root.mkdir(parents=True)
(source_root / "requirement.json").write_text(
json.dumps(requirement.to_dict(), sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
(source_root / "plan.json").write_text(
json.dumps(migration.to_dict(), sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
assert resolver.resolve(VERSION, CANDIDATE, COMMIT).sha256 == migration.sha256
raw = json.loads((source_root / "plan.json").read_text())
raw["rollback_window_seconds"] = 12
(source_root / "plan.json").write_text(json.dumps(raw), encoding="utf-8")
with pytest.raises(MigrationPlanError):
resolver.resolve(VERSION, CANDIDATE, COMMIT)
def test_state_probe_is_additive_and_persists_pre_migration_snapshot(tmp_path: Path) -> None:
live = tmp_path / "legacy.sqlite3"
with sqlite3.connect(live) as connection:
connection.execute(
"CREATE TABLE legacy_task_claims (task_id TEXT PRIMARY KEY, status TEXT NOT NULL)"
)
connection.execute(
"INSERT INTO legacy_task_claims(task_id,status) VALUES('task-1','DONE')"
)
connection.commit()
snapshot = tmp_path / "out" / "snapshots" / "task-admission.sqlite3"
contract = HostMigrationCompiler._state_compatibility(
live,
"state:task-admission",
snapshot_path=snapshot,
snapshot_name="snapshots/task-admission.sqlite3",
)
assert contract.mode == "REUSE_IN_PLACE_WITH_SNAPSHOT"
assert contract.snapshot_sha256 == hashlib.sha256(snapshot.read_bytes()).hexdigest()
with sqlite3.connect(snapshot) as connection:
assert connection.execute(
"SELECT status FROM legacy_task_claims WHERE task_id='task-1'"
).fetchone()[0] == "DONE"
# The rollback snapshot is pre-migration; target tables are created
# only on the private compatibility probe.
tables = {
row[0]
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
assert "task_admission_grants" not in tables
assert contract.before_schema_sha256 != contract.after_schema_sha256
def test_migration_pre_switch_proof_is_exact_and_fail_closed() -> None:
migration = migration_plan()
proof = {
"migration_plan_sha256": migration.sha256,
"migration_id": migration.migration_id,
"source_commit": migration.source_commit,
"tree_oid": migration.tree_oid,
"activation_sha256": migration.activation_sha256,
"state_snapshot_sha256s": ["f" * 64],
"writer_disabled": True,
"task_quiescent": True,
"state_snapshot_verified": True,
"credentials_ready": True,
"rollback_ready": True,
}
validator = MigrationPreSwitchValidator(lambda _plan: dict(proof))
assert validator.validate(migration)["verified"] is True
proof["task_quiescent"] = False
with pytest.raises(MigrationPlanError, match="PRE_SWITCH"):
validator.validate(migration)
def test_host_compiler_generates_bound_plan_from_live_read_only_probe(
tmp_path: Path, monkeypatch
) -> None:
runtime_a = tmp_path / "runtime-model"
runtime_b = tmp_path / "runtime-bridge"
for runtime in (runtime_a, runtime_b):
(runtime / "app").mkdir(parents=True)
(runtime_a / "app" / "model.py").write_text("MODEL=1\n")
(runtime_a / "app" / "marker.py").write_text("MARKER=1\n")
(runtime_b / "app" / "bridge.py").write_text("BRIDGE=1\n")
(runtime_b / "app" / "legacy.py").write_text("LEGACY=1\n")
state_db = tmp_path / "task-admission.sqlite3"
with sqlite3.connect(state_db) as connection:
connection.execute(
"CREATE TABLE legacy_task_claims (task_id TEXT PRIMARY KEY, status TEXT NOT NULL)"
)
connection.execute(
"INSERT INTO legacy_task_claims VALUES('task-live','DONE')"
)
connection.commit()
activation = tmp_path / "activation.json"
activation.write_text(
json.dumps(
{
"schema": "aios.task-admission-host-activation.v1",
"state_db": str(state_db),
},
sort_keys=True,
separators=(",", ":"),
)
+ "\n",
encoding="utf-8",
)
profile = {
"schema": "aios.release-migration-profile.v1",
"profile_id": "test-cross-code-line-profile-v1",
"version": VERSION,
"repository": "aios/aios",
"current_components": [
{
"component_id": "model",
"service": "model.service",
"code_line_id": "legacy-model",
"expected_source_oid": "6" * 40,
"source_oid_env": "SOURCE",
"artifact_sha_env": "ARTIFACT",
"interface_files": ["app/model.py"],
"marker_files": ["app/marker.py"],
},
{
"component_id": "bridge",
"service": "bridge.service",
"code_line_id": "legacy-bridge",
"expected_source_oid": "6" * 40,
"source_oid_env": "SOURCE",
"artifact_sha_env": "ARTIFACT",
"interface_files": ["app/bridge.py", "app/legacy.py"],
"marker_files": ["app/legacy.py"],
},
],
"target_components": [
{
"component_id": "model",
"service": "model.service",
"code_line_id": "baseline-model",
"interface_files": ["active-runtime/app/model.py"],
},
{
"component_id": "bridge",
"service": "bridge.service",
"code_line_id": "baseline-bridge",
"interface_files": ["active-runtime/app/bridge.py"],
},
],
"activation_file": str(activation),
"state_ref": "state:task-admission",
"state_db": str(state_db),
"state_mode": "REUSE_IN_PLACE_WITH_SNAPSHOT",
"rollback_window_seconds": 3600,
}
profile_path = tmp_path / "profile.json"
profile_path.write_text(
json.dumps(profile, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
def fake_systemd(service: str):
root = runtime_a if service == "model.service" else runtime_b
return {
"ActiveState": "active",
"WorkingDirectory": str(root),
"environment": {
"SOURCE": "6" * 40,
"ARTIFACT": "7" * 64,
},
}
monkeypatch.setattr(migration_runtime, "_systemd_properties", fake_systemd)
monkeypatch.setattr(
migration_runtime,
"_hash_git_files",
lambda _git, _source, files: hashlib.sha256(
"\0".join(files).encode()
).hexdigest(),
)
compiler = HostMigrationCompiler(
authority_git_dir=tmp_path / "authority.git",
profile_path=profile_path,
output_root=tmp_path / "migration-output",
expected_owner_uid=None,
)
requirement, plan = compiler.compile(
source_admission={
"schema": "aios.release-source-admission.v1",
"admission_id": "source-admission-compiler-test-001",
"repository": "aios/aios",
"source_commit": COMMIT,
"tree_oid": TREE,
"version": VERSION,
"candidate_id": CANDIDATE,
},
candidate_id=CANDIDATE,
)
assert requirement.required is True
assert requirement.migration_plan_sha256 == plan.sha256
snapshot = (
tmp_path
/ "migration-output"
/ COMMIT
/ plan.state_contracts[0].snapshot_name
)
assert snapshot.is_file()
assert hashlib.sha256(snapshot.read_bytes()).hexdigest() == plan.state_contracts[0].snapshot_sha256
# The planner probes a private copy only; live legacy state is untouched.
with sqlite3.connect(state_db) as connection:
assert connection.execute(
"SELECT status FROM legacy_task_claims WHERE task_id='task-live'"
).fetchone()[0] == "DONE"
assert connection.execute(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='task_admission_grants'"
).fetchone()[0] == 0
def test_migration_systemd_units_keep_write_gate_and_builder_network_closed() -> None:
planner = (
ROOT / "deploy" / "systemd" / "aios-release-migration-planner.service"
).read_text(encoding="utf-8")
builder = (
ROOT / "deploy" / "systemd" / "aios-trusted-candidate-builder.service"
).read_text(encoding="utf-8")
assert "AIOS_GITOPS_WRITES_ENABLED=false" in planner
assert "LoadCredentialEncrypted=source_admission_hmac:" in planner
assert "builder_hmac" not in planner
assert "PrivateNetwork=true" in planner
assert "ReadOnlyPaths=" in planner and "/data/aios-gitops/repo.git" in planner
assert "AIOS_TRUSTED_BUILDER_MIGRATION_PLAN_ROOT=/var/lib/aios-release-migration" in builder
assert "AIOS_TRUSTED_BUILDER_MIGRATION_PROFILE_ROOT=/etc/aios-gitops/release-migration-profiles" in builder
assert "PrivateNetwork=true" in builder
def test_baseline11_migration_profile_binds_observed_old_code_lines() -> None:
profile = json.loads(
(
ROOT
/ "config"
/ "gitops"
/ "release-migration-profiles"
/ "V2R2C2-baseline11-principal-recovery.json"
).read_text(encoding="utf-8")
)
assert profile["schema"] == "aios.release-migration-profile.v1"
assert profile["version"] == VERSION
assert profile["state_mode"] == "REUSE_IN_PLACE_WITH_SNAPSHOT"
current = {item["component_id"]: item for item in profile["current_components"]}
assert current["gitops-model-api-staging"]["expected_source_oid"] == "91f104ef62a3b688a9259b96e2ca247e0363b564"
assert current["chatgpt-bridge-v2-staging"]["expected_source_oid"] == "91f104ef62a3b688a9259b96e2ca247e0363b564"
assert "app/aios_task_principal_admission.py" in current["chatgpt-bridge-v2-staging"]["marker_files"]
target = {item["component_id"]: item for item in profile["target_components"]}
assert "active-runtime/app/aios_task_admission_live_adapters.py" in target["chatgpt-bridge-v2-staging"]["interface_files"]
+252
View File
@@ -0,0 +1,252 @@
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from release_source_admission import ( # noqa: E402
ReleaseSourceAdmissionError,
ReleaseSourceAdmissionStore,
ReleaseSourceAdmitter,
)
MAIN = "1" * 40
SOURCE = "2" * 40
TREE = "3" * 40
POLICY = "4" * 64
REF = "refs/heads/agent/baseline-1.1-principal-recovery-20260908"
VERSION = "V2R2C4"
TASK = "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE"
class FakeBackend:
def __init__(
self,
*,
remote: str = SOURCE,
fetched: str = SOURCE,
tree: str = TREE,
main: str = MAIN,
lineage: bool = True,
) -> None:
self.remote = remote
self.fetched = fetched
self.tree = tree
self.main = main
self.lineage = lineage
self.fetch_calls: list[tuple[str, str]] = []
self.lineage_calls: list[tuple[str, str]] = []
def authoritative_main_oid(self) -> str:
return self.main
def remote_ref_oid(self, source_ref: str) -> str:
assert source_ref == REF
return self.remote
def fetch_exact_ref(self, source_ref: str, source_commit: str) -> str:
self.fetch_calls.append((source_ref, source_commit))
return self.fetched
def tree_oid(self, source_commit: str) -> str:
assert source_commit == SOURCE
return self.tree
def require_lineage(self, base_oid: str, source_commit: str) -> None:
self.lineage_calls.append((base_oid, source_commit))
if not self.lineage:
raise ReleaseSourceAdmissionError("SOURCE_LINEAGE_MISMATCH")
def make_store(tmp_path: Path, *, writable: bool = True) -> ReleaseSourceAdmissionStore:
owner = os.geteuid() if hasattr(os, "geteuid") else None
return ReleaseSourceAdmissionStore(
tmp_path / "admissions",
repository="aios/aios",
policy_sha256=POLICY,
hmac_key=b"h" * 32,
expected_owner_uid=owner,
writable=writable,
)
def test_admit_binds_remote_ref_fetch_tree_main_lineage_and_recipe(tmp_path: Path) -> None:
store = make_store(tmp_path)
backend = FakeBackend()
record = ReleaseSourceAdmitter(store, backend).admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
assert record["schema"] == "aios.release-source-admission.v1"
assert record["repository"] == "aios/aios"
assert record["source_commit"] == SOURCE
assert record["remote_ref_oid"] == SOURCE
assert record["source_ref"] == REF
assert record["base_oid"] == MAIN
assert record["tree_oid"] == TREE
assert record["version"] == VERSION
assert record["task_id"] == TASK
assert record["candidate_id"] == "rc-v2r2c4-" + SOURCE[:12]
assert backend.fetch_calls == [(REF, SOURCE)]
assert backend.lineage_calls == [(MAIN, SOURCE)]
recipe = store.resolve_recipe(VERSION, None, SOURCE)
assert recipe == {
"mode": "governed-source-admission",
"task_id": TASK,
"candidate_id": record["candidate_id"],
"base_oid": MAIN,
"source_commit": SOURCE,
"version": VERSION,
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
}
def test_repeated_exact_admission_is_idempotent(tmp_path: Path) -> None:
store = make_store(tmp_path)
admitter = ReleaseSourceAdmitter(store, FakeBackend())
first = admitter.admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
second = admitter.admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
assert second == first
assert len(list((tmp_path / "admissions").glob("*.json"))) == 1
@pytest.mark.parametrize(
("backend", "error"),
(
(FakeBackend(remote="5" * 40), "SOURCE_REF_MOVED"),
(FakeBackend(fetched="5" * 40), "SOURCE_FETCH_MISMATCH"),
(FakeBackend(lineage=False), "SOURCE_LINEAGE_MISMATCH"),
),
)
def test_admission_rejects_moved_fetch_mismatch_or_non_descendant(
tmp_path: Path,
backend: FakeBackend,
error: str,
) -> None:
store = make_store(tmp_path)
with pytest.raises(ReleaseSourceAdmissionError, match=error):
ReleaseSourceAdmitter(store, backend).admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
assert not list((tmp_path / "admissions").glob("*.json"))
@pytest.mark.parametrize(
"source_ref",
(
"agent/bad",
"refs/tags/v1",
"refs/heads/../main",
"refs/heads/agent//bad",
"refs/heads/agent/bad.lock",
),
)
def test_admission_rejects_unsafe_or_non_head_refs(
tmp_path: Path, source_ref: str
) -> None:
store = make_store(tmp_path)
with pytest.raises(ReleaseSourceAdmissionError, match="SOURCE_REF_INVALID"):
ReleaseSourceAdmitter(store, FakeBackend()).admit(
version=VERSION,
task_id=TASK,
source_ref=source_ref,
source_commit=SOURCE,
)
def test_store_rejects_caller_candidate_rebinding(tmp_path: Path) -> None:
store = make_store(tmp_path)
record = ReleaseSourceAdmitter(store, FakeBackend()).admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
with pytest.raises(
ReleaseSourceAdmissionError,
match="SOURCE_ADMISSION_CANDIDATE_MISMATCH",
):
store.resolve_recipe(VERSION, "rc-attacker", SOURCE)
assert record["candidate_id"] != "rc-attacker"
def test_store_rejects_policy_change_and_record_tamper(tmp_path: Path) -> None:
store = make_store(tmp_path)
record = ReleaseSourceAdmitter(store, FakeBackend()).admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
path = tmp_path / "admissions" / f"{SOURCE}.json"
payload = json.loads(path.read_text(encoding="utf-8"))
payload["task_id"] = "other-task"
path.write_text(
json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
with pytest.raises(
ReleaseSourceAdmissionError,
match="SOURCE_ADMISSION_MAC_INVALID",
):
store.lookup_source(SOURCE)
owner = os.geteuid() if hasattr(os, "geteuid") else None
other_policy = ReleaseSourceAdmissionStore(
tmp_path / "admissions",
repository="aios/aios",
policy_sha256="6" * 64,
hmac_key=b"h" * 32,
expected_owner_uid=owner,
writable=False,
)
with pytest.raises(
ReleaseSourceAdmissionError,
match="SOURCE_ADMISSION_POLICY_MISMATCH",
):
other_policy.lookup_source(SOURCE)
def test_store_rejects_group_writable_record(tmp_path: Path) -> None:
store = make_store(tmp_path)
ReleaseSourceAdmitter(store, FakeBackend()).admit(
version=VERSION,
task_id=TASK,
source_ref=REF,
source_commit=SOURCE,
)
path = tmp_path / "admissions" / f"{SOURCE}.json"
os.chmod(path, 0o660)
with pytest.raises(
ReleaseSourceAdmissionError,
match="SOURCE_ADMISSION_PERMISSIONS_INVALID",
):
store.lookup_source(SOURCE)
@@ -726,3 +726,129 @@ def test_systemd_shadow_unit_has_both_gates_and_no_secret_environment():
assert "PRIVATE_KEY=" not in unit
assert "TOKEN=" not in unit
assert "PASSWORD=" not in unit
def test_release_intent_derives_version_from_governed_source_admission(tmp_path):
service, _ = trusted_service(tmp_path)
service.source_admission_resolver = lambda source_commit: {
"schema": "aios.release-source-admission.v1",
"admission_id": "source-admission-test-001",
"source_commit": source_commit,
"version": "V2R2C4",
}
result = service.create_release_intent(
{"event_id": "webhook-admitted-source-001", "source_commit": COMMIT}
)
assert result["version"] == "V2R2C4"
assert result["source_admission_id"] == "source-admission-test-001"
intent_path = service.intents_root / f"{result['intent_id']}.json"
persisted = json.loads(intent_path.read_text(encoding="utf-8"))
assert persisted["version"] == "V2R2C4"
assert persisted["source_admission_id"] == "source-admission-test-001"
def test_release_intent_rejects_version_rebinding_against_source_admission(tmp_path):
service, _ = trusted_service(tmp_path)
service.source_admission_resolver = lambda source_commit: {
"schema": "aios.release-source-admission.v1",
"admission_id": "source-admission-test-002",
"source_commit": source_commit,
"version": "V2R2C4",
}
with pytest.raises(
CapabilityAuthorizationError,
match="release source admission version mismatch",
):
service.create_release_intent(
{
"event_id": "webhook-admitted-source-002",
"source_commit": COMMIT,
"version": "V2R2C5",
}
)
def test_release_intent_surfaces_source_admission_required_as_typed_blocker(tmp_path):
service, _ = trusted_service(tmp_path)
class AdmissionRequiredBuilder:
def build(self, _payload):
raise CapabilityAuthorizationError(
"release source admission required"
)
service.candidate_builder = AdmissionRequiredBuilder()
result = service.create_release_intent(
{
"event_id": "webhook-source-admission-required-001",
"source_commit": COMMIT,
}
)
assert result["status"] == "SOURCE_ADMISSION_REQUIRED"
assert result["failure_code"] == "SOURCE_ADMISSION_REQUIRED"
assert result["retryable"] is True
assert result["attempt_count"] == 0
assert result["side_effects_executed"] is False
def test_source_admission_prerequisite_replays_same_intent_after_verified_binding(tmp_path):
service, _ = trusted_service(tmp_path)
class AdmissionRequiredBuilder:
def build(self, _payload):
raise CapabilityAuthorizationError("release source admission required")
service.candidate_builder = AdmissionRequiredBuilder()
first = service.create_release_intent(
{"event_id": "webhook-source-admission-replay-001", "source_commit": COMMIT}
)
assert first["status"] == "SOURCE_ADMISSION_REQUIRED"
assert first["attempt_count"] == 0
service.source_admission_resolver = lambda source_commit: {
"schema": "aios.release-source-admission.v1",
"admission_id": "source-admission-replay-001",
"source_commit": source_commit,
"version": "V2R2C4",
}
second = service.create_release_intent(
{"event_id": "webhook-source-admission-replay-001", "source_commit": COMMIT}
)
assert second["intent_id"] == first["intent_id"]
assert second["source_admission_id"] == "source-admission-replay-001"
assert second["version"] == "V2R2C4"
assert second["status"] == "SOURCE_ADMISSION_REQUIRED"
assert second["attempt_count"] == 0
def test_migration_plan_required_is_machine_repairable_without_attempt_budget(tmp_path):
service, _ = trusted_service(tmp_path)
class MigrationRequiredBuilder:
def build(self, _payload):
raise CapabilityAuthorizationError("migration plan required")
service.candidate_builder = MigrationRequiredBuilder()
result = service.create_release_intent(
{
"event_id": "webhook-migration-plan-required-001",
"source_commit": COMMIT,
}
)
assert result["status"] == "MIGRATION_PLAN_REQUIRED"
assert result["failure_code"] == "MIGRATION_PLAN_REQUIRED"
assert result["retryable"] is True
assert result["attempt_count"] == 0
replay = service.create_release_intent(
{
"event_id": "webhook-migration-plan-required-001",
"source_commit": COMMIT,
}
)
assert replay["intent_id"] == result["intent_id"]
assert replay["status"] == "MIGRATION_PLAN_REQUIRED"
assert replay["attempt_count"] == 0
@@ -418,8 +418,10 @@ def test_bridge_persistent_consumer_is_opt_in_and_default_fail_closed():
ROOT / "active-runtime" / "app" / "aios_chatgpt_bridge_v2_server.py"
).read_text(encoding="utf-8")
assert 'os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip()' in source
assert "build_task_admission_handlers_from_environment" in source
assert "build_task_admission_runtime_from_environment" in source
assert "task_admission_handlers is None" in source
assert "admission_runtime.principal_verifier" in source
assert "TaskPrincipal" in source
def test_bridge_persistent_host_is_opt_in_and_activation_file_is_fail_closed():
@@ -428,7 +430,7 @@ def test_bridge_persistent_host_is_opt_in_and_activation_file_is_fail_closed():
assert 'AIOS_TASK_ADMISSION_ACTIVATION_FILE' in text
assert '/var/lib/aios-gitops/bridge-v2-staging/task-admission-activation.json' in text
assert 'Path(activation_raw).is_file()' in text
assert 'build_task_admission_handlers_from_activation_file' in text
assert 'build_task_admission_runtime_from_activation_file' in text
def test_host_default_state_database_stays_inside_existing_bridge_state_root():
+246
View File
@@ -0,0 +1,246 @@
from __future__ import annotations
from pathlib import Path
import sys
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_task_admission_bootstrap import ( # noqa: E402
OwnerDelegationEpoch,
TaskAdmissionError,
TaskAdmissionGrant,
TaskAdmissionStore,
)
from aios_task_admission_live_adapters import ( # noqa: E402
TaskAdmissionPrincipalProvider,
)
def _grant(*, capabilities=None) -> TaskAdmissionGrant:
return TaskAdmissionGrant(
grant_id="grant-1",
envelope_id="env-1",
task_id="task-1",
repository="aios/aios",
authority_base_oid="a" * 40,
target_branch="agent/task-1",
generation=1,
execution_digest="b" * 64,
capabilities=tuple(
capabilities
or (
"task.principal.issue",
"writer.lease",
"governed.write.file",
"governed.write.files",
)
),
file_allowlist=("active-runtime/app/example.py",),
risk="medium",
trust_domain="aios.owner",
policy_digest="c" * 64,
authorization_kind="delegation_epoch",
owner_identity="owner",
parent_epoch_id="epoch-1",
issued_at=100,
expires_at=1000,
one_time_use=True,
)
def _epoch(*, capabilities=None) -> OwnerDelegationEpoch:
return OwnerDelegationEpoch(
epoch_id="epoch-1",
owner_identity="owner",
trust_domain="aios.owner",
repository="aios/aios",
allowed_branch_prefixes=("agent/",),
allowed_capabilities=frozenset(
capabilities
or {
"task.principal.issue",
"writer.lease",
"governed.write.file",
"governed.write.files",
}
),
allowed_file_prefixes=("active-runtime/app/",),
risk_ceiling="medium",
issued_at=1,
not_before=1,
expires_at=2000,
revocation_epoch=0,
policy_digest="c" * 64,
)
class Store:
def __init__(self, grant):
self.grant = grant
def get_grant_by_task_id(self, task_id):
return self.grant if self.grant and self.grant.task_id == task_id else None
class Authorization:
def __init__(self, epoch=None, error=None):
self.epoch = epoch or _epoch()
self.error = error
self.calls = 0
def verify_delegation_epoch(self, epoch_id):
self.calls += 1
if self.error is not None:
raise self.error
assert epoch_id == self.epoch.epoch_id
return self.epoch
class Port:
def __init__(self, *, state="CLAIMED", job_id="job-1"):
self.state = state
self.job_id = job_id
self.calls = 0
def verify_active_task(self, grant):
self.calls += 1
if self.state not in {"CLAIMED", "RUNNING"}:
raise TaskAdmissionError(
"TASK_PRINCIPAL_TASK_NOT_CLAIMED", 409, details={"state": self.state}
)
return (
{"task_id": grant.task_id, "state": self.state},
{
"task_id": grant.task_id,
"repository": grant.repository,
"target_branch": grant.target_branch,
"generation": grant.generation,
"external_job_id": self.job_id,
},
)
def _request(**updates):
value = {
"task_id": "task-1",
"job_id": "job-1",
"repository": "aios/aios",
"base_oid": "a" * 40,
"target_branch": "agent/task-1",
"generation": 1,
}
value.update(updates)
return value
def test_provider_recovers_principal_from_claimed_task_without_caller_bearer():
auth = Authorization()
provider = TaskAdmissionPrincipalProvider(
store=Store(_grant()),
authorization=auth,
task_port=Port(state="CLAIMED"),
epoch_id="epoch-1",
clock=lambda: 200,
)
result = provider({"operation": "acquireWriterLease", "headers": {}}, _request())
assert result["schema"] == "aios.task-principal-server-attestation.v1"
assert result["task_id"] == "task-1"
assert result["job_id"] == "job-1"
assert result["capability"] == "writer.lease"
assert result["task_state"] == "CLAIMED"
assert auth.calls == 1
assert provider.readiness()["ready"] is True
def test_provider_requires_claim_before_writer_lease():
provider = TaskAdmissionPrincipalProvider(
store=Store(_grant()),
authorization=Authorization(),
task_port=Port(state="QUEUED"),
epoch_id="epoch-1",
clock=lambda: 200,
)
with pytest.raises(TaskAdmissionError, match="TASK_PRINCIPAL_TASK_NOT_CLAIMED"):
provider({"operation": "acquireWriterLease"}, _request())
@pytest.mark.parametrize(
("field", "value"),
[
("job_id", "job-other"),
("repository", "other/repo"),
("target_branch", "agent/other"),
("generation", 2),
("base_oid", "d" * 40),
],
)
def test_provider_rejects_request_binding_drift(field, value):
provider = TaskAdmissionPrincipalProvider(
store=Store(_grant()),
authorization=Authorization(),
task_port=Port(),
epoch_id="epoch-1",
clock=lambda: 200,
)
with pytest.raises(TaskAdmissionError, match="TASK_PRINCIPAL_REQUEST_BINDING_MISMATCH"):
provider(
{"operation": "acquireWriterLease"},
_request(**{field: value}),
)
def test_provider_reverifies_revocation_on_every_use():
provider = TaskAdmissionPrincipalProvider(
store=Store(_grant()),
authorization=Authorization(
error=TaskAdmissionError("TASK_ADMISSION_EPOCH_REVOKED", 403)
),
task_port=Port(),
epoch_id="epoch-1",
clock=lambda: 200,
)
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_REVOKED"):
provider({"operation": "acquireWriterLease"}, _request())
def test_provider_rejects_missing_execution_capability():
provider = TaskAdmissionPrincipalProvider(
store=Store(_grant(capabilities=("task.principal.issue",))),
authorization=Authorization(),
task_port=Port(),
epoch_id="epoch-1",
clock=lambda: 200,
)
with pytest.raises(TaskAdmissionError, match="TASK_PRINCIPAL_CAPABILITY_DENIED"):
provider({"operation": "acquireWriterLease"}, _request())
def test_provider_does_not_treat_missing_admission_as_owner_reauthorization():
provider = TaskAdmissionPrincipalProvider(
store=Store(None),
authorization=Authorization(),
task_port=Port(),
epoch_id="epoch-1",
clock=lambda: 200,
)
with pytest.raises(TaskAdmissionError, match="TASK_PRINCIPAL_AUTHORIZATION_REQUIRED"):
provider({"operation": "acquireWriterLease"}, _request())
def test_store_recovers_exact_grant_by_task_id(tmp_path):
store = TaskAdmissionStore(tmp_path / "admission.sqlite3")
grant = _grant()
store.save_grant_and_receipt(
envelope_id=grant.envelope_id,
grant=grant,
receipt={"schema": "fixture", "task_id": grant.task_id},
created_at=200,
)
recovered = store.get_grant_by_task_id(grant.task_id)
assert recovered == grant
assert store.get_grant_by_task_id("task-missing") is None
+180
View File
@@ -29,6 +29,12 @@ from aios_trusted_candidate_builder import ( # noqa: E402
load_builder_policy,
)
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
from release_migration_plan import ( # noqa: E402
MigrationPlan,
MigrationPlanRequired,
RuntimeComponentBinding,
StateMigrationContract,
)
from silent_release_hardening import ( # noqa: E402
BuilderAttestation,
BuilderAttestationKeyRecord,
@@ -590,3 +596,177 @@ def test_systemd_socket_and_service_are_af_unix_hardened_and_credential_bound()
assert policy["allowed_peer_cgroups"] == [
"/system.slice/aios-gitops-model-api-v2r2c1-staging.service"
]
def test_governed_source_admission_resolves_unknown_version_without_overriding_fixed_recipe() -> None:
policy = load_builder_policy(POLICY_PATH)
calls: list[tuple[str, str | None, str]] = []
def resolver(version: str, candidate_id: str | None, source_commit: str):
calls.append((version, candidate_id, source_commit))
return {
"mode": "governed-source-admission",
"task_id": "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE",
"candidate_id": "rc-v2r2c4-" + source_commit[:12],
"base_oid": "1" * 40,
"source_commit": source_commit,
"version": version,
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
}
config = dataclasses.replace(
TrustedCandidateBuilderConfig.from_policy(policy),
source_admission_resolver=resolver,
)
client = UnixTrustedCandidateBuilderClient(
config,
BuilderKeyring(builder_hmac=b"h" * 32),
clock=lambda: NOW,
)
source = "2" * 40
request = client.create_request(
"V2R2C4",
None,
source,
nonce="z" * 32,
)
assert request["candidate_id"] == "rc-v2r2c4-" + source[:12]
assert request["base_oid"] == "1" * 40
assert request["source_commit"] == source
assert request["task_id"] == "AIOS-BASELINE-1.1-PRODUCTION-CLOSURE"
assert calls == [("V2R2C4", None, source)]
with pytest.raises(BuilderError, match="REQUEST_RELEASE_BINDING_INVALID"):
config.resolve_recipe(
"V2R2C2",
None,
source,
)
# Fixed policy versions never fall through to the admission resolver.
assert calls == [("V2R2C4", None, source)]
def test_unknown_release_source_requires_explicit_admission() -> None:
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
with pytest.raises(BuilderError, match="RECIPE_UNKNOWN"):
config.resolve_recipe("V2R2C4", None, "2" * 40)
def test_governed_source_admission_rejects_caller_candidate_rebinding() -> None:
policy = load_builder_policy(POLICY_PATH)
def resolver(version: str, candidate_id: str | None, source_commit: str):
return {
"mode": "governed-source-admission",
"task_id": "task-admitted-source",
"candidate_id": "rc-v2r2c4-" + source_commit[:12],
"base_oid": "1" * 40,
"source_commit": source_commit,
"version": version,
"purpose": "AIOS_TRUSTED_RELEASE_BUILDER",
}
config = dataclasses.replace(
TrustedCandidateBuilderConfig.from_policy(policy),
source_admission_resolver=resolver,
)
with pytest.raises(BuilderError, match="REQUEST_RELEASE_BINDING_INVALID"):
config.resolve_recipe("V2R2C4", "rc-attacker", "2" * 40)
def _migration_for_fixed_recipe(config: TrustedCandidateBuilderConfig) -> MigrationPlan:
recipe = config.fixed_recipe("V2R2C2")
digest = lambda label: hashlib.sha256(
f"{label}\0{recipe['base_oid']}".encode()
).hexdigest()
return MigrationPlan(
migration_id="migration-builder-test-000000001",
version="V2R2C2",
candidate_id=recipe["candidate_id"],
repository_identity="aios/aios",
source_admission_id="source-admission-builder-test-001",
source_commit=recipe["source_commit"],
tree_oid=digest("tree")[:40],
profile_id="migration-profile-builder-test-v1",
profile_sha256="1" * 64,
from_components=(
RuntimeComponentBinding(
component_id="gitops-model-api-staging",
service="aios-gitops-model-api-v2r2c1-staging.service",
code_line_id="legacy-v1",
source_oid="2" * 40,
artifact_sha256="3" * 64,
interface_sha256="4" * 64,
),
),
to_components=(
RuntimeComponentBinding(
component_id="gitops-model-api-staging",
service="aios-gitops-model-api-v2r2c1-staging.service",
code_line_id="baseline11-v1",
source_oid=recipe["source_commit"],
artifact_sha256=None,
interface_sha256="5" * 64,
),
),
state_contracts=(
StateMigrationContract(
state_id="task-admission",
state_ref="state:task-admission",
mode="REUSE_IN_PLACE_WITH_SNAPSHOT",
before_schema_sha256="6" * 64,
after_schema_sha256="7" * 64,
snapshot_name="snapshots/task-admission.sqlite3",
snapshot_sha256="8" * 64,
snapshot_required=True,
rollback_compatible=True,
),
),
activation_schema="aios.task-admission-host-activation.v1",
activation_sha256="9" * 64,
rollback_window_seconds=3600,
)
def test_collector_injects_root_owned_migration_plan_into_release_plan():
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
migration = _migration_for_fixed_recipe(config)
config = dataclasses.replace(
config,
migration_plan_resolver=lambda version, candidate_id, source_commit: migration,
)
recipe = config.fixed_recipe("V2R2C2")
def migration_compatible_bundle(recipe_name, recipe_value):
bundle = dict(passing_bundle(recipe_name, recipe_value))
release = dict(bundle["release_plan"])
tag = dict(release["tag"])
finalization = dict(release["finalization"])
tag["version"] = "V2R2C2"
finalization["version"] = "V2R2C2"
release["tag"] = tag
release["finalization"] = finalization
bundle["release_plan"] = release
return bundle
result = DeterministicEvidenceCollector(
config, evidence_source=migration_compatible_bundle
).collect("V2R2C2", recipe)
assert result["release_plan"]["migration"]["sha256"] == migration.sha256
assert ReleasePlan.from_dict(result["release_plan"]).migration.sha256 == migration.sha256
def test_collector_surfaces_missing_migration_plan_as_typed_builder_blocker():
config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
config = dataclasses.replace(
config,
migration_plan_resolver=lambda *_args: (_ for _ in ()).throw(
MigrationPlanRequired("MIGRATION_PLAN_REQUIRED")
),
)
recipe = config.fixed_recipe("V2R2C2")
with pytest.raises(BuilderError, match="MIGRATION_PLAN_REQUIRED"):
DeterministicEvidenceCollector(
config, evidence_source=passing_bundle
).collect("V2R2C2", recipe)
+402
View File
@@ -0,0 +1,402 @@
#!/usr/bin/env python3
"""Create, inspect and stage an AIOS source change package. Never activate services.
The package is a reproducible collection of byte-level changes, not an authorization
credential. Obtain it through a trusted delivery channel. SHA-256 detects corruption;
it does not establish who approved the package.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import stat
import subprocess
import tempfile
import zipfile
SCHEMA = "aios.source-change-bundle.v1"
MAX_MEMBER = 32 * 1024 * 1024
MAX_TOTAL = 256 * 1024 * 1024
MAX_ENTRIES = 20000
HASH = re.compile(r"^[0-9a-f]{64}$")
REVISION = re.compile(r"^[0-9a-f]{40,64}$")
MODES = {"100644", "100755"}
class PackageError(ValueError):
pass
def _hash(body):
return hashlib.sha256(body).hexdigest()
def _json(body):
return json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
def _path(value):
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value or ":" in value:
raise PackageError("unsafe package path")
parts = value.split("/")
if any(part in {"", ".", "..", ".git"} for part in parts) or PurePosixPath(value).is_absolute():
raise PackageError(f"unsafe package path: {value!r}")
return value
def _absolute(value):
path = Path(os.path.abspath(value))
for current in (path, *path.parents):
if current.is_symlink():
raise PackageError(f"symlink refused: {current}")
return path
def _member(root, relative):
return _absolute(root / _path(relative))
def _git(repo, *arguments, text=False):
command = ["git", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-C", str(repo), *arguments]
try:
result = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True, check=True)
except subprocess.CalledProcessError as error:
raise PackageError(f"Git precondition failed: {error.stderr.decode('utf-8', 'replace')[:500]}") from error
return result.stdout.decode("utf-8").strip() if text else result.stdout
def _base_tree(repo, revision):
actual = _git(repo, "rev-parse", "--verify", "--end-of-options", f"{revision}^{{commit}}", text=True)
if not REVISION.fullmatch(actual):
raise PackageError("invalid baseline revision")
files, bodies = {}, {}
for entry in _git(repo, "ls-tree", "-rz", "--full-tree", actual).split(b"\x00"):
if not entry:
continue
header, raw_path = entry.split(b"\t", 1)
mode, kind, object_id = header.decode().split(" ")
path = _path(raw_path.decode("utf-8"))
if mode not in MODES or kind != "blob":
raise PackageError(f"symlink or non-regular Git entry refused: {path}")
body = _git(repo, "cat-file", "blob", object_id)
if len(body) > MAX_MEMBER:
raise PackageError(f"oversized source member: {path}")
files[path] = {"sha256": _hash(body), "mode": mode}
bodies[path] = body
return actual, files, bodies
def _read_file(path):
path = _absolute(path)
if not path.exists():
return None, None
file_stat = path.stat()
if not stat.S_ISREG(file_stat.st_mode) or file_stat.st_size > MAX_MEMBER:
raise PackageError(f"non-regular or oversized source file: {path}")
return path.read_bytes(), "100755" if file_stat.st_mode & 0o111 else "100644"
def create_bundle(repo, base_revision, output):
repo, output = _absolute(repo), _absolute(output)
if output.exists():
raise PackageError("output already exists")
if repo in output.parents:
raise PackageError("bundle output must be outside source repository")
revision, base_files, base_bodies = _base_tree(repo, base_revision)
paths = set(base_files)
for raw in _git(repo, "ls-files", "-z", "--cached", "--others", "--exclude-standard").split(b"\x00"):
if raw:
paths.add(_path(raw.decode("utf-8")))
changes, payloads = [], {}
for path in sorted(paths):
body, mode = _read_file(_member(repo, path))
before = base_files.get(path)
after = {"sha256": _hash(body), "mode": mode} if body is not None else None
if before == after:
continue
if before is None and after is None:
continue
action = "add" if before is None else "delete" if after is None else "modify"
changes.append({"path": path, "action": action,
"before_sha256": before["sha256"] if before else None,
"before_mode": before["mode"] if before else None,
"after_sha256": after["sha256"] if after else None,
"after_mode": after["mode"] if after else None})
if before:
payloads["old/" + path] = base_bodies[path]
if after:
payloads["new/" + path] = body
if not changes:
raise PackageError("source has no changes relative to baseline")
manifest = {"schema": SCHEMA, "base_revision": revision, "base_files": base_files,
"changes": changes, "payload_sha256": {name: _hash(body) for name, body in sorted(payloads.items())}}
payloads["manifest.json"] = _json(manifest)
if len(payloads) > MAX_ENTRIES or sum(map(len, payloads.values())) > MAX_TOTAL:
raise PackageError("package exceeds supported size")
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=output.parent, prefix=".aios-bundle-", delete=False) as temporary:
temporary_path = Path(temporary.name)
try:
with zipfile.ZipFile(temporary_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name, body in sorted(payloads.items()):
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = (stat.S_IFREG | 0o600) << 16
archive.writestr(info, body)
verify_bundle(temporary_path)
# Hard-link publication is atomic and cannot overwrite an existing file.
os.link(temporary_path, output)
finally:
temporary_path.unlink(missing_ok=True)
return {"status": "CREATED", "bundle": str(output), "sha256": _hash(output.read_bytes()),
"base_revision": revision, "changes": len(changes)}
def _load_bundle(filename):
filename = _absolute(filename)
try:
with zipfile.ZipFile(filename) as archive:
entries = archive.infolist()
if len(entries) > MAX_ENTRIES or sum(item.file_size for item in entries) > MAX_TOTAL:
raise PackageError("package exceeds supported size")
bodies = {}
for entry in entries:
name = _path(entry.filename)
kind = stat.S_IFMT(entry.external_attr >> 16)
if name in bodies or entry.is_dir() or kind not in {0, stat.S_IFREG} or entry.flag_bits & 1:
raise PackageError("duplicate, encrypted, directory or symlink ZIP entry")
if entry.file_size > MAX_MEMBER:
raise PackageError("oversized ZIP entry")
bodies[name] = archive.read(entry)
document = json.loads(bodies.pop("manifest.json"))
except (OSError, zipfile.BadZipFile, KeyError, json.JSONDecodeError, UnicodeDecodeError) as error:
raise PackageError(f"invalid bundle: {type(error).__name__}") from error
expected_fields = {"schema", "base_revision", "base_files", "changes", "payload_sha256"}
if not isinstance(document, dict) or set(document) != expected_fields or document["schema"] != SCHEMA:
raise PackageError("unsupported manifest schema or fields")
if not isinstance(document["base_revision"], str) or not REVISION.fullmatch(document["base_revision"]):
raise PackageError("invalid baseline revision")
base, changes, declared = document["base_files"], document["changes"], document["payload_sha256"]
if not isinstance(base, dict) or not isinstance(changes, list) or not changes or not isinstance(declared, dict):
raise PackageError("invalid manifest collections")
for path, item in base.items():
_path(path)
if not isinstance(item, dict) or set(item) != {"sha256", "mode"} or not isinstance(item["mode"], str) or item["mode"] not in MODES or not isinstance(item["sha256"], str) or not HASH.fullmatch(item["sha256"]):
raise PackageError("invalid baseline file identity")
required_payloads, seen = {}, set()
for change in changes:
if not isinstance(change, dict) or set(change) != {"path", "action", "before_sha256", "before_mode", "after_sha256", "after_mode"}:
raise PackageError("invalid change fields")
path = _path(change["path"])
if path in seen:
raise PackageError("duplicate change path")
seen.add(path)
action = change["action"]
if not isinstance(action, str) or action not in {"add", "modify", "delete"}:
raise PackageError("invalid change action")
before = base.get(path)
if action == "add":
if before is not None or change["before_sha256"] is not None or change["before_mode"] is not None:
raise PackageError("add conflicts with baseline")
elif before != {"sha256": change["before_sha256"], "mode": change["before_mode"]}:
raise PackageError("before identity differs from baseline")
for prefix, present in (("before", action != "add"), ("after", action != "delete")):
sha, mode = change[prefix + "_sha256"], change[prefix + "_mode"]
if present:
if not isinstance(sha, str) or not HASH.fullmatch(sha) or not isinstance(mode, str) or mode not in MODES:
raise PackageError("invalid change content identity")
required_payloads[("old/" if prefix == "before" else "new/") + path] = sha
elif sha is not None or mode is not None:
raise PackageError("deleted or absent content must have null identity")
if declared != required_payloads or set(bodies) != set(required_payloads):
raise PackageError("unexpected or missing payload")
for name, sha in required_payloads.items():
if _hash(bodies[name]) != sha:
raise PackageError(f"payload checksum mismatch: {name}")
return document, bodies
def verify_bundle(filename):
document, _ = _load_bundle(filename)
return {"status": "VERIFIED_BYTES", "base_revision": document["base_revision"],
"changes": len(document["changes"]), "sha256": _hash(Path(filename).read_bytes()),
"authorization": "NOT_ESTABLISHED_BY_CHECKSUM"}
def _check_clean_base(source, document):
if _git(source, "status", "--porcelain", "--untracked-files=all", text=True):
raise PackageError("source repository must be clean, including untracked files")
if _git(source, "rev-parse", "HEAD", text=True) != document["base_revision"]:
raise PackageError("source HEAD differs from package baseline")
revision, files, _ = _base_tree(source, document["base_revision"])
if files != document["base_files"]:
raise PackageError("manifest baseline differs from source Git tree")
for path, item in files.items():
body, mode = _read_file(_member(source, path))
if body is None or _hash(body) != item["sha256"] or mode != item["mode"]:
raise PackageError(f"baseline working file differs: {path}")
def _write(root, path, body, mode):
target = _member(root, path)
target.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=target.parent, prefix=".aios-write-", delete=False) as temporary:
temporary.write(body)
temporary.flush()
os.fsync(temporary.fileno())
temporary_path = Path(temporary.name)
try:
temporary_path.chmod(0o755 if mode == "100755" else 0o644)
os.replace(temporary_path, target)
finally:
temporary_path.unlink(missing_ok=True)
def stage_bundle(filename, source, destination, *, dry_run=False):
filename, source, destination = _absolute(filename), _absolute(source), _absolute(destination)
document, bodies = _load_bundle(filename)
if destination.exists():
raise PackageError("destination already exists; use a new isolated directory")
if source == destination or source in destination.parents or destination in source.parents:
raise PackageError("source and destination must be separate trees")
if not destination.parent.is_dir():
raise PackageError("destination parent must already exist")
_check_clean_base(source, document)
result = {"status": "DRY_RUN" if dry_run else "STAGED", "source": str(source),
"destination": str(destination), "changes": len(document["changes"]),
"services_activated": False}
if dry_run:
return result
lock = destination.parent / ("." + destination.name + ".aios-stage.lock")
try:
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError as error:
raise PackageError("another stage operation holds destination lock") from error
os.close(descriptor)
temporary = Path(tempfile.mkdtemp(dir=destination.parent, prefix=".aios-stage-"))
try:
# Clone metadata without checkout: no repository content is executed and no
# checkout/smudge filters are used. Materialize verified Git blobs ourselves.
_git(destination.parent, "clone", "--quiet", "--no-checkout", "--no-local", "--no-hardlinks", str(source), str(temporary))
_git(temporary, "symbolic-ref", "HEAD", "refs/heads/baseline-1.1-staged")
_git(temporary, "update-ref", "HEAD", document["base_revision"])
_git(temporary, "read-tree", document["base_revision"])
_, _, base_bodies = _base_tree(source, document["base_revision"])
for path, body in base_bodies.items():
_write(temporary, path, body, document["base_files"][path]["mode"])
for change in document["changes"]:
path = change["path"]
if change["action"] == "delete":
_remove(temporary, path)
for change in document["changes"]:
path = change["path"]
if change["action"] != "delete":
_write(temporary, path, bodies["new/" + path], change["after_mode"])
metadata = temporary / ".git" / "aios-baseline-1.1"
metadata.mkdir(mode=0o700)
shutil.copyfile(filename, metadata / "bundle.zip")
receipt = {"schema": "aios.source-stage-receipt.v1", "state": "STAGED",
"bundle_sha256": _hash(filename.read_bytes()), "base_revision": document["base_revision"]}
(metadata / "receipt.json").write_bytes(_json(receipt))
if destination.exists() or destination.is_symlink():
raise PackageError("destination appeared during staging")
os.rename(temporary, destination)
finally:
if temporary.exists():
shutil.rmtree(temporary)
lock.unlink(missing_ok=True)
return result
def _remove(root, relative):
target = _member(root, relative)
target.unlink()
parent = target.parent
while parent != root:
try:
parent.rmdir()
except OSError:
break
parent = parent.parent
def rollback(directory, *, dry_run=False):
directory = _absolute(directory)
metadata = _absolute(directory / ".git" / "aios-baseline-1.1")
try:
receipt = json.loads(_absolute(metadata / "receipt.json").read_bytes())
except (OSError, json.JSONDecodeError) as error:
raise PackageError("no valid stage receipt") from error
if not isinstance(receipt, dict) or receipt.get("schema") != "aios.source-stage-receipt.v1" or receipt.get("state") != "STAGED":
raise PackageError("installation is not in STAGED state")
filename = _absolute(metadata / "bundle.zip")
document, bodies = _load_bundle(filename)
if _hash(filename.read_bytes()) != receipt.get("bundle_sha256"):
raise PackageError("stored bundle differs from stage receipt")
if _git(directory, "rev-parse", "HEAD", text=True) != document["base_revision"]:
raise PackageError("Git HEAD changed after staging; preserve local commits")
if _git(directory, "diff", "--cached", "--name-only", "-z"):
raise PackageError("Git index changed after staging; preserve local staged changes")
# Complete preflight before the first write: preserve all local post-stage edits.
for change in document["changes"]:
body, mode = _read_file(_member(directory, change["path"]))
expected = change["after_sha256"]
if (body is None) != (expected is None) or (body is not None and (_hash(body) != expected or mode != change["after_mode"])):
raise PackageError(f"staged file changed; rollback refused: {change['path']}")
if dry_run:
return {"status": "DRY_RUN", "directory": str(directory), "changes": len(document["changes"])}
for change in document["changes"]:
path = change["path"]
if change["action"] == "add":
_remove(directory, path)
for change in document["changes"]:
path = change["path"]
if change["action"] != "add":
_write(directory, path, bodies["old/" + path], change["before_mode"])
receipt["state"] = "ROLLED_BACK"
_write(metadata, "receipt.json", _json(receipt), "100644")
return {"status": "ROLLED_BACK", "directory": str(directory), "services_activated": False}
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
create = commands.add_parser("create", help="build source changes from a local Git baseline")
create.add_argument("--repo", required=True)
create.add_argument("--base", required=True)
create.add_argument("--output", required=True)
verify = commands.add_parser("verify", help="read-only byte and manifest verification")
verify.add_argument("bundle")
stage = commands.add_parser("stage", help="create a new isolated staged copy; never alter source")
stage.add_argument("bundle")
stage.add_argument("--source", required=True)
stage.add_argument("--destination", required=True)
stage.add_argument("--dry-run", action="store_true")
undo = commands.add_parser("rollback", help="restore staged source only if it is unchanged")
undo.add_argument("--directory", required=True)
undo.add_argument("--dry-run", action="store_true")
arguments = parser.parse_args(argv)
try:
if arguments.command == "create":
result = create_bundle(arguments.repo, arguments.base, arguments.output)
elif arguments.command == "verify":
result = verify_bundle(arguments.bundle)
elif arguments.command == "stage":
result = stage_bundle(arguments.bundle, arguments.source, arguments.destination, dry_run=arguments.dry_run)
else:
result = rollback(arguments.directory, dry_run=arguments.dry_run)
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
return 0
except (PackageError, OSError, ValueError) as error:
print(json.dumps({"status": "REFUSED", "reason": str(error)}, ensure_ascii=False, sort_keys=True))
return 2
if __name__ == "__main__":
raise SystemExit(main())
+10
View File
@@ -0,0 +1,10 @@
# Tested offline wheel set: CPython 3.12, Linux x86_64, glibc >= 2.34.
cffi==1.17.1 --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93
cryptography==46.0.0 --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0
iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
pycparser==2.23 --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
Pygments==2.21.0 --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9
pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
PyYAML==6.0.3 --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Run local acceptance and record actual exit status; never activates a service."""
from __future__ import annotations
import argparse
from datetime import datetime,timezone
import hashlib
import importlib.metadata
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--profile',choices=('baseline','all'),default='baseline')
parser.add_argument('--output',type=Path,required=True)
args=parser.parse_args()
root=Path(__file__).resolve().parents[2]
output=args.output.absolute()
output.mkdir(parents=True,exist_ok=True)
source={}
for path in sorted(root.rglob('*.py')):
if not path.is_symlink() and not {'.git','__pycache__','.venv','execution'}.intersection(path.relative_to(root).parts):
source[str(path.relative_to(root))]=hashlib.sha256(path.read_bytes()).hexdigest()
started=datetime.now(timezone.utc).isoformat()
# Some historical tests write to relative runtime/ paths. Exercise an exact
# disposable source copy so verification never dirties the delivered source.
with tempfile.TemporaryDirectory(prefix='aios-acceptance-') as temporary:
workspace=Path(temporary)/'source'
shutil.copytree(root,workspace,ignore=shutil.ignore_patterns('.git','__pycache__','.pytest_cache'))
# Offline acceptance never inherits an ambient opt-in to a deployed signer.
(workspace/'aios_offline_test_gate.py').write_text(
'import pytest\n'
'def pytest_collection_modifyitems(items):\n'
' for item in items:\n'
' if item.nodeid.endswith("::test_real_signer_help_and_self_test_are_available"):\n'
' item.add_marker(pytest.mark.skip(reason="offline acceptance: deployed signer requires separate site run"))\n')
command=[sys.executable,'-m','pytest','-q','tests/baseline_v1_1' if args.profile=='baseline' else 'tests',
'-p','aios_offline_test_gate','--junitxml='+str(output/'junit.xml')]
environment=os.environ.copy()
environment['PYTHONPATH']=os.pathsep.join((str(workspace),str(workspace/'active-runtime/app')))
environment['PYTHONDONTWRITEBYTECODE']='1'
environment['AIOS_GITOPS_LIVE_GITEA_TEST']='0'
environment['AIOS_RUN_REAL_SIGNER_TESTS']='0'
with (output/'pytest.log').open('w') as log:
completed=subprocess.run(command,cwd=workspace,env=environment,stdout=log,stderr=subprocess.STDOUT)
counts={'tests':0,'failures':0,'errors':0,'skipped':0}
if (output/'junit.xml').exists():
tree=ET.parse(output/'junit.xml').getroot()
for suite in tree.iter('testsuite'):
for key in counts: counts[key]+=int(suite.attrib.get(key,'0'))
result={'schema':'aios.local-acceptance-report.v1','profile':args.profile,
'started_at':started,'finished_at':datetime.now(timezone.utc).isoformat(),
'python':platform.python_version(),'platform':platform.platform(),
'dependencies':{name:importlib.metadata.version(name) for name in ('pytest','cryptography','cffi')},
'command':command,'exit_code':completed.returncode,'counts':counts,
'junit_counts_include_subtests':True,
'test_workspace_isolated':True,
'status':'LOCAL_TESTS_PASSED' if completed.returncode==0 and counts['tests']>0 else 'FAILED',
'live_activation':'NOT_PERFORMED','source_files_sha256':source}
(output/'result.json').write_text(json.dumps(result,ensure_ascii=False,sort_keys=True,indent=2)+'\n')
print(json.dumps({key:result[key] for key in ('status','exit_code','counts','live_activation')}))
return completed.returncode if counts['tests'] else 2
if __name__=='__main__': raise SystemExit(main())