Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save sini/60930ea76c7dd1b3e2963860decfb5c0 to your computer and use it in GitHub Desktop.

Select an option

Save sini/60930ea76c7dd1b3e2963860decfb5c0 to your computer and use it in GitHub Desktop.
den: entity-isolation-aware subtree extraction — fixing delivered child-host content leak (design spec)

Entity-isolation-aware subtree extraction (den core)

Date: 2026-06-11 Status: Design — approved for planning Target: den core (feat/delivered-child-host-lazy, off 4f317c5a which nix-config pins) Unblocks: Task 4 of 2026-06-10-delivered-child-host-foundation-plan.md (cortex-cuda rewire)

Problem

extractSubtreeModules (den nix/lib/aspects/fx/resolve.nix ~215-254) builds a host's class config by collecting perScope.<sid>.<targetClass> across the host's entire scope subtree — with no awareness of entity boundaries. So when a delivered child host (the cortex-cuda microvm guest) resolves as a child scope, its nixos-class content — produced by reused fleet aspects (roles.inference → ollama, hardware.gpu.nvidia, authored as nixos.*) — is absorbed into the parent host's own nixos config. The guest's microvm.guest.enable = true (valid only inside the microvm's own system) lands on cortex's top-level → option 'microvm.guest' does not exist.

The guest's content was only ever meant to reach the parent at one path, microvm.vms.<name>.config, via its explicit delivery route. It leaks everywhere else because extraction is class-keyed but scope-blind.

Root cause + the precedent that defines the fix

emit-classes keys content by its authored key (nixos), ignoring the scope's entity class (which is available in ctx). Reused nixos-authored aspects therefore emit nixos-class content even inside a guest-os scope, and extractSubtreeModules collects all nixos across the subtree indiscriminately.

Home-manager shows the intended contract — and refutes the naive fix. A user's home config reaches the host only via a forward route producing home-manager.users.<u> (nixos-class); extractSubtreeModules(nixos) legitimately needs that. Crucially, verification found the forward appends at the user (child) scope, and the host's extraction does cross into the user scope to collect it. So a blanket "skip all child entities" boundary would break home-manager.

The real discriminator is the entity's role, not whether it's a child:

  • Compose entities (user, home): their forward-transformed content is meant to become part of the host's config. They author no raw nixos.*; their nixos bucket holds only the intended forwarded content. The parent must collect it.
  • Delivered/isolated entities (the guest): their content is their own system, reaching the host through one explicit route to a single path. Their nixos bucket holds raw system config that must never be absorbed.

Design: an explicit isolation marker + isolation-aware extraction

No class remap (the guest's content is legitimately nixos — it's a nixos-flavored guest). Fix scope awareness instead, with an explicit marker so compose entities are untouched.

1. Isolation marker (core plumbing)

The delivered-guest kind declares its entities isolated/delivered (it already sets intoAttr = [] and delivers via route; this names that intent). Mechanism, mirroring the existing scopeEntityClass/scopeEntityKind plumbing:

  • A kind-level schema property (e.g. den.schema.<kind>.isolated = true), set by the delivered-guest kind in modules/policies/delivered-child-host.nix.
  • push-scope records it in a scopeIsolated map { scopeId → bool } when a scope of that kind is created (alongside scopeEntityClass).
  • The map is threaded to applyInstantiates via result.state (exactly as scopeEntityClass already is at resolve.nix ~737).

2. Isolation-aware extractSubtreeModules (core)

The host's generic instantiate-time extraction skips subtree scopes marked isolated:

subtreeScopes' = filter (sid: !(scopeIsolated.${sid} or false)) subtreeScopes

→ the guest's raw nixos never enters cortex's own config. Compose entities (user/home — not isolated) are still collected → home-manager unaffected.

3. Explicit delivery: a decoupled route (collect-from-guest, append-at-host)

Blocker found in spec review, resolved here. The guest's reused content is in the nixos class (no remap), so the delivery route must collect fromClass = "nixos" scoped to the guest subtree and land it at the host scope's microvm.vms.<name>.config. But today route uses a single sourceScopeId for both the collection root and the append target (route/apply.nix:181 collects at sourceScopeId; :231 appends at the same sourceScopeId). So:

  • source at the host → collection grabs the host's own nixos too (wrong);
  • source at the guest → the append lands at the (isolated, skipped) guest scope → the delivered content is dropped.

Neither achieves "delivered once." (This is exactly why the shipped primitive used a distinct guest-os class — scope-blind collection was safe because the host owns no guest-os. But that class only carries content authored as guest-os; it never carried the reused nixos fleet aspects, which is the whole problem.)

The real fix — a den-core route enhancement: decouple the collection root from the append target. Add a collectScopeId (or fromScopeId) to the route spec, distinct from the append scope. The delivery route then:

  • collectScopeId = <guest scope>, fromClass = "nixos", collectSubtree = true → gathers the guest subtree's nixos (and only the guest's, since it's rooted at the guest, not the host);
  • appends the nested result at the host scope at path = ["microvm" "vms" "<name>" "config"] → the host's (non-isolated) extraction picks it up.

This makes the guest-os class unnecessary — the guest authors everything as ordinary nixos (honest: it's a nixos guest), the decoupled route carries it, and isolation (§2) prevents the parallel leak. Fully consistent with "no class remap, fix scope awareness" — the scope awareness is fixed at both the extraction and the route.

3a. collectFromSubtree must also skip isolated descendants

The leak must close at both subtree-collection call sites, not just extractSubtreeModules. Under no-remap, any other fromClass = "nixos" collectSubtree route rooted at/above the host would otherwise pull the isolated guest's nixos (the class no longer discriminates). So collectFromSubtree (route/apply.nix ~147-163) applies the same rule, with one nuance:

Skip isolated descendant scopes; always include the collection root.

  • Other routes (root = host) → skip the isolated guest descendant → no accidental pull.
  • The guest's own delivery route (root = guest) → the guest is the root → its content is collected → delivered. Only nested isolated entities (guest-under-guest) are skipped.

extractSubtreeModules is the same rule with root = host (the host is never isolated, so "skip isolated descendants" == "skip the guest").

Why this is the right shape

  • Surgical, scope-correct: one marker + one guard in extraction; the explicit route is the only path across the isolation boundary. Matches the maintainer's framing ("the guest is a different scope; its content shouldn't leak; fix scope awareness, don't change the class").
  • Compose entities untouched: the marker is opt-in; user/home aren't isolated, so home-manager's append-at-user-scope + cross-boundary collection still works.
  • Generalizes: any future "delivered" entity (not just microvm guests) sets the marker and is isolated for free.
  • No class churn: content keeps its honest nixos class; the guest re-instantiates as nixos at the microvm boundary, reading the delivered static config.

Verification (denTest, on the 4f317c5a base)

  • Guest isolation: a host with a delivered child whose content includes a guest-only option (e.g. microvm.guest.enable) — the parent's toplevel evaluates with no leak (the option does not appear on / break the parent), and the content does appear at …microvm.vms.<name>.config.
  • Home-manager intact (the load-bearing regression): a host with a home-manager user still gets home-manager.users.<u> in its nixos — proving compose entities are still collected across their (non-isolated) boundary.
  • Delivery carries reused nixos aspects: a delivered guest composed from a shared nixos-authored aspect (mirroring roles.inference) lands that content at …microvm.vms.<name>.config (confirms the fromClass = nixos route).
  • Full den CI green (the current 909/909 + the new isolation tests); no regression in any existing extraction/route/home test.

Open implementation questions (for the plan/spike)

  1. Marker name + level. den.schema.<kind>.isolated, read in push-scope directly from den.schema.${entityKind}.isolated (push-scope already has den and entityKind in scope — no new schema-param plumbing needed). Simplest, and matches how isEntity/parent are already read.
  2. Route decoupling shape. The exact spec field (collectScopeId/fromScopeId) and how the delivered-guest policy supplies the guest scopeId (it fires at host scope with the guest's raw binding — it must derive the guest's mkScopeId, or the route effect resolves it). Confirm collection rooted at the guest gathers only the guest subtree and the append lands at the host.
  3. Dual-call-site isolation rule. Implement "skip isolated descendants, include the root" once and apply at both extractSubtreeModules and collectFromSubtree. Confirm no other extraction site (hostConfigs reuses mkInstantiateArgs — covered; spawn-node uses routes — covered via collectFromSubtree) is missed.
  4. Nested isolated entities (guest-under-guest) — confirm the "include root, skip isolated descendants" rule degrades sanely.

Blast radius

Core change touches: push-scope (record scopeIsolated from den.schema.<kind>.isolated); resolve.nix (extractSubtreeModules skip + thread the map through mkInstantiateArgs, alongside the existing scopeEntityClass); **route/apply.nix (decouple collect-root from append-target

  • make collectFromSubtree skip isolated descendants)**; and the delivered-guest kind (set isolated = true, drop the now-unnecessary guest-os class, point the delivery route's collectScopeId at the guest with fromClass = "nixos"). Compose-entity paths (user/home/forward) are deliberately untouched — they're not isolated, and the route decoupling is additive (existing routes that omit collectScopeId default to today's behavior). Validated against full den CI + the home-manager regression test before nix-config consumes it.

Note on "no class remap" (spec-review reconciliation). The shipped primitive leaned on the guest-os class to make scope-blind collection safe. This design removes that reliance the right way — by teaching the two subtree-collection sites about entity isolation and decoupling the route's collect/append scopes — rather than relabeling user-authored nixos content. The class becomes redundant and is dropped; content keeps its honest nixos identity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment