Skip to content

Commit 11866c1

Browse files
authored
feat: pipe.broadcast cross-scope push + collect reads exposed (#623)
## Summary Adds a cross-scope **broadcast** primitive to the pipe/quirk system, makes `collect` compose with `expose`, and fixes config-thunk resolution to honour the **producing** class + scope — then makes that resolution **class-neutral** so it no longer assumes NixOS. ### `pipe.broadcast pred` — push primitive (dual of `pipe.expose`) Where `pipe.expose` routes a value **up to the parent** applying source-side stages, `pipe.broadcast` routes the (transformed) value **laterally to every other scope matching a receiver predicate**, fleet-wide. Receivers read the pipe normally — no `collect` policy required. ```nix den.policies.broadcast-dev = { host, user, ... }: [ (pipe.from "peer-dev" [ (pipe.transform redact) # source-side, applied before send (pipe.broadcast ({ user, ... }: true)) # → every user scope, fleet-wide ]) ]; ``` - **Receiver-only predicate** (same signature as `collectAll`); selects receivers by their own context — kind, name, tags, `host`. Compound predicates like `{ host, user, ... }: host.name == "edge"` target user scopes on a specific host. - **Self-excluded** (`S≠R`) — a source represents itself via its own base. - Implemented as `collectAllBroadcast` (Pass 1b) mirroring `collectAllExposed`; reuses `findMatchingAll` entity-kind filtering. `bindsPipeLocally` gains a broadcast clause so a pure receiver doesn't fall through to ancestor inheritance. ### `collect`/`collectAll` read raw **+ exposed** `collectTagged` now reads `resolveThunks(raw) ++ allExposed.${sid}`, so a peer's collect sees data that children `pipe.expose`d up into a host. Fixes the expose-then-fleet-collect path. `allExposed` is Pass-1 data → no eval cycle. ### Producer-class config-thunk resolution A pipe config-thunk now resolves against the **producing** class module + scope, not the consuming one (the prior behaviour was a latent bug — a host-produced thunk consumed in a home resolved against the home config). - **Cross-scope** (broadcast/collect): the source's thunk resolves against the producer's class config. - **Deferred** (`__configThunk`): the marker carries the producer's class + name; the class-module wrapper resolves against the producing class's config and hands each thunk both `config` (producer class) and the **owner** config (the enclosing host), mirroring home-manager. The owner arg is requested from the module system only when a marker needs it, so standalone homes keep working. - `isConfigDependent` also detects owner-config-only thunks. ### Class-neutral resolution (no host/osConfig hard-coding) The host↔producer navigation is **registry-driven**, not baked into core fx — so den can describe a config-owner that isn't a NixOS host (e.g. terranix, nixidy) without core edits: - `den.classes.<class>.parentPath` — `name → path` locating a member within the enclosing config-**owner** (the same route its content is delivered to); `den.classes.<class>.parentArg` — the module arg a nested member reaches the owner by. Both `null` for **root** classes that own a top-level config (nixos, darwin, …). The home-manager battery registers `parentPath` + `parentArg = "osConfig"` from the same value as its forward-delivery path. - `producerConfigs`/`resolveEntry` (assemble-pipes) and the class-module wrapper navigate via the registry, keyed by `scopeEntityClass`. `isConfigDependent` detects `config` or **any** registered `parentArg`. **Zero `host`/`osConfig`/`home-manager` literals remain in core fx.** - `hostConfigs` was already generic over instantiates (any entity with an `intoAttr` output), so the model already supported non-host owners; this removes the remaining NixOS-flavored literals. (Building actual terranix/nixidy entity/class defs is follow-on work this unblocks; the two soft `or "nixos"` class-default fallbacks are left intact.) ## Tests - **`pipe-broadcast`** (10): all-to-all · user→remote-host · source-transform · predicate scoping (incl. `{ host, user }` compound) · self-exclusion · targeted no-leak · pure-receiver binding · config-thunk from host / user source. - **`pipe-broadcast-isolation`** (4): cross-pipe-name · host-target-excludes-home (entity-kind via shared `host` context) · no-match predicate · broadcast↔collect boundary (a fleet `collectAll` counts each raw emit once, not the broadcast-amplified view). - **`pipe-config-scope`** (3): host→home, user→own-home, user→host producer-class resolution. - **`pipe-scope`**: the `test-expose-then-fleet-collect` witness now passes; the `exposed-config-thunk` witness reads the owner arg. Full CI: **1042/1042**. `treefmt` clean. ## Commits 1. `pipe.broadcast` cross-scope push + collect reads exposed 2. producer-class config-thunk resolution + broadcast review fixes 3. derive config-thunk host navigation from the class registry 4. make config-thunk resolution class-neutral (no host/osConfig literals)
1 parent 5df0987 commit 11866c1

13 files changed

Lines changed: 1395 additions & 65 deletions

File tree

modules/aspects/batteries/home-manager.nix

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
...
77
}:
88
let
9+
# Where a home-manager user's config nests inside the enclosing host config.
10+
# Single source of truth for both the forward delivery target and the
11+
# den.classes.homeManager.hostPath the pipe layer resolves producers against.
12+
userHostPath = userName: [
13+
"home-manager"
14+
"users"
15+
userName
16+
];
917
result = den.lib.home-env.makeHomeEnv {
1018
className = "homeManager";
1119
ctxName = "hm";
1220
optionPath = "home-manager";
1321
getModule = { host, ... }: inputs.home-manager."${host.class}Modules".home-manager;
14-
forwardPathFn =
15-
{ user, ... }:
16-
[
17-
"home-manager"
18-
"users"
19-
user.userName
20-
];
22+
forwardPathFn = { user, ... }: userHostPath user.userName;
2123
schemaIncludes = config.den.schema.hm-host.includes or [ ];
2224
};
2325

@@ -29,4 +31,7 @@ in
2931
den.schema.user.includes = [ result.userDetect ];
3032

3133
den.classes.homeManager.description = "Home Manager user environment";
34+
# home-manager nests under its host; a member reaches the host config via osConfig.
35+
den.classes.homeManager.parentPath = userHostPath;
36+
den.classes.homeManager.parentArg = "osConfig";
3237
}

modules/options.nix

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ let
3232
type = lib.types.nullOr lib.types.raw;
3333
default = null;
3434
};
35+
options.parentPath = lib.mkOption {
36+
description = ''
37+
For a class whose members nest inside an enclosing config-owner (e.g.
38+
home-manager inside a host), a function `name -> path` locating a named
39+
member within that owner's config — the same route the class's content
40+
is delivered to. The pipe layer uses it to resolve a producer's config
41+
at its producing class + scope. null for root classes that own a
42+
top-level config (nixos, darwin, terranix, …).
43+
'';
44+
type = lib.types.nullOr lib.types.raw;
45+
default = null;
46+
};
47+
options.parentArg = lib.mkOption {
48+
description = ''
49+
For a nested class, the module argument by which a member reaches the
50+
enclosing config-owner (home-manager exposes the host config as
51+
`osConfig`). The pipe layer hands a deferred config-thunk the owner
52+
config under this name. null for root classes.
53+
'';
54+
type = lib.types.nullOr lib.types.str;
55+
default = null;
56+
};
3557
}
3658
);
3759

0 commit comments

Comments
 (0)