Skip to content

feat(runner): subprocess-isolated verifier + typed fail-loud run stubs (#55 phase 1) - #72

Merged
SollanSystems merged 2 commits into
mainfrom
feat/s3b-subprocess-verifier
Jul 15, 2026
Merged

SollanSystems merged 2 commits into
mainfrom
feat/s3b-subprocess-verifier

Conversation

@SollanSystems

Copy link
Copy Markdown
Owner

Summary

S3b of the phase3 run — completes #55 phase 1 (Refs, deliberately not Closes): the loop run verb gains a real subprocess-isolated verifier and typed fail-loud stubs for the not-yet-implemented run modes.

  • _subprocess_verifier: shlex-parsed argv, shell=False, cwd=workspace, 300s wall-clock timeout, errors="replace" decoding. Exit 0 → VerifyOutcome(True); nonzero/timeout → VerifyOutcome(False, bounded 2000-char tail) — never an exception.
  • New typed VerifierExecutionError (RunnerError subclass): unparseable verify strings (shlex ValueError) and unlaunchable commands (any launch OSError, incl. ENOEXEC) → CLI exit 2, no traceback, zero workspace writes.
  • VerifierNotImplementedError narrows to missing/blank TASKS.json verify fields ("no verify command declared…").
  • --continuous / --approve refuse with typed RunModeNotImplementedError before mode validation, target checks, and any event-store access.
  • reference/repo-os-contract.md §16 documents the verifier isolation boundary.
  • Kernel modules (events/reducer/evidence/fsm/completion/contract/scaffold/paths/plan/runtime/emit) byte-unchanged.

Governed-lane evidence

  • Packets s3b-verifier (attempt 1 honest stop on a real packet contradiction → adjudicated; attempt 2 gates-green but review BLOCKER → failed_verification) + replacement packet s3b-verifier-b (sha256 delta-gated directed 3-finding repair) — receipts cx_s3b_verifier_a1 / cx_s3b_verifier_a2 / cx_s3b_verifier_b1 (accepted; codex sessions 019f673b / 019f67d6 / 019f67e5).
  • Deterministic gates (fresh worktree at base ee22b41, exact): extras 760 passed / 16 skipped / 0 xfail (741 + 19), pyyaml-only 710 / 66 / 0 (691 + 19), frontmatter 9/9, self_eval 13/13.
  • Fresh sonnet reviews: full first-pass review (caught the shlex ValueError blocker, governor-reproduced live) + post-repair adversarial exhaustiveness pass — PASS, zero findings (per-statement exception enumeration of _subprocess_verifier; argv==[] proven unreachable; TimeoutExpired/OSError clause interplay verified).
  • Governor-only holdout gate the workers never saw: 5/5 Succeeded, false_completion false — mid-run events.db tamper refused by the append-only triggers; wall-clock timeout on a zero-CPU I/O-blocked child; 5MB stdout bounded to exactly 2000 chars; out-of-workspace verify runs as declared (containment is deliberately evidence@1's boundary, not the runner's); combined --continuous --approve --mode strict on a nonexistent target → exactly one typed refusal.
  • CLI probe: malformed verify string → exit 2, empty stdout, no traceback, events.db byte-unchanged.

Test plan

  • Extras suite exact 760/16/0x; pyyaml-only exact 710/66/0x
  • 19 new unconditional tests (16 design + 3 hardening regressions: malformed shlex string, ENOEXEC launch, non-UTF8 output)
  • Crash-consistency preserved with the real subprocess verifier (SIGKILL-after-verify test)
  • Zero-write proofs on every typed refusal path (full-directory SHA-256)

Refs #55

#55 phase 1)

Replace the placeholder default verifier with a real subprocess-isolated
verifier: shlex-parsed argv, shell=False, cwd=workspace, 300s wall-clock
timeout, errors=replace decoding. Exit 0 -> VerifyOutcome(True); nonzero or
timeout -> VerifyOutcome(False, bounded 2000-char tail) - never an exception.
Unlaunchable or unparseable verify commands raise the new typed
VerifierExecutionError (RunnerError subclass -> CLI exit 2, no traceback,
zero workspace writes). VerifierNotImplementedError narrows to missing/blank
TASKS.json verify fields. New --continuous/--approve run flags refuse with
typed RunModeNotImplementedError before mode validation, target checks, and
any event-store access.

16 new tests in scripts/test_runner_verifier.py + 3 hardening regressions
(malformed shlex string, ENOEXEC-class launch failure, non-UTF8 output);
2 default-verifier fixtures in test_runner_dispatch.py now declare blank
verify commands. Kernel modules byte-unchanged.

Refs #55
Copilot AI review requested due to automatic review settings July 15, 2026 22:40
@SollanSystems
SollanSystems enabled auto-merge (squash) July 15, 2026 22:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances the loop run command toward Phase 1 of issue #55 by implementing a subprocess-isolated default task verifier, adding typed “fail-loud” errors for unimplemented run modes, and expanding regression coverage to validate crash-safety and refusal semantics.

Changes:

  • Replace the default verifier with _subprocess_verifier() that uses shlex.split() + subprocess.run(shell=False, cwd=workspace, timeout=...) and returns VerifyOutcome on exit/timeout.
  • Introduce typed runner errors (VerifierExecutionError, VerifierNotImplementedError, RunModeNotImplementedError) and wire CLI behavior to exit 2 with a typed message (no traceback).
  • Add a new regression test module covering verifier isolation, timeout behavior, typed refusal paths, and crash-injection retry semantics; update existing dispatch tests/messages accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/test_runner_verifier.py Adds end-to-end and hardening regression tests for the subprocess verifier and typed refusal paths.
scripts/test_runner_dispatch.py Updates existing runner/CLI tests to reflect the new “no verify command declared” behavior.
reference/repo-os-contract.md Documents the verifier subprocess isolation boundary and typed refusal cases.
loop/runner.py Implements _subprocess_verifier, adds typed errors, and routes the default verifier through the subprocess implementation.
loop/main.py Adds early typed refusal for unimplemented run stub flags (--continuous, --approve) before mode/target validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread loop/runner.py
Comment on lines +94 to +95
except subprocess.TimeoutExpired:
return VerifyOutcome(False, summary=f"verify command timed out after {_VERIFY_TIMEOUT_SECONDS}s")
Comment thread loop/runner.py
Comment on lines 201 to 205
run_id, projection = _projection(target, mode)
# Safe only because each dispatch_once invocation appends at most one event.
if projection.get("terminal") is not None:
_reconcile_legacy_terminal(target, projection)
return {"ok": True, "action": "noop_terminal", "run_id": run_id}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f9ac86cb2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread loop/runner.py
raise VerifierExecutionError(f"cannot parse verify command {cmd!r}: {exc}") from exc
try:
proc = subprocess.run(
argv, cwd=str(workspace), shell=False, timeout=_VERIFY_TIMEOUT_SECONDS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Kill the verifier process group on timeout

When a verifier script starts a subprocess and then waits, for example sleep 999 & wait, the timeout here only terminates the direct child process; the grandchild remains running after _subprocess_verifier returns a failed outcome. That leaves orphaned test servers/workers around to contaminate later dispatches, so the timeout is not a reliable isolation boundary unless the verifier is started in its own process group/session and that group is killed on timeout.

Useful? React with 👍 / 👎.

Comment thread loop/runner.py
)
except subprocess.TimeoutExpired:
return VerifyOutcome(False, summary=f"verify command timed out after {_VERIFY_TIMEOUT_SECONDS}s")
except OSError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap subprocess ValueError as verifier errors

When a TASKS.json command contains an escaped NUL, such as "verify": "echo \\u0000", shlex.split succeeds and the launch raises ValueError: embedded null byte, not OSError; because this handler only wraps OSError, loop run bypasses the top-level RunnerError handling and prints a traceback with exit 1 instead of the typed verifier error/exit 2 expected for malformed declarations.

Useful? React with 👍 / 👎.

Comment thread scripts/test_runner_verifier.py Outdated
counter = tmp_path / "counter"
command = _cmd(_script(tmp_path, "pass.py", f"from pathlib import Path\np=Path({str(counter)!r})\np.write_text(str(int(p.read_text()) + 1) if p.exists() else '1')\n"))
workspace, store = _ws(tmp_path, [_task(command)])
body = "import os,signal,sqlite3,sys\nreal=sqlite3.connect\nclass Kill(sqlite3.Connection):\n def execute(self,sql,*a,**kw):\n if isinstance(sql,str) and sql.strip().upper()=='COMMIT': os.kill(os.getpid(),signal.SIGKILL)\n return super().execute(sql,*a,**kw)\nsqlite3.connect=lambda *a,**kw: real(*a,factory=Kill,**kw)\nfrom loop.runner import dispatch_once\ndispatch_once(sys.argv[1])\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the repo root to the crash helper import path

When the tests run from an uninstalled checkout, this generated tmp script has sys.path[0] set to tmp_path, and cwd=ROOT in the subprocess call does not make the loop package importable; the test exits with ModuleNotFoundError before exercising crash recovery. The existing crash helper in scripts/test_runner_dispatch.py inserts os.getcwd(), so this helper needs the same setup or an explicit PYTHONPATH.

Useful? React with 👍 / 👎.

@SollanSystems
SollanSystems merged commit e896c69 into main Jul 15, 2026
10 checks passed
@SollanSystems
SollanSystems deleted the feat/s3b-subprocess-verifier branch July 15, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants