Skip to content

feat(sessions): patch source/policies home expansion - #372

Merged
evanspearman merged 2 commits into
mainfrom
evan/varexp
Jun 12, 2026
Merged

feat(sessions): patch source/policies home expansion#372
evanspearman merged 2 commits into
mainfrom
evan/varexp

Conversation

@evanspearman

@evanspearman evanspearman commented Jun 9, 2026

Copy link
Copy Markdown
Member

Patch source FileSet patterns and PatchPolicy allow/deny/ignore
patterns are commonly written ~/-relative (source = "~/dotfiles/...",
deny = ["~/.ssh/**"]). The walker doesn't interpret ~, so today
those silently match nothing — a dotfile patch contributes zero files,
a ~/.ssh/** deny rule never fires.

The resolver now expands ~ against a configurable host home
(HomeLookupdirs::home_dir by default) before walking and before
policy check. Both halves keep their ~/-relative form in the
returned policy, so save/load is lossless.

Behavior:

  • Lookup is lazy: invoked only when at least one ~-prefixed pattern
    is in scope. A loadout with no ~ anywhere never asks for home.
  • Lookup is re-attempted mid-resolution if the hook adds a ~-prefixed
    rule and home wasn't previously needed.
  • PatchDest is unchanged — destinations are implicitly relative to the
    sandbox user's home and reject ~ / absolute paths at construction.

Failures surface as ResolveError::HomeUnresolved(HomeResolutionFailure)
distinguishing three causes (Unavailable, NotUtf8, NotAbsolute) so
the user knows whether to set $HOME, fix encoding, or report a buggy
lookup. No silent fallback to root or to an empty path.

Type choices that came out of review along the way:

  • resolve_home returns HostAbsPath, surfacing relative results as
    NotAbsolute instead of letting them flow through.
  • expand_home and expand_policy_home take &HostAbsPath
    unconditionally; the Option lives at the caller, so the
    "tilde + no home" combination is impossible at the type level rather
    than enforced by expect.
  • compute_dest's strip_prefix is camino's path-component-aware
    variant, so /etc/xdg no longer falsely matches /etc/xdgfoo.

Tests cover all three home-failure modes, the lazy-lookup precondition,
hook-introduced tilde rules, and the path-component-aware strip-prefix
regression. 127 lib tests pass.

Resolves https://github.com/gominimal/inbox/issues/196

Summary by CodeRabbit

  • New Features

    • Patch source and policy patterns with leading ~ are expanded against the host home during session resolution; raw ~ is preserved in saved/returned policies.
    • Resolver skips host-home lookup when no ~ patterns exist.
    • Policies returned/modified by hooks that introduce ~ rules are re-expanded and enforced.
  • Documentation

    • Clarified ~ semantics for sources vs destinations, the resolution stages, and explicit failure cases for unresolved home paths.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dffa033-fe53-4ee3-bb64-6a5f667c535c

📥 Commits

Reviewing files that changed from the base of the PR and between dc3df0e and dc11611.

📒 Files selected for processing (3)
  • crates/sessions/docs/RESOLUTION.md
  • crates/sessions/src/composable.rs
  • crates/sessions/src/loadout.rs
 ____________________________________________________________________________________________
< It is easier to change the specification to fit the program than vice versa. - Alan Perlis >
 --------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

This PR implements home directory (~) expansion in patch source patterns and policy rules during session resolution. Leading ~ in patch sources and policies is now expanded against the host home directory before file walking and policy matching, while the raw ~/-relative form is preserved in returned policies for lossless round-trip persistence.

Changes

Home directory expansion in patch resolution

Layer / File(s) Summary
Public API and error types
crates/sessions/src/composable.rs, crates/sessions/Cargo.toml
Introduces HomeLookup<'a> function type for caller-supplied home lookup, HomeResolutionFailure enum distinguishing Unavailable/NotUtf8/NotAbsolute cases, and ResolveError::HomeUnresolved variant with automatic From conversion. Adds dirs workspace dependency.
Composer home lookup threading
crates/sessions/src/composable.rs
Extends Composer with StoredHome boxed closure field, updates Composer::new() to install default home lookup, converts with_env() to instance method, adds with_home() method, updates Debug impl, and threads home through Composer::resolve into patch resolution.
Pattern expansion helpers and file enumeration
crates/sessions/src/composable.rs
Implements expand_home() and resolve_home() helper functions, adds tilde detection and policy-expansion logic that maintains separate raw (for round-trip) and expanded (for matching) policy forms, and updates enumerate_patch_files to conditionally expand source patterns before filesystem walking.
Resolution entrypoint and policy handling
crates/sessions/src/composable.rs
Changes resolve_patches signature to accept HomeLookup, implements lazy home-resolution caching (invoked only when ~ patterns are present), handles hook-introduced tilde rules via immediate re-resolution, and updates the UseRule pass to match against expanded policies while preserving raw form for return.
Documentation clarifications across modules
crates/sessions/docs/RESOLUTION.md, crates/sessions/src/loadout.rs, crates/sessions/src/patches.rs, crates/sessions/src/composable.rs
Updates flow diagrams, module docstrings, and type documentation across the codebase to clarify tilde-expansion timing (source patterns and policies during resolution vs destination paths at apply time), responsibility boundaries, lossless round-trip preservation of ~/-prefixed patterns, and error-handling semantics.
Comprehensive test suite for tilde expansion
crates/sessions/src/composable.rs
Adds no_home() test helper for controlled home-lookup testing, updates all existing resolution test calls with home-lookup parameter, and adds new tests covering missing-home errors, successful ~/ expansion in sources, policy-based denials using expanded tildes, non-UTF-8/non-absolute home failures, round-trip preservation, fast-path behavior (no lookup when no tildes), and hook-injected tilde rules triggering mid-flight re-expansion.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • gominimal/inbox#196: This PR directly implements the acceptance criteria from the issue describing host home expansion for patch source patterns and policy rules, including HomeLookup, HomeResolutionFailure error variants, lazy resolution, raw vs expanded policy forms, and comprehensive test coverage.

Possibly related PRs

  • gominimal/minimal#348: This PR builds directly on the composability infrastructure added in PR #348 by extending the resolve_patches and Composer APIs with home lookup threading and tilde-expansion logic.

Suggested reviewers

  • twitchyliquid64

Poem

A rabbit hops through home paths, ~ in paw,
Expanding tildes where they never saw—
Policies match patterns, lossless preserved,
No silent shadows, every rule deserved. 🐰✨

🚥 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): patch source/policies home expansion' clearly and concisely summarizes the main change: adding tilde expansion for patch sources and policies against the host home directory.
Linked Issues check ✅ Passed The PR fully implements all requirements from issue #196: HomeLookup API, HomeResolutionFailure variants, lazy lookup, round-trip preservation, home expansion in sources/policies, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to the stated objectives of expanding leading tilde in patch sources and policies. No unrelated modifications detected in dependencies, documentation, or code logic.
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/docs/RESOLUTION.md`:
- Around line 179-181: Update the documentation to list all inner variants
returned by ResolveError::HomeUnresolved: explicitly include
HomeResolutionFailure::NotAbsolute alongside HomeResolutionFailure::Unavailable
and HomeResolutionFailure::NotUtf8 so the docs match the resolver's error
surface; locate the paragraph referencing ResolveError::HomeUnresolved and
change the inner variant enumeration to `HomeResolutionFailure::{Unavailable,
NotUtf8, NotAbsolute}` (or equivalent wording) to reflect the current API.

In `@crates/sessions/src/composable.rs`:
- Around line 551-555: The doc comment for the HomeUnresolved variant is out of
sync with the HomeResolutionFailure enum: update the documentation on
HomeUnresolved to enumerate all actual failure modes in HomeResolutionFailure
(no home available, non-UTF-8 home path, and NotAbsolute/non-absolute home path)
and keep the guidance to match on the inner HomeResolutionFailure to distinguish
causes (reference the HomeUnresolved variant and the HomeResolutionFailure enum
in your wording so callers aren't misled).
- Around line 1198-1202: The current checks use starts_with('~') and thus match
`~user/...` which expand_home doesn't handle, leading to
ResolveError::HomeUnresolved; update the logic to only treat a pattern as
home-expansion candidate when it is exactly "~" or begins with "~/". Change the
function any_tilde (and the other occurrences that currently call
starts_with('~') around the expand_home use) to check fs.pattern() == "~" ||
fs.pattern().starts_with("~/") (or the equivalent string checks where patterns
are plain &str) so only bare "~" and "~/..." are passed to expand_home and other
code paths that expect home expansion.

In `@crates/sessions/src/loadout.rs`:
- Around line 14-20: The file-level doc comment in loadout.rs is inconsistent:
it currently states "dest `~` is expanded at apply time", but the resolver/patch
layer now treats destination `~` as non-expanded and validates it as
sandbox-home-relative. Update the doc paragraph (the "~-expansion is split by
realm" section referencing "source `~`" and "dest `~`") to state that source `~`
is expanded at session-resolution (host home) while dest `~` is left unexpanded
by the resolver and is validated as sandbox-home-relative (with expansion
occurring only in the sandbox runtime), ensuring the wording matches the
resolver/patch contract.
🪄 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: 3f6d12e8-d742-408f-ab93-d136f1211655

📥 Commits

Reviewing files that changed from the base of the PR and between 00c1dc8 and ce635db.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • crates/sessions/Cargo.toml
  • crates/sessions/docs/RESOLUTION.md
  • crates/sessions/src/composable.rs
  • crates/sessions/src/loadout.rs
  • crates/sessions/src/patches.rs

Comment thread crates/sessions/docs/RESOLUTION.md Outdated
Comment thread crates/sessions/src/composable.rs Outdated
Comment thread crates/sessions/src/composable.rs Outdated
Comment thread crates/sessions/src/loadout.rs
/// The home argument is unconditional: callers are expected to gate
/// the call on whether any tilde pattern is in scope, so by the time
/// this function runs we always know a usable home is available.
fn expand_home<'a>(

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.

format!("{home}/{suffix}") feeds the home into a glob unescaped — a home containing *?[]{}\ (e.g. /home/u[1]) reinterprets the home segment, so walk_root() truncates at the metachar and the patch silently matches nothing.

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.

Thanks. Good catch!

This is actually being completely replaced in the var expansion stuff I'm currently working on and the bug should no longer be present. I'll add an explicit test for this case in that change though.

@evanspearman
evanspearman merged commit 06ff820 into main Jun 12, 2026
65 of 83 checks passed
@evanspearman
evanspearman deleted the evan/varexp branch June 12, 2026 15:08
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.

3 participants