- 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.
First release carrying the consumer-findings remediation (CF-001..004),
the feature-gated gateway dispatch spine (ADR-048), and the
registration-time publish_schema validation behavior change (CF-003).
Gate is version-only for existing consumers: the public 0.1.1 API
surface is unchanged (probe-verified).
Coverage:
- CF-002: demux skipped-bytes budget teardown test (268 MiB skip in-memory;
a budgetless demux wedges in the 17th skip, the real one tears down) and
the skip-hits-EOF arm (truncated oversized payload ends the loop).
- CF-001: retryable CONNECTION_CLOSED pinned for subscribe write failures
in both stream modes and single-stream publish request-frame failures;
non-retryable INTERNAL pinned for mid-publish failures in both modes
(deterministic FailOnFlushN write half).
- Gateway: invoke_sink with with_deadline(None) completes a slow sink.
Docs:
- Fix broken intra-doc link on lib.rs's feature-gated gateway mention
(rustdoc warned on default-feature builds).
- Re-point 40 src/ references from the old alknet mono-repo ADR numbering
(049/050/052/065/070/074/092) to this crate's numbering
(021/011/034/007/008/009/005); drop into_sub_streams references
removed by ADR-035.
Verification: 565 default / 582 all-features (8 new), clippy -D warnings
on default/gateway/all-features/wasm32, fmt clean, rustdoc warning-free
on default and all-features, publish dry-run clean. Consumer-facing API
continuity 0.1.1 -> 0.2.0 verified by compiling an API-surface probe
against both versions.
- 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.