Files
alkssh/tasks/implementation/w2-bounded-resources.md
T
glm-5.3-flash 5b2654f492 docs(tasks): close the wave-2 review gate — verdict green
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.
2026-10-28 11:53:51 +00:00

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
w2-spawn-return
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 beyond Limits::max_concurrent_channels are 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-mount mapped inactivity_timeout always-Some, max_auth_attempts, window_size, maximum_packet_size, channel_buffer_size from Limits) — 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_len is 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: beyond Limits::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 --check pass

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 with admit (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 as CHANNEL_CLOSE, firing the hook) — so the parking_lot convention's short-held shared state does not apply: no lock exists because no shared mutation does. Saturating release means 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 own MaxSessions answers the same code, so vanilla clients see the vanilla-shaped policy refusal (POC-1 §5).
  • Decrement invariant: every fired channel_close corresponds to exactly one previously-admitted open — russh only routes CHANNEL_CLOSE for 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_CLOSE arrives back, so Channel::wait() never delivers the close (the harness's await_close observes 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_attempts verified behaviorally: ceiling 2 (tunable) → two within-ceiling failures stay ordinary AuthResult::Failure riding the {publickey} advertisement; the attempt past the ceiling is answered with the server's Disconnect::NoMoreAuthMethodsAvailable, whose client-side shape is russh's synthesized Failure with an empty remaining-methods set (no reply came; the advertisement is gone) — the session ends cleanly server-side (row: tests/bounded_resources.rs auth_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_attempts mapping and the always-set inactivity_timeout are pinned one-for-one by the adapter's config-mapping unit tests (adapter.rs limits_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 the Config fields it owns); not cheaply behavioral, so recorded, per the task's split.
  • max_exec_command_len stays carried, not enforced (wave 3's grammar owns it) — unchanged from src/config.rs/src/dispatch.rs documentation; 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.