feat(sessions): Add composability/resolution to loadouts - #348
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (12)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (8)
📝 WalkthroughWalkthroughAdds a composable session resolution pipeline: provenance-tagged contributions, three-pass policy-driven resolution for vars and patches, patch glob expansion and destination computation, FileSet/PatchPolicy redesign, LoadoutName validation, thiserror refactors, and extensive tests/docs. ChangesSession Resolution Pipeline with Provenance & Policy
Sequence DiagramsequenceDiagram
participant User as User
participant Composer as Composer
participant VarsPolicy as VarsPolicy
participant PolicyHooks as PolicyHooks
participant PatchPolicy as PatchPolicy
participant FileSystem as FileSystem
User->>Composer: add(Loadout / Config)
User->>Composer: resolve(policy, hooks, options)
Composer->>VarsPolicy: resolve_vars (pass 1 categorize)
VarsPolicy->>PolicyHooks: on_var_unapproved(items)
PolicyHooks-->>Composer: decisions (+policy updates)
Composer->>FileSystem: enumerate_patch_files (globs)
FileSystem-->>Composer: matched files / walk errors
Composer->>PatchPolicy: resolve_patches (pass 1 categorize)
PatchPolicy->>PolicyHooks: on_patch_unapproved(items)
PolicyHooks-->>Composer: patch decisions
Composer-->>User: (Resolution, updated UserPolicy)
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
crates/sessions/src/composable.rs (1)
467-479: 💤 Low valueConsider adding doc comments to
PolicyHookstrait methods.The public trait methods
on_var_unapprovedandon_patch_unapprovedlack documentation. Since this is a public API that implementors will need to understand, documenting the expected behavior, parameters, and return value semantics would help downstream consumers.🤖 Prompt for 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. In `@crates/sessions/src/composable.rs` around lines 467 - 479, Add Rustdoc comments to the PolicyHooks trait and both methods (PolicyHooks, on_var_unapproved, on_patch_unapproved) describing purpose and expected behavior, explain each parameter (policy: VarsPolicy/PatchPolicy and items: &[Unapproved<...>]) including what the Unapproved item represents, and document the return type HookResult<VarsPolicy>/HookResult<PatchPolicy> semantics (e.g., whether Ok means accept/modify policy, Err aborts processing, any side effects). Keep docs concise, include example usage or panics only if relevant, and ensure doc comments appear above the trait and each method signature.crates/sessions/src/patches.rs (2)
683-702: 💤 Low valueThe
checkmethod consumesitemeven when returningIgnored.When the path matches
ignore, the method returnsDecision::Ignoredbut dropsitem. This is fine if callers don't need the item back for logging/diagnostics, but it differs fromDeniedandAllowedwhich preserve the item.If callers might want to log ignored items with their provenance, consider returning
Decision::Ignored(item)for consistency. Otherwise, the current design is acceptable if "ignored" truly means "discard without trace."🤖 Prompt for 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. In `@crates/sessions/src/patches.rs` around lines 683 - 702, The check method currently returns CheckOutcome::Decided(Decision::Ignored) and drops the provided item when filesets_match(&self.ignore, path) is true, which is inconsistent with Denied/Allowed branches that preserve the item; update the ignore branch in check (function name: check, type: impl for patches) to return the provenance with the ignored decision (e.g., CheckOutcome::Decided(Decision::Ignored(item))) so callers can still access item metadata for logging/diagnostics, ensuring the other branches (Denied/Allowed) remain unchanged and still use filesets_match(&self.deny, path) and filesets_match(&self.allow, path).
184-197: 💤 Low value
walk_rootdoes not account for escaped glob metacharacters.The scan treats
\*,\?,\[,\{the same as unescaped metacharacters, so a pattern likefoo/bar\*/baz.txt(where\*is a literal asterisk) would incorrectly returnSome("foo/bar")instead ofSome("foo/bar*/baz.txt").This is likely acceptable given:
- Escaped metacharacters in paths are rare in practice
- The consequence is a broader (but still correct) walk root, not incorrect matching
globset::Globhandles the actual matching correctlyConsider documenting this limitation if edge-case correctness is important.
🤖 Prompt for 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. In `@crates/sessions/src/patches.rs` around lines 184 - 197, The walk_root implementation incorrectly treats escaped glob metacharacters as active; update walk_root to skip characters that are escaped by a backslash: when iterating pattern.bytes().enumerate() inside walk_root, if you see a backslash (b'\\') treat the next byte as literal by advancing/skipping the subsequent byte in the loop (so escaped '*', '?', '[', '{' are ignored for triggering the glob handling), then continue the existing logic to record last_slash and return HostPath::new(...) as before; ensure this uses the same pattern() value and preserves behavior when the backslash is the final byte.
🤖 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/paths/src/lib.rs`:
- Around line 200-207: The constructors AbsPath::new_unchecked and
RelPath::new_unchecked currently are public and only use debug_assert! to check
inner.is_absolute()/inner.is_relative(), allowing invalid values in release
builds; fix this by making them either non-public (change signature to
pub(crate) fn new_unchecked(...)) or mark them explicitly unsafe (pub unsafe fn
new_unchecked(...)) and add a clear # Safety doc comment plus the required //
SAFETY: comment per repo standards describing the caller responsibility to
ensure the path invariant (absolute for AbsPath, relative for RelPath); update
both functions (new_unchecked) accordingly so the invariant is enforced by the
type system rather than only debug assertions.
In `@crates/sessions/docs/RESOLUTION.md`:
- Line 13: Update the documentation to use the current API names: replace
occurrences of Composable::compose_into(&mut Composer) with
Composable::contribute(...) and update references to Composer::add/add_all where
appropriate; also update the resolve signature block to reflect the new
Contribute/Composer method names so examples and docs consistently reference
Composable::contribute and Composer::add/add_all instead of the old
compose_into/Composer naming.
- Line 7: The fenced code block opened on line 7 lacks a language tag (triggers
markdownlint MD040); update the opening fence in RESOLUTION.md (the triple
backtick block) to include a language identifier such as ```text (or another
appropriate language) so the fenced block becomes ```text and resolves the
linter warning.
In `@crates/sessions/src/vars.rs`:
- Around line 507-512: Change build_set to return Result<GlobSet, Error> instead
of unwrapping: replace the call to GlobSetBuilder::build().expect(...) with
b.build().map_err(|e| Error::GlobSetBuild(e.into()))? (or add a new
Error::GlobSetBuild(globset::Error) variant and map accordingly). Update callers
(try_new and with_pattern) to propagate the Result using ? and adapt their
signatures to return Result where needed, and add the new Error enum variant
mapping globset::Error into your crate's Error type so build failures are
returned as typed errors rather than panicking.
---
Nitpick comments:
In `@crates/sessions/src/composable.rs`:
- Around line 467-479: Add Rustdoc comments to the PolicyHooks trait and both
methods (PolicyHooks, on_var_unapproved, on_patch_unapproved) describing purpose
and expected behavior, explain each parameter (policy: VarsPolicy/PatchPolicy
and items: &[Unapproved<...>]) including what the Unapproved item represents,
and document the return type HookResult<VarsPolicy>/HookResult<PatchPolicy>
semantics (e.g., whether Ok means accept/modify policy, Err aborts processing,
any side effects). Keep docs concise, include example usage or panics only if
relevant, and ensure doc comments appear above the trait and each method
signature.
In `@crates/sessions/src/patches.rs`:
- Around line 683-702: The check method currently returns
CheckOutcome::Decided(Decision::Ignored) and drops the provided item when
filesets_match(&self.ignore, path) is true, which is inconsistent with
Denied/Allowed branches that preserve the item; update the ignore branch in
check (function name: check, type: impl for patches) to return the provenance
with the ignored decision (e.g., CheckOutcome::Decided(Decision::Ignored(item)))
so callers can still access item metadata for logging/diagnostics, ensuring the
other branches (Denied/Allowed) remain unchanged and still use
filesets_match(&self.deny, path) and filesets_match(&self.allow, path).
- Around line 184-197: The walk_root implementation incorrectly treats escaped
glob metacharacters as active; update walk_root to skip characters that are
escaped by a backslash: when iterating pattern.bytes().enumerate() inside
walk_root, if you see a backslash (b'\\') treat the next byte as literal by
advancing/skipping the subsequent byte in the loop (so escaped '*', '?', '[',
'{' are ignored for triggering the glob handling), then continue the existing
logic to record last_slash and return HostPath::new(...) as before; ensure this
uses the same pattern() value and preserves behavior when the backslash is the
final byte.
🪄 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: 0e12e885-0c1d-4843-8034-5c4145fa702c
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
Cargo.tomlcrates/paths/Cargo.tomlcrates/paths/src/lib.rscrates/sessions/Cargo.tomlcrates/sessions/docs/RESOLUTION.mdcrates/sessions/src/composable.rscrates/sessions/src/lib.rscrates/sessions/src/lifecyclehook.rscrates/sessions/src/loadout.rscrates/sessions/src/patches.rscrates/sessions/src/policy.rscrates/sessions/src/vars.rs
8205629 to
778a0c1
Compare
| vars_lenient: Vec<LenientVarEntry>, | ||
| /// Patches contributed by this loadout. | ||
| #[serde(default, skip_serializing_if = "Patches::is_empty")] | ||
| patches: Patches, |
There was a problem hiding this comment.
iirc Patches contains the path to the file but not the file contents - that might complicate us transmitting the loadout set to minimald.
There was a problem hiding this comment.
I think we probably don't want that actually. While the CLI will need to be the one reading the files, since the daemon might be running under a different user or in theory on a different machine, we probably don't want to read all files into memory when deserializing the loadouts. Instead we probably want to open up a tarball stream to the daemon from the CLI directly and avoid having the files completely in memory at all. For Config files it's fine, but it could become problematic if someone tries to bring in model weights or some other larger asset.
399866c to
741d3b6
Compare
GitLab-hosted upstreams (the fpottier trio menhir/visitors/unionFind on gitlab.inria.fr; mesa) had no honest provenance category, so a build.ncl couldn't declare one and they were pushed onto gs:// mirrors + Cpe workarounds. Add a `source_provenance_gitlab` sub-contract (category = 'Gitlab, with required host / owner / repo) and its dispatch arm. `host` is required because GitLab is federated — the same owner/repo on gitlab.com vs gitlab.inria.fr are different projects. Pairs with minimal-supply-chain#347/#348, which adds the matching `Provenance::Gitlab { host, owner, repo }` (parse + pkg:generic identity). Verified with nickel: a valid Gitlab block exports, a block missing `host` is a contract error, and the existing categories still validate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…752) * feat(stdlib): accept 'Gitlab source_provenance category GitLab-hosted upstreams (the fpottier trio menhir/visitors/unionFind on gitlab.inria.fr; mesa) had no honest provenance category, so a build.ncl couldn't declare one and they were pushed onto gs:// mirrors + Cpe workarounds. Add a `source_provenance_gitlab` sub-contract (category = 'Gitlab, with required host / owner / repo) and its dispatch arm. `host` is required because GitLab is federated — the same owner/repo on gitlab.com vs gitlab.inria.fr are different projects. Pairs with minimal-supply-chain#347/#348, which adds the matching `Provenance::Gitlab { host, owner, repo }` (parse + pkg:generic identity). Verified with nickel: a valid Gitlab block exports, a block missing `host` is a contract error, and the existing categories still validate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(stdlib): bump crate version to 0.0.17 Requested on review — the embedded stdlib changed (new 'Gitlab source_provenance contract), so bump the crate + the workspace dep pin + lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds the composability layer that turns a [Loadout] (and, eventually, project configs and packages) into a Resolution of vars, patches, packages, and lifecycle hooks ready for the apply layer.
The core flow:
What's in here
New composable module (crates/sessions/src/composable.rs):
Policy resolution (var + patch domains):
impl Composable for Loadout: materializes Source::UserLoadout, resolves inherited vars through the env closure, and emits all four primitive kinds. End-to-end test covers all four reaching Resolution.
thiserror adopted across paths and sessions error enums (replaces manual Display/Error impls). Adds thiserror = "2" workspace dep.
paths crate gains AbsPath::new_unchecked, AbsPath::root(), and RelPath::new_unchecked for the resolver's by-construction-absolute paths.
patches crate gets:
vars crate gets VarsPolicy::check (matching the patches one).
policy::UserPolicy gets into_parts() -> (VarsPolicy, PatchPolicy). Resolver moves narrow policies by value; no &mut threading.
Docs: crates/sessions/docs/RESOLUTION.md — data-flow diagram and invariants.
Deferred (intentional)
Test plan
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Documentation