Skip to content

fix(linux,wsl,mcp,envfile): window controls, WSL path bridge, MCP param naming, and .env write gating - #19

Merged
maosuarez merged 19 commits into
mainfrom
batch/tier2-independent-fixes
Aug 6, 2026
Merged

fix(linux,wsl,mcp,envfile): window controls, WSL path bridge, MCP param naming, and .env write gating#19
maosuarez merged 19 commits into
mainfrom
batch/tier2-independent-fixes

Conversation

@maosuarez

Copy link
Copy Markdown
Owner

Summary

Five independent fixes with no cross-dependencies. Stacked on #11/#12 (PR #18) only because they share the test harness.

Closes #5
Closes #8
Closes #10
Closes #3
Refs #6

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🔒 Security improvement
  • 📖 Documentation

What Changed

#5 — window controls now render on Linux as well as Windows; platform decision moved into a pure chromeFor(os) and out of a useEffect that left the first paint control-less. Adds focus-visible outlines.

#3 (Phase 1) — new wsl module (wsl_list_distros, wsl_distro_home) and a "WSL" button beside the browse control, so Windows users with projects inside WSL2 pick a distro instead of typing \\wsl.localhost\.... Also lands the data-loss gate its own plan made non-negotiable: inject_environment's read now distinguishes NotFound (start empty) from every other error kind (abort), instead of treating any read failure as "file is empty" and overwriting. Phase 2 (running the CLI inside WSL) is explicitly not committed — three blocking design questions, one with no design at all. Worth a separate issue if you want it tracked.

#10crypt_env_inject_environment and crypt_env_generate_example_env were the only two MCP tools naming the environment identifier id. An LLM inferring the name from the other 13 tools would pass environment_id, have it silently ignored, and — when project+environment were also present — inject a different environment's decrypted variables with no error. Both now advertise environment_id. Bare id stays as an unadvertised deprecated alias for 1.0.x (removed in 1.1.0); calls using it succeed but the response appends a deprecation notice. Tests assert no environment-scoped tool declares a bare id.

#8 — new envfile module. Writes to a caller-supplied output_path/output_dir refuse to clobber a file crypt-env didn't create (no # crypt-env: marker) unless overwrite: true, in which case the prior contents are preserved in <path>.bak. The gate runs strictly before any decryption. Owner-configured environment.paths[] are grandfathered — reported in unmanagedPaths, never hard-gated — so existing installs don't break on upgrade. Adds environment_inject_preview for a pre-inject confirm dialog.

#6 — README "known issue" note only. Root cause is unconfirmed and the plan's Phase A diagnosis requires a native X11/Wayland session (WSLg is not a valid repro environment), so this does not close #6.

Behavior change worth calling out

Under #8, injecting into a pre-existing configured .env that lacks the marker no longer merges into it — the file is backed up to .bak and rewritten from the environment's keys, then self-heals (the marker makes every subsequent inject a normal merge). Unrelated keys survive in the backup, not in the live file. This supersedes the old merge contract and its test.

Testing

Security Implications

#8 closes a silent-truncation path: /fill, /inject, and /example previously wrote to output_path with a plain std::fs::write and no existence check, so pointing one at an unrelated file truncated it. Decryption now happens only past the gate, so a refused write never decrypts. .bak files are created 0600 on Unix. The marker contains project and environment names but never secret material.

maosuarez and others added 18 commits August 4, 2026 17:11
Phase A diagnosis from docs/plans/issue-6-oversized-cursor-linux.md could
not reach a verdict: this environment is WSL, and CLAUDE.local.md
explicitly documents WSLg as unsupported for reproducing/verifying this
bug (needs a real X11/Wayland desktop session). Static analysis ruled out
the app-side candidates (no cursor: CSS rules, no zoom/transform/
image-rendering hacks, no set_zoom call, no custom cursor assets, no
GTK/XCURSOR env vars set in lib.rs), but D1/D2/D3/D4/D6 all require a
live compositor and remain unrun.

No Phase B code change is warranted without a confirmed H3/H4/H5 verdict.
Posted findings + needs-linux-verifier label on issue #6 instead of
guessing a cause.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend the custom titlebar controls to Linux (issue #5), matching the
Windows gate. Replaces the isWindows boolean + useEffect with a
platform-derived Chrome config computed via a lazy useState initializer
(platform() is synchronous in Tauri v2, so the effect + one-frame
no-controls flash is unnecessary). macOS behaviour is unchanged by
design (see plan §4.3) and maximize stays out of scope (§4.4).

Also adds focus-visible outline styling to the minimize/close buttons
so keyboard focus is visible (§4.5) — previously only hover: classes
existed.

No Rust changes: existing capabilities (allow-minimize, allow-close,
allow-start-dragging) are not platform-scoped and already apply on
Linux.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…10)

crypt_env_inject_environment and crypt_env_generate_example_env were the
only two MCP tools naming the environment identifier `id`, diverging from
every other environment-scoped tool's `environment_id`. An LLM caller that
inferred the parameter name from the majority would pass `environment_id`
to these two, have it silently ignored, and — when project/environment
were also present — inject a different environment's full decrypted
variable set with no error.

Both schemas now advertise `environment_id` with the canonical wording
used everywhere else. `resolve_environment_id` accepts `id` as an
unadvertised, deprecated alias for the 1.0.x line only (removed in 1.1.0,
marked DEPRECATED in-source); using it still succeeds but appends a fixed
deprecation notice to the tool response. The ambiguity error in
crypt_env_inject_env_by_name now names environment_id too.

Adds an in-crate #[cfg(test)] mod tests (cargo test --bin crypt-env-mcp)
asserting every environment-scoped tool declares environment_id and not a
bare id, that the five item/category/workspace tools keep bare id, and
that the resolver prefers environment_id over the alias. Updates
docs/reference.md and adds a CHANGELOG 1.0.2 entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ect names

Issue #12: adds idx_environments_name_nocase (project_id, name COLLATE
NOCASE) and retrofits the same pre-check to idx_projects_name_nocase, so
an install that already holds a case-colliding pair (e.g. MyApp/myapp,
production/Production) is deterministically deduped (lowest id keeps its
name, losers get -2/-3 suffixes, audit persisted under settings key
env_name_dedup_v1) instead of bricking VaultDb::open.

Unique-constraint violations are now detected via sqlx's
is_unique_violation() and mapped to a stable "conflict:" sentinel string
instead of substring-matching sqlx's error text, closing a raw-SQL leak
in the 500 fallback path. project::resolve_environment now rejects
ambiguous case-insensitive project/environment matches instead of
silently taking the first one (SQLite NOCASE folds ASCII only, so
non-ASCII collisions like PRODUCCIÓN/producción survive the index and
need this Unicode-aware check) — the HTTP layer maps that to 409
AMBIGUOUS_SCOPE.

New src-tauri/tests/environment_naming.rs covers T1-T8/T11 from the
plan plus a projects-side dedup case; sqlx added to [dev-dependencies]
to seed pre-collision state that's otherwise unreachable through the
public API once the index exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r (issue #3, Phase 1)

Adds a new `wsl` module (`wsl_list_distros`, `wsl_distro_home`) so Windows
users with projects living inside a WSL2 distro can attach a project's .env
by picking a distro instead of typing `\\wsl.localhost\...` UNC paths by
hand. `project_pick_env_path` gains an optional `start_dir` to seed the
native dialog there; the environment editor gets a "WSL" button beside the
existing browse control, gated to Windows and non-empty distro lists.

Also lands the data-loss gate this feature's own plan flagged as
non-negotiable: `inject_environment`'s .env read now distinguishes
`ErrorKind::NotFound` (start from empty) from every other error kind
(abort with an error) instead of treating any read failure as "file is
empty" and silently overwriting it — the most likely real failure mode
once WSL/UNC paths are reachable from the picker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bering (#8)

Adds a shared envfile module (std-only leaf: inspect/commit/marker policy)
that every filesystem sink writing an env file now goes through before any
secret is decrypted. A resolved path that already exists and doesn't carry
the crypt-env marker is refused (409 TARGET_EXISTS) unless the caller opts
into `overwrite: true`, which first preserves the prior bytes in `<path>.bak`
(refusing again with 409 BACKUP_EXISTS if one is already there).

- POST /fill, POST /environments/:id/example, POST /environments/:id/inject
  all gain `overwrite` and surface `backup`/`unmanagedPaths` in responses.
- inject_environment restructures into resolve -> gate -> decrypt -> write,
  with the gate strictly before decryption; owner-configured
  environment.paths[] are grandfathered (reported, not blocked) per the
  upgrade-safety trade-off, while output_path/output_dir-derived paths are
  hard-gated.
- TempEnvFile now delegates to envfile::commit (fixes the write-then-chmod
  window) and, on the error path, truncates rather than removes a
  pre-existing file it didn't create.
- CLI `fill --force`, MCP `overwrite` param + 409 handling on the three
  affected tools, and a GUI confirm-before-overwrite dialog backed by a new
  environment_inject_preview command.
- Replaces the `read_to_string(..).unwrap_or_default()` at the old
  project/mod.rs:328 (which turned any read error, not just NotFound, into
  "treat as empty") with envfile::inspect's explicit NotFound/other-error
  split — this also happens to be the fix for the silent-overwrite failure
  mode on a stopped-WSL-distro UNC path that issue #3 flags as blocking.

Explicitly out of scope: path traversal/containment validation on
output_dir (issue #7's territory — see envfile/mod.rs's doc comment on the
boundary).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…overage baseline; drop stale Postman collection

Implements docs/plans/issue-11-test-coverage-and-postman.md: a crate::test_support
harness (TestVault fixture + router()/req() oneshot helpers) so every module's
private/pub(crate) logic is unit-testable without a Tauri runtime or live socket,
plus 119 new test cases across api/, project/, vault/, db/, share/, the CLI's
scope resolver, and the MCP binary's environment-picking logic (17 -> 136 total).

Two pure refactors enable the harness, both behavior-preserving:
- api::build_router / ApiState::new extracted from start_server (pub(crate),
  not pub — zero external API widening).
- vault::set_item_global's fork logic extracted the same way create_project_item
  already was, so the Tauri command becomes a thin delegating wrapper.
- crypt-env-mcp's resolve_environment_id split into network-fetching +
  pure pick_environment_id, mirroring the plan's resolve_environment_id split.

Deletes the stale Postman collection (asserted a removed /health field, every
request 422'd without the mandatory scope params) and replaces it with curl
examples + the executed api::tests::* suite in docs/reference.md. Adds
.github/workflows/test.yml running cargo test on push/PR with an informational
cargo-llvm-cov summary step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lity, sentinel visibility

Addresses adversarial review of the issue-12 case-insensitive-uniqueness
work:

- Critical: next_free_{project,environment}_name folded the rename
  candidate with Rust's to_lowercase() (full Unicode) but compared it
  against SQLite's own LOWER(name) (ASCII-only). For a non-ASCII base
  this made the two folds disagree, so a real collision against a
  pre-existing suffixed sibling (e.g. producciÓn-2) was missed, the
  "free" candidate was accepted, and the following UPDATE then hit the
  table's exact-match UNIQUE constraint -- init_schema returned Err and
  the vault could not be opened at all. Fixed by comparing via
  LOWER(name) = LOWER(?) so both sides are folded by SQLite consistently.
  Added a regression test reproducing the exact crash shape.

- env_name_dedup_v1 (the sole reversal path for an otherwise
  irreversible rename) is now persisted per-rename, inside the dedup
  loops themselves, instead of batched and written only after both
  CREATE UNIQUE INDEX statements succeed -- a renamed row can no longer
  land in the DB without also being recorded, even if a later migration
  step fails or the process is killed mid-run.

- PROJECT_NAME_CONFLICT bumped from private to pub (matching
  ENVIRONMENT_NAME_CONFLICT, also bumped from pub(crate) to pub): each
  src/bin/* target is its own crate separate from crypt_env_lib, so
  pub(crate) was invisible cross-crate. Exported
  project::AMBIGUOUS_MATCH_PREFIX so the ambiguity sentinel is a named
  constant instead of a bare string literal at every match site.

- persist_rename_report no longer silently discards prior rename
  history on a JSON parse failure -- logs to stderr and preserves the
  raw value verbatim under env_name_dedup_v1_corrupt before starting a
  fresh list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	src-tauri/src/bin/crypt-env-mcp.rs
# Conflicts:
#	docs/reference.md
#	src-tauri/src/bin/crypt-env-mcp.rs
#	src-tauri/src/lib.rs
#	src-tauri/src/project/mod.rs
The five branches in this batch add four integration targets under
src-tauri/tests/ (environment_naming, path_containment, project_relay,
vault_integration). `cargo test --lib --bins` builds none of them, so
they would have been invisible to CI while appearing to be covered.

Refs #11
…nt-fixes

# Conflicts:
#	docs/reference.md
#	src-tauri/src/bin/crypt-env-mcp.rs
#	src-tauri/src/project/mod.rs
@maosuarez
maosuarez changed the base branch from batch/tier1-harness-and-naming to main August 6, 2026 00:13
A regex-based bulk merge-conflict resolution (main -> tier2) swallowed
content between adjacent conflict hunks in two spots: the duplicate
`mod test_support` declaration in lib.rs, and the doc-comment +
pick_environment_id signature plus the entire append_scope_params/
is_safe_env_key/pick_environment_id test block in crypt-env-mcp.rs.
Restored both so the merge commit actually compiles and tests pass.
@maosuarez
maosuarez merged commit 97fa763 into main Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment