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.
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 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 main2026-10-19 22:35:49 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
Why the vendor's fix is a dead end here
/dev/accel/accel0isroot:render 0660; a rootless container maps exactly one uid and one gid (dank), sorenderis unmapped inside the namespace and no flag can grant a group the namespace does not map. Measured 2026-10-19 from adankshell that is already inrender:--devicealonenobody:nogroup 660--device --group-add keep-groupsnobody:nogroup 660MODE="0666"nobody:nogroup 666This is the same wall
99-comfyui-gpu-access.rulesalready documented and solved for/dev/kfdand/dev/dri/renderD128(both0666today) from the systemd-user-spawn direction. The accel node was the last compute node still at0660.Change (approved by Frank in-session: the node becomes group-and-world rw)
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_dragooninstalls 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.shrefuses 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.stat -c %a:[ -o FILE ]is not "other-writable" (it means "exists", and-ois also the OR operator insidetest) — the first cut of the gate refused everything and the seam said so immediately.Evidence
tests/frank_dragoon_unit.bats35/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.shbefore 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 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.