- version 0.8.0 -> 0.8.1 (bug-fix release; semver-checks 196 pass vs 0.8.0)
- CHANGELOG 0.8.1: the manager_routing-found duplicate adopt/open fix,
the fuzz harness, and the verification summary
- publish exclude gains docs/research/ (internal research notes;
fuzz/ was already excluded — package verified clean of both)
- fuzzing.md §7.9: standing no-hosted-CI policy — corpus replay is the
release-verification fuzz gate; campaigns manual via the detached
runner; OSS-Fuzz out; no workflow files in this repo
- AGENTS.md: corpus replay added to the verification checklist
- lockfiles: alkcall 0.8.1 (root + fuzz)
Verification: 684 tests; clippy -D warnings (main, wasm, fuzz/shared);
fmt clean; doc clean; wasm32-unknown-unknown check+clippy clean;
semver-checks 196 pass; publish --dry-run 116 files (no fuzz/research/
reviews/sdd/AGENTS in the tarball); fuzz corpus replay 5/5 green
\u00a77.8 campaign results: manager_routing 1.06M execs (exact-counter and
parked-bytes invariants held across every adversarial sequence),
envelope_semantic 2.44M execs (coverage saturated, all event kinds
round-trip), spec_parse 10.8M execs (registry compiled every
attacker-shaped schema or rejected cleanly). All exited 0, zero
artifacts.
Also records the two harness-model defects the fuzzer caught in the
interim (per-receiver EOF-sentinel latch; odd/even Adopt range per
ADR-047 \u00a75). Doc-only + harness fixes; no crate changes.
Found by the manager_routing fuzz target (docs/research/fuzzing.md \u00a77.8):
- ChannelManager::open_channel / adopt_channel used
HashMap::insert(...).is_some() as a collision check \u2014 insert REPLACES
the existing entry, so a duplicate adopt/open installed the new state,
dropped the live channel's demux_sender (spurious EOF to its readers,
subsequently routed chunks lost) and still returned
Err(ChannelExists). Fixed with contains_key pre-check; map untouched
on collision. Regression tests: adopt_channel_duplicate_id_leaves_
live_channel_intact, open_channel_duplicate_id_leaves_live_channel_
intact.
New fuzz targets (\u00a77.4 step 3):
- manager_routing: Arbitrary op sequences over ChannelManager; exact
counter models (parked/dropped must equal the manager's monotonic
counters), parked-bytes bound per \u00a76.2-1, clear_all ledger-vs-map
semantics, drainer-byte reconciliation (lossless routing)
- envelope_semantic: constructors -> serde -> write_frame/read_frame
structural round-trip; event-type constants; call.error parse-back
- spec_parse: OpRegisterRequest::from_json -> rebuild -> registry
registration (attacker schemas compile at register, CF-003)
- fuzz/shared/src/arbitrary_value.rs: bounded Arbitrary for
serde_json::Value; 20 spec_parse + 4 manager_routing seeds
- corpus replay for the new targets in fuzz/shared tests
Verification: cargo test 684 passed (682 + 2 regression); clippy
-D warnings clean (main + fuzz/shared); fmt clean (main + fuzz);
cargo fuzz build clean; 20 s smoke on all three new targets clean
(manager_routing 79k, envelope_semantic 141k, spec_parse 517k runs);
crash input replays clean post-fix
chunk_header: saturated (203.9M execs, ~350k exec/s, 33 fork jobs clean);
envelope_frame: 2119 edges/1664 corpus entries, still growing at budget
end, 7.08M execs, 33 fork jobs clean. No crash/OOM/timeout artifacts in
either 10-min detached campaign; exit 0 both. Grown corpus not merged
(per \u00a77.1 corpus policy). Doc-only change.
- fuzz/ workspace (nightly-pinned via rust-toolchain.toml, excluded from
the main workspace and the published package): chunk_header and
envelope_frame targets per fuzzing.md \u00a77.1
- invariant logic in fuzz/shared (stable-toolchain crate): committed
corpus replay as plain cargo test (quinn CI pattern, \u00a77.2 tier 3)
- envelope target adds FrameError shape-partition asserts, exact
consumption accounting, structural write_frame round-trip, serde
key-contract, and a trailing-byte probe (prefix counts body only)
- chunk_header target adds round-trip identity, TooLarge/short error
shape, is_eof, 8-byte consumption, input-never-mutated
- committed seed corpora: 245 deterministic seeds via
fuzz/gen_fuzz_seeds.py (truncations, len=0/MAX+1/u32::MAX, channel 0,
invalid UTF-8, deep nesting); grown corpora + artifacts gitignored
- fuzz/run-detached.sh: \u00a77.6 detached runner (setsid+nohup+log, fork
mode, rss/malloc limits) \u2014 campaigns never share fate with a session
- fuzz/json.dict; README; research doc \u00a77.7 records step-1 status
Verification: cargo fuzz build clean; stable side green (cargo test 682
passed, clippy -D warnings, fmt --check incl. fuzz/shared); corpus
replay 245 seeds green; detached 10-min campaigns on both targets
completed with zero crashes/OOMs/timeouts
- OOM in a fuzz target must cost the fuzzer, never the opencode host
- three defense layers: soft rss/malloc limits, fork-mode blast radius,
setsid+nohup detachment with log-file polling (fuzz/run-detached.sh)
- no ulimit -v with ASAN; corpus replay + CI flag parity notes
- sequencing and summary updated to make the detached runner mandatory
Review 009's coverage debt in full — the paths the review-008 gates
never walk. No wire or API changes.
- C-1: pin the plain-bundle install-failure arm (un-compilable
input_schema; the as-filed duplicate-name route does not fail
registration) — the install task ends before the dispatch loop,
channel 0 never dispatches. ADR-051 §5 gains the loud-install
coverage note: relay-openable + plain-bundle arms pinned,
generic-ops + bootstrap-discovery arms documented as
best-effort-loud (crate-internal specs compile by construction).
- C-2: the HubLegImports filter — filtered closure path + only
partition unit-tested (errata: only was already pinned at filing);
the empty-stash e2e gate (generic ops + discovery only, dropped
ops resolve NOT_FOUND).
- C-3: the batch-form reserved-reply-key rejection pinned (reason
handler_error, teardown, ledger decrement, no pump spawn).
- C-4: open_channel_with_reply's failure path pinned e2e (the typed
error carries the channel:open_failed code + details reason/message).
- C-5: both byte-identical claims golden-pinned — the no-fields reply
against {"channel_id": 2} and the standard-shape wire payload
against the full 9-key literal.
- C-6: derivation edge shapes pinned — channels//sub, channels//direct,
channels → None; the 4-segment strict superset annotated as the
pre-amendment behavior change (errata: actual is Some("x/sub"), the
multi-segment-ALPN rule, not the as-filed Some("alk/x/sub")).
- C-7: builder overwrite pinned last-win (single + batch) with the
doc sentence on with_reply_field.
Verification: 682 tests pass, clippy -D warnings clean, fmt clean,
doc clean, wasm32 check clean.
File: docs/reviews/009 (resolved; errata marked per finding)
Post-landing audit of the 0.7.1 -> 0.8.0 remediation diff: two
hardening guards, one rejection-posture fix, two log/message
corrections, and the deferred coverage debt filed as review 009.
- RelayPlan owns the producer-leg ChannelManager and reclaims the
adopted spoke channel_id via a Drop guard (replaces the pump
handler's post-pump_bidi explicit reclaim). Closes the leak
windows the pump's normal path cannot reach: the wrapper's
establishment bound expiring after the adopt, and the pump
handler's early-return arms (plan absent, downcast failure,
try_unwrap failure, accept_bi failure). The send-half drop still
EOFs the spoke leg via the mux pump's implicit-EOF sentinel, so
the spoke-side cascade is unchanged. ADR-051 §6 documents the
closed post-adopt window (the pre-adopt §6 window and the
inside-adopt_channel cancellation point stay as documented).
- rebuild_spec_for trims and rejects empty/whitespace
channel_open_alpn strings — an empty explicit string previously
overrode a sane name-derived ALPN.
- op_name_is_standard_channel_open_shape applies the same
empty-segment guard as the derivation: channels//sub no longer
serializes boolean-only and then reconstructs unmarked (silent
stub for a marked op); the explicit string rides instead.
- reserved_reply_key_call_error interpolates RESERVED_REPLY_KEY;
the establisher-bug log fires at warn! (programming error).
- Regression tests: the plan drop guard, the empty-ALPN fallback,
the empty-segment shape check (672 tests, 3 new).
- CHANGELOG [Unreleased] entry for the audit fixes.
- docs/reviews/009 — the audit's deferred test-coverage gaps
(template failure arms, filtered/only, batch reserved key,
wire failure path, golden pins, derivation edge shapes, builder
overwrite semantics), each with the test to add and gates.
Verification: cargo test 672 passed; clippy --all-targets -D
warnings clean; fmt --check clean; doc --no-deps clean; wasm32
check clean.
- src/channels/gate2_tests.rs (ADR-051 gates 6/7, review 008 gate 2):
the full consumer -> hub (HubLegTemplate) -> spoke relay e2e —
the open resolves with the hub-allocated channel_id, `bound`
survives the relay, data flows both directions with a fake 8-byte
chunk header riding verbatim (the hub never parses the data plane,
ADR-034/035), spoke-side close cascades to clean reclaim on both
legs; hub-side disconnect (the consumer's duplex end dropped via a
killable transport proxy) tears down both legs with the ledger
decremented; the mid-establishment window (ADR-051 §6) pinned with
its two reclaim signals — the consumer leg reclaims at its own
transport EOF, the spoke channel (allocated before the establisher
replied) is the honest residual, reclaimed when the spoke-leg
transport ends; the channels/tty/sub standard-shape companion pins
no derivation regression.
- src/channels/relay.rs: the relay's adopted producer-leg channel
entry now reclaims when the relayed pump completes (teardown after
pump_bidi) — the consumer-leg wrapper's teardown cannot see the
producer leg's manager; the RelayPlan carries the spoke id for the
reclaim.
- Release bookkeeping: 0.7.1 -> 0.8.0, the CHANGELOG entry covering
Units 1-3 (Establishment reply projection, open_channel_with_reply,
flavor-form discovery derivation, ChannelRelay, HubLegTemplate,
gate-2 harness); review 008 Status -> Resolved with the U-1/U-2
commit refs and the 955->945 errata note; ADR-051 Status ->
all units landed + the §6 mid-establishment residual expanded to
the two-reclaim-signal shape the gate pins; the stale "0.7.2"
version mentions in ADR-047/049 corrected to 0.8.0 (the units land
unreleased).
Verification: cargo test 669 passed / 0 failed; clippy --all-targets
-- -D warnings clean; fmt --check clean; cargo doc --no-deps clean;
cargo check + clippy on wasm32-unknown-unknown clean;
cargo publish --dry-run --allow-dirty passed.
- src/channels/hub_leg.rs: HubLegImports (the Clone discover/stash,
bundles split by the channel_open marker, with the per-consumer
filter) and HubLegTemplate (the hub-leg install hook: per-connection
fork + generic channel ops + plain bundles as-is + marked specs via
ChannelRelay::register_relay_openable + bootstrap discovery closed
over the fork (review 004 F-06) + serving-identity resolution
(CF-005) + the single-stream dispatch loop).
- The loud assembly posture holds at install: a Pub-typed (or
unregistrable) marked spec ends the leg's install task — channel 0
never dispatches, never a silent stub (ADR-051 §6).
- Assembly rule surfaced by the gate e2e: from_call discovers the
spoke's bootstrap discovery ops like any plain op; the template
skips BOOTSTRAP_DISCOVERY_OPS (new pub const, discovery.rs) when
re-registering plain bundles — the template's own install, closed
over the fork, supersedes the imported copies.
- Gate tests (7): stash split + filter, the full template e2e
(services/list shows the re-exposed ops, a plain imported op
round-trips through the hub, the marked op opens through the relay
with bound surviving, data both directions), serving-identity
precedence (scope-bearing override resolves, scope-less fails
closed), the loud Pub-typed posture, and the filtered-stash subset
run (NOT_FOUND for the filtered-out marked op).
- Docs: ADR-051 status (3a + 3b implemented, 3c remains); review 008
Unit 3b landed note (the bootstrap-op skip rule).
Verification: cargo test (665 passed), clippy --all-targets
-D warnings, fmt --check, doc --no-deps — all clean.
Design session outcome for the relay unit. The hub/spoke family (hub
or spoke may relay; the hub re-exposes spoke services by ACL without
binding ports) needs the call-half support too, so the unit's scope
grew beyond the as-pinned sketch and is now three sub-units.
- ADR-051 (new): the relay is the wrapper composition — translate hop
= the ADR-049 establisher shape, byte-forward hop = pump_bidi per
ADR-050; the relay holds Arc<CallConnection> + ChannelManager per
producer leg (not a ChannelClient — take_call_connection's detach
is wrong for a hub with three CallConnection claimants); the reason
mapping preserves the spoke's code+message (timeout is the one
non-1:1 case, mapping to dial_failed); the registration seam is
two-phase (discover/stash → per-connection fork-register — the
ADR-047 §4 fork mechanism makes a one-call import impossible); the
ADR-042 relay map dissolves (implicit per-channel mapping) and
channel/close needs no translation surface (EOF cascade propagates
with correct ledger accounting on both legs); Pub-typed marked
specs are a loud assembly error; establishment bounds compound per
hop (noted, no fix); the ACL layering note is pinned (the spoke's
AccessControl sees only the hub identity; forwarded_for is never
checked).
- ADR-042 amended: the §Scope note is revised (implementation is an
alkcall export; hub crates compose it) and the two mechanism
supersessions are recorded — the contract and auth-model rationale
unchanged.
- ADR-047 amendment 3's forward reference and the README ADR index
updated (001..051).
- Review 008 remediation plan: Unit 3 split into 3a (ChannelRelay
component + gates), 3b (hub-leg install template — the call-half
support), 3c (gate-2 e2e incl. the mid-establishment disconnect
window); sequencing note updated; adoption note records the
session's decisions.
Verified: cargo doc --no-deps (markdown-only change).
Flavor-form open op ids (channels/tunnel/direct, channels/tunnel/
forwarded — alktunnels ADR-007/008) now survive discovery + the
op/register announce path: the marker reconstructs, so a hub consuming
through discovery wraps the op with relay machinery instead of the
silent plain-forwarding-stub failure.
Per the review-008 plan's combined (b)+(c) shape:
- derive_alpn_from_op_name generalizes from strip-/sub|/pub to
strip-last-segment (rsplit_once) — a strict superset: standard
two-suffix shapes derive identically, multi-segment ALPNs survive
the same way, and the flavor form derives. The boolean marker
remains the gate (consulted only for marked ops in
rebuild_spec_for) — a plain op named channels/tty/query is
unaffected.
- spec_to_json_pub emits channel_open_alpn (the explicit string)
beside the boolean when the op name is not the standard
channels/<seg>/(sub|pub) shape; standard shapes stay byte-identical
to the pre-amendment payload. The advertised operation_spec_schema
documents the optional ["string","null"] property.
- rebuild_spec_for prefers the explicit string, else boolean →
generalized derivation. Both wire consumers (from_call import,
op/register announce) parse through the same parser, so one change
covers both. A channel_open_alpn without the boolean never marks an
op (the boolean is the dispatch hint).
Residual ambiguity pinned in the ADR: an ALPN segment colliding with
a flavor name (channels/x/direct → alk/x vs alk/x/direct) is
undecidable from the name alone; the explicit string is the
disambiguator.
Tests (review gates 1 + 3; gate 2 lands with the Unit 3 relay):
- channels/tunnel/direct reconstructs WITH channel_open = alk/tunnel,
both via the explicit string and via the boolean alone (the
deployment-skew case: old producer, new consumer)
- standard shapes (channels/tty/sub, multi-segment
channels/custom/proto/sub) round-trip byte-stable — no new key
- explicit string overrides a colliding derivation; string without
boolean is ignored
- op/register announced flavor-form spec round-trips with the marker
- derivation unit tests: flavor form, non-op-type suffix not
special-cased, prior shapes unchanged
Docs: ADR-047 amendment 3 — the convention sentence (mirroring
alktunnels ADR-002 Amendment 1: flavors are new op ids, additive, the
.../sub op is never reused), the wire shape, the preference order,
the residual-ambiguity note, one-way-door timing.
Verification: cargo test 649 passed (12 new); clippy -D warnings
clean; cargo fmt --check clean; cargo doc --no-deps clean.
The establisher can now contribute to the open-op success reply — the
establisher → opener direction amendment 2 left empty. First consumer:
alktunnels ADR-008's bind-first listen establisher, whose observed
OS-chosen bound address rides the reply as an additive `bound` field
(the SOCKS5 BIND reply#1 BND.ADDR fidelity ask).
- Establishment gains reply_fields (private, builder-constructed):
with_reply_field / with_reply_fields / reply_fields. The map shape
generalizes beyond `bound` without a fourth amendment; the
#[non_exhaustive] carrier (amendment 2) makes the extension additive
— no construction-site break.
- run_open_wrapper merges contributed fields into the success output
after reserving `channel_id`. The key is wrapper-owned: an
establisher supplying it fails the open loudly
(channel:open_failed, reason handler_error, message naming the
reserved key), tearing the just-allocated channel down — never
shadowing. Absent fields leave the reply byte-identical to the
pre-amendment shape (asserted exactly).
- ChannelClient::open_channel_with_reply returns
(channel_id, reply, send, recv) — the full success output — so
consumers read `bound` first-class; open_channel delegates and
discards the fields, signature unchanged.
Tests (all gates from the review + plan):
- projection produces { channel_id, bound } on the wire; no-fields
replies are byte-identical (establisher and no-establisher shapes)
- reserved-key establisher fails with reason handler_error; channel
torn down, ledger decremented, pump handler never spawned
- registry-level register_openable_with_establisher projects fields
- e2e over a real channels connection: bound reaches
open_channel_with_reply; open_channel unchanged against the same
accept side
- existing establishment tests (establisher-success round-trip,
plan-flow, no-establisher compat) pass unchanged
Docs: ADR-049 amendment 3 (projection, reservation, read path,
compatibility posture, door type).
Verification: cargo test 637 passed; clippy -D warnings clean;
cargo fmt --check clean; cargo doc --no-deps clean.
Both findings verified against HEAD 3b36b40 (0.7.1); all citations
check out (one line drift: U-2 reply construction is operations.rs:945,
errata noted rather than silently edited).
Plan decisions recorded above the as-filed option lists:
- U-1 shape: combined (b)+(c) — explicit channel_open_alpn string for
non-standard shapes; derivation generalized to strip-last-segment,
gated on the boolean marker. As-filed option (a) is unsound (the
derivation runs consumer-side; a registration-threaded allowlist has
no path to rebuild_spec_for).
- U-1 gate 2 escalated to an in-tree relay component (src/channels/
relay.rs), amending ADR-042's downstream-implementation scope note —
the re-produce/relay shape is general (alkhttp fallback hub, alknodes)
and a real consumer now requires it.
- U-2 adds open_channel_with_reply (ergonomic read path for `bound`);
open_channel unchanged.
Units, sequenced 1 -> 2 -> 3, one minor release:
1. U-2 reply projection (ADR-049 am. 3) — lands first; the relay's
establisher projects spoke reply fields through it.
2. U-1 derivation + wire field (ADR-047 am.) — standard shapes
byte-stable; round-trip gates for flavor-form and boolean shapes.
3. Relay component + gate-2 e2e (ADR-042 am.) — establisher = translate
hop (forwarded_for, adopt_channel, reason-code mapping, bound
projection), OpenHandler = inline-await byte-forward hop;
consumer -> hub -> producer e2e asserting bound survives the relay.
As-filed findings retained untouched above the divider (the review-007
pattern).
Verification: docs-only change; no build/lint/test impact.
Filed from the alktunnels graduation spec work (ADRs 007/008):
U-1 extends ADR-047's op-name → ALPN derivation to flavor-form open
op ids (channels/tunnel/direct, channels/tunnel/forwarded) so the
marker survives discovery and hub relay wraps them as channels, not
plain forwarding stubs; U-2 (ADR-049 amendment 3 sketch) gives the
establisher a path to contribute additive reply fields (the bind-first
listen 'bound' address — BND.ADDR fidelity). Both additive; data
plane untouched. alktunnels implementation is sequenced after these
land.
Batch-fixes every remaining open alkcall-side finding from downstream
consumers so 0.7.0 is the only release they need to absorb.
- CF-006 (the CF-005 corollary): run_open_wrapper derives a per-call
AuthContext — the opener's dispatch-resolved identity (the same
identity the ACL gate and cap check saw) overlaid onto the
install-time context — and passes it to both the establisher and
the pump handler. Identity-less calls keep the install-time
identity (no synthetic-anonymous rewrite); transport-truthful
fields are never rewritten. Signatures unchanged — behavior-only;
identical on per-connection registries, hub-forwarded opens now
show the end client. Gates:
open_wrapper_overlays_per_call_identity_on_install_time_auth +
open_wrapper_keeps_install_time_identity_when_call_identityless.
- CF-007 (alkhttp review 006 Part C doc drift): ADR-016 amended to
the eight-code list — ALREADY_EXISTS + CONNECTION_CLOSED in the
Context, §3 table, and from_openapi collision rule; new §2a
documents the undelivered-vs-ambiguous write-failure distinction.
- ChannelPlan type doc now states the Send + Sync payload constraint
(alktunnels POC F-1 re-derived it by compiler error).
- Ledger: CF-006 and CF-007 filed + resolved; CF-005's corollary
note points at CF-006; the Open section is empty. ADR-049 gains
the per-call-identity note. Changelog 0.7.0 covers the batch.
Sweep result (all downstream reviews): no other open alkcall items —
alktunnels W3/F-2 are downstream-by-design, alktty R3/P14 are closed
constraints, alknet has none; OQ-24/37/39/40/41 stay
deferred-by-design (no consumer pull yet).
Verification: cargo test (631) + --all-features (648), clippy
(all-targets, all-features, -D warnings), fmt --check, doc
--no-deps, wasm32 check, semver-checks (no update required),
publish --dry-run.
Verifies and fixes CF-005 (alktunnels reverse-flow POC W1): the
connect-side serving path built channel 0 internally and never set an
identity, so a scope-gated serving op could only be satisfied via the
payload auth_token. Token is now the fallback (hub-forwarding /
browser path); transport/key-based identity is the primary path.
- ServingConfig gains identity: Option<Identity> — the explicit
override (remediation a). Semver-relevant struct-literal change →
0.7.0 (minor bump at 0.x, wire surface unchanged).
- from_connection_with_serving propagates the transport
Connection::identity() to the channel-0 connection via set_identity
before the serving loop starts (remediation b) — mirrors the accept
side's install-hook set_identity; process-local, nothing new on the
wire.
- Dispatch identity precedence on the serving loop: payload
auth_token → identity_provider, then ServingConfig.identity, then
transport identity; identity-less dispatch still fails closed
(FORBIDDEN).
- Public core::auth::NoopIdentityProvider (resolves nothing; the
ServingConfig::default() provider — three private test copies
existed).
- Regression gates: four cf005_* e2e tests (transport propagation,
override precedence, identity-less denial, token fallback +
precedence).
- Ledger CF-005 → resolved; ADR-022 §connect-side-serving amended;
README example updated; changelog 0.7.0.
Verification: cargo test (629) + --all-features (646), clippy
(all-targets, all-features, -D warnings), fmt --check, doc
--no-deps, wasm32 check, semver-checks (no update required at
0.7.0), publish --dry-run.
- architecture README: index rows for ADR-049 (establishment phase) and
ADR-050 (pump_bidi); stale ADR-001..045 range labels corrected
- channel-operations.md: design-decision table + references list the
two new ADRs
- CHANGELOG: missing link references for 0.4.0/0.4.1/0.5.0/0.6.0
- AGENTS.md: ADR range 001..050
Status → Resolved with the three unit commits; both deviations from
the review's sketches (typed-opaque ChannelPlan over Option<Value>;
(u64, u64) over io::Result) recorded up top with their rationale, as
the ADRs carry them.
Extracts the two-pump data-plane helper alknet ADR-078 deferred until
the shapes converged (they have: alktunnels POC pump_halves + alktty's
channels session). Purely additive.
- channels::pump::pump_bidi(channel, peer_read, peer_write) -> (u64, u64):
two joined pumps, shutdown-on-completion per direction; copy counts
for observability. The channel side is a single AsyncRead +
AsyncWrite value (the accept_bi BiStream); the peer side takes split
halves — the establisher's natural dial result (into_split).
- Return (u64, u64), not the review sketch's io::Result<(u64, u64)>:
both pumps swallow copy errors by contract (mid-stream error =
abrupt close, no error channel mid-stream per ADR-049 §6), so an
Err state would be dead code. Deviation recorded in ADR-050.
- alktty's three-pump session does not fit (exit future as a third
signal) and stays as-is, per the review's scope.
- Tests reproduce the POC's two-pump semantics through the helper:
bidirectional flow with exact counts, EOF-from-one-side completes
the other's shutdown (clean EOF at the far end), dead-source =
EOF-shaped teardown.
- ADR-050 records the decision, deviations, and two-way door type.
Verification: cargo test (625 passed, +2), clippy -D warnings, fmt
--check, doc clean, test --all-features clean, wasm32-unknown-unknown
check clean.
Implements ADR-049 amendment 2 — the reserved Establishment payload is
filled, and the OpenHandler lifetime contract is documented.
- Establishment { plan: Option<ChannelPlan> } with ChannelPlan =
Arc<dyn Any + Send + Sync>: typed-opaque, because the payload an
establisher hands the pump handler is a live handle (dialed socket,
TTY handle), not JSON — the review's Option<Value> sketch could not
satisfy its own verification gate. #[non_exhaustive] keeps a future
carrier change from being another break. Construction:
Establishment::new(plan) / Establishment::default().
- OpenHandler gains the plan parameter:
Fn(Value, Option<ChannelPlan>, Connection, AuthContext) ->
JoinHandle<()>. Separate parameter (not merged into input) — a
typed payload cannot ride the JSON input; no schema collision.
Wire surface unchanged: the plan is process-local (establisher ->
wrapper -> handler).
- run_open_wrapper threads establishment.plan to the handler; None
when no establisher is registered. Kills the alktunnels-POC
side-channel handoff (resource-keyed slot + poll loop) whose
concurrent same-resource race is now unreachable — each open's
establisher result flows to its own handler.
- Lifetime contract documented (R-02, doc-only half): the returned
JoinHandle must track the data-plane lifetime — the wrapper awaits
it and its completion triggers teardown; early return = teardown
at birth. Noted on the OpenHandler type docs and both registration
entry points.
- Breaking at 0.6.0 (the point of landing it before alktunnels
Phase 1): Ok(Establishment {}) sites become
Ok(Establishment::default()) mechanically.
Verification: cargo test (621 passed, +4: plan-flows-to-handler,
concurrent same-resource opens get distinct plans, no-establisher
None plan, Establishment construction), clippy -D warnings, fmt
--check, doc clean, test --all-features clean.
Findings filed to prevent a second fix->publish->update-dependents
cycle; each was reached by building working code against 0.5.0:
- R-01 [major] — Establishment is payloadless but the channel plan
is exactly what establishers need to hand to the pump handler
(ADR-049's own 'reserved for a channel plan' note). Costs verified
in two consumers: alktunnels POC side-channel handoff (same-resource
opens race the slot), alktty forced to keep backend allocate
post-open in-band (allocate_failed stays an in-band frame — the
shape ADR-049 eliminates, alive one layer down). Ask: fill the
reserved field (plan: Option<Value>, process-local, wire unchanged)
in a 0.6.0 sweep; the break is mechanical (Establishment::default).
- R-02 [minor] — the OpenHandler JoinHandle lifetime contract is
undocumented and load-bearing: the wrapper's await of the returned
handle IS the teardown trigger; a handler that returns before its
pumps finish tears the channel down at birth (the POC found this
empirically — every tunnel EOF'd instantly). Doc note + ADR-049
amendment; optional debug warning.
- R-03 [minor, optional] — the ADR-078 two-pump helper's convergence
test is satisfied (POC gives both shapes); extracting pump_bidi
now is additive (no break) and pins the contract upstream. Decide
in the same 0.6 sweep.
Non-findings: reverse-flow (-R) needs no upstream mechanism
(from_connection_with_serving + serving-side allocation, verified by
trace); establisher receives registry-validated input; typed
establishment errors complete on the wire; early-arrival cap
unchanged; EstablishmentError reason set sufficient.
Goal stated in the review: 0.6 is the last breaking sweep forced by
known work. alkcall tests: 617 passed (docs-only change; baseline
check).
- E-03: the open wrapper's handler-exit teardown no longer discards
UnknownChannel silently — debug log + benign-race pinning comment
(ledger take is the atomic gate; no double-decrement)
- E-04: consumer-facing doc note on the 64-parked-chunks observable
bound (channel-client.md + EARLY_ARRIVAL_CAP const doc) for
tunnel-style push-first producers
- N-2: ChannelManager::early_arrival_count() accessor (the
observability choice over removing the write-only counter),
documented monotonic, with a park/adopt-drain monotonicity test
- review 006: Unit 3 marked implemented in Status and remediation plan
Verification: 617 tests pass; clippy -D warnings clean (host +
wasm32 check); fmt clean; doc clean
Implements ADR-049 Unit 1 — the open-op wrapper gains an awaited,
bounded establishment phase, and the client stops erasing the error.
- OpenEstablisher hook + Establishment/EstablishmentError types:
register_openable_with_establisher awaits the establisher bounded
(earlier of dispatch deadline and per-registration timeout, else
ESTABLISHMENT_TIMEOUT = 10s) after allocation, before the reply and
before the pump handler is spawned (ADR-049 §1/§2). Implementation
note: the establisher takes (input, auth) only — the channel's
yield-once BiStream belongs exclusively to the pump handler
(amendment recorded in ADR-049).
- Establishment failure: teardown_channel + opener-ledger take +
policy.on_close un-increment (allocation and teardown balance;
the ledger take is the atomic gate, ADR-047 §7), reply
channel:open_failed with details {reason, message} — reason ∈
dial_failed / unknown_resource / resource_shortage / handler_error
/ timeout (ADR-049 §3). SSH contract consumer-visible: a failed
open never returns a channel_id.
- register_openable unchanged (no establisher = always-OK; existing
registrations compile and behave identically — compat gate test).
- ChannelClient::open_channel returns ChannelOpenError (breaking at
0.5.0): CallFailed { error: CallError } carries the wire error
verbatim (establishment_reason() branches on details.reason);
MissingChannelId / AdoptFailed cover the local-only shapes
(ADR-049 §4, review 006 N-1).
- Tests cover all four verification gates from the review: e2e
establisher failure through a real channels connection (typed
reason + no-channel + ledger un-increment), bounded timeout,
no-establisher compat, establisher-success pump round-trip; plus
reason-vocabulary mapping and bound arithmetic.
- Bump to 0.5.0 (open_channel error-type change is semver-relevant).
Verification: cargo test (608 passed), clippy --all-targets -D
warnings, fmt --check, doc --no-deps, wasm32 check — all clean.
Design review from the alktunnels Phase 0 research pass, verified
against tree a22b2b8 (0.4.1). Findings numbered E-01..E-04:
- E-01 [major] — the open op cannot fail after allocation: the
wrapper replies {channel_id} the moment the OpenHandler is spawned;
establishment failures (params-valid-but-rejected, backend lookup
failure, target dial failure) present to the consumer as a
successful open followed by an instant, indistinguishable clean
EOF (implicit-EOF mux path + unified poll_read EOF arms). SSH
semantics (RFC 4254 §5.1 open-failure reply with reason codes;
channel never exists opener-side), SOCKS5 reply codes, and
udpgw's opaque ERR bit (counterexample) surveyed in
alktunnels/docs/research/ssh-socks5-survey.md. alktty's in-band
error-frame mechanism (send_negotiation_error, 0x00-peek) is the
per-crate workaround this upstream establisher obsoletes for the
channels path. Proposed shape: an awaited establishment hook
(OpenEstablisher) or await-and-inspect OpenHandler, tearing down on
failure and replying channel:open_failed with SSH-four reason codes
in ADR-016 details. Remediation sketch + verification gates
included.
- E-02 [minor] — services/list discloses no per-op metadata; OQ-40
(channel/resources/subscribe) becomes load-bearing for the first
time via the alktunnels discovery resolution (OQ-TN-08).
- E-03 [minor] — OpenHandler-exit vs channel/close teardown race is
benign (ledger take is the gate) but the let _ = discard at
operations.rs:509 is silent; recommend log-or-comment.
- E-04 [minor] — early-arrival park cap (64) is an observable bound
for push-first producers under slow adopters; no change requested,
filed so the constraint is visible to the next consumer.
Non-findings recorded: open-op ACL path complete across all three
dispatch entry points; input_schema enforcement covers open params;
EOF arms unified; channel_open marker + resource_id_path wire
round-trip intact; opener-ledger decrement atomic at every call site.
alkcall tests: 597 passed (docs-only change; baseline check).
Surfaced by alkhttp review 006 (UP-03): services/list-peers showed
every peer with an empty operations array. PeerCompositeEnv overrode
peer_ids only, so peer_operations fell to the trait default
(Vec::new()) and the ADR-022 amendment's "announced op is discoverable
via services/list-peers" promise never resolved on the wire. ADR-030
prescribed the fix but it had never been ported into alkcall. The
existing list-peers unit tests passed because they mock
peer_operations with hand-rolled envs.
Implements ADR-030 as specified:
- OperationEnv gains list_operation_names (default Vec::new(),
back-compat for all existing implementors)
- OverlayOperationEnv overrides it with its overlay's registered names
- PeerCompositeEnv::peer_operations delegates to the peer overlay's
list_operation_names; PeerCompositeEnv::list_operation_names
aggregates session + connections + base (mirrors its contains())
- LocalOperationEnv enumerates its registry; ChannelsSessionEnv
delegates to base
Gate: announced_op_is_discoverable_via_services_list_peers in
src/registry/op_register.rs — announces an op through op/register,
then asserts both the direct peer_operations probe and the
services/list-peers wire shape attribute the announced op to the peer,
over the exact compose_root_env shape (PeerCompositeEnv + attached
connection overlay). Verified load-bearing: reverting the
peer_operations override fails the gate.
ADR-030 status Proposed -> Accepted with the UP-03 provenance note.
Verification: 590 default / 607 all-features tests, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean,
semver-checks 196 pass against v0.3.0 (defaulted trait method is
non-breaking).
- resource_id_path rides both halves of the spec wire round-trip:
spec_to_json_pub serializes it (optional string key), rebuild_spec_for
parses it. Additive optional field - absent stays absent. Previously
an announced (or from_call-imported) op declaring ownership-scoped
resource extraction silently rebuilt with resource_id: None, so ACL
checks ran without the resource ID.
- Gates: spec_round_trips_resource_id_path (serialize -> parse ->
field intact) + spec_without_resource_id_path_stays_absent (additive
field breaks no consumer).
- ADR-022 amendment: bootstrap-op set gains services/list-peers with a
dated G-05 note (the installer has registered it since the amendment
landed; the doc lagged the code). Set remains closed at four.
Verification: cargo test 589 / --all-features 606, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.
Refs docs/reviews/005-...md (G-04, G-05; all findings closed).
- op_register_handler takes the serving registry alongside the
connection and rejects announced names that collide with the serving
side's own registrations (ALREADY_EXISTS regardless of replace).
Peer-announced ops may collide with peer-announced ops (replace
governs, the reconnect path) but never shadow the deployment's own
ops: the connection overlay resolves before base in PeerCompositeEnv,
so an unscreened same-name announce would silently rewrite what a
wire-dispatched handler's ctx.env.invoke resolves. Composition
authority (ADR-018) stays with the deployer.
- ADR-022 amendment (2026-09-04): collision policy recorded in the
2026-09-03 amendment's op/register section (rationale + visibility
irrelevance); status line notes the sub-amendment.
- Gates: base-External collision rejected even with replace (overlay
stays clean, serving registration untouched); Internal base op
equally protected; overlay/overlay collisions still follow replace;
nested composition of a base op resolves the serving side's own op
after an unrelated announce (real compose_root_env env shape).
- PeerCompositeEnv resolution order deliberately unchanged.
Verification: cargo test 587 / --all-features 604, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.
Refs docs/reviews/005-...md (G-03; Unit 3 open).
- Split dispatch() into dispatch_start() (sync prefix) + spawned
invocation: both single-stream loops (serve_single_stream and the
accept-side run_loop_single_stream) spawn Once invocations, Sub
pumps, and sink response writers; only the Pub sink start stays
inline (chunk_tx must register before the next call.published).
Inline dispatch deadlocked same-connection nested composition: the
read loop awaited the parent handler, which awaited a nested call
whose response only the same read loop could resolve (resolved only
via the 30s sweeper). Spawned handles tracked + aborted at loop exit;
in_flight_sinks behind an Arc<parking_lot::Mutex> with guards dropped
before awaits.
- run_loop_single_stream gains the pending-resolution arms
(RESPONDED/COMPLETED/ERROR): the accept side previously served only
and had no loop resolving its own outbound pendings in single-stream
mode — the latent accept-side imported-op composition hazard is
mechanized shut.
- Write-failure in the spawned Once path warns instead of closing the
loop (matches the Sink arm; dying transport still surfaces via
ConnectionClosed on the next read).
- G-02 gate: hub_handler_composes_peer_announced_op_via_nested_composition
— announce -> consumer calls hub/compose -> hub's serving loop
wire-dispatches it -> handler composes via ctx.env -> forwarding
stub's nested call crosses back to the consumer. The F-05 gate
bypassed this path entirely.
- Interleaved-directions gate: outbound_call_resolves_while_inbound_
subscription_is_being_served — consumer serves a live Sub while a
wire-dispatched hub handler issues an outbound call on the same
connection.
- Both gates verified load-bearing: run against the pre-fix loop each
reproduces the G-01 hang (no progress, bounded-timeout failure);
post-fix both resolve in <0.2s, no sweeper evictions.
Verification: cargo test 583 / --all-features 600, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.
Refs docs/reviews/005-...md (G-01, G-02; Units 2-3 open).
Post-remediation review of f84d214 (review 004 Units 1-3). Five
findings, verified in source and (for G-01) empirically via a probe
test that was added, run, and removed:
- G-01 [major]: serve_single_stream awaits dispatch inline; a
wire-dispatched handler composing a peer-announced op (or a
from_call import) over the same connection deadlocks — the nested
call resolves only via the 30s sweeper (probe: TIMEOUT at 30.0007s).
- G-02 [major]: the F-05 e2e gate calls the announced op directly,
bypassing the forwarding stub — the one path G-01 breaks.
- G-03 [major]: op/register's collision gate is overlay-only;
PeerCompositeEnv resolves connections before base, so an announced
op can shadow the serving side's own ops in nested composition.
- G-04 [minor]: resource_id_path does not survive the spec wire
round-trip (pre-existing shape, load-bearing for op/register).
- G-05 [minor]: install_bootstrap_discovery registers
services/list-peers; ADR-022's bootstrap set doesn't name it.
Non-findings bound the re-review: fork surface lock discipline,
bootstrap discovery closure, frame-arm equivalence of the composed
loop, unchanged pure-consumer default, alkhttp cross-repo claims,
CJK sweep (none), all gates reproduce (581/598, clippy, fmt, wasm,
doc).
Remediation plan: Unit 1 (concurrent serving loop + stub-exercising
gate) gates Unit 4 downstream; Unit 2 (collision policy); Unit 3
(round-trip completeness + doc alignment).
Verification: cargo doc --no-deps clean; tree unchanged apart from
this review doc.
Focused design-mismatch review found via alkhttp's WS data-channel
drill-down (alkhttp review 003 WS-24/WS-25). The channel machinery is
done and proven; the gaps are the dispatch-resolution mechanism and
the connect-side serving half — both upstream of any transport.
Findings:
- F-01 [major]: top-level dispatch consults only the dispatcher's
base registry — ops registered per the ADR-047 §4 amendment's
overlay mechanism resolve NOT_FOUND on the wire; the only proven
shape (per-session registry as the dispatcher's base) differs from
the ADR's wording
- F-02 [major]: no Clone/fork surface on OperationRegistry or
HandlerRegistration — every inner payload type IS Clone-able
(verified type-by-type, incl. jsonschema::Validator and
Capabilities), so the fork is a small addition
- F-03 [minor]: fork must carry handlers + validators, not just specs
- F-04 [major]: connect-side channel-0 read pump resolves responses
only; inbound call.requested frames are silently dropped — no
serving half on the single-stream shape (ADR-022/AGENTS §8
bidirectionality unreachable from the connect side)
- F-05 [major]: no wire mechanism announces client-side ops; the
six call.* kinds are closed. Resolution candidate: bootstrap op
(op/register) served per-session, handler writes into the
connection-local overlay; discovery rides services/list-peers
- F-06 [minor]: per-session fork must carry bootstrap discovery ops
for per-session openables to be discoverable
Includes a non-findings section (e2e reference shape,
register_openable completeness, channel-id split, envelope-kind
closure, wasm-cleanliness) and a 4-unit plan: ADR decisions (Unit 1)
-> fork surface (Unit 2) -> client serving + bootstrap op (Unit 3)
-> alkhttp wiring downstream (Unit 4, tracked in alkhttp review 003).
Verification: cargo test (565), clippy --all-targets -D warnings,
fmt, doc --no-deps — all clean at c16b069. No source changes.
- CF-004: services_schema_handler now applies the same visibility +
AccessControl gates as invoke() (identity resolution mirrors invoke:
handler_identity under internal). Restricted ops return spec-404 NOT_FOUND
— matches "restricted ops don't exist" and leaks nothing about the
restricted surface. Closes the unauthenticated /call-path disclosure.
- CF-003: publish_schema compiled at registration time (both
OperationRegistry::register and OperationRegistryBuilder::store);
un-compilable schemas are a registration error — an unvalidated ingest
path can no longer be constructed. Compiled validator cached per-op
(publish_validator) and consumed by dispatch; per-request compile gone.
BEHAVIOR CHANGE: register/builder reject un-compilable publish_schema.
- CF-002: demux TooLarge skip streams through a fixed 64 KiB buffer
instead of allocating the peer-declared length (u32, up to ~4 GiB);
cumulative 256 MiB skipped-bytes budget tears down dribbling peers.
Existing resync test passes unchanged.
- CF-001: new retryable CallError::connection_closed (CONNECTION_CLOSED)
applied only where the call is provably undelivered — request-frame
write failures on all consumer paths (call/subscribe/publish, both
stream modes; publish pump tags write stages). Mid-publish failures and
producer-side fail_all stay non-retryable INTERNAL (delivery ambiguous).
New code string is additive; retryable flag is the machine-readable
signal.
Verification: cargo test (558 pass, 15 new), clippy --all-targets -D
warnings, fmt --check, wasm32-unknown-unknown check.
Found via alkhttp Review 002 (PRJ-16): the services/schema handler
does a bare registry.registration(name) with no Visibility and no
AccessControl check, so POST /call (and the MCP call tool) can fetch
any Internal op's complete spec unauthenticated. The GET /schema route
and MCP schema tool enforce the pre-checks; the /call path is the
hole.
Found while fixing alkhttp's HTTP-side instance
(review-001-publish-schema-validation-robust): the identical
warn-and-skip pattern exists at src/protocol/dispatch.rs:352-366 — a
compile failure of a Pub op's publish_schema proceeds with
validator: None, so arbitrary unvalidated JSON reaches the sink
handler over the wire. Suggested direction: fail-closed + per-
registration validator cache (worked shape in alkhttp 1572a9d,
src/gateway/schema_cache.rs) + registration-time schema compilation.
Single compile site verified (both pump arms consume the one
InFlightSink.publish_validator).
Cross-crate finding from alkhttp Review 001 (WS-12), filed here per the
ledger's purpose (consumer-surfaced alkcall findings).
src/channels/adapter.rs:144: the ChunkError::TooLarge arm allocates
vec![0u8; length] from the peer's untrusted 8-byte header before
reading; length is u32, so ~4 GiB can be pinned per connection and held
indefinitely by a dribbling peer. Reachable via the alkhttp WS path by
any authenticated browser. Skip/resync logic is correct; the memory
shape is wrong — stream-skip with a bounded buffer instead.
The normal payload arm (:159) is safe (parse_header bounds it at
MAX_CHUNK_LEN); only the TooLarge arm is unbounded.
- Remove alktype from Cargo.toml (its only usage was a thin wrapper
over jsonschema::options().build())
- Replace alktype::validation::build_validator with direct jsonschema
in dispatch.rs
- Add docs/architecture/chunk-header.bast.json — the chunk header's
BAST (Binary Abstract Syntax Tree) machine-readable wire spec
- Embed as channels::wire::CHUNK_HEADER_BAST via include_str! so
downstream Rust crates can consume it without a file lookup
- Add test asserting the embedded BAST doc is valid JSON and matches
the wire format
- Update AGENTS.md §10 and implementation-specialist.md: BAST docs
are the contract; trivial/hot-path formats stay hand-rolled,
complex formats use the alktype engine or codegen
Verification:
- cargo test: 543 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
- BAST doc compiles + round-trips against alktype v0.2.0 engine
The R-02 acceptance gate was a line-level grep
(`grep -v 'mod tests'`) that cannot return zero while any test uses
`.expect()` — `grep -v` filters per-line, not per-scope, so every
`.expect(` inside a `#[cfg(test)] mod tests {}` block whose
individual line lacks the literal "mod tests" still matches. The suite
has ~269 such test-only `expect` calls, making the literal gate
unsatisfiable (an impossible-goal spec bug, surfaced during Unit 1
remediation).
Replace both copies of the gate (the R-02 finding gate and the Unit 1
acceptance gate) with a scope-aware verification: either (a) a targeted
grep for the specific removed strings, or (b) file-by-file confirmation
that every remaining `.expect(` is within a `#[cfg(test)] mod tests`
boundary. The intent — no `expect` in library (non-test) code — is
unchanged; only the verification method is corrected.
- docs/architecture/README.md: add Roles and Composition section with
the four roles (producer, consumer, hub, spoke), dependency layering
diagram, protocol crate pattern, and the two-path adapter model
- docs/architecture/channels-overview.md: fix stale alknet ADR numbers
(071/072/073/074/075/076/077/078/079/080/081 -> 034-044), update
relationship section for post-extraction world, update crate
dependencies to reflect single-crate alkcall
- src/lib.rs: add Downstream composition section with role table and
protocol crate pattern, linking to the architecture README
Verification: cargo test (483 passed), cargo clippy (clean),
cargo fmt (clean), cargo doc (no warnings)
The publish path's decided-but-unimplemented behaviors land: per-chunk
schema validation, initiator call.error terminates the stream, and an
early handler return short-circuits the feed. alktype::validation::
build_validator is now used on both dispatch paths (the dependency was
declared and unused); jsonschema was added as a direct dependency
because its Validator type is part of alktype's public API.
P-03 — OperationSpec::publish_schema was declared, had a builder,
defaulted to None, and was never read. It now round-trips through
discovery: spec_to_json emits "publish_schema" when Some,
operation_spec_schema() advertises the property, and rebuild_spec_for
parses it back (null/absent treated as None, so an imported Pub op no
longer loses the schema). Both dispatch paths (pump_sink and
run_loop_single_stream) validate each call.published chunk's input
against the schema before yielding it to the SinkHandler; on
validation failure an Err(CallError::invalid_input(...)) with the
offending chunk in `details` is injected and the feed terminates,
matching the SinkHandler contract that an Err terminates the stream.
The validator is built once at dispatch time and carried in the
SinkDispatch (and, for the single-stream path, in a new InFlightSink
struct alongside chunk_tx); a schema that fails to compile logs a
warn and falls back to no validation. When publish_schema is None,
chunks are yielded as-is (unchanged behavior).
P-04 — pump_sink matched only call.published/call.completed/call.
aborted and dropped everything else into a debug-log ignore branch; a
call.error from the initiator was silently ignored. Both dispatch
paths now match call.error, parse the CallError from the payload,
inject it as Err(call_error) into the sink's chunk_tx, and terminate
the feed — the handler sees the initiator's error, not a synthetic
"aborted" message. A malformed payload falls back to
CallError::internal("publish error from initiator (malformed)").
P-07 — pump_sink used tokio::join!(feed_fut, handler) and only wrote
the response after both completed; an early-returning handler (e.g.
rejects after chunk 1) had its response deferred until the initiator
finished publishing or the next chunk_tx.send failed. Replaced with a
tokio::select! loop over handler.fuse() and reader.read_frame(): when
the handler completes first, the response is captured and the loop
breaks immediately, the feed is short-circuited (chunk_tx dropped,
the handler's PublishStream sees EOF), and the response is written
without waiting for the feed. The feed-wins branch (natural end /
abort / error / read failure) awaits the handler after the loop as
before. This removes the unbounded stall on a path ADR-046 intends
for long-lived streams.
Tests (15 new, 480 total):
- discovery: spec_to_json emits/omits publish_schema; operation_spec_
schema documents the property (3).
- from_call: rebuild_spec_for parses publish_schema (present/omitted/
null) and round-trips with spec_to_json (4).
- dispatch (stream-per-request): publish_schema rejects invalid chunk
+ passes valid chunks + no-schema yields as-is (3); initiator
call.error terminates with the initiator's error + malformed
fallback (2); early handler return short-circuits a slow feed and
the response is not deferred (1).
- channels/client (single-stream): publish_schema rejects invalid
chunk over channel 0; initiator call.error terminates the publish
over channel 0 (drives run_loop_single_stream directly with crafted
frames since the public publish() API takes Stream<Item = Value> and
cannot emit an initiator call.error) (2).
Verification:
- cargo test → 480 passed, 0 failed (was 465; +15 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 0 warnings
No panic paths in library code, no misleading public fields, honest
error envelopes, latent bugs in derive_alpn_from_op_name and the mux
pump map fixed, per-discovery Box::leak removed, demux observability
counter added, resources-snapshot unbounded loop replaced with an
O(open channels) iterator.
C-21 — write_header (wire.rs) sliced out[..CHUNK_HEADER_LEN] and
panicked on short buffers; the doc comment declared the panic. Returns
Result<_, ChunkError::HeaderTooShort> instead (the variant already
existed for the parse side). write_chunk and all callers updated (they
pass [0u8; 8] today so the Result is always Ok, but the API contract
is typed, not panic-documented). Added a short-buffer test.
C-23 — dispatch_requested's Sink and Stream error arms used
String::new() as the request id (dispatch.rs), producing envelopes
with an empty id. The request_id is now cloned before the move into
dispatch and used in the error arms, so the envelope carries the real
id (matching the Once arm, which already did this correctly).
P-13 — SinkDispatch::chunk_tx was pub (dispatch.rs), exposing the
futures::mpsc::Sender type and the 64-slot buffer size as effective
public API. Made pub(crate); handle_stream and pump_sink (the only
readers) are in the same module, and the dispatch tests are in the
same module too.
C-19 (mux pump map leak + duplicate registration) — MuxRunner::run
inserted each pump's JoinHandle into self.pumps and never removed
finished ones (they accumulated for the connection's lifetime).
Duplicate register(channel_id) silently overwrote the old handle while
the old pump kept running (two pumps, one channel id). Fixed: before
insert, check for an existing entry; if it is_finished(), reap it and
proceed (leak fix); if it is still running, reject the registration by
dropping the responder without sending (the caller's receiver.await
yields RecvError, which ChannelManager maps to ChannelExists). Added
two tests: duplicate-rejected-while-running and
reap-finished-then-reregister.
C-19 (derive_alpn_from_op_name) — from_call.rs took only the first
path segment (rest.split('/').next()), so channels/custom/proto/sub
derived alknet/custom instead of the full ALPN custom/proto. The
segment.starts_with("alknet/") branch was dead (a single segment
cannot contain /), and segment == "alknet" yielded the nonsense ALPN
"alknet". Fixed: strip the known /sub or /pub suffix from rest
instead of taking the first segment, so multi-segment ALPNs survive.
The rule (ADR-047 Negative): alknet/*-prefixed segments and
multi-segment non-alknet/* ALPNs are returned as-is; single-segment
non-alknet names get the alknet/ prefix prepended. Added tests for the
multi-segment non-alknet case, the explicit alknet/ prefix case, the
/pub suffix, and the no-suffix (non-channel-open-op) case.
C-19 (Box::leak per discovery) — from_call.rs leaked a String to
'static on every leak_alpn call (bounded per unique ALPN in theory,
but leaks on every rediscovery of every marked op). Refactored
ChannelOpenSpec::alpn from &'static str to Cow<'static, str>
(spec.rs); the common case (ALPN crates register at compile time with a
&'static str literal) pays no allocation via Into<Cow>, while from_call
supplies an owned String without leaking. Removed leak_alpn. Amended
ADR-047 §2 to record the Cow<'static, str> shape (two-way-door type
detail; the wire format — a boolean channel_open marker — is
unchanged).
C-19 (REQ-CH-04 error counter) — the demux's lenient unknown-channel
drop (manager.rs) only debug!-logged; there was no counter or stats
surface. Added an AtomicU64 dropped_unknown_chunks counter to
ChannelManager, incremented on the unknown-channel drop in
route_payload, with a dropped_unknown_chunks() accessor for
observability. Added two tests: counter increments per drop, and
known-channel routing does not increment it.
C-19 (resources-snapshot unbounded loop) — operations.rs iterated
0..u32::MAX with a per-iteration mutex lock, breaking when
resources.len() >= manager.open_count(). With sparse ids (monotonic
after churn) or a channel closing mid-iteration, this could iterate
millions of times. Replaced with an iterator over ChannelManager::
channel_ids() — a new accessor that returns a point-in-time Vec<u32>
of open channel ids — so the snapshot is O(open channels). Added
channel_ids tests and behavioral tests for the resources/subscribe
handler (open channels emit their ALPNs; no channels emit an empty
set). Also added close/control handler tests (C-25 #8 coverage the
review noted was missing): close rejects channel 0, close on unknown
channel returns NOT_FOUND, control on unknown channel returns
NOT_FOUND, control missing channel_id returns INVALID_INPUT.
Verification:
- cargo test → 465 passed, 0 failed (was 449; +16 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 0 warnings