Files
alkssh/tasks/implementation/w2-bounded-resources.md
T
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
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.
2026-11-04 20:28:00 +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, 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 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, 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 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, 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 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, since 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, because 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, so 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), and 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. 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.