Skip to content

CSRG-U3: admission, attempt identity and the pre-spawn slot lifecycle - #88

Merged
novelKR merged 4 commits into
mainfrom
codex/csrg-u3-admission
Oct 7, 2026
Merged

novelKR merged 4 commits into
mainfrom
codex/csrg-u3-admission

Conversation

@novelKR

@novelKR novelKR commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Requested by Seongjae

CSRG-U3: admission, attempt identity and the pre-spawn slot lifecycle. A governed execution now goes through authorization and workspace readiness, then a CodeSpace process slot, then DevGuard admission, then a one-shot prepared execution. Managed launch is U4. Until it lands, a successful preparation cancels the unstarted attempt and returns MANAGED_LAUNCH_UNAVAILABLE, the default the owner chose for section 7. No governed payload is spawned, and nothing runs through a non-governed path.

Issue

Scope: the CSRG-U3 section of the owner's U1–U6 order (DevGuard docs/handoff/2026-10-02-cs-rg-u1-u6-instruction.md), with the standing rules of the U2 order. It does not close an issue. Tracking: #76 and novelKR/DevGuard#14.

Base, head and pins

Item Value
Base main; started from ebed574 (#84's merge, U2), with main at 579c075 (#87's merge) merged in as 87c0055
Head 4af3febee402ed6050d6ca90f95394f3b83dc5e5
Commits 5dc5332 (slot, adapter, owner-side preparation, policy, error codes), 5fc4d3c (gateway, worker, fixture, CI filter, docs), 87c0055 (merge of main at 579c075), 4af3feb (default request sized for small hosts)
DevGuard source pin f1f908429abea962d62d6b53c56a26c17250179b (devguard-client, devguard-contract; dev-only devguard-daemon fixture), unchanged
Codex gitlink 6b9826e3aa83b1a5947db50f4332cb9c65f1b340 (rust-v0.154.0), unchanged
DevGuard main at start 334472a (no change under crates/ or Cargo.lock since the pin)
Bookkeeping novelKR/DevGuard#25 marks CSRG-U3 active; this PR does not edit DevGuard's ledger

Summary

Order. For a workspace with resources.participation: required, under --devguard register:

  1. Authorization and workspace readiness (gateway): allow, require_exec, argv, the approvals: confirm hold, then the workspace mutation lease.
  2. Process slot (execution owner's runner): InProcessRunner::reserve_slot, before anything is created.
  3. Admission (owner's bounded DevGuard session): Register, then Admit.
  4. One-shot PreparedExecution: owns the command, the slot and the attempt.

Admission state is never treated as a process handle. Spawn, PTY, output, timeout, termination and reaping stay with CodeSpace; DevGuard keeps admission, the reservation and the attempt's durable identity and accounting.

Execution owner per mode (the same as U2's registration):

Runner mode Prepares How
in-process the gateway Preparer beside its own InProcessRunner, under its OwnerRegistration
uds + --runtime-bin the worker the gateway started one new worker request, prepare, stated in the worker's Hello as admission; answered beside other requests and never cached for replay
uds + --runner-socket nobody refused with U2's RESOURCE_POLICY_UNSUPPORTED, before any approval or lease

Attempt identity (AttemptIdentity):

  • Minted fields: consumer and generation (the owner's), a fresh attempt_id (cs-attempt- plus 32 hex digits; never a transport request ID), the CodeSpace process_id, the workspace, PTY or not, and the resource request.
  • Digest: DevGuard's ExecutionMeaning v1 encoding of the argv as given; the workspace ID, root, profile and network policy; the exact environment the child would get (the same spawn_env the spawner uses); the PTY flag; the timeout; and the request.
  • Binding: DevGuard refuses the same attempt ID with another digest (attempt_conflict). The adapter accepts a record only if it has this attempt's key, this request's fingerprint and the owner's registered identity; anything else is treated as unknown. Launching with another argv, process_id or PTY choice is DigestMismatch and cancels the attempt.

Slot before spawn. This is a product change for every execution, governed or not. Before U3, spawn_pipe and spawn_pty created the child and only then checked the live-process limit, killing the over-limit child. Now spawn_host takes a SlotReservation first. Slots held for processes not yet created count against the limit, so a ninth request is refused with the same WORKSPACE_BUSY "live process limit reached" before any child exists. Slot, attempt, workspace and retained-output lifetimes are separate:

  • Slot: RAII; it is filled by the created process or freed on drop.
  • Attempt: DevGuard's; it ends when DevGuard records it denied, cancelled or expired.
  • Workspace lease: held while the gateway waits, and released when nothing starts.
  • Retained output: belongs to processes; a preparation creates none.

Admission account (AdmissionAccount). Kept as distinct fields: requested (quantities), required (the minimum level per resource), supported (DevGuard's plan: scope, plus level and method per resource), reserved (quantities and the prepared TTL), and applied (not_launched; no runtime enforcement is claimed before U4).

PreparedExecution.

  • Not Clone; launch and cancel take self, so it is used once.
  • Deadline: DevGuard's prepared TTL (5 s at the pin), measured from before Admit was sent. An optional lower bound is used only by tests.
  • launch(request) with the prepared command before the deadline is where U4 will call BeginLaunch. In U3 it cancels and reports LaunchUnavailable. After the deadline it cancels and reports Expired. With another command it cancels and reports DigestMismatch.
  • Dropped without being settled: cancelled in the background, never assumed released.
  • Preparer::prepare_and_dispose runs as its own task, so an MCP request that stops waiting still lets the preparation settle.
  • It is never rebuilt from argv after uncertainty.

Contract changes

  • New tool error codes: MANAGED_LAUNCH_UNAVAILABLE, RESOURCE_UNAVAILABLE, RESOURCE_AUTHORITY_UNAVAILABLE, ADMISSION_UNKNOWN. WORKSPACE_BUSY and RESOURCE_POLICY_UNSUPPORTED are reused.
  • Messages: every governed refusal states the attempt ID, the process_id and the attempt state (not_asked, not_recorded, denied, cancelled, expired, other, unknown), and ends with "nothing was started". Only enumerations and identifiers appear; no DevGuard free text.
  • Registry: resources.request (cpu_milli, memory_bytes, tasks, minimum.{cpu,memory,pids} ∈ accounted|cooperative|kernel). The default is 250 m, 256 MiB, 32 tasks, all accounted, small enough for a three-core host whose DevGuard work capacity is under 500 m. Quantities must be positive, and unknown keys fail the load.
  • Runner wire: a prepare op and its prepared result, behind the feature, plus a Hello.admission flag that is omitted when false. WIRE_PROTOCOL is unchanged by this PR (7 since runner: report a signal death that CodeSpace did not cause as signaled #87). The op is sent only to a worker that states admission, as U2 did for registration.
  • Behavior for non-register modes: unchanged (refused before approval or lease). Only the message tail changed, to "this gateway cannot admit executions through the resource authority, so nothing was started".

Error mapping:

Owner's outcome Attempt state Tool error
Admitted (Prepared), then cancelled cancelled (or what the cancel found) MANAGED_LAUNCH_UNAVAILABLE
No slot not_asked WORKSPACE_BUSY
Denied: resource_unavailable (shortage) denied RESOURCE_UNAVAILABLE
Denied: resource_policy_unsupported denied RESOURCE_POLICY_UNSUPPORTED
Denied: other code denied RESOURCE_AUTHORITY_UNAVAILABLE
Session ended before Admit was sent (registration state and code given) not_asked RESOURCE_AUTHORITY_UNAVAILABLE
DevGuard answered Admit with an error not_recorded RESOURCE_AUTHORITY_UNAVAILABLE
Expired before launch expired/cancelled RESOURCE_UNAVAILABLE
Launch digest mismatch (not reachable from MCP in U3) cancelled INTERNAL
Timeout, lost reply, EOF, foreign record, transport code from one Lookup ADMISSION_UNKNOWN
UDS: prepare sent and not answered unknown ADMISSION_UNKNOWN
UDS: prepare never left not_asked (owner_unreachable) RESOURCE_AUTHORITY_UNAVAILABLE

Uncertainty rules (§8).

  • An error answer is definite: each DevGuard request is one journal transaction that commits only before a record is answered.
  • resource_control_unavailable is treated as unknown, because DevGuard's client uses it for every transport failure.
  • After an unknown, the owner does one Lookup. A record found Prepared is cancelled. A settled record is reported as found. NotFound or no answer stays unknown: NotFound is not proof, since the admission may still be in flight.
  • The attempt is never admitted again and never rebuilt.
  • Cancel is the only settlement used. Its precondition holds because the owner never sent BeginLaunch; AbandonLaunch is not used.

Tests

Against DevGuard's real authority (fixture with native host evidence, macOS), in crates/devguard:

  • prepared, reserved and not applied, then cancelled; reservation returned (committed == 0)
  • re-admitting the same meaning returns the same record; the same attempt ID with another digest is attempt_conflict and is never matched
  • a shortage (over capacity) and an unsupported minimum (kernel memory) are each Denied, with nothing reserved
  • a timed-out Admit is Unknown, and DevGuard then shows it Prepared: a timeout is not a refusal (hold_authority after Register)
  • an unlaunched attempt expires on its own after 5 s
  • another owner instance cannot look the attempt up or cancel it

Scripted authority (adapter): each wire error keeps only its code; a timeout or lost reply on Admit, Lookup and Cancel is Unknown; a record for another attempt, digest or owner is Unknown; an invalid admission opens no session; a session that closes before the request is NotSent.

Runner (admission, deterministic fake authority; process, real spawns), against each item of §9:

§9 item Test
successful prepare a_prepared_execution_holds_its_identity_slot_and_account_and_launches_nothing
insufficient resources, unsupported policy, unavailable authority denials_and_an_unreachable_authority_reserve_nothing_and_cancel_nothing
occupied CodeSpace slot an_occupied_slot_refuses_before_the_authority_is_asked
concurrent over-limit concurrent_requests_over_the_limit_are_refused_before_admission; concurrent_requests_over_the_limit_create_only_the_children_allowed (feature-off, real children: exactly 3 of 12 markers)
ninth request before process creation a_request_over_the_limit_creates_no_child (feature-off, real children)
attempt expiry an_expired_preparation_is_not_launched_and_is_cancelled; adapter an_unlaunched_attempt_expires_on_its_own
cancellation before launch a_preparation_cancelled_before_launch_frees_its_slot
lost reply, timeout an_unknown_admission_is_looked_up_never_replayed_and_settled_conservatively; adapter timeout and lost-reply tests; gateway a_preparation_lost_with_the_worker_is_unknown
duplicate or replayed request admit count stays 1 after unknown; adapter same-ID tests; gateway re-resume returns the stored refusal; attempt IDs are never reused
digest mismatch another_command_is_not_launched_under_the_prepared_attempt; adapter the_same_attempt_for_another_command_is_refused; the_meaning_follows_the_command_workspace_and_request
task cancellation a_caller_that_stops_waiting_leaves_the_preparation_to_settle (request future dropped; preparation dropped unsettled)
workspace release versus attempt release the_slot_and_the_attempt_are_released_separately (cancel unreachable: slot and lease freed, attempt unknown, not claimed released)
no child in any U3-only path every runner and gateway test checks a touch marker and zero occupied slots

Gateway (codespace-server --features devguard, out-of-process fixture authority, plus the devguard worker for UDS):

  • the InProcess gateway and the UDS worker each prepare pipe, PTY and approval-resumed governed executions: MANAGED_LAUNCH_UNAVAILABLE with cancelled, nothing charged afterwards, lease released, reads unaffected, re-resume returns the stored refusal
  • workspace resources.request produces RESOURCE_UNAVAILABLE (shortage) and RESOURCE_POLICY_UNSUPPORTED (kernel minimum)
  • a closed endpoint gives RESOURCE_AUTHORITY_UNAVAILABLE with not_asked
  • a lost worker gives ADMISSION_UNKNOWN, then owner_unreachable
  • U2's process-control-without-registration and descriptor tests still pass

Without native evidence (Linux before DG-LINUX), the owner cannot register, so governed executions are RESOURCE_AUTHORITY_UNAVAILABLE (incompatible); the gateway tests assert that branch there.

Local validation (macOS 26, Rust 1.98.0 Homebrew, scripts/validate-upstream.py, CARGO_INCREMENTAL=0):

  • On 87c0055: all 18 stages passed across three runs. One run (u3-local-2) passed 16 of them: policy, python, pin, format, the three clippy stages, unit-devguard, unit-patch, unit-pty, unit-file-system, unit-linux-sandbox-protocol, macos-fork-race (400 trials), integration and dependencies. macos-core passed in full on its rerun (u3-local-3), and unit-codex-runtime on its rerun (u3-local-4).
  • On 4af3feb: policy, python, pin, format, the three clippy stages, unit-devguard, macos-core and dependencies passed (u3-local-5). The integration stage's test commands passed when run directly (u3-local-7): the workspace, plus server, runner and domain with devguard. cargo test --test runtime_binary passed 6/6 on its third run (failure 8 below).
  • Not rerun on 4af3feb, because 4af3feb does not touch them: macos-fork-race and the patch, pty, file-system and linux-sandbox-protocol unit stages. All passed on 87c0055.
  • Not run locally: linux-isolation, which needs Linux (it ran in CI).

CI on the exact head 4af3feb: run 37599585285 passed every job: Plan, Format + upstream pin, the three Clippy jobs, every Unit job, Integration, Linux isolation, macOS contracts, policy-scan, docs-build and the rust gate. The previous head's run 37595340009 failed in macOS contracts; see failure 7.

Security scenarios

  • Credentials: no new credential path. Admission sessions reuse U2's owner credential (the file in InProcess mode, the handed secret in the worker), so the secret is never in arguments, the environment, logs, statuses, MCP output or payloads.
  • Descriptors: admission sessions use the same DevGuard client sockets U2's descriptor and inheritance tests cover. No governed child is ever created in U3, so there is nothing new for a session to leak into.
  • Free text: no DevGuard message reaches MCP; every reported field is an enumeration, a number or a CodeSpace-minted ID.
  • No fallback: required never runs without the authority; in every mode that cannot prepare it is refused before an approval or lease exists.

Dependency delta

None: no Cargo.toml, Cargo.lock or workflow changed. The new code uses only devguard-contract and devguard-client, already at the pin. The dependencies stage passed locally on 4af3feb, as did CI's dependency and pin checks; the feature-off graphs are unchanged.

Feature-off result

Without the devguard feature, the only behavior change is the pre-spawn slot: an over-limit request is refused before any child is created, rather than created and then killed. The error code and message are unchanged. The registry gains resources.request; a workspace without resources encodes exactly as before. The required refusal message tail changed (above).

Supported and unsupported modes

  • Supported: macOS with a native DevGuard service, InProcess and UDS (--runtime-bin): admitted, then cancelled.
  • Unsupported:
    • UDS with --runner-socket, and any build or run without --devguard register: refused before any approval or lease.
    • Linux without native evidence: cannot register, so RESOURCE_AUTHORITY_UNAVAILABLE.
    • Managed launch: U4.

Local validation failures (kept)

Kept in the local evidence set <DEVGUARD_CHECKOUT>/evidence/csrg-u3-2026-10-07/ (local/dev/failures.md, with logs and a SHA-256 MANIFEST.json):

  1. Build output. One build ran without CARGO_TARGET_DIR and started a fresh 4.7 GiB target/debug. I stopped it and removed that output.
  2. Vacuous test. The first version of the adapter's timeout test passed without testing anything: the registration timed out first, and the test returned early. It was rewritten so the authority lock is held only after Register, and the early return was removed.
  3. Gateway suite, run 1. spawn_hello_then_drop_kills_worker failed on the first launch of a just-relinked worker; runs 2 and 3 passed 56/56.
  4. Interrupted run. I stopped a full run at 5fc4d3c to merge main. Its passed stages are kept, but it is not a result for any head.
  5. Full run at 87c0055. unit-codex-runtime failed (relinked worker), and the pty test created_terminal_has_advertised_size read "", which stopped macos-core early. crates/pty is identical to main.
  6. Rerun at 87c0055. macos-core passed in full. unit-codex-runtime failed again, because the binary at the shared output path was the devguard build; a feature-off rebuild then passed.
  7. CI on 87c0055. Five native adapter tests were denied resource_unavailable: the 3-core macOS runner's DevGuard work capacity is 480–500 m CPU. The default request (1000 m) would have been denied on such a host too. Fixed by 4af3feb, which sets the default to 250 m / 256 MiB / 32 tasks and the test request to 100 m.
  8. Runs at 4af3feb. Worker-launch failures came right after relinks. A fresh copy of the worker takes 1.41 s to bind on its first launch and 0.08 s afterwards, and the test launches six workers within a 2 s window. main (579c075) shows the same behavior in this worktree: it fails right after the relink and passes on the next run. This is the Mac's behavior on new binaries, not U3.

Out of scope

Rollback

Revert this PR's merge commit. Nothing persistent was added: no store schema, no DevGuard ledger change, and attempts are DevGuard's and expire on their own. A workspace that sets resources.request must drop that key after a revert, because U2's registry rejects unknown resources keys.

novelKR and others added 3 commits October 7, 2026 13:55
…d executions (CSRG-U3)

- runner: a SlotReservation is taken before any process is created, so a request over the
  live process limit is refused before a child exists (previously the child was spawned and
  then killed). Slots held for processes not yet created count against the limit.
- devguard adapter: Admit, Lookup and Cancel of one attempt, each a bounded session of the
  registered owner. Answers are NotSent, Refused (a definite DevGuard error), Attempt (this
  owner's record for this meaning) or Unknown (timeout, lost reply, EOF, a foreign record or
  the client's transport code); no DevGuard free text is kept.
- runner (devguard feature): AttemptIdentity, AdmissionAccount (requested, required,
  supported, reserved, applied), a Preparer that takes the slot, computes the digest and asks
  for admission, and a one-shot, non-Clone PreparedExecution. Its launch cancels the unstarted
  attempt and reports LaunchUnavailable until managed launch exists (U4). Unknown admissions
  are looked up once, cancelled only when found prepared, and never replayed.
- policy: resources.request, the operator's per-execution request with positive quantities
  and minimum control levels.
- domain: MANAGED_LAUNCH_UNAVAILABLE, RESOURCE_UNAVAILABLE, RESOURCE_AUTHORITY_UNAVAILABLE and
  ADMISSION_UNKNOWN.

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

- gateway: a workspace with `resources.participation: required`, under `--devguard register`
  with an owner that can prepare, goes through policy, the approval hold and the workspace
  lease, then the owner's preparation. Every preparation ends in a refusal that starts
  nothing and releases the lease: MANAGED_LAUNCH_UNAVAILABLE after the unstarted attempt is
  cancelled, or WORKSPACE_BUSY, RESOURCE_UNAVAILABLE, RESOURCE_POLICY_UNSUPPORTED,
  RESOURCE_AUTHORITY_UNAVAILABLE or ADMISSION_UNKNOWN, each naming the attempt and what is
  known of it. Other modes keep U2's refusal before any approval or lease.
- worker: a `prepare` request, stated in its Hello as `admission` and answered beside the
  others, never cached for replay. A request the worker may have received without answering
  is reported unknown; one that never left, owner_unreachable.
- fixture authority: waits until DevGuard admits before printing its settings, and reports
  the attempts and budget still charged.
- CI: the macOS stage also runs the runner's `admission` tests.
- docs: the CSRG-U3 section in English and Korean; the process limit refuses before spawn.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@novelKR novelKR self-assigned this Oct 7, 2026
…CSRG-U3)

The macOS CI runner's DevGuard work capacity is under 500 m CPU, so the default request of
1000 m was denied there, as were the adapter tests' 500 m. The default request is now 250 m CPU,
256 MiB and 32 tasks; the adapter tests ask for 100 m, 64 MiB and 8 tasks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@novelKR
novelKR marked this pull request as ready for review October 7, 2026 09:45
@novelKR
novelKR merged commit d543ea5 into main Oct 7, 2026
19 checks passed
novelKR pushed a commit to novelKR/DevGuard that referenced this pull request Oct 7, 2026
CSRG-U3 merged in CodeSpace on 2026-10-07 as novelKR/CodeSpace#88
(d543ea5, at head 4af3feb), and its post-merge CI is green. Record U3 as
merged-verified, keep U4-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. 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 8, 2026
The owner directed CSRG-U4 to start on 2026-10-08, after CSRG-U3 merged in
CodeSpace as novelKR/CodeSpace#88 (d543ea5) and CodeSpace moved its DevGuard
pin to 6e7e065 as novelKR/CodeSpace#89 (e0610c4), both with their post-merge
CI green. Record U4 as active with the units dated 2026-10-08, keep U5 and 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. 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>
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