Compare commits

...

3 Commits

48 changed files with 9291 additions and 811 deletions
+31
View File
@@ -0,0 +1,31 @@
"""Read-only verification of a trusted, source-bound assessment receipt; no services."""
import hashlib
import json
from pathlib import Path
def verify(repository, receipt_path, expected_sha256):
repository = Path(repository).resolve()
raw = Path(receipt_path).read_bytes()
if len(raw) > 8 * 1024 * 1024 or hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError('Assessment receipt differs from the protected manifest pin')
receipt = json.loads(raw)
if (receipt.get('schemaVersion') != 1 or receipt.get('scope') != 'P10-00-ASSESSMENT-ONLY'
or receipt.get('integrity') != 'PASS' or not receipt.get('files')
or not receipt.get('nativeReports') or not receipt.get('retainedDecisions')):
raise ValueError('Incomplete assessment qualification receipt')
for name, expected in receipt['files'].items():
path = (repository / name).resolve()
if not path.is_relative_to(repository) or not path.is_file():
raise ValueError('Frozen assessment source missing or outside repository: ' + name)
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
raise ValueError('Frozen assessment source changed; fresh affected qualification required: ' + name)
evidence_root = Path(receipt_path).resolve().parent
for name, expected in receipt.get('evidenceFiles', {}).items():
path = (evidence_root / name).resolve()
if not path.is_relative_to(evidence_root) or not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected:
raise ValueError('Accepted handoff evidence changed or missing: ' + name)
return dict(kind='REUSED_SOURCE_BOUND_EVIDENCE', receiptSha256=expected_sha256,
nativeReports=receipt['nativeReports'],
retainedDecisions=receipt['retainedDecisions'],
limits='P10-00 assessment only. No fresh product runs or implementation qualification.')
+273 -26
View File
@@ -18,6 +18,8 @@ import tempfile
import time
import uuid
import xml.etree.ElementTree as ET
import local_mode
import assessment_adapter
class GateError(RuntimeError):
@@ -82,6 +84,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:
@@ -154,6 +166,13 @@ def validate_manifest(m, root=None):
if check['id'] in check_ids or not check.get('argv'):
raise GateError('Duplicate check or empty test command')
check_ids.add(check['id'])
if check.get('adapter') == 'assessment-evidence-inspection':
if not m.get('executionScope') or not re.fullmatch(r'[0-9a-f]{64}',check.get('receiptSha256','')):
raise GateError('Assessment adapter requires a restricted policy and pinned receipt')
if check.get('configured') is not False:
raise GateError('Evidence inspection cannot qualify the command-running composite adapter')
if root and sha_file(under(root,check['receipt'])) != check['receiptSha256']:
raise GateError('Assessment qualification receipt changed')
if not all(isinstance(a, str) and '\x00' not in a for a in check['argv']):
raise GateError('Command arguments must be strings')
if check.get('report') and check['report']['format'] not in ('trx', 'jest', 'playwright', 'junit'):
@@ -169,6 +188,17 @@ def validate_manifest(m, root=None):
raise GateError('Supplemental prompt missing/changed')
if sequences != sorted(set(sequences)):
raise GateError('Queue sequence must be unique and ascending')
scope = m.get('executionScope')
if scope:
stage = next((s for s in m['stages'] if s['id'] == scope.get('stage')), None)
cp = next((x for x in stage['checkpoints'] if x['id'] == scope.get('checkpoint')), None) if stage else None
if (scope.get('mode') != 'assessment-only' or scope.get('stage') != 'PHASE-10'
or scope.get('checkpoint') != 'P10-00' or not cp
or scope.get('reportDirectory') != 'docs/automation/stages/PHASE-10'
or 'candidate-specific-cleanup-scope' not in cp['approvalsAfter']):
raise GateError('Assessment policy requires an exact checkpoint and protected stop')
if not re.fullmatch(r'[0-9a-f]{40}', scope.get('handoffCommit','')) or not SAFE_BRANCH.fullmatch(scope.get('handoffBranch','')):
raise GateError('Invalid published handoff identity')
class StateStore:
@@ -398,6 +428,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 +437,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 +539,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 +572,10 @@ class ProcessRunner:
return data
def run_check(self, engine, check, directory):
if check.get('adapter') == 'assessment-evidence-inspection':
raise GateError('Evidence inspection cannot replace command-running composite qualification')
if local_mode.single(engine.config):
local_mode.require_unattended(engine.root,engine.config)
working = engine.repo if check['cwd'] == '.' else under(engine.repo, check['cwd'])
report_spec = check.get('report')
report = None
@@ -535,8 +583,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:
@@ -555,6 +602,9 @@ class Engine:
self.root = Path(root).resolve()
self.manifest = manifest
self.config = config
if manifest.get('executionScope'):
self.config = dict(config)
self.config['assessmentReportDirectory'] = manifest['executionScope']['reportDirectory']
self.repo = Path(config['repository']).resolve()
self.store = StateStore(self.root)
self.runner = runner or ProcessRunner()
@@ -563,7 +613,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'})
@@ -595,6 +645,8 @@ class Engine:
if kind in ('environment-qualified', 'roadmap-execution'):
request.update(stage='QUEUE', checkpoint='POLICY', baselineCommit=self.manifest['baselineCommit'],
candidateCommit=None, evidence=[], detail='Entire exact manifest; development only; no production authority')
if self.manifest.get('executionScope'):
request['executionScope'] = self.manifest['executionScope']
path = self.root / 'requests' / (digest(request) + '.json')
atomic(path, request)
approval = self.approval_verifier(request)
@@ -630,7 +682,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', '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):
@@ -652,6 +705,12 @@ 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)
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')
@@ -660,7 +719,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 +738,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.
@@ -764,9 +828,17 @@ class Engine:
if ss['entryBaseline'] is None:
if self.git('status', '--porcelain', '--untracked-files=all'):
raise GateError('New stage requires a clean worktree, including untracked files')
self.verify_remote(self.manifest['baselineBranch'] if not any(
x['status'] == 'COMPLETED' for x in state['stages'].values())
else self.manifest['integrationBranch'], state['integrationCommit'])
scope = self.manifest.get('executionScope')
if scope and not any(x['status'] == 'COMPLETED' for x in state['stages'].values()):
self.verify_remote(scope['handoffBranch'], scope['handoffCommit'])
self.git('merge-base', '--is-ancestor', state['integrationCommit'], scope['handoffCommit'])
changed = self.git('diff','--name-only',state['integrationCommit'],scope['handoffCommit']).splitlines()
if any(not name.startswith('docs/automation/handoffs/pre-phase10/') for name in changed):
raise GateError('Published handoff changed outside the evidence envelope')
else:
self.verify_remote(self.manifest['baselineBranch'] if not any(
x['status'] == 'COMPLETED' for x in state['stages'].values())
else self.manifest['integrationBranch'], state['integrationCommit'])
baseline = state['integrationCommit']
branches = self.git('for-each-ref', '--format=%(refname:short)', 'refs/heads').splitlines()
if stage['branch'] in branches:
@@ -887,7 +959,8 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
return False
ss.pop('requestedDecision')
prompt = self.prompt(stage, cp, ss, state)
if not cp['checks'] or any(not check.get('configured', True) for check in cp['checks']):
if not cp['checks'] or any(check.get('adapter') == 'assessment-evidence-inspection'
or not check.get('configured', True) for check in cp['checks']):
raise GateError('Independent test adapters must be configured and owner-qualified before execution')
ss['attempts'] += 1
ss['sessions'] += 1
@@ -901,6 +974,7 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
sourceCommit=ss['checkpointBase'], manifestSha256=digest(self.manifest), at=utc()))
self.store.write(state) # durable RUNNING before launching child
result = self.runner.run_session(self, stage, cp, directory, prompt)
self.verify_assessment_writes()
ss['lastResult'] = result
state['recentSessions'].append(dict(id=session_id, stage=stage['id'], checkpoint=cp['id'],
outcome=result['outcome'], summary=redact(result['summary'])[:600], directory=str(directory)))
@@ -927,6 +1001,7 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
ss['verifiedChecks'] = []
for check in cp['checks']:
ss['verifiedChecks'].append(self.runner.run_check(self, check, directory / 'checks' / check['id']))
self.verify_assessment_writes()
ss['evidence'] = []
for name in cp['requiredArtifacts']:
artifact = under(self.repo, name)
@@ -962,6 +1037,17 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
self.store.write(state)
return self.finish_checkpoint(stage, cp, ss, state)
def verify_assessment_writes(self):
scope = self.manifest.get('executionScope')
if not scope:
return
prefix = scope['reportDirectory'] + '/'
changed = self.git('diff', '--name-only', 'HEAD', '-z').split('\0')
changed += self.git('ls-files', '--others', '--exclude-standard', '-z').split('\0')
deleted = self.git('diff', '--name-only', '--diff-filter=D', 'HEAD')
if deleted or any(name and (not name.startswith(prefix) or not name.endswith('.md')) for name in changed):
raise GateError('Read-only assessment modified a forbidden path or deleted a file')
def content_hash(self):
# Git blob hashes include tracked modifications and untracked source without reading secrets into logs.
files = self.git('ls-files', '--cached', '--others', '--exclude-standard', '-z').split('\0')
@@ -1050,6 +1136,9 @@ checkpoint; complete means ready for independent checks. Do not infer acceptance
self.store.write(state)
return
try:
scope = self.manifest.get('executionScope')
if scope and (stage['id'], cp['id']) != (scope['stage'], scope['checkpoint']):
raise GateError('Restricted assessment scope ends after P10-00; new owner-scoped policy required')
# Check source before requesting approval or modifying Git.
self.prompt(stage, cp, ss, state)
if ss['entryBaseline'] is None:
@@ -1126,14 +1215,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 +1232,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', '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()
@@ -1157,23 +1252,179 @@ 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','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 == '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','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 ('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-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)
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 +1489,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
+245
View File
@@ -0,0 +1,245 @@
"""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()
assessment = config.get('assessmentReportDirectory')
rules = {':root':'deny', ':minimal':'read', repo.as_posix():'read' if assessment else 'write',
Path(config['workerTemp']).resolve().as_posix():'write'}
if assessment:
if assessment != 'docs/automation/stages/PHASE-10':
raise ValueError('Invalid assessment report directory')
for name in (assessment, '.artifacts/orchestrator-probes'):
path = (repo/name).resolve()
if not path.is_relative_to(repo):
raise ValueError('Assessment output escapes repository')
rules[path.as_posix()] = 'write'
for name in config.get('toolReadRoots',[]):
path = Path(name).resolve()
if path == Path(path.anchor) or path == Path.home():
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','assessment_adapter.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
+61
View File
@@ -0,0 +1,61 @@
// Trusted operator preparation. Validates published native evidence; runs no product suite.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
import { pathToFileURL } from 'node:url';
import { execFileSync } from 'node:child_process';
const [repository, destination] = process.argv.slice(2);
if (!repository || !destination) throw new Error('Usage: node prepare_assessment.mjs <published-handoff-checkout> <new-output-directory>');
const root = fs.realpathSync(repository), output = path.resolve(destination);
if (fs.existsSync(output)) throw new Error('Use a new receipt directory; preserve previous qualification');
const git = (...args) => execFileSync('rtk', ['proxy','git','-C',root,...args], {encoding:'utf8'}).trim();
const handoff = '5cb903b989742b7a96cb22ddf4c0e24993b18980';
const baseline = 'fc5b9d377199ce51f480a920a3772108094929e3';
if (git('rev-parse','HEAD') !== handoff || git('status','--porcelain')) throw new Error('Require the exact clean published handoff checkout');
const remote = git('ls-remote','--exit-code','origin','refs/heads/test/pre-phase10-ux-acceptance');
if (remote.split(/\s/)[0] !== handoff) throw new Error('Handoff publication differs');
if (git('rev-parse',handoff+'^') !== baseline) throw new Error('Baseline relationship differs');
const relative = 'docs/automation/handoffs/pre-phase10';
const directory = path.join(root,relative);
const {validatePackage, measureNative} = await import(pathToFileURL(path.join(directory,'verify.mjs')).href);
const verified = validatePackage();
if (verified.integrity !== 'PASS' || !verified.observedWorkingTreeClean) throw new Error('Handoff integrity failed');
const evidence = JSON.parse(fs.readFileSync(path.join(directory,'TEST_EVIDENCE.json')));
const artifact = id => {
const row=evidence.artifacts.find(item=>item.id===id);
if (!row) throw new Error('Missing native artifact');
const raw=fs.readFileSync(path.join(directory,row.path));
return row.path.endsWith('.gz') ? zlib.gunzipSync(raw) : raw;
};
const hash = raw => crypto.createHash('sha256').update(raw).digest('hex');
const files = {};
// Freeze all tracked non-envelope sources, including tests/config/security/preservation documentation.
for (const name of git('ls-files','-z').split('\0').filter(Boolean)) {
if (!name.startsWith(relative+'/')) files[name]=hash(fs.readFileSync(path.join(root,name)));
}
const nativeReports = evidence.reports.map(report=>{
let observation=JSON.parse(artifact(report.processArtifactId));
const parts=report.processPointer.slice(1).split('/').map(value=>value.replaceAll('~1','/').replaceAll('~0','~'));
for (const key of parts.slice(0,-1)) observation=observation[key];
return {...report, measured:report.nativeFormat ? measureNative(report.nativeFormat,artifact(report.artifactId).toString('utf8')) : null,
nativeProcess:observation};
});
fs.mkdirSync(output,{recursive:true});
const evidenceFiles={};
for (const name of git('ls-files',relative).split(/\r?\n/).filter(Boolean)) {
const local=path.relative(directory,path.join(root,name));
const target=path.join(output,'handoff',local);
fs.mkdirSync(path.dirname(target),{recursive:true});
fs.copyFileSync(path.join(root,name),target);
evidenceFiles['handoff/'+local.split(path.sep).join('/')]=hash(fs.readFileSync(target));
}
const receipt={schemaVersion:1,scope:'P10-00-ASSESSMENT-ONLY',integrity:'PASS',
generatedUtc:new Date().toISOString(),baselineCommit:baseline,handoffCommit:handoff,
testedCommit:evidence.testedCommit,files,evidenceFiles,nativeReports,
retainedDecisions:JSON.parse(fs.readFileSync(path.join(directory,'ACCEPTANCE.json'))).verdicts,
revisedS01:verified.revisedS01,verifierObservation:verified,
limits:'Source-bound reuse for unchanged read-only assessment. Original failures and conditional/blocked decisions retained. No fresh product, security, model, human or production acceptance.'};
fs.writeFileSync(path.join(output,'receipt.json'),JSON.stringify(receipt,null,2)+'\n');
console.log(JSON.stringify({integrity:'PASS',receipt:path.join(output,'receipt.json'),sha256:hash(fs.readFileSync(path.join(output,'receipt.json'))),frozenSources:Object.keys(files).length,nativeReports:nativeReports.length,productSuitesExecuted:0},null,2));
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
"""Synthetic native assessment qualification only. Never starts the development queue."""
import json
from pathlib import Path
import shutil
import sys
import uuid
import controller as c
import local_mode
def main():
root, worker, report_path = map(Path,sys.argv[1:])
config=c.read_json(root/'config.json')
policy=c.read_json(root/'proposed-p10-assessment.json')
config['repository']=str(worker.resolve())
engine=c.Engine(root,policy,config)
if c.read_json(root/'backlog.json')['executionEnabled'] or any(s['sessions'] for s in engine.store.read()['stages'].values()):
raise c.GateError('Qualification requires an unused disabled queue')
with c.RepoLock(worker):
prior=(root/'qualification.json').read_bytes() if (root/'qualification.json').exists() else None
try:
native=local_mode.qualify(engine)
finally:
if prior is not None:
(root/'qualification.json').write_bytes(prior)
else:
(root/'qualification.json').unlink(missing_ok=True)
if native['status'] != 'PASS':
c.atomic(report_path,dict(native=native,actualQueueStarted=False,assessment='NOT_RUN'))
raise c.GateError('Native signing isolation qualification failed; no further child launched')
token=uuid.uuid4().hex
probe=worker/'.artifacts/orchestrator-probes'/token
probe.mkdir(parents=True)
canary=worker/('.assessment-readonly-'+token)
canary.write_text('SYNTHETIC READ-ONLY CANARY')
output=worker/'docs/automation/stages/PHASE-10'/('native-probe-'+token+'.md')
output.parent.mkdir(parents=True,exist_ok=True)
logs=root/'probe'/('assessment-'+token)
receipt=root/'assessment-activation-01'
try:
shutil.copytree(receipt,probe/'receipt')
shutil.copyfile(Path(__file__).with_name('assessment_adapter.py'),probe/'assessment_adapter.py')
plan=dict(worker=str(worker.resolve()),receipt=str(probe/'receipt/receipt.json'),
pin=c.sha_file(receipt/'receipt.json'),canary=str(canary),output=str(output))
c.atomic(probe/'plan.json',plan)
(probe/'run.py').write_text('''import json, pathlib
import assessment_adapter
p=json.loads(pathlib.Path(__file__).with_name('plan.json').read_text())
r=assessment_adapter.verify(p['worker'],p['receipt'],p['pin'])
canary=pathlib.Path(p['canary'])
assert canary.read_text()=='SYNTHETIC READ-ONLY CANARY'
try:
with canary.open('r+b'): pass
except PermissionError: write_denied=True
else: write_denied=False
try: canary.unlink()
except PermissionError: delete_denied=True
else: delete_denied=False
pathlib.Path(p['output']).write_text('Synthetic native report creation')
assert write_denied and delete_denied
print(json.dumps(dict(evidenceIntegrity='PASS',sourceWriteDenied=write_denied,sourceDeleteDenied=delete_denied,reportWrite=True,kind=r['kind'],nativeReports=len(r['nativeReports']))))
''',encoding='utf-8')
argv=[config['codexExecutable'],'sandbox','-C',str(worker),
*local_mode.permission_args(root,engine.config,'sandbox'),
config['pythonExecutable'],'-B',str(probe/'run.py')]
engine.runner.supervise(engine,argv,logs,180)
observation=json.loads((logs/'stdout.log').read_text())
c.atomic(report_path,dict(native=native,assessment=observation,
candidateCommit=engine.git('rev-parse','HEAD'),actualQueueStarted=False,
compositeRuntime='BLOCKED_DOCKER_AND_SERVICE_BOUNDARY_UNQUALIFIED',logs=str(logs)))
finally:
canary.unlink(missing_ok=True)
output.unlink(missing_ok=True)
if probe.resolve().is_relative_to(worker.resolve()/'.artifacts/orchestrator-probes'):
shutil.rmtree(probe)
print('Synthetic native assessment checks complete; composite command runtime remains BLOCKED')
if __name__=='__main__':
main()
@@ -0,0 +1 @@
{"actualQueueStarted":false,"assessment":{"evidenceIntegrity":"PASS","kind":"REUSED_SOURCE_BOUND_EVIDENCE","nativeReports":34,"reportWrite":true,"sourceDeleteDenied":true,"sourceWriteDenied":true},"bindingMatchesProjectedProfile":true,"candidateCommit":"fc5b9d377199ce51f480a920a3772108094929e3","compositeRuntime":"BLOCKED_DOCKER_AND_SERVICE_BOUNDARY_UNQUALIFIED","logs":"C:\\Users\\Connor\\.jobjakt-backlog\\single-user-local\\probe\\assessment-b2c85d9a53474a7db172c73969586734","native":{"binding":"3b1ec3040285e5991dac5fc5e60cb362f7ea54691b9f790a6a496aadc12ec5db","failure":null,"issuedUtc":"2026-10-30T08:35:40.581635+00:00","ownerKeyDenied":true,"probes":{"files":[{"category":"approvals","readDenied":true,"writeDenied":true},{"category":"configuration","readDenied":true,"writeDenied":true},{"category":"key-file","readDenied":true,"writeDenied":true},{"category":"normal-codex","readDenied":true,"writeDenied":true},{"category":"other-repository","readDenied":true,"writeDenied":true},{"category":"personal","readDenied":true,"writeDenied":true},{"category":"production-canary","readDenied":true,"writeDenied":true}],"networkDenied":true,"ownerKeyDenied":true,"positiveControl":true,"signingKeyDenied":true},"scope":"Synthetic filesystem/network/signing canaries; real owner key probed","status":"PASS"}}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"activeProcessMarker":false,"actualSessions":0,"baselineCommit":"fc5b9d377199ce51f480a920a3772108094929e3","executionEnabled":false,"executionPolicy":"operator-controlled","executionWorktree":"C:\\Users\\Connor\\.codex\\worktrees\\p10-assessment\\Job tracker","handoffCommit":"5cb903b989742b7a96cb22ddf4c0e24993b18980","installedRepository":"C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker","installedSourceMatches":true,"materialisedLineEndingOnlyFiles":47,"nativeQualification":{"binding":"b1bdcfd929af4829d316481c376e8c92d1b3182b61199080d2f915279f51ec7f","failure":null,"issuedUtc":"2026-10-30T08:35:24.011490+00:00","ownerKeyDenied":true,"probes":{"files":[{"category":"approvals","readDenied":true,"writeDenied":true},{"category":"configuration","readDenied":true,"writeDenied":true},{"category":"key-file","readDenied":true,"writeDenied":true},{"category":"normal-codex","readDenied":true,"writeDenied":true},{"category":"other-repository","readDenied":true,"writeDenied":true},{"category":"personal","readDenied":true,"writeDenied":true},{"category":"production-canary","readDenied":true,"writeDenied":true}],"networkDenied":true,"ownerKeyDenied":true,"positiveControl":true,"signingKeyDenied":true},"scope":"Synthetic filesystem/network/signing canaries; real owner key probed","status":"PASS"},"ownerKeyConfigured":true,"pending":[{"kind":"manifest-policy-update","request":"C:\\Users\\Connor\\.jobjakt-backlog\\single-user-local\\requests\\d637caf72c3fed5910f7c28e75dc82324d8febc12597ddb5627ae022af49a622.json"}],"publicCertificatePinMatches":true,"qualificationBindingMatches":true,"recoveryRequired":false}
@@ -0,0 +1 @@
{"exitCode":2,"modelSessionsAfter":0,"modelSessionsBefore":0,"stderr":"Adopt the signed restricted assessment policy first\n","stdout":""}
@@ -0,0 +1 @@
{"clean":true,"executionBranch":"codex/p10-assessment-ready","executionCommit":"fc5b9d377199ce51f480a920a3772108094929e3","handoffChanges":["docs/automation/handoffs/pre-phase10/ACCEPTANCE.json","docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL.json","docs/automation/handoffs/pre-phase10/TEST_EVIDENCE.json","docs/automation/handoffs/pre-phase10/evidence/p10-entry-proposal.gz","docs/automation/handoffs/pre-phase10/evidence/p10-final-gate-processes.gz","docs/automation/handoffs/pre-phase10/evidence/p10-proposal-check-source.gz","docs/automation/handoffs/pre-phase10/evidence/p10-protected-entry-native.gz"],"handoffParent":"fc5b9d377199ce51f480a920a3772108094929e3","policyFileMatchesInstalledProposal":true,"proposedManifestSha256":"0ff54dadbae7dde08e68c247e1365fd54f063e5b09066cb9e9cd577afe11a061","remoteHeads":["140e182516e86c8718054cf0907593fcd6a38aa7\trefs/heads/codex/autonomous-roadmap-single-user","5cb903b989742b7a96cb22ddf4c0e24993b18980\trefs/heads/test/pre-phase10-ux-acceptance"]}
@@ -0,0 +1,21 @@
{
"tests": 81,
"failures": 0,
"errors": 0,
"skipped": 0,
"sourceStable": true,
"sourceHashes": {
"automation/assessment_adapter.py": "6f6b4e7b96f1e9ca896e1d2a9ab26c12bf38d603dbf087acf61969f5d3906a21",
"automation/controller.py": "f2c05a3bccb43cc81f76e35e7adefd2e590934ae99f2a1fdc9206844e8732d88",
"automation/local_mode.py": "ccf4a5e8c208d17d87b47dd1aca8ebf540af27a274ae7333da9fb8075cb9b5b2",
"automation/prepare_queue.py": "6cad22305bc354ccbae2f031cb2b324596b3a7d42503b954c27f1e63e47a5f7d",
"automation/qualify_assessment.py": "0891520943779da0ff722d8deba62151d5c1dfad1329026310baab216459374d",
"automation/prepare_assessment.mjs": "a13f3d0458d7e4006cdfc07c3de2d563a6795a8cd4421be2b7de36fb70d68c10",
"automation/tests/test_assessment.py": "d3d30aaa808ead6927a444c1f91aa583835d3a787893b8f67db6b68261aa243c",
"automation/tests/test_controller.py": "458da3c60571e61aad42987ce156842efd73ef0e2aa0c7bce719d36d626d7e7d",
"automation/tests/test_local_mode.py": "524d40ede6379138f729c06876e43b6ef1cb15ee427947f2358a0439cb02ff69",
"scripts/Run-JobjaktBacklog.ps1": "2b11f5016560890262f0ca96bdba5a13240b0250c6bf6a96b43c33929cbcd070"
},
"elapsedSeconds": 161.283,
"scope": "Synthetic controller and adapter tests; no actual backlog or product suite"
}
@@ -0,0 +1,5 @@
{
"constraints": "PASS",
"metaSchemaValidation": "NOT_PERFORMED_DRAFT_2020_META_SCHEMA_UNAVAILABLE",
"errors": null
}
+86
View File
@@ -0,0 +1,86 @@
test_assessment_profile_keeps_product_read_only (test_assessment.ReadOnlyProfileTests.test_assessment_profile_keeps_product_read_only) ... ok
test_receipt_rejects_changed_product_or_changed_receipt (test_assessment.ReceiptTests.test_receipt_rejects_changed_product_or_changed_receipt) ... ok
test_assessment_reaches_protected_stop_and_cannot_advance (test_assessment.ScopeTests.test_assessment_reaches_protected_stop_and_cannot_advance) ... ok
test_assessment_reports_are_permitted (test_assessment.ScopeTests.test_assessment_reports_are_permitted) ... ok
test_entry_cannot_prepare_approvals_without_composite_adapter (test_assessment.ScopeTests.test_entry_cannot_prepare_approvals_without_composite_adapter) ... ok
test_evidence_inspection_cannot_be_marked_runtime_qualified (test_assessment.ScopeTests.test_evidence_inspection_cannot_be_marked_runtime_qualified) ... ok
test_generic_approval_cannot_launch_next_checkpoint (test_assessment.ScopeTests.test_generic_approval_cannot_launch_next_checkpoint) ... ok
test_inspection_requires_explicit_false_and_never_launches_worker (test_assessment.ScopeTests.test_inspection_requires_explicit_false_and_never_launches_worker) ... ok
test_native_isolation_failure_stops_before_additional_probe (test_assessment.ScopeTests.test_native_isolation_failure_stops_before_additional_probe) ... ok
test_policy_cannot_relabel_p10_01_as_assessment (test_assessment.ScopeTests.test_policy_cannot_relabel_p10_01_as_assessment) ... ok
test_scope_rejects_product_write_and_deletion (test_assessment.ScopeTests.test_scope_rejects_product_write_and_deletion) ... ok
test_scope_requires_protected_stop (test_assessment.ScopeTests.test_scope_requires_protected_stop) ... ok
test_accidental_main_checkout_rejected (test_controller.EngineTests.test_accidental_main_checkout_rejected) ... ok
test_after_approval_binds_tested_commit_without_rerunning_worker (test_controller.EngineTests.test_after_approval_binds_tested_commit_without_rerunning_worker) ... ok
test_changed_prompt_blocks (test_controller.EngineTests.test_changed_prompt_blocks) ... ok
test_conditional_can_advance_only_with_explicit_signed_deferral (test_controller.EngineTests.test_conditional_can_advance_only_with_explicit_signed_deferral) ... ok
test_conditional_requires_explicit_deferrable_gates (test_controller.EngineTests.test_conditional_requires_explicit_deferrable_gates) ... ok
test_corrupt_state_recovers_last_good_without_success (test_controller.EngineTests.test_corrupt_state_recovers_last_good_without_success) ... ok
test_cryptographically_approved_resume_e2e (test_controller.EngineTests.test_cryptographically_approved_resume_e2e) ... ok
test_dependency_cannot_be_bypassed (test_controller.EngineTests.test_dependency_cannot_be_bypassed) ... ok
test_design_revision_reruns_current_checkpoint_and_keeps_owner_selection (test_controller.EngineTests.test_design_revision_reruns_current_checkpoint_and_keeps_owner_selection) ... ok
test_disabled_manifest_never_executes (test_controller.EngineTests.test_disabled_manifest_never_executes) ... ok
test_double_corruption_fails_closed (test_controller.EngineTests.test_double_corruption_fails_closed) ... ok
test_duplicate_lock_rejected (test_controller.EngineTests.test_duplicate_lock_rejected) ... ok
test_execution_window_stops_before_worker (test_controller.EngineTests.test_execution_window_stops_before_worker) ... ok
test_failed_stage_stops_and_exhausts_retry_budget (test_controller.EngineTests.test_failed_stage_stops_and_exhausts_retry_budget) ... ok
test_failing_checks_never_commit_or_advance (test_controller.EngineTests.test_failing_checks_never_commit_or_advance) ... ok
test_interruption_is_not_success_and_resume_has_budget (test_controller.EngineTests.test_interruption_is_not_success_and_resume_has_budget) ... ok
test_manifest_initially_disabled (test_controller.EngineTests.test_manifest_initially_disabled) ... ok
test_missing_approval_does_not_run_model (test_controller.EngineTests.test_missing_approval_does_not_run_model) ... ok
test_missing_prompt_blocks (test_controller.EngineTests.test_missing_prompt_blocks) ... ok
test_model_cannot_approve_itself (test_controller.EngineTests.test_model_cannot_approve_itself) ... ok
test_pause_persists_and_resume_clears_it (test_controller.EngineTests.test_pause_persists_and_resume_clears_it) ... ok
test_push_failure_preserves_tested_commit (test_controller.EngineTests.test_push_failure_preserves_tested_commit) ... ok
test_push_verification_mismatch_stops (test_controller.EngineTests.test_push_verification_mismatch_stops) ... ok
test_recovered_running_without_marker_requires_reconciliation (test_controller.EngineTests.test_recovered_running_without_marker_requires_reconciliation) ... ok
test_restart_running_marker_cannot_mean_completed (test_controller.EngineTests.test_restart_running_marker_cannot_mean_completed) ... ok
test_same_repo_other_control_root_cannot_take_lock (test_controller.EngineTests.test_same_repo_other_control_root_cannot_take_lock) ... ok
test_success_orders_stages_and_verifies_remote (test_controller.EngineTests.test_success_orders_stages_and_verifies_remote) ... ok
test_tampered_evidence_cannot_be_approved (test_controller.EngineTests.test_tampered_evidence_cannot_be_approved) ... ok
test_unavailable_remote_preserves_commit_and_retries_publication_only (test_controller.EngineTests.test_unavailable_remote_preserves_commit_and_retries_publication_only) ... ok
test_unconfigured_test_adapter_blocks_before_model (test_controller.EngineTests.test_unconfigured_test_adapter_blocks_before_model) ... ok
test_unsigned_recovery_cannot_clear_running_guard (test_controller.EngineTests.test_unsigned_recovery_cannot_clear_running_guard) ... ok
test_forward_dependency_rejected (test_controller.QueueTests.test_forward_dependency_rejected) ... ok
test_production_branches_rejected (test_controller.QueueTests.test_production_branches_rejected) ... ok
test_real_queue_is_ordered_and_missing_sources_are_explicit (test_controller.QueueTests.test_real_queue_is_ordered_and_missing_sources_are_explicit) ... ok
test_empty_report_is_not_a_pass (test_controller.ReportTests.test_empty_report_is_not_a_pass) ... ok
test_json_log_redaction_preserves_event_structure (test_controller.ReportTests.test_json_log_redaction_preserves_event_structure) ... ok
test_path_escape_rejected (test_controller.ReportTests.test_path_escape_rejected) ... ok
test_playwright_retry_and_expected_failure_are_not_pass (test_controller.ReportTests.test_playwright_retry_and_expected_failure_are_not_pass) ... ok
test_redacts_common_credentials (test_controller.ReportTests.test_redacts_common_credentials) ... ok
test_report_membership_and_skip_enforcement (test_controller.ReportTests.test_report_membership_and_skip_enforcement) ... ok
test_cli_nonzero_exit_never_accepts (test_controller.SubprocessTests.test_cli_nonzero_exit_never_accepts) ... ok
test_cli_protocol_real_process_and_complete_prompt (test_controller.SubprocessTests.test_cli_protocol_real_process_and_complete_prompt) ... ok
test_killed_controller_terminates_worker_and_grandchild (test_controller.SubprocessTests.test_killed_controller_terminates_worker_and_grandchild) ... ok
test_marker_write_failure_does_not_leave_worker (test_controller.SubprocessTests.test_marker_write_failure_does_not_leave_worker) ... ok
test_no_newline_output_obeys_log_budget (test_controller.SubprocessTests.test_no_newline_output_obeys_log_budget) ... ok
test_prompt_stall_still_obeys_timeout (test_controller.SubprocessTests.test_prompt_stall_still_obeys_timeout) ... ok
test_real_check_report_must_match_expected_membership (test_controller.SubprocessTests.test_real_check_report_must_match_expected_membership) ... ok
test_worker_cannot_execute_before_job_assignment (test_controller.SubprocessTests.test_worker_cannot_execute_before_job_assignment) ... ok
test_pending_recovery_can_be_signed_from_idle_operator_terminal (test_local_mode.LocalFlowTests.test_pending_recovery_can_be_signed_from_idle_operator_terminal) ... ok
test_single_user_concurrent_controller_and_interrupted_guard (test_local_mode.LocalFlowTests.test_single_user_concurrent_controller_and_interrupted_guard) ... ok
test_single_user_main_checkout_is_still_prohibited (test_local_mode.LocalFlowTests.test_single_user_main_checkout_is_still_prohibited) ... ok
test_single_user_missing_approval_stops_before_worker (test_local_mode.LocalFlowTests.test_single_user_missing_approval_stops_before_worker) ... ok
test_single_user_pause_and_publication_recovery (test_local_mode.LocalFlowTests.test_single_user_pause_and_publication_recovery) ... ok
test_single_user_subprocess_protocol_and_ordered_publication (test_local_mode.LocalFlowTests.test_single_user_subprocess_protocol_and_ordered_publication) ... ok
test_auth_home_inside_worker_is_rejected (test_local_mode.LocalPolicyTests.test_auth_home_inside_worker_is_rejected) ... ok
test_blocked_status_rejects_otherwise_successful_probes (test_local_mode.LocalPolicyTests.test_blocked_status_rejects_otherwise_successful_probes) ... ok
test_different_or_missing_sid_rejected (test_local_mode.LocalPolicyTests.test_different_or_missing_sid_rejected) ... ok
test_existing_normal_auth_home_is_rejected (test_local_mode.LocalPolicyTests.test_existing_normal_auth_home_is_rejected) ... ok
test_failed_probe_and_unconfigured_owner_key_cannot_qualify (test_local_mode.LocalPolicyTests.test_failed_probe_and_unconfigured_owner_key_cannot_qualify) ... ok
test_missing_or_stale_qualification_never_enables_unattended (test_local_mode.LocalPolicyTests.test_missing_or_stale_qualification_never_enables_unattended) ... ok
test_mxc_is_explicit_without_legacy_or_unrestricted_fallback (test_local_mode.LocalPolicyTests.test_mxc_is_explicit_without_legacy_or_unrestricted_fallback) ... ok
test_noninteractive_approval_is_refused (test_local_mode.LocalPolicyTests.test_noninteractive_approval_is_refused) ... ok
test_operator_controlled_policy_cannot_launch_a_worker (test_local_mode.LocalPolicyTests.test_operator_controlled_policy_cannot_launch_a_worker) ... ok
test_plugins_are_not_an_implicit_same_user_capability (test_local_mode.LocalPolicyTests.test_plugins_are_not_an_implicit_same_user_capability) ... ok
test_probe_summary_rejects_partial_and_failed_boundaries (test_local_mode.LocalPolicyTests.test_probe_summary_rejects_partial_and_failed_boundaries) ... ok
test_profile_denies_root_control_auth_and_normal_configuration (test_local_mode.LocalPolicyTests.test_profile_denies_root_control_auth_and_normal_configuration) ... ok
test_same_chatgpt_auth_is_explicit_and_tokens_are_not_copied (test_local_mode.LocalPolicyTests.test_same_chatgpt_auth_is_explicit_and_tokens_are_not_copied) ... ok
test_same_windows_identity_supported (test_local_mode.LocalPolicyTests.test_same_windows_identity_supported) ... ok
test_tool_or_temp_roots_cannot_reopen_protected_parent (test_local_mode.LocalPolicyTests.test_tool_or_temp_roots_cannot_reopen_protected_parent) ... ok
----------------------------------------------------------------------
Ran 81 tests in 161.136s
OK
@@ -0,0 +1 @@
{"observations":[{"status":"BLOCKED","stderr":"Failed to resolve full path of the current executable [C:\\Users\\Connor\\.dotnet\\dotnet.exe]\n","stdout":"","tool":"accepted-dotnet"},{"status":"BLOCKED","stderr":"No pyvenv.cfg file\n","stdout":"","tool":"accepted-docling-python"},{"stderr":"","stdout":"v24.19.0\n","tool":"node"},{"stderr":"","stdout":"\nHost:\n Version: 9.0.20\n Architecture: x64\n Commit: 3879076d9a\n RID: win-x64\n\n.NET SDKs installed:\n No SDKs were found.\n\n.NET runtimes installed:\n Microsoft.NETCore.App 8.0.30 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App]\n Microsoft.NETCore.App 9.0.20 [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App]\n Microsoft.WindowsDesktop.App 8.0.30 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]\n Microsoft.WindowsDesktop.App 9.0.20 [C:\\Program Files\\dotnet\\shared\\Microsoft.WindowsDesktop.App]\n\nOther architectures found:\n None\n\nEnvironment variables:\n Not set\n\nglobal.json file:\n C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker\\global.json\n\nLearn more:\n https://aka.ms/dotnet/info\n\nDownload .NET:\n https://aka.ms/dotnet/download\n","tool":"dotnet"},{"stderr":"WARNING: Error loading config file: open C:\\Users\\Connor\\.docker\\config.json: Access is denied.\npermission denied while trying to connect to the docker API at npipe:////./pipe/docker_engine\n","stdout":"\n","tool":"docker"}],"profile":"Existing native MXC, no permissions widened","realProductSuitesExecuted":0}
@@ -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,20 @@
{
"configuration": "PASS",
"mode": "single-user-local",
"executionPolicy": "operator-controlled",
"runtimeReady": false,
"executionEnabled": false,
"sourceGaps": [],
"unqualifiedAdapters": [
"qualified-provider-browser-python-pdf-security-preservation"
],
"checkpoints": 49,
"phase10": "NO-GO",
"note": "Read-only planning; no model, Git mutation, network, service, signing or scheduling action",
"checkedCommit": "1e6c80f708b2cc7c3dfa497e082e8a498045c751",
"sameWindowsIdentity": true,
"statePreservedOnRepeatedSetup": true,
"authenticationProfileEmpty": true,
"realSessions": 0,
"scope": "Fresh committed local clone; source and installed setup, disabled dry-run; no model or real roadmap"
}
@@ -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
+16
View File
@@ -11,6 +11,17 @@
"baselineBranch": {"$ref": "#/$defs/branch"},
"remote": {"type": "string", "pattern": "^[A-Za-z0-9_-]+$"},
"integrationBranch": {"$ref": "#/$defs/branch"},
"executionScope": {
"type": "object", "additionalProperties": false,
"required": ["stage", "checkpoint", "mode", "reportDirectory", "handoffCommit", "handoffBranch"],
"properties": {
"stage": {"const": "PHASE-10"}, "checkpoint": {"const": "P10-00"},
"mode": {"const": "assessment-only"},
"reportDirectory": {"const": "docs/automation/stages/PHASE-10"},
"handoffCommit": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
"handoffBranch": {"$ref": "#/$defs/branch"}
}
},
"stages": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/stage"}}
},
"$defs": {
@@ -56,11 +67,16 @@
"check": {
"type": "object",
"required": ["id", "argv", "cwd", "report"],
"if": {"required": ["adapter"], "properties": {"adapter": {"const": "assessment-evidence-inspection"}}},
"then": {"required": ["configured"], "properties": {"configured": {"const": false}}},
"properties": {
"id": {"type": "string", "pattern": "^[A-Za-z0-9-]+$"},
"argv": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"cwd": {"type": "string"},
"configured": {"type": "boolean"},
"adapter": {"enum": ["assessment-evidence-inspection"]},
"receipt": {"type": "string"},
"receiptSha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
"report": {"type": ["object", "null"], "properties": {
"format": {"enum": ["trx", "jest", "playwright", "junit"]},
"path": {"type": "string"},
+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)
+188
View File
@@ -0,0 +1,188 @@
"""Synthetic scope tests. No product backlog, model, signing or services."""
import copy
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import controller as c
import local_mode
import test_controller as fixture
def scope():
return dict(stage='PHASE-10', checkpoint='P10-00', mode='assessment-only',
reportDirectory='docs/automation/stages/PHASE-10',
handoffCommit='1'*40, handoffBranch='test/handoff')
class ScopeTests(unittest.TestCase):
def setUp(self):
fixture.EngineTests.setUp(self)
first=self.manifest['stages'][0]
first['id']='PHASE-10'
first['checkpoints'][0]['id']='P10-00'
self.manifest['stages'][1]['dependencies']=['PHASE-10']
state=self.engine.store.read()
state['stages']['PHASE-10']=state['stages'].pop('ONE')
state['manifestSha256']=c.digest(self.manifest)
self.engine.store.write(state)
tearDown = fixture.EngineTests.tearDown
git_at = staticmethod(fixture.EngineTests.git_at)
def test_scope_requires_protected_stop(self):
self.manifest['executionScope'] = scope()
with self.assertRaisesRegex(c.GateError, 'protected stop'):
c.validate_manifest(self.manifest)
def test_scope_rejects_product_write_and_deletion(self):
self.manifest['executionScope'] = scope()
(self.repo/'README').write_text('unauthorised')
with self.assertRaisesRegex(c.GateError, 'assessment'):
self.engine.verify_assessment_writes()
(self.repo/'README').unlink()
with self.assertRaises(c.GateError):
self.engine.verify_assessment_writes()
def test_assessment_reports_are_permitted(self):
self.manifest['executionScope'] = scope()
output=self.repo/scope()['reportDirectory']/'P10-00.md'
output.parent.mkdir(parents=True)
output.write_text('Synthetic assessment')
self.engine.verify_assessment_writes()
def test_generic_approval_cannot_launch_next_checkpoint(self):
first=self.manifest['stages'][0]
first['checkpoints'].append(copy.deepcopy(first['checkpoints'][0]))
first['checkpoints'][1]['id']='implementation'
self.manifest['executionScope']=scope()
state=self.engine.store.read()
state['manifestSha256']=c.digest(self.manifest)
state['stages']['PHASE-10']['checkpoint']=1
self.engine.store.write(state)
self.engine.run()
self.assertEqual(0,self.runner.calls)
self.assertIn('assessment', self.engine.store.read()['latestFailure'])
def test_assessment_reaches_protected_stop_and_cannot_advance(self):
output='docs/automation/stages/PHASE-10/P10-00.md'
class ReportRunner(fixture.SyntheticRunner):
def run_session(inner, engine, stage, checkpoint, directory, prompt):
inner.calls += 1
report=engine.repo/output
report.parent.mkdir(parents=True,exist_ok=True)
report.write_text('Synthetic assessment only')
return dict(outcome='complete',summary='synthetic',blockers=[],requestedApprovals=[],checkpoint=checkpoint['id'])
first=self.manifest['stages'][0]
cp=first['checkpoints'][0]
cp['requiredArtifacts']=[output]
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
first['checkpoints'].append(copy.deepcopy(cp))
first['checkpoints'][1]['id']='implementation'
self.manifest['executionScope']={**scope(),'handoffCommit':self.manifest['baselineCommit'],'handoffBranch':'codex/base'}
state=self.engine.store.read()
state['manifestSha256']=c.digest(self.manifest)
self.engine.store.write(state)
self.engine.runner=ReportRunner()
self.engine.approval_verifier=lambda request: request['kind']!='candidate-specific-cleanup-scope'
self.engine.run()
self.assertEqual(1,self.engine.runner.calls)
self.assertEqual('candidate-specific-cleanup-scope', self.engine.store.read()['stages']['PHASE-10']['awaiting'][0]['kind'])
self.engine.approval_verifier=lambda request: True
self.engine.run()
self.assertEqual(1,self.engine.runner.calls)
self.assertIn('assessment',self.engine.store.read()['latestFailure'])
def test_policy_cannot_relabel_p10_01_as_assessment(self):
self.manifest['executionScope']={**scope(),'checkpoint':'P10-01'}
cp=self.manifest['stages'][0]['checkpoints'][0]
cp['id']='P10-01'
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
with self.assertRaisesRegex(c.GateError,'protected stop'):
c.validate_manifest(self.manifest)
def test_evidence_inspection_cannot_be_marked_runtime_qualified(self):
self.manifest['executionScope']=scope()
cp=self.manifest['stages'][0]['checkpoints'][0]
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
cp['checks'][0].update(adapter='assessment-evidence-inspection',configured=True,receiptSha256='a'*64)
with self.assertRaisesRegex(c.GateError,'cannot qualify'):
c.validate_manifest(self.manifest)
def test_inspection_requires_explicit_false_and_never_launches_worker(self):
self.manifest['executionScope']=scope()
cp=self.manifest['stages'][0]['checkpoints'][0]
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
cp['checks'][0].update(adapter='assessment-evidence-inspection',receiptSha256='a'*64)
cp['checks'][0].pop('configured',None)
with self.assertRaisesRegex(c.GateError,'cannot qualify'):
c.validate_manifest(self.manifest)
state=self.engine.store.read()
with self.assertRaisesRegex(c.GateError,'adapters must be configured'):
self.engine.checkpoint(self.manifest['stages'][0],cp,state['stages']['PHASE-10'],state)
self.assertEqual(0,self.runner.calls)
def test_native_isolation_failure_stops_before_additional_probe(self):
import qualify_assessment
policy=copy.deepcopy(self.manifest)
policy['executionEnabled']=False
c.atomic(self.control/'backlog.json',policy)
c.atomic(self.control/'proposed-p10-assessment.json',policy)
c.atomic(self.control/'config.json',self.config)
report=self.root/'failed-native.json'
with mock.patch.object(qualify_assessment.local_mode,'qualify',return_value={'status':'FAIL'}), \
mock.patch.object(sys,'argv',['qualify',str(self.control),str(self.repo),str(report)]):
with self.assertRaisesRegex(c.GateError,'no further child'):
qualify_assessment.main()
self.assertEqual('NOT_RUN',c.read_json(report)['assessment'])
self.assertFalse((self.repo/'.artifacts').exists())
def test_entry_cannot_prepare_approvals_without_composite_adapter(self):
self.manifest['executionScope']={**scope(),'handoffCommit':self.manifest['baselineCommit'],'handoffBranch':'codex/base'}
cp=self.manifest['stages'][0]['checkpoints'][0]
cp['approvalsAfter']=['candidate-specific-cleanup-scope']
cp['checks']=[]
c.atomic(self.control/'backlog.json',self.manifest)
c.atomic(self.control/'config.json',self.config)
with mock.patch.object(c,'Engine',return_value=self.engine), \
mock.patch.object(local_mode,'require_unattended'):
with self.assertRaisesRegex(c.GateError,'composite adapter is not qualified'):
c.main(['prepare-entry','--control-root',str(self.control)])
self.assertEqual(0,self.runner.calls)
self.assertFalse((self.control/'requests').exists())
class ReadOnlyProfileTests(unittest.TestCase):
def test_assessment_profile_keeps_product_read_only(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
config=dict(repository=str(root/'worker'), workerTemp=str(root/'temp'),
workerCodeXHome=str(root/'control/auth'), normalCodeXHome=str(root/'normal'),
assessmentReportDirectory='docs/automation/stages/PHASE-10')
rules=local_mode.filesystem_rules(root/'control',config)
self.assertEqual('read',rules[(root/'worker').resolve().as_posix()])
self.assertEqual('write',rules[(root/'worker/docs/automation/stages/PHASE-10').resolve().as_posix()])
class ReceiptTests(unittest.TestCase):
def test_receipt_rejects_changed_product_or_changed_receipt(self):
import assessment_adapter as adapter
with tempfile.TemporaryDirectory() as directory:
repo=Path(directory)
(repo/'product.txt').write_text('accepted synthetic source')
receipt=dict(schemaVersion=1, scope='P10-00-ASSESSMENT-ONLY',
integrity='PASS', files={'product.txt':c.sha_file(repo/'product.txt')},
nativeReports=[dict(id='synthetic',status='PASS',exitCode=0,
membershipSha256='a'*64)], retainedDecisions={'production':'NO-GO'})
path=repo/'receipt.json'
c.atomic(path,receipt)
pin=c.sha_file(path)
self.assertEqual('REUSED_SOURCE_BOUND_EVIDENCE',adapter.verify(repo,path,pin)['kind'])
(repo/'product.txt').write_text('changed')
with self.assertRaises(ValueError):
adapter.verify(repo,path,pin)
with self.assertRaises(ValueError):
adapter.verify(repo,path,'0'*64)
+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)
+80
View File
@@ -0,0 +1,80 @@
# AUTO-ACTIVATION-01 — blocked before activation
30 October 2026. **P10-00 execution remains NO-GO.** The command-running composite application adapter is not qualified inside the existing security boundary. The queue remains disabled and operator-controlled. No autonomous/model session, owner signature, Phase 10 assessment, product edit, migration, main merge or deployment was performed.
## What is verified
| Deliverable | Result |
|---|---|
| Composite application adapter | **BLOCKED.** Existing command-running provider/browser/Python/PDF/security/preservation adapter remains `configured:false`. The new evidence-inspection utility is deliberately prevented from being marked runtime-qualified. |
| Real application evidence | Existing published handoff verifier passes. Its 34 native report records, exact process exits, original test membership, source bindings and conditional decisions are preserved. A receipt freezes 3,483 non-envelope source files. **These are reused executions, not newly run product suites.** |
| Controller tests | 81 distinct tests pass, zero failures/errors/skips, source stable. Includes the original 69, twelve assessment tests, cryptographic approval/resume, missing-approval refusal and disposable-Git protected-stop simulation. Do not add focused reruns to 81. |
| Native sandbox | Current installed real-key/files/network canaries PASS. Separately tested projected assessment profile: source read succeeds, source write/delete fail, report creation succeeds; frozen-source/evidence inspection succeeds inside MXC. |
| Authentication | Installed `auth-status` reports `CHATGPT_SIGNED_IN`. No tokens copied/read; account identity and a live model request were not verified. |
| Owner signing | Existing nonexportable key retained. Pinned public certificate matches, host signing positive control succeeds and worker access to that actual key is denied. No replacement owner key or owner approval was created. |
| Exact product baseline | `fc5b9d377199ce51f480a920a3772108094929e3`, published as the direct parent of `5cb903b989742b7a96cb22ddf4c0e24993b18980`. |
| Published handoff | Fresh remote `test/pre-phase10-ux-acceptance` equals `5cb903b989742b7a96cb22ddf4c0e24993b18980`. Parent/envelope changes are confined to the handoff directory. No baseline substitution. |
| Execution checkout | `C:\Users\Connor\.codex\worktrees\p10-assessment\Job tracker`, branch `codex/p10-assessment-ready`, exact baseline and clean Git index/worktree. 47 checkout line-ending differences were materialised as exact accepted bytes; each Git blob was checked identical before refreshing index stat information. |
| Policy | Disabled proposal prepared, preserving all seven stages/order. P10-00 scope is explicitly assessment-only with protected `candidate-specific-cleanup-scope` stop. Latest handoff decisions are pinned supplemental prompt content. |
| P10-01 | Hard rejected outside the P10-00 execution cap, even with generic synthetic approvals. Its implementation adapter remains unconfigured. A new exact owner-controlled implementation policy and runtime qualification are required. |
| Actual state | Zero roadmap sessions, no recovery needed, no scheduling. Existing installed repository and baseline policy remain unchanged; the clean execution checkout has **not** been selected or activated. |
## The remaining runtime blocker
Native probes used the existing MXC policy without widening permissions:
- Docker cannot read the owner's Docker configuration or access `docker_engine`. Giving an autonomous worker the general host Docker socket would undermine the isolation boundary.
- The accepted user-local `.NET` executable cannot resolve its executable under this profile. The separately installed system `.NET` runtime starts but reports **no SDKs**; that is not a passing backend test run.
- The accepted Docling virtual environment, outside the worker checkout, cannot resolve `pyvenv.cfg` under the current profile. Starting the normal isolated Python interpreter is not Docling qualification.
- Worker network access, including loopback, remains denied. Real browser/API/MariaDB test services have not been qualified through a bounded isolated executor.
The accepted command catalogue includes the actual backend/frontend/build/browser/MariaDB/Python/Docling/CV/PDF commands and their native exits/membership. It is an inventory of previous executions, **not an installed test runtime**. Security, human/device, hosted CI, model and production limitations remain separate and unchanged. Existing PDF/S01 acceptance was not reopened.
Completing AUTO-ADAPTER-01 requires a qualified test executor that can run these exact applicable commands against disposable services without exposing the owner's general Docker socket, files, signing key or production resources to the worker. That executor is **not implemented/qualified by this change**. Do not turn `configured` on, approve evidence reuse as a substitute, or run `enable-unattended`, `start` or `resume` to bypass it. A second account, subscription or VM has not been prescribed.
## Prepared policy and protected requests
Installed proposal:
```text
C:\Users\Connor\.jobjakt-backlog\single-user-local\proposed-p10-assessment.json
```
Review copy: `automation/qualification/p10-assessment.policy.json`.
The controller has generated one genuine **unsigned disabled** `manifest-policy-update` request:
```text
C:\Users\Connor\.jobjakt-backlog\single-user-local\requests\d637caf72c3fed5910f7c28e75dc82324d8febc12597ddb5627ae022af49a622.json
```
It binds the proposed manifest hash. Its baseline field describes the **currently installed old policy**, not acceptance of that old product baseline. The proposed manifest names the correct `fc5b9d3` baseline and separate `5cb903b` envelope. Even approving/adopting this proposal would leave execution disabled and adapters unqualified. **No owner action on this request is necessary while the runtime blocker remains.**
Environment, roadmap, Phase 10 entry and restricted-execution requests were not made eligible or signed. Generating misleading eligible requests before mandatory adapter qualification would bypass the handoff gate. Valid future approvals must bind the final installed policy/code/configuration, correct execution checkout, baseline/envelope, full prompt hashes, qualified runtime reports and exact scope. Changing those inputs invalidates applicability.
The `prepare-entry` command is implemented to prepare successive protected entry requests without launching a worker. It requires an adopted enabled restricted policy, fresh qualified runtime, clean exact baseline and configured adapters. It refuses the current disabled queue with native exit 2 and zero sessions. The RTK wrapper may map nonzero child exits to 1.
When the actual runtime blocker is resolved, the existing protected sequence remains: configure the genuinely qualified final manifest, owner `approve`, configure the same exact file, refresh qualification after real config changes, owner `enable-unattended`, then `prepare-entry` / owner `approve` until the exact grants are verified. Each grant authorises only its requested scope: environment evidence; exact capped policy; Phase 10 entry; restricted P10-00 assessment. The owner then deliberately invokes `start`. **There is no safe activation command for the present unqualified manifest.**
## Commands safe to run now
```powershell
. "$HOME\.jobjakt-backlog\single-user-local\Use-JobjaktBacklog.ps1"
Jobjakt status
Jobjakt auth-status
Jobjakt dry-run
```
`dry-run` should still report execution disabled and the unqualified composite adapter. The existing owner key/authentication setup needs no repetition. The refreshed qualification is valid only for its recorded code/config binding and 24-hour window; retargeting to the execution checkout or adopting the restricted profile needs the applicable requalification.
## Evidence
- `automation/reports/activation/results.json` and `tests.txt`: 81 current tests and exact source hashes.
- `automation/reports/activation-native.json`: real restricted-profile/key/source/evidence probes.
- `automation/reports/activation/control-state.json`: disabled queue, zero sessions, public pin and current installed binding checks.
- `automation/reports/activation/entry-rejection.json`: actual no-execution entry refusal.
- `automation/reports/activation/accepted-command-catalog.json`: prior genuine command/process/report identities; no new product execution claim.
- `automation/reports/activation/tool-probes.json`: actual runtime capability failures.
- `automation/reports/activation/schema-validation.json`: manifest constraints checked with existing Ajv; Draft 2020 meta-schema validation was unavailable. Runtime manifest validation also passes.
Earlier failed development-test attempts were corrected; the final 81-test run supersedes them. Historical product totals and original PDF oracle failures remain intact. No new PDF export, model inference, provider suite, human/device check, hosted CI or production qualification was performed.
+15
View File
@@ -0,0 +1,15 @@
# AUTO-ACTIVATION-01
Implement the owner's 30 October activation request in the existing isolated tooling worktree. Stop before execution or signing. Preserve the existing key/authentication, seven workstream order and all later gates.
- [x] Resolve published handoff 5cb903b and its direct parent candidate fc5b9d3; keep their identities separate.
- [x] Check existing authentication and native real-key isolation. Docker pipe access is correctly denied; no system .NET SDK. Do not widen service access.
- [x] Inspect source-bound evidence using the existing handoff verifier, exact native report membership and frozen source hashes. Retain conditional/security/human dispositions. Evidence inspection is explicitly prevented from substituting for the mandatory command-running composite adapter.
- [x] Add a restricted P10-00 policy, report-only filesystem permissions and independent write-set verification. Reject P10-01 regardless of a generic roadmap signature. Test missing approvals and the protected stop using disposable Git remotes.
- [x] Prepare a clean execution checkout at fc5b9d3, exact unsigned disabled-policy request and an owner command for preparing successive eligible entry requests without launching a worker.
- [x] Run existing controller tests, new adapter/scope tests and native assessment probes; preserve their distinct evidence. Document remaining owner actions.
- [ ] AUTO-ADAPTER-01 remains BLOCKED: command-running application/service runtime not isolated and qualified. Stop before activation; do not expose general Docker/socket or widen worker access to bypass this barrier.
Ruling: the evidence-reuse inspector cannot satisfy the owner's explicit real composite runtime gate. Keep proposed and installed policies disabled, retain configured:false, and do not generate eligible environment/entry grants. The 81-test final run and native probes qualify control-plane changes only, not actual roadmap readiness. See ACTIVATION_01.md.
Changes reach controller and installed packaging, local filesystem policy, manifest schema, evidence adapter, PowerShell command dispatch, tests and operator documentation. Product files, existing acceptance evidence and the signing key remain outside the change scope. The adapter uses the existing trusted handoff verifier to prepare a receipt, then verifies exact immutable evidence/source bytes; this avoids a second report parser and unnecessary product reruns. Fresh provider runtime qualification remains a separate barrier to implementation.
+3 -1
View File
@@ -1,5 +1,7 @@
# Autonomous Jobjakt backlog controller
**For your normal Windows login and existing ChatGPT account, use [single-user setup](SINGLE_USER.md).** It requires no second Windows account, subscription or VM. The new default is an operator-controlled pause, with explicit MXC qualification before unattended work. The separate-identity instructions below describe the optional legacy `dedicated-runner` mode only. Neither mode activates the roadmap automatically.
This implementation builds the orchestrator only. **The real backlog is disabled. Phase 10 is NO-GO pending its own entry decision.** See [audit](AUDIT.md), [gate register](GATES.md), [security boundary](SECURITY.md), and [acceptance evidence](ACCEPTANCE.md).
## What is installed
@@ -32,7 +34,7 @@ pwsh -NoProfile -File automation/tests/Test-Approvals.ps1 -Verifier "$PWD\script
Tests create disposable local worker repositories and bare remotes. The subprocess double exercises the real supervision/result/report paths without a model or network. RSA tests use temporary in-memory keys/certificates and do not install an owner key. They do not qualify the Windows sandbox or real application acceptance.
## Provision an actual runner, after approval
## Optional legacy dedicated-runner provisioning
Use a dedicated development Windows account/VM with no production credentials, mounts, Docker socket, remote admin access or production network routes. Keep the owner's signing identity separate. A source worktree provides edit isolation; it is not a production security boundary. Prefer a **separate clone** for unattended execution, avoiding shared Git metadata/hooks with ongoing human work. The owner must protect main/production refs server-side and grant the publisher only the intended development refs.
+8
View File
@@ -1,5 +1,13 @@
# Controller trust boundaries
## Single-user-local update
`single-user-local` supports the same Windows SID for owner/controller. It uses a dedicated auth-only CODEX_HOME with a fresh sign-in to the same ChatGPT account; no tokens are copied. See [single-user operation](SINGLE_USER.md) and its [acceptance report](SINGLE_USER_ACCEPTANCE.md). The older separate-owner requirements below apply to `dedicated-runner`, not to initializing the single-user mode.
The single-user worker uses an explicit MXC named permission profile: filesystem root denied, minimal trusted tools readable, exact worker/temp paths writable, control/auth/normal-Codex roots denied, Git metadata read-only, known production environment paths denied, command network disabled. Apps, plugins, remote plugins, MCP servers and web search are explicitly disabled. Neither legacy fallback nor unrestricted execution is allowed. Real canary probes cover workspace read/write, seven denied file categories, a listening host-loopback socket and temporary certificate/CNG signing. The exact configured owner key additionally needs a positive host signing control and negative worker probes before unattended mode.
Same-account isolation protects against the confined worker, not against arbitrary unrestricted processes already running as the owner. Such processes can alter owner files or invoke owner credentials. This does not provide the independent administrative boundary of a separate runner identity. A stopped operator terminal, native restrictions, protected state and signed scope are all still required. Failure, incomplete proof, cleanup failure, changed policy or expired qualification retains operator-controlled pause; it never substitutes writable approval JSON.
## Threat model
Treat the worker model, repository content, test/build scripts, uploaded inputs and tool output as untrusted execution. Protect owner approvals, controller policy/code, durable state, test receipts, source identity, development publication and all production assets. Threats include forged PASS/approval JSON, replayed decisions, modified tests/artifacts, policy replacement, overlapping workers, branch/push manipulation, secret exposure and orphan processes.
+109
View File
@@ -0,0 +1,109 @@
# Run from your existing Windows and ChatGPT account
**30 October 2026:** see [current activation status](ACTIVATION_01.md) before activation. Existing authentication and owner-key setup are verified; do not repeat them. The real composite runtime remains unqualified and the queue remains disabled.
`single-user-local` removes the second Windows-account/VM requirement. It keeps the original queue, signed decisions, Git guards, recovery, locking and finite resource budgets. **Phase 10 and the actual roadmap remain disabled.** The current accepted development baseline is still an explicit policy decision, not something setup infers from main.
## Setup once
From the tooling worktree on `codex/autonomous-roadmap-single-user`:
```powershell
pwsh -NoProfile -File .\scripts\Run-JobjaktBacklog.ps1 -Command setup
. .\scripts\Use-JobjaktBacklog.ps1
Jobjakt status
Jobjakt dry-run
```
Setup detects the current username/SID, Codex, Git, PowerShell, Python and this separate repository worktree. It installs at `$HOME\.jobjakt-backlog\single-user-local`, with an auth-only child directory and a separate task temp directory. This stable home-relative location avoids Codex desktop package LOCALAPPDATA redirection. Use `-Repository <isolated clone/worktree>` and `-ControlRoot <new location>` for explicit paths. A separate clone remains preferable for long-running development, but another Windows identity is unnecessary.
Existing state, prompts, activation policy and authentication are never reset by setup. A disabled, unused single-user installation may refresh the trusted code only; requalify after code changes. An existing dedicated installation remains separate and unchanged. No machine user, VM, paid service, signing key, scheduled task or token is created by basic setup. The optional `init` command retains the older dedicated-runner mode.
In later terminals, load the short command once:
```powershell
. "$HOME\.jobjakt-backlog\single-user-local\Use-JobjaktBacklog.ps1"
Jobjakt status
```
This adds a function to that terminal only. It does not edit your normal PowerShell profile or Codex configuration. If you selected a custom ControlRoot, invoke its `Run-JobjaktBacklog.ps1` directly with `-Command`; it detects its own installed directory.
## Sign into the same ChatGPT account
Run this yourself in a real PowerShell terminal:
```powershell
Jobjakt auth
Jobjakt auth-status
```
Choose the **same ChatGPT account you already use** in the browser login. This is a new local login session to the same service account, not a second subscription. Authentication is forced to ChatGPT with file storage in the dedicated CODEX_HOME. No API-key login, access-token piping or copying of `auth.json` is performed. `auth-status` reports the authentication kind without printing credentials; the controller cannot establish that two human-selected accounts are the same merely from a directory name. Actual successful sign-in remains a human step; it was not done by this implementation task.
Normal authentication/config reuse is deliberately not offered: the normal home may contain unrelated plugins, servers, rules and credentials. An auth-only home plus explicit capability restrictions avoids inheriting that configuration. Account quotas/rate limits remain those of your existing account; no new subscription or usage allowance is promised.
## Signing key and real qualification
These operator actions require your interactive terminal and no active controller worker:
```powershell
Jobjakt key
Jobjakt qualify
```
`key` creates a nonexportable RSA owner certificate in **your current Windows user** certificate store, exports only its public certificate to ControlRoot, and pins its hash/thumbprint/SID. It does not replace an existing configured key. It grants no roadmap authority. The private key is never written to the repository or copied between accounts.
`qualify` launches only trusted synthetic probes through native MXC. It first proves canaries and keys are usable from the trusted operator context, then checks the worker cannot read/open for writing protected configuration, approvals, a signing-key file canary, normal Codex data, personal-file canaries, an unrelated-repository canary or synthetic production credentials. It tests denied socket access against a real listening loopback port and attempts certificate/CNG signatures with a temporary same-user key. When your actual owner key is configured, the exact pinned key also needs a host-positive and worker-negative signing test. No real production credential, personal document, external endpoint or model is used. Synthetic certificates/files are cleaned up; any cleanup failure blocks qualification.
Results are saved at `qualification.json`, including scope, binding, issue time and actual owner-key disposition. **A canary PASS alone is not unattended approval.** Missing owner key, incomplete/failed/stale probes or changed configuration keeps the controller paused. Qualification expires after 24 hours, and start rechecks native boundaries. The stock legacy sandbox's earlier CryptUnprotectData failure was not bypassed: this mode explicitly selects the [documented MXC implementation](https://learn.chatgpt.com/docs/windows/windows-sandbox) and refuses fallback. Its deny/read/write rules use the supported [permission-profile format](https://learn.chatgpt.com/docs/permissions).
After reviewing a successful qualification of your real key, you can select the runtime policy:
```powershell
Jobjakt enable-unattended
```
This requires your typed confirmation, but still does **not** enable the queue or authorize Phase 10. The enabled manifest, environment grant, roadmap grant and exact phase entry approvals remain independently signed through the existing `configure` workflow. Do not perform that activation until current Pre-Phase-10 acceptance and publication are finished and you explicitly approve Phase 10 entry.
If the native boundaries cannot be enforced, use the default `operator-controlled` policy. `start`/`resume` cannot launch autonomous workers under that policy. You can inspect status/logs/requests and use `prepare` to generate a protected inspection-only checkpoint brief. This is a deliberate execution pause, not an unsandboxed “interactive” fallback. Resolve the precise boundary or use an independently protected approval/execution mechanism before allowing untrusted automation.
## Routine commands
```powershell
Jobjakt validate
Jobjakt dry-run
Jobjakt status
Jobjakt current
Jobjakt logs
Jobjakt start # only after qualified runtime and separately signed phase activation
Jobjakt pause
Jobjakt resume
Jobjakt retry -Stage PHASE-10
Jobjakt recover
Jobjakt approve
```
`approve` shows the exact pending JSON request and requires a typed challenge, an explicit Approve/Revise/Reject disposition and your concrete decision text. It signs with the pinned owner key. It holds the repository lock and refuses active workers. A worker subprocess has no terminal input and cannot use the key when qualification is valid. A JSON file claiming approved remains invalid.
For homepage decisions, choose elements or revisions in the decision text. `Revise` reruns that checkpoint; final `Approve` advances after all gates pass. Product scope, provider rights, translation review, material security decisions and release evidence retain their gates. There is no automatic deployment or main merge path.
For an interrupted run: inspect the recorded process tree, run `recover` to create its exact request, run `approve` after confirming the tree is gone, then `recover` again and `resume`. Only the recovery request can be signed while recovery-required is set. A live recorded PID or an unverifiable PID marker still blocks signing. Never kill a reused PID blindly. Signing alone does not clear recovery-required. Publication failures reuse the exact verified local commit; no model rerun or force push is needed.
## Overnight and scheduling
The controller works without its GUI remaining open. A qualified run can continue with the desktop **locked**, provided the existing user session, dedicated authentication and signing provider are available and the PC is awake. No lock, sleep, firewall or elevation setting is changed. Logging out is different from locking: unattended execution before login requires you to configure the existing account's batch credentials/profile in Windows Task Scheduler and qualify that actual context. This implementation does not collect or store your Windows password.
Optional task definition, after real qualification:
```powershell
pwsh -NoProfile -File .\scripts\Register-JobjaktBacklogTask.ps1
# Review its preview, then explicitly register:
pwsh -NoProfile -File .\scripts\Register-JobjaktBacklogTask.ps1 -Install
```
It uses your existing login, limited privileges, logon plus 15-minute continuation, an eight-hour task limit and no overlapping instances. Scheduled continuation uses `start`, preserving a manual pause; `resume` is the owner's deliberate unpause. The registration script refuses unqualified runtime/key state. No task was registered by this work.
## Security difference and current handoff
Real canaries verify the tested native-worker boundaries. They are not an exhaustive proof against every Windows API, credential provider or future sandbox defect. Unrestricted applications running under your same Windows identity are outside the worker boundary and can still act as you. This is not equivalent to an independently administered account/VM. Apps, plugins, MCP and web search remain disabled in this mode; a later security scan/provider task needing them must pause for independently qualified capabilities, not silently enable your normal configuration.
The initial AUTO-02 [acceptance snapshot](SINGLE_USER_ACCEPTANCE.md) predates the owner's sign-in and real-key setup. Those missing-auth/key statements are historical; [ACTIVATION_01.md](ACTIVATION_01.md) records the verified current setup and remaining runtime blocker. Operation is still operator-controlled and the queue is disabled.
+51
View File
@@ -0,0 +1,51 @@
# AUTONOMOUS-ROADMAP-02 acceptance — 29 October 2026
**Single-user mode implemented and installed. Actual roadmap execution remains disabled. Phase 10: NO-GO without its separate formal entry approval.**
Source: `codex/autonomous-roadmap-single-user`, based on `c1663df`, in the existing isolated orchestrator worktree. The ongoing Pre-Phase-10 worktree was not edited, checked out, tested or operated. No main merge or production operation was performed.
## Decisions
| Area | Result | Evidence / boundary |
|---|---|---|
| Current Windows login setup | **PASS** | Setup detected the actual current SID for both owner/controller and installed outside the worker. No account, VM or subscription was created. Stable `$HOME/.jobjakt-backlog/single-user-local` location avoids desktop package path redirection. |
| Same ChatGPT account support | **IMPLEMENTED; HUMAN SIGN-IN PENDING** | Dedicated auth-only CODEX_HOME, forced ChatGPT/file credentials and operator-terminal login. Existing tokens/config were not read/copied. The new profile is currently unsigned-in; actual identity, entitlement and inference are not claimed. |
| Native filesystem boundaries | **PASS for real canaries** | Actual MXC process could read/write its workspace; all seven protected canary categories denied reads and write-handle opens. Files existed and were host-readable/writable before probing. No private documents/production secrets were used. |
| Native command networking | **PASS for real loopback canary** | Host-positive listening TCP socket; worker connection denied under the same explicit network-disabled profile used for future sessions/checks. No production or third-party endpoint was contacted. This is finite evidence, not exhaustive network testing. |
| Same-user signing protection | **CANARY PASS; REAL KEY NOT CONFIGURED** | Temporary nonexportable current-user key was usable by the host, inaccessible through worker certificate and direct CNG attempts, then cleaned up. The actual pinned owner key must pass its own host-positive/worker-negative tests before unattended mode. |
| Apps/plugins/MCP | **PASS for compiled configuration** | Installed CLI reported apps, plugins and remote-plugin features false under the exact overrides; effective MCP server list was empty. Web search is explicitly disabled. No unrelated normal configuration is inherited by design; no model-backed tool session was run. |
| Approval workflow | **PASS in tests; live owner action pending** | Signed verifier retained; typed challenge plus concrete disposition/decision; no active-worker signing; exact recovery request can be signed after reconciliation. Noninteractive approval and arbitrary writable approval JSON remain rejected. |
| Regression | **PASS** | 69 distinct Python tests: all original 48 plus 21 single-user tests, zero failures/errors/skips in the final source-stable run. Six separate original .NET signature cases also pass; focused reruns are not added to those totals. |
| Git/pause/recovery/concurrency | **PASS in disposable flows** | Single-user tests cover ordered publication, missing approval, pause/resume, publication-only retry, common-directory locking, interrupted-state guard and main protection. Original safety tests remain unchanged. |
| Overnight/scheduling | **IMPLEMENTED; LIVE SCHEDULE NOT QUALIFIED** | Native Job controls retained, optional limited-user task script refuses unqualified state. No task was installed. No physical reboot/locked-desktop/logged-out run was performed. |
| Unattended real roadmap | **BLOCKED / DISABLED** | Default operator-controlled policy, zero real sessions, no actual owner key, no ChatGPT sign-in, unconfigured application qualification adapter, and no Phase 10 authorization. No security control was bypassed to enable it. |
## Evidence
`automation/reports/single-user/` contains:
- `results.json` and `tests.txt`: final complete regression, distinct totals, before/after source-stability assertion and hashes.
- `signature-results.json` and `signature-tests.txt`: six disjoint actual .NET signature-verifier cases.
- `native-qualification.json`: actual MXC canary results, policy/source binding, issue time, cleanup status and explicit real-owner-key NOT_CONFIGURED scope.
- `setup-results.json`: same Windows identity, empty dedicated authentication, zero sessions, disabled queue and Task Scheduler refusal before registration.
- `capabilities.json`: actual CLI feature-state verification and zero effective MCP servers.
- `format-check.json`: packaging removed one trailing space in the new test file after the full run; its Python AST is identical to the tested bytes. The original full-run hash remains recorded, with the packaged hash separately identified. No extra test count is added for formatting.
- `packaging.json`: a fresh committed clone at `1e6c80f` installed the mode successfully; repeated setup through the installed wrapper preserved state, the auth profile stayed empty, all prompt sources validated, and zero real sessions were launched.
The first new MXC probe could not start because the old subprocess environment omitted SystemDrive. Only that non-secret OS variable was added to the existing allowlist; no unrestricted environment or sandbox fallback was enabled. Explicit MXC startup and the subsequent real canary probes passed. The previous legacy CryptUnprotectData result remains historical evidence; the user's normal saved sandbox selection was not changed.
The independent read-only review found two issues: the convenient approve command could not sign its own recovery request, and successful probe fields could override a BLOCKED qualification status. Both were reproduced with regression tests and fixed. Recovery still needs operator confirmation and a stopped/verifiably absent recorded process; signing alone never clears recovery-required. Qualification now requires PASS with no failure as well as complete probes, current binding, freshness and the real owner-key denial.
Setup from both source and the installed wrapper was exercised. Existing state/auth/policy is preserved. A packaging/setup correction makes the installed wrapper reuse its stored repository path rather than incorrectly require ControlRoot to be a Git checkout. This change was verified and included in the final regression source hashes.
## Restrictions retained
The original numbered prompt snapshots, order, dependencies, stage-specific owner gates, retry/session/time budgets, report/artifact verification and protected approval mechanism remain. Development completion is not production release. Failed publication keeps the exact tested commit; no force push, main merge, deployment, destructive migration, production credentials or live model activation is added.
Single-user confinement is not an independent administrator boundary. An unrestricted process already acting as the same Windows user can access owner files/credentials outside the worker sandbox. Tested canaries do not prove all Windows APIs, credential providers, future files or sandbox defects are harmless. An actual owner key, current sign-in and the qualified execution context are still required. A failed or incomplete boundary keeps the operator-controlled pause; it never launches an unconfined interactive worker.
The source still carries the intentional unconfigured provider/browser/Python/PDF/security/preservation adapter gate from AUTO-01. This task does not invent or waive application qualification. Actual roadmap activation must wait for finished Pre-Phase-10 acceptance, a published accepted baseline, explicit candidate/scope entry and the existing signed grants.
## Next owner actions
See [SINGLE_USER.md](SINGLE_USER.md) for exact commands. In your own terminal, load `Use-JobjaktBacklog.ps1`, run `Jobjakt auth` with your existing ChatGPT account, then `Jobjakt key` and `Jobjakt qualify` to test the actual key. Do not activate the queue yet. `Jobjakt status`, `dry-run`, `logs`, `pause`, `resume`, `retry`, `recover` and `approve` remain available through the short command; workers remain disabled until their independent runtime and phase gates are met.
+11
View File
@@ -0,0 +1,11 @@
# AUTONOMOUS-ROADMAP-02
Implement on `codex/autonomous-roadmap-single-user`, based on `c1663df`, in the existing isolated tooling worktree. No real roadmap, main merge or production operation.
- [x] Add `single-user-local` setup under the current Windows SID, a dedicated auth-only CODEX_HOME, and short PowerShell commands. Preserve the explicit disabled baseline. Never copy tokens or change the normal Codex configuration.
- [x] Retain legacy dedicated-runner checks. In the new mode use an explicit MXC permission profile: deny filesystem root, allow minimal tools and the worker only, deny control/auth/normal Codex roots, keep command network off and plugins disabled. Apply the exact same profile to worker and test subprocesses.
- [x] Implement real canary read/write, loopback network, and certificate/CNG signing probes. A successful startup alone is insufficient. Record failed/not-tested boundaries independently.
- [x] Default to operator-controlled pause. Only a current successful qualification, protected signed approvals and explicit phase authorization can enable unattended operation. No writable approval-JSON fallback. Approval UI requires a real operator terminal and no active workers.
- [x] Extend tests while retaining all existing tests. Verify disabled setup/dry-run, auth-home isolation, pause/recovery/locking/publication, and actual native sandbox canaries; publish truthful acceptance and usage.
Choice: the existing owner account and same ChatGPT account are supported; different accounts/VMs are optional, not setup prerequisites. Same-account approval is conditional on an inaccessible signing key and protected filesystem boundaries. If either fails, keep workers stopped and require operator action rather than claim equivalence to an independently administered runner.
+4
View File
@@ -1,5 +1,9 @@
# Autonomous roadmap follow-up
30 October AUTO-ACTIVATION-01: pre-Phase-10 preparation is conditionally closed at published handoff `5cb903b`; proposed product baseline remains its parent `fc5b9d3`. Existing owner key/authentication and real-key isolation are verified. **Activation remains blocked by the real command-running composite runtime.** See [ACTIVATION_01.md](ACTIVATION_01.md) for current evidence and safe commands; the historical provisioning bullets below do not supersede that report.
AUTONOMOUS-ROADMAP-02 adds the normal-login route in [SINGLE_USER.md](SINGLE_USER.md). Single-user setup and native canary qualification are implemented; the real owner key, same-account sign-in and Phase 10 authorization remain owner actions. The separate-account provisioning below is now optional; single-user MXC qualification is the alternative.
Implementation checkpoints AUTO-00–08 are complete in the qualified scope described in ACCEPTANCE.md. The following are deliberately **not authorized/completed** by installing the controller:
- [ ] Finish current Pre-Phase-10 acceptance; owner records final accepted baseline and candidate-specific cleanup entry.
+1 -1
View File
@@ -1 +1 @@
{"ambiguous_scale": "", "epoch": "v0.9.96-s27-g7154a062ba57", "excludes": [], "follow_symlinks": false, "full_clean": true, "gitignore": true, "graph_mtime_ns": 1793280464911549300, "graph_size": 26584787, "html_present": true, "no_cluster": false, "rcfile": "", "scan_digest": "90bc56c0ff378578f13b3800314eb853", "scan_root": "C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker", "viz_node_limit": ""}
{"ambiguous_scale": "", "epoch": "v0.9.96-s27-g7154a062ba57", "excludes": [], "follow_symlinks": false, "full_clean": true, "gitignore": true, "graph_mtime_ns": 1793294346346335600, "graph_size": 26667137, "html_present": true, "no_cluster": false, "rcfile": "", "scan_digest": "6d4290a488b780835228ade24add6f33", "scan_root": "C:\\Users\\Connor\\.codex\\worktrees\\autonomous-roadmap-01\\Job tracker", "viz_node_limit": ""}
File diff suppressed because one or more lines are too long
+143 -99
View File
@@ -5,13 +5,13 @@
"3": "DashboardView.tsx",
"4": "CancellationToken",
"5": "useI18n",
"6": "Task",
"6": "SummarizerService",
"7": "docs/README.md",
"8": "JobTrackerApi.Services",
"9": "CoverLetterDocumentSettings",
"10": "applicationWorkspace.ts",
"10": "Complete 57-capability matrix",
"11": "CvVariantController",
"12": "ApplicationUser",
"12": "AuthController",
"13": "App.tsx",
"14": "DateTimeOffset",
"15": "translations",
@@ -23,22 +23,22 @@
"21": "translations.ts",
"22": "phase-7/PRESERVATION.md",
"23": "ApplicationUser",
"24": "pathlib",
"24": "hashlib",
"25": "Jobjakt — Master Redesign Specification",
"26": ".DeserializePersisted",
"27": "TailoredCvDocument",
"28": "AccountDeletionService",
"28": "Task",
"29": "StructuredCvProfileJson",
"30": "p5-15/README.md",
"30": "Phase 6 execution plan",
"31": "system_text_json",
"32": "re",
"32": "json",
"33": "AiWorkspaceService",
"34": "ControllerBase",
"35": "MicrosoftGraphOAuthService",
"36": "PRESERVATION_IMPACT_MATRIX.md",
"36": "P4-02 — Named controls and modal state",
"37": "JobTrackerContext",
"38": "CareerProfileService",
"39": "reminders-presentation.test.tsx",
"39": "correspondence-inbox-page.test.tsx",
"40": "ref_node_path",
"41": "test_app.py",
"42": "MariaDbDatabase",
@@ -53,8 +53,8 @@
"51": "JobTracker programme decisions and assumptions",
"52": "Fixture",
"53": ".Get",
"54": "AppShell",
"55": ".InFixtureAsync",
"54": "StructuredCvProfile",
"55": "UserSession",
"56": "net9.0",
"57": "SystemEmailSettings",
"58": "ValueTask",
@@ -66,7 +66,7 @@
"64": "net9.0",
"65": "localisation.mjs",
"66": "todo.md",
"67": "ApplicationChecklistItem",
"67": "ApplicationChecklistService",
"68": "net9.0",
"69": ".Build",
"70": "dependencies",
@@ -109,7 +109,7 @@
"107": ".CreateUserManager",
"108": "cv-import/README.md",
"109": "p8-05/FINAL_ACCEPTANCE.md",
"110": "phase7-cv-library.spec.ts",
"110": "local_mode.py",
"111": "AiOperationExecutionScope",
"112": "mariadb-standardisation/README.md",
"113": "pre-phase10-ux/ACCEPTANCE.md",
@@ -129,14 +129,14 @@
"127": "dependencies",
"128": "CvRenderSection",
"129": "StartupInitializationExtensions",
"130": ".CreateController",
"130": "UsersController",
"131": "cv-integration-qa.spec.ts",
"132": "Job",
"133": "SavedJobSearch",
"134": "JobImportService",
"135": ".New",
"135": "ThemedCvRenderer",
"136": "Career Workspace — Product Strategy & Roadmap",
"137": "p9-requal-01/reconcile.py",
"137": ".Upload",
"138": "p9-requal-01/README.md",
"139": "p9-sec-02/ACCEPTANCE_ADDENDUM_2026-10-25.md",
"140": ".IsSupported",
@@ -145,20 +145,20 @@
"143": "Microsoft.Extensions.DependencyInjection.Abstractions@9.0.14",
"144": "EngineTests",
"145": "mariadb-test",
"146": "ExternalOrigin",
"146": "ExternalOriginTests",
"147": "break-jobjakt.mjs",
"148": "AUTO-00: repository, prompt and CLI audit",
"149": "phase7-oauth-sender.spec.ts",
"150": "AiGenerationException",
"151": "CompaniesController",
"151": "CareerEvidence",
"152": "UserOperationStore",
"153": "ImportExportJobs.tsx",
"153": ".Normalize",
"154": "Fixture",
"155": "P5-15-01R-01 — Saved-search loading and error visibility",
"156": "ListLogger",
"155": "Mock",
"156": "ClientErrorsController",
"157": "CV Builder Product Teardown — Jobbjakt Internal Review",
"158": "JobCvMatchService",
"159": "CvVariant",
"159": "AttachmentConsistencyTests",
"160": "Evidence and production limits",
"161": "phase7-reminders.spec.ts",
"162": "Microsoft.Extensions.Logging.Abstractions@9.0.14",
@@ -178,12 +178,12 @@
"176": "CoverLetterVersion",
"177": "deploy.sh",
"178": "phase7-job-import.spec.ts",
"179": ".TimeInStage",
"179": ".GetAnalyticsOverviewAsync",
"180": "AiWorkspaceNote",
"181": "Disposable runtime evidence",
"182": "phase7-notifications.spec.ts",
"183": "DocumentsPage.tsx",
"184": "GateError",
"183": "documents-page.test.tsx",
"184": ".Password_change_rotates_the_session_and_keeps_only_the_current_trusted_device",
"185": "_archive/docs/01-glossary.md",
"186": "pre-docs-cleanup-2026-09-11/docs/01-glossary.md",
"187": "Jobjakt — Application Discovery Report",
@@ -201,15 +201,15 @@
"199": "generate-inventory.cjs",
"200": "phase7-discovery.spec.ts",
"201": "phase7-settings-preferences.spec.ts",
"202": "Attachment",
"202": "AiInteraction",
"203": ".SeedJobAsync",
"204": "run-ollama-evaluation.py",
"205": "Infrastructure Investigation — Deployment Failure (Server Unresponsive, Containers Stopped)",
"204": "AiWorkspaceTests",
"205": "subprocess",
"206": "A — Backend suite fails only on the runner",
"207": "pre-docs-cleanup-2026-09-11/docs/product/roadmap.md",
"208": "Candidate widget library",
"209": "publicPushReceipt",
"210": "account-security-flows.test.tsx",
"210": "getApiErrorMessage",
"211": "StrategySnapshotService",
"212": "ApplicationIntelligenceService",
"213": "Findings and implementation packages",
@@ -231,12 +231,12 @@
"229": ".SeedAsync",
"230": "properties",
"231": ".InvokeAsync",
"232": "Fixture",
"232": "cv-import/benchmarks/generate.py",
"233": "phase7-admin-system.spec.ts",
"234": "System.IdentityModel.Tokens.Jwt",
"235": "atomic",
"236": "Dictionary",
"237": "first-production-deployment.md",
"235": "ApplicationChecklistItem",
"236": "Closed since the first pass",
"237": "infrastructure/deployment.md",
"238": "B06 — Ingress boundary implementation and verification record",
"239": "Every original P0/P1 finding",
"240": "Astra / Coding-Agent Handoff",
@@ -262,20 +262,20 @@
"260": "Responsive Design and Accessibility",
"261": "performance/sustained.spec.ts",
"262": "phase7-admin-users.spec.ts",
"263": "devDependencies",
"263": "supply-chain.py",
"264": "Pomelo.EntityFrameworkCore.MySql",
"265": "CvRenderPerson",
"265": "InterviewAiContextTests",
"266": "JobTrackerApi.Data",
"267": "Microsoft.Extensions.Primitives@9.0.14",
"268": "compare-cv-approved-fixtures.py",
"268": "FakeEnv",
"269": "coverage.md",
"270": "JobTracker security threat model",
"271": "Remaining risks",
"272": "CV-UX verification execution plan",
"272": "cv-redesign/ACCEPTANCE.md",
"273": "AI Principles",
"274": "P5-15 — Phase 5 final acceptance",
"275": "Phase 7 — Full migration",
"276": "AdminSystemPage.tsx",
"276": "PageSection",
"277": "JobTracker full-application audit",
"278": "Microsoft.TestPlatform.TestHost",
"279": "InterviewPrepItem",
@@ -290,13 +290,13 @@
"288": "Jobjakt — Implementation Roadmap",
"289": "Verification summary",
"290": "Previous implementation checklist — 2026-08-24",
"291": "CvVariantVersion",
"291": "Version",
"292": "Jobjakt application documents Implementation Plan",
"293": "P5-04b readiness — shared Jobs query",
"294": "P5-13 — CV AI grounding readiness",
"295": "phase7-career-editor.spec.ts",
"296": "phase8-dashboard-customisation.spec.ts",
"297": "GateError",
"297": "controller.py",
"298": "networks",
"299": "Smart Gmail Job Correspondence Integration",
"300": "Jobbjakt next session handoff",
@@ -306,7 +306,7 @@
"304": "pre-docs-cleanup-2026-09-11/docs/product/vision.md",
"305": "pre-docs-cleanup-2026-09-11/docs/ux/onboarding.md",
"306": "Completed",
"307": "CoverLetterWriterTests",
"307": "CvImportLifecycleConcurrencyTests",
"308": "Implementation Plan",
"309": "P5-10 — CV section editing and source/override clarity",
"310": "supported",
@@ -314,7 +314,7 @@
"312": "TailoredCvDraft",
"313": "diagnostics/persistence.spec.ts",
"314": "REVIEW_RULINGS.md",
"315": "tempfile",
"315": "seed-operation.py",
"316": "phase7-account-security.spec.ts",
"317": "phase7-documents-contract.spec.ts",
"318": "phase7-onboarding.spec.ts",
@@ -322,9 +322,9 @@
"320": "xunit.core",
"321": "Castle.Core",
"322": "xunit.core",
"323": ".HandleAsync",
"324": "validate_manifest",
"325": "SubprocessTests",
"323": "UserManager",
"324": ".CreateUserManager",
"325": "Functional Guardrails",
"326": "frontend",
"327": "Independently reviewable Phase 0 work packages",
"328": "Phased sequence (each phase ships independently, app stays green)",
@@ -341,19 +341,19 @@
"339": ".MoveAsync",
"340": "TestAuthentication",
"341": "Full European Internationalisation, Localisation and Cross-Industry Language Support",
"342": "Backups, restore and rollback",
"343": "AiUsageRecord",
"342": "layout.tsx",
"343": "POL-001 Free/Pro entitlement verification",
"344": "JobTracker audit remediation backlog",
"345": "Information Stored",
"346": "Initial Theme Library",
"347": "Production AI hardware and runtime assessment",
"348": "pre-docs-cleanup-2026-09-11/docs/ux/navigation.md",
"349": "UserSession",
"349": ".SeedJobAsync",
"350": "Delivery roadmap",
"351": "S07 UAT Closure: Daily-loop evidence seam",
"352": "ObjectResult",
"352": "Jobbjakt (Job Tracker) — System Overview",
"353": "P7-01 — Authentication and account-security presentation",
"354": "SkillTagger",
"354": "CV-UX verification execution plan",
"355": "Manual acceptance cards — P9-REQUAL-01",
"356": "DocumentsIndexService",
"357": "AiUsageMeter",
@@ -361,7 +361,7 @@
"359": "SQLitePCLRaw.core@2.1.10",
"360": "xunit",
"361": "SQLitePCLRaw.core@2.1.10",
"362": "GmailOAuthService.cs",
"362": "system_net",
"363": "MicrosoftTokenValidatorTests",
"364": "xunit",
"365": "browser-extension/manifest.json",
@@ -373,12 +373,12 @@
"371": "work.md",
"372": "CAREER-002 CV Builder redesign",
"373": "Primary job workflow",
"374": "TailoredCvEducationItem",
"375": "MeteredSummarizerServiceTests",
"374": "p9-requal-01/COMMANDS.md",
"375": "responsive-accessibility-audit.spec.ts",
"376": "P5-00 — Shell readiness gates",
"377": "P9-REQUAL-01 independent source review",
"378": "smoke.spec.ts",
"379": "StructuredCvFieldMetadata",
"379": "StructuredCvContact",
"380": "phase7-public-cv.spec.ts",
"381": "International, multilingual, cross-industry job discovery",
"382": "GJ-08 — Full Cross-Country, Cross-Industry Acceptance and Release Qualification",
@@ -387,7 +387,7 @@
"385": "pre-docs-cleanup-2026-09-11/docs/audits/2026-09-07/validation/b06-completion.md",
"386": "Product coherence, operational readiness and reusable quality gates",
"387": "Threat model, abuse, data lifecycle and invariants",
"388": "Validated implementation queue",
"388": ".Serialize",
"389": "JobTracker full-application audit progress",
"390": "Production AI rollout and rollback",
"391": "flowcv-analysis.md",
@@ -402,9 +402,9 @@
"400": "P5-06 — Personal Dashboard presentation",
"401": "P5-15-01R-03 — Analytics authentication return-path repair",
"402": "P6-03 — Accessibility and visual evidence triage",
"403": "phase7-career-overview.spec.ts",
"403": "Phase 2.1 — CV extraction: current implementation, weaknesses, proposal",
"404": "International Job Discovery Implementation Plan",
"405": "CareerEvidence",
"405": "AUTONOMOUS-ROADMAP-01 acceptance — 29 October 2026",
"406": ".UpdateStatus",
"407": ".Evaluate",
"408": "Microsoft.NET.Test.Sdk",
@@ -416,7 +416,7 @@
"414": "GoogleTokenValidatorTests.cs",
"415": "Microsoft.NET.Test.Sdk",
"416": "s06-acceptance-data.sh",
"417": "unittest",
"417": "pathlib",
"418": "MANUAL VERIFICATION — only the owner can do these",
"419": "healthcheck",
"420": "ollama",
@@ -438,7 +438,7 @@
"436": "Core product principles",
"437": "P4-04 — Page layout and typography primitives",
"438": "P4-05 — Table/list density contract",
"439": "P5-15 preservation acceptance — 57 original groups",
"439": "MicrosoftTokenValidator",
"440": "P5-13 semantic qualification rubric",
"441": "P5-15-01 — Functional and preservation audit",
"442": "P5-15-02 — Responsive and accessibility audit",
@@ -503,7 +503,7 @@
"501": "Desktop and mobile wireframe specifications",
"502": "PRE-P10-UX design and scope",
"503": "Jobs workspace",
"504": "Ownership verification",
"504": "InterviewPrepNote",
"505": "P4-06 — Shared validation checkpoint",
"506": "P5-14 — scoped responsive CV workflow checkpoint",
"507": "P5-15 route and visible-action acceptance plan",
@@ -520,7 +520,7 @@
"518": "QRCoder",
"519": "Moq",
"520": "s06-preflight.sh",
"521": "BrowserRuntimeRetryTests",
"521": "offer-comparison.test.tsx",
"522": "JobLifecycleEvents",
"523": "GJ-00 — Product and Provider Discovery / Architecture Sign-off",
"524": "GJ-03 — Universal Occupation Autocomplete, Skills and Cross-Language Queries",
@@ -586,28 +586,28 @@
"584": "logging.md",
"585": "pre-docs-cleanup-2026-09-11/docs/ux/design-system.md",
"586": "responsive.md",
"587": "AI-004 durable CV processing",
"588": "CORE-001 — SQLite/provider parity verification",
"587": "SubmittedPackageAttachment",
"588": "running",
"589": "DEP-001 frontend advisory remediation",
"590": "JOBS-001 job discovery verification",
"591": "MariaDB production bootstrap",
"591": "saved-search-repair.spec.ts",
"592": "SEC-006 parser dependency verification",
"593": "VER-001 complete local regression",
"594": "Job Tracker frontend",
"595": "S06 Acceptance Run",
"596": ".InFixtureAsync",
"597": "cv-import/benchmarks/evaluate.py",
"598": "P5-08b readiness — native packages and workspace boundaries",
"596": "RewriteSectionRequest",
"597": "P5-09 readiness — CV integrity and rendering evidence",
"598": "P5-11 — Live preview presentation",
"599": "Acceptance criteria and test plan",
"600": "Template audit and versioned design library",
"601": "Job detail UX",
"602": "P5-13 grounding qualification checkpoint",
"603": "11. Technical debt",
"602": "Completed redesign release preparation — 2026-10-08",
"603": "ExternalOrigin",
"604": "Autonomous Jobjakt backlog controller",
"605": "P4-03 — Builder preview keyboard boundary",
"606": "P5-01 — AppShell and grouped navigation",
"607": "P5-04b — Table/Board integration",
"608": "P5-13 human review packet",
"608": "Application shell",
"609": "P5-13 qualification matrix — frozen 366-call contract",
"610": "P5-15 bounded acceptance programme",
"611": "P5-15-02 responsive measurements",
@@ -626,7 +626,7 @@
"624": "SQLitePCLRaw.core",
"625": "Microsoft.TestPlatform.ObjectModel",
"626": "xunit.extensibility.core",
"627": "ClientErrorsController",
"627": "Phase 4 implementation plan",
"628": "Microsoft.AspNetCore.Cryptography.KeyDerivation",
"629": "Microsoft.IdentityModel.Logging",
"630": "System.Drawing.Common",
@@ -675,8 +675,8 @@
"673": "pre-docs-cleanup-2026-09-11/docs/architecture/frontend.md",
"674": "architecture/migrations.md",
"675": "Resilience, partial failure and the 50 failure answers",
"676": "B01 — HTTP import destination enforcement",
"677": "Release checklist",
"676": "StartupAndLockTests",
"677": ".Geometry_is_durable_owner_scoped_and_stale_or_legacy_writes_preserve_other_preferences",
"678": "QA-001 job-analysis and important-term quality",
"679": "JT-006 — document-parser upgrades and resource isolation",
"680": "JT-001 — Microsoft tenant/issuer validation and safe identity linking",
@@ -706,7 +706,7 @@
"704": "SEC-007 parser isolation verification",
"705": "SEC-008 attachment consistency verification",
"706": "UX-003 Kanban theme-state verification",
"707": "SEC-009 account export and deletion lifecycle",
"707": "DatabaseBackupRunnerTests",
"708": "CV-QA structural benchmark",
"709": "FlowCV UI/UX investigation",
"710": "17 October 2026 release repair",
@@ -716,7 +716,7 @@
"714": "Accessibility plan",
"715": "ROADMAP.md — Jobbjakt Product & Engineering Roadmap",
"716": "Documents UX",
"717": "Gate authority and Phase 10 entry",
"717": "Run from your existing Windows and ChatGPT account",
"718": "Route plan",
"719": "Phase 5 visual and workflow acceptance",
"720": "P6-01 — Shared design-system corrections",
@@ -729,19 +729,19 @@
"727": "Phase 7 final acceptance — 21 October 2026",
"728": "P7-12 — Integrated migration acceptance",
"729": "shell-readiness.spec.ts",
"730": "CvBlockClassificationResult",
"730": "JobTracker master work plan",
"731": "P9-REQUAL-01 findings and independent gate register",
"732": ".GetAnalyticsOverviewAsync",
"732": "OracleTests",
"733": "P9-SEC-02 hosted CI assessment — 25 October 2026",
"734": "RD21 acceptance addendum — 23 October 2026",
"735": "json",
"736": "s06-acceptance-data.test.sh",
"737": "start-ollama-cv.sh script",
"738": "status",
"739": "Test baseline and gaps",
"740": "3. Target User Personas",
"738": "CV-UX-01 acceptance and handoff",
"739": "CV-UX implementation checkpoint — 25 October 2026",
"740": "Career Profile UX",
"741": "bootstrap-and-test.sh",
"742": "Production readiness review",
"742": "Commands, notifications and tasks",
"743": "pre-docs-cleanup-2026-09-11/docs/ai/prompts.md",
"744": "Attachment storage invariants",
"745": "Phase 2 — regression tests, observability and recovery",
@@ -773,12 +773,12 @@
"771": "D owner-addendum qualification",
"772": "Owner additions, 27 October 2026",
"773": "P5-04a — Board presentation",
"774": "Qualification-environment recovery — 2026-10-08",
"775": "Procedure when the configured target becomes available",
"774": "CV workspace",
"775": "Design-system architecture",
"776": "P5-15-02 — Accessibility and responsive findings",
"777": "Phase 5 — Representative screens / major UI redesign plan",
"778": "Release-candidate preservation review — 2026-10-08",
"779": "AI interaction model",
"779": "p5-13-readiness/README.md",
"780": "Phase 7 — Remaining migration handoff",
"781": "P6-05 — Preservation reconciliation",
"782": "P7-00 — Remaining migration inventory",
@@ -786,15 +786,15 @@
"784": "P7-06 — Inbox and read review",
"785": "P7-09 — OAuth callback sender repair",
"786": "Owner-authorized main promotion — 2026-10-17",
"787": "Phase 9 acceptance — 23 October 2026",
"787": "p9-00/INVENTORY.md",
"788": "P4-01 — semantic colors, focus and loaded-state protection",
"789": "P7-09 — Job import compatibility prerequisite",
"789": ".exercise",
"790": "RD21 early frontend review",
"791": "RD21 final-review fix wave",
"792": "Save and conflict contract",
"793": "Phase 7 execution — 2026-10-11",
"794": "overrides",
"795": "account-isolation.spec.ts",
"795": "RD21 remediation plan",
"796": "Microsoft.EntityFrameworkCore.Sqlite",
"797": "Otp.NET",
"798": "SkiaSharp.NativeAssets.Linux.NoDependencies",
@@ -806,13 +806,13 @@
"804": "start-ollama-cv.ps1",
"805": "Jobjakt Quick Capture",
"806": "CV Changes",
"807": "semantic-foundations.spec.ts",
"807": "TailoredCvTemplateCatalog",
"808": "Dependency and supply-chain evidence",
"809": "Current Product Goals",
"810": "Themes",
"811": "Authentication Requirements",
"812": "Source-Of-Truth Documents",
"813": "Job opportunity cutover",
"813": ".Build",
"814": "feature-comparison.md",
"815": "DateTime",
"816": "AUTONOMOUS-ROADMAP-01 design and implementation checklist",
@@ -829,9 +829,9 @@
"827": "Provider rights and feasibility — GJ-00",
"828": "Import/history entry and findings",
"829": "Jobjakt expanded production-readiness audit",
"830": "Phase 0 — validated implementation design",
"831": "Phase 1 — broken core workflows and production reliability",
"832": "BG-001 tenant-safe worker foundation verification",
"830": "Implementation contract",
"831": "Verification record",
"832": "SEC-001 canonical-origin verification",
"833": "TODO",
"834": "fields",
"835": "BouncyCastle.Cryptography",
@@ -911,7 +911,7 @@
"909": "next.config.js",
"910": "Dependencies, source authority and entry gates",
"911": "AvatarImageProcessor.cs",
"912": "Product requirements — proposed, GJ-00",
"912": "CV-UX-01 scope and entry decision",
"913": "export-cv-corpus-json.sh",
"914": "SKILL.md",
"915": "ALL_PHASE_PROMPTS.md",
@@ -1096,14 +1096,58 @@
"1094": "Approve-JobjaktBacklog.ps1",
"1095": "Run-JobjaktBacklog.ps1",
"1096": "P5-15-02 — Inherited axe triage",
"1097": "P7-08 — Recent notifications and shell count",
"1098": "P7-09 — Preferences and Connections",
"1097": "Dashboard architecture",
"1098": "Job workspace and guided journey",
"1099": "Shared native fixture isolation",
"1100": "RevisionConflictExceptionHandler.cs",
"1101": "Acceptance and evidence matrix",
"1102": "Phase 5 open blockers and deferred issues",
"1103": "CsvExportTests.cs",
"1104": "Actionable findings",
"1105": "useWorkspaceTabCache.ts",
"1106": "Synthetic-Signer.ps1"
"1103": "Phase 3 — UX architecture checkpoint",
"1104": "P8-03 backend persistence evidence",
"1105": ".exercise",
"1106": "Synthetic-Signer.ps1",
"1107": "CorrespondenceAttachmentMetadata",
"1108": "MicrosoftGraphTokenResponse",
"1109": "Authorized release attempt and CI prerequisite correction",
"1110": "SEC-002 ingress and Compose verification",
"1111": "Information architecture",
"1112": "Open decisions and requirement boundaries",
"1113": "Responsive strategy and density",
"1114": "Configured-model environment checkpoint — 2026-10-08",
"1115": "P9-REMED-02 hosted runner isolation — 24 October 2026",
"1116": "P9-REQUAL-01 — Hosted CI and Phase 9 Requalification",
"1117": "Phase 5 — optional enhancements/hardening",
"1118": "Current route and page map",
"1119": "Component architecture",
"1120": "Preservation impact and traceability",
"1121": "P4-01 preservation and visual evidence",
"1122": "P5-15-01 route results",
"1123": "P5-15-02 — Native audit coverage checkpoint",
"1124": "P5-15-03 — CV and document correctness",
"1125": "P5-15-04 — Targeted acceptance repairs",
"1126": "P6-00 — Baseline and design consistency audit",
"1127": "P7-09 — Privacy, usage and backup presentation",
"1128": "P7-10 Audit",
"1129": "Phase8 handoff — Dashboard customisation",
"1130": "SQLite startup concurrency repair",
"1131": "Phase 3 — architecture and maintainability",
"1132": "Phase 2 preservation matrix",
"1133": "P5-15-01 intermittent observations",
"1134": "Native journeys, actionable controls and evidence limits",
"1135": "P5-15-04-A11Y-01 — Native file-picker keyboard repair",
"1136": "P5-15-04-A11Y-02 — Mobile navigation focus lifecycle",
"1137": "P5-15-04-CV-01 — Initially hidden preview lifecycle",
"1138": "SURFACES.md",
"1139": "P6-02 — Existing AI interaction review",
"1140": "P6-03 — Responsive, accessibility and localisation",
"1141": "P7-10 System",
"1142": "SINGLE_USER_PLAN.md",
"1143": "automation/TODO.md",
"1144": "P4-06 visual review",
"1145": "p5-15-03/VISUAL_REVIEW.md",
"1146": "P5-15-04-A11Y-04 — Recorded-status targets",
"1147": "Phase 5 preservation impact",
"1148": "Probe-JobjaktSandbox.ps1",
"1149": "Use-JobjaktBacklog.ps1",
"1150": "Register-JobjaktBacklogTask.ps1"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+123 -21
View File
@@ -2922,9 +2922,9 @@
"semantic_hash": ""
},
"automation/controller.py": {
"mtime": 1793280238.5908332,
"seen": 1793280471.2824943,
"ast_hash": "455ea261b83eeb6bd532a4976d7cc24e",
"mtime": 1793293595.874426,
"seen": 1793294358.1061978,
"ast_hash": "8e59b2c1c9bef998b9b779b51d06590f",
"semantic_hash": ""
},
"automation/prepare_queue.py": {
@@ -2952,9 +2952,9 @@
"semantic_hash": ""
},
"automation/tests/fake_codex.py": {
"mtime": 1793278677.5298085,
"seen": 1793279715.6414454,
"ast_hash": "cae0d4d55269a46e2079b55e2299a622",
"mtime": 1793293370.5754836,
"seen": 1793294358.1126983,
"ast_hash": "b654b8a000b6d0eebd21594266202ac0",
"semantic_hash": ""
},
"automation/tests/test_controller.py": {
@@ -8286,15 +8286,15 @@
"semantic_hash": ""
},
"scripts/Approve-JobjaktBacklog.ps1": {
"mtime": 1793279053.0434895,
"seen": 1793279715.6424453,
"ast_hash": "5c813d83e116a1a50e0cc0552547e571",
"mtime": 1793283148.0269737,
"seen": 1793294358.787304,
"ast_hash": "f24a0ed65eb915765da2028c85ee6f8b",
"semantic_hash": ""
},
"scripts/Run-JobjaktBacklog.ps1": {
"mtime": 1793278677.5288026,
"seen": 1793279715.6424453,
"ast_hash": "52f0ef618b316d473f4889039f8cc8e3",
"mtime": 1793294154.75885,
"seen": 1793294358.787304,
"ast_hash": "a73b489fc9a7aa534bc334ca894c698c",
"semantic_hash": ""
},
"scripts/compare-cv-approved-fixtures.py": {
@@ -11556,21 +11556,21 @@
"semantic_hash": ""
},
"docs/automation/PLAN.md": {
"mtime": 1793277922.1833713,
"seen": 1793279715.6429453,
"ast_hash": "6ac873996c73eaba07827dc9288c6497",
"mtime": 1793280594.0974283,
"seen": 1793294359.0804758,
"ast_hash": "8a47d335c2c4d7c21f2d99a1ac8b921d",
"semantic_hash": ""
},
"docs/automation/README.md": {
"mtime": 1793280116.1318583,
"seen": 1793280471.59496,
"ast_hash": "8edb3478e9aa72a72372c22cdc73bba8",
"mtime": 1793293778.9563334,
"seen": 1793294359.0804758,
"ast_hash": "8780bb7fa6af66ba3f10b125f7221dc9",
"semantic_hash": ""
},
"docs/automation/SECURITY.md": {
"mtime": 1793280116.1308582,
"seen": 1793280471.59496,
"ast_hash": "e6cb7e47b43f7771b2582ad2cb5e5644",
"mtime": 1793293778.956833,
"seen": 1793294359.0804758,
"ast_hash": "945210658c7d46829477a0c1667da879",
"semantic_hash": ""
},
"docs/backend/README.md": {
@@ -20879,6 +20879,108 @@
"ast_hash": "68ee75ee0f35aabac679ff5018a9911a",
"semantic_hash": ""
},
"automation/local_mode.py": {
"mtime": 1793293595.8729236,
"seen": 1793294358.1061978,
"ast_hash": "66233045a5fe2f53d9a3524961c08079",
"semantic_hash": ""
},
"automation/reports/single-user/capabilities.json": {
"mtime": 1793294232.3041594,
"seen": 1793294358.108197,
"ast_hash": "fcf579a2cd7fd07b4d0d14773466d99c",
"semantic_hash": ""
},
"automation/reports/single-user/native-qualification.json": {
"mtime": 1793293951.3249533,
"seen": 1793294358.108197,
"ast_hash": "29afb7bbe4a77d8462cee956ac411049",
"semantic_hash": ""
},
"automation/reports/single-user/results.json": {
"mtime": 1793294007.3021033,
"seen": 1793294358.108197,
"ast_hash": "595d5b4f1a74c12988822cb607f7a95a",
"semantic_hash": ""
},
"automation/reports/single-user/setup-results.json": {
"mtime": 1793293952.1028857,
"seen": 1793294358.108197,
"ast_hash": "5764dc1fc311f8b4c7b63784e922a874",
"semantic_hash": ""
},
"automation/reports/single-user/signature-results.json": {
"mtime": 1793293887.7429514,
"seen": 1793294358.108197,
"ast_hash": "adf52d333b4c7f6684ee00f9050ca928",
"semantic_hash": ""
},
"automation/tests/test_local_mode.py": {
"mtime": 1793293778.9553325,
"seen": 1793294358.1136973,
"ast_hash": "5ca228ad851df1c0634cc3f4cd642f6e",
"semantic_hash": ""
},
"scripts/Probe-JobjaktSandbox.ps1": {
"mtime": 1793283366.6595008,
"seen": 1793294358.787304,
"ast_hash": "78ac4dba473a8a360f58d4c6f06aa646",
"semantic_hash": ""
},
"scripts/Register-JobjaktBacklogTask.ps1": {
"mtime": 1793283729.3885343,
"seen": 1793294358.787304,
"ast_hash": "76bee12ffce53004ae82bc0126039935",
"semantic_hash": ""
},
"scripts/Use-JobjaktBacklog.ps1": {
"mtime": 1793283366.6574895,
"seen": 1793294358.787304,
"ast_hash": "d5e222409eeac0c25e2bcc8554e731c5",
"semantic_hash": ""
},
"automation/prompts/global-jobs-planning/TODO.md": {
"mtime": 1793279995.0849574,
"seen": 1793294358.8412056,
"ast_hash": "bf102097522ebe88c5201b3c76254535",
"semantic_hash": ""
},
"automation/reports/single-user/signature-tests.txt": {
"mtime": 1793293887.7419424,
"seen": 1793294358.865369,
"ast_hash": "ad94d54fe0b4bd3df6da109833c1e6b9",
"semantic_hash": ""
},
"automation/reports/single-user/tests.txt": {
"mtime": 1793294353.7928636,
"seen": 1793294358.865369,
"ast_hash": "75addcfe721beab1e7ba831cef95006c",
"semantic_hash": ""
},
"docs/automation/ACCEPTANCE.md": {
"mtime": 1793280594.098433,
"seen": 1793294359.0794702,
"ast_hash": "46485dec20c3a433783726b33664c02c",
"semantic_hash": ""
},
"docs/automation/SINGLE_USER.md": {
"mtime": 1793293778.9578352,
"seen": 1793294359.0804758,
"ast_hash": "aa5e82f50dd993baf30cd46e22344c80",
"semantic_hash": ""
},
"docs/automation/SINGLE_USER_PLAN.md": {
"mtime": 1793282880.4392192,
"seen": 1793294359.0804758,
"ast_hash": "dc5c7a4cb7a12d2229e94168ca652b85",
"semantic_hash": ""
},
"docs/automation/TODO.md": {
"mtime": 1793280594.0994372,
"seen": 1793294359.0804758,
"ast_hash": "5d0be19d6032e101a97a0cc8ce85b386",
"semantic_hash": ""
},
"job-tracker-ui/package-lock.json": {
"mtime": 1793277698.2901168,
"seen": 1793279715.6444461,
+4 -2
View File
@@ -1,6 +1,7 @@
#Requires -Version 7.2
<# The owner runs NewKey/Sign on a separate Windows identity or signing machine.
Only the public certificate reaches the runner. Never export the private key. #>
<# The owner signs from a trusted operator terminal, with workers stopped.
Same-user unattended use additionally requires native signing-key denial probes.
Dedicated-runner installations may keep the key under a separate identity. #>
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateSet('NewKey','ExportPublicKey','Sign','Verify')][string]$Action,
@@ -15,6 +16,7 @@ param(
)
$ErrorActionPreference = 'Stop'
try {
if ($Action -ne 'Verify' -and $env:JOBBJAKT_ORCHESTRATED) { throw 'Autonomous workers cannot invoke owner signing actions' }
if ($Action -eq 'NewKey') {
$certificate = New-SelfSignedCertificate -Subject 'CN=Jobjakt Roadmap Owner' -CertStoreLocation Cert:\CurrentUser\My `
-KeyAlgorithm RSA -KeyLength 3072 -HashAlgorithm SHA256 -KeyExportPolicy NonExportable `
+75
View File
@@ -0,0 +1,75 @@
#Requires -Version 7.2
param([ValidateSet('CreateKey','RemoveKey','Probe','TestOwnerKey')][string]$Action,[string]$Tag,[string]$Thumbprint,[string]$PlanPath)
$ErrorActionPreference = 'Stop'
if ($Action -eq 'CreateKey') {
$cert = New-SelfSignedCertificate -Subject "CN=Jobjakt-SYNTHETIC-$Tag" -CertStoreLocation Cert:\CurrentUser\My `
-KeyAlgorithm RSA -KeyLength 2048 -KeyExportPolicy NonExportable -KeyUsage DigitalSignature -NotAfter (Get-Date).AddHours(2)
try {
$rsa = [Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$null = $rsa.SignData([Text.Encoding]::UTF8.GetBytes('synthetic-positive-control'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
@{thumbprint=$cert.Thumbprint;keyName=$rsa.Key.KeyName;tag=$Tag} | ConvertTo-Json -Compress
} catch {
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -DeleteKey
throw
} finally { if ($rsa) { $rsa.Dispose() } }
exit 0
}
if ($Action -eq 'TestOwnerKey') {
$cert=Get-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint"
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
try {
$null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('positive-control-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
@{keyName=$rsa.Key.KeyName;certificateSha256=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($cert.RawData)).ToLowerInvariant()} | ConvertTo-Json -Compress
} finally { $rsa.Dispose() }
exit 0
}
if ($Action -eq 'RemoveKey') {
$cert = Get-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint"
if ($cert.Subject -cne "CN=Jobjakt-SYNTHETIC-$Tag") { throw 'Refusing to remove any non-canary certificate' }
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$Thumbprint" -DeleteKey
exit 0
}
$plan = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json
$records = @()
foreach ($item in $plan.files) {
$readDenied=$false; $writeDenied=$false
try { $f=[IO.File]::Open($item.path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite); $f.Dispose() } catch { $readDenied=$true }
try { $f=[IO.File]::Open($item.path,[IO.FileMode]::Open,[IO.FileAccess]::Write,[IO.FileShare]::ReadWrite); $f.Dispose() } catch { $writeDenied=$true }
$records += @{category=$item.category;readDenied=$readDenied;writeDenied=$writeDenied}
}
function Can-SignCertificate([string]$CertificateThumbprint) {
if (-not $CertificateThumbprint) { return $false }
try {
$cert=Get-Item -LiteralPath "Cert:\CurrentUser\My\$CertificateThumbprint"
$rsa=[Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); return $true }
finally { if ($rsa) { $rsa.Dispose() } }
} catch { return $false }
}
$canSign = Can-SignCertificate $plan.key.thumbprint
try {
$key=[Security.Cryptography.CngKey]::Open($plan.key.keyName)
$rsa=[Security.Cryptography.RSACng]::new($key)
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); $canSign=$true }
finally { $rsa.Dispose(); $key.Dispose() }
} catch {}
$ownerCanSign = Can-SignCertificate $plan.ownerThumbprint
if ($plan.ownerKeyName) {
try {
$key=[Security.Cryptography.CngKey]::Open($plan.ownerKeyName)
$rsa=[Security.Cryptography.RSACng]::new($key)
try { $null=$rsa.SignData([Text.Encoding]::UTF8.GetBytes('sandbox-canary-only-no-approval'),[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1); $ownerCanSign=$true }
finally { $rsa.Dispose(); $key.Dispose() }
} catch {}
}
$networkDenied=$false
$client=[Net.Sockets.TcpClient]::new()
try { $connect=$client.ConnectAsync('127.0.0.1',[int]$plan.port); if (-not $connect.Wait(2000)) { $networkDenied=$true } else { $networkDenied=-not $client.Connected } }
catch { $networkDenied=$true }
finally { $client.Dispose() }
$positive=([IO.File]::ReadAllText($PlanPath).Length -gt 0)
$positivePath=Join-Path (Split-Path $PlanPath) 'positive.txt'
try { [IO.File]::WriteAllText($positivePath,'synthetic'); $positive=$positive -and [IO.File]::ReadAllText($positivePath).Equals('synthetic') }
catch { $positive=$false }
@{positiveControl=$positive;files=$records;networkDenied=$networkDenied;signingKeyDenied=(-not $canSign);
ownerKeyDenied=([bool]$plan.ownerThumbprint -and -not $ownerCanSign)} | ConvertTo-Json -Depth 8 -Compress
+26
View File
@@ -0,0 +1,26 @@
#Requires -Version 7.2
[CmdletBinding()]
param(
[string]$ControlRoot = (Join-Path ([Environment]::GetFolderPath('UserProfile')) '.jobjakt-backlog/single-user-local'),
[string]$Name = 'Jobjakt development backlog',
[switch]$Install
)
$ErrorActionPreference='Stop'
$root=[IO.Path]::GetFullPath($ControlRoot)
$config=Get-Content -LiteralPath (Join-Path $root 'config.json') -Raw | ConvertFrom-Json
$check='import sys,json; from pathlib import Path; root=Path(sys.argv[1]); sys.path.insert(0,str(root)); import local_mode; c=json.loads((root/"config.json").read_text()); local_mode.require_unattended(root,c); local_mode.validate_identity(c,local_mode.detect_identity()[1])'
& $config.pythonExecutable -c $check $root
if ($LASTEXITCODE -ne 0) { throw 'Task registration requires current native filesystem/network/owner-key qualification; no task created' }
if (-not $Install) {
Write-Output "Preview: limited task '$Name', every 15 minutes and at logon, command start (preserves manual pause). Add -Install to register."
exit 0
}
if (-not [Environment]::UserInteractive -or $env:JOBBJAKT_ORCHESTRATED) { throw 'Register from your operator terminal' }
$action=New-ScheduledTaskAction -Execute $config.powershellExecutable -WorkingDirectory $root `
-Argument ('-NoProfile -WindowStyle Hidden -File "'+(Join-Path $root 'Run-JobjaktBacklog.ps1')+'" -Command start -ControlRoot "'+$root+'"')
$repeat=New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 15)
$logon=New-ScheduledTaskTrigger -AtLogOn -User $config.windowsUsername
$principal=New-ScheduledTaskPrincipal -UserId $config.windowsUsername -LogonType Interactive -RunLevel Limited
$settings=New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 8)
Register-ScheduledTask -TaskName $Name -Action $action -Trigger @($repeat,$logon) -Principal $principal -Settings $settings | Out-Null
Write-Output 'Registered for the existing login. A locked desktop is supported; logged-out execution needs separate Task Scheduler credential setup. No wake/sleep/security setting changed.'
+19 -3
View File
@@ -1,22 +1,38 @@
#Requires -Version 7.2
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateSet('init','validate','dry-run','start','resume','status','pause','retry','current','logs','recover','configure')][string]$Command,
[Parameter(Mandatory)][string]$ControlRoot,
[Parameter(Mandatory)][ValidateSet('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')][string]$Command,
[string]$ControlRoot,
[string]$Repository,
[string]$Stage,
[string]$Manifest,
[string]$Python = 'python'
)
$ErrorActionPreference = 'Stop'
if (-not $ControlRoot) {
$ControlRoot = if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'config.json')) { $PSScriptRoot } else { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.jobjakt-backlog/single-user-local' }
}
$root = [IO.Path]::GetFullPath($ControlRoot)
if ($Command -eq 'init') {
if ($Command -in @('init','setup')) {
$engine = Join-Path $PSScriptRoot '../automation/controller.py'
if (-not (Test-Path -LiteralPath $engine)) { $engine = Join-Path $root 'controller.py' }
if (-not $Repository) {
if (Test-Path -LiteralPath (Join-Path $root 'config.json')) {
$Repository = (Get-Content -LiteralPath (Join-Path $root 'config.json') -Raw | ConvertFrom-Json).repository
} else {
$Repository = & git -C $PSScriptRoot rev-parse --show-toplevel
if ($LASTEXITCODE -ne 0) { throw 'Specify the isolated worker repository with -Repository' }
}
}
} else {
$engine = Join-Path $root 'controller.py'
}
if (-not (Test-Path -LiteralPath $engine -PathType Leaf)) { throw "Controller missing: $engine" }
$arguments = @($engine, $Command, '--control-root', $root)
if ($Command -notin @('init','setup') -and (Test-Path -LiteralPath (Join-Path $root 'config.json'))) {
$config = Get-Content -LiteralPath (Join-Path $root 'config.json') -Raw | ConvertFrom-Json
if ($Python -eq 'python' -and $config.pythonExecutable) { $Python = $config.pythonExecutable }
}
if ($Repository) { $arguments += @('--repository', $Repository) }
if ($Stage) { $arguments += @('--stage', $Stage) }
if ($Manifest) { $arguments += @('--manifest', $Manifest) }
+6
View File
@@ -0,0 +1,6 @@
# Dot-source after setup. This changes only the current terminal, never your profile.
function global:Jobjakt {
$control = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.jobjakt-backlog/single-user-local'
$config = Get-Content -LiteralPath (Join-Path $control 'config.json') -Raw | ConvertFrom-Json
& $config.powershellExecutable -NoProfile -File (Join-Path $control 'Run-JobjaktBacklog.ps1') @args
}