feat(automation): add durable runner and signed approval recovery

This commit is contained in:
cesnimda
2026-10-29 14:33:50 +01:00
parent 4da3939547
commit 607ee1853e
29 changed files with 29694 additions and 7 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"repository": "REPLACE_WITH_ISOLATED_DEVELOPMENT_CLONE",
"codexExecutable": "codex",
"powershellExecutable": "pwsh",
"publicKeyCertificate": "owner.cer",
"ownerCertificateSha256": "REPLACE_WITH_PUBLIC_CERTIFICATE_SHA256",
"ownerSid": "REPLACE_WITH_OWNER_SIGNING_SID",
"runnerSid": "REPLACE_WITH_SEPARATE_RUNNER_SID",
"workerCodeXHome": "C:\\JobjaktRunnerAuth",
"trustedCodexOverrides": [],
"executionWindows": [],
"maxSessionSeconds": 7200,
"maxCheckSeconds": 3600,
"maxRunSeconds": 28800,
"maxLogBytes": 52428800,
"maxProcesses": 64,
"maxJobMemoryMiB": 8192
}
File diff suppressed because it is too large Load Diff
+170
View File
@@ -0,0 +1,170 @@
"""Import verified original prompt bytes into a NEW queue; never import into a live control plane."""
import argparse
import hashlib
import json
from pathlib import Path
import re
import shutil
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def prepare(source, output, project):
if (output / 'backlog.json').exists():
raise SystemExit('Queue already exists; preserve it and prepare changes in a new directory for owner review')
output.mkdir(parents=True, exist_ok=True)
inventory = []
for path in sorted(source.rglob('*')):
if path.is_file() and path.suffix.lower() in ('.md', '.txt'):
inventory.append(dict(filename=str(path.relative_to(source)), bytes=path.stat().st_size, sha256=sha(path)))
def find(pattern):
matches = [p for p in source.rglob('*') if p.is_file() and re.search(pattern, p.name, re.I)]
if len(matches) > 1:
raise SystemExit('Ambiguous prompt match: ' + pattern)
return matches[0] if matches else None
def snapshot(path):
target = output / 'prompts' / path.relative_to(source)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, target)
return target.relative_to(output).as_posix(), sha(target)
def check(name, argv, report=None, cwd='.', configured=True):
return dict(id=name, argv=argv, cwd=cwd, report=report, configured=configured)
core_checks = [
check('backend', ['dotnet','test','JobTrackerApi.Tests/JobTrackerApi.Tests.csproj','--logger',
'trx;LogFileName=backend.trx','--results-directory','.artifacts/orchestrator/backend'],
dict(format='trx',path='.artifacts/orchestrator/backend/backend.trx',minimumPassed=1)),
check('frontend', ['cmd','/d','/c','npm','test','--','--runInBand','--json',
'--outputFile=../.artifacts/orchestrator/frontend.json'],
dict(format='jest',path='.artifacts/orchestrator/frontend.json',minimumPassed=1), 'job-tracker-ui'),
check('types', ['cmd','/d','/c','npm','run','typecheck'], cwd='job-tracker-ui'),
check('lint', ['cmd','/d','/c','npm','run','lint'], cwd='job-tracker-ui'),
check('build', ['cmd','/d','/c','npm','run','build'], cwd='job-tracker-ui'),
check('qualified-provider-browser-python-pdf-security-preservation',
['pwsh','-NoProfile','-File','automation/qualification/Run-StageQualification.ps1'],
dict(format='junit',path='.artifacts/orchestrator/qualification.xml',minimumPassed=1),
configured=False)
]
# The composite adapter is intentionally unconfigured: a local sandbox cannot invent hosted,
# provider, PDF, preservation, AI or human qualification. Owner installs a concrete trusted plan.
context = ['docs/pre-phase10-ux/ACCEPTANCE.md','docs/pre-phase10-ux/CV_QA_HANDOFF.md',
'docs/cv-integration-qa/PHASE_10_HANDOFF.md','docs/redesign/IMPLEMENTATION_PLAN.md']
definitions = [
('PHASE-10',5,r'^Prompt 5\b','feature/phase10-cleanup',
['phase10-development-entry'], ['P10-00','P10-01','P10-02','P10-03','P10-04','P10-05']),
('PRODUCT-REVIEW-01',6,r'^Prompt 6\b','planning/product-review-01',
[], ['independent-assessment','ranked-roadmap']),
('INT-GLOBAL-JOBS-01',7,r'^Prompt 7\b','feature/int-global-jobs-01',
['product-ranked-scope','global-jobs-implementation'], ['GJ-01','GJ-02','GJ-03','GJ-04','GJ-05','GJ-06','GJ-07','GJ-08']),
('I18N-01',8,r'^I18N-01\b','feature/i18n-01',
['i18n-scope'], ['locale-inventory','translation-implementation','locale-qualification']),
('LAUNCH-SITE-01',9,r'^Prompt 10\b','feature/launch-site-redesign',
[], ['homepage-research','four-designs','approved-design-implementation','homepage-qualification']),
('SECURITY-AUDIT-01',10,r'^Prompt 8\b','test/security-audit-01',
['security-test-environment-and-scope'], ['threat-model','adversarial-testing','remediation','security-retest']),
('RELEASE-READINESS-01',11,r'^prompt 11\b','test/release-readiness-01',
[], ['release-inventory','qualification','release-recommendation'])
]
stages = []
for workstream, sequence, pattern, branch, approvals, checkpoints in definitions:
if workstream == 'I18N-01':
original = source / 'I18N/00_MASTER_ORCHESTRATOR.md'
original = original if original.is_file() else None
checkpoints = [f'I18N-{n:02d}' for n in range(9)]
elif workstream == 'SECURITY-AUDIT-01':
original = source / 'jobjakt-security/MASTER_PROMPT.md'
original = original if original.is_file() else None
checkpoints = [f'SEC-{n:02d}' for n in range(17)]
else:
original = find(pattern)
filename, prompt_hash = snapshot(original) if original else (f'prompts/MISSING-{workstream}.txt', '')
source_status = 'MISSING' if not original else ('EMPTY' if original.stat().st_size == 0 else 'VERIFIED')
stage = dict(id=workstream,sequence=sequence,sourcePrompt=filename,promptSha256=prompt_hash,
sourceFilename=str(original) if original else None,sourceStatus=source_status,
dependencies=[stages[-1]['id']] if stages else [],branch=branch,
requiredOwnerApprovals=approvals,retryPolicy=dict(maxAttempts=3,maxSessions=30,backoffSeconds=30),
status='PENDING',evidence=[],reports=[],contextFiles=context,checkpoints=[],supplementalPrompts=[])
for checkpoint in checkpoints:
after = []
if workstream == 'PRODUCT-REVIEW-01' and checkpoint == 'ranked-roadmap':
after = ['product-ranked-scope']
if workstream == 'I18N-01' and checkpoint == 'I18N-08':
after = ['human-linguistic-and-market-qualification']
if checkpoint == 'four-designs':
after = ['homepage-design-selection']
if checkpoint == 'release-recommendation':
after = ['release-evidence-review'] # review only; never deployment approval
before = []
if checkpoint == 'GJ-04':
before = ['provider-licences-and-display-rights']
if checkpoint == 'SEC-13':
before = ['isolated-adversarial-targets']
if checkpoint == 'SEC-15':
after = ['material-security-findings-disposition']
artifacts = [f'docs/automation/stages/{workstream}/{checkpoint}.md']
if checkpoint == 'four-designs':
artifacts += [f'docs/automation/stages/{workstream}/concept-{letter}.png' for letter in 'ABCD']
stage['checkpoints'].append(dict(id=checkpoint,
instruction=f'Complete only {checkpoint} within {workstream}, preserving every full source requirement. '
'Read prior evidence. Stop for any source-required owner decision. No production actions.',
approvalsBefore=before,approvalsAfter=after,deferrableGates=[],requiredArtifacts=artifacts,
checks=core_checks,evidenceRequirements=[
'Process exits and exact structured suite membership; no silent skips/retries',
'Preservation IDs, canonical owners, old-data and document compatibility',
'Separate local/provider/hosted/human/AI/production decisions',
'All source-prompt deliverables and stage-specific acceptance criteria; independent review required']))
if workstream == 'INT-GLOBAL-JOBS-01':
pack = project / 'docs/planning/global-jobs-prompts'
if pack.is_dir():
for path in sorted(pack.glob('*.md')):
target = output / 'prompts/global-jobs-pack' / path.name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, target)
stage['supplementalPrompts'].append(dict(path=target.relative_to(output).as_posix(),sha256=sha(target)))
match = re.search(r'GJ_(\d\d)_', path.name)
if match:
stage['supplementalPrompts'][-1]['checkpoints'] = ['GJ-' + match[1]]
planning = project / 'docs/global-jobs/00'
if planning.is_dir():
for path in sorted(planning.glob('*.md')):
target = output / 'prompts/global-jobs-planning' / path.name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path,target)
stage['supplementalPrompts'].append(dict(path=target.relative_to(output).as_posix(),sha256=sha(target)))
if workstream in ('I18N-01','SECURITY-AUDIT-01'):
pack = source / ('I18N' if workstream == 'I18N-01' else 'jobjakt-security')
for path in sorted(pack.rglob('*')):
if path.is_file():
saved_path, saved_hash = snapshot(path)
if saved_path != filename:
stage['supplementalPrompts'].append(dict(path=saved_path,sha256=saved_hash))
match = re.search(r'I18N_(\d\d)_', path.name) if workstream == 'I18N-01' else re.match(r'(\d\d)_',path.name)
if match:
stage['supplementalPrompts'][-1]['checkpoints'] = [('I18N-' if workstream == 'I18N-01' else 'SEC-') + match[1]]
elif path.name == 'ALL_PHASE_PROMPTS.md':
stage['supplementalPrompts'][-1]['checkpoints'] = [] # preserved duplicate compilation, individual full prompts drive sessions
# Qualitative source requirements cannot be accepted by the implementation model itself.
stage['checkpoints'][-1]['approvalsAfter'].append('stage-requirements-review')
stages.append(stage)
# Preserve UI subprompts without falsely classifying them as the missing I18N contract.
for path in source.glob('prompt 9 UI/*.txt'):
snapshot(path)
manifest = dict(schemaVersion=1,executionEnabled=False,minimumCodexVersion='0.163.0',
baselineCommit='',baselineBranch='test/pre-phase10-ux-acceptance',
remote='origin',integrationBranch='codex/roadmap-integration',stages=stages)
# Resolve the requested local commit instead of guessing its expansion.
import subprocess
manifest['baselineCommit'] = subprocess.check_output(['git','-C',str(project),'rev-parse','5032ccb^{commit}'],text=True).strip()
(output / 'backlog.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
(output / 'SOURCE_INVENTORY.json').write_text(json.dumps(inventory,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(f'Prepared {len(stages)} ordered stages; execution disabled. Original source bytes preserved.')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source',type=Path,required=True)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--project',type=Path,required=True)
args = parser.parse_args()
prepare(args.source,args.output,args.project)
+13
View File
@@ -0,0 +1,13 @@
{
"configuration": "PASS",
"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",
"realSessionsLaunched": 0,
"runtimeQualification": "NOT_CONFIGURED; separate identity, certificate and sandbox qualification required"
}
+12
View File
@@ -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 .NET RSA certificates only; no owner key installed"
}
+6
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
{
"testsRun": 48,
"failures": [],
"errors": [],
"skipped": [],
"successful": true,
"sourceStable": true,
"scope": "Disposable repositories, synthetic worker and actual subprocess/Windows containment/signature paths; no real backlog",
"sourceHashes": {
"automation/controller.py": "bfdd6a2b11020f6a1117719e721a92a3d78cc51e3c8f97637aac40e35a285338",
"automation/backlog.json": "b0a32cfdedfbedcf089f0dc929e774fc968221bd66f4e510fd981a99eaf4fe88",
"automation/tests/test_controller.py": "458da3c60571e61aad42987ce156842efd73ef0e2aa0c7bce719d36d626d7e7d",
"scripts/Approve-JobjaktBacklog.ps1": "a64f22aa85c1c2e9056c73e095975eaf17d1ffb201c7c27c52a34a24a5790ea0",
"scripts/Run-JobjaktBacklog.ps1": "fed8289dd78d0583c45a71a5d570492cc54400b46372d3823d530f4d43d48e41"
}
}
+53
View File
@@ -0,0 +1,53 @@
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
----------------------------------------------------------------------
Ran 48 tests in 112.446s
OK
+25
View File
@@ -0,0 +1,25 @@
#Requires -Version 7.2
# Disposable test key only. Production signing uses the owner's nonexportable certificate.
param([ValidateSet('Setup','Sign')][string]$Action, [string]$Root, [string]$RequestPath, [string]$ApprovalPath)
$ErrorActionPreference = 'Stop'
$rsa = [Security.Cryptography.RSA]::Create(2048)
try {
if ($Action -eq 'Setup') {
$request = [Security.Cryptography.X509Certificates.CertificateRequest]::new('CN=DISPOSABLE SYNTHETIC ONLY', $rsa,
[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
$cert = $request.CreateSelfSigned([DateTimeOffset]::UtcNow.AddMinutes(-1),[DateTimeOffset]::UtcNow.AddHours(2))
[IO.File]::WriteAllBytes((Join-Path $Root 'owner.cer'),$cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
[IO.File]::WriteAllText((Join-Path $Root 'synthetic-private.pem'),$rsa.ExportPkcs8PrivateKeyPem())
$cert.Dispose()
} else {
$rsa.ImportFromPem([IO.File]::ReadAllText((Join-Path $Root 'synthetic-private.pem')))
$hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([IO.File]::ReadAllBytes($RequestPath))).ToLowerInvariant()
$payload = @{requestSha256=$hash;approved=$true;ownerSid='OWNER';decision='Synthetic owner approves exact request';disposition='Approve';
issuedUtc=[DateTimeOffset]::UtcNow.ToString('o');expiresUtc=[DateTimeOffset]::UtcNow.AddHours(1).ToString('o')} | ConvertTo-Json -Compress
$bytes = [Text.Encoding]::UTF8.GetBytes($payload)
$signature = $rsa.SignData($bytes,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($ApprovalPath)) | Out-Null
@{payloadBase64=[Convert]::ToBase64String($bytes);signatureBase64=[Convert]::ToBase64String($signature)} |
ConvertTo-Json -Compress | Set-Content -LiteralPath $ApprovalPath -Encoding utf8NoBOM
}
} finally { $rsa.Dispose() }
+53
View File
@@ -0,0 +1,53 @@
#Requires -Version 7.2
param([Parameter(Mandatory)][string]$Verifier)
$ErrorActionPreference = 'Stop'
$folder = Join-Path ([IO.Path]::GetTempPath()) ('jobjakt-signature-test-' + [Guid]::NewGuid().ToString('N'))
[IO.Directory]::CreateDirectory($folder) | Out-Null
try {
$rsa = [Security.Cryptography.RSA]::Create(2048)
$request = [Security.Cryptography.X509Certificates.CertificateRequest]::new('CN=Synthetic only', $rsa,
[Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
$certificate = $request.CreateSelfSigned([DateTimeOffset]::UtcNow.AddDays(-1), [DateTimeOffset]::UtcNow.AddDays(1))
$certPath = Join-Path $folder 'owner.cer'
[IO.File]::WriteAllBytes($certPath, $certificate.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
$requestPath = Join-Path $folder 'request.json'
$approvalPath = Join-Path $folder 'approval.json'
$original = '{"schemaVersion":1,"kind":"design-selection","manifestSha256":"manifest","policySha256":"policy","candidateCommit":"synthetic"}'
[IO.File]::WriteAllText($requestPath, $original, [Text.UTF8Encoding]::new($false))
$requestHash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([IO.File]::ReadAllBytes($requestPath))).ToLowerInvariant()
function Write-Envelope([string]$Owner='SYNTHETIC-OWNER', [int]$Hours=1, [bool]$Forge=$false) {
$payload = @{ requestSha256=$requestHash; approved=$true; ownerSid=$Owner; decision='Select concept B';
issuedUtc=[DateTimeOffset]::UtcNow.ToString('o'); expiresUtc=[DateTimeOffset]::UtcNow.AddHours($Hours).ToString('o') } | ConvertTo-Json -Compress
$bytes = [Text.Encoding]::UTF8.GetBytes($payload)
$signature = $rsa.SignData($bytes,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)
if ($Forge) { $signature[0] = $signature[0] -bxor 255 }
@{ payloadBase64=[Convert]::ToBase64String($bytes); signatureBase64=[Convert]::ToBase64String($signature) } |
ConvertTo-Json -Compress | Set-Content -LiteralPath $approvalPath -Encoding utf8NoBOM
}
function Assert-Verify([bool]$Expected, [string]$Case) {
$messages = & (Get-Process -Id $PID).Path -NoProfile -File $Verifier -Action Verify -RequestPath $requestPath `
-ApprovalPath $approvalPath -CertificatePath $certPath -ExpectedOwnerSid 'SYNTHETIC-OWNER' 2>&1
if (($LASTEXITCODE -eq 0) -ne $Expected) { throw "Signature assertion failed: $Case $messages" }
Write-Output "PASS $Case"
}
Write-Envelope
Assert-Verify $true 'valid signature and concrete decision'
[IO.File]::WriteAllText($requestPath, $original.Replace('synthetic','changed'), [Text.UTF8Encoding]::new($false))
Assert-Verify $false 'replayed approval on changed commit'
[IO.File]::WriteAllText($requestPath, $original, [Text.UTF8Encoding]::new($false))
Write-Envelope -Forge $true
Assert-Verify $false 'forged signature'
Write-Envelope -Hours -1
Assert-Verify $false 'expired approval'
Write-Envelope -Owner 'OTHER-OWNER'
Assert-Verify $false 'wrong signing owner'
'{"approved":true}' | Set-Content -LiteralPath $approvalPath
Assert-Verify $false 'model-written approval JSON'
$rsa.Dispose()
$certificate.Dispose()
} finally {
$resolved = [IO.Path]::GetFullPath($folder)
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
if (-not $resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Cleanup escaped temp directory' }
Remove-Item -LiteralPath $resolved -Recurse -Force
}
+31
View File
@@ -0,0 +1,31 @@
"""Subprocess protocol double, used only in a disposable test checkout. No model/network."""
import json
from pathlib import Path
import subprocess
import sys
import time
args = sys.argv[1:]
if args[0] == 'exec':
prompt = sys.stdin.read()
if Path('simulate-interruption').exists():
sys.exit(17)
if Path('simulate-timeout').exists():
time.sleep(60)
if '--sandbox' not in args or args[args.index('--sandbox') + 1] != 'workspace-write':
sys.exit(18)
checkpoint = 'work'
Path('result.txt').write_text('subprocess worker output', encoding='utf-8')
Path('observed-prompt.txt').write_text(prompt, encoding='utf-8')
result = dict(outcome='complete', summary='synthetic subprocess', blockers=[], requestedApprovals=[], checkpoint=checkpoint)
Path(args[args.index('--output-last-message') + 1]).write_text(json.dumps(result), encoding='utf-8')
print(json.dumps(dict(type='thread.started', thread_id='synthetic-thread-id')))
print(json.dumps(dict(type='turn.completed')))
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
sys.exit(subprocess.run(args[index:], cwd=Path.cwd()).returncode)
else:
sys.exit(19)
+569
View File
@@ -0,0 +1,569 @@
"""Disposable Git repositories only. Never imports or calls application services."""
import copy
import json
import os
import shutil
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import controller as c
class QueueTests(unittest.TestCase):
def test_real_queue_is_ordered_and_missing_sources_are_explicit(self):
root = Path(__file__).resolve().parents[1]
manifest = json.loads((root / 'backlog.json').read_text(encoding='utf-8'))
c.validate_manifest(manifest, root)
self.assertEqual(list(range(5, 12)), [s['sequence'] for s in manifest['stages']])
self.assertEqual('PHASE-10', manifest['stages'][0]['id'])
self.assertFalse(manifest['executionEnabled'])
def test_production_branches_rejected(self):
m = fixture_manifest()
m['stages'][0]['branch'] = 'main'
with self.assertRaises(c.GateError):
c.validate_manifest(m)
def test_forward_dependency_rejected(self):
m = fixture_manifest()
m['stages'][0]['dependencies'] = ['TWO']
with self.assertRaises(c.GateError):
c.validate_manifest(m)
def fixture_manifest():
def stage(name, sequence, deps):
return dict(id=name, sequence=sequence, sourcePrompt='prompts/task.txt',
promptSha256='', sourceStatus='VERIFIED', dependencies=deps,
branch='codex/' + name.lower(), requiredOwnerApprovals=['entry'],
retryPolicy=dict(maxAttempts=2, maxSessions=4, backoffSeconds=0),
status='PENDING', evidence=[], reports=[], contextFiles=[],
checkpoints=[dict(id='work', instruction='Synthetic task only',
approvalsBefore=[], approvalsAfter=[], deferrableGates=[],
requiredArtifacts=['result.txt'], checks=[dict(id='self-check',
argv=[sys.executable, '-c', 'print("verified")'], cwd='.',
report=None)], evidenceRequirements=['synthetic-only'])])
return dict(schemaVersion=1, executionEnabled=True, integrationBranch='codex/integration',
minimumCodexVersion='0.163.0', remote='origin', baselineCommit='0'*40,
baselineBranch='codex/base', stages=[stage('ONE', 5, []), stage('TWO', 6, ['ONE'])])
class SyntheticRunner:
def __init__(self, result='complete', fail_checks=False):
self.result = result
self.fail_checks = fail_checks
self.calls = 0
def run_session(self, engine, stage, checkpoint, directory, prompt):
self.calls += 1
(engine.repo / 'result.txt').write_text(f'{stage["id"]} {self.calls}', encoding='utf-8')
if self.result == 'interrupt':
raise c.Interrupted('simulated CLI termination')
if self.result == 'fail':
raise c.GateError('synthetic failure')
return dict(outcome=self.result, summary='synthetic', blockers=[],
requestedApprovals=[], checkpoint=checkpoint['id'])
def run_check(self, engine, check, directory):
if self.fail_checks:
raise c.GateError('synthetic test exit 1')
return {'id': check['id'], 'exitCode': 0, 'report': None, 'passed': None, 'failed': 0}
class EngineTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='jobjakt-orchestrator-test-')
self.root = Path(self.temp.name)
self.repo = self.root / 'worker'
self.repo.mkdir()
self.remote = self.root / 'remote.git'
self.git_at(self.root, 'init', '--bare', str(self.remote))
self.git_at(self.repo, 'init', '-b', 'codex/base')
self.git_at(self.repo, 'config', 'user.name', 'Synthetic Test')
self.git_at(self.repo, 'config', 'user.email', 'synthetic@invalid.example')
(self.repo / 'README').write_text('synthetic only')
self.git_at(self.repo, 'add', '.')
self.git_at(self.repo, 'commit', '-m', 'baseline')
self.git_at(self.repo, 'remote', 'add', 'origin', str(self.remote))
self.git_at(self.repo, 'push', 'origin', 'HEAD:refs/heads/codex/base')
self.control = self.root / 'control'
(self.control / 'prompts').mkdir(parents=True)
(self.control / 'prompts/task.txt').write_text('Complete synthetic task. No real backlog.')
self.manifest = fixture_manifest()
self.manifest['baselineCommit'] = self.git_at(self.repo, 'rev-parse', 'HEAD')
for s in self.manifest['stages']:
s['promptSha256'] = c.sha_file(self.control / s['sourcePrompt'])
self.config = dict(repository=str(self.repo), codexExecutable='codex',
powershellExecutable='pwsh', publicKeyCertificate='owner.cer',
ownerSid='OWNER', runnerSid='RUNNER', executionWindows=[],
maxSessionSeconds=15, maxCheckSeconds=15, maxRunSeconds=60,
maxLogBytes=1048576, workerCodeXHome=str(self.root / 'codex-home'))
self.runner = SyntheticRunner()
self.engine = c.Engine(self.control, self.manifest, self.config, runner=self.runner,
approval_verifier=lambda request: True, host_verifier=lambda: None)
self.engine.initialize()
def tearDown(self):
self.temp.cleanup()
@staticmethod
def git_at(directory, *args):
p = subprocess.run(['git', '-c', 'core.hooksPath=', '-C', str(directory), *args],
text=True, capture_output=True, encoding='utf-8')
if p.returncode:
raise AssertionError(p.stderr)
return p.stdout.strip()
def run_engine(self):
self.engine.run()
return self.engine.store.read()
def test_success_orders_stages_and_verifies_remote(self):
state = self.run_engine()
self.assertEqual(['COMPLETED', 'COMPLETED'], [s['status'] for s in state['stages'].values()])
self.assertEqual(2, self.runner.calls)
remote = self.git_at(self.repo, 'ls-remote', 'origin', 'refs/heads/codex/two').split()[0]
self.assertEqual(remote, state['integrationCommit'])
def test_missing_approval_does_not_run_model(self):
self.engine.approval_verifier = lambda request: False
state = self.run_engine()
self.assertEqual('AWAITING_APPROVAL', state['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
self.assertTrue(list((self.control / 'requests').glob('*.json')))
def test_dependency_cannot_be_bypassed(self):
state = self.engine.store.read()
state['stages']['ONE']['status'] = 'CONDITIONALLY_ACCEPTED'
self.engine.store.write(state)
self.engine.approval_verifier = lambda request: False
self.assertEqual(0, self.runner.calls)
self.assertNotEqual('COMPLETED', self.run_engine()['stages']['TWO']['status'])
def test_failed_stage_stops_and_exhausts_retry_budget(self):
self.runner.result = 'fail'
state = self.run_engine()
self.assertEqual('FAILED', state['stages']['ONE']['status'])
self.assertEqual(2, self.runner.calls)
self.engine.approval_verifier = lambda request: request['kind'] != 'retry-budget-reset'
self.engine.retry('ONE')
self.run_engine()
self.assertEqual(2, self.runner.calls)
def test_interruption_is_not_success_and_resume_has_budget(self):
self.runner.result = 'interrupt'
self.assertNotEqual('COMPLETED', self.run_engine()['stages']['ONE']['status'])
self.runner.result = 'complete'
state = self.run_engine()
self.assertEqual('COMPLETED', state['stages']['ONE']['status'])
def test_missing_prompt_blocks(self):
(self.control / 'prompts/task.txt').unlink()
self.assertEqual('BLOCKED', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
def test_changed_prompt_blocks(self):
(self.control / 'prompts/task.txt').write_text('changed')
self.assertEqual('BLOCKED', self.run_engine()['stages']['ONE']['status'])
def test_unavailable_remote_preserves_commit_and_retries_publication_only(self):
self.git_at(self.repo, 'remote', 'set-url', 'origin', str(self.root / 'absent.git'))
state = self.run_engine()
self.assertEqual(0, self.runner.calls) # baseline publication must be verified first
self.assertEqual('BLOCKED', state['stages']['ONE']['status'])
def test_push_failure_preserves_tested_commit(self):
original = self.engine.publish
self.engine.publish = lambda *args: (_ for _ in ()).throw(c.PublicationError('push unavailable'))
state = self.run_engine()
commit = state['stages']['ONE']['pendingPublication']
self.assertTrue(commit)
self.assertEqual(1, self.runner.calls)
self.engine.publish = original
state = self.run_engine()
self.assertEqual('COMPLETED', state['stages']['ONE']['status'])
self.assertEqual(2, self.runner.calls) # only stage TWO ran after retry
def test_push_verification_mismatch_stops(self):
self.engine.verify_remote = lambda branch, commit: (_ for _ in ()).throw(c.PublicationError('mismatch'))
self.assertEqual('BLOCKED', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
def test_conditional_requires_explicit_deferrable_gates(self):
self.runner.result = 'conditional'
state = self.run_engine()
self.assertEqual('CONDITIONALLY_ACCEPTED', state['stages']['ONE']['status'])
self.assertEqual(1, self.runner.calls)
def test_pause_persists_and_resume_clears_it(self):
self.engine.pause()
self.assertEqual(0, self.runner.calls)
self.run_engine()
self.assertEqual(0, self.runner.calls)
self.engine.resume()
self.run_engine()
self.assertEqual(2, self.runner.calls)
def test_duplicate_lock_rejected(self):
with c.RepoLock(self.repo):
with self.assertRaises(c.GateError):
with c.RepoLock(self.repo):
pass
def test_corrupt_state_recovers_last_good_without_success(self):
self.engine.pause()
self.engine.store.path.write_text('{corrupt')
state = self.engine.store.read()
self.assertTrue(state['recoveryRequired'])
self.assertEqual('PENDING', state['stages']['ONE']['status'])
def test_double_corruption_fails_closed(self):
self.engine.store.path.write_text('bad')
self.engine.store.backup.write_text('bad')
with self.assertRaises(c.GateError):
self.engine.store.read()
def test_accidental_main_checkout_rejected(self):
self.git_at(self.repo, 'switch', '-c', 'main')
state = self.run_engine()
self.assertEqual('BLOCKED', state['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
def test_failing_checks_never_commit_or_advance(self):
self.runner.fail_checks = True
self.assertEqual('FAILED', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(self.manifest['baselineCommit'], self.git_at(self.repo, 'rev-parse', 'HEAD'))
def test_model_cannot_approve_itself(self):
self.engine.approval_verifier = lambda request: False
(self.repo / 'approval.json').write_text('{"approved":true}')
self.assertEqual('AWAITING_APPROVAL', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
def test_disabled_manifest_never_executes(self):
self.engine.manifest['executionEnabled'] = False
with self.assertRaises(c.GateError):
self.run_engine() # mutating policy without signed adoption is rejected
self.assertEqual(0, self.runner.calls)
def test_manifest_initially_disabled(self):
state = self.engine.store.read()
self.engine.manifest['executionEnabled'] = False
state['manifestSha256'] = c.digest(self.engine.manifest)
self.engine.store.write(state)
self.run_engine()
self.assertEqual(0, self.runner.calls)
def test_after_approval_binds_tested_commit_without_rerunning_worker(self):
self.manifest['stages'][0]['checkpoints'][0]['approvalsAfter'] = ['design-selection']
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.engine.approval_verifier = lambda r: r['kind'] != 'design-selection'
state = self.run_engine()
self.assertEqual('AWAITING_APPROVAL', state['stages']['ONE']['status'])
self.assertTrue(state['stages']['ONE']['checkpointCommit'])
self.assertEqual(1, self.runner.calls)
self.engine.approval_verifier = lambda r: {'decision': 'Use concept B, cyan buttons from D'}
self.assertEqual('COMPLETED', self.run_engine()['stages']['TWO']['status'])
self.assertEqual(2, self.runner.calls)
def test_tampered_evidence_cannot_be_approved(self):
self.manifest['stages'][0]['checkpoints'][0]['approvalsAfter'] = ['review']
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.engine.approval_verifier = lambda r: r['kind'] != 'review'
self.run_engine()
(self.repo / 'result.txt').write_text('tampered')
self.engine.approval_verifier = lambda r: True
self.assertEqual('BLOCKED', self.run_engine()['stages']['ONE']['status'])
def test_restart_running_marker_cannot_mean_completed(self):
state = self.engine.store.read()
state['stages']['ONE']['status'] = 'RUNNING'
self.engine.store.write(state)
c.atomic(self.control / 'active-process.json', {'pid': 999999})
with self.assertRaises(c.GateError):
self.run_engine()
self.assertEqual(0, self.runner.calls)
def test_same_repo_other_control_root_cannot_take_lock(self):
with c.RepoLock(self.repo):
other = c.Engine(self.root / 'other-control', self.manifest, self.config)
with self.assertRaises(c.GateError):
with c.RepoLock(other.repo):
pass
def test_execution_window_stops_before_worker(self):
self.engine.in_window = lambda: False
self.run_engine()
self.assertEqual(0, self.runner.calls)
def test_conditional_can_advance_only_with_explicit_signed_deferral(self):
for stage in self.manifest['stages']:
stage['checkpoints'][0]['deferrableGates'] = ['production-human-review']
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.runner.result = 'conditional'
self.engine.approval_verifier = lambda r: r['kind'] != 'conditional-deferral'
self.assertEqual('AWAITING_APPROVAL', self.run_engine()['stages']['ONE']['status'])
self.engine.approval_verifier = lambda r: True
self.assertEqual('COMPLETED', self.run_engine()['stages']['TWO']['status'])
def test_unconfigured_test_adapter_blocks_before_model(self):
self.manifest['stages'][0]['checkpoints'][0]['checks'][0]['configured'] = False
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.assertEqual('BLOCKED', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(0, self.runner.calls)
def test_design_revision_reruns_current_checkpoint_and_keeps_owner_selection(self):
self.manifest['stages'][0]['checkpoints'][0]['approvalsAfter'] = ['homepage-design-selection']
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.engine.approval_verifier = lambda r: ({'decision': 'Revise A using buttons from D', 'disposition': 'Revise'}
if r['kind'] == 'homepage-design-selection' else True)
self.assertEqual('PENDING', self.run_engine()['stages']['ONE']['status'])
self.assertEqual(1, self.runner.calls)
self.engine.approval_verifier = lambda r: {'decision': 'Approve revised concept A', 'disposition': 'Approve'}
state = self.run_engine()
self.assertEqual('COMPLETED', state['stages']['TWO']['status'])
self.assertEqual(3, self.runner.calls)
self.assertTrue(any('buttons from D' in d['decision'] for d in state['ownerDecisions']))
def test_recovered_running_without_marker_requires_reconciliation(self):
state = self.engine.store.read()
state['stages']['ONE']['status'] = 'RUNNING'
self.engine.store.write(state)
with self.assertRaisesRegex(c.GateError, 'RUNNING'):
self.run_engine()
self.assertEqual(0, self.runner.calls)
def test_unsigned_recovery_cannot_clear_running_guard(self):
state = self.engine.store.read()
state['stages']['ONE']['status'] = 'RUNNING'
self.engine.store.write(state)
c.atomic(self.control / 'config.json', self.config)
c.atomic(self.control / 'backlog.json', self.manifest)
self.engine.approval_verifier = lambda r: False
with mock.patch.object(c, 'Engine', return_value=self.engine):
self.assertEqual(3, c.main(['recover','--control-root',str(self.control)]))
with self.assertRaises(c.GateError):
self.engine.resume()
self.run_engine()
self.assertEqual(0, self.runner.calls)
def test_cryptographically_approved_resume_e2e(self):
pwsh = shutil.which('pwsh')
self.assertIsNotNone(pwsh, 'PowerShell is a required orchestrator test prerequisite')
signer = Path(__file__).with_name('Synthetic-Signer.ps1')
verifier = Path(c.__file__).parent.parent / 'scripts/Approve-JobjaktBacklog.ps1'
shutil.copyfile(verifier,self.control / 'Approve-JobjaktBacklog.ps1')
subprocess.run([pwsh,'-NoProfile','-File',str(signer),'-Action','Setup','-Root',str(self.control)],
check=True,capture_output=True)
self.engine.config['powershellExecutable'] = pwsh
self.engine.approval_verifier = self.engine.verify_approval
for _ in range(8):
state = self.run_engine()
if all(ss['status'] == 'COMPLETED' for ss in state['stages'].values()):
break
pending = next(ss for ss in state['stages'].values() if ss['awaiting'])
request = Path(pending['awaiting'][0]['request'])
if state['stages']['ONE']['entryBaseline'] is None:
self.assertEqual(0,self.runner.calls)
subprocess.run([pwsh,'-NoProfile','-File',str(signer),'-Action','Sign','-Root',str(self.control),
'-RequestPath',str(request),'-ApprovalPath',str(self.control / 'approvals' / request.name)],
check=True,capture_output=True)
self.assertTrue(all(ss['status'] == 'COMPLETED' for ss in state['stages'].values()))
self.assertEqual(2,self.runner.calls)
class ReportTests(unittest.TestCase):
def test_report_membership_and_skip_enforcement(self):
with tempfile.TemporaryDirectory() as td:
path = Path(td) / 'report.xml'
path.write_text('<testsuite><testcase classname="A" name="B"/></testsuite>')
spec = dict(format='junit', expectedNames=['A.B'])
self.assertEqual(1, c.parse_report(path, spec)['passed'])
spec['expectedNames'] = ['A.Missing']
with self.assertRaises(c.GateError):
c.parse_report(path, spec)
path.write_text('<testsuite><testcase name="B"><skipped/></testcase></testsuite>')
with self.assertRaises(c.GateError):
c.parse_report(path, dict(format='junit'))
def test_empty_report_is_not_a_pass(self):
with tempfile.TemporaryDirectory() as td:
path = Path(td) / 'report.xml'
path.write_text('<testsuite/>')
with self.assertRaises(c.GateError):
c.parse_report(path, dict(format='junit'))
def test_playwright_retry_and_expected_failure_are_not_pass(self):
with tempfile.TemporaryDirectory() as td:
path = Path(td) / 'report.json'
for case in [dict(projectName='x',results=[dict(status='failed'),dict(status='passed')]),
dict(projectName='x',expectedStatus='failed',results=[dict(status='failed')])]:
c.atomic(path, dict(suites=[dict(specs=[dict(title='a',tests=[case])])]))
with self.assertRaises(c.GateError):
c.parse_report(path, dict(format='playwright'))
def test_path_escape_rejected(self):
with tempfile.TemporaryDirectory() as td:
with self.assertRaises(c.GateError):
c.under(td, '../outside')
def test_redacts_common_credentials(self):
result = c.redact('password=secret Bearer abc https://user:pass@example.invalid token=hello')
for secret in ['secret', 'abc', 'user:pass', 'hello']:
self.assertNotIn(secret, result)
def test_json_log_redaction_preserves_event_structure(self):
line = json.dumps({'type':'item.completed','item':{'text':'password=secret','authorization':'Bearer hidden'}})
value = json.loads(c.redact(line))
self.assertEqual('item.completed',value['type'])
self.assertNotIn('secret',json.dumps(value))
self.assertNotIn('hidden',json.dumps(value))
class ProtocolRunner(c.ProcessRunner):
def codex_command(self, engine, *args):
return [sys.executable, str(Path(__file__).with_name('fake_codex.py')), *args]
class SubprocessTests(unittest.TestCase):
tearDown = EngineTests.tearDown
git_at = staticmethod(EngineTests.git_at)
run_engine = EngineTests.run_engine
def setUp(self):
EngineTests.setUp(self)
self.engine.runner = ProtocolRunner()
self.engine.config['workerCodeXHome'] = str(self.root / 'auth-only')
def test_cli_protocol_real_process_and_complete_prompt(self):
self.assertEqual('COMPLETED', self.run_engine()['stages']['TWO']['status'])
observed = (self.repo / 'observed-prompt.txt').read_text(encoding='utf-8')
self.assertIn('Complete synthetic task. No real backlog.', observed)
self.assertIn('ACTIVE CHECKPOINT ONLY', observed)
self.assertTrue(list((self.control / 'sessions').rglob('session.json')))
def test_cli_nonzero_exit_never_accepts(self):
(self.repo / 'simulate-interruption').write_text('synthetic')
self.git_at(self.repo,'add','.')
self.git_at(self.repo,'commit','-m','synthetic interruption fixture')
commit = self.git_at(self.repo,'rev-parse','HEAD')
self.git_at(self.repo,'push','origin','HEAD:refs/heads/codex/base')
self.manifest['baselineCommit'] = commit
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
state['integrationCommit'] = commit
self.engine.store.write(state)
self.assertEqual('FAILED', self.run_engine()['stages']['ONE']['status'])
def test_real_check_report_must_match_expected_membership(self):
check = self.manifest['stages'][0]['checkpoints'][0]['checks'][0]
xml = '<testsuite><testcase name="proof"/></testsuite>'
check['argv'] = [sys.executable,'-c',
f'from pathlib import Path; Path("suite.xml").write_text({xml!r})']
check['report'] = dict(format='junit',path='suite.xml',expectedNames=['.proof'])
state = self.engine.store.read()
state['manifestSha256'] = c.digest(self.manifest)
self.engine.store.write(state)
self.assertEqual('COMPLETED', self.run_engine()['stages']['ONE']['status'])
def test_prompt_stall_still_obeys_timeout(self):
started = time.monotonic()
with self.assertRaises(c.Interrupted):
self.engine.runner.supervise(self.engine,
[sys.executable,'-c','import time; time.sleep(8)'], self.control / 'stall',
1, stdin='x' * 4_000_000)
self.assertLess(time.monotonic() - started, 5)
def test_no_newline_output_obeys_log_budget(self):
self.engine.config['maxLogBytes'] = 1024
with self.assertRaisesRegex(c.GateError, 'log byte budget'):
self.engine.runner.supervise(self.engine,
[sys.executable,'-c','import sys,time; sys.stdout.write("x"*200000); sys.stdout.flush(); time.sleep(8)'],
self.control / 'huge-line', 3)
def test_marker_write_failure_does_not_leave_worker(self):
original = c.atomic
def fail_marker(path, value):
if Path(path).name == 'active-process.json' and value.get('pid'):
raise OSError('synthetic disk full')
return original(path,value)
with mock.patch.object(c,'atomic',side_effect=fail_marker):
with self.assertRaises(OSError):
self.engine.runner.supervise(self.engine,
[sys.executable,'-c','import time; time.sleep(8)'],self.control / 'disk-full',3)
child = self.engine.runner.child
if child and child.poll() is None:
child.kill()
child.wait()
self.fail('Worker survived marker-write failure')
@unittest.skipUnless(os.name == 'nt', 'Windows suspended launch only')
def test_worker_cannot_execute_before_job_assignment(self):
early = self.repo / 'early.txt'
original = c.WindowsJob.assign
def delayed_assign(job, process):
time.sleep(.3)
self.assertFalse(early.exists(), 'Worker ran before containment assignment')
return original(job, process)
with mock.patch.object(c.WindowsJob,'assign',delayed_assign):
self.engine.runner.supervise(self.engine,[sys.executable,'-c',
f'from pathlib import Path; Path({str(early)!r}).write_text("started")'],self.control / 'suspended',5)
self.assertTrue(early.exists())
@unittest.skipUnless(os.name == 'nt', 'Windows Job Objects only')
def test_killed_controller_terminates_worker_and_grandchild(self):
script = self.root / 'parent.py'
grandchild = self.root / 'grandchild.pid'
worker_code = (f'import subprocess,sys,time; from pathlib import Path; '
f'p=subprocess.Popen([sys.executable,"-c","import time; time.sleep(60)"]); '
f'Path({str(grandchild)!r}).write_text(str(p.pid)); time.sleep(60)')
script.write_text('import sys\nfrom pathlib import Path\n'
f'sys.path.insert(0,{str(Path(c.__file__).parent)!r})\nimport controller as c\n'
f'e=c.Engine(Path({str(self.control)!r}), {self.manifest!r}, {self.config!r})\n'
f'e.runner.supervise(e,[sys.executable,"-c",{worker_code!r}],Path({str(self.control / "orphan")!r}),60)\n',encoding='utf-8')
parent = subprocess.Popen([sys.executable,str(script)],stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
try:
until = time.monotonic() + 10
while not grandchild.exists() and parent.poll() is None and time.monotonic() < until:
time.sleep(.05)
self.assertTrue(grandchild.exists(), parent.stderr.read().decode() if parent.poll() is not None else 'child did not start')
worker_pid = c.read_json(self.control / 'active-process.json')['pid']
grandchild_pid = int(grandchild.read_text())
parent.kill()
parent.wait(timeout=5)
import ctypes
api = ctypes.WinDLL('kernel32',use_last_error=True)
api.OpenProcess.restype = ctypes.c_void_p
api.OpenProcess.argtypes = [ctypes.c_ulong,ctypes.c_int,ctypes.c_ulong]
api.WaitForSingleObject.argtypes = [ctypes.c_void_p,ctypes.c_ulong]
api.CloseHandle.argtypes = [ctypes.c_void_p]
for pid in (worker_pid,grandchild_pid):
handle = api.OpenProcess(0x100000,0,pid)
if handle:
self.assertEqual(0,api.WaitForSingleObject(handle,5000),f'contained process {pid} survived')
api.CloseHandle(handle)
finally:
if parent.poll() is None:
parent.kill()
parent.wait()
parent.stderr.close()
if __name__ == '__main__':
unittest.main(verbosity=2)