Skip to content

feat: observe native host evidence and verify cooperative scope policy (DG1-P2) - #3

Merged
novelKR merged 3 commits into
mainfrom
codex/dg1-p2
Sep 23, 2026
Merged

novelKR merged 3 commits into
mainfrom
codex/dg1-p2

Conversation

@novelKR

@novelKR novelKR commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Behavior

This PR delivers DG1-P2, made up of DG1-C03 and DG1-C04. It replaces the fake clock, process identity and pressure inputs with actual macOS evidence. It also adds cooperative policy application with readback and observed process-group scopes for the launch helper that arrives in P3. Registration over the wire, principals, leases and execution stay closed until P3 installs launch and reconciliation.

The new crate devguard-macos feeds the core only through its Clock and Backend traits and depends only on contract and core. The explicit workspace dependency allowlist is extended.

C03 — boot, identity and pressure (defde38)

  • Boot clock. boot_id comes from kern.bootsessionuuid, and time from CLOCK_MONOTONIC_RAW (boot-relative, and it keeps counting during sleep).
  • Process identity. start_ticks is ri_proc_start_abstime.
    • Each read brackets a PROC_PIDTBSDINFO snapshot with two start reads, with errno cleared before each call.
    • Zombies and reaped PIDs are both treated as absent. A refused observation is an error, never absence.
  • Pressure sampler. A reading is taken every two seconds.
    • Inputs: the memorystatus level, page-outs in kernel pages, swap growth measured from the window's low point, control-loop lag, and the most constrained of the state volume and each registered project root.
    • Rates use a ten-second window and round up. Admission stays closed until the second valid reading.
    • Failures: a failed reading closes admission immediately through PressureController::observation_failed. A same-millisecond reading yields no rate rather than a spurious failure.
    • Durations: each reading records its duration (read_ms).
  • Service.
    • Policy: devguardd serve derives the policy from the observed host with the approved headroom. On the 8-CPU/16-GiB target that leaves 5,500 mCPU, 11.75 GiB and 144 tasks of workload capacity.
    • Activation: it activates the journal with the native clock and backend, and samples without catch-up bursts. An overrun is reported as the next sample's control-loop lag.
    • Receipts: JSON-line receipts are written outside the authority lock.
    • Failure handling: a sampler that ends stops the service with an error, and a probe stuck in the kernel cannot block shutdown.
    • Other platforms report "unsupported", which is distinct from a failed macOS observation.

C04 — policy and scopes (c0ad115)

  • Scope roots. A root leads its own process group and re-executes under the utility QoS clamp (POSIX_SPAWN_SETEXEC).
  • Policy readback.
    • The authority applies nice +10 after rechecking the root's identity.
    • CPU counts as applied only when pbi_nice ≥ 10, pti_priority ≤ 20 and every readable thread's pth_maxpriority ≤ 20. The root's identity is rechecked after the readback.
    • On this host an unclamped root reads task priority 21 and thread priority 63, and binding refuses it. Memory and tasks are accounted; kernel methods are unsupported.
  • Group trust.
    • A group ID is trusted only while the root holds its PID before and after the listing, or while a known member is in the group.
    • Unknown members of a group that can't be proven, and children outside the group whose parentage can't be verified, count as tracking loss.
    • Once a group ends with its root reaped, its ID is never listed again. A listing that returns zero entries with errno set counts as a failure.
  • Observation.
    • Emptiness needs a confirming listing that is itself empty, and zombies still count as present.
    • Gone identities are pruned.
    • Escape and tracking loss are sticky.
  • Termination. signal_scope sends a positive signal to rechecked identities one PID at a time, never to a stale PID or a process group. It still reaches verified identities when the group can't be proven, and marks the receipt incomplete.

Core fix found by native evidence.

  • The defect: bind_scope and reconcile read the clock before calling the backend. With a real clock, evidence observed during the call was rejected as coming from the future, which made binding fail and pushed runs to Suspect intermittently.
  • The fix: freshness, including for unbound-launch evidence, is now judged against the clock read after the backend returns. Evidence older than two seconds or from another boot is still refused.
  • Regression tests fail on the old code; this was mutation-checked for both paths.

Validation

  • Rust 1.95.0, one Cargo job and one test thread.

    • The full validator passed formatting, Clippy with warnings denied and the full dependency-graph checks, with 127 Rust tests on local macOS 27.0 (arm64, 8 CPUs, 16 GiB).
    • The original 44-test DG-0 baseline is unchanged.
    • The C03 commit alone passes the validator with 105 tests.
  • Functional suites (declared raw receipts and hashed logs):

    Suite Cases Notes
    dg1-authority 20
    dg1-auth 20 Native sampling tests moved to dg1-probes
    dg1-probes 28 Boot clock, identities, capacity, native readings, stale/failed-probe closure, a service-loop failure and a stuck probe
    dg1-scopes 21 Real scope lifecycle, capability matrix, sticky escape, refusals, and scripted races and reuse
  • Clamped environments. When the environment clamps every child of the harness (as governed self-use will), the unclamped-root case is recorded as not_run and the suite reports incomplete, not passed.

  • Stability. Native scope and pressure tests were repeated dozens of times, plain and under utility, background and maintenance clamps. No failures, and no leftover processes.

  • 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.

  • Foreground smoke on the normal authority at c0ad115:

    • two start/stop cycles with native activation
    • authenticated status closed, with the native reason
    • duplicate authority refused, and clean shutdown
    • journal counts unchanged (0 attempts, 0 instances)

    An earlier two-minute cycle recorded Normal → Constrained when the host entered real memory warning.

  • Independent reviews.

    • The boundary review found three must-fix defects, all fixed:
      • a non-macOS unit test abort
      • libproc failures that read as an empty group
      • process-group ID reuse after a scope ended
    • The test/evidence review found:
      • not_run reported as passed
      • timing assertions tighter than needed
      • evidence gaps
    • A new stuck-probe test uncovered a sampler failure on same-millisecond catch-up readings, which is fixed.
    • A follow-up verification found that a confirming listing could miss a member that vanished before its read; it now must be literally empty.
    • All findings are fixed, each with a regression test.
  • Hosted checks. The existing macOS 14 and Ubuntu 24.04 workflow runs on this head: 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.

    • First run on c0ad115: Ubuntu passed. On macOS 14 the validator (127 tests) and dg1-probes (28) passed, and the clamped readback values matched macOS 27 (task and thread priority 20). dg1-scopes reported incomplete with exit code 2.
    • Why: the hosted runner runs every process under a QoS clamp (the harness itself reads task and thread priority 20), so an unclamped root cannot exist there.
    • Follow-up commit (9b1c288): adds qualify.py --allow-incomplete, which returns success without changing the report, used only for the CI dg1-scopes step. The summary still shows incomplete, and the unclamped case passes on the local qualification host.

Compatibility, limitations and rollback

  • Compatibility. No journal schema, contract type or wire protocol changes. A C02 artifact can reopen the same state, because activation only runs the existing recovery. Configuration schema 1 is unchanged; CPU and memory capacity now come from the observed host.
  • Limitations.
    • Cooperative only. QoS and nice are cooperative scheduling, not a tree-wide cap. Memory and tasks are accounting only, with no kernel limits.
    • Library evidence only. The service establishes no scopes before P3.
    • In-memory tracking. Scope tracking is held in memory, so after a restart previously bound scopes become Suspect until explicit reconciliation (DG1-C06).
    • Unobservable members. A setuid member causes permanent tracking loss.
    • P3 requirement. The P3 launch path must observe a scope before its root is reaped.
    • Linux. Linux CI runs the portable validator; native suites record not_run there.
  • Operational note. If the host is already in memory warning when the service starts, admission stays Critical until memory has been normal for 30 seconds. This follows the approved pressure rules; a decision on it is requested before P3.
  • Rollback. Stop the foreground process and select an artifact compatible with the preserved configuration and schema-1 journal. Preserve credentials, the journal and evidence. No workloads can have started through C01–C04.

Local reports, raw receipts, spike evidence 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 3 commits September 23, 2026 22:05
Add the devguard-macos crate, which supplies actual host evidence to the
authority core through its Clock and Backend traits:

- BootClock: kern.bootsessionuuid plus CLOCK_MONOTONIC_RAW milliseconds.
- Process identity: ri_proc_start_abstime bracketing a PROC_PIDTBSDINFO
  snapshot with errno cleared first; zombies and reaped PIDs are absent,
  refused observations are errors rather than absence.
- Host capacity (hw.logicalcpu, hw.memsize) and a two-second pressure
  sampler over the memorystatus level, page-outs in kernel pages, swap
  growth from the window's low point, control-loop lag and the most
  constrained registered volume. Rates need a prior reading and round up;
  each reading records its duration; failed or inconsistent readings
  restart the window, while a same-millisecond reading only yields no rate.

Core gains an explicit pressure observation failure input and a disk volume
severity helper; journal schema, contract types and traits are unchanged.
devguardd serve derives the policy from the observed host with the approved
headroom, activates the journal with the native clock and backend, and
samples pressure without catch-up bursts, reporting an overrun as the next
sample's control-loop lag. Receipts are non-panicking JSON lines written
outside the authority lock. A sampler that ends stops the service with an
error, and a probe stuck in the kernel cannot block shutdown. Registration
over the wire and execution stay closed; other platforms report
unsupported, not failed.

Add scripts/qualify.py dg1-probes (macOS only, declared raw receipts,
hashed logs, not_run cases reported as incomplete), a stuck-probe service
test, the explicit dependency allowlist entry and English/Korean docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add observed process-group scopes to the native backend for the future
launch helper:

- Roots lead their own group and re-execute under the utility QoS clamp
  (POSIX_SPAWN_SETEXEC). The authority applies nice +10, rechecking the
  root's identity first, and counts CPU as applied only when pbi_nice,
  pti_priority and the maximum priority of every readable thread fall
  within the utility ceiling, with the identity rechecked after the
  readback. Memory and tasks are accounted; kernel methods are
  unsupported.
- establish_scope refuses roots that are not live same-user group leaders
  alone in their group, and the authority process itself; binding repeats
  the readback; a failed application stays tracked for termination.
- A group ID is trusted only while the root holds its PID before and after
  the listing, or a known member is in the group. Unknown members of an
  unprovable group, and children outside the group whose parentage cannot
  be verified, are tracking loss; once the group ends with its root reaped
  its ID is never listed again. Listings treat a zero result with errno as
  failure. Emptiness needs a confirming listing that is itself empty,
  zombies stay present, gone identities are pruned, and escape and
  tracking loss are sticky.
- signal_scope sends a positive signal to rechecked identities one PID at
  a time, never a stale PID or process group, and still reaches verified
  identities when the group cannot be proven, marking the receipt
  incomplete.

Core now judges backend evidence freshness in bind_scope and reconcile,
including unbound launch evidence, against the clock read after the
backend returns. Real-clock evidence exposed that evidence observed during
a transition was rejected as future; regression tests cover both paths and
stale evidence is still refused.

Add scripts/qualify.py dg1-scopes (macOS only) with real scope roots,
scripted-table race, reuse and failure tests, and English/Korean docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted runners (DG1-C04)

The hosted macOS 14 runner runs every process under a QoS clamp (its
harness reads task and thread priority 20), so the unclamped-root case of
dg1-scopes cannot be produced there and the suite honestly reports
incomplete with exit code 2. Everything else passed on that runner.

Add scripts/qualify.py --allow-incomplete, which returns success for an
incomplete suite without changing its report, and use it only for the
dg1-scopes CI step. The step summary still shows incomplete, and the local
qualification host must still produce passed. Document both languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@novelKR
novelKR merged commit 7c7aed9 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