Persistent VM hostname status indicator in Claude and Pi harnesses #8

Merged
vnprc merged 1 commit from agent/agent-vm-status into master 2026-07-16 21:28:37 +01:00
Member

Closes allod/profiles#7

Every pi and claude session on a dev VM now shows a persistent VM: <hostname> indicator in its own footer/status line, sourced once from osConfig.networking.hostName and fanned out to both harnesses so they render the identical string.

What changed

modules/ai-agents.nix gains osConfig in the inner module args and binds hostName = osConfig.networking.hostName. Four Nix store artifacts are added (immutable, content-addressed, directly testable): claudeStatusLine (a statusLine command script that discards stdin and prints VM: <hostname>), claudeStatusMerge (a jq merge that sets only the .statusLine key of ~/.claude/settings.json, guarded by a single-JSON-object preflight and an atomic mv), piStatusExtension (an auto-discovered Pi .ts that re-applies the footer status on session_start), and piStatusInstall (an installer that refuses to replace any non-managed file, directory, or symlink at the extension target). A dedicated home.activation.agentVmStatus entry (entryAfter ["writeBoundary"], kept separate from llmMemoryLinks) invokes the Pi installer and the Claude merge, each NON-FATALLY via || warnEcho, so a refusal prints a loud error and leaves operator state byte-unchanged without aborting the shared set -eu activation script. flake.nix gains an agent-vm-status check that mirrors pi-integration, embeds the activation .data via pkgs.writeText (whose string context realizes all four artifacts into the check closure), and exercises the artifacts plus their negative/sabotage paths.

Risk

Residual risk R2 (Medium), unchanged from the plan. Blast radius is dev VMs only (ai-agents.nix is imported solely by the dev-VM builder); no secret, provisioning phase, privacy VM, host authority, or cross-repo sequencing is touched. The only persistent, operator-authoritative state is ~/.claude/settings.json (merged, not owned; every non-statusLine key preserved) and the exact Pi extension target ~/.pi/agent/extensions/vm-status.ts (refused if it is a non-managed file, directory, or symlink). Both mutable-state helpers are invoked non-fatally, so a target collision or malformed operator settings.json cannot skip a later activation entry — containment comes from non-fatality, not from DAG ordering, because installPackages and syncPrBranchProtection sort after this entry. The R2 score rests on the behavioral fixture coverage below; without it the honest score would drift to R3. Rollback is a straight revert plus a human rebuild; the residual runtime symlink and .statusLine key are inert if left and trivially removable.

Validation

All agent-runnable acceptance tests from the plan (steps 1-6) were run here on allod-dev (x86_64-linux) and passed. Step 1: nix build .#checks.x86_64-linux.agent-vm-status passes, and the check asserts both helpers are named in the activation text, both are invoked non-fatally via warnEcho, and the Pi extension bakes VM: allod-dev with ctx.ui.setStatus("vm-status" on session_start. Step 1a: the Pi installer fixtures pass (fresh install creates the managed symlink; an old managed store symlink is replaced; operator file, directory, and custom symlink are each refused byte-unchanged). Step 1b: the Claude merge fixtures pass (operator keys preserved, .statusLine.command points at the realized script, idempotent byte-for-byte; empty, scalar, array, multi-document, and malformed inputs are all refused with the file byte-unchanged). Step 2: the Claude render pipes a representative stdin object and stdout carries VM: allod-dev. Step 3: a hostname-mutated statusline script fails closed. Step 4: pi --mode rpc --offline on the real generated extension emits {"type":"extension_ui_request",...,"method":"setStatus","statusKey":"vm-status","statusText":"VM: allod-dev"} with exit 0. Step 5: a syntactically broken extension exits non-zero and emits no setStatus. Step 6: the full allod-dev toplevel closure still evaluates. warnEcho was confirmed defined in the built home-manager activate script (sourced from lib/bash/home-manager.sh).

One deviation from the plan text: the acceptance-test store-path extraction uses the character class [^"* ]+ rather than [^" ]+. The plan's [^" ]+-pi-vm-status.ts also matches the installer's own case-guard glob /nix/store/*-pi-vm-status.ts (which likewise ends in -pi-vm-status.ts), and head -1 selected that glob instead of the real ln target, so piext became a literal /nix/store/*-pi-vm-status.ts and the render assertions failed on a nonexistent file. Excluding * (a real store path never contains *, ", or a space) selects the real ln target; the generated piStatusInstall artifact itself is exactly per the plan's Interface Contract and was not changed.

Remaining human-gated follow-ups (cannot run without a rebuild): a human runs nixos-rebuild on allod-dev to make the indicator live, then eyeballs a real pi and claude TUI to confirm the footer/status row persistently shows VM: allod-dev across turns and after /new (pi) / new messages (claude). Claude's row needs an accepted workspace-trust prompt and is transiently hidden during autocomplete/permission prompts — those UI caveats are expected, not defects. If a row is absent after rebuild, check journalctl -u home-manager-allod.service for the helper ERROR/warning first.

Closes allod/profiles#7 Every `pi` and `claude` session on a dev VM now shows a persistent `VM: <hostname>` indicator in its own footer/status line, sourced once from `osConfig.networking.hostName` and fanned out to both harnesses so they render the identical string. ## What changed `modules/ai-agents.nix` gains `osConfig` in the inner module args and binds `hostName = osConfig.networking.hostName`. Four Nix store artifacts are added (immutable, content-addressed, directly testable): `claudeStatusLine` (a `statusLine` command script that discards stdin and prints `VM: <hostname>`), `claudeStatusMerge` (a `jq` merge that sets only the `.statusLine` key of `~/.claude/settings.json`, guarded by a single-JSON-object preflight and an atomic `mv`), `piStatusExtension` (an auto-discovered Pi `.ts` that re-applies the footer status on `session_start`), and `piStatusInstall` (an installer that refuses to replace any non-managed file, directory, or symlink at the extension target). A dedicated `home.activation.agentVmStatus` entry (`entryAfter ["writeBoundary"]`, kept separate from `llmMemoryLinks`) invokes the Pi installer and the Claude merge, each NON-FATALLY via `|| warnEcho`, so a refusal prints a loud error and leaves operator state byte-unchanged without aborting the shared `set -eu` activation script. `flake.nix` gains an `agent-vm-status` check that mirrors `pi-integration`, embeds the activation `.data` via `pkgs.writeText` (whose string context realizes all four artifacts into the check closure), and exercises the artifacts plus their negative/sabotage paths. ## Risk Residual risk R2 (Medium), unchanged from the plan. Blast radius is dev VMs only (`ai-agents.nix` is imported solely by the dev-VM builder); no secret, provisioning phase, privacy VM, host authority, or cross-repo sequencing is touched. The only persistent, operator-authoritative state is `~/.claude/settings.json` (merged, not owned; every non-`statusLine` key preserved) and the exact Pi extension target `~/.pi/agent/extensions/vm-status.ts` (refused if it is a non-managed file, directory, or symlink). Both mutable-state helpers are invoked non-fatally, so a target collision or malformed operator `settings.json` cannot skip a later activation entry — containment comes from non-fatality, not from DAG ordering, because `installPackages` and `syncPrBranchProtection` sort after this entry. The R2 score rests on the behavioral fixture coverage below; without it the honest score would drift to R3. Rollback is a straight revert plus a human rebuild; the residual runtime symlink and `.statusLine` key are inert if left and trivially removable. ## Validation All agent-runnable acceptance tests from the plan (steps 1-6) were run here on `allod-dev` (`x86_64-linux`) and passed. Step 1: `nix build .#checks.x86_64-linux.agent-vm-status` passes, and the check asserts both helpers are named in the activation text, both are invoked non-fatally via `warnEcho`, and the Pi extension bakes `VM: allod-dev` with `ctx.ui.setStatus("vm-status"` on `session_start`. Step 1a: the Pi installer fixtures pass (fresh install creates the managed symlink; an old managed store symlink is replaced; operator file, directory, and custom symlink are each refused byte-unchanged). Step 1b: the Claude merge fixtures pass (operator keys preserved, `.statusLine.command` points at the realized script, idempotent byte-for-byte; empty, scalar, array, multi-document, and malformed inputs are all refused with the file byte-unchanged). Step 2: the Claude render pipes a representative stdin object and stdout carries `VM: allod-dev`. Step 3: a hostname-mutated statusline script fails closed. Step 4: `pi --mode rpc --offline` on the real generated extension emits `{"type":"extension_ui_request",...,"method":"setStatus","statusKey":"vm-status","statusText":"VM: allod-dev"}` with exit 0. Step 5: a syntactically broken extension exits non-zero and emits no `setStatus`. Step 6: the full `allod-dev` toplevel closure still evaluates. `warnEcho` was confirmed defined in the built home-manager `activate` script (sourced from `lib/bash/home-manager.sh`). One deviation from the plan text: the acceptance-test store-path extraction uses the character class `[^"* ]+` rather than `[^" ]+`. The plan's `[^" ]+-pi-vm-status.ts` also matches the installer's own `case`-guard glob `/nix/store/*-pi-vm-status.ts` (which likewise ends in `-pi-vm-status.ts`), and `head -1` selected that glob instead of the real `ln` target, so `piext` became a literal `/nix/store/*-pi-vm-status.ts` and the render assertions failed on a nonexistent file. Excluding `*` (a real store path never contains `*`, `"`, or a space) selects the real `ln` target; the generated `piStatusInstall` artifact itself is exactly per the plan's Interface Contract and was not changed. Remaining human-gated follow-ups (cannot run without a rebuild): a human runs `nixos-rebuild` on `allod-dev` to make the indicator live, then eyeballs a real `pi` and `claude` TUI to confirm the footer/status row persistently shows `VM: allod-dev` across turns and after `/new` (pi) / new messages (claude). Claude's row needs an accepted workspace-trust prompt and is transiently hidden during autocomplete/permission prompts — those UI caveats are expected, not defects. If a row is absent after rebuild, check `journalctl -u home-manager-allod.service` for the helper ERROR/warning first.
Generate a Claude statusLine script, a Claude settings-merge script, a Pi
status extension, and a Pi installer script as Nix store artifacts in
modules/ai-agents.nix, all baking VM: <hostName> from osConfig.networking.hostName.
Add a dedicated home.activation.agentVmStatus entry that invokes the Pi installer
and the Claude .statusLine merge non-fatally via warnEcho so a refusal cannot
abort the shared activation script. Add an agent-vm-status flake check mirroring
pi-integration that exercises the generated artifacts and their negative paths.
vnprc approved these changes 2026-07-16 21:28:27 +01:00
vnprc merged commit bdafe66b84 into master 2026-07-16 21:28:37 +01:00
vnprc deleted branch agent/agent-vm-status 2026-07-16 21:28:37 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
allod/profiles!8
No description provided.