From 651531126e84cfb4ae6f2a5047da047829256ab9 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 15:45:36 -0500 Subject: [PATCH 01/20] fix: bind fail-fast validation to source and runtime (#356) --- docs/agents/source-bound-validation.md | 75 +++++++ scripts/validate_candidate.py | 272 +++++++++++++++++++++++++ tests/test_validate_candidate.py | 212 +++++++++++++++++++ 3 files changed, 559 insertions(+) create mode 100644 docs/agents/source-bound-validation.md create mode 100644 scripts/validate_candidate.py create mode 100644 tests/test_validate_candidate.py diff --git a/docs/agents/source-bound-validation.md b/docs/agents/source-bound-validation.md new file mode 100644 index 00000000..76c3ee67 --- /dev/null +++ b/docs/agents/source-bound-validation.md @@ -0,0 +1,75 @@ +# Source-bound validation + +For #356 / parent specification #350, run existing CI or host acceptance commands +through `scripts/validate_candidate.py` when their success gates a later automated +step. This is an operator development tool; it does not deploy, renew an allocation, +or authorize the commands in its manifest. + +Create a manifest outside the candidate, for example: + +```json +{ + "checks": [ + { + "name": "undefined names", + "axis": "ci", + "argv": ["{runtime}", "-m", "ruff", "check", "--select", "F821,F822,F823", "src", "scripts", "tests", ".codex/hooks"] + }, + { + "name": "policy wiring", + "axis": "policy", + "argv": ["{runtime}", "scripts/check_policy_wiring.py"] + } + ], + "on_success": [] +} +``` + +```sh +python scripts/validate_candidate.py --repo "$PWD" \ + --runtime /absolute/selected-prefix/bin/python \ + --manifest /tmp/candidate-checks.json --receipt /tmp/candidate-checks-receipt.json +``` + +Use commands from `.github/workflows/ci.yml` and the existing public host acceptance +scripts for the relevant surface. This runner wraps their outcomes rather than +implementing another version of their checks. `{runtime}` expands to the absolute +invocation path, preserving virtual-environment selection. `--timeout` bounds each +command in seconds (default 300). Commands run from the candidate's Git root and +inherit the runner environment. Commands must report their own failures correctly; +an explicitly supplied shell program remains responsible for its exit status. + +Every check is required by default. A required failure stops later checks and all +`on_success` actions, exits nonzero, and records its exact argv, exit code, stdout +and stderr. Unexecuted steps remain `BLOCKED`. Optional checks (`"required": false`) +retain their individual failure without blocking required checks; a run-level +`PASS` means its required checks and success actions passed, not that every axis +passed. Success actions stop on their first failure even if marked optional. + +The new receipt is exclusively created outside the candidate; rerunning requires a +new receipt path and cannot overwrite an earlier failure. It binds outcomes to Git +HEAD, index digest, working content digest, dirty status and exact dirty paths, +selected runtime invocation/resolved path and binary digest, manifest digest, virtual environment `pyvenv.cfg` digest (or its absence), +platform, runner Python version, and a digest of the complete inherited environment. +Environment values are not published. Keep the environment/configuration used for +reproduction separately under its existing controls. + +Source identity covers tracked and nonignored untracked files, modes, deletions, +and regular-file symlink targets. Ignored build artifacts are excluded. Submodule +candidates and directory/dangling symlinks fail qualification; validate those sources +separately. Checks must leave the candidate unchanged, including creating new +nonignored files. Put logs and artifacts outside it or in established ignored paths. +The manifest may list additional `runtime_files` paths (absolute or relative to the +candidate) to fingerprint external configuration or pinned dependency metadata. +External dependencies/configuration must be pinned by the existing acceptance +command; the runtime binary digest does not certify every dependency or remote host. + +Identity is checked before and after each check and before each success action. +Detected source, manifest or runtime changes invalidate prior passing check results +and block remaining actions. This is drift detection, not a filesystem lock or a +sandbox against a command that changes and restores files during one check. For +concurrent editors, validate a dedicated immutable checkout. A success action may +intentionally commit the qualified candidate; receipts refer to the source identity +before that action. A source-changing action cannot precede another success action +without invalidating validation. Interrupted runs retain `FAIL` with the interrupted +step `RUNNING`; they cannot become passing by rerun overwrite. diff --git a/scripts/validate_candidate.py b/scripts/validate_candidate.py new file mode 100644 index 00000000..acb358ac --- /dev/null +++ b/scripts/validate_candidate.py @@ -0,0 +1,272 @@ +"""Fail-fast validation of one Git candidate using existing check commands. + +Manifest: {"checks": [{"name": "unit", "axis": "ci", "argv": ["python", +"-m", "unittest"], "required": true}], "on_success": []}. +Commands are argv arrays, executed without an implicit shell. Use the selected runtime's absolute +path in commands (or {runtime}). Receipts must be outside the candidate and new. +This runner does not grant permission to run checks or success actions. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import subprocess +import sys +import time + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def git(repo, *args): + return subprocess.check_output(["git", "-C", str(repo), *args]) + + +def source_identity(repo): + entries = [] + for record in git(repo, "ls-files", "--stage", "-z").split(b"\0"): + if record and record.startswith(b"160000 "): + raise ValueError("submodule candidates require separate validation") + paths = git( + repo, "ls-files", "--cached", "--others", "--exclude-standard", "-z" + ).split(b"\0") + for raw in sorted(set(paths) - {b""}): + path = repo / os.fsdecode(raw) + try: + mode = path.lstat().st_mode + if stat.S_ISLNK(mode): + if not path.is_file(): + raise ValueError( + f"candidate symlink must target a regular file: {path}" + ) + content = os.fsencode(os.readlink(path)) + b"\0" + path.read_bytes() + elif stat.S_ISREG(mode): + content = path.read_bytes() + else: + raise ValueError(f"unsupported candidate file: {path}") + entries.append([os.fsdecode(raw), mode, digest(content)]) + except FileNotFoundError: + entries.append([os.fsdecode(raw), "deleted"]) + return dict( + root=str(repo), + head=git(repo, "rev-parse", "HEAD").decode().strip(), + index=digest(git(repo, "ls-files", "--stage", "-z")), + content=digest(json.dumps(entries, ensure_ascii=True).encode()), + dirty=bool(git(repo, "status", "--porcelain=v1", "--untracked-files=all")), + dirty_state=git( + repo, "status", "--porcelain=v1", "--untracked-files=all" + ).decode(), + ) + + +def runtime_identity(runtime, files): + config = runtime.parent.parent / "pyvenv.cfg" + paths = {config, *files} + return dict( + path=str(runtime), + resolved_path=str(runtime.resolve()), + sha256=digest(runtime.read_bytes()), + files={ + str(path): digest(path.read_bytes()) if path.is_file() else None + for path in sorted(paths) + }, + ) + + +def run(args): + repo = args.repo.resolve() + if ( + Path(git(repo, "rev-parse", "--show-toplevel").decode().strip()).resolve() + != repo + ): + raise ValueError("--repo must identify the Git candidate root") + runtime_path = shutil.which(args.runtime) + if runtime_path is None: + raise ValueError("selected runtime is unavailable") + runtime = Path(runtime_path).absolute() + receipt_path = args.receipt.resolve() + if receipt_path.is_relative_to(repo): + raise ValueError("receipt must be outside candidate") + # Exclusive creation prevents a rerun from erasing the original failure. + with receipt_path.open("x") as output: + receipt = dict( + schema=1, status="FAIL", checks=[], actions=[], started_at=time.time() + ) + + def save(): + output.seek(0) + json.dump(receipt, output, indent=2) + output.truncate() + output.flush() + os.fsync(output.fileno()) + + save() + try: + manifest_bytes = args.manifest.read_bytes() + manifest = json.loads(manifest_bytes) + receipt["manifest_sha256"] = digest(manifest_bytes) + checks = manifest["checks"] + actions = manifest.get("on_success", []) + runtime_files = manifest.get("runtime_files", []) + if not isinstance(runtime_files, list) or not all( + isinstance(path, str) for path in runtime_files + ): + raise ValueError("runtime_files must be an array of file paths") + runtime_files = [ + Path(path).resolve() + if Path(path).is_absolute() + else (repo / path).resolve() + for path in runtime_files + ] + if not all(path.is_file() for path in runtime_files): + raise ValueError("runtime_files must exist as regular files") + if ( + not isinstance(checks, list) + or not checks + or not isinstance(actions, list) + ): + raise ValueError("nonempty checks and an actions list are required") + for row in checks + actions: + if ( + not isinstance(row, dict) + or not isinstance(row.get("name"), str) + or not isinstance(row.get("argv"), list) + or not row["argv"] + or not all(isinstance(part, str) and part for part in row["argv"]) + or type(row.get("required", True)) is not bool + ): + raise ValueError( + "each step needs a name, argv array and boolean required" + ) + receipt["source"] = source_identity(repo) + receipt["runtime"] = runtime_identity(runtime, runtime_files) + # Bind the entire inherited environment without publishing secrets. + receipt["environment"] = dict( + platform=sys.platform, + runner_python=sys.version, + sha256=digest(json.dumps(dict(os.environ), sort_keys=True).encode()), + ) + receipt["checks"] = [ + dict( + name=row["name"], + axis=row.get("axis", row["name"]), + required=row.get("required", True), + argv=[ + part.replace("{runtime}", str(runtime)) for part in row["argv"] + ], + status="BLOCKED", + ) + for row in checks + ] + receipt["actions"] = [ + dict( + name=row["name"], + argv=[ + part.replace("{runtime}", str(runtime)) for part in row["argv"] + ], + status="BLOCKED", + ) + for row in actions + ] + save() + + def unchanged(): + if ( + source_identity(repo) != receipt["source"] + or runtime_identity(runtime, runtime_files) != receipt["runtime"] + or args.manifest.read_bytes() != manifest_bytes + ): + for row in receipt["checks"]: + if row["status"] == "PASS": + row["status"] = "INVALIDATED" + receipt["qualified"] = False + raise ValueError( + "candidate, runtime or check manifest changed during validation" + ) + + def execute(row): + row["status"] = "RUNNING" + save() + try: + result = subprocess.run( + row["argv"], + cwd=repo, + capture_output=True, + text=True, + errors="replace", + timeout=args.timeout, + ) + row.update( + returncode=result.returncode, + stdout=result.stdout, + stderr=result.stderr, + status="PASS" if result.returncode == 0 else "FAIL", + ) + except (OSError, subprocess.TimeoutExpired) as error: + row.update(status="FAIL", error=str(error)) + save() + + for row in receipt["checks"]: + unchanged() + execute(row) + unchanged() + if row["status"] != "PASS" and row["required"]: + receipt["failure"] = dict( + reason="required check failed", command=row["argv"] + ) + break + else: + unchanged() + receipt["qualified"] = True + # These steps may intentionally commit the qualified candidate. Their + # outcomes remain separate from the source-bound check outcomes. + for row in receipt["actions"]: + unchanged() + execute(row) + if row["status"] != "PASS": + receipt["failure"] = dict( + reason="success action failed", command=row["argv"] + ) + break + else: + receipt["status"] = "PASS" + except ( + OSError, + ValueError, + KeyError, + TypeError, + subprocess.SubprocessError, + ) as error: + receipt["failure"] = dict(reason=str(error)) + receipt["finished_at"] = time.time() + save() + print(json.dumps(receipt)) + return int(receipt["status"] != "PASS") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=Path, required=True) + parser.add_argument("--runtime", required=True) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--timeout", type=float, default=300) + args = parser.parse_args() + if args.timeout <= 0: + parser.error("--timeout must be positive") + try: + return run(args) + except (OSError, ValueError) as error: + print(str(error), file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_validate_candidate.py b/tests/test_validate_candidate.py new file mode 100644 index 00000000..a64406d2 --- /dev/null +++ b/tests/test_validate_candidate.py @@ -0,0 +1,212 @@ +"""Public runner regressions: failure and candidate drift cannot authorize success.""" + +import json +from pathlib import Path +import subprocess +import shutil +import sys +import tempfile +import unittest + +RUNNER = Path(__file__).resolve().parents[1] / "scripts/validate_candidate.py" + + +class CandidateValidationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.base = Path(self.temp.name) + self.repo = self.base / "candidate" + self.repo.mkdir() + self.runtime = self.base / "selected-python" + shutil.copyfile(sys.executable, self.runtime) + self.runtime.chmod(0o755) + self.git("init", "-q") + self.git("config", "user.email", "fixture@example.invalid") + self.git("config", "user.name", "Fixture") + (self.repo / "source.txt").write_text("original") + self.git("add", ".") + self.git("commit", "-qm", "candidate") + + def git(self, *args): + return subprocess.check_output( + ["git", "-C", str(self.repo), *args], text=True + ).strip() + + def run_candidate(self, checks, actions=None): + manifest = self.base / "checks.json" + manifest.write_text( + json.dumps( + dict( + checks=checks, + on_success=actions + if actions is not None + else [ + dict( + name="commit", + argv=[ + "git", + "commit", + "--allow-empty", + "-qm", + "validated candidate", + ], + ) + ], + ) + ) + ) + receipt = self.base / "receipt.json" + process = subprocess.run( + [ + sys.executable, + str(RUNNER), + "--repo", + str(self.repo), + "--runtime", + str(self.runtime), + "--manifest", + str(manifest), + "--receipt", + str(receipt), + ], + capture_output=True, + text=True, + ) + self.assertTrue(receipt.exists(), process.stderr) + return process, json.loads(receipt.read_text()) + + def test_required_failure_blocks_later_checks_and_commit(self): + command = [sys.executable, "-c", "raise SystemExit(23)"] + process, receipt = self.run_candidate( + [ + dict(name="host", axis="host", argv=command), + dict(name="ci", axis="ci", argv=[sys.executable, "-c", "pass"]), + ] + ) + self.assertNotEqual(process.returncode, 0) + self.assertEqual(receipt["status"], "FAIL") + self.assertEqual(receipt["checks"][0]["argv"], command) + self.assertEqual(receipt["checks"][0]["returncode"], 23) + self.assertEqual(receipt["checks"][1]["status"], "BLOCKED") + self.assertEqual(self.git("rev-parse", "HEAD"), receipt["source"]["head"]) + + def test_source_and_runtime_changes_invalidate_pass_and_block_success(self): + for target in ( + "source.txt", + "untracked.txt", + str(self.runtime), + str(self.base / "checks.json"), + ): + with self.subTest(target=target): + # Each run gets a new receipt; the failure history remains intact. + receipt_path = self.base / "receipt.json" + if receipt_path.exists(): + receipt_path.rename( + self.base + / ("prior-" + str(len(list(self.base.glob("prior-*"))))) + ) + code = ( + f"from pathlib import Path; Path({target!r}).write_text('changed')" + ) + process, receipt = self.run_candidate( + [ + dict(name="unit", argv=[sys.executable, "-c", "pass"]), + dict(name="host", argv=[sys.executable, "-c", code]), + ] + ) + self.assertNotEqual(process.returncode, 0) + self.assertEqual(receipt["checks"][0]["status"], "INVALIDATED") + self.assertIn("changed during validation", receipt["failure"]["reason"]) + self.assertEqual( + self.git("rev-parse", "HEAD"), receipt["source"]["head"] + ) + + def test_dirty_candidate_and_independent_optional_axis_are_explicit(self): + self.runtime = Path(sys.executable).resolve() + (self.repo / "source.txt").write_text("reviewed dirty content") + process, receipt = self.run_candidate( + [ + dict( + name="host", + axis="host", + required=False, + argv=[sys.executable, "-c", "raise SystemExit(7)"], + ), + dict(name="ci", axis="ci", argv=["{runtime}", "-c", "pass"]), + ] + ) + self.assertEqual(process.returncode, 0, receipt) + self.assertEqual([r["status"] for r in receipt["checks"]], ["FAIL", "PASS"]) + self.assertTrue(receipt["source"]["dirty"]) + self.assertEqual(len(receipt["source"]["head"]), 40) + self.assertEqual(receipt["runtime"]["path"], str(self.runtime)) + self.assertEqual(len(receipt["environment"]["sha256"]), 64) + self.assertNotEqual(self.git("rev-parse", "HEAD"), receipt["source"]["head"]) + self.assertEqual(receipt["actions"][0]["status"], "PASS") + # An attempted rerun cannot replace the earlier outcomes. + original = (self.base / "receipt.json").read_bytes() + subprocess.run( + [ + sys.executable, + str(RUNNER), + "--repo", + str(self.repo), + "--runtime", + str(self.runtime), + "--manifest", + str(self.base / "checks.json"), + "--receipt", + str(self.base / "receipt.json"), + ], + capture_output=True, + ) + self.assertEqual((self.base / "receipt.json").read_bytes(), original) + + def test_success_action_cannot_mutate_then_commit_unvalidated_content(self): + process, receipt = self.run_candidate( + [dict(name="ci", argv=[sys.executable, "-c", "pass"])], + actions=[ + dict( + name="rewrite", + argv=[ + sys.executable, + "-c", + "from pathlib import Path; Path('source.txt').write_text('untested')", + ], + ), + dict( + name="commit", + argv=["git", "commit", "--allow-empty", "-qm", "untested commit"], + ), + ], + ) + self.assertNotEqual(process.returncode, 0) + self.assertEqual(receipt["actions"][1]["status"], "BLOCKED") + self.assertEqual(self.git("rev-parse", "HEAD"), receipt["source"]["head"]) + + def test_selected_virtual_environment_invocation_is_preserved(self): + prefix = self.base / "runtime" + subprocess.run( + [sys.executable, "-m", "venv", "--without-pip", str(prefix)], + check=True, + capture_output=True, + ) + self.runtime = prefix / "bin/python" + process, receipt = self.run_candidate( + [ + dict( + name="runtime", + argv=["{runtime}", "-c", "import sys; print(sys.prefix)"], + ) + ], + actions=[], + ) + self.assertEqual(process.returncode, 0, receipt.get("failure")) + self.assertEqual(receipt["checks"][0]["stdout"].strip(), str(prefix)) + self.assertEqual(receipt["runtime"]["path"], str(self.runtime)) + self.assertIn(str(prefix / "pyvenv.cfg"), receipt["runtime"]["files"]) + + +if __name__ == "__main__": + unittest.main() From 6e11c34fff976be72666088f3288b7fc5319e776 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 15:48:35 -0500 Subject: [PATCH 02/20] feat: announce governor activation and expose intervention delivery (#351) --- GLOSSARY.md | 6 +++ docs/dev/active-lanes.yaml | 14 +++++ docs/dev/evidence/plan0175/351-visibility.md | 13 +++++ ...026-10-25-transparent-governor-delivery.md | 30 +++++++++++ scripts/goal_notice_native_acceptance.py | 24 ++++++++- src/codex_wake/goal_notices.py | 40 ++++++++++++++- src/codex_wake/goal_supervision.py | 6 ++- tests/test_goal_engagement.py | 3 +- tests/test_governor_visibility.py | 51 +++++++++++++++++++ 9 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 docs/dev/evidence/plan0175/351-visibility.md create mode 100644 docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md create mode 100644 tests/test_governor_visibility.py diff --git a/GLOSSARY.md b/GLOSSARY.md index 3330a4a7..53e8356e 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -241,3 +241,9 @@ alone grants no permission, executes no effect and authorizes no uncertain repla **Background job result wake**: An existing exact-owner wake observing the durable result of one approved bounded local job. Awaiting the result does not relaunch the job or renew its task allowance. + +**Governor**: +The operator-visible controller that applies authorized resource limits to a selected objective and explains its interventions to the operator and model. + +**Governor deactivation**: +The operator's durable choice to stop governor interventions in a selected scope. It preserves accounting and work, does not renew resources or resume stopped work, and remains in effect until explicitly changed. diff --git a/docs/dev/active-lanes.yaml b/docs/dev/active-lanes.yaml index 8234d028..8832fce6 100644 --- a/docs/dev/active-lanes.yaml +++ b/docs/dev/active-lanes.yaml @@ -1,6 +1,20 @@ schema_version: 1 work_item_tracking: required lanes: + - id: P175 + owner: primary / ecochran76 + objective: Transparent operator-controlled governor and bounded execution + work_items: [https://github.com/CochranResearchGroup/codex-wake/issues/351, https://github.com/CochranResearchGroup/codex-wake/issues/352, https://github.com/CochranResearchGroup/codex-wake/issues/353, https://github.com/CochranResearchGroup/codex-wake/issues/354, https://github.com/CochranResearchGroup/codex-wake/issues/355, https://github.com/CochranResearchGroup/codex-wake/issues/356, https://github.com/CochranResearchGroup/codex-wake/issues/357] + plan: docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md + branch: feat/transparent-governor + target: origin/main + plan_state: OPEN + custody_state: ACTIVE_WORKTREE + checkpoint: ce1ad93 + dependencies: [] + overlaps: [] + updated_at: 2026-10-25 + disposition: primary 351; isolated agents dispatch355 and validation356; no live effects - id: P173-GOAL-UX owner: primary / ecochran76 objective: Implement visible goal supervision and approved renewal diff --git a/docs/dev/evidence/plan0175/351-visibility.md b/docs/dev/evidence/plan0175/351-visibility.md new file mode 100644 index 00000000..84094bb7 --- /dev/null +++ b/docs/dev/evidence/plan0175/351-visibility.md @@ -0,0 +1,13 @@ +# #351 visibility acceptance + +Source: feat/transparent-governor, baseline ce1ad93; fixture native server SHA256 `86fad00e38f0c26a5c8d2eb2dbe34536c74655fb2fb45b4efc5d94e97e683b1f`, stock frontend 0.164.0. + +Public red: `PYTHONPATH=src:tests /bin/python -m unittest test_governor_visibility -q` failed because enrollment created no activation notice and show omitted stop presentation status. Green: same module plus test_goal_notices/test_goal_engagement, 13 PASS (2.806s). A 94-test goal-suite run had one old assertion expecting only a stop; updated to require activation and stop, and its focused rerun passes. + +Native command: `PYTHONPATH=src /bin/python scripts/goal_notice_native_acceptance.py --app-server --frontend --output /tmp/governor351-native-qualified`. + +PASS: generated activation and stop rendered in the actual stock TUI; model context retained; no inference for notices; explicit shell-tool turn interrupted; reconnect rendered the retained notice once; peer unchanged; lost append acknowledgement reconciled once. Provider turns: 0. Owned process census after cleanup: empty. This qualifies the pinned fixture server, not arbitrary installed hosts or provider inference. + +Preserved first failures: /tmp/351-red.log (missing activation/status); /tmp/governor351-native-debug.log (native 768-byte notice ceiling); /tmp/governor351-native-bounded.log (tmux last-session exit race); /tmp/governor351-native-final.log (orphan plugin-fetch cleanup). Renderer compacted; owned tmux server retained through reconnect; previously censused exact process identities retired at cleanup. Terminal qualified receipt: /tmp/governor351-native-qualified/receipt.json. + +Model-context availability, UI rendering observed by this acceptance, and a human reading the notice remain separate facts. Journal proof remains `ui: unverified`; the TUI capture is the separate rendering evidence. No production configuration, watcher mask, allocation or session was changed. diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md new file mode 100644 index 00000000..315631ed --- /dev/null +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -0,0 +1,30 @@ +# Transparent governor delivery + +State: OPEN +Owner: primary / ecochran76 +Work items: GitHub CochranResearchGroup/codex-wake #351–#357 +Specification: https://github.com/CochranResearchGroup/codex-wake/issues/350 +Review fixed point: ce1ad93 (origin/main at lane creation) +Branch: feat/transparent-governor + +## Scope and authority + +Operator explicitly requested publication, implementation, delegation of independent ready tickets, ticket closure from evidence, one final code review, and one TDD remediation pass. Preserve production, unrelated worktrees, and original exhausted campaign accounting. Manual monitoring remains authorized; no resource allowance is implicitly renewed. No deployment is claimed or performed. + +## Acceptance and definition of done + +Each ticket's public behavior and acceptance evidence must be verified before closure. Publish one integration branch/PR with source-bound checks. After all seven tickets close, run one Standards/Spec review against the pinned baseline, adjudicate findings, then one bounded TDD remediation pass. Do not close an unproved native hard-cap ticket merely because Python reservations pass. + +## Current state + +Published #351–#357, read back bodies and native subissue links. Dependency edges: #352 <- #351; #353 <- #351/#352; #354 <- #353; #357 <- #354/#355. Root-ready #351 primary; #355 agent dispatch355; #356 agent validation356, each on isolated worktree. Existing main includes prior goal UX/native-notice integration; reuse after qualification. + +Primary #351 seams: enroll/show/poll plus retained notices and native notice adapter; prove activation registration, actual transport results, intervention failures independently of model inference. Existing native operator-render acceptance must be qualified separately from journal/helper tests. + +## Working controls + +Use compact outputs and focused tests; inspect process/resource state before expensive fan-out. Do not repeat whole-source discovery. Counters for requested final review: 0; post-review remediation passes: 0. Keep historical red/green logs in /tmp and curate source evidence before closeout. + +## Non-goals + +No release, service unmask, production session dispatch, automatic resource renewal, or unrelated old-ticket closure. diff --git a/scripts/goal_notice_native_acceptance.py b/scripts/goal_notice_native_acceptance.py index 814a1e95..e1388443 100644 --- a/scripts/goal_notice_native_acceptance.py +++ b/scripts/goal_notice_native_acceptance.py @@ -6,6 +6,7 @@ from pathlib import Path import shlex import shutil +import signal import subprocess import tempfile import time @@ -79,6 +80,7 @@ def rpc(method, **params): command = shlex.join(['env','CODEX_HOME='+str(home),'TERM=xterm-256color',str(frontend), '--remote',endpoint,'--no-alt-screen','resume',thread_id]) tmux('new-session','-d','-s','notice','-x','160','-y','55',command) + tmux('set-option', '-g', 'exit-empty', 'off') # Confirm the actual frontend is attached before emitting a notice. baseline = wait_for(lambda: text if (text := capture()) and ('mock-model' in text or 'context left' in text) else None) (output/'before.txt').write_text(baseline) @@ -115,6 +117,13 @@ def rpc(method, **params): _save(_directory(wake_root)/(_name(thread_id)+'.json'),receipt) notice = record_notice(wake_root,receipt,'stop') native_adapter = NativeNoticeAdapter(wake_root) + activation = record_notice(wake_root, receipt, 'activation') + activation_delivery = present_notice(wake_root, activation['event_id'], adapter=native_adapter) + assert activation_delivery['proof']['model_context']['state'] == 'available', activation_delivery + activation_frame = wait_for(lambda: frame if activation['event_id'] in (frame := capture()) else None) + (output/'activation.txt').write_text(activation_frame) + assert 'Governor activated' in activation_frame + assert 'sampled_stop' in activation_frame class LostAcknowledgement: def append(self, event): native_adapter.append(event) @@ -125,7 +134,10 @@ def reconcile(self, event): return native_adapter.reconcile(event) reconciled = present_notice(wake_root,notice['event_id'],adapter=native_adapter) assert reconciled['delivery']['state'] == 'recorded', reconciled assert reconciled['delivery']['attempts'] == 1, reconciled - duplicate = rpc('thread/notice/append',threadId=thread_id,eventId=event_id,text=text) + stop_frame = wait_for(lambda: frame if notice['event_id'] in (frame := capture()) else None) + (output/'governor-stop.txt').write_text(stop_frame) + assert 'Deactivate governor' in stop_frame + duplicate = rpc('thread/notice/append' ,threadId=thread_id,eventId=event_id,text=text) assert duplicate['alreadyRecorded'] is True and duplicate['notificationPublished'] is False, duplicate retained = rpc('thread/notice/read',threadId=thread_id,eventId=event_id) assert retained['notice'] == dict(eventId=event_id,text=text), retained @@ -146,7 +158,7 @@ def reconcile(self, event): return native_adapter.reconcile(event) report.update(state='passed', thread_id=thread_id, peer_id=peer_id, append=appended, duplicate=duplicate, retained=retained, native_model_turns_started=0, explicit_shell_turns_started=1, rendered_live=True, rendered_reconnected_once=True, busy_tool_notice=busy_result, - model_context_retained=True, peer_unchanged=True, fixture_external_usage=True, lost_ack_reconciled_once=True) + activation_rendered=True, generated_stop_rendered=True, model_context_retained=True, peer_unchanged=True, fixture_external_usage=True, lost_ack_reconciled_once=True) except Exception as error: report.update(state='failed', failure=type(error).__name__) (output/'last-frame.txt').write_text(capture()) @@ -172,6 +184,14 @@ def reconcile(self, event): return native_adapter.reconcile(event) if process.poll() is None: process.terminate() process.wait(timeout=15) + # Native plugin fetches may outlive their parent. Retire only the + # previously censused owned identities, never shared host processes. + for pid, identity in identities.items(): + try: + if process_identity(pid) == identity: + os.kill(pid, signal.SIGTERM) + except (OSError, ValueError): + pass log.close() end = time.monotonic()+5 while True: diff --git a/src/codex_wake/goal_notices.py b/src/codex_wake/goal_notices.py index 84c34dc2..f4f99104 100644 --- a/src/codex_wake/goal_notices.py +++ b/src/codex_wake/goal_notices.py @@ -11,7 +11,7 @@ import re from datetime import datetime, timezone -KINDS = frozenset(('engagement', 'checkpoint', 'stop', 'recovery')) +KINDS = frozenset(('activation', 'engagement', 'checkpoint', 'stop', 'recovery')) _EVENT = re.compile(r'notice_[a-f0-9]{64}') @@ -101,6 +101,7 @@ def record_notice(root, receipt, kind, *, controller=None): snapshot = {key: receipt.get(key) for key in ('used_tokens', 'token_limit', 'token_accounting', 'deadline', 'checkpoint_token_percent', 'checkpoint_seconds', 'state', 'reason', 'lifetime_tokens_before_window')} event = dict(version=1, event_id=event_id, identity=identity, snapshot=snapshot, + recorded_at=datetime.now(timezone.utc).isoformat(), controller_generation=(controller or {}).get('generation', receipt.get('controller_generation')), proof=dict(control=dict(stop_verified=receipt.get('stop_proof') is True, state=receipt.get('state'), reason=receipt.get('reason')), @@ -123,6 +124,8 @@ def render_notice(event): title = 'Goal paused' if event['proof']['control']['stop_verified'] else 'Stop requested; interruption unverified' elif kind == 'checkpoint': title = 'Checkpoint now within the remaining allowance' + elif kind == 'activation': + title = 'Governor activated' elif kind == 'engagement': title = 'External goal watcher engaged' else: @@ -139,8 +142,10 @@ def render_notice(event): f"{snapshot.get('checkpoint_seconds')} seconds. Watcher healthy. ") else: text += ('Extend allowance / Reset budget window / Keep stopped. ' - 'Use goal-limits extend --help or goal-limits reset --help. ' 'Native /goal resume alone does not renew this external allowance. ') + text += ('Mode sampled_stop; maximum overshoot is not bounded. ' + 'Deactivate governor: goal-limits disable. ' + f"At {event.get('recorded_at', 'unknown')}. " ) return text + f"[External supervision event {event['event_id']}]" @@ -273,3 +278,34 @@ def run_presenter(root, stop): pass if finishing:return stop.wait(.2) + + +def governor_status(root, receipt): + """Independent inspection exposes control state even if notice delivery fails.""" + status = dict(identity='Codex Wake governor', scope=receipt['allocation_id'], + mode='sampled_stop', maximum_token_overshoot=None, + controls=['Inspect governor', 'Deactivate governor', 'Keep stopped'], + intervention=None) + ids = receipt.get('notice_ids', {}) + for kind in ('activation', 'engagement', 'checkpoint', 'stop', 'recovery'): + event_id = ids.get(kind) + if not event_id: + continue + try: + event = read_notice(root, event_id) + summary = dict(event_id=event_id, kind=kind, text=event['text'], + reason=event['snapshot'].get('reason'), + presentation=event['delivery']['state'], + next_action=event['delivery'].get('next_action') or + 'Inspect retained notices; verify native model delivery and operator display separately.', + ui=event['proof']['ui']['state'], + model_context=event['proof']['model_context']['state']) + except (OSError, ValueError, KeyError): + summary = dict(event_id=event_id, kind=kind, presentation='unavailable', + next_action='Inspect governor notice storage and native delivery; intervention remains in effect.') + status[kind] = summary + if kind in ('checkpoint', 'stop', 'recovery'): + status['intervention'] = summary + if receipt.get('notice_record_error'): + status['notice_error'] = receipt['notice_record_error'] + return status diff --git a/src/codex_wake/goal_supervision.py b/src/codex_wake/goal_supervision.py index 6056b870..85e435bd 100644 --- a/src/codex_wake/goal_supervision.py +++ b/src/codex_wake/goal_supervision.py @@ -451,6 +451,8 @@ def show(root,policy_path,caller_thread_id,allocation_id=None,*,permission_princ receipt['sampled']=True;receipt['maximum_token_overshoot']=None from .goal_readiness import engagement result=_public(receipt) + from .goal_notices import governor_status + result['governor']=governor_status(root,receipt) if receipt.get('stop_latched'): observation_fault=receipt.get('reason')=='observation_unavailable' result['recovery']=dict(status='requires_authorized_recovery' if observation_fault else 'not_recoverable', @@ -632,10 +634,12 @@ def hold(reason): if watcher_setup is not None:receipt['watcher_setup']=watcher_setup if goal.get('status') not in NATIVE_GOAL_STATUSES: hold('native_goal_state_unknown') + _record_supervision_notice(path,receipt,'activation',controller) _save(path,receipt) from .goal_readiness import engagement + from .goal_notices import governor_status return dict(_public(receipt),controller=controller,sampled=True,maximum_token_overshoot=None, - engagement=engagement(receipt,controller)) + governor=governor_status(root,receipt),engagement=engagement(receipt,controller)) def _effect(path,receipt,key,method,params): diff --git a/tests/test_goal_engagement.py b/tests/test_goal_engagement.py index 2755b50a..58cb3e03 100644 --- a/tests/test_goal_engagement.py +++ b/tests/test_goal_engagement.py @@ -138,7 +138,8 @@ def test_hard_stop_retains_notice_even_when_native_visibility_is_unsupported(sel stopped=poll(root)[0] self.assertEqual(stopped['state'],'stopped') notices=execute(root,policy,THREAD)['notices'] - self.assertEqual(len(notices),1) + self.assertEqual({event['identity']['kind'] for event in notices}, {'activation', 'stop'}) + notices=[event for event in notices if event['identity']['kind']=='stop'] self.assertEqual(notices[0]['identity']['kind'],'stop') self.assertIn('Used 40 of 40',notices[0]['text']) self.assertEqual(notices[0]['proof']['ui']['state'],'unverified') diff --git a/tests/test_governor_visibility.py b/tests/test_governor_visibility.py new file mode 100644 index 00000000..a53bdc2c --- /dev/null +++ b/tests/test_governor_visibility.py @@ -0,0 +1,51 @@ +"""Governor activation and intervention are explicit through public journeys.""" +import tempfile +import unittest +from unittest.mock import patch + +import test_goal_supervision as fixtures +from codex_wake.goal_supervision import enroll, show, poll +from codex_wake.goal_notices import list_notices + + +class GovernorVisibilityTests(unittest.TestCase): + fixture = fixtures.GoalSupervisionTests.fixture + controller = fixtures.GoalSupervisionTests.controller + + def test_activation_precedes_poll_and_exposes_mode_and_controls(self): + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with self.controller(root), patch('codex_wake.lifecycle_budget.trusted_upper', return_value=fixtures.NOW): + enrolled = enroll(root, policy, fixtures.THREAD, 100, 120) + notices = list_notices(root, fixtures.THREAD) + self.assertEqual([n['identity']['kind'] for n in notices], ['activation']) + notice = notices[0] + self.assertIn('sampled_stop', notice['text']) + self.assertIn('maximum overshoot is not bounded', notice['text']) + self.assertIn('Deactivate governor', notice['text']) + self.assertIsNotNone(notice['recorded_at']) + self.assertLessEqual(len(notice['text'].encode()), 768) + self.assertEqual(enrolled['governor']['mode'], 'sampled_stop') + self.assertFalse(any(m in ('thread/goal/set', 'turn/interrupt') for m, _ in native['calls'])) + enroll(root, policy, fixtures.THREAD, 100, 120) + self.assertEqual(len(list_notices(root, fixtures.THREAD)), 1) + + def test_stop_presentation_failure_is_visible_outside_model_delivery(self): + from codex_wake.goal_notices import present_notice + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with self.controller(root), patch('codex_wake.lifecycle_budget.trusted_upper', return_value=fixtures.NOW): + enroll(root, policy, fixtures.THREAD, 40, 120) + stopped = poll(root)[0] + event_id = stopped['notice_ids']['stop'] + class Unsupported: + def reconcile(self, event): + raise ValueError('notice transport unsupported') + present_notice(root, event_id, adapter=Unsupported()) + status = show(root, policy, fixtures.THREAD)['governor'] + self.assertEqual(status['intervention']['reason'], 'allocation_exhausted') + self.assertEqual(status['intervention']['presentation'], 'unsupported') + self.assertTrue(status['intervention']['next_action']) + self.assertEqual(status['intervention']['ui'], 'unverified') + self.assertEqual(native['goal']['status'], 'paused') + self.assertFalse(any(m == 'turn/start' for m, _ in native['calls'])) From a86c0340d5aaf71f71678da6ca03ccdd4156a434 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 15:51:59 -0500 Subject: [PATCH 03/20] docs: record governor ticket ownership and acceptance checkpoints (#350) --- docs/dev/active-lanes.yaml | 5 +++-- .../plans/0175-2026-10-25-transparent-governor-delivery.md | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/dev/active-lanes.yaml b/docs/dev/active-lanes.yaml index 8832fce6..4b660ada 100644 --- a/docs/dev/active-lanes.yaml +++ b/docs/dev/active-lanes.yaml @@ -10,11 +10,12 @@ lanes: target: origin/main plan_state: OPEN custody_state: ACTIVE_WORKTREE - checkpoint: ce1ad93 + checkpoint: 6e11c34 + pull_request: https://github.com/CochranResearchGroup/codex-wake/pull/358 dependencies: [] overlaps: [] updated_at: 2026-10-25 - disposition: primary 351; isolated agents dispatch355 and validation356; no live effects + disposition: 351/356 CLOSED; isolated agents dispatch355 and validation356 on 352; no live effects - id: P173-GOAL-UX owner: primary / ecochran76 objective: Implement visible goal supervision and approved renewal diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index 315631ed..6f13a153 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -28,3 +28,9 @@ Use compact outputs and focused tests; inspect process/resource state before exp ## Non-goals No release, service unmask, production session dispatch, automatic resource renewal, or unrelated old-ticket closure. + +## Checkpoint: first closures + +#351 CLOSED at6e11c34 (13 focused tests and real pinned-native/stock-TUI acceptance, empty owned process census). #356 CLOSED at6515311 (worker81c4610, five public runner subprocess regressions independently rerun). Remote integration readback6e11c34. Draft PR: https://github.com/CochranResearchGroup/codex-wake/pull/358. + +#352 assigned to agent validation356 in isolated feat/governor-disable-352 based6e11c34. #355 remains with dispatch355. #353/#354/#357 remain dependency-blocked; no implementation closure claimed. Final review/remediation counters remain0/0. From 3a813f1c65f1e8f319dc80775cd84ff44593b7da Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 15:52:35 -0500 Subject: [PATCH 04/20] feat(dispatch): send compact bounded agent assignments (#355) --- GLOSSARY.md | 6 ++ docs/agent-lifecycle.md | 5 + docs/dev/evidence/issue0355/acceptance.md | 110 ++++++++++++++++++++++ docs/task-briefs.md | 56 +++++++++++ src/codex_wake/agent_lifecycle.py | 30 ++++-- src/codex_wake/codex_runtime.py | 11 +++ src/codex_wake/dispatch_packet.py | 71 ++++++++++++++ src/codex_wake/task_briefs.py | 34 ++++++- tests/test_agent_handoff_preflight.py | 2 + tests/test_agent_spawn.py | 65 ++++++++++++- tests/test_task_brief_commands.py | 27 ++++++ tests/test_task_brief_lifecycle.py | 26 +++++ 12 files changed, 432 insertions(+), 11 deletions(-) create mode 100644 docs/dev/evidence/issue0355/acceptance.md create mode 100644 src/codex_wake/dispatch_packet.py diff --git a/GLOSSARY.md b/GLOSSARY.md index 53e8356e..35b02e2c 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -50,6 +50,12 @@ and requires fresh grants before new work; original history stays fenced. **Agent spawn**: Starting a separate agent conversation with an explicit assignment and scope. +**Compact dispatch packet**: +A bounded assignment containing objective, authority, scope, acceptance, evidence +locators, allowance, stop conditions and governance readback requirements. It +starts fresh context by default; full-history context requires a recorded +task-specific reason and a pinned source. A packet snapshot creates no authority. + **Agent refresh**: Reloading selected runtime resources while preserving the conversation identity and unfinished work. Legacy profiles reopen its client; approved shared MCP diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index 06e5e0d6..61545010 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -84,6 +84,11 @@ admits a bounded initial assignment only after its exact client is ready. Use `sessions open` for attachment without task delivery. Tab creation, task admission and completed execution have separate receipt evidence. +New assignments use a [compact bounded packet](task-briefs.md#compact-delegated-dispatch), +including plain `--task` launches. Inspect `dispatch` for input byte size/hash and +`execution.output_summary` for bounded native output metadata. Packet snapshots +never replenish allowance or override current operator controls. + The operator supplies a host launch registry, an existing approved workspace, an existing owned app-server socket and tmux session. The workspace must already be trusted in the selected Codex home. Example registry: diff --git a/docs/dev/evidence/issue0355/acceptance.md b/docs/dev/evidence/issue0355/acceptance.md new file mode 100644 index 00000000..c43150b6 --- /dev/null +++ b/docs/dev/evidence/issue0355/acceptance.md @@ -0,0 +1,110 @@ +# Issue #355: compact bounded agent dispatch + +Source baseline: `ce1ad93eec437c1b4854dde2c9a174d5c5f94255` (`origin/main`). +Ticket: . +Parent contract: , story 21. +Validation: isolated source worktree, CPython 3.12.13, deterministic native Unix +WebSocket transport, fixture tmux/client and controlled clock/rollout usage. No +provider inference, installed binary qualification, production launch or campaign +renewal. The coordinator owns the parent plan and integration. + +## Observable checks + +- New public `sessions spawn` input is a compact `BOUNDED_ASSIGNMENT` packet for + both plain instructions and approved briefs. The exact native thread creation + has only `cwd`/`ephemeral`; no parent history/capability is transferred. +- The packet carries objective/instructions, approved pin/authority, workspace, + non-goals, acceptance, evidence, allowance limits/deadline, stop conditions and + a compact return contract. The fixture packet stays below 8 KiB, with exact + rendered UTF-8 bytes and SHA256 inspectable in the lifecycle receipt. +- A fresh deterministic worker reads the pinned raw operands `[6,7]` through the + packet locator and reports `42`. Raw operands are absent from the packet; + observation time, coverage and ambiguity survive its summary. The original + admission's verified source status is retained with explicit snapshot freshness. +- The observed output summary reports exactly two agent-message text bytes, the + native thread/turn locator and coverage limits. It retains at most 16 message + locators; full bodies remain at the native source. This measures bytes rather + than tokens or allocation savings, and does not certify result acceptance. +- Full-history mode records a task-specific reason and required pinned source. + Public CLI proposal rejects missing/empty reasons, unreferenced sources and + summaries lacking ambiguity. Full history is fetched by locator rather than + copied automatically. +- Historical version-1 lifecycle assignments keep their original bytes and + fingerprint on retry; changing instructions under the same intent is denied. +- Existing configured objective reservation, overspend, expiry, final usage, + queue reconciliation and handoff preflight cases remain regression checks. + +## Red and green record + +`uv run --extra dev python -m unittest + tests.test_agent_spawn.AgentSpawnTests.test_spawn_executes_exact_assignment_once_across_pending_observation -q` +initially failed: delivered text had no `BOUNDED_ASSIGNMENT` packet (`IndexError`). +After the packet was wired into public spawn, this and the shared-actor budget +case passed: 2 tests, 3.307 s. + +`uv run --extra dev python -m unittest + tests.test_agent_spawn.AgentSpawnTests.test_fresh_worker_uses_bounded_brief_and_summary_without_parent_history -q` +initially failed because the existing brief validator rejected `dispatch`. +After optional dispatch validation and native output metadata, it passed: +1 test, 1.091 s. + +`uv run --extra dev python -m unittest + tests.test_task_brief_lifecycle.TaskBriefLifecycleTests.test_preexisting_spawn_intent_keeps_original_assignment_on_upgrade -q` +initially failed with `intent key already binds a different lifecycle assignment`; +after preserving the historical format it passed: 1 test, 0.172 s. + +The first expanded handoff selection was interrupted after its mock worker tried +to parse an escaped preflight contract directly from raw task text. The fixture +now decodes packet `instructions` first and retains the exact original native +input for reconciliation. The pinned-brief successor check then passed: +1 test, 4.147 s. The interrupted run is not counted as a pass. Initial default +Python lacked `websockets`; qualified runs used the development environment. + +Final focused acceptance used +`PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python -W ignore::DeprecationWarning -m unittest` +with these cases: plain exact-once spawn, fresh brief/evidence worker, +full-history exception, historical intent preservation and invalid dispatch CLI +proposals. Result: **5 tests passed, 5.973 s**. + +The same interpreter/environment ran +`tests.test_native_delivery tests.test_lifecycle_mcp -q`: +**18 tests passed, 8.143 s**. + +Expanded source regression: + +```sh +uv run --extra dev python -W ignore::DeprecationWarning -m unittest \ + tests.test_agent_spawn tests.test_task_brief_lifecycle \ + tests.test_task_briefs tests.test_task_brief_commands \ + tests.test_agent_handoff_preflight -q +``` + +Result: **96 tests passed, 273.087 s**, exit 0. This used the isolated worktree's +uv development environment, with the same CPython version. The five focused +checks above were rerun after the final fixture assertions changed; the expanded +selection was not mislabeled as the full repository suite. + +Touched production and focused test files pass Ruff with only existing E701/E702 +style debt excluded. Baseline/current lint counts match exactly for all touched +tracked Python files; the pre-existing handoff fixture also has F841 and is not +claimed lint-clean. `git diff --check` passes. CodeGraph was initialized for this +verified worktree, then synchronized after changes. + +## Limits and integration + +Current governor state is not supplied to this spawn seam by a verified public +result. Packets explicitly label it **unknown** and require authorized public +inspection; they never claim active enforcement or propagate invented counters. +Integration with sibling governor-state work must carry any already-verified +state, including operator deactivation, without treating a packet as renewed +allowance. Existing expiry-only profiles remain labelled as such. + +No installed-runtime/provider or allocation-efficiency claim is made. The raw +input/turn locators remain authoritative; task completion is separate from +primary acceptance and issue closure. Optional `dispatch` fields extend brief +content without migrating existing revisions; older proposal validators reject +those new fields. Existing admitted intents retain their prior format. + +Memory disposition: **forbidden**. This delegated lane is explicitly confined to +its worktree; user-scoped memory writes and coordinator closeout are outside its +authorized write surface. diff --git a/docs/task-briefs.md b/docs/task-briefs.md index 9c45e218..756d432d 100644 --- a/docs/task-briefs.md +++ b/docs/task-briefs.md @@ -65,3 +65,59 @@ checkpoint projections. `validate_pin` checks a carried link against the durable original admission and immutable approved revision. It does not broaden the successor's authority or refresh resource counters. Existing lifecycle/checkpoint integration is owned by the coordinator for this feature. + +## Compact delegated dispatch + +New `sessions spawn` assignments, including plain `--task` instructions, deliver +one `BOUNDED_ASSIGNMENT` JSON packet to a fresh native thread. The packet includes +the objective, authority references, workspace/non-goals, acceptance, evidence +locators, original expiry and configured limits, stop conditions and a compact +return contract. A parent objective is a separate allowance reference; it does +not replace the delegated objective. No parent transcript, identity or capability +is copied. A brief supplies the approved task-specific acceptance and scope; +plain instructions use conservative report/partial-result defaults. + +The optional `dispatch` field in a proposed brief supports: + +```json +{ + "stop_conditions": ["Return partial evidence after one attempt"], + "evidence_summaries": [{ + "path": "/approved/workspace/raw-evidence.json", + "summary": "Two qualified observations", + "observed_at": "2026-10-25T12:00:00Z", + "coverage": "Only the named fixture", + "ambiguity": "Other inputs remain unreviewed" + }] +} +``` + +Each summary must name an existing `context` reference. Its observation time, +coverage and ambiguity are required, and raw evidence remains at the pinned +path/digest. Packets also carry the original admission's source status and time; +these are snapshots, including optional changed/unavailable sources, rather than +claims of current freshness. Sources should be rechecked before use. + +Full-history context is an explicit exception: set +`dispatch.history` to `{"mode":"full_history","path":"/approved/workspace/history.json","reason":"Task-specific continuity need"}`. +The reason must be nonempty and the path must identify a required, digest-pinned +`context` source. It is recorded with the immutable approved revision and dispatch +receipt. The packet supplies its locator; it never automatically ingests that +file or copies it inline. Default mode is `compact`. + +Packets are bounded to 64 KiB of rendered UTF-8. The lifecycle receipt's +`dispatch` exposes exact `input_bytes`, `input_sha256`, context mode and any +history reason. Exact native turn reconciliation adds `execution.output_summary`: +aggregate agent-message text bytes/count and up to 16 item locators, with explicit +coverage/truncation and the native thread/turn source. Bodies remain retrievable +from that native source. These are byte counts, not token or allocation savings. +Native completion still does not establish acceptance of the work. + +The packet references existing allowance and control authority; it does not +renew resources or attest that a governor is active. If current governor state +is unavailable in the dispatch context it is explicitly `unknown`, with a +requirement to inspect through the authorized public operator interface. Existing +operator choices remain authoritative. Legacy expiry-only profiles remain +explicitly identified and acquire no token containment guarantee. Previously +admitted lifecycle intents retain their original task bytes and fingerprint on +retry, rather than being rewritten into a new format. diff --git a/src/codex_wake/agent_lifecycle.py b/src/codex_wake/agent_lifecycle.py index 4c821bec..962a0889 100644 --- a/src/codex_wake/agent_lifecycle.py +++ b/src/codex_wake/agent_lifecycle.py @@ -63,6 +63,9 @@ def spawn_command(args, root): if not isinstance(replacement,dict) or not replacement.get('source_thread_id') or not isinstance(replacement.get('attachment'),dict): raise WakeError('same-tab spawn requires an exact source attachment') intent['replacement']=replacement + approved = None + pin = None + admission = None task_id = getattr(args, 'task_id', None) assignment_id = getattr(args, 'assignment_id', None) carried_pin = getattr(args, 'task_brief_pin', None) @@ -81,7 +84,9 @@ def spawn_command(args, root): if args.caller_thread_id else BriefAuthority('operator:local-cli', 'operator')) briefs.admit(task_id, assignment_id, authority=caller, revision=brief_revision) pin = briefs.assignment_pin(task_id, assignment_id, authority=caller) - approved = briefs.inspect(pin['task_id'])['revisions'][pin['revision'] - 1]['content'] + brief_state = briefs.inspect(pin['task_id']) + approved = brief_state['revisions'][pin['revision'] - 1]['content'] + admission = next(item for item in brief_state['admissions'] if item['assignment_id'] == pin['assignment_id']) if Path(approved['workspace']).resolve() != Path(profile['cwd']).resolve(): raise WakeError('approved task brief workspace differs from the launch profile') if approved['permission_ref'] != args.profile: @@ -91,14 +96,22 @@ def spawn_command(args, root): if approved['allocation_ref'] != objective_identity(binding['continuation']): raise WakeError('approved task brief allocation differs from the existing objective') intent['task_brief'] = pin - intent['task'] = ('Assignment instructions, subject to the approved task brief:\n' + args.task + legacy_intent = dict(intent) + if approved is not None: + legacy_intent['task'] = ('Assignment instructions, subject to the approved task brief:\n' + args.task + '\n\nAPPROVED_TASK_BRIEF=' + json.dumps(dict(pin=pin, content=approved), sort_keys=True)) - if len(intent['task'].encode()) > 65536: - raise WakeError('rendered task brief and instructions exceed the bounded assignment input') - fingerprint = hashlib.sha256(json.dumps(intent, sort_keys=True).encode()).hexdigest() journal = Path(root) / 'lifecycle' key = hashlib.sha256(args.intent.encode()).hexdigest() path = journal / (key + '.json') + previous = json.loads(path.read_text()) if path.exists() else None + legacy_fingerprint = hashlib.sha256(json.dumps(legacy_intent, sort_keys=True).encode()).hexdigest() + if previous and 'dispatch' not in previous and previous['fingerprint'] == legacy_fingerprint: + intent, dispatch = legacy_intent, None + else: + from .dispatch_packet import render_assignment + intent['task'], dispatch = render_assignment(intent, root=root, brief=approved, pin=pin, + admission=admission, receipt_path=path) + fingerprint = hashlib.sha256(json.dumps(intent, sort_keys=True).encode()).hexdigest() if not path.exists() and 'strict_execution' not in profile: endpoint=profile['app_server'] try: @@ -115,10 +128,13 @@ def spawn_command(args, root): if path.exists(): receipt = json.loads(path.read_text()) if receipt['fingerprint'] != fingerprint: - raise WakeError('intent key already binds a different lifecycle assignment') + # Historical admission stays byte-for-byte stable across upgrades. + legacy_fingerprint = hashlib.sha256(json.dumps(legacy_intent, sort_keys=True).encode()).hexdigest() + if 'dispatch' in receipt or receipt['fingerprint'] != legacy_fingerprint: + raise WakeError('intent key already binds a different lifecycle assignment') else: receipt = dict(version=1, intent_key=args.intent, fingerprint=fingerprint, - intent=intent, state='prepared', thread_id=None) + intent=intent, state='prepared', thread_id=None, dispatch=dispatch) if permission_receipt: receipt['permission_context']=permission_receipt['permission_context'] if not binding:receipt['budget_mode']='explicit_expiry_only_legacy_profile' diff --git a/src/codex_wake/codex_runtime.py b/src/codex_wake/codex_runtime.py index 6ff9355d..d78e3bf8 100644 --- a/src/codex_wake/codex_runtime.py +++ b/src/codex_wake/codex_runtime.py @@ -199,10 +199,21 @@ def exact(items): if previous.get("source") == "native_turn" and ( previous["turn_id"] != turn["id"] or previous["user_message_id"] != item["id"]): return RuntimeReconciliation(None, "multiple_native_submissions") + outputs = [entry for entry in turn['items'] + if entry.get('type') == 'agentMessage' and isinstance(entry.get('text'), str)] evidence[identity] = dict( previous, source="native_turn", turn_id=turn["id"], user_message_id=item["id"], client_message_id=identity, execution="turn_" + turn["status"], + output_summary=dict( + text_bytes=sum(len(entry['text'].encode()) for entry in outputs), + item_count=len(outputs), + item_ids=[entry.get('id') for entry in outputs[:16]], + locators_truncated=len(outputs) > 16, + coverage='Native agentMessage text only; excludes tools, reasoning and token usage.', + source=dict(thread_id=recipient.session_id, turn_id=turn['id']), + freshness='Observed with this exact native turn reconciliation', + ambiguity='Completion establishes execution, not acceptance of the result.'), ) if len(evidence) != 1: return RuntimeReconciliation( diff --git a/src/codex_wake/dispatch_packet.py b/src/codex_wake/dispatch_packet.py new file mode 100644 index 00000000..a95b7e07 --- /dev/null +++ b/src/codex_wake/dispatch_packet.py @@ -0,0 +1,71 @@ +"""Compact spawn inputs reference existing authority; they never grant resources.""" +from __future__ import annotations + +import hashlib +import json + +from .records import WakeError + + +MAX_PACKET_BYTES = 65536 + + +def render_assignment(intent, *, root, brief=None, pin=None, admission=None, receipt_path=None): + """Render a fresh worker's bounded assignment without inheriting history.""" + brief = brief or {} + options = brief.get('dispatch', {}) + configuration = intent.get('configuration', {}) + policy = intent.get('continuation', {}) + configured = bool(configuration) + mode = 'configured_objective' if configured else 'explicit_expiry_only_legacy_profile' + allocation_ref = brief.get('allocation_ref') + if configured and allocation_ref is None: + from .lifecycle_budget import objective_identity + allocation_ref = objective_identity(policy) + authority = dict(permission_ref=brief.get('permission_ref', intent['profile']), + source_thread_id=intent.get('source_thread_id'), + approval=policy.get('approval'), task_brief=pin) + allowance = dict(allocation_ref=allocation_ref, + expires_at=intent['expires_at'], + objective_ref=intent.get('objective_ref'), + parent_objective=policy.get('objective'), + inherited_actor=intent.get('budget_inherited'), + limits={key: configuration[key] for key in ( + 'session_token_limit', 'session_time_limit_seconds', + 'objective_token_limit', 'objective_time_limit_seconds', + 'objective_deadline') if key in configuration}) + if configured: + allowance['limits']['session_token_limit'] = policy.get( + 'spawn_token_limit', configuration['session_token_limit']) + packet = dict(version=1, context_mode='compact', + objective=brief.get('objective', intent['task']), + instructions=intent['task'], authority=authority, + scope=dict(workspace=intent['launch']['cwd'], phase=brief.get('phase'), + non_goals=brief.get('non_goals', ['Do not expand the assignment or its authority.'])), + acceptance=brief.get('acceptance', ['Report the requested result with evidence, or a partial/blocked outcome.']), + evidence=brief.get('context', []), evidence_summaries=options.get('evidence_summaries', []), + evidence_admission=(dict(observed_at=admission['admitted_at'], + sources=admission['context_evidence'], freshness='Admission snapshot; recheck source digests before relying on them.') + if admission else None), + allowance=allowance, + stop_conditions=options.get('stop_conditions', []) + [ + 'Stop at the original expiry or earlier authorized resource limit.', + 'Return partial evidence when blocked; do not renew allowance or expand scope.', + 'Reconcile current governance and allowance before work; unknown authority holds work.'], + governance=dict(budget_mode=mode, configuration_revision=configuration.get('revision'), + state='unknown', + required_action='Inspect current governance through the authorized public operator interface before work.', + lifecycle_root=str(root), receipt_path=str(receipt_path), + source='Host launch configuration and durable lifecycle/objective receipts; packet is a snapshot.', + controls='Existing controls and operator choices remain authoritative; this packet changes none.'), + return_contract='Return compact status, result, evidence locators, freshness, coverage, ambiguity and remaining obligations. Keep raw evidence available at its source.') + if 'history' in options: + packet['context_mode'] = 'full_history' + packet['history'] = options['history'] + text = 'BOUNDED_ASSIGNMENT=' + json.dumps(packet, sort_keys=True, separators=(',', ':'), allow_nan=False) + size = len(text.encode()) + if size > MAX_PACKET_BYTES: + raise WakeError('rendered dispatch exceeds the bounded assignment input') + return text, dict(version=1, context_mode=packet['context_mode'], input_bytes=size, + input_sha256=hashlib.sha256(text.encode()).hexdigest(), + history_reason=options.get('history', {}).get('reason')) diff --git a/src/codex_wake/task_briefs.py b/src/codex_wake/task_briefs.py index 2ee35609..dabfacbb 100644 --- a/src/codex_wake/task_briefs.py +++ b/src/codex_wake/task_briefs.py @@ -95,7 +95,7 @@ def require_workspace(self, content): def _brief(content): fields = {'objective', 'phase', 'non_goals', 'acceptance', 'context', 'workspace', 'allocation_ref', 'permission_ref'} - if type(content) is not dict or set(content) != fields: + if type(content) is not dict or not fields <= set(content) or set(content) - fields - {'dispatch'}: raise TaskBriefError('invalid_argument', 'brief fields differ from version 1 contract') value = _copy(content) if len(_json(value).encode()) > MAX_BRIEF_BYTES: @@ -121,9 +121,41 @@ def _brief(content): raise TaskBriefError('invalid_argument', 'context paths must be canonical within the workspace') if type(ref['required']) is not bool or type(ref['sha256']) is not str or not _SHA256.fullmatch(ref['sha256']): raise TaskBriefError('invalid_argument', 'context reference requires a digest and required flag') + if 'dispatch' in value: + _dispatch(value['dispatch'], value['context']) return value +def _dispatch(options, context): + if type(options) is not dict or set(options) - {'stop_conditions', 'evidence_summaries', 'history'}: + raise TaskBriefError('invalid_argument', 'dispatch options are invalid') + stops = options.get('stop_conditions', []) + if type(stops) is not list or len(stops) > 32: + raise TaskBriefError('invalid_argument', 'dispatch stop conditions must be bounded') + for stop in stops: + _text(stop, 'stop condition') + summaries = options.get('evidence_summaries', []) + if type(summaries) is not list or len(summaries) > 16: + raise TaskBriefError('invalid_argument', 'dispatch summaries must be bounded') + from .records import parse_timestamp + sources = {ref['path']: ref for ref in context} + for summary in summaries: + if (type(summary) is not dict or set(summary) != {'path', 'summary', 'observed_at', 'coverage', 'ambiguity'} + or type(summary.get('path')) is not str or summary['path'] not in sources): + raise TaskBriefError('invalid_argument', 'summary requires a raw context source and evidence caveats') + for key in ('summary', 'observed_at', 'coverage', 'ambiguity'): + _text(summary[key], key) + parse_timestamp(summary['observed_at']) + if 'history' in options: + history = options['history'] + if (type(history) is not dict or set(history) != {'mode', 'reason', 'path'} + or history.get('mode') != 'full_history' + or type(history.get('path')) is not str + or not sources.get(history['path'], {}).get('required')): + raise TaskBriefError('invalid_argument', 'full history requires a reason and a required context source') + _text(history['reason'], 'task-specific full-history reason') + + def _verify_context(content): observations = [] workspace = Path(content['workspace']) diff --git a/tests/test_agent_handoff_preflight.py b/tests/test_agent_handoff_preflight.py index 351bd63f..6b5009bf 100644 --- a/tests/test_agent_handoff_preflight.py +++ b/tests/test_agent_handoff_preflight.py @@ -342,6 +342,8 @@ def handler(ws): elif method=='thread/queue/list':value={'data':[queue[tid]] if tid in queue else [],'nextCursor':None} elif method=='thread/queue/start': q=queue.pop(tid);task=q['input'][0]['text'] + if task.startswith('BOUNDED_ASSIGNMENT='): + task=json.loads(task.split('BOUNDED_ASSIGNMENT=',1)[1])['instructions'] contract=json.loads(task.split('PREFLIGHT_CONTRACT=',1)[1].split('\n',1)[0]) corrected='CORRECTION_PACKET=' in task if corrected: diff --git a/tests/test_agent_spawn.py b/tests/test_agent_spawn.py index 401a23c2..f58be529 100644 --- a/tests/test_agent_spawn.py +++ b/tests/test_agent_spawn.py @@ -163,6 +163,12 @@ def test_peer_cannot_select_profile_without_host_caller_grant(self): def test_spawn_executes_exact_assignment_once_across_pending_observation(self): self.spawn_scenario() + def test_fresh_worker_uses_bounded_brief_and_summary_without_parent_history(self): + self.spawn_scenario(brief_dispatch=True) + + def test_full_history_exception_carries_recorded_task_specific_reason(self): + self.spawn_scenario(brief_dispatch=True,full_history=True) + def test_configured_spawn_reserves_and_settles_one_shared_actor(self): self.spawn_scenario(budget=True) @@ -227,7 +233,7 @@ def test_shared_profile_resource_checks_do_not_override_spawn_launch(self): self.spawn_scenario(resource_profile=True) def spawn_scenario(self, *, loss=None, check_environment=False, expire_at_task=False, - check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False): + check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False,brief_dispatch=False,full_history=False): thread_id = '00000000-0000-7000-8000-000000000157' starts, tasks, windows = [], [], [] turn_starts = [] @@ -273,6 +279,11 @@ def handler(ws): status='completed' if loss else 'inProgress', items=[ dict(id='user157', type='userMessage', clientId=task['clientUserMessageId'], content=task['input'])])] + if brief_dispatch: + packet = json.loads(task['input'][0]['text'].split('BOUNDED_ASSIGNMENT=', 1)[1]) + operands = json.loads(Path(packet['evidence'][0]['path']).read_text()) + state['turns'][0]['items'].append(dict(type='agentMessage', id='result157', + text=str(operands[0] * operands[1]))) result = {'turn': state['turns'][0]} if loss == 'queue/start': ws.close() @@ -324,6 +335,26 @@ def handler(ws): continuations={'bootstrap':dict(approval='operator fixture',objective='Finish fixture', started_at='1970-01-01T00:16:40Z')}) if budget_overrun:registry['continuations']['bootstrap']['spawn_token_limit']=50 + if brief_dispatch: + from codex_wake.task_briefs import BriefAuthority, TaskBriefStore + import hashlib + evidence = root / 'operands.json' + evidence.write_text('[6,7]') + brief = dict(objective='Multiply the evidence operands', phase='verification', + non_goals=['Never deploy'], acceptance=['Report the product 42'], + workspace=tmp, allocation_ref='existing-fixture-allowance', permission_ref='worker', + context=[dict(path=str(evidence), sha256=hashlib.sha256(evidence.read_bytes()).hexdigest(), required=True)], + dispatch=dict(stop_conditions=['Return partial evidence after one attempt'], + evidence_summaries=[dict(path=str(evidence), summary='Two operands', + observed_at='1970-01-01T00:16:40Z', coverage='Only this fixture', + ambiguity='No inference about other inputs')])) + if full_history: + brief['dispatch']['history'] = dict(mode='full_history', path=str(evidence), + reason='Exact original operands are required to reproduce this task') + store = TaskBriefStore(root) + authority = BriefAuthority('operator:fixture', 'operator') + store.propose('multiply', brief, authority=authority) + store.approve('multiply', 1, authority=authority) profiles.write_text(json.dumps(registry)) master, slave = pty.openpty() client = None @@ -357,6 +388,8 @@ def command(argv, **kwargs): '--task', 'Calculate 6 * 7 and report 42.', '--expires-at', '2030-01-01T00:00:00Z', '--json'] if budget:argv.extend(['--objective-ref','bootstrap']) + if brief_dispatch: + argv.extend(['--task-id','multiply','--assignment-id','multiply-one']) interrupted = False budget_expired=False def clock(): @@ -480,11 +513,37 @@ def clock(): 'reason': 'ConnectionClosedOK'}, receipt['history']) self.assertIsNone(receipt.get('reason')) self.assertEqual(len(starts), 1) + self.assertEqual(starts[0], {'cwd': tmp, 'ephemeral': False}) self.assertEqual(len(tasks), 1) self.assertEqual(len(windows), 1) self.assertEqual(len(turn_starts), 0 if auto_start else 1) - self.assertEqual(tasks[0]['input'], [ - {'type': 'text', 'text': 'Calculate 6 * 7 and report 42.'}]) + packet = json.loads(tasks[0]['input'][0]['text'].split('BOUNDED_ASSIGNMENT=', 1)[1]) + self.assertEqual(packet['objective'], 'Multiply the evidence operands' if brief_dispatch else 'Calculate 6 * 7 and report 42.') + self.assertEqual(packet['instructions'], 'Calculate 6 * 7 and report 42.') + self.assertEqual(packet['context_mode'], 'full_history' if full_history else 'compact') + self.assertEqual(packet['scope']['workspace'], tmp) + self.assertEqual(packet['authority']['permission_ref'], 'worker') + self.assertTrue(packet['acceptance']) + self.assertTrue(packet['stop_conditions']) + self.assertEqual(packet['allowance']['expires_at'], '2030-01-01T00:00:00Z') + self.assertEqual(packet['governance']['budget_mode'], + 'configured_objective' if budget else 'explicit_expiry_only_legacy_profile') + self.assertEqual(receipt['dispatch']['input_bytes'], len(tasks[0]['input'][0]['text'].encode())) + self.assertLess(receipt['dispatch']['input_bytes'], 8192) + if full_history: + self.assertEqual(receipt['dispatch']['history_reason'], + 'Exact original operands are required to reproduce this task') + self.assertEqual(packet['history']['path'], str(evidence)) + if brief_dispatch: + self.assertNotIn('[6,7]', tasks[0]['input'][0]['text']) + self.assertEqual(packet['evidence_admission']['sources'][0]['status'], 'verified') + self.assertEqual(packet['evidence_summaries'][0]['coverage'], 'Only this fixture') + self.assertEqual(packet['evidence_summaries'][0]['ambiguity'], 'No inference about other inputs') + self.assertEqual(packet['evidence_summaries'][0]['observed_at'], '1970-01-01T00:16:40Z') + self.assertEqual(packet['stop_conditions'][0], 'Return partial evidence after one attempt') + self.assertEqual(state['turns'][0]['items'][-1]['text'], '42') + self.assertEqual(receipt['execution']['output_summary']['text_bytes'], 2) + self.assertEqual(receipt['execution']['output_summary']['source']['turn_id'], 'turn157') if check_environment: variables = json.loads((root / 'client-env.json').read_text()) self.assertFalse(set(variables) & { diff --git a/tests/test_task_brief_commands.py b/tests/test_task_brief_commands.py index dc48f4ac..96713fb4 100644 --- a/tests/test_task_brief_commands.py +++ b/tests/test_task_brief_commands.py @@ -49,6 +49,33 @@ def command(verb, *extra, authority=None): self.assertEqual(code, 0) self.assertEqual(checkpoint['brief']['task_id'], 'task-1') + def test_dispatch_refuses_unqualified_summaries_and_unjustified_full_history(self): + from codex_wake.cli import run + import hashlib + with tempfile.TemporaryDirectory() as tmp: + directory = Path(tmp) + evidence = directory / 'raw.txt' + evidence.write_text('Preserved raw evidence') + bus, capability = BusStore.configure(directory / 'bus') + content = dict(objective='Inspect fixture', phase='verification', + non_goals=['No release'], acceptance=['Return evidence'], + context=[dict(path=str(evidence), sha256=hashlib.sha256(evidence.read_bytes()).hexdigest(), required=True)], + workspace=tmp, allocation_ref='existing', permission_ref='worker') + summary = dict(path=str(evidence), summary='Evidence summary', coverage='Fixture only', + ambiguity='Unreviewed outside fixture', observed_at='2030-01-01T00:00:00Z') + invalid = [dict(history=dict(mode='full_history', path=str(evidence))), + dict(history=dict(mode='full_history', path=str(evidence), reason=' ')), + dict(history=dict(mode='full_history', path='/other/raw.txt', reason='Continuity')), + dict(evidence_summaries=[{key: value for key, value in summary.items() if key != 'ambiguity'}])] + for options in invalid: + output = StringIO() + with redirect_stdout(output): + code = run(['--wake-root', str(directory / 'wake'), 'task-briefs', 'propose', + '--task-id', 'task', '--brief-json', json.dumps(dict(content, dispatch=options)), + '--bus-root', str(bus.root), '--operator-capability', str(capability)]) + self.assertEqual(code, 5, output.getvalue()) + self.assertFalse(json.loads(output.getvalue())['success']) + def test_worker_workspace_is_enforced_for_revision_selection_and_retries(self): from codex_wake.task_briefs import TaskBriefStore with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/test_task_brief_lifecycle.py b/tests/test_task_brief_lifecycle.py index 302e4e68..dc2fd364 100644 --- a/tests/test_task_brief_lifecycle.py +++ b/tests/test_task_brief_lifecycle.py @@ -68,6 +68,32 @@ def invoke(): self.assertEqual(repeated['intent']['task_brief']['revision'], 1) self.assertNotIn('New approved objective', repeated['intent']['task']) + def test_preexisting_spawn_intent_keeps_original_assignment_on_upgrade(self): + endpoint = socket.socket(socket.AF_UNIX) + endpoint.bind(str(self.endpoint)) + self.addCleanup(endpoint.close) + args = self.spawn_args() + def invoke(): + output = StringIO() + with redirect_stdout(output), patch('codex_wake.native_clock.snapshot', + return_value={'utc': {'lower': 1893456001, 'upper': 1893456002}}): + self.assertEqual(spawn_command(args, self.root), 5) + return json.loads(output.getvalue()) + saved = invoke() + # Persisted version-1 fixture from the previous public spawn format. + original = ('Assignment instructions, subject to the approved task brief:\n' + args.task + + '\n\nAPPROVED_TASK_BRIEF=' + json.dumps(dict(pin=saved['intent']['task_brief'], + content=self.content), sort_keys=True)) + saved['intent']['task'] = original + saved.pop('dispatch') + saved['fingerprint'] = hashlib.sha256(json.dumps(saved['intent'], sort_keys=True).encode()).hexdigest() + path = self.root / 'lifecycle' / (hashlib.sha256(args.intent.encode()).hexdigest() + '.json') + path.write_text(json.dumps(saved)) + self.assertEqual(invoke()['intent']['task'], original) + args.task = 'A different assignment' + with self.assertRaisesRegex(WakeError, 'different lifecycle assignment'): + invoke() + def test_unapproved_brief_cannot_reach_spawn_journal(self): self.store.propose('task-2', self.content, authority=self.operator) args = self.spawn_args() From 7f81f0d00e32b10190be22aad7c2cc20dc80a555 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:00:44 -0500 Subject: [PATCH 05/20] feat: add durable operator governor deactivation (#352) --- docs/governor-control.md | 65 ++++ docs/session-permissions.md | 6 +- src/codex_wake/goal_governance.py | 278 +++++++++++++++++ src/codex_wake/goal_limits_cli.py | 13 +- src/codex_wake/goal_notices.py | 42 ++- src/codex_wake/goal_readiness.py | 3 + src/codex_wake/goal_renewal.py | 4 + src/codex_wake/goal_supervision.py | 24 ++ src/codex_wake/mcp_server.py | 4 +- src/codex_wake/permission_enforcement.py | 36 +-- tests/test_goal_supervision.py | 12 +- tests/test_governor_deactivation.py | 373 +++++++++++++++++++++++ 12 files changed, 817 insertions(+), 43 deletions(-) create mode 100644 docs/governor-control.md create mode 100644 src/codex_wake/goal_governance.py create mode 100644 tests/test_governor_deactivation.py diff --git a/docs/governor-control.md b/docs/governor-control.md new file mode 100644 index 00000000..a48f525a --- /dev/null +++ b/docs/governor-control.md @@ -0,0 +1,65 @@ +# Governor control + +The existing native conversation exposes the selected governor scope in its +activation/status notice. To deactivate it, send the whole message: + +> Deactivate governor + +The model calls `wake_goal_limits_disable` as a mechanical adapter. Wake reads the +actual native user message for that exact current caller turn; model arguments, +quoted or embedded instructions, historical turns and tool outputs cannot grant +operator authority. No further confirmation or model approval is required. The +same control works with a missing/stopped watcher. A shared allocation is the +selected scope, including its existing and future members; peer allocations stay +unchanged. A mismatched explicit allocation is refused. + +The result reports `governor_enabled: false`, `state: inactive`, and +`governor.mode: inactive`. `campaign_state` retains the underlying work state. +`governor_revision` identifies the durable control choice. Accounting, checkpoints, +stop latches, original limits/deadline, service masks and unrelated objectives are +preserved. Deactivation does not resume work or renew resource authority. The model +must drop superseded governor checkpoint/stop instructions while honoring +independent platform controls and the operator's existing work authority. + +To reactivate, send the whole message: + +> Reactivate governor + +`wake_goal_limits_enable` verifies the same native provenance and announces the +scope and sampled stopping mode. It does not resume stopped work, clear a stop, +reset usage, extend a deadline, or undo a service mask. A stopped watcher remains +stopped. Watcher readiness and operator/model notice delivery remain separate +reported facts; neither is invented by changing this switch. + +Both audiences receive a revision-bound no-turn native notice when the selected +host supports the existing notice extension. Delivery failures remain explicit in +`notices` and retained governor status; effective deactivation does not depend on +notice availability. The control notice says its revision supersedes older +notices. Unsubmitted older notices are presented as historical with a current +scope-state reference; original text is retained. Already attempted/uncertain +notice text is never rewritten or replayed. Historical delivery cannot reactivate +the governor. UI rendering and human reading still require native qualification. + +The conversational adapter requires a connected model capable of making that +mechanical tool call and a native endpoint capable of reading the caller turn. +For a disconnected model/native runtime, a trusted operator host can independently +call `goal_governance.change(root, thread, enabled=False, principal=operator)`. +Only a host-authenticated `Principal(thread, 'operator')` is accepted; an agent +principal cannot self-disable, including under permissive policy. The existing +capability-authenticated `goal-limits disable` / `enable` CLI also exposes this +operation for host integrations. Current mandatory common-policy prohibitions +remain enforceable and failures are explicit. The runtime is a same-UID authority +boundary, not hostile same-UID isolation. + +Scope state commits under the existing enrollment/membership and all member +receipt locks, in the controller's receipt-before-allocation order. Queued polls +observe the switch before effects; in-flight effects complete before it takes +effect. The allocation ledger commits first so a partial member-copy failure still +leaves the authoritative switch disabled. Retrying reconciles copies/notices at +the same revision. A late approved renewal resume is held while disabled. + +`goal_governance.snapshot(root, thread_id)` provides a read-only host snapshot for +first dispatch intent creation: `state`, `enabled`, `mode`, `revision`, `scope`, +`thread_id`, `source` and `observed_at`. Missing/unqualified state is explicitly +unavailable and is not a new work/repair gate. Keep the original snapshot on intent +retry; it is context, while current durable scope authority governs effects. diff --git a/docs/session-permissions.md b/docs/session-permissions.md index 8e877362..38a8ec0d 100644 --- a/docs/session-permissions.md +++ b/docs/session-permissions.md @@ -58,8 +58,10 @@ refresh/handoff/resume, enforcement enrollment, managed A2A and task/recipient- authorized wake routes. Shared reload requires standing approval. Increasing an allowance, disabling enforcement and recovering an observation-fault stop require their own authority. Observation recovery requires an explicit matching approval or authenticated -operator request and fresh runtime qualification. Disable execution remains -unsupported; inspecting permissions never clears latches. +operator request and fresh runtime qualification. [Governor deactivation](governor-control.md) +requires an authenticated operator control or an exact host-verified native user +directive, even if a model has standing policy approval. Inspecting permissions +never clears latches; deactivation preserves latches and the original allowance. The optional `global`, named `profiles`, `sessions` and task `assignments` layers have `default` and `actions` fields. Decisions are `allow`, `deny`, diff --git a/src/codex_wake/goal_governance.py b/src/codex_wake/goal_governance.py new file mode 100644 index 00000000..33925bff --- /dev/null +++ b/src/codex_wake/goal_governance.py @@ -0,0 +1,278 @@ +"""Operator-owned governor switch, independent of watcher health and budgets.""" + +from contextlib import ExitStack +import hashlib +import json +from pathlib import Path +import time + +from . import goal_supervision as supervision +from .permission_enforcement import authorize +from .session_permissions import PermissionDenied, Principal + + +def sync(receipt): + """Allocation state is authoritative, including after a partial member write.""" + source = receipt + if receipt.get("allocation_path"): + path = Path(receipt["allocation_path"]) + with supervision._lock(path.parent, path.stem): + supervision._private(path) + source = json.loads(path.read_text()) + for key in ("governor_enabled", "governor_revision", "governor_control"): + if key in source: + receipt[key] = source[key] + return receipt.get("governor_enabled", True) is False + + +def native_operator(root, receipt, native_context, enabled): + """Only a whole native user message in the exact live caller turn authorizes. + + Claimed roles, tool outputs, quoted instructions and historical user messages + never confer operator authority. The model mechanically consumes this action. + """ + caller = supervision.verified_native_caller(native_context, receipt["thread_id"]) + native = supervision._request( + receipt["grant"], + "thread/read", + dict(threadId=receipt["thread_id"], includeTurns=True), + )["thread"] + if native.get("id") != receipt["thread_id"]: + raise PermissionDenied("governor_operator_required") + turns = [ + turn for turn in native.get("turns", []) if turn.get("id") == caller["turn_id"] + ] + command = "Reactivate governor" if enabled else "Deactivate governor" + if len(turns) != 1 or turns[0].get("status") != "inProgress": + raise PermissionDenied("governor_operator_required") + users = [ + item for item in turns[0].get("items", []) if item.get("type") == "userMessage" + ] + content = users[0].get("content") if len(users) == 1 else None + if ( + not isinstance(content, list) + or len(content) != 1 + or not isinstance(content[0], dict) + or content[0].get("type") != "text" + or content[0].get("text") != command + ): + raise PermissionDenied("governor_operator_required") + # A stale caller packet cannot consume an old directive after another turn. + _, active = supervision._native(receipt["grant"], receipt["thread_id"]) + if active != [caller["turn_id"]]: + raise PermissionDenied("governor_operator_required") + return dict( + source="native_user_message", + turn_id=caller["turn_id"], + call_id=caller["call_id"], + ) + + +def change( + root, thread, *, enabled, principal=None, native_context=None, allocation_id=None +): + """Commit one scope switch under the same locks as every controller effect. + + Maintenance fences enrollment/membership. All member locks precede the shared + ledger lock (the existing poll order), so pending polls either precede the + durable switch or observe it before any effect. No native work is resumed. + """ + if type(enabled) is not bool: + raise ValueError("boolean governor state required") + directory = supervision._directory(root) + path = directory / (supervision._name(thread) + ".json") + notices = [] + with ( + supervision._lock(directory, "recovery"), + supervision._lock(directory, "maintenance"), + ): + supervision._private(path) + first = json.loads(path.read_text()) + allocation_path = ( + Path(first["allocation_path"]) if first.get("allocation_path") else None + ) + if allocation_id is not None and allocation_id != first["allocation_id"]: + raise ValueError("governor selected scope does not match session") + allocation = None + if allocation_path: + expected = supervision._allocation_path(directory, first["allocation_id"]) + if allocation_path != expected: + raise ValueError("governor allocation scope path changed") + supervision._private(allocation_path) + allocation = json.loads(allocation_path.read_text()) + if ( + allocation["allocation_id"] != first["allocation_id"] + or thread not in allocation["members"] + ): + raise ValueError("governor allocation membership changed") + members = sorted((allocation or {}).get("members", {thread: {}})) + with ExitStack() as locks: + for member in members: + locks.enter_context( + supervision._lock(directory, supervision._name(member)) + ) + if allocation_path: + locks.enter_context( + supervision._lock(allocation_path.parent, allocation_path.stem) + ) + allocation = json.loads(allocation_path.read_text()) + if sorted(allocation["members"]) != members: + raise ValueError( + "governor scope changed; inspect current allocation" + ) + first = json.loads(path.read_text()) + if first["thread_id"] != thread: + raise ValueError("governor target identity changed") + if native_context is not None: + control = native_operator(root, first, native_context, enabled) + principal = Principal(thread, "operator") + else: + if ( + not isinstance(principal, Principal) + or principal.role != "operator" + or principal.subject != thread + ): + raise PermissionDenied("governor_operator_required") + control = dict(source="authenticated_operator") + permission = authorize(root, "enforcement.disable", principal) + for member in members: + authorize(root, "enforcement.disable", Principal(member, "operator")) + authority = allocation if allocation is not None else first + revision = authority.get("governor_revision", 0) + changed = authority.get("governor_enabled", True) != enabled + if changed: + revision += 1 + control.update( + at=time.time(), + revision=revision, + enabled=enabled, + scope=first["allocation_id"], + members=members, + ) + else: + control = authority.get("governor_control", control) + state = dict( + governor_enabled=enabled, + governor_revision=revision, + governor_control=control, + ) + # Commit scope authority first. A crash leaves disabled member copies + # repairable from this record, never an active orphaned member. + if allocation_path: + allocation.update(state) + supervision._save(allocation_path, allocation) + for member in members: + member_path = directory / (supervision._name(member) + ".json") + if not member_path.exists(): + continue + supervision._private(member_path) + receipt = json.loads(member_path.read_text()) + if receipt["allocation_id"] != first["allocation_id"]: + continue # historical finalized member, now in another scope + receipt.update(state) + kind = "reactivation" if enabled else "deactivation" + # Record/reconcile the same revision notice on an idempotent retry. + if revision: + supervision._record_supervision_notice(member_path, receipt, kind) + event = receipt.get("notice_ids", {}).get(kind) + if event: + notices.append(event) + supervision._save(member_path, receipt) + if member == thread: + first = receipt + # Delivery is separate from effective state and needs no running watcher. + from .goal_notices import present_notice, governor_status + + presentation = [] + for event in notices: + try: + notice = present_notice(root, event) + presentation.append(dict(event_id=event, state=notice["delivery"]["state"])) + except Exception as error: + presentation.append( + dict(event_id=event, state="unavailable", error=type(error).__name__) + ) + return dict( + supervision._public(first), + governor=governor_status(root, first), + changed=changed, + permission=permission, + notices=presentation, + members=members, + resumed=False, + renewed_resources=False, + controller=supervision._heartbeat(directory), + message=( + "Governor inactive in this scope. Drop stale governor checkpoint/stop instructions; " + "independent platform controls remain. Accounting and checkpoints are preserved; " + "no work is resumed and no resources renewed." + if not enabled + else "Governor reactivated in this scope in sampled_stop mode; no work resumed or resources renewed." + ), + ) + + +def snapshot(root, thread_id): + """Read-only host snapshot for a first dispatch intent, never a work gate. + + Callers retain this original snapshot on retry; fresh scope authority wins at + effect boundaries. Unknown availability neither enables nor disables work. + """ + try: + name = supervision._name(thread_id) + root = Path(root).absolute() + if root != root.resolve(): + raise ValueError("noncanonical root") + supervision._private(root, True) + directory = root / "goal-supervision" + supervision._private(directory, True) + path = directory / (name + ".json") + supervision._private(path) + receipt = json.loads(path.read_text()) + if receipt["thread_id"] != thread_id: + raise ValueError("session identity changed") + authority, source = receipt, "session_receipt" + if receipt.get("allocation_path"): + expected = ( + directory + / "allocations" + / ( + hashlib.sha256(receipt["allocation_id"].encode()).hexdigest() + + ".json" + ) + ) + if ( + Path(receipt["allocation_path"]) != expected + or expected.resolve() != expected + ): + raise ValueError("allocation scope path changed") + supervision._private(expected) + authority = json.loads(expected.read_text()) + if ( + authority["allocation_id"] != receipt["allocation_id"] + or thread_id not in authority["members"] + ): + raise ValueError("allocation membership changed") + source = "allocation_ledger" + enabled = authority.get("governor_enabled", True) + if type(enabled) is not bool: + raise ValueError("invalid governor control state") + return dict( + state="available", + enabled=enabled, + mode="sampled_stop" if enabled else "inactive", + revision=authority.get("governor_revision", 0), + scope=receipt["allocation_id"], + thread_id=thread_id, + source=source, + observed_at=time.time(), + ) + except (OSError, ValueError, KeyError, TypeError) as error: + return dict( + state="unavailable", + enabled=None, + mode="unknown", + thread_id=thread_id, + reason=type(error).__name__, + observed_at=time.time(), + ) diff --git a/src/codex_wake/goal_limits_cli.py b/src/codex_wake/goal_limits_cli.py index cbf69f7d..2cfe718c 100644 --- a/src/codex_wake/goal_limits_cli.py +++ b/src/codex_wake/goal_limits_cli.py @@ -9,7 +9,7 @@ def add_goal_limits_parser(subparsers): parser = subparsers.add_parser('goal-limits', help='supervise resource limits in an existing native session') commands = parser.add_subparsers(dest='goal_limits_command', required=True) - for name in ('set', 'show', 'readiness', 'diagnose', 'notices', 'recover', 'disable', 'watcher-status', 'start-watcher'): + for name in ('set', 'show', 'readiness', 'diagnose', 'notices', 'recover', 'disable', 'enable', 'watcher-status', 'start-watcher'): command = commands.add_parser(name, help='enroll limits' if name == 'set' else 'read enforcement evidence') command.add_argument('--policy', type=Path, required=True, help='private host-approved caller allocations') command.add_argument('--caller-thread-id', required=True) @@ -55,7 +55,7 @@ def goal_limits_command(args, root): from .goal_supervision import enroll, show from .session_permissions import PermissionStore principal=None - if args.goal_limits_command!='prepare-maintenance' and (PermissionStore(root).active() or args.goal_limits_command in ('recover','disable','start-watcher')): + if args.goal_limits_command!='prepare-maintenance' and (PermissionStore(root).active() or args.goal_limits_command in ('recover','disable','enable','start-watcher')): from .permission_enforcement import cli_principal principal=cli_principal(args,root) if args.goal_limits_command in ('watcher-status','start-watcher'): @@ -78,10 +78,9 @@ def goal_limits_command(args, root): from .goal_recovery import recover result=recover(root,args.policy,args.caller_thread_id,principal=principal, task_id=args.task_id,operation_id=args.operation_id,allocation_id=args.allocation_id) - elif args.goal_limits_command=='disable': - from .permission_enforcement import diagnostic - result=diagnostic(root,args.caller_thread_id,'enforcement.'+args.goal_limits_command, - principal=principal,task_id=args.task_id,operation_id=args.operation_id) + elif args.goal_limits_command in ('disable','enable'): + from .goal_governance import change + result=change(root,args.caller_thread_id,enabled=args.goal_limits_command=='enable',principal=principal,allocation_id=args.allocation_id) else: result = enroll(root, args.policy, args.caller_thread_id, args.token_limit, args.time_limit_seconds, @@ -120,4 +119,4 @@ def goal_limits_command(args, root): print('Conversation presentation: '+readiness['presentation']+'; operator reading: unverified.') print(readiness['next_action']) else:print(json.dumps(result)) - return 0 if args.goal_limits_command in ('show','readiness','diagnose','notices','watcher-status') or result.get('state') in ('armed', 'checkpoint_requested', 'prepared') or result.get('reason')=='watcher_running' else 2 + return 0 if args.goal_limits_command in ('show','readiness','diagnose','notices','watcher-status','disable','enable') or result.get('state') in ('armed', 'checkpoint_requested', 'prepared','inactive') or result.get('reason')=='watcher_running' else 2 diff --git a/src/codex_wake/goal_notices.py b/src/codex_wake/goal_notices.py index f4f99104..c559e476 100644 --- a/src/codex_wake/goal_notices.py +++ b/src/codex_wake/goal_notices.py @@ -11,7 +11,7 @@ import re from datetime import datetime, timezone -KINDS = frozenset(('activation', 'engagement', 'checkpoint', 'stop', 'recovery')) +KINDS = frozenset(('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation')) _EVENT = re.compile(r'notice_[a-f0-9]{64}') @@ -75,7 +75,7 @@ def record_notice(root, receipt, kind, *, controller=None): receipt=dict(receipt,active_stop_id=recovery['stop_episode_id'],reason=recovery['reason']) identity = dict(thread_id=receipt['thread_id'], allocation_id=receipt['allocation_id'], generation=receipt['generation'], enrollment_revision=receipt.get('enrollment_revision', 1), - kind=kind) + kind=kind,governor_revision=receipt.get('governor_revision',0)) if kind in ('stop', 'recovery'): identity.update(stop_episode=receipt.get('active_stop_id') or receipt.get('stop_episode_id') or [stop.get('boot_id'), stop.get('observed_boottime'), receipt.get('pause_epoch', 0)], @@ -120,6 +120,16 @@ def render_notice(event): usage = f'{used:,} of {limit:,}' if type(used) is int and type(limit) is int else 'usage unavailable' remaining = f'{max(0, limit-used):,}' if type(used) is int and type(limit) is int else 'unknown' kind = identity['kind'] + if kind in ('deactivation','reactivation'): + text=('Governor inactive. Drop stale governor checkpoint/stop instructions. Independent platform controls remain. ' + 'Say "Reactivate governor" to enable sampled stopping. ' if kind=='deactivation' else + 'Governor reactivated. Mode sampled_stop; maximum overshoot is not bounded. Say "Deactivate governor" to disable. ') + text += (f"Governor revision {identity.get('governor_revision',0)} supersedes older notices; latest durable scope state is authoritative. " + f"Scope {identity['allocation_id']}; session {identity['thread_id']}. " + 'Accounting and checkpoints preserved; no resource renewal or work resume. ' + f"At {event.get('recorded_at','unknown')}. [External supervision event {event['event_id']}]") + if len(text.encode())>768:raise ValueError('governor notice exceeds native bound') + return text if kind == 'stop': title = 'Goal paused' if event['proof']['control']['stop_verified'] else 'Stop requested; interruption unverified' elif kind == 'checkpoint': @@ -144,7 +154,7 @@ def render_notice(event): text += ('Extend allowance / Reset budget window / Keep stopped. ' 'Native /goal resume alone does not renew this external allowance. ') text += ('Mode sampled_stop; maximum overshoot is not bounded. ' - 'Deactivate governor: goal-limits disable. ' + 'Say "Deactivate governor". ' f"At {event.get('recorded_at', 'unknown')}. " ) return text + f"[External supervision event {event['event_id']}]" @@ -220,6 +230,25 @@ def present_notice(root, event_id, *, adapter=None): event['delivery']['last_error'] = type(error).__name__ _save(path, event) return event + # Unsubmitted historical events still explain their original outcome, + # without presenting an old activation/stop as today's control choice. + # Never rewrite text after any append attempt (including an unknown one). + if event['delivery']['attempts']==0: + from .goal_governance import snapshot + current=snapshot(root,event['identity']['thread_id']) + if (current['state']=='available' and current['revision']!=event['identity'].get('governor_revision',0)): + event.setdefault('historical',dict(original_text=event['text'])) + mode=current['mode'] + identity=event['identity'];observed=event['snapshot'] + event['text']=(f"Historical {identity['kind']} event: {observed.get('reason') or identity['kind']}; " + f"usage {observed.get('used_tokens')}/{observed.get('token_limit')}. " + f"Current governor revision {current['revision']} is {mode}. This historical delivery does not reactivate governance. " + 'Honor latest durable scope state; drop superseded governor instructions. Independent platform controls remain. ' + f"Scope {identity['allocation_id']}; session {identity['thread_id']}. " + f"[External supervision event {event['event_id']}]") + if len(event['text'].encode())>768: + raise ValueError('historical notice exceeds native bound') + _save(path,event) # The read-only lookup also qualifies support before append. Only this # positively unsubmitted path can return pending/unsupported and retry. try: @@ -283,11 +312,12 @@ def run_presenter(root, stop): def governor_status(root, receipt): """Independent inspection exposes control state even if notice delivery fails.""" status = dict(identity='Codex Wake governor', scope=receipt['allocation_id'], - mode='sampled_stop', maximum_token_overshoot=None, - controls=['Inspect governor', 'Deactivate governor', 'Keep stopped'], + mode='inactive' if receipt.get('governor_enabled',True) is False else 'sampled_stop', + enabled=receipt.get('governor_enabled',True), maximum_token_overshoot=None, + controls=['Inspect governor', 'Reactivate governor' if receipt.get('governor_enabled',True) is False else 'Deactivate governor', 'Keep stopped'], intervention=None) ids = receipt.get('notice_ids', {}) - for kind in ('activation', 'engagement', 'checkpoint', 'stop', 'recovery'): + for kind in ('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation'): event_id = ids.get(kind) if not event_id: continue diff --git a/src/codex_wake/goal_readiness.py b/src/codex_wake/goal_readiness.py index c5d56d18..019fa354 100644 --- a/src/codex_wake/goal_readiness.py +++ b/src/codex_wake/goal_readiness.py @@ -5,6 +5,9 @@ def engagement(receipt, controller): """Describe current proof without manufacturing a watcher or resource authority.""" + if receipt.get('governor_enabled',True) is False: + return dict(status='inactive',engaged=False,enforcement='inactive',presentation='pending', + operator_read='unverified',next_action='Governor disabled by operator. Drop stale governor instructions; independent platform controls remain. No resources renewed or work resumed.') status = 'waiting_for_first_poll' action = 'Wait for the independent watcher to poll this enrollment; inspect goal-limits show.' accounting = receipt.get('accounting') or {} diff --git a/src/codex_wake/goal_renewal.py b/src/codex_wake/goal_renewal.py index 2a09847b..6431f523 100644 --- a/src/codex_wake/goal_renewal.py +++ b/src/codex_wake/goal_renewal.py @@ -947,6 +947,10 @@ def _resume(root, policy, thread, path, journal, principal, *, caller=None): if result.get("resumed"): return result with _campaign(root, policy, thread) as (_, receipts, allocation_path, allocation): + if (allocation or next(iter(receipts.values()))).get('governor_enabled',True) is False: + result['resume_state'] = 'governor_inactive' + result['next_action'] = 'Governor was deactivated; this queued resume cannot start work. Reactivation does not itself authorize resume.' + return result now = _now() controller = supervision._heartbeat(supervision._directory(root)) if not controller.get("fresh"): diff --git a/src/codex_wake/goal_supervision.py b/src/codex_wake/goal_supervision.py index 85e435bd..62e81f4c 100644 --- a/src/codex_wake/goal_supervision.py +++ b/src/codex_wake/goal_supervision.py @@ -170,6 +170,9 @@ def verified_native_caller(context,claimed): def _public(receipt): receipt=dict(receipt) + receipt.setdefault('governor_enabled',True) + if receipt['governor_enabled'] is False: + receipt.update(campaign_state=receipt.get('state'),state='inactive') if type(receipt.get('used_tokens')) is int: receipt['lifetime_used_tokens']=receipt.get('lifetime_tokens_before_window',0)+receipt['used_tokens'] return dict({key:value for key,value in receipt.items() if key not in ('grant','policy_path','prior_allocation')}, @@ -422,6 +425,8 @@ def show(root,policy_path,caller_thread_id,allocation_id=None,*,permission_princ ledger_path=Path(receipt['allocation_path']);_private(ledger_path) with _lock(ledger_path.parent,ledger_path.stem): ledger=json.loads(ledger_path.read_text()) + from .goal_governance import sync + sync(receipt) pending_recovery=ledger.get('pending_recovery') if pending_recovery: receipt.update(state='held',reason='recovery_pending_reconciliation',stop_latched=True) @@ -531,6 +536,11 @@ def enroll(root,policy_path,caller_thread_id,token_limit,time_limit_seconds, directory=_directory(root);path=directory/(name+'.json') # Configured service repair is a distinct common-policy action. Never undo a # mask or infer authority from enrollment permission alone. + from .goal_governance import sync + if path.exists(): + with _lock(directory,name): + _private(path);retained=json.loads(path.read_text()) + if sync(retained):return _public(retained) watcher_setup=None if principal and not _heartbeat(directory).get('fresh'): from .session_permissions import PermissionStore @@ -545,6 +555,7 @@ def enroll(root,policy_path,caller_thread_id,token_limit,time_limit_seconds, old=None if path.exists():_private(path);old=json.loads(path.read_text()) if old: + if sync(old):return _public(old) from .goal_renewal import renewal_hold pending=renewal_hold(root,old) if pending:return dict(_public(old),**pending) @@ -592,6 +603,10 @@ def enroll(root,policy_path,caller_thread_id,token_limit,time_limit_seconds, if first.get('state')!='qualified':raise AccountingUnavailable('first enrollment requires qualified original history',first) if first.get('parent_thread_id'):raise ValueError('successor must select its existing objective allocation') if grant.get('host_delegated'):_allocation_admit(directory,receipt,old) + if sync(receipt): + _record_supervision_notice(path,receipt,'deactivation') + _save(path,receipt) + return _public(receipt) observation=_usage(receipt) # Enrollment is not admitted until the first complete observation is # qualified. A transient setup scan must not publish a stop receipt that @@ -643,6 +658,8 @@ def hold(reason): def _effect(path,receipt,key,method,params): + from .goal_governance import sync + if sync(receipt):return False if method=='turn/steer': from .permission_enforcement import controller_permission permission=controller_permission(path.parent.parent,receipt) @@ -755,6 +772,8 @@ def _record_supervision_notice(path,receipt,kind,controller=None): def _stop(path,receipt,reason,goal=None,active=None): + from .goal_governance import sync + if sync(receipt):return dict(receipt,state='inactive') _stop_episode(path,receipt,reason) _first_stop(path,receipt,reason) receipt.update(reason=reason,state='stop_unverified',stop_latched=True) @@ -846,6 +865,9 @@ def poll(root): with _lock(directory,path.stem): _private(path);receipt=json.loads(path.read_text()) try: + from .goal_governance import sync + if sync(receipt): + results.append(dict(receipt,state='inactive'));continue from .goal_renewal import renewal_hold pending=renewal_hold(root,receipt) if pending: @@ -911,6 +933,8 @@ def _acknowledge_enrollments(directory,controller,observations): path=directory/(_name(observed['thread_id'])+'.json') with _lock(directory,path.stem): _private(path);receipt=json.loads(path.read_text()) + from .goal_governance import sync + if sync(receipt):continue if any(receipt.get(key)!=observed.get(key) for key in ('generation','enrollment_revision','token_limit','deadline','used_tokens')):continue if (observed.get('state') not in ('armed','checkpoint_requested') or diff --git a/src/codex_wake/mcp_server.py b/src/codex_wake/mcp_server.py index a3a61f42..d2169c78 100644 --- a/src/codex_wake/mcp_server.py +++ b/src/codex_wake/mcp_server.py @@ -317,7 +317,7 @@ def call(self, name, arguments, native_context=None): return explain_error(exc),True if verb=='readiness':verb='show' from .session_permissions import PermissionStore - if PermissionStore(goal_root).active() or verb in ('recover','disable'): + if PermissionStore(goal_root).active() or verb in ('recover','disable','enable'): packet=dict(verb=verb,arguments=arguments,native_context=native_context, runner=dict(wake_root=str(self.wake_root) if self.wake_root else None, goal_limits_root=str(goal_root),goal_limits_policy=str(self.goal_limits_policy), @@ -334,7 +334,7 @@ def call(self, name, arguments, native_context=None): value=enroll(goal_root,self.goal_limits_policy,native_context=native_context,**arguments) else: value=show(goal_root,self.goal_limits_policy,arguments['caller_thread_id'],arguments.get('allocation_id')) - return value, verb=='set' and value.get('state') not in ('armed','checkpoint_requested') + return value, verb=='set' and value.get('state') not in ('armed','checkpoint_requested','inactive') except Exception as exc: # Legacy admission keeps its public CLI diagnostics. Delegated # callers must never fall back to an unbound claimed identity. diff --git a/src/codex_wake/permission_enforcement.py b/src/codex_wake/permission_enforcement.py index 4f1ba074..de453cb1 100644 --- a/src/codex_wake/permission_enforcement.py +++ b/src/codex_wake/permission_enforcement.py @@ -56,26 +56,11 @@ def enrollment_action(previous, token_limit, seconds, token_accounting, allocati def diagnostic(root, thread, action, *, principal, task_id=None, operation_id=None): - """Classify unavailable control without writing the existing campaign.""" - from .goal_supervision import _name, _private, _public - decision = authorize(root, action, principal, task_id=task_id, operation_id=operation_id) - path = Path(root) / 'goal-supervision' / (_name(thread) + '.json') - receipt = None - if path.exists(): - _private(path) - receipt = json.loads(path.read_text()) - if receipt.get('thread_id') != thread: - raise PermissionDenied('permission_target_mismatch') + """Compatibility adapter for an authenticated operator disable request.""" if action!='enforcement.disable': raise PermissionDenied('recovery_requires_qualified_command') - result=dict(state='held',armed=False,effect_attempted=False,permission=decision, - capability='unsupported', - readiness='stop_latched' if receipt and receipt.get('stop_latched') else 'not_observed', - reason='enforcement_disable_unsupported', - next_action='Keep existing safety enforcement active; disabling is not implemented by this policy check.') - if receipt is not None: - result['campaign'] = _public(receipt) - return result + from .goal_governance import change + return change(root,thread,enabled=False,principal=principal) def describe(root, thread, *, principal): @@ -84,18 +69,18 @@ def describe(root, thread, *, principal): return None actions = ('enforcement.enroll', 'enforcement.increase', 'enforcement.disable', 'enforcement.recover') return {action: store.evaluate(action, principal, target=thread, - capability='unsupported' if action=='enforcement.disable' else 'supported', + capability='supported', readiness='not_observed') for action in actions} def tool_specs(): from .harness_mcp import CALLER, TEXT result = {} - for verb in ('recover', 'disable'): + for verb in ('recover', 'disable', 'enable'): result['wake_goal_limits_' + verb] = ('goal-limits', verb, ('Recover an observation-fault stop only with explicit approval, repeated exact native observations, ' 'qualified accounting, fresh controller, same boot and original remaining resources. Does not resume the goal or renew limits.' - if verb=='recover' else 'Check explicit disable authority; disabling runtime enforcement remains unsupported.'), + if verb=='recover' else ('Consume only the exact native operator message Reactivate governor.' if verb=='enable' else 'Consume only the exact native operator message Deactivate governor. Models cannot self-authorize. Works without watcher; does not resume or renew.')), dict(caller_thread_id=CALLER, allocation_id=TEXT, task_id=TEXT, assignment_id=TEXT, operation_id=TEXT), ['caller_thread_id'], False) return result @@ -137,9 +122,10 @@ def mcp_call(runner, verb, arguments, native_context): return recover(root,runner.goal_limits_policy,thread,principal=principal, task_id=arguments.get('task_id'),operation_id=arguments.get('operation_id'), allocation_id=arguments.get('allocation_id')) - if verb=='disable': - return diagnostic(root, thread, 'enforcement.' + verb, principal=principal, - task_id=arguments.get('task_id'), operation_id=arguments.get('operation_id')) + if verb in ('disable','enable'): + from .goal_governance import change + return change(root, thread, enabled=verb=='enable', native_context=native_context, + allocation_id=arguments.get('allocation_id')) options = {k: v for k, v in arguments.items() if k not in ('assignment_id',)} return enroll(root, runner.goal_limits_policy, native_context=native_context, permission_principal=principal, **options) @@ -195,7 +181,7 @@ def main(): packet=json.loads(raw) runner=SimpleNamespace(**{k:Path(v) if v is not None else None for k,v in packet['runner'].items()}) value=mcp_call(runner,packet['verb'],packet['arguments'],packet['native_context']) - failed=packet['verb']!='show' and value.get('state') not in ('armed','checkpoint_requested') + failed=packet['verb'] not in ('show','disable','enable') and value.get('state') not in ('armed','checkpoint_requested','inactive') except (ValueError,OSError,KeyError,TypeError,WakeError) as exc: from .goal_readiness import explain_error value=explain_error(exc) diff --git a/tests/test_goal_supervision.py b/tests/test_goal_supervision.py index 0677d300..5455905b 100644 --- a/tests/test_goal_supervision.py +++ b/tests/test_goal_supervision.py @@ -661,9 +661,19 @@ def fixture(self,directory,cached=0): def request(endpoint,executable,method,params,timeout): selected=native if params['threadId']==THREAD else native['siblings'][params['threadId']] selected['calls'].append((method,params)) + if selected.get('request_hook'):selected['request_hook'](method,params) if method=='thread/goal/get':return {'goal':dict(selected['goal'])} - if method=='thread/read':return {'thread':dict(id=params['threadId'],turns=[dict(id=t,status='inProgress') for t in selected['active']])} + if method=='thread/read': + items=selected.get('user_items', [dict(type='userMessage',content=[dict(type='text',text=selected['user_text'])])] if 'user_text' in selected else []) + return {'thread':dict(id=params['threadId'],turns=[dict(id=t,status='inProgress',items=items) for t in selected['active']])} if method=='thread/turns/list':return dict(data=[dict(id=t,status='inProgress') for t in selected['active']],nextCursor=None) + if method.startswith('thread/notice/') and 'notices' in selected: + notice=selected['notices'].get(params['eventId']) + if method=='thread/notice/append': + notice=dict(eventId=params['eventId'],text=params['text']) + selected['notices'][params['eventId']]=notice + return dict(eventId=params['eventId'],recorded=True,modelContextAvailable=True,notificationPublished=True) + return dict(notice=notice,modelContextAvailable=notice is not None) if method.startswith('thread/notice/'): raise NotImplementedError('fixture native runtime has no notice capability') receipts=list((root/'goal-supervision').glob('*.json')) diff --git a/tests/test_governor_deactivation.py b/tests/test_governor_deactivation.py new file mode 100644 index 00000000..8a580953 --- /dev/null +++ b/tests/test_governor_deactivation.py @@ -0,0 +1,373 @@ +"""Operator deactivation through the native public tool boundary.""" + +import json +import subprocess +import sys +import threading +import tempfile +import unittest +from unittest.mock import patch + +import test_goal_supervision as fixtures +from codex_wake.goal_supervision import enroll, show, poll +from codex_wake.mcp_server import CommandRunner + + +class GovernorDeactivationTests(unittest.TestCase): + fixture = fixtures.GoalSupervisionTests.fixture + controller = fixtures.GoalSupervisionTests.controller + + def context(self, turn="operator-control"): + return dict( + threadId=fixtures.THREAD, + sessionId=fixtures.THREAD, + callId="control-call", + **{ + "x-codex-turn-metadata": dict( + thread_id=fixtures.THREAD, session_id=fixtures.THREAD, turn_id=turn + ) + }, + ) + + def test_exact_native_operator_directive_deactivates_without_watcher_or_resume( + self, + ): + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with patch( + "codex_wake.lifecycle_budget.trusted_upper", return_value=fixtures.NOW + ): + with self.controller(root): + enrolled = enroll(root, policy, fixtures.THREAD, 40, 120) + poll(root) + native["active"] = ["operator-control"] + native["user_text"] = "Deactivate governor" + native["notices"] = {} + native["calls"].clear() + result, failed = CommandRunner( + wake_root=root, goal_limits_policy=policy + ).call( + "wake_goal_limits_disable", + dict(caller_thread_id=fixtures.THREAD), + native_context=self.context(), + ) + self.assertFalse(failed, result) + self.assertFalse(result["governor_enabled"]) + self.assertEqual(result["notices"][0]["state"], "recorded") + text = next(iter(native["notices"].values()))["text"] + self.assertIn("Governor inactive", text) + self.assertLessEqual(len(text.encode()), 768) + self.assertIn("revision 1", text) + from codex_wake.goal_notices import list_notices, present_notice + + for event in list_notices(root, fixtures.THREAD): + if event["identity"]["kind"] in ("activation", "stop"): + old = present_notice(root, event["event_id"]) + self.assertIn("Historical", old["text"]) + self.assertIn("inactive", old["text"]) + self.assertLessEqual(len(old["text"].encode()), 768) + status = show(root, policy, fixtures.THREAD) + self.assertEqual(status["governor"]["mode"], "inactive") + self.assertFalse(status["controller"]["fresh"]) + self.assertEqual(status["used_tokens"], enrolled["used_tokens"]) + self.assertEqual(status["deadline"], enrolled["deadline"]) + self.assertTrue(status["stop_latched"]) + self.assertEqual(native["goal"]["status"], "paused") + poll(root) + self.assertEqual(native["active"], ["operator-control"]) + self.assertFalse( + any( + m + in ( + "thread/goal/set", + "turn/interrupt", + "turn/steer", + "turn/start", + ) + for m, _ in native["calls"] + ) + ) + + def test_models_cannot_forge_operator_text_or_replay_a_different_turn(self): + for scenario in ( + "missing", + "quoted", + "substring", + "tool-output", + "stale-turn", + "forged-argument", + ): + with ( + self.subTest(scenario=scenario), + tempfile.TemporaryDirectory() as directory, + ): + root, policy, _, native, _ = self.fixture(directory) + with ( + self.controller(root), + patch( + "codex_wake.lifecycle_budget.trusted_upper", + return_value=fixtures.NOW, + ), + ): + enroll(root, policy, fixtures.THREAD, 100, 120) + native["active"] = ["operator-control"] + if scenario != "missing": + native["user_text"] = "Deactivate governor" + if scenario == "quoted": + native["user_text"] = '"Deactivate governor"' + if scenario == "substring": + native["user_text"] = "Please explain Deactivate governor" + if scenario == "tool-output": + native["user_items"] = [ + dict( + type="mcpToolCall", + content=[dict(type="text", text="Deactivate governor")], + ) + ] + args = dict(caller_thread_id=fixtures.THREAD) + if scenario == "forged-argument": + args["operator"] = True + result, failed = CommandRunner( + wake_root=root, goal_limits_policy=policy + ).call( + "wake_goal_limits_disable", + args, + native_context=self.context( + "past" if scenario == "stale-turn" else "operator-control" + ), + ) + self.assertTrue(failed, result) + self.assertTrue( + show(root, policy, fixtures.THREAD)["governor_enabled"] + ) + + def test_direct_operator_api_and_restart_retain_choice_without_native_or_watcher( + self, + ): + from codex_wake.goal_governance import change, snapshot + from codex_wake.session_permissions import Principal + + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with patch( + "codex_wake.lifecycle_budget.trusted_upper", return_value=fixtures.NOW + ): + with self.controller(root): + enroll(root, policy, fixtures.THREAD, 100, 120) + first = change( + root, + fixtures.THREAD, + enabled=False, + principal=Principal(fixtures.THREAD, "operator"), + ) + original = snapshot(root, fixtures.THREAD) + self.assertFalse(original["enabled"]) + self.assertEqual(original["mode"], "inactive") + self.assertEqual(original["scope"], "allocation-1") + repeated = change( + root, + fixtures.THREAD, + enabled=False, + principal=Principal(fixtures.THREAD, "operator"), + ) + self.assertFalse(repeated["changed"]) + self.assertEqual( + repeated["governor_revision"], first["governor_revision"] + ) + code = "from codex_wake.goal_governance import snapshot; import json,sys; print(json.dumps(snapshot(sys.argv[1],sys.argv[2])))" + restarted = json.loads( + subprocess.check_output( + [sys.executable, "-c", code, str(root), fixtures.THREAD], + text=True, + ) + ) + self.assertFalse(restarted["enabled"]) + native["calls"].clear() + again = enroll(root, policy, fixtures.THREAD, 100, 120) + self.assertFalse(again["governor_enabled"]) + self.assertEqual(native["calls"], []) + native["active"] = ["operator-control"] + native["user_text"] = "Reactivate governor" + native["notices"] = {} + enabled, failed = CommandRunner( + wake_root=root, goal_limits_policy=policy + ).call( + "wake_goal_limits_enable", + dict(caller_thread_id=fixtures.THREAD), + native_context=self.context(), + ) + self.assertFalse(failed, enabled) + self.assertTrue(enabled["governor_enabled"]) + self.assertEqual( + enabled["governor_revision"], first["governor_revision"] + 1 + ) + self.assertFalse(enabled["resumed"]) + self.assertEqual(enabled["used_tokens"], first["used_tokens"]) + self.assertIn( + "Governor reactivated", + next(iter(native["notices"].values()))["text"], + ) + self.assertLessEqual( + len(next(iter(native["notices"].values()))["text"].encode()), 768 + ) + + def test_shared_allocation_inherits_to_new_members_and_peer_stays_enabled(self): + from codex_wake.goal_governance import change, snapshot + from codex_wake.session_permissions import Principal + + with tempfile.TemporaryDirectory() as directory: + root, policy, log, native, _ = self.fixture(directory) + template = json.loads(policy.read_text())["callers"][fixtures.THREAD] + template.pop("allocation_id") + policy.write_text( + json.dumps( + dict( + version=2, + native_caller_binding=True, + allocations={}, + self_enrollment=template, + ) + ) + ) + child = "00000000-0000-7000-8000-000000000290" + peer = "00000000-0000-7000-8000-000000000291" + later = "00000000-0000-7000-8000-000000000292" + native["siblings"] = {} + for member in (child, peer, later): + rows = json.loads( + json.dumps( + [json.loads(line) for line in log.read_text().splitlines()] + ).replace(fixtures.THREAD, member) + ) + (log.parent / (member + ".jsonl")).write_text( + "".join(json.dumps(row) + "\n" for row in rows) + ) + native["siblings"][member] = dict( + goal=dict(threadId=member, createdAt=fixtures.NOW, status="active"), + active=["work"], + calls=[], + ) + + def context(member): + return dict( + threadId=member, + sessionId=member, + callId="member-call", + **{ + "x-codex-turn-metadata": dict( + thread_id=member, session_id=member, turn_id="work" + ) + }, + ) + + with ( + self.controller(root), + patch( + "codex_wake.lifecycle_budget.trusted_upper", + return_value=fixtures.NOW, + ), + ): + first = enroll( + root, + policy, + fixtures.THREAD, + 100, + 120, + native_context=context(fixtures.THREAD), + ) + enroll( + root, + policy, + child, + 100, + 120, + allocation_id=first["allocation_id"], + native_context=context(child), + ) + enroll(root, policy, peer, 100, 120, native_context=context(peer)) + change( + root, + fixtures.THREAD, + enabled=False, + principal=Principal(fixtures.THREAD, "operator"), + ) + self.assertFalse( + show(root, policy, child, first["allocation_id"])[ + "governor_enabled" + ] + ) + added = enroll( + root, + policy, + later, + 100, + 120, + allocation_id=first["allocation_id"], + native_context=context(later), + ) + self.assertFalse(added["governor_enabled"]) + self.assertFalse(snapshot(root, later)["enabled"]) + self.assertTrue(snapshot(root, peer)["enabled"]) + self.assertEqual(added["deadline"], first["deadline"]) + native["siblings"][child]["calls"].clear() + native["siblings"][later]["calls"].clear() + poll(root) + self.assertEqual(native["siblings"][child]["calls"], []) + self.assertEqual(native["siblings"][later]["calls"], []) + + def test_inflight_poll_precedes_deactivation_and_queued_polls_cannot_intervene( + self, + ): + from concurrent.futures import ThreadPoolExecutor + from codex_wake.goal_governance import change, snapshot + from codex_wake.session_permissions import Principal + + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with patch( + "codex_wake.lifecycle_budget.trusted_upper", return_value=fixtures.NOW + ): + with self.controller(root): + enroll(root, policy, fixtures.THREAD, 40, 120) + observed, release, started = ( + threading.Event(), + threading.Event(), + threading.Event(), + ) + self.addCleanup(release.set) + + def gate(method, params): + if method == "thread/goal/get" and not observed.is_set(): + observed.set() + if not release.wait(3): + raise TimeoutError("test gate did not release") + + native["request_hook"] = gate + + def disable(): + started.set() + return change( + root, + fixtures.THREAD, + enabled=False, + principal=Principal(fixtures.THREAD, "operator"), + ) + + with ThreadPoolExecutor(max_workers=3) as pool: + inflight = pool.submit(poll, root) + self.assertTrue(observed.wait(3)) + control = pool.submit(disable) + self.assertTrue(started.wait(3)) + self.assertTrue(snapshot(root, fixtures.THREAD)["enabled"]) + queued = pool.submit(poll, root) + release.set() + inflight.result(timeout=5) + disabled = control.result(timeout=5) + queued.result(timeout=5) + self.assertFalse(disabled["governor_enabled"]) + # A later native resume or stale work turn is not fresh governor authority. + native["goal"]["status"] = "active" + native["active"] = ["work"] + native["calls"].clear() + self.assertEqual(poll(root)[0]["state"], "inactive") + self.assertEqual(native["calls"], []) From c34592bfc27c40a890d624df6dfecb5ffe984d95 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:03:40 -0500 Subject: [PATCH 06/20] fix: carry operator governor choice through bounded dispatch (#352, #355) --- docs/dev/evidence/issue0355/acceptance.md | 2 ++ .../plan0175/352-355-control-integration.md | 11 ++++++++++ scripts/goal_notice_native_acceptance.py | 13 +++++++++++ src/codex_wake/agent_lifecycle.py | 11 ++++++++-- src/codex_wake/dispatch_packet.py | 8 ++++--- tests/test_agent_spawn.py | 22 ++++++++++++++++++- 6 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 docs/dev/evidence/plan0175/352-355-control-integration.md diff --git a/docs/dev/evidence/issue0355/acceptance.md b/docs/dev/evidence/issue0355/acceptance.md index c43150b6..9d35111c 100644 --- a/docs/dev/evidence/issue0355/acceptance.md +++ b/docs/dev/evidence/issue0355/acceptance.md @@ -108,3 +108,5 @@ those new fields. Existing admitted intents retain their prior format. Memory disposition: **forbidden**. This delegated lane is explicitly confined to its worktree; user-scoped memory writes and coordinator closeout are outside its authorized write surface. + +Primary integration supersedes the unknown-snapshot gap above: host-side read-only governance snapshots now carry available enabled/mode/scope/revision into first dispatch; admitted bytes remain immutable on retries. See `docs/dev/evidence/plan0175/352-355-control-integration.md`. Unknown governor availability alone is not a work-authority gate. diff --git a/docs/dev/evidence/plan0175/352-355-control-integration.md b/docs/dev/evidence/plan0175/352-355-control-integration.md new file mode 100644 index 00000000..f2b41b7b --- /dev/null +++ b/docs/dev/evidence/plan0175/352-355-control-integration.md @@ -0,0 +1,11 @@ +# #352 / #355 control integration acceptance + +#352 source: 7f81f0d (worker2448156). Worker public seam red: `test_governor_deactivation` failed with enforcement_disable_unsupported. Worker green: five public control tests, plus impacted focused selection21PASS and control/permission/renewal/recovery selection71PASS (see worker report). Primary focused selection23PASS before dispatch inheritance case; final combined selection recorded below. + +Expanded native fixture acceptance: `PYTHONPATH=src /bin/python scripts/goal_notice_native_acceptance.py --app-server --frontend --output /tmp/governor352-native`. PASS: deactivation rendered in actual TUI, retained in model context, durable inactive snapshot, no goal resume or budget renewal. Activation/stop/reconnect/peer/lost-ack checks retained. Zero provider inference; owned process census empty. Native SHA25686fad00e38f0c26a5c8d2eb2dbe34536c74655fb2fb45b4efc5d94e97e683b1f. This exercises authenticated host control; exact conversational user-message authentication is separately covered by public CommandRunner + deterministic native transport tests. Conversational control needs a connected model/native endpoint; independent authenticated host control does not. + +#355 propagation regression: run new public spawn inheritance case against worker355source (`PYTHONPATH=/src:tests /bin/python -m unittest test_agent_spawn.AgentSpawnTests.test_deactivated_governor_choice_is_carried_without_reactivation_on_retry -q`) FAIL: packet state unknown instead of inactive. Primary source green: same case PASS (0.772s). Host-selected source snapshot carries disabled scope/revision; admitted assignment bytes remain unchanged across retry even if subsequent operator state changes. Fresh effective state remains authoritative at controller effect boundaries; packet is explicitly a snapshot, not fresh authority or renewed allowance. Missing governor availability does not revoke existing work authority. + +Remaining current-host/provider qualification is reported rather than inferred. No production service/configuration/mask/allocation changes. This does not prove hard-cap admission (#354). + +Final combined primary selection: `PYTHONPATH=src:tests /bin/python -W ignore::DeprecationWarning -m unittest test_governor_deactivation test_governor_visibility test_goal_notices test_enforcement_permissions test_agent_spawn.AgentSpawnTests.test_deactivated_governor_choice_is_carried_without_reactivation_on_retry test_agent_spawn.AgentSpawnTests.test_spawn_executes_exact_assignment_once_across_pending_observation test_agent_spawn.AgentSpawnTests.test_fresh_worker_uses_bounded_brief_and_summary_without_parent_history -q` —24PASS,11.172s. Repo-wide pinned Ruff F821/F822/F823 PASS. diff --git a/scripts/goal_notice_native_acceptance.py b/scripts/goal_notice_native_acceptance.py index e1388443..52649cd9 100644 --- a/scripts/goal_notice_native_acceptance.py +++ b/scripts/goal_notice_native_acceptance.py @@ -137,6 +137,19 @@ def reconcile(self, event): return native_adapter.reconcile(event) stop_frame = wait_for(lambda: frame if notice['event_id'] in (frame := capture()) else None) (output/'governor-stop.txt').write_text(stop_frame) assert 'Deactivate governor' in stop_frame + from codex_wake.goal_governance import change, snapshot + from codex_wake.session_permissions import Principal + disabled = change(wake_root, thread_id, enabled=False, principal=Principal(thread_id, 'operator')) + assert disabled['governor']['mode'] == 'inactive', disabled + assert disabled['resumed'] is False and disabled['renewed_resources'] is False + disabled_event = disabled['notice_ids']['deactivation'] + disabled_frame = wait_for(lambda: frame if disabled_event in (frame := capture()) else None) + (output/'deactivated.txt').write_text(disabled_frame) + assert snapshot(wake_root, thread_id)['enabled'] is False + assert rpc('thread/notice/read', threadId=thread_id, eventId=disabled_event)['modelContextAvailable'] is True + assert rpc('thread/goal/get', threadId=thread_id)['goal']['status'] == 'paused' + report.update(deactivation_rendered=True, deactivation_model_context=True, + deactivation_retained=True, deactivation_resumed_nothing=True) duplicate = rpc('thread/notice/append' ,threadId=thread_id,eventId=event_id,text=text) assert duplicate['alreadyRecorded'] is True and duplicate['notificationPublished'] is False, duplicate retained = rpc('thread/notice/read',threadId=thread_id,eventId=event_id) diff --git a/src/codex_wake/agent_lifecycle.py b/src/codex_wake/agent_lifecycle.py index 962a0889..4510d5b7 100644 --- a/src/codex_wake/agent_lifecycle.py +++ b/src/codex_wake/agent_lifecycle.py @@ -104,11 +104,17 @@ def spawn_command(args, root): key = hashlib.sha256(args.intent.encode()).hexdigest() path = journal / (key + '.json') previous = json.loads(path.read_text()) if path.exists() else None + request_fingerprint = hashlib.sha256(json.dumps(intent, sort_keys=True).encode()).hexdigest() legacy_fingerprint = hashlib.sha256(json.dumps(legacy_intent, sort_keys=True).encode()).hexdigest() - if previous and 'dispatch' not in previous and previous['fingerprint'] == legacy_fingerprint: + if previous and previous.get('request_fingerprint') == request_fingerprint: + # A status change or renderer upgrade cannot rewrite an admitted task. + intent, dispatch = previous['intent'], previous.get('dispatch') + elif previous and 'dispatch' not in previous and previous['fingerprint'] == legacy_fingerprint: intent, dispatch = legacy_intent, None else: from .dispatch_packet import render_assignment + from .goal_governance import snapshot + intent['governance_snapshot'] = snapshot(root, intent.get('source_thread_id')) intent['task'], dispatch = render_assignment(intent, root=root, brief=approved, pin=pin, admission=admission, receipt_path=path) fingerprint = hashlib.sha256(json.dumps(intent, sort_keys=True).encode()).hexdigest() @@ -134,7 +140,8 @@ def spawn_command(args, root): raise WakeError('intent key already binds a different lifecycle assignment') else: receipt = dict(version=1, intent_key=args.intent, fingerprint=fingerprint, - intent=intent, state='prepared', thread_id=None, dispatch=dispatch) + intent=intent, state='prepared', thread_id=None, dispatch=dispatch, + request_fingerprint=request_fingerprint) if permission_receipt: receipt['permission_context']=permission_receipt['permission_context'] if not binding:receipt['budget_mode']='explicit_expiry_only_legacy_profile' diff --git a/src/codex_wake/dispatch_packet.py b/src/codex_wake/dispatch_packet.py index a95b7e07..752fa3da 100644 --- a/src/codex_wake/dispatch_packet.py +++ b/src/codex_wake/dispatch_packet.py @@ -37,6 +37,7 @@ def render_assignment(intent, *, root, brief=None, pin=None, admission=None, rec if configured: allowance['limits']['session_token_limit'] = policy.get( 'spawn_token_limit', configuration['session_token_limit']) + control = intent.get('governance_snapshot') or {} packet = dict(version=1, context_mode='compact', objective=brief.get('objective', intent['task']), instructions=intent['task'], authority=authority, @@ -51,10 +52,11 @@ def render_assignment(intent, *, root, brief=None, pin=None, admission=None, rec stop_conditions=options.get('stop_conditions', []) + [ 'Stop at the original expiry or earlier authorized resource limit.', 'Return partial evidence when blocked; do not renew allowance or expand scope.', - 'Reconcile current governance and allowance before work; unknown authority holds work.'], + 'Honor the existing authorized allowance and operator choices; unknown governor availability alone does not revoke work authority.'], governance=dict(budget_mode=mode, configuration_revision=configuration.get('revision'), - state='unknown', - required_action='Inspect current governance through the authorized public operator interface before work.', + state=control.get('mode', 'unknown'), snapshot=control, + enabled=control.get('enabled'), + required_action='Use available public governance status; do not insert a governor or block already-authorized repair merely because its status is unavailable.', lifecycle_root=str(root), receipt_path=str(receipt_path), source='Host launch configuration and durable lifecycle/objective receipts; packet is a snapshot.', controls='Existing controls and operator choices remain authoritative; this packet changes none.'), diff --git a/tests/test_agent_spawn.py b/tests/test_agent_spawn.py index f58be529..94086429 100644 --- a/tests/test_agent_spawn.py +++ b/tests/test_agent_spawn.py @@ -209,6 +209,9 @@ def test_fixed_expiry_survives_startup_and_prevents_late_task_admission(self): def test_reconciled_completion_keeps_original_uncertain_effect_history(self): self.spawn_scenario(loss='queue/add', check_history=True) + def test_deactivated_governor_choice_is_carried_without_reactivation_on_retry(self): + self.spawn_scenario(governor_disabled=True) + def test_host_profile_uses_exact_isolated_socket_and_codex_home(self): self.spawn_scenario(isolated_profile=True) @@ -233,7 +236,7 @@ def test_shared_profile_resource_checks_do_not_override_spawn_launch(self): self.spawn_scenario(resource_profile=True) def spawn_scenario(self, *, loss=None, check_environment=False, expire_at_task=False, - check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False,brief_dispatch=False,full_history=False): + check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False,brief_dispatch=False,full_history=False,governor_disabled=False): thread_id = '00000000-0000-7000-8000-000000000157' starts, tasks, windows = [], [], [] turn_starts = [] @@ -322,6 +325,13 @@ def handler(ws): (root / 'codex-home').mkdir() profile.update(tmux_socket=str(root / 'tmux.sock'), codex_home=str(root / 'codex-home')) registry={'version': 1, 'profiles': {'worker': profile}} + source_thread='00000000-0000-7000-8000-000000000158' + if governor_disabled: + from codex_wake.goal_supervision import _directory, _name, _save + _save(_directory(root)/(_name(source_thread)+'.json'), dict( + thread_id=source_thread,allocation_id='operator-disabled-scope', + governor_enabled=False,governor_revision=3)) + registry['callers']={source_thread:['worker']} if budget: home=root/'home';(home/'sessions').mkdir(parents=True) profile['codex_home']=str(home);rollout=home/'sessions/fixture.jsonl' @@ -387,6 +397,7 @@ def command(argv, **kwargs): '--profile', 'worker', '--intent', 'one-worker', '--name', 'worker', '--task', 'Calculate 6 * 7 and report 42.', '--expires-at', '2030-01-01T00:00:00Z', '--json'] + if governor_disabled:argv.extend(['--caller-thread-id',source_thread]) if budget:argv.extend(['--objective-ref','bootstrap']) if brief_dispatch: argv.extend(['--task-id','multiply','--assignment-id','multiply-one']) @@ -494,6 +505,11 @@ def clock(): self.assertEqual(actor['used'],70) self.assertEqual(len(tasks),1) return + if governor_disabled: + # A later operator change cannot rewrite an already admitted assignment. + _save(_directory(root)/(_name(source_thread)+'.json'), dict( + thread_id=source_thread,allocation_id='operator-disabled-scope', + governor_enabled=True,governor_revision=4)) for _ in range(2): output = StringIO() with redirect_stdout(output): @@ -519,6 +535,10 @@ def clock(): self.assertEqual(len(turn_starts), 0 if auto_start else 1) packet = json.loads(tasks[0]['input'][0]['text'].split('BOUNDED_ASSIGNMENT=', 1)[1]) self.assertEqual(packet['objective'], 'Multiply the evidence operands' if brief_dispatch else 'Calculate 6 * 7 and report 42.') + if governor_disabled: + self.assertEqual(packet['governance']['state'], 'inactive') + self.assertIs(packet['governance']['enabled'], False) + self.assertEqual(packet['governance']['snapshot']['revision'], 3) self.assertEqual(packet['instructions'], 'Calculate 6 * 7 and report 42.') self.assertEqual(packet['context_mode'], 'full_history' if full_history else 'compact') self.assertEqual(packet['scope']['workspace'], tmp) From 2d1914bf3f3877f08f3e2692b5fb628111e399d4 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:08:31 -0500 Subject: [PATCH 07/20] docs: expose governor controls and record verified delivery (#352, #355) --- README.md | 8 ++++++++ ...175-2026-10-25-transparent-governor-delivery.md | 8 ++++++++ .../codex-wake/references/existing-goal-limits.md | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/README.md b/README.md index 0d4b6fc5..9b44c09d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,14 @@ provides MCP enrollment and an independent watcher: request checkpoint, then pause the current native goal and interrupt its exact active turn. Limits persist across replacement goals until an explicit user reset. Start with the [existing-session workflow](skills/codex-wake/references/existing-goal-limits.md). +Governor activation and interventions must be explicit to both the operator and +model. In builds with the new governor controls, send **Deactivate governor** in +the selected conversation to disable its scope without another confirmation; +**Reactivate governor** explicitly restores it. Deactivation survives continuation, +delegation and restart, preserves accounting, and resumes nothing. See +[governor controls and offline host access](docs/governor-control.md) for capability +requirements; these source changes do not upgrade an installed runtime. + Each new conversation needs its own approved host grant; reusing a tab or CWD does not transfer a previous conversation's resource authority. Host setup and qualified accounting are required; polling and publication make diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index 6f13a153..fcfcc30c 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -34,3 +34,11 @@ No release, service unmask, production session dispatch, automatic resource rene #351 CLOSED at6e11c34 (13 focused tests and real pinned-native/stock-TUI acceptance, empty owned process census). #356 CLOSED at6515311 (worker81c4610, five public runner subprocess regressions independently rerun). Remote integration readback6e11c34. Draft PR: https://github.com/CochranResearchGroup/codex-wake/pull/358. #352 assigned to agent validation356 in isolated feat/governor-disable-352 based6e11c34. #355 remains with dispatch355. #353/#354/#357 remain dependency-blocked; no implementation closure claimed. Final review/remediation counters remain0/0. + +## Checkpoint: control and dispatch closure + +#351/#352/#355/#356 CLOSED. Integrationc34592b includes immutable compact dispatch snapshots and durable operator deactivation. #352 real pinned-native/stock-TUI deactivation acceptance PASS with no provider turns and empty owned process census; combined focused selection24PASS. #353 assigned dispatch355 on feat/governor-preflight-353, basec34592b. #354/#357 remain dependency-blocked. Final review/remediation counters0/0. + +Bounded #354 source discovery (not implementation): native prototype b0ce988 client sends HTTP and WebSocket without Wake admission; request shape lacks max_output_tokens. Existing custom provider fixture explicitly supports HTTP-only configuration. A request reservation helper alone cannot prove hard-cap enforcement. Any chosen adapter must establish complete selected-path coverage and enforceable input/output bounds, preserve unsupported-host truth, and qualify against an actual native request path. Do not fabricate an input-byte/token bound or install an unqualified native binary to close a ticket. + +The operator's implementation request implicitly accepts using existing public operator/agent journeys as testing seams. Source behavior and isolated pinned-host acceptance remain separate from installation/deployment. diff --git a/skills/codex-wake/references/existing-goal-limits.md b/skills/codex-wake/references/existing-goal-limits.md index f645f2d2..11cf307e 100644 --- a/skills/codex-wake/references/existing-goal-limits.md +++ b/skills/codex-wake/references/existing-goal-limits.md @@ -5,6 +5,20 @@ moving to another tab or agent. Installation, host delegation and a running cont are required. Host policy version2 lets native callers self-enroll without per-session UUID grants. Check the installed version and controller receipt before claiming enforcement. +## Visible operator control + +Announce governor activation, scope, actual enforcement mode and interventions to +both the model and operator. Never insert governance silently or treat a recorded +event as proof that either audience received it. New control-capable builds accept +the operator's whole conversation message **Deactivate governor** through the +verified native adapter, without repeated confirmation. Honor the resulting +inactive state across retries, delegates and restart; it neither resumes work nor +renews resources. **Reactivate governor** is the explicit inverse. A missing or +deactivated governor is not a reason to block already-authorized manual repair. +See [governor controls](../../../docs/governor-control.md) for offline authenticated +host access and notice-delivery limitations. Verify installed capabilities before +claiming these source features are present. + ## Agent workflow When the user supplies a token or time allowance, load this guide before heavy From 6118f1343fc88a5954611f5dd4939ec45b4525c4 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:22:52 -0500 Subject: [PATCH 08/20] feat(governance): preflight host dispatch and retain manual repair authority (#353) --- GLOSSARY.md | 5 + docs/agent-lifecycle.md | 68 ++++++++ docs/dev/evidence/issue0353/acceptance.md | 128 +++++++++++++++ src/codex_wake/agent_lifecycle.py | 34 +++- src/codex_wake/dispatch_packet.py | 3 +- src/codex_wake/goal_host_preflight.py | 188 ++++++++++++++++++++++ tests/test_agent_spawn.py | 55 ++++++- tests/test_goal_supervision.py | 2 +- tests/test_host_preflight.py | 105 ++++++++++++ 9 files changed, 578 insertions(+), 10 deletions(-) create mode 100644 docs/dev/evidence/issue0353/acceptance.md create mode 100644 src/codex_wake/goal_host_preflight.py create mode 100644 tests/test_host_preflight.py diff --git a/GLOSSARY.md b/GLOSSARY.md index 35b02e2c..15579341 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -253,3 +253,8 @@ The operator-visible controller that applies authorized resource limits to a sel **Governor deactivation**: The operator's durable choice to stop governor interventions in a selected scope. It preserves accounting and work, does not renew resources or resume stopped work, and remains in effect until explicitly changed. + +**Host compatibility preflight**: +A bounded read-only check before opted-in dispatch, separating reusable static +host observations from fresh accounting, controller and operator-control state. +It reports the actual enforcement mode without granting work or resource authority. diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index 61545010..6056b9bb 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -374,3 +374,71 @@ separates paid native candidate runs, public fixture coverage and exact released installation checks. The [0.15.1 installed closeout](dev/evidence/plan0162/final-acceptance.md) records later tab targeting and installation qualification. These receipts do not configure profiles or grant authority on another host. + +## Host compatibility before delegated dispatch + +An operator can opt a spawn profile into a read-only compatibility preflight. +Add this field to that existing approved launch profile; it does not change the +strict private goal-policy grant schema or introduce caller-selected authority: + +```json +{ + "governance_preflight": { + "policy": "/owned/goal-policy.json", + "requested_mode": "sampled_stop", + "manual_authorization": "existing approved repair ticket or authorization locator" + } +} +``` + +`manual_authorization` is optional. It records already authorized manual work, +not a request for another approval. `thread_id` can name the host-selected source +for an operator CLI launch; a caller-bound launch must match its exact caller. +The goal grant must select the same endpoint, client path and Codex home as the +launch profile. Existing profiles without this field retain their behavior; +preflight neither enrolls them nor inserts a governor. + +Public spawn returns `compatibility_preflight` before thread, tab or task effects +when a required capability is unavailable. It also rechecks at effect boundaries. +The worker's compact packet carries the initial mode/notice and a receipt locator; +retry retains its exact input, while current dynamic preflight results are fresh. + +- `hard_admission` is a requested guarantee, currently **unsupported**. The result + is `degraded`, `admitted:false`, with actionable supported routes; it does not + silently use sampled stopping or manual fallback. No native per-request hook + or enforceable output ceiling is claimed, even when a ledger exists. +- `sampled_stop` requires current qualified accounting, a fresh resident first + poll, existing control readback and observed notice-read/model-delivery proof. + It has no exact token overshoot bound. Read-only preflight does not attest that + the operator UI rendered a notice; that remains an independent qualification. +- `manual` requires the retained host authorization locator. It can also be an + explicitly preauthorized fallback when sampled requirements are unavailable. + It carries an explicit notice and may bypass the resident *lifecycle watcher* + prerequisite for that repair assignment. It still checks and settles existing + budget reservations, original deadlines, permissions and strict executor gates. + It never starts/unmasks a service, clears a stop, changes counters, enrolls or + resumes the original goal, or renews an exhausted campaign. +- `inactive` requires an explicit disabled control snapshot for the exact selected + allocation. Unavailable/unknown governance is not deactivation or revocation. + Independent lifecycle/strict-executor controls remain unchanged. +- `degraded` identifies missing requested supervision, accounting, notice or + control proof without granting authority or blocking unrelated ordinary work. + +Static observations are private receipts beneath +`WAKE_ROOT/goal-supervision/host-preflight/`. Reuse is keyed by the selected source +file hashes, client executable hash, connected Unix peer process generation and +executable hash, native metadata, exact profile/policy hashes and caller/workspace +identity. A connected peer executable is distinct from the selected client; +Python/wrapper/native binary differences and unqualified request-path coverage +remain explicit. The cache cannot promote a native admission guarantee. + +Every attempt rechecks those bindings and performs fresh public `diagnose`/`show` +and operator-control reads. Controller heartbeat, accounting, usage, stop state +and current operator choices are not cached. Only the static notice-read capability probe is +reused; current notice/model-context evidence is read again. Unknown probe +outcomes are not qualified cache entries. Static probes use the +smaller selected timeout (at most five seconds); diagnostic/native-notice reads +retain the host grant timeout (also at most five seconds), and accounting scans +retain their owner's bounds. File hashing is capped at 256 MiB per executable +and 1 MiB per selected source/policy file. An unavailable static observation is +reported rather than turned into installed capability proof. diff --git a/docs/dev/evidence/issue0353/acceptance.md b/docs/dev/evidence/issue0353/acceptance.md new file mode 100644 index 00000000..b0384298 --- /dev/null +++ b/docs/dev/evidence/issue0353/acceptance.md @@ -0,0 +1,128 @@ +# Issue #353: early host preflight and authorized repair fallback + +Work item: . +Parent specification: , +stories 4, 16, 17, 20 and 22. +Source baseline: `c34592bfc27c40a890d624df6dfecb5ffe984d95`. +The coordinator owns the parent plan, integration and external issue disposition. + +## Public behavior and boundaries + +The existing public `sessions spawn` path accepts an optional host launch-profile +`governance_preflight`, selecting an existing private goal grant, requested mode +and optional retained manual authorization. No model-facing authority selector or +new approval exchange was added. Legacy profiles remain feature-off. + +Preflight precedes thread/tab/queue effects and rechecks opted-in effects. It +reads actual selected endpoint metadata/Unix peer PID, process generation and +executable, separately hashes the selected client and source/configuration, and +performs fresh public diagnosis, status, control and native notice/model-context +readback. It never enrolls a goal or changes service, resource or control state. + +An unsupported explicit `hard_admission` request returns one actionable held +result, with no silent downgrade or thread/tab/task effects. Native request-path +admission remains unsupported; successful ledger operations or matching client +and server executables do not establish a hard token ceiling. + +The deterministic public journey proves `sampled_stop` only after qualified +accounting, actual resident first-poll acknowledgement, current controls and +fresh notice/model-context readback. Operator UI rendering remains independently +unverified by this read-only seam. Sampled mode has no exact overshoot bound. + +Retained manual authorization carries an explicit manual-mode notice and permits +a repair assignment without the resident lifecycle watcher. Existing independent +budget reservations are still charged and settled (the fixture uses 70 tokens), +with original deadline and all permission/strict executor gates retained. No +watcher is started or unmasked; no goal is resumed, stop cleared, counter reset, +resource renewal or campaign renewal occurs. An explicit inactive choice applies +only to its exact selected allocation. Unknown control state is not deactivation +or revocation. Inactive control does not bypass independent lifecycle supervision. + +Static receipt reuse is separate from fresh eligibility: source/client/connected +server/configuration/caller bindings are rechecked every attempt. Only static +notice-read capability qualification is reused. Accounting, remaining allowance, +controller heartbeat, operator control and actual notice/model-context readback +are not cached. Unknown native probe outcomes do not become qualified cache +entries. The first assignment's compact mode snapshot is retained on retry; +current results remain available as `compatibility_preflight`. + +## Red/green and focused validation + +Tests use CPython 3.12.13, `PYTHONPATH=src:tests`, the existing private Unix +WebSocket/tmux/client fixtures, controlled clocks, original response-usage files +and an actual isolated resident watcher process. No provider inference, +production dispatch, installation or existing service change was authorized. + +Interpreter prefix for commands below: + +```sh +PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python \ + -W ignore::DeprecationWarning -m unittest +``` + +1. `tests.test_agent_spawn.AgentSpawnTests.test_hard_admission_preflight_refuses_before_thread_tab_or_task_effects -q` + initially failed because the public profile rejected the new optional host + field. After wiring opt-in preflight, **1 passed, 0.670 s**. Assertions prove + zero thread creation, tab creation and task submission. +2. `tests.test_agent_spawn.AgentSpawnTests.test_authorized_manual_repair_spawns_without_a_resident_watcher_or_renewal -q` + initially failed with `resident_objective_supervisor_required` despite the + retained manual authority. After the bounded manual fallback, **1 passed, + 1.714 s**, including original budget settlement and no watcher artifact. +3. The public sampled/cache fixture initially returned `degraded` correctly: + enrollment alone lacked a resident first-poll acknowledgement. The test now + waits at most six seconds for actual public engagement, rather than weakening + that qualification. Its next run passed **1 test, 6.805 s**. + +Affected regression selection: + +```sh +PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python \ + -W ignore::DeprecationWarning -m unittest \ + tests.test_agent_spawn tests.test_host_preflight tests.test_task_brief_lifecycle -q +``` + +**40 tests passed, 37.430 s**, exit 0. This is an affected source selection, not the +full repository suite. Subsequent bounded review tightened fresh notice readback, +remaining-allowance checks, newest control readback and explicit peer PID binding; +those surfaces were then rerun through the final five-case selection below. + +Final acceptance selection: + +```sh +PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python \ + -W ignore::DeprecationWarning -m unittest \ + tests.test_host_preflight.HostPreflightTests \ + tests.test_agent_spawn.AgentSpawnTests.test_hard_admission_preflight_refuses_before_thread_tab_or_task_effects \ + tests.test_agent_spawn.AgentSpawnTests.test_authorized_manual_repair_spawns_without_a_resident_watcher_or_renewal \ + tests.test_agent_spawn.AgentSpawnTests.test_inactive_preflight_honors_operator_choice_without_manual_permission \ + tests.test_agent_spawn.AgentSpawnTests.test_sampled_preflight_reports_degraded_before_costly_dispatch -q +``` + +**5 tests passed, 10.744 s**, exit 0. The matrix proves static +reuse, client binary drift, server metadata drift, profile drift, separate peer +PID/client identities, and fresh degradation after either loss of current native +notice/model delivery or death of the resident controller. It asserts no native +thread/queue/goal/interrupt effect during the compatibility matrix. + +New files pass Ruff 0.14.2 without exclusions. Affected legacy lifecycle/spawn +files pass with their existing E701/E702 style debt excluded. `git diff --check` +passes. CodeGraph was initialized for the verified worktree and synchronized. + +## Remaining qualification + +This is source/public deterministic acceptance. Actual selected native/client +binaries, deployment configuration and operator UI rendering require separate +runtime qualification by the coordinator. A private v2 host-delegated grant that +needs native caller attestation remains unqualified when this CLI dispatch +context cannot supply that attestation; manual authority is never invented to +compensate. Native request-level hard admission remains with #354 and must not be +claimed from this preflight, ledger or sampled watcher. + +Memory disposition: **forbidden**. The delegated lane is restricted to this +worktree; user-scoped memory and parent closeout are outside its write scope. + +## Tested production source hashes + +- `src/codex_wake/goal_host_preflight.py`: `ea0609744752a192594c6a2ba593eb82fe5432f509363ce68a2f4a1e5acd9866` +- `src/codex_wake/agent_lifecycle.py`: `a212ea404f9724af971beba3c590ee5d606cb4af22cc00703dc9eb1ed8afd981` +- `src/codex_wake/dispatch_packet.py`: `35300c5dc475a6160c23c8496981815fccabe580297d8c6f24f744c4c84c052e` diff --git a/src/codex_wake/agent_lifecycle.py b/src/codex_wake/agent_lifecycle.py index 4510d5b7..c9fb8f17 100644 --- a/src/codex_wake/agent_lifecycle.py +++ b/src/codex_wake/agent_lifecycle.py @@ -36,7 +36,7 @@ def spawn_command(args, root): raise WakeError('host caller grant does not authorize this launch profile') required = {'cwd', 'app_server', 'codex_path', 'tmux_session', 'timeout'} strings = (required - {'timeout'}) | (set(profile) & {'tmux_socket', 'codex_home'}) - if (not required <= set(profile) or set(profile) - required - {'tmux_socket', 'codex_home', 'resources', 'strict_execution'} + if (not required <= set(profile) or set(profile) - required - {'tmux_socket', 'codex_home', 'resources', 'strict_execution', 'governance_preflight'} or ('resources' in profile and not isinstance(profile['resources'], dict)) or any(not isinstance(profile.get(k), str) or not profile[k] or '\x00' in profile[k] for k in strings) @@ -66,6 +66,14 @@ def spawn_command(args, root): approved = None pin = None admission = None + compatibility = None + if 'governance_preflight' in profile: + from .goal_host_preflight import inspect_host + compatibility = inspect_host(root, profile, args.caller_thread_id) + if not compatibility['admitted']: + from .sessions_cli import emit + emit(dict(state='held', reason=compatibility['reason'], compatibility_preflight=compatibility), args.as_json) + return 5 task_id = getattr(args, 'task_id', None) assignment_id = getattr(args, 'assignment_id', None) carried_pin = getattr(args, 'task_brief_pin', None) @@ -115,6 +123,11 @@ def spawn_command(args, root): from .dispatch_packet import render_assignment from .goal_governance import snapshot intent['governance_snapshot'] = snapshot(root, intent.get('source_thread_id')) + if compatibility is not None: + intent['preflight_snapshot'] = {key: compatibility.get(key) for key in + ('mode', 'reason', 'requested_mode', 'message', 'authorization', 'observed_at')} + intent['preflight_snapshot']['static_receipt'] = compatibility['static'].get('receipt_path') + intent['governance_snapshot'] = compatibility['governance'] intent['task'], dispatch = render_assignment(intent, root=root, brief=approved, pin=pin, admission=admission, receipt_path=path) fingerprint = hashlib.sha256(json.dumps(intent, sort_keys=True).encode()).hexdigest() @@ -146,11 +159,14 @@ def spawn_command(args, root): receipt['permission_context']=permission_receipt['permission_context'] if not binding:receipt['budget_mode']='explicit_expiry_only_legacy_profile' save(path, receipt) + if compatibility is not None: + receipt['compatibility_preflight'] = compatibility if receipt['state'] == 'starting' and receipt['thread_id'] is None: receipt.update(state='uncertain', uncertain_effect='thread/start') save(path, receipt) if receipt['state'] == 'prepared': - if binding and not intent.get('budget_inherited'): + if (binding and not intent.get('budget_inherited') + and (compatibility or {}).get('mode') != 'manual'): from .lifecycle_supervision import supervision_ready if not supervision_ready(root,profile['timeout']): receipt['reason']='resident_objective_supervisor_required' @@ -192,7 +208,7 @@ def budget_request(method,params): runtime = SimpleNamespace(**(dict(tmux_socket=None) | profile), codex=resolve_codex_cmd(profile['codex_path'], required=True)) tmux_command(runtime, 'has-session', '-t', runtime.tmux_session) - if permission_receipt and not before_effect(path,receipt): + if (permission_receipt or compatibility is not None) and not before_effect(path,receipt): from .sessions_cli import emit emit(receipt,args.as_json) return 5 @@ -411,6 +427,15 @@ def request(method, params): def before_effect(path, receipt): + profile = receipt['intent']['launch'] + if 'governance_preflight' in profile: + from .goal_host_preflight import inspect_host + preflight = inspect_host(path.parent.parent, profile, receipt['intent'].get('source_thread_id')) + receipt['compatibility_preflight'] = preflight + if not preflight['admitted']: + receipt['reason'] = preflight['reason'] + save(path, receipt) + return False if receipt.get('permission_context'): from .permission_handoff import before_effect as permission_before_effect if not permission_before_effect(path,receipt):return False @@ -423,7 +448,8 @@ def before_effect(path, receipt): save(path,receipt) return False if (receipt['intent']['operation']=='spawn' and receipt['intent'].get('configuration') - and not receipt['intent'].get('budget_inherited')): + and not receipt['intent'].get('budget_inherited') + and receipt.get('compatibility_preflight', {}).get('mode') != 'manual'): from .lifecycle_supervision import supervision_ready if not supervision_ready(path.parent.parent,receipt['intent']['launch']['timeout']): receipt['reason']='resident_objective_supervisor_required' diff --git a/src/codex_wake/dispatch_packet.py b/src/codex_wake/dispatch_packet.py index 752fa3da..e6a0cdea 100644 --- a/src/codex_wake/dispatch_packet.py +++ b/src/codex_wake/dispatch_packet.py @@ -54,7 +54,8 @@ def render_assignment(intent, *, root, brief=None, pin=None, admission=None, rec 'Return partial evidence when blocked; do not renew allowance or expand scope.', 'Honor the existing authorized allowance and operator choices; unknown governor availability alone does not revoke work authority.'], governance=dict(budget_mode=mode, configuration_revision=configuration.get('revision'), - state=control.get('mode', 'unknown'), snapshot=control, + state=(intent.get('preflight_snapshot') or {}).get('mode', control.get('mode', 'unknown')), + preflight=intent.get('preflight_snapshot'), snapshot=control, enabled=control.get('enabled'), required_action='Use available public governance status; do not insert a governor or block already-authorized repair merely because its status is unavailable.', lifecycle_root=str(root), receipt_path=str(receipt_path), diff --git a/src/codex_wake/goal_host_preflight.py b/src/codex_wake/goal_host_preflight.py new file mode 100644 index 00000000..5e6b6735 --- /dev/null +++ b/src/codex_wake/goal_host_preflight.py @@ -0,0 +1,188 @@ +"""Read-only host compatibility before opted-in lifecycle dispatch. + +Static observations are cached by source/client/server/config/identity. Accounting, +controller health and operator control are observed afresh, never cached authority. +""" +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import socket +import struct +import time + +from . import goal_supervision as supervision +from .records import WakeError + + +MODES = {'hard_admission', 'sampled_stop', 'manual'} + + +def _digest(value): + return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() + + +def _file(path, maximum=256 * 1024 * 1024): + path = Path(path).resolve(strict=True) + with path.open('rb') as stream: + info = os.fstat(stream.fileno()) + if not path.is_file() or info.st_size > maximum: + raise ValueError('identity file exceeds preflight bound') + digest = hashlib.file_digest(stream, 'sha256').hexdigest() + after = os.fstat(stream.fileno()) + if (info.st_size, info.st_mtime_ns) != (after.st_size, after.st_mtime_ns): + raise ValueError('identity file changed during preflight') + return dict(path=str(path), sha256=digest, size=info.st_size) + + +def _options(profile, caller): + options = profile['governance_preflight'] + if (type(options) is not dict or set(options) - {'policy', 'thread_id', 'requested_mode', 'manual_authorization'} + or not {'policy', 'requested_mode'} <= set(options) + or type(options['requested_mode']) is not str or options['requested_mode'] not in MODES): + raise WakeError('invalid host governance preflight configuration') + thread = options.get('thread_id') or caller + supervision._name(thread) + if caller is not None and thread != caller: + raise WakeError('governance preflight cannot select another caller') + if 'manual_authorization' in options and (type(options['manual_authorization']) is not str + or not options['manual_authorization'].strip() or len(options['manual_authorization']) > 4096): + raise WakeError('manual repair requires its existing authorization locator') + return options, thread + + +def _static(root, profile, options, thread): + from .shared_app_server import SharedAppServerReader + from .process import process_identity + from .native_protocol import native_request, NativeCapabilityUnavailable + grant = supervision._grant(options['policy'], thread, root=root) + if (grant['app_server'] != profile['app_server'] or grant['codex_path'] != profile['codex_path'] + or profile.get('codex_home', grant['codex_home']) != grant['codex_home']): + raise ValueError('selected launch host differs from the supervision grant') + timeout = min(profile['timeout'], grant['rpc_timeout'], 5) + with SharedAppServerReader(profile['app_server'], timeout=timeout) as reader: + peer_pid, peer_uid, _ = struct.unpack('3i', reader.connection.socket.getsockopt( + socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i'))) + if peer_uid != os.geteuid(): + raise ValueError('native peer owner differs') + metadata = reader.server_metadata + peer = dict(pid=peer_pid, process=process_identity(peer_pid), binary=_file('/proc/' + str(peer_pid) + '/exe')) + source = {name: _file(Path(__file__).with_name(name), 1024 * 1024)['sha256'] for name in ( + 'goal_host_preflight.py', 'goal_supervision.py', 'goal_governance.py', 'goal_notices.py', + 'goal_usage.py', 'native_protocol.py', 'shared_app_server.py', 'agent_lifecycle.py', 'dispatch_packet.py')} + binding = dict(source=source, client=_file(profile['codex_path']), server=peer, + metadata=metadata, profile=profile, policy=_file(options['policy'], 1024 * 1024), + identity=dict(thread_id=thread, workspace=str(Path(profile['cwd']).resolve()), uid=os.geteuid()), + endpoint=dict(path=str(reader.path), inode=reader.path.stat().st_ino)) + if peer['process'] is None: + raise ValueError('native peer generation unavailable') + key = _digest(binding) + directory = supervision._directory(root) / 'host-preflight' + directory.mkdir(mode=0o700, exist_ok=True) + supervision._private(directory, True) + path = directory / (key + '.json') + with supervision._lock(directory, key): + if path.exists(): + supervision._private(path) + saved = json.loads(path.read_text()) + if saved.get('binding') == binding and saved.get('version') == 1: + return dict(saved, cache='reused', receipt_path=str(path)) + notice_read = 'unknown' + try: + response = native_request(profile['app_server'], profile['codex_path'], 'thread/notice/read', + dict(threadId=thread, eventId='preflight-' + key), timeout) + notice_read = 'supported' if 'notice' in response else 'unqualified_response' + except NativeCapabilityUnavailable: + notice_read = 'unsupported' + except (OSError, ValueError, WakeError): + pass + saved = dict(version=1, receipt_id=key, observed_at=time.time(), binding=binding, + capabilities=dict(hard_admission=False, notice_read=notice_read, + request_path='unqualified', client_server_same_binary=binding['client']['sha256'] == peer['binary']['sha256']), + limitations=['Client executable and connected server executable are identified separately; neither proves model request-path admission.', + 'No qualified native per-request admission hook or enforceable output ceiling is available.', + 'A successful notice read does not prove operator rendering, model delivery or control authority.']) + if notice_read == 'unknown': + return dict(saved, cache='not_qualified', receipt_path=None) + supervision._save(path, saved) + return dict(saved, cache='created', receipt_path=str(path)) + + +def inspect_host(root, profile, caller): + """Return an explicit mode decision using retained host authority, no effects.""" + options, thread = _options(profile, caller) + # A retained repair locator cannot substitute for the selected host grant. + grant = supervision._grant(options['policy'], thread, root=root) + if (grant['app_server'] != profile['app_server'] or grant['codex_path'] != profile['codex_path'] + or profile.get('codex_home', grant['codex_home']) != grant['codex_home']): + raise WakeError('selected launch host differs from the supervision grant') + from .goal_governance import snapshot + control = snapshot(root, thread) + result = dict(version=1, thread_id=thread, requested_mode=options['requested_mode'], + observed_at=time.time(), mode='degraded', admitted=False, governance=control, + static=None, dynamic=None, effect_attempted=False, resources_renewed=False, + native_admission='unsupported', maximum_token_overshoot=None) + try: + result['static'] = _static(root, profile, options, thread) + except (OSError, ValueError, TypeError, KeyError, WakeError) as error: + result['static'] = dict(cache='unavailable', reason=type(error).__name__, message=str(error)[:256]) + if options['requested_mode'] == 'hard_admission': + result.update(reason='hard_admission_unsupported', message='Hard admission is unsupported on this selected request path. No work was dispatched and no downgrade was applied. Use an already authorized manual route or explicitly select sampled stopping; no exact token ceiling is claimed.') + return result + try: + diagnosis = supervision.diagnose(root, options['policy'], thread) + status = supervision.show(root, options['policy'], thread) + governor = status.get('governor', {}) + from .goal_notices import NativeNoticeAdapter, read_notice + notices = {} + for name in ('activation', 'engagement', 'deactivation'): + if name not in governor: + continue + notice = dict(governor[name]) + try: + observed = NativeNoticeAdapter(root).reconcile(read_notice(root, notice['event_id'])) + notice['current_model_context'] = observed.get('model_context') is True + except (OSError, ValueError, KeyError, TypeError, WakeError): + notice['current_model_context'] = False + notices[name] = notice + result['dynamic'] = dict(observed_at=time.time(), accounting=diagnosis['accounting'], + controller=diagnosis['controller'], engagement=status.get('engagement'), + state=status.get('state'), stop_latched=status.get('stop_latched', False), + notice_surfaces=notices, controls=governor.get('controls', [])) + except (OSError, ValueError, KeyError, TypeError, WakeError) as error: + result['dynamic'] = dict(observed_at=time.time(), state='unavailable', reason=type(error).__name__, message=str(error)[:256]) + control = snapshot(root, thread) + result['governance'] = control + manual = options.get('manual_authorization') + if (control.get('state') == 'available' and control.get('enabled') is False + and control.get('scope') == grant['allocation_id']): + result.update(mode='inactive', admitted=True, reason='operator_disabled', + message='Governor remains inactive by explicit operator choice. Independent limits remain; no watcher was started, work resumed or resources renewed.') + elif manual and options['requested_mode'] == 'manual': + _manual(result, manual) + else: + dynamic = result['dynamic'] + surfaces = dynamic.get('notice_surfaces', {}) + model_delivery = any(item.get('current_model_context') is True for item in surfaces.values()) + dynamic['operator_rendering'] = 'unverified_by_read_only_preflight' + remaining = (dynamic.get('engagement') or {}).get('card') or {} + if (options['requested_mode'] == 'sampled_stop' and dynamic.get('accounting', {}).get('state') == 'qualified' + and dynamic.get('controller', {}).get('fresh') is True + and dynamic.get('engagement', {}).get('engaged') is True + and remaining.get('remaining_tokens', 0) > 0 and remaining.get('remaining_seconds', 0) > 0 + and dynamic.get('controls') and model_delivery and result['static'].get('capabilities', {}).get('notice_read') == 'supported'): + result.update(mode='sampled_stop', admitted=True, reason='fresh_sampled_supervision', + message='Sampled stopping is qualified now; it is not request admission and has no exact token overshoot bound. Notice read/model delivery are observed; operator rendering requires its independent qualification receipt.') + elif manual: + _manual(result, manual) + else: + result.update(reason='required_supervision_unqualified', + message='Requested supervision is degraded or unqualified. Retained accounting/control history is unchanged. Previously authorized manual repair remains a supported separate host route; this preflight neither asks again nor manufactures that authority.') + return result + + +def _manual(result, authorization): + result.update(mode='manual', admitted=True, reason='existing_manual_authorization', + authorization=authorization, message='Previously authorized manual work may proceed without the watcher. No automatic containment is promised; original scope, independent limits and stop conditions remain. No repeated approval, watcher change or budget renewal occurred.') diff --git a/tests/test_agent_spawn.py b/tests/test_agent_spawn.py index 94086429..eb0a58d6 100644 --- a/tests/test_agent_spawn.py +++ b/tests/test_agent_spawn.py @@ -160,6 +160,18 @@ def test_peer_cannot_select_profile_without_host_caller_grant(self): '--task', 'Report readiness.', '--expires-at', '2030-01-01T00:00:00Z', '--caller-thread-id', '00000000-0000-7000-8000-000000000099', '--json']) + def test_hard_admission_preflight_refuses_before_thread_tab_or_task_effects(self): + self.spawn_scenario(host_preflight='hard_admission') + + def test_authorized_manual_repair_spawns_without_a_resident_watcher_or_renewal(self): + self.spawn_scenario(host_preflight='manual',budget=True) + + def test_inactive_preflight_honors_operator_choice_without_manual_permission(self): + self.spawn_scenario(host_preflight='sampled_stop',governor_disabled=True) + + def test_sampled_preflight_reports_degraded_before_costly_dispatch(self): + self.spawn_scenario(host_preflight='sampled_stop') + def test_spawn_executes_exact_assignment_once_across_pending_observation(self): self.spawn_scenario() @@ -236,7 +248,7 @@ def test_shared_profile_resource_checks_do_not_override_spawn_launch(self): self.spawn_scenario(resource_profile=True) def spawn_scenario(self, *, loss=None, check_environment=False, expire_at_task=False, - check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False,brief_dispatch=False,full_history=False,governor_disabled=False): + check_history=False, isolated_profile=False, normalized_input=False, auto_start=False, composer=None, interrupted_admission=False, recipient_guard=None, resource_profile=False,budget=False,budget_stop=False,budget_delayed=False,budget_read_error=False,budget_queue_stop=False,budget_overrun=False,brief_dispatch=False,full_history=False,governor_disabled=False,host_preflight=None): thread_id = '00000000-0000-7000-8000-000000000157' starts, tasks, windows = [], [], [] turn_starts = [] @@ -326,6 +338,19 @@ def handler(ws): profile.update(tmux_socket=str(root / 'tmux.sock'), codex_home=str(root / 'codex-home')) registry={'version': 1, 'profiles': {'worker': profile}} source_thread='00000000-0000-7000-8000-000000000158' + if host_preflight: + home = root / 'preflight-home' + (home / 'sessions').mkdir(parents=True) + policy = root / 'preflight-policy.json' + policy.write_text(json.dumps(dict(version=1, callers={source_thread: dict( + app_server=profile['app_server'], codex_path=str(client_script), codex_home=str(home), + allocation_id='operator-disabled-scope' if governor_disabled else 'repair-scope', max_tokens=1000, max_seconds=300, rpc_timeout=1)}))) + policy.chmod(0o600) + profile['governance_preflight'] = dict(policy=str(policy), thread_id=source_thread, + requested_mode=host_preflight) + if host_preflight == 'manual': + profile['governance_preflight']['manual_authorization'] = 'existing approved repair ticket' + registry['callers'] = {source_thread: ['worker']} if governor_disabled: from codex_wake.goal_supervision import _directory, _name, _save _save(_directory(root)/(_name(source_thread)+'.json'), dict( @@ -365,6 +390,10 @@ def handler(ws): authority = BriefAuthority('operator:fixture', 'operator') store.propose('multiply', brief, authority=authority) store.approve('multiply', 1, authority=authority) + if host_preflight and 'codex_home' in profile: + grant = json.loads(policy.read_text()) + grant['callers'][source_thread]['codex_home'] = profile['codex_home'] + policy.write_text(json.dumps(grant)) profiles.write_text(json.dumps(registry)) master, slave = pty.openpty() client = None @@ -397,7 +426,7 @@ def command(argv, **kwargs): '--profile', 'worker', '--intent', 'one-worker', '--name', 'worker', '--task', 'Calculate 6 * 7 and report 42.', '--expires-at', '2030-01-01T00:00:00Z', '--json'] - if governor_disabled:argv.extend(['--caller-thread-id',source_thread]) + if governor_disabled or host_preflight and not budget:argv.extend(['--caller-thread-id',source_thread]) if budget:argv.extend(['--objective-ref','bootstrap']) if brief_dispatch: argv.extend(['--task-id','multiply','--assignment-id','multiply-one']) @@ -417,7 +446,7 @@ def clock(): try: with patch('subprocess.run', side_effect=command), patch( 'codex_wake.native_clock.snapshot', side_effect=clock): - if budget: + if budget and host_preflight != 'manual': from codex_wake.lifecycle_supervision import publish_supervision publish_supervision(root,1) if interrupted_admission: @@ -427,6 +456,15 @@ def clock(): pending = StringIO() with redirect_stdout(pending): self.assertEqual(run(argv), 5, pending.getvalue()) + if host_preflight == 'hard_admission' or host_preflight == 'sampled_stop' and not governor_disabled: + preflight = json.loads(pending.getvalue())['compatibility_preflight'] + self.assertEqual(preflight['mode'], 'degraded') + self.assertEqual(preflight['reason'], 'hard_admission_unsupported' if host_preflight == 'hard_admission' else 'required_supervision_unqualified') + self.assertFalse(preflight['admitted']) + self.assertEqual(starts, []) + self.assertEqual(windows, []) + self.assertEqual(tasks, []) + return if recipient_guard: self.assertEqual(tasks, []) self.assertEqual(len(starts), 1) @@ -505,7 +543,7 @@ def clock(): self.assertEqual(actor['used'],70) self.assertEqual(len(tasks),1) return - if governor_disabled: + if governor_disabled and not host_preflight: # A later operator change cannot rewrite an already admitted assignment. _save(_directory(root)/(_name(source_thread)+'.json'), dict( thread_id=source_thread,allocation_id='operator-disabled-scope', @@ -518,6 +556,12 @@ def clock(): self.assertEqual(receipt['state'], 'completed') self.assertEqual(receipt['thread_id'], thread_id) self.assertEqual(receipt['execution']['turn_id'], 'turn157') + if host_preflight == 'manual': + self.assertEqual(receipt['compatibility_preflight']['mode'], 'manual') + self.assertEqual(receipt['compatibility_preflight']['authorization'], 'existing approved repair ticket') + self.assertFalse(receipt['compatibility_preflight']['resources_renewed']) + self.assertEqual(receipt['compatibility_preflight']['static']['cache'], 'reused') + self.assertFalse((root / 'lifecycle/supervision.json').exists()) if budget: view=receipt['objective_budget'] self.assertEqual(view['tokens_used'],70) @@ -535,6 +579,9 @@ def clock(): self.assertEqual(len(turn_starts), 0 if auto_start else 1) packet = json.loads(tasks[0]['input'][0]['text'].split('BOUNDED_ASSIGNMENT=', 1)[1]) self.assertEqual(packet['objective'], 'Multiply the evidence operands' if brief_dispatch else 'Calculate 6 * 7 and report 42.') + if host_preflight == 'manual': + self.assertEqual(packet['governance']['state'], 'manual') + self.assertIn('No automatic containment', packet['governance']['preflight']['message']) if governor_disabled: self.assertEqual(packet['governance']['state'], 'inactive') self.assertIs(packet['governance']['enabled'], False) diff --git a/tests/test_goal_supervision.py b/tests/test_goal_supervision.py index 5455905b..6cbf38f6 100644 --- a/tests/test_goal_supervision.py +++ b/tests/test_goal_supervision.py @@ -690,7 +690,7 @@ def handler(ws): for raw in ws: message=json.loads(raw) if 'id' not in message:continue - if message['method']=='initialize':result={} + if message['method']=='initialize':result=native.get('metadata', {}) else: try:result=request(None,None,message['method'],message['params'],None) except NotImplementedError: diff --git a/tests/test_host_preflight.py b/tests/test_host_preflight.py new file mode 100644 index 00000000..b36ce7c8 --- /dev/null +++ b/tests/test_host_preflight.py @@ -0,0 +1,105 @@ +"""Selected host qualification through public spawn, using the existing native fixture.""" +from contextlib import redirect_stdout +from io import StringIO +import json +import os +from pathlib import Path +import tempfile +import time +import unittest +from unittest.mock import patch + +from codex_wake.cli import run +from codex_wake.goal_supervision import enroll, poll, show +from codex_wake.goal_notices import list_notices, present_notice +from tests import test_goal_supervision as fixtures + +THREAD, NOW = fixtures.THREAD, fixtures.NOW + + +class HostPreflightTests(unittest.TestCase): + def test_static_receipt_reuse_binary_metadata_drift_and_fresh_controller_check(self): + helper = fixtures.GoalSupervisionTests() + self.addCleanup(helper.doCleanups) + with tempfile.TemporaryDirectory() as tmp: + root, policy, _, native, _ = helper.fixture(tmp) + native['notices'] = {} + native['metadata'] = dict(userAgent='qualified-fixture-v1') + client = Path(tmp) / 'selected-client' + client.write_text('#!/bin/sh\nexit 0\n') + client.chmod(0o700) + config = json.loads(policy.read_text()) + config['callers'][THREAD]['codex_path'] = str(client) + policy.write_text(json.dumps(config)) + grant = config['callers'][THREAD] + profile = dict(cwd=tmp, app_server=grant['app_server'], codex_path=str(client), + codex_home=grant['codex_home'], tmux_session='fixture', timeout=1, + governance_preflight=dict(policy=str(policy), requested_mode='sampled_stop')) + profiles = Path(tmp) / 'profiles.json' + def write_profile(): + profiles.write_text(json.dumps(dict(version=1, profiles={'worker': profile}, + callers={THREAD: ['worker']}))) + write_profile() + def invoke(intent='preflight-once'): + output = StringIO() + with redirect_stdout(output): + code = run(['--wake-root', str(root), 'sessions', 'spawn', '--profiles', str(profiles), + '--profile', 'worker', '--caller-thread-id', THREAD, '--intent', intent, + '--name', 'worker', '--task', 'Inspect approved fixture', + '--expires-at', '2029-01-01T00:00:00Z', '--json']) + self.assertEqual(code, 5, output.getvalue()) + return json.loads(output.getvalue())['compatibility_preflight'] + with helper.controller(root) as controller, patch('codex_wake.lifecycle_budget.trusted_upper', return_value=NOW), patch( + 'codex_wake.native_clock.snapshot', return_value={'utc': {'lower': NOW, 'upper': NOW + 1}}): + enroll(root, policy, THREAD, 100, 120) + poll(root) + deadline = time.monotonic() + 6 + while not show(root, policy, THREAD)['engagement']['engaged']: + self.assertIsNone(controller.poll()) + self.assertLess(time.monotonic(), deadline, 'resident first poll did not qualify') + time.sleep(.02) + for event in list_notices(root, THREAD): + present_notice(root, event['event_id']) + first = invoke() + self.assertEqual(first['mode'], 'sampled_stop', json.dumps(first, sort_keys=True)) + self.assertTrue(first['admitted']) + self.assertEqual(first['static']['cache'], 'created') + self.assertEqual(first['static']['binding']['server']['pid'], os.getpid()) + self.assertFalse(first['static']['capabilities']['hard_admission']) + self.assertFalse(first['static']['capabilities']['client_server_same_binary']) + self.assertEqual(first['dynamic']['accounting']['used_tokens'], 40) + repeated = invoke() + self.assertEqual(repeated['static']['cache'], 'reused') + self.assertEqual(repeated['static']['receipt_id'], first['static']['receipt_id']) + self.assertGreater(repeated['dynamic']['observed_at'], first['dynamic']['observed_at']) + client.write_text(client.read_text() + '# different selected binary\n') + changed = invoke() + self.assertEqual(changed['static']['cache'], 'created') + self.assertNotEqual(changed['static']['receipt_id'], repeated['static']['receipt_id']) + native['metadata']['userAgent'] = 'qualified-fixture-v2' + server_changed = invoke() + self.assertEqual(server_changed['static']['cache'], 'created') + self.assertNotEqual(server_changed['static']['receipt_id'], changed['static']['receipt_id']) + retained_notices = native['notices'] + native['notices'] = {} + missing_delivery = invoke() + self.assertEqual(missing_delivery['mode'], 'degraded') + self.assertEqual(missing_delivery['static']['cache'], 'reused') + self.assertTrue(missing_delivery['dynamic']['controller']['fresh']) + self.assertTrue(all(not item['current_model_context'] + for item in missing_delivery['dynamic']['notice_surfaces'].values())) + native['notices'] = retained_notices + profile['tmux_session'] = 'another-approved-tab-group' + write_profile() + config_changed = invoke('preflight-new-configuration') + self.assertEqual(config_changed['static']['cache'], 'created') + self.assertNotEqual(config_changed['static']['receipt_id'], server_changed['static']['receipt_id']) + controller.kill() + controller.wait(timeout=3) + stale = invoke('preflight-new-configuration') + self.assertEqual(stale['mode'], 'degraded') + self.assertFalse(stale['admitted']) + self.assertEqual(stale['static']['cache'], 'reused') + self.assertFalse(stale['dynamic']['controller']['fresh']) + self.assertFalse(any(method in ('thread/start', 'thread/queue/add', 'thread/goal/set', 'turn/interrupt') + for method, _ in native['calls'])) From 49df3a91b5f01c7bb877b1ebdefe6c440d8d4e7e Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:28:03 -0500 Subject: [PATCH 09/20] fix: qualify preflight against actual native executable sizes (#353) --- .../evidence/plan0175/353-native-preflight.md | 11 ++++++++++ scripts/goal_notice_native_acceptance.py | 22 ++++++++++++++++++- src/codex_wake/goal_host_preflight.py | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/dev/evidence/plan0175/353-native-preflight.md diff --git a/docs/dev/evidence/plan0175/353-native-preflight.md b/docs/dev/evidence/plan0175/353-native-preflight.md new file mode 100644 index 00000000..d9fb1889 --- /dev/null +++ b/docs/dev/evidence/plan0175/353-native-preflight.md @@ -0,0 +1,11 @@ +# #353 primary native qualification + +Primary integrated source6118f13 plus binary streaming-bound correction. Pinned actual native executable1,114,954,128bytes, SHA25686fad00e38f0c26a5c8d2eb2dbe34536c74655fb2fb45b4efc5d94e97e683b1f; actual stock0.164.0 frontend308,139,304bytes. Initial static256MiB bound excluded both real executables; changed bounded streaming hashing to2GiB before qualification. Public policy RPC timeout remains at most5seconds; the probe's earlier direct-adapter15second timeout was corrected when introducing an actual grant. + +Command: `PYTHONPATH=src /bin/python scripts/goal_notice_native_acceptance.py --app-server --frontend --output /tmp/governor353-native-qualified`. + +Terminal receipt PASS: real connected server PID/executable and separate client identified; static receipt created then reused; explicit inactive operator state preserved despite missing qualified inference accounting; hard_admission refused truthfully. Activation, stop and deactivation actualTUI rendering/model-context checks also pass; no provider requests; peer unchanged; owned process census empty. Full preflight observations: /tmp/governor353-native-qualified/host-preflight.json. No sampled or hardcap guarantee is inferred from a read-only capability probe; unobserved accounting remains unqualified. + +Primary five-case public selection PASS before bound correction (12.114s); same final selection rerun after correction, recorded in /tmp/353-primary-final.log. Worker affected40casesPASS and final5casesPASS remain source regression evidence, not a replacement for this selected-native proof. Repo-wide pinned undefined-name checks and diffcheck PASS. No installed runtime/service or shared session was changed. + +Final primary five-case selection:5PASS,12.695s after bound correction. diff --git a/scripts/goal_notice_native_acceptance.py b/scripts/goal_notice_native_acceptance.py index 52649cd9..d69f8b27 100644 --- a/scripts/goal_notice_native_acceptance.py +++ b/scripts/goal_notice_native_acceptance.py @@ -113,7 +113,8 @@ def rpc(method, **params): receipt = dict(thread_id=thread_id,allocation_id='owned-presentation',generation='owned-window', state='stopped',reason='allocation_exhausted',used_tokens=100000,token_limit=100000, token_accounting='fresh',deadline=time.time(),stop_turns=[work_turn],stop_proof=True, - grant=dict(app_server=endpoint,codex_path=str(frontend),rpc_timeout=15)) + grant=dict(app_server=endpoint,codex_path=str(frontend),codex_home=str(home), + allocation_id='owned-presentation',max_tokens=100000,max_seconds=600,rpc_timeout=5)) _save(_directory(wake_root)/(_name(thread_id)+'.json'),receipt) notice = record_notice(wake_root,receipt,'stop') native_adapter = NativeNoticeAdapter(wake_root) @@ -150,6 +151,25 @@ def reconcile(self, event): return native_adapter.reconcile(event) assert rpc('thread/goal/get', threadId=thread_id)['goal']['status'] == 'paused' report.update(deactivation_rendered=True, deactivation_model_context=True, deactivation_retained=True, deactivation_resumed_nothing=True) + from codex_wake.goal_host_preflight import inspect_host + policy = wake_root/'preflight-policy.json' + _save(policy, dict(version=1, callers={thread_id: receipt['grant']})) + profile = dict(cwd=str(root), app_server=endpoint, codex_path=str(frontend), + codex_home=str(home), timeout=15, tmux_session='notice', governance_preflight=dict( + policy=str(policy), thread_id=thread_id, requested_mode='sampled_stop')) + first_preflight = inspect_host(wake_root, profile, thread_id) + assert first_preflight['mode'] == 'inactive', first_preflight + assert first_preflight['static']['cache'] == 'created', first_preflight + assert first_preflight['static']['capabilities']['notice_read'] == 'supported' + reused_preflight = inspect_host(wake_root, profile, thread_id) + assert reused_preflight['static']['cache'] == 'reused', reused_preflight + profile['governance_preflight']['requested_mode'] = 'hard_admission' + unsupported = inspect_host(wake_root, profile, thread_id) + assert unsupported['admitted'] is False and unsupported['reason'] == 'hard_admission_unsupported' + (output/'host-preflight.json').write_text(json.dumps(dict(first=first_preflight, + reused=reused_preflight, unsupported=unsupported), indent=2)+'\n') + report.update(preflight_real_peer_identified=True, preflight_reused=True, + preflight_inactive_preserved=True, preflight_no_hardcap_claim=True) duplicate = rpc('thread/notice/append' ,threadId=thread_id,eventId=event_id,text=text) assert duplicate['alreadyRecorded'] is True and duplicate['notificationPublished'] is False, duplicate retained = rpc('thread/notice/read',threadId=thread_id,eventId=event_id) diff --git a/src/codex_wake/goal_host_preflight.py b/src/codex_wake/goal_host_preflight.py index 5e6b6735..e0637969 100644 --- a/src/codex_wake/goal_host_preflight.py +++ b/src/codex_wake/goal_host_preflight.py @@ -24,7 +24,7 @@ def _digest(value): return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() -def _file(path, maximum=256 * 1024 * 1024): +def _file(path, maximum=2 * 1024 * 1024 * 1024): path = Path(path).resolve(strict=True) with path.open('rb') as stream: info = os.fstat(stream.fileno()) From 4a723674fa3f4dc00e45bc253868b445a16986f0 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 16:54:07 -0500 Subject: [PATCH 10/20] feat: enforce reviewed HTTP request admission and settlement (#354) --- src/codex_wake/goal_governance.py | 4 +- src/codex_wake/goal_host_preflight.py | 15 + src/codex_wake/goal_notices.py | 24 +- src/codex_wake/goal_supervision.py | 8 + src/codex_wake/objective_ledger.py | 86 +++ src/codex_wake/request_admission.py | 769 ++++++++++++++++++++++++++ tests/test_governor_deactivation.py | 6 +- tests/test_request_admission.py | 459 +++++++++++++++ 8 files changed, 1362 insertions(+), 9 deletions(-) create mode 100644 src/codex_wake/request_admission.py create mode 100644 tests/test_request_admission.py diff --git a/src/codex_wake/goal_governance.py b/src/codex_wake/goal_governance.py index 33925bff..7aee0633 100644 --- a/src/codex_wake/goal_governance.py +++ b/src/codex_wake/goal_governance.py @@ -19,7 +19,7 @@ def sync(receipt): with supervision._lock(path.parent, path.stem): supervision._private(path) source = json.loads(path.read_text()) - for key in ("governor_enabled", "governor_revision", "governor_control"): + for key in ("governor_enabled", "governor_revision", "governor_control", "request_accounting"): if key in source: receipt[key] = source[key] return receipt.get("governor_enabled", True) is False @@ -260,7 +260,7 @@ def snapshot(root, thread_id): return dict( state="available", enabled=enabled, - mode="sampled_stop" if enabled else "inactive", + mode=authority.get("request_accounting",{}).get("mode","sampled_stop") if enabled else "inactive", revision=authority.get("governor_revision", 0), scope=receipt["allocation_id"], thread_id=thread_id, diff --git a/src/codex_wake/goal_host_preflight.py b/src/codex_wake/goal_host_preflight.py index e0637969..5bc6f4b2 100644 --- a/src/codex_wake/goal_host_preflight.py +++ b/src/codex_wake/goal_host_preflight.py @@ -128,7 +128,22 @@ def inspect_host(root, profile, caller): result['static'] = _static(root, profile, options, thread) except (OSError, ValueError, TypeError, KeyError, WakeError) as error: result['static'] = dict(cache='unavailable', reason=type(error).__name__, message=str(error)[:256]) + if control.get('state')=='available' and control.get('enabled') is False and control.get('scope')==grant['allocation_id']: + result.update(mode='inactive',admitted=True,reason='operator_disabled',message='Governor remains disabled; no request-path gate is imposed and no resource authority is renewed.') + return result if options['requested_mode'] == 'hard_admission': + try: + from .request_admission import qualify_gateway + qualified=qualify_gateway(root,profile,thread) + result.update(qualified) + result['native_admission']='selected_http_gateway_only' + return result + except (OSError,ValueError,KeyError,TypeError,WakeError) as error: + latest=snapshot(root,thread) + if latest.get('enabled') is False and latest.get('scope')==grant['allocation_id']: + result.update(mode='inactive',admitted=True,governance=latest,reason='operator_disabled') + return result + result['request_path_error']=type(error).__name__ result.update(reason='hard_admission_unsupported', message='Hard admission is unsupported on this selected request path. No work was dispatched and no downgrade was applied. Use an already authorized manual route or explicitly select sampled stopping; no exact token ceiling is claimed.') return result try: diff --git a/src/codex_wake/goal_notices.py b/src/codex_wake/goal_notices.py index c559e476..d96e7f6b 100644 --- a/src/codex_wake/goal_notices.py +++ b/src/codex_wake/goal_notices.py @@ -11,7 +11,7 @@ import re from datetime import datetime, timezone -KINDS = frozenset(('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation')) +KINDS = frozenset(('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation', 'admission','request_activation')) _EVENT = re.compile(r'notice_[a-f0-9]{64}') @@ -80,6 +80,8 @@ def record_notice(root, receipt, kind, *, controller=None): identity.update(stop_episode=receipt.get('active_stop_id') or receipt.get('stop_episode_id') or [stop.get('boot_id'), stop.get('observed_boottime'), receipt.get('pause_epoch', 0)], reason=receipt.get('reason') or stop.get('reason'), work_turns=sorted(receipt.get('stop_turns', []))) + if kind=='request_activation':identity['request_accounting']=receipt['request_accounting'] + if kind=='admission':identity['request_admission_event']=receipt['request_admission_event'] stable_identity=dict(identity) if receipt.get('active_stop_id'): # Captured turns may be refined by later control readback in this episode. @@ -99,7 +101,7 @@ def record_notice(root, receipt, kind, *, controller=None): if sum(1 for _ in directory.glob('notice_*.json')) >= 10000: raise ValueError('supervision notice capacity reached; history retained') snapshot = {key: receipt.get(key) for key in ('used_tokens', 'token_limit', 'token_accounting', - 'deadline', 'checkpoint_token_percent', 'checkpoint_seconds', 'state', 'reason', 'lifetime_tokens_before_window')} + 'deadline', 'checkpoint_token_percent', 'checkpoint_seconds', 'state', 'reason', 'lifetime_tokens_before_window','governor_enabled')} event = dict(version=1, event_id=event_id, identity=identity, snapshot=snapshot, recorded_at=datetime.now(timezone.utc).isoformat(), controller_generation=(controller or {}).get('generation', receipt.get('controller_generation')), @@ -120,6 +122,18 @@ def render_notice(event): usage = f'{used:,} of {limit:,}' if type(used) is int and type(limit) is int else 'usage unavailable' remaining = f'{max(0, limit-used):,}' if type(used) is int and type(limit) is int else 'unknown' kind = identity['kind'] + if kind=='request_activation': + selected=identity['request_accounting'] + return (f"Governor request-path owner configured. Effective mode {'inactive' if snapshot.get('governor_enabled') is False else selected['mode']} for reviewed HTTP Responses path only; WS/compact/bypass remain unqualified. " + f"Scope {identity['allocation_id']}; session {identity['thread_id']}. " + 'ObjectiveLedger owns aggregate request accounting; original prior usage, caps and deadlines retained. No work resumed or resources renewed. ' + 'Say "Deactivate governor"; verified operator control needs no model inference. ' + f"[External supervision event {event['event_id']}]") + if kind=='admission': + return (f"Model request denied before provider submission. Scope {identity['allocation_id']}; session {identity['thread_id']}. " + 'Reason: objective/session resources unavailable or accounting uncertain. Mode hard_admission applies only to the selected qualified HTTP path. ' + 'Say "Deactivate governor" in this conversation; verified operator control needs no provider inference. No work resumed or resources renewed. ' + f"[External supervision event {event['event_id']}]") if kind in ('deactivation','reactivation'): text=('Governor inactive. Drop stale governor checkpoint/stop instructions. Independent platform controls remain. ' 'Say "Reactivate governor" to enable sampled stopping. ' if kind=='deactivation' else @@ -312,12 +326,12 @@ def run_presenter(root, stop): def governor_status(root, receipt): """Independent inspection exposes control state even if notice delivery fails.""" status = dict(identity='Codex Wake governor', scope=receipt['allocation_id'], - mode='inactive' if receipt.get('governor_enabled',True) is False else 'sampled_stop', + mode='inactive' if receipt.get('governor_enabled',True) is False else receipt.get('request_accounting',{}).get('mode','sampled_stop'), enabled=receipt.get('governor_enabled',True), maximum_token_overshoot=None, controls=['Inspect governor', 'Reactivate governor' if receipt.get('governor_enabled',True) is False else 'Deactivate governor', 'Keep stopped'], intervention=None) ids = receipt.get('notice_ids', {}) - for kind in ('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation'): + for kind in ('activation', 'engagement', 'checkpoint', 'stop', 'recovery', 'deactivation', 'reactivation','admission','request_activation'): event_id = ids.get(kind) if not event_id: continue @@ -334,7 +348,7 @@ def governor_status(root, receipt): summary = dict(event_id=event_id, kind=kind, presentation='unavailable', next_action='Inspect governor notice storage and native delivery; intervention remains in effect.') status[kind] = summary - if kind in ('checkpoint', 'stop', 'recovery'): + if kind in ('checkpoint', 'stop', 'recovery','admission'): status['intervention'] = summary if receipt.get('notice_record_error'): status['notice_error'] = receipt['notice_record_error'] diff --git a/src/codex_wake/goal_supervision.py b/src/codex_wake/goal_supervision.py index 62e81f4c..f215cace 100644 --- a/src/codex_wake/goal_supervision.py +++ b/src/codex_wake/goal_supervision.py @@ -339,6 +339,14 @@ def _allocation_admit(directory,receipt,old): def _usage(receipt): + if receipt.get('request_accounting'): + from .objective_ledger import ObjectiveLedger + from .lifecycle_budget import trusted_upper + binding=receipt['request_accounting'] + view=ObjectiveLedger(binding['root'],binding['objective_id']).inspect(now=trusted_upper()) + return dict(state='qualified' if view['accounting_known'] else 'unknown', + used_tokens=view['tokens_used'],token_accounting='fresh',sampled=False, + reason=None if view['accounting_known'] else 'request_completion_unknown',request_accounting=binding) from .goal_usage import observe_usage if not receipt.get('grant',{}).get('host_delegated'): return observe_usage(receipt['grant']['codex_home'],receipt['thread_id'],receipt['goal_created_at'], diff --git a/src/codex_wake/objective_ledger.py b/src/codex_wake/objective_ledger.py index 80ee9abc..c233be6c 100644 --- a/src/codex_wake/objective_ledger.py +++ b/src/codex_wake/objective_ledger.py @@ -120,6 +120,8 @@ def _view(self, state, now, admitted=None): original_deadline=state['original_deadline'], correction_cycles=state['correction_cycles'], goal_aliases=state.get('goal_aliases',{}), + request_binding=state.get('request_binding'), + request_sessions=state.get('request_sessions',{}), authorization_history=state['authorization_history'], remaining_seconds=max(0, _epoch(config['objective_deadline']) - max(now, state.get('observed_at', now))), actors={key: dict(value, tokens_remaining=max(0, value['reservation'] - value['used']), @@ -167,6 +169,7 @@ def reserve(self, actor_id, tokens, seconds, *, now): with self._transaction() as state: self._observe(state, now) view = self._view(state, now) + if state.get('request_binding'):raise ValueError('request transport owns accounting; lifecycle reservation refused') actor = state['actors'].get(actor_id) if actor and actor.get('terms') is not None: if actor['terms'] != [tokens, seconds]: @@ -212,6 +215,7 @@ def adopt_active_goal(self, goal_id, token_budget, deadline, *, now): with self._transaction() as state: self._observe(state,now) view=self._view(state,now) + if state.get('request_binding'):raise ValueError('request transport owns accounting; native goal reservation refused') alias=state.get('goal_aliases',{}).get(goal_id) actor_id=alias['actor_id'] if alias else goal_id offset=alias['preflight_tokens'] if alias else 0 @@ -235,6 +239,7 @@ def bind_goal(self, goal_id, actor_id, preflight_tokens, *, now): _count(preflight_tokens) with self._transaction() as state: self._observe(state, now) + if state.get('request_binding'):raise ValueError('request transport owns accounting; native alias refused') actor = state['actors'][actor_id] if actor['used'] < preflight_tokens: raise ValueError('preflight accounting must precede goal binding') @@ -253,6 +258,7 @@ def _record(self, actor_id, cumulative_tokens, settled=False, *, goal=False, now with self._transaction() as state: self._observe(state, now) self._view(state, now) + if state.get('request_binding'):raise ValueError('request transport owns accounting; sampled actor charge refused') if goal: binding = state.get('goal_aliases', {}).get(actor_id) if binding: @@ -269,6 +275,86 @@ def _record(self, actor_id, cumulative_tokens, settled=False, *, goal=False, now actor['settled'] = actor['settled'] or bool(settled) return self._view(state, now) + def bind_requests(self, binding, thread_prior_tokens, *, now): + """Select one authoritative request transport without adding allowance. + + Migration from live lifecycle actors is deliberately unsupported. The + imported prior charges remain in prior_tokens, with per-session floors. + """ + with self._transaction() as state: + self._view(state, now) + if state.get('request_binding') == binding: + return self._view(state, now) + if state.get('request_binding') or state['actors'] or state.get('goal_aliases'): + raise ValueError('request accounting requires explicit migration from existing actor accounting') + if sum(_count(value) for value in thread_prior_tokens.values()) != state['prior_tokens']: + raise ValueError('request binding must import all prior usage without reset') + state['request_binding'] = binding + config = state['configuration'] + state['request_sessions'] = {thread: dict(prior_tokens=value, + deadline=min(_epoch(config['objective_deadline']), _epoch(config['started_at'])+config['session_time_limit_seconds'])) + for thread, value in thread_prior_tokens.items()} + state['request_completions'] = {} + return self._view(state, now) + + def register_request_thread(self, thread_id, parent_thread_id, provenance, *, now): + with self._transaction() as state: + self._view(state,now) + if not state.get('request_binding') or parent_thread_id not in state['request_sessions']: + raise ValueError('qualified parent request binding required') + config=state['configuration'] + sessions=state['request_sessions'] + if thread_id not in sessions: + sessions[thread_id]=dict(prior_tokens=0,parent_thread_id=parent_thread_id,provenance=provenance, + deadline=min(_epoch(config['objective_deadline']),_epoch(now)+config['session_time_limit_seconds'])) + elif sessions[thread_id].get('parent_thread_id')!=parent_thread_id: + raise ValueError('request thread provenance changed') + return self._view(state,now) + + def reserve_request(self, actor_id, thread_id, tokens, seconds, *, enforce=True, now): + _count(tokens);_count(seconds) + with self._transaction() as state: + self._observe(state, now) + view = self._view(state, now) + if not state.get('request_binding') or not actor_id.startswith('request:'): + raise ValueError('authoritative request binding required') + if actor_id in state['actors']: + raise ValueError('an admitted attempt cannot be forwarded twice') + config = state['configuration'] + session = state['request_sessions'].setdefault(thread_id, dict(prior_tokens=0, + deadline=min(_epoch(config['objective_deadline']), _epoch(now)+config['session_time_limit_seconds']))) + used = session['prior_tokens'] + sum(max(a['used'], a['reservation']) if not a['settled'] else a['used'] + for a in state['actors'].values() if a.get('request_thread')==thread_id) + admitted = not enforce or (view['ready'] and tokens<=view['remaining_tokens'] + and used+tokens<=config['session_token_limit'] and seconds<=config['session_time_limit_seconds'] + and state['observed_at']+seconds<=session['deadline']) + if admitted: + state['actors'][actor_id]=dict(terms=[tokens,seconds],reservation=tokens,used=0, + settled=False,known=True,session_deadline=session['deadline'],request_thread=thread_id) + return self._view(state, now, admitted=admitted) + + def settle_request(self, actor_id, response_id, tokens, request_fingerprint, *, now): + """Unknown completion retains the reservation; duplicate exact proof is inert.""" + if tokens is not None:_count(tokens) + with self._transaction() as state: + self._observe(state, now) + self._view(state, now) + actor = state['actors'][actor_id] + if not state.get('request_binding') or not actor_id.startswith('request:'): + raise ValueError('authoritative request binding required') + prior = state['request_completions'].get(response_id) if response_id else None + proof = dict(actor_id=actor_id,tokens=tokens,fingerprint=request_fingerprint) + if prior == proof: + return self._view(state, now) + if (tokens is None or not response_id or prior is not None or actor['settled'] + or (actor['reservation'] and tokens>actor['reservation'])): + actor.update(known=False,settled=False) + if tokens is not None:actor['used']=max(actor['used'],tokens) + else: + state['request_completions'][response_id]=proof + actor.update(used=tokens,settled=True,known=True) + return self._view(state, now) + def correction(self, *, now, key=None, actor_id=None): with self._transaction() as state: self._observe(state, now) diff --git a/src/codex_wake/request_admission.py b/src/codex_wake/request_admission.py new file mode 100644 index 00000000..ae214955 --- /dev/null +++ b/src/codex_wake/request_admission.py @@ -0,0 +1,769 @@ +"""Explicit host-selected HTTP Responses admission; no provider is assumed qualified. + +The operator supplies an exact reviewed provider contract digest. Bounds are the +provider's enforced maxima for the WHOLE request, never token-count estimates. +No configuration/renewal of objective allowances occurs in this adapter. +""" + +import hashlib +import hmac +import secrets +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from http.client import HTTPConnection, HTTPSConnection +import json +import math +import os +from pathlib import Path +import time +import threading +import socket +from urllib.parse import urlsplit +from urllib.request import Request +from uuid import uuid4 + +from . import goal_supervision as supervision +from .goal_governance import native_operator, change, sync, snapshot +from .objective_ledger import ObjectiveLedger +from .session_permissions import PermissionDenied, Principal +from .records import WakeError + +MAX_BODY = 2 * 1024 * 1024 +MAX_RESPONSE = 8 * 1024 * 1024 + + +def contract_digest(value): + return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() + + +def _error(code, message): + return dict(error=dict(type=code, message=message)) + + +def _sse(text): + identifier = "governor-" + uuid4().hex + values = [ + dict(type="response.created", response=dict(id=identifier)), + dict( + type="response.output_item.done", + item=dict( + type="message", + role="assistant", + id=identifier, + content=[dict(type="output_text", text=text)], + ), + ), + dict( + type="response.completed", + response=dict( + id=identifier, + usage=dict( + input_tokens=0, + input_tokens_details=dict(cached_tokens=0), + output_tokens=0, + total_tokens=0, + ), + ), + ), + ] + return "".join( + "event: " + value["type"] + "\ndata: " + json.dumps(value) + "\n\n" + for value in values + ).encode() + + +class RequestGateway: + def __init__( + self, + root, + objective_id, + *, + upstream_url, + contract, + reviewed_contract_sha256, + thread_ids, + thread_prior_tokens=None, + request_timeout=2, + now=None, + upstream_headers=None, + client_token=None, + ): + from .lifecycle_budget import trusted_upper + + self.root = Path(root).absolute() + self.objective_id = objective_id + self.now = now or trusted_upper + self.thread_ids = tuple(thread_ids) + if not self.thread_ids or len(set(self.thread_ids)) != len(self.thread_ids): + raise ValueError("explicit native source thread binding required") + self.source_thread = self.thread_ids[0] + self.directory = supervision._directory(self.root) + for thread in self.thread_ids: + supervision._name(thread) + self.source_path = self.directory / ( + supervision._name(self.source_thread) + ".json" + ) + supervision._private(self.source_path) + source = json.loads(self.source_path.read_text()) + if ( + source["allocation_id"] != objective_id + or source["thread_id"] != self.source_thread + ): + raise ValueError("objective/native scope mismatch") + self.grant = source["grant"] + parsed = urlsplit(upstream_url) + if ( + parsed.scheme not in ("http", "https") + or parsed.username + or parsed.password + or parsed.query + or parsed.fragment + or not parsed.hostname + ): + raise ValueError( + "explicit provider URL without embedded credentials required" + ) + self.upstream_url = upstream_url + self.provider_address = parsed + self.upstream_headers = dict(upstream_headers or {}) + self.contract = json.loads(json.dumps(contract)) + self.contract_sha256 = contract_digest(contract) + self.eligible = ( + reviewed_contract_sha256 == self.contract_sha256 + and contract.get("version") == 1 + and contract.get("input_limit") == "provider_enforced_reject_before_billing" + and contract.get("output_limit") == "max_output_tokens_enforced" + and contract.get("usage") == "response.completed:fresh-v1" + and all( + type(contract.get(key)) is int and contract[key] > 0 + for key in ("max_input_tokens", "max_output_tokens") + ) + and all( + isinstance(contract.get(key), str) and contract[key].strip() + for key in ("provider", "model", "review") + ) + ) + if ( + type(request_timeout) not in (int, float) + or not math.isfinite(request_timeout) + or not 0 < request_timeout <= 30 + ): + raise ValueError("request timeout must be positive and at most 30 seconds") + self.timeout = request_timeout + if not self.eligible:raise ValueError('selected provider contract is unsupported or unreviewed') + self.ledger = ObjectiveLedger(self.root, objective_id) + view = self.ledger.inspect(now=self.now()) + if source.get("token_accounting") != "fresh": + raise ValueError("gateway requires qualified fresh input/output accounting") + if ( + view["objective_token_limit"] != source["token_limit"] + or view["objective_deadline"] != source["deadline"] + ): + raise ValueError( + "gateway must retain the existing objective limits/deadline" + ) + prior = thread_prior_tokens or {self.source_thread: source["used_tokens"]} + if set(prior) != set(self.thread_ids): + raise ValueError( + "every initial native thread needs an explicit prior-usage binding" + ) + if ( + not view.get("request_binding") + and sum(prior.values()) != source["used_tokens"] + ): + raise ValueError("imported prior usage differs from qualified source") + self.binding = dict( + version=1, + owner="http-responses", + scope=objective_id, + source_thread_id=self.source_thread, + upstream_url_sha256=hashlib.sha256(upstream_url.encode()).hexdigest(), + contract_sha256=self.contract_sha256, + accounting="fresh", + thread_ids=list(self.thread_ids), + ) + self.ledger.bind_requests(self.binding, prior, now=self.now()) + token_path = self.directory / ( + "request-gateway-" + + hashlib.sha256(objective_id.encode()).hexdigest() + + ".token" + ) + if token_path.exists(): + supervision._private(token_path) + self.client_token = token_path.read_text() + if client_token is not None and not hmac.compare_digest( + client_token, self.client_token + ): + raise ValueError("existing gateway client authority differs") + else: + self.client_token = client_token or secrets.token_hex(32) + if ( + not isinstance(self.client_token, str) + or len(self.client_token) < 32 + or "\n" in self.client_token + ): + raise ValueError( + "private gateway client token must contain at least 32 characters" + ) + descriptor = os.open( + token_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600 + ) + with os.fdopen(descriptor, "w") as target: + target.write(self.client_token) + target.flush() + os.fsync(target.fileno()) + # Hand off the accounting source, not the operator's enabled choice. + with ( + supervision._lock(self.directory, "maintenance"), + supervision._lock(self.directory, supervision._name(self.source_thread)), + ): + source = json.loads(self.source_path.read_text()) + authority = dict( + root=str(self.root), + objective_id=objective_id, + contract_sha256=self.contract_sha256, + owner="http-responses", + source_thread_id=self.source_thread, + gateway_token_file=str(token_path), + mode="hard_admission" if self.eligible else "unsupported", + coverage="selected HTTP request path only", + ) + source["request_accounting"] = authority + if source.get("allocation_path"): + path = Path(source["allocation_path"]) + with supervision._lock(path.parent, path.stem): + supervision._private(path) + allocation = json.loads(path.read_text()) + allocation["request_accounting"] = authority + supervision._save(path, allocation) + supervision._save(self.source_path, source) + + def status(self): + current = snapshot(self.root, self.source_thread) + view = self.ledger.inspect(now=self.now()) + mode = ( + "inactive" + if current.get("enabled") is False + else ("hard_admission" if view["accounting_known"] else "degraded") + if self.eligible + else "unsupported" + ) + return dict( + mode=mode, + scope=self.objective_id, + source_thread_id=self.source_thread, + contract_sha256=self.contract_sha256, + reviewed_provider_contract=self.eligible, + request_path="HTTP POST /v1/responses only; WebSocket/compact/bypass paths are not qualified", + governance=current, + accounting=self.ledger.inspect(now=self.now()), + provider_qualification="host-reviewed contract; native path qualification is separate", + ) + + def _context(self, body): + raw = body.get("client_metadata", {}).get("x-codex-turn-metadata") + metadata = json.loads(raw) if isinstance(raw, str) else raw + if not isinstance(metadata, dict): + raise ValueError("native request metadata required") + thread = metadata.get("thread_id") + supervision._name(thread) + if metadata.get("session_id", thread) != thread or not metadata.get("turn_id"): + raise ValueError("native request identity differs") + if thread not in self.thread_ids: + self._inherit_thread(thread) + return thread, dict( + threadId=thread, + sessionId=thread, + callId="gateway-" + uuid4().hex, + **{"x-codex-turn-metadata": dict(metadata, session_id=thread)}, + ) + + def _inherit_thread(self, thread): + """Native session metadata proves ancestry before a child's first usage.""" + view = self.ledger.inspect(now=self.now()) + if thread in view["request_sessions"]: + return + sessions = Path(self.grant["codex_home"]) / "sessions" + metadata = {} + started = time.monotonic() + size = 0 + for count, path in enumerate(sessions.rglob("*.jsonl"), 1): + if count > 4096 or time.monotonic() - started > 2: + raise ValueError("native ancestry inventory bound exceeded") + if ( + path.is_symlink() + or path.resolve() != path.absolute() + or path.stat().st_uid != os.geteuid() + ): + raise ValueError("native ancestry source is not owned/canonical") + with path.open("rb") as source: + raw = source.readline(65537) + size += len(raw) + if len(raw) > 65536 or size > 8 * 1024 * 1024: + raise ValueError("native ancestry metadata bound exceeded") + row = json.loads(raw) + if row.get("type") != "session_meta": + continue + item = row["payload"] + identity = item.get("id") + if identity in metadata: + raise ValueError("native ancestry identity ambiguous") + metadata[identity] = (item, path) + pending = [] + identity = thread + seen = set() + while identity not in view["request_sessions"]: + if identity in seen or len(seen) > 32 or identity not in metadata: + raise ValueError("unregistered native request thread") + seen.add(identity) + item, path = metadata[identity] + with path.open("rb") as native_source: + history = native_source.read(8 * 1024 * 1024 + 1) + if len(history) > 8 * 1024 * 1024 or any( + json.loads(line).get("type") == "token_usage_record" + for line in history.splitlines() + if line.strip() + ): + raise ValueError( + "pre-existing descendant usage requires explicit prior-usage binding, not a new allowance" + ) + source = item.get("source", {}) + spawn = ( + source.get("subagent", {}).get("thread_spawn", {}) + if isinstance(source, dict) + else {} + ) + parent = item.get("parent_thread_id") or spawn.get("parent_thread_id") + supervision._name(parent) + pending.append((identity, parent, contract_digest(item))) + identity = parent + for child, parent, proof in reversed(pending): + self.ledger.register_request_thread( + child, + parent, + dict(source="native_session_meta", sha256=proof), + now=self.now(), + ) + + def _control(self, thread, context): + source = json.loads(self.source_path.read_text()) + native = dict(source, thread_id=thread) + for enabled in (False, True): + try: + proof = native_operator(self.root, native, context, enabled) + except PermissionDenied: + continue + result = change( + self.root, + self.source_thread, + enabled=enabled, + principal=Principal(self.source_thread, "operator"), + allocation_id=self.objective_id, + ) + return _sse( + "Governor-generated acknowledgement; no model inference or provider usage. " + + result["message"] + + " Verified native operator turn " + + proof["turn_id"] + + "." + ) + return None + + def _notice(self, receipt, reason, request_fingerprint): + receipt["request_admission_event"] = dict( + reason=reason, fingerprint=request_fingerprint + ) + supervision._record_supervision_notice(self.source_path, receipt, "admission") + supervision._save(self.source_path, receipt) + return receipt.get("notice_ids", {}).get("admission") + + def execute(self, body): + """One incoming attempt -> at most one reserved upstream attempt. + + Public transport seam used by HTTP and resource planning; caller bodies + cannot select ledger, policy, provider endpoint, bounds or native grant. + """ + thread, context = self._context(body) + control = self._control(thread, context) + if control is not None: + return 200, "text/event-stream", control + fingerprint = contract_digest(body) + actor = "request:" + uuid4().hex + notice = None + with supervision._lock(self.directory, supervision._name(self.source_thread)): + supervision._private(self.source_path) + receipt = json.loads(self.source_path.read_text()) + inactive = sync(receipt) + if not inactive and ( + not self.eligible + or body.get("model") != self.contract["model"] + or body.get("stream") is not True + or body.get("previous_response_id") + ): + return ( + 501, + "application/json", + json.dumps( + _error( + "governor_path_unsupported", + "Selected provider bound contract or HTTP request path is unqualified. Use existing authorized manual work or a reviewed supported provider; no downgrade or renewal.", + ) + ).encode(), + ) + goal, active = supervision._native(self.grant, thread) + if active != [context["x-codex-turn-metadata"]["turn_id"]]: + raise ValueError("request is not the exact current native turn") + outgoing = dict(body) + bound = ( + 0 + if inactive + else self.contract["max_input_tokens"] + + self.contract["max_output_tokens"] + ) + seconds = math.ceil(self.timeout) + reserved = self.ledger.reserve_request( + actor, thread, bound, seconds, enforce=not inactive, now=self.now() + ) + if not reserved["admitted"]: + notice = self._notice( + receipt, "objective_or_session_allowance_unavailable", fingerprint + ) + result = ( + 429, + "application/json", + json.dumps( + _error( + "governor_admission_denied", + "Request denied before provider submission. Objective/session resources are unavailable. Say Deactivate governor in the native conversation; no inference is needed for that control.", + ) + ).encode(), + ) + upstream = None + else: + if not inactive: + outgoing["max_output_tokens"] = self.contract["max_output_tokens"] + address = self.provider_address + connection = ( + HTTPSConnection if address.scheme == "https" else HTTPConnection + )(address.hostname, address.port, timeout=self.timeout) + expired = threading.Event() + socket_ref = {} + + def expire(): + expired.set() + active_socket = socket_ref.get("socket") + if active_socket is not None: + try: + active_socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass + + request_timer = threading.Timer(self.timeout, expire) + request_timer.daemon = True + request_timer.start() + try: + connection.connect() + socket_ref["socket"] = connection.sock + if expired.is_set(): + raise TimeoutError( + "request wall-clock allowance expired before send" + ) + # request() completes the one socket submission; response headers + # are awaited outside the control lock so disable stays available. + connection.request( + "POST", + address.path or "/", + body=json.dumps(outgoing).encode(), + headers=dict( + self.upstream_headers, + **{ + "Content-Type": "application/json", + "Accept": "text/event-stream", + }, + ), + ) + upstream = connection + except Exception: + request_timer.cancel() + connection.close() + self.ledger.settle_request( + actor, None, None, fingerprint, now=self.now() + ) + return ( + 502, + "application/json", + json.dumps( + _error( + "governor_completion_unknown", + "Provider outcome unknown; reservation retained.", + ) + ).encode(), + ) + if notice: + from .goal_notices import present_notice + + present_notice(self.root, notice) + if upstream is None: + return result + try: + with upstream.getresponse() as response_stream: + if response_stream.status != 200: + raise ValueError("provider did not return a completion") + raw = response_stream.read(MAX_RESPONSE + 1) + if expired.is_set(): + raise TimeoutError("request wall-clock allowance expired") + if len(raw) > MAX_RESPONSE: + raise ValueError("provider response exceeds protocol bound") + completions = [ + json.loads(line[5:].strip()) + for line in raw.decode().splitlines() + if line.startswith("data:") and line[5:].strip() != "[DONE]" + ] + completed = [ + row["response"] + for row in completions + if row.get("type") == "response.completed" + ] + if len(completed) != 1: + raise ValueError("one attributable completion required") + response = completed[0] + usage = response["usage"] + values = [ + usage["input_tokens"], + usage["input_tokens_details"]["cached_tokens"], + usage["output_tokens"], + usage["total_tokens"], + ] + if ( + any(type(value) is not int or value < 0 for value in values) + or values[1] > values[0] + or values[3] != values[0] + values[2] + ): + raise ValueError("provider usage unqualified") + if not isinstance(response.get("id"), str) or not response["id"]: + raise ValueError("completion identity unavailable") + if not inactive and ( + values[0] > self.contract["max_input_tokens"] + or values[2] > self.contract["max_output_tokens"] + ): + self.ledger.settle_request( + actor, + response["id"], + values[0] - values[1] + values[2], + fingerprint, + now=self.now(), + ) + raise ValueError("reviewed provider bounds violated") + settled = self.ledger.settle_request( + actor, + response["id"], + values[0] - values[1] + values[2], + fingerprint, + now=self.now(), + ) + if not inactive and not settled["accounting_known"]: + raise ValueError( + "provider completion conflicts with prior evidence" + ) + return 200, "text/event-stream", raw + except Exception: + self.ledger.settle_request(actor, None, None, fingerprint, now=self.now()) + if inactive and "raw" in locals(): + return response_stream.status, "text/event-stream", raw + return ( + 502, + "application/json", + json.dumps( + _error( + "governor_completion_unknown", + "Provider completion/usage unavailable or conflicting; reservation retained.", + ) + ).encode(), + ) + finally: + request_timer.cancel() + upstream.close() + + def server(self, host="127.0.0.1", port=0): + if host != "127.0.0.1": + raise ValueError("gateway binds loopback only") + gateway = self + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *args): + pass + + def _send(self, status, kind, data): + self.send_response(status) + self.send_header("Content-Type", kind) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + try: + self.wfile.write(data) + except (BrokenPipeError, ConnectionResetError): + pass + + def _authorized(self): + if hmac.compare_digest( + self.headers.get("Authorization", ""), + "Bearer " + gateway.client_token, + ): + return True + self._send( + 401, + "application/json", + json.dumps( + _error( + "gateway_authentication_required", + "Host-selected gateway credential required.", + ) + ).encode(), + ) + return False + + def do_GET(self): + if not self._authorized(): + return + if self.path != "/status": + self.send_error(404) + return + self._send( + 200, "application/json", json.dumps(gateway.status()).encode() + ) + + def do_POST(self): + if not self._authorized(): + return + if self.path != "/v1/responses": + self.send_error(501) + return + try: + if self.headers.get("Transfer-Encoding"): + raise ValueError("chunked request unsupported") + length = int(self.headers.get("Content-Length", "0")) + if not 0 < length <= MAX_BODY: + raise ValueError("request body out of protocol bounds") + body = json.loads(self.rfile.read(length)) + result = gateway.execute(body) + self._send(*result) + except (OSError, ValueError, KeyError, TypeError, WakeError) as error: + self._send( + 403 if isinstance(error, PermissionDenied) else 400, + "application/json", + json.dumps( + _error("governor_request_unqualified", str(error)[:160]) + ).encode(), + ) + + server = ThreadingHTTPServer((host, port), Handler) + self.bind_listener(f"http://{host}:{server.server_port}") + return server + + def bind_listener(self, url): + """Pin a host-owned listener (including an authenticated bootstrap delegate).""" + parsed = urlsplit(url) + if ( + parsed.scheme != "http" + or parsed.hostname != "127.0.0.1" + or not parsed.port + or parsed.path + or parsed.query + or parsed.fragment + or parsed.username + ): + raise ValueError("owned loopback listener origin required") + with ( + supervision._lock(self.directory, "maintenance"), + supervision._lock(self.directory, supervision._name(self.source_thread)), + ): + source = json.loads(self.source_path.read_text()) + source["request_accounting"]["gateway_url"] = url + if source.get("allocation_path"): + path = Path(source["allocation_path"]) + with supervision._lock(path.parent, path.stem): + allocation = json.loads(path.read_text()) + allocation["request_accounting"] = source["request_accounting"] + supervision._save(path, allocation) + supervision._record_supervision_notice( + self.source_path, source, "request_activation" + ) + supervision._save(self.source_path, source) + event = source.get("notice_ids", {}).get("request_activation") + if event: + from .goal_notices import present_notice + + present_notice(self.root, event) + + +def qualify_gateway(root, profile, thread): + """Fresh selected-provider inspection; no claim about bypass transports.""" + import tomllib + from urllib.request import urlopen + + path = supervision._directory(root) / (supervision._name(thread) + ".json") + supervision._private(path) + receipt = json.loads(path.read_text()) + binding = receipt.get("request_accounting") or {} + url = binding.get("gateway_url") + if not url: + raise ValueError("no selected HTTP request gateway binding") + home = Path(receipt["grant"]["codex_home"]) + config_path = home / "config.toml" + config = tomllib.loads(config_path.read_text()) + provider = config.get("model_provider") + selected = config.get("model_providers", {}).get(provider, {}) + if ( + selected.get("base_url", "").rstrip("/") != url + "/v1" + or selected.get("supports_websockets") is not False + or selected.get("wire_api") != "responses" + ): + raise ValueError( + "selected native provider is not the qualified HTTP gateway path" + ) + native = supervision._request( + receipt["grant"], "thread/read", dict(threadId=thread, includeTurns=False) + )["thread"] + if native.get("modelProvider") != provider: + raise ValueError( + "native thread selected provider differs from host configuration" + ) + token_path = Path(binding["gateway_token_file"]) + expected = supervision._directory(root) / ( + "request-gateway-" + + hashlib.sha256(receipt["allocation_id"].encode()).hexdigest() + + ".token" + ) + if token_path != expected: + raise ValueError("gateway credential scope differs") + supervision._private(token_path) + request = Request( + url + "/status", headers={"Authorization": "Bearer " + token_path.read_text()} + ) + with urlopen(request, timeout=1) as response: + raw = response.read(1024 * 1024 + 1) + if len(raw) > 1024 * 1024: + raise ValueError("gateway status exceeds inspection bound") + status = json.loads(raw) + if ( + status.get("scope") != receipt["allocation_id"] + or status.get("source_thread_id") != thread + or status.get("contract_sha256") != binding.get("contract_sha256") + or status.get("mode") != "hard_admission" + or not status.get("reviewed_provider_contract") + or not status.get("accounting", {}).get("ready") + ): + raise ValueError("gateway contract/scope/accounting is unavailable") + return dict( + mode="hard_admission", + admitted=True, + reason="qualified_selected_http_request_path", + scope=receipt["allocation_id"], + gateway=url, + contract_sha256=status["contract_sha256"], + config_sha256=hashlib.sha256(config_path.read_bytes()).hexdigest(), + accounting=status["accounting"], + maximum_token_overshoot=0, + coverage="Only requests through this reviewed HTTP provider path. WS, compact, alternative providers and bypass routes remain unsupported.", + message="The selected HTTP request path reserves enforceable reviewed provider bounds before each send. This is not qualification of arbitrary native/provider paths.", + ) diff --git a/tests/test_governor_deactivation.py b/tests/test_governor_deactivation.py index 8a580953..c3afd1c5 100644 --- a/tests/test_governor_deactivation.py +++ b/tests/test_governor_deactivation.py @@ -312,8 +312,10 @@ def context(member): native["siblings"][child]["calls"].clear() native["siblings"][later]["calls"].clear() poll(root) - self.assertEqual(native["siblings"][child]["calls"], []) - self.assertEqual(native["siblings"][later]["calls"], []) + self.assertFalse(any(method in ("thread/goal/set","turn/interrupt","turn/steer","turn/start") + for method,_ in native["siblings"][child]["calls"])) + self.assertFalse(any(method in ("thread/goal/set","turn/interrupt","turn/steer","turn/start") + for method,_ in native["siblings"][later]["calls"])) def test_inflight_poll_precedes_deactivation_and_queued_polls_cannot_intervene( self, diff --git a/tests/test_request_admission.py b/tests/test_request_admission.py new file mode 100644 index 00000000..f88307d4 --- /dev/null +++ b/tests/test_request_admission.py @@ -0,0 +1,459 @@ +"""HTTP request-boundary admission: a denied request never reaches the provider.""" + +from concurrent.futures import ThreadPoolExecutor +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import tempfile +import threading +import unittest +from urllib.request import Request, urlopen +from urllib.error import HTTPError +from unittest.mock import patch + +import test_goal_supervision as fixtures +from codex_wake.goal_supervision import enroll +from codex_wake.objective_ledger import ObjectiveLedger + + +class RequestAdmissionTests(unittest.TestCase): + fixture = fixtures.GoalSupervisionTests.fixture + controller = fixtures.GoalSupervisionTests.controller + + def test_concurrent_requests_share_allowance_before_provider_submission(self): + from codex_wake.request_admission import RequestGateway, contract_digest + + with tempfile.TemporaryDirectory() as directory: + root, policy, log, native, _ = self.fixture(directory) + with ( + self.controller(root), + patch( + "codex_wake.lifecycle_budget.trusted_upper", + return_value=fixtures.NOW, + ), + ): + enroll(root, policy, fixtures.THREAD, 110, 120) + native["notices"] = {} + ledger = ObjectiveLedger(root, "allocation-1") + ledger.configure( + dict( + revision=1, + objective_token_limit=110, + objective_time_limit_seconds=120, + session_token_limit=110, + session_time_limit_seconds=120, + max_correction_cycles=0, + started_at=fixtures.NOW, + objective_deadline=fixtures.NOW + 120, + ), + "fixture-operator", + prior_tokens=40, + now=fixtures.NOW, + ) + arrived, release = threading.Event(), threading.Event() + calls = [] + + class Provider(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_POST(self): + calls.append( + json.loads( + self.rfile.read(int(self.headers["Content-Length"])) + ) + ) + arrived.set() + release.wait(5) + response = dict( + id="response-one", + usage=dict( + input_tokens=8, + input_tokens_details=dict(cached_tokens=3), + output_tokens=2, + total_tokens=10, + ), + ) + data = ( + "data: " + + json.dumps( + dict(type="response.completed", response=response) + ) + + "\n\n" + ).encode() + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + provider = ThreadingHTTPServer(("127.0.0.1", 0), Provider) + thread = threading.Thread(target=provider.serve_forever, daemon=True) + thread.start() + self.addCleanup(provider.server_close) + self.addCleanup(provider.shutdown) + contract = dict( + version=1, + provider="owned-inert-fixture", + model="fixture-model", + max_input_tokens=40, + max_output_tokens=10, + input_limit="provider_enforced_reject_before_billing", + output_limit="max_output_tokens_enforced", + usage="response.completed:fresh-v1", + review="Reviewed inert fixture maximum usage: input 8, output 2; provider-free contract proof.", + ) + gateway = RequestGateway( + root, + "allocation-1", + upstream_url=f"http://127.0.0.1:{provider.server_port}/v1/responses", + contract=contract, + reviewed_contract_sha256=contract_digest(contract), + thread_ids=[fixtures.THREAD], + now=lambda: fixtures.NOW, + ) + server = gateway.server() + threading.Thread(target=server.serve_forever, daemon=True).start() + self.addCleanup(server.server_close) + self.addCleanup(server.shutdown) + + def submit(turn="work"): + body = dict( + model="fixture-model", + stream=True, + input=[], + client_metadata={ + "x-codex-turn-metadata": json.dumps( + dict( + thread_id=fixtures.THREAD, + session_id=fixtures.THREAD, + turn_id=turn, + ) + ) + }, + ) + request = Request( + f"http://127.0.0.1:{server.server_port}/v1/responses", + data=json.dumps(body).encode(), + headers={ + "Content-Type": "application/json", + "Authorization": "Bearer " + gateway.client_token, + }, + ) + try: + with urlopen(request, timeout=8) as response: + return response.status, response.read() + except HTTPError as error: + return error.code, error.read() + + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(submit) + self.assertTrue(arrived.wait(3)) + second = pool.submit(submit) + denied = second.result(timeout=3) + self.assertEqual(denied[0], 429, denied) + self.assertEqual(len(calls), 1) + release.set() + self.assertEqual(first.result(timeout=5)[0], 200) + self.assertEqual(calls[0]["max_output_tokens"], 10) + view = ledger.inspect(now=fixtures.NOW) + self.assertEqual(view["tokens_used"], 47) + self.assertEqual(view["tokens_reserved"], 0) + + # Native history is no longer the accounting authority after binding. + from codex_wake.goal_supervision import poll + + log.unlink() + sampled = poll(root)[0] + self.assertEqual(sampled["used_tokens"], 47) + self.assertEqual(sampled["state"], "armed") + self.assertTrue(native["notices"]) + self.assertTrue( + any( + "denied before provider submission" in event["text"] + for event in native["notices"].values() + ) + ) + # Same response id on another actual attempt is ambiguous billing, + # not evidence that the second reservation may be refunded. + duplicate = submit() + self.assertEqual(duplicate[0], 502) + unknown = ledger.inspect(now=fixtures.NOW) + self.assertFalse(unknown["accounting_known"]) + self.assertGreater(unknown["tokens_reserved"], 0) + attempts = len(calls) + native["user_text"] = '"Deactivate governor"' + self.assertEqual(submit()[0], 429) + self.assertEqual(len(calls), attempts) + # The actual whole native user message bypasses inference admission. + native["active"] = ["operator-control"] + native["user_text"] = "Deactivate governor" + control = submit("operator-control") + self.assertEqual(control[0], 200, control) + self.assertIn(b"Governor-generated acknowledgement", control[1]) + self.assertEqual(len(calls), attempts) + self.assertEqual(gateway.status()["mode"], "inactive") + before = ledger.inspect(now=fixtures.NOW) + self.assertEqual(before["tokens_used"], unknown["tokens_used"]) + self.assertEqual(before["tokens_reserved"], unknown["tokens_reserved"]) + native["user_text"] = "A normal operator follow-up" + self.assertEqual(submit("operator-control")[0], 200) + self.assertEqual(len(calls), attempts + 1) + + def test_heartbeat_cannot_extend_request_wallclock_or_refund_missing_completion( + self, + ): + from codex_wake.request_admission import RequestGateway, contract_digest + import time + + with tempfile.TemporaryDirectory() as directory: + root, policy, _, native, _ = self.fixture(directory) + with ( + self.controller(root), + patch( + "codex_wake.lifecycle_budget.trusted_upper", + return_value=fixtures.NOW, + ), + ): + enroll(root, policy, fixtures.THREAD, 110, 120) + ledger = ObjectiveLedger(root, "allocation-1") + ledger.configure( + dict( + revision=1, + objective_token_limit=110, + objective_time_limit_seconds=120, + session_token_limit=110, + session_time_limit_seconds=120, + max_correction_cycles=0, + started_at=fixtures.NOW, + objective_deadline=fixtures.NOW + 120, + ), + "fixture-operator", + prior_tokens=40, + now=fixtures.NOW, + ) + finish = threading.Event() + beats = [] + + class Heartbeat(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_POST(self): + self.rfile.read(int(self.headers["Content-Length"])) + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.end_headers() + try: + stop_at = time.monotonic() + 2.5 + while not finish.wait(0.01) and time.monotonic() < stop_at: + self.wfile.write( + b": heartbeat without a completion\n\n" + ) + self.wfile.flush() + beats.append(1) + except (BrokenPipeError, ConnectionResetError): + pass + + provider = ThreadingHTTPServer(("127.0.0.1", 0), Heartbeat) + threading.Thread(target=provider.serve_forever, daemon=True).start() + self.addCleanup(provider.server_close) + self.addCleanup(provider.shutdown) + self.addCleanup(finish.set) + contract = dict( + version=1, + provider="owned-inert-fixture", + model="fixture-model", + max_input_tokens=40, + max_output_tokens=10, + input_limit="provider_enforced_reject_before_billing", + output_limit="max_output_tokens_enforced", + usage="response.completed:fresh-v1", + review="Heartbeat fixture has zero inference and no completed usage.", + ) + gateway = RequestGateway( + root, + "allocation-1", + upstream_url=f"http://127.0.0.1:{provider.server_port}/v1/responses", + contract=contract, + reviewed_contract_sha256=contract_digest(contract), + thread_ids=[fixtures.THREAD], + request_timeout=0.2, + now=lambda: fixtures.NOW, + ) + body = dict( + model="fixture-model", + stream=True, + input=[], + client_metadata={ + "x-codex-turn-metadata": json.dumps( + dict( + thread_id=fixtures.THREAD, + session_id=fixtures.THREAD, + turn_id="work", + ) + ) + }, + ) + started = time.monotonic() + response = gateway.execute(body) + self.assertEqual(response[0], 502) + self.assertLess(time.monotonic() - started, 1.5) + self.assertGreater(len(beats), 3) + view = ledger.inspect(now=fixtures.NOW) + self.assertEqual(view["tokens_reserved"], 50) + self.assertEqual(view["tokens_used"], 40) + self.assertFalse(view["accounting_known"]) + + def test_native_descendant_inherits_scope_without_usage_and_session_spend_survives_retry( + self, + ): + from codex_wake.request_admission import RequestGateway, contract_digest + + with tempfile.TemporaryDirectory() as directory: + root, policy, log, native, _ = self.fixture(directory) + with ( + self.controller(root), + patch( + "codex_wake.lifecycle_budget.trusted_upper", + return_value=fixtures.NOW, + ), + ): + enroll(root, policy, fixtures.THREAD, 200, 120) + ledger = ObjectiveLedger(root, "allocation-1") + ledger.configure( + dict( + revision=1, + objective_token_limit=200, + objective_time_limit_seconds=120, + session_token_limit=70, + session_time_limit_seconds=120, + max_correction_cycles=0, + started_at=fixtures.NOW, + objective_deadline=fixtures.NOW + 120, + ), + "fixture-operator", + prior_tokens=40, + now=fixtures.NOW, + ) + calls = [] + + class Provider(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_POST(self): + self.rfile.read(int(self.headers["Content-Length"])) + calls.append(1) + response = dict( + id="response-" + str(len(calls)), + usage=dict( + input_tokens=8, + input_tokens_details=dict(cached_tokens=3), + output_tokens=2, + total_tokens=10, + ), + ) + data = ( + "data: " + + json.dumps( + dict(type="response.completed", response=response) + ) + + "\n\n" + ).encode() + self.send_response(200) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + provider = ThreadingHTTPServer(("127.0.0.1", 0), Provider) + threading.Thread(target=provider.serve_forever, daemon=True).start() + self.addCleanup(provider.server_close) + self.addCleanup(provider.shutdown) + contract = dict( + version=1, + provider="owned-inert-fixture", + model="fixture-model", + max_input_tokens=40, + max_output_tokens=10, + input_limit="provider_enforced_reject_before_billing", + output_limit="max_output_tokens_enforced", + usage="response.completed:fresh-v1", + review="Reviewed inert provider input8 output2, no inference.", + ) + options = dict( + upstream_url=f"http://127.0.0.1:{provider.server_port}/v1/responses", + contract=contract, + reviewed_contract_sha256=contract_digest(contract), + thread_ids=[fixtures.THREAD], + now=lambda: fixtures.NOW, + ) + gateway = RequestGateway(root, "allocation-1", **options) + + def body(thread, turn="work"): + return dict( + model="fixture-model", + stream=True, + input=[], + client_metadata={ + "x-codex-turn-metadata": json.dumps( + dict(thread_id=thread, session_id=thread, turn_id=turn) + ) + }, + ) + + with self.assertRaisesRegex(ValueError, "unsupported or unreviewed"): + RequestGateway(root, "allocation-1", **dict(options, reviewed_contract_sha256="unknown")) + self.assertEqual(calls, []) + # Original40 + worst-case50 exceeds session70 despite objective200. + self.assertEqual(gateway.execute(body(fixtures.THREAD))[0], 429) + self.assertEqual(calls, []) + child = "00000000-0000-7000-8000-000000000290" + meta = json.loads(log.read_text().splitlines()[0]) + meta["payload"].update( + id=child, session_id=child, parent_thread_id=fixtures.THREAD + ) + (log.parent / "child-without-usage.jsonl").write_text( + json.dumps(meta) + "\n" + ) + native["siblings"] = { + child: dict( + goal=dict( + threadId=child, createdAt=fixtures.NOW, status="active" + ), + active=["work"], + calls=[], + ) + } + self.assertEqual( + [gateway.execute(body(child))[0] for _ in range(4)], + [200, 200, 200, 429], + ) + self.assertEqual(len(calls), 3) + view = ledger.inspect(now=fixtures.NOW) + self.assertEqual(view["tokens_used"], 61) + original_deadline = view["request_sessions"][child]["deadline"] + self.assertEqual( + view["request_sessions"][child]["parent_thread_id"], fixtures.THREAD + ) + restarted = RequestGateway(root, "allocation-1", **options) + self.assertEqual(restarted.execute(body(child))[0], 429) + self.assertEqual( + ledger.inspect(now=fixtures.NOW)["request_sessions"][child][ + "deadline" + ], + original_deadline, + ) + # Child control uses independently verified native text, and the + # root allocation choice governs children without enrollment usage. + native["siblings"][child].update( + active=["child-control"], user_text="Deactivate governor" + ) + control = restarted.execute(body(child, "child-control")) + self.assertEqual(control[0], 200) + self.assertEqual(len(calls), 3) + native["siblings"][child]["user_text"] = "Continue authorized repair" + self.assertEqual( + restarted.execute(body(child, "child-control"))[0], 200 + ) + self.assertEqual(len(calls), 4) + self.assertEqual(restarted.status()["mode"], "inactive") From fd4e2056d5359b36b0f5a8cd060567c9beb898d1 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 17:05:06 -0500 Subject: [PATCH 11/20] fix: preserve paused work and document request-path qualification (#354) --- docs/request-admission.md | 178 ++++++++++++++++++++++++++++ src/codex_wake/goal_governance.py | 2 +- src/codex_wake/goal_notices.py | 9 +- src/codex_wake/request_admission.py | 93 +++++++++++++-- tests/test_request_admission.py | 26 +++- 5 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 docs/request-admission.md diff --git a/docs/request-admission.md b/docs/request-admission.md new file mode 100644 index 00000000..8705d9f2 --- /dev/null +++ b/docs/request-admission.md @@ -0,0 +1,178 @@ +# Reviewed HTTP request admission + +Ticket #354 adds an explicit host-selected HTTP Responses gateway. It does not +qualify every native host, provider, or model route. No external provider contract +is currently supplied as qualified. Native transport acceptance uses an inert +local provider and is separate from external provider eligibility. + +The gateway is ordinary production transport/accounting code. A trusted host must +select a reviewed provider contract with enforceable maxima for the **entire** +accepted request input and generated output. There is no byte/token estimate. +The provider must reject excessive input before billing and enforce the injected +`max_output_tokens`, including every kind of billable generated output. Review +must substantiate those properties for the exact selected model/provider/version +and the `response.completed` usage schema. An operator's desired ceiling is not +provider-bound evidence. Unknown or unreviewed contracts fail configuration +explicitly; they cannot silently enable hard admission. + +A contract has these fields: + +```json +{ + "version": 1, + "provider": "EXACT REVIEWED PROVIDER", + "model": "EXACT MODEL", + "max_input_tokens": 100, + "max_output_tokens": 20, + "input_limit": "provider_enforced_reject_before_billing", + "output_limit": "max_output_tokens_enforced", + "usage": "response.completed:fresh-v1", + "review": "LOCATOR AND SCOPE OF ACTUAL REVIEWED BOUND EVIDENCE" +} +``` + +The numbers above illustrate the schema and are **not a qualified contract**. +The trusted host supplies the separately reviewed exact `contract_digest(contract)` +as `reviewed_contract_sha256`; models cannot select it, bounds, endpoint, ledger, +credential or purpose through a request body. Source/runtime/provider changes +require fresh contract qualification. A false provider guarantee cannot be made +true by a digest: this trust boundary must be reviewed before use. + +## Existing allowance and ownership + +First enroll an existing native goal and retain qualified original fresh usage. +Configure the existing `ObjectiveLedger` using its original authorized objective +configuration, approval, cap, absolute deadline and `prior_tokens`. Do not create +another objective or a new time window to gain allowance. The gateway constructor +requires the receipt's allocation ID, cap and deadline to agree with that ledger, +and requires imported per-thread prior usage to sum to the qualified source usage. + +`bind_requests` makes that ledger the authoritative accounting owner. An existing +lifecycle actor, native goal alias, or changed binding refuses attachment: a +live migration needs its own explicit reconciliation. Later lifecycle/session +reservations and sampled charges are refused rather than added on top of request +charges. This bounded version does not silently migrate those workflows. + +The source receipt and shared allocation retain `request_accounting` with the +owner, ledger scope, selected contract and listener. The sampled watcher reads +that ledger after handoff; it cannot overwrite request usage from a missing or +lagging native writer. The operator's enabled choice, prior usage, limits, +checkpoints and stop history remain intact. Listener attachment publishes a new +mode-bound `request_activation` notice to native model/operator surfaces without +starting a model turn. It does not reuse an earlier sampled activation event. + +## Host interface + +```python +from codex_wake.request_admission import RequestGateway, contract_digest + +gateway = RequestGateway( + root, existing_allocation_id, + upstream_url=reviewed_responses_url, + contract=reviewed_contract, + reviewed_contract_sha256=separately_reviewed_digest, + thread_ids=[verified_source_thread], + request_timeout=2, + client_token=host_private_bootstrap_token, +) +server = gateway.server() # loopback only; host runs serve_forever() +``` + +The constructor never configures, extends or resets the objective ledger. With +multiple initial threads, supply `thread_prior_tokens` for every initial thread; +all original charges must be accounted for. A fresh native descendant/reviewer +inherits its registered parent's scope automatically using bounded owned, +canonical native `session_meta` ancestry, before its first usage record exists. +Existing descendant usage needs an explicit prior-usage binding instead of a +fresh quota. An unrelated or ambiguous thread is refused before forwarding. + +The same ledger retains per-thread prior spending, all request actors' thread +identities, and original per-thread deadlines. Retries do not create another +session ceiling/window. Children share the objective's remaining allowance; the +host may later attach purpose/phase metadata to that durable thread mapping. +A model-supplied phase is never admission authority. + +`execute(body)` is the stable trusted host transport seam and returns +`(http_status, content_type, response_bytes)`. `server()` exposes authenticated +`POST /v1/responses` and `GET /status`. `bind_listener(origin)` supports an existing +authenticated host listener, including a provider-free warmup delegate. That host +must verify `Authorization: Bearer TOKEN` before calling `execute` or `status`. +The generated/supplied token persists separately in a private mode-0600 file; +its locator may appear in binding receipts but its value never does. Provide a +native provider `env_key` before launch for a bootstrap token. `upstream_headers` +are host-only credentials and are not copied from arbitrary incoming headers. + +Native configuration selects this listener's `/v1` base URL, `wire_api="responses"` +and `supports_websockets=false`. WebSocket, compact, previous-response shortcuts, +alternative providers and bypass routes remain unqualified. The native-generated +`client_metadata['x-codex-turn-metadata']`, exact current native turn, host credential, +and registered lineage bind requests to scope. This is a host authority boundary, +not hostile same-UID isolation. + +## Admission and completion + +Before its single upstream submission, each incoming attempt atomically reserves +`max_input_tokens + max_output_tokens` against objective and cumulative session +limits. HTTP retries/fallback attempts through this route each need a fresh +reservation. No implicit retry or redirect follows an upstream failure. Output +limits are inserted by the gateway, never trusted from model arguments. Protocol +limits are 2 MiB per request and 8 MiB per buffered response; these are transport +limits, not token estimates. + +The existing receipt/scope locks serialize admission/submission with deactivation. +Response headers/body are awaited outside those locks so a hanging stream cannot +hold the control. An independent monotonic wall-clock timer closes the upstream +socket, including a continuously heartbeating stream. Admission requires that +this full request interval fits the original objective/session deadline. Client +cancellation cannot leave an unbounded upstream reader. Disconnecting cannot +promise that remote inference itself was recalled; its reservation remains. + +A qualified completion settles `input_tokens - cached_tokens + output_tokens` +from attributable provider evidence. Exact repeated settlement for the same +immutable attempt is idempotent. Reusing a response ID for a different actual +attempt is ambiguous billing and retains that attempt's reservation. Missing, +conflicting, out-of-bound or incomplete usage never creates a refund; accounting +becomes unknown and future governed admission holds. Actual known usage floors +are retained. Settlement and recovery must use the same immutable request +identity/fingerprint; inventing another request cannot reconcile the old one. + +Denials are returned before forwarding and emit durable no-turn intervention +notices. They pause future continuation of the exact registered native goal, +retaining its identity and readback proof, without interrupting independently +admitted requests. A failed/unverified pause is reported explicitly. They do not require another model request to become operator-visible. +Native append, model context, UI rendering and human reading remain distinct +proof boundaries. No live/provider or UI guarantee follows from unit tests. + +## Deactivation at zero allowance + +Before request admission, the gateway independently reads the exact native current +user message. The whole message `Deactivate governor` invokes the existing scoped +operator control mechanically; quoted/tool/body text cannot authorize it. The +adapter restores a previously verified registered-goal pause if native operator +input automatically activated that goal. Pause verification is retained; a pause +failure cannot veto durable deactivation and remains explicit. The response is +explicitly governor-generated SSE with zero provider inference and +zero provider usage. It is not a model decision or another permission prompt. +`Reactivate governor` uses the same verification and does not resume/renew work. + +The current durable disabled choice bypasses governor budget denials for that +scope, including registered children. Original usage/reservations remain; new +attributable passthrough usage remains recorded without manufacturing allowance. +Previously issued/in-flight decisions cannot be recalled; no new admission +intervention is issued while disabled. Protocol, authentication and independent +platform controls still apply. The authenticated operator API remains callable +when the model/native request route is disconnected. + +## Qualification + +The existing host preflight inspects this selected native provider configuration, +the thread's actual `modelProvider`, the private binding, live authenticated gateway +status, reviewed contract and current ledger. It can qualify only this declared +HTTP request path. Unsupported paths keep the original explicit unsupported result. +Exhausted/unknown accounting remains a separate resource outcome. A fresh same-scope +disabled operator choice takes precedence over an old hard-mode request, even at +zero remaining allowance; it is an explicit mode change, not a silent downgrade. + +Focused proof uses `tests/test_request_admission.py` with real HTTP dispatch, +controlled native transport/usage and no external inference. Actual pinned-native +and stock-frontend proof is a separate acceptance artifact owned by integration. diff --git a/src/codex_wake/goal_governance.py b/src/codex_wake/goal_governance.py index 7aee0633..79df0805 100644 --- a/src/codex_wake/goal_governance.py +++ b/src/codex_wake/goal_governance.py @@ -207,7 +207,7 @@ def change( "independent platform controls remain. Accounting and checkpoints are preserved; " "no work is resumed and no resources renewed." if not enabled - else "Governor reactivated in this scope in sampled_stop mode; no work resumed or resources renewed." + else f"Governor reactivated in this scope in {first.get('request_accounting',{}).get('mode','sampled_stop')} mode; no work resumed or resources renewed." ), ) diff --git a/src/codex_wake/goal_notices.py b/src/codex_wake/goal_notices.py index d96e7f6b..42d5bb30 100644 --- a/src/codex_wake/goal_notices.py +++ b/src/codex_wake/goal_notices.py @@ -75,7 +75,8 @@ def record_notice(root, receipt, kind, *, controller=None): receipt=dict(receipt,active_stop_id=recovery['stop_episode_id'],reason=recovery['reason']) identity = dict(thread_id=receipt['thread_id'], allocation_id=receipt['allocation_id'], generation=receipt['generation'], enrollment_revision=receipt.get('enrollment_revision', 1), - kind=kind,governor_revision=receipt.get('governor_revision',0)) + kind=kind,governor_revision=receipt.get('governor_revision',0), + governor_mode='inactive' if receipt.get('governor_enabled') is False else receipt.get('request_accounting',{}).get('mode','sampled_stop')) if kind in ('stop', 'recovery'): identity.update(stop_episode=receipt.get('active_stop_id') or receipt.get('stop_episode_id') or [stop.get('boot_id'), stop.get('observed_boottime'), receipt.get('pause_epoch', 0)], @@ -136,8 +137,8 @@ def render_notice(event): f"[External supervision event {event['event_id']}]") if kind in ('deactivation','reactivation'): text=('Governor inactive. Drop stale governor checkpoint/stop instructions. Independent platform controls remain. ' - 'Say "Reactivate governor" to enable sampled stopping. ' if kind=='deactivation' else - 'Governor reactivated. Mode sampled_stop; maximum overshoot is not bounded. Say "Deactivate governor" to disable. ') + 'Say "Reactivate governor" to enable the configured governor. ' if kind=='deactivation' else + f"Governor reactivated. Mode {identity.get('governor_mode','sampled_stop')}; independent controls remain. Say \"Deactivate governor\" to disable. ") text += (f"Governor revision {identity.get('governor_revision',0)} supersedes older notices; latest durable scope state is authoritative. " f"Scope {identity['allocation_id']}; session {identity['thread_id']}. " 'Accounting and checkpoints preserved; no resource renewal or work resume. ' @@ -250,7 +251,7 @@ def present_notice(root, event_id, *, adapter=None): if event['delivery']['attempts']==0: from .goal_governance import snapshot current=snapshot(root,event['identity']['thread_id']) - if (current['state']=='available' and current['revision']!=event['identity'].get('governor_revision',0)): + if (current['state']=='available' and (current['revision']!=event['identity'].get('governor_revision',0) or current['mode']!=event['identity'].get('governor_mode','sampled_stop'))): event.setdefault('historical',dict(original_text=event['text'])) mode=current['mode'] identity=event['identity'];observed=event['snapshot'] diff --git a/src/codex_wake/request_admission.py b/src/codex_wake/request_admission.py index ae214955..905d0d99 100644 --- a/src/codex_wake/request_admission.py +++ b/src/codex_wake/request_admission.py @@ -149,7 +149,8 @@ def __init__( ): raise ValueError("request timeout must be positive and at most 30 seconds") self.timeout = request_timeout - if not self.eligible:raise ValueError('selected provider contract is unsupported or unreviewed') + if not self.eligible: + raise ValueError("selected provider contract is unsupported or unreviewed") self.ledger = ObjectiveLedger(self.root, objective_id) view = self.ledger.inspect(now=self.now()) if source.get("token_accounting") != "fresh": @@ -344,6 +345,42 @@ def _inherit_thread(self, thread): now=self.now(), ) + def _pause_registered_goal(self, receipt, reason): + """Pause future native continuation, never interrupt an admitted request.""" + proof = dict( + reason=reason, goal_created_at=receipt["goal_created_at"], verified=False + ) + try: + goal, _ = supervision._native(self.grant, self.source_thread) + if not goal or goal.get("createdAt") != receipt["goal_created_at"]: + proof["reason"] = "registered_goal_identity_unverified" + else: + if goal.get("status") != "paused": + supervision._effect( + self.source_path, + receipt, + "request-pause:" + uuid4().hex, + "thread/goal/set", + dict( + threadId=self.source_thread, + status="paused", + origin="automatic", + ), + ) + observed, _ = supervision._native(self.grant, self.source_thread) + proof["verified"] = bool( + observed + and observed.get("createdAt") == receipt["goal_created_at"] + and observed.get("status") == "paused" + ) + except (OSError, ValueError, KeyError, TypeError, WakeError) as error: + proof.update( + reason="registered_goal_pause_unavailable", error=type(error).__name__ + ) + receipt["request_admission_pause"] = proof + supervision._save(self.source_path, receipt) + return proof + def _control(self, thread, context): source = json.loads(self.source_path.read_text()) native = dict(source, thread_id=thread) @@ -352,6 +389,22 @@ def _control(self, thread, context): proof = native_operator(self.root, native, context, enabled) except PermissionDenied: continue + # Native operator input/attachment may reactivate the underlying goal. + # Preserve a retained admission pause before committing deactivation; + # this restores work state, never interrupts this operator response. + if not enabled: + with supervision._lock( + self.directory, supervision._name(self.source_thread) + ): + retained = json.loads(self.source_path.read_text()) + if retained.get("request_admission_pause", {}).get( + "verified" + ) and not sync(retained): + restored = self._pause_registered_goal( + retained, "operator_control_preserves_pause" + ) + # A pause failure cannot veto the operator's durable disable. + # It remains explicit in the acknowledgement and receipt. result = change( self.root, self.source_thread, @@ -362,6 +415,17 @@ def _control(self, thread, context): return _sse( "Governor-generated acknowledgement; no model inference or provider usage. " + result["message"] + + ( + " Registered goal pause " + + ( + "verified" + if restored["verified"] + else "unverified; inspect native work state" + ) + + "." + if not enabled and "restored" in locals() + else "" + ) + " Verified native operator turn " + proof["turn_id"] + "." @@ -424,6 +488,7 @@ def execute(self, body): actor, thread, bound, seconds, enforce=not inactive, now=self.now() ) if not reserved["admitted"]: + pause = self._pause_registered_goal(receipt, "request_admission_denied") notice = self._notice( receipt, "objective_or_session_allowance_unavailable", fingerprint ) @@ -433,7 +498,9 @@ def execute(self, body): json.dumps( _error( "governor_admission_denied", - "Request denied before provider submission. Objective/session resources are unavailable. Say Deactivate governor in the native conversation; no inference is needed for that control.", + "Request denied before provider submission. Registered goal pause " + + ("verified" if pause["verified"] else "unverified") + + "; independently admitted requests were not interrupted. Say Deactivate governor in the native conversation; no inference is needed for that control.", ) ).encode(), ) @@ -710,7 +777,8 @@ def qualify_gateway(root, profile, thread): raise ValueError("no selected HTTP request gateway binding") home = Path(receipt["grant"]["codex_home"]) config_path = home / "config.toml" - config = tomllib.loads(config_path.read_text()) + config_bytes = config_path.read_bytes() + config = tomllib.loads(config_bytes.decode()) provider = config.get("model_provider") selected = config.get("model_providers", {}).get(provider, {}) if ( @@ -749,19 +817,26 @@ def qualify_gateway(root, profile, thread): status.get("scope") != receipt["allocation_id"] or status.get("source_thread_id") != thread or status.get("contract_sha256") != binding.get("contract_sha256") - or status.get("mode") != "hard_admission" + or status.get("mode") not in ("hard_admission", "degraded") or not status.get("reviewed_provider_contract") - or not status.get("accounting", {}).get("ready") ): raise ValueError("gateway contract/scope/accounting is unavailable") + if config_path.read_bytes() != config_bytes: + raise ValueError("native provider configuration changed during qualification") + ready = status["accounting"]["ready"] + known = status["accounting"]["accounting_known"] return dict( - mode="hard_admission", - admitted=True, - reason="qualified_selected_http_request_path", + mode="hard_admission" if known else "degraded", + admitted=ready, + reason="qualified_selected_http_request_path" + if ready + else "objective_resources_unavailable" + if known + else "request_accounting_unknown", scope=receipt["allocation_id"], gateway=url, contract_sha256=status["contract_sha256"], - config_sha256=hashlib.sha256(config_path.read_bytes()).hexdigest(), + config_sha256=hashlib.sha256(config_bytes).hexdigest(), accounting=status["accounting"], maximum_token_overshoot=0, coverage="Only requests through this reviewed HTTP provider path. WS, compact, alternative providers and bypass routes remain unsupported.", diff --git a/tests/test_request_admission.py b/tests/test_request_admission.py index f88307d4..91880c38 100644 --- a/tests/test_request_admission.py +++ b/tests/test_request_admission.py @@ -115,6 +115,17 @@ def do_POST(self): threading.Thread(target=server.serve_forever, daemon=True).start() self.addCleanup(server.server_close) self.addCleanup(server.shutdown) + from codex_wake.goal_notices import list_notices + + activation = [ + event + for event in list_notices(root, fixtures.THREAD) + if event["identity"]["kind"] == "request_activation" + ] + self.assertEqual(len(activation), 1) + self.assertIn("Effective mode hard_admission", activation[0]["text"]) + self.assertEqual(activation[0]["delivery"]["state"], "recorded") + self.assertLessEqual(len(activation[0]["text"].encode()), 768) def submit(turn="work"): body = dict( @@ -155,6 +166,10 @@ def submit(turn="work"): release.set() self.assertEqual(first.result(timeout=5)[0], 200) self.assertEqual(calls[0]["max_output_tokens"], 10) + self.assertEqual(native["goal"]["status"], "paused") + self.assertFalse( + any(method == "turn/interrupt" for method, _ in native["calls"]) + ) view = ledger.inspect(now=fixtures.NOW) self.assertEqual(view["tokens_used"], 47) self.assertEqual(view["tokens_reserved"], 0) @@ -187,11 +202,16 @@ def submit(turn="work"): # The actual whole native user message bypasses inference admission. native["active"] = ["operator-control"] native["user_text"] = "Deactivate governor" + # Native input can autoactivate its goal; deactivation must + # restore the already verified pause, without interrupting input. + native["goal"]["status"] = "active" control = submit("operator-control") self.assertEqual(control[0], 200, control) self.assertIn(b"Governor-generated acknowledgement", control[1]) self.assertEqual(len(calls), attempts) self.assertEqual(gateway.status()["mode"], "inactive") + self.assertEqual(native["goal"]["status"], "paused") + self.assertEqual(native["active"], ["operator-control"]) before = ledger.inspect(now=fixtures.NOW) self.assertEqual(before["tokens_used"], unknown["tokens_used"]) self.assertEqual(before["tokens_reserved"], unknown["tokens_reserved"]) @@ -402,7 +422,11 @@ def body(thread, turn="work"): ) with self.assertRaisesRegex(ValueError, "unsupported or unreviewed"): - RequestGateway(root, "allocation-1", **dict(options, reviewed_contract_sha256="unknown")) + RequestGateway( + root, + "allocation-1", + **dict(options, reviewed_contract_sha256="unknown"), + ) self.assertEqual(calls, []) # Original40 + worst-case50 exceeds session70 despite objective200. self.assertEqual(gateway.execute(body(fixtures.THREAD))[0], 429) From f6a5b99e5b3315c0ceca30235401dfb564a614d4 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 17:34:14 -0500 Subject: [PATCH 12/20] test: prove native admission and zero-allowance operator deactivation (#354) --- .../plan0175/354-native-request-admission.md | 31 ++ .../request_admission_native_acceptance.py | 310 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 docs/dev/evidence/plan0175/354-native-request-admission.md create mode 100644 scripts/request_admission_native_acceptance.py diff --git a/docs/dev/evidence/plan0175/354-native-request-admission.md b/docs/dev/evidence/plan0175/354-native-request-admission.md new file mode 100644 index 00000000..13477000 --- /dev/null +++ b/docs/dev/evidence/plan0175/354-native-request-admission.md @@ -0,0 +1,31 @@ +# #354 actual native request-path acceptance + +Qualified source checkpoint: fd4e205 (worker1c72cf6 +69dc8c5), plus acceptance driver whose SHA256 is `5f51bac419d9396b3a9e9ed8f91445f4f3ecc95346cd34cd97c118a775055e68`. + +Command: `PYTHONPATH=src /bin/python scripts/request_admission_native_acceptance.py --app-server --frontend --output /tmp/governor354-native-qualified`. + +Terminal PASS against native SHA25686fad00e38f0c26a5c8d2eb2dbe34536c74655fb2fb45b4efc5d94e97e683b1f and actual stock frontend SHA256b93f6811d3f653f0fb83be4050d857fb9abbb111dfa2ce160563327daff7f306: + +- One inert native warmup cost7fresh tokens. Actual native accounting qualified it; imported into the existing cap57 without erasing prior spending. Warmup explicitly precedes the hard-coverage window. +- The selected HTTP-only native provider route and private client token used an owned authenticated listener. A distinct hard_admission owner notice rendered in the connected TUI before the first guarded request. +- Provider-enforced whole-request maxima45input+5output reserved50 before dispatch. That actual native request settled50fresh, reaching EXACT57/57. The provider fixture actually enforces these consumption maxima; no input-byte/token approximation is used. +- The next actual native request was denied before provider forwarding. The registered goal remained paused. A transport-completed native turn is not itself proof of product work; provider count is the acceptance signal. +- The stock TUI accepted the operator's single whole message Deactivate governor at ZERO remaining allowance. The gateway verified the real native current user message and returned an explicitly Governor-generated acknowledgement. No model/provider inference was used for control. No extra deactivation confirmation was required. +- Effective inactive state, original usage57, cap57 and original deadline/history were preserved; no work resumed, and no resource allowance was renewed. +- Provider requests: one warmup +one guarded request. External provider calls:0. Fresh post-cleanup owned-home process census:EMPTY; native and frontend terminal. No shared runtime/configuration/mask or provider credentials were touched. + +## Explicit qualification limits + +This proves the real native HTTP route, cost admission/settlement and operator/model delivery with an inert reviewed provider adapter. It does not qualify an arbitrary external provider contract, stock default provider configuration, WebSocket or compaction bypass route, or installed deployment. External eligibility requires an exact host-reviewed enforceable whole-request bound contract. Unsupported/unknown paths refuse before forwarding. Native/client/source/config/identity drift invalidates compatibility receipts. Old lifecycle/session-accounted ledgers cannot silently become a request-owned budget. + +## Retained failed receipts + +- /tmp/governor354-native-first: quota/denial passed; frontend startup resume modal consumed attempted input. Native source budgetdenial subsequently gained verified pause/readback to suppress automatic continuation. +- /tmp/governor354-native-control: bootstrap accounting origin publication did not qualify. History/counters were not rewritten. +- /tmp/governor354-native-ready: writer readiness fixed; stock startup modal remained and an unregistered auxiliary title request was refused rather than billed. +- /tmp/governor354-native-paste: bracketed paste exposed the paused-goal startup dialog behind the visible composer. +- /tmp/governor354-native-dialog: native creation led the real trusted clock briefly; enrollment refused correctly. + +The final driver primes the native session writer, waits for the existing trusted clock to qualify the original creation, names its owned thread to avoid unrelated title generation, and dismisses the native startup dialog BEFORE activating hard coverage. It waits for the actual composer text before Enter. These are explicit setup readiness checks, not rewritten accounting or hidden deactivation approvals. All failed owned roots were cleaned; their failed receipts remain failed. + +Worker final focused sweep:27testsPASS18.423s across gateway, ledger, deactivation, notices and preflight. Public cases also prove concurrent admission, original perthread deadlines, child inheritance before first usage, conservative unknown/duplicate settlement, zero-inference native operator authorization and bounded heartbeat stream duration. Final integration checks follow after #357. diff --git a/scripts/request_admission_native_acceptance.py b/scripts/request_admission_native_acceptance.py new file mode 100644 index 00000000..fe34b148 --- /dev/null +++ b/scripts/request_admission_native_acceptance.py @@ -0,0 +1,310 @@ +"""Owned native request admission and zero-allowance operator control proof. + +Only an inert localhost provider is called. Warmup precedes the declared hard +coverage window and its measured charge is imported, never erased. No shared +runtime or external provider credentials are used. +""" +import argparse +from contextlib import ExitStack +import hashlib +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import os +from pathlib import Path +import secrets +import signal +import subprocess +import tempfile +import threading +import time + +from codex_wake.native_protocol import native_request +from codex_wake.process import process_identity + + +def sse(identifier, text, *, input_tokens, cached, output_tokens): + events = [dict(type='response.created', response=dict(id=identifier)), + dict(type='response.output_item.done', item=dict(type='message', role='assistant', + id='message-'+identifier, content=[dict(type='output_text', text=text)])), + dict(type='response.completed', response=dict(id=identifier, usage=dict( + input_tokens=input_tokens, input_tokens_details=dict(cached_tokens=cached), + output_tokens=output_tokens, total_tokens=input_tokens+output_tokens)))] + return ''.join('event: '+row['type']+'\ndata: '+json.dumps(row)+'\n\n' for row in events).encode() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--app-server', type=Path, required=True) + parser.add_argument('--frontend', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + options = parser.parse_args() + options.output.mkdir(mode=0o700, parents=True, exist_ok=False) + appserver, frontend = options.app_server.resolve(strict=True), options.frontend.resolve(strict=True) + report = dict(state='running', external_provider_calls=0, shared_runtime_changed=False, + warmup_outside_hard_coverage=True, provider='owned inert bounded Responses adapter') + for name, path in (('native', appserver), ('frontend', frontend)): + with path.open('rb') as stream: + report[name+'_sha256'] = hashlib.file_digest(stream, 'sha256').hexdigest() + report['script_sha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() + expiry = time.monotonic()+150 + calls, inbound = [], [] + selected = {'gateway': None} + token = secrets.token_urlsafe(32) + processes = [] + tmux_name = 'cw354-owned-'+str(os.getpid()) + with tempfile.TemporaryDirectory(prefix='cw354-native-') as temporary, ExitStack() as stack: + base = Path(temporary) + home, root = base/'home', base/'wake' + home.mkdir(mode=0o700); root.mkdir(mode=0o700) + env = dict(os.environ, CODEX_HOME=str(home), REQUEST_GATEWAY_TOKEN=token) + def save(name, value): + path = options.output/name + path.write_text(json.dumps(value, indent=2, sort_keys=True)+'\n') + path.chmod(0o600) + def wait_for(callback, timeout=25): + until=min(expiry,time.monotonic()+timeout) + while time.monotonic()= goal['createdAt']+1) + from codex_wake.objective_ledger import ObjectiveLedger + from codex_wake.request_admission import RequestGateway, contract_digest + policy=root/'policy.json' + policy.write_text(json.dumps(dict(version=1,callers={thread:dict(app_server='unix://'+str(endpoint), + codex_path=str(frontend),codex_home=str(home),allocation_id='owned-request-scope', + max_tokens=57,max_seconds=120,rpc_timeout=5)}))) + policy.chmod(0o600) + enrolled=enroll(root,policy,thread,57,120,checkpoint_token_percent=0,checkpoint_seconds=0) + save('enrollment.json',enrolled) + assert enrolled['used_tokens']==7, enrolled + # Interactive warmup may activate the goal. Pause it outside the hard + # window before attaching the frontend, then prove activation live. + rpc('thread/goal/set', threadId=thread, status='paused', origin='automatic') + tmux('new-session','-d','-s','governor','-x','170','-y','60',str(frontend),'--remote','unix://'+str(endpoint),'--no-alt-screen','resume',thread) + tmux('set-option','-g','exit-empty','off') + wait_for(lambda: 'Resume paused goal?' in capture()) + tmux('send-keys','-t','governor:0.0','Escape') + wait_for(lambda: 'Resume paused goal?' not in capture()) + report['native_startup_dialog_dismissed_before_coverage']=True + wait_for(lambda: 'Ask Codex to do anything' in capture() and not any( + row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) + assert len(calls)==1, 'frontend warmup created an unaccounted request' + now=trusted_upper() + configuration=dict(revision='owned-native-request-v1',objective_token_limit=57,objective_time_limit_seconds=120, + session_token_limit=57,session_time_limit_seconds=120,max_correction_cycles=1, + started_at=enrolled['time_origin'],objective_deadline=enrolled['deadline']) + ledger=ObjectiveLedger(root,'owned-request-scope') + ledger.configure(configuration,'explicit owned provider-free qualification',prior_tokens=7,now=now) + contract=dict(version=1,provider='owned-inert-adapter',model='mock-model',max_input_tokens=45,max_output_tokens=5, + input_limit='provider_enforced_reject_before_billing',output_limit='max_output_tokens_enforced', + usage='response.completed:fresh-v1',review='This owned provider emits input<=45 and output<=5, with no external model.') + gateway=RequestGateway(root,'owned-request-scope',upstream_url='http://127.0.0.1:'+str(provider.server_port)+'/v1/responses', + contract=contract,reviewed_contract_sha256=contract_digest(contract),thread_ids=[thread], + now=trusted_upper,request_timeout=10,client_token=token) + gateway.bind_listener(listener_url); selected['gateway']=gateway + activation_frame=wait_for(lambda: value if 'Effective mode hard_admission' in (value:=capture()) else None) + (options.output/'hard-activation-tui.txt').write_text(activation_frame) + report['hard_activation_rendered_before_guarded_request']=True + first=turn('Run exactly one inert guarded request. Do not use tools.') + assert first['status']=='completed',first + assert len(calls)==2,calls + status=gateway.status(); save('after-admitted.json',status) + assert status['accounting']['tokens_used']==57,status + denied=turn('This request must be stopped by the governor before reaching the provider.') + assert len(calls)==2,calls + save('after-denied.json',gateway.status()) + assert rpc('thread/goal/get',threadId=thread)['goal']['status']=='paused', 'Admission denial did not pause autonomous continuation' + wait_for(lambda: 'Ask Codex to do anything' in capture() and not any( + row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) + tmux('set-buffer','-b','governor-control','Deactivate governor') + tmux('paste-buffer','-p','-b','governor-control','-t','governor:0.0') + wait_for(lambda: '› Deactivate governor' in capture()) + tmux('send-keys','-t','governor:0.0','Enter') + wait_for(lambda: gateway.status()['mode']=='inactive') + frame=wait_for(lambda: value if 'Governor inactive' in (value:=capture()) or 'Governor deactivated' in value else None) + (options.output/'deactivated-tui.txt').write_text(frame) + wait_for(lambda: not any(row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) + ack=wait_for(lambda: value if 'Governor-generated acknowledgement' in (value:=capture()) else None) + (options.output/'zero-inference-control-ack.txt').write_text(ack) + assert len(calls)==2,'control action reached provider' + final=gateway.status(); save('after-control.json',final) + assert final['accounting']['objective_token_limit']==57,final + assert final['accounting']['tokens_used']==57,final + assert rpc('thread/goal/get',threadId=thread)['goal']['status']=='paused' + report.update(state='passed',warmup_requests=1,guarded_provider_requests=1, + denied_request_never_forwarded=True,control_at_zero_allowance=True, + control_external_inference=0,control_rendered_in_stock_tui=True, + original_usage_and_cap_preserved=True,thread_id=thread,denied_turn_status=denied['status']) + except Exception as error: + report.update(state='failed',failure=type(error).__name__,message=str(error)[:1000]) + (options.output/'last-frame.txt').write_text(capture()) + save('observed-requests.json',dict(provider=calls,native_metadata=inbound)) + if 'native_thread' in locals() and native_thread.get('path'): + accounting=[] + for line in Path(native_thread['path']).read_text().splitlines(): + row=json.loads(line); payload=row.get('payload',{}) + if row.get('type')=='event_msg' and payload.get('type') in ( + 'thread_goal_updated','task_started','task_complete','raw_response_completed'): + accounting.append(dict(timestamp=row.get('timestamp'),event=payload.get('type'), + goal_created_at=payload.get('goal',{}).get('createdAt'), + goal_status=payload.get('goal',{}).get('status'), + response_id=payload.get('response_id'),token_usage=payload.get('token_usage'))) + save('accounting-events.json',accounting) + raise + finally: + # Capture identities while parents still exist, then check fresh + # unique-home ownership as well as lineage after stopping them. + parents={int(pid):int(ppid) for pid,ppid in (row.split() for row in subprocess.check_output(['ps','-eo','pid=,ppid='],text=True).splitlines())} + owned={process.pid for process in processes} + tmux_pid=tmux('display-message','-p','#{pid}',check=False).stdout.strip() + if tmux_pid.isdigit():owned.add(int(tmux_pid)) + while True: + expanded=owned|{pid for pid,parent in parents.items() if parent in owned} + if expanded==owned:break + owned=expanded + identities={} + for pid in owned: + try:identities[pid]=process_identity(pid) + except (OSError,ValueError):pass + tmux('kill-server',check=False) + for pid,identity in identities.items(): + try: + if process_identity(pid)==identity:os.kill(pid,signal.SIGTERM) + except (OSError,ValueError):pass + for process in processes: + try:process.wait(timeout=10) + except subprocess.TimeoutExpired:process.kill();process.wait(timeout=5) + remaining=[] + for path in Path('/proc').iterdir(): + if not path.name.isdigit():continue + try: + if b'CODEX_HOME='+str(home).encode()+b'\0' in (path/'environ').read_bytes(): + os.kill(int(path.name),signal.SIGTERM);remaining.append(int(path.name)) + except (OSError,ValueError):pass + report['fresh_owned_home_cleanup_signaled']=remaining + until=time.monotonic()+5 + while True: + remaining=[] + for path in Path('/proc').iterdir(): + if not path.name.isdigit():continue + try: + if (b'CODEX_HOME='+str(home).encode()+b'\0' in (path/'environ').read_bytes() + and (path/'stat').read_text().rsplit(')',1)[1].split()[0]!='Z'): + remaining.append(int(path.name)) + except (OSError,ValueError):pass + if not remaining or time.monotonic()>=until:break + time.sleep(.02) + report['remaining_owned_pids']=remaining + report['owned_processes_terminal']=not remaining and all(process.poll() is not None for process in processes) + if not report['owned_processes_terminal']:report['state']='cleanup_incomplete' + save('receipt.json',report) + print(json.dumps(report)) + + +if __name__=='__main__': + main() From b5f5f7720914fc990d63c22600a460dbbe1502d4 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 17:40:39 -0500 Subject: [PATCH 13/20] fix: keep spawn preflight from blocking handoff correction (#353) --- docs/dev/evidence/plan0175/353-native-preflight.md | 4 ++++ src/codex_wake/agent_lifecycle.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/dev/evidence/plan0175/353-native-preflight.md b/docs/dev/evidence/plan0175/353-native-preflight.md index d9fb1889..95f4abcc 100644 --- a/docs/dev/evidence/plan0175/353-native-preflight.md +++ b/docs/dev/evidence/plan0175/353-native-preflight.md @@ -9,3 +9,7 @@ Terminal receipt PASS: real connected server PID/executable and separate client Primary five-case public selection PASS before bound correction (12.114s); same final selection rerun after correction, recorded in /tmp/353-primary-final.log. Worker affected40casesPASS and final5casesPASS remain source regression evidence, not a replacement for this selected-native proof. Repo-wide pinned undefined-name checks and diffcheck PASS. No installed runtime/service or shared session was changed. Final primary five-case selection:5PASS,12.695s after bound correction. + +## Full-CI correction compatibility repair + +Run40235630606 on49df3a9 failed on both Python versions: two existing public handoff-correction cases became successor_assignment_blocked. #353 was reopened; local exact two-case reproduction failed2/2 in3.547s. The new shared before_effect guard unconditionally required a spawn-only launch field on handoff/refresh receipts. Restricting optional preflight lookup to available launch metadata preserves the existing non-spawn seam. Exact existing cases then PASS2/2 in5.307s. Combined gateway/ledger/control/notice/preflight/spawn/correction selection PASS33/33 in17.187s (/tmp/353-354-final-selection.log). No test was removed or weakened. Prior CI failure remains preserved; final full CI must run on the successor commit. diff --git a/src/codex_wake/agent_lifecycle.py b/src/codex_wake/agent_lifecycle.py index c9fb8f17..e12af5a0 100644 --- a/src/codex_wake/agent_lifecycle.py +++ b/src/codex_wake/agent_lifecycle.py @@ -427,7 +427,8 @@ def request(method, params): def before_effect(path, receipt): - profile = receipt['intent']['launch'] + # Refresh/handoff guards share this seam without a spawn launch profile. + profile = receipt['intent'].get('launch', {}) if 'governance_preflight' in profile: from .goal_host_preflight import inspect_host preflight = inspect_host(path.parent.parent, profile, receipt['intent'].get('source_thread_id')) From 2ecf4e9b67c753fef209cfc23164ef6f4901e853 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 17:59:11 -0500 Subject: [PATCH 14/20] docs: record native request proof and six closed governor tickets (#350) --- docs/dev/active-lanes.yaml | 4 ++-- .../0175-2026-10-25-transparent-governor-delivery.md | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/dev/active-lanes.yaml b/docs/dev/active-lanes.yaml index 4b660ada..fe8689ea 100644 --- a/docs/dev/active-lanes.yaml +++ b/docs/dev/active-lanes.yaml @@ -10,12 +10,12 @@ lanes: target: origin/main plan_state: OPEN custody_state: ACTIVE_WORKTREE - checkpoint: 6e11c34 + checkpoint: b5f5f77 pull_request: https://github.com/CochranResearchGroup/codex-wake/pull/358 dependencies: [] overlaps: [] updated_at: 2026-10-25 - disposition: 351/356 CLOSED; isolated agents dispatch355 and validation356 on 352; no live effects + disposition: 351-356 CLOSED; dispatch355 owns 357; native request/control proof PASS; final review/remediation pending; no live effects - id: P173-GOAL-UX owner: primary / ecochran76 objective: Implement visible goal supervision and approved renewal diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index fcfcc30c..9e79ad38 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -42,3 +42,13 @@ No release, service unmask, production session dispatch, automatic resource rene Bounded #354 source discovery (not implementation): native prototype b0ce988 client sends HTTP and WebSocket without Wake admission; request shape lacks max_output_tokens. Existing custom provider fixture explicitly supports HTTP-only configuration. A request reservation helper alone cannot prove hard-cap enforcement. Any chosen adapter must establish complete selected-path coverage and enforceable input/output bounds, preserve unsupported-host truth, and qualify against an actual native request path. Do not fabricate an input-byte/token bound or install an unqualified native binary to close a ticket. The operator's implementation request implicitly accepts using existing public operator/agent journeys as testing seams. Source behavior and isolated pinned-host acceptance remain separate from installation/deployment. + +## Checkpoint: six child tickets closed + +#351/#352/#353/#354/#355/#356 CLOSED. #353 was explicitly reopened after fullCI49df3a9 failed two handoff-correction cases; b5f5f77 repaired the spawn-only launch assumption. Local exact red2FAIL ->green2PASS; combined33-case selectionPASS17.187s. Successor fullCI pending; prior failure retained. + +#354 source4a72367/fd4e205 plus driverf6a5b99 has actual native/stock-TUI proof: originalwarmup7 imported, guardedcost50 reaches EXACT57/57, nextnative requestnotforwarded, livehardactivationbeforeforward, singletypedoperatorDeactivate atZERO produces governor-generated acknowledgement with0provider/modelinference, originalusage/capretained, goalpaused, freshownedprocesscensusEMPTY. Supportednativepath and auditedprovidercontract eligibility remain separate; no externalprovider/defaultinstalledhost claim. No production services/masks/allocationschanged. + +#357 assigned agent dispatch355 in isolated feat/completion-reserve-357 basedb5f5f77. Add small named completion obligations/protection to existing requestledger/publicHTTPseam; preserve operator controls and originalgrant. Rootprimary owns final integration/acceptance. Formal review count0; post-review TDD remediation passes0. Fixedpoint remainsce1ad93. + +Full successor CI on b5f5f7720914fc990d63c22600a460dbbe1502d4 PASS both Python3.11/3.12 release-gates, run40241319799. The older49df3a9 failure remains superseded by a verified fix, not erased or retried asclean. #357 remains in progress. From e8eaabe3bd80c122b535506f6a72c6b97263b1b0 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:06:39 -0500 Subject: [PATCH 15/20] feat(governance): protect completion resources in request admission (#357) --- GLOSSARY.md | 6 + docs/dev/evidence/issue0357/acceptance.md | 136 ++++++++++++++++ docs/request-admission.md | 90 +++++++++++ src/codex_wake/goal_notices.py | 37 +++-- src/codex_wake/objective_ledger.py | 169 +++++++++++++++++++- src/codex_wake/request_admission.py | 38 ++++- tests/test_completion_reserve.py | 184 ++++++++++++++++++++++ tests/test_request_admission.py | 13 +- 8 files changed, 654 insertions(+), 19 deletions(-) create mode 100644 docs/dev/evidence/issue0357/acceptance.md create mode 100644 tests/test_completion_reserve.py diff --git a/GLOSSARY.md b/GLOSSARY.md index 15579341..c53d36fc 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -258,3 +258,9 @@ The operator's durable choice to stop governor interventions in a selected scope A bounded read-only check before opted-in dispatch, separating reusable static host observations from fresh accounting, controller and operator-control state. It reports the actual enforcement mode without granting work or resource authority. + +**Protected completion resources**: +The unused acceptance, review and bounded remediation estimates retained inside +an existing objective allowance. Host-bound completion work may use them; +implementation must leave them available. Deferral keeps mandatory work pending, +and verified completion releases unused protection without renewing the grant. diff --git a/docs/dev/evidence/issue0357/acceptance.md b/docs/dev/evidence/issue0357/acceptance.md new file mode 100644 index 00000000..b86b022b --- /dev/null +++ b/docs/dev/evidence/issue0357/acceptance.md @@ -0,0 +1,136 @@ +# Issue #357: protect completion resources inside the existing allowance + +Work item: . +Parent: , stories 25–26. +Baseline: `b5f5f7720914fc990d63c22600a460dbbe1502d4`. +The coordinator owns the parent plan, actual native/UI qualification, integration, +issue closure and program review/remediation counters. + +## Implemented public path + +The existing `ObjectiveLedger` owns a small optional completion plan: exactly one +named acceptance, review and bounded remediation obligation, each with a frozen +criterion, token/time estimate and pinned existing source. The existing gateway's +trusted host methods install the plan before implementation request actors, +bind exact native descendant threads to purposes and record source-backed +completed/deferred dispositions. There is no new HTTP authority endpoint, +allocator, grant, deadline window or operator-consent loop. + +`reserve_request` protects unfinished completion resources atomically alongside +the existing cumulative request/session accounting. HTTP body role/purpose claims +cannot choose those funds. Original actors retain their phase, spending and +original session deadline across retries and phase changes. The first actual +remediation request consumes one existing correction cycle; the plan's bounded +subsequent requests cannot create another cycle or reset that counter. + +Time feasibility uses conservative serial remaining work against the original +objective and already assigned session deadlines. Each admitted request's full +timeout remains charged to its obligation's time estimate. An infeasible path +returns an explicit checkpoint/narrow-work explanation before forwarding. +Deferral retains mandatory protection, pending obligations and a partial checkpoint +outcome. It cannot free implementation funds. Completed dispositions need a +verified source and cannot release unsettled/unknown work. They release only the +unused estimate, never increase the original grant, and never infer objective +acceptance/closure from request or test counts. + +Current disabled state bypasses scheduling enforcement, with usage retained and +`completion_enforced:false`. Existing manual routes are unchanged; planning adds +no watcher/start/unmask/resume requirement. The native zero-inference operator +control is processed before admission exactly as before. + +Hard-path activation/denial notices now project the fresh ledger: actual used/cap, +reserved/remaining, protected funds/time, original deadline and the actual denial +trigger. No raw actors or credentials enter the notice. All tested notices stay +within 768 bytes; no UI-rendering claim follows from fixture native append. + +## Deterministic public campaign + +`tests.test_completion_reserve` uses authenticated real HTTP gateway/provider +servers, the existing native transport fixture, original qualified prior usage, +controlled time and the existing request owner. No external inference, production +state, actual native/frontend binary or installed service is changed. + +The literal campaign starts with cap **200**, prior usage **40**, original deadline +**NOW+120**, and three completion estimates of **40 tokens / 4 seconds**. The +reviewed inert request maximum is **40**; attributable completion usage is **10**. + +1. Implementation forwards once and settles, reaching **50/200**. +2. An ordinary child claiming reviewer purpose in its HTTP body is denied before + forwarding; the **120-token/12-second** completion protection remains intact. + The notice reads **Used 50/200**, rather than the stale native receipt's 40. +3. The host assigns the child to acceptance; that funded request still forwards. + Source-verified acceptance releases unused protection, leaving **80** protected. +4. Another already authorized implementation request uses released funds. Review + and remediation remain protected and then each execute under host assignments. +5. Only their verified named dispositions report all obligations completed. + Five forwarded requests plus prior usage total **90/200**, reserved tokens are + **0**, original deadline/authorization history remain unchanged, and the + original fixture's authorized correction counter is **1**, not reset by phases. + `objective_complete` remains false. A request in the now-closed completion + purpose is denied without falsely describing its completed obligations as pending. + +A separate time case advances to **NOW+112**: the **12-second** mandatory path no +longer fits the original **8 seconds**. Admission checkpoints before forwarding; +source-backed deferral keeps all mandatory funds protected and visible. Exact +native `Deactivate governor` then produces the existing governor-generated +acknowledgement with **zero provider calls**, retains the registered goal pause, +and allows an ordinary inactive-mode request. Actual usage is retained without +renewing the grant; deferred obligations remain visible as incomplete. + +## Red/green and validation selection + +Commands used this prefix: + +```sh +PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python \ + -W ignore::DeprecationWarning -m unittest +``` + +- `tests.test_completion_reserve.CompletionReserveTests.test_implementation_leaves_completion_funded_and_body_role_cannot_select_it -q` + failed first because `RequestGateway.protect_completion` did not exist; after + the existing ledger/gateway integration, **1 passed, 1.344 s**. +- The full tail/release case next failed because `resolve_completion` did not + exist. With source-backed dispositions, all three public campaign cases passed, + **3 tests, 4.278 s**. +- The remediation-counter assertion detected **0 != 1** before its first request + was charged to the original correction bound. The three-case campaign then + passed with that retained counter. +- The shared notice check first failed because activation omitted literal + **Used 40/100**. Its next trigger check also detected the old generic denial + cause. The public concurrent-request fixture now verifies actual activation + **40/100**, reserved **0**, remaining **60**, UTC deadline, and denial + **objective_token_allowance_unavailable**, used **40**, reserved **50**, + remaining **10**. This is fresh request accounting, not a native display total. +- A bounded closeout rerun found one case-sensitive test assertion after the + explanation changed from `checkpoint` to `Checkpoint`; the test now checks the + semantic explanation without depending on capitalization. That first failure + is retained; it is not reported as a pass or a flaky retry. + +Final focused selection: + +```sh +PYTHONPATH=src:tests /home/ecochran76/workspace.local/codex-wake/.venv/bin/python \ + -W ignore::DeprecationWarning -m unittest \ + tests.test_completion_reserve tests.test_request_admission \ + tests.test_objective_ledger tests.test_goal_notices -q +``` + +**24 tests passed, 7.452 s**, exit 0. This is the affected selection, not the +full repository suite. It includes existing concurrent admission, request timeout, +unknown/duplicate completion, native descendant/session accounting and exact +zero-inference deactivation behavior. These fixtures do not qualify external +provider contracts, actual operator rendering or a production installation. + +Ruff 0.14.2 passes for the changed selection with existing E701/E702 style debt +excluded; the gateway/new test file also pass without exclusions. +`git diff --check` passes. CodeGraph was initialized and synchronized for this +verified worktree. Only source, docs and deterministic fixture tests changed. + +Memory disposition: **forbidden**. This delegated lane is restricted to its +worktree; user-scoped memory writes and the parent closeout are outside its scope. + +## Tested production source hashes + +- `src/codex_wake/objective_ledger.py`: `33f1db376adabd43ecc08255ee53d92bcf2b44c3a5a68d0c65d000cf5b843745` +- `src/codex_wake/request_admission.py`: `4fde9a0fa4c0c30322e3914816a1306d357b753d366bb5d272ff7b6cc0357586` +- `src/codex_wake/goal_notices.py`: `6dabf464710a3b712fe631e73d9c9dc54d7572c7dff4a2f202632a5191adeaf0` diff --git a/docs/request-admission.md b/docs/request-admission.md index 8705d9f2..72cd9691 100644 --- a/docs/request-admission.md +++ b/docs/request-admission.md @@ -176,3 +176,93 @@ zero remaining allowance; it is an explicit mode change, not a silent downgrade. Focused proof uses `tests/test_request_admission.py` with real HTTP dispatch, controlled native transport/usage and no external inference. Actual pinned-native and stock-frontend proof is a separate acceptance artifact owned by integration. + +## Protect the completion path + +A trusted host can protect acceptance, review and bounded remediation **inside** +the existing request-owned ledger before its first implementation request. This +is an optional planning contract, not a new allowance, approval workflow or +allocator. Legacy gateways without a plan retain their admission behavior. + +```python +# Each source is an existing reviewed, canonical, owned regular file plus SHA256. +# The host selects this authority; no HTTP body can install or change it. +plan_source = {"path": "/owned/approved-plan.md", "sha256": "PINNED_SHA256"} +gateway.protect_completion([ + {"id": "acceptance", "kind": "acceptance", "tokens": 40, "seconds": 4, + "criterion": "Verify the frozen public behavior"}, + {"id": "review", "kind": "review", "tokens": 40, "seconds": 4, + "criterion": "Resolve the bounded review findings"}, + {"id": "remediation", "kind": "remediation", "tokens": 40, "seconds": 4, + "criterion": "Verify the authorized remediation or evidenced no-finding result"}, +], source=plan_source) + +# The gateway verifies existing native ancestry before its first request: +gateway.assign_completion(worker_thread, "review", source=assignment_source) +# The host assesses the criterion and supplies its pinned evidence and decision: +gateway.resolve_completion("review", "completed", source=review_evidence, + note="Named criterion verified against the frozen objective") +``` + +There must be exactly one obligation of each mandatory kind. IDs, criteria, +resource estimates and the source are immutable; reapplying the same plan keeps +all progress and charges. Source files must be canonical, owned, regular, no +larger than 1 MiB and match their digest. This verifies bytes/provenance, not the +semantic conclusion: the trusted host owns that assessment. A source-bound +validation receipt can be evidence, but a passing test count is never automatic +quality acceptance. These APIs are not HTTP endpoints and do not accept a model's +claimed role. Unassigned descendants, retries and successors remain ordinary +implementation requests until the host records their exact durable purpose. + +Before forwarding, `reserve_request` atomically leaves the pending obligations' +unused token and time bounds available. An assigned completion request can use +its own remaining bound while leaving other mandatory work protected. Each +request still uses the provider-reviewed whole-request maximum and the existing +objective/session ceilings and deadlines. Phase changes do not reclassify old +actors, reset per-thread spending, create another session window or replenish +resources. The first actual remediation request consumes one existing correction +cycle; retries in that same bounded obligation do not mint another cycle. + +Time feasibility uses conservative serial future work against the original +objective deadline and already assigned workers' original session deadlines. +Every admitted request's full timeout bound remains charged to its obligation's +time estimate, even if it returned sooner. This is conservative planning, not an +optimal parallel schedule or a new wall-clock budget. Actual wall time continues +to reduce the unchanged original allowance. + +Only `completed` with verified source evidence releases unused protection. +An unresolved/unknown attempt cannot release it. `deferred` requires a source and +an explicit reason, stays mandatory/pending, retains its funds and produces a +checkpoint outcome; it does not free funds for more implementation. When the +remaining path cannot fit, admission returns its exact reason and an explicit +checkpoint/narrow-work explanation before forwarding. The host can finish funded +completion work, record valid unaffected evidence, or checkpoint; it cannot use +phase changes to renew an exhausted campaign. + +`GET /status` exposes `accounting.completion`: named pending/deferred/completed +obligations, protected tokens/seconds, feasibility, and the next action. All +obligations completed means only that the host recorded their evidence-backed +dispositions. `objective_complete` remains false: objective acceptance and closure +stay with their existing owner. A disabled governor bypasses this scheduling gate +and reports `completion_enforced:false`; retained usage and unfinished obligations +remain visible. No new watcher requirement or per-packet consent is introduced. + +Worked provider-free campaign: original cap 200, prior usage 40, three protected +40-token/4-second obligations, and a reviewed maximum of 40 tokens per request. +One implementation request settles 10, leaving 150 available and 120 protected; +the next 40-token implementation request is denied before forwarding, including +when its body claims to be a reviewer. A host-bound acceptance request still +fits. Its verified completion releases unused protection, permitting another +already authorized implementation request while review/remediation stay funded. +Acceptance, review and remediation each settle 10. Five forwarded requests plus +prior usage total **90/200**, reservations settle to zero, one authorized +correction cycle is retained, and the original deadline/history are unchanged. +Only after all three evidence-backed dispositions are recorded are their +obligations reported completed; no objective closure is inferred. + +Hard activation and admission notices use a fresh ledger projection, not a +possibly lagging native source counter. Within the existing 768-byte bound they +show actual used/cap, reserved/remaining, protected completion funds/time, deadline +and denial trigger. They disclose no raw actors or credentials. Delivery to +native model context, operator rendering and human reading retain their separate +qualification boundaries. diff --git a/src/codex_wake/goal_notices.py b/src/codex_wake/goal_notices.py index 42d5bb30..32a1f609 100644 --- a/src/codex_wake/goal_notices.py +++ b/src/codex_wake/goal_notices.py @@ -123,18 +123,33 @@ def render_notice(event): usage = f'{used:,} of {limit:,}' if type(used) is int and type(limit) is int else 'usage unavailable' remaining = f'{max(0, limit-used):,}' if type(used) is int and type(limit) is int else 'unknown' kind = identity['kind'] - if kind=='request_activation': - selected=identity['request_accounting'] - return (f"Governor request-path owner configured. Effective mode {'inactive' if snapshot.get('governor_enabled') is False else selected['mode']} for reviewed HTTP Responses path only; WS/compact/bypass remain unqualified. " - f"Scope {identity['allocation_id']}; session {identity['thread_id']}. " - 'ObjectiveLedger owns aggregate request accounting; original prior usage, caps and deadlines retained. No work resumed or resources renewed. ' - 'Say "Deactivate governor"; verified operator control needs no model inference. ' - f"[External supervision event {event['event_id']}]") - if kind=='admission': - return (f"Model request denied before provider submission. Scope {identity['allocation_id']}; session {identity['thread_id']}. " - 'Reason: objective/session resources unavailable or accounting uncertain. Mode hard_admission applies only to the selected qualified HTTP path. ' - 'Say "Deactivate governor" in this conversation; verified operator control needs no provider inference. No work resumed or resources renewed. ' + if kind in ('request_activation', 'admission'): + selected = identity['request_accounting'] if kind == 'request_activation' else identity['request_admission_event'] + resources = selected.get('resources') or {} + if resources: + deadline = resources['deadline'] + if isinstance(deadline, (int, float)): + deadline = datetime.fromtimestamp(deadline, timezone.utc).isoformat().replace('+00:00', 'Z') + card = (f"Used {resources['used']}/{resources['cap']}; reserved {resources['reserved']}; remaining {resources['remaining']}; " + f"completion protected {resources['protected_tokens']} tokens/{resources['protected_seconds']}s; deadline {deadline}. " + + ('' if resources['accounting_known'] else 'Accounting uncertain; reservations retained. ')) + else: + card = 'Accounting snapshot unavailable; inspect the retained ledger. ' + scope = str(identity['allocation_id']) + if len(scope) > 64: + scope = scope[:61] + '...' + if kind == 'request_activation': + text = (f"Governor request-path owner configured. Effective mode {'inactive' if snapshot.get('governor_enabled') is False else selected['mode']}; " + 'reviewed HTTP Responses only; WS/compact/bypass unqualified. ') + else: + text = (f"Model request denied before provider submission. Reason {selected['reason']}. " + 'Hard admission: selected HTTP path only. Checkpoint/narrow when completion work remains. ') + text += (f"Scope {scope}; session {identity['thread_id']}. " + card + + 'Say "Deactivate governor"; no provider inference. No work resumed or resources renewed. ' f"[External supervision event {event['event_id']}]") + if len(text.encode()) > 768: + raise ValueError('request governance notice exceeds native bound') + return text if kind in ('deactivation','reactivation'): text=('Governor inactive. Drop stale governor checkpoint/stop instructions. Independent platform controls remain. ' 'Say "Reactivate governor" to enable the configured governor. ' if kind=='deactivation' else diff --git a/src/codex_wake/objective_ledger.py b/src/codex_wake/objective_ledger.py index c233be6c..27a9a2fb 100644 --- a/src/codex_wake/objective_ledger.py +++ b/src/codex_wake/objective_ledger.py @@ -36,6 +36,54 @@ def _private(path, directory=False): raise ValueError('ledger paths must be owned and private without symlinks') +def _completion_source(source): + if type(source) is not dict or set(source) != {'path', 'sha256'}: + raise ValueError('completion authority/evidence requires a pinned source') + path = Path(source['path']) + if not path.is_absolute() or path != path.resolve(strict=True): + raise ValueError('completion source must be canonical') + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, 'rb') as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_size > 1024 * 1024: + raise ValueError('completion source must be bounded, regular and owned') + raw = stream.read(1024 * 1024 + 1) + if len(raw) > 1024 * 1024 or hashlib.sha256(raw).hexdigest() != source['sha256']: + raise ValueError('completion source digest differs') + return dict(source) + + +def _completion_status(state, view, now): + plan = state.get('completion_plan') + if not plan: + return None + obligations = {} + for name, obligation in plan['obligations'].items(): + actors = [a for a in state['actors'].values() if a.get('completion_obligation') == name] + charged = sum(a['used'] if a['settled'] else max(a['reservation'], a['used']) for a in actors) + seconds = sum(a['terms'][1] for a in actors) + closed = obligation['state'] == 'completed' + obligations[name] = dict(obligation, charged_tokens=charged, charged_request_seconds=seconds, + protected_tokens=0 if closed else max(0, obligation['tokens']-charged), + protected_seconds=0 if closed else max(0, obligation['seconds']-seconds)) + tokens = sum(item['protected_tokens'] for item in obligations.values()) + seconds = sum(item['protected_seconds'] for item in obligations.values()) + pending = [name for name, item in obligations.items() if item['state'] != 'completed'] + deferred = [name for name, item in obligations.items() if item['state'] == 'deferred'] + deadline = min([_epoch(view['objective_deadline'])] + [session['deadline'] + for session in state.get('request_sessions', {}).values() if session.get('completion_obligation') in pending]) + available_seconds = max(0, deadline - max(now, state.get('observed_at', now))) + feasible = view['accounting_known'] and tokens <= view['remaining_tokens'] and seconds <= available_seconds + return dict(source=plan['source'], obligations=obligations, protected_tokens=tokens, protected_seconds=seconds, + pending=pending, deferred=deferred, feasible=feasible, obligations_completed=not pending, + completion_deadline=deadline, available_seconds=available_seconds, + outcome='obligations_completed' if not pending else 'checkpoint_required' if not feasible or deferred else 'pending', + objective_complete=False, next_action='Host must assess objective acceptance from the named evidence; request/test counts never establish closure.' if not pending else + 'Checkpoint or narrow authorized work; mandatory deferred obligations retain protected funds.' if not feasible or deferred else + 'Complete the named acceptance, review and bounded remediation within the existing allowance.', + time_basis='Conservative serial future work; accepted request timeout bounds remain charged to each obligation.') + + class ObjectiveLedger: _lock_flags = fcntl.LOCK_EX @@ -127,6 +175,7 @@ def _view(self, state, now, admitted=None): actors={key: dict(value, tokens_remaining=max(0, value['reservation'] - value['used']), session_deadline=value.get('session_deadline')) for key, value in actors.items()}) + result['completion'] = _completion_status(state, result, now) if admitted is not None: result['admitted'] = admitted return result @@ -311,6 +360,80 @@ def register_request_thread(self, thread_id, parent_thread_id, provenance, *, no raise ValueError('request thread provenance changed') return self._view(state,now) + def protect_completion(self, obligations, *, source, now): + """Trusted host reserves purpose, not another allowance or approval.""" + source = _completion_source(source) + if type(obligations) is not list or len(obligations) != 3: + raise ValueError('acceptance, review and bounded remediation obligations required') + values = {} + for item in obligations: + if (type(item) is not dict or set(item) != {'id', 'kind', 'tokens', 'seconds', 'criterion'} + or type(item['id']) is not str or not 0 < len(item['id']) <= 64 + or item['id'] in values or item['kind'] not in {'acceptance', 'review', 'remediation'} + or type(item['criterion']) is not str or not item['criterion'].strip() or len(item['criterion']) > 4096 + or _count(item['tokens']) == 0 or _count(item['seconds']) == 0): + raise ValueError('invalid bounded completion obligation') + values[item['id']] = dict(item, state='pending') + if {item['kind'] for item in values.values()} != {'acceptance', 'review', 'remediation'}: + raise ValueError('all mandatory completion kinds are required') + with self._transaction() as state: + self._observe(state, now) + self._view(state, now) + plan = dict(version=1, source=source, obligations=values) + if state.get('completion_plan'): + original = state['completion_plan'] + terms = ('id', 'kind', 'tokens', 'seconds', 'criterion') + if (original['source'] != source or set(original['obligations']) != set(values) + or any({key: original['obligations'][name][key] for key in terms} != + {key: values[name][key] for key in terms} for name in values)): + raise ValueError('completion protection already has immutable terms') + else: + if not state.get('request_binding') or state['actors']: + raise ValueError('completion protection must precede implementation request dispatch') + state['completion_plan'] = plan + state['completion_history'] = [] + return self._view(state, now) + + def assign_completion(self, thread_id, obligation_id, *, source, now): + """Only a host API can select purpose for an existing durable thread.""" + source = _completion_source(source) + with self._transaction() as state: + self._observe(state, now) + plan = state.get('completion_plan') + if not plan or obligation_id not in plan['obligations'] or thread_id not in state['request_sessions']: + raise ValueError('existing completion obligation and registered thread required') + session = state['request_sessions'][thread_id] + if session.get('completion_obligation') != obligation_id: + session['completion_obligation'] = obligation_id + state['completion_history'].append(dict(action='assign', thread_id=thread_id, + obligation_id=obligation_id, source=source, at=state['observed_at'])) + return self._view(state, now) + + def resolve_completion(self, obligation_id, outcome, *, source, note, now): + """Host disposition with verified evidence; deferral never frees protection.""" + source = _completion_source(source) + if outcome not in {'completed', 'deferred'} or type(note) is not str or not note.strip() or len(note) > 4096: + raise ValueError('explicit bounded completion disposition required') + with self._transaction() as state: + self._observe(state, now) + plan = state.get('completion_plan') + if not plan or obligation_id not in plan['obligations']: + raise ValueError('existing completion obligation required') + item = plan['obligations'][obligation_id] + disposition = dict(state=outcome, source=source, note=note) + if item['state'] == 'completed': + if item.get('disposition') != disposition: + raise ValueError('completed obligation disposition is immutable') + return self._view(state, now) + if outcome == 'completed' and any(not actor['settled'] or not actor['known'] + for actor in state['actors'].values() if actor.get('completion_obligation') == obligation_id): + raise ValueError('unsettled completion work cannot release protection') + if item.get('disposition') != disposition: + item.update(state=outcome, disposition=disposition) + state['completion_history'].append(dict(action=outcome, obligation_id=obligation_id, + source=source, note=note, at=state['observed_at'])) + return self._view(state, now) + def reserve_request(self, actor_id, thread_id, tokens, seconds, *, enforce=True, now): _count(tokens);_count(seconds) with self._transaction() as state: @@ -328,10 +451,52 @@ def reserve_request(self, actor_id, thread_id, tokens, seconds, *, enforce=True, admitted = not enforce or (view['ready'] and tokens<=view['remaining_tokens'] and used+tokens<=config['session_token_limit'] and seconds<=config['session_time_limit_seconds'] and state['observed_at']+seconds<=session['deadline']) + purpose = session.get('completion_obligation') + reason = None + if enforce and not admitted: + if not view['accounting_known']: + reason = 'request_accounting_unknown' + elif state['observed_at'] + seconds > _epoch(config['objective_deadline']): + reason = 'objective_deadline_unavailable' + elif tokens > view['remaining_tokens']: + reason = 'objective_token_allowance_unavailable' + elif used + tokens > config['session_token_limit']: + reason = 'session_token_allowance_unavailable' + else: + reason = 'session_time_allowance_unavailable' + completion = view['completion'] + if enforce and completion and admitted: + own = completion['obligations'].get(purpose, {}) + other_tokens = completion['protected_tokens'] - own.get('protected_tokens', 0) + other_seconds = completion['protected_seconds'] - own.get('protected_seconds', 0) + remediation = (state['completion_plan']['obligations'].get(purpose) or {}).get('kind') == 'remediation' + cycle_needed = remediation and not state['completion_plan']['obligations'][purpose].get('started') + if cycle_needed and state['correction_cycles'] >= config['max_correction_cycles']: + reason = 'completion_remediation_cycle_limit' + elif not completion['feasible']: + reason = 'completion_path_infeasible' + elif not purpose and completion['deferred']: + reason = 'mandatory_completion_deferred' + elif purpose and (own.get('state') == 'completed' or tokens > own.get('protected_tokens', 0) + or seconds > own.get('protected_seconds', 0)): + reason = 'completion_obligation_allowance_unavailable' + elif tokens + other_tokens > view['remaining_tokens'] or seconds + other_seconds > completion['available_seconds']: + reason = 'completion_resources_protected' + if reason: + admitted = False if admitted: + if completion and purpose: + item = state['completion_plan']['obligations'][purpose] + if item['kind'] == 'remediation' and not item.get('started'): + state['correction_cycles'] += 1 + item['started'] = True state['actors'][actor_id]=dict(terms=[tokens,seconds],reservation=tokens,used=0, - settled=False,known=True,session_deadline=session['deadline'],request_thread=thread_id) - return self._view(state, now, admitted=admitted) + settled=False,known=True,session_deadline=session['deadline'],request_thread=thread_id, + completion_obligation=purpose) + result = self._view(state, now, admitted=admitted) + if reason: + result['admission_reason'] = reason + return result def settle_request(self, actor_id, response_id, tokens, request_fingerprint, *, now): """Unknown completion retains the reservation; duplicate exact proof is inert.""" diff --git a/src/codex_wake/request_admission.py b/src/codex_wake/request_admission.py index 905d0d99..90e470f9 100644 --- a/src/codex_wake/request_admission.py +++ b/src/codex_wake/request_admission.py @@ -238,6 +238,19 @@ def __init__( supervision._save(path, allocation) supervision._save(self.source_path, source) + def protect_completion(self, obligations, *, source): + """Host-only planning; no HTTP body can install or release protection.""" + return self.ledger.protect_completion(obligations, source=source, now=self.now()) + + def assign_completion(self, thread_id, obligation_id, *, source): + """Retain the host's exact purpose assignment in request_sessions.""" + if thread_id not in self.thread_ids: + self._inherit_thread(thread_id) + return self.ledger.assign_completion(thread_id, obligation_id, source=source, now=self.now()) + + def resolve_completion(self, obligation_id, outcome, *, source, note): + return self.ledger.resolve_completion(obligation_id, outcome, source=source, note=note, now=self.now()) + def status(self): current = snapshot(self.root, self.source_thread) view = self.ledger.inspect(now=self.now()) @@ -250,6 +263,7 @@ def status(self): ) return dict( mode=mode, + completion_enforced=current.get("enabled") is not False, scope=self.objective_id, source_thread_id=self.source_thread, contract_sha256=self.contract_sha256, @@ -432,9 +446,18 @@ def _control(self, thread, context): ) return None + def _resource_summary(self): + view = self.ledger.inspect(now=self.now()) + completion = view.get("completion") or {} + return dict(used=view["tokens_used"], reserved=view["tokens_reserved"], + remaining=view["remaining_tokens"], cap=view["objective_token_limit"], + deadline=view["objective_deadline"], accounting_known=view["accounting_known"], + protected_tokens=completion.get("protected_tokens", 0), + protected_seconds=completion.get("protected_seconds", 0)) + def _notice(self, receipt, reason, request_fingerprint): receipt["request_admission_event"] = dict( - reason=reason, fingerprint=request_fingerprint + reason=reason, fingerprint=request_fingerprint, resources=self._resource_summary() ) supervision._record_supervision_notice(self.source_path, receipt, "admission") supervision._save(self.source_path, receipt) @@ -490,15 +513,22 @@ def execute(self, body): if not reserved["admitted"]: pause = self._pause_registered_goal(receipt, "request_admission_denied") notice = self._notice( - receipt, "objective_or_session_allowance_unavailable", fingerprint + receipt, reserved.get("admission_reason", "objective_or_session_allowance_unavailable"), fingerprint ) + explanation = (reserved.get("admission_reason") or "objective_or_session_allowance_unavailable") + ". " + completion = reserved.get("completion") + if completion: + explanation += ("Checkpoint or narrow authorized work; completion obligations remain pending. " + if completion["pending"] else "Named completion dispositions are recorded; objective acceptance remains separate. ") result = ( 429, "application/json", json.dumps( _error( "governor_admission_denied", - "Request denied before provider submission. Registered goal pause " + "Request denied before provider submission. " + + explanation + + "Registered goal pause " + ("verified" if pause["verified"] else "unverified") + "; independently admitted requests were not interrupted. Say Deactivate governor in the native conversation; no inference is needed for that control.", ) @@ -745,6 +775,8 @@ def bind_listener(self, url): supervision._lock(self.directory, supervision._name(self.source_thread)), ): source = json.loads(self.source_path.read_text()) + if source["request_accounting"].get("gateway_url") != url or "resources" not in source["request_accounting"]: + source["request_accounting"]["resources"] = self._resource_summary() source["request_accounting"]["gateway_url"] = url if source.get("allocation_path"): path = Path(source["allocation_path"]) diff --git a/tests/test_completion_reserve.py b/tests/test_completion_reserve.py new file mode 100644 index 00000000..f868c4b7 --- /dev/null +++ b/tests/test_completion_reserve.py @@ -0,0 +1,184 @@ +"""Completion funding through the public authenticated HTTP gateway.""" +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import hashlib +import json +from pathlib import Path +import tempfile +import threading +import unittest +from unittest.mock import patch +from urllib.error import HTTPError +from urllib.request import Request, urlopen + +import test_goal_supervision as fixtures +from codex_wake.goal_supervision import enroll +from codex_wake.objective_ledger import ObjectiveLedger +from codex_wake.request_admission import RequestGateway, contract_digest + +CHILD = '00000000-0000-7000-8000-000000000358' + + +class CompletionReserveTests(unittest.TestCase): + fixture = fixtures.GoalSupervisionTests.fixture + controller = fixtures.GoalSupervisionTests.controller + + @contextmanager + def campaign(self): + with tempfile.TemporaryDirectory() as directory: + root, policy, log, native, _ = self.fixture(directory) + clock = [fixtures.NOW] + with self.controller(root), patch('codex_wake.lifecycle_budget.trusted_upper', return_value=fixtures.NOW): + enroll(root, policy, fixtures.THREAD, 200, 120) + native['notices'] = {} + native['siblings'] = {CHILD: dict(goal=dict(threadId=CHILD, createdAt=fixtures.NOW, status='active'), active=['work'], calls=[])} + (log.parent / 'child.jsonl').write_text(json.dumps(dict(type='session_meta', + payload=dict(id=CHILD, parent_thread_id=fixtures.THREAD, source='cli'))) + '\n') + ledger = ObjectiveLedger(root, 'allocation-1') + ledger.configure(dict(revision=1, objective_token_limit=200, objective_time_limit_seconds=120, + session_token_limit=200, session_time_limit_seconds=120, max_correction_cycles=1, + started_at=fixtures.NOW, objective_deadline=fixtures.NOW + 120), + 'existing-fixture-authority', prior_tokens=40, now=clock[0]) + calls = [] + class Provider(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + def do_POST(self): + calls.append(json.loads(self.rfile.read(int(self.headers['Content-Length'])))) + response = dict(id='response-' + str(len(calls)), usage=dict(input_tokens=8, + input_tokens_details=dict(cached_tokens=0), output_tokens=2, total_tokens=10)) + data = ('data: ' + json.dumps(dict(type='response.completed', response=response)) + '\n\n').encode() + self.send_response(200) + self.send_header('Content-Type', 'text/event-stream') + self.send_header('Content-Length', str(len(data))) + self.end_headers() + self.wfile.write(data) + provider = ThreadingHTTPServer(('127.0.0.1', 0), Provider) + threading.Thread(target=provider.serve_forever, daemon=True).start() + self.addCleanup(provider.server_close) + self.addCleanup(provider.shutdown) + contract = dict(version=1, provider='owned-inert-fixture', model='fixture-model', + max_input_tokens=30, max_output_tokens=10, + input_limit='provider_enforced_reject_before_billing', output_limit='max_output_tokens_enforced', + usage='response.completed:fresh-v1', review='Inert local provider enforces literal input8/output2 test contract.') + gateway = RequestGateway(root, 'allocation-1', upstream_url=f'http://127.0.0.1:{provider.server_port}/v1/responses', + contract=contract, reviewed_contract_sha256=contract_digest(contract), + thread_ids=[fixtures.THREAD], now=lambda: clock[0]) + server = gateway.server() + threading.Thread(target=server.serve_forever, daemon=True).start() + self.addCleanup(server.server_close) + self.addCleanup(server.shutdown) + def submit(thread=fixtures.THREAD, turn='work', **claims): + body = dict(model='fixture-model', stream=True, input=[], client_metadata={ + 'x-codex-turn-metadata': json.dumps(dict(thread_id=thread, session_id=thread, turn_id=turn))}, **claims) + request = Request(f'http://127.0.0.1:{server.server_port}/v1/responses', data=json.dumps(body).encode(), + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + gateway.client_token}) + try: + with urlopen(request, timeout=5) as response: + return response.status, response.read() + except HTTPError as error: + return error.code, error.read() + def status(): + request = Request(f'http://127.0.0.1:{server.server_port}/status', headers={'Authorization': 'Bearer ' + gateway.client_token}) + with urlopen(request, timeout=3) as response: + return json.loads(response.read()) + def source(name, text): + path = Path(directory) / (name + '.txt') + path.write_text(text) + return dict(path=str(path), sha256=hashlib.sha256(path.read_bytes()).hexdigest()) + obligations = [dict(id=name, kind=name, tokens=40, seconds=4, + criterion='Host verifies ' + name + ' against the frozen objective') for name in ('acceptance', 'review', 'remediation')] + yield gateway, submit, status, source, obligations, clock, native, calls, ledger + + def test_implementation_leaves_completion_funded_and_body_role_cannot_select_it(self): + with self.campaign() as (gateway, submit, status, source, obligations, _, _, calls, _): + gateway.protect_completion(obligations, source=source('approved-plan', 'Existing approved objective completion obligations')) + self.assertEqual(submit()[0], 200) + denied = submit(CHILD, purpose='review', completion_obligation='review') + self.assertEqual(denied[0], 429, denied) + self.assertEqual(len(calls), 1) + self.assertIn(b'completion_resources_protected', denied[1]) + from codex_wake.goal_notices import list_notices + notice = [event for event in list_notices(gateway.root, fixtures.THREAD) if event['identity']['kind'] == 'admission'][-1] + self.assertIn('Used 50/200; reserved 0; remaining 150', notice['text']) + self.assertIn('completion protected 120 tokens/12s', notice['text']) + self.assertIn('completion_resources_protected', notice['text']) + self.assertLessEqual(len(notice['text'].encode()), 768) + current = status()['accounting']['completion'] + self.assertEqual(current['protected_tokens'], 120) + self.assertFalse(current['obligations_completed']) + self.assertEqual({item['state'] for item in current['obligations'].values()}, {'pending'}) + gateway.assign_completion(CHILD, 'acceptance', source=source('host-assignment', 'Existing controller assigns this exact child to acceptance')) + self.assertEqual(submit(CHILD)[0], 200) + current = status()['accounting'] + self.assertEqual(current['tokens_used'], 60) + self.assertEqual(current['tokens_reserved'], 0) + self.assertEqual(len(current['actors']), 2) + self.assertEqual(len(calls), 2) + + def test_verified_completion_releases_unused_protection_without_claiming_objective_closed(self): + with self.campaign() as (gateway, submit, status, source, obligations, clock, _, calls, _): + authority = source('approved-plan', 'Existing approved completion path') + gateway.protect_completion(obligations, source=authority) + self.assertEqual(submit()[0], 200) + for name in ('acceptance', 'review', 'remediation'): + gateway.assign_completion(CHILD, name, source=source('assign-' + name, 'Host assignment to ' + name)) + self.assertEqual(submit(CHILD)[0], 200) + evidence = source('result-' + name, 'Host verifies named ' + name + ' criterion against observed HTTP journey') + gateway.resolve_completion(name, 'completed', source=evidence, + note='Named criterion verified from the source artifact; request/test counts are not acceptance') + # Same approved plan is restart-safe; it cannot reset dispositions or spent resources. + gateway.protect_completion(obligations, source=authority) + if name == 'acceptance': + self.assertEqual(status()['accounting']['completion']['protected_tokens'], 80) + self.assertEqual(submit()[0], 200) # unused accepted-stage funds are available without a new grant + current = status()['accounting'] + self.assertEqual(current['tokens_used'], 90) + self.assertEqual(current['tokens_reserved'], 0) + self.assertEqual(len(calls), 5) + self.assertEqual(current['completion']['protected_tokens'], 0) + self.assertTrue(current['completion']['obligations_completed']) + self.assertFalse(current['completion']['objective_complete']) + self.assertEqual(current['correction_cycles'], 1) + self.assertEqual(current['objective_token_limit'], 200) + self.assertEqual(current['original_deadline'], fixtures.NOW + 120) + self.assertEqual(len(current['authorization_history']), 1) + closed = submit(CHILD) + self.assertEqual(closed[0], 429) + self.assertNotIn(b'obligations remain pending', closed[1]) + self.assertEqual(len(calls), 5) + + def test_time_infeasibility_and_deferral_checkpoint_without_releasing_mandatory_funds(self): + with self.campaign() as (gateway, submit, status, source, obligations, clock, native, calls, _): + gateway.protect_completion(obligations, source=source('approved-plan', 'Existing completion path')) + clock[0] = fixtures.NOW + 112 + denied = submit() + self.assertEqual(denied[0], 429) + self.assertIn(b'completion_path_infeasible', denied[1]) + self.assertIn(b'checkpoint', denied[1].lower()) + self.assertEqual(calls, []) + gateway.resolve_completion('remediation', 'deferred', source=source('checkpoint', 'Not enough original time remains'), + note='Partial checkpoint; mandatory remediation remains outstanding') + current = status()['accounting']['completion'] + self.assertEqual(current['protected_tokens'], 120) + self.assertEqual(current['protected_seconds'], 12) + self.assertEqual(current['deferred'], ['remediation']) + self.assertEqual(current['outcome'], 'checkpoint_required') + self.assertFalse(current['obligations_completed']) + self.assertEqual(submit()[0], 429) + self.assertEqual(calls, []) + native.update(active=['operator-control'], user_text='Deactivate governor') + native['goal']['status'] = 'active' + control = submit(turn='operator-control') + self.assertEqual(control[0], 200) + self.assertIn(b'Governor-generated acknowledgement', control[1]) + self.assertEqual(calls, []) + self.assertEqual(native['goal']['status'], 'paused') + native['user_text'] = 'Already-authorized ordinary follow-up' + self.assertEqual(submit(turn='operator-control')[0], 200) + current = status() + self.assertEqual(current['mode'], 'inactive') + self.assertFalse(current['completion_enforced']) + self.assertEqual(current['accounting']['tokens_used'], 50) + self.assertEqual(current['accounting']['completion']['deferred'], ['remediation']) + self.assertEqual(len(current['accounting']['authorization_history']), 1) diff --git a/tests/test_request_admission.py b/tests/test_request_admission.py index 91880c38..85d3519f 100644 --- a/tests/test_request_admission.py +++ b/tests/test_request_admission.py @@ -31,15 +31,15 @@ def test_concurrent_requests_share_allowance_before_provider_submission(self): return_value=fixtures.NOW, ), ): - enroll(root, policy, fixtures.THREAD, 110, 120) + enroll(root, policy, fixtures.THREAD, 100, 120) native["notices"] = {} ledger = ObjectiveLedger(root, "allocation-1") ledger.configure( dict( revision=1, - objective_token_limit=110, + objective_token_limit=100, objective_time_limit_seconds=120, - session_token_limit=110, + session_token_limit=100, session_time_limit_seconds=120, max_correction_cycles=0, started_at=fixtures.NOW, @@ -125,6 +125,9 @@ def do_POST(self): self.assertEqual(len(activation), 1) self.assertIn("Effective mode hard_admission", activation[0]["text"]) self.assertEqual(activation[0]["delivery"]["state"], "recorded") + self.assertIn("Used 40/100", activation[0]["text"]) + self.assertIn("reserved 0; remaining 60", activation[0]["text"]) + self.assertIn("2030-01-01T00:02:00Z", activation[0]["text"]) self.assertLessEqual(len(activation[0]["text"].encode()), 768) def submit(turn="work"): @@ -163,6 +166,10 @@ def submit(turn="work"): denied = second.result(timeout=3) self.assertEqual(denied[0], 429, denied) self.assertEqual(len(calls), 1) + denial_notice = [event for event in list_notices(root, fixtures.THREAD) if event["identity"]["kind"] == "admission"][-1] + self.assertIn("objective_token_allowance_unavailable", denial_notice["text"]) + self.assertIn("Used 40/100; reserved 50; remaining 10", denial_notice["text"]) + self.assertLessEqual(len(denial_notice["text"].encode()), 768) release.set() self.assertEqual(first.result(timeout=5)[0], 200) self.assertEqual(calls[0]["max_output_tokens"], 10) From da2f8480c7b8d673b999c91cdc0488856f5e9958 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:15:21 -0500 Subject: [PATCH 16/20] test: prove protected completion funding through real native requests (#357) --- .../plan0175/357-native-completion.md | 15 +++++ .../request_admission_native_acceptance.py | 55 ++++++++++++++++--- 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 docs/dev/evidence/plan0175/357-native-completion.md diff --git a/docs/dev/evidence/plan0175/357-native-completion.md b/docs/dev/evidence/plan0175/357-native-completion.md new file mode 100644 index 00000000..4677f122 --- /dev/null +++ b/docs/dev/evidence/plan0175/357-native-completion.md @@ -0,0 +1,15 @@ +# #357 native completion funding acceptance + +Source e8eaabe (worker82a30b9), extended native driver SHA256 `b61ac79908ec73dc90ed788c1b8609a280e7e38d70783d0bc0cf48b5f06fb9bf`. + +Command: `PYTHONPATH=src /bin/python scripts/request_admission_native_acceptance.py --app-server --frontend --output /tmp/governor357-native --completion`. + +PASS with the same pinned actual native/stock frontend as #354. Original cap207, measured warmup7 imported before hard coverage, per-request provider-enforced maximum50. Three named obligations protect50tokens/10seconds each. First implementation request spends50, bringing usage57; the next implementation request is denied BEFORE forwarding with150tokens protected. Actual TUI shows completion_resources_protected and fresh used/reserved/remaining/cap/deadline values. The host binds each named acceptance/review/remediation funding request; each real native request forwards once and settles50. Final usage207/207, reservations0, original deadline retained, correctioncycles1. All named funding obligations have source-backed host dispositions; objective_complete remainsFALSE because funding is not semantic acceptance. + +The single typed Deactivate governor command still works at ZERO through actual stockTUI, produces explicit Governor-generated acknowledgement with0paidinference, preserves usage/cap207 and pausedgoal, and starts no work. Live hardactivation includes actual Used7/207 and precedes guardeddispatch. Provider calls:1warmup+4guarded; externalprovidercalls0; freshownedhome processcensusEMPTY. No production service/mask/configuration or budget was changed. + +The named stages are INERT FUNDING FIXTURES, not actual code review or remediation of this repository. The requested one formal Standards/Spec review and one postreviewTDDpass remain unexecuted at this checkpoint. + +Primary affected selection: `PYTHONPATH=src:tests /bin/python -W ignore::DeprecationWarning -m unittest test_completion_reserve test_request_admission test_objective_ledger test_goal_notices test_governor_deactivation test_host_preflight -q` —30PASS15.201s. Initial local command named a nonexistent test_completion_resources module; its failed log /tmp/357-primary.log is not counted as passing evidence. Correct command /tmp/357-primary-final.log above. Worker24affectedcasesPASS7.452s; broader finalCI remains required. + +No arbitrary external provider or default installed route is claimed qualified. Semantic completion dispositions are trusted host/controller evidence, not inferred from counts. All source changes remain in the integration branch; deployment is separate. diff --git a/scripts/request_admission_native_acceptance.py b/scripts/request_admission_native_acceptance.py index fe34b148..0e1fa1d7 100644 --- a/scripts/request_admission_native_acceptance.py +++ b/scripts/request_admission_native_acceptance.py @@ -37,6 +37,7 @@ def main(): parser.add_argument('--app-server', type=Path, required=True) parser.add_argument('--frontend', type=Path, required=True) parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--completion', action='store_true', help='also prove protected native acceptance/review/remediation funding') options = parser.parse_args() options.output.mkdir(mode=0o700, parents=True, exist_ok=False) appserver, frontend = options.app_server.resolve(strict=True), options.frontend.resolve(strict=True) @@ -46,6 +47,7 @@ def main(): with path.open('rb') as stream: report[name+'_sha256'] = hashlib.file_digest(stream, 'sha256').hexdigest() report['script_sha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() + token_cap = 207 if options.completion else 57 expiry = time.monotonic()+150 calls, inbound = [], [] selected = {'gateway': None} @@ -80,7 +82,7 @@ def do_POST(self): self.send_error(413); return body = json.loads(self.rfile.read(size)) number = len(calls)+1 - assert number<=3, 'bounded provider request count exceeded' + assert number <= (5 if options.completion else 3), 'bounded provider request count exceeded' guarded = selected['gateway'] is not None if guarded: assert body.get('max_output_tokens') == 5 @@ -177,9 +179,9 @@ def turn(text): policy=root/'policy.json' policy.write_text(json.dumps(dict(version=1,callers={thread:dict(app_server='unix://'+str(endpoint), codex_path=str(frontend),codex_home=str(home),allocation_id='owned-request-scope', - max_tokens=57,max_seconds=120,rpc_timeout=5)}))) + max_tokens=token_cap,max_seconds=120,rpc_timeout=5)}))) policy.chmod(0o600) - enrolled=enroll(root,policy,thread,57,120,checkpoint_token_percent=0,checkpoint_seconds=0) + enrolled=enroll(root,policy,thread,token_cap,120,checkpoint_token_percent=0,checkpoint_seconds=0) save('enrollment.json',enrolled) assert enrolled['used_tokens']==7, enrolled # Interactive warmup may activate the goal. Pause it outside the hard @@ -195,8 +197,8 @@ def turn(text): row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) assert len(calls)==1, 'frontend warmup created an unaccounted request' now=trusted_upper() - configuration=dict(revision='owned-native-request-v1',objective_token_limit=57,objective_time_limit_seconds=120, - session_token_limit=57,session_time_limit_seconds=120,max_correction_cycles=1, + configuration=dict(revision='owned-native-request-v1',objective_token_limit=token_cap,objective_time_limit_seconds=120, + session_token_limit=token_cap,session_time_limit_seconds=120,max_correction_cycles=1, started_at=enrolled['time_origin'],objective_deadline=enrolled['deadline']) ledger=ObjectiveLedger(root,'owned-request-scope') ledger.configure(configuration,'explicit owned provider-free qualification',prior_tokens=7,now=now) @@ -206,10 +208,21 @@ def turn(text): gateway=RequestGateway(root,'owned-request-scope',upstream_url='http://127.0.0.1:'+str(provider.server_port)+'/v1/responses', contract=contract,reviewed_contract_sha256=contract_digest(contract),thread_ids=[thread], now=trusted_upper,request_timeout=10,client_token=token) + def completion_source(name, text): + directory=root/'completion-evidence'; directory.mkdir(mode=0o700,exist_ok=True) + path=directory/(name+'.txt'); path.write_text(text); path.chmod(0o600) + return dict(path=str(path),sha256=hashlib.sha256(path.read_bytes()).hexdigest()) + if options.completion: + gateway.protect_completion([dict(id=name,kind=name,tokens=50,seconds=10, + criterion='Prove the inert native funding path for '+name) + for name in ('acceptance','review','remediation')], + source=completion_source('plan','Host-approved inert funding proof; same original cap/deadline.')) gateway.bind_listener(listener_url); selected['gateway']=gateway activation_frame=wait_for(lambda: value if 'Effective mode hard_admission' in (value:=capture()) else None) (options.output/'hard-activation-tui.txt').write_text(activation_frame) + assert f'Used 7/{token_cap}' in activation_frame, activation_frame report['hard_activation_rendered_before_guarded_request']=True + report['activation_limits_rendered']=True first=turn('Run exactly one inert guarded request. Do not use tools.') assert first['status']=='completed',first assert len(calls)==2,calls @@ -218,6 +231,30 @@ def turn(text): denied=turn('This request must be stopped by the governor before reaching the provider.') assert len(calls)==2,calls save('after-denied.json',gateway.status()) + if options.completion: + # These names exercise resource funding, not semantic code review + # or remediation of this repository. The final review is separate. + current=gateway.status()['accounting'] + assert current['tokens_used']==57 and current['completion']['protected_tokens']==150,current + protected_frame=wait_for(lambda: value if 'completion_resources_protected' in (value:=capture()) else None) + (options.output/'protected-tail-tui.txt').write_text(protected_frame) + for number,name in enumerate(('acceptance','review','remediation'),1): + gateway.assign_completion(thread,name,source=completion_source('assign-'+name,'Host binds this fixture request to '+name)) + outcome=turn('Inert funding-path fixture for '+name+'. Do not use tools.') + assert outcome['status']=='completed',outcome + assert len(calls)==2+number,calls + gateway.resolve_completion(name,'completed',source=completion_source('result-'+name, + 'Observed native provider completion; measured50 and no protected-fund bypass for '+name), + note='Funding-path criterion observed; this is not semantic acceptance of the repository.') + current=gateway.status()['accounting']; save('after-tail.json',current) + assert current['tokens_used']==207 and current['tokens_reserved']==0,current + assert current['correction_cycles']==1,current + assert current['completion']['obligations_completed'] is True,current + assert current['completion']['objective_complete'] is False,current + assert current['original_deadline']==configuration['objective_deadline'],current + report.update(native_completion_funding=True,tail_phases_are_funding_fixtures=True, + tail_phases=['acceptance','review','remediation'],completion_protected_before_dispatch=True, + original_deadline_preserved=True,correction_cycles=1) assert rpc('thread/goal/get',threadId=thread)['goal']['status']=='paused', 'Admission denial did not pause autonomous continuation' wait_for(lambda: 'Ask Codex to do anything' in capture() and not any( row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) @@ -231,12 +268,12 @@ def turn(text): wait_for(lambda: not any(row['status']=='inProgress' for row in rpc('thread/turns/list',threadId=thread,limit=128,itemsView='notLoaded')['data'])) ack=wait_for(lambda: value if 'Governor-generated acknowledgement' in (value:=capture()) else None) (options.output/'zero-inference-control-ack.txt').write_text(ack) - assert len(calls)==2,'control action reached provider' + assert len(calls)==(5 if options.completion else 2),'control action reached provider' final=gateway.status(); save('after-control.json',final) - assert final['accounting']['objective_token_limit']==57,final - assert final['accounting']['tokens_used']==57,final + assert final['accounting']['objective_token_limit']==token_cap,final + assert final['accounting']['tokens_used']==token_cap,final assert rpc('thread/goal/get',threadId=thread)['goal']['status']=='paused' - report.update(state='passed',warmup_requests=1,guarded_provider_requests=1, + report.update(state='passed',warmup_requests=1,guarded_provider_requests=4 if options.completion else 1, denied_request_never_forwarded=True,control_at_zero_allowance=True, control_external_inference=0,control_rendered_in_stock_tui=True, original_usage_and_cap_preserved=True,thread_id=thread,denied_turn_status=denied['status']) From 545ec79c522f669300b5434f72c68dc867479c8d Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:21:15 -0500 Subject: [PATCH 17/20] docs: freeze completed governor tickets for requested review (#350) --- .../plans/0175-2026-10-25-transparent-governor-delivery.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index 9e79ad38..f75fc3e9 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -23,7 +23,7 @@ Primary #351 seams: enroll/show/poll plus retained notices and native notice ada ## Working controls -Use compact outputs and focused tests; inspect process/resource state before expensive fan-out. Do not repeat whole-source discovery. Counters for requested final review: 0; post-review remediation passes: 0. Keep historical red/green logs in /tmp and curate source evidence before closeout. +Use compact outputs and focused tests; inspect process/resource state before expensive fan-out. Do not repeat whole-source discovery. Counters for requested final review: 1 (in progress); post-review remediation passes: 0. Keep historical red/green logs in /tmp and curate source evidence before closeout. ## Non-goals @@ -52,3 +52,7 @@ The operator's implementation request implicitly accepts using existing public o #357 assigned agent dispatch355 in isolated feat/completion-reserve-357 basedb5f5f77. Add small named completion obligations/protection to existing requestledger/publicHTTPseam; preserve operator controls and originalgrant. Rootprimary owns final integration/acceptance. Formal review count0; post-review TDD remediation passes0. Fixedpoint remainsce1ad93. Full successor CI on b5f5f7720914fc990d63c22600a460dbbe1502d4 PASS both Python3.11/3.12 release-gates, run40241319799. The older49df3a9 failure remains superseded by a verified fix, not erased or retried asclean. #357 remains in progress. + +## Checkpoint: all child tickets closed; formal review started + +#351–#357 are CLOSED, read back from GitHub. Source checkpoint da2f848 has actual native completion-funding proof; latest release-gates run40243924355 is pending. The single requested formal review now starts with independent Standards and Spec axes against the pinned ce1ad93 baseline. Funding-fixture phase names did not consume the formal review or post-review TDD pass. Counters: review1 in progress; post-review TDD0. From 0a76bdf4ebf9addeab07ac2794c33348e88ec9b3 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:29:06 -0500 Subject: [PATCH 18/20] fix: enforce visible admission and honor disabled governor transport (#352, #353, #354) --- docs/dev/evidence/plan0175/review-and-tdd.md | 39 ++++++++++++ ...026-10-25-transparent-governor-delivery.md | 6 +- docs/request-admission.md | 17 +++++ src/codex_wake/goal_host_preflight.py | 3 + src/codex_wake/goal_supervision.py | 16 +++++ src/codex_wake/request_admission.py | 63 +++++++++++++------ tests/test_completion_reserve.py | 17 ++++- tests/test_governor_review_remediation.py | 52 +++++++++++++++ tests/test_request_admission.py | 2 + 9 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 docs/dev/evidence/plan0175/review-and-tdd.md create mode 100644 tests/test_governor_review_remediation.py diff --git a/docs/dev/evidence/plan0175/review-and-tdd.md b/docs/dev/evidence/plan0175/review-and-tdd.md new file mode 100644 index 00000000..c08b6b1f --- /dev/null +++ b/docs/dev/evidence/plan0175/review-and-tdd.md @@ -0,0 +1,39 @@ +# One formal review and one TDD remediation pass + +Frozen range: ce1ad93eec437c1b4854dde2c9a174d5c5f94255...545ec79c522f669300b5434f72c68dc867479c8d. Both independent axes completed before primary adjudication. Review invocation1; remediation pass1 completed. No second review is planned. + +## Standards + +Accepted nonblocking P3: duplicated allocation/session accounting persistence in request_admission.py constructor and bind_listener; only construction validates private allocation ownership/mode. Verified against both hunks. Consolidate in the supervision owner, consistently qualify the state. Baseline Duplicated Code/Feature Envy judgment supported by architecture policy0014; no mandatory standards breach found. + +## Spec + +Accepted blocking P1: hard preflight returns gateway readiness after static host qualification failure; execute does not gate provider submission on current activation delivery. Verified inspect_host hard branch and bind_listener/execute. This violates preflight/activation requirements. Reopen353/354 until public-path regression passes. Actual rendering remains a separately qualified native/TUI fact, never inferred from a notice read. + +Accepted blocking P2: execute starts the resource timer even when sync reports inactive; verified HTTPConnection and Timer both retain self.timeout. This violates deactivation semantics for requests dispatched after disable. Reopen352/354. Already-issued requests are a separate truthful boundary. + +## TDD seams + +- Public gateway execute/HTTP submission with native notice capability removed: catches unannounced hard-governed paid dispatch, not physical screen rendering. +- Public gateway HTTP response after durable operator deactivation and provider completion later than active timeout: catches stale timer/socket interventions; not an external provider qualification. +- Public bind_listener with an allocation made nonprivate: catches divergent persistent-state qualification; not filesystem atomicity against a hostile process. + +Work vertically: one red regression, minimal green, then next finding. Exact commands/outcomes follow below. + +Accepted findings: Standards1 P3; Spec2, highest P1. + +## Red/green receipt + +Runtime `/home/ecochran76/workspace.local/codex-wake/.venv/bin/python`, environment `PYTHONPATH=src:tests`, cwd integration worktree. Exact unittest commands: + +- `python -m unittest test_governor_review_remediation.GovernorReviewTests.test_missing_current_activation_prevents_provider_submission`: red200 vs expected503 (`/tmp/governor-review-p1-red.log`), greenPASS (`/tmp/governor-review-p1-green.log`). A lost current native activation now withholds provider I/O and reports recovery controls. +- `python -m unittest test_governor_review_remediation.GovernorReviewTests.test_disabled_governor_allows_completion_after_its_former_timeout`: red502 vs expected200 (`/tmp/governor-review-p2-red.log`), greenPASS (`/tmp/governor-review-p2-green-final.log`). The first green attempt had an indentation error, retained at `/tmp/governor-review-p2-green.log`; it was corrected, not counted as a pass. The final regression waits for actual provider arrival before crossing the former timeout. +- `python -m unittest test_governor_review_remediation.GovernorReviewTests.test_listener_refuses_nonprivate_shared_accounting_state`: qualified redfailed-to-raise (`/tmp/governor-review-p3-red-qualified-final.log`), followed by three-regression greenPASS (`/tmp/governor-review-all-green.log`). Earlier fixture setup errors are retained in `/tmp/governor-review-p3-red*.log`; they were not defect detections. Qualified red temporarily restored only the old listener write block, then the owner helper was reinstated. Both constructor and listener now use supervision-owned persistence with the same private-state validation. + +Affected selection `python -m unittest test_governor_review_remediation test_request_admission test_completion_reserve test_host_preflight test_governor_deactivation test_governor_visibility`:17PASS19.520s. The explicit selection is not the full suite. Pinned Ruff undefined-name checks and diff whitespace check PASS. + +Actual native completion/control driver rerun: `PYTHONPATH=src python scripts/request_admission_native_acceptance.py --app-server /home/ecochran76/.local/share/codex-wake/native/345-b0ce988429/bin/codex-app-server --frontend /home/ecochran76/.codex/packages/standalone/releases/0.164.0-x86_64-unknown-linux-musl/bin/codex --output /tmp/governor-review-native-final --completion` PASS. Receipt `/tmp/governor-review-native-final/receipt.json`: activation rendered before guarded send, numeric limits visible, four guarded requests, implementation denial never forwarded, completion protected, original deadline/cap/history preserved, typed deactivation at zero with0external inference, final owned PID census empty. Funding fixture names remain separate from the actual formal review/TDD recorded here. + +Full CI at pre-remediation da2f848: run40243924355 PASS3.11/3.12. Final-source successor CI remains required. No deployment or production-control changes. One formal review, one TDD remediation pass; all accepted findings addressed, no rejected or needs-evidence candidates remain. + +Source-bound final presubmit runner PASS: `/tmp/governor-final-validation-receipt.json`, manifest `/tmp/governor-final-manifest.json`. Required axes: pinned Ruff, policy wiring and17 affected public-seam cases. This receipt binds the exact dirty candidate, runtime and inherited environment; logs remain outside Git. Final three review regressions separately3PASS3.546s. diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index f75fc3e9..66ea5476 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -23,7 +23,7 @@ Primary #351 seams: enroll/show/poll plus retained notices and native notice ada ## Working controls -Use compact outputs and focused tests; inspect process/resource state before expensive fan-out. Do not repeat whole-source discovery. Counters for requested final review: 1 (in progress); post-review remediation passes: 0. Keep historical red/green logs in /tmp and curate source evidence before closeout. +Use compact outputs and focused tests; inspect process/resource state before expensive fan-out. Do not repeat whole-source discovery. Counters for requested final review: 1 (completed); post-review remediation passes: 1 (completed). Keep historical red/green logs in /tmp and curate source evidence before closeout. ## Non-goals @@ -56,3 +56,7 @@ Full successor CI on b5f5f7720914fc990d63c22600a460dbbe1502d4 PASS both Python3. ## Checkpoint: all child tickets closed; formal review started #351–#357 are CLOSED, read back from GitHub. Source checkpoint da2f848 has actual native completion-funding proof; latest release-gates run40243924355 is pending. The single requested formal review now starts with independent Standards and Spec axes against the pinned ce1ad93 baseline. Funding-fixture phase names did not consume the formal review or post-review TDD pass. Counters: review1 in progress; post-review TDD0. + +## Checkpoint: single review and remediation complete + +Frozen review545ec79 againstce1ad93: Standards1 P3 accepted; Spec2 accepted, highestP1. All addressed in one TDD pass with retained red/green evidence at docs/dev/evidence/plan0175/review-and-tdd.md. 352/353/354 reopened for explicit findings and now qualify for reclosure. Source-bound presubmit PASS all3axes; actual native completion/control rerun PASS, externalcalls0/ownedPIDs0. Full successor CI pending. Formal review1 completed; post-review TDD1 completed; no second invocation. diff --git a/docs/request-admission.md b/docs/request-admission.md index 72cd9691..47f835c0 100644 --- a/docs/request-admission.md +++ b/docs/request-admission.md @@ -266,3 +266,20 @@ show actual used/cap, reserved/remaining, protected completion funds/time, deadl and denial trigger. They disclose no raw actors or credentials. Delivery to native model context, operator rendering and human reading retain their separate qualification boundaries. + +## Activation delivery and inactive transport + +Governed provider sends require the current activation retained in native history, +available to model context, and acknowledged as published to operator clients. +Failure returns `governor_activation_unavailable` before reservation/provider I/O; +status exposes the failure independently. Native publication is not physical +screen-render proof; the pinned native/stock-TUI acceptance qualifies that separate +boundary. Hard-mode lifecycle preflight also requires successful selected-host +and notice capability qualification. Explicitly authorized manual repair and the +zero-inference deactivation control remain available. + +Requests submitted after durable deactivation have no governor output ceiling, +resource timer or socket timeout. Protocol/authentication/response-size constraints +remain independent transport boundaries. Disabling does not recall a timer already +issued for an admitted request or automatically resume paused work. Accounting +continues, without resource renewal. diff --git a/src/codex_wake/goal_host_preflight.py b/src/codex_wake/goal_host_preflight.py index 5bc6f4b2..04f2c62c 100644 --- a/src/codex_wake/goal_host_preflight.py +++ b/src/codex_wake/goal_host_preflight.py @@ -132,6 +132,9 @@ def inspect_host(root, profile, caller): result.update(mode='inactive',admitted=True,reason='operator_disabled',message='Governor remains disabled; no request-path gate is imposed and no resource authority is renewed.') return result if options['requested_mode'] == 'hard_admission': + if result['static'].get('capabilities', {}).get('notice_read') != 'supported': + result.update(reason='hard_host_notice_unqualified', message='Hard admission withheld: selected native host/notice qualification is unavailable. Existing authorized manual repair remains available; no downgrade or renewal occurred.') + return result try: from .request_admission import qualify_gateway qualified=qualify_gateway(root,profile,thread) diff --git a/src/codex_wake/goal_supervision.py b/src/codex_wake/goal_supervision.py index f215cace..2a53beb8 100644 --- a/src/codex_wake/goal_supervision.py +++ b/src/codex_wake/goal_supervision.py @@ -231,6 +231,22 @@ def _allocation_path(directory,allocation_id): return child/(hashlib.sha256(allocation_id.encode()).hexdigest()+'.json') +def _save_request_accounting(path, receipt): + """Persist shared accounting before its session projection. + + Caller holds maintenance then session locks; allocation lock is innermost. + A failed qualification must not overwrite the allocation or source receipt. + """ + if receipt.get('allocation_path'): + allocation_path = Path(receipt['allocation_path']) + with _lock(allocation_path.parent, allocation_path.stem): + _private(allocation_path) + allocation = json.loads(allocation_path.read_text()) + allocation['request_accounting'] = receipt['request_accounting'] + _save(allocation_path, allocation) + _save(path, receipt) + + def _allocation_admit(directory,receipt,old): renewal=bool(old and old['allocation_id']!=receipt['allocation_id']) anchor=None diff --git a/src/codex_wake/request_admission.py b/src/codex_wake/request_admission.py index 90e470f9..8d842a22 100644 --- a/src/codex_wake/request_admission.py +++ b/src/codex_wake/request_admission.py @@ -71,6 +71,21 @@ def _sse(text): ).encode() +def _activation_status(root, receipt): + """Current native delivery; screen rendering remains separate qualification.""" + from .goal_notices import NativeNoticeAdapter, read_notice + event_id = receipt.get("notice_ids", {}).get("request_activation") + try: + event = read_notice(root, event_id) + observed = NativeNoticeAdapter(root).reconcile(event) + published = event["proof"]["notification"]["state"] == "published" + return dict(qualified=bool(published and observed.get("transcript") + and observed.get("model_context")), event_id=event_id, + notification_published=published, operator_rendering="separately_qualified") + except (OSError, ValueError, KeyError, TypeError, WakeError): + return dict(qualified=False, event_id=event_id, reason="activation_delivery_unavailable") + + class RequestGateway: def __init__( self, @@ -229,14 +244,7 @@ def __init__( coverage="selected HTTP request path only", ) source["request_accounting"] = authority - if source.get("allocation_path"): - path = Path(source["allocation_path"]) - with supervision._lock(path.parent, path.stem): - supervision._private(path) - allocation = json.loads(path.read_text()) - allocation["request_accounting"] = authority - supervision._save(path, allocation) - supervision._save(self.source_path, source) + supervision._save_request_accounting(self.source_path, source) def protect_completion(self, obligations, *, source): """Host-only planning; no HTTP body can install or release protection.""" @@ -264,6 +272,7 @@ def status(self): return dict( mode=mode, completion_enforced=current.get("enabled") is not False, + activation=_activation_status(self.root, json.loads(self.source_path.read_text())), scope=self.objective_id, source_thread_id=self.source_thread, contract_sha256=self.contract_sha256, @@ -496,6 +505,20 @@ def execute(self, body): ) ).encode(), ) + if not inactive: + from .goal_notices import present_notice + if not receipt.get("notice_ids", {}).get("request_activation"): + receipt["request_accounting"]["resources"] = self._resource_summary() + supervision._record_supervision_notice(self.source_path, receipt, "request_activation") + supervision._save(self.source_path, receipt) + event = receipt.get("notice_ids", {}).get("request_activation") + if event: + present_notice(self.root, event) + if not _activation_status(self.root, receipt)["qualified"]: + return (503, "application/json", json.dumps(_error( + "governor_activation_unavailable", + "Hard-governed provider dispatch withheld: current model/operator activation delivery is unqualified. Inspect governor status or Deactivate governor; existing authorized manual repair remains available." + )).encode()) goal, active = supervision._native(self.grant, thread) if active != [context["x-codex-turn-metadata"]["turn_id"]]: raise ValueError("request is not the exact current native turn") @@ -541,7 +564,7 @@ def execute(self, body): address = self.provider_address connection = ( HTTPSConnection if address.scheme == "https" else HTTPConnection - )(address.hostname, address.port, timeout=self.timeout) + )(address.hostname, address.port, timeout=None if inactive else self.timeout) expired = threading.Event() socket_ref = {} @@ -554,9 +577,11 @@ def expire(): except OSError: pass - request_timer = threading.Timer(self.timeout, expire) - request_timer.daemon = True - request_timer.start() + request_timer = None + if not inactive: + request_timer = threading.Timer(self.timeout, expire) + request_timer.daemon = True + request_timer.start() try: connection.connect() socket_ref["socket"] = connection.sock @@ -580,7 +605,8 @@ def expire(): ) upstream = connection except Exception: - request_timer.cancel() + if request_timer is not None: + request_timer.cancel() connection.close() self.ledger.settle_request( actor, None, None, fingerprint, now=self.now() @@ -677,7 +703,8 @@ def expire(): ).encode(), ) finally: - request_timer.cancel() + if request_timer is not None: + request_timer.cancel() upstream.close() def server(self, host="127.0.0.1", port=0): @@ -778,12 +805,7 @@ def bind_listener(self, url): if source["request_accounting"].get("gateway_url") != url or "resources" not in source["request_accounting"]: source["request_accounting"]["resources"] = self._resource_summary() source["request_accounting"]["gateway_url"] = url - if source.get("allocation_path"): - path = Path(source["allocation_path"]) - with supervision._lock(path.parent, path.stem): - allocation = json.loads(path.read_text()) - allocation["request_accounting"] = source["request_accounting"] - supervision._save(path, allocation) + supervision._save_request_accounting(self.source_path, source) supervision._record_supervision_notice( self.source_path, source, "request_activation" ) @@ -851,6 +873,7 @@ def qualify_gateway(root, profile, thread): or status.get("contract_sha256") != binding.get("contract_sha256") or status.get("mode") not in ("hard_admission", "degraded") or not status.get("reviewed_provider_contract") + or not status.get("activation", {}).get("qualified") ): raise ValueError("gateway contract/scope/accounting is unavailable") if config_path.read_bytes() != config_bytes: diff --git a/tests/test_completion_reserve.py b/tests/test_completion_reserve.py index f868c4b7..df1385d3 100644 --- a/tests/test_completion_reserve.py +++ b/tests/test_completion_reserve.py @@ -24,12 +24,19 @@ class CompletionReserveTests(unittest.TestCase): controller = fixtures.GoalSupervisionTests.controller @contextmanager - def campaign(self): + def campaign(self, *, request_timeout=2, provider_release=None, provider_arrived=None, shared_allocation=False): with tempfile.TemporaryDirectory() as directory: root, policy, log, native, _ = self.fixture(directory) + if shared_allocation: + config = json.loads(policy.read_text()) + grant = config['callers'][fixtures.THREAD] + grant.pop('allocation_id') + policy.write_text(json.dumps(dict(version=2, allocations={'allocation-1': grant}, default_allocation='allocation-1', native_caller_binding=True))) clock = [fixtures.NOW] with self.controller(root), patch('codex_wake.lifecycle_budget.trusted_upper', return_value=fixtures.NOW): - enroll(root, policy, fixtures.THREAD, 200, 120) + context = dict(threadId=fixtures.THREAD, sessionId=fixtures.THREAD, callId='fixture-native', + **{'x-codex-turn-metadata': dict(thread_id=fixtures.THREAD, session_id=fixtures.THREAD, turn_id='work')}) + enroll(root, policy, fixtures.THREAD, 200, 120, native_context=context if shared_allocation else None) native['notices'] = {} native['siblings'] = {CHILD: dict(goal=dict(threadId=CHILD, createdAt=fixtures.NOW, status='active'), active=['work'], calls=[])} (log.parent / 'child.jsonl').write_text(json.dumps(dict(type='session_meta', @@ -45,6 +52,10 @@ def log_message(self, *args): pass def do_POST(self): calls.append(json.loads(self.rfile.read(int(self.headers['Content-Length'])))) + if provider_arrived is not None: + provider_arrived.set() + if provider_release is not None: + provider_release.wait(2) response = dict(id='response-' + str(len(calls)), usage=dict(input_tokens=8, input_tokens_details=dict(cached_tokens=0), output_tokens=2, total_tokens=10)) data = ('data: ' + json.dumps(dict(type='response.completed', response=response)) + '\n\n').encode() @@ -63,7 +74,7 @@ def do_POST(self): usage='response.completed:fresh-v1', review='Inert local provider enforces literal input8/output2 test contract.') gateway = RequestGateway(root, 'allocation-1', upstream_url=f'http://127.0.0.1:{provider.server_port}/v1/responses', contract=contract, reviewed_contract_sha256=contract_digest(contract), - thread_ids=[fixtures.THREAD], now=lambda: clock[0]) + thread_ids=[fixtures.THREAD], request_timeout=request_timeout, now=lambda: clock[0]) server = gateway.server() threading.Thread(target=server.serve_forever, daemon=True).start() self.addCleanup(server.server_close) diff --git a/tests/test_governor_review_remediation.py b/tests/test_governor_review_remediation.py new file mode 100644 index 00000000..27df54f0 --- /dev/null +++ b/tests/test_governor_review_remediation.py @@ -0,0 +1,52 @@ +"""Regressions from the single governor Standards/Spec review.""" +import unittest +import threading +from concurrent.futures import ThreadPoolExecutor +from codex_wake.goal_governance import change +from codex_wake.session_permissions import Principal +import test_goal_supervision as fixtures +import test_completion_reserve as completion_fixtures + + +class GovernorReviewTests(unittest.TestCase): + fixture = completion_fixtures.CompletionReserveTests.fixture + controller = completion_fixtures.CompletionReserveTests.controller + campaign = completion_fixtures.CompletionReserveTests.campaign + + def test_missing_current_activation_prevents_provider_submission(self): + with self.campaign() as (gateway, submit, status, source, obligations, clock, native, calls, ledger): + native['notices'] = {} + code, body = submit() + self.assertEqual(code, 503, body) + self.assertIn(b'governor_activation_unavailable', body) + self.assertEqual(calls, []) + self.assertEqual(ledger.inspect(now=clock[0])['tokens_used'], 40) + + def test_disabled_governor_allows_completion_after_its_former_timeout(self): + release, arrived = threading.Event(), threading.Event() + with self.campaign(request_timeout=0.05, provider_release=release, provider_arrived=arrived) as (gateway, submit, status, source, obligations, clock, native, calls, ledger): + change(gateway.root, fixtures.THREAD, enabled=False, + principal=Principal(fixtures.THREAD, 'operator'), allocation_id='allocation-1') + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(submit) + try: + self.assertTrue(arrived.wait(2), 'provider never received the inactive request') + # This wait tests the duration policy, not service readiness. + threading.Event().wait(0.15) + finally: + release.set() + code, body = future.result(timeout=3) + self.assertEqual(code, 200, body) + self.assertEqual(status()['mode'], 'inactive') + self.assertEqual(ledger.inspect(now=clock[0])['tokens_used'], 50) + + def test_listener_refuses_nonprivate_shared_accounting_state(self): + with self.campaign(shared_allocation=True) as (gateway, submit, status, source, obligations, clock, native, calls, ledger): + # Simulate a host permission change after successful construction. + allocation, = (gateway.root / 'goal-supervision' / 'allocations').glob('*.json') + allocation.chmod(0o644) + try: + with self.assertRaisesRegex(ValueError, 'private and owned'): + gateway.bind_listener('http://127.0.0.1:12345') + finally: + allocation.chmod(0o600) diff --git a/tests/test_request_admission.py b/tests/test_request_admission.py index 85d3519f..c73628ca 100644 --- a/tests/test_request_admission.py +++ b/tests/test_request_admission.py @@ -242,6 +242,7 @@ def test_heartbeat_cannot_extend_request_wallclock_or_refund_missing_completion( ), ): enroll(root, policy, fixtures.THREAD, 110, 120) + native["notices"] = {} ledger = ObjectiveLedger(root, "allocation-1") ledger.configure( dict( @@ -346,6 +347,7 @@ def test_native_descendant_inherits_scope_without_usage_and_session_spend_surviv ), ): enroll(root, policy, fixtures.THREAD, 200, 120) + native["notices"] = {} ledger = ObjectiveLedger(root, "allocation-1") ledger.configure( dict( From 38afb9c44b7817a518b3a329fd9566b19ea7a0ec Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:35:13 -0500 Subject: [PATCH 19/20] fix: preserve public preflight refusal reason after review (#353) --- docs/dev/evidence/plan0175/review-and-tdd.md | 2 ++ src/codex_wake/goal_host_preflight.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/evidence/plan0175/review-and-tdd.md b/docs/dev/evidence/plan0175/review-and-tdd.md index c08b6b1f..bc75e5f7 100644 --- a/docs/dev/evidence/plan0175/review-and-tdd.md +++ b/docs/dev/evidence/plan0175/review-and-tdd.md @@ -37,3 +37,5 @@ Actual native completion/control driver rerun: `PYTHONPATH=src python scripts/re Full CI at pre-remediation da2f848: run40243924355 PASS3.11/3.12. Final-source successor CI remains required. No deployment or production-control changes. One formal review, one TDD remediation pass; all accepted findings addressed, no rejected or needs-evidence candidates remain. Source-bound final presubmit runner PASS: `/tmp/governor-final-validation-receipt.json`, manifest `/tmp/governor-final-manifest.json`. Required axes: pinned Ruff, policy wiring and17 affected public-seam cases. This receipt binds the exact dirty candidate, runtime and inherited environment; logs remain outside Git. Final three review regressions separately3PASS3.546s. + +Final fullCI run40244973786 at0a76bdf failed exactly one existing public spawn regression on each Python version:1723tests,1failure,1skip. The new static qualification guard changed the established reason from hard_admission_unsupported to hard_host_notice_unqualified. Integration correction preserves the existing public reason and exposes qualification_failure=host_notice_unqualified separately. Full failed log retained `/tmp/governor-final-ci-failed.log`; this is the same reviewed P1 fix, not a second review/TDD invocation. Dispatch remains blocked before effects. Successor CI required. diff --git a/src/codex_wake/goal_host_preflight.py b/src/codex_wake/goal_host_preflight.py index 04f2c62c..5fea227e 100644 --- a/src/codex_wake/goal_host_preflight.py +++ b/src/codex_wake/goal_host_preflight.py @@ -133,7 +133,7 @@ def inspect_host(root, profile, caller): return result if options['requested_mode'] == 'hard_admission': if result['static'].get('capabilities', {}).get('notice_read') != 'supported': - result.update(reason='hard_host_notice_unqualified', message='Hard admission withheld: selected native host/notice qualification is unavailable. Existing authorized manual repair remains available; no downgrade or renewal occurred.') + result.update(reason='hard_admission_unsupported', qualification_failure='host_notice_unqualified', message='Hard admission withheld: selected native host/notice qualification is unavailable. Existing authorized manual repair remains available; no downgrade or renewal occurred.') return result try: from .request_admission import qualify_gateway From d69408ec7a5113a3a5b653851a978fbcf0dcafc7 Mon Sep 17 00:00:00 2001 From: Eric W Cochran Date: Sun, 25 Oct 2026 18:36:12 -0500 Subject: [PATCH 20/20] docs: close governor ticket delivery and record final validation boundary (#350) --- docs/dev/active-lanes.yaml | 8 ++++---- .../0175-2026-10-25-transparent-governor-delivery.md | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/dev/active-lanes.yaml b/docs/dev/active-lanes.yaml index fe8689ea..95565d86 100644 --- a/docs/dev/active-lanes.yaml +++ b/docs/dev/active-lanes.yaml @@ -8,14 +8,14 @@ lanes: plan: docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md branch: feat/transparent-governor target: origin/main - plan_state: OPEN - custody_state: ACTIVE_WORKTREE - checkpoint: b5f5f77 + plan_state: CLOSED + custody_state: PUBLISHED_REVIEW + checkpoint: 38afb9c44b7817a518b3a329fd9566b19ea7a0ec pull_request: https://github.com/CochranResearchGroup/codex-wake/pull/358 dependencies: [] overlaps: [] updated_at: 2026-10-25 - disposition: 351-356 CLOSED; dispatch355 owns 357; native request/control proof PASS; final review/remediation pending; no live effects + disposition: 351-357 CLOSED; review1/TDD1 complete with all3 findings addressed; native control/funding and source-bound checks PASS; final successor CI pending; PR358 integration open; no live effects - id: P173-GOAL-UX owner: primary / ecochran76 objective: Implement visible goal supervision and approved renewal diff --git a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md index 66ea5476..1da6224a 100644 --- a/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md +++ b/docs/dev/plans/0175-2026-10-25-transparent-governor-delivery.md @@ -1,6 +1,6 @@ # Transparent governor delivery -State: OPEN +State: CLOSED Owner: primary / ecochran76 Work items: GitHub CochranResearchGroup/codex-wake #351–#357 Specification: https://github.com/CochranResearchGroup/codex-wake/issues/350 @@ -60,3 +60,9 @@ Full successor CI on b5f5f7720914fc990d63c22600a460dbbe1502d4 PASS both Python3. ## Checkpoint: single review and remediation complete Frozen review545ec79 againstce1ad93: Standards1 P3 accepted; Spec2 accepted, highestP1. All addressed in one TDD pass with retained red/green evidence at docs/dev/evidence/plan0175/review-and-tdd.md. 352/353/354 reopened for explicit findings and now qualify for reclosure. Source-bound presubmit PASS all3axes; actual native completion/control rerun PASS, externalcalls0/ownedPIDs0. Full successor CI pending. Formal review1 completed; post-review TDD1 completed; no second invocation. + +## Source delivery closeout + +All seven child tickets351–357 are CLOSED after the single formal review and single TDD pass. Source delivery and delegation are complete; PR358 remains the integration boundary. One CI compatibility correction preserved the existing hard_admission_unsupported reason and adds qualification_failure detail, without changing the new host/notice guard. Exact existing spawn regression plus3 review regressions:4PASS3.595s. Failed full run40244973786 (1723tests,1failure,1skip on each version) is preserved; final successor matrix on the published closeout candidate must pass before PR readiness, with its exact run recorded in PR358. This plan's source-delivery closure does not claim a pending CI result, merge, release or deployment. + +Review1/1 completed; TDD remediation1/1 completed. Accepted Standards1 P3 and Spec2 (highestP1), all addressed. Source-bound presubmit and actual native/TUI proof remain at the evidence locator above. No new inference, shared runtime changes, original allocation renewal or silent service activation. Memory disposition: forbidden; no user request authorized a persistent memory update, machine receipt /tmp/governor-memory-disposition.json records0writes.