Repository navigation
provision: setup-ai-stack and setup-npu-worker target sibling group names the deploy gate does not cover #14567
Description
Problem
_DECLARED_ONLY_GROUPS (#14552) gates the group names update-all-nodes.yml tests in its when: "'x' in group_names" gates: aiml, npu, browser, backend, frontend, plus Play 1's slm_server.
But _ROLE_TO_GROUPS grants sibling names alongside each of those:
ai-stack -> {ai_stack, aiml, ai, llm_nodes}
npu-worker -> {npu, npu_worker, npu_workers}
browser-service -> {browser, browser_worker}
Only the gated name is stripped from an undeclared node. The siblings remain — and two provisioning playbooks target them directly by hosts::
ansible/setup-ai-stack.yml:19—hosts: ai_stackansible/setup-npu-worker.yml:31—hosts: npu_worker
Both are reachable from the same /infrastructure/execute surface that runs update-all-nodes.yml. ExecutePlaybookRequest.limit_hosts is optional and requires_confirmation is UI-only, so nothing server-side scopes them.
So a node whose ai-stack or npu-worker membership came only from detection keeps ai_stack/npu_worker, and would receive the full provisioning playbook if either is ever run fleet-wide without --limit.
Not a regression from #14552
This predates that change — #14552 only made the asymmetry visible by gating the sibling of each pair. Worth recording that browser-service does not have this gap: setup-browser-worker.yml:19 targets hosts: browser, the already-gated name, even though api/infrastructure.py:291 labels it target_hosts=["browser_worker"]. That metadata/reality mismatch is harmless today but is its own small inconsistency.
ansible/site.yml also targets hosts: ai_stack / hosts: npu_workers, but appears to be dead: no Python trigger references it and the roles it lists (cuda_drivers, ai_models, python) do not exist under ansible/roles/. Worth confirming before relying on that.
Fix direction
Decide whether group membership should be per-role-name at all, or whether the sibling aliases exist only for historical inventory compatibility and can be collapsed. Collapsing them would remove this whole class rather than adding four more names to a gate list — the enumerate-vs-derive lesson from #14552.
If they must stay, the gate should cover every group a node acquires from a role whose canonical group is gated, derived from _ROLE_TO_GROUPS rather than listed.
Acceptance criteria
- A node that never declared
ai-stackcannot be targeted bysetup-ai-stack.ymlrun without--limit; same fornpu-worker. - The rule is derived from the role/group mapping, not a hand-maintained list of names.
-
site.yml's status (live or dead) is established rather than assumed.
Related
- provision: detection still grants the frontend deploy group, so a vnc node runs npx vite build #14552 / PR fix(slm): declaration-gate every group the update playbook deploys to (#14552) #14555 — where this was found
- detected_roles records every probed role, so fleet nodes join slm_server and Play 1 targets them #14513 — the original declaration-gating change
Activity
fix(slm): derive the deploy gate over the role map so siblings are co…
fix(slm): derive the deploy gate over the role map so siblings are co…
style(slm): black-format the sibling-group guard (#14567)
style(slm): black-format the sibling-group guard (#14567)
fix(slm): format inventory_sibling_groups_test.py so base code-qualit…
revert(slm): back out the deploy-gate closure that broke base (#14567)
mrveiss commentedon Aug 21, 2026
Attempted, reverted — analysis for whoever picks this up
The gap is confirmed real:
setup-ai-stack.yml:19targetshosts: ai_stackandsetup-npu-worker.yml:31targetshosts: npu_worker, both sibling names_ROLE_TO_GROUPSgrants alongside the gatedaiml/npu, both reachable from/infrastructure/executewherelimit_hostsis optional.I attempted a fix, it broke base, and I reverted it (PR #14664). What I learned is worth more than the attempt.
The derivation that seems right
Close over
_ROLE_TO_GROUPS: if a role can place a node in a deploy-gated group, every group that role grants is equally a deploy target. Enumerating names has now been too short three times (#14513 → #14552 → this), so deriving is the right instinct.Refinement 1 — needed, and it is not optional
The naive closure gates
slm/slm_nodes, because theslm-prefix role grants them alongsideslm_server.deploy-slm-agent.ymltargetsslm_nodes. That version would withhold the agent-repair playbook from agent nodes — the remedy the agent's own 401 message names (#14350 / #14351) becomes unreachable for exactly the nodes that need it.Fix: a group stays ungated when a non-deploy role also grants it.
slm-agentgrantsslm/slm_nodesand is not a deploy target, so those stay broad.Refinement 2 — where it collides with other work, and why I stopped
Even with refinement 1, the closure breaks two deliberate designs:
inventory_privileged_node_roles_test.py::test_privileged_facts_use_declared_only_node_roles(provision: role_active_facts activates privileged roles from unfiltered node_roles, bypassing the #14552 declaration gate #14560). Gatingllm_nodesmakesrole_llm_activea privileged fact, but it still reads the raw unionnode_roles. Converting it tonode_roles_declaredsatisfies the guard — I verified that works — but it means this change cannot land without also touching provision: role_active_facts activates privileged roles from unfiltered node_roles, bypassing the #14552 declaration gate #14560's model.test_wizard_inventory_parity_14286.py::test_deliberately_union_role_keeps_its_non_privileged_group(Fleet health degraded on fresh install: VM agent dies after early-404 heartbeats + optional assigned roles never activate (vnc detection gap) #9965). Gatingnpu_workerbreakstts-worker's deliberately non-privileged membership in that group.tts-workermaps to{ai_stack, aiml, ai}— it grants the seededaiml, so any closure classifies it as a deploy role and sweeps its groups in. This one I do not have a clean answer for:tts-workergenuinely is an ai-stack-family role, andnpu_workermembership for it is genuinely legacy vocabulary that must survive detection alone.The tension is that "deploy role" and "legacy vocabulary group" overlap in
tts-worker, and resolving it needs someone who holds the #9965 intent.Suggested shape for the next attempt
role_llm_activeconversion, not before it.Process note
The reverted attempt reached
Dev_new_guiby a direct push from the main tree (mycdinto the worktree did not persist), which is how base went red rather than a PR catching this. That is recorded on PR #14664.revert(slm): back out the deploy-gate closure that reddened base (#14567
revert(slm): back out the deploy-gate closure that broke base (#14567) (
test(slm): pin the derived deploy gate and the agent-membership refin…
fix(slm): ai_stack is a deploy target, so role_llm_active requires de…
fix(slm): sync role_llm_active across all three fact copies (#14567)
fix(slm): derive the deploy gate over the role map, and require decla…
github-actions commentedon Aug 21, 2026
PR #14675 (merged to
Dev_new_gui) references this issue with a close keyword.If this issue is fully resolved, close it manually. If work remains, no action is needed.
mrveiss commentedon Aug 23, 2026
Resolved by #14675 (
d6505d0210), not by anything filed since. Closing with the evidence, because the squash toDev_new_guinever auto-closes.This issue's claim was that
_DECLARED_ONLY_GROUPSgated only the namesupdate-all-nodes.ymltests in itswhen:gates, leaving sibling group names uncovered._close_over_role_groupsnow derives the gate over the role map. Evaluating that closure against the real_ROLE_TO_GROUPS:All six siblings named here are in the gate.
The derived-ness is what this issue asked for and it introduced a second-order risk, since a change to the role map can now shrink the gate silently. That is #14681, fixed in PR #14855 by pinning the whole 13-group set rather than the four names that happened to be listed.