Skip to content

feat: fence launch helpers and reconcile uncertain execution (DG1-P3) - #4

Merged
novelKR merged 4 commits into
mainfrom
codex/dg1-p3
Sep 23, 2026
Merged

novelKR merged 4 commits into
mainfrom
codex/dg1-p3

Conversation

@novelKR

@novelKR novelKR commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Behavior

This PR delivers DG1-P3, made up of DG1-C05 and DG1-C06. It adds the devguard-launch helper 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. Register derives the instance from the OS-observed peer and its native start identity; the caller supplies only an instance ID. 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; 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:

    1. leads its own process group (a pseudo-terminal session leader already does),
    2. re-executes itself under the utility QoS clamp (POSIX_SPAWN_SETEXEC),
    3. consumes the permit descriptor and marks the transcript close-on-exec,
    4. presents the grant,
    5. on the first successful authorization reports READY and execs the program.

    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 owner must be registered in this service lifetime.
    • The presenting process must be a live same-user process whose parent is the running owner.
    • The permit must match, and the grant must still be committed or already claimed by this helper.

    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::spawn consumes 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:

    • claimed or bound attempts are released only on observed scope termination;
    • an escape or an incomplete observation makes an attempt sticky Suspect;
    • an unclaimed grant is left alone while its owner runs, and turns Suspect, not released, once the owner is gone;
    • a scope from a previous boot is released as PreviousBoot, even after lost tracking;
    • an attempt the pass does not change is not rewritten.

    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 rejects NoHelperCreated with a scope and ScopeTerminated without one.

  • Owner requests.

    • AbandonLaunch carries the report above.
    • Observe reconciles at once, for example while an exited root is still unreaped so its survivors are adopted.
    • Terminate signals 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.

    • The full validator passed formatting, Clippy with warnings denied and the explicit dependency graph, with 177 Rust tests on local macOS 27.0 (arm64, 8 CPUs, 16 GiB). The graph now has six crates, and the launch crate's daemon edge is enforced as test-only.
    • The C05 commit alone passes the validator with 154 tests.
    • The original 44-test DG-0 baseline is unchanged.
  • Functional suites (declared raw receipts and hashed logs, all on the final tree 33f8511):

    Suite Cases Notes
    dg1-authority 20
    dg1-auth 25 Registration now opens with native evidence
    dg1-probes 28
    dg1-scopes 25 Adds helper identity and establishment cleanup
    dg1-launch 26 Real helper: lifecycle, descriptor inventory, racing and late helpers, refusals, cancellation, exec failure, replay, refusal after claim, pseudo-terminal, a reply that misses its deadline, concurrent launches from four threads
    dg1-reconcile 23 Prepared cancel/expiry against the boot clock, NoHelperCreated reports, 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 totals
  • Clamped 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 runs dg1-launch with --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:

    • At 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.
    • At 6d9aa71, the last change to non-test code, pressure was normal and admission opened after 2.4 seconds.

    Both runs:

    • completed two cycles with all five capabilities advertised and a duplicate authority refused;
    • ran one real managed launch of /usr/bin/true from a short-lived owner process: READY, exit 0, released as scope termination with the helper as scope root, and the owner instance retired;
    • left no permit or credential in the receipts.

    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.

    • The boundary review found no must-fix issue: no path releases a reservation early or lets a payload exec without the single first authorization. Its should-fix items are fixed: a previous-boot PID held by another user blocked settlement, the helper's parentage check ran outside the lock, and the claim ordering was documented backwards.
    • The test and evidence review found two must-fix items. A raw test receipt stored the payload's whole environment; local copies were redacted and nothing was uploaded, and receipts now record variable names only and are checked for credential values. There was also no journal-failure test; one now exists.
    • Its should-fix items and nits are addressed as listed above. The history was rebuilt before the first push, so each commit contains its own fixes.
  • 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.

    • The first run, at 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.
    • ad32192 changes 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.
    • At ad32192 the branch run passed on both runners. On macOS 14 that covered the validator (177), dg1-authority (20), dg1-auth (25), dg1-probes (28) and dg1-reconcile (23). dg1-scopes (25) and dg1-launch (26) were incomplete only 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) and dg1-auth (25). Every report's source fingerprint matches the local runs.
    • The PR run of the same commit failed once in 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. 33f8511 makes 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

  • Compatibility. No journal schema or contract type changes. A claimed but unbound attempt is a committed record with a scope and no applied resources; earlier artifacts read it and turn it Suspect on restart. The wire keeps version 1, and new requests are gated by the advertised fenced-launch capability. Configuration schema 1 is unchanged.
  • Limitations.
    • Cooperative only. QoS and nice are cooperative scheduling; memory and tasks are accounting only.
    • Restart. Scope tracking is in memory. After a restart, a running scope cannot be re-adopted or signalled through the service, and its attempt stays Suspect and charged until a reboot proves termination.
    • Observe before reap. An owner that reaps a short-lived root before any observation can leave survivors as tracking loss. The attempt then stays Suspect rather than being released early; the C07 CLI will observe before reaping.
    • Descriptors. Owners must not leak descriptors. The helper passes through everything the owner leaves inheritable, and owners that spawn other processes from other threads must hold the spawn guard.
    • Startup under memory warning. Admission stays Critical until memory has been normal for 30 seconds, as decided before P3.
    • Linux. Linux CI runs the portable validator; native suites record not_run there.
  • Rollback. Stop starting new work and let charged attempts reach a terminal phase before selecting an artifact without reconciliation. A C05 or earlier artifact reads the same journal but keeps launch closed and does not reconcile. Preserve the journal, credentials and evidence.

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

novelKR and others added 4 commits September 24, 2026 03:45
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>
@novelKR
novelKR merged commit daafe32 into main Sep 23, 2026
4 checks passed
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