feat(automation): enforce assessment scope and document activation blockers
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""Read-only verification of a trusted, source-bound assessment receipt; no services."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def verify(repository, receipt_path, expected_sha256):
|
||||
repository = Path(repository).resolve()
|
||||
raw = Path(receipt_path).read_bytes()
|
||||
if len(raw) > 8 * 1024 * 1024 or hashlib.sha256(raw).hexdigest() != expected_sha256:
|
||||
raise ValueError('Assessment receipt differs from the protected manifest pin')
|
||||
receipt = json.loads(raw)
|
||||
if (receipt.get('schemaVersion') != 1 or receipt.get('scope') != 'P10-00-ASSESSMENT-ONLY'
|
||||
or receipt.get('integrity') != 'PASS' or not receipt.get('files')
|
||||
or not receipt.get('nativeReports') or not receipt.get('retainedDecisions')):
|
||||
raise ValueError('Incomplete assessment qualification receipt')
|
||||
for name, expected in receipt['files'].items():
|
||||
path = (repository / name).resolve()
|
||||
if not path.is_relative_to(repository) or not path.is_file():
|
||||
raise ValueError('Frozen assessment source missing or outside repository: ' + name)
|
||||
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
||||
raise ValueError('Frozen assessment source changed; fresh affected qualification required: ' + name)
|
||||
evidence_root = Path(receipt_path).resolve().parent
|
||||
for name, expected in receipt.get('evidenceFiles', {}).items():
|
||||
path = (evidence_root / name).resolve()
|
||||
if not path.is_relative_to(evidence_root) or not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
||||
raise ValueError('Accepted handoff evidence changed or missing: ' + name)
|
||||
return dict(kind='REUSED_SOURCE_BOUND_EVIDENCE', receiptSha256=expected_sha256,
|
||||
nativeReports=receipt['nativeReports'],
|
||||
retainedDecisions=receipt['retainedDecisions'],
|
||||
limits='P10-00 assessment only. No fresh product runs or implementation qualification.')
|
||||
@@ -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()
|
||||
|
||||
@@ -61,8 +61,17 @@ def validate_config(root, config):
|
||||
|
||||
def filesystem_rules(root, config):
|
||||
repo = Path(config['repository']).resolve()
|
||||
rules = {':root':'deny', ':minimal':'read', repo.as_posix():'write',
|
||||
assessment = config.get('assessmentReportDirectory')
|
||||
rules = {':root':'deny', ':minimal':'read', repo.as_posix():'read' if assessment else 'write',
|
||||
Path(config['workerTemp']).resolve().as_posix():'write'}
|
||||
if assessment:
|
||||
if assessment != 'docs/automation/stages/PHASE-10':
|
||||
raise ValueError('Invalid assessment report directory')
|
||||
for name in (assessment, '.artifacts/orchestrator-probes'):
|
||||
path = (repo/name).resolve()
|
||||
if not path.is_relative_to(repo):
|
||||
raise ValueError('Assessment output escapes repository')
|
||||
rules[path.as_posix()] = 'write'
|
||||
for name in config.get('toolReadRoots',[]):
|
||||
path = Path(name).resolve()
|
||||
if path == Path(path.anchor) or path == Path.home():
|
||||
@@ -94,7 +103,7 @@ def permission_args(root, config, command):
|
||||
|
||||
def binding(root,config):
|
||||
value = {k:v for k,v in config.items() if k != 'executionPolicy'}
|
||||
for name in ('local_mode.py','controller.py','Probe-JobjaktSandbox.ps1'):
|
||||
for name in ('local_mode.py','controller.py','assessment_adapter.py','Probe-JobjaktSandbox.ps1'):
|
||||
source = Path(root)/name
|
||||
if source.exists():
|
||||
value[name+'Sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Trusted operator preparation. Validates published native evidence; runs no product suite.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import zlib from 'node:zlib';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const [repository, destination] = process.argv.slice(2);
|
||||
if (!repository || !destination) throw new Error('Usage: node prepare_assessment.mjs <published-handoff-checkout> <new-output-directory>');
|
||||
const root = fs.realpathSync(repository), output = path.resolve(destination);
|
||||
if (fs.existsSync(output)) throw new Error('Use a new receipt directory; preserve previous qualification');
|
||||
const git = (...args) => execFileSync('rtk', ['proxy','git','-C',root,...args], {encoding:'utf8'}).trim();
|
||||
const handoff = '5cb903b989742b7a96cb22ddf4c0e24993b18980';
|
||||
const baseline = 'fc5b9d377199ce51f480a920a3772108094929e3';
|
||||
if (git('rev-parse','HEAD') !== handoff || git('status','--porcelain')) throw new Error('Require the exact clean published handoff checkout');
|
||||
const remote = git('ls-remote','--exit-code','origin','refs/heads/test/pre-phase10-ux-acceptance');
|
||||
if (remote.split(/\s/)[0] !== handoff) throw new Error('Handoff publication differs');
|
||||
if (git('rev-parse',handoff+'^') !== baseline) throw new Error('Baseline relationship differs');
|
||||
const relative = 'docs/automation/handoffs/pre-phase10';
|
||||
const directory = path.join(root,relative);
|
||||
const {validatePackage, measureNative} = await import(pathToFileURL(path.join(directory,'verify.mjs')).href);
|
||||
const verified = validatePackage();
|
||||
if (verified.integrity !== 'PASS' || !verified.observedWorkingTreeClean) throw new Error('Handoff integrity failed');
|
||||
const evidence = JSON.parse(fs.readFileSync(path.join(directory,'TEST_EVIDENCE.json')));
|
||||
const artifact = id => {
|
||||
const row=evidence.artifacts.find(item=>item.id===id);
|
||||
if (!row) throw new Error('Missing native artifact');
|
||||
const raw=fs.readFileSync(path.join(directory,row.path));
|
||||
return row.path.endsWith('.gz') ? zlib.gunzipSync(raw) : raw;
|
||||
};
|
||||
const hash = raw => crypto.createHash('sha256').update(raw).digest('hex');
|
||||
const files = {};
|
||||
// Freeze all tracked non-envelope sources, including tests/config/security/preservation documentation.
|
||||
for (const name of git('ls-files','-z').split('\0').filter(Boolean)) {
|
||||
if (!name.startsWith(relative+'/')) files[name]=hash(fs.readFileSync(path.join(root,name)));
|
||||
}
|
||||
const nativeReports = evidence.reports.map(report=>{
|
||||
let observation=JSON.parse(artifact(report.processArtifactId));
|
||||
const parts=report.processPointer.slice(1).split('/').map(value=>value.replaceAll('~1','/').replaceAll('~0','~'));
|
||||
for (const key of parts.slice(0,-1)) observation=observation[key];
|
||||
return {...report, measured:report.nativeFormat ? measureNative(report.nativeFormat,artifact(report.artifactId).toString('utf8')) : null,
|
||||
nativeProcess:observation};
|
||||
});
|
||||
fs.mkdirSync(output,{recursive:true});
|
||||
const evidenceFiles={};
|
||||
for (const name of git('ls-files',relative).split(/\r?\n/).filter(Boolean)) {
|
||||
const local=path.relative(directory,path.join(root,name));
|
||||
const target=path.join(output,'handoff',local);
|
||||
fs.mkdirSync(path.dirname(target),{recursive:true});
|
||||
fs.copyFileSync(path.join(root,name),target);
|
||||
evidenceFiles['handoff/'+local.split(path.sep).join('/')]=hash(fs.readFileSync(target));
|
||||
}
|
||||
const receipt={schemaVersion:1,scope:'P10-00-ASSESSMENT-ONLY',integrity:'PASS',
|
||||
generatedUtc:new Date().toISOString(),baselineCommit:baseline,handoffCommit:handoff,
|
||||
testedCommit:evidence.testedCommit,files,evidenceFiles,nativeReports,
|
||||
retainedDecisions:JSON.parse(fs.readFileSync(path.join(directory,'ACCEPTANCE.json'))).verdicts,
|
||||
revisedS01:verified.revisedS01,verifierObservation:verified,
|
||||
limits:'Source-bound reuse for unchanged read-only assessment. Original failures and conditional/blocked decisions retained. No fresh product, security, model, human or production acceptance.'};
|
||||
fs.writeFileSync(path.join(output,'receipt.json'),JSON.stringify(receipt,null,2)+'\n');
|
||||
console.log(JSON.stringify({integrity:'PASS',receipt:path.join(output,'receipt.json'),sha256:hash(fs.readFileSync(path.join(output,'receipt.json'))),frozenSources:Object.keys(files).length,nativeReports:nativeReports.length,productSuitesExecuted:0},null,2));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
"""Synthetic native assessment qualification only. Never starts the development queue."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import uuid
|
||||
import controller as c
|
||||
import local_mode
|
||||
|
||||
|
||||
def main():
|
||||
root, worker, report_path = map(Path,sys.argv[1:])
|
||||
config=c.read_json(root/'config.json')
|
||||
policy=c.read_json(root/'proposed-p10-assessment.json')
|
||||
config['repository']=str(worker.resolve())
|
||||
engine=c.Engine(root,policy,config)
|
||||
if c.read_json(root/'backlog.json')['executionEnabled'] or any(s['sessions'] for s in engine.store.read()['stages'].values()):
|
||||
raise c.GateError('Qualification requires an unused disabled queue')
|
||||
with c.RepoLock(worker):
|
||||
prior=(root/'qualification.json').read_bytes() if (root/'qualification.json').exists() else None
|
||||
try:
|
||||
native=local_mode.qualify(engine)
|
||||
finally:
|
||||
if prior is not None:
|
||||
(root/'qualification.json').write_bytes(prior)
|
||||
else:
|
||||
(root/'qualification.json').unlink(missing_ok=True)
|
||||
if native['status'] != 'PASS':
|
||||
c.atomic(report_path,dict(native=native,actualQueueStarted=False,assessment='NOT_RUN'))
|
||||
raise c.GateError('Native signing isolation qualification failed; no further child launched')
|
||||
token=uuid.uuid4().hex
|
||||
probe=worker/'.artifacts/orchestrator-probes'/token
|
||||
probe.mkdir(parents=True)
|
||||
canary=worker/('.assessment-readonly-'+token)
|
||||
canary.write_text('SYNTHETIC READ-ONLY CANARY')
|
||||
output=worker/'docs/automation/stages/PHASE-10'/('native-probe-'+token+'.md')
|
||||
output.parent.mkdir(parents=True,exist_ok=True)
|
||||
logs=root/'probe'/('assessment-'+token)
|
||||
receipt=root/'assessment-activation-01'
|
||||
try:
|
||||
shutil.copytree(receipt,probe/'receipt')
|
||||
shutil.copyfile(Path(__file__).with_name('assessment_adapter.py'),probe/'assessment_adapter.py')
|
||||
plan=dict(worker=str(worker.resolve()),receipt=str(probe/'receipt/receipt.json'),
|
||||
pin=c.sha_file(receipt/'receipt.json'),canary=str(canary),output=str(output))
|
||||
c.atomic(probe/'plan.json',plan)
|
||||
(probe/'run.py').write_text('''import json, pathlib
|
||||
import assessment_adapter
|
||||
p=json.loads(pathlib.Path(__file__).with_name('plan.json').read_text())
|
||||
r=assessment_adapter.verify(p['worker'],p['receipt'],p['pin'])
|
||||
canary=pathlib.Path(p['canary'])
|
||||
assert canary.read_text()=='SYNTHETIC READ-ONLY CANARY'
|
||||
try:
|
||||
with canary.open('r+b'): pass
|
||||
except PermissionError: write_denied=True
|
||||
else: write_denied=False
|
||||
try: canary.unlink()
|
||||
except PermissionError: delete_denied=True
|
||||
else: delete_denied=False
|
||||
pathlib.Path(p['output']).write_text('Synthetic native report creation')
|
||||
assert write_denied and delete_denied
|
||||
print(json.dumps(dict(evidenceIntegrity='PASS',sourceWriteDenied=write_denied,sourceDeleteDenied=delete_denied,reportWrite=True,kind=r['kind'],nativeReports=len(r['nativeReports']))))
|
||||
''',encoding='utf-8')
|
||||
argv=[config['codexExecutable'],'sandbox','-C',str(worker),
|
||||
*local_mode.permission_args(root,engine.config,'sandbox'),
|
||||
config['pythonExecutable'],'-B',str(probe/'run.py')]
|
||||
engine.runner.supervise(engine,argv,logs,180)
|
||||
observation=json.loads((logs/'stdout.log').read_text())
|
||||
c.atomic(report_path,dict(native=native,assessment=observation,
|
||||
candidateCommit=engine.git('rev-parse','HEAD'),actualQueueStarted=False,
|
||||
compositeRuntime='BLOCKED_DOCKER_AND_SERVICE_BOUNDARY_UNQUALIFIED',logs=str(logs)))
|
||||
finally:
|
||||
canary.unlink(missing_ok=True)
|
||||
output.unlink(missing_ok=True)
|
||||
if probe.resolve().is_relative_to(worker.resolve()/'.artifacts/orchestrator-probes'):
|
||||
shutil.rmtree(probe)
|
||||
print('Synthetic native assessment checks complete; composite command runtime remains BLOCKED')
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
{"actualQueueStarted":false,"assessment":{"evidenceIntegrity":"PASS","kind":"REUSED_SOURCE_BOUND_EVIDENCE","nativeReports":34,"reportWrite":true,"sourceDeleteDenied":true,"sourceWriteDenied":true},"bindingMatchesProjectedProfile":true,"candidateCommit":"fc5b9d377199ce51f480a920a3772108094929e3","compositeRuntime":"BLOCKED_DOCKER_AND_SERVICE_BOUNDARY_UNQUALIFIED","logs":"C:\\Users\\Connor\\.jobjakt-backlog\\single-user-local\\probe\\assessment-b2c85d9a53474a7db172c73969586734","native":{"binding":"3b1ec3040285e5991dac5fc5e60cb362f7ea54691b9f790a6a496aadc12ec5db","failure":null,"issuedUtc":"2026-10-30T08:35:40.581635+00:00","ownerKeyDenied":true,"probes":{"files":[{"category":"approvals","readDenied":true,"writeDenied":true},{"category":"configuration","readDenied":true,"writeDenied":true},{"category":"key-file","readDenied":true,"writeDenied":true},{"category":"normal-codex","readDenied":true,"writeDenied":true},{"category":"other-repository","readDenied":true,"writeDenied":true},{"category":"personal","readDenied":true,"writeDenied":true},{"category":"production-canary","readDenied":true,"writeDenied":true}],"networkDenied":true,"ownerKeyDenied":true,"positiveControl":true,"signingKeyDenied":true},"scope":"Synthetic filesystem/network/signing canaries; real owner key probed","status":"PASS"}}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"activeProcessMarker":false,"actualSessions":0,"baselineCommit":"fc5b9d377199ce51f480a920a3772108094929e3","executionEnabled":false,"executionPolicy":"operator-controlled","executionWorktree":"C:\\Users\\Connor\\.codex\\worktrees\\p10-assessment\\Job tracker","handoffCommit":"5cb903b989742b7a96cb22ddf4c0e24993b18980","installedRepository":"C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker","installedSourceMatches":true,"materialisedLineEndingOnlyFiles":47,"nativeQualification":{"binding":"b1bdcfd929af4829d316481c376e8c92d1b3182b61199080d2f915279f51ec7f","failure":null,"issuedUtc":"2026-10-30T08:35:24.011490+00:00","ownerKeyDenied":true,"probes":{"files":[{"category":"approvals","readDenied":true,"writeDenied":true},{"category":"configuration","readDenied":true,"writeDenied":true},{"category":"key-file","readDenied":true,"writeDenied":true},{"category":"normal-codex","readDenied":true,"writeDenied":true},{"category":"other-repository","readDenied":true,"writeDenied":true},{"category":"personal","readDenied":true,"writeDenied":true},{"category":"production-canary","readDenied":true,"writeDenied":true}],"networkDenied":true,"ownerKeyDenied":true,"positiveControl":true,"signingKeyDenied":true},"scope":"Synthetic filesystem/network/signing canaries; real owner key probed","status":"PASS"},"ownerKeyConfigured":true,"pending":[{"kind":"manifest-policy-update","request":"C:\\Users\\Connor\\.jobjakt-backlog\\single-user-local\\requests\\d637caf72c3fed5910f7c28e75dc82324d8febc12597ddb5627ae022af49a622.json"}],"publicCertificatePinMatches":true,"qualificationBindingMatches":true,"recoveryRequired":false}
|
||||
@@ -0,0 +1 @@
|
||||
{"exitCode":2,"modelSessionsAfter":0,"modelSessionsBefore":0,"stderr":"Adopt the signed restricted assessment policy first\n","stdout":""}
|
||||
@@ -0,0 +1 @@
|
||||
{"clean":true,"executionBranch":"codex/p10-assessment-ready","executionCommit":"fc5b9d377199ce51f480a920a3772108094929e3","handoffChanges":["docs/automation/handoffs/pre-phase10/ACCEPTANCE.json","docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL.json","docs/automation/handoffs/pre-phase10/TEST_EVIDENCE.json","docs/automation/handoffs/pre-phase10/evidence/p10-entry-proposal.gz","docs/automation/handoffs/pre-phase10/evidence/p10-final-gate-processes.gz","docs/automation/handoffs/pre-phase10/evidence/p10-proposal-check-source.gz","docs/automation/handoffs/pre-phase10/evidence/p10-protected-entry-native.gz"],"handoffParent":"fc5b9d377199ce51f480a920a3772108094929e3","policyFileMatchesInstalledProposal":true,"proposedManifestSha256":"0ff54dadbae7dde08e68c247e1365fd54f063e5b09066cb9e9cd577afe11a061","remoteHeads":["140e182516e86c8718054cf0907593fcd6a38aa7\trefs/heads/codex/autonomous-roadmap-single-user","5cb903b989742b7a96cb22ddf4c0e24993b18980\trefs/heads/test/pre-phase10-ux-acceptance"]}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"tests": 81,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0,
|
||||
"sourceStable": true,
|
||||
"sourceHashes": {
|
||||
"automation/assessment_adapter.py": "6f6b4e7b96f1e9ca896e1d2a9ab26c12bf38d603dbf087acf61969f5d3906a21",
|
||||
"automation/controller.py": "f2c05a3bccb43cc81f76e35e7adefd2e590934ae99f2a1fdc9206844e8732d88",
|
||||
"automation/local_mode.py": "ccf4a5e8c208d17d87b47dd1aca8ebf540af27a274ae7333da9fb8075cb9b5b2",
|
||||
"automation/prepare_queue.py": "6cad22305bc354ccbae2f031cb2b324596b3a7d42503b954c27f1e63e47a5f7d",
|
||||
"automation/qualify_assessment.py": "0891520943779da0ff722d8deba62151d5c1dfad1329026310baab216459374d",
|
||||
"automation/prepare_assessment.mjs": "a13f3d0458d7e4006cdfc07c3de2d563a6795a8cd4421be2b7de36fb70d68c10",
|
||||
"automation/tests/test_assessment.py": "d3d30aaa808ead6927a444c1f91aa583835d3a787893b8f67db6b68261aa243c",
|
||||
"automation/tests/test_controller.py": "458da3c60571e61aad42987ce156842efd73ef0e2aa0c7bce719d36d626d7e7d",
|
||||
"automation/tests/test_local_mode.py": "524d40ede6379138f729c06876e43b6ef1cb15ee427947f2358a0439cb02ff69",
|
||||
"scripts/Run-JobjaktBacklog.ps1": "2b11f5016560890262f0ca96bdba5a13240b0250c6bf6a96b43c33929cbcd070"
|
||||
},
|
||||
"elapsedSeconds": 161.283,
|
||||
"scope": "Synthetic controller and adapter tests; no actual backlog or product suite"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"constraints": "PASS",
|
||||
"metaSchemaValidation": "NOT_PERFORMED_DRAFT_2020_META_SCHEMA_UNAVAILABLE",
|
||||
"errors": null
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
test_assessment_profile_keeps_product_read_only (test_assessment.ReadOnlyProfileTests.test_assessment_profile_keeps_product_read_only) ... ok
|
||||
test_receipt_rejects_changed_product_or_changed_receipt (test_assessment.ReceiptTests.test_receipt_rejects_changed_product_or_changed_receipt) ... ok
|
||||
test_assessment_reaches_protected_stop_and_cannot_advance (test_assessment.ScopeTests.test_assessment_reaches_protected_stop_and_cannot_advance) ... ok
|
||||
test_assessment_reports_are_permitted (test_assessment.ScopeTests.test_assessment_reports_are_permitted) ... ok
|
||||
test_entry_cannot_prepare_approvals_without_composite_adapter (test_assessment.ScopeTests.test_entry_cannot_prepare_approvals_without_composite_adapter) ... ok
|
||||
test_evidence_inspection_cannot_be_marked_runtime_qualified (test_assessment.ScopeTests.test_evidence_inspection_cannot_be_marked_runtime_qualified) ... ok
|
||||
test_generic_approval_cannot_launch_next_checkpoint (test_assessment.ScopeTests.test_generic_approval_cannot_launch_next_checkpoint) ... ok
|
||||
test_inspection_requires_explicit_false_and_never_launches_worker (test_assessment.ScopeTests.test_inspection_requires_explicit_false_and_never_launches_worker) ... ok
|
||||
test_native_isolation_failure_stops_before_additional_probe (test_assessment.ScopeTests.test_native_isolation_failure_stops_before_additional_probe) ... ok
|
||||
test_policy_cannot_relabel_p10_01_as_assessment (test_assessment.ScopeTests.test_policy_cannot_relabel_p10_01_as_assessment) ... ok
|
||||
test_scope_rejects_product_write_and_deletion (test_assessment.ScopeTests.test_scope_rejects_product_write_and_deletion) ... ok
|
||||
test_scope_requires_protected_stop (test_assessment.ScopeTests.test_scope_requires_protected_stop) ... ok
|
||||
test_accidental_main_checkout_rejected (test_controller.EngineTests.test_accidental_main_checkout_rejected) ... ok
|
||||
test_after_approval_binds_tested_commit_without_rerunning_worker (test_controller.EngineTests.test_after_approval_binds_tested_commit_without_rerunning_worker) ... ok
|
||||
test_changed_prompt_blocks (test_controller.EngineTests.test_changed_prompt_blocks) ... ok
|
||||
test_conditional_can_advance_only_with_explicit_signed_deferral (test_controller.EngineTests.test_conditional_can_advance_only_with_explicit_signed_deferral) ... ok
|
||||
test_conditional_requires_explicit_deferrable_gates (test_controller.EngineTests.test_conditional_requires_explicit_deferrable_gates) ... ok
|
||||
test_corrupt_state_recovers_last_good_without_success (test_controller.EngineTests.test_corrupt_state_recovers_last_good_without_success) ... ok
|
||||
test_cryptographically_approved_resume_e2e (test_controller.EngineTests.test_cryptographically_approved_resume_e2e) ... ok
|
||||
test_dependency_cannot_be_bypassed (test_controller.EngineTests.test_dependency_cannot_be_bypassed) ... ok
|
||||
test_design_revision_reruns_current_checkpoint_and_keeps_owner_selection (test_controller.EngineTests.test_design_revision_reruns_current_checkpoint_and_keeps_owner_selection) ... ok
|
||||
test_disabled_manifest_never_executes (test_controller.EngineTests.test_disabled_manifest_never_executes) ... ok
|
||||
test_double_corruption_fails_closed (test_controller.EngineTests.test_double_corruption_fails_closed) ... ok
|
||||
test_duplicate_lock_rejected (test_controller.EngineTests.test_duplicate_lock_rejected) ... ok
|
||||
test_execution_window_stops_before_worker (test_controller.EngineTests.test_execution_window_stops_before_worker) ... ok
|
||||
test_failed_stage_stops_and_exhausts_retry_budget (test_controller.EngineTests.test_failed_stage_stops_and_exhausts_retry_budget) ... ok
|
||||
test_failing_checks_never_commit_or_advance (test_controller.EngineTests.test_failing_checks_never_commit_or_advance) ... ok
|
||||
test_interruption_is_not_success_and_resume_has_budget (test_controller.EngineTests.test_interruption_is_not_success_and_resume_has_budget) ... ok
|
||||
test_manifest_initially_disabled (test_controller.EngineTests.test_manifest_initially_disabled) ... ok
|
||||
test_missing_approval_does_not_run_model (test_controller.EngineTests.test_missing_approval_does_not_run_model) ... ok
|
||||
test_missing_prompt_blocks (test_controller.EngineTests.test_missing_prompt_blocks) ... ok
|
||||
test_model_cannot_approve_itself (test_controller.EngineTests.test_model_cannot_approve_itself) ... ok
|
||||
test_pause_persists_and_resume_clears_it (test_controller.EngineTests.test_pause_persists_and_resume_clears_it) ... ok
|
||||
test_push_failure_preserves_tested_commit (test_controller.EngineTests.test_push_failure_preserves_tested_commit) ... ok
|
||||
test_push_verification_mismatch_stops (test_controller.EngineTests.test_push_verification_mismatch_stops) ... ok
|
||||
test_recovered_running_without_marker_requires_reconciliation (test_controller.EngineTests.test_recovered_running_without_marker_requires_reconciliation) ... ok
|
||||
test_restart_running_marker_cannot_mean_completed (test_controller.EngineTests.test_restart_running_marker_cannot_mean_completed) ... ok
|
||||
test_same_repo_other_control_root_cannot_take_lock (test_controller.EngineTests.test_same_repo_other_control_root_cannot_take_lock) ... ok
|
||||
test_success_orders_stages_and_verifies_remote (test_controller.EngineTests.test_success_orders_stages_and_verifies_remote) ... ok
|
||||
test_tampered_evidence_cannot_be_approved (test_controller.EngineTests.test_tampered_evidence_cannot_be_approved) ... ok
|
||||
test_unavailable_remote_preserves_commit_and_retries_publication_only (test_controller.EngineTests.test_unavailable_remote_preserves_commit_and_retries_publication_only) ... ok
|
||||
test_unconfigured_test_adapter_blocks_before_model (test_controller.EngineTests.test_unconfigured_test_adapter_blocks_before_model) ... ok
|
||||
test_unsigned_recovery_cannot_clear_running_guard (test_controller.EngineTests.test_unsigned_recovery_cannot_clear_running_guard) ... ok
|
||||
test_forward_dependency_rejected (test_controller.QueueTests.test_forward_dependency_rejected) ... ok
|
||||
test_production_branches_rejected (test_controller.QueueTests.test_production_branches_rejected) ... ok
|
||||
test_real_queue_is_ordered_and_missing_sources_are_explicit (test_controller.QueueTests.test_real_queue_is_ordered_and_missing_sources_are_explicit) ... ok
|
||||
test_empty_report_is_not_a_pass (test_controller.ReportTests.test_empty_report_is_not_a_pass) ... ok
|
||||
test_json_log_redaction_preserves_event_structure (test_controller.ReportTests.test_json_log_redaction_preserves_event_structure) ... ok
|
||||
test_path_escape_rejected (test_controller.ReportTests.test_path_escape_rejected) ... ok
|
||||
test_playwright_retry_and_expected_failure_are_not_pass (test_controller.ReportTests.test_playwright_retry_and_expected_failure_are_not_pass) ... ok
|
||||
test_redacts_common_credentials (test_controller.ReportTests.test_redacts_common_credentials) ... ok
|
||||
test_report_membership_and_skip_enforcement (test_controller.ReportTests.test_report_membership_and_skip_enforcement) ... ok
|
||||
test_cli_nonzero_exit_never_accepts (test_controller.SubprocessTests.test_cli_nonzero_exit_never_accepts) ... ok
|
||||
test_cli_protocol_real_process_and_complete_prompt (test_controller.SubprocessTests.test_cli_protocol_real_process_and_complete_prompt) ... ok
|
||||
test_killed_controller_terminates_worker_and_grandchild (test_controller.SubprocessTests.test_killed_controller_terminates_worker_and_grandchild) ... ok
|
||||
test_marker_write_failure_does_not_leave_worker (test_controller.SubprocessTests.test_marker_write_failure_does_not_leave_worker) ... ok
|
||||
test_no_newline_output_obeys_log_budget (test_controller.SubprocessTests.test_no_newline_output_obeys_log_budget) ... ok
|
||||
test_prompt_stall_still_obeys_timeout (test_controller.SubprocessTests.test_prompt_stall_still_obeys_timeout) ... ok
|
||||
test_real_check_report_must_match_expected_membership (test_controller.SubprocessTests.test_real_check_report_must_match_expected_membership) ... ok
|
||||
test_worker_cannot_execute_before_job_assignment (test_controller.SubprocessTests.test_worker_cannot_execute_before_job_assignment) ... ok
|
||||
test_pending_recovery_can_be_signed_from_idle_operator_terminal (test_local_mode.LocalFlowTests.test_pending_recovery_can_be_signed_from_idle_operator_terminal) ... ok
|
||||
test_single_user_concurrent_controller_and_interrupted_guard (test_local_mode.LocalFlowTests.test_single_user_concurrent_controller_and_interrupted_guard) ... ok
|
||||
test_single_user_main_checkout_is_still_prohibited (test_local_mode.LocalFlowTests.test_single_user_main_checkout_is_still_prohibited) ... ok
|
||||
test_single_user_missing_approval_stops_before_worker (test_local_mode.LocalFlowTests.test_single_user_missing_approval_stops_before_worker) ... ok
|
||||
test_single_user_pause_and_publication_recovery (test_local_mode.LocalFlowTests.test_single_user_pause_and_publication_recovery) ... ok
|
||||
test_single_user_subprocess_protocol_and_ordered_publication (test_local_mode.LocalFlowTests.test_single_user_subprocess_protocol_and_ordered_publication) ... ok
|
||||
test_auth_home_inside_worker_is_rejected (test_local_mode.LocalPolicyTests.test_auth_home_inside_worker_is_rejected) ... ok
|
||||
test_blocked_status_rejects_otherwise_successful_probes (test_local_mode.LocalPolicyTests.test_blocked_status_rejects_otherwise_successful_probes) ... ok
|
||||
test_different_or_missing_sid_rejected (test_local_mode.LocalPolicyTests.test_different_or_missing_sid_rejected) ... ok
|
||||
test_existing_normal_auth_home_is_rejected (test_local_mode.LocalPolicyTests.test_existing_normal_auth_home_is_rejected) ... ok
|
||||
test_failed_probe_and_unconfigured_owner_key_cannot_qualify (test_local_mode.LocalPolicyTests.test_failed_probe_and_unconfigured_owner_key_cannot_qualify) ... ok
|
||||
test_missing_or_stale_qualification_never_enables_unattended (test_local_mode.LocalPolicyTests.test_missing_or_stale_qualification_never_enables_unattended) ... ok
|
||||
test_mxc_is_explicit_without_legacy_or_unrestricted_fallback (test_local_mode.LocalPolicyTests.test_mxc_is_explicit_without_legacy_or_unrestricted_fallback) ... ok
|
||||
test_noninteractive_approval_is_refused (test_local_mode.LocalPolicyTests.test_noninteractive_approval_is_refused) ... ok
|
||||
test_operator_controlled_policy_cannot_launch_a_worker (test_local_mode.LocalPolicyTests.test_operator_controlled_policy_cannot_launch_a_worker) ... ok
|
||||
test_plugins_are_not_an_implicit_same_user_capability (test_local_mode.LocalPolicyTests.test_plugins_are_not_an_implicit_same_user_capability) ... ok
|
||||
test_probe_summary_rejects_partial_and_failed_boundaries (test_local_mode.LocalPolicyTests.test_probe_summary_rejects_partial_and_failed_boundaries) ... ok
|
||||
test_profile_denies_root_control_auth_and_normal_configuration (test_local_mode.LocalPolicyTests.test_profile_denies_root_control_auth_and_normal_configuration) ... ok
|
||||
test_same_chatgpt_auth_is_explicit_and_tokens_are_not_copied (test_local_mode.LocalPolicyTests.test_same_chatgpt_auth_is_explicit_and_tokens_are_not_copied) ... ok
|
||||
test_same_windows_identity_supported (test_local_mode.LocalPolicyTests.test_same_windows_identity_supported) ... ok
|
||||
test_tool_or_temp_roots_cannot_reopen_protected_parent (test_local_mode.LocalPolicyTests.test_tool_or_temp_roots_cannot_reopen_protected_parent) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 81 tests in 161.136s
|
||||
|
||||
OK
|
||||
@@ -0,0 +1 @@
|
||||
{"observations":[{"status":"BLOCKED","stderr":"Failed to resolve full path of the current executable [C:\\Users\\Connor\\.dotnet\\dotnet.exe]\n","stdout":"","tool":"accepted-dotnet"},{"status":"BLOCKED","stderr":"No pyvenv.cfg file\n","stdout":"","tool":"accepted-docling-python"},{"stderr":"","stdout":"v24.19.0\n","tool":"node"},{"stderr":"","stdout":"\nHost:\n Version: 9.0.20\n Architecture: x64\n Commit: 3879076d9a\n RID: win-x64\n\n.NET SDKs installed:\n No SDKs were found.\n\n.NET runtimes installed:\n Microsoft.NETCore.App 8.0.30 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App]\n Microsoft.NETCore.App 9.0.20 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App]\n Microsoft.WindowsDesktop.App 8.0.30 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]\n Microsoft.WindowsDesktop.App 9.0.20 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]\n\nOther architectures found:\n None\n\nEnvironment variables:\n Not set\n\nglobal.json file:\n C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker\\global.json\n\nLearn more:\n https://aka.ms/dotnet/info\n\nDownload .NET:\n https://aka.ms/dotnet/download\n","tool":"dotnet"},{"stderr":"WARNING: Error loading config file: open C:\\Users\\Connor\\.docker\\config.json: Access is denied.\npermission denied while trying to connect to the docker API at npipe:////./pipe/docker_engine\n","stdout":"\n","tool":"docker"}],"profile":"Existing native MXC, no permissions widened","realProductSuitesExecuted":0}
|
||||
@@ -11,6 +11,17 @@
|
||||
"baselineBranch": {"$ref": "#/$defs/branch"},
|
||||
"remote": {"type": "string", "pattern": "^[A-Za-z0-9_-]+$"},
|
||||
"integrationBranch": {"$ref": "#/$defs/branch"},
|
||||
"executionScope": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["stage", "checkpoint", "mode", "reportDirectory", "handoffCommit", "handoffBranch"],
|
||||
"properties": {
|
||||
"stage": {"const": "PHASE-10"}, "checkpoint": {"const": "P10-00"},
|
||||
"mode": {"const": "assessment-only"},
|
||||
"reportDirectory": {"const": "docs/automation/stages/PHASE-10"},
|
||||
"handoffCommit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
"handoffBranch": {"$ref": "#/$defs/branch"}
|
||||
}
|
||||
},
|
||||
"stages": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/stage"}}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -56,11 +67,16 @@
|
||||
"check": {
|
||||
"type": "object",
|
||||
"required": ["id", "argv", "cwd", "report"],
|
||||
"if": {"required": ["adapter"], "properties": {"adapter": {"const": "assessment-evidence-inspection"}}},
|
||||
"then": {"required": ["configured"], "properties": {"configured": {"const": false}}},
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9-]+$"},
|
||||
"argv": {"type": "array", "minItems": 1, "items": {"type": "string"}},
|
||||
"cwd": {"type": "string"},
|
||||
"configured": {"type": "boolean"},
|
||||
"adapter": {"enum": ["assessment-evidence-inspection"]},
|
||||
"receipt": {"type": "string"},
|
||||
"receiptSha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"report": {"type": ["object", "null"], "properties": {
|
||||
"format": {"enum": ["trx", "jest", "playwright", "junit"]},
|
||||
"path": {"type": "string"},
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Synthetic scope tests. No product backlog, model, signing or services."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import controller as c
|
||||
import local_mode
|
||||
import test_controller as fixture
|
||||
|
||||
|
||||
def scope():
|
||||
return dict(stage='PHASE-10', checkpoint='P10-00', mode='assessment-only',
|
||||
reportDirectory='docs/automation/stages/PHASE-10',
|
||||
handoffCommit='1'*40, handoffBranch='test/handoff')
|
||||
|
||||
|
||||
class ScopeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
fixture.EngineTests.setUp(self)
|
||||
first=self.manifest['stages'][0]
|
||||
first['id']='PHASE-10'
|
||||
first['checkpoints'][0]['id']='P10-00'
|
||||
self.manifest['stages'][1]['dependencies']=['PHASE-10']
|
||||
state=self.engine.store.read()
|
||||
state['stages']['PHASE-10']=state['stages'].pop('ONE')
|
||||
state['manifestSha256']=c.digest(self.manifest)
|
||||
self.engine.store.write(state)
|
||||
tearDown = fixture.EngineTests.tearDown
|
||||
git_at = staticmethod(fixture.EngineTests.git_at)
|
||||
|
||||
def test_scope_requires_protected_stop(self):
|
||||
self.manifest['executionScope'] = scope()
|
||||
with self.assertRaisesRegex(c.GateError, 'protected stop'):
|
||||
c.validate_manifest(self.manifest)
|
||||
|
||||
def test_scope_rejects_product_write_and_deletion(self):
|
||||
self.manifest['executionScope'] = scope()
|
||||
(self.repo/'README').write_text('unauthorised')
|
||||
with self.assertRaisesRegex(c.GateError, 'assessment'):
|
||||
self.engine.verify_assessment_writes()
|
||||
(self.repo/'README').unlink()
|
||||
with self.assertRaises(c.GateError):
|
||||
self.engine.verify_assessment_writes()
|
||||
|
||||
def test_assessment_reports_are_permitted(self):
|
||||
self.manifest['executionScope'] = scope()
|
||||
output=self.repo/scope()['reportDirectory']/'P10-00.md'
|
||||
output.parent.mkdir(parents=True)
|
||||
output.write_text('Synthetic assessment')
|
||||
self.engine.verify_assessment_writes()
|
||||
|
||||
def test_generic_approval_cannot_launch_next_checkpoint(self):
|
||||
first=self.manifest['stages'][0]
|
||||
first['checkpoints'].append(copy.deepcopy(first['checkpoints'][0]))
|
||||
first['checkpoints'][1]['id']='implementation'
|
||||
self.manifest['executionScope']=scope()
|
||||
state=self.engine.store.read()
|
||||
state['manifestSha256']=c.digest(self.manifest)
|
||||
state['stages']['PHASE-10']['checkpoint']=1
|
||||
self.engine.store.write(state)
|
||||
self.engine.run()
|
||||
self.assertEqual(0,self.runner.calls)
|
||||
self.assertIn('assessment', self.engine.store.read()['latestFailure'])
|
||||
|
||||
def test_assessment_reaches_protected_stop_and_cannot_advance(self):
|
||||
output='docs/automation/stages/PHASE-10/P10-00.md'
|
||||
class ReportRunner(fixture.SyntheticRunner):
|
||||
def run_session(inner, engine, stage, checkpoint, directory, prompt):
|
||||
inner.calls += 1
|
||||
report=engine.repo/output
|
||||
report.parent.mkdir(parents=True,exist_ok=True)
|
||||
report.write_text('Synthetic assessment only')
|
||||
return dict(outcome='complete',summary='synthetic',blockers=[],requestedApprovals=[],checkpoint=checkpoint['id'])
|
||||
first=self.manifest['stages'][0]
|
||||
cp=first['checkpoints'][0]
|
||||
cp['requiredArtifacts']=[output]
|
||||
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
|
||||
first['checkpoints'].append(copy.deepcopy(cp))
|
||||
first['checkpoints'][1]['id']='implementation'
|
||||
self.manifest['executionScope']={**scope(),'handoffCommit':self.manifest['baselineCommit'],'handoffBranch':'codex/base'}
|
||||
state=self.engine.store.read()
|
||||
state['manifestSha256']=c.digest(self.manifest)
|
||||
self.engine.store.write(state)
|
||||
self.engine.runner=ReportRunner()
|
||||
self.engine.approval_verifier=lambda request: request['kind']!='candidate-specific-cleanup-scope'
|
||||
self.engine.run()
|
||||
self.assertEqual(1,self.engine.runner.calls)
|
||||
self.assertEqual('candidate-specific-cleanup-scope', self.engine.store.read()['stages']['PHASE-10']['awaiting'][0]['kind'])
|
||||
self.engine.approval_verifier=lambda request: True
|
||||
self.engine.run()
|
||||
self.assertEqual(1,self.engine.runner.calls)
|
||||
self.assertIn('assessment',self.engine.store.read()['latestFailure'])
|
||||
|
||||
def test_policy_cannot_relabel_p10_01_as_assessment(self):
|
||||
self.manifest['executionScope']={**scope(),'checkpoint':'P10-01'}
|
||||
cp=self.manifest['stages'][0]['checkpoints'][0]
|
||||
cp['id']='P10-01'
|
||||
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
|
||||
with self.assertRaisesRegex(c.GateError,'protected stop'):
|
||||
c.validate_manifest(self.manifest)
|
||||
|
||||
def test_evidence_inspection_cannot_be_marked_runtime_qualified(self):
|
||||
self.manifest['executionScope']=scope()
|
||||
cp=self.manifest['stages'][0]['checkpoints'][0]
|
||||
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
|
||||
cp['checks'][0].update(adapter='assessment-evidence-inspection',configured=True,receiptSha256='a'*64)
|
||||
with self.assertRaisesRegex(c.GateError,'cannot qualify'):
|
||||
c.validate_manifest(self.manifest)
|
||||
|
||||
def test_inspection_requires_explicit_false_and_never_launches_worker(self):
|
||||
self.manifest['executionScope']=scope()
|
||||
cp=self.manifest['stages'][0]['checkpoints'][0]
|
||||
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
|
||||
cp['checks'][0].update(adapter='assessment-evidence-inspection',receiptSha256='a'*64)
|
||||
cp['checks'][0].pop('configured',None)
|
||||
with self.assertRaisesRegex(c.GateError,'cannot qualify'):
|
||||
c.validate_manifest(self.manifest)
|
||||
state=self.engine.store.read()
|
||||
with self.assertRaisesRegex(c.GateError,'adapters must be configured'):
|
||||
self.engine.checkpoint(self.manifest['stages'][0],cp,state['stages']['PHASE-10'],state)
|
||||
self.assertEqual(0,self.runner.calls)
|
||||
|
||||
def test_native_isolation_failure_stops_before_additional_probe(self):
|
||||
import qualify_assessment
|
||||
policy=copy.deepcopy(self.manifest)
|
||||
policy['executionEnabled']=False
|
||||
c.atomic(self.control/'backlog.json',policy)
|
||||
c.atomic(self.control/'proposed-p10-assessment.json',policy)
|
||||
c.atomic(self.control/'config.json',self.config)
|
||||
report=self.root/'failed-native.json'
|
||||
with mock.patch.object(qualify_assessment.local_mode,'qualify',return_value={'status':'FAIL'}), \
|
||||
mock.patch.object(sys,'argv',['qualify',str(self.control),str(self.repo),str(report)]):
|
||||
with self.assertRaisesRegex(c.GateError,'no further child'):
|
||||
qualify_assessment.main()
|
||||
self.assertEqual('NOT_RUN',c.read_json(report)['assessment'])
|
||||
self.assertFalse((self.repo/'.artifacts').exists())
|
||||
|
||||
def test_entry_cannot_prepare_approvals_without_composite_adapter(self):
|
||||
self.manifest['executionScope']={**scope(),'handoffCommit':self.manifest['baselineCommit'],'handoffBranch':'codex/base'}
|
||||
cp=self.manifest['stages'][0]['checkpoints'][0]
|
||||
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
|
||||
cp['checks']=[]
|
||||
c.atomic(self.control/'backlog.json',self.manifest)
|
||||
c.atomic(self.control/'config.json',self.config)
|
||||
with mock.patch.object(c,'Engine',return_value=self.engine), \
|
||||
mock.patch.object(local_mode,'require_unattended'):
|
||||
with self.assertRaisesRegex(c.GateError,'composite adapter is not qualified'):
|
||||
c.main(['prepare-entry','--control-root',str(self.control)])
|
||||
self.assertEqual(0,self.runner.calls)
|
||||
self.assertFalse((self.control/'requests').exists())
|
||||
|
||||
|
||||
class ReadOnlyProfileTests(unittest.TestCase):
|
||||
def test_assessment_profile_keeps_product_read_only(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory)
|
||||
config=dict(repository=str(root/'worker'), workerTemp=str(root/'temp'),
|
||||
workerCodeXHome=str(root/'control/auth'), normalCodeXHome=str(root/'normal'),
|
||||
assessmentReportDirectory='docs/automation/stages/PHASE-10')
|
||||
rules=local_mode.filesystem_rules(root/'control',config)
|
||||
self.assertEqual('read',rules[(root/'worker').resolve().as_posix()])
|
||||
self.assertEqual('write',rules[(root/'worker/docs/automation/stages/PHASE-10').resolve().as_posix()])
|
||||
|
||||
|
||||
class ReceiptTests(unittest.TestCase):
|
||||
def test_receipt_rejects_changed_product_or_changed_receipt(self):
|
||||
import assessment_adapter as adapter
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
repo=Path(directory)
|
||||
(repo/'product.txt').write_text('accepted synthetic source')
|
||||
receipt=dict(schemaVersion=1, scope='P10-00-ASSESSMENT-ONLY',
|
||||
integrity='PASS', files={'product.txt':c.sha_file(repo/'product.txt')},
|
||||
nativeReports=[dict(id='synthetic',status='PASS',exitCode=0,
|
||||
membershipSha256='a'*64)], retainedDecisions={'production':'NO-GO'})
|
||||
path=repo/'receipt.json'
|
||||
c.atomic(path,receipt)
|
||||
pin=c.sha_file(path)
|
||||
self.assertEqual('REUSED_SOURCE_BOUND_EVIDENCE',adapter.verify(repo,path,pin)['kind'])
|
||||
(repo/'product.txt').write_text('changed')
|
||||
with self.assertRaises(ValueError):
|
||||
adapter.verify(repo,path,pin)
|
||||
with self.assertRaises(ValueError):
|
||||
adapter.verify(repo,path,'0'*64)
|
||||
Reference in New Issue
Block a user