Files
alkcall/docs/reviews/consumer-findings-ledger.md
T
glm-5.3-flash 8cb2a6eb6d fix: remediate consumer findings CF-001..004 (alkhttp ledger)
- 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.
2026-08-31 08:22:55 +00:00

111 lines
5.8 KiB
Markdown

# 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(&reg.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.