# Consumer findings ledger — alkhttp consuming alkcall Findings from writing the first real consumer (alkhttp) against alkcall. alkcall is deliberately minimal; expect missing features and edge-case bugs to surface here first. Extract items into alkcall tasks/reviews as the project sees fit. Format: date | found-in (alkhttp context) | severity | status. --- ## Open (none) --- ## Resolved ### CF-004 — `services_schema_handler` discloses Internal/ACL-restricted op specs — no visibility or AccessControl check (2026-08-30) — RESOLVED 2026-08-31 - **Found in:** alkhttp Review 002, finding PRJ-16 (`alkhttp/docs/reviews/002-post-remediation-review.md`, Part F', verified against tree `91483a7` + alkcall source). Filed to this ledger 2026-08-30. `src/registry/discovery.rs:327-343` (`services_schema_handler`) — the handler did a bare `registry.registration(&name)` and returned `spec_to_json(®.spec)` verbatim, with **no `Visibility` check and no `AccessControl::check(peer_identity)`**. - **Fix:** the handler now applies the same two gates as `OperationRegistry::invoke` — the Internal-visibility rejection (`!ctx.internal` → spec-404) and `AccessControl::check` with the same identity resolution invoke() uses (`handler_identity` when internal, `identity` otherwise). Restricted ops return `NOT_FOUND` (spec-404) rather than `FORBIDDEN` — no information leaks about the restricted surface's existence or shape. The gate is in the handler itself, so every transport (wire `/call`, HTTP routes, MCP tools) is covered. - **Status:** resolved — 2026-08-30. ### CF-003 — `publish_schema` compile failure is fail-open on the wire dispatch path (2026-08-30) — RESOLVED 2026-08-30 - **Found in:** alkhttp Review 001 post-remediation task `review-001-publish-schema-validation-robust` (the GW-01/#1 follow-up work). While fixing alkhttp's HTTP-side instance of the pattern, the identical instance was verified on the wire dispatch path: `src/protocol/dispatch.rs:352-366` (`Dispatcher::dispatch`, `OperationType::Pub` arm) — when a Pub op's `publish_schema` failed to **compile**, the pump logged a warn and proceeded with `validator: None` (silent unvalidated ingest, plus per-request recompile cost). - **Fix:** fail-closed at the source — `publish_schema` is now compiled at **registration time** in both insertion points (`OperationRegistry::register` and `OperationRegistryBuilder::store`); an un-compilable schema is a registration error, so an unvalidated-ingest path can never be constructed. The compiled validator is cached per-op (`OperationRegistry::publish_validator`) and the dispatch path consumes the cache — the per-request compile is gone. The `from_call` import path inherits the guarantee (imports register through the same choke point); forwarded-chunk validation on the producer side is the remote peer's dispatch path and now inherits the same fail-closed property. - **Behavior change (semver-relevant):** `register`/builder methods return `Err` for an un-compilable `publish_schema` (previously: accepted). Code that registered garbage schemas will now get a registration error instead of a silently-unvalidated op. - **Status:** resolved — 2026-08-30. ### CF-002 — demux `TooLarge` skip allocates the full peer-declared length up front (up to ~4 GiB from an 8-byte header) (2026-08-30) — RESOLVED 2026-08-30 - **Found in:** alkhttp Review 001, finding WS-12 (cross-crate; `docs/reviews/001-initial-implementation-review.md`, Part B, verified against tree `4a825d3`). Filed to this ledger 2026-08-30. `src/channels/adapter.rs:144` (`run_demux_loop_for_client`) — the `ChunkError::TooLarge` arm allocated `let mut discard = vec![0u8; length as usize];` **before** reading: the buffer was sized from the peer's untrusted 8-byte header (`u32` `length`, up to ~4 GiB). - **Fix:** stream-skip with a fixed 64 KiB buffer + `read_exact` loop consuming `length` bytes — no allocation sized from peer input. Plus a cumulative skipped-bytes budget (256 MiB) that tears down the connection when a peer loops `TooLarge` headers to burn bandwidth/CPU. The budget resets on each valid chunk, so legitimate isolated oversized chunks (the resync path, covered by the existing test) are unaffected. The existing `demux_resyncs_after_oversized_chunk` test passes unchanged. - **Status:** resolved — 2026-08-30. ### CF-001 — `call_single_stream` write-failure maps to non-retryable `INTERNAL` (2026-08-29) — RESOLVED 2026-08-30 - **Found in:** alkhttp `from_wss` drop-monitor race tests (WS-02/CON-02, review-001-ws-eof-signal). When the transport mux died mid-call, the write path failed fast and the write-failure mapping produced a non-retryable `INTERNAL: failed to write request frame` — even though the call never reached the producer and a reconnect/retry would be safe. - **Fix:** new retryable `CallError::connection_closed` constructor (`CONNECTION_CLOSED` code, `retryable: true`), applied **only** to write failures where the call is provably undelivered: `call.requested` frame write failures on all consumer paths (`call`/`subscribe`/`publish`, single-stream and stream-per-request; the publish pump tags its write stages so only the request-frame stage is retryable). Mid-publish and completed-frame write failures stay `INTERNAL` (delivery ambiguous — retry unsafe), and the producer-side `fail_all(...)` on connection close stays `INTERNAL`. The new code string is additive to the wire error vocabulary; `CONNECTION_CLOSED` is new — consumers should treat unknown codes per their existing policy (the `retryable` flag is the machine-readable signal). - **Status:** resolved — 2026-08-30. alkhttp follow-up: the `review-001-ws-eof-signal` race test can now tighten back to retryable-only asserts.