Gate reading (commits b90e89b..196289f) against the architecture line by line: ADR-001 mount contract, ADR-002 Config construction, ADR-003 feature gates, ADR-004 dispositions, ADR-006 constraints, mapping.md skeleton decline matrix per build, bounded resources, and the harness — all confirmed. All four flagged interpretations confirmed as recorded. Fixed inline before committing (task-file hygiene only): - w2-loopback-harness, w2-dispatch-decline-core: acceptance boxes checked once verified against the landed rows/runs - w2-bounded-resources: stale placeholder lines removed - review-wave-2: verdict recorded Booked finding (next wave's decomposition): decline-path tracing/observability — the reason a given wire decline carried is currently docs-only (tracing unused in src/). Verification: cargo test 104 rows green; --all-features green; clippy ×2 -D warnings green; fmt clean; cargo doc 0 warnings; taskgraph validate 19 tasks.
8.0 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w2-bounded-resources | Wave 2 — bounded resources (the in-crate channel cap at the handler surface) | completed |
|
narrow | low | component | implementation |
Description
Enforce the in-crate bounded-resources posture at the handler surface
(overview.md §Bounded-resources policy; AGENTS.md convention 9 —
SSH servers are internet-facing; unbounded loops and unbounded buffers
are bugs):
- The in-crate concurrent-channel cap — a count of active channels
enforced at every
channel_open_*hook: opens beyondLimits::max_concurrent_channelsare declined fail-closed (a clean refuse, never queued, never a half-open channel). The count decrements on channel close so the cap recovers. Short-held internal state (parking_lot, the project convention). - The russh knobs are already wired (
w2-server-mountmappedinactivity_timeoutalways-Some,max_auth_attempts,window_size,maximum_packet_size,channel_buffer_sizefromLimits) — this task verifies them behaviorally where cheap (e.g., the auth-attempt ceiling disconnects an exceeding client through the harness) and records anything not cheaply observable as covered by the config-mapping tests. - Per-length exec-parse limits stay reserved for the wave-3
grammar's consumption (
Limits::max_exec_command_lenis carried, not yet enforced — the grammar owns it).
The per-identity policy layer (when the alkcall bridge carries it) is additional and arrives with the bridge itself (wave 3's tunnel leg) — not from this task.
Acceptance Criteria
- The concurrent-channel cap is enforced at every
channel_open_*hook: beyondLimits::max_concurrent_channels→ clean decline; the count recovers on close - Harness tests: cap reached → next open declines; a close → an
open succeeds again; the cap is
Limits-tunable - The wired russh knobs verified behaviorally where cheap (auth-attempt ceiling through the harness) or recorded as covered by the config-mapping tests
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
References
- docs/architecture/overview.md §Bounded-resources policy (the knob table)
- docs/architecture/mapping.md §Data-plane / limits
- docs/plans/implementation.md §Wave 2 (the bounded-resources bullet)
- src/config.rs (
Limits— the budget model and its documented postures)
Notes
- The count is handler-owned, not locked. New private module
src/channel_cap.rs(ChannelCap): the per-session active-channel count withadmit(check+increment at accept sites) /has_room(pure check) /release(saturating decrement). The only writer is the connection's russh packet loop — every hook runs inline with exclusive&mut self(ADR-006 decision 4 puts serving paths on spawned tasks, but their closes cycle back through the packet loop asCHANNEL_CLOSE, firing the hook) — so theparking_lotconvention's short-held shared state does not apply: no lock exists because no shared mutation does. Saturatingreleasemeans a stray decrement cannot manufacture capacity. - Gate ordering (handler.rs): feature gate first, then the cap.
Session-channel opens admit-and-count exactly at the accept point.
The tunnel rows check the cap before the seam stub — the stub
declines without opening a channel, so a counted admission it never
opens would leak against the cap forever (decline-first-alloc-never
applies to the budget, not just capability). Wave 3's
accept-after-fabric-open flow replaces the stub and moves admission
to the real accept point; the wire behavior this wave: a
cap-exhausted tunnel row answers the cap reason instead of the
seam's
ConnectFailed(pinned by a row, not by convention). x11 declines in every build and never consumes a count. - Decline reason:
dispatch::cap_exceeded_open_decline()→AdministrativelyProhibited— the cap is the assembly's configured policy ceiling; OpenSSH's ownMaxSessionsanswers the same code, so vanilla clients see the vanilla-shaped policy refusal (POC-1 §5). - Decrement invariant: every fired
channel_closecorresponds to exactly one previously-admitted open — russh only routesCHANNEL_CLOSEfor channels it considers established (a declined open never established one) and consumes the close at most once (the channel is removed from russh's map before the hook). - Recovery observability (russh-pin fact, recorded in the test
doc): a client-initiated channel close is not observable
client-side at this pin — russh removes a locally-closed channel
from its client map before the server's
CHANNEL_CLOSEarrives back, soChannel::wait()never delivers the close (the harness'sawait_closeobserves only server-initiated closes, which is what the spawn-return/dispatch rows use). The recovery row therefore rests on wire ordering (deterministic): close and open ride the client loop's single FIFO event queue, are written in that order, and the server's packet loop processes them in order — the counted release precedes the recovery admission; had the release not happened, the recovery open declines and the row fails. max_auth_attemptsverified behaviorally: ceiling2(tunable) → two within-ceiling failures stay ordinaryAuthResult::Failureriding the{publickey}advertisement; the attempt past the ceiling is answered with the server'sDisconnect::NoMoreAuthMethodsAvailable, whose client-side shape is russh's synthesizedFailurewith an empty remaining-methods set (no reply came; the advertisement is gone) — the session ends cleanly server-side (row:tests/bounded_resources.rsauth_attempt_ceiling_disconnects_an_exceeding_client).- The size/window/clock knobs recorded as config-mapping-covered:
window_size,maximum_packet_size,channel_buffer_size,max_auth_attemptsmapping and the always-setinactivity_timeoutare pinned one-for-one by the adapter's config-mapping unit tests (adapter.rslimits_knobs_map_one_for_one_with_inactivity_always_set,default_limits_map_to_the_documented_postures); the client direction's keepalive/wall-clock postures are pinned by the harness (client_config_maps_the_limits_postures). Re-driving them through the wire buys no new information (russh reads theConfigfields it owns); not cheaply behavioral, so recorded, per the task's split. max_exec_command_lenstays carried, not enforced (wave 3's grammar owns it) — unchanged fromsrc/config.rs/src/dispatch.rsdocumentation; nothing here consumes it.- Verification:
cargo test(default), and--features session/git/tunnel/--all-features(the feature-gated cap rows run in the builds whose opens can be accepted),cargo clippy --all-targets -- -D warnings(+--all-features),cargo fmt --check,cargo doc --no-deps— all green.
Summary
Landed the in-crate concurrent-channel cap at the handler surface:
new private channel_cap module (count + admit/release semantics,
unit-tested), wired into SessionHandler's every open hook — the
session accept path admits-and-counts at the accept, the tunnel rows
cap-gate before the seam stub, x11 is unaffected — and decremented
through the new channel_close hook (single release point, single
writer). Cap-exceeded opens decline fail-closed with
AdministrativelyProhibited (dispatch::cap_exceeded_open_decline,
OpenSSH MaxSessions parity). Contract rows in
tests/bounded_resources.rs: cap reached → clean decline; close →
open succeeds again (wire-order deterministic); zero cap refuses
everything without poisoning; exhausted cap answers the tunnel row
with the cap reason; the max_auth_attempts ceiling verified
behaviorally through the harness. The per-identity policy layer stays
with the wave-3 bridge; the per-length exec-parse cap stays the
grammar's. All five feature-build test runs, clippy (both) and fmt
green.