Files
alkssh/tasks/implementation/w2-bounded-resources.md
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

156 lines
8.0 KiB
Markdown

---
id: w2-bounded-resources
name: "Wave 2: bounded resources (the in-crate channel cap at the handler surface)"
status: completed
depends_on: [w2-spawn-return]
scope: narrow
risk: low
impact: component
level: 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
- [x] The concurrent-channel cap is enforced at every
`channel_open_*` hook: beyond `Limits::max_concurrent_channels`
→ clean decline; the count recovers on close
- [x] Harness tests: cap reached → next open declines; a close → an
open succeeds again; the cap is `Limits`-tunable
- [x] The wired russh knobs verified behaviorally where cheap
(auth-attempt ceiling through the harness) or recorded as
covered by the config-mapping tests
- [x] `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.