A ServiceDefinition could not declare that it needs the host's Intel QuickSync render device. The schema is additionalProperties: false, and apps/ is generated from catalog/ + the schema, so there was no authorable form for it at all and a hand-edit to apps/ would be reverted by the next render.
This adds the capability. It does not wire Plex — that is separate work on top of this.
New optional top-level SD property hardware, with one optional member gpu, an enum whose only current value is intel-quicksync. Schema 1.7.0 → 1.8.0.
hardware:gpu:intel-quicksync
Why an enum and not a device path
The SD declares what class of device it needs; the hostPath and the mount live only in catalog/render.py. Migrating to the Intel GPU device plugin (gpu.intel.com/i915 resource requests, no hostPath at all) is then a renderer change with zero catalog edits and no per-service device wiring to re-review.
Letting an SD name /dev/dri directly would look like a simplification and would make that migration a catalog-wide edit. Both the schema description and renderer-contract.md §hardware say so explicitly, so the next person does not "simplify" it. That property also happens to be this PR's insurance policy — see the open risk at the bottom.
What the renderer emits
Exactly two things, on the app container only (never a sidecar):
containers:- name:toyimage:index.docker.io/plexinc/pms-docker:1.41.0volumeMounts:- name:configmountPath:/data/config- name:drimountPath:/dev/drisecurityContext:# <- the SD's OWN, passed through untouchedrunAsUser:1000supplementalGroups:- 44volumes:- name:configpersistentVolumeClaim:claimName:toy-config- name:drihostPath:path:/dev/dritype:DirectoryOrCreate
(a real render of a throwaway SD carrying hardware, a Longhorn volume and its own podSecurityContext: {runAsUser: 1000, supplementalGroups: [44]} — note [44], not[44, something]: the field adds nothing to it)
The mount is deliberately not readOnly — VA-API opens the render node read-write.
No supplementalGroups, and that is the interesting part
/dev/dri/renderD128 is root:render0660, so the obvious move is to push the host render GID into podSecurityContext.supplementalGroups. An earlier revision of this branch did exactly that. It was measured to be a no-op and has been removed — measured in the running container on prod-2, 2026-09-30:
/etc/services.d/plex/run ends in exec s6-setuidgid plex …, and the child's group list is exactly the /etc/group account-database list — root's gid 0, which PID 1 holds, has vanished. That is a setgroups() that replaces rather than appends, so anything Kubernetes supplies reaches s6-svscan and is discarded before the app ever starts.
What actually works is the image's own /etc/cont-init.d/45-plex-hw-transcode-and-connected-tuner: it finds every char device under /dev/dri, stat -c '%g's its GID, groupadds a matching group inside the container when none exists, and usermod -a -Gs the app user into it — every start, nothing hardcoded. cont-init.d completes before services.d, so the database is already correct at exec time. Mounting the device is sufficient; the group handling is the image's job and it does it correctly.
An inert supplementalGroups would have read as the mechanism to whoever debugged this next, so the absence is documented at the emission site and in the contract, and --selftest asserts it. Corollary: because the hook re-stats the device on every start, host GID drift (a rebuild renumbering the systemd dynamicrender group) is self-healing, and no GID is pinned anywhere in this repo.
DirectoryOrCreate is load-bearing — please do not tighten it to Directory
A node that comes back without the render device (rebuild, BIOS change, driver regression, hardware swap) must degrade the workload to software transcoding, not fail the mount. apps-wave-0 is wait: true and apps-wave-1dependsOn it, so a failed hostPath mount stalls the entire app tier for a feature that is not load-bearing. /dev is tmpfs, so the empty directory the kubelet creates leaves nothing durable behind. The reasoning is at the emission site.
Its cost is stated rather than papered over: a missing device is silent. No metric exists to alert on (media apps here declare no metricsEndpoint, so no ServiceMonitor exists), and an alert that cannot fire is worse than none — see the 2026-09-13 correction in renderer-contract.md §observability.
Also deliberately NOT emitted
no privileged: true — Kyverno restrict-host-namespaces is Enforce on app namespaces and the render node does not need it;
no nodeSelector — placement is a separate concern with its own justification;
no resource requests/limits — there is no app-container resource render path and inventing one here is scope creep.
Verification
python3 catalog/render.py --selftest → render.py selftest: PASS, rc 0. New cases in the existing check(...) style: the positive (volume + mount, and nosecurityContext / privileged / nodeSelector), the negative that matters (no hardware ⇒ no volume, no mount, no securityContext — mirroring the existing podSecurityContext negative), and a pass-through case (an SD's own podSecurityContext survives byte-for-byte).
The self-tests bite. Mutating the guard to if True: in a scratch copy → rc 1, failing on the negative. A test that cannot fail is not a test.
Re-render is a zero diff.--all catalog -o apps --wave-layout --no-image-automation then git diff --stat apps/ → empty, git status --porcelain apps/ → empty. The change is inert until a service opts in.
Schema valid; all 12catalog/*.yaml validate with 0 errors. gpu: /dev/dri rejected (enum), an extra key rejected (additionalProperties: false), hardware: {} rejected (minProperties: 1).
Gate: rc 3 — UNDETERMINED, NOT green. Nothing failed. spec-immutability SKIPPED (sibling spec repo absent from a worktree — tolerated per CLAUDE.md §3), gitea-settings SKIPPED (SETTINGS_API_TOKEN unset), conformance SKIPPED (CLUSTER_AVAILABLE!=true, Phase-0 standing skip). decisions-log, lint, l0 (19 cases, 0 failures/skips/unwired), render-golden ("all 10 onboarded service golden(s) reproduce"), l1 (3 cases, 0 failures/skips/unwired) and approval-policy all pass. No stage skipped for a reason caused by this change. Full per-stage table in the handoff entry.
Known gaps (also in the handoff)
Whether containerd's device cgroup permits a non-privileged pod to open() a bind-mounted char device. Still unverified, and it is the single largest technical risk in this workstream. Settling it requires applying a pod, which is an operator action. The container's PID 1 running as uid 0 helps on the DAC side but does not bypass the cgroup gate. If it blocks, the fallback is the Intel device plugin — which the intent-shaped enum makes a renderer-only change with no catalog edit. That is the design justifying itself, and the reason to keep the enum even if the hostPath mechanism loses.
The hook dependency is pinned to an image that floats. All of the in-container measurements above were read from digest sha256:e0ab27395614… (plexmediaserver 1.43.4.10903), but the deployment runs plexinc/pms-docker:latest with imagePullPolicy: Always. A silent pull can change the behaviour this feature depends on, with no commit anywhere in the repo to show for it. Noted, not fixed here.
No gate stage runs render.py --selftest (git grep -- --selftest ci/ is empty). The tests were run by hand. Wiring them in touches ci/, a sensitive path, so it is deliberately not in this PR.
The prod node facts, the in-container measurements and the Kyverno posture are recon handed to me, not re-measured by me (CLAUDE.md §2b). I ran no kubectl and opened no container shell.
Touches only schemas/, catalog/, decisions/ — none sensitive per infra/gitea/approval-policy.json. conformance/** and clusters/** untouched.
## What this is
A ServiceDefinition could not declare that it needs the host's Intel QuickSync render device. The schema is `additionalProperties: false`, and `apps/` is generated from `catalog/` + the schema, so there was no authorable form for it at all and a hand-edit to `apps/` would be reverted by the next render.
This adds the **capability**. It does **not** wire Plex — that is separate work on top of this.
New optional top-level SD property `hardware`, with one optional member `gpu`, an enum whose only current value is `intel-quicksync`. Schema **1.7.0 → 1.8.0**.
```yaml
hardware:
gpu: intel-quicksync
```
## Why an enum and not a device path
The SD declares **what class of device it needs**; the hostPath and the mount live only in `catalog/render.py`. Migrating to the Intel GPU device plugin (`gpu.intel.com/i915` resource requests, no hostPath at all) is then a renderer change with **zero catalog edits** and no per-service device wiring to re-review.
Letting an SD name `/dev/dri` directly would look like a simplification and would make that migration a catalog-wide edit. Both the schema `description` and `renderer-contract.md` §`hardware` say so explicitly, so the next person does not "simplify" it. That property also happens to be this PR's insurance policy — see the open risk at the bottom.
## What the renderer emits
Exactly two things, on the app container only (never a sidecar):
```yaml
containers:
- name: toy
image: index.docker.io/plexinc/pms-docker:1.41.0
volumeMounts:
- name: config
mountPath: /data/config
- name: dri
mountPath: /dev/dri
securityContext: # <- the SD's OWN, passed through untouched
runAsUser: 1000
supplementalGroups:
- 44
volumes:
- name: config
persistentVolumeClaim:
claimName: toy-config
- name: dri
hostPath:
path: /dev/dri
type: DirectoryOrCreate
```
(a real render of a throwaway SD carrying `hardware`, a Longhorn volume and its own `podSecurityContext: {runAsUser: 1000, supplementalGroups: [44]}` — note `[44]`, **not** `[44, something]`: the field adds nothing to it)
The mount is deliberately not `readOnly` — VA-API opens the render node read-write.
## No `supplementalGroups`, and that is the interesting part
`/dev/dri/renderD128` is `root:render` `0660`, so the obvious move is to push the host `render` GID into `podSecurityContext.supplementalGroups`. An earlier revision of this branch did exactly that. **It was measured to be a no-op and has been removed** — measured in the running container on prod-2, 2026-09-30:
```
PID 1 s6-svscan Uid: 0 Gid: 0 Groups: 0
pid 246 Plex Media Server Uid: 1000 Gid: 1000 Groups: 100 1000
```
`/etc/services.d/plex/run` ends in `exec s6-setuidgid plex …`, and the child's group list is exactly the `/etc/group` account-database list — root's gid `0`, which PID 1 holds, has **vanished**. That is a `setgroups()` that *replaces* rather than appends, so anything Kubernetes supplies reaches `s6-svscan` and is discarded before the app ever starts.
What actually works is the image's own `/etc/cont-init.d/45-plex-hw-transcode-and-connected-tuner`: it `find`s every char device under `/dev/dri`, `stat -c '%g'`s its GID, `groupadd`s a matching group *inside the container* when none exists, and `usermod -a -G`s the app user into it — every start, nothing hardcoded. `cont-init.d` completes before `services.d`, so the database is already correct at exec time. **Mounting the device is sufficient; the group handling is the image's job and it does it correctly.**
An inert `supplementalGroups` would have read as *the mechanism* to whoever debugged this next, so the absence is documented at the emission site and in the contract, and `--selftest` asserts it. Corollary: because the hook re-`stat`s the device on every start, host GID drift (a rebuild renumbering the *systemd dynamic* `render` group) is **self-healing**, and no GID is pinned anywhere in this repo.
## `DirectoryOrCreate` is load-bearing — please do not tighten it to `Directory`
A node that comes back without the render device (rebuild, BIOS change, driver regression, hardware swap) must degrade the workload to **software transcoding**, not fail the mount. `apps-wave-0` is `wait: true` and `apps-wave-1` `dependsOn` it, so a failed hostPath mount stalls the **entire app tier** for a feature that is not load-bearing. `/dev` is tmpfs, so the empty directory the kubelet creates leaves nothing durable behind. The reasoning is at the emission site.
Its cost is stated rather than papered over: **a missing device is silent.** No metric exists to alert on (media apps here declare no `metricsEndpoint`, so no ServiceMonitor exists), and an alert that cannot fire is worse than none — see the 2026-09-13 correction in `renderer-contract.md` §`observability`.
## Also deliberately NOT emitted
- no `privileged: true` — Kyverno `restrict-host-namespaces` is Enforce on app namespaces and the render node does not need it;
- no `nodeSelector` — placement is a separate concern with its own justification;
- no resource requests/limits — there is no app-container resource render path and inventing one here is scope creep.
## Verification
- `python3 catalog/render.py --selftest` → `render.py selftest: PASS`, **rc 0**. New cases in the existing `check(...)` style: the positive (volume + mount, and *no* `securityContext` / privileged / nodeSelector), the negative that matters (no `hardware` ⇒ no volume, no mount, no `securityContext` — mirroring the existing `podSecurityContext` negative), and a pass-through case (an SD's own `podSecurityContext` survives byte-for-byte).
- **The self-tests bite.** Mutating the guard to `if True:` in a scratch copy → **rc 1**, failing on the negative. A test that cannot fail is not a test.
- **Re-render is a zero diff.** `--all catalog -o apps --wave-layout --no-image-automation` then `git diff --stat apps/` → empty, `git status --porcelain apps/` → empty. The change is inert until a service opts in.
- Schema valid; all **12** `catalog/*.yaml` validate with 0 errors. `gpu: /dev/dri` rejected (enum), an extra key rejected (`additionalProperties: false`), `hardware: {}` rejected (`minProperties: 1`).
- **Gate: `rc 3` — UNDETERMINED, NOT green.** Nothing failed. `spec-immutability` SKIPPED (sibling spec repo absent from a worktree — tolerated per CLAUDE.md §3), `gitea-settings` SKIPPED (`SETTINGS_API_TOKEN` unset), `conformance` SKIPPED (`CLUSTER_AVAILABLE!=true`, Phase-0 standing skip). `decisions-log`, `lint`, `l0` (19 cases, 0 failures/skips/unwired), `render-golden` (*"all 10 onboarded service golden(s) reproduce"*), `l1` (3 cases, 0 failures/skips/unwired) and `approval-policy` all pass. No stage skipped for a reason caused by this change. Full per-stage table in the handoff entry.
## Known gaps (also in the handoff)
- **Whether containerd's device cgroup permits a non-privileged pod to `open()` a bind-mounted char device. Still unverified, and it is the single largest technical risk in this workstream.** Settling it requires applying a pod, which is an operator action. The container's PID 1 running as uid 0 helps on the DAC side but does not bypass the cgroup gate. If it blocks, the fallback is the Intel device plugin — which the intent-shaped enum makes a **renderer-only change** with no catalog edit. That is the design justifying itself, and the reason to keep the enum even if the hostPath mechanism loses.
- **The hook dependency is pinned to an image that floats.** All of the in-container measurements above were read from digest `sha256:e0ab27395614…` (`plexmediaserver 1.43.4.10903`), but the deployment runs `plexinc/pms-docker:latest` with `imagePullPolicy: Always`. A silent pull can change the behaviour this feature depends on, with no commit anywhere in the repo to show for it. Noted, not fixed here.
- No gate stage runs `render.py --selftest` (`git grep -- --selftest ci/` is empty). The tests were run by hand. Wiring them in touches `ci/`, a sensitive path, so it is deliberately not in this PR.
- The prod node facts, the in-container measurements and the Kyverno posture are recon handed to me, not re-measured by me (CLAUDE.md §2b). I ran no `kubectl` and opened no container shell.
Touches only `schemas/`, `catalog/`, `decisions/` — none sensitive per `infra/gitea/approval-policy.json`. `conformance/**` and `clusters/**` untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A ServiceDefinition could not say it needs the host's Intel QuickSync render
device. The schema is `additionalProperties: false`, so there was no authorable
form for it at all, and `apps/` is generated — a hand-edit would be reverted by
the next render.
Adds optional top-level `hardware`, with one optional member `gpu`, an enum
whose only current value is `intel-quicksync` (schema 1.7.0 -> 1.8.0).
The enum is the point. The SD declares WHAT CLASS of device it needs; the
hostPath, the mount and the GID live only in catalog/render.py. Migrating to
the Intel GPU device plugin (`gpu.intel.com/i915` resource requests, no
hostPath) is then a renderer change with zero catalog edits. Letting an SD name
`/dev/dri` directly would look simpler and make that migration a catalog-wide
edit, so both the schema description and the renderer contract say so
explicitly.
When `hardware.gpu == intel-quicksync`, r_deployment emits, on the app
container / pod spec only:
- a hostPath volume `dri` -> /dev/dri, type DirectoryOrCreate;
- a read-write volumeMount of it at /dev/dri (VA-API opens the render node
read-write, so deliberately not readOnly);
- INTEL_RENDER_GID MERGED into podSecurityContext.supplementalGroups, never
clobbering what the SD already declares.
`DirectoryOrCreate` is load-bearing. A node that comes back without the render
device (rebuild, BIOS change, driver regression, hardware swap) must degrade
the workload to software transcoding, not fail the mount: apps-wave-0 is
`wait: true` and apps-wave-1 dependsOn it, so a failed hostPath mount stalls
the whole app tier for a feature that is not load-bearing. /dev is tmpfs, so
the empty directory the kubelet creates leaves nothing durable behind. Its cost
— a missing device is silent, and no metric exists to alert on — is recorded in
the contract rather than papered over with an alert that could not fire.
Deliberately NOT emitted: no `privileged: true` (Kyverno restrict-host-
namespaces is Enforce and it is not needed), no nodeSelector (placement is a
separate concern with its own justification), no resource requests (there is no
app-container resource render path).
INTEL_RENDER_GID = 992 is a single named constant carrying its provenance:
measured on all three prod nodes on 2026-09-30, and a *systemd dynamic*
allocation rather than a static GID, so a node rebuild can move it.
No catalog entry uses the field. Re-rendering `--all catalog -o apps` is a zero
diff, which is the proof the change is inert until opted into.
Three self-test cases in the existing check(...) style: the positive, the
negative (an SD without `hardware` gains no volume, no mount, no
securityContext — mirroring the podSecurityContext negative) and the merge case
([44] + 992 -> [44, 992], fsGroup preserved). Note that NO gate stage runs
`render.py --selftest`; they were run by hand and the gap is recorded in the
handoff.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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.
What this is
A ServiceDefinition could not declare that it needs the host's Intel QuickSync render device. The schema is
additionalProperties: false, andapps/is generated fromcatalog/+ the schema, so there was no authorable form for it at all and a hand-edit toapps/would be reverted by the next render.This adds the capability. It does not wire Plex — that is separate work on top of this.
New optional top-level SD property
hardware, with one optional membergpu, an enum whose only current value isintel-quicksync. Schema 1.7.0 → 1.8.0.Why an enum and not a device path
The SD declares what class of device it needs; the hostPath and the mount live only in
catalog/render.py. Migrating to the Intel GPU device plugin (gpu.intel.com/i915resource requests, no hostPath at all) is then a renderer change with zero catalog edits and no per-service device wiring to re-review.Letting an SD name
/dev/dridirectly would look like a simplification and would make that migration a catalog-wide edit. Both the schemadescriptionandrenderer-contract.md§hardwaresay so explicitly, so the next person does not "simplify" it. That property also happens to be this PR's insurance policy — see the open risk at the bottom.What the renderer emits
Exactly two things, on the app container only (never a sidecar):
(a real render of a throwaway SD carrying
hardware, a Longhorn volume and its ownpodSecurityContext: {runAsUser: 1000, supplementalGroups: [44]}— note[44], not[44, something]: the field adds nothing to it)The mount is deliberately not
readOnly— VA-API opens the render node read-write.No
supplementalGroups, and that is the interesting part/dev/dri/renderD128isroot:render0660, so the obvious move is to push the hostrenderGID intopodSecurityContext.supplementalGroups. An earlier revision of this branch did exactly that. It was measured to be a no-op and has been removed — measured in the running container on prod-2, 2026-09-30:/etc/services.d/plex/runends inexec s6-setuidgid plex …, and the child's group list is exactly the/etc/groupaccount-database list — root's gid0, which PID 1 holds, has vanished. That is asetgroups()that replaces rather than appends, so anything Kubernetes supplies reachess6-svscanand is discarded before the app ever starts.What actually works is the image's own
/etc/cont-init.d/45-plex-hw-transcode-and-connected-tuner: itfinds every char device under/dev/dri,stat -c '%g's its GID,groupadds a matching group inside the container when none exists, andusermod -a -Gs the app user into it — every start, nothing hardcoded.cont-init.dcompletes beforeservices.d, so the database is already correct at exec time. Mounting the device is sufficient; the group handling is the image's job and it does it correctly.An inert
supplementalGroupswould have read as the mechanism to whoever debugged this next, so the absence is documented at the emission site and in the contract, and--selftestasserts it. Corollary: because the hook re-stats the device on every start, host GID drift (a rebuild renumbering the systemd dynamicrendergroup) is self-healing, and no GID is pinned anywhere in this repo.DirectoryOrCreateis load-bearing — please do not tighten it toDirectoryA node that comes back without the render device (rebuild, BIOS change, driver regression, hardware swap) must degrade the workload to software transcoding, not fail the mount.
apps-wave-0iswait: trueandapps-wave-1dependsOnit, so a failed hostPath mount stalls the entire app tier for a feature that is not load-bearing./devis tmpfs, so the empty directory the kubelet creates leaves nothing durable behind. The reasoning is at the emission site.Its cost is stated rather than papered over: a missing device is silent. No metric exists to alert on (media apps here declare no
metricsEndpoint, so no ServiceMonitor exists), and an alert that cannot fire is worse than none — see the 2026-09-13 correction inrenderer-contract.md§observability.Also deliberately NOT emitted
privileged: true— Kyvernorestrict-host-namespacesis Enforce on app namespaces and the render node does not need it;nodeSelector— placement is a separate concern with its own justification;Verification
python3 catalog/render.py --selftest→render.py selftest: PASS, rc 0. New cases in the existingcheck(...)style: the positive (volume + mount, and nosecurityContext/ privileged / nodeSelector), the negative that matters (nohardware⇒ no volume, no mount, nosecurityContext— mirroring the existingpodSecurityContextnegative), and a pass-through case (an SD's ownpodSecurityContextsurvives byte-for-byte).if True:in a scratch copy → rc 1, failing on the negative. A test that cannot fail is not a test.--all catalog -o apps --wave-layout --no-image-automationthengit diff --stat apps/→ empty,git status --porcelain apps/→ empty. The change is inert until a service opts in.catalog/*.yamlvalidate with 0 errors.gpu: /dev/drirejected (enum), an extra key rejected (additionalProperties: false),hardware: {}rejected (minProperties: 1).rc 3— UNDETERMINED, NOT green. Nothing failed.spec-immutabilitySKIPPED (sibling spec repo absent from a worktree — tolerated per CLAUDE.md §3),gitea-settingsSKIPPED (SETTINGS_API_TOKENunset),conformanceSKIPPED (CLUSTER_AVAILABLE!=true, Phase-0 standing skip).decisions-log,lint,l0(19 cases, 0 failures/skips/unwired),render-golden("all 10 onboarded service golden(s) reproduce"),l1(3 cases, 0 failures/skips/unwired) andapproval-policyall pass. No stage skipped for a reason caused by this change. Full per-stage table in the handoff entry.Known gaps (also in the handoff)
open()a bind-mounted char device. Still unverified, and it is the single largest technical risk in this workstream. Settling it requires applying a pod, which is an operator action. The container's PID 1 running as uid 0 helps on the DAC side but does not bypass the cgroup gate. If it blocks, the fallback is the Intel device plugin — which the intent-shaped enum makes a renderer-only change with no catalog edit. That is the design justifying itself, and the reason to keep the enum even if the hostPath mechanism loses.sha256:e0ab27395614…(plexmediaserver 1.43.4.10903), but the deployment runsplexinc/pms-docker:latestwithimagePullPolicy: Always. A silent pull can change the behaviour this feature depends on, with no commit anywhere in the repo to show for it. Noted, not fixed here.render.py --selftest(git grep -- --selftest ci/is empty). The tests were run by hand. Wiring them in touchesci/, a sensitive path, so it is deliberately not in this PR.kubectland opened no container shell.Touches only
schemas/,catalog/,decisions/— none sensitive perinfra/gitea/approval-policy.json.conformance/**andclusters/**untouched.🤖 Generated with Claude Code
hardware.gpu: intel-quicksyncA ServiceDefinition could not say it needs the host's Intel QuickSync render device. The schema is `additionalProperties: false`, so there was no authorable form for it at all, and `apps/` is generated — a hand-edit would be reverted by the next render. Adds optional top-level `hardware`, with one optional member `gpu`, an enum whose only current value is `intel-quicksync` (schema 1.7.0 -> 1.8.0). The enum is the point. The SD declares WHAT CLASS of device it needs; the hostPath, the mount and the GID live only in catalog/render.py. Migrating to the Intel GPU device plugin (`gpu.intel.com/i915` resource requests, no hostPath) is then a renderer change with zero catalog edits. Letting an SD name `/dev/dri` directly would look simpler and make that migration a catalog-wide edit, so both the schema description and the renderer contract say so explicitly. When `hardware.gpu == intel-quicksync`, r_deployment emits, on the app container / pod spec only: - a hostPath volume `dri` -> /dev/dri, type DirectoryOrCreate; - a read-write volumeMount of it at /dev/dri (VA-API opens the render node read-write, so deliberately not readOnly); - INTEL_RENDER_GID MERGED into podSecurityContext.supplementalGroups, never clobbering what the SD already declares. `DirectoryOrCreate` is load-bearing. A node that comes back without the render device (rebuild, BIOS change, driver regression, hardware swap) must degrade the workload to software transcoding, not fail the mount: apps-wave-0 is `wait: true` and apps-wave-1 dependsOn it, so a failed hostPath mount stalls the whole app tier for a feature that is not load-bearing. /dev is tmpfs, so the empty directory the kubelet creates leaves nothing durable behind. Its cost — a missing device is silent, and no metric exists to alert on — is recorded in the contract rather than papered over with an alert that could not fire. Deliberately NOT emitted: no `privileged: true` (Kyverno restrict-host- namespaces is Enforce and it is not needed), no nodeSelector (placement is a separate concern with its own justification), no resource requests (there is no app-container resource render path). INTEL_RENDER_GID = 992 is a single named constant carrying its provenance: measured on all three prod nodes on 2026-09-30, and a *systemd dynamic* allocation rather than a static GID, so a node rebuild can move it. No catalog entry uses the field. Re-rendering `--all catalog -o apps` is a zero diff, which is the proof the change is inert until opted into. Three self-test cases in the existing check(...) style: the positive, the negative (an SD without `hardware` gains no volume, no mount, no securityContext — mirroring the podSecurityContext negative) and the merge case ([44] + 992 -> [44, 992], fsGroup preserved). Note that NO gate stage runs `render.py --selftest`; they were run by hand and the gap is recorded in the handoff. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>2279d14ec5to1040d253be1040d253betof8ad3a6f81f8ad3a6f81tod9baa2a482d9baa2a482tof716e9d167