UnENVerse 2026-10-08
Bulk uid register in the pane; --fix in the app's doctor; strict-write on the CLI against a remote; the archive/WAL caveat above.
Phases 33.1b to 33.7: the remaining parity gaps (2026-10-08)
- Bulk
uid registerin the pane;--fixin the app's doctor; strict-write on the CLI against a remote; the archive/WAL caveat above.
Phase 32.3 and Phase 33 (UI ⇄ CLI parity, mechanical half) (2026-10-08)
Phase 32.2: the last controls (2026-10-08)
tests/probe.ts:probeKindsaccepts an asyncresetand anafterResethook (lets a reset click "Run Scan" and then clear the toast it raised).tests/efficacy-surfaces.test.ts+efficacy-surfaces-silent.json(empty): surfaceshealth(vault with a weak entry and an empty bundle; click#health-scan-btn; probe#health-results [data-action]),settings(sidebar sections minustags, so "+ Show" exists;#settings-overlay [data-action]),icons(openIconPicker();#icon-picker-overlay [data-action]),feeds(aRemoteVaultStoreinstance withtests/efficacy-settings.test.ts+efficacy-settings-silent.json: for each of the 19 inputs/selects in the Settings overlay plus 5 segmented/theme groups, change it, press Save, and requireSettings.getAll()to differ. Onlysettings-backup-pwis silent (an action's password field), allowed with that reason.tests/efficacy-cards.test.ts: selector now includes#bundle-suggest [data-action]; reset clearsdismissedBundleSuggestions(a Dismiss probe persisted into the next probe).tests/efficacy-config.test.ts:extras(type)adds anenv_filechunk (docker), a docker service with a${Alpha}env field, a wg_peer with a vault-reference field, an nginx_key + nginx_server, and a certificate entry forexample.com.tests/honest-controls.test.ts+7 tests: banner Accept creates a bundle, banner Dismiss stores'twin'(lower-cased), a${ref}badge from a config view shows the entry, a health-scan finding for a filtered-out entry shows it, unlock and re-lock screens refuse an empty password visibly, Enter on therole=buttoncard icon opens the icon picker.
Phase 32.1: efficacy audit of the dynamic controls (2026-10-08)
tests/probe.ts(new, shared):snapshot()(body HTML with inline style reduced to display/visibility, plus every input/textarea/select value/checked/disabled),clearTransient()(toast, every.open, and now every.tool-statusline, so two probes ending in the same status text cannot hide each other),hadEffect(act, waitMs)(MutationObserver records OR snapshot diff), `probeKinds({reset, selector, sample,tests/efficacy.test.ts: now uses the shared helpers (behaviour identical; 145 buttons, 143 clicked).tests/efficacy-cards.test.ts+efficacy-cards-silent.json(empty): one entry per registered secret type (fromregistry()), plus TOTP seed, 2-key pool, 4-member bundle, same-provider twins, long descriptions;allExpanded; probes#card-grid [data-action]. 582 controls, 34 kinds, 0 silent, partial:bundle-order(first member cannot move up),bulk-toggle,bundle-slot-tab(active tab).tests/efficacy-config.test.ts+efficacy-config-silent.json(empty): one project per type using the real starter factories (wireguard, docker, nginx, kubernetes, ssh_config, traefik, apache, haproxy, ansible, postgres), selected viast.currentSelectedProjectIdsandrender(). 346 controls, 122 kinds (type-prefixed), 0 silent after the fix below.tests/efficacy-closers.test.ts: every allow-listed "Closes a dialog" id is pressed with its dialog open (.openon the nearest[role=dialog]/.modal-overlay) and must close it;settings-cancelneedsopenSettings()because its handler is assigned there (invariant 9), so anopenersmap exists for such cases. 19 tests.tests/honest-controls.test.ts: +4 tests (chunk move refused with a toast, chunk move among peers works, pool card with a bundle present, icon apply with empty name).- 15
data-actionkinds unreached; listed in the 32.2 row with what each needs. <select>/checkbox/toggle controls not probed: most apply on Save. 32.2(b).- The probe waits 40-60 ms; a control whose effect lands later reads as silent.
- State leaks between probes through module state not reset by
resetState. Add to the reset rather than reordering. - Pre-existing uncommitted changes in the tree (including
tests/fixtures/*,index.html) are not from this step.
Phase 32: "Does it actually do anything?" (2026-10-08)
tests/efficacy.test.ts(new): boots the real shell exactly liketests/vault-shell.test.ts(realindex.html,mountToolsPanes(),import('../src/ts/vault'), keep the bound nodes, re-append per probe), seeds a 2-entry vault, and for each of the 145button[id](143 clicked; 2 disabled) resets the toast and every.opendialog, clicks, waits 60 ms, and records "silent" when neither aMutationObserverrecortests/efficacy-silent.json(new): the allow-list, 40 entries, each with a reason (closers with no dialog open, controls hidden until a precondition, handlers bound only when a lock screen is shown, empty-input converters, native file picker).tests/honest-controls.test.ts(new, 26 tests): 12 Copy buttons x (empty -> "Nothing to copy yet" err toast; full -> "Copied ✓" ok toast), bulk delete with nothing ticked, import confirm with nothing staged.src/ts/tools.ts:copyOrExplain(v)replaces 11if (v) void clipboardWrite(v);sites and the fmt-copy one. Bulk delete and import confirm toast when their precondition is unmet.src/ts/modals.ts: cookie split with no parseable cookies and the session-preset header copy with no preset now toast.- 32.1 (new row in AGENTS.md) holds the rest:
[data-action]card controls, menu items, selects/toggles,role="button"divs, config-view handlers, closers with their dialog open, theaudit-verifyprobe artefact. - The probe waits a fixed 60 ms per click; a control whose effect lands later (a slow IPC) is falsely silent. The 17 s runtime is mostly that wait.
- Probes leak state across each other (entries deleted, settings changed) since only the vault and a few
stfields are reset; order effects like the audit one are possible. Add to the reset rather than reordering. - Vitest here swallows
console.log; debug by writing a file. - Pre-existing uncommitted work in the tree (many files in
git status) is not from this session; stage explicitly.
Phase 31: the provider catalogue (2026-10-08)
vault-core/src/catalogue.rs(new, owns all rules):Provider,Catalogue, wire form{payload: <JSON string>, signature: <b64>}so the signature covers exact bytes.verify(Ed25519 against a key arg),sign(seed),store(verify, refuse rollback bygenerated_at, atomic write 0600),load_cached(re-verifies every call),cache_path(ENVV_CATALOGUE_FILEoverride, else `dirs::data_dir()/io.envvault/cavault-core/Cargo.toml:ed25519-dalek = "2"(already in Cargo.lock via ssh-key 0.6.7, no new crate downloaded; Cargo.lock got the dependency edge).vault-core/src/lib.rs:pub mod catalogue;.envv-cli/src/catalogue_cmd.rs(new):CatalogueCmd=update [--url|--file],show,diff,export(compiled table as providers JSON),sign <in> --key-file -o. https only,build_public_client(the one allowed CA-validated client, per the guard test in tests/agent.rs), 4 MiB cap. Public reference data, so stdout is allowed (unlike emit).envv-cli/src/main.rs:Commands::Catalogue, handled beforerun()likeDescribeso it never asks for a password; added to the no-op arm indispatch.envv-cli/src/lib.rs,envv-cli/Cargo.toml(hex = "=0.4.3", same pin as vault-core).envv-cli/src/enrich.rs: new ownedHit,catalogue()(OnceLock, loaded once per process),lookup()= longest matching prefix across cached catalogue and compiledSIGNATURES, tie to the catalogue;plan_entryuses it;bundled_providers()public for export/diff.AXIS_SIGNATURES(acts_as/exposure) is still compiled-only and matched by prefix equality, so a catalogue-only prefix gets type/icon/url/env/tagsrc-tauri/src/lib.rs:catalogue_status,catalogue_update(url)(https only, 20 s, 4 MiB, thencatalogue::store), registered ingenerate_handler!.catalogue/providers.json(new, 331 lines): seed source, produced byenvv catalogue exportfrom the compiled table (NOT retyped). This is the file maintainers edit..github/workflows/docs.yml: daily cron + a "Sign the provider catalogue" step writingsite/catalogue/catalogue.json; skips cleanly when theCATALOGUE_SIGNING_KEYsecret is absent.- Default URL
https://darthdemono.github.io/EnvVault/catalogue/catalogue.jsonis unverified against a live deployment (Pages enablement is still manual, Phase 24). - No UI: the IPC commands exist but no button or Settings row calls them (invariant 10 gap, record as such).
tests/ipc-contract.test.tsonly checks frontend->registered, so it does not notice. - Catalogue
docs_url/rotate_url/revoke_urlare carried and validated but nothing consumes them yet; axes (acts_as/exposure) are compiled-only. - The catalogue content is only the 48 compiled prefixes; "a large set of providers" needs real curation, with sources, outside this change.
Phase 30: storage, row-per-entry, schema v2
- A delta API from clients (the remaining O(vault) CPU), chunk-level rows, history-per-revision rows, and indexes beyond the audit ones: nothing queries by them yet.
load_entries_whereexists instorage.rsand has no caller. - No visual check of the conflict dialog text in a real window.
time-based pruning ofvault_saves: retention is "newest 2000 saves", which a vault saved thousands of times a day (a polling script) would burn through in hours, making an hour-old token a whole-vault conflict. If that bites, raiseKEEP_SAVES(it is one constant) or prune by age.- The remote store over the pinned-HTTPS proxy surfaces no headers, so it still has no ETag there and writes unconditionally, as before.
Phase 29: the config compiler
- Six designed checks, eight rule ids (two checks split into an error case and a softer one). All fire only on positive evidence; all skip
${ref}, disabled chunks and anything that could be satisfied somewhere this function cannot see.
| # | id | severity | fires when | silent when | | 1 | nginx-proxy-pass-unknown-service | warning | proxy_pass host is a bare name that is neither a docker service/container_name nor an nginx_upstream | the project has no docker_service chunk; host is dotted, an IP, localhost, a $variable, a unix socket or a ${ref} | | 2a | wireguard-allowed-ips-duplicate | error | two peers claim the same masked network | one peer repeating itself; ${ref} values; disabled peers | | 2b | wireguard-allowed-ips-overlap | warning | one peer's network strictly contains another peer's | a /0 default route; different families; disjoint networks | | 3 | traefik-middleware-missing | warning | a router middlewares item matches no traefik_middleware name (case-insensitive) | item contains @ (another provider) | | 4a | k8s-deployment-secret-missing | error | a Deployment's secretEnv/secretMounts names a Secret no k8s_secret chunk creates in that namespace | the project has no k8s_secret chunk; ${ref} names; a mount with no path | | 4b | k8s-ingress-service-missing | warning | Ingress serviceName (or its own name) has no k8s_service of that name in that namespace | the project has no k8s_service chunk | | 5 | compose-env-ref-unresolved | warning | a ${NAME} anywhere in a service value (or a ref_name) is neither an env_file key nor a vault entry name | ${NAME:-d}, -, :?, + operator forms; ${NAME}; chunk:/bundle: refs | | 6 | pg-host-network-unreachable | warning | pg_connection.host is a docker service S and a consumer C shares no network with it; consumer = lists S in depends_on, spells S in an env value, or reads ${chunk:<pg>/…} | no consumer; either side has network_mode; both on the implicit default network |
- No ui-lab screen for the panel: the lab seeds a vault and does not run IPC, and the panel only exists under Tauri. The panel's markup and escaping are unit-tested; its visual layout in a real window is unchecked (WebKitGTK window tests remain a human task).
- "A gate a node runs before
apply" from the design: Nodes (Phase 34) do not exist yet.
Phase 28: escape by construction
- Exporter functions and other string-building code that is not markup were not touched (by design).
esc/escAttrstill exist and are exported;render.tsandtools.tsno longer import them.- The
innerHTML = ''sites becamesetHtml(x, ''), notreplaceChildren(); either is fine. - Static markup in
index.htmlandtools-markup.tsis not templated (it has no interpolation). - A real WebKitGTK window was not run (A4/A9/A10 remain human tasks).
Phase 27, UI-lab triage (layout backlog 1,155 → 0)
| Check | Result |
|---|---|
npx vitest run | 58 files, 1,257 tests pass (82 s) |
npm run typecheck | clean |
npx eslint ui-lab | clean |
npx prettier --check ui-lab src/css | clean (after prettier --write src/css/a11y.css: I had written [role='button'], prettier wants double quotes) |
- (If the count changes, read
SCREENSxVIEWPORTSinui-lab/layout.spec.tsplus the single tests inlayout.spec.tsandbehaviour.spec.ts.) - This is the part that went wrong first and then right. Read the Dead ends section
- before changing the rule again.
- What the 19 actually were (
detailstrings fromlayout-findings.json): #add-btn,#copy-all-btn,#copy-all-arrow"overlaps#settings-close/button.settings-tab" on settings-* screens: header buttons sitting *under* the
Phase 26, outward redaction engine
- Phase 26 is complete and uncommitted.
envv-cli/src/shield.rsbuilds one Aho-Corasick matcher from every vault value- the fail-closed CLI redactor treats as private. It uses the existing
sha256:<12 hex>fingerprints for replacements and reports.envv shield -- <command>runs a child with piped stdout and stderr, shields- both streams independently, and returns the child's exit code. It keeps the
- final
max_secret_len - 1bytes between reads, so a secret that crosses a - pipe read boundary cannot leak a prefix.
- The wrapper refuses NUL-containing or invalid-UTF-8 output rather than
- half-masking binary data. It drains the rest of that pipe before failing so a
- child cannot block on a full pipe.
--jsonis refused because the wrapper - deliberately forwards the child's ordinary output.
Phase 25, route coverage and permission-expression parity
- Phase 25 is complete, but remains uncommitted with the rest of the Phase 24
- worktree.
envv-server/src/lib.rsnow exercises the actualbuild_routerpath across- and revocation.
tests/fixtures/parity/permex.jsonis the shared truth table for permission- expressions.
vault-core/src/permex.rsandtests/permex.test.tsboth read - it, so a semantic disagreement fails either suite.
- The TypeScript evaluator gained the core's composition primitives
- (
combine,anyOf,requireAll). The permission editor preview now shows - The UI had fields for those values but could not receive them, so its former
- preview could not model the server's effective access rule.