Files
jobtrackingapp/automation/controller.py
T

1514 lines
86 KiB
Python

"""Trusted control-plane source. Deploy outside the Codex-writable checkout before use."""
from __future__ import annotations
import argparse
import base64
import contextlib
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import uuid
import xml.etree.ElementTree as ET
import local_mode
import assessment_adapter
class GateError(RuntimeError):
pass
class PublicationError(GateError):
pass
class Interrupted(GateError):
pass
STATUSES = {'PENDING', 'READY', 'RUNNING', 'BLOCKED', 'AWAITING_APPROVAL', 'FAILED',
'CONDITIONALLY_ACCEPTED', 'ACCEPTED', 'COMPLETED'}
OUTCOMES = {'complete', 'checkpoint', 'blocked', 'approval', 'conditional', 'failed'}
SAFE_BRANCH = re.compile(r'^(?:codex|feature|planning|test)/[A-Za-z0-9][A-Za-z0-9/_-]*$')
def utc():
return datetime.now(timezone.utc).isoformat()
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8')
def digest(value):
return hashlib.sha256(canonical(value)).hexdigest()
def sha_file(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def under(root, name):
root = Path(root).resolve()
p = (root / name).resolve()
if not p.is_relative_to(root) or p == root:
raise GateError(f'Path must be a child of {root}: {name}')
# resolve() checks junction/symlink escape as well as traversal.
return p
def atomic(path, value):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix='.' + path.name, dir=path.parent)
try:
with os.fdopen(fd, 'wb') as stream:
stream.write(canonical(value))
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def read_json(path):
return json.loads(Path(path).read_text(encoding='utf-8-sig'))
def recorded_process_exists(marker):
pid=marker.get('pid')
if not isinstance(pid,int) or pid <= 0:
raise GateError('Launch intent has no valid PID; operator must inspect all runner children before clearing it')
p=subprocess.run(['tasklist','/FI',f'PID eq {pid}','/FO','CSV','/NH'],capture_output=True,text=True,timeout=15)
if p.returncode:
raise GateError('Could not verify recorded process termination')
return str(pid) in p.stdout
def redact(text):
if text.lstrip().startswith(('{', '[')):
try:
value = json.loads(text)
except ValueError:
value = None
if isinstance(value, (dict, list)):
def scrub(item):
if isinstance(item, dict):
return {key: '[REDACTED]' if re.fullmatch(
r'(?i)password|token|secret|api[_-]?key|authorization|access_token|refresh_token|id_token', key)
else scrub(child) for key, child in item.items()}
if isinstance(item, list):
return [scrub(child) for child in item]
return redact(item) if isinstance(item, str) else item
return json.dumps(scrub(value), ensure_ascii=False)
text = re.sub(r'(?i)(bearer\s+)\S+', r'\1[REDACTED]', text)
text = re.sub(r'(?i)((?:password|token|secret|api[_-]?key|authorization)\s*[=:]\s*)[^\s,;]+',
r'\1[REDACTED]', text)
text = re.sub(r'(https?://)[^/@\s]+:[^/@\s]+@', r'\1[REDACTED]@', text)
return text
def validate_manifest(m, root=None):
if m.get('schemaVersion') != 1 or not isinstance(m.get('executionEnabled'), bool):
raise GateError('Unsupported manifest or missing executionEnabled boolean')
if not SAFE_BRANCH.fullmatch(m.get('integrationBranch', '')):
raise GateError('Unsafe integration branch')
if not re.fullmatch(r'[0-9a-f]{40}', m.get('baselineCommit', '')):
raise GateError('Baseline must be an exact 40-character commit')
if not SAFE_BRANCH.fullmatch(m.get('baselineBranch', '')):
raise GateError('Unsafe baseline branch')
if not re.fullmatch(r'[A-Za-z0-9_-]+', m.get('remote', '')):
raise GateError('Remote must be a configured name, not a URL or option')
seen, sequences, branches = set(), [], set()
if not m.get('stages'):
raise GateError('Queue is empty')
for s in m['stages']:
required = {'id', 'sequence', 'sourcePrompt', 'promptSha256', 'sourceStatus', 'dependencies',
'branch', 'requiredOwnerApprovals', 'retryPolicy', 'status', 'evidence',
'reports', 'contextFiles', 'checkpoints'}
if not required.issubset(s) or s['id'] in seen or not re.fullmatch(r'[A-Z0-9-]+', s['id']):
raise GateError('Missing stage fields or duplicate/invalid workstream ID')
if not set(s['dependencies']).issubset(seen):
raise GateError(f'Forward, unknown or cyclic dependency: {s["id"]}')
if not SAFE_BRANCH.fullmatch(s['branch']) or '..' in s['branch'] or s['branch'] in branches:
raise GateError('Unsafe or duplicate stage branch')
if s['branch'] == m['integrationBranch'] or s['status'] not in STATUSES:
raise GateError('Invalid branch/status')
seen.add(s['id'])
branches.add(s['branch'])
sequences.append(s['sequence'])
p = s['retryPolicy']
if not (1 <= p['maxAttempts'] <= 10 and 1 <= p['maxSessions'] <= 100
and 0 <= p['backoffSeconds'] <= 3600):
raise GateError('Retry/session budget out of bounds')
checkpoints = set()
if not s['checkpoints']:
raise GateError('Stage has no checkpoints')
for cp in s['checkpoints']:
if cp['id'] in checkpoints or not re.fullmatch(r'[A-Za-z0-9-]+', cp['id']):
raise GateError('Duplicate/invalid checkpoint')
checkpoints.add(cp['id'])
for field in ('instruction', 'approvalsBefore', 'approvalsAfter', 'deferrableGates',
'requiredArtifacts', 'checks', 'evidenceRequirements'):
if field not in cp:
raise GateError(f'Missing checkpoint field: {field}')
check_ids = set()
for check in cp['checks']:
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'):
raise GateError('Unsupported structured test format')
if root and s['sourceStatus'] == 'VERIFIED':
path = under(root, s['sourcePrompt'])
if not path.is_file() or not path.stat().st_size or sha_file(path) != s['promptSha256']:
raise GateError(f'Prompt missing/changed: {s["id"]}')
if root:
for item in s.get('supplementalPrompts', []):
path = under(root, item['path'])
if not path.is_file() or sha_file(path) != item['sha256']:
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:
def __init__(self, root):
self.path = Path(root) / 'state.json'
self.backup = Path(root) / 'state.last-good.json'
def decode(self, path):
envelope = read_json(path)
if envelope['sha256'] != digest(envelope['state']):
raise ValueError('state checksum mismatch')
return envelope['state']
def read(self):
try:
return self.decode(self.path)
except (OSError, ValueError, KeyError, TypeError):
try:
state = self.decode(self.backup)
except (OSError, ValueError, KeyError, TypeError) as error:
raise GateError('State and last-good copy unreadable; restore verified backup, never reinitialize') from error
state['recoveryRequired'] = True
return state
def write(self, state):
if self.path.exists():
try:
previous = self.decode(self.path)
atomic(self.backup, {'state': previous, 'sha256': digest(previous)})
except (OSError, ValueError, KeyError, TypeError):
pass # keep the last verified backup, never copy corrupt bytes
state['updatedUtc'] = utc()
atomic(self.path, {'state': state, 'sha256': digest(state)})
if not self.backup.exists():
atomic(self.backup, {'state': state, 'sha256': digest(state)})
class RepoLock:
"""OS releases this lock on process death. Shared Git directory locks all worktrees."""
def __init__(self, repo):
p = subprocess.run(['git', '-C', str(repo), 'rev-parse', '--path-format=absolute', '--git-common-dir'],
capture_output=True, text=True)
if p.returncode:
raise GateError('Repository is not a Git checkout')
self.path = Path(p.stdout.strip()) / 'jobjakt-orchestrator.lock'
self.stream = None
def __enter__(self):
self.stream = open(self.path, 'a+b')
self.stream.seek(0)
self.stream.write(b'0')
self.stream.flush()
self.stream.seek(0)
try:
if os.name == 'nt':
import msvcrt
msvcrt.locking(self.stream.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl
fcntl.flock(self.stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as error:
self.stream.close()
raise GateError('Another orchestrator controls this repository') from error
return self
def __exit__(self, *args):
if self.stream:
self.stream.seek(0)
if os.name == 'nt':
import msvcrt
msvcrt.locking(self.stream.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(self.stream.fileno(), fcntl.LOCK_UN)
self.stream.close()
def parse_report(path, spec):
"""Count assertions independently, require membership, and reject skips/failures."""
fmt = spec['format']
names, passed, failed, skipped = [], 0, 0, 0
if fmt == 'trx':
tree = ET.parse(path)
for node in tree.iter():
if node.tag.split('}')[-1] == 'UnitTestResult':
names.append(node.attrib['testName'])
outcome = node.attrib.get('outcome')
passed += outcome == 'Passed'
failed += outcome not in ('Passed', 'NotExecuted')
skipped += outcome == 'NotExecuted'
elif fmt == 'junit':
for node in ET.parse(path).iter('testcase'):
names.append(node.attrib.get('classname', '') + '.' + node.attrib['name'])
bad = node.find('failure') is not None or node.find('error') is not None
skip = node.find('skipped') is not None
failed += bad
skipped += skip
passed += not (bad or skip)
elif fmt == 'jest':
data = read_json(path)
if data.get('success') is not True:
raise GateError('Jest success flag is false')
for suite in data['testResults']:
for case in suite['assertionResults']:
names.append(case['fullName'])
passed += case['status'] == 'passed'
failed += case['status'] == 'failed'
skipped += case['status'] not in ('passed', 'failed')
else:
data = read_json(path)
if data.get('errors'):
raise GateError('Playwright global errors')
def visit(suites):
nonlocal passed, failed, skipped
for suite in suites:
for test_spec in suite.get('specs', []):
for case in test_spec['tests']:
names.append(f'{test_spec.get("file", "")}:{test_spec["title"]}:{case["projectName"]}')
results = case.get('results', [])
ok = (len(results) == 1 and results[0]['status'] == 'passed'
and case.get('expectedStatus', 'passed') == 'passed')
passed += ok
failed += not ok
visit(suite.get('suites', []))
visit(data['suites'])
if not names or passed < spec.get('minimumPassed', 1) or failed or skipped:
raise GateError(f'Invalid/empty/skipped/failing report: passed={passed}, failed={failed}, skipped={skipped}')
expected = spec.get('expectedNames', [])
if expected and sorted(names) != sorted(expected):
raise GateError('Expected test membership differs from report')
if len(names) != len(set(names)):
raise GateError('Duplicate test identities in report')
return dict(passed=passed, failed=failed, skipped=skipped, membershipSha256=digest(sorted(names)))
class WindowsJob:
"""A non-inherited kill-on-close Job handle bounds the complete child process tree."""
def __init__(self, config):
import ctypes as ct
from ctypes import wintypes as wt
class Basic(ct.Structure):
_fields_ = [('processTime', ct.c_longlong), ('jobTime', ct.c_longlong),
('flags', wt.DWORD), ('minimumWorkingSet', ct.c_size_t),
('maximumWorkingSet', ct.c_size_t), ('activeProcesses', wt.DWORD),
('affinity', ct.c_size_t), ('priority', wt.DWORD), ('scheduling', wt.DWORD)]
class Io(ct.Structure):
_fields_ = [(name, ct.c_ulonglong) for name in
('readOps', 'writeOps', 'otherOps', 'readBytes', 'writeBytes', 'otherBytes')]
class Extended(ct.Structure):
_fields_ = [('basic', Basic), ('io', Io), ('processMemory', ct.c_size_t),
('jobMemory', ct.c_size_t), ('peakProcessMemory', ct.c_size_t), ('peakJobMemory', ct.c_size_t)]
self.api = ct.WinDLL('kernel32', use_last_error=True)
self.api.CreateJobObjectW.argtypes = [ct.c_void_p, wt.LPCWSTR]
self.api.CreateJobObjectW.restype = wt.HANDLE
self.api.SetInformationJobObject.argtypes = [wt.HANDLE, ct.c_int, ct.c_void_p, wt.DWORD]
self.api.SetInformationJobObject.restype = wt.BOOL
self.api.AssignProcessToJobObject.argtypes = [wt.HANDLE, wt.HANDLE]
self.api.AssignProcessToJobObject.restype = wt.BOOL
self.api.CloseHandle.argtypes = [wt.HANDLE]
self.api.CloseHandle.restype = wt.BOOL
self.handle = self.api.CreateJobObjectW(None, None)
if not self.handle:
raise GateError('Cannot create Windows process containment job')
limits = Extended()
limits.basic.flags = 0x2000 | 0x8 | 0x200 # KILL_ON_JOB_CLOSE | ACTIVE_PROCESS | JOB_MEMORY
limits.basic.activeProcesses = config.get('maxProcesses', 64)
limits.jobMemory = config.get('maxJobMemoryMiB', 8192) * 1024 * 1024
if not self.api.SetInformationJobObject(self.handle, 9, ct.byref(limits), ct.sizeof(limits)):
self.close()
raise GateError('Cannot enforce Windows process/memory limits')
def assign(self, process):
if not self.api.AssignProcessToJobObject(self.handle, int(process._handle)):
raise GateError('Cannot attach worker to Windows containment job; refusing uncontained execution')
def resume(self, process):
import ctypes as ct
from ctypes import wintypes as wt
class ThreadEntry(ct.Structure):
_fields_ = [('size', wt.DWORD), ('usage', wt.DWORD), ('threadId', wt.DWORD),
('processId', wt.DWORD), ('basePriority', wt.LONG),
('deltaPriority', wt.LONG), ('flags', wt.DWORD)]
self.api.CreateToolhelp32Snapshot.argtypes = [wt.DWORD, wt.DWORD]
self.api.CreateToolhelp32Snapshot.restype = wt.HANDLE
self.api.Thread32First.argtypes = [wt.HANDLE, ct.POINTER(ThreadEntry)]
self.api.Thread32Next.argtypes = [wt.HANDLE, ct.POINTER(ThreadEntry)]
self.api.OpenThread.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]
self.api.OpenThread.restype = wt.HANDLE
self.api.ResumeThread.argtypes = [wt.HANDLE]
self.api.ResumeThread.restype = wt.DWORD
snapshot = self.api.CreateToolhelp32Snapshot(0x4, 0) # TH32CS_SNAPTHREAD
if snapshot in (None, ct.c_void_p(-1).value):
raise GateError('Cannot enumerate suspended worker thread')
entry = ThreadEntry()
entry.size = ct.sizeof(entry)
resumed = False
try:
valid = self.api.Thread32First(snapshot, ct.byref(entry))
while valid:
if entry.processId == process.pid:
thread = self.api.OpenThread(0x2, False, entry.threadId)
if not thread:
raise GateError('Cannot open suspended worker thread')
try:
if self.api.ResumeThread(thread) == 0xFFFFFFFF:
raise GateError('Cannot resume contained worker')
resumed = True
finally:
self.api.CloseHandle(thread)
valid = self.api.Thread32Next(snapshot, ct.byref(entry))
finally:
self.api.CloseHandle(snapshot)
if not resumed:
raise GateError('Suspended worker thread was not found')
def close(self):
if self.handle:
self.api.CloseHandle(self.handle)
self.handle = None
class ProcessRunner:
def __init__(self):
self.child = None
def codex_command(self, engine, *args):
return [engine.config['codexExecutable'], *args]
def capability_args(self, engine):
if local_mode.single(engine.config) and engine.config.get('trustedCodexOverrides'):
raise GateError('single-user-local plugin/MCP capabilities require separate qualification')
result = []
for item in engine.config.get('trustedCodexOverrides', []):
if not re.fullmatch(r'(?:mcp_servers|plugins)\.[A-Za-z0-9_.-]+', item['key']):
raise GateError('Only separately qualified MCP/plugin capability overrides are permitted')
result += ['-c', item['key'] + '=' + item['toml']]
return result
def permission_args(self, engine, command):
if local_mode.single(engine.config):
return local_mode.permission_args(engine.root,engine.config,command)
if command == 'exec':
return ['--sandbox','workspace-write','-c','approval_policy="never"',
'-c','sandbox_workspace_write.network_access=false']
return ['-c','sandbox_mode="workspace-write"','-c','approval_policy="never"',
'-c','sandbox_workspace_write.network_access=false']
def supervise(self, engine, argv, directory, seconds, stdin=None, cwd=None):
directory.mkdir(parents=True, exist_ok=True)
env = {k: v for k, v in os.environ.items() if k.upper() in {
'PATH', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP', 'USERPROFILE', 'LOCALAPPDATA',
'APPDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'COMSPEC', 'PATHEXT', 'HOME','SYSTEMDRIVE'}}
env['CODEX_HOME'] = engine.config['workerCodeXHome']
env['JOBBJAKT_ORCHESTRATED'] = '1'
if local_mode.single(engine.config):
env['TEMP'] = env['TMP'] = engine.config['workerTemp']
# Explicitly do not inherit production credentials, Docker/SSH sockets or API key variables.
flags = (subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW | 0x4) if os.name == 'nt' else 0
started = time.monotonic()
input_path = directory / 'stdin.txt'
input_path.write_text(stdin or '', encoding='utf-8', newline='')
marker = dict(pid=None, argv=argv, startedUtc=utc(), session=str(directory))
atomic(engine.root / 'active-process.json', marker) # durable launch intent before spawn
process = None
job = WindowsJob(engine.config) if os.name == 'nt' else None
# Reader threads drain both pipes; logs are local, redacted, bounded, and never notifications.
import threading
log_error = []
def drain(stream, target):
total, pending = 0, b''
with open(target, 'w', encoding='utf-8', newline='') as output:
while chunk := stream.read1(8192):
total += len(chunk)
if total > engine.config['maxLogBytes']:
if not log_error:
log_error.append('log byte budget exceeded')
continue
pending += chunk
if len(pending) > min(engine.config['maxLogBytes'], 1048576):
if not log_error:
log_error.append('log byte budget exceeded for a single line')
pending = b''
continue
while b'\n' in pending:
line, pending = pending.split(b'\n', 1)
output.write(redact(line.rstrip(b'\r').decode('utf-8', errors='replace')) + '\n')
output.flush()
if pending and not log_error:
output.write(redact(pending.decode('utf-8', errors='replace')))
threads = []
try:
with open(input_path, 'rb') as input_stream:
process = subprocess.Popen(argv, cwd=cwd or engine.repo, env=env, stdin=input_stream,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
creationflags=flags, start_new_session=os.name != 'nt')
self.child = process
marker['pid'] = process.pid
atomic(engine.root / 'active-process.json', marker)
if job:
job.assign(process)
job.resume(process) # no worker instruction can execute before containment
threads = [threading.Thread(target=drain, args=(process.stdout, directory / 'stdout.log'), daemon=True),
threading.Thread(target=drain, args=(process.stderr, directory / 'stderr.log'), daemon=True)]
for thread in threads:
thread.start()
while process.poll() is None:
if (engine.root / 'pause.request').exists():
raise Interrupted('Owner requested pause')
if time.monotonic() - started > seconds or time.monotonic() > engine.deadline:
raise Interrupted('Session/run time budget exceeded')
if not engine.in_window():
raise Interrupted('Execution window closed')
if log_error:
raise GateError(log_error[0])
time.sleep(0.25)
if process.returncode:
raise GateError(f'Process exited {process.returncode}; see redacted local logs')
finally:
if job:
job.close() # closes/kills all descendants, including on normal parent-child completion
if process is not None and process.poll() is None:
if os.name == 'nt':
subprocess.run(['taskkill', '/PID', str(process.pid), '/T', '/F'],
capture_output=True, timeout=20)
else:
os.killpg(process.pid, signal.SIGKILL)
process.wait(timeout=20)
for thread in threads:
thread.join(timeout=10)
if any(t.is_alive() for t in threads):
raise GateError('Descendant still holds log handles; reconcile active-process marker')
if process is not None:
process.stdout.close()
process.stderr.close()
self.child = None
(engine.root / 'active-process.json').unlink(missing_ok=True)
if log_error:
raise GateError(log_error[0])
def run_session(self, engine, stage, checkpoint, directory, prompt):
if local_mode.single(engine.config):
local_mode.require_unattended(engine.root,engine.config)
result = directory / 'result.json'
argv = self.codex_command(engine, 'exec', '--ignore-user-config',
*self.permission_args(engine,'exec'),
'-c', 'features.multi_agent=false', *self.capability_args(engine), '--json', '--color', 'never',
'--output-schema', str(engine.root / 'schemas/result.schema.json'),
'--output-last-message', str(result), '--cd', str(engine.repo), '-')
# Output is written by the CLI parent, not a model shell; worker cannot write protected state.
self.supervise(engine, argv, directory, engine.config['maxSessionSeconds'], stdin=prompt)
events = []
for line in (directory / 'stdout.log').read_text(encoding='utf-8').splitlines():
if not line.strip():
continue
try:
events.append(json.loads(line))
except ValueError:
raise GateError('CLI event stream is not valid JSONL')
if (not any(e.get('type') == 'turn.completed' for e in events)
or any(e.get('type') in ('turn.failed', 'error') for e in events)):
raise GateError('CLI did not produce an unambiguous completed turn')
thread_ids = [e['thread_id'] for e in events if e.get('type') == 'thread.started']
atomic(directory / 'session.json', dict(threadIds=thread_ids, finishedUtc=utc()))
data = read_json(result)
if (set(data) != {'outcome', 'summary', 'blockers', 'requestedApprovals', 'checkpoint'}
or data['outcome'] not in OUTCOMES or data['checkpoint'] != checkpoint['id']
or not isinstance(data['summary'], str)
or not isinstance(data['blockers'], list) or not isinstance(data['requestedApprovals'], list)
or not all(isinstance(x, str) for x in data['blockers'] + data['requestedApprovals'])):
raise GateError('Invalid structured handoff')
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'])
report_spec = check.get('report')
report = None
if report_spec:
report = under(engine.repo, report_spec['path'])
report.unlink(missing_ok=True) # old evidence cannot satisfy a new run
argv = self.codex_command(engine, 'sandbox', '-C', str(working),
*self.permission_args(engine,'sandbox'), *check['argv'])
self.supervise(engine, argv, directory, engine.config['maxCheckSeconds'], cwd=working)
result = dict(id=check['id'], exitCode=0, passed=None, failed=0, report=None)
if report:
if not report.is_file():
raise GateError(f'Required test report missing: {report_spec["path"]}')
result.update(parse_report(report, report_spec))
# Preserve exact structured report separately; private local evidence, never notifications.
shutil.copyfile(report, directory / report.name)
result['report'] = {'path': str(directory / report.name), 'sha256': sha_file(report)}
atomic(directory / 'verification.json', result)
return result
class Engine:
def __init__(self, root, manifest, config, *, runner=None, approval_verifier=None, host_verifier=None):
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()
self.approval_verifier = approval_verifier or self.verify_approval
self.host_verifier = host_verifier or self.verify_host
self.deadline = float('inf')
def git(self, *args):
p = subprocess.run([self.config.get('gitExecutable','git'), '-c', 'core.hooksPath=', '-c', 'credential.interactive=false',
'-C', str(self.repo), *args], text=True, capture_output=True,
encoding='utf-8', timeout=60,
env={**os.environ, 'GIT_TERMINAL_PROMPT': '0'})
if p.returncode:
raise GateError(redact(f'git {args[0]} failed: {p.stderr.strip()}'))
return p.stdout.strip()
def initialize(self):
validate_manifest(self.manifest)
if self.store.path.exists() or self.store.backup.exists():
raise GateError('State already exists; use resume, never overwrite progress')
state = dict(schemaVersion=1, manifestSha256=digest(self.manifest),
integrationCommit=self.manifest['baselineCommit'], paused=False,
recoveryRequired=False, stages={}, recentSessions=[], latestFailure=None, ownerDecisions=[],
productionRelease='NOT_AUTHORISED')
for stage in self.manifest['stages']:
state['stages'][stage['id']] = dict(status='PENDING', checkpoint=0, attempts=0,
sessions=0, pendingPublication=None, latestCommit=None, evidence=[], reports=[],
lastResult=None, entryBaseline=None, checkpointBase=None, checkpointCommit=None,
awaiting=[], verifiedChecks=[], failures=[])
self.store.write(state)
def request(self, stage, cp, kind, state, detail=None):
ss = state['stages'][stage['id']]
request = dict(schemaVersion=1, manifestSha256=digest(self.manifest),
policySha256=self.policy_hash(), kind=kind, stage=stage['id'], checkpoint=cp['id'],
baselineCommit=ss['entryBaseline'] or state['integrationCommit'],
candidateCommit=ss['checkpointCommit'], evidence=ss['evidence'], detail=detail)
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)
if not approval:
ss['awaiting'] = [dict(kind=kind, request=str(path))]
ss['status'] = 'AWAITING_APPROVAL'
self.store.write(state)
return False
ss['awaiting'] = []
if isinstance(approval, dict):
record = dict(requestSha256=digest(request), kind=kind, stage=stage['id'], checkpoint=cp['id'],
decision=approval.get('decision', 'Approved as requested'),
disposition=approval.get('disposition', 'Approve'), manifestSha256=digest(self.manifest))
if record not in state['ownerDecisions']:
state['ownerDecisions'].append(record)
self.store.write(state)
if approval.get('disposition', 'Approve') != 'Approve':
if approval.get('disposition') == 'Revise' and ss['checkpointCommit'] and kind in (
'homepage-design-selection', 'worker-decision', 'stage-requirements-review'):
ss['latestCommit'] = ss['checkpointCommit']
ss['checkpointCommit'] = None
ss['pendingPublication'] = None
ss['evidence'] = []
ss['verifiedChecks'] = []
ss['verificationPrepared'] = False
ss['attempts'] = 0
ss['status'] = 'PENDING'
else:
ss['status'] = 'BLOCKED'
ss['awaiting'] = [dict(kind=kind, request=str(path), decision='Owner rejected or requested changed scope')]
self.store.write(state)
return False
return True
def policy_hash(self):
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()})
def verify_approval(self, request):
path = self.root / 'approvals' / (digest(request) + '.json')
if not path.is_file():
return False
p = subprocess.run([self.config['powershellExecutable'], '-NoProfile', '-File',
str(self.root / 'Approve-JobjaktBacklog.ps1'), '-Action', 'Verify',
'-RequestPath', str(self.root / 'requests' / (digest(request) + '.json')),
'-ApprovalPath', str(path), '-CertificatePath', str(self.root / self.config['publicKeyCertificate']),
'-ExpectedOwnerSid', self.config['ownerSid']], capture_output=True, text=True, timeout=30)
if p.returncode:
return False
envelope = read_json(path)
return json.loads(base64.b64decode(envelope['payloadBase64']))
def verify_host(self):
if self.root.is_relative_to(self.repo) or self.repo.is_relative_to(self.root):
raise GateError('Control plane and worker checkout must be disjoint')
if os.name != 'nt':
raise GateError('Real execution currently qualifies Windows hosts only')
if local_mode.single(self.config):
_,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')
for field in ('maxSessionSeconds', 'maxCheckSeconds', 'maxRunSeconds', 'maxLogBytes'):
if not isinstance(self.config[field], int) or self.config[field] <= 0:
raise GateError('Positive integer resource budgets required')
identity = subprocess.run(['whoami', '/user', '/fo', 'csv', '/nh'], capture_output=True,
text=True, timeout=15).stdout
if not local_mode.single(self.config) and (self.config['runnerSid'] not in identity or self.config['ownerSid'] in identity
or not re.fullmatch(r'S-1-[0-9-]+', self.config['runnerSid'])):
raise GateError('Run under the configured dedicated runner identity, separate from signing owner')
version = subprocess.run([self.config['codexExecutable'], '--version'], capture_output=True,
text=True, timeout=15)
found = re.search(r'(\d+)\.(\d+)\.(\d+)', version.stdout)
if not found or tuple(map(int, found.groups())) < (0, 163, 0):
raise GateError('Codex CLI 0.163.0 or later required; qualify new versions before deployment')
worker_home = Path(self.config['workerCodeXHome']).resolve()
if worker_home.is_relative_to(self.repo):
raise GateError('CODEX_HOME must be outside worker workspace')
# Capability settings are a protected, signed policy input, not worker-controlled config.
ProcessRunner().capability_args(self)
# Refuse user/plugin configuration and repo MCP endpoints. A dedicated auth-only home is required.
if (worker_home / 'config.toml').exists() or (worker_home / 'plugins').exists():
raise GateError('Use a dedicated auth-only CODEX_HOME; do not inherit MCP/plugins/config')
for name in ('.mcp.json', '.codex/config.toml'):
if (self.repo / name).exists():
raise GateError('Repository MCP/Codex config needs removal from isolated runner clone')
if local_mode.single(self.config):
report = local_mode.qualify(self)
if report['status'] != 'PASS' or report['ownerKeyDenied'] is not True:
raise GateError('Native MXC/key qualification failed; operator-controlled pause required')
else:
self.probe_sandbox()
def probe_sandbox(self):
# A real negative write probe, with a harmless disposable sentinel in the protected root.
sentinel = self.root / ('sandbox-deny-' + uuid.uuid4().hex + '.txt')
script = ('try { [IO.File]::WriteAllText(' + "'" + str(sentinel).replace("'", "''")
+ "','unexpected'); exit 9 } catch { exit 0 }")
argv = [self.config['codexExecutable'], 'sandbox', '-C', str(self.repo),
'-c', 'sandbox_mode="workspace-write"', '-c', 'approval_policy="never"',
'-c', 'sandbox_workspace_write.network_access=false',
self.config['powershellExecutable'], '-NoProfile', '-Command', script]
probe = self.root / 'probe' / uuid.uuid4().hex
try:
self.runner.supervise(self, argv, probe, 30)
if sentinel.exists():
raise GateError('Sandbox wrote to protected control plane')
finally:
sentinel.unlink(missing_ok=True)
def verify_remote(self, branch, commit):
try:
lines = self.git('ls-remote', '--exit-code', self.manifest['remote'], 'refs/heads/' + branch)
if lines.split()[0] != commit:
raise PublicationError(f'Remote HEAD differs: {branch}')
except (GateError, IndexError) as error:
raise PublicationError(f'Publication not verified for {branch}: {error}') from error
def publish(self, branch, commit):
if not SAFE_BRANCH.fullmatch(branch):
raise PublicationError('Refusing publication to protected branch')
try:
self.git('push', self.manifest['remote'], f'{commit}:refs/heads/{branch}')
self.verify_remote(branch, commit)
except GateError as error:
raise PublicationError(str(error)) from error
def event(self, kind, stage, message):
path = self.root / 'events.jsonl'
# Events contain IDs and bounded redacted status only, never prompts or documents.
with open(path, 'a', encoding='utf-8') as stream:
stream.write(json.dumps(dict(at=utc(), event=kind, stage=stage,
message=redact(message)[:600])) + '\n')
def pause(self):
atomic(self.root / 'pause.request', dict(at=utc()))
def resume(self):
with RepoLock(self.repo):
state = self.store.read()
if state.get('recoveryRequired'):
raise GateError('Recovered backup requires signed state-recovery approval; run recovery command')
(self.root / 'pause.request').unlink(missing_ok=True)
state['paused'] = False
self.store.write(state)
def retry(self, stage_id):
with RepoLock(self.repo):
state = self.store.read()
stage = next(s for s in self.manifest['stages'] if s['id'] == stage_id)
ss = state['stages'][stage_id]
if ss['status'] != 'FAILED':
raise GateError('Only FAILED stages can be retried; resume handles blocked publication/approvals')
if ss['attempts'] >= stage['retryPolicy']['maxAttempts']:
# Budget resets are never implicit. Owner signs an exact reset request through run().
ss['status'] = 'AWAITING_APPROVAL'
ss['budgetResetRequested'] = True
else:
ss['status'] = 'PENDING'
self.store.write(state)
def in_window(self):
windows = self.config.get('executionWindows', [])
if not windows:
return True
now = datetime.now().strftime('%H:%M')
return any((w['start'] <= now < w['end']) if w['start'] < w['end']
else (now >= w['start'] or now < w['end']) for w in windows)
def prepare_branch(self, stage, ss, state):
current = self.git('branch', '--show-current')
if current in ('main', 'master', 'production', ''):
raise GateError('Checkout of main/master/production or detached HEAD is prohibited')
if ss['entryBaseline'] is None:
if self.git('status', '--porcelain', '--untracked-files=all'):
raise GateError('New stage requires a clean worktree, including untracked files')
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:
if self.git('rev-parse', stage['branch']) != baseline:
raise GateError('Existing stage branch is not exactly the accepted baseline')
self.git('switch', stage['branch'])
else:
self.git('switch', '-c', stage['branch'], baseline)
ss['entryBaseline'] = baseline
self.store.write(state)
elif current != stage['branch']:
raise GateError('Unexpected branch after interruption; reconcile manually')
self.git('merge-base', '--is-ancestor', ss['entryBaseline'], 'HEAD')
expected = ss['checkpointCommit'] or ss['latestCommit'] or ss['entryBaseline']
if self.git('rev-parse', 'HEAD') != expected:
# Crash between commit and state save: exact controller trailer permits reconciliation.
trailer = f'Orchestrator-Checkpoint: {ss.get("transaction", "missing")}'
body = self.git('log', '-1', '--format=%B')
parent = self.git('rev-parse', 'HEAD^')
if (trailer not in body or parent != expected or not ss.get('verificationPrepared')
or self.content_hash() != ss.get('testedContentSha256')):
raise GateError('Unexpected HEAD; refusing stale evidence or unrelated commit')
self.verify_evidence(ss)
ss['checkpointCommit'] = self.git('rev-parse', 'HEAD')
ss['pendingPublication'] = ss['checkpointCommit']
self.store.write(state)
def prompt(self, stage, cp, ss, state):
source = under(self.root, stage['sourcePrompt'])
if stage['sourceStatus'] != 'VERIFIED' or not source.is_file() or not source.stat().st_size:
raise GateError(f'Missing complete authoritative prompt for {stage["id"]}')
if sha_file(source) != stage['promptSha256']:
raise GateError('Prompt hash changed; owner must install/re-authorize a new manifest')
chunks = [source.read_text(encoding='utf-8-sig')]
for item in stage.get('supplementalPrompts', []):
path = under(self.root, item['path'])
if sha_file(path) != item['sha256']:
raise GateError('Supplemental prompt hash changed')
if item.get('checkpoints') is not None and cp['id'] not in item['checkpoints']:
continue
chunks.append(f'\nCOMPLETE SUPPLEMENTAL INSTRUCTIONS {item["path"]}\n' + path.read_text(encoding='utf-8-sig'))
for name in stage['contextFiles']:
path = under(self.repo, name)
if not path.is_file():
raise GateError('Required prior context missing: ' + name)
chunks.append(f'\nPRIOR AUTHORITY {name}\n' + path.read_text(encoding='utf-8-sig'))
for dependency in stage['dependencies']:
prior = state['stages'][dependency]
if prior['reports']:
receipt = prior['reports'][-1]
report_path = Path(receipt['path'])
if sha_file(report_path) != receipt['sha256']:
raise GateError('Prior acceptance report changed')
chunks.append(f'\nFULL ACCEPTED DEPENDENCY REPORT {dependency}\n' + report_path.read_text(encoding='utf-8'))
for evidence in prior['evidence']:
path = under(self.repo, evidence['path'])
if sha_file(path) != evidence['sha256']:
raise GateError('Prior accepted artifact changed')
if path.suffix.lower() in ('.md', '.txt', '.json'):
chunks.append(f'\nFULL PRIOR ACCEPTANCE ARTIFACT {evidence["path"]}\n' + path.read_text(encoding='utf-8-sig'))
chunks.append('\nPRIOR CONTROLLER REPORTS\n' + json.dumps(state['recentSessions'][-12:], ensure_ascii=False))
chunks.append('\nVERIFIED OWNER DECISIONS\n' + json.dumps(state['ownerDecisions'], ensure_ascii=False))
chunks.append('\nACTIVE CHECKPOINT ONLY\n' + json.dumps(dict(stage=stage['id'], checkpoint=cp,
integrationCommit=state['integrationCommit'], lastResult=ss['lastResult'],
priorEvidence=ss['evidence'], failures=ss['failures'][-5:],
remainingAttempts=stage['retryPolicy']['maxAttempts'] - ss['attempts']),
ensure_ascii=False))
chunks.append('''\nCONTROL CONTRACT: Work only on this checkpoint and current branch. Read repository AGENTS.md.
Do not start another checkpoint, another Codex process, or another orchestrator. Do not commit,
push, switch branches, merge, deploy, change approval/control files, access production services,
enable models, accept security risks, buy services or remove failing tests. The controller owns Git
publication and independent verification. Report blockers honestly. Preserve ALL source prompt
requirements; where a prompt stops pending owner approval, return outcome approval. A signed
checkpoint entry grant permits only its named implementation scope; it never approves production.
Return the required structured JSON. outcome checkpoint means more work is needed on this same
checkpoint; complete means ready for independent checks. Do not infer acceptance from a test count.
''')
return '\n'.join(chunks)
def verify_evidence(self, ss):
for artifact in ss['evidence']:
path = under(self.repo, artifact['path'])
if not path.is_file() or sha_file(path) != artifact['sha256']:
raise GateError('Checkpoint evidence changed since verification')
for check in ss['verifiedChecks']:
if check.get('report'):
report = Path(check['report']['path'])
if not report.is_file() or sha_file(report) != check['report']['sha256']:
raise GateError('Trusted test report missing or changed')
def checkpoint(self, stage, cp, ss, state):
# Recovery after commit/push uses stored evidence, never another model session.
if ss['pendingPublication']:
self.verify_evidence(ss)
self.publish(stage['branch'], ss['pendingPublication'])
ss['latestCommit'] = ss['pendingPublication']
ss['pendingPublication'] = None
self.store.write(state)
return self.finish_checkpoint(stage, cp, ss, state)
if ss['checkpointCommit']:
return self.finish_checkpoint(stage, cp, ss, state)
if ss['attempts'] >= stage['retryPolicy']['maxAttempts']:
if not ss.get('budgetResetRequested'):
ss['status'] = 'FAILED'
self.store.write(state)
return False
if not self.request(stage, cp, 'retry-budget-reset', state, ss['failures']):
return False
ss['attempts'] = 0
ss['budgetResetRequested'] = False
if ss['sessions'] >= stage['retryPolicy']['maxSessions']:
raise GateError('Total stage session limit reached; owner must revise policy')
for kind in cp['approvalsBefore']:
if not self.request(stage, cp, kind, state):
return False
if ss.get('requestedDecision'):
if not self.request(stage, cp, 'worker-decision', state, ss['requestedDecision']):
return False
ss.pop('requestedDecision')
prompt = self.prompt(stage, cp, ss, state)
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
ss['status'] = 'RUNNING'
ss['checkpointBase'] = self.git('rev-parse', 'HEAD')
session_id = uuid.uuid4().hex
ss['transaction'] = session_id
directory = self.root / 'sessions' / stage['id'] / cp['id'] / session_id
directory.mkdir(parents=True)
atomic(directory / 'input.json', dict(prompt=prompt, promptSha256=stage['promptSha256'],
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)))
self.store.write(state)
if self.git('rev-parse', 'HEAD') != ss['checkpointBase'] or self.git('branch', '--show-current') != stage['branch']:
raise GateError('Worker performed forbidden Git operation')
if result['outcome'] == 'blocked':
ss['requestedDecision'] = result
ss['status'] = 'BLOCKED'
self.store.write(state)
return False
if result['outcome'] == 'failed':
raise GateError('Worker reports failure: ' + redact(result['summary']))
if result['outcome'] == 'conditional' and not cp['deferrableGates']:
ss['status'] = 'CONDITIONALLY_ACCEPTED'
ss['requestedDecision'] = result
self.store.write(state)
return False
if result['outcome'] == 'checkpoint':
ss['status'] = 'PENDING'
ss['attempts'] -= 1 # bounded by total sessions, not an ordinary failure
self.store.write(state)
return True
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)
if not artifact.is_file() or artifact.stat().st_size == 0:
raise GateError('Required nonempty artifact missing: ' + name)
ss['evidence'].append(dict(path=name, sha256=sha_file(artifact)))
if result['outcome'] == 'complete' and (result['blockers'] or result['requestedApprovals']):
raise GateError('Complete result still contains blockers/approval requests')
# Pin tested content before Git hooks or unrelated processes can alter it.
before = self.content_hash()
ss['testedContentSha256'] = before
ss['verificationPrepared'] = True
self.store.write(state) # crash after commit can reconcile only these independently verified bytes
self.git('add', '--all')
if self.content_hash() != before:
raise GateError('Worktree changed during verification/commit')
if self.git('diff', '--cached', '--name-only'):
self.git('commit', '-m', f'{stage["id"]}: {cp["id"]} validated checkpoint\n\nOrchestrator-Checkpoint: {session_id}')
ss['checkpointCommit'] = self.git('rev-parse', 'HEAD')
ss['pendingPublication'] = ss['checkpointCommit']
ss['testedContentSha256'] = before
ss['status'] = 'ACCEPTED'
report = dict(stage=stage['id'], checkpoint=cp['id'], candidateCommit=ss['checkpointCommit'],
promptSha256=stage['promptSha256'], checks=ss['verifiedChecks'], evidence=ss['evidence'],
result=result, productionRelease='NOT_AUTHORISED')
report_path = self.root / 'reports' / (session_id + '.json')
atomic(report_path, report)
ss['reports'].append(dict(path=str(report_path), sha256=sha_file(report_path)))
self.store.write(state)
self.publish(stage['branch'], ss['pendingPublication'])
ss['latestCommit'] = ss['pendingPublication']
ss['pendingPublication'] = None
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')
return digest({name: sha_file(under(self.repo, name)) for name in sorted(set(files))
if name and under(self.repo, name).is_file()})
def finish_checkpoint(self, stage, cp, ss, state):
self.verify_evidence(ss)
if ss.get('testedContentSha256') and self.content_hash() != ss['testedContentSha256']:
raise GateError('Tested source changed before acceptance')
self.verify_remote(stage['branch'], ss['checkpointCommit'])
kinds = list(cp['approvalsAfter'])
if ss['lastResult']['outcome'] == 'approval':
kinds.append('worker-decision')
if ss['lastResult']['outcome'] == 'conditional':
kinds.append('conditional-deferral')
for kind in kinds:
detail = dict(requirements=cp['evidenceRequirements'], workerResult=ss['lastResult'],
deferrableGates=cp['deferrableGates'] if kind == 'conditional-deferral' else [])
if not self.request(stage, cp, kind, state, detail):
return False
ss['checkpoint'] += 1
ss['attempts'] = 0
ss['checkpointCommit'] = None
ss['checkpointBase'] = None
ss['verificationPrepared'] = False
ss['lastVerifiedChecks'] = ss['verifiedChecks']
ss['verifiedChecks'] = []
ss['status'] = 'PENDING'
self.store.write(state)
if ss['checkpoint'] == len(stage['checkpoints']):
self.integrate_stage(stage, ss, state)
return True
def integrate_stage(self, stage, ss, state):
commit = ss['latestCommit']
integration = self.manifest['integrationBranch']
if self.git('rev-parse', 'HEAD') != commit or self.git('status', '--porcelain', '--untracked-files=all'):
raise GateError('Integration requires the clean, verified stage commit')
self.verify_remote(stage['branch'], commit)
self.git('merge-base', '--is-ancestor', state['integrationCommit'], commit)
existing = self.git('for-each-ref', '--format=%(objectname)', 'refs/heads/' + integration)
if existing and existing not in (state['integrationCommit'], commit):
raise GateError('Integration branch diverged; no automatic merge/rebase')
self.git('update-ref', 'refs/heads/' + integration, commit, existing or '0'*40)
self.publish(integration, commit)
state['integrationCommit'] = commit
ss['status'] = 'COMPLETED'
self.event('stage-completed', stage['id'], 'Development accepted and published; production remains unauthorised')
self.store.write(state)
def run(self):
self.deadline = time.monotonic() + self.config['maxRunSeconds']
with RepoLock(self.repo):
state = self.store.read()
if state['manifestSha256'] != digest(self.manifest):
raise GateError('Manifest changed since initialization; owner-controlled migration required')
if not self.manifest['executionEnabled']:
state['latestFailure'] = 'Execution disabled: initial orchestrator-building task only'
self.store.write(state)
return
if (self.root / 'pause.request').exists() or state['paused']:
state['paused'] = True
self.store.write(state)
return
if state.get('recoveryRequired') or (self.root / 'active-process.json').exists():
raise GateError('Recovery/orphan reconciliation required; no model launched')
if any(s['status'] == 'RUNNING' for s in state['stages'].values()):
state['recoveryRequired'] = True
self.store.write(state)
raise GateError('Interrupted RUNNING state requires signed orphan reconciliation, even without a PID marker')
self.host_verifier()
for stage in self.manifest['stages']:
ss = state['stages'][stage['id']]
if ss['status'] == 'COMPLETED':
continue
if not all(state['stages'][dep]['status'] == 'COMPLETED' for dep in stage['dependencies']):
ss['status'] = 'BLOCKED'
state['latestFailure'] = 'Dependencies are not accepted and published'
self.store.write(state)
break
while ss['checkpoint'] < len(stage['checkpoints']):
cp = stage['checkpoints'][ss['checkpoint']]
if (self.root / 'pause.request').exists() or time.monotonic() > self.deadline or not self.in_window():
state['paused'] = (self.root / 'pause.request').exists()
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:
for kind in ['environment-qualified', 'roadmap-execution', *stage['requiredOwnerApprovals']]:
if kind == 'product-ranked-scope' and any(d['kind'] == kind and
d.get('disposition') == 'Approve' and d.get('manifestSha256') == digest(self.manifest)
for d in state['ownerDecisions']):
continue
if not self.request(stage, cp, kind, state):
return
self.prepare_branch(stage, ss, state)
if not self.checkpoint(stage, cp, ss, state):
return
except Interrupted as error:
ss['status'] = 'BLOCKED'
state['latestFailure'] = str(error)
self.store.write(state)
self.event('interrupted', stage['id'], str(error))
return
except PublicationError as error:
ss['status'] = 'BLOCKED'
state['latestFailure'] = str(error)
self.store.write(state)
self.event('publication-blocked', stage['id'], str(error))
return
except (GateError, OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
message = redact(str(error))
state['latestFailure'] = message
ss['failures'].append(dict(at=utc(), checkpoint=cp['id'], message=message))
if (self.root / 'active-process.json').exists():
ss['status'] = 'BLOCKED'
self.store.write(state)
return # failed cleanup can never launch an overlapping remediation worker
# Failures after acceptance are policy/evidence failures, never remediation input.
if ss['attempts'] and not ss['checkpointCommit'] and ss['status'] == 'RUNNING':
ss['status'] = 'FAILED'
self.store.write(state)
if ss['attempts'] < stage['retryPolicy']['maxAttempts']:
wait = stage['retryPolicy']['backoffSeconds']
until = time.monotonic() + wait
while time.monotonic() < until:
if (self.root / 'pause.request').exists() or time.monotonic() > self.deadline:
return
time.sleep(0.25)
continue
else:
ss['status'] = 'BLOCKED'
self.store.write(state)
self.event('blocked', stage['id'], message)
return
# Resume after a crash while publishing the integration ref.
if ss['checkpoint'] == len(stage['checkpoints']) and ss['status'] != 'COMPLETED':
try:
self.integrate_stage(stage, ss, state)
except GateError as error:
ss['status'] = 'BLOCKED'
state['latestFailure'] = str(error)
self.store.write(state)
return
if all(s['status'] == 'COMPLETED' for s in state['stages'].values()):
self.event('roadmap-development-completed', None, 'Awaiting independent production release approval; no deploy action exists')
def status(engine):
state = engine.store.read()
stages = []
for stage in engine.manifest['stages']:
ss = state['stages'][stage['id']]
checks = ss['verifiedChecks'] or ss.get('lastVerifiedChecks', [])
stages.append(dict(id=stage['id'], status=ss['status'],
checkpoint=stage['checkpoints'][ss['checkpoint']]['id'] if ss['checkpoint'] < len(stage['checkpoints']) else 'done',
branch=stage['branch'], latestVerifiedCommit=ss['latestCommit'],
attempts=ss['attempts'], sessions=ss['sessions'], requiredOwnerAction=ss['awaiting'],
testsPassed=sum(x.get('passed') or 0 for x in checks), testsFailed=sum(x.get('failed') or 0 for x in checks),
testState='VERIFIED' if checks else 'NOT_RUN',
reports=ss['reports']))
return dict(mode=engine.config.get('mode','dedicated-runner'),
executionPolicy=engine.config.get('executionPolicy','dedicated-runner'),
paused=state['paused'] or (engine.root / 'pause.request').exists(),
overall='COMPLETED_DEVELOPMENT' if all(s['status'] == 'COMPLETED' for s in stages) else 'GATED',
integrationCommit=state['integrationCommit'], currentGitBranch=engine.git('branch', '--show-current'),
productionRelease='NOT_AUTHORISED', stages=stages, latestFailure=state['latestFailure'],
recentSessions=state['recentSessions'][-10:], recoveryRequired=state['recoveryRequired'])
def install(source, root, repository, mode='dedicated-runner'):
root = root.resolve()
repo = Path(repository).resolve()
if root.exists():
raise GateError('ControlRoot must be a new directory; do not overwrite trusted state')
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', '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)
shutil.copyfile(source.parent / 'scripts/Approve-JobjaktBacklog.ps1', root / 'Approve-JobjaktBacklog.ps1')
shutil.copyfile(source.parent / 'scripts/Run-JobjaktBacklog.ps1', root / 'Run-JobjaktBacklog.ps1')
for name in ('Probe-JobjaktSandbox.ps1','Use-JobjaktBacklog.ps1','Register-JobjaktBacklogTask.ps1'):
shutil.copyfile(source.parent/'scripts'/name,root/name)
config = read_json(root / 'config.example.json')
config['repository'] = str(repo)
if mode == 'single-user-local':
config = local_mode.configure_current_user(root,config)
atomic(root / 'config.json', config)
manifest = read_json(root / 'backlog.json')
Engine(root, manifest, config).initialize()
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','prepare-entry','key','enable-unattended'])
parser.add_argument('--control-root', type=Path, required=True)
parser.add_argument('--repository')
parser.add_argument('--stage')
parser.add_argument('--manifest', type=Path)
args = parser.parse_args(argv)
root = args.control_root.resolve()
if args.command in ('init','setup'):
if not args.repository:
parser.error('init requires --repository (isolated development clone/worktree)')
if args.command == 'setup' and (root/'config.json').exists():
existing = read_json(root/'config.json')
state = StateStore(root).read()
source = Path(__file__).resolve().parent
if (local_mode.single(existing) and source != root and not (root/'active-process.json').exists()
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','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'):
shutil.copyfile(source.parent/'scripts'/name,root/name)
print(json.dumps(dict(existingInstallation=str(root),mode=existing.get('mode','dedicated-runner'),
note='Disabled unused single-user installation may refresh code; state/auth/policy preserved; requalify after code changes'),indent=2))
return 0
install(Path(__file__).resolve().parent, root, args.repository,
mode='single-user-local' if args.command == 'setup' else 'dedicated-runner')
print('Initialized disabled queue. Review protected config, identities, source gaps and gates before authorization.')
return 0
engine = Engine(root, read_json(root / 'backlog.json'), read_json(root / 'config.json'))
if args.command in ('key','enable-unattended'):
if not local_mode.single(engine.config) or not sys.stdin.isatty() or os.environ.get('JOBBJAKT_ORCHESTRATED'):
raise GateError('Run this single-user operator action yourself from an interactive PowerShell terminal')
with RepoLock(engine.repo):
state=engine.store.read()
if (root/'active-process.json').exists() or state['recoveryRequired'] or any(s['status']=='RUNNING' for s in state['stages'].values()):
raise GateError('Stop/reconcile workers before changing signing or execution settings')
_,sid=local_mode.detect_identity()
local_mode.validate_identity(engine.config,sid)
if args.command == 'key':
if engine.config.get('ownerCertificateThumbprint'):
print('Owner certificate already configured; it will not be replaced automatically')
return 0
created=subprocess.run([engine.config['powershellExecutable'],'-NoProfile','-File',
str(root/'Approve-JobjaktBacklog.ps1'),'-Action','NewKey'],capture_output=True,text=True,check=True)
metadata=json.loads(created.stdout)
if metadata['OwnerSid'] != sid:
raise GateError('Signing certificate was created under an unexpected identity')
certificate=root/'owner.cer'
subprocess.run([engine.config['powershellExecutable'],'-NoProfile','-File',str(root/'Approve-JobjaktBacklog.ps1'),
'-Action','ExportPublicKey','-Thumbprint',metadata['Thumbprint'],'-CertificatePath',str(certificate)],check=True)
engine.config.update(ownerCertificateThumbprint=metadata['Thumbprint'],publicKeyCertificate='owner.cer',
ownerCertificateSha256=sha_file(certificate))
atomic(root/'config.json',engine.config)
print('Nonexportable owner key configured under your existing login. Run qualify to test that exact key. No phase authorized.')
else:
proposed={**engine.config,'executionPolicy':'qualified-unattended'}
local_mode.require_unattended(root,proposed)
if input('Type ENABLE to allow qualified unattended workers (roadmap approval still separate): ').strip() != 'ENABLE':
raise GateError('Unattended selection cancelled')
engine.config.update(executionPolicy='qualified-unattended')
atomic(root/'config.json',engine.config)
print('Qualified unattended policy selected. Queue activation and phase entry grants are still required.')
elif args.command in ('auth','auth-status'):
if not local_mode.single(engine.config):
raise GateError('Use the single-user-local setup for the managed auth-only profile')
local_mode.validate_config(root,engine.config)
if args.command == 'auth' and (not sys.stdin.isatty() or os.environ.get('JOBBJAKT_ORCHESTRATED')):
raise GateError('Run auth yourself from a PowerShell terminal; no token copying or background login')
env = {**os.environ,'CODEX_HOME':engine.config['workerCodeXHome']}
result = subprocess.run(local_mode.auth_command(engine.config,args.command == 'auth-status'),env=env,
capture_output=args.command == 'auth-status',text=True)
if args.command == 'auth-status':
kind = 'CHATGPT_SIGNED_IN' if result.returncode == 0 and 'chatgpt' in (result.stdout+result.stderr).lower() else 'NOT_SIGNED_IN_WITH_CHATGPT'
print(json.dumps(dict(authentication=kind,profile=engine.config['workerCodeXHome'],
sameAccount='Sign in with your existing ChatGPT account; identity not inferred or copied')))
return result.returncode
elif args.command == 'qualify':
if not local_mode.single(engine.config):
raise GateError('qualify is for single-user-local; legacy preflight is unchanged')
with RepoLock(engine.repo):
state = engine.store.read()
if (root/'active-process.json').exists() or any(s['status']=='RUNNING' for s in state['stages'].values()):
raise GateError('Stop/reconcile all workers before qualification')
report = local_mode.qualify(engine)
print(json.dumps(report,indent=2))
return 0 if report['status']=='PASS' else 3
elif args.command == 'approve':
if not sys.stdin.isatty() or os.environ.get('JOBBJAKT_ORCHESTRATED'):
raise GateError('Human approval requires your interactive terminal, never an autonomous subprocess')
with RepoLock(engine.repo):
state = engine.store.read()
pending = next((ss['awaiting'][0] for ss in state['stages'].values() if ss['awaiting']),None)
if not pending:
raise GateError('No pending owner request')
request = Path(pending['request'])
recovery = (pending['kind']=='recovery-reconciled' and read_json(request).get('kind')=='recovery-reconciled')
if (state['recoveryRequired'] or any(s['status']=='RUNNING' for s in state['stages'].values())) and not recovery:
raise GateError('Only the exact recovery request may be approved during reconciliation')
if (root/'active-process.json').exists():
if not recovery or recorded_process_exists(read_json(root/'active-process.json')):
raise GateError('Stop and verify the recorded process tree before approval')
_,sid = local_mode.detect_identity()
if sid != engine.config['ownerSid']:
raise GateError('Wrong owner identity')
print(request.read_text(encoding='utf-8'))
if recovery and input('Type RECONCILED after inspecting that the recorded worker and all descendants are gone: ').strip() != 'RECONCILED':
raise GateError('Operator reconciliation not confirmed')
challenge = uuid.uuid4().hex[:8]
if input('Type '+challenge+' to review/sign this exact request: ').strip() != challenge:
raise GateError('Approval cancelled')
disposition = input('Disposition (Approve/Revise/Reject): ').strip()
if disposition not in ('Approve','Revise','Reject'):
raise GateError('Invalid disposition')
decision = input('Your scope, design selection or revision: ').strip()
if not decision:
raise GateError('Concrete owner decision required')
thumb = engine.config.get('ownerCertificateThumbprint')
if not thumb:
raise GateError('Configure your owner signing certificate first; no writable-JSON approval fallback exists')
result = subprocess.run([engine.config['powershellExecutable'],'-NoProfile','-File',
str(root/'Approve-JobjaktBacklog.ps1'),'-Action','Sign','-RequestPath',str(request),
'-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()
stage = next(s for s in engine.manifest['stages'] if state['stages'][s['id']]['status']!='COMPLETED')
ss = state['stages'][stage['id']]
cp = stage['checkpoints'][min(ss['checkpoint'],len(stage['checkpoints'])-1)]
path = root/'operator-review.txt'
path.write_text('INSPECTION ONLY. This file grants no Phase 10 or execution authorization.\n'+
engine.prompt(stage,cp,ss,state),encoding='utf-8')
print('Operator-controlled review prepared at '+str(path)+'; no worker/test/Git operation launched')
elif args.command in ('validate', 'dry-run'):
validate_manifest(engine.manifest, root)
if local_mode.single(engine.config):
local_mode.validate_config(root,engine.config)
report = dict(configuration='PASS',mode=engine.config.get('mode','dedicated-runner'),
executionPolicy=engine.config.get('executionPolicy','dedicated-runner'),
runtimeReady=False if local_mode.single(engine.config) else None,
executionEnabled=engine.manifest['executionEnabled'],
sourceGaps=[dict(id=s['id'], status=s['sourceStatus']) for s in engine.manifest['stages'] if s['sourceStatus'] != 'VERIFIED'],
unqualifiedAdapters=sorted({check['id'] for s in engine.manifest['stages']
for cp in s['checkpoints'] for check in cp['checks'] if not check.get('configured', True)}),
checkpoints=sum(len(s['checkpoints']) for s in engine.manifest['stages']),
phase10='NO-GO', note='Read-only planning; no model, Git mutation, network, service, signing or scheduling action')
print(json.dumps(report, indent=2))
elif args.command == 'pause':
engine.pause()
print('Pause persisted; active process tree will be stopped by the controller.')
elif args.command in ('status', 'current'):
data = status(engine)
if args.command == 'current':
data = next((s for s in data['stages'] if s['status'] != 'COMPLETED'), {'status': 'COMPLETED_DEVELOPMENT'})
print(json.dumps(data, indent=2))
elif args.command == 'logs':
path = root / 'events.jsonl'
print('\n'.join(path.read_text(encoding='utf-8').splitlines()[-25:]) if path.exists() else 'No events yet')
elif args.command == 'retry':
if not args.stage:
parser.error('retry requires --stage')
engine.retry(args.stage)
elif args.command == 'configure':
if not args.manifest:
parser.error('configure requires --manifest (owner-prepared proposed manifest)')
proposed = read_json(args.manifest)
validate_manifest(proposed, root)
with RepoLock(engine.repo):
state = engine.store.read()
if (root / 'active-process.json').exists() or state['recoveryRequired']:
raise GateError('Recover/reconcile active work before changing policy')
if [s['id'] for s in proposed['stages']] != [s['id'] for s in engine.manifest['stages']]:
raise GateError('This policy adoption preserves the existing stage IDs/order; new queues need a new installation')
for old, new in zip(engine.manifest['stages'], proposed['stages']):
if old != new and state['stages'][old['id']]['sessions']:
raise GateError('Cannot rewrite a started stage; preserve its immutable policy and evidence')
if any(s['sessions'] for s in state['stages'].values()) and (
proposed['baselineCommit'] != engine.manifest['baselineCommit'] or
proposed['integrationBranch'] != engine.manifest['integrationBranch'] or
proposed['remote'] != engine.manifest['remote']):
raise GateError('Cannot replace baseline/integration/remote after execution starts')
stage = next((s for s in engine.manifest['stages'] if state['stages'][s['id']]['status'] != 'COMPLETED'),
engine.manifest['stages'][-1])
cp = stage['checkpoints'][min(state['stages'][stage['id']]['checkpoint'], len(stage['checkpoints']) - 1)]
if not engine.request(stage, cp, 'manifest-policy-update', state,
dict(proposedManifestSha256=digest(proposed))):
return 3
state['manifestSha256'] = digest(proposed)
if not any(s['sessions'] for s in state['stages'].values()):
state['integrationCommit'] = proposed['baselineCommit']
atomic(root / 'backlog.json', proposed)
engine.store.write(state)
print('Signed manifest adopted. Resume still enforces environment and stage entry approvals.')
elif args.command == 'recover':
with RepoLock(engine.repo):
state = engine.store.read()
state['recoveryRequired'] = True
engine.store.write(state) # a pending/unsigned request cannot clear the interrupted-run guard
stage = next(s for s in engine.manifest['stages'] if state['stages'][s['id']]['status'] != 'COMPLETED')
cp = stage['checkpoints'][min(state['stages'][stage['id']]['checkpoint'], len(stage['checkpoints']) - 1)]
marker = read_json(root / 'active-process.json') if (root / 'active-process.json').exists() else None
if not engine.request(stage, cp, 'recovery-reconciled', state, marker):
return 3
# Operator must first prove process tree is gone. Never kill an unverified reused PID.
if marker:
if recorded_process_exists(marker):
raise GateError('Recorded process may still exist; inspect identity/tree before owner stops it')
(root / 'active-process.json').unlink()
state['recoveryRequired'] = False
for ss in state['stages'].values():
if ss['status'] == 'RUNNING':
ss['status'] = 'BLOCKED'
engine.store.write(state)
else:
if args.command == 'resume':
engine.resume()
engine.run()
print(json.dumps(status(engine), indent=2))
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except (GateError, OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
print(redact(str(error)), file=sys.stderr)
sys.exit(2)