feat(automation): support qualified single-user Windows operation

This commit is contained in:
cesnimda
2026-10-29 18:27:17 +01:00
parent c1663df44c
commit 1e6c80f708
30 changed files with 2213 additions and 807 deletions
+188 -22
View File
@@ -18,6 +18,7 @@ import tempfile
import time
import uuid
import xml.etree.ElementTree as ET
import local_mode
class GateError(RuntimeError):
@@ -82,6 +83,16 @@ 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:
@@ -398,6 +409,8 @@ class ProcessRunner:
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']):
@@ -405,13 +418,24 @@ class ProcessRunner:
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'}}
'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()
@@ -496,10 +520,11 @@ class ProcessRunner:
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',
'--sandbox', 'workspace-write', '-c', 'approval_policy="never"',
'-c', 'sandbox_workspace_write.network_access=false',
*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), '-')
@@ -528,6 +553,8 @@ class ProcessRunner:
return data
def run_check(self, engine, check, directory):
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
@@ -535,8 +562,7 @@ class ProcessRunner:
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),
'-c', 'sandbox_mode="workspace-write"', '-c', 'approval_policy="never"',
'-c', 'sandbox_workspace_write.network_access=false', *check['argv'])
*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:
@@ -563,7 +589,7 @@ class Engine:
self.deadline = float('inf')
def git(self, *args):
p = subprocess.run(['git', '-c', 'core.hooksPath=', '-c', 'credential.interactive=false',
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'})
@@ -630,7 +656,8 @@ class Engine:
return True
def policy_hash(self):
files = ['controller.py', 'Approve-JobjaktBacklog.ps1', 'config.json', 'schemas/result.schema.json']
files = ['controller.py', 'local_mode.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):
@@ -652,6 +679,10 @@ class Engine:
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)
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')
@@ -660,7 +691,7 @@ class Engine:
raise GateError('Positive integer resource budgets required')
identity = subprocess.run(['whoami', '/user', '/fo', 'csv', '/nh'], capture_output=True,
text=True, timeout=15).stdout
if (self.config['runnerSid'] not in identity or self.config['ownerSid'] in identity
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,
@@ -679,7 +710,12 @@ class Engine:
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')
self.probe_sandbox()
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.
@@ -1126,14 +1162,16 @@ def status(engine):
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(paused=state['paused'] or (engine.root / 'pause.request').exists(),
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):
def install(source, root, repository, mode='dedicated-runner'):
root = root.resolve()
repo = Path(repository).resolve()
if root.exists():
@@ -1141,14 +1179,18 @@ def install(source, root, repository):
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', 'backlog.json', 'config.example.json'):
for name in ('controller.py', 'local_mode.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()
@@ -1157,23 +1199,151 @@ def install(source, root, repository):
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'])
'pause', 'retry', 'current', 'logs', 'recover', 'configure',
'setup','auth','auth-status','qualify','approve','prepare','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 == 'init':
if args.command in ('init','setup'):
if not args.repository:
parser.error('init requires --repository (isolated development clone/worktree)')
install(Path(__file__).resolve().parent, root, args.repository)
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'):
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 ('validate', 'dry-run'):
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':
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)
report = dict(configuration='PASS', executionEnabled=engine.manifest['executionEnabled'],
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)}),
@@ -1238,11 +1408,7 @@ def main(argv=None):
return 3
# Operator must first prove process tree is gone. Never kill an unverified reused PID.
if marker:
if not marker.get('pid'):
raise GateError('Launch intent has no PID: operator must inspect all runner children and clear marker before signed recovery')
p = subprocess.run(['tasklist', '/FI', f'PID eq {marker["pid"]}', '/FO', 'CSV', '/NH'],
capture_output=True, text=True, timeout=15)
if str(marker['pid']) in p.stdout:
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
+236
View File
@@ -0,0 +1,236 @@
"""Same-login setup and native MXC qualification. Never grants roadmap authorization."""
import csv
from datetime import datetime, timezone, timedelta
import hashlib
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import sys
import uuid
PROFILE = 'jobjakt-local'
CATEGORIES = {'configuration', 'approvals', 'key-file', 'personal', 'normal-codex', 'other-repository', 'production-canary'}
def single(config):
return config.get('mode') == 'single-user-local'
def detect_identity():
result = subprocess.run(['whoami','/user','/fo','csv','/nh'],capture_output=True,text=True,check=True,timeout=15)
username,sid = next(csv.reader(result.stdout.splitlines()))
return username,sid
def validate_identity(config, sid):
if not sid.startswith('S-1-') or config['ownerSid'] != sid or config['runnerSid'] != sid:
raise ValueError('single-user-local requires the configured current Windows identity for both owner and runner')
def validate_config(root, config):
root = Path(root).resolve()
repo = Path(config['repository']).resolve()
auth = Path(config['workerCodeXHome']).resolve()
normal = Path(config['normalCodeXHome']).resolve()
temporary = Path(config['workerTemp']).resolve()
if root.is_relative_to(repo) or repo.is_relative_to(root):
raise ValueError('ControlRoot and worker must be disjoint')
if auth == normal or auth.is_relative_to(repo) or not auth.is_relative_to(root):
raise ValueError('Dedicated auth-only CODEX_HOME must be inside ControlRoot, outside worker, distinct from normal auth')
if (temporary.is_relative_to(root) or temporary.is_relative_to(normal) or temporary == repo
or root.is_relative_to(temporary) or normal.is_relative_to(temporary)):
raise ValueError('Use a separate disposable worker temp directory')
for name in config.get('toolReadRoots',[]):
tool=Path(name).resolve()
if any(tool.is_relative_to(private) or private.is_relative_to(tool) for private in (root,normal)):
raise ValueError('Tool roots cannot reopen protected control or normal Codex paths')
if config.get('trustedCodexOverrides'):
raise ValueError('single-user-local plugin/MCP access is disabled until independently qualified')
if config.get('executionPolicy') not in ('operator-controlled','qualified-unattended'):
raise ValueError('Unknown single-user execution policy')
for name in ('config.toml','plugins','skills','hooks.json','AGENTS.md'):
if (auth / name).exists():
raise ValueError('Auth-only CODEX_HOME contains unrelated config, hooks, skills or plugins')
for name in ('.codex/config.toml','.mcp.json'):
if (repo / name).exists():
raise ValueError('Worker has an unqualified repository Codex/MCP configuration')
def filesystem_rules(root, config):
repo = Path(config['repository']).resolve()
rules = {':root':'deny', ':minimal':'read', repo.as_posix():'write',
Path(config['workerTemp']).resolve().as_posix():'write'}
for name in config.get('toolReadRoots',[]):
path = Path(name).resolve()
if path == Path(path.anchor) or path == Path.home():
raise ValueError('Tool read roots cannot grant an entire drive or home directory')
rules[path.as_posix()] = 'read'
for path in (Path(root),Path(config['workerCodeXHome']),Path(config['normalCodeXHome']),
Path.home()/'.ssh',Path.home()/'AppData/Roaming/Microsoft/Crypto'):
rules[path.resolve().as_posix()] = 'deny'
for name in ('.env','.env.local','.env.production','appsettings.Production.json'):
rules[(repo/name).as_posix()] = 'deny'
rules[(repo/'.git').as_posix()] = 'read'
return rules
def permission_args(root, config, command):
validate_config(root,config)
table = '{' + ','.join(json.dumps(k)+'='+json.dumps(v) for k,v in filesystem_rules(root,config).items()) + '}'
args = ['-c','windows.sandbox="mxc"','-c','approval_policy="never"',
'-c','cli_auth_credentials_store="file"','-c','forced_login_method="chatgpt"',
'-c','features.apps=false','-c','features.plugins=false','-c','features.remote_plugin=false',
'-c','mcp_servers={}','-c','plugins={}','-c','web_search="disabled"',
'-c',f'default_permissions="{PROFILE}"',
'-c',f'permissions.{PROFILE}.filesystem={table}',
'-c',f'permissions.{PROFILE}.network.enabled=false']
if command == 'sandbox':
args += ['--include-managed-config','--permission-profile',PROFILE,'--']
return args
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'):
source = Path(root)/name
if source.exists():
value[name+'Sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
return hashlib.sha256(json.dumps(value,sort_keys=True).encode()).hexdigest()
def probes_passed(result):
files = result.get('files',[])
return (result.get('positiveControl') is True and result.get('networkDenied') is True
and result.get('signingKeyDenied') is True
and {f.get('category') for f in files} == CATEGORIES
and all(f.get('readDenied') is True and f.get('writeDenied') is True for f in files))
def require_unattended(root,config):
validate_config(root,config)
if config['executionPolicy'] != 'qualified-unattended':
raise ValueError('operator-controlled pause: no autonomous worker is launched; inspect requests and qualify MXC first')
path = Path(root)/'qualification.json'
if not path.is_file():
raise ValueError('Native sandbox qualification is missing')
report = json.loads(path.read_text(encoding='utf-8'))
issued = datetime.fromisoformat(report['issuedUtc'])
now = datetime.now(timezone.utc)
if (report.get('status') != 'PASS' or report.get('failure')
or report.get('binding') != binding(root,config) or not probes_passed(report.get('probes',{}))
or report.get('ownerKeyDenied') is not True
or issued > now+timedelta(minutes=5) or now-issued > timedelta(hours=24)):
raise ValueError('Native sandbox/key qualification failed, expired or changed; operator-controlled pause required')
def configure_current_user(root,config):
username,sid = detect_identity()
root = Path(root).resolve()
normal = Path(os.environ.get('CODEX_HOME',str(Path.home()/'.codex'))).resolve()
config.update(mode='single-user-local',executionPolicy='operator-controlled',
windowsUsername=username,ownerSid=sid,runnerSid=sid,normalCodeXHome=str(normal),
workerCodeXHome=str(root/'auth'), workerTemp=str(root.parent/(root.name+'-worker-temp')),
codexExecutable=shutil.which('codex') or 'codex',gitExecutable=shutil.which('git') or 'git',
powershellExecutable=shutil.which('pwsh') or 'pwsh',pythonExecutable=sys.executable,
trustedCodexOverrides=[],toolReadRoots=sorted({str(Path(sys.executable).parent),
str(Path(shutil.which('pwsh') or sys.executable).parent)}), ownerCertificateThumbprint=None)
validate_config(root,config)
Path(config['workerCodeXHome']).mkdir(parents=True,exist_ok=True)
Path(config['workerTemp']).mkdir(parents=True,exist_ok=True)
return config
def auth_command(config, status=False):
args = [config['codexExecutable'],'-c','cli_auth_credentials_store="file"',
'-c','forced_login_method="chatgpt"','login']
return args + (['status'] if status else [])
def qualify(engine):
"""Only synthetic canaries and a local TCP listener. No model or production contact."""
root,config = engine.root,engine.config
validate_config(root,config)
_,sid = detect_identity()
validate_identity(config,sid)
token = uuid.uuid4().hex
workspace = engine.repo/'.artifacts/orchestrator-probes'/token
outside = root.parent/('orchestrator-probes-'+token)
normal = Path(config['normalCodeXHome'])/('.jobjakt-probe-'+token)
workspace.mkdir(parents=True)
outside.mkdir()
paths = []
owned = []
key = None
report = dict(issuedUtc=datetime.now(timezone.utc).isoformat(),binding=binding(root,config),
status='BLOCKED',ownerKeyDenied=False,probes={},failure=None)
listener = socket.socket()
try:
for category in sorted(CATEGORIES):
path = normal if category == 'normal-codex' else (
root/('canary-'+token+'-'+category) if category in ('configuration','approvals','key-file')
else outside/category)
path.write_text('SYNTHETIC-'+token,encoding='utf-8')
owned.append(path)
with path.open('r+b'):
pass # prove a denial is not a missing file or preexisting host ACL failure
paths.append(dict(category=category,path=str(path)))
helper = root/'Probe-JobjaktSandbox.ps1'
setup = subprocess.run([config['powershellExecutable'],'-NoProfile','-File',str(helper),
'-Action','CreateKey','-Tag',token],text=True,capture_output=True,timeout=30)
if setup.returncode:
raise ValueError('Temporary signing-key positive control failed: '+setup.stderr[:250])
key = json.loads(setup.stdout)
owner_key = None
if config.get('ownerCertificateThumbprint'):
positive = subprocess.run([config['powershellExecutable'],'-NoProfile','-File',str(helper),
'-Action','TestOwnerKey','-Thumbprint',config['ownerCertificateThumbprint']],
text=True,capture_output=True,timeout=30)
if positive.returncode:
raise ValueError('Configured owner key is not usable in the trusted operator context')
owner_key = json.loads(positive.stdout)
if owner_key['certificateSha256'] != config['ownerCertificateSha256']:
raise ValueError('Configured owner private key does not match the pinned public certificate')
listener.bind(('127.0.0.1',0))
listener.listen(4)
with socket.create_connection(listener.getsockname(),timeout=2):
pass
accepted,_ = listener.accept()
accepted.close()
plan = dict(files=paths,port=listener.getsockname()[1],key=key,
ownerThumbprint=config.get('ownerCertificateThumbprint'),ownerKeyName=owner_key['keyName'] if owner_key else None)
(workspace/'plan.json').write_text(json.dumps(plan),encoding='utf-8')
shutil.copyfile(helper,workspace/'probe.ps1')
argv = [config['codexExecutable'],'sandbox','-C',str(engine.repo),
*permission_args(root,config,'sandbox'),config['powershellExecutable'],'-NoProfile',
'-File',str(workspace/'probe.ps1'),'-Action','Probe','-PlanPath',str(workspace/'plan.json')]
logs = root/'probe'/token
engine.runner.supervise(engine,argv,logs,45)
probe = json.loads((logs/'stdout.log').read_text(encoding='utf-8'))
report.update(probes=probe,ownerKeyDenied=probe.get('ownerKeyDenied') is True,
status='PASS' if probes_passed(probe) else 'FAIL')
report['scope'] = 'Synthetic filesystem/network/signing canaries; real owner key '+('probed' if owner_key else 'NOT_CONFIGURED')
except Exception as error:
report['failure'] = str(error)[:500]
finally:
listener.close()
if key:
cleanup = subprocess.run([config['powershellExecutable'],'-NoProfile','-File',
str(root/'Probe-JobjaktSandbox.ps1'),'-Action','RemoveKey','-Tag',token,
'-Thumbprint',key['thumbprint']],capture_output=True,text=True,timeout=30)
if cleanup.returncode:
report['status'] = 'BLOCKED'
report['failure'] = 'Canary certificate cleanup failed; inspect the recorded synthetic thumbprint'
for path in owned:
path.unlink(missing_ok=True)
# Only the exact disposable directories created above are removed.
if workspace.resolve().is_relative_to(engine.repo.resolve()/'.artifacts/orchestrator-probes'):
shutil.rmtree(workspace)
if outside.parent.resolve() == root.parent.resolve() and outside.name == 'orchestrator-probes-'+token:
shutil.rmtree(outside)
engine_file = root/'qualification.json'
from controller import atomic
atomic(engine_file,report)
return report
@@ -0,0 +1,12 @@
{
"features": {
"apps": "false",
"plugins": "false",
"remote_plugin": "false"
},
"profile": "jobjakt-local",
"commandNetworkEnabled": false,
"webSearch": "disabled",
"mcpServersConfigured": {},
"verifiedMcpServerCount": 0
}
@@ -0,0 +1,8 @@
{
"file": "automation/tests/test_local_mode.py",
"change": "Remove one trailing space after the full regression run",
"testedSha256": "197a743c3d796679a84825b43354a1abd503b1158883285be3148839c1098083",
"packagedSha256": "524d40ede6379138f729c06876e43b6ef1cb15ee427947f2358a0439cb02ff69",
"pythonAstIdentical": true,
"additionalTestsCounted": 0
}
@@ -0,0 +1 @@
{"binding":"1d4e22171398d8613b854bac311580381a37b3f2684a27120ab2c9ab0338c634","failure":null,"issuedUtc":"2026-10-29T17:11:31.292867+00:00","ownerKeyDenied":false,"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":false,"positiveControl":true,"signingKeyDenied":true},"scope":"Synthetic filesystem/network/signing canaries; real owner key NOT_CONFIGURED","status":"PASS"}
@@ -0,0 +1,19 @@
{
"testsRun": 69,
"failures": [],
"errors": [],
"skipped": [],
"successful": true,
"sourceStable": true,
"scope": "All 48 original controller tests plus single-user policy/disposable subprocess flows; no real roadmap/model",
"sourceHashes": {
"automation/controller.py": "25625cb7e1780d3dba438b6d1b60e0be2d1f3af281af8aecd9eccd51a6ac7018",
"automation/local_mode.py": "e80b33cd0a915cf432d498e4a6f655ede7fb1b6ddf79ab7b1dee1e5a79d33629",
"automation/tests/test_controller.py": "458da3c60571e61aad42987ce156842efd73ef0e2aa0c7bce719d36d626d7e7d",
"automation/tests/test_local_mode.py": "197a743c3d796679a84825b43354a1abd503b1158883285be3148839c1098083",
"automation/tests/fake_codex.py": "d50bc330392fe7aa15f2141f1ca2cde1bad797674ad9bc74304907f5dbe66cb3",
"scripts/Approve-JobjaktBacklog.ps1": "cc941717cd2ae69a0aa14fb4de1d36c1e15f8a26526fb57d2bed307152108aca",
"scripts/Probe-JobjaktSandbox.ps1": "39c0892698bb6ded29aed8e052500ab85b9b04f858a6d4fd245c57a77ec333db",
"scripts/Run-JobjaktBacklog.ps1": "70679acf9ccf76d0ab8edb6fa8faa01a36fc3d384092804515e3e511b0e22814"
}
}
@@ -0,0 +1,12 @@
{
"mode": "single-user-local",
"sameWindowsIdentity": true,
"authentication": "NOT_SIGNED_IN_WITH_CHATGPT",
"tokensCopied": false,
"actualOwnerKeyConfigured": false,
"executionPolicy": "operator-controlled",
"executionEnabled": false,
"realSessions": 0,
"configuredBaseline": "5032ccb41514807c8ca3008c24ca6326be1170de",
"taskRegistration": "REFUSED_UNQUALIFIED_NO_TASK_CREATED"
}
@@ -0,0 +1,12 @@
{
"exitCode": 0,
"cases": [
"PASS valid signature and concrete decision",
"PASS replayed approval on changed commit",
"PASS forged signature",
"PASS expired approval",
"PASS wrong signing owner",
"PASS model-written approval JSON"
],
"scope": "Temporary in-memory test keys/certificates only"
}
@@ -0,0 +1,6 @@
PASS valid signature and concrete decision
PASS replayed approval on changed commit
PASS forged signature
PASS expired approval
PASS wrong signing owner
PASS model-written approval JSON
+74
View File
@@ -0,0 +1,74 @@
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 69 tests in 174.662s
OK
+14 -3
View File
@@ -12,7 +12,10 @@ if args[0] == 'exec':
sys.exit(17)
if Path('simulate-timeout').exists():
time.sleep(60)
if '--sandbox' not in args or args[args.index('--sandbox') + 1] != 'workspace-write':
legacy = '--sandbox' in args and args[args.index('--sandbox') + 1] == 'workspace-write'
local = ('default_permissions="jobjakt-local"' in args and 'windows.sandbox="mxc"' in args
and 'permissions.jobjakt-local.network.enabled=false' in args)
if not (legacy or local):
sys.exit(18)
checkpoint = 'work'
Path('result.txt').write_text('subprocess worker output', encoding='utf-8')
@@ -24,8 +27,16 @@ if args[0] == 'exec':
elif args[0] == 'sandbox':
# Remove known sandbox switches; execute the real deterministic check in the disposable repo.
index = 1
while args[index] in ('-C', '-c'):
index += 2
while index < len(args):
if args[index] in ('-C','-c','--permission-profile'):
index += 2
elif args[index] == '--include-managed-config':
index += 1
elif args[index] == '--':
index += 1
break
else:
break
sys.exit(subprocess.run(args[index:], cwd=Path.cwd()).returncode)
else:
sys.exit(19)
+236
View File
@@ -0,0 +1,236 @@
"""Single-user policy tests; no real credentials, models or production services."""
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
from datetime import datetime,timezone,timedelta
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import local_mode as local
import controller as c
import test_controller as fixture
class LocalPolicyTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='jobjakt-single-user-test-')
self.base = Path(self.temp.name)
self.root = self.base / 'control'
self.repo = self.base / 'worker'
self.repo.mkdir()
self.config = dict(mode='single-user-local', executionPolicy='operator-controlled',
ownerSid='S-1-5-21-100', runnerSid='S-1-5-21-100', repository=str(self.repo),
workerCodeXHome=str(self.root / 'auth'), normalCodeXHome=str(self.base / 'normal-codex'),
workerTemp=str(self.base / 'worker-temp'), toolReadRoots=[], trustedCodexOverrides=[])
def tearDown(self):
self.temp.cleanup()
def test_same_windows_identity_supported(self):
local.validate_identity(self.config, self.config['ownerSid'])
def test_different_or_missing_sid_rejected(self):
with self.assertRaises(ValueError):
local.validate_identity(self.config, 'S-1-5-21-999')
def test_existing_normal_auth_home_is_rejected(self):
self.config['workerCodeXHome'] = self.config['normalCodeXHome']
with self.assertRaisesRegex(ValueError, 'auth'):
local.validate_config(self.root, self.config)
def test_auth_home_inside_worker_is_rejected(self):
self.config['workerCodeXHome'] = str(self.repo / 'auth')
with self.assertRaises(ValueError):
local.validate_config(self.root,self.config)
def test_profile_denies_root_control_auth_and_normal_configuration(self):
rules = local.filesystem_rules(self.root,self.config)
self.assertEqual('deny',rules[':root'])
self.assertEqual('read',rules[':minimal'])
for path in [self.root,Path(self.config['workerCodeXHome']),Path(self.config['normalCodeXHome'])]:
self.assertEqual('deny',rules[path.resolve().as_posix()])
self.assertEqual('write',rules[self.repo.resolve().as_posix()])
self.assertEqual('read',rules[(self.repo / '.git').resolve().as_posix()])
def test_mxc_is_explicit_without_legacy_or_unrestricted_fallback(self):
args = local.permission_args(self.root,self.config,'exec')
self.assertIn('windows.sandbox="mxc"',args)
self.assertIn('permissions.jobjakt-local.network.enabled=false',args)
self.assertNotIn('--sandbox',args)
self.assertNotIn('--dangerously-bypass-approvals-and-sandbox',args)
for value in ('features.apps=false','features.plugins=false','features.remote_plugin=false','mcp_servers={}'):
self.assertIn(value,args)
def test_plugins_are_not_an_implicit_same_user_capability(self):
self.config['trustedCodexOverrides'] = [{'key':'mcp_servers.any.command','toml':'"unsafe"'}]
with self.assertRaisesRegex(ValueError,'plugin'):
local.validate_config(self.root,self.config)
def test_tool_or_temp_roots_cannot_reopen_protected_parent(self):
self.config['toolReadRoots']=[str(self.base)]
with self.assertRaises(ValueError):
local.validate_config(self.root,self.config)
self.config['toolReadRoots']=[]
self.config['workerTemp']=str(self.base)
with self.assertRaises(ValueError):
local.validate_config(self.root,self.config)
def test_operator_controlled_policy_cannot_launch_a_worker(self):
with self.assertRaisesRegex(ValueError,'operator-controlled'):
local.require_unattended(self.root,self.config)
def test_missing_or_stale_qualification_never_enables_unattended(self):
self.config['executionPolicy'] = 'qualified-unattended'
with self.assertRaises(ValueError):
local.require_unattended(self.root,self.config)
def test_probe_summary_rejects_partial_and_failed_boundaries(self):
result = dict(positiveControl=True,networkDenied=True,signingKeyDenied=True,
files=[dict(readDenied=True,writeDenied=False)])
self.assertFalse(local.probes_passed(result))
result['files'][0]['writeDenied'] = True
self.assertFalse(local.probes_passed(result)) # required categories cannot be omitted
def test_same_chatgpt_auth_is_explicit_and_tokens_are_not_copied(self):
normal = Path(self.config['normalCodeXHome'])
normal.mkdir()
(normal/'auth.json').write_text('synthetic-existing-token-do-not-copy')
with mock.patch.object(local,'detect_identity',return_value=('synthetic-user',self.config['ownerSid'])), \
mock.patch.dict(local.os.environ,{'CODEX_HOME':str(normal)}):
config = local.configure_current_user(self.root,dict(self.config))
self.assertEqual(config['ownerSid'],config['runnerSid'])
self.assertEqual([],list(Path(config['workerCodeXHome']).iterdir()))
args = local.auth_command({**config,'codexExecutable':'codex'})
self.assertIn('forced_login_method="chatgpt"',args)
self.assertIn('cli_auth_credentials_store="file"',args)
self.assertNotIn('--with-api-key',args)
self.assertEqual('synthetic-existing-token-do-not-copy',(normal/'auth.json').read_text())
def test_failed_probe_and_unconfigured_owner_key_cannot_qualify(self):
self.root.mkdir()
self.config['executionPolicy']='qualified-unattended'
valid = dict(positiveControl=True,networkDenied=True,signingKeyDenied=True,
files=[dict(category=k,readDenied=True,writeDenied=True) for k in sorted(local.CATEGORIES)])
report = dict(status='PASS',failure=None,binding=local.binding(self.root,self.config),issuedUtc=datetime.now(timezone.utc).isoformat(),probes=valid,ownerKeyDenied=False)
c.atomic(self.root/'qualification.json',report)
with self.assertRaises(ValueError):
local.require_unattended(self.root,self.config)
report['ownerKeyDenied']=True
report['probes']['networkDenied']=False
c.atomic(self.root/'qualification.json',report)
with self.assertRaises(ValueError):
local.require_unattended(self.root,self.config)
report['probes']['networkDenied']=True
c.atomic(self.root/'qualification.json',report)
local.require_unattended(self.root,self.config)
report['issuedUtc']=(datetime.now(timezone.utc)-timedelta(days=2)).isoformat()
c.atomic(self.root/'qualification.json',report)
with self.assertRaises(ValueError):
local.require_unattended(self.root,self.config)
def test_blocked_status_rejects_otherwise_successful_probes(self):
self.root.mkdir()
self.config['executionPolicy']='qualified-unattended'
c.atomic(self.root/'qualification.json',dict(status='BLOCKED',failure='cleanup failed',
binding=local.binding(self.root,self.config),issuedUtc=datetime.now(timezone.utc).isoformat(),ownerKeyDenied=True,
probes=dict(positiveControl=True,networkDenied=True,signingKeyDenied=True,
files=[dict(category=k,readDenied=True,writeDenied=True) for k in sorted(local.CATEGORIES)])))
with self.assertRaises(ValueError):
local.require_unattended(self.root,self.config)
def test_noninteractive_approval_is_refused(self):
c.atomic(self.root/'config.json',self.config)
c.atomic(self.root/'backlog.json',{})
with mock.patch.object(c.sys.stdin,'isatty',return_value=False):
with self.assertRaisesRegex(c.GateError,'interactive terminal'):
c.main(['approve','--control-root',str(self.root)])
class LocalFlowTests(unittest.TestCase):
tearDown=fixture.EngineTests.tearDown
git_at=staticmethod(fixture.EngineTests.git_at)
run_engine=fixture.EngineTests.run_engine
def setUp(self):
fixture.EngineTests.setUp(self)
self.config.update(mode='single-user-local',executionPolicy='qualified-unattended',
ownerSid='S-1-5-21-100',runnerSid='S-1-5-21-100',
normalCodeXHome=str(self.root/'normal-codex'),workerCodeXHome=str(self.control/'auth'),
workerTemp=str(self.root/'worker-temp'),toolReadRoots=[],trustedCodexOverrides=[])
Path(self.config['workerCodeXHome']).mkdir()
Path(self.config['workerTemp']).mkdir()
c.atomic(self.control/'qualification.json',dict(status='PASS',failure=None,binding=local.binding(self.control,self.config),
issuedUtc=datetime.now(timezone.utc).isoformat(),ownerKeyDenied=True,
probes=dict(positiveControl=True,networkDenied=True,signingKeyDenied=True,
files=[dict(category=k,readDenied=True,writeDenied=True) for k in sorted(local.CATEGORIES)])))
def test_single_user_subprocess_protocol_and_ordered_publication(self):
self.engine.runner=fixture.ProtocolRunner()
self.assertEqual('COMPLETED',self.run_engine()['stages']['TWO']['status'])
def test_single_user_missing_approval_stops_before_worker(self):
self.engine.approval_verifier=lambda request:False
self.assertEqual('AWAITING_APPROVAL',self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0,self.runner.calls)
def test_single_user_pause_and_publication_recovery(self):
self.engine.pause()
self.run_engine()
self.assertEqual(0,self.runner.calls)
self.engine.resume()
publish=self.engine.publish
self.engine.publish=lambda *args: (_ for _ in ()).throw(c.PublicationError('synthetic outage'))
self.assertEqual('BLOCKED',self.run_engine()['stages']['ONE']['status'])
self.assertEqual(1,self.runner.calls)
self.engine.publish=publish
self.assertEqual('COMPLETED',self.run_engine()['stages']['TWO']['status'])
self.assertEqual(2,self.runner.calls)
def test_single_user_concurrent_controller_and_interrupted_guard(self):
with c.RepoLock(self.repo):
with self.assertRaises(c.GateError):
self.run_engine()
state=self.engine.store.read()
state['stages']['ONE']['status']='RUNNING'
self.engine.store.write(state)
with self.assertRaises(c.GateError):
self.run_engine()
def test_single_user_main_checkout_is_still_prohibited(self):
self.git_at(self.repo,'switch','-c','main')
self.assertEqual('BLOCKED',self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0,self.runner.calls)
def test_pending_recovery_can_be_signed_from_idle_operator_terminal(self):
self.config['ownerCertificateThumbprint']='synthetic-owner-key'
c.atomic(self.control/'config.json',self.config)
c.atomic(self.control/'backlog.json',self.manifest)
state=self.engine.store.read()
state['stages']['ONE']['status']='RUNNING'
self.engine.store.write(state)
self.engine.approval_verifier=lambda r:False
def answer(prompt):
if prompt.startswith('Type '):
return prompt.split()[1]
if prompt.startswith('Disposition'):
return 'Approve'
return 'Operator inspected the stopped synthetic process tree'
with mock.patch.object(c,'Engine',return_value=self.engine):
self.assertEqual(3,c.main(['recover','--control-root',str(self.control)]))
run=c.subprocess.run
def operator_command(argv,**kwargs):
return mock.Mock(returncode=0) if 'Sign' in argv else run(argv,**kwargs)
with mock.patch.object(c.sys.stdin,'isatty',return_value=True), \
mock.patch.object(local,'detect_identity',return_value=('synthetic-user',self.config['ownerSid'])), \
mock.patch('builtins.input',side_effect=answer), \
mock.patch.object(c.subprocess,'run',side_effect=operator_command) as signed:
self.assertEqual(0,c.main(['approve','--control-root',str(self.control)]))
self.assertIn('Sign',signed.call_args.args[0])
self.assertTrue(self.engine.store.read()['recoveryRequired']) # signing alone never marks recovery complete
if __name__ == '__main__':
unittest.main(verbosity=2)