Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
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, and 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, where 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, because 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, since 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, because 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, so 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), and 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. Counts decrement
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.