tasks: decompose the review-002 fix round — eight dependency-ordered tasks

The general review (docs/reviews/002-protocol-core-implementation-review.md)
decomposed into the fix-round group, grouped so no session carries
both a code-fix vertical and the test sweep:

- fix-bind-reply2-reentry: C-1 (the BIND reply#2 re-entry guard) +
  D-1 (the reader decision) + N-6 (the constructor prologue
  helper) + S-2 (the reap doc line) — one consumer session
- fix-producer-gate-policy: C-3 (the explicit BIND gate input) +
  D-2 (the register/builder coverage) + S-1/N-2 (the stage doc
  lines) — one producer session
- fix-rfc1928-posture: N-1 (the keep-or-cut pass) + N-4 (the honest
  refusal vocabulary) — one fork posture session
- fix-hygiene-residue: D-3 (the ErrorDefinition declaration) +
  N-3 (the unreachable let-else) + N-9 (the is_mid_frame note)
- fix-coverage-residue: N-8's attributed line list — tests only,
  after the code fixes (final shapes)
- fix-test-helper-consolidation: N-5 — after the coverage round
- architecture/adr-014-idle-timeout-posture: C-2 — the ADR-sized
  decision, independent, before the doors exist
- review-fix-round-002: the closing checkpoint; the batch-2 gate

The review doc's disposition table now names the landed tasks.
N-7 is recorded-only (the review's own recommendation).
This commit is contained in:
glm-5.3-flash committed 2026-09-24 11:18:21 +00:00
1 parent d612d7f7f2
commit 6d14d35a54
9 files changed
+828 -10

No files matched your search

@@ -0,0 +1,106 @@
---
id: architecture/adr-014-idle-timeout-posture
name: "ADR-014 — the idle-timeout posture for the pre-command handshake and the association (review-002 C-2)"
status: pending
depends_on: []
scope: narrow
risk: medium
impact: project
level: implementation
---
## Description
Review 002's C-2 (major): the pre-command RFC handshake and the
association have **no timeout surface** — a silent channel holds a
spawned task and reader state indefinitely (bounded memory,
unbounded time). The alkcall 10s establishment bound governs the
*open-op call*, not the in-stream conversation
(`src/producer.rs:183-191` parks on `accept_bi` → `accept_no_auth`
→ `read_command` with no timeout; the datagram stage's loop exits
only on EOF/error/data). BIND's quiet wait got an explicit decision
(ADR-007 — teardown-governed, "FTP-length waits stay legal"); the
handshake and the association lifetime got "inherited from the
channel" — implicit, never decided.
The mitigations are real (the 256-channel cap bounds the count,
memory per idle channel is bounded, teardown fires on channel
close — nothing leaks, nothing grows), but the aggregate is: any
identity with `socks5:open` can hold 256 parked tasks + 256 channel
entries indefinitely *without sending a byte*. And batch 2's
providers (`local`, `composed`) are about to route *vanilla
clients* through these same phases via the front doors, where the
peer is a kernel socket often outside alkcall's channel-cap regime
entirely — slowloris-shaped holds become the assembly layer's
problem, and the protocol layer is where a timeout seam would live.
**The deliverable is a decision, not code** — ADR-014
(`docs/architecture/decisions/014-*.md`), landing *before* the
`local` doors exist. Decide between the review's two options:
1. **Idle timeout is the substrate's/assembly's job** — the
protocol layer never times out (consistent with "no in-band
control vocabulary" and association-lifetime-equals-channel);
record the DoS posture explicitly (the cap bounds it; doors
apply their own accept timeouts). Cheapest, defensible.
2. **A producer-side idle bound** — an optional per-session
inactivity timeout at the pump level (config on
`Socks5ProducerBuilder`), enforced by racing the handshake/stage
reads with a sleep timer; a timeout = clean EOF-shaped teardown,
no reply (the RFC has no timeout reply code; 0x01
`GeneralFailure` on a pre-reply timeout is the closest and is
what upstream's connect-timeout path surfaces). More work;
touches no wire format.
The ADR must state, whichever option wins: the BIND quiet wait's
existing decision (ADR-007) and how the new decision relates to it
(same regime or different — and why the difference is principled);
the DoS posture in the doors' terms (what a `local` door's accept
path must do if the protocol layer does not); and whether the
decision is revisitable post-batch-2 or is itself a one-way door
(it should not be — a timeout seam is additive — but say so).
Also update `docs/architecture/open-questions.md` if C-2 was
tracked there, and the affected spec lines (wire.md §UDP
ASSOCIATE's association-lifetime bullet; producer.md's pump
description) to carry the decided posture rather than the implicit
one.
## Acceptance Criteria
- [ ] ADR-014 exists at
`docs/architecture/decisions/014-<slug>.md`, status accepted,
deciding explicitly between the two options (or a reasoned
third)
- [ ] The BIND quiet wait's ADR-007 relationship is stated (same
regime or a principled difference)
- [ ] The doors' DoS posture is recorded (what the assembly layer
must do if the protocol layer does not)
- [ ] The affected spec lines (wire.md, producer.md) carry the
decided posture — no implicit residue
- [ ] The decision lands before any `local`/`composed` door work
(the batch-2 tasks may depend on this one)
- [ ] `cargo doc --no-deps` clean if any doc-comment carries the
posture; the full verification sweep green
## References
- docs/reviews/002-protocol-core-implementation-review.md §C-2 (the
finding — the task's source of truth)
- docs/architecture/decisions/007-bind-in-base.md (the quiet wait's
existing decision — the contrast case)
- docs/architecture/decisions/008-provider-defaults.md,
009-feature-gates.md (the batch-2 provider posture the decision
precedes)
- docs/architecture/wire.md §UDP ASSOCIATE (association lifetime =
channel lifetime — the contract the decision qualifies)
- The POC findings (the stage's measured shapes — the
"sufficient for relay postures" lineage)
## Notes
> Agent fills this during implementation.
## Summary
> Agent fills this on completion.