diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index 341ebb6a..134faf27 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -783,6 +783,16 @@ in "--hive-client-prefix ${lib.escapeShellArg autheliaCfg.hiveClientPrefix}" "--agent-client-suffix ${lib.escapeShellArg autheliaCfg.agentClientSuffix}" "--reader-client ${lib.escapeShellArg controllerCfg.queueClientId}" + # What an agent may publish to, `{hive}` standing for the + # hive its client id names. Without it the responder has no + # agent grant to hand out and refuses every agent at CONNECT, + # so the terminal stream below depends on this line existing. + # + # `$$`, not `$`: systemd substitutes `$NAME` in `ExecStart` + # whether or not it is quoted, so a single dollar reaches the + # responder as the empty expansion of an unset `SWARM` and the + # grant silently becomes `.term.{hive}.>`. + "--agent-publish-subject ${lib.escapeShellArg "\$\$SWARM.term.{hive}.>"}" ]; # Every credential arrives by `LoadCredential` and is named # on the command line only as a **path** — `argv` is diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 4cc04261..a754f806 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -773,6 +773,24 @@ let name = "the queue's payload ceiling is set, not inherited from the server's default"; ok = natsOldPath.containers.swarm-nats.config.services.nats.settings.max_payload == 8388608; } + { + # Reads the RENDERED unit text, not the module's source, because the + # failure this defends against renders perfectly: systemd substitutes + # `$NAME` in `ExecStart` regardless of quoting, so a single dollar + # here hands the responder `.term.{hive}.>` — a grant that parses, is + # accepted, and matches nothing an agent ever publishes to. Asserting + # the doubled dollar is the only way to tell the two apart before + # deploy. The flag's presence is asserted separately so that dropping + # the grant entirely fails as its own arm rather than as an escaping + # complaint. + name = "the responder grants agents their hive's terminal subject, and the dollar survives systemd"; + ok = + let + exec = + natsOldPath.containers.swarm-nats.config.systemd.services.swarm-nats-auth.serviceConfig.ExecStart; + in + lib.hasInfix "--agent-publish-subject " exec && lib.hasInfix "$$SWARM.term.{hive}.>" exec; + } { # Reads the host's tmpfiles rules, not the options: the socket directory # nginx and the container share is created there, so a rename that