Skip to content
Merged
89 changes: 75 additions & 14 deletions CHANGELOG.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ How a version reaches npm: the owner signs a tag, CI (`.github/workflows/release
8. **Pin the bootstrap** for its PR: `node scripts/pin-engine.mjs vX.Y.Z --allowed-signers .github/release-signers`.
9. **After it installs on each release-channel machine** (the updater takes the new tag): when the release changes the machine helper's sudoers rules, rerun them as root, once, with every instance on that machine: `sudo bash /opt/worklane/current/scripts/setup/machine-helper.sh <every instance>`. Until then, coordinators can't run the helper's new commands (sudo refuses them), and nothing else changes. Not before the install: the rules must match the installed helper.
- **0.5.1:** yes. It adds the Claude account profile commands (`claude-account profile-add|remove|use <profile>`, `failover … back|stay`, and `login <profile>` if #495 is in).
- **0.5.3:** no. The machine helper's rules are unchanged; they still list `claude-account switch`, `profile-add` and `login`, which the helper now refuses (#749). Its other root steps are in CHANGELOG.md's v0.5.3 "After install".
10. **GPU test runs, after it installs on a release-channel machine that has an NVIDIA GPU** (0.5.1 adds the `worklane-gpu` group): if you'll use `worklane gpu hold`, make yourself the GPU lock's holder once, then log in again: `sudo bash /opt/worklane/current/scripts/setup/testrun-gpu.sh holder <your user>`. Not before the install. Turning GPU runs on for an instance (`testrun-gpu.sh enable …`) is separate, and needs the owner's go per machine.
- **Then close the GPU to everyone else** (docs/test-runs.md "GPU devices"; the nodes are `root:root 0666` until this runs), as root, once: `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh check` (says FAIL; note who has a node open) → `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh apply --users <your user>` (add `--no-dynamic-nodes` on a GPU that doesn't run MIG: NVML then changes no node, and `/dev/nvidia-caps` stays empty) → `sudo reboot` (not a driver reload) → `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh check` **before touching anything** (must say PASS, with the `nvidia_uvm` module loaded and you, a member, opening `/dev/nvidia-uvm`) → `id <your user>` lists `worklane-gpu-dev` (log in again if a session doesn't have it). A FAIL after a boot is a bug to report, not something to fix by hand each time: `sudo /usr/local/libexec/worklane-gpu-devices` only sets the nodes back for now.
- **Then close the GPU to everyone else** (docs/test-runs.md "GPU devices"; the nodes are `root:root 0666` until this runs), as root, once: `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh check` (says FAIL; note who has a node open) → `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh apply --users <your user>` (`--no-dynamic-nodes`, only on a GPU that doesn't run MIG, leaves `/dev/nvidia-caps` empty; it does not stop a root NVML call such as `sudo nvidia-smi -L` putting `nvidia-uvm` and `nvidia-uvm-tools` back to `root:root 0666`, measured in #583, corrected 2026-10-19) → `sudo reboot` (not a driver reload) → `sudo bash /opt/worklane/current/scripts/setup/gpu-devices.sh check` **before touching anything** (must say PASS, with the `nvidia_uvm` module loaded and you, a member, opening `/dev/nvidia-uvm`) → `id <your user>` lists `worklane-gpu-dev` (log in again if a session doesn't have it). A FAIL after a boot is a bug to report, not something to fix by hand each time: `sudo /usr/local/libexec/worklane-gpu-devices` only sets the nodes back for now. After any root NVML call (with `--no-dynamic-nodes` too), run `sudo /usr/local/libexec/worklane-gpu-devices`, then `check`, until #583's fixer lands.
- **Already applied on 0.5.1:** 0.5.2's `apply` adds a boot unit (`worklane-gpu-devices.service`) and drop-ins for `nvidia-cdi-refresh.service` and `nvidia-persistenced.service`, so rerun `apply --users <your user> [--no-dynamic-nodes]` as root once after the install, `sudo reboot`, then `check` before touching anything. It adds no sudoers rules, so there is no machine-helper rerun for it (step 9). It refuses while another `modprobe.d` file sets `NVreg_DeviceFile*` or `NVreg_ModifyDeviceFiles`: remove that file first.
11. **Test-run network, before it installs on a machine whose instance runs test runs** (0.5.1: a run's commands have no network, and its database is a Unix socket, docs/test-runs.md "Network"). No root step: the updater installs the new helper. But a project whose `tests.yaml` `test_run.setup` installs from the network (plain `npm ci`) fails every run after the update until its config changes, on main:
- npm with a lockfile of registry packages: add `fetch: [{ npm: <its npm cache's name> }]` (a cache with `env: npm_config_cache`) and make the step `npm ci --offline`;
Expand Down
2 changes: 1 addition & 1 deletion docs/test-runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ The locks only order the harness's own GPU runs. By default NVIDIA's device node
- **How the nodes get made, and what apply changes** (measured on Ubuntu 24.04 with its packaged driver, `nvidia-driver-<version>` 595; other distributions may differ):
- Ubuntu's packages make the nodes from udev: `71-nvidia.rules` runs `/sbin/ub-device-create` on the driver's `add` and on `nvidia_uvm`'s, and it sets every node it makes (`nvidiactl`, `nvidia<N>`, `nvidia-modeset`, `nvidia-uvm`, `nvidia-uvm-tools`) to `root:root 0666`. apply installs `/etc/udev/rules.d/99-worklane-gpu-devices.rules`, which runs `/usr/local/libexec/worklane-gpu-devices` (from `scripts/machine/worklane-gpu-devices.sh`) after it, on the same events: every `/dev/nvidia*` node becomes `root:worklane-gpu-dev 0660`, and each `/dev/nvidia-caps/*` node `root:worklane-gpu-dev` with nothing for others (MIG's config capability stays root's alone).
- **A root NVML call undoes that, whenever it runs.** `nvidia-smi` as root, and the `nvidia-cdi-refresh.service` the container toolkit starts at boot (a oneshot: `nvidia-smi -L`, then `nvidia-ctk system create-device-nodes`, then `nvidia-ctk cdi generate`), put `nvidia-uvm` and `nvidia-uvm-tools` back to `root:root 0666` and the `nvidia-caps` nodes to `root:root` (`0400` and `0444`: the driver's own modes). `nvidiactl`, `nvidia<N>` and `nvidia-modeset` stay `root:worklane-gpu-dev 0660`, because NVML applies the driver's options to them: `NVreg_DeviceFileUID`, `NVreg_DeviceFileGID` and `NVreg_DeviceFileMode` (default 0, 0, 0666; shown in `/proc/driver/nvidia/params`). apply writes them to `/etc/modprobe.d/worklane-gpu-devices.conf` (`options nvidia NVreg_DeviceFileUID=0 NVreg_DeviceFileGID=<the group's gid> NVreg_DeviceFileMode=0660`) and updates the initramfs when it carries the nvidia module. If `nvidia-cdi-refresh.service` exists, apply also writes a drop-in for it, `/etc/systemd/system/nvidia-cdi-refresh.service.d/worklane-gpu-devices.conf`: `ExecStartPost=+/usr/local/libexec/worklane-gpu-devices` runs the fixer after it (the `+` runs it with full privileges: that unit's `CapabilityBoundingSet` has no `CAP_CHOWN`), and `OnFailure=worklane-gpu-devices.service` runs it when the unit fails, which is when its `ExecStartPost` never runs. apply also writes `/etc/systemd/system/worklane-gpu-devices.service` and enables it: at every boot, after `systemd-modules-load.service` and `nvidia-cdi-refresh.service`, it waits for udev (`udevadm settle`, at most 30 s) and runs the fixer. That covers a machine where `nvidia-cdi-refresh.service` is absent or skipped, and a node made after udev's own fixer run (measured on one machine: `nvidia-uvm` and `nvidia-uvm-tools` were made a millisecond after the last fixer pass, and nothing ran it after). A root `nvidia-smi` you run later still undoes it: `check` then says FAIL, and `sudo /usr/local/libexec/worklane-gpu-devices` sets the nodes back.
- **`apply --users ... --no-dynamic-nodes`** also writes `NVreg_ModifyDeviceFiles=0`, and then NVML changes no node at all (measured: a root `nvidia-smi` left every node's status-change time alone), so nothing undoes the fixer later. The cost: `/dev/nvidia-caps` stays empty, so MIG's capability nodes don't exist: don't use it on a GPU that runs MIG. CUDA as a group member still works (measured: `cuInit` returned 0). `check` then requires `ModifyDeviceFiles: 0` in `/proc/driver/nvidia/params`.
- **`apply --users ... --no-dynamic-nodes`** also writes `NVreg_ModifyDeviceFiles=0`. **It does not stop a root NVML call resetting `nvidia-uvm` and `nvidia-uvm-tools`** (measured on a machine after a reboot, #583: with `ModifyDeviceFiles: 0` in effect and `check` passing, `sudo nvidia-smi -L` put both back to `root:root 0666`). This page used to claim the opposite (corrected 2026-10-19): the earlier trial behind that claim, where a root `nvidia-smi` left every node's status-change time alone, started with `nvidia-uvm` already at 0666, so there was nothing to change (inferred, #583: NVML changes only a node that differs). So after any root NVML call, with or without this option, run `sudo /usr/local/libexec/worklane-gpu-devices`, then `gpu-devices.sh check`, until #583's fixer lands. All the option does is leave `/dev/nvidia-caps` empty, so MIG's capability nodes don't exist: don't use it on a GPU that runs MIG. CUDA as a group member still works (measured: `cuInit` returned 0). `check` then requires `ModifyDeviceFiles: 0` in `/proc/driver/nvidia/params`.
- **`nvidia-persistenced`** (the driver's persistence daemon) starts as root and then drops to its own user by setting only its uid and gid, so it never gets the groups from `/etc/group`: measured, it failed every boot although its user was in the device group, and with `SupplementaryGroups=worklane-gpu-dev` on its unit it started and stayed active. When the unit and the user exist, apply writes `/etc/systemd/system/nvidia-persistenced.service.d/worklane-gpu-devices.conf` with that line (systemd gives its root process the group before the drop, and it stays). `check` FAILs without it, or accepts a masked unit.
- A distribution that ships `nvidia-modprobe` (Ubuntu's driver package doesn't, at 595) has it make `nvidia-uvm` and `nvidia-uvm-tools` `root:root 0666` whatever the options say, and installed setuid root, any user could put them back. Where it is installed, apply takes its setuid bit away (`dpkg-statoverride`, so upgrades keep it off); `check` says FAIL while it is setuid.
- Without a setuid `nvidia-modprobe`, nothing loads `nvidia-uvm` (which CUDA needs) on demand for a user. apply writes `/etc/modules-load.d/worklane-gpu-devices.conf` (`nvidia-uvm`), so it loads at every boot.
Expand Down
4 changes: 3 additions & 1 deletion scripts/machine/worklane-gpu-devices.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
# nvidia_uvm's "add"), and that program sets every node it makes to root:root 0666, fixing any other mode back,
# whatever the driver's options say. This runs after it, on the same events. A root NVML call later (nvidia-smi, the
# nvidia-cdi-refresh unit at boot) puts nvidia-uvm, nvidia-uvm-tools and the nvidia-caps nodes back, so it runs again
# after that unit.
# after that unit. NVreg_ModifyDeviceFiles=0 (gpu-devices.sh apply --no-dynamic-nodes) does not stop that reset
# (measured, #583: `sudo nvidia-smi -L` put nvidia-uvm and nvidia-uvm-tools back to root:root 0666 with it in effect):
# after any root NVML call, root runs this again, then gpu-devices.sh check.
set -uo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
GROUP=worklane-gpu-dev
Expand Down
8 changes: 5 additions & 3 deletions scripts/setup/gpu-devices.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
# that unit fails; the same unit runs at every boot after systemd-modules-load and nvidia-cdi-refresh, once udev
# has settled, for a machine where that unit is absent or skipped, or whose late writer is something else (measured:
# nvidia-uvm and nvidia-uvm-tools made after the last fixer run, with none after them).
# apply --no-dynamic-nodes also sets NVreg_ModifyDeviceFiles=0: NVML then changes no node, and
# /dev/nvidia-caps stays empty (MIG's capability nodes aren't made: don't use it on a MIG GPU).
# apply --no-dynamic-nodes also sets NVreg_ModifyDeviceFiles=0, and /dev/nvidia-caps stays empty (MIG's
# capability nodes aren't made: don't use it on a MIG GPU). That is all it does: it does NOT stop a root NVML call
# resetting nvidia-uvm and nvidia-uvm-tools (measured, #583: with ModifyDeviceFiles: 0 in effect, `sudo nvidia-smi
# -L` put both back to root:root 0666). After any root NVML call, run the fixer, then check.
# - the driver's options (NVreg_DeviceFileUID, NVreg_DeviceFileGID, NVreg_DeviceFileMode) are what NVML applies to
# nvidiactl, nvidia<N> and nvidia-modeset: /etc/modprobe.d/worklane-gpu-devices.conf sets them to 0, the group, 0660.
# - nvidia-modprobe, where a distribution ships it, makes nvidia-uvm and nvidia-uvm-tools 0666 root:root whatever
Expand Down Expand Up @@ -425,7 +427,7 @@ apply() {
dropin_ok || { echo "the drop-in came out wrong" >&2; exit 1; }
echo "installed: the fixer runs after $CDI_UNIT, and as $FIXER_UNIT_NAME when it fails"
else
echo "no $CDI_UNIT here: only $FIXER_UNIT_NAME runs the fixer, at boot. A root nvidia-smi later still puts nvidia-uvm back to 0666 (--no-dynamic-nodes stops that, or run $FIXER)."
echo "no $CDI_UNIT here: only $FIXER_UNIT_NAME runs the fixer, at boot. A root nvidia-smi later still puts nvidia-uvm back to 0666, with --no-dynamic-nodes too: run $FIXER after it, then check."
fi

if p="$(setuid_modprobe)"; then
Expand Down
19 changes: 19 additions & 0 deletions test/changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ test("every section of the repo's CHANGELOG.md keeps its hand-written text when
}
});

/** The lines of a CHANGELOG's v<version> section that `release.mjs changelog` (withSection) would drop. */
function droppedByRelease(text: string, version: string): string[] {
const after = c.sectionFor(c.withSection(text, version, c.changelogSection(version, '2026-12-31', [{ number: 1, title: 'A PR' }])), version)!.split('\n');
return c.sectionFor(text, version)!.split('\n').slice(1).filter((l) => l.trim() && !after.includes(l));
}

test('an unreleased section is all hand-written, so the release keeps every line of it, under any heading', () => {
// WHY: the check above reads only the lines above the first generated heading. Notes written by hand under a
// heading the tool also writes ("### Guardrail changes") sat below it, unchecked, and the release dropped them.
const unreleased = '# Changelog\n\n## v9.9.9 (unreleased)\n\n### Upgrading\n\n- a step\n\n### Guardrail changes\n\n- a hand-written guardrail note\n\n## v9.9.8 (2026-01-01)\n\n### Features\n\n- old (#1)\n';
assert.deepEqual(droppedByRelease(unreleased, '9.9.9'), ['- a hand-written guardrail note'], 'the fixture shows the drop this test exists for');
const fixed = unreleased.replace('### Guardrail changes\n', '### Guardrail changes: what each one does\n').replace('note\n\n## v9.9.8', 'note\n\n### Guardrail changes\n\n## v9.9.8');
assert.deepEqual(droppedByRelease(fixed, '9.9.9'), [], 'notes under a heading the tool never writes, ended by an empty generated heading, are all kept');
const text = readFileSync(join(repoRoot, 'CHANGELOG.md'), 'utf8');
for (const [, v] of text.matchAll(/^## v(\d+\.\d+\.\d+) \(unreleased\)/gm)) {
assert.deepEqual(droppedByRelease(text, v!), [], `v${v} (unreleased): the release would drop these lines; put them above the first "### Features", "### Fixes", "### Security and guardrails", "### Reverted in this release" or "### Guardrail changes" heading`);
}
});

test('a release section: each group, then guardrail changes with their files; written first in CHANGELOG.md, replacing a section of the same version', () => {
const prs: Pr[] = [
{ number: 10, title: 'Dashboard: a chat panel (#10)', files: ['web/src/x.tsx'] },
Expand Down
Loading