Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Phase log 60: the rename to UnENVerse, the Discord acceptance, restore-into-chunks, and a way to see the app

Date: 2026-10-09. NOTE: this phase was renumbered 40 -> 42 on 2026-10-09 (see AGENTS.md). Version 0.39.0 (npm run version 0.39.0, all six copies and Cargo.lock agree).

Goal

The user asked, in one message: (a) run the Discord setup.py as the Phase 24.1 acceptance test; (b) rename the project UnENVerse (pun: "universe" and "un-env"), CLI unv; (c) bring the README up to date; (d) set the npm version to the latest phase (0.x.0); (e) add everything still unbuilt; (f) find a way to see the real app, for UI bug fixing and screenshots; (g) regenerate handoffs and commit scripts.

Current progress

1. Discord acceptance (24.1) is done

The user's setup.py (values blanked to X) is tests/fixtures/parity/bundle-discord-setup.py; the golden output is bundle-discord-setup.json, generated by the Rust importer and asserted from both vault-core/src/bundle_import.rs and src/ts/bundle-import.ts. It exercises: snowflake ids kept as strings (large_id), colour = 0x800000 as a hex int, api_key assigned twice (first kept as api_key, second imported as api_key_2, later references rewritten, all three facts reported), two f-strings (one tainted by api_key), a markdown link, a repeated scope= parameter, and a hole-less f-string (inv, title) becoming a plain string. Export back to Python round-trips by meaning (tests/bundle-export.test.ts).

New in this round: public inference (ImportedVar.public). A variable is marked public only when it is plainly not a secret: names that look secret (key, token, secret, password, webhook, sig, auth, passkey) always win; hex ints, bools and floats are public; short ints are public; URLs are public unless they carry userinfo, a credential query parameter, a webhook path or a token-looking path segment; prose strings are public; a template is public only if every key it references is public. Both twins implement it; unv bundle import prints "public": true; the app copies it onto the bundle's extra_vars. Verified public set for the Discord file: defprefix, boturl, inv, title, description, colour, blink.

2. Rename

Done mechanically over 178 files, with data compatibility as the rule (ADR-0149):

  • Renamed: product name, binaries unv and unv-server (Cargo [[bin]]; the package names envv-cli and envv-server are unchanged), Docker service, productName and window title, artifact names, README, docs, env vars UNV_*, package.json name unenverse.
  • Kept on purpose: io.envvault identifier and data dir, $XDG_STATE_HOME/envv/, the node default config dir, .envv.json, all signing and hash domain strings, localStorage envvault-* keys, CXF envvault_* extension keys, crate and directory names. Reason: changing any of them orphans existing vaults or breaks enrolled nodes and history chains.
  • vault_core::compat::adopt_legacy_env() copies a set ENVV_* to UNV_* when the new name is unset; the three mains call it first. Eleven test files clear both prefixes (.env_remove("ENVV_...") after each UNV_), since a developer shell with ENVV_SERVER_URL otherwise leaks in through the shim.
  • Renamed files: envv-cli/examples/unv.py, packaging/unv-node.service, vscode-extension/src/unv.ts.
  • Found by the full test run: the rename script rewrote tests/fixtures/parity/calendar.ics with LF line endings (the fixture is CRLF by RFC 5545, and the Rust test failed). Restored from HEAD with only X-WR-CALNAME:UnENVerse changed. git diff --numstat | awk '$1==$2 && $1>50' is the quick check for a whole-file rewrite.

3. Version

npm run version 0.39.0. Phase 39 was the last numbered phase, so 0.39.0 is correct under the 0.N.0 rule. The README says 0.39.0 and names Phase 40 (this round's rename, viewer and restore) in the history table. Phase 40 has no queue number of its own beyond that row: it is follow-up work, and the next number to allocate is still 40 if a real phase is needed.

4. Restore into chunks (unbuilt item from phase log 59)

src/ts/restore-chunks.ts reads a rendered config back into a project's chunks with the app's own import parsers (WireGuard, nginx, Apache, HAProxy, Compose, ssh config). planRestore computes what a restore would replace (the chunk family of that format, so a note chunk is left alone), describePlan words it for the confirmation, applyRestore swaps the chunks. Wired into:

  • History pane: a "Restore into chunks" button on each snapshot row for formats with a reader. It fetches the real text with reveal: true, confirms, applies, saves, repaints.
  • Nodes pane: "Read into a chunk" is now offered for those formats as well as .env, using the same plan and confirmation. Values arrive as literal text (the deployed file had no references), and the confirmation says so. There is deliberately no Rust twin (six formats parsed twice would be six chances to disagree); no new Tauri command, so the capability map needs no entry. Test: tests/restore-chunks.test.ts.

5. A way to see the app (ADR-0150)

tools/viewer/: a Docker image (Xvfb, WebKitGTK 4.1, tauri-driver, webkit2gtk-driver, mold) and shots.py, a small W3C WebDriver client. tools/viewer/run.sh [scenario...] builds dist/ on the host, compiles the app in the container (named volume unv-viewer-target; first build about six minutes), starts it on a virtual display with a throwaway HOME, creates a vault through the real unlock screen, seeds it with the UI lab's vault, walks 18 scenarios (lock screens, grid, expanded card, tools, nodes, history, authenticator, remote, add dialog, five settings tabs) and writes PNGs and report.json (page errors per screen; exit 1 on any). It is the real WebKit engine, the real IPC bridge and a real SQLCipher vault; the host is untouched. shots/ is gitignored.

Things that went wrong getting it to work, so nobody repeats them:

  • bash -lc in the container resets PATH (cargo not found): use bash -c.
  • The repo's .cargo/config.toml links with mold; the image needed mold and clang.
  • WebDriver's own /screenshot hangs for over 60 s under software compositing. The script grabs the Xvfb root with ImageMagick import and crops to 1440x900 instead; the window is placed at 0,0 (otherwise it opens offset and clipped).
  • location.reload() drops injected page-error listeners: collect() runs again after it.
  • Typing the master password through WebDriver after the reload raced the focus and landed in the server-URL field; the script sets the field with JavaScript and clicks submit.
  • The seeded settings blob must include panelOrder with auth, or the Authenticator button is hidden (the default list has it; a partial stored blob does not). Not a product bug for a fresh install, but see "UI findings".

First full run: 18 screens, no page errors except one scenario failure that was the missing panelOrder, then 0 errors.

UI findings from the screenshots (not fixed; each needs a decision or a look)

  1. Grid cards have a large empty body when the entry has only a primary value: the "Authenticator entry" card in the seeded grid is about 240 px tall with the content in the top third. A pool card (llm-fallback) renders as a short collapsed row centred inside a tall column slot beside taller cards. The bundle commit before this round set a min-height floor for members; the same floor appears to apply to the pool row. Check src/css for the .card min-height.
  2. Nodes pane on a local vault shows the enrolment token form, "Use this device to approve" and the blast-radius date, then an empty bordered bar, although the pane says it needs a remote vault on a server started with --nodes. It should disable or hide the controls the way the LAN card does.
  3. Authenticator panel: the seed card shows - - - for the code in the seeded vault; the seed in the UI-lab fixture is not a valid base32 seed, so this is probably the fixture, not the product. Confirm with a real seed.
  4. The authenticator list row does not truncate a very long provider name (it wraps to four lines); the card header does (ellipsis). Make them agree.

Verification

  • cargo test --workspace --no-fail-fast: every binary ok (32 test-result lines ok; the one failure, parity::calendar_ics, was the CRLF loss above, fixed and re-run green).
  • cargo clippy --workspace --all-targets -- -D warnings: clean.
  • npm run check (version, Prettier, ESLint --max-warnings 0, tsc, Vitest): 88 files, 1,497 tests pass. Three failures found and fixed on the way: two test regexes still saying envv (rename), and the Nodes-pane test expecting only .env targets to offer Read into a chunk.
  • npm run ui-lab: 185 passed.
  • Not re-run after the rename: docker build . (the Dockerfile copies unv-server; build it before the next release).

Decisions

  • ADR-0149 (rename, what is kept, env shim), ADR-0150 (the viewer).
  • Restore-into-chunks has no Rust twin (reason above).
  • Screenshots are not committed (shots/ ignored); the README does not embed them yet.

Still not built

  • Lazy version_history loading in the app; concurrent polls for listening nodes; certificate and hub transport key rotation; a Nextcloud rotation adapter; the 24.4 container benchmark (needs a 2 vCPU host); a hardware or passkey approver; Grafana token expiry in the probe. Each needs either a decision or a real service; none is a code gap that tests can close.
  • A4, A9, A10 smoke tests: the viewer now makes them possible (Xvfb has X11 only; Wayland is still not covered, and Caps Lock and the input method need xdotool and an IM inside the image).

Next steps

  1. Run tools/viewer/run.sh after the next visual change and read the PNGs.
  2. Fix UI findings 1 and 2; decide on 4.
  3. Add xdotool to the viewer image and script the Caps Lock hint (A4) and settings drag (A10).
  4. The maintainer's list: enable Pages, add CATALOGUE_SIGNING_KEY, enable Dependabot alerts and code scanning, run the pentest checklist against a real deployment, and decide whether to drop the ENVV_* shim.

Notes for whoever picks this up

  • The Rust crates, directories and Cargo package names still say envv. That is deliberate (ADR-0149); renaming a crate changes nothing a user sees.
  • unv's describe output and the capability map use the new binary name; tests that spawn the CLI use env!("CARGO_BIN_EXE_unv") through the existing helpers.
  • Commits: ~/Code/envvault-commit-phase34.ps1 .. 42.ps1, one commit per file, no trailers. Run foreach ($n in 34..42) { pwsh -File "$HOME/Code/envvault-commit-phase$n.ps1" }.