- version bump 0.8.0 -> 0.8.1 (patch per the dep-wave convention:
0.4.1 rode alkcall 0.7.1; alkcall 0.10.1 is a patch release with no
API or wire-format change, see the preceding consume commit)
- changelog: cut [0.8.1] with the alkcall 0.10.1 consumption (the
adopt-window ordering fix; no surface reached through the HTTP API)
- README dependency snippet 0.7.0 -> 0.8.1 (was left stale by the
0.8.0 release)
Verification: cargo test (467), clippy --all-targets -D warnings,
fmt --check, cargo publish --dry-run clean
Picks up the adoption-window ordering fix (alkcall review 013, routed
from alkgit's wave-2 gate): a chunk routed by the channels demux inside
the adopt_channel install->drain window was delivered ahead of
earlier-parked early arrivals, inverting the byte-stream order (alkgit
pkt-line desync), plus a related loss variant that parked a chunk
forever. This crate reaches the fixed paths via the shared
ChannelManager; no alkcall API surface used by this crate changed.
The requirement is tightened 0.10 -> 0.10.1 so downstream projects
composing this crate with alkcall resolve the fixed version rather than
an older 0.10.x lockfile entry.
Verified: cargo test (467), cargo test --all-features (530), clippy
--all-targets -- -D warnings, fmt --check, fuzz corpus replay 21/21.
Three inline findings from the session review of ADR-072/ADR-073:
- fix(auth): Basic door parsing no longer requires UTF-8 — the decoded
password bytes pass through to resolve_from_token byte-faithfully, so
RFC 7617 legacy ISO-8859-1 credentials resolve instead of parsing to
None. ADR-073 already specced raw-bytes passthrough ("decoded and fed
as the AuthToken raw bytes"); the String::from_utf8 round-trip
contradicted it.
- test: pin the with_ws_max_sessions fix against the shape it fixed —
a builder call after with_ws_max_sessions (the pre-router_state()
builder silently reverted the cap).
- test: pin that ownership wiring survives a later HttpAdapter builder
rebuild at the HTTP surface.
Verified: cargo test (451), --all-features (530), clippy --all-targets
-D warnings, fmt --check, fuzz corpus replay 21/21, cargo doc.
review 008 resolution:
- AH-1: door_auth_middleware on extras doors resolves
Authorization: Basic base64(user:pass) — password field as the
token (GitHub-PAT shape, git-lfs / git smart-HTTP clients) — through
the same IdentityProvider::resolve_from_token backend as Bearer; no
new trait method, resolve-fails -> None fallback preserved. Bearer
surfaces stay Bearer-only: the gateway endpoints, /openapi.json,
/healthz, the MCP nest, and the WS upgrade path never resolve Basic.
The 'Bearer-only is the auth mechanism' doc line flips (ADR-073).
- AH-2 ride-along: extras door route tables are static — the merged
router owns the decoy fallback, a second fallback panics at
construction; documented on with_extra_routes and ADR-046 §1.
docs: ADR-073, ADR-046 §1/§4 amendment, ADR-004 presentation table,
review 008 marked resolved; new server::* re-exports; dep:base64
behind the server feature.
Verification: cargo test (449 pass, incl. door Basic resolve-through
and gateway-stays-Bearer-only pins), test --all-features (528),
clippy --all-targets -D warnings, fmt --check, fuzz corpus replay
(21/21), cargo doc.
Ride the alkcall 0.10 gateway ownership seam (review 012, ADR-048
amendment) into the door layer:
- HttpAdapter::with_ownership_provider /
HttpAdapter::with_static_ownership_fallback mirror alkcall's builder
shape; OwnershipProvider re-exported next to GatewayDispatch
- RouterState carries the wiring; GatewayState::dispatch is the single
spine builder for both the gateway routes and the MCP nest
- loud default passes through: a resource_type-gated op on an unwired
spine is refused (FORBIDDEN envelope -> 403/401) on HTTP exactly as
on the native wire
- WS native-session path untouched (consults ownership natively)
fix: with_ws_max_sessions never stored the fresh session-slot
semaphore; later builder calls silently reverted the cap. Builder
state now assembled through one router_state() helper.
infra: fuzz/shared follows to alkcall 0.10 (two alkcall copies in one
build would type-clash); alkhttp 0.8.0 per the dep-wave convention.
Verification: cargo test (444 pass, incl. 3 new call_resource_gated_*
HTTP-surface pins), clippy --all-targets -D warnings, fmt --check,
test --all-features (520), fuzz corpus replay (21/21), doc.
Report-only from alkgit's LFS POC: AH-1 the shared bearer middleware
is Bearer-only while real git-lfs authenticates Basic on the batch;
AH-2 extras doors cannot carry a second route fallback (docs-only
record). No mechanism change fixed here; dispositions session-owned.
- version bump 0.6.0 -> 0.7.0 (minor per the dep-wave convention:
0.5.0 rode alkcall 0.8.0; see the preceding consume commit)
- README usage snippet 0.5.0 -> 0.7.0
- changelog: cut [0.7.0] with the alkcall 0.9.0 consumption under
Changed (the stub-sweep audit: no ManagerError/RelayError matching,
no Establishment construction, no marked-pub surface reached through
the HTTP API) and the two ride-through fixes under Fixed
(register_imported Result in the WS overlay tests, fuzz/shared spec
alignment)
Verification: cargo test/clippy/fmt/doc green in the preceding deps
commit; no source change in this commit
- bump alkcall 0.8.1 -> 0.9 (gateway feature unchanged). No code change
forced: none of the 0.9.0 breaking surfaces are constructed or
pattern-matched in this crate — no ManagerError match arms (the new
InvalidAdopt variant changes nothing here), no RelayError matching
(the removed PubTypedOpen variant is unreachable from this crate),
no Establishment construction or Debug dependence (the WS ferry
passes Option<OpenEstablisher> through unchanged, per the 0.8.0
audit), and the op/register forward-through refactor does not touch
the from_wss import filter or the adapter-registered leaf bundles
- ride-through fixes: tests/ws_overlay_ops.rs now expects
register_imported's new Result<(), String> return (8 sites assert
registration success instead of discarding the value — the bool
return they consumed was ignored anyway); fuzz/shared follows the
main crate to alkcall 0.9 so the harness replay resolves one
alkcall version (the stale 0.8.1 spec pinned two copies against
alkhttp's requirement and broke the shared-crate build)
- minor bump 0.6.0 -> 0.7.0 per the dep-wave convention (0.3.0 rode
alkcall 0.6.0, 0.4.0 rode 0.7.0, 0.5.0 rode 0.8.0): this crate's
public API carries alkcall types through re-exports and the
ProtocolHandler impl, so a semver-breaking upstream is
semver-relevant here
Verification: cargo test 441+70 pass (default), 520+70 pass
(all-features), clippy --all-features --all-targets -D warnings clean,
fmt --check clean, doc --no-deps clean, fuzz corpus replay 21/21
- websocket/upgrade.rs: OpenableAlpn's spec doc advertised the removed
`channel:pub_open_not_implemented` stub (upstream C-08, fixed in
alkcall with the single-publish fixes) — rewritten to the current
contract: Pub-typed marked specs are refused loudly at registration
by `register_openable_with_establisher`; marked pub openables route
through `ChannelCore::register_pub_openable`. Also dropped the
stale 'alkcall 0.6.0' version pin
- adapters/from_openapi.rs: clippy rust-1.99
`cloned_ref_to_slice_refs` — std::slice::from_ref instead of a
single-element to_string slice
Verified: cargo test (458 ok), clippy --all-targets -D warnings,
fmt --check
Exited 0 at 2765 s: 96k execs at ~33/s, coverage 1378 edges / 6655
features / 734 corpus, still growing at budget end, zero findings.
Wave 4 is fully done: five of five targets exited 0.
45-min release-budget campaigns, all five targets launched
concurrently: ws_chunk_framing / spec_import / ws_session_opseq /
retry_throttle all exited 0 (coverage saturated or still growing at
budget end; details in plan §5). sse_parser aborted at 1913 s on
FWD-22 (harness-assert fix, previous commit); 45-min post-fix rerun
in flight. Corpus merges deliberately deferred.
The sse_parser re-frame probe emitted 'data: <line>' unguarded, so a
payload CR (interior CR in multi-line data: the payload line is '\r')
was consumed by strip_cr at re-parse time and the round-trip lost it.
Wave 1 had skipped only the terminal-CR case. Found ~32 min into the
first 45-min sse_parser campaign.
- frame_event emits 'data: <line>\r': the guard CR is the line
terminator, strip_cr removes exactly it, the payload CR survives
- drop the terminal-CR skip; the round trip is now total over
JSON-encodable payloads (JSON never encodes a raw CR, so the
residual asymmetry is the serde_json non-preserved key-order class)
- no crate-side change: the parser was correct (data: x\r\r\n ->
x\r); only the harness re-encode was weak
- regression: 2 new seeds (interior CR via \r escape; raw CR via CRLF
framing, two-line doc) + unit cases
Verification: shared 21/21 (default + no-default-features), main
crate green, clippy -D warnings clean, fmt clean, cargo fuzz build
clean
Two stateful op-sequence harnesses (fuzz/shared):
- ws_session_opseq: the WS byte adapter's generic pumps driven
end-to-end over a mock socket flavor through the new
byte_adapter::fuzzing drive hub (FuzzMockSocket flavor,
FuzzChannels/FuzzWriteSide channel bundle, pump spawn funnels,
FuzzInboundChunkProgress). Asserts lossless inbound forwarding
(exact after the teardown joins), the WS-01 walk's completed-chunk
count vs the declared frame count, the WS-15 close mapping
(1002/1011/verbatim-requested + per-cause reasons), lossless write
framing onto the declared chunk stream (FWD-21's failure-carry
included), lane-failure isolation (the alkcall 0.8.1 class), and
pump termination within the bound.
- retry_throttle: the inlined RetryAfterMiddleware's throttle map
through the new client::fuzzing hub — capacity bound, the
expired-first + Farthest-future eviction policy, the ceiling clamp,
refresh contraction, the Retry-After parse partition, and the
jitter bounds.
16 committed seeds (8 per target) in gen_fuzz_seeds.py, hand-encoded
against the arbitrary 1.4.x derive layout with ceiling-division
variant tags and per-element keep-going bytes, pinned by
seed-decode tests. fuzz/shared gains bytes/futures/tokio/url/
http/httpdate/arbitrary deps.
Verification: main crate 441 default tests, fuzz shared replay 21/21
(default + no-default-features), cargo fuzz build clean (5 targets),
clippy+fmt clean both crates.
FWD-21, found minutes into the first ws_session_opseq campaign: one
feed that drains a complete chunk and then hits an oversize header
(or the pending cap) in the same call returned
Walk::Oversize/PendingOverCap and silently discarded the already
drained pieces — the walk's own consumption-accounting contract
('every parsed byte is either emitted or retained', wave-1
invariant 6) was violated by the crate. Production could not reach
the shape (the mux's per-write invariant caps pending at one chunk +
header), so impact was nil, but WriteChunkWalk is public API and the
wave-1 split_walk equality check was skipped on exactly this path.
- Walk::Oversize(Vec<Vec<u8>>) / Walk::PendingOverCap(Vec<Vec<u8>>)
carry the pre-failure pieces (user-approved semver-relevant API
change)
- run_write_pump emits the carried pieces before fail_write_pump, so
a 1011 close never swallows a legal chunk the mux already counted
as written
- the ws_chunk_framing harness's split-drive failure arm now
reconciles the parsed-before-failure bytes across the whole and
split drives; a regression seed covers the drain-then-fail shape
Verification: main crate 441 default / 520 all-features tests, fuzz
shared replay 21/21 (default + no-default-features), cargo fuzz build
clean, clippy+fmt clean both crates.
Both 10-minute detached campaigns (§5 budget) ran to completion:
sse_parser 18.9k execs / 1100 edges (still discovering at budget end),
ws_chunk_framing 4.8M execs / 245 edges (saturated) — zero crashes,
hangs, OOMs, or leaks on all fork jobs; artifact dirs empty.
One policy gap the first campaigns exposed: the grown corpus entries
(hash-named files in fuzz/corpus/<target>/) leaked as git noise under
the un-ignored corpus dirs. The hash-pattern gitignore now separates
the committed seed-* files from campaign output, per the quinn/h2
model.
docs/plans/fuzzing.md: §3 table marks targets 1-2 implemented; §5
gains the grow-corpus policy + campaign results; §6 sequencing marks
step 1 done. FWD-20 already carried the fix log from 5971c0a.
Verification: corpus replay 80/80 green, cargo test 441 passed, fmt
--check clean.
Adopt the alk*/fuzzing posture (alkcall 0.8.1 precedent) — the dep
bump this ride-along builds on was 4005517:
- docs/plans/fuzzing.md: the four-target plan (sse_parser,
ws_chunk_framing this session; spec_import + the stateful wave
later), the infrastructure blueprint, and the corpus/campaign
policies (§3's table tracks progress)
- fuzz/ workspace (the sibling layout): nightly-pinned subtree,
stable fuzz/shared invariant library (corpus replay = the standing
fuzz gate), detached runner (never a foreground child of an agent
session), JSON dict, deterministic seed generator; 80 committed seeds
- two wave-1 targets: sse_parser (SseParser::feed + the FWD-17
envelope mapping — the parser every subscription forwards through)
and ws_chunk_framing (WriteChunkWalk, the write pump's chunk walk)
- crate-side surfacing: SseParser/SseEvent/sse_event_envelope are pub
behind the adapters::forward::fuzzing re-export hub; the write
pump's inline chunk loop extracted as WriteChunkWalk (pump byte
behavior unchanged — 441 tests green); retained_bytes() diagnostic
(the alktunnels/alksocks posture)
- root Cargo.toml gains the explicit [workspace] table with
fuzz/ excluded (auto-discovery would pull the nightly-consumed
dev-deps into the stable build — the alkcall trap); fuzz/ joins the
publish exclude list
- AGENTS.md verification checklist gains the corpus-replay line
FWD-20 fix, found by driving the new sse_parser harness: feed()
rescanned the whole reassembly buffer per feed — a dribble-fed
subscription (one unterminated line over many small chunks) cost
quadratic CPU (1 MiB dribble ≈ minutes per connection, unbounded
total). Line assembly now walks only the arriving chunk: the buffer
holds just the current line, the scan never revisits drained bytes.
1 MiB dribble: ~5 min (quadratic) → 21 ms (linear). Regression test
sse_parser_dribble_feeds_do_not_rescan_the_buffer.
Verification: cargo test 441 passed; cargo test --all-features ok;
clippy --all-targets -- -D warnings (main + fuzz/shared) clean;
fmt --check (main, fuzz, fuzz/shared) clean; cargo fuzz build clean;
corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml) 80
seeds green.
Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alksocks
08a3694 (the sibling wave-1 shape), docs/plans/fuzzing.md §2
Adopts the fuzzing pattern proven in alkcall/alktty/alktunnels/
alksocks: fuzz/ workspace with stable-toolchain shared invariant
crate, committed seed corpora, corpus replay as the standing gate,
detached runner for campaigns. Four targets planned — sse_parser and
ws_chunk_framing (this session), spec_import and the stateful wave
(later sessions). §3 table and §6 sequencing track progress.
Picks up the duplicate adopt/open collision fix found by alkcall's
manager_routing fuzz campaign (alkcall CHANGELOG 0.8.1): a duplicate
adopt_channel/open_channel no longer replaces the live channel's
state. alkhttp reaches the fixed paths via the shared ChannelManager;
no alkcall API surface used by this crate changed.
Verified: cargo test (456 passed), cargo test --all-features,
clippy --all-targets -- -D warnings, fmt --check.
- version bump 0.4.1 -> 0.5.0 (minor per the dep-wave convention:
0.3.0 rode alkcall 0.6.0, 0.4.0 rode alkcall 0.7.0)
- README usage snippet 0.4.1 -> 0.5.0
- changelog: cut [0.5.0] with the alkcall 0.8.0 consumption under
Changed — the additive-only audit (reply fields, relay/hub-leg,
flavor-form discovery) and the one ride-through discovery change
(explicit channel_open_alpn for non-standard open-op names)
Verification: cargo test/clippy/fmt/doc green in the preceding deps
commit; no source change in this commit
- bump alkcall 0.7 -> 0.8 (gateway feature unchanged). No code change
forced: alkcall 0.8.0 is additive on the call-plane JSON and the
registry seams (review 008's remediation — the establisher reply
projection, flavor-form open-op ids in discovery, the in-tree
ChannelRelay + hub-leg install template) and none of the new
surfaces are constructed in this crate: no Establishment reply
fields (the WS ferry passes Option<OpenEstablisher> through
unchanged), no marked specs imported through from_call (the from_wss
filter excludes the protocol-session ops), no ChannelRelay /
HubLegImports / HubLegTemplate consumption (hub-leg assembly stays
at the assembly layer)
- one ride-through on this crate's discovery surface: a deployment's
with_ws_openable_alpns open-op specs with non-standard names now
serve an explicit channel_open_alpn string through services/list +
services/schema (alkcall 0.8.0 U-1, ADR-047 amendment 3) so hubs
relaying via from_call reconstruct the marker; standard-shape names
stay byte-stable
Verification: cargo test 456 pass (default) / 589 (all-features),
clippy --all-targets -D warnings clean (default + all-features),
fmt --check clean, doc --no-deps clean
- version bump 0.4.0 -> 0.4.1 (patch: the MSRV raise is a metadata
change that breaks no downstream able to build the crate before)
- README usage snippet 0.4 -> 0.4.1
- changelog: cut [0.4.1] with the alkcall 0.7.1 consumption + the
rust-version 1.85 -> 1.88 floor raise under Changed
Verification: cargo publish --dry-run --allow-dirty (cargo test/clippy/
fmt green in the preceding deps commit)
- the 1.85 claim was already false at the dependency level: the resolved
lockfile pulls icu_* 2.x (MSRV 1.86 via jsonschema -> idna) and, since
alkcall 0.7.1, alkcall itself declares 1.88 — 1.85 toolchains cannot
build the tree regardless of what Cargo.toml declared
- raising the floor to 1.88 aligns with the ecosystem MSRV decision
(noq QUIC path, per the alknet MSRV audit; recorded for alktls in
OQ-TLS-08) and breaks no downstream that could build the crate before
- alkcall 0.7.1 rides in via the lockfile (version = "0.7" already
covered it); no code change forced — 0.7.1 is its own MSRV raise plus
the cf-006/cf-007 work consumed in the 0.7.0 wave
- one 1.88 clippy lint fixed: uninlined_format_args in a forward.rs test
Verification: 1.85 check fails as documented (icu_* 1.88 floors); 1.88
toolchain check/test/clippy --all-targets -D warnings clean; stable 1.94
test 456 (default) / 589 (all-features), clippy -D warnings (default +
all-features), fmt --check, doc --no-deps
- version bump 0.3.0 -> 0.4.0 (the CONNECTION_CLOSED -> 503 gateway
mapping is a wire-visible behavior change for HTTP callers matching
on the previously-emitted 500 — a minor bump, not a patch)
- README usage snippet 0.3 -> 0.4
- changelog: cut [0.4.0] with the alkcall 0.7.0 wave entry
(CF-005/006/007, no code change forced) + the CONNECTION_CLOSED
mapping under Changed (wire-surface behavior)
Verification: cargo test (456 passed), cargo test --all-features
(589 passed), clippy -D warnings (default + all-features), fmt
--check, cargo publish --dry-run --allow-dirty
Dependency wave + one wire-surface behavior fix:
- bump alkcall 0.6 -> 0.7 (gateway feature unchanged). No code change
forced: this crate constructs no ServingConfig (the one 0.7.0
struct-literal break — from_wss keeps the pure-consumer
from_connection; opt-in serving remains the review-006 Part C
deferred decision), and the CF-006 per-call opener-identity overlay
is behavior-only upstream (hub-forwarded WS-ferry opens now resolve
the end client's identity, the intended effect)
- gateway error table maps CONNECTION_CLOSED -> 503 Service
Unavailable: alkcall 0.7.0's CF-007 amendment makes it the eighth
protocol code (ADR-016's list is formal at eight); it previously
fell to the catch-all 500, misreporting a retryable,
provably-undelivered call as a permanent server failure. retryable:
true, so the retry_after details path applies on 503. Tests pin the
mapping and the Retry-After passthrough
- ADR-023 amended: the protocol-code list is eight (ALREADY_EXISTS +
CONNECTION_CLOSED rows added to the §3 table, normative rule and
call.error code list updated); http-adapters.md six-code references
de-staled
Verification: cargo test (456 passed), cargo test --all-features
(589 passed), clippy -D warnings (default + all-features), fmt
--check, cargo doc --no-deps (0 warnings)
Mechanical pass for the alkcall 0.6.0 bump (review 007's
establishment follow-ups sweep: R-01 Establishment plan payload,
R-02 OpenHandler lifetime doc note, R-03 pump_bidi extraction):
- bump alkcall 0.5 -> 0.6 (gateway feature unchanged)
- the OpenHandler signature gained a plan parameter —
Fn(Value, Option<ChannelPlan>, Connection, AuthContext) ->
JoinHandle<()> — so the test's echo_open_handler closure gains
`_plan` (the only closure-construction site in this crate)
- no ferry change: this crate constructs no Establishment and passes
the Option<OpenEstablisher> through unchanged (OpenableAlpn fields
and register_openable_with_establisher threading are unchanged in
0.6.0); the establisher's Establishment.plan now reaches the pump
handler's second parameter process-locally (ADR-049 amendment 2)
- docs updated: websocket.md (plan threading + pump_bidi pointer),
OpenableAlpn field docs, with_ws_openable_alpns doc comment
- CHANGELOG [Unreleased] consumption-wave entry
Verification: cargo test (454 passed), cargo test --all-features
(587 passed), clippy -D warnings (default + all-features),
fmt --check, cargo doc --no-deps clean
AGENTS.md, lib.rs, the README, and adapters/mod.rs docs all state
openapi is the shared spec model implied by both feature sides, and
the README's client row claims from_openapi under client — but the
feature graph only had server -> openapi. A lean client-only build
compiled yet silently lacked from_openapi (gated all(client,
openapi)).
Verification: cargo test (default + all-features), clippy both
feature sets -D warnings, fmt, publish --dry-run — all green.
The [Unreleased] section still led with the alkcall 0.2 bump; the
0.3 (per-session fork, op/register, UP-03), 0.4.0 (registry
input_schema enforcement), and 0.4.1 (early-arrival chunk parking)
waves were unrecorded. Also removes a copy-paste duplication in the
CF-003 entry.
Verification: cargo test, clippy (both feature sets), fmt, doc,
publish --dry-run — all green.
alkcall 0.4.0 now enforces OperationSpec.input_schema at dispatch
(INVALID_INPUT). All 438+16 tests pass; the gateway path is
unaffected because alkhttp's imported ops validate through their own
CompiledInputSchema closure, which composes with the new registry
check (registry = raw JSON Schema semantics, adapter = closed by
default).
cargo doc --no-deps was not actually clean before review 007's
remediation — it emitted 5 warnings that went uncounted (grep for
"^error|^warning" in the gate runs matched only on error/warning at
line start, and the review baselines recorded "clean" without a
counting check; the warnings were first noticed while re-verifying
the review-007 gate set). All pre-existing, none introduced by
review 007:
- src/adapters/mod.rs: the bare [`OpenAPISpec`] intra-doc link did
not resolve (the re-export sits in the same module, but rustdoc
resolves module-level links against the module's own scope) —
spelled out as `openapi_spec::OpenAPISpec`
- src/websocket/mod.rs: [`upgrade`] did not resolve from the
`websocket` module doc (the submodule is not in scope for the
parent's doc links) — plain `upgrade`
- src/websocket/upgrade.rs:215: [`CallConnection`] had no import in
scope — spelled out via the full alkcall path
- src/websocket/upgrade.rs:291: [`install_channel_zero`] linked a
private item from public docs — plain `install_channel_zero` hook
- src/server/adapter.rs:274: [`crate::websocket::OpenHandler`] is
not re-exported from `websocket` — spelled out via the alkcall
path
Verification: cargo doc --no-deps 0 warnings (was 5); cargo test 454
passed / 0 failed; cargo test --all-features 587 passed / 0 failed;
clippy (both configs) clean; fmt clean.
Review: docs/reviews/007-ws-data-channel-surface-review.md (the
baseline's "cargo doc → clean" claim was wrong on this dimension)
- WS-31: websocket.md §"Data channels for browsers" + ADR-067's landed
note record that WS-session discovery is the bootstrap set — the
hook's bootstrap `services/*` registrations overwrite a
base-registry `services/*` registration on the WS path by design
(a deployment's custom `services/list` is shadowed on WS sessions
only).
- ADR-048's landed note: correction + completion — the WS-26
retention sentence was aspirational at the landed commit (WS-28) and
is now real; the UP-02 posture's override half is now an explicit
surface (`with_ws_op_register_acl` / `OpRegisterAcl`), with the
note that `ChannelsPolicy` could not carry an op ACL.
- ADR-067's landed note: review-007 notes (WS-28 fix + gate, WS-29
surface, WS-31 record).
- OQ-05 resolution: the retention claim carries the WS-28 correction.
- review-006 UP-02 log + WS-26 paragraph: corrections marking what the
pre-fix tree did not have, with the landed remediation named.
- review-002 WS-17: the "bounded at 64 sessions" claim corrected —
the bare-registry semaphore was per-request and bounded nothing;
`SessionSlots` is the shared-cap surface.
- review-007 status: open for remediation → remediated, with the
decisions taken (both "implement" options) and the gate names.
Verification: cargo test 454 passed / 0 failed; cargo doc --no-deps
clean (6 pre-existing warnings, identical at baseline).
Review: docs/reviews/007-ws-data-channel-surface-review.md
Review 007 Unit 2 (the two "implement" decisions taken during
remediation, plus the coverage gap):
- WS-29: the `op/register` override surface review-006 UP-02 and
ADR-048 recorded as landed is now implemented. The hook threads an
`op_register_acl` `AccessControl` into `op_register_spec` (the
permissive `AccessControl::default()` remains the default
everywhere); the built-in surface sets it via
`HttpAdapter::with_ws_op_register_acl`, bare-registry/custom routes
via the `OpRegisterAcl` request extension (mirroring
`ChannelsPolicy`/`WsTimeouts`/`OpenableAlpns`). A peer whose
identity does not satisfy the ACL gets `FORBIDDEN` on the announce.
Gates: builder path (`FORBIDDEN` scope-less / announce-ok scoped)
+ extension path.
- WS-30: the bare-registry `SessionState` is built by `FromRef` per
request, so its default-cap semaphore bounds nothing across
requests (corrects review-002 WS-17's "bounded at 64 sessions"
claim). New `SessionSlots` request extension carries the shared
semaphore for routes that need an effective cap; the upgrade
handler prefers it over the state value. Doc comments corrected
(`SessionState`, `WsTimeouts`, `ws_upgrade_handler`). Gate:
cap-1 route → 503 over cap → slot freed on session end.
- WS-32: the built-in openables threading
(`with_ws_openable_alpns` → `RouterState` → `SessionState` → hook)
gets its first gate — every Unit-3 gate rode the `OpenableAlpns`
extension fallback. `builder_path_openables_serve_the_data_channel_
surface` discovers the openable via `services/list`, opens the
channel, and round-trips bytes through the builder-built router.
Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 587 passed / 0 failed (+5 gates); clippy (both
configs) clean; fmt clean.
Review: docs/reviews/007-ws-data-channel-surface-review.md
The WS-26 `ConnectionGuard` was bound inside its `if let` block, so it
dropped microseconds after insertion instead of living for the
channel-0 dispatcher task — `live_connections()` /
`live_connection_count()` were permanently empty for every session
(review 007 WS-28 [major], reproduced empirically; the handle
retention ADR-048 + review-006 record was aspirational at that commit).
Remediation (review 007 Unit 1):
- bind the guard as an `Option<ConnectionGuard>` in the channel-0
task's frame, mirroring `SessionGuard`'s shape in
`run_channels_session`; the block comment now describes the real
scope
- gate `live_connections_visible_mid_session_and_drain_after_teardown`
— handle visible mid-session (after a completed call proves the
dispatcher is up), drained after teardown; verified to fail against
the pre-fix tree and pass with the fix
Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 582 passed / 0 failed; clippy (both configs) clean; fmt
clean.
Review: docs/reviews/007-ws-data-channel-surface-review.md (WS-28)
First review whose subject is the landed wiring itself (review 006
Units 2-4, 030c5ef/2053420/64fa10b), not the gap to it.
Findings:
- WS-28 [major] — the WS-26 ConnectionGuard is bound inside the
`if let` block and drops when it ends, not when the channel-0 task
ends: live_connections() is permanently empty for every session.
Reproduced empirically through the live WS path (scratch test,
run + removed). ADR-048's landed note and review-006's log record
behavior the code does not have.
- WS-29 [minor] — the op/register ACL override recorded as landed
(review-006 UP-02, ADR-048) is not implemented; the hook hardcodes
AccessControl::default() and ChannelsPolicy cannot express an op ACL.
- WS-30 [minor] — the bare-registry default session cap is built per
request (FromRef) and bounds nothing; corrects review-002 WS-17.
- WS-31 [minor] — install_bootstrap_discovery silently shadows a
deployment's own services/list on WS sessions (upstream-mandated,
unrecorded).
- WS-32 [minor] — the router-state openables threading
(with_ws_openable_alpns) has no gate; every Unit-3 gate rides the
extension fallback.
Plus non-findings bounding the re-review (UP-01 arm, policy threading,
from_wss exclusion set, extension precedence, gate fidelity) and a
sequenced remediation plan.
Baseline at 64fa10b: 454 / 582 tests, clippy both, fmt, doc — all clean.
- OQ-05: deferred → resolved (2026-09-04, review 006 Unit 2+3); the
consumer-set reframe recorded (WS is also the native-client fallback
behind hostile NAT/firewall; OQ-04 does not block the wiring).
- ADR-067: status amendment + the v1-cut blockquote gains the Wired
(2026-09-04) note — per-session-fork shape, openable surface, gates.
- ADR-048: landed-state amendment — §4's hub→browser direction has its
object (op/register → connection overlay, hub composes via the
retained Arc<CallConnection>); the op/register ACL posture (UP-02,
SRV-10 precedent) recorded.
- websocket.md: the step-7 deferral note and the §"Data channels for
browsers" status block removed (the section now documents the landed
surface: with_ws_openable_alpns, the OpenableAlpns fallback, cap
policy, discovery, gates); idle-knob deployment note for silent data
channels (semantics unchanged; the 60 s default bites more often —
set None at assembly for long-lived interactive channels).
- Review 003 status → remediated (all findings closed; log in review
006); its Unit-4 section marked landed.
- alknet-ADR-044 §5 pointer checked: not stale.
Verification: cargo test 454 / 0; --all-features 582 / 0; clippy
(both) clean; fmt clean; doc clean.
The alkcall 0.3.1 fix (fd21230, alkhttp review 006 UP-03) lands
PeerCompositeEnv::peer_operations, so services/list-peers now lists
peer-announced ops. Version pin stays 0.3 (semver-compatible); the
lockfile bump carries no source change.
- Extend op_register_served_per_session_and_collision_is_already_exists
with the discovery assertion that originally surfaced UP-03: after
the announce resolves, services/list-peers attributes consumer/exec
to the alice peer entry (fails against 0.3.0, verified by the
original draft).
- Review 006 UP-03 entry records the 0.3.1 landing (alkcall ADR-030).
Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 582 / 0; clippy (default + all-features, all-targets,
-D warnings) clean; fmt clean; doc clean.
- Unit 2/Unit 3 recorded as landed (030c5ef): hook rework shape,
policy threading (the ChannelsPolicy seam the ledger gate caught),
WS-26 handle retention, from_wss protocol-op exclusion, UP-01, the
six new gates, and the harness hardening notes (request-id filter,
flat CallError payload shape).
- UP-02 decided: op/register serves per session with the default ACL
(SRV-10 posture); the visibility tension resolves as ACL-gate.
- UP-03 [minor, alkcall-side] filed: services/list-peers cannot list
peer-announced ops — PeerCompositeEnv overrides peer_ids but not
peer_operations (trait default Vec::new()), so the amendment's
peer-discovery promise does not resolve on the wire. Discovered by
the gates; fix lives in alkcall; alkhttp's gate asserts
overlay-landing + collision semantics instead and is not blocked.
- Unit 4 (docs reconciliation) remains open.
Verification unchanged: cargo test 454/582, clippy both sides, fmt,
doc — clean.
The WS path wires the alkcall 0.3 per-session mechanisms — the OQ-05
deferred half (review-003 WS-20/21/22/25/26, the decisions WS-24/WS-25
resolved upstream):
- install_channel_zero reworked per the ADR-047 §4 amendment #2 shape:
fork the deployment's base registry, register the generic channel
ops (channel/close, channel/control, channel/resources/subscribe —
WS-21), the deployment's openable ALPNs (ChannelCore::register_openable,
WS-22), the bootstrap discovery set closed over the fork
(install_bootstrap_discovery, F-06), and op/register (WS-25; the
collision set is the fork per review-005 G-03), then dispatch over
the fork. The session's ChannelsPolicy rides the hook (one policy
instance across open wrappers and the demux teardown path).
- OpenableAlpn { spec, open_handler } + HttpAdapter::with_ws_openable_alpns,
threaded RouterState -> SessionState -> hook, with the OpenableAlpns
request-extension fallback (mirroring ChannelsPolicy/WsTimeouts).
- WsSessions retains the channel-0 Arc<CallConnection> (WS-26) with a
self-removing guard (ConnectionGuard); live_connections() is the
deployment-visible surface.
- UP-01: ALREADY_EXISTS maps to 409 Conflict in the gateway error map.
- from_wss import excludes the protocol-session ops (bootstrap set +
channel lifecycle ops): the fork serves them per session, and proxying
session-scoped machinery (e.g. channel/close across sessions) would be
nonsense. Discovery runs first, the filter is the listing minus those
names.
- adapter_install_channel_zero cfg matches its caller (WS-27); it
inherits the reworked hook (session ops now served in the from_wss
test-server producer too).
Gates (Unit 3, tests/ws_upgrade_session.rs; the WS-23 e2e set):
open -> channel_id -> discoverable in services/list -> chunks both
ways -> handler sees bytes; channel/close resolves + ledger decrement;
cap denial (channel:-prefixed); mid-open disconnect teardown; TooLarge
demux resync through the WS path (16 MiB + 1 skip consumed);
op/register announce + overlay-collision + serving-registry-collision
ALREADY_EXISTS through the WS path.
call_and_await now filters by request id and tolerates data-channel
chunks (a prior Sub's trailing call.completed may interleave).
Verification: cargo test 454 (default) / 582 (all-features), clippy
both sides -D warnings clean, fmt clean, doc clean.
Records the review pass of alkcall 0.3.0 against this crate:
- WS-24 (dispatch-resolution mechanism) and WS-25 (client-side op
registration) verified resolved upstream — per-session fork (alkcall
ADR-047 §4 amendment #2) + op/register (alkcall ADR-022 amendment
2026-09-03 + 2026-09-04 collision sub-amendment); the G-01 serving-
loop concurrency rework lands here via the bump alone.
- One new finding: UP-01 [minor] — ALREADY_EXISTS (new 0.3 protocol
code) unmapped in the gateway error table (would 500); fix rides
Unit 2. UP-02 records the op/register ACL posture decision for
Unit 2.
- WS-20/21/22/26/27/23 re-verified still open, now unblocked; the
remediation plan re-sequences (Unit 2 wiring with the concrete
fork shape, Unit 3 gates + two upstream-mirroring additions,
Unit 4 docs).
Baseline gates at df86f89: cargo test 453/575, clippy both sides,
fmt, doc — all clean.
Commits the re-point edits left uncommitted in the working tree
(5b62307's follow-through; alkcall review 005 verified these lines at
that tree), updated to the post-review-005 state:
- ADR-048 reconciliation note gains the 2026-09-04 update: alkcall
review 005 remediated the landed mechanisms (serving-loop
concurrency G-01/G-02, op/register collision policy G-03, spec
round-trip G-04, bootstrap-list alignment G-05) and alkcall 0.3.0
shipped them; alkhttp now consumes 0.3. The ADR-022 collision
sub-amendment binds here: a peer-announced op never shadows the
serving side's own registrations — the WS session's op/register
handler gates on the session fork.
- OQ-05 resolution gains the same dated update and extends the
cross-references to alkcall reviews 004-005.
What remains here is still alkhttp-side wiring only (review 003
Unit 2).
Bump alkcall 0.2 -> 0.3 (published 2026-09-04; the remediation of
alkcall reviews 004-005, the upstream resolution of alkhttp review 003
WS-24/WS-25). Two source-break classes, both mechanical:
- take_call_connection now returns Option<Arc<CallConnection>>
(was bare value) — drop the double-wrap in WssSession::connect.
- register/register_on/register_openable take &OperationRegistry
(was &mut) — drop 51 stale `mut` bindings (clippy --fix), which is
the interior-mutability change that makes the per-session fork
surface possible.
No behavior change in this crate yet; the 0.3 mechanisms (fork,
serve_single_stream, op/register, install_bootstrap_discovery,
collision policy) are the composition surface for alkhttp review 003
Unit 2 (WS-20..22, WS-26 wiring) — not wired here yet.
Verification: cargo test 453 passed (default) / 575 passed
(all-features), clippy both sides -D warnings clean, fmt clean,
cargo doc --no-deps clean.
The alkcall decision work (WS-24 dispatch-resolution mechanism, WS-25
client-side op registration) now lives upstream as alkcall review 004.
Unit 1 records the leading candidates and their alkhttp consequences:
- WS-24 -> option (a), per-session base registry (the only
wire-proven dispatch shape; fork surface = alkcall F-02)
- WS-25 -> channel-0 bootstrap op (op/register as an assumed op in
the bootstrap set; requires the alkcall client serving half, F-04)
- discovery composition: services/list on the fork + peer-registered
ops via the already-built services/list-peers
Consumer-set reframe from WS-23 unchanged; no findings added or
removed.
Focused review of the OQ-05 deferral (review-001 WS-03): what is
missing to wire browser/native data channels over WS, the deferral
rationale check, and the design gaps found behind the 'nothing new
to design' assumption.
Findings (continuing review-002 numbering):
- WS-20..22 [major]: mechanical wiring gap — install_channel_zero
discards the ChannelManager, generic channel lifecycle ops never
registered, no openable-ALPN deployment surface
- WS-23 [minor]: no browser-opened-channel e2e test; OQ-05 reopen
trigger reframed (native WS consumers are not blocked on OQ-04)
- WS-24 [major, cross-crate]: top-level dispatch never consults the
connection overlay — the ADR-047 §4 amendment mechanism cannot
resolve open ops on the wire (needs an alkcall decision)
- WS-25 [major]: no wire mechanism for client-side op registration —
ADR-048 browser-side bidirectionality is undesignable as specified
- WS-26 [major]: no retained live connection handle; hub cannot
reach a session's overlay or push toward it
- WS-27 [minor]: dead-code-gated test hook cleanup
Includes a non-findings section (channel-id split, demux hardening,
open-op wrapper completeness, idle-knob posture) and a 4-unit
remediation plan sequenced so the alkcall decision task (Unit 1)
gates the alkhttp wiring shape (Unit 2).
Verification: cargo test (453) / --all-features (575), clippy
(all-targets, both feature sets), fmt — all clean at 58665f2.
- README covering the gateway surface, both import/export adapter
families, feature flags, and security posture (all claims verified
against the current code)
- standard MIT and Apache-2.0 license texts matching the
license = "MIT OR Apache-2.0" manifest field
- Cargo.toml: point the manifest at README.md and exclude scripts/
from the published package
Split the feature graph so consumers pulling only the import adapters
(from_openapi / from_jsonschema / from_mcp) no longer compile the axum
/ hyper server stack, and server-only deployments no longer compile
reqwest. One crate, one import path — sides cut by features, not by a
crate split.
Feature graph:
- server (default): axum host, gateway, WS upgrade, to_openapi, to_mcp
- client (default): client host, forward, from_jsonschema, from_openapi
- openapi: shared OpenAPISpec model (implied by both sides)
- mcp: from_mcp needs client, to_mcp needs server
- wss: tungstenite transport (from_wss); tungstenite half of the
shared WS↔byte-stream adapter
- h2/http1: hyper protocol features; imply server
Wire-contract neutral: gateway endpoints, ALPNs, and all public API
shapes unchanged; defaults keep both sides on.
Supporting changes:
- forward.rs drops its axum::body::Bytes type leak (bytes crate types)
- bounded_join + error-echo caps move to input_validation (usable by
both sides; openapi_spec no longer imports from forward)
- byte_adapter: axum flavor compiles under server, tungstenite under
wss; the generic pumps stay shared (WS-11)
- input_validation / openapi_spec import-only internals gated to the
side that consumes them
- http-body-util moves to dev-dependencies (was test-only)
- integration-test required-features updated for the new sides
- from_wss unit tests (axum producer harness) gated to server
Verified: cargo test (defaults, 453) and --all-features (575) pass;
lean side builds (client / server / client,mcp / client,wss /
server,wss / openapi-only) build clean with zero warnings;
clippy -D warnings clean across all feature combinations; fmt clean.
Bump the alkcall dependency to 0.2 (with the gateway feature) and
converge on the promoted shared pieces:
- The local dispatch spine (gateway/dispatch.rs, 721 lines) is deleted;
GatewayDispatch, schema_disclosure_denial, and DEFAULT_DEADLINE are
re-exported from alkcall::gateway (alkcall ADR-048). The 30 s default
deadline preserves the previous behavior exactly.
- gateway/schema_cache.rs (PublishSchemaCache) is deleted: alkcall CF-003
compiles publish_schema at registration time and exposes
OperationRegistry::publish_validator; the /publish chunk stream
resolves against it. Un-compilable schemas are now rejected at
registration, so the two end-to-end fail-closed tests were reworked
into a registration-rejection test (a stronger guarantee).
- schema_disclosure_denial consumers (to_mcp, routes) use alkcall's
promoted implementation; the alkhttp-local copy is gone (ADR-071
updated: the guard stays as defense-in-depth, the implementation no
longer forks).
- CF-001: from_wss drop monitor and the WS overlay tests use
CallError::connection_closed; the review-001-ws-eof-signal race tests
now assert retryable CONNECTION_CLOSED on both resolution paths (the
tolerated non-retryable INTERNAL write-failure outcome is gone).
- Added CHANGELOG.md (Keep a Changelog), Unreleased section records the
bump and convergence.
Verification: cargo test default 453 ok, wss 470 ok, mcp 526 ok,
all-features 575 ok; clippy -D warnings clean (default + all-features,
all-targets); fmt clean; cargo doc warning-free.
Net: -1093 lines.
Decision: advertise == enforce. The key allowlist (OAI-02) stays as the
first gate with its established unknown-key message; a compiled leaf
validator now runs second, so required/type/enum/pattern/bounds
violations surface as INVALID_INPUT 422 naming the keyword — not as
upstream round-trips.
- new src/adapters/input_validation.rs: CompiledInputSchema compiles an
op's input_schema once at import with the jsonschema crate (same
2020-12 dialect publish_schema uses) and validates peer input at call
time; the compile-time copy is hardened closed-by-default
(additionalProperties: false injected when absent) so the validator
reproduces the allowlist's unknown-key semantics; explicit
additionalProperties:true catch-all and schema values are preserved;
the original spec value is never mutated
- from_openapi/from_jsonschema import(): compile per registration,
capture the validator in the handler closure (re-import recompiles —
the closure capture is the invalidation story); a non-compilable
input schema fails import loudly (AdapterError::SchemaParse naming
the operation), matching the publish_schema fail-closed precedent
- from_openapi generated input schemas now carry
additionalProperties:false explicitly, so the /schema advert states
the enforced rule and external schema-driven validators reach the
same verdicts
- forward/forward_stream/build_request gain an
Option<&CompiledInputSchema> parameter; enforcement runs after the
allowlist
- round-trip test (review 002 Test-gap 10): the /schema-exported
input_schema is compiled with the same validator and driven against
build_request over a 10-input violation matrix — accept-sets exactly
equal in both directions; the chain-test that lets advertise/enforce
drift surface as a CI failure
- ADR-066: new decision section (advertise==enforce) with the trust-
boundary reasoning and the rejected option (b) rationale
- module + enforce_input_schema docs updated to the two-gate shape
cargo test --all-features 596 pass; clippy --all-features/-D warnings,
fmt, doc gates clean.
docs(tasks): mark review-002-fu-oai18-decision done
- forward_stream build-error arm (forward.rs): wire-level test asserts
one INVALID_INPUT envelope then stream end with zero upstream
contact (a panicking responder counts as the contact guard), plus
the from_jsonschema integration mirror (undeclared key + non-scalar
placeholder, each naming its rejection source)
- PEM read-failure arms (http_client.rs): nonexistent CA path →
CaBundleRead (sync new), nonexistent client-cert path → ClientCertRead
(async reload, prior generation retained)
- Over-cap poll_write rejection leg (byte_adapter.rs): cap+1 write →
InvalidData naming the cap; stream stays usable for an at-cap write
afterwards
- SSE parser edges: CRLF split across feed chunks frames one line;
invalid-UTF8 data lines drop without killing the frame stream
- from_value structural rejects: non-object doc, missing `info`,
missing `paths`, non-object `paths` each name the member
- Connection-failure arms: accept-path ConnectionClosed →
HandlerError::ConnectionClosed via stream_error_to_handler; read-pump
demux-gone break ends the pump when the byte-stream side is dropped
- Delete the caller-less `impl Default for WsTimeouts` (the extension
is constructed explicitly)
cargo llvm-cov --all-features: all named arms covered; TOTAL regions
94.18% (was 93.86%), lines 96.04% (was 95.77%); http_client.rs
86.56% lines (was 81.72%).
docs(tasks): mark review-002-fu-stream-error-coverage done
- dispatch.rs: qualify [invoke]/[invoke_streaming] as
[GatewayDispatch::invoke]/[::invoke_streaming] (module-level docs
need the type-qualified path)
- byte_adapter.rs: drop redundant explicit link targets for
DEFAULT_WS_WRITE_TIMEOUT / DEFAULT_WS_IDLE_TIMEOUT (already
re-exported at crate::websocket)
- from_wss.rs, byte_adapter.rs: backtick WssSession::drop and the
private WsFraming (all-features-only link errors, same class)
- Ws pump/timeout module docs spot-checked against WS-13/18/17 impls:
no drift found
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps now exits clean both
default-features and --all-features.
docs(tasks): mark review-002-fu-doc-warnings done
The bracketed planning task's five candidates, re-validated against
the post-bulk tree (0a932e5, all 23 bulk tasks done, 446/568 green):
1. OAI-18 -> review-002-fu-oai18-decision: still key-allowlist-only;
bulk blockers (yaml/path-item) both landed. Task records the
enforce-vs-scope-the-advert decision framework from review 002.
2. CON-08/CON-09 close() lever: deferred still (documented v1
contracts unchanged, no consumer pressure) — not tasked.
3. Cross-crate: CF-001..004 all open in alkcall, no alkhttp-side task;
PRJ-16 guard already documents its defense-in-depth re-scope
disposition (no work when CF-004 lands).
4. Stale-comment sweep: mostly absorbed by the bulk (SRV-11 comment
rewritten in-fix); residue = 3 cargo-doc warnings + WsTimeouts
redundant Default -> review-002-fu-doc-warnings.
5. ADR-045 version audit: clean (1.4.0 + annotations + test pin).
Post-bulk coverage re-pass (95.89% regions, bulk's new code fully
covered: PRJ-16 guard, body cap, router reorder, batch cap, WS
knobs, OAI-11 node budget all exercised) surfaced one unanticipated
residue -> review-002-fu-stream-error-coverage (forward_stream
build-error arm never exercised on a Sub op; PEM read-failure arms;
over-cap poll_write; SSE split-CRLF edge; from_value structural
rejects; WsTimeouts Default).
Bracketed pass itself marked completed (planning consumed).
taskgraph: 69 valid, no cycles; 3 pending.
The OAI-06 matrix lived only in the completed review-001 task notes;
acceptance for the review-002 loudness cluster requires the successor
doc section. New 'Loud unsupported-feature handling' section on
from_openapi: refused/warned/projected feature tables covering cookie
params, style/explode forms, servers, webhooks, callbacks, security,
oneOf requestBodies, unresolvable or content-less requestBody refs,
path-template validation, collision rejection, ref-sibling warns,
discriminator/xml warns, error-projection mappings, and the OAI-17
error-bounding contract.
build_request inserts header params first, then default_headers, then
credential headers — HeaderMap::insert replaces, so a declared
in: header parameter whose name matched a default or credential header
silently never delivered the peer's value upstream.
check_header_param_collisions runs at import (the assembly's
HttpServiceConfig is visible there): Authorization on an authed
namespace is rejected outright, a default_headers name match (compared
case-insensitively) fails import naming both keys, and an ApiKey
header_name match fails naming the credential header. Per the task's
decision note: the import-time surface does see the adapter's config,
so the loud point stays at import rather than first-call warn-once.
Tests: Authorization+Bearer, X-Tenant/x-tenant case-insensitive
default_headers, x-api-key/X-API-Key, and the no-collision clean path.
Internal/ACL-restricted op specs were readable through
POST /call {"operation":"services/schema","input":{"name":...}}
(the MCP call/batch tools identically): the outer-name pre-checks pass
(services/schema is External) and alkcall's services_schema_handler
projects any registered spec with no visibility/ACL check of its own
(alkcall CF-004 is the complete fix there).
- GatewayDispatch.invoke/invoke_streaming now apply the GET /schema
route's is-internal + access-control checks to the meta-op's inner
name input before dispatch (404 Internal / FORBIDDEN ACL), one
interception point covering /call, /batch, /subscribe and the MCP
call/batch tools; /publish cannot reach the Query-typed meta-op
- the visibility+ACL check is one shared fn (schema_disclosure_denial)
used by the HTTP /schema route, the dispatch guard, and the MCP
schema tool, so transports cannot drift
- when CF-004 lands, this guard remains as defense-in-depth (ADR-071)
Tests: dispatch-spine guard unit tests; /call 404 + 401/403 matrix,
/batch NOT_FOUND entry, /subscribe error event; MCP call/batch tools
via services/schema with an Internal inner name (mcp feature).
Verify: cargo test (405), --all-features (523), clippy default and
--all-features --all-targets -D warnings, fmt --check — all pass.
- corrupt CA bundle section -> CaBundleParse naming the path (the
pure-non-PEM variant parses as zero sections and is accepted by
reqwest, so the arm needs structurally broken PEM)
- garbage client identity -> ClientCertParse naming the cert path,
with the error asserting key material is never echoed
- config() reflects a reloaded CA bundle on the TLS path (FWD-12)
Import errors echoed unbounded spec-derived strings: a 100k-path
servers-override list produced a multi-megabyte SchemaParse message.
forward::bounded_join caps list echoes at 8 items / 128 chars per item
with a ', … (+N more)' suffix, and is applied at the servers/callbacks
/security location lists, the unbound-placeholder and unbound-remnant
lists in build_registration/forward, and the call-time declared-keys
echo. resolve_ref caps interpolated $ref strings at 128 chars.
Tests: 100k-path servers fixture asserts message < 4 KiB (linear,
completes fast); bounded_join unit test pins count+width truncation
with unchanged small-case output.
GW-16: empty body / malformed first line / missing header fields /
per-line cap / batch over-cap rejections now route through
call_error_to_http_response_with_identity, mapping INVALID_INPUT to
422 — same status as mid-stream chunk errors. One error class, one
status.
GW-17: invoke_sink wraps the registry sink invoke in the same 30 s
tokio::time::timeout the Once-op invoke uses; a hung sink handler
surfaces as a TIMEOUT (504, retryable) error envelope instead of
holding the HTTP connection forever. The sink wrapper bounds the
whole dispatch (chunk pacing included), matching http-server.md's
deadline contract.
Docs: http-server.md error table documents the 422 triggers and the
sink deadline; http-adapters.md batch cap status corrected.
to_openapi: gateway spec version 1.3.0 -> 1.4.0 (ADR-045 minor):
/publish framing faults and /batch cap reject documented at 422 (the
400 slots moved with the runtime); /publish 400 slot removed; 504
description covers the sink dispatch.
Verification: scripts/verify.sh OK (397 tests); cargo test
--all-features OK (513 tests); clippy --all-features --all-targets -D
warnings OK; cargo fmt --check OK.
The OAI-06 loudness matrix had holes: callbacks, security requirement
blocks, top-level oneOf requestBodies, and schema-level
discriminator/xml keywords all vanished silently at import.
- callbacks + security: OpenAPISpec::validate_import_loud_features
(new, run from FromOpenAPI::import) rejects with locations and
remediation. Scoped to the service-import path, not the shared
from_value parse — the published gateway doc round-trips through
from_value inside to_openapi and legitimately declares security
markers for its external clients.
- top-level oneOf requestBody (no content map): refused in
parse_operation as an unresolvable/content-less body (OAI-15 arm,
message names OAI-14 oneOf).
- discriminator/xml inside consumed schemas: per-operation
tracing::warn listing the ignored keys (JSON-forwarding-only stance).
Tests: op-level callbacks, doc-level + op-level security, oneOf
requestBody each fail import naming the feature and location.
- new tests/retry_policy_wire.rs: POST+500 = 1 upstream hit (method
gate), GET 500/503/200 = 3 hits (retry-to-success), budget exhaustion
stops retries under a 50-attempt cap (wall-time + hit-count bounds)
- forward_stream terminal arms on the wire: oversized SSE line ->
one INTERNAL terminal envelope; mid-stream transport abort (staged
via a notify gate so the abort is genuinely mid-stream) -> terminal
envelope after the delivered frame; pending event EOF-flush; dead
port through both forward() and forward_stream()
- Once-path decode arms: malformed application/json 200 -> INTERNAL
decode envelope; application/octet-stream 200 -> byte-array envelope
- COV-12 credential arms: ApiKey and Basic malformed values fail
loudly without echoing secret material; declared header-param
invalid name/value rejections
- new tests/client_config_reload.rs: config() reflects a reloaded
config (FWD-12 atomicity half)
$ref siblings are ignored under OpenAPI 3.0 semantics but would apply
under 3.1 — a 3.1-authored constraint beside a $ref previously
vanished silently, overstating /schema. warn_ref_siblings now fires at
each $ref consumption point (operation parameters, requestBody,
path-item parameters) naming the location and dropped keys, and the
module doc records the version stance: no openapi 3.1 gate, 3.0
reading with the warn as the visibility mechanism.
A requestBody $ref that could not resolve (or resolved to a shape
without a content map) previously turned into a content-less Operation
registered silently — every call would INVALID_INPUT on the gateway
body key. parse_operation now treats an unresolvable request-body ref
and a resolved-but-content-less body the same unfaithful-modeling way
as an unresolvable parameter ref: the operation fails import via the
OAI-04 'unresolvable $ref' arm, whose message now names the
requestBody case (OAI-15) alongside the parameter one.
Tests: self-ref requestBody, missing-component requestBody ref, and
content-less component requestBody each fail import loudly.
- same-host 302 followed with the credential header arriving at the
followed hop (FWD-03's scrub is cross-host only)
- cross-host 302 surfaced as HTTP_302 with the attacker endpoint
receiving zero requests (load-bearing FWD-03 property, pinned)
- redirect loop trips the hop cap -> loud INTERNAL transport error
Wire-level round trip through the real rmcp server: a scalar input
reaches the remote tool as {"value": <input>}, matching the
value_to_json_object wrap.
Import-time path-template checks now run on both adapters:
- from_jsonschema: validate_path_template moves to forward.rs (same
checks, same messages, shared implementation)
- from_openapi: build_registration calls it before building each op, so
a template like /x{open fails import with 'unterminated placeholder'
instead of a per-call INTERNAL on first invoke
- new test: unterminated_path_template_fails_import_not_first_call
peer.call_tool through the real rmcp streamable-HTTP transport for
schema/call/batch/unknown — the production ServerHandler::call_tool
routing shell, previously only exercised via the invoke_tool bypass
for search.
Verification: cargo test --features 'mcp test-support' --test full_surface
assert_eq! in library code was a panic-family residue in the import
collision check (review 002 JS-03). The three vectors are built in
lockstep by import(); zipping them preserves the pairwise walk without
the panic path.
A logical request's Retry-After waits are now bounded by
max_total_retry_duration, and a retry storm can no longer re-arm a
full ceiling per attempt:
- BudgetClock anchored per logical request by RetryGateMiddleware
(into the request Extensions, shared across retry attempts) and
read by the inner RetryAfterMiddleware every attempt; the monotonic
anchor projects the hard stop through wall-clock steps.
- maybe_sleep_for truncates the sleep to the remaining budget;
a spent budget skips the sleep entirely.
- record() keeps the EARLIEST deadline per URL (retry storms cannot
extend the first-seen deadline); a refresh that cannot make it
under the budget hard stop drops the entry so the next attempt
starts immediately instead of parking.
- Middleware without a budget anchor (budget = 0) keeps the prior
semantics; the shared client now wires
HttpClientConfig.max_total_retry_duration into the Retry-After
middleware.
- Wire tests (tests/retry_after_budget.rs): always-429 responder with
a 300 s Retry-After is bounded by budget + one attempt; separate
logical requests still honor the recorded throttle window.
- FWD-12 atomic reload pairing and the FWD-15 stream-client split
untouched (stack built in build_client_with_pems for both).
- assemble_request_url now verifies after Url::set_path that the decoded
URL path segments are byte-identical to base_dir + decoded rendered
segments; a mismatch (any future normalizer rewrite in the url crate)
fails loudly with INTERNAL instead of silently re-routing an
authenticated request
- property-style corpus test over dot/percent/binary values: accepted
values must survive byte-identical as one literal segment with no lone
dot segments; failures may only be INVALID_INPUT
Verification: scripts/verify.sh (383 passed) and --all-features (499
passed), clippy -D warnings, fmt --check all pass.
- value_to_path_segment now rejects scalar values whose decoded form is
exactly '.', '..', '%2e', '%2E', '%2e%2e', or '%2E%2E' (case-insensitive)
with INVALID_INPUT: url 2.5.8 Url::set_path silently normalizes lone
dot segments away, so such values would route to a different upstream
endpoint than the template describes, with namespace credentials attached
- rejection is exact-match on the full decoded segment: dotted values
like v1.2.3, .hidden-file, ..hidden, ... still render
- error names the failure mode but never echoes the raw value
- empirically pins the set_path normalization behavior in a test
(tenants/../resources -> /resources, /files/.. -> /, %2E%2E -> normalized)
Verification: scripts/verify.sh (382 passed) and --all-features (498
passed), clippy -D warnings, fmt --check all pass.
The FWD-08 remediation made every malformed credential fail loudly, but
an authed operation whose registry capability was entirely absent fell
through the build_request match: the request was sent with no credential
and no diagnostic, producing corrupted upstream 401s at call time.
- build_request now returns an INTERNAL error naming the missing
capability keys (api_key:{ns} / http_token:{ns}) when an auth scheme
is declared and Capabilities::get is empty; the request is not sent
- auth_scheme: None behavior unchanged (unauthenticated ops stay
unauthenticated); error message carries key names only, no secret
- module doc: loud-missing matrix now covers malformed name/value AND
absent capability
- tests: unit loud-error across all three schemes, unchanged-arm pin,
wire test asserting the upstream receives zero requests (mirrors the
FWD-08 test family)
- from_openapi/from_jsonschema no_env_vars tests updated: they pinned
the old silent fall-through; still assert no env material echoes
Verification: cargo test (380 passed), cargo test --all-features
(496 passed), clippy --all-targets -D warnings (default + all-features),
cargo fmt --check — all via scripts/verify.sh
- PRJ-16b: BatchResultEntry.error now refs a defined BatchError
component (oneOf over the six protocol-code envelopes plus a generic
BatchOperationError arm carrying the operation-declared code enum);
the dangling #/components/schemas/CallError ref is gone
- PRJ-17: operation-declared errors at protocol statuses with
HTTP_-prefixed codes merge into the shared protocol response's oneOf
(per-code CallError_<code> components) instead of clobbering it —
the runtime genuinely emits both; non-protocol statuses overwrite
as before
- PRJ-18: /publish 400 dropped the INVALID_OPERATION_TYPE claim
(runtime reports that at 401 without a token, error.rs); the 401
entry is the true one and already documented
- PRJ-19: 415 (missing/non-JSON Content-Type) and plain-text 422
(shape-rejection) extractor slots documented, extending the
plain-text extractor rejection family; /subscribe gains 415/422,
/call gains 415 with the shape-rejection noted on 422, /search and
/schema gain the slots too
- PRJ-20: /call 401 now carries the identity-split oneOf (FORBIDDEN +
INVALID_OPERATION_TYPE), matching error.rs's 401-without-identity
mapping for both
- PRJ-21: /batch's unreachable 500 removed (all dispatch failures are
in-band entries; routes.rs has no 500 path)
- PRJ-23: the OAS-invalid x-operation-error-statuses pseudo-schema key
inside components.schemas removed (nothing consumed it; any openapiv3
registry rejects it as an invalid schema name)
- info.version 1.3.0 per ADR-045 (doc-contract corrections, wire
contract unchanged)
Verification: cargo test (to_openapi suite 42/42 green, incl. the
populated-registry openapiv3 parse and deterministic golden checks)
- enforce the same 100-operation cap the HTTP /batch endpoint enforces;
over-cap \x60calls\x60 reject with a structured INVALID_INPUT (retryable:
false, matching CallError::invalid_input) before any dispatch
- hoist MAX_BATCH_OPERATIONS to gateway/mod.rs and reuse it in routes,
to_openapi (removing a pre-existing duplicate literal), and to_mcp
- state the limit in the batch tool description and add maxItems to the
input schema (doc previously advertised no limit)
- GatewayDispatch gains a per-instance invoke_count spy accessor so the
over-cap test proves zero dispatches (process-global counters raced
under the parallel test runner)
- tests: over-cap -> INVALID_INPUT + invoke_count()==0; at-cap -> 100
results + invoke_count()==100
verification: scripts/verify.sh (352 passed) and scripts/verify.sh
--all-features (468 passed); cargo clippy --all-targets -D warnings and
cargo fmt --check clean
Coverage-confirmed dead code (every binary, zero hits):
- server/state.rs: drop FromRef<RouterState> impls for
Arc<OperationRegistry> and Arc<dyn IdentityProvider> — no route
extracts these types; the auth middleware receives the provider
directly via from_fn_with_state
- gateway/dispatch.rs: drop identity_provider() and resolve_bearer()
accessors; resolve_bearer's doc promised an auth hook the middleware
never calls (spec/code drift). Wire-or-delete resolved to delete:
bearer resolution lives in the middleware (SRV-11 single-resolve
ordering), the dispatch spine only needs the per-call
Option<Identity>. GatewayDispatch::new consequently takes the
registry alone (GatewayState loses its unused identity_provider
passthrough; dispatch.rs/to_mcp.rs tests simplified)
- websocket/upgrade.rs: drop FromRef<SessionState> for
Arc<OperationRegistry> — no router carries SessionState as its state
type; the inverse FromRef<Arc<OperationRegistry>> for SessionState
(custom upgrade routes, integration tests) remains
Verification: ./scripts/verify.sh (352 passed), ./scripts/verify.sh
--all-features (466 passed), clippy -D warnings, fmt --check.
§5 records the post-OAI-12 from_yaml contract: duplicate keys rejected
loudly on YAML (with the empirical correction that serde_json 1.0.151's
Value path last-wins rather than errors — the YAML side is the stricter
one), non-finite floats rejected with JSON-pointer context, merge keys
applied via apply_merge (shallow, referencing keys win; the one
deliberate YAML 1.2 deviation), scalar-key stringification matching the
core schema, and the no-new-bounds note (the walk stays inside
yaml_serde's parse-time limits).
Verification: cargo doc --no-deps clean; module-doc cross-check in
openapi_spec.rs matches this contract.
from_yaml previously went straight yaml_serde::from_str::<serde_json::Value>
with zero post-parse normalization. Three verified corruptions flowed
through unimpeded:
- duplicate mapping keys silently last-won (serde_json's visit_map
insert semantics — verified against 1.0.151: the JSON path last-wins
too, so the YAML path is now deliberately the stricter one);
- .inf/-.inf/.nan scalars silently became Value::Null because
serde_json::Number::from_f64(non-finite) is None;
- YAML 1.1 merge keys (<<: *anchor) survived as literal '<<' properties;
- non-string mapping keys (null keys, collection keys) either stringified
through YAML's debug rendering or panicked the conversion.
The new from_yaml pipeline: explicit yaml_serde::Value parse (native
loud duplicate-key rejection with line/column) -> apply_merge() (merge
keys applied, shallow per yaml_serde semantics; scalar/invalid merge
values fail loudly) -> one structural normalization pass into
serde_json::Value that rejects non-finite floats and non-string keys
with the offending JSON pointer, and stringifies scalar keys exactly as
the YAML 1.2 core schema renders them (200: -> "200", matching the
JSON path's {"200": ...}).
The walk stays inside yaml_serde's own parse-time bounds (recursion
limit 128, alias jump limit, RepetitionLimitExceeded); no new budgets
and no resolver changes. 14 seam tests added at the from_yaml boundary.
Verification: cargo test 366 passed / 0 failed; clippy
--all-targets -D warnings clean; fmt --check clean (scripts/verify.sh).