feat(mappings): add four-tier macOS-to-nix-darwin mapping layer - #21
Draft
wgordon17 wants to merge 29 commits into
Draft
feat(mappings): add four-tier macOS-to-nix-darwin mapping layer#21wgordon17 wants to merge 29 commits into
wgordon17 wants to merge 29 commits into
Conversation
Ghostty cannot build from source on Darwin via nixpkgs (no Swift 6/ xcodebuild support in the sandboxed builder). pkgs.ghostty-bin is the correct Darwin-buildable package.
…g registry into classifier Adds ENVIRONMENT_MAP (aliases/env_vars/path_components -> environment.* nix-darwin options) and expands classify_shell_setting() to emit ClassificationResults for ShellConfig.aliases/env_vars/path_components, routing frameworks/dynamic_commands to Tier 4 manual report. Wires the previously-unreachable TIMEZONE_NIX_OPTION into classify_system_setting() for the "timezone" field, and adds classify_app_config() to route app_config_registry lookups through the four-tier classifier.
…onfigInfo.config_paths HMModuleInfo and NixpkgsEquivalent were plain @DataClass while every other lookup-result type in this layer is frozen+slots; they are stored in module-level "constant" dicts and should not be mutable. Also changes AppConfigInfo.config_paths from list[str] to tuple[str, ...] since a mutable list inside a frozen dataclass could still be corrupted via in-place mutation. Additionally deep-copies the raw_plist snapshot in classify_launch_agent() (previously a shallow dict comprehension shared nested dict/list references with the LaunchAgentEntry the orchestrator still holds) and documents that Phase 6 generators must check metadata.get("skipped") before surfacing MANUAL_REPORT entries in a manual-steps report.
…duleInfo paths classify_shell_setting() had three near-identical if-blocks for aliases/env_vars/path_components that only differed by field name; collapsed into a loop over ENVIRONMENT_MAP's keys, which match the ShellConfig field names exactly. HMModuleInfo.config_paths/darwin_config_paths were still list[str] with mutable defaults despite the class being frozen+slots in the prior commit -- the same corruption risk AppConfigInfo.config_paths was just fixed for. Converted both to tuple[str, ...] for consistency and updated the 40+ registry entries and tests accordingly.
…fore classification classify_shell_setting() wrapped aliases, env_vars, and dynamic_commands into ClassificationResult.metadata with no sensitivity checking, unlike classify_preference()'s existing SENSITIVE_KEY_PATTERNS gate. Shell aliases and dynamic commands routinely embed credentials as inline text (e.g. AWS_SECRET_ACCESS_KEY=... in an alias body), and these values route to Tier NATIVE destinations that a Phase 6 generator will eventually write into a git-tracked, world-readable /nix/store module. - Generalize _is_sensitive_key() into _contains_sensitive_pattern() so the same substring check covers preference keys, alias/env-var names AND values, and full dynamic-command lines. - Add _redact_sensitive_dict_entries()/_redact_sensitive_list_entries() helpers that redact only the matching entry (not the whole field), and set metadata["potentially_sensitive_entries_redacted"] when any redaction occurs. - Iterate ENVIRONMENT_MAP directly in classify_shell_setting() instead of a separately-hardcoded field-name tuple, so the two can't drift. Also adds a regression test proving classify_launch_agent()'s raw_plist cleanup performs a true deep copy (nested dict/list values are not shared with entry.raw_plist) -- the existing test only used flat values, which can't distinguish a deep copy from a shallow one.
…e-key gaps, normalize whitespace - ephemeral_filter: exclude keys ending in a quantity suffix (size, limit, count, bytes, length, capacity, max, min, buffer, quota, threshold) from the numeric timestamp-range check, since byte counts/limits can coincide with the Unix/CFAbsoluteTime ranges (e.g. DiskCacheSize=1073741824) - scanners/_utils: add _PASSPHRASE and _BEARER to SENSITIVE_KEY_PATTERNS so values like DOCKER_..._PASSPHRASE=hunter2 get redacted - app_to_package: collapse any whitespace run (not just literal spaces) into a single hyphen in normalize_app_name, fixing tab/newline pass-through and double-space double-hyphen bugs
Deep-copies the shared DEFAULTS_TO_NIX conditions dict before attaching it to a classification result's metadata, since a shared reference let one result's mutation corrupt every future lookup. Also fixes the tier-2 preferences destination check to use Path.is_relative_to() instead of a naive startswith() string comparison, which could match sibling directories with a shared prefix. Broadens sensitive-value detection with a camelCase/delimiter-aware tokenizer so redaction catches concepts split across boundaries (e.g. authToken, Bearer ...) in addition to underscore-delimited patterns, and extends the precheck to scan preference values (recursively for nested dict/list) in addition to keys. Adds missing app_to_package entries for Linear.app and Ice.app, which previously fell through to "unrecognized app" despite being named in the architect plan.
…ro-exit warning Widens attribute_to_scanner()'s scope in _run_scanner_async to cover the whole scanner lifecycle (construction, is_available, scan, and exception handling), not just the scan() call itself. Previously a crashing scanner's logger.exception() call fired after the attribution context had already exited, landing the traceback in the unattributed bucket and duplicating it under "General warnings" alongside the scanner's own error row. Also adds a warn_on_nonzero opt-out to run_command() (default True, preserving existing behavior everywhere else) and applies it to npm's package-listing call, which routinely exits non-zero on peer-dependency warnings while still producing valid output on stdout -- a known, benign case that would otherwise show as a false WARNING status.
…sses _is_daemon_running() probes both known launchd labels and both known process names for the nix daemon, since a machine only ever runs one variant -- checking the other is expected to exit non-zero on every healthy install, and pgrep follows the same not-a-failure convention for "no match". Both call sites now opt out of run_command()'s non-zero-exit warning, matching the existing fix already applied to npm's equally benign non-zero-exit case.
Rich's Text() only parses its own markup syntax and click.echo() only strips ANSI on non-tty output -- neither neutralizes literal control bytes on a real terminal. Since scanner warnings/errors now embed raw subprocess stderr and exception text, a compromised or malicious local package's output could inject terminal escape sequences (clear-screen, cursor movement, OSC title/link spoofing) into the live table or the general-warnings section. Strips C0 control characters (preserving tab and newline) at the single point where log records are captured, plus at the orchestrator's exception-message construction site since that one bypasses the log-capture path entirely. Also pops any log records accumulated during a scanner's construction or is_available() check when it turns out to be skipped, matching the success/warning/error branches, which already do this.
An exhaustive audit of all 116 run_command() call sites in scanners/ found 18 more instances of the same pattern already fixed for npm and nix-daemon detection: a non-zero exit that represents a normal, expected state (feature off, not installed, empty result, no crontab) rather than a command failure. Left unfixed, these would show as false ScannerStatus.WARNING in the live table on very common, healthy Mac configurations -- system_scanner.py alone (always dispatched, not opt-in) covered screen sharing off, file sharing off, Rosetta not installed, no printers configured, and not signed into iCloud.
ThreadPoolExecutor.submit() does not copy contextvars into its worker threads the way asyncio.to_thread() does, so warnings logged inside a scanner's nested directory-walk pool were losing their scanner attribution and surfacing as unattributed general warnings instead of on the correct scanner's row. Copies a fresh context per submitted item (not one shared across concurrent workers, which raises "context already entered") so nested pool workers see the same _current_scanner value as the thread that created them. Also widens the control-character sanitizer to cover the C1 range (U+0080-U+009F, including the 8-bit CSI), closing a narrow defense-in-depth gap on top of the existing C0/DEL stripping.
Same gap as parallel_walk_dirs(): the two ThreadPoolExecutor usages here (parallel detector dispatch, parallel Go binary inspection) ran their workers without the calling thread's contextvars, so warnings from any of the 7 detectors or the go version -m check lost scanner attribution. Converts the Go inspection loop from pool.map() to explicit submit() so each item gets its own copy_context() call, same as detector dispatch. These were the last two ThreadPoolExecutor sites in the codebase.
…y tally Distinguishes the two distinct causes of a plist PermissionError per Apple's own DTS guidance: EACCES means a traditional BSD permission/ACL denial (e.g. a root-owned, 0600 system file, which Full Disk Access cannot override), while EPERM means TCC/sandboxing blocked it, which Full Disk Access genuinely fixes. The "Grant Full Disk Access" hint was previously shown for the EACCES case -- exactly backwards, since two real system files (com.apple.apsd.plist, com.apple.wifi.known-networks.plist) are root-owned 0600 and would never be readable regardless of Full Disk Access. The EACCES case now gets an accurate, non-actionable hint instead; the EPERM case is now surfaced (previously silent at DEBUG) with the Full Disk Access hint it actually deserves. Also caps the live table's message column width with text folding, so a single long warning/error line no longer stretches the whole table edge-to-edge in a wide terminal, and renames the tally's "warning" label to "completed with warnings" so the summary line doesn't read as ambiguous about whether a scanner actually finished.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/mac2nix/mappings/— 9 modules (defaults, apps, home-manager programs, dotfiles, brew formulae, app configs, ephemeral-state filtering, non-defaults options, and a four-tier classifier) mapping scanned macOS settings to nix-darwin destinations