feat: fence launch helpers and reconcile uncertain execution (DG1-P3) - #4
Merged
Merged
Conversation
Add the devguard-launch helper and the wire requests that lead to it. - Wire: Register derives the instance from the OS-observed peer and its native start identity. Admit, BeginLaunch, Lookup and Cancel act for the instance registered in the session. A helper presents the one-time grant with Launch in a session that can make no other request; it is not a caller credential. New variants are used only after the service advertises fenced launch; wire version 1 and the journal schema are unchanged. - Core: verify_launch checks a grant without changing it, and claim_launch durably records the first helper's established scope before binding. After a claim no other scope can claim, bind or be authorized, so a helper refused after its claim is reconciled through its scope, never as proof that nothing started. Cancel no longer rewrites Suspect, Draining or terminal attempts, and a prepared attempt past its deadline reports its expiry. - Native: a helper must be a live same-user process whose parent is the running owner, rechecked after the owner check. A scope whose root is gone before readback is no longer tracked, and an unclaimed scope can be forgotten. - Daemon: under one authority lock the helper path checks the registered owner and the helper's parentage, verifies the grant, establishes the scope (nice plus readback), claims, binds and authorizes. Racing, late and cancelled-grant helpers are refused; a replay after a lost reply gets may_exec=false; a claimed helper refused afterwards is killed. Every presentation, including a refusal before any claim, yields one receipt with its authorization time. Registration and launch stay closed in the normal service until DG1-C06 ships reconciliation; a test-fixtures feature serves isolated authorities with launch open and a synthetic healthy probe. - Helper: leads its process group (a PTY session leader already does), re-executes under the utility QoS clamp, consumes the permit descriptor, marks its transcript close-on-exec, reports READY only after authorization and then execs; exec failure after READY is reported separately. The client creates a grant's descriptors above the standard three under a process-wide spawn guard, and HelperCommand::spawn consumes the command so the owner's copies close. Add scripts/qualify.py dg1-launch (macOS only; the unclamped-helper case is not_run where every child is clamped, so CI allows incomplete), with cases for a reply that misses its deadline and concurrent launches, raw receipts checked for credential values, the launch crate in the explicit dependency allowlist (daemon for tests only, enforced) and English/Korean docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (DG1-C06) Open registration, fenced launch and reconciliation together in the normal service whenever native host evidence is available. - Reconciler: every second, one authority lock per attempt, it releases a claimed or bound attempt only on observed scope termination and makes an escape or incomplete observation sticky Suspect. An unclaimed grant is left alone while its owner runs and turns Suspect, never released, once the owner is gone. Dead registered processes are retired when they own no charged work and become suspect otherwise. A process of an earlier boot is never running, whatever holds its PID now, so reboots do not strand grants or instance slots. A failed pass is reported and retried; a poisoned authority ends the reconciler, which stops the service. - No helper created: the owner, the only holder of a permit, can report that it holds no helper and will start none (spawn failed, helper exited before READY, or the grant response was lost). The report is bound to the owner's registered identity and is launcher evidence for the core, which releases an unclaimed grant as NoHelperCreated; a claimed grant is settled only through its scope. Late helpers are fenced. - Owner requests: AbandonLaunch, Observe (reconcile now, for example while an exited root is still unreaped so survivors are adopted) and Terminate (signal every rechecked identity; delivery changes no phase). - Core: a scope from a previous boot is released as PreviousBoot even after lost tracking; instance reconciliation treats other-boot identities as ended; reconciliation that changes nothing writes nothing; the journal rejects NoHelperCreated with a scope and ScopeTerminated without one; charged attempts and live instances can be listed. Journal schema is unchanged. - Session closure no longer implies instance suspicion; process lifetime does. A restart keeps every committed attempt charged and Suspect. Add scripts/qualify.py dg1-reconcile (macOS only) with thirteen real-process cases: journal write failures for binding and release, a daemon crash and restart in a child process with measured committed totals and receipts checked for secrets, and observation cases that pause the background reconciler. Adapt the launch tests to background reconciliation, update devguardd help text and English/Korean docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launch fixtures charge real host capacity. The hosted macOS 14 runner has 3 logical CPUs and 7 GiB, which leaves 500 mCPU of work capacity. The fixture request asked for 1,000 mCPU, so admission denied it as resource_unavailable and every real-helper launch test failed before its helper started. Request 50 mCPU, 64 MiB and 4 tasks instead. Four concurrent launches then fit even at Constrained pressure, which halves the target. The concurrent test now asserts that each launch is released as scope termination before its worker's next grant, which bounds that peak. Lower the daemon session test from 500 to 100 mCPU for the same reason. Admission rules and capacity derivation are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…05, DG1-C06) The PR run at ad32192 failed once in dg1-reconcile on the hosted macOS 14 runner; the branch run of the same commit passed. The dead-owner test waited for the orphaned attempt to turn suspect, then read the owner's instance at once. A reconciler pass handles attempts before instances, so the read could land between the two steps. The receipts show the service marked the instance suspect right after. The test now waits for the instance as well. The deadline test held the authority for 800 ms. A helper then had 550 ms to present if its 250 ms reply deadline was to expire inside the hold. Hold for 2 s instead, so a helper that starts slowly on a loaded host still presents while the lock is held. Co-Authored-By: Claude Opus 5.5 <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.
Behavior
This PR delivers DG1-P3, made up of DG1-C05 and DG1-C06. It adds the
devguard-launchhelper and the service's reconciler. With native host evidence, the normal service now opens registration over the wire, fenced launch and reconciliation together. There is still no execution CLI (DG1-C07).C05 — fenced helper preparation and executable start (
c4859c3)Wire.
Registerderives the instance from the OS-observed peer and its native start identity; the caller supplies only an instance ID.Admit,BeginLaunch,LookupandCancelact for the instance registered in the session. A helper presents the one-time grant withLaunchin a session that can make no other request; the grant is not a caller credential. The new variants are used only after the service advertises fenced launch. Wire version 1 and the journal schema are unchanged.Helper. The owner starts at most one helper per grant, as its direct child. The permit travels on a private descriptor, and a second private descriptor carries the helper's transcript. The helper:
POSIX_SPAWN_SETEXEC),Exec failure after READY is reported separately. The executable keeps the helper's PID, process group, directory, environment and other inherited descriptors; the helper closes only its own.
Authority checks, all under one authority lock.
The authority then establishes the scope, applying nice and reading the policy back. The first helper to get this far claims the grant: its scope is recorded durably before binding (
claim_launch). The authority then binds the scope and authorizes the run.Refusals. Racing, late and cancelled-grant helpers are refused. A replay after a lost reply gets
may_exec = false. A helper refused after its claim is killed and settled only through its scope; that release is not evidence that nothing started. Every presentation yields one receipt with its authorization time.Cancel. A Draining, Suspect or terminal attempt is left unchanged, and a Prepared attempt past its deadline reports its expiry.
Descriptor hygiene. macOS cannot create a pipe or socket pair close-on-exec atomically. The client therefore creates a grant's descriptors above the standard three under a process-wide spawn guard, and
HelperCommand::spawnconsumes the command so the owner's copies close.C06 — reconcile cancellation, expiry and uncertain execution (
6d9aa71)Reconciler. Every second, taking the authority lock per attempt:
PreviousBoot, even after lost tracking;Dead registered processes are retired when they own no charged work and become suspect otherwise. A process of an earlier boot is never counted as running, whatever holds its PID now. A failed pass is reported and retried. A poisoned authority ends the reconciler, which stops the service.
No helper created. Only the owner holds a permit, so it can report that it holds no helper and will start none: the spawn failed, the helper exited before READY, or the grant response was lost. The report is bound to the owner's registered identity and is launcher evidence for the core, which releases an unclaimed grant as
NoHelperCreated. This is sound because a released or suspect grant can never be claimed. The journal rejectsNoHelperCreatedwith a scope andScopeTerminatedwithout one.Owner requests.
AbandonLaunchcarries the report above.Observereconciles at once, for example while an exited root is still unreaped so its survivors are adopted.Terminatesignals every rechecked identity; delivery changes no phase.Instances and restart. Closing a session no longer makes an instance suspect; the registered process's lifetime does. A restart keeps every committed attempt charged and Suspect.
Validation
Rust 1.95.0, one Cargo job and one test thread.
Functional suites (declared raw receipts and hashed logs, all on the final tree
33f8511):dg1-authoritydg1-authdg1-probesdg1-scopesdg1-launchdg1-reconcileNoHelperCreatedreports, claimed-grant abandonment, observe before reap, reap before observation, known escape, termination, cancel after authorization, dead owner, retired instance, unresponsive helper, journal write failures, daemon crash and restart with measured totalsClamped environments. A helper refused after its claim needs an unclamped helper. Where every child is clamped, as on the hosted macOS 14 runner, that case is recorded as
not_run, so CI runsdg1-launchwith--allow-incomplete. It passes on the local host. The journal-failure case exercises the same kill-after-claim path without depending on the clamp.Stability. The real-process suites were repeated with no failure, including while other builds loaded the host. No test left processes or temporary directories behind.
Foreground checks on the normal authority:
3038145(before the review fixes), memory was in warning at startup. Admission stayed Critical, became Constrained after 30 seconds of normal memory, and opened after 64.5 seconds.6d9aa71, the last change to non-test code, pressure was normal and admission opened after 2.4 seconds.Both runs:
/usr/bin/truefrom a short-lived owner process: READY, exit 0, released as scope termination with the helper as scope root, and the owner instance retired;Each run added one uncharged record and one retired instance to the journal.
Documentation. 16 reviewed English/Korean pairs, eight checker tests, 46 work units and 23 logical groups. The approved design checksum and Apache-2.0 license are unchanged.
Independent reviews.
Hosted checks. The existing macOS 14 and Ubuntu 24.04 workflow runs the full validator and the portable suites on both, and the native suites on macOS only. Separate post-merge main CI is verified before cleanup.
6d9aa71, failed on macOS 14. The runner has 3 logical CPUs and 7 GiB, which leaves 500 mCPU of work capacity. The launch fixture asked for 1,000 mCPU, so admission denied it before any helper started, and the later suites never ran.ad32192changes test fixtures only. Requests are now 50 mCPU, 64 MiB and 4 tasks, so the concurrent test's peak of four charged launches fits even at Constrained pressure, which halves the target. The concurrent test now asserts each release that bounds that peak. Admission and capacity derivation are unchanged.ad32192the branch run passed on both runners. On macOS 14 that covered the validator (177),dg1-authority(20),dg1-auth(25),dg1-probes(28) anddg1-reconcile(23).dg1-scopes(25) anddg1-launch(26) wereincompleteonly because the runner clamps every child, so their one unclamped case each could not run. On Ubuntu it covered the validator (129),dg1-authority(19) anddg1-auth(25). Every report's source fingerprint matches the local runs.dg1-reconcile. The dead-owner test read the owner's instance between the two steps of one reconciler pass, which handles attempts before instances; the receipts show the service marked the instance suspect right after.33f8511makes the test wait for the instance too. It also lengthens the deadline test's authority hold from 800 ms to 2 s, so a helper that starts slowly still presents while the lock is held. Both changes are test-only.Compatibility, limitations and rollback
not_runthere.Local reports, raw receipts and foreground logs are preserved outside the disposable worktree. After a normal exact-head merge and a separate main CI success, remove only this task's clean worktree, the merged local branch and regenerable build output. The remote branch, operational state, credentials and evidence are retained.
🤖 Generated with Claude Code