chore(sessions): Split client and daemon loadout resolution logic - #528
Conversation
|
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 selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThe PR refactors session composition out of the client composer into a shared ChangesSession composition and wire-flow rearchitecture
Sequence Diagram(s)sequenceDiagram
participant UserComposer
participant compose_contribution
participant gate_vars
participant gate_patches
participant Daemon as SessionComposer
participant WireLayer as Wire / RPC
rect rgba(100, 149, 237, 0.5)
note over UserComposer,gate_patches: Phase 1 – Client composes
UserComposer->>compose_contribution: Contribution + UserPolicy + ComposeOptions
compose_contribution->>gate_vars: VarsPolicy + provenanced vars
gate_vars-->>compose_contribution: Vec~SessionVar~
compose_contribution->>gate_patches: PatchPolicy + expanded patches
gate_patches-->>compose_contribution: Vec~SessionPatch~
compose_contribution-->>UserComposer: Composition
UserComposer->>WireLayer: SessionCreateRequest { WireContribution }
end
rect rgba(144, 238, 144, 0.5)
note over WireLayer,Daemon: Phases 2–4 – Daemon collects, publishes, applies verdicts
WireLayer->>Daemon: WireContribution
Daemon->>Daemon: add daemon Composable contributors
Daemon->>compose_contribution: daemon Contribution + UserPolicy
compose_contribution-->>Daemon: Composition
Daemon->>Daemon: extend_from_wire(client WireContribution)
Daemon-->>WireLayer: SessionStep::Response { ContributionResponse }
WireLayer-->>UserComposer: pending vars/patches/hooks
UserComposer->>WireLayer: ContributionVerdict
WireLayer->>Daemon: verdicts applied
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
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 |
|
This pull request has no accompanying spec. Comment |
3502fd8 to
3707a08
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
crates/sessions/docs/COMPOSITION.md (2)
44-53: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winMove the transition note out of the main spec.
This reads like an implementation caveat (
SessionComposer::composeplaceholder, Phases 2–4 unwired) while the rest of the document is written as the current contract. If this is just migration guidance, put it in a separate status section; otherwise rewrite it in present tense.🤖 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/docs/COMPOSITION.md` around lines 44 - 53, The status note about SessionComposer::compose being a placeholder and Phases 2–4 being unwired contains implementation caveats that conflict with the specification-focused tone of the rest of the document. Move this entire note (including all references to compose_contribution, UserComposer::compose, and the phase implementation status) into a separate status or implementation notes section at the end of the document, or rewrite the content in present tense to describe the actual current contract and behavior rather than future planned changes.
68-76: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winKeep Phase 2 policy-free.
This paragraph says the daemon decides items from the user's “existing policy,” which clashes with the earlier rule that user policy is enforced only on the client. Reword Phase 2 so the daemon only forwards already-gated items and emits pending ones.
♻️ Suggested rewrite
-Items decidable from the user's existing policy go straight into the building Composition; -items the policy can't decide are collected as WirePendingVar/WirePendingPatch and emitted in one -ContributionResponse. +Items that are already decided by the client's prior gating go straight into the building +Composition; items that still need user approval are collected as WirePendingVar/WirePendingPatch +and emitted in one ContributionResponse.🤖 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/docs/COMPOSITION.md` around lines 68 - 76, The Phase 2 section in the COMPOSITION.md documentation incorrectly states that the daemon decides items based on the user's existing policy, which contradicts the earlier established rule that user policy is enforced only on the client. Reword the Phase 2 — Daemon collects and emits pending items paragraph to remove any reference to the daemon applying policy decisions, and instead clarify that the daemon only forwards already-gated items and collects undecidable items as pending variants, ensuring the description remains policy-free and consistent with the overall architecture where policy enforcement happens exclusively on the client side.
🤖 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/client/composer.rs`:
- Line 74: The `self.contribution` field is being cleared by `std::mem::take`
before the fallible `merge` operation completes. If the `merge` call fails and
returns an error, `self.contribution` remains empty instead of preserving its
original value. To fix this, perform the `merge` operation on a temporary value
first without mutating `self.contribution` until the merge succeeds, then only
update `self.contribution` with the merged result after confirming success. This
ensures that `self.contribution` retains its original state if the merge
operation fails.
In `@crates/sessions/src/daemon/composer.rs`:
- Line 73: The current implementation uses std::mem::take on self.contribution
which immediately empties it, then attempts to merge with incoming. If the merge
operation fails and returns an error via the ? operator, the original
contribution is lost. Instead, perform the merge operation on a reference or
clone of self.contribution without using std::mem::take, and only assign the
result to self.contribution after the merge succeeds. This ensures that if the
merge fails, self.contribution retains its original value.
- Around line 111-113: The order of operations causes daemon patches to be
expanded before client variables are available in scope. Move the
`extend_from_wire` call on `self.client` to occur before calling
`compose_contribution`, so that client-provided variables and configuration from
WireContribution are available when the daemon patches and policy are expanded.
This ensures that package/project sources can properly resolve user-gated
variables like $HOME from the client context rather than falling back to daemon
process environment variables. Refactor the code so that `self.client` is
processed and converted first, making its variables available to the
`compose_contribution` function.
In `@crates/sessions/src/wire/errors.rs`:
- Around line 52-59: The `From<crate::core::compose::ComposeError>`
implementation for `WireError` currently converts all `ComposeError` variants to
`WireError::Internal`, but `ComposeError::InvalidWireItem` represents invalid
client input and should be reported as `WireError::InvalidContribution` instead.
Update the implementation to pattern match on the error and check if it is the
`InvalidWireItem` variant; if so, convert it to `InvalidContribution` with the
error message, otherwise convert other compose errors to `Internal` as they
currently are.
---
Nitpick comments:
In `@crates/sessions/docs/COMPOSITION.md`:
- Around line 44-53: The status note about SessionComposer::compose being a
placeholder and Phases 2–4 being unwired contains implementation caveats that
conflict with the specification-focused tone of the rest of the document. Move
this entire note (including all references to compose_contribution,
UserComposer::compose, and the phase implementation status) into a separate
status or implementation notes section at the end of the document, or rewrite
the content in present tense to describe the actual current contract and
behavior rather than future planned changes.
- Around line 68-76: The Phase 2 section in the COMPOSITION.md documentation
incorrectly states that the daemon decides items based on the user's existing
policy, which contradicts the earlier established rule that user policy is
enforced only on the client. Reword the Phase 2 — Daemon collects and emits
pending items paragraph to remove any reference to the daemon applying policy
decisions, and instead clarify that the daemon only forwards already-gated items
and collects undecidable items as pending variants, ensuring the description
remains policy-free and consistent with the overall architecture where policy
enforcement happens exclusively on the client side.
🪄 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: 5a53ceaf-a6a4-4a17-a60b-6de728332a27
📒 Files selected for processing (22)
crates/minimald-rpc/src/lib.rscrates/sessions/docs/COMPOSITION.mdcrates/sessions/docs/RESOLUTION.mdcrates/sessions/src/client/composer.rscrates/sessions/src/client/mod.rscrates/sessions/src/core/compose.rscrates/sessions/src/core/decision.rscrates/sessions/src/core/enumerate.rscrates/sessions/src/core/expansion.rscrates/sessions/src/core/hooks.rscrates/sessions/src/core/lifecyclehook.rscrates/sessions/src/core/loadout.rscrates/sessions/src/core/mod.rscrates/sessions/src/core/policy.rscrates/sessions/src/core/primitives.rscrates/sessions/src/core/source.rscrates/sessions/src/daemon/composer.rscrates/sessions/src/daemon/mod.rscrates/sessions/src/lib.rscrates/sessions/src/wire/errors.rscrates/sessions/src/wire/primitives.rscrates/sessions/src/wire/request.rs
💤 Files with no reviewable changes (2)
- crates/sessions/docs/RESOLUTION.md
- crates/sessions/src/client/mod.rs
| let (mut composition, _final_policy) = | ||
| compose_contribution(self.contribution, policy, None, options, &*self.env)?; | ||
| composition.extend_from_wire(self.client)?; |
There was a problem hiding this comment.
Compose daemon patches with client vars already in scope.
compose_contribution expands daemon patches and policy before Line 113 imports self.client, so package/project sources like $HOME/... cannot use user-gated vars from the WireContribution and may fall back to the daemon process HOME instead. Convert/validate the client wire contribution first and feed its vars into daemon patch expansion before final assembly.
🤖 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/daemon/composer.rs` around lines 111 - 113, The order of
operations causes daemon patches to be expanded before client variables are
available in scope. Move the `extend_from_wire` call on `self.client` to occur
before calling `compose_contribution`, so that client-provided variables and
configuration from WireContribution are available when the daemon patches and
policy are expanded. This ensures that package/project sources can properly
resolve user-gated variables like $HOME from the client context rather than
falling back to daemon process environment variables. Refactor the code so that
`self.client` is processed and converted first, making its variables available
to the `compose_contribution` function.
|
/derive-spec |
89b2237 to
5068281
Compare
|
Pull request created: #529
|
|
A derived spec has been opened in a
Each unit carries The Gap Analysis calls out four items for human triage:
See
|
5068281 to
3b1440a
Compare
Rebuild the spec PR as docs-only. The sdd-spec derivation committed a large sessions-composition code refactor and several reverts alongside the spec document; a spec PR must change only the spec under docs/specs/. Reset every code and non-spec-doc file to current main (161b763), keeping just the derived spec so the PR no longer reverts merged work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
The execute branched before #528 (Split client and daemon) merged, so the diff reverted all of crates/sessions/** on top of #500's DNS work. Reset crates/sessions to match current main exactly, keeping only #500's intended minimald DNS registry changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
The execute branched before #528 (Split client and daemon) merged, so the diff reverted all of crates/sessions/** on top of #500's DNS work. Reset crates/sessions to match current main exactly, keeping only #500's intended minimald DNS registry changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS
…l hostname registry (#546) * feat(minimald): add *.localhost hostname registry, resolver probe, and HostNet registration * fix: drop stale-base revert of #528 sessions split from #500 PR The execute branched before #528 (Split client and daemon) merged, so the diff reverted all of crates/sessions/** on top of #500's DNS work. Reset crates/sessions to match current main exactly, keeping only #500's intended minimald DNS registry changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS * fix(minimald): address review feedback on hostname registry Respond to review comments on PR #546: - Mark `ProbeOutcome` `#[non_exhaustive]`, per the repository's Rust coding standards for public enums that may grow. - Stop leaking a hostname registry entry on session teardown: derive the deregister name from the record up front and let a read error propagate (the subsequent `delete` would fail on the same error) instead of silently dropping it via `.ok()`. `deregister` is called unconditionally since it is a no-op for a session that never registered a hostname. - Align spec R3.5 to the registry-by-name architecture: the structured tracing field is `session_name` (the registry key), not `session_id`, which is a distinct, separate identifier not available at this layer. Gate (native Linux, -p minimald, --locked): cargo fmt --check, build, clippy --all-targets -D warnings, and test (54 passed) all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(minimald): emit R3.5 session_id and deregister before delete Address the open, in-scope review comments on the hostname registry: - R3.5 tracing fields (dns.rs:102, deregister): the events emitted `session_name` only. R3.5 governs the tracing event field and calls for `session_id` -- the stable, unique identifier log pipelines correlate on, where `session_name` is mutable (RenameSession) and reusable after a session exits. Plumb the `SessionId` through `register`/`register_host_net`, store it on the registration so `deregister` emits the same id, and carry both `session_id` and `session_name`. Realign R3.5 in the networking spec to list both. - `ip` field format mismatch (dns.rs deregister): the `deregistered` event used Debug on `Option<IpAddr>` (`ip=Some(V4(127.0.0.1))`) while `registered` used Display (`ip=127.0.0.1`), so a log filter on `ip=127.0.0.1` missed deregistrations. `by_host` is kept in sync with `by_session` by `register`, so unwrap with `expect` and format with Display to match. - Registry entry leaked on teardown (sessions.rs DestroySession): the `?` on `store.delete` returned early before `deregister`, leaving a stale routing entry pointing at a destroyed session. Deregister the hostname before the fallible on-disk delete, so a delete failure leaves a repairable on-disk record but never a stale routing entry. Gate (native Linux, -p minimald, --locked): cargo fmt --check, build, clippy --all-targets -D warnings, and test --lib (54 passed) all green. * fix(minimald): parse bracketed IPv6 Host header in resolve The `Host:` header port strip used split-on-first-colon, which yields `[` for a bracketed IPv6 literal such as `[::1]:8080` instead of `[::1]`. Extract a `host_component` helper that strips the optional `:port` after the closing bracket for the IPv6 form and at the first colon otherwise. The registry only holds `*.localhost` names so an IPv6 literal never routes, but the parse is now correct rather than latently wrong. Addresses a review comment on #546. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(minimald): re-scope Unit 3 DNS to B5 host-side egress proxy Re-align PR #546 to the re-scoped #500 task body: the DNS mechanism is the spec B5 host-side egress proxy, not the systemd-resolved narrowing. - Keep the in-memory `HostnameRegistry` (register/deregister + R3.5/R3.6 structured tracing) but remove the resolver write/probe path; no host resolver is ever written or read. - Add `net/proxy.rs`: the B5 host-side egress proxy plus the shared `Router`/`HostRoute` routing core that #502 (B8 HTTPS/mTLS) extends. A forward/`CONNECT` proxy routes by `Host:` header / authority through the registry — `HostNet` to `127.0.0.1:<port>`, `OwnIp` to its gvproxy switch IP. The host resolver is never consulted; the TLD is opaque. - Replace the systemd-resolved startup probe (R3.4) with an egress-proxy listener reachability check that warns `component="dns-proxy", status="unavailable"` on bind failure. - Tests: registry/proxy routing contract, OwnIp routing to its switch IP, and the bind-failure warn; drop the systemd-resolved NXDOMAIN test. - Spec: close Open Question 1 with the B5 host-side-egress-proxy decision, superseding spike #485's systemd-resolved finding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(minimald): time-box egress proxy head read and clarify routing Address CodeRabbit and sdd-review feedback on the B5 egress proxy: - Bound the request-head read with a 30s timeout, returning 408 so a client that connects but never sends \r\n\r\n cannot hold a connection task indefinitely (proxy.rs read_head deadline). - Add a forward-proxy test asserting an absolute-form request target (GET http://web.dev.localhost/path) is routed by Host: header and replayed verbatim to the upstream, per RFC 9112. - Document on Router::route that the upstream port is client-supplied and per-PTask loopback port restriction is a deferred multi-tenant follow-up (accepted single-tenant limitation). - Report the startup bind check as status="reachable" rather than "listening", since the probe drops the listener without serving. * refactor(minimald): rename PTask TLD .localhost -> .min.internal Under the B5 host-side egress proxy the TLD is a free label (the proxy routes by Host header, no host resolver), so .min.internal is the semantically honest choice for internal PTask services and drops the misleading loopback connotation of .localhost. Renames the hostname suffix constant and updates the registry/proxy docs, routing tests, and the spec hostname format + Open Question 1 closure. Bare loopback localhost (mesh tunnel, listen address) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143kv2BRrRqGxmVwwHskQtS --------- Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Co-authored-by: Norrie Taylor <norrie@minimal.dev> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Splits the sessions crate's composition pipeline. The old monolithic Composer becomes a shared core (core::compose) plus two narrow composers:
Both delegate to shared
pub(crate)gate functions, so the policy logic lives in one place.Start with crates/sessions/docs/COMPOSITION.md — entry point to the new shape, with a mermaid sequence diagram and per-phase walkthrough.
Headline changes
source expansion). All affected types/functions/modules renamed.
complete) are gone.
Reviewer notes
a deny rule they've added to their own policy.
ContributionResponse. That routing isn't wired yet; non-user items that can't be auto-decided surface as ComposeError::HookRequired today. The hook-driven gate code still lives
in core::compose for the upcoming client-side verdict-generation path.
conflict-detection rules.
Summary by CodeRabbit
denyapplies to user-origin declarations as well.