Skip to content

fix(env): stop shipping the development profile - #666

Open
Alex Shabalin (alex-sparus) wants to merge 1 commit into
mainfrom
alex/fix-dev-profile-on-prod
Open

fix(env): stop shipping the development profile#666
Alex Shabalin (alex-sparus) wants to merge 1 commit into
mainfrom
alex/fix-dev-profile-on-prod

Conversation

@alex-sparus

@alex-sparus Alex Shabalin (alex-sparus) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Every shipped release ran its frontend on env.development.json, with
VITE_NIXMAC_SKIP_PERMISSIONS on and the permissions gate disabled. The Rust
side was unaffected, so backend logs stayed honest while the UI acted as if
permissions were fine — which is why this survived so long. Confirmed against a
CI production bundle.

Each problem below is paired with what replaces it.

The bundled profile

Problem. __NIXMAC_PROFILE_JSON__ was defined as JSON.stringify(merged),
an object. A Vite define is raw text substitution, so the bundle got an object
literal where a string literal was meant.

Solution. One object-valued define, validated and coerced at build time.
No JSON round trip, so there is no parse for anything to fail.

Problem. JSON.parse threw on that object and a catch silently loaded the
development profile. The failure had no symptom.

Solution. No fallback. A bad profile fails the build, or throws at startup.
Substituting a different profile is exactly how this shipped.

The selector

Problem. NIXMAC_ENV was both the file selector and an overridable profile
key, so NIXMAC_ENV=prod overwrote env.release.json's own "production". The
two disagreed on every build.

Solution. The selector no longer writes the key. One vocabulary —
development, production, e2e — names both the accepted selector values and
each profile's own NIXMAC_ENV.

Problem. An unrecognised selector fell through to the development profile, so
a typo produced a build that looked like a release and carried development
settings.

Solution. Unknown selectors fail the build, in nixmac-profile.ts and
build.rs alike.

Problem. Nothing checked that the selected file names the environment that
selected it. A profile mislabelled development would pass every existing check.

Solution. Both selectors refuse a file whose own NIXMAC_ENV is not the
value that picked it.

The gates

Problem. A profile could switch off gates that exist for real users, and the
mislabelling above would have been enough to permit it.

Solution. A production profile refuses the skip-permissions and
nix-installed bypasses whatever the profile asks for.

Problem. With the gate live, computeCurrentStep returns permissions or
nix-setup for a completed user whose prerequisite is missing — steps the widget
has no case for, which it reports as an internal error.

Solution. Both branches are gated on onboarding being unfinished. After
setup, a missing prerequisite is a repair banner, which is what the wizard's
completion latch already implies.

Problem. check_full_disk_access returned Pending when none of its probe
paths existed, which establishes nothing. That row is required, so users who may
well have granted FDA would see a blocking banner.

Solution. An inconclusive probe returns Unknown, drops the required flag
for that row only, and explains itself in the row's instructions.

The Rust side

Problem. The deployment environment resolved through three paths, and the
startup log resolved the version differently from telemetry, so the two could
disagree.

Solution. One path. crate::env owns the precedence and both the log line
and telemetry read through it.

Behaviour changes to expect

  • Telemetry environment. The frontend reported development in every
    release; it now reports production. env.release.json also declares
    production rather than the old prod selector token, so the Rust-side value
    is unchanged and Sentry's existing environment series is continuous.
  • Telemetry release tag. Was unknown in every release (the fallback profile
    has no NIXMAC_VERSION); now the real version. Sentry/PostHog series will step
    at this release.
  • The react-query persisted cache invalidates once on upgrade — its buster is
    the version string, which finally changes.
  • The permissions gate becomes real. It has never run in a shipped build.
    Existing users may see a permissions banner for the first time.
image
  • NIXMAC_ENV=prod is no longer accepted; use production. All in-repo
    callers are updated. production was already accepted before this change, so
    any external caller using it keeps working.
  • The startup log's version now resolves process env before the build embed,
    matching what telemetry already did. A release bundle launched from inside the
    devenv shell logs local-$(whoami) rather than the embedded version.

Test Plan

  • Manual test
  • bunx tsc --noEmit and cargo check clean
  • bun run test:unit — 52 files, 351 tests; cargo test — 843 passed
  • bun run gen:schemas produces no diff
  • New env.test.ts covers the bypass lockout per profile and asserts the
    baked profile round-trips; new utils.test.ts cases cover both prerequisite
    branches completed and not
  • Both build-time agreement checks proven to fire: TypeScript against
    temp-dir fixtures, build.rs by doctoring a profile file (mislabelled,
    wrong type, and absent key each produce a distinct message)
  • Walk one fresh onboarding in a packaged build — the permissions gate has
    never run in a shipped build

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

@darkmatter

darkmatter Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for a105991


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Flows/Evolve › Playground

Flows/Evolve › Playground

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › Evolving With Error Event

Flows/Evolve › Evolving With Error Event

Flows/Evolve › 3. Review (changes generated)

Flows/Evolve › 3. Review (changes generated)


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

Messages
📖

🤖 This PR touches CI / infra — reviewers please pay extra attention:

  • .github/workflows/build.yaml

📋 PR Overview

Lines changed 636 (+459 / -177)
Files 2 added, 20 modified, 0 deleted
Draft / WIP no
Has Test Plan yes
Linear issue no
No Test Plan Needed no
New UI components no
New Storybook stories no
New Rust modules no
New TS source files yes (1)
New tests yes (1)
package.json touched no
Cargo.toml touched no
Infra / CI touched yes

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 35.6% 35.2% 30.5% 29.5%

Generated by 🚫 dangerJS against a105991

@prelint

prelint Bot commented Aug 19, 2026

Copy link
Copy Markdown

Ship with changes Permissions gate now active in production builds

Product decisions in this change

Agree with concerns 1. All existing users see the permissions banner for the first time when they launch the next production build, with no prior notice.

The fix is necessary and correct. Every prior release shipped with the gate disabled, so the banner is new to every existing user. The PR description shows awareness of this with a screenshot but marks "No docs update needed" and includes no in-app notice, release note, or changelog entry. Users who have not granted the required permissions will be blocked at launch with no context about why the behavior changed. The concern is not about the banner itself. It is about surprise. A one-sentence release note or an in-app "what changed" line would remove the surprise without slowing the fix.

Agree with concerns 2. An unrecognized `NIXMAC_ENV` value stops the build rather than silently selecting the development profile.

The silent fallback to the development profile is the root cause of the bug this PR fixes. Removing that fallback is correct. Every caller inside this repository is updated from prod and local to production and development. The concern from the prior review remains: the PR has no comment, companion change, or migration notice covering callers outside this repository. Any external CI pipeline, partner tooling, or published script that passes NIXMAC_ENV=prod, NIXMAC_ENV=dev, or NIXMAC_ENV=local now gets a hard build failure with no prior warning. If nixmac has no public callers or documented external consumers, this concern is low risk. If it does, a migration note in the changelog would close it.

Agree 3. After onboarding completes, a missing prerequisite (permissions not granted, Nix not installed) shows a repair banner rather than routing back into the wizard.

This matches the documented design decision that the onboarding wizard must never reappear after completion. Banners are the intended post-onboarding repair path, as stated in the permission revocation testing docs. The implementation uses onboardingCompletedAt as a timestamp rather than a boolean flag, which gives the system a precise completion point and is useful for future debugging. The tests cover both the in-onboarding and post-onboarding states and verify the routing boundary.

Agree 4. A production build refuses permission-gate and nix-installed bypasses regardless of what the profile file contains.

This is defense in depth. The primary protection is that the selectors in nixmac-profile.ts and build.rs now refuse any value they do not recognize, so a development profile should never reach a production build. Locking the bypasses out at the settings layer is a second check that costs nothing and protects against a future profile-selection regression.

Agree 5. The `NIXMAC_ENV` selector cannot be overridden by process environment after the profile file is chosen.

The selector picks a file. The file owns its own NIXMAC_ENV value. Allowing process env to overwrite the file's value is exactly what let the selector and the selected file drift apart. Locking NIXMAC_ENV out of the post-selection merge directly closes that mechanism.

Agree with concerns 6. When the Full Disk Access probe cannot reach a verdict, the FDA row becomes advisory (non-blocking) and shows an explanation, rather than showing a blocking "not yet granted" status.

The check_all_permissions path now sets required = false when the FDA probe returns Unknown, and the row shows a plain-language explanation. This is the correct behavior for users with unusual Mac setups who may well have granted FDA but have no probe files. The concern is in the request_permission path: when a user clicks "grant FDA," that code path constructs a Permission object with required: true hardcoded, regardless of whether the re-probe came back Unknown. If the UI merges that returned object into the displayed state, an advisory row silently becomes blocking the moment the user tries to act on it.

Disagree 7. When a user clicks "grant Full Disk Access," the action always returns `required: true` in its response, even when the re-probe comes back inconclusive.

The request_permission response for full-disk hardcodes required: true regardless of the probe outcome. A user on an unusual Mac (no Safari bookmarks, no Mail directory) sees an advisory FDA row because no probe file exists. If that user clicks "grant" (the reasonable thing to do), the action opens System Settings, re-probes, still finds nothing, and returns a Permission object with required: true and status Unknown. Whether the UI merges that object back into the permissions list remains unanswered (prior question 3 from the earlier review). If it does, the row flips from advisory to blocking as a direct consequence of the user trying to help. The fix is to apply the same required = false logic when the re-probe returns Unknown inside request_permission.

Option What it gives users What it costs Effort to change later
Hardcoded required: true (current) Consistent "FDA is required" framing after clicking grant Advisory row may become blocking when the user acts on it Low — one field in one match arm
Mirror check_all_permissions logic Advisory row stays advisory after the grant action No visible state change after clicking grant, which is correct but may feel unresponsive Low

Since the last review

  • Still open: Callers outside the repository that pass old env names (prod, dev, local) now get a hard build failure with no prior warning. (No PR comment, companion change, migration guide, or changelog entry covers external consumers of the old env name set. Only callers inside this repository are updated.)
  • Addressed in code: FDA inconclusive probe returned Pending (implying not granted) when it could not determine the actual state. (check_full_disk_access now returns (PermissionStatus::Unknown, Some(explanation)) when inconclusive, and check_all_permissions sets perm.required = false for the full-disk arm when status is Unknown.)
  • Still open: The request_permission action for FDA returns required: true regardless of probe outcome, which could flip an advisory row to blocking when the user clicks "grant." (request_permission in permissions.rs still constructs Permission { required: true, ... } with a hardcoded value even when check_full_disk_access() returns Unknown.)
  • Still open: External repositories, partner CI, or published documentation may use NIXMAC_ENV=prod, dev, or local and now get a hard build failure. (No PR comment, companion change, or changelog entry addresses external consumers of the old env name set.)
  • Still open: No plan exists to tell existing users that they will see the permissions banner for the first time on their next launch. (PR description shows a screenshot of the banner but checks "No docs update needed" and includes no in-app notice, release note, or changelog entry for existing users.)

Open questions

  • Does the UI merge the Permission object returned by request_permission back into the displayed permissions list? If so, a user whose FDA row is advisory (Unknown, non-blocking) sees it flip to blocking after clicking "grant," because the returned object hardcodes required: true.

  • Do external repositories, partner CI pipelines, or published documentation set NIXMAC_ENV to prod, dev, local, or any value other than development, production, or e2e? Any such caller now gets a hard build failure with no prior warning.

  • What is the plan to tell existing users that the permissions banner will appear for the first time on their next launch? Every prior production build shipped with the gate disabled, so this is a new experience for the entire user base.

Recommendation

Ship with changes
The core fix is correct and important: stopping the development profile from reaching production builds closes the bug that silently disabled the permissions gate in every prior release. Two issues need resolution before shipping. First, request_permission for FDA still hardcodes required: true in its response even when the probe is inconclusive, which can silently promote an advisory row to blocking the moment the user tries to act on it. Second, no user communication plan exists for a banner that every existing production user will see for the first time on their next launch.

@prelint prelint Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

request_permission("full-disk") always returns required: true, but this PR makes check_all_permissions dynamically set required: false when FDA status is Unknown (inconclusive probe).

apps/native/src-tauri/src/system/permissions.rs:553

1 finding(s) posted as inline comments.

Comment thread apps/native/src-tauri/src/system/permissions.rs
@alex-sparus
Alex Shabalin (alex-sparus) changed the base branch from main to alex/run-autoformat August 19, 2026 15:38
@alex-sparus

Alex Shabalin (alex-sparus) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by jj-ryu.

@prelint prelint Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apps/native/src-tauri/src/system/permissions.rs (line 550) [behavior_change]: request_permission("full-disk") still hardcodes required: true unconditionally, while check_all_permissions now applies if status == PermissionStatus::Unknown { perm.required = false } (lines 378–382). When a user clicks the "request" button in an inconclusive FDA state, the Settings → Permissions UI receives a Permission with required: true, which can re-trigger the amber permissions banner the PR intends to suppress. The fix is to mirror the same guard in the "full-disk" arm of request_permission: compute required: status != PermissionStatus::Unknown instead of hardcoding true.

@alex-sparus
Alex Shabalin (alex-sparus) changed the base branch from alex/run-autoformat to main August 19, 2026 15:48
@alex-sparus
Alex Shabalin (alex-sparus) changed the base branch from main to alex/run-autoformat August 19, 2026 15:48
@prelint

prelint Bot commented Aug 19, 2026

Copy link
Copy Markdown

Prelint Review Summary

Active findings (1)

Warning

request_permission("full-disk") always returns required: true, but this PR makes check_all_permissions dynamically set required: false when FDA status is Unknown (inconclusive probe).

apps/native/src-tauri/src/system/permissions.rs:553

1 active, 0 resolved

Base automatically changed from alex/run-autoformat to main August 21, 2026 06:41
@prelint

prelint Bot commented Aug 21, 2026

Copy link
Copy Markdown

Prelint reached the review limit for this pull request (5 reviews of each kind).

New pushes do not start a review.

  • To review the latest push, comment @prelint. A tag skips the limit check, and its review still spends the quota.
  • To change the limit, open review configuration.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just suggestions about consolidating some distributed but similar/repetitive inline comments.

Comment thread apps/native/src-tauri/src/env/config.rs Outdated
)]
pub sentry_dsn: String,

/// No `build_embed`: `NIXMAC_ENV` selects which profile to embed, so

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is maybe one of those comments that got added as part of the PR but once the PR is merged isn't really important to future maintenance, in which case I suggest removing (most of) it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Shortened to briefly explain why there's no build_embed

}

/// JSON profile from `apps/native/env.{development,release,e2e}.json`, embedded at compile time.
///

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as previous comment about comment.

None => {
return (
PermissionStatus::Unknown,
Some("Could not determine Full Disk Access: this Mac reported no home directory, so the check could not run. If a rebuild fails with a permissions error, add nixmac under System Settings → Privacy & Security → Full Disk Access.".to_string()),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you have FULL_DISK_ACCESS_INCONCLUSIVE as a const above suggest this message string should also be a const for symmetry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also factored out the common parts between those messages

use std::process::Command;

/// Embed `apps/native/env.{development,release,e2e}.json` selected by `NIXMAC_ENV`.
///

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another comment that can probably be cut down to just what's needed later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've adjusted the comments to focus on the future maintenance

.unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
let value: serde_json::Value = serde_json::from_str(&json)
.unwrap_or_else(|error| panic!("cannot parse {}: {error}", path.display()));
// The selector picks the file; the file names the same environment in its own

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After seeing related comments in several places I wonder if maybe they should just all be consolidated to a single location in README or another md file as design documentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more afraid of those docs becoming stale

@alex-sparus

Copy link
Copy Markdown
Contributor Author

Review comments addressed and also added an extra CI check that the production profile was actually selected. Not sure if you'd like to re-review, Scott McMaster (@scottmcmaster)

A Vite define is text substitution, so passing an object emitted an
object literal where a string was expected; JSON.parse threw and the
catch silently loaded env.development.json. Every release therefore
ran its frontend with VITE_NIXMAC_SKIP_PERMISSIONS on, disabling the
permissions gate. Confirmed against a CI production bundle.

- one object-valued define, validated and coerced at build time, and
  no fallback: a bad profile fails the build, or throws at startup
- NIXMAC_ENV only selects a profile and no longer overwrites the
  selected file's own value; unknown selectors fail the build instead
  of falling through to development
- a prod profile refuses the skip-permissions and nix-installed
  bypasses whatever the profile asks for
- Rust resolves the environment through one path instead of three
- prerequisite steps are gated on onboarding being unfinished, so a
  missing permission after setup is a banner, not an internal error
- an inconclusive Full Disk Access probe no longer counts against the
  required-permission gate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants