Skip to content

runner, server: opt-in independent Runner that outlives its gateways (P1R-C01) - #102

Merged
novelKR merged 1 commit into
mainfrom
claude/p1r-c01-runner-lifecycle-cjzogu
Oct 14, 2026
Merged

novelKR merged 1 commit into
mainfrom
claude/p1r-c01-runner-lifecycle-cjzogu

Conversation

@novelKR

@novelKR novelKR commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Issue

Part of #101: P1R-C01, the first P1-RECOVERY unit. This PR does not close an issue.

Item Value
Base main at 0f585b5, whose code tree is the qualified candidate ca306ee's
Head c22e823
Pins Codex gitlink with its recorded patch, and DevGuard 6e7e065; both unchanged
Decisions D3, D9, D11, D13, D14 and D15 at their recommended options (the decision column was left blank)

Summary

Before: the worker served the one gateway that started it and exited when that connection closed. Whenever a gateway shut down, was killed or lost its connection, every process it had started ended, and their output and timeouts went with them.

After: an operator can start the worker as an independent Runner. Its processes, their output and their original timeouts outlive the gateway: a gateway detaches from it, is restarted, is killed or loses its connection, and the processes keep running. The next gateway can query, read, write to, resize and terminate them with the same process_id, but it starts no new work until the Runner is stopped and a new one is started. Nothing else changes: managed stays the default, and the in-process runner and a gateway-started uds worker behave as before. The mode is documented as not supported yet. Reconnecting (P1R-C03) and reconciling (P1R-C04) come in later units.

An operator starts the Runner, and a gateway only connects to it:

codespace-codex-runtime "$RUNNER_DIR/runner.sock" --lifecycle independent
codespace-mcp --runner uds --runner-socket "$RUNNER_DIR/runner.sock" \
  --runner-lifecycle independent --runner-exit-intent detach

How.

  • Runner (crates/runner): IndependentRunner serves one connection at a time. Exit events go through a hub that outlives connections and holds an event a lost connection did not deliver for the next one. The next connection is accepted only after every request of the previous one has ended, the DevGuard preparation and launch answered beside the others included. Detach ends a connection and keeps the processes. Stop, or SIGTERM or SIGINT to the Runner, terminates every process, waits up to 5 seconds for them and exits. A connection that ends without either is a loss, never a stop. The Runner prepares its socket directory itself, 0700 and owned by its user, and refuses /, /tmp, /var/tmp and $HOME.
  • Hello states the lifecycle, the Runner's identity (boot id, pid, start time, nonce), whether an earlier connection started work and how many running processes this connection inherits.
  • Gateway (crates/server): it checks the new settings at startup, refuses a Runner whose lifecycle differs, and at shutdown sends Detach or Stop as --runner-exit-intent says. SIGUSR1 always detaches. It releases no lease when the connection ends and does not reconnect. A gateway whose Runner had an earlier controller refuses exec_command, apply_patch and operation_resume before an approval is claimed, so the approval stays as it was. In independent mode the gateway exits without waiting for a pending stdin read, so a signal ends it while its client keeps stdin open.
  • DevGuard (feature devguard): the independent Runner registers itself with the consumer secret from --devguard-credential-file, and the gateway hands it no secret. A workspace that requires participation runs only when the Runner states that it registers and launches.
  • Docs, English and Korean: runner isolation gains an "Independent Runner" section with what each event does in each lifecycle; error codes, the DevGuard integration guide (the P1R exception in the notice, the owner directive of 2026-10-14, the CS-RG adoption step naming ca306ee itself), operations, agent integration, architecture and Codex reuse are updated, with the registry hashes recorded.
  • CI: the macos-core stage of scripts/validate-upstream.py also runs the new runner, worker and gateway tests on hosted macOS.

Contract changes

These need the owner's approval at merge (D15). The error code names are proposed here.

  • Worker CLI: --lifecycle managed|independent (default managed), and, with the devguard feature, --devguard-credential-file <path>, accepted only with independent.
  • Gateway CLI: --runner-lifecycle managed|independent (default managed) and --runner-exit-intent detach|stop. independent requires --runner uds, --runner-socket and an exit intent, and refuses --runtime-bin and --runner-dir. An exit intent is refused without independent. In independent mode only, the gateway handles SIGTERM and SIGINT as a shutdown, and SIGUSR1 as a restart that detaches.
  • Worker protocol 7 → 8, still matched exactly. Hello gains lifecycle, identity (boot_id, pid, started_at_ms, nonce), previous_controller and inherited_processes. New operations Detach (answered Detached) and Stop (answered Stopped { terminated }), which a managed worker refuses.
  • Error codes: RECOVERY_PENDING for new work refused on a Runner that an earlier gateway controlled, and RUNNER_DISCONNECTED for any request after this gateway lost its Runner connection.
  • MCP instructions: in independent mode the process-lifetime paragraph is replaced by one of two texts, for detach and stop, that state only what the Runner guarantees, including that terminating a pipe process signals that process alone (OX-01). The default text is unchanged.
  • workspace_info.execution.process.capabilities.lifetime in independent mode: runner_disconnect is keep_running, and gateway_shutdown is keep_running with detach and terminate with stop. Other modes are unchanged.

Tests

The brief's cases and the tests that cover them. Three of them run a pipe and a tty process side by side: the runner's the_next_gateway_inherits_the_processes_but_starts_nothing and a_stop_ends_the_processes_and_the_runner, and the worker binary's an_independent_runner_outlives_its_gateways_until_one_stops_it. The others use pipe processes.

Case Tests
After a detach, a new gateway reads the same process_id's output and status, and the process times out at its original deadline runner the_next_gateway_inherits_the_processes_but_starts_nothing; worker binary an_independent_runner_outlives_its_gateways_until_one_stops_it; gateway binary an_independent_runner_keeps_a_killed_gateways_process_to_its_original_timeout
That gateway refuses new exec, apply_patch and approval resume, and the approval stays gateway an_independent_gateway_after_a_detach_starts_nothing_and_keeps_the_approval; the runner test above
An explicit stop ends the processes and the Runner runner a_stop_ends_the_processes_and_the_runner, a_stop_request_ends_the_connection_and_the_processes; worker binary sigterm_stops_an_independent_runner_and_its_processes; gateway binary a_gateway_leaves_or_stops_its_independent_runner_as_its_exit_intent_says
Wrong mode combinations and a start without an exit intent are refused gateway only_an_independent_runner_with_an_exit_intent_is_accepted; gateway binary a_gateway_refuses_independent_settings_or_a_runner_that_do_not_fit; worker binary an_independent_runner_refuses_a_shared_directory_and_unknown_settings
A detach or a stop whose answer is lost with the connection runner a_detach_or_stop_whose_answer_is_lost_still_holds
The gateway is SIGKILLed during the original timeout gateway binary an_independent_runner_keeps_a_killed_gateways_process_to_its_original_timeout
A call past its deadline closes the connection runner a_call_past_its_deadline_closes_the_connection_and_keeps_the_processes
A lost connection keeps the processes and their exit events runner a_lost_connection_keeps_the_processes_and_their_exit_events
The existing runtime and disconnect tests unchanged, and run in full below

Verified locally (cloud Linux, Rust 1.97.0):

  • cargo fmt --check for the root and every adapter, python3 scripts/check_docs.py ("Documentation registry verified: 20 paired documents"), the script tests (102) and scripts/check-no-model-deps.sh.
  • clippy with -D warnings: the root with --all-targets; the server, runner and domain with devguard; the worker with and without devguard; the patch adapter.
  • cargo test --workspace, with the patch helper and the worker built: 361 passed, none failed, independent_runner included.
  • cargo test -p codespace-server --features devguard: 141 passed, none failed. The 5 ignored tests are the existing child-process tests, and the tests that need DevGuard's fixture authority and launch helper return early without them here; CI's integration stage builds them.
  • The worker's tests (runtime_binary: 9 passed) and its DevGuard binary tests; cargo test -p codespace-domain --features devguard --lib: 32 passed.
  • cargo test -p codespace-runner --features devguard --lib: 168 passed and 1 failed, launch::tests::nothing_is_committed_when_the_owner_cannot_launch. It fails here because the cloud container runs as root, so the test's group-writable helper belongs to group 0, which counts as private. This PR changes neither launch.rs nor admission.rs, and CI runs the test as a non-root user.

Not verified locally: the macOS legs and the documentation site build (it needs Node 24.21.0); CI covers both.

CI: passed on this head in run 38583282332: all 17 jobs, Integration, Linux isolation and macOS contracts included. The hosted macOS leg ran the new runner, worker and gateway tests in its macos-core stage, and its 400 fork-race trials had no failure. The documentation site's run 38583283114 built the site.

Security scenarios

  • Socket directory. The Runner creates its directory 0700, or accepts an existing one only when it is a directory of the same user with mode 0700 and not a symbolic link. It never changes an existing directory and refuses shared ones (/, /tmp, /var/tmp, $HOME). It removes its socket when it stops, and the directory if it created it.
  • No secret over the wire. The independent Runner reads the DevGuard consumer secret from its own file; its gateway hands it none. The identity in Hello holds a boot id, a pid, a start time and a random nonce, none of them secret.
  • Rollback honesty. A gateway that lost its Runner refuses every later request with RUNNER_DISCONNECTED instead of pretending the processes ended, and a request in flight when the connection ended keeps the uncertain result it has today (dispatch_status: unknown). A gateway on a Runner it did not first control starts nothing, and an approval it refuses to resume is not consumed.
  • Process groups. Terminating a pipe process signals that process alone (OX-01), so its children can outlive it, a stop included. The instructions and docs say so rather than promise a group kill.
  • Rollback. Stop starting the new mode and stop the Runner, which drains its processes. Nothing moves running work into another mode.

Out of scope

  • Reconnection and Claim (P1R-C03), process records (P1R-C02), reconciliation (P1R-C04), Runner loss handling (P1R-C05) and the qualification harness (P1R-C06).
  • Fixes for OX-01 and OX-02, and any Codex or DevGuard pin change.
  • Supporting the mode: it stays unsupported until P1R-C06 qualifies it.

Requested by Seongjae · project thread

…(P1R-C01)

The worker takes `--lifecycle managed|independent`. `managed`, the
default, is unchanged. An independent Runner is started by an operator
or a harness and prepares its own 0700 socket directory. It serves one
gateway connection at a time: its processes, their output, their
timeouts and their exit events outlive each connection, and it accepts
the next connection only after every request of the previous one has
ended, a preparation or a launch included. A gateway leaves it with
Detach or ends it with Stop; a connection that ends without either is a
loss, and the processes keep running. SIGTERM or SIGINT stops the
Runner, which waits up to 5 seconds for its processes.

Hello states the lifecycle, the Runner's identity (boot id, pid, start
time and a nonce), whether an earlier connection controlled it and how
many processes it inherits. The worker protocol is now 8.

The gateway takes `--runner-lifecycle independent` only with `--runner
uds`, `--runner-socket` and `--runner-exit-intent detach|stop`, and
refuses a Runner whose lifecycle differs. At shutdown it detaches or
stops the Runner as the exit intent says, and SIGUSR1 always detaches.
It releases no lease when the connection ends and does not reconnect:
later requests fail with RUNNER_DISCONNECTED. A gateway that inherits
an earlier controller's processes can query, read, write to, resize and
terminate them, but refuses new executions, patches and approval
resumes with RECOVERY_PENDING, checked before an approval is claimed so
that the approval stays as it was. An independent gateway exits without
waiting for a pending stdin read, so a signal ends it while its client
keeps stdin open.

With DevGuard, the independent Runner reads its consumer secret from
`--devguard-credential-file`, and workspaces that require participation
run only when it states that it launches.

The MCP instructions and workspace_info's lifetime values follow the
mode. The docs describe the mode as not supported yet, with what each
event does in each lifecycle, and the CS-RG adoption step keeps naming
the qualified tree. The macos-core validation stage runs the new tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@novelKR novelKR self-assigned this Oct 14, 2026
@novelKR
novelKR marked this pull request as ready for review October 14, 2026 14:54
@novelKR
novelKR merged commit feb5f52 into main Oct 14, 2026
19 checks passed
novelKR pushed a commit to novelKR/DevGuard that referenced this pull request Oct 14, 2026
P1R-C01 (novelKR/CodeSpace#102) was merged on the owner's approval as
CodeSpace main feb5f52, and its post-merge CI is green. On the same
approval the owner started P1R-C02. The ledger records P1R-C01 as
merged-verified and P1R-C02 as active, and the dated notes say so.

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.

2 participants