Skip to content

pty, runner, server: observe governed PTY executions before the reap (CSRG-U5) - #95

Merged
novelKR merged 3 commits into
mainfrom
codex/csrg-u5-pty-observer
Oct 12, 2026
Merged

novelKR merged 3 commits into
mainfrom
codex/csrg-u5-pty-observer

Conversation

@novelKR

@novelKR novelKR commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Requested by Seongjae

Step 5 of option 3 in the owner's 2026-10-11 PTY decision. Governed PTY executions now get the same observation before the reap as governed pipe executions, through the Codex patch recorded in #93. Ungoverned PTY executions spawn exactly as before.

No pin change.

Issue

Scope: the CSRG-U5 follow-up that #92 and #93 prepared. Tracked in #76 and novelKR/DevGuard#14. This PR does not close an issue.

Item Value
Base main at 3d051fa (#93's merge)
Head 634a19f
Pins Codex gitlink 6b9826e3 with the recorded patch (tree 19999371), DevGuard 6e7e065; both unchanged

Summary

PTY adapter (codespace-pty). spawn_observed wraps the patch's spawn_pty_process_observed. It offers ExitWindow, ExitWatch, WindowEnd and WindowExpired; Codex types stay inside the adapter.

  • Release the ExitWindow to let the child be reaped; dropping it releases the child at once.

Runner.

  • A PTY process with an exit observer, which means a governed execution, is spawned observed, with OBSERVE_BOUND (2 s) as the window's bound.
  • When the window comes, the reaper's observer runs at BeforeReap through the pipe path's own bounded observe, and then the window is released. AfterReap follows Codex's reap, as before.
  • Processes without an observer spawn as before: spawn_inheriting with no descriptor is what spawn did.

Termination and the timeout in the window. I found this while designing, before writing the code.

  • During the window Codex's has_exited() is still false. Without a guard, a terminate_process or a timeout arriving then would record a kill, and a root that had exited on its own would be reported as terminated or timeout.
  • A pipe root's reaper already avoids this: exited_unreaped, pinned by U5's an_exited_child_keeps_its_one_reaper_whatever_else_acts_on_it.
  • For parity, the window's arrival marks the lifecycle exited, and the PTY termination and timeout paths leave the root to its reaper. This applies the pipe contract to the PTY rather than making a new decision.

DevGuard fixture authority (test binary). It gains pause and resume for its reconciler, and hold <ms> for its lock, macOS only.

Docs, in English and Korean.

  • The roadmap's CSRG-U5 section and the current status.
  • The patch record's "Use in CodeSpace" row.
  • The architecture page.
  • A follow-up sentence in the patch policy's case study.

Commits:

  • dc6bdbc pty, runner: observe a governed PTY root before its reap
  • 70a2441 devguard, server: governed PTY window tests against DevGuard's fixture authority
  • 634a19f docs: governed PTY executions are observed before the reap

Contract changes

  • A governed PTY root's exit status can be reported up to 2 s later while DevGuard observes it before the reap. A governed pipe root already behaves this way.
  • A terminate_process or a timeout arriving while a governed PTY root waits in its window no longer kills it or records a kill. The process reports how it ended, and the kill hook (DevGuard Terminate) is not called. A pipe root behaves the same way.
  • No tool schema, error code or status value changes.

Tests

W3 experiment B's cases on the PTY path, and pipe/PTY parity:

Case Tests
surviving same-group descendant released gateway a_surviving_pty_descendant_keeps_a_governed_attempt_charged_until_it_ends. The reconciler is paused around the exit, so only the window's observation can adopt the descendant. With observed spawns switched off, this test fails with the attempt suspect
fast exit runner a_pty_childs_observer_sees_the_exited_root_unreaped_then_reaped (replaces U5's after-reap-only test); the gateway's governed launch-once tests on a PTY
observer stall reaches the bound and stays suspect runner a_pty_observer_that_does_not_return_does_not_keep_the_child_unreaped; gateway a_stalled_observation_leaves_a_pty_survivor_suspect, with the authority held through the window
observer death reaps at once runner a_failing_observer_does_not_hold_the_child (pipe and PTY); adapter a_dropped_window_releases_the_child_at_once; the gateway lost-authority test (PTY)
the exit race runner an_exited_pty_child_keeps_how_it_ended_whatever_else_acts_on_it, the PTY counterpart of the pipe's an_exited_child_keeps_its_one_reaper_whatever_else_acts_on_it. Without the exited guard it fails with terminated against exited 3
output intact runner a_pty_childs_output_drains_while_it_is_unreaped (200,000 bytes in the window)
adapter mapping an_observed_child_is_offered_its_window_before_the_reap (both Unix paths), an_unreleased_window_is_reaped_at_its_bound
parity the governed launch test now counts two observations on a PTY; the stalled-authority control test keeps its bound for both

Verified on the owner's Mac (macOS 27.0, Apple M1). Every run used a minimal environment (env -i with HOME, PATH and TMPDIR), so no session variable reached any test child.

  • Runner: 118 tests without features, 157 with --features devguard.
  • Gateway: DevGuard tests 38 (5 ignored), and 31 without features.
  • PTY adapter: 10. Scripts: 65.
  • Clippy is clean for runner and server with and without the feature, and for both adapters.
  • validate-upstream.py all: every stage passed; linux-isolation needs Linux and did not run. Integration ran 663 tests.
  • macos-core macos-fork-race: passed. The gateway's DevGuard tests (38, the D6 cases and the new PTY window tests among them) passed. The fork race ran 400 trials with 0 failed, across 61,478 children.

CI on 634a19f: CI run 38187762188 passed all 17 jobs: plan, format and pin, policy scan, the three clippy legs, the seven unit legs, Linux isolation, integration, macOS contracts, and the rust summary. Documentation site run 38187762532 passed.

Security scenarios

  • No new descriptor or credential handling. The observed spawn passes the same two launch descriptors as before, and only to the helper.
  • The window never adds a reaper. Codex's single waiter reaps once, and a stalled or failed observation is bounded (2 s) and ends in that reap.
  • Local runs use a minimal environment, so no session variable reaches a test child or a log.

Out of scope

  • Pin changes, DevGuard releases, and changes to the installed services.
  • P1-RECOVERY and any qualification claim.
  • Governed execution on Linux, which waits for DG-LINUX.

novelKR and others added 3 commits October 12, 2026 09:05
The pinned Codex PTY reaps a PTY child in its own waiter at once, so U5
observed a governed PTY execution only after the reap. A descendant
that appeared, with its root exiting, before any observation saw it was
then lost to DevGuard's tracking. The Codex patch recorded in #93 lets
that waiter offer an observer one bounded window while the exited child
is still unreaped.

- codespace-pty: spawn_observed, with ExitWindow (release it, or drop
  it to release the child at once), ExitWatch and WindowEnd. Codex types
  stay inside the adapter.
- runner: a PTY process with an exit observer, that is a governed
  execution, is spawned observed, with OBSERVE_BOUND as the window's
  bound.
  - When the window comes, the reaper's observer runs as for a pipe
    root (BeforeReap, bounded); AfterReap follows the reap as before.
  - While a PTY root waits in its window, termination and the timeout
    leave it to its reaper, as for a pipe root, so how it ended is
    reported unchanged.
  - Processes without an observer spawn as before.
- Tests:
  - adapter, on both Unix paths: the window comes while the child is
    unreaped; an unreleased window is reaped at its bound and refuses a
    late release; a dropped window reaps at once.
  - runner, PTY: the observer sees the root unreaped, then reaped; a
    stalled observer holds it at most the bound; a failing observer
    releases it at once (pipe and PTY); termination and the timeout in
    the window keep its result and call no kill hook; output drains in
    full in the window.
  - The governed launch test now expects two observations on a PTY.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e authority (CSRG-U5)

- The fixture authority binary gains three commands, macOS only:
  `pause` and `resume` for its background reconciler, so that only an
  owner's Observe reconciles meanwhile, and `hold <ms>` for its lock,
  as a slow transition would hold it.
- A governed PTY root that leaves a descendant ignoring the terminal's
  hangup. With the reconciler paused around the exit, only the window's
  observation can adopt the descendant. The attempt stays charged while
  the descendant runs and is released after it ends. With observed
  spawns switched off, the old after-reap-only behaviour, this test
  fails with the attempt Suspect.
- A stalled observation: the authority is held through the window. The
  root is reaped at the bound, and the descendant, still running when
  DevGuard can look again, cannot be proven the scope's. The attempt
  stays Suspect, also after the descendant ends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pages that described PTY observation after the reap as an interim
state now describe the window, in English and Korean:

- The roadmap's CSRG-U5 section:
  - observation before the reap, for pipe and PTY alike;
  - termination and the timeout while a PTY root waits in its window;
  - the PTY paragraph;
  - the "not claimed" line;
  - the current status.
- The patch record's "Use in CodeSpace" row.
- The architecture page's current behaviour.
- A follow-up sentence in the policy's case study.

Translations recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@novelKR novelKR self-assigned this Oct 12, 2026
@novelKR
novelKR marked this pull request as ready for review October 12, 2026 00:39
@novelKR
novelKR merged commit 181a020 into main Oct 12, 2026
19 checks passed
novelKR pushed a commit to novelKR/DevGuard that referenced this pull request Oct 12, 2026
…n note)

CodeSpace merged novelKR/CodeSpace#95 on 2026-10-12 (main 181a020): the
recorded patch on the pinned Codex (novelKR/CodeSpace#93) now gives a
governed PTY root the same observation before the reap as a governed pipe
root, completing the PTY option the owner chose on 2026-10-11. The CS-RG
plan's dated current-state note said that until a separate Codex change
lands, U5 observes PTY executions only after Codex reaps them. Date the
note 2026-10-12 and say what holds now, English and Korean. The units
ledger is unchanged: U5 stays merged-verified, U6 not started, CS-RG
in-progress with qualification not-run. Korean counterpart reviewed;
planning-milestones-cs-rg hashes recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant