feat(automation): enforce assessment scope and document activation blockers

This commit is contained in:
cesnimda
2026-10-30 09:40:12 +01:00
parent 140e182516
commit 750d76317c
22 changed files with 7065 additions and 12 deletions
+89 -8
View File
@@ -19,6 +19,7 @@ import time
import uuid
import xml.etree.ElementTree as ET
import local_mode
import assessment_adapter
class GateError(RuntimeError):
@@ -165,6 +166,13 @@ def validate_manifest(m, root=None):
if check['id'] in check_ids or not check.get('argv'):
raise GateError('Duplicate check or empty test command')
check_ids.add(check['id'])
if check.get('adapter') == 'assessment-evidence-inspection':
if not m.get('executionScope') or not re.fullmatch(r'[0-9a-f]{64}',check.get('receiptSha256','')):
raise GateError('Assessment adapter requires a restricted policy and pinned receipt')
if check.get('configured') is not False:
raise GateError('Evidence inspection cannot qualify the command-running composite adapter')
if root and sha_file(under(root,check['receipt'])) != check['receiptSha256']:
raise GateError('Assessment qualification receipt changed')
if not all(isinstance(a, str) and '\x00' not in a for a in check['argv']):
raise GateError('Command arguments must be strings')
if check.get('report') and check['report']['format'] not in ('trx', 'jest', 'playwright', 'junit'):
@@ -180,6 +188,17 @@ def validate_manifest(m, root=None):
raise GateError('Supplemental prompt missing/changed')
if sequences != sorted(set(sequences)):
raise GateError('Queue sequence must be unique and ascending')
scope = m.get('executionScope')
if scope:
stage = next((s for s in m['stages'] if s['id'] == scope.get('stage')), None)
cp = next((x for x in stage['checkpoints'] if x['id'] == scope.get('checkpoint')), None) if stage else None
if (scope.get('mode') != 'assessment-only' or scope.get('stage') != 'PHASE-10'
or scope.get('checkpoint') != 'P10-00' or not cp
or scope.get('reportDirectory') != 'docs/automation/stages/PHASE-10'
or 'candidate-specific-cleanup-scope' not in cp['approvalsAfter']):
raise GateError('Assessment policy requires an exact checkpoint and protected stop')
if not re.fullmatch(r'[0-9a-f]{40}', scope.get('handoffCommit','')) or not SAFE_BRANCH.fullmatch(scope.get('handoffBranch','')):
raise GateError('Invalid published handoff identity')
class StateStore:
@@ -553,6 +572,8 @@ class ProcessRunner:
return data
def run_check(self, engine, check, directory):
if check.get('adapter') == 'assessment-evidence-inspection':
raise GateError('Evidence inspection cannot replace command-running composite qualification')
if local_mode.single(engine.config):
local_mode.require_unattended(engine.root,engine.config)
working = engine.repo if check['cwd'] == '.' else under(engine.repo, check['cwd'])
@@ -581,6 +602,9 @@ class Engine:
self.root = Path(root).resolve()
self.manifest = manifest
self.config = config
if manifest.get('executionScope'):
self.config = dict(config)
self.config['assessmentReportDirectory'] = manifest['executionScope']['reportDirectory']
self.repo = Path(config['repository']).resolve()
self.store = StateStore(self.root)
self.runner = runner or ProcessRunner()
@@ -621,6 +645,8 @@ class Engine:
if kind in ('environment-qualified', 'roadmap-execution'):
request.update(stage='QUEUE', checkpoint='POLICY', baselineCommit=self.manifest['baselineCommit'],
candidateCommit=None, evidence=[], detail='Entire exact manifest; development only; no production authority')
if self.manifest.get('executionScope'):
request['executionScope'] = self.manifest['executionScope']
path = self.root / 'requests' / (digest(request) + '.json')
atomic(path, request)
approval = self.approval_verifier(request)
@@ -656,7 +682,7 @@ class Engine:
return True
def policy_hash(self):
files = ['controller.py', 'local_mode.py', 'Probe-JobjaktSandbox.ps1',
files = ['controller.py', 'local_mode.py', 'assessment_adapter.py', 'Probe-JobjaktSandbox.ps1',
'Approve-JobjaktBacklog.ps1', 'config.json', 'schemas/result.schema.json']
return digest({name: sha_file(self.root / name) for name in files if (self.root / name).exists()})
@@ -683,6 +709,8 @@ class Engine:
_,sid = local_mode.detect_identity()
local_mode.validate_identity(self.config,sid)
local_mode.require_unattended(self.root,self.config)
if self.manifest.get('executionScope') and not local_mode.single(self.config):
raise GateError('Restricted assessment filesystem profile currently qualifies single-user-local only')
certificate = under(self.root, self.config['publicKeyCertificate'])
if not certificate.is_file() or sha_file(certificate) != self.config['ownerCertificateSha256']:
raise GateError('Owner public certificate is missing or differs from protected configuration pin')
@@ -800,9 +828,17 @@ class Engine:
if ss['entryBaseline'] is None:
if self.git('status', '--porcelain', '--untracked-files=all'):
raise GateError('New stage requires a clean worktree, including untracked files')
self.verify_remote(self.manifest['baselineBranch'] if not any(
x['status'] == 'COMPLETED' for x in state['stages'].values())
else self.manifest['integrationBranch'], state['integrationCommit'])
scope = self.manifest.get('executionScope')
if scope and not any(x['status'] == 'COMPLETED' for x in state['stages'].values()):
self.verify_remote(scope['handoffBranch'], scope['handoffCommit'])
self.git('merge-base', '--is-ancestor', state['integrationCommit'], scope['handoffCommit'])
changed = self.git('diff','--name-only',state['integrationCommit'],scope['handoffCommit']).splitlines()
if any(not name.startswith('docs/automation/handoffs/pre-phase10/') for name in changed):
raise GateError('Published handoff changed outside the evidence envelope')
else:
self.verify_remote(self.manifest['baselineBranch'] if not any(
x['status'] == 'COMPLETED' for x in state['stages'].values())
else self.manifest['integrationBranch'], state['integrationCommit'])
baseline = state['integrationCommit']
branches = self.git('for-each-ref', '--format=%(refname:short)', 'refs/heads').splitlines()
if stage['branch'] in branches:
@@ -923,7 +959,8 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
return False
ss.pop('requestedDecision')
prompt = self.prompt(stage, cp, ss, state)
if not cp['checks'] or any(not check.get('configured', True) for check in cp['checks']):
if not cp['checks'] or any(check.get('adapter') == 'assessment-evidence-inspection'
or not check.get('configured', True) for check in cp['checks']):
raise GateError('Independent test adapters must be configured and owner-qualified before execution')
ss['attempts'] += 1
ss['sessions'] += 1
@@ -937,6 +974,7 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
sourceCommit=ss['checkpointBase'], manifestSha256=digest(self.manifest), at=utc()))
self.store.write(state) # durable RUNNING before launching child
result = self.runner.run_session(self, stage, cp, directory, prompt)
self.verify_assessment_writes()
ss['lastResult'] = result
state['recentSessions'].append(dict(id=session_id, stage=stage['id'], checkpoint=cp['id'],
outcome=result['outcome'], summary=redact(result['summary'])[:600], directory=str(directory)))
@@ -963,6 +1001,7 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
ss['verifiedChecks'] = []
for check in cp['checks']:
ss['verifiedChecks'].append(self.runner.run_check(self, check, directory / 'checks' / check['id']))
self.verify_assessment_writes()
ss['evidence'] = []
for name in cp['requiredArtifacts']:
artifact = under(self.repo, name)
@@ -998,6 +1037,17 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
self.store.write(state)
return self.finish_checkpoint(stage, cp, ss, state)
def verify_assessment_writes(self):
scope = self.manifest.get('executionScope')
if not scope:
return
prefix = scope['reportDirectory'] + '/'
changed = self.git('diff', '--name-only', 'HEAD', '-z').split('\0')
changed += self.git('ls-files', '--others', '--exclude-standard', '-z').split('\0')
deleted = self.git('diff', '--name-only', '--diff-filter=D', 'HEAD')
if deleted or any(name and (not name.startswith(prefix) or not name.endswith('.md')) for name in changed):
raise GateError('Read-only assessment modified a forbidden path or deleted a file')
def content_hash(self):
# Git blob hashes include tracked modifications and untracked source without reading secrets into logs.
files = self.git('ls-files', '--cached', '--others', '--exclude-standard', '-z').split('\0')
@@ -1086,6 +1136,9 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
self.store.write(state)
return
try:
scope = self.manifest.get('executionScope')
if scope and (stage['id'], cp['id']) != (scope['stage'], scope['checkpoint']):
raise GateError('Restricted assessment scope ends after P10-00; new owner-scoped policy required')
# Check source before requesting approval or modifying Git.
self.prompt(stage, cp, ss, state)
if ss['entryBaseline'] is None:
@@ -1179,7 +1232,7 @@ def install(source, root, repository, mode='dedicated-runner'):
if root.is_relative_to(repo) or repo.is_relative_to(root):
raise GateError('Install control plane outside worker checkout')
root.mkdir(parents=True)
for name in ('controller.py', 'local_mode.py', 'backlog.json', 'config.example.json'):
for name in ('controller.py', 'local_mode.py', 'assessment_adapter.py', 'backlog.json', 'config.example.json'):
shutil.copyfile(source / name, root / name)
for name in ('schemas', 'prompts'):
shutil.copytree(source / name, root / name)
@@ -1200,7 +1253,7 @@ def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command', choices=['init', 'validate', 'dry-run', 'start', 'resume', 'status',
'pause', 'retry', 'current', 'logs', 'recover', 'configure',
'setup','auth','auth-status','qualify','approve','prepare','key','enable-unattended'])
'setup','auth','auth-status','qualify','approve','prepare','prepare-entry','key','enable-unattended'])
parser.add_argument('--control-root', type=Path, required=True)
parser.add_argument('--repository')
parser.add_argument('--stage')
@@ -1218,7 +1271,7 @@ def main(argv=None):
and not read_json(root/'backlog.json')['executionEnabled']
and not any(s['sessions'] for s in state['stages'].values())):
with RepoLock(Path(existing['repository'])):
for name in ('controller.py','local_mode.py'):
for name in ('controller.py','local_mode.py','assessment_adapter.py'):
shutil.copyfile(source/name,root/name)
for name in ('Run-JobjaktBacklog.ps1','Approve-JobjaktBacklog.ps1','Probe-JobjaktSandbox.ps1',
'Use-JobjaktBacklog.ps1','Register-JobjaktBacklogTask.ps1'):
@@ -1326,6 +1379,34 @@ def main(argv=None):
'-ApprovalPath',str(root/'approvals'/request.name),'-Thumbprint',thumb,'-ExpectedOwnerSid',sid,
'-Disposition',disposition,'-Decision',decision])
return result.returncode
elif args.command == 'prepare-entry':
with RepoLock(engine.repo):
validate_manifest(engine.manifest,root)
state=engine.store.read()
scope=engine.manifest.get('executionScope')
if not scope or not engine.manifest['executionEnabled']:
raise GateError('Adopt the signed restricted assessment policy first')
if (root/'active-process.json').exists() or state['recoveryRequired'] or any(s['sessions'] for s in state['stages'].values()):
raise GateError('Entry preparation requires an unused reconciled queue')
local_mode.require_unattended(root,engine.config)
if engine.git('status','--porcelain','--untracked-files=all') or engine.git('rev-parse','HEAD') != engine.manifest['baselineCommit']:
raise GateError('Assessment entry requires a clean exact baseline checkout')
engine.verify_remote(scope['handoffBranch'],scope['handoffCommit'])
engine.git('merge-base','--is-ancestor',engine.manifest['baselineCommit'],scope['handoffCommit'])
stage=next(s for s in engine.manifest['stages'] if s['id']==scope['stage'])
cp=next(x for x in stage['checkpoints'] if x['id']==scope['checkpoint'])
engine.prompt(stage,cp,state['stages'][stage['id']],state)
if not any(check['id']=='qualified-provider-browser-python-pdf-security-preservation'
and check.get('configured') is True for check in cp['checks']):
raise GateError('P10-00 composite adapter is not qualified')
for check in cp['checks']:
if check.get('adapter') == 'assessment-evidence-inspection' or not check.get('configured'):
raise GateError('P10-00 composite adapter is not qualified')
for kind in ['environment-qualified','roadmap-execution',*stage['requiredOwnerApprovals'],*cp['approvalsBefore']]:
if not engine.request(stage,cp,kind,state):
print('Protected entry request prepared; approve it and run prepare-entry again. No worker launched.')
return 3
print('All exact restricted entry approvals verified. Owner may deliberately run start. No worker launched.')
elif args.command == 'prepare':
with RepoLock(engine.repo):
state = engine.store.read()