Repository navigation
devguard, runner, server, store: lifecycle of governed executions after launch (CSRG-U5) - #92
Merged
Merged
Conversation
A pipe child could be reaped by any of four paths sharing its tokio Child: its reaper task, the timeout task and request_kill (both called try_wait before killing), and tokio's orphan queue once the child was dropped. Whichever ran first reaped, so no path could observe the exit before the reap, as DevGuard needs for a governed scope. reap_child is now the only path that waits. The timeout and termination paths record their intent and kill only a child that has not exited, which they see with waitid(WEXITED | WNOHANG | WNOWAIT) without reaping it; an exited child is left to the reaper, which records how it ended. Until the reaper waits, the PID is still this process's child, so a kill never reaches another process. kill_on_drop stays as the hand-off at shutdown, once the reaper task is gone. Deterministic tests hold the reaper while the child is an exited, unreaped zombie and check that termination and the timeout neither reap it nor change its result, and that it is reaped exactly once. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 11, 2026
… reaped (CSRG-U5) DevGuard tracks what is left in a scope's group only while the exited root still holds its PID: a scope observed only after its root was reaped loses track of survivors, and the attempt stays charged as Suspect. DevGuard's own CLI waits for the root without reaping it, observes, reaps and observes again. The runner gets a generic exit observer: a pipe child's one reaper sees the exit without reaping (waitid with WNOWAIT), calls the observer while the root is an unreaped zombie, reaps it, and calls it again. The observer runs on a blocking thread for at most two seconds; when it fails or runs longer the reaper reaps anyway, so it never adds a reaper. A PTY child is reaped inside the pinned Codex PTY, whose API has no hook before the reap, so its observer runs after the reap only: a descendant DevGuard has not yet seen when the root exits is then lost to tracking, and its attempt stays charged rather than released. A managed launch's observer asks DevGuard to Observe the attempt (Owner::observe, a bounded session like lookup); the answer changes nothing CodeSpace does with the process. Tests: the runner's observer is called with the root exited and unreaped, then reaped; a slow observer does not keep the child unreaped; a PTY's observer is called after the reap only; a managed launch observes twice on a pipe and once on a PTY. Against DevGuard's fixture authority and helper (macOS): observed before the reap, a surviving descendant keeps the attempt charged and is released as ScopeTerminated once it ends; reaped first, the attempt turns Suspect with lost tracking and stays charged; and through the gateway, a governed pipe execution whose root leaves a descendant stays charged until the descendant ends, then is released. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ontrol without the authority (CSRG-U5) CodeSpace's termination and timeout kill a pipe child's root only, so a governed execution's descendants outlived them and kept its attempt charged. A kill hook now joins the exit observer in the spawn's hooks: when CodeSpace kills a process it calls the hook on its own blocking thread, never awaited, and a managed launch's hook asks DevGuard to Terminate the scope (Owner::terminate: SIGKILL to every identity it can recheck, members and known escapes). CodeSpace's own kill never depends on it, and delivery is not release evidence. An authority that stops answering after the launch leaves the process under CodeSpace's control: status comes from CodeSpace's own state, termination and the timeout kill it at once, and its reaper waits at most its bound for an observation before reaping. Tests: the kill hook runs once per kill CodeSpace makes, on a pipe and a PTY, and not for a process that ended on its own; a governed timeout asks DevGuard to terminate the scope once; with the authority stalled, termination and the timeout still end a governed process within the observation bound; against the fixture authority and helper (macOS), Terminate kills both the root and a background descendant and the scope is released as ScopeTerminated; through the gateway, with the fixture stopped, governed pipe and PTY processes report their status, are terminated, and free their workspaces. A regression test pins that a request cancelled after the launch's commit does not drop the launch's settlement (already so: the launch runs in its own task). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…approval (CSRG-U5) The CS-RG design binds approvals to the attempt's lifecycle: a resource refusal leaves the approval's hold unconsumed, and only a refusal proven not to have started the matching attempt permits the approval's reuse; a timeout, a lost answer or a missing process handle is not that proof. The gateway marked a resume as dispatching before the owner prepared the execution, so a refusal consumed the approval with the error, and a second resume replayed the refusal. ManagedPreparation::proves_not_started holds when the authority's state of the attempt proves the command never started: never asked, not recorded, denied, cancelled, expired, or released on the owner's report that it holds no helper. For such a refusal of a resumed approval, the gateway gives the hold back (Store::return_resume: resuming to granted), finish_resume persists nothing for it, and the approval can be resumed again, as a new attempt. Every other refusal, and any unknown, launched or scope-ended attempt, consumes the approval as before, and an interrupted resume is still reported ambiguous without a restart. Tests: the store gives back only an approval being resumed, keeps it granted with nothing persisted, and claims it for execution again; a resume the fixture authority denies keeps its approval, and resuming again is refused for a new attempt instead of replaying the refusal; U3's starts-nothing check now expects that new attempt, and one more owner session for it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ecycle (CSRG-U5) The order lists the ambiguity, release and race cases a governed execution must handle (sections 7 to 9). Most are covered by the tests of U3, U4 and the earlier U5 commits; this adds tests for the rest. No product code changes. - Authority restart while a governed process runs: CodeSpace keeps the process, its status and its workspace lease, and the owner registers again, so a new execution is admitted and released. DevGuard no longer knows the scope, so the ended attempt stays charged as Suspect, and resuming its approval again returns the same process. The gateway's fixture authority gains a `restart` command: it stops the authority and serves the same journal again, and answers once it admits. - A worker lost after it took a governed launch: the dispatch is unknown, the launch is never sent again, the workspace stays busy, and the resumed approval keeps that answer. - Shutdown: a running child is killed and, like a child that exited just before and is not reaped yet, left to tokio's orphan queue, never also to CodeSpace's reaper. - DevGuard's record: only a release on the owner's no-helper report reads as never started. A Suspect attempt (after a restart or a lost track) reads as launched, and a scope-ended or previous-boot release as ended. Only a state that proves no start gives an approval back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DevGuard integration roadmap gains a CSRG-U5 section, in English and Korean: - one reaper per process; - DevGuard observing a governed pipe root's scope before the reap, bounded; - PTY executions observed after Codex's reap. This is described as the interim state until the separate Codex pin change the owner chose, with the merge either including that change or asked about again; - a kill of a governed process also terminating its scope; - process control without the authority; - root exit, output EOF, handle, lease, scope end and accounting as separate events; - approvals whose execution provably never started keeping their hold; - the cases that are never replayed. The header notice and the current status name the unit, and U4's "not yet" paragraph points to the new section. The architecture page's current behaviour no longer says that three pipe paths each call try_wait. The execution contracts' approval paragraph states the hold returned on a proven non-start. Translations recorded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
novelKR
marked this pull request as ready for review
October 11, 2026 16:45
novelKR
pushed a commit
to novelKR/DevGuard
that referenced
this pull request
Oct 11, 2026
CSRG-U5 merged in CodeSpace on 2026-10-11 as novelKR/CodeSpace#92 (0c21a7f, at head 7b65c76, whose CI passed in full; the merge commit has that head's tree). Record U5 as merged-verified in the units dated 2026-10-11, keep U6 not started, and say so in AGENTS.md, the milestone table and note, the planning index and the CS-RG plan, English and Korean. The CS-RG plan also notes that until a separate Codex change lands, U5 observes PTY executions only after Codex reaps them, as the owner chose on 2026-10-11. CS-RG stays in-progress with qualification not-run, and the suspension of the CSRG-C00-C09 plan is unchanged. Korean counterparts reviewed; planning-readme and planning-milestones-cs-rg hashes recorded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
novelKR
pushed a commit
to novelKR/DevGuard
that referenced
this pull request
Oct 12, 2026
The owner directed CSRG-U6 to start on 2026-10-12, after CSRG-U5 merged in CodeSpace as novelKR/CodeSpace#92 (0c21a7f), recorded here by #31, and governed PTY executions came to be observed before the reap through novelKR/CodeSpace#93 and novelKR/CodeSpace#95, recorded by #32. Record U6 as active with the units dated 2026-10-12, and say so in AGENTS.md, the milestone table and note, the planning index and the CS-RG plan, English and Korean. CS-RG stays in-progress with qualification not-run, DG-1's qualified release is unchanged, and the suspension of the CSRG-C00-C09 plan is unchanged. Korean counterparts reviewed; planning-readme and planning-milestones-cs-rg hashes recorded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Seongjae
CSRG-U5 covers the lifecycle of a governed execution after launch:
Full local validation and CI passed on the final head (see Tests). No Codex or DevGuard pin change.
On PTY, observation still happens after the reap only. This is the interim state while a separate Codex pin PR is prepared, as the owner chose (option 3, 2026-10-11); it is not a final CS-RG limitation. The merge of this PR will either include that change, if it has merged first, or be asked about again.
Issue
Scope: the CSRG-U5 section of the owner's U1–U6 order (DevGuard
docs/handoff/2026-10-02-cs-rg-u1-u6-instruction.md), on top of merged U1–U4 and #91. This PR does not close an issue. Tracking: #76 and novelKR/DevGuard#14.mainat470069c(#91's merge)7b65c766b9826e3and DevGuard source6e7e065, both unchangedSummary
Inventory (order section 2), taken on
470069cbefore this PR:Child)reap_child(try_waitevery 20 ms)child.wait()right after the spawntry_wait, else kill the roothas_exited(), elsesession.kill()request_kill:try_wait, else kill the roothas_exited(), elsesession.kill()kill_on_dropand tokio's orphan queueProcessHandledrop (terminate)Preparer::prepare_and_launch, U4). A cancelled request drops neither its transcript wait nor its settlementSettlerpolls status and kills throughkill_host; it never waitsA pipe child had up to four possible reapers sharing one
Child. There was no doublewaitpid, but whichever path ran first reaped. A PTY child has one reaper, inside Codex.Correction: the first version of this body said a cancelled request drops a managed launch's settlement. A test written to show it (
a_cancelled_request_does_not_drop_a_committed_launch) passed before any change. It stays as the regression test for that race.Observation before reap (section 3). DevGuard releases a claimed scope only once it has observed the scope's end. Survivors stay tracked only if the scope is observed while the exited root still holds its PID (
Observe). The pinned client already hasobserveandterminate.waitidwithWNOWAIT), has DevGuard observe, then reaps it and observes again.Dispatching state (section 6). An approval's dispatching state (
resuming) is written before the owner prepares the governed execution, so it never lags the launch boundary (BeginLaunch).granted.BeginLaunchitself would need a wire change: inudsmode the worker would have to wait for the gateway's write before it sendsBeginLaunch. Recovering governed attempts after a restart is P1-RECOVERY. Both are outside this unit.Commits:
024e477runner: each pipe child has one reaper.reap_childis the only path that waits.waitidwithout reaping.kill_on_dropstays as the hand-off at shutdown.9c57245devguard, runner, server: a governed scope is observed before its root is reaped.ProcessHooks,ExitPoint::{BeforeReap, AfterReap}). It runs on a blocking thread with a 2 s bound, and its timeout or failure only skips the observation.Observe(Owner::observe).efd5070devguard, runner, server: killing a governed process also terminates its scope.Owner::terminate, which sends SIGKILL to every identity DevGuard can recheck. It runs on its own thread and is never awaited.c143b2crunner, server, store: a resumed approval whose governed execution is refused with proof of non-start keeps its hold (ManagedPreparation::proves_not_started,Store::return_resume). Every other outcome consumes it as before.956b3b8runner, server: tests for the section 7–9 cases that were not yet covered. These are an authority restart, a worker lost after a launch, shutdown, and how DevGuard's record maps to "never started". The gateway's fixture authority gains arestartcommand for the restart test.7b65c76docs: a new "lifecycle after launch" section in English and Korean, plus fixes to the architecture page's current behaviour and to the execution contracts' approval paragraph.Cases in order sections 7–9 and their tests. Tests added by this PR are marked U5. The adapter and gateway tests run real process trees (background descendants) against DevGuard's fixture authority and its real launch helper.
a_transcript_that_does_not_settle_in_time_is_an_unknown_dispatch(U4); adaptera_timed_out_admission_may_still_have_reserved(U3)a_transcript_that_does_not_settle_in_time_is_an_unknown_dispatch(U4); adaptertranscripts_keep_ready_apart_from_what_the_executable_did(U4)a_launch_lost_with_the_worker_is_unknown_and_never_sent_again(U5); gatewaya_preparation_lost_with_the_worker_is_unknown(U3); adaptera_timeout_or_a_lost_reply_is_unknown_not_a_refusal(U3)a_cancelled_request_does_not_drop_a_committed_launch(U5)a_launch_lost_with_the_worker_is_unknown_and_never_sent_again(U5: unknown dispatch, workspace busy, approval consumed); runneronly_a_state_that_proves_no_start_releases_an_approval(U5)a_reap_before_any_observation_loses_a_surviving_descendant(U5: suspect, tracking lost); runneronly_a_no_helper_release_reads_as_never_started(U5: a suspect attempt reads as launched)an_authority_restart_keeps_a_running_attempt_charged_and_never_replays_it(U5)the_in_process_gateway_launches_each_governed_execution_onceandthe_uds_worker_launches_each_governed_execution_once(U4: resuming again returns the same process); runnerbegin_launch_is_sent_once_and_never_after_an_unknown_answer(U4); adapterthe_same_attempt_for_another_command_is_refused(U3); gatewaya_denied_resume_keeps_its_approval_for_another_resume(U5: a new attempt, never a replayed refusal)a_granted_launch_runs_the_executable_once_in_its_bound_scope(U4); the gateway launch-once tests (everything released afterwards, U4)an_exec_failure_after_ready_is_settled_by_its_scope(U4); runneran_exec_failure_after_ready_is_not_reported_as_unstarted(U4)terminate_kills_the_whole_scope_and_it_is_released(U5); gatewaya_governed_process_is_controlled_like_any_other(U4)a_helper_that_cannot_present_the_grant_never_runs_the_executable(U4); runnera_helper_that_never_runs_the_command_is_reported_once_it_is_reaped(U4)a_surviving_descendant_keeps_a_governed_attempt_charged_until_it_ends(U5); adapteran_observation_before_the_reap_keeps_a_surviving_descendant_tracked(U5)an_observer_that_does_not_return_does_not_keep_the_child_unreaped(U5) andan_authority_that_stops_answering_leaves_process_control_to_codespace(U5). After that the release is DevGuard's, by its own evidencea_lost_authority_leaves_a_governed_process_under_codespaces_control(U5)a_shutdown_leaves_each_child_to_one_reaper(U5)a_reap_before_any_observation_loses_a_surviving_descendant(U5); the gateway restart test (stays charged as suspect, U5)Durability: CodeSpace states a release, or a proof that a command never started, only from DevGuard's record of the attempt in DevGuard's answer. It never infers either from its own process state.
a_terminated_child_keeps_its_one_reaper(U5)a_timed_out_child_keeps_its_one_reaper(U5)a_shutdown_leaves_each_child_to_one_reaper(U5: a running child and an exited, unreaped one)a_surviving_descendant_keeps_a_governed_attempt_charged_until_it_ends(U5); runnerroot_exit_output_eof_and_release_are_separate_events(U5)the_observer_sees_the_exited_root_unreaped_then_reaped(U5); the adapter's two observation-order tests (U5)a_helper_that_never_runs_the_command_is_reported_once_it_is_reaped(U4); adaptertranscripts_keep_ready_apart_from_what_the_executable_did(U4)a_launch_lost_with_the_worker_is_unknown_and_never_sent_again(U5); runneruds_exec_lost_response_is_ambiguous(earlier)a_cancelled_request_does_not_drop_a_committed_launch(U5)a_lost_authority_leaves_a_governed_process_under_codespaces_control(U5); runneran_authority_that_stops_answering_leaves_process_control_to_codespace(U5)Contract changes
terminate_process, workspace termination or a timeout), it also asks DevGuard to terminate the process's scope. This is best effort and never awaited. Before, only the pipe root was killed.grantedinstead of being consumed with the refusal. Resuming it again asks for a new attempt. U3's starts-nothing check now expects that new attempt and one more owner session.Tests
7b65c76, on the owner's Mac (macOS 27.0, Apple M1):scripts/validate-upstream.py allpassed every stage.linux-isolationneeds Linux and was not run there.integrationran 653 tests (5 ignored),unit-devguard58 (4 ignored),unit-codex-runtime8 andunit-pty7.macos-core macos-fork-racepassed. Adapter 58 (4 ignored), runner DevGuard filter 48, gateway DevGuard 36 (5 ignored); fork race 400 trials, 0 failed, 59,952 children, 0 without an exit code, 0 new crash reports.--features devguard; gateway DevGuard 36, and 31 without features; store 48; the scripts' 55 tests.7b65c76: success, every job (Linux isolationskipped by the plan). Linux integration 653 (5 ignored); unit-devguard 38 (3 ignored); macOS contracts 183 (9 ignored); fork race 400 trials, 0 failed. Docs site run 38155528045: success.024e477,9c57245,efd5070and956b3b8were cancelled by the next push. In each, every Linux job passed; onlyRust / macOS contractswas cancelled, and therustaggregate failed on that alone. The run onc143b2cpassed.Security scenarios
restartcommand and the new test helpers are test-only (an example binary andcfg(test)code). No product links them.Out of scope
past_the_credentialtest retries, dropped by the owner's decision.