Repository navigation
runner, server, store: Claim and controller epoch for the independent Runner (P1R-C03) - #104
Merged
Merged
Conversation
… Runner (P1R-C03) An independent Runner now serves every connection it accepts, and one connection at a time controls it after a Claim that names the gateway's operations database. The Runner grants a Claim only when the peer credentials of the connection show its own user, and only for the operations database of its first Claim, whose random identity the database keeps in a new `store_identity` table: a guard against a gateway with the wrong database, not a credential. A Claim is refused while another connection controls the Runner and is open, a stopped gateway's included. Once that connection ended, a Claim waits up to 10 seconds for its requests, preparations and launches included, and is refused past that bound. Each granted Claim raises the controller epoch, which never decreases, and answers with every process the Runner holds and the number of the last exit event that list reflects. Exit events are numbered and go to the controller only; one whose write failed goes to the next controller. Only the controller's requests are answered beyond Hello, Claim and Detach. Exec, apply_patch, write_stdin, resize, terminate, terminate workspace, Stop, and with DevGuard Prepare and Launch carry the epoch of the Claim they were sent under, and the Runner refuses one from an earlier epoch with the new STALE_CONTROLLER. The PR1 barrier holds: a controller after one that started work, its own earlier connection included, starts nothing and gets RECOVERY_PENDING. A gateway claims its Runner at start, asking again for up to 30 seconds while another connection controls it. When its connection is lost, or it closed it after a call deadline, it connects again, checks that the same Runner answers, and claims it again, pausing from 100 ms up to 5 s between tries. Meanwhile it refuses requests that need the Runner with the new RUNNER_RECONNECTING; once nothing accepts connections at the socket, another Runner answers there, or the Runner refuses the Claim for a reason that waiting cannot end, it refuses them with RUNNER_DISCONNECTED. Each exit is handed on once. Executions are recorded with the epoch they are sent under. A request refused with STALE_CONTROLLER, RECOVERY_PENDING, RUNNER_RECONNECTING or RUNNER_DISCONNECTED never ran: the approval it resumes is given back with return_resume or stays queued, a patch's operation is forgotten so its operation_key begins afresh, and an execution is recorded as not started. Whether new work can be sent is checked before claim_resume, and again once the workspace is held. The worker binds Tokio's listener, which codex-uds wraps on Unix, to read each connection's peer credentials, and logs each Claim with its peer's pid. The worker protocol is now 10, and Hello no longer states previous_controller and inherited_processes, which the Claim's answer replaces. The docs, English and Korean, describe the Claim, the epoch, the reconnect, the new codes, and the exposure that remains: while no gateway controls the Runner, any process of the same user that can reach its socket can claim it, a command run without the Linux sandbox included. The macOS validation stage, which runs these tests, now says that it covers the Claim's peer credentials. The mode stays unsupported. 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 15, 2026
P1R-C03 (novelKR/CodeSpace#104) was merged on the owner's approval as CodeSpace main 1fda789, and its post-merge CI is green. On the same approval the owner started P1R-C04. The ledger records P1R-C03 as merged-verified and P1R-C04 as active, and the dated notes say so as of 2026-10-15. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 15, 2026
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 · project thread
Issue
Part of #101: P1R-C03, the third P1-RECOVERY unit. This PR does not close an issue.
mainat5976df0, with P1R-C02 merged4e543096e7e065; both unchangedSummary
Before: an independent Runner served one connection at a time: the next gateway's connection waited, unanswered, until every request of the previous one had ended, and nothing checked who connected beyond the socket directory's permissions. Requests carried nothing that tied them to a controller. A gateway that lost its connection refused every later request with
RUNNER_DISCONNECTEDuntil it was restarted.After: one connection at a time controls the Runner, after a
Claimthat names the gateway's operations database, and every request that changes a process or the workspace carries the controller epoch of its Claim. The Runner refuses a request from an earlier epoch withSTALE_CONTROLLER, and nothing it asked for is done. A gateway that loses its connection claims the same Runner again without a restart, refusing requests meanwhile withRUNNER_RECONNECTING. A request refused with one of these codes never ran: it consumes no approval and leaves no patch result for itsoperation_key. The PR1 barrier stays global: a controller after one that started work, the same gateway after a lost connection included, starts nothing (RECOVERY_PENDING). While no gateway controls the Runner, any process of the same user that can reach its socket can claim it; the operator docs and the new mode's MCP instructions say so. The mode stays unsupported.How.
crates/runner,wire.rs): the Runner serves every connection it accepts. It grants a Claim only when the connection's peer credentials (Tokio'speer_cred:SO_PEERCREDon Linux,getpeereidon macOS) show its own user, and only for the operations database of its first granted Claim. It refuses a Claim while another connection controls it and is open, a stopped gateway's included (D6: nothing pushes a live controller out). Once that connection has ended, a Claim waits up toCLAIM_WAIT_BOUND(10 s) for the requests it sent, thePrepareandLaunchthat run beside the others included, and is refused past that bound with nothing changed. A granted Claim raises the epoch by one (it never decreases while the Runner lives) and answers with every process the Runner holds, each with its workspace and status, andevents_through, the number of the last exit event that list reflects. Refusals name a reason:peer_uid,store_mismatch,controller_active,requests_runningorstopping. The Runner logs each Claim it grants or refuses with the peer's pid.wire.rs): a connection that does not control the Runner gets answers toHello,ClaimandDetachonly.exec,apply_patch,write_stdin,resize,terminate,TerminateWorkspace,Stop, and with DevGuardPrepareandLaunchmust carry the current epoch, or the Runner answersSTALE_CONTROLLER. Exit events are numbered, go to the controller only, and an event whose write failed goes to the next controller.crates/store): the operations database keeps a random identity in a newstore_identitytable, created the first time it is asked for; the schema version stays 1. It guards against a gateway started with the wrong database and is not a credential, as the database's owner can read it.uds.rs,crates/server): at start the gateway claims the Runner, asking again with a growing pause for up to 30 s while another connection controls it or that connection's requests run, and refuses to start on any other refusal. When its connection is lost, or it closed it after a call passed its 120 s deadline, it connects again, checks that the same Runner (by its identity) answers, and claims it again: at once, then after 100 ms, doubling up to 5 s. Meanwhile requests that need the Runner getRUNNER_RECONNECTING. When nothing accepts connections at the socket, another Runner answers there, or the Runner refuses for a reason waiting cannot end, the gateway stops and refuses them withRUNNER_DISCONNECTED. Each exit is handed on once, by its event number. Each execution's record (PR2) keeps the epoch it was sent under.crates/server/src/mcp.rs,crates/store): whether new work can be sent is checked beforeclaim_resume(risk 15) and again once the workspace is held. A refusal withSTALE_CONTROLLER,RECOVERY_PENDING,RUNNER_RECONNECTINGorRUNNER_DISCONNECTEDproves that nothing ran: aresumingapproval is given back withreturn_resume, as CSRG-U5 does for a governed refusal, aqueuedone stays queued, a patch's operation is forgotten so itsoperation_keybegins afresh, and an execution is recorded as not started.crates/codex-runtime): the independent Runner binds Tokio'sUnixListener, whichcodex-udswraps on Unix, to read each connection's peer credentials, and logs each Claim.Contract changes
These need the owner's approval through the merge (D15).
claim({"store_id": …}), answeredclaimed(epoch,previous_controller,processes:[{workspace_id, status}],events_through) orclaim_refused(refusal:peer_uid|store_mismatch|controller_active|requests_running|stopping,message). A managed worker refusesclaimas it refusesdetachandstop.epochon requests andseqon events, both left out when absent.hellono longer carriesprevious_controllerandinherited_processes; the Claim's answer replaces them.STALE_CONTROLLER.RUNNER_RECONNECTING: the gateway is claiming its Runner again; the request was not sent and consumed no approval; retry after a pause.STALE_CONTROLLER: the Runner refused a request sent under an earlier Claim; nothing was done, and the approval it resumed is granted again.RUNNER_DISCONNECTEDnow means that the gateway cannot claim its Runner again; a lost connection alone givesRUNNER_RECONNECTING.RECOVERY_PENDINGalso follows a reconnect after this gateway started work.store_identity; schema version unchanged;processes.controller_epochis now filled.--runner-lifecycle independent, the gateway waits up to 30 s for another controller, then refuses to start, and refuses at once a Runner claimed for another operations database. No option changes.Tests
The brief's cases and the tests that cover them.
the_next_gateway_inherits_the_processes_but_starts_nothing(next epoch, the inherited processes, the original timeout),a_claim_states_the_inventory_and_the_exits_it_reflects,a_controller_that_starts_nothing_leaves_no_barrier; worker binaryan_independent_runner_outlives_its_gateways_until_one_stops_it(epochs 1 and 2 with the inventory and the Runner's Claim log)a_claim_from_another_user_is_refusedrequests_outside_the_current_claim_are_refused(no epoch, another epoch, and a request prepared under the earlier Claim that arrives after the next one),a_client_claims_its_runner_again_after_a_lost_connection; gatewaya_request_refused_as_stale_never_ran_and_gives_its_approval_backa_claim_for_another_operations_database_is_refused; storean_operations_database_keeps_its_identity; gateway binarya_gateway_with_another_operations_database_is_refusedof_claims_made_at_once_one_winsa_claim_is_refused_while_the_controller_is_open; gateway binarya_claim_is_refused_while_a_stopped_gateway_holds_the_runner_and_granted_once_it_endsa_claim_whose_answer_is_lost_is_asked_againa_claim_waits_for_the_requests_of_the_controller_before_it,a_claim_past_its_wait_bound_is_refused_and_changes_nothinga_sandboxed_command_cannot_reach_the_runner_socket(restricted and enabled network; the same command outside the sandbox connects)a_client_claims_its_runner_again_after_a_lost_connection(nothing runs twice, each exit once),a_runner_claimed_by_another_connection_meanwhile_is_claimed_once_it_ends,a_client_that_cannot_claim_its_runner_again_is_disconnected; clientan_exit_event_is_delivered_once; gatewaya_gateway_refuses_new_work_while_it_claims_its_runner_again(no record, no reservation, the approval unconsumed),a_gateway_that_started_work_claims_its_runner_again_and_starts_nothingqueued_scheduler_errors_do_not_consume_approval(the four codes),an_abandoned_operation_begins_afresh; gatewaya_request_refused_as_stale_never_ran_and_gives_its_approval_backa_managed_worker_refuses_claim_detach_and_stop,an_independent_runner_answers_registration_before_the_next_claimClaim latency (risk 12), measured in this container:
the_next_gateway_inherits_the_processes_but_starts_nothing).a_claim_waits_for_the_requests_of_the_controller_before_it).a_claim_past_its_wait_bound_is_refused_and_changes_nothing).a_claim_whose_answer_is_lost_is_asked_again).a_claim_is_refused_while_a_stopped_gateway_holds_the_runner_and_granted_once_it_ends).Verified locally (cloud Linux, Rust 1.99.0):
cargo fmt --checkfor the root and every adapter,python3 scripts/check_docs.py("Documentation registry verified: 20 paired documents"), the script tests (102),scripts/check-no-model-deps.shandscripts/check-upstream-pin.sh(Codex6b9826ewith its 1 recorded patch).-D warnings: the root with--all-targets; the server, runner and domain withdevguard; the worker with and withoutdevguard; the patch adapter.cargo test --workspace, with the worker, the patch helper and the Linux sandbox helper built and bubblewrap installed, so commands ran sandboxed as in CI'sintegrationstage: 413 passed, none failed,independent_runner(8) included.cargo test -p codespace-server --features devguard, with the DevGuard worker, fixture authority and launch helper built as CI'sintegrationstage builds them andCODESPACE_REQUIRE_DEVGUARD_BINS=1: 157 passed, none failed. The 5 ignored tests are the existing child-process tests that their parent tests run.runtime_binary: 9 passed, its handshake test now expecting protocol 10) and its DevGuard binary tests (2 passed);cargo test -p codespace-domain --features devguard --lib: 32 passed; the patch and DevGuard adapters' tests passed.cargo test -p codespace-runner --features devguard --lib: 184 passed and 1 failed,launch::tests::nothing_is_committed_when_the_owner_cannot_launch. As on runner, server: opt-in independent Runner that outlives its gateways (P1R-C01) #102 and store, runner, server: process records for the independent Runner (P1R-C02) #103, it fails here only because the cloud container runs as root; CI runs it as a non-root user.devguard(31 and 59 tests each time), and the gateway's independent tests once more (15): none failed.Not verified locally: the macOS legs and the documentation site build; CI covers both.
CI: passed on
4e54309: every job of run 38631018229 (17 jobs, the integration stage, Linux isolation and the macOS contracts included) and the documentation site's run 38631019062.Security scenarios
Hello,ClaimandDetachanswers, and no exit event.dispatch_status: unknown,APPROVAL_AMBIGUOUS).--runner-exit-intent stop) or with SIGTERM so it ends and drains its processes, then start the earlier version's Runner and gateway. The epoch is never decreased: a Runner's epochs only rise, and a new Runner is a new identity in the records. Keep the operations database; the earlier version leavesstore_identityalone.Out of scope
unknownphase and gap reasons (P1R-C05), the qualification harness (P1R-C06), and supporting the mode.