Repository navigation
store, runner, server: process records for the independent Runner (P1R-C02) - #103
Merged
Merged
Conversation
…R-C02) A gateway with an independent Runner records each execution in its operations database, in a new `processes` table, before it sends the execution to the Runner, and follows it to its end, so that a gateway that lost its memory can identify the same execution again. A record names the Runner by the identity its Hello states, the workspace, the approval it resumes, the DevGuard attempt, the participation, the tty and the SHA-256 digest of the argv; never the argv, its environment or a secret. It is `dispatching` until the Runner answers that it started the execution, `running` after that, and `terminal` once the Runner reports the end or answers that it did not start it. It keeps the deadline, the root process with its start identity and the furthest output cursor a gateway observed. The controller epoch stays empty until P1R-C03, and nothing records `unknown` until P1R-C05. An execution that resumes an approval is recorded in the transaction that marks the approval resuming. When the record cannot be written, the execution is refused with INTERNAL and nothing is sent. Exits that happened while no gateway was connected are recorded by the next gateway, and process_status and read_process record what the Runner reports. Updates only touch records of the Runner with the same full identity, so a Runner with an earlier Runner's pid changes nothing. A record never revives a handle and never alone proves that an execution started or how it ended. The database states its schema version in `user_version`, now 1. A database without one is brought up to it; a newer one is refused before anything is written. The patch ledger and approvals keep their columns, and an earlier CodeSpace still opens the database and leaves the new table alone. `--runner-lifecycle independent` now needs `--operations-db` and a Runner that states its identity. Such a gateway is the database's only writer: before it opens the database it takes an exclusive lock on `<database>.gateway.lock`, which names its pid, waits up to 15 seconds for a gateway that is still ending, and then refuses to start. Only this mode takes the lock. The Runner reports, in the exec answer and the governed launch, when the process times out and its root process: the pid with, on Linux and macOS, a start identity read before the reaper runs. A PTY root is not named, since the pinned Codex PTY does not give its pid. The worker protocol is now 9. The docs describe the records, the single writer, the schema version and why an earlier CodeSpace must not share the database, in English and Korean. The macos-core validation stage runs the store tests and the runner's root report test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Inside the Linux sandbox the shell's `$$` is its PID in the sandbox's own PID namespace, and the root the Runner reports is the sandbox helper, so the two record tests compared a host PID with a namespace PID wherever bubblewrap works, as in CI's integration stage. They now compare them only without the sandbox, and the restarted-gateway test checks, sandboxed or not, that the running execution's recorded root is a live child of the Runner. 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 14, 2026
P1R-C02 (novelKR/CodeSpace#103) was merged on the owner's approval as CodeSpace main 5976df0, and its post-merge CI is green. On the same approval the owner started P1R-C03. The ledger records P1R-C02 as merged-verified and P1R-C03 as active, and the dated notes say so. 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 · project thread
Issue
Part of #101: P1R-C02, the second P1-RECOVERY unit. This PR does not close an issue.
mainatfeb5f52, with P1R-C01 mergeda4f362c: the change82262c8and a test fix for the Linux sandbox6e7e065; both unchangedSummary
Before: a gateway connected to an independent Runner knew its executions only in memory. Once it was restarted or killed, nothing said which executions it had sent, to which Runner, for which approval, or how they ended. Any number of gateways could write to the same operations database, and the database did not say which CodeSpace version wrote it.
After: a gateway with an independent Runner records each execution in its operations database before it sends the execution to the Runner, and follows it to its end. A gateway that lost its memory finds the same execution by its
process_id: the Runner it was sent to, its workspace, approval and DevGuard attempt, its phase, its deadline, its root process and how it ended. A record never revives a handle and never alone proves that an execution started or how it ended; the Runner stays the authority. Such a gateway needs--operations-db, and it is the database's only writer: it waits up to 15 seconds for a gateway that is still ending, then refuses to start. The database now states its schema version, and a newer one is refused. Nothing changes outside the independent mode, which stays unsupported. Using the records to restore occupancy and approvals comes in P1R-C04.How.
crates/store): a newprocessestable. A record holds theprocess_id; the Runner's identity from itsHello(boot id, pid, start time, nonce); the controller epoch, empty until P1R-C03; the workspace, the approval it resumes, the DevGuard attempt, the participation (offorrequired), the tty flag and the argv's SHA-256 digest; the phase; the deadline the Runner reported; the root process's pid and start identity; the termination, exit code and signal; the furthest output cursor a gateway observed; and when it was created and last changed. The phase isdispatchinguntil the Runner answers that it started the execution,runningafter that, andterminalonce the Runner reports the end or answers that it did not start it. Nothing recordsunknownuntil P1R-C05. Only a gateway on the Runner with the same full identity changes a record, so a later Runner that reuses a pid changes nothing. An end reported before the start answer stays the end, and how an execution ended is recorded once.crates/server): the record is written before the request goes to the Runner, the DevGuard preparation and launch included. A resumed approval is markedresumingin the same transaction. When the record cannot be written, the request is refused withINTERNALand nothing is sent. The record becomesrunningwith the Runner's answer, or with a governed launch once its helper exists, and staysdispatchingwhen the answer is lost (dispatch_status: unknown). Each exit event releases the lease as before and is then recorded with whatprocess_statusreports. The next gateway records the exits that happened while no gateway was connected, andprocess_statusandread_processrecord what the Runner reports.PRAGMA user_version, now 1. A database without one, which every earlier CodeSpace wrote, is brought up to it, and the version is written last, so an upgrade cut short runs again. A database with a newer version is refused before anything is written. The patch ledger, works, intents and approvals keep their columns.flockon<database>.gateway.lock(0600), writes its pid there, and holds it until it exits, however it exits. A gateway that finds it held logs the holder, retries for up to 15 seconds and then refuses to start, naming the holder. Other modes take no lock.crates/runner): the exec answer and the governed launch report when the process times out and its root process, by pid and, on Linux and macOS, a start identity read before the reaper runs. A PTY root is not named, since the pinned Codex PTY does not give its pid. The worker protocol is now 9.macos-corestage ofscripts/validate-upstream.pyalso runs the store tests and the runner's root report test on hosted macOS.Contract changes
exec_commandandoperation_resumecan answer the existingINTERNALcode when the record cannot be written ("the execution could not be recorded before it was sent, so nothing was started"). A resume refused this way is consumed with that error, as any failed resume is today.--runner-lifecycle independentnow requires--operations-db(orCODESPACE_OPERATIONS_DB) and a Runner whoseHellostates its identity, which every P1R-C01 Runner does.<database>.gateway.lockbeside the operations database, mode 0600, holding the gateway's pid; a 15-second wait, then refusal. Independent mode only.PRAGMA user_version. Version 0 (no version) is upgraded in place, and a newer version is refused. New tableprocesseswith the indexidx_processes_workspace_phase; the other tables are unchanged.RunnerExecResult) and the governed launch (ManagedLaunch) gain the optional fieldsroot(pid,started) anddeadline_at_ms.Tests
The brief's cases and the tests that cover them.
a_new_gateway_finds_each_execution_from_dispatch_to_its_end,an_execution_the_runner_did_not_start_ends_as_not_started,an_end_without_details_is_completed_once; gatewaya_gateway_records_an_execution_from_its_dispatch_to_its_end,a_resumed_execution_is_recorded_with_its_approvala_crash_while_writing_leaves_neither_the_record_nor_the_resume(aborts a child process between the record's statements and its commit),a_damaged_database_is_refused_and_kept; gatewayan_execution_that_cannot_be_recorded_is_not_senta_newer_database_is_refused_and_left_as_it_isa_runner_with_the_same_pid_is_another_runner,a_root_is_the_recorded_process_only_with_its_start_identity; runnera_start_identity_names_one_process_until_it_is_reaped,a_spawn_reports_its_root_and_its_deadlinea_second_writer_waits_up_to_its_bound_and_is_refused,a_writer_that_lets_go_within_the_bound_is_waited_for,a_writer_killed_holding_the_lock_lets_go_of_it,the_lock_is_a_private_file_beside_the_database_naming_its_holder; gateway binarya_gateway_with_an_independent_runner_is_its_databases_only_writer,a_restarted_gateway_waits_for_the_one_before_it_and_finds_its_recordsan_execution_is_recorded_before_the_runner_receives_it(the gateway process aborts as the Runner receives the request, and the record is alreadydispatching),an_execution_whose_answer_is_lost_stays_dispatchingan_end_while_no_gateway_is_connected_is_recorded_by_the_next_gateway,an_end_a_gateway_missed_is_recorded_when_the_next_gateway_asks_the_runner,an_execution_that_ends_at_once_is_recorded_as_it_ended(24 rounds); storean_end_reported_before_the_start_answer_stays_the_end; gateway binarya_restarted_gateway_waits_for_the_one_before_it_and_finds_its_recordsa_database_from_before_schema_versions_is_brought_up_to_this_one,an_earlier_codespace_leaves_the_records_it_does_not_know(opens a database this version wrote withfeb5f52's statements and adds a patch operation)the_patch_ledger_and_the_approvals_keep_their_columns,a_record_keeps_no_argv_and_no_secret(searches every file of the database for the argv and a secret)only_an_independent_runner_with_an_exit_intent_is_accepted(now also needs--operations-db); gateway binarya_gateway_with_an_independent_runner_is_its_databases_only_writerVerified 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.cargo test --workspace, with the worker and the patch helper built: 391 passed, none failed, the new store, runner and gateway tests andindependent_runnerincluded.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: 151 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 9) and its DevGuard binary tests (2 passed);cargo test -p codespace-domain --features devguard --lib: 32 passed.cargo test -p codespace-runner --features devguard --lib: 170 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, it fails here only because the cloud container runs as root, so the test's group-writable helper belongs to group 0, which counts as private. CI runs it as a non-root user.integrationstage's tests again with the Linux sandbox on, as CI runs them (bubblewrap installed, the sandbox helper built and named):cargo test --workspace --no-fail-fast391 passed,cargo test -p codespace-server --features devguard151 passed, none failed.Not verified locally: the macOS legs and the documentation site build (it needs Node 24.21.0); CI covers both.
CI: passed on
a4f362cin run 38608571156: all 17 jobs,Integration,Linux isolationandmacOS contractsincluded. The hosted macOS leg ran the store tests, the runner's root report test, the independent gateway tests andindependent_runnerin itsmacos-corestage, and its 400 fork-race trials had no failure. The documentation site's run 38608572440 built the site.The first run on
82262c8, 38606709531, failed onlyIntegration: two record tests compared the shell's$$with the recorded root's pid, and inside the Linux sandbox$$is the shell's pid in the sandbox's own PID namespace while the root is the sandbox helper.a4f362ccompares them only without the sandbox, and checks, sandboxed or not, that the running execution's recorded root is a live child of the Runner.Security scenarios
dispatching, and only the Runner's answer or exit report makes itrunningorterminal. A record is changed only by a gateway on the Runner with the same full identity, so a reused pid changes nothing. For the reconciliation in P1R-C04, the store counts a root as the recorded process only with the same start identity.Out of scope
unknownphase (P1R-C05), and the qualification harness (P1R-C06).