feat(frank-dragoon): open the NPU node the way rootless can actually reach it #46

Merged
dank merged 1 commits from feat/frank-dragoon-npu-lane into main 2026-10-19 22:35:49 -07:00
Owner

The lane reached the engine on the second attempt and died in the engine's own preflight, crash-looping 189 times while chat was down:

halogen npu: /dev/accel/accel0 is here, but this process cannot open it
  (crw-rw---- nobody:nogroup).

Why the vendor's fix is a dead end here

/dev/accel/accel0 is root:render 0660; a rootless container maps exactly one uid and one gid (dank), so render is unmapped inside the namespace and no flag can grant a group the namespace does not map. Measured 2026-10-19 from a dank shell that is already in render:

arm node inside container open
--device alone nobody:nogroup 660 Permission denied
--device --group-add keep-groups nobody:nogroup 660 Permission denied
same, after MODE="0666" nobody:nogroup 666 OPEN_OK

This is the same wall 99-comfyui-gpu-access.rules already documented and solved for /dev/kfd and /dev/dri/renderD128 (both 0666 today) from the systemd-user-spawn direction. The accel node was the last compute node still at 0660.

Change (approved by Frank in-session: the node becomes group-and-world rw)

  • New dankclaw/udev/98-halogen-npu-access.rules: KERNEL=="accel[0-9]*", SUBSYSTEM=="accel", MODE="0666", with the measurement and the blast radius written into the file.
  • deploy_llm_dragoon installs the rule, udevadm control --reload-rules, re-triggers the node, and then refuses the deploy if the node is still not other-writable — a host that never had the rule self-heals instead of failing.
  • serve-frank-dragoon.sh refuses to arm the lane on a node it cannot hand to the container, naming the rule and the two commands that fix it. That converts today's failure mode (crash loop, 42-minute health wait, chat down) into a boot-time refusal.
  • Both checks read the bit off stat -c %a: [ -o FILE ] is not "other-writable" (it means "exists", and -o is also the OR operator inside test) — the first cut of the gate refused everything and the seam said so immediately.

Evidence

$ sudo udevadm trigger --sysname-match=accel0 --verbose
/sys/devices/.../accel/accel0
$ stat -c '%n %U:%G %a' /dev/accel/accel0
/dev/accel/accel0 root:render 666
$ podman run --rm --device /dev/accel/accel0 --entrypoint sh <dragoon-image> \
      -c 'if : </dev/accel/accel0; then echo OPEN_OK; fi'
OPEN_OK

tests/frank_dragoon_unit.bats 35/35 (fixture node now 0666 like the installed one; new arms: 0660 node refused with the rule named and nothing rendered, rule shape + deployer install/trigger asserted). Full ./tests/run.sh before merge.


cwd: /home/blank/.omp/wt/wt-20261019-020309-6d1fc1f · session: 01a15366-7601-7175-bb69-c62186c3f8b5
(build worktree: /home/blank/.omp/wt/wt-dragoon-npu)

The lane reached the engine on the second attempt and died in the engine's own preflight, crash-looping 189 times while chat was down: ``` halogen npu: /dev/accel/accel0 is here, but this process cannot open it (crw-rw---- nobody:nogroup). ``` # Why the vendor's fix is a dead end here `/dev/accel/accel0` is `root:render 0660`; a rootless container maps exactly one uid and one gid (dank), so `render` is **unmapped** inside the namespace and no flag can grant a group the namespace does not map. Measured 2026-10-19 from a `dank` shell that is already in `render`: | arm | node inside container | open | |---|---|---| | `--device` alone | `nobody:nogroup 660` | Permission denied | | `--device --group-add keep-groups` | `nobody:nogroup 660` | Permission denied | | same, after `MODE="0666"` | `nobody:nogroup 666` | **OPEN_OK** | This is the same wall `99-comfyui-gpu-access.rules` already documented and solved for `/dev/kfd` and `/dev/dri/renderD128` (both `0666` today) from the systemd-user-spawn direction. The accel node was the last compute node still at `0660`. # Change (approved by Frank in-session: the node becomes group-and-world rw) - New `dankclaw/udev/98-halogen-npu-access.rules`: `KERNEL=="accel[0-9]*", SUBSYSTEM=="accel", MODE="0666"`, with the measurement and the blast radius written into the file. - `deploy_llm_dragoon` installs the rule, `udevadm control --reload-rules`, re-triggers the node, and **then** refuses the deploy if the node is still not other-writable — a host that never had the rule self-heals instead of failing. - `serve-frank-dragoon.sh` refuses to arm the lane on a node it cannot hand to the container, naming the rule and the two commands that fix it. That converts today's failure mode (crash loop, 42-minute health wait, chat down) into a boot-time refusal. - Both checks read the bit off `stat -c %a`: `[ -o FILE ]` is **not** "other-writable" (it means "exists", and `-o` is also the OR operator inside `test`) — the first cut of the gate refused everything and the seam said so immediately. # Evidence ``` $ sudo udevadm trigger --sysname-match=accel0 --verbose /sys/devices/.../accel/accel0 $ stat -c '%n %U:%G %a' /dev/accel/accel0 /dev/accel/accel0 root:render 666 $ podman run --rm --device /dev/accel/accel0 --entrypoint sh <dragoon-image> \ -c 'if : </dev/accel/accel0; then echo OPEN_OK; fi' OPEN_OK ``` `tests/frank_dragoon_unit.bats` 35/35 (fixture node now 0666 like the installed one; new arms: 0660 node refused with the rule named and nothing rendered, rule shape + deployer install/trigger asserted). Full `./tests/run.sh` before merge. --- cwd: /home/blank/.omp/wt/wt-20261019-020309-6d1fc1f · session: 01a15366-7601-7175-bb69-c62186c3f8b5 (build worktree: /home/blank/.omp/wt/wt-dragoon-npu)
dank added 1 commit 2026-10-19 22:35:16 -07:00
The armed boot reached the engine and died in its own preflight:

  halogen npu: /dev/accel/accel0 is here, but this process cannot open it
    (crw-rw---- nobody:nogroup).

The node is root:render 0660 and a rootless container maps exactly one uid/gid,
so `render` is unmapped inside it and `--group-add keep-groups` — the vendor's
advice, and the ComfyUI pattern for /dev/kfd — cannot grant a group the
namespace does not map. Measured 2026-10-19 from a `dank` shell already in
`render`: without the flag denied, with the flag denied, with the udev mode
0666 the open succeeds (OPEN_OK). Same wall 99-comfyui-gpu-access.rules already
solved for the GPU compute nodes, so the accel node gets its counterpart:

  dankclaw/udev/98-halogen-npu-access.rules — KERNEL=="accel[0-9]*",
  SUBSYSTEM=="accel", MODE="0666"

The deployer installs the rule, reloads rules, re-triggers the node and only
then checks the mode; the serve script refuses any node that is not
other-writable, naming the rule, so this cannot return as a crash loop (189
restarts while chat was down is the failure mode being closed).

Both checks read the permission bit directly: `[ -o FILE ]` is not
"other-writable" — it means "exists" and `-o` doubles as the OR operator inside
test, which is why the first cut of the gate refused everything.

Dragoon seam: 33 -> 35 tests. The fixture node is 0666 like the installed one,
a 0660 node is refused with the rule named and nothing rendered, and the rule
file's shape plus the deployer's install/trigger are asserted.
dank merged commit af327bcc5e into main 2026-10-19 22:35:49 -07:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#46