Skip to content

feat(sessions): var expansion and path canonicalization - #389

Merged
evanspearman merged 1 commit into
mainfrom
evan/varexp2
Jun 15, 2026
Merged

feat(sessions): var expansion and path canonicalization#389
evanspearman merged 1 commit into
mainfrom
evan/varexp2

Conversation

@evanspearman

@evanspearman evanspearman commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

  • Unifies tilde + $VAR expansion behind one expand_source call,
    driven by resolved session vars
  • Adds path canonicalization + dual-path policy check for symlinks
  • Rips out the old HomeLookup / HomeResolutionFailure /
    expand_home machinery

Behavior changes

  • Patterns must be absolute (~/x or $HOME/x for home-relative).
    Relative → ExpandError::NotAbsolute.
  • .. rejected everywhere (raw or substituted).
  • Substituted glob metas now escaped (fixes the parent PR's
    unescaped-home regression for ~/).
  • follow_symlinks: true checks both link + canonical target; deny
    on either wins.
  • PatchPolicy setters are infallible; validation moves to
    expand_with at resolve time.

Test plan

  • cargo test -p sessions (174 lib + 5 doc)
  • cargo clippy -p sessions --all-targets -- -D warnings

Resolves #384

Summary by CodeRabbit

  • New Features

    • Patch source patterns now expand ~ and $VAR/${VAR} using session-resolved values (with clearer errors when expansion fails).
    • Patch policy rules now evaluate against pre-expanded patterns with improved allow/deny/ignore precedence and more accurate matching when symlinks are involved.
  • Bug Fixes/Improvements

    • Safer path handling: rejects non-absolute patterns and prevents traversal via .. from expanded inputs.
    • Patch destinations are normalized at creation (removes redundant slashes and . components).
  • Tests / CI

    • Expanded coverage for expansion failures, symlink dual-path behavior, and policy re-expansion; macOS CI now runs the sessions test suite.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9c37003-b58c-400f-84df-b4efbbca0e62

📥 Commits

Reviewing files that changed from the base of the PR and between c123616 and 12c39d8.

📒 Files selected for processing (7)
  • .github/workflows/ci-macos.yml
  • crates/sessions/src/composable.rs
  • crates/sessions/src/expansion.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/loadout.rs
  • crates/sessions/src/patches.rs
  • crates/sessions/src/policy.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/sessions/src/lib.rs
  • crates/sessions/src/loadout.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/sessions/src/policy.rs
  • .github/workflows/ci-macos.yml
  • crates/sessions/src/expansion.rs
  • crates/sessions/src/patches.rs
  • crates/sessions/src/composable.rs

📝 Walkthrough

Walkthrough

This PR implements ~/ and $VAR pattern expansion for patch source strings using session-resolved variables. It introduces a new expansion module that parses and substitutes these references with strict validation, glob-meta escaping, and normalization. Patch sources and policies are refactored to store raw pattern strings, deferring expansion to resolution time. The resolver is updated to use an EnvLookup interface instead of home-lookup, applies expansion before file walking, and supports dual-path symlink policy checks. Tests are rewritten to use resolved session variables instead of home-lookup closures.

Changes

Patch source and policy pattern expansion via environment variables and tilde

Layer / File(s) Summary
Expansion module for patch sources and environment variables
crates/sessions/src/expansion.rs, crates/sessions/src/lib.rs
New expand_source function parses ~/ and $VAR/${VAR} references against resolved session variables. Variable names validated as [A-Z_][A-Z0-9_]*. Substituted values are glob-meta escaped (including *, ?, [, ], {, }, ,). Paths normalized to remove . and empty components. Rejects .. traversal and relative patterns. Supports optional home_fallback for tilde-prefix only; explicit $HOME requires strict lookup. Maps final result to FileSet with error handling. Comprehensive test suite covers substitution forms, escaping, normalization, and error cases.
Deferred expansion: raw strings in Patch and PatchPolicy
crates/sessions/src/patches.rs, crates/sessions/src/loadout.rs, crates/sessions/src/policy.rs
Patch::source changed from FileSet to String to preserve raw ~/ and $VAR forms. PatchPolicy fields (allow/deny/ignore) changed from Vec<FileSet> to Vec<String>. Patches deserialization updated to preserve raw sources via fan-out logic creating one Patch per pattern string. PatchPolicy::expand_with(resolved_vars) produces ExpandedPatchPolicy with fully-expanded FileSet lists. PatchDest::try_new normalizes . and redundant slashes while rejecting .. during component-walk. New Error variants for canonicalize I/O failures and non-UTF8 paths. FileSet::walk_root fixed to unescape single-byte bracket literals. Tests validate normalization, raw-string storage, and expansion behaviors.
Resolution contracts: EnvLookup, ResolveError, ExpandedPatchPolicy
crates/sessions/src/composable.rs
Introduces EnvLookup type alias (&dyn Fn(&str) -> Result<String, VarError>) replacing HomeLookup. Updates ResolveError to remove PatchConfig and HomeUnresolved variants and add Expansion(ExpandError) for expansion failures. Adds public ExpandedPatchPolicy type storing fully-expanded allow/deny/ignore rule lists as FileSets with path-decision logic supporting dual-path symlink checks. Adds test-only SessionVar::new constructor. Refactors Composer to store EnvLookup instead of home-lookup, removes home-directory plumbing from initialization and Debug, removes Composer::with_home from public API.
Patch resolution: expansion, enumeration, and dual-path matching
crates/sessions/src/composable.rs
Composer::resolve drives patch resolution with resolved session variables and optional ambient HOME fallback instead of stored home-lookup closures. PatchFile struct tracks optional symlink link_path and canonical target_path for dual-path policy checks when follow_symlinks enabled. expand_patch_sources expands raw patch source patterns before walking. enumerate_patch_files walks FileSets via walkdir, filters by pattern match, canonicalizes to capture both link and target paths, accumulates walker/canonicalization/UTF-8 errors. resolve_patches expands raw policy via expand_with, enumerates files, performs per-file dual-path categorization, handles hook re-expansion, constructs ResolvedPatch using canonical target-path with dest from user-facing walk root. UseRule re-checks evaluate both paths with deny precedence.
Test updates: helpers, home_var, resolve_patches signatures
crates/sessions/src/composable.rs, crates/sessions/src/loadout.rs, crates/sessions/src/policy.rs
Removes home-lookup test helpers; adds home_var helper constructing SessionVar for HOME. Updates single_file_patch helper to construct Patch from raw string source. Updates all resolve_patches calls to pass &vars slice and optional home_fallback instead of closures. Rewrites tilde/env expansion, policy pattern, walking failure, and symlink dual-path behavior tests to use resolved session variables and new error shapes. Updates compute_dest tests to use Utf8Path directly. Updates end-to-end loadout and FileSet tests with revised import/construction patterns.

macOS CI coverage for sessions crate

Layer / File(s) Summary
macOS workflow path filtering and test step
.github/workflows/ci-macos.yml
Adds crates/sessions/** to both push and pull_request paths filters so workflow triggers on sessions code changes. Adds new cargo test -p sessions test step to macOS job after clippy and minvmd steps, extending platform-specific test coverage to the sessions crate with comments documenting platform-sensitive behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related issues

  • Expand $VAR in patch source patterns #384: Directly implements the $VAR patch source pattern expansion feature with strict variable-name validation ([A-Z_][A-Z0-9_]*), escape support ($$ for literal $), undefined-var errors, and policy patterns remaining literal during policy checks—fulfilling all acceptance criteria from the design discussion.

  • gominimal/inbox#197: Implements the same $VAR and ~/ pattern expansion for patch sources at resolution time using an env lookup with hard errors for unset variables and glob-meta escaping of substitutions.

Possibly related PRs

  • gominimal/minimal#372: Introduces the HomeLookup/HomeUnresolved home-expansion mechanism that this PR removes and replaces with a unified EnvLookup-based expansion of both tilde and variables.

  • gominimal/minimal#348: Establishes the resolver pipeline and EnvLookup interface for environment/variable handling that this PR leverages for patch source and policy pattern expansion integration.

Suggested reviewers

  • twitchyliquid64

Poem

🐰 A tilde and dollar sign walked through the patch,
Variables resolved with a careful match.
Raw strings deferred till the walker began,
Glob metas escaped by the expansion plan.
Now symlinks see both their paths side by side,
Dual-path decisions with nowhere to hide! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(sessions): var expansion and path canonicalization' directly summarizes the main changes: introducing variable expansion and path canonicalization features in the sessions crate.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #384: $VAR expansion in patch sources, syntax handling, unset variable errors, policy/dest non-expansion, escaping, and symlink dual-path canonicalization.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue scope: variable expansion implementation, path canonicalization, policy refactoring, and CI infrastructure fixes for testing the affected crate.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/sessions/src/composable.rs`:
- Around line 1395-1406: The policy is expanded after enumerate_patch_files
causing filesystem walks on unresolved/invalid patterns; call
policy.expand_with(resolved_vars) first and use that expanded policy when
constructing any checks before calling expand_patch_sources and
enumerate_patch_files (i.e., move let mut expanded =
policy.expand_with(resolved_vars)? above
expand_patch_sources/enumerate_patch_files), so validation happens before
PatchWalk and directory traversal and any returned errors surface
deterministically.
- Around line 977-982: resolve_patches/expand_patch_sources now only consult
resolved_vars (via resolve_patches, expand_source) which removes access to the
host env and breaks ~/ and $HOME expansion; fix by restoring ambient-home
lookup: either seed the session vars with HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME
etc. when Composer::new() builds initial vars, or modify
resolve_patches/expand_source to fall back to std::env when a name is not found
in &vars (keep resolve_vars precedence). Update code paths referencing
resolve_patches, expand_patch_sources, and expand_source to use the chosen
approach so patch expansion again supports ~ and $VAR without requiring users to
predeclare them.

In `@crates/sessions/src/expansion.rs`:
- Around line 104-121: The escaped substitution values produced by
escape_glob_metas cause FileSet::walk_root() to stop early on inserted `[` and
widen the root; fix by having the walk-root logic use the original unescaped
variable value (or compute the literal prefix before escaping) when scanning for
glob metacharacters instead of the escaped output. Concretely, when expanding
vars in parse_var_ref/where escape_glob_metas is called, preserve or pass the
raw/decoded value into FileSet::walk_root() (or derive the concrete literal
prefix from the unescaped bytes) so FileSet::walk_root() no longer treats
escaped `[`/`*`/`?` as metacharacters; update FileSet::walk_root() and places
that call it to accept/consume the unescaped prefix (or an explicit
literal-prefix) rather than the escaped string.
- Around line 110-123: The current $-handling branch should only perform
expansion for strict variable names and treat other forms (malformed names,
lowercase, `${...}` non-strict, and escaped `\$`) as literals instead of
erroring; update the b == b'$' branch so it first detects an escape (`\$`) and
emits a literal `$` (consuming the backslash and dollar), otherwise attempt
parse_var_ref but only proceed with lookup and expansion when the parsed name
matches the StrictVarName rule (or use an existing is_strict check); if
parse_var_ref fails or the name is non-strict, push the raw `$` (and leave
subsequent characters intact) instead of returning ExpandError::UndefinedVar,
and continue using escape_glob_metas on expanded values when lookup succeeds
(functions to touch: parse_var_ref, lookup, ExpandError::UndefinedVar,
StrictVarName, escape_glob_metas).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32f95551-1a43-4435-8b80-4b72003c231d

📥 Commits

Reviewing files that changed from the base of the PR and between 06ff820 and 52c5184.

📒 Files selected for processing (6)
  • crates/sessions/src/composable.rs
  • crates/sessions/src/expansion.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/loadout.rs
  • crates/sessions/src/patches.rs
  • crates/sessions/src/policy.rs

Comment thread crates/sessions/src/composable.rs Outdated
Comment thread crates/sessions/src/composable.rs Outdated
Comment thread crates/sessions/src/expansion.rs
Comment thread crates/sessions/src/expansion.rs

@norrietaylor norrietaylor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This patch is breaking local macos builds, this is my fault as the ci-macos.yml path filter I added omits crates/sessions/**, so the workflow never runs for this PR. Even when it runs, the macOS jobs only test minvmd (cargo test -p minvmd, clippy -p minvmd at ci-macos.yml:360-367). Adding sessions to the paths would trigger the workflow but still never execute a sessions test.

Two edits to .github/workflows/ci-macos.yml are needed

  1. Add sessions to both path filters (push and pull_request):
      paths:
        - "crates/minvmd/**"
        - "crates/minimal2/**"
        - "crates/sessions/**"     # platform-sensitive: canonicalization, symlinks
        ...

Run the sessions tests in the existing clippy+test job (the one at line ~336 on the self-hosted runner) — one line, reuses the job's build cache rather than adding a new job to the bottlenecked single runner:

        - run: cargo clippy -p minvmd --all-targets -- -D warnings
        - run: cargo test -p minvmd
        - run: cargo test -p sessions

/// Glob metacharacters (`**`, `*`, `?`, `[...]`, `{...}`) are
/// path components in their own right and pass through unchanged —
/// only literal `.` and `..` components are touched.
fn normalize_path(s: &str) -> Result<String, ExpandError> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Path traversal is blanket illegal?

Seems like a safe starting point, but i wonder if we want to loosen this in the future. Handling path traversal is a mess tho (absolutize() isnt good enough if the user can do symlinks - canonicalize is best but requires files to exist + eats IOPS).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm just having a hard time seeing a good reason to allow .., but I may be missing something and there may be workflows that would benefit from it. I don't think it would be that hard to support in the future if we need to though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A note on symlinks: We do need to support symlinks because home-manager (nix) uses them extensively and I want to support home-manager generated files for admittedly selfish reasons. That said to mitigate security concerns around them:

  • Policies apply to both the link and the file it's linking to. If one is ignored or denied it treats them as both being such and if one isn't allowed (outside of user loadouts which bypass the user allowlist for ergonomic reasons) neither are allowed
  • There will be a user config option to turn off resolving symlinks entirely

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we dont support .., i dont think its possible to use symlinks to admit sensitive files in a way that wasnt possible already without this primitive. Because if someone can create symlinks on the host, they can already access files on the host.


/// Parse a `$VAR` or `${VAR}` reference starting at `bytes[at]` (which
/// is the `$`). Returns `(name, bytes_consumed_including_dollar)`.
fn parse_var_ref(bytes: &[u8], at: usize) -> Result<(&str, usize), ExpandError> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might be worth adding a comment here that escaping variable declarations (i.e. $$var) must be handled earlier

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed

@twitchyliquid64 twitchyliquid64 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved tho note Norries comment above about fixing CI

@evanspearman

Copy link
Copy Markdown
Member Author

This patch is breaking local macos builds, this is my fault as the ci-macos.yml path filter I added omits crates/sessions/**, so the workflow never runs for this PR. Even when it runs, the macOS jobs only test minvmd (cargo test -p minvmd, clippy -p minvmd at ci-macos.yml:360-367). Adding sessions to the paths would trigger the workflow but still never execute a sessions test.

Two edits to .github/workflows/ci-macos.yml are needed

1. Add sessions to both path filters (push and pull_request):
      paths:
        - "crates/minvmd/**"
        - "crates/minimal2/**"
        - "crates/sessions/**"     # platform-sensitive: canonicalization, symlinks
        ...

Run the sessions tests in the existing clippy+test job (the one at line ~336 on the self-hosted runner) — one line, reuses the job's build cache rather than adding a new job to the bottlenecked single runner:

        - run: cargo clippy -p minvmd --all-targets -- -D warnings
        - run: cargo test -p minvmd
        - run: cargo test -p sessions

Fixed

@norrietaylor norrietaylor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@evanspearman
evanspearman merged commit 343e3a2 into main Jun 15, 2026
44 checks passed
@evanspearman
evanspearman deleted the evan/varexp2 branch June 15, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expand $VAR in patch source patterns

3 participants