Skip to content

chore(sessions): Split client and daemon loadout resolution logic - #528

Merged
evanspearman merged 2 commits into
mainfrom
evan/split03
Jun 23, 2026
Merged

chore(sessions): Split client and daemon loadout resolution logic#528
evanspearman merged 2 commits into
mainfrom
evan/split03

Conversation

@evanspearman

@evanspearman evanspearman commented Jun 22, 2026

Copy link
Copy Markdown
Member

Splits the sessions crate's composition pipeline. The old monolithic Composer becomes a shared core (core::compose) plus two narrow composers:

  • UserComposer (client) — takes loadouts, returns a WireContribution
  • SessionComposer (daemon) — takes the client's wire contribution + project/package contributions, returns the final Composition

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

  • Pipeline rename: resolve (the gate pipeline) → compose. "Resolve" is now reserved for turning a deferred reference into a concrete value (ResolvedVar::resolve_with, patch
    source expansion). All affected types/functions/modules renamed.
  • Single-round wire flow: the daemon ↔ client dialogue is one round-trip (SessionCreate → ContributionResponse → ContributionVerdict). The multi-round wire fields (round,
    complete) are gone.
  • client::enumerate and client::hooks moved to core:: — they're shared infrastructure; fixes a layering inversion where core::compose was importing from client::.
  • docs/RESOLUTION.md → docs/COMPOSITION.md, fully rewritten to describe the new linear 4-phase pipeline.

Reviewer notes

  • User policy deny now applies to user-origin items. Previously Source::UserLoadout bypassed both allow and deny; now it auto-passes only allow. Rationale: protect the user from
    a deny rule they've added to their own policy.
  • SessionComposer::compose takes no hooks and doesn't return the policy. Architectural rule: hooks run on the client only — the daemon routes pending items back via
    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.
  • Contribution::merge is fallible: Result<Contribution, Conflict>. Conflict is #[non_exhaustive] and empty today (merge is pure concat); the shape is reserved for upcoming
    conflict-detection rules.
  • ItemDecision::DenyOnce removed — functionally identical to HookResult::Abort.

Summary by CodeRabbit

  • Documentation
    • Added/updated end-to-end “Session Composition” and session-creation RPC flow docs, including clarified gating stages, invariants, and failure modes.
  • New Features
    • Streamlined session creation to use an already-gated contribution payload, simplifying the client/daemon exchange to a single gating round-trip before submission.
    • Introduced daemon-side session composition support and improved hook/lifecycle data conversion for the wire boundary.
  • Bug Fixes
    • Updated policy precedence so deny applies to user-origin declarations as well.
  • Refactor
    • Reworked the client/daemon composition pipeline and revised wire request/response structures and error mapping accordingly.

@coderabbitai

coderabbitai Bot commented Jun 22, 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: 8408f1ab-0430-4728-8b5d-afdfc1a23879

📥 Commits

Reviewing files that changed from the base of the PR and between 5068281 and 3b1440a.

📒 Files selected for processing (4)
  • crates/sessions/src/client/composer.rs
  • crates/sessions/src/core/compose.rs
  • crates/sessions/src/daemon/composer.rs
  • crates/sessions/src/wire/errors.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/sessions/src/daemon/composer.rs
  • crates/sessions/src/wire/errors.rs
  • crates/sessions/src/core/compose.rs
  • crates/sessions/src/client/composer.rs

📝 Walkthrough

Walkthrough

The PR refactors session composition out of the client composer into a shared core/compose.rs module, rebuilds UserComposer (client) and SessionComposer (daemon) on top of it, simplifies the wire session-create protocol to a single round-trip (removing multi-round fields), updates policy precedence so deny applies before user-origin auto-allow, and replaces RESOLUTION.md with a new COMPOSITION.md spec.

Changes

Session composition and wire-flow rearchitecture

Layer / File(s) Summary
Shared core composition contracts and conversions
crates/sessions/src/core/mod.rs, crates/sessions/src/core/compose.rs, crates/sessions/src/core/primitives.rs, crates/sessions/src/core/lifecyclehook.rs, crates/sessions/src/core/source.rs
Introduces Contribution, Composable, StoredEnv, ComposeError, SessionVar, SessionPatch, Composition, ComposeOptions in core/compose.rs. Adds into_parts destructuring and wire↔domain From/TryFrom conversions to ResolvedVar, ResolvedPatch, HookScript, LifecycleHook, ProvenancedPackage, and ProvenancedHook. Removes Patches::with_patch in favor of push.
Policy precedence and decision semantics updates
crates/sessions/src/core/policy.rs, crates/sessions/src/core/decision.rs, crates/sessions/src/core/hooks.rs
Reorders VarsPolicy::check and ExpandedPatchPolicy::decide so deny is evaluated before user-origin auto-allow. Removes ItemDecision::DenyOnce; per-item denial now aborts the whole composition. Aligns all hook/decision documentation to reference the "gate" rather than the "resolver".
Gate execution and compose orchestration
crates/sessions/src/core/compose.rs, crates/sessions/src/core/enumerate.rs, crates/sessions/src/core/expansion.rs
Implements gate_vars (3-pass categorize/prompt/apply), expand_patch_sources, gate_patches (3-pass with hook-driven re-expansion), and compose_contribution orchestrator. Switches enumerate_patch_files return type from ResolveError to ComposeError. Updates expansion and enumerate terminology to "compose invariant". Adds comprehensive unit tests including user vs. project origin handling, hook contract validation, patch glob expansion, and Unix symlink walking.
Client UserComposer rewrite and loadout integration
crates/sessions/src/client/composer.rs, crates/sessions/src/client/mod.rs, crates/sessions/src/core/loadout.rs
Replaces the old client resolution pipeline with UserComposer accumulating loadouts via add/add_all and delegating to compose_contribution/composition_to_wire. Migrates Loadout to implement core::compose::Composable. Removes enumerate and hooks from client module exports. Tests verify wire emission, ignore filtering, env override behavior, and end-to-end compilation.
Wire protocol simplification and boundary conversions
crates/sessions/src/wire/request.rs, crates/sessions/src/wire/primitives.rs, crates/sessions/src/wire/errors.rs
Replaces ResolvedContribution with WireContribution in SessionCreateRequest. Removes round/complete from ContributionResponse and round from ContributionVerdict. Renames SessionStep::Round to SessionStep::Response. Adds domain→wire From impls for all core primitives and wires error handling from ResolveError to ComposeError. Updates round-trip serialization tests and pending-item correlation-token documentation.
Daemon SessionComposer and crate wiring
crates/sessions/src/daemon/composer.rs, crates/sessions/src/daemon/mod.rs, crates/sessions/src/lib.rs
Introduces SessionComposer seeded from a client WireContribution, accumulating daemon Composable contributions via add/add_all, running compose_contribution on the daemon side, and extending the result with the client wire contribution via extend_from_wire. Wires the new daemon module into the crate's public surface. Tests verify empty and merged compositions.
Composition and RPC documentation alignment
crates/sessions/docs/COMPOSITION.md, crates/minimald-rpc/src/lib.rs
Adds COMPOSITION.md describing the 4-phase pipeline, shared client-side gate pipeline with patch pre-walk/3-pass flow, vocabulary (resolve/gate/compose distinction), and key end-to-end invariants and failure modes. Removes RESOLUTION.md. Updates RPC docstrings to reflect the single-round-trip gated flow and SubmitVerdict assembling the final session.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • gominimal/minimal#348: Introduced Composable/Contribution/Composer concepts that this PR refactors into core/compose.rs and rebuilds the client/daemon composer model on top of.
  • gominimal/minimal#427: Reorganized sessions/src/client/composer.rs in the same areas this PR further transforms, moving toward the core gating pipeline and shared core types.
  • gominimal/minimal#443: Added multi-round SessionCreate/SubmitVerdict/SessionAbort RPCs whose wire message shapes are directly replaced by the single-round-trip contract introduced here.

Suggested labels

needs-human

Suggested reviewers

  • norrietaylor

Poem

🐇 A resolver once hopped through the code,
But "gate" and "compose" now share the road.
Four phases, one trip, verdicts sent back neat,
The daemon assembles with nothing to repeat.
DenyOnce is gone — abort is the rule!
Provenance travels, the pipeline is cool. 🌿

🚥 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 PR title 'Split client and daemon loadout resolution logic' accurately describes the main architectural change of dividing the monolithic Composer into UserComposer (client) and SessionComposer (daemon), though it uses 'resolution' terminology that the PR itself updates to 'composition'.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor

This pull request has no accompanying spec. Comment /derive-spec to have one derived retrospectively from the code — it opens a separate spec/<slug> documentation PR with demoable units, acceptance criteria, and a gap analysis (implementation gaps, missing failure paths, weak acceptance criteria). Ignore this to defer; the weekly unspecced-PR scan will re-surface it. See ADR 0027.

@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

🧹 Nitpick comments (2)
crates/sessions/docs/COMPOSITION.md (2)

44-53: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Move the transition note out of the main spec.

This reads like an implementation caveat (SessionComposer::compose placeholder, 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 win

Keep 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa2cd7d and 3707a08.

📒 Files selected for processing (22)
  • crates/minimald-rpc/src/lib.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/docs/RESOLUTION.md
  • crates/sessions/src/client/composer.rs
  • crates/sessions/src/client/mod.rs
  • crates/sessions/src/core/compose.rs
  • crates/sessions/src/core/decision.rs
  • crates/sessions/src/core/enumerate.rs
  • crates/sessions/src/core/expansion.rs
  • crates/sessions/src/core/hooks.rs
  • crates/sessions/src/core/lifecyclehook.rs
  • crates/sessions/src/core/loadout.rs
  • crates/sessions/src/core/mod.rs
  • crates/sessions/src/core/policy.rs
  • crates/sessions/src/core/primitives.rs
  • crates/sessions/src/core/source.rs
  • crates/sessions/src/daemon/composer.rs
  • crates/sessions/src/daemon/mod.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/wire/errors.rs
  • crates/sessions/src/wire/primitives.rs
  • crates/sessions/src/wire/request.rs
💤 Files with no reviewable changes (2)
  • crates/sessions/docs/RESOLUTION.md
  • crates/sessions/src/client/mod.rs

Comment thread crates/sessions/src/client/composer.rs Outdated
Comment thread crates/sessions/src/daemon/composer.rs Outdated
Comment on lines +111 to +113
let (mut composition, _final_policy) =
compose_contribution(self.contribution, policy, None, options, &*self.env)?;
composition.extend_from_wire(self.client)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

Comment thread crates/sessions/src/wire/errors.rs
@evanspearman

Copy link
Copy Markdown
Member Author

/derive-spec

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor

Pull request created: #529

Generated by sdd-derive

@gominimal-aw-bot

Copy link
Copy Markdown
Contributor

A derived spec has been opened in a spec/sessions-composition-pipeline documentation PR. It covers five demoable units:

  1. Shared core::compose gate pipelineContribution, Composable, gate_vars/gate_patches, compose_contribution
  2. Client UserComposer — loadout accumulation → WireContribution
  3. Daemon SessionComposer — seeded from wire, daemon composables, extend_from_wire
  4. Wire protocol single-round-tripWireContribution, ContributionResponse (no round/complete), SessionStep::Response, Abort/AbortReason
  5. Policy gate semantics and vocabularydeny before user-origin auto-allow, DenyOnce removal, module restructuring, COMPOSITION.md

Each unit carries R{unit}.{seq} acceptance criteria and 1–2 proof artifacts (all test-based, passing against the current tree).

The Gap Analysis calls out four items for human triage:

  1. Phases 2–4 not wired (daemon ContributionResponse + client Phase 3 verdict handler absent)
  2. Contribution::merge conflict detection absent (Conflict enum uninhabited)
  3. No test for var-name collision across the wire boundary
  4. ItemDecision::DenyOnce removal is a breaking hook-API change without an in-code migration note

See docs/specs/05-spec-sessions-composition-pipeline/ on the spec/sessions-composition-pipeline branch.

Generated by sdd-derive for issue #528 ·

norrietaylor added a commit that referenced this pull request Jun 22, 2026
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
@evanspearman
evanspearman merged commit 1b89403 into main Jun 23, 2026
63 checks passed
@evanspearman
evanspearman deleted the evan/split03 branch June 23, 2026 14:26
norrietaylor added a commit that referenced this pull request Jun 23, 2026
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
norrietaylor added a commit that referenced this pull request Jun 23, 2026
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
norrietaylor added a commit that referenced this pull request Jun 23, 2026
…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>
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