30 Commits
Author SHA1 Message Date
glm-5.3-flash 0f16c4f2f9 chore: release alkhttp 0.8.1
- 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
2026-10-22 06:29:32 +00:00
glm-5.3-flash cc80188e3d chore(deps): consume alkcall 0.10.1
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.
2026-10-22 06:28:51 +00:00
glm-5.3-flash dfa0216186 review: post-adoption pass over the alkcall-0.10 wiring pair (0766f4b, ad63507)
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.
2026-10-17 16:25:07 +00:00
glm-5.3-flash 0766f4bebe feat(server): door-surface Basic auth tolerance (review 008)
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.
2026-10-17 16:09:36 +00:00
glm-5.3-flash ad63507124 feat(server): thread alkcall 0.10 ownership wiring onto the HTTP surface
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.
2026-10-17 16:00:12 +00:00
glm-5.3-flash 1cac63fdba docs(review): 008 — HTTP-door consumer evidence (LFS Basic auth, route-fallback constraint)
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.
2026-10-17 07:28:45 +00:00
glm-5.3-flash 449661eb87 chore: release alkhttp 0.7.0
- 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
2026-10-14 05:20:13 +00:00
glm-5.3-flash c89aa65fb2 chore(deps): consume alkcall 0.9.0
- 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
2026-10-14 05:19:29 +00:00
glm-5.3-flash 2cf0dc7bea docs + clippy: drop the retired C-08 stub reference; cloned_ref_to_slice_refs
- 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
2026-10-13 11:32:16 +00:00
glm-5.3-flash 4b32f84bd2 chore: release alkhttp 0.6.0
The fuzzing adoption waves (commits 5971c0a..a16dee7) plus the alkcall
0.8.1 patch pickup, released as a minor bump:

- Added: fuzz/ infra (5 nightly targets, stable-toolchain corpus-replay
  gate), public SSE parse types (SseParser/SseEvent/SseParseError/
  SSE_EVENT_BUFFER_CAP), the WriteChunkWalk/Walk chunk-walk API, and
  the fuzzing re-export hubs.
- Fixed: FWD-20 (SSE dribble drain quadratic -> linear), FWD-21
  (WriteChunkWalk failure arms carry the pre-failure pieces), FWD-22
  (harness re-frame guard-CR; no crate change).
- Changed: alkcall 0.8.1 (duplicate adopt/open collision fix).
- Doc: fixed broken intra-doc link surfaced by SSE_EVENT_BUFFER_CAP
  going public.

Verified: cargo test (457 passed), cargo test --all-features (590),
cargo clippy --all-targets -- -D warnings, cargo fmt --check,
cargo doc --no-deps (0 warnings), fuzz corpus replay (21 passed),
cargo publish --dry-run --allow-dirty.
2026-09-29 21:02:32 +00:00
glm-5.3-flash a16dee7385 docs(fuzzing): sse_parser 45-min post-FWD-22 rerun clean
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.
2026-09-29 20:17:06 +00:00
glm-5.3-flash a7d3e3afc3 docs(fuzzing): wave-4 long-campaign results + FWD-22 recorded
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.
2026-09-29 19:25:42 +00:00
glm-5.3-flash b78b9a2618 fix(fuzzing): FWD-22 — guard-CR re-frame makes the SSE round-trip total
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
2026-09-29 19:25:39 +00:00
glm-5.3-flash 4e8a2cfddd docs(fuzz): record wave-3 campaign results + FWD-21 2026-09-29 17:28:23 +00:00
glm-5.3-flash e023d3275e feat(fuzz): target 4 stateful wave — ws_session_opseq + retry_throttle
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.
2026-09-29 17:28:20 +00:00
glm-5.3-flash 0ae53d2d27 fix(websocket): WriteChunkWalk failure arms carry the parsed pieces
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.
2026-09-29 17:28:14 +00:00
glm-5.3-flash 27478f936b docs(fuzz): wave-2 spec_import campaign clean — 2.34M execs, zero findings 2026-09-29 10:18:21 +00:00
glm-5.3-flash e6491e31c7 feat(fuzz): target 3 spec_import — import-surface fuzz harness + 42 seeds
- fuzz/shared/src/spec_import.rs: invariants over the import-parse
  surface — the three OpenAPISpec constructors (str/json/yaml,
  ADR-051 JSON-first), the YAML→JSON normalization boundary
  (reject-loudly partition: non-finite floats, pointer keys), the
  compile/harden/validate lane (hardening partition, validate
  determinism, closed-by-default unknown-key probe), the FromMCP
  build_spec tool-shape lane, and the $ref-bounded resolver lane
  (cycles/hop chains/deep nesting fail cleanly, no hang, no stack
  blowout).
- fuzz/fuzz_targets/spec_import.rs: thin fuzz_target! wrapper.
- 42 committed corpus seeds via gen_fuzz_seeds.py (valid JSON/YAML
  docs, structural failures, YAML normalization edges, $ref cycles/
  chains/unresolvable/external, schema-validation edges, MCP
  tool-shape edges); corpus-replay gate count floor 12.
- fuzzing exposure hubs next to the wave-1 one:
  openapi_spec::fuzzing, input_validation::fuzzing (opaque
  FuzzCompiledValidator newtype — the pub(crate) validator never
  leaks), from_mcp::fuzzing (mcp feature).
- fuzz/shared gains openapi/mcp features (default on); json.dict
  extended with the OpenAPI token set (75 unique tokens).
- clippy follow-up in wave-1's ws_chunk_framing replay test
  (unnecessary u32 casts against the newer lint).
- docs/plans/fuzzing.md §3/§6 target 3 → implemented; fuzz/README
  target table + campaign-advice lines updated.

Verification: corpus replay 11/11 green (default and
--no-default-features), cargo fuzz build clean, main crate 457
default / 590 all-features tests green, clippy+fmt clean both crates.
Campaign: detached 10-min smoke run — see docs/plans/fuzzing.md §5.
2026-09-29 10:06:03 +00:00
glm-5.3-flash 84c4b48407 fuzz: wave-1 smoke campaigns clean + grow-corpus gitignore fix
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.
2026-09-29 09:18:08 +00:00
glm-5.3-flash 5971c0a0d6 fuzz: adopt fuzzing (wave 1) + fix the SSE dribble quadratic (FWD-20)
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
2026-09-29 08:32:29 +00:00
glm-5.3-flash 779e3ea908 docs(plans): fuzzing plan for alkhttp (4 targets, sibling pattern)
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.
2026-09-29 07:22:15 +00:00
glm-5.3-flash 40055170a0 build(deps): bump alkcall to 0.8.1
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.
2026-09-29 06:42:17 +00:00
glm-5.3-flash 911f186629 chore: release alkhttp 0.5.0
- 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
2026-09-18 13:48:26 +00:00
glm-5.3-flash a63f4ed2c9 chore(deps): consume alkcall 0.8.0
- 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
2026-09-18 13:48:23 +00:00
glm-5.3-flash e854947c86 chore: release alkhttp 0.4.1
- 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)
2026-09-10 03:05:45 +00:00
glm-5.3-flash 660ccf3e76 chore(deps): consume alkcall 0.7.1; raise rust-version floor 1.85 -> 1.88
- 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
2026-09-10 03:05:27 +00:00
glm-5.3-flash 8832160db1 chore: release alkhttp 0.4.0
- 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
2026-09-07 18:11:07 +00:00
glm-5.3-flash dbaaa4ec8c chore(deps): consume alkcall 0.7.0 — CONNECTION_CLOSED maps 503
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)
2026-09-07 18:01:36 +00:00
glm-5.3-flash 126fdec243 chore: release alkhttp 0.3.0
- version bump 0.2.0 -> 0.3.0 (the alkcall 0.6.0 consumption wave
  carries a compile-breaking change for downstream users of
  `OpenableAlpn::new`: handler closures gain the `_plan` parameter,
  so this release cannot be a patch)
- README usage snippet 0.2 -> 0.3
- changelog: cut [0.3.0] with the alkcall 0.6.0 wave entry
  (OpenHandler plan parameter, Establishment.plan threading,
  pump_bidi availability)

Verification: cargo test (454 passed), cargo test --all-features
(587 passed), clippy -D warnings (default + all-features),
cargo fmt --check, cargo doc --no-deps,
cargo publish --dry-run --allow-dirty
2026-09-07 09:37:38 +00:00
glm-5.3-flash a9429dfb1d chore(deps): consume alkcall 0.6.0 — OpenHandler gains the plan parameter
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
2026-09-07 09:24:00 +00:00
193 changed files with 8897 additions and 255 deletions

No files matched your search

+9
View File
@@ -1,3 +1,12 @@
target/
node_modules/
.worktrees/
fuzz/target/
fuzz/artifacts/
fuzz/coverage/
# Grown corpora: the hash-named files the campaigns drop into
# fuzz/corpus/<target>/ are gitignored (the quinn/h2 policy); the
# committed seeds are the seed-* files, kept via the per-dir
# .gitignore un-ignores.
fuzz/corpus/*/[0-9a-f][0-9a-f]*
+10
View File
@@ -176,12 +176,22 @@ cargo test # full suite
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps # if docs changed
cargo test --manifest-path fuzz/shared/Cargo.toml # fuzz corpus replay (the fuzz gate)
cargo publish --dry-run --allow-dirty # before a release
```
If feature flags are added, also run `cargo test --all-features` and
`cargo clippy --all-features --all-targets -- -D warnings`.
The corpus replay is the standing fuzz gate (the alkcall
`docs/research/fuzzing.md` §7.9 posture): it replays every committed
seed through the same invariant functions the fuzz targets run, on
stable, without nightly. Campaigns (nightly, cargo-fuzz) run manually
via `fuzz/run-detached.sh` — never as a foreground child of an agent
session — after touching the SSE parser, the WS byte adapter, or the
import adapters, and before releases. See `docs/plans/fuzzing.md` and
`fuzz/README.md`.
## Architecture Context
- `docs/architecture/` — the authoritative spec. Read it before
+291 -1
View File
@@ -4,7 +4,297 @@ All notable changes to this crate are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this crate adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [0.8.1] — 2026-10-22
### Changed
- **`alkcall` dependency consumed at 0.10.1** (patch pickup; 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 channel's byte-stream
order, and a related loss variant parked a chunk forever after the
drain had removed its queue. This crate reaches the fixed paths via
the shared `ChannelManager`; no alkcall API surface used here
changed — no code change in this crate. The dependency 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.
Patch bump 0.8.0 → 0.8.1 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 (`EventEnvelope`, the channels 8-byte chunk
header), so nothing semver-relevant moves on this crate's surface.
## [0.8.0] — 2026-10-17
### Added
- **Door-surface Basic auth tolerance (review 008 AH-1, ADR-073).**
The extras-door surface now resolves `Authorization: Basic
base64(user:pass)` — the password field acts as the token and rides
the same `IdentityProvider::resolve_from_token()` backend as Bearer
(the GitHub-PAT shape `git-lfs` and git's smart-HTTP transport
authenticate with). No new trait method; the provider's grant table
governs exactly as it does for Bearer, and "unparseable /
unresolvable → None" fallback semantics are preserved. Basic is
scoped to the door surface: `bearer_auth_middleware` stays
Bearer-only on the gateway endpoints, `/openapi.json`, `/healthz`,
and the MCP nest, and the WS upgrade path is untouched — a Basic
credential contributes no identity on any path a deployment has not
explicitly mounted doors on. New public API:
`server::door_auth_middleware`, `server::extract_door_identity`
(`bearer_auth_middleware` / `extract_bearer_identity` keep their
Bearer-only semantics and their coverage).
### Changed
- **`alkcall` dependency bumped to 0.10.0** (the gateway ownership-seam
release, alkcall review 012) and the loud ownership default passes
through to the HTTP surface (ADR-072):
`HttpAdapter::with_ownership_provider` /
`HttpAdapter::with_static_ownership_fallback` thread the provider
into both spines this crate constructs — the gateway endpoints'
per-request spine and the MCP nest's spine (single wiring point in
`GatewayState::dispatch`). An unwired, undeclared spine now refuses
a `resource_type`-gated op with a `FORBIDDEN` envelope (**403** with
identity / **401** without) — the behavior break is inherited, not
re-decided; the WS native-session path is untouched (it consults
ownership natively).
- The gateway test-state builder threads ownership configuration; the
three dispositions (unwired refusal, live grant table, declared
static fallback) are pinned at the HTTP surface by
`call_resource_gated_*` regression tests.
### Documented
- **Extras door route tables are static (review 008 AH-2).** One doc
line on the door-facing surface (`HttpAdapter::with_extra_routes`)
and in ADR-046: the merged router already owns the decoy fallback
and axum refuses a second, so a door attempting `fallback_layer`
panics at construction — enumerate multi-segment namespace shapes
(`{user}/{repo}/...`) as pattern routes. Not a mechanism ask; the
next door author should not rediscover the panic.
### Fixed
- `HttpAdapter::with_ws_max_sessions` never stored the freshly built
session-slot semaphore on the adapter — any subsequent builder call
rebuilt the router with the pre-cap-change semaphore, silently
reverting the cap. Builder state is now assembled in one shared
`router_state()` helper, so every `with_*` rebuild sees the current
configuration. Pinned by the WS session-cap integration test running
with a builder call after `with_ws_max_sessions`, and by an
ownership-wiring-survives-rebuild test at the HTTP surface.
- Door-surface Basic parsing no longer requires the password field to
be UTF-8: the decoded password bytes reach
`IdentityProvider::resolve_from_token()` byte-faithfully, so RFC 7617
legacy ISO-8859-1 credentials (e.g. a latin-1 user password) that a
provider resolves still authenticate instead of parsing to `None`.
Minor bump 0.7.0 → 0.8.0 per the dep-wave convention (0.4.0 rode
alkcall 0.7.0, 0.5.0 rode 0.8.0, 0.7.0 rode 0.9.0): the re-exported
alkcall types are on this crate's public API, so a semver-moving
upstream is semver-relevant here.
## [0.7.0] — 2026-10-14
### Changed
- **`alkcall` dependency bumped to 0.9.0** (the stub-sweep release: the
four registered-but-always-failing stubs across nine published
versions are replaced with real implementations — the marked-pub
open contract (S-01), `channel/control` (S-02),
`channel/resources/subscribe` (S-03), and `op/register`
forward-through (S-04)). No code change in this crate — none of the
0.9.0 breaking surfaces are constructed or pattern-matched here: no
`ManagerError` match arms (the new `InvalidAdopt` variant changes
nothing), 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), and the
`op/register` forward-through refactor does not touch the
`from_wss` import filter or the adapter-registered leaf bundles.
The S-01 marked-pub machinery is consumer-side assembly, not an
HTTP-surface concern; the four gateway operations and the WS
session shape are unchanged.
### Fixed
- `tests/ws_overlay_ops.rs` now expects `register_imported`'s new
`Result<(), String>` return (alkcall 0.9.0 API change): the eight
connection-overlay registration sites assert success instead of
discarding the value. Test-only.
- `fuzz/shared` follows the main crate to alkcall 0.9 (the harness
replay previously resolved alkcall 0.8.1 alongside alkhttp's 0.9.0,
two incompatible copies in one build). Infra-only.
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.
## [0.6.0] — 2026-09-29
### Added
- **Fuzzing infrastructure adopted** (the sibling-crate pattern:
alkcall, alktty, alktunnels, alksocks). The `fuzz/` subtree holds
five nightly targets (`sse_parser`, `ws_chunk_framing`,
`spec_import`, `ws_session_opseq`, `retry_throttle`), a stable
toolchain shared crate replaying every committed seed (80 + 42 + 16
committed) through the identical invariant functions as plain
`cargo test` — the standing fuzz gate,
`cargo test --manifest-path fuzz/shared/Cargo.toml` — plus a
detached campaign runner and a deterministic seed generator. `fuzz/`
is excluded from publish; nightly stays confined to the subtree
(MSRV 1.88 unchanged).
- **New public API for the tested parse surfaces.** The incremental
SSE parser widens `pub(crate)` → `pub` (`SseParser`, `SseEvent`,
`SseParseError`, `SSE_EVENT_BUFFER_CAP`, `sse_event_envelope`) and
the WS write pump's chunk walk is extracted as public API
(`WriteChunkWalk`, `Walk`) — both direct-drive test targets. Five
`fuzzing` re-export hubs (`adapters::forward::fuzzing`,
`adapters::openapi_spec::fuzzing`, `adapters::input_validation::fuzzing`
with the opaque `FuzzCompiledValidator`, `adapters::from_mcp::fuzzing`
(\`mcp\`), `client::fuzzing` + `websocket::byte_adapter::fuzzing`
with the mock-socket pump drive) gather the harness access surface
at one named path each; the underlying crate-internal items keep
their visibility.
### Fixed
- **SSE dribble drain was quadratic in the dribbled bytes (FWD-20).**
`SseParser::feed` re-walked the whole reassembly buffer per feed, so
a subscription drip-feeding one giant unterminated line paid O(n²)
(64 KiB ≈ 12.5 s CPU per connection; 1 MiB ran to minutes) with no
per-stream total-CPU bound. Line assembly now walks only the
arriving chunk — the reassembly buffer holds just the current line —
measured linear (1 MiB dribble: 21 ms). Regression test:
`sse_parser_dribble_feeds_do_not_rescan_the_buffer`.
- **`WriteChunkWalk` failure arms discarded the already-drained pieces
(FWD-21).** One feed that drained complete chunks and then hit an
oversize header (or the pending cap) returned
`Walk::Oversize`/`PendingOverCap` empty, dropping bytes the mux had
already counted as written (the walk's consumption-accounting
contract was violated; production could not reach the shape, but
`WriteChunkWalk` is public API and the silent drop broke it). The
failure variants now carry the pre-failure pieces (`Vec<Vec<u8>>`)
and the write pump emits them before failing the stream (1011
close). Regression seeds cover the drain-then-fail shape in both
chunk harnesses.
- **FWD-22 (harness-assert fix):** the fuzz harness's SSE re-frame
round-trip probe is now total over anything JSON-encodable
(`frame_event` emits a guard CR as the line terminator; the
terminal-CR skip is dropped). No crate change.
### Changed
- **`alkcall` dependency consumed at 0.8.1** (patch pickup): the
duplicate adopt/open collision fix found by alkcall's
`manager_routing` fuzz campaign — a duplicate adopt/open no longer
replaces the live channel's state. This crate reaches the fixed
paths via the shared `ChannelManager`; no alkcall API surface used
here changed.
### Changed
- **`alkcall` dependency bumped to 0.8.0** (review 008's remediation
wave: the graduation upstream asks + the in-tree channel relay). No
code change in this crate — alkcall 0.8.0 is additive on the
call-plane JSON and the registry seams, and none of its new surfaces
are constructed here: this crate builds no `Establishment` reply
fields (the WS ferry passes `Option<OpenEstablisher>` through
unchanged; reply-field projection is an establisher-supplier
feature), imports no marked specs through `from_call` (the
`from_wss` operation filter excludes the protocol-session ops; any
domain op the remote serves imports as a forwarding bundle exactly
as before), and consumes neither `ChannelRelay` nor
`HubLegImports`/`HubLegTemplate` (hub-leg assembly remains the
consumer's concern at the assembly layer). One ride-through visible
on this crate's discovery surface: for a deployment's
`with_ws_openable_alpns` open-op specs whose names are not standard
`channels/<seg>/(sub|pub)` shapes, the served `services/list` /
`services/schema` output now carries an explicit
`channel_open_alpn` string alongside the boolean marker (alkcall
0.8.0's U-1, ADR-047 amendment 3) — so hubs relaying through a
`from_call` import of those ops reconstruct the marker instead of
seeing plain forwarding stubs. Standard-shape open-op names stay
byte-stable. Minor bump per the dep-wave convention (0.3.0 rode
alkcall 0.6.0; 0.4.0 rode 0.7.0).
## [0.4.1] — 2026-09-10
### Changed
- **`rust-version` floor raised 1.85 → 1.88** (patch bump, no API change).
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, so 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; recorded for alktls in OQ-TLS-08) and breaks no downstream that
could build the crate before. Verified: 1.88 toolchain `cargo test
--locked` + clippy `--all-targets -D warnings` clean; one 1.88 clippy
lint fixed (`uninlined_format_args` in a `forward.rs` test).
- **`alkcall` dependency consumed at 0.7.1** (lockfile bump; the
`version = "0.7"` requirement already covered it). No code change
forced — alkcall 0.7.1's changes are its own MSRV-floor raise plus the
cf-006/cf-007 feature work already consumed in the 0.7.0 wave.
## [0.4.0] — 2026-09-07
### Changed
- **`alkcall` dependency bumped to 0.7.0** (CF-005/006/007 — the
connect-side caller-identity seam + the downstream-findings sweep).
No code change in this crate: it constructs no `ServingConfig`
(the one struct-literal break — `from_wss` keeps the pure-consumer
`ChannelClient::from_connection`; opt-in serving remains the
review-006 Part C deferred decision) and the per-call opener-identity
overlay (CF-006) is behavior-only upstream — hub-forwarded opens on
the WS ferry now resolve the end client's identity instead of the
hub's, the intended effect. alkcall 0.7.0's ADR-016 amendment (the
protocol-code list is eight codes) resolves the upstream half of
review-006 Part C's doc-drift note.
### Changed (wire-surface behavior)
- **Gateway error table maps `CONNECTION_CLOSED` → 503 Service
Unavailable** (the alkcall 0.7.0 CF-007 amendment makes it the eighth
protocol code; ADR-023 amended). Previously it fell to the catch-all
500, which misreported a retryable, provably-undelivered call as a
permanent server failure. Reachable through the gateway when an
imported op's forwarding handler calls into a dropped transport
(`from_wss`'s pending-call fail-all is retryable
`CONNECTION_CLOSED`). The code is `retryable: true`, so the existing
`retry_after` details path applies on 503. Tests pin the mapping and
the `Retry-After` passthrough. Behavioral break for HTTP callers
matching on the 500 previously emitted for this code — hence a minor
bump, not a patch.
## [0.3.0] — 2026-09-07
### Changed
- **`alkcall` dependency bumped to 0.6.0** (review 007's
establishment follow-ups sweep). One consumer-visible change: the
`OpenHandler` signature gained a plan parameter —
`Fn(Value, Option<ChannelPlan>, Connection, AuthContext) ->
JoinHandle<()>` — so `OpenableAlpn::new` callers' handler closures
gain a `_plan` argument (mechanical). With an establisher attached
(ADR-049), its `Establishment.plan` (a typed-opaque
`Arc<dyn Any + Send + Sync>`) is threaded process-locally to the
handler's plan parameter: ALPN crates dial/allocate in the
establisher and deliver the live handle without a side-channel
handoff. This crate's WS ferry passes the `Option<OpenEstablisher>`
through unchanged and constructs no `Establishment` itself, so the
ride is otherwise additive — alkcall 0.6.0's `channels::pump_bidi`
helper (ADR-050) is available to data-plane handlers this crate
ferries. Docs updated (websocket.md, `OpenableAlpn` field docs,
adapter.rs).
## [0.2.0] — 2026-09-06
Generated
+4 -3
View File
@@ -27,9 +27,9 @@ dependencies = [
[[package]]
name = "alkcall"
version = "0.5.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbaeb718c370b74f9136d1d59b4608249bd7d1dd2332c0259f98351d709737f"
checksum = "690d747f42baf385dc22cd120f79d43cf1ea239bc426675e0e1e225b57fc031a"
dependencies = [
"async-trait",
"bytes",
@@ -48,12 +48,13 @@ dependencies = [
[[package]]
name = "alkhttp"
version = "0.2.0"
version = "0.8.1"
dependencies = [
"alkcall",
"arc-swap",
"async-trait",
"axum",
"base64 0.22.1",
"bytes",
"futures",
"http",
+10 -5
View File
@@ -1,19 +1,23 @@
[package]
name = "alkhttp"
version = "0.2.0"
version = "0.8.1"
edition = "2021"
rust-version = "1.85"
rust-version = "1.88"
license = "MIT OR Apache-2.0"
description = "HTTP interface for the alk stack: serves HTTP/1.1 + HTTP/2 on standard ALPNs (with WebSocket upgrade carrying the channels protocol) and hosts the HTTP-backed call-protocol adapters"
readme = "README.md"
repository = "https://git.alk.dev/alkdev/alkhttp"
keywords = ["http", "websocket", "rpc", "openapi", "mcp"]
categories = ["network-programming", "web-programming", "asynchronous"]
exclude = [".opencode/", "AGENTS.md", "docs/", "tasks/", "scripts/"]
exclude = [".opencode/", "AGENTS.md", "docs/", "tasks/", "scripts/", "fuzz/"]
[lib]
name = "alkhttp"
[workspace]
members = ["."]
exclude = ["fuzz"]
[features]
default = ["server", "client", "h2", "http1"]
# Producer side: the axum `Router` host (gateway routes, /healthz,
@@ -21,7 +25,7 @@ default = ["server", "client", "h2", "http1"]
# driver. `openapi` rides along because /openapi.json is part of the
# default surface (ADR-042/045); `http1` rides along so the hyper
# auto-builder always has at least one protocol compiled.
server = ["dep:axum", "dep:hyper", "dep:hyper-util", "dep:uuid", "openapi", "http1"]
server = ["dep:axum", "dep:base64", "dep:hyper", "dep:hyper-util", "dep:uuid", "openapi", "http1"]
# Consumer side: the shared outbound client host and the HTTP-backed
# import adapters (`from_jsonschema`, `from_openapi` with `openapi`,
# `from_mcp` with `mcp`).
@@ -42,9 +46,10 @@ h2 = ["server", "dep:hyper", "hyper-util/http2", "hyper/http2"]
http1 = ["server", "dep:hyper", "hyper-util/http1", "hyper/http1"]
[dependencies]
alkcall = { version = "0.5", features = ["gateway"] }
alkcall = { version = "0.10.1", features = ["gateway"] }
arc-swap = { version = "1", optional = true }
axum = { version = "0.8", optional = true, features = ["ws"] }
base64 = { version = "0.22", optional = true }
bytes = "1"
futures = "0.3"
http = "1"
+1 -1
View File
@@ -161,7 +161,7 @@ A lean single-side build takes `default-features = false` plus the side
it needs:
```toml
alkhttp = { version = "0.2", default-features = false, features = ["server"] }
alkhttp = { version = "0.8.1", default-features = false, features = ["server"] }
```
## Security posture
+2
View File
@@ -69,6 +69,8 @@ types — the former `alknet-core` and `alknet-call` merged).
| [069](decisions/069-webtransport-out-of-scope.md) | WebTransport Out of Scope | h3/WebTransport removed from alkhttp scope entirely (an alknet concern); supersedes the deferral framing of ADR-044 |
| [070](decisions/070-from-wss-consumer-adapter.md) | `from_wss` Consumer Adapter | Import a remote node's operations over WSS — same-protocol importer, channels-over-WS as transport; `wss` feature gate |
| [071](decisions/071-dispatch-schema-guard.md) | Dispatch-Spine `services/schema` Op-Path Guard | `GatewayDispatch` applies the GET `/schema` visibility+ACL checks to the meta-op's inner `name` (review-002 PRJ-16); alkcall CF-004 is the complete fix, this stays as defense-in-depth (now via the promoted `alkcall::gateway` shared check — spine + guard themselves live in alkcall per ADR-048) |
| [072](decisions/072-http-surface-ownership-wiring.md) | HTTP-Surface Ownership Wiring (Gateway + MCP) | alkcall 0.10's loud ownership default passes through: `HttpAdapter::with_ownership_provider` / `with_static_ownership_fallback` thread the provider into the gateway and MCP spines (single wiring in `GatewayState::dispatch`) |
| [073](decisions/073-door-surface-basic-auth-tolerance.md) | Door-Surface Basic Auth Tolerance | `door_auth_middleware` on extras doors resolves `Basic base64(user:pass)` (password field as the token) through the same `resolve_from_token` backend; gateway/MCP/WS stay Bearer-only (review 008 AH-1) |
## Relevant Open Questions
@@ -36,7 +36,7 @@ Credential presentation per handler:
|---------|------------------------|-------------|
| SshAdapter | SSH public key handshake | `resolve_from_fingerprint()` |
| CallAdapter | AuthToken in first frame | `resolve_from_token()` |
| HttpAdapter | `Authorization: Bearer` header | `resolve_from_token()` |
| HttpAdapter | `Authorization` header — Bearer (all HTTP surfaces); Basic additionally on the extras-door surface (ADR-073, the password field as the token) | `resolve_from_token()` |
| DnsAdapter | AuthToken in query labels | `resolve_from_token()` |
| WebTransportAdapter | AuthToken in CONNECT headers | `resolve_from_token()` |
| GitAdapter | Signed push certificate | `resolve_from_fingerprint()` |
@@ -4,21 +4,25 @@
## Status
Accepted (amended by alknet ADR-049 — protocol-level code list extended to six; alkcall ADR-021)
Accepted (amended by alknet ADR-049 — protocol-level code list extended to six; alkcall ADR-021; alkcall 0.7.0 CF-007 — the list is eight: `ALREADY_EXISTS` (alkcall ADR-022, mapped 409 Conflict by review 006 UP-01) and `CONNECTION_CLOSED` (alkcall ADR-016 CF-001, mapped 503 Service Unavailable in this crate's gateway error table))
## Context
The `OperationSpec` in the call crate (now alkcall) has `input_schema` and
`output_schema` but no `error_schemas`. The `call.error` payload (the alkcall
crate's `call-protocol.md`) carries a `code` and `message`, where `code` is
one of six infrastructure codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`,
`INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`.
one of eight infrastructure codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`,
`INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`, `ALREADY_EXISTS`,
`CONNECTION_CLOSED`.
These six codes cover **protocol-level failures** — the call protocol
These eight codes cover **protocol-level failures** — the call protocol
itself can always fail to find an operation, deny access, reject bad input,
reject the wrong dispatch method for the operation type, time out, or hit
an internal error. They are emitted by the dispatch machinery (the registry,
the adapter), not by operation handlers. `INVALID_OPERATION_TYPE` was added
reject the wrong dispatch method for the operation type, time out, hit an
internal error, reject an `op/register` collision (`ALREADY_EXISTS`, alkcall
ADR-022), or observe a provably-undelivered call (`CONNECTION_CLOSED`,
alkcall ADR-016 CF-001 — the only retryable protocol code). They are emitted
by the dispatch machinery (the registry, the adapter), not by operation
handlers. `INVALID_OPERATION_TYPE` was added
by alknet ADR-049 (streaming handler for subscriptions — now alkcall ADR-021;
`invoke()` called on a `Sub`, or `invoke_streaming()` on a `Query`/`Mutation`).
@@ -173,8 +177,8 @@ optional-array convention.
- `code` — the error code. Either a protocol-level code (`NOT_FOUND`,
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
`TIMEOUT`) or an operation-level domain code from `error_schemas` (e.g.,
`FILE_NOT_FOUND`, `RATE_LIMITED`).
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`) or an operation-level
domain code from `error_schemas` (e.g., `FILE_NOT_FOUND`, `RATE_LIMITED`).
- `message` — human-readable error message. Unstructured — for logging and
debugging, not for programmatic handling. Consumers should switch on
`code`, not parse `message`.
@@ -190,8 +194,8 @@ optional-array convention.
### 3. Protocol-level vs operation-level error codes
The six existing codes are **protocol-level** — emitted by the dispatch
machinery, not by handlers:
The eight protocol-level codes are **protocol-level** — emitted by the
dispatch machinery, not by handlers:
| Code | Emitted by | Meaning |
|------|-----------|---------|
@@ -201,6 +205,8 @@ machinery, not by handlers:
| `INVALID_OPERATION_TYPE` | Registry / `OperationEnv` | Wrong dispatch path for the operation's type (`invoke()` on a `Sub`, `invoke_streaming()` on a `Query`/`Mutation`, `invoke_sink()` on a `Query`/`Mutation`/`Sub`, or `OperationEnv::invoke()` on a `Sub` during composition — alkcall ADR-021; `Pub` ops dispatch via `invoke_sink()`, alkcall ADR-046) |
| `INTERNAL` | Registry / Adapter | Handler panic, unhandled error, connection failure |
| `TIMEOUT` | Adapter | Request timed out |
| `ALREADY_EXISTS` | Registry / collision gate | `op/register` collision — a peer-announced op may not replace the serving side's own registrations (alkcall ADR-022 G-03) |
| `CONNECTION_CLOSED` | Client write path | The `call.requested` frame could not be written — the call is provably undelivered (alkcall ADR-016 CF-001). `retryable: true` — the only protocol code a caller may auto-retry. |
Operation-level domain codes are emitted by **handlers** — the operation's
own logic determines what went wrong. They are declared in `error_schemas`
@@ -251,9 +257,9 @@ accordingly.
```
**Normative rule (review #002 W20)**: `from_openapi` must not produce error
codes that collide with the six protocol-level codes (`NOT_FOUND`,
codes that collide with the protocol-level codes (`NOT_FOUND`,
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
`TIMEOUT`). The adapter prefixes
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`). The adapter prefixes
imported error codes with `HTTP_` and the status number (e.g., `HTTP_404`,
`HTTP_429`) to avoid collision. This is a requirement for the adapter, not
a naming convention — the `from_openapi` example above was previously shown
@@ -95,6 +95,16 @@ the one-way constraint is that the assembly layer can inject routes
and they coexist with the default surface. axum's `Router::merge` /
`Router::nest` are the natural primitives.
**Route tables are static (review 008 AH-2).** The merged router
already owns the fallback route (the decoy) — axum refuses a second
fallback, and a door attempting `fallback_layer`/`fallback` on the
merged router panics at construction, not at request time. Door route
tables must therefore be statically enumerated: multi-segment
repository namespaces (`/{user}/{repo}/objects/batch` shape) are
pattern routes with wildcard segments, never a dynamic fallback match.
This is a recorded constraint of the router assembly, not a mechanism
ask.
### 2. Custom routes are raw HTTP, not call-protocol operations
A custom route is a raw axum handler — it receives an HTTP request and
@@ -134,16 +144,21 @@ is natural (`/v1/...` doesn't collide).
### 4. Custom routes carry the same auth middleware by default; per-route opt-out is the deployment's choice
Custom routes run under the same Bearer-auth resolution as the default
surface (the `Authorization: Bearer` → `resolve_from_token` path). A
deployment that wants a custom route to be unauthenticated (a public
webhook receiver, a health endpoint with a different shape than
`/healthz`) applies axum middleware to opt that route out of auth —
the deployment owns its custom routes' middleware stack. The
`HttpAdapter` provides the identity provider and the default auth
middleware; the custom `Router` the assembly layer passes in can
layer its own middleware on top. This is standard axum composition; no
alknet-specific mechanism.
Custom routes run under the door-surface auth middleware: Bearer
resolution identically to the default surface
(`Authorization: Bearer` → `resolve_from_token`), plus — ADR-073 —
Basic tolerance (`Basic base64(user:pass)`; the password field rides
the same `resolve_from_token` backend). The default token-auth surface
(gateway endpoints, `/openapi.json`, `/healthz`, MCP) stays
Bearer-only: Basic resolution is scoped to the door surface
specifically constructed for legacy clients. A deployment that wants a
custom route to be unauthenticated (a public webhook receiver, a
health endpoint with a different shape than `/healthz`) applies axum
middleware to opt that route out of auth — the deployment owns its
custom routes' middleware stack. The `HttpAdapter` provides the
identity provider and the default middlewares; the custom `Router` the
assembly layer passes in can layer its own middleware on top. This is
standard axum composition; no alknet-specific mechanism.
### 5. Custom routes are not part of the published `to_openapi` doc
@@ -0,0 +1,89 @@
# ADR-072: HTTP-Surface Ownership Wiring (Gateway + MCP)
## Status
Accepted
## Context
alkcall 0.10.0 landed the gateway ownership seam (alkcall review 012,
downstream evidence from alkgit's LFS POC): the dispatch spine's
previously-silent static `Identity.resources` fallback becomes loud — a
`resource_type`-gated op served through an unwired spine is refused
with a `FORBIDDEN` error envelope unless the deployment wires a
runtime ownership provider
(`GatewayDispatch::with_ownership_provider`) or declares the static
fallback explicitly
(`GatewayDispatch::with_static_ownership_fallback`). The change closes
the two-authorization-paths drift: a namespace-ACL-gated op served
through an HTTP door previously ran against the token-carried static
claim, while the same op on the native wire consulted the revocable
grant table.
alkhttp is the only live spine consumer. It constructs the spine
internally in two places — the gateway endpoints' per-request spine
(`gateway::routes::GatewayState`) and the MCP nest's spine
(`server::build_router`) — so an assembly layer wiring a provider for
the native wire cannot reach those spines through alkcall's API. Once
alkhttp rode 0.10, any `resource_type`-gated op in the registry would
break on the HTTP surface even for deployments that wired providers
correctly.
## Decision
**alkhttp mirrors alkcall's builder shape on `HttpAdapter` and threads
the wiring into every spine it constructs.**
- `HttpAdapter::with_ownership_provider(Arc<dyn OwnershipProvider>)`
and `HttpAdapter::with_static_ownership_fallback()` — the two
alkcall-named opt-ins, one method each, no alkcall-specific
vocabulary changed. `OwnershipProvider` is re-exported from
`alkhttp::gateway` next to `GatewayDispatch`.
- The wiring lives on `RouterState` and is threaded by
`GatewayState::dispatch()` (routes) and `RouterState::dispatch()`
(the MCP nest — the gateway routes extract `GatewayState` via
`FromRef`, so `GatewayState` is the single wiring point).
- Both spines share one wiring: the gateway endpoints and the MCP
surface are the same door contract; no separate MCP wiring exists.
The door contract (alkcall review 012, unchanged here): any
deployment serving `resource_type`-gated ops through the HTTP surface
must wire the same provider the native wire consults —
visible-surface = authorized-surface. The loud default passes through
from alkcall unchanged: an unwired, undeclared spine refuses a
`resource_type`-gated op with the spine's `FORBIDDEN` envelope, which
the gateway error mapping serves as 403 (with identity) / 401
(without).
The WS native-session path needs no wiring — it speaks the native
call protocol, which consults ownership natively in alkcall's serving
layer (alkcall ADR-048).
## Consequences
**Positive:**
- The HTTP surface cannot silently diverge from the native wire's
ownership truth — alkcall's loud default is inheritable at the door
layer, and `with_static_ownership_fallback` keeps the static-claims
posture a code-review-visible declaration here too.
- alkcall's builder shape is mirrored on alkhttp's builder, so an
assembly layer configures both spines the same way.
**Negative:**
- Deployments upgrading alkhttp to the 0.10-riding release must wire
the provider (or declare the fallback) before serving
`resource_type`-gated ops over HTTP — the 403 refusal is the
reminder, not a compile error.
## References
- alkcall review 012
(`/workspace/@alkdev/alkcall/docs/reviews/012-gateway-ownership-seam-consumer-input.md`)
— the consumer evidence and the two recorded decisions
- alkcall 0.10.0 CHANGELOG (the loud-default behavior break)
- [ADR-048](048-websocket-native-session-not-gateway.md) — the WS path
this does not touch
- [Review 008](../../reviews/008-http-door-consumer-review.md)
— the same consumption round's alkhttp-filed findings (AH-1, AH-2)
@@ -0,0 +1,87 @@
# ADR-073: Door-Surface Basic Auth Tolerance (git / git-lfs Clients)
## Status
Accepted
## Context
Review 008 AH-1, from alkgit's LFS POC (POC-L): a real `git-lfs`
3.7.0 client authenticates the batch endpoint with **Basic**
credentials (URL userinfo — the spec-shape behavior of the official
client; config `http://user:pass@host/repo`). The shared identity
middleware was Bearer-only — the module doc said so explicitly
("Bearer-only is the auth mechanism") — so a door mounted under the
shared auth layer received the batch request with the middleware
already having folded in `None`, and the door had to re-parse the
`Authorization` header itself (Basic *or* Bearer) to get any identity.
The middleware is shared by the gateway endpoints and `to_mcp`. Every
HTTP-door surface whose real client authenticates with Basic (LFS
today; git's smart-HTTP transport is tomorrow's `git` door) would
silently re-implement the fallback locally — the quiet-bypass shape
alkcall review 012 records for the ownership seam, one layer up in the
same stack.
## Decision
**Split the auth surface into token surfaces and the door surface,
and teach the door surface Basic — through the same resolution
backend, with no new `IdentityProvider` method.**
- `bearer_auth_middleware` stays Bearer-only and continues to cover
the token-auth surfaces: the gateway endpoints, `/openapi.json`,
`/healthz`, and the `to_mcp` nest. A Basic header contributes no
identity there. `/call`, `/batch`, `/subscribe`, `/publish`, `/mcp`,
and the WS upgrade path are token surfaces — this is the "no blanket
method" constraint: a Basic credential never gains identity anywhere
the deployment has not explicitly set up for it.
- `door_auth_middleware` (new) is applied by `build_router` to the
extras-door router (`HttpAdapter::with_extra_routes`) — the surface
legacy clients authenticate on.
- Both middlewares resolve through `IdentityProvider::resolve_from_token()`.
Basic parsing: `Basic base64(user:pass)` — the password field (the
GitHub-PAT shape; the user segment is ignored) is decoded and fed as
the `AuthToken` raw bytes. A colon-less value is treated as a bare
password. No new trait method: the provider's grant table governs a
Basic credential exactly as it governs the same credential presented
as Bearer — and the same backend mechanism that grants privileges to
tokens grants them to these credentials (SOCKS5
userinfo-style integrations can follow the same pattern later).
- Fallback semantics unchanged: no header → `None`; unparseable or
unresolvable credential → `None` (unauthenticated, not an error).
A parse-time failure never becomes a 4xx of its own.
- Doors may opt out with their own middleware stack as before
([ADR-046](046-assembly-layer-custom-http-routes.md) §4).
## Consequences
**Positive:**
- `git-lfs` / `git` doors work against a deployment whose provider
maps password-tokens through `resolve_from_token`, with zero
door-side header re-parsing.
- Scoping is structural (per-middleware), not
per-path-inspection: Basic tolerance exists only where the
assembly layer mounted the door surface.
**Negative:**
- A door surface deployment that does *not* want its door credentials
resolvable as Basic must now ensure the provider does not resolve
those values as tokens — or opt the door out of the shared layer.
- Scheme prefixes remain case-sensitive ("Bearer "/"Basic "), matching
the pre-existing Bearer parsing; strictly RFC-noncompliant
lowercase-scheme clients parse to `None`.
## References
- [Review 008](../../reviews/008-http-door-consumer-review.md) — AH-1
evidence and disposition space
- [ADR-004](004-auth-as-shared-core.md) — the shared-core resolution
backend this rides
- [ADR-046](046-assembly-layer-custom-http-routes.md) — the extras-door
surface the door middleware covers (§4 amended; AH-2 static-route
constraint recorded there)
- alkcall review 012 — the same-round quiet-bypass analysis this
avoids repeating at the door layer
+4 -3
View File
@@ -553,8 +553,9 @@ for the factoring recommendation (thin shared struct, not a trait).
`from_openapi` maps OpenAPI non-2xx response status codes to
`ErrorDefinition`s (alkcall ADR-016 §5). The normative rule (review
#002 W20): `from_openapi` must not produce error codes that collide
with the six protocol-level codes (`NOT_FOUND`, `FORBIDDEN`,
`INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`). The
with the protocol-level codes (`NOT_FOUND`, `FORBIDDEN`,
`INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`, `TIMEOUT`,
`ALREADY_EXISTS`, `CONNECTION_CLOSED`). The
adapter prefixes imported error codes with `HTTP_` and the status
number:
@@ -601,7 +602,7 @@ request-level cap failure (422; GW-16 unified it with the
`INVALID_INPUT → 422` mapping — it was hand-rolled as 400 before
review-002). `BatchResultEntry.error` references
the `BatchError` component (review-002 PRJ-16b): the serialized
`CallError` as a oneOf over the six protocol-code envelopes plus a
`CallError` as a oneOf over the protocol-code envelopes plus a
generic arm carrying the operation-declared codes. See alkcall ADR-016.
## Why
+9 -1
View File
@@ -285,7 +285,15 @@ semantically-validating hook dials/prepares the backend before the
reply, and a bounded failure resolves `channel:open_failed` with
`details.reason` (the channel never exists consumer-side). The
per-registration timeout override bounds the establisher when the
dispatch carries no deadline. The `OpenableAlpns` request-extension
dispatch carries no deadline. The establisher's `Establishment.plan`
payload (alkcall 0.6.0 / ADR-049 amendment 2 — a typed-opaque
`ChannelPlan`) is threaded process-locally to the pump handler's
second parameter: the establisher and the handler agree on the
concrete type, so ALPN crates dial/allocate in the establisher and
deliver the live handle to their handler without a side-channel
handoff. Data-plane handlers that pump a channel stream against a
peer's split halves can use alkcall's `channels::pump_bidi` helper
(alkcall 0.6.0 / ADR-050). The `OpenableAlpns` request-extension
fallback is available for bare-registry/custom
upgrade routes. Cap policy is the `ChannelsPolicy` extension (one
instance consulted by both the open wrappers and the demux teardown).
+511
View File
@@ -0,0 +1,511 @@
# Plan: alkhttp fuzzing
Adopted from alkcall's `docs/research/fuzzing.md` (the pattern is
operational in four sibling crates: alkcall, alktty, alktunnels,
alksocks — each with the same `fuzz/` layout, the detached runner, and
the corpus-replay-as-plain-test gate). This document is the working
plan for alkhttp; the sibling docs are the reference for rationale
(alkcall's doc is the deep version: tool landscape, comparable-crate
survey, CI tiers) — this plan stays focused on what alkhttp fuzzes and
in what order.
**Status:** targets 1–3 implemented and verified (1–2 on 2026-09-29,
wave 1; 3 on 2026-09-29, wave 2); target 4 implemented (2026-09-29,
wave 3: `ws_session_opseq` + `retry_throttle`; campaign results in
§5); the only crash found so far (FWD-21, the `WriteChunkWalk`
failure-carry drop) was a crate contract fix, not a harness
misassert.
Progress log:
- **2026-09-29 — targets 1–2 + infra landed (commit 5971c0a).** The
`fuzz/` workspace, 80 committed seeds (52 + 28), the detached
runner, the corpus-replay gate (AGENTS.md verification checklist
gains the line), nightly pinned subtree, explicit root `[workspace]`
exclusion. One crate finding on the way (below). `cargo fuzz build`
clean; corpus replay 80/80 green; campaigns pending at commit time.
- **2026-09-29 — FWD-20, the SSE dribble-drain quadratic (fixed in
the same commit).** Found by driving the new `sse_parser` harness's
split-feed invariants: `SseParser::feed` re-walked the whole
reassembly buffer per feed — a subscription dribble-fed one giant
unterminated line (the attacker can hold `data: <n MiB>` open
forever without sending `\n`) paid O(n²) in the dribbled bytes:
64 KiB dribble ≈ 12.5 s CPU, 1 MiB would run to minutes per
connection, with no per-stream total-CPU bound (the alkcall
research §6.2-3 shape, in this crate). The fix: line assembly walks
only the arriving chunk — the reassembly buffer holds just the
current line, so the scan never revisits drained bytes; measured
1 MiB dribble now 21 ms (linear, 4× data = ~4× time). Regression
test `sse_parser_dribble_feeds_do_not_rescan_the_buffer`. Also
fixed by corpus replay pre-campaign: the harness's own re-frame
round-trip was wrong for data ending in `\r` (un-re-encodable under
WHATWG CR-stripping — the serde_json key-order class; the harness
skips that documented-lossy shape).
- **2026-09-29 — target 3 (`spec_import`) implemented (wave 2).**
Harness + 42 seeds + the three `fuzzing` hubs; no crate findings in
the initial corpus replay (11 shared tests green, main crate 457
default / 590 all-features). Smoke campaign clean: 2.34M execs /
10 min, zero crash/OOM/timeout, coverage still growing at budget
end (see §5). One harness-assert fix on the way (the quoted-`""`
YAML key noted above).
- **2026-09-29 — target 4 (`ws_session_opseq` + `retry_throttle`)
implemented (wave 3).** Two stateful harnesses + 16 seeds + the
`byte_adapter::fuzzing` drive hub (generic pump bodies over a mock
socket flavor) + the `client::fuzzing` throttle hub. One crate
finding on the way: FWD-21 (above) — fixed same commit.
- **2026-09-29 — FWD-21, the `WriteChunkWalk` failure-carry drop.**
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 **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, not the harness. 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 silently skipped on exactly this path. Fix (user-approved
semver-relevant API change): `Walk::Oversize(Vec<Vec<u8>>)` /
`Walk::PendingOverCap(Vec<Vec<u8>>)` carry the pre-failure pieces;
the write pump emits them before `fail_write_pump`, so a 1011
close never swallows a legal chunk the mux already counted as
written. Regression seeds cover the drain-then-fail shape in both
chunk harnesses.
- **2026-09-29 — FWD-22, the round-trip guard-CR (harness-assert
fix; wave 4).** Found ~32 min into the first long `sse_parser`
campaign: the re-frame round-trip probe's `frame_event` emitted
`data: <line>` unguarded, so a JSON payload carrying an *interior*
CR (multi-line data `"\r\n…"` — the payload line is `"\r"`) had
its payload CR consumed by `strip_cr` at re-parse time and the
round-trip lost it (wave-1 had skipped only the *terminal*-CR
case). The parser was never wrong — the shape is representable on
input (`data: x\r\r\n` → value `x\r`) and both drives agreed — the
re-encode was weak: one CR in the wire form is ambiguous between
"payload CR" and "line-terminator CR". Fix: `frame_event` emits
`data: <line>\r` for every data line (the guard CR is the
terminator, `strip_cr` removes exactly it, the payload CR
survives), and the terminal-CR skip is dropped — the round trip is
now total over anything JSON-encodable (JSON never encodes a raw
CR, so the residual asymmetry stays the serde_json non-preserved
key-order class). No crate API change. Regression seeds + unit
cases cover the interior-CR JSON shape (`{"k": "a\\rb"}`, two-line
`{…}\r\r\n{…}`).
---
## 1. Why alkhttp fuzzes (the if)
Same three converging arguments as every sibling crate, with one
stronger premise:
1. **This is the outermost shell.** alkcall's peers are authenticated
by construction; alkhttp's HTTP surface faces anonymous callers —
gateway routes accept bytes on the wire before any identity is
resolved, `/healthz` and the stealth decoy are fully public, and the
`from_*` adapters parse responses from remote HTTP servers nobody
in this repo controls. The quinn-CVE class (RUSTSEC-2026-0037: a
panic on attacker wire bytes is a remote DoS) applies here at its
most direct. A remote crash in this crate does not need a peer
trust step to reach.
2. **Real hand-rolled byte parsing on untrusted streams.** The HTTP
framing itself is hyper/axum/reqwest (upstream-fuzzed, not our job),
but the crate owns an incremental SSE frame parser, the
WS↔byte-stream chunk parser on both directions, a YAML→JSON
conversion pass with its own number/key/pointer semantics, and
bounded-schema compilation over arbitrary input values. These are
the exact shapes (incremental, splitting-sensitive,
cap-check-ordered) where example tests miss off-by-one bugs. No
`unsafe` exists (grep-verified), so the expected yield is the
alkcall profile: panics, arithmetic, allocation, and state-growth
bugs — not memory corruption.
3. **The integration seam is untested from this side.** alkhttp sits
downstream of the alkcall 0.8.1 fix (duplicate adopt/open
destroying the live channel — found by alkcall's `manager_routing`
campaign minutes into its first run). alkhttp's WS path hands raw
bytes to alkcall's demux; `fuzz/shared` depending on alkcall means
corpus replay exercises the alkcall surface from the integration
crate, a direction alkcall's own harness does not cover.
Also the standing empirical argument: the 0.8.1 bug was a stateful
interleaving invisible to example-based tests, and it cost a patch
release across every sibling. The infrastructure is already proven and
cheap to replicate; the marginal cost here is only the target logic.
## 2. Infrastructure (identical to the siblings)
Layout (copy of alksocks/alktty/alktunnels/alkcall):
```
fuzz/
├── Cargo.toml alkhttp-fuzz (nightly-only bins; own [workspace])
├── rust-toolchain.toml pins nightly + llvm-tools for this subtree only
├── fuzz_targets/ thin fuzz_target! wrappers (3 lines each)
├── shared/ alkhttp-fuzz-shared — STABLE-toolchain library:
│ invariant logic + corpus-replay tests
├── corpus/<target>/ committed seeds (generated by gen_fuzz_seeds.py)
├── artifacts/ gitignored crash/oom/timeout artifacts + logs
├── gen_fuzz_seeds.py deterministic seed generator (quiche pattern)
├── json.dict JSON token dictionary (spec_import)
├── run-detached.sh detached campaign runner (copied from the siblings)
└── README.md operational cheat-sheet
```
Load-bearing details (all four siblings hit these):
- **Invariant logic lives in `fuzz/shared/`**, not the target binaries.
The stable-toolchain shared crate replays every committed seed
through the identical invariant functions as plain `cargo test` —
the standing fuzz gate (alkcall research §7.9 tier-3 deliverable,
this repo has no hosted CI by policy) — and the `fuzz_target!`
binaries are thin wrappers.
- **Root `Cargo.toml` needs an explicit `[workspace]` table**
(`members = ["."]`, `exclude = ["fuzz"]`); without it auto-discovery
pulls `fuzz/shared/` into the main workspace and the stable
toolchain builds nightly-consumed dev-deps. alkcall hit this trap.
- **`fuzz/rust-toolchain.toml` pins nightly** so `cargo fuzz build`
works from any CWD; nightly stays confined to `fuzz/`, MSRV 1.88
untouched. `fuzz/` joins the publish `exclude` list.
- **`.gitignore` additions**: `fuzz/artifacts/`, grown-corpus dirs
(committed seeds stay).
- **Detached runner (non-negotiable operating rule).** Campaigns never
run as a foreground child of an agent session (an OOM in a target
can take down the session host's process tree). Runner pins
`-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048 -timeout=25` and
detaches via `setsid` + `nohup` + log redirect; the agent polls the
log and artifact directory, never waits. Copied from alksocks
(§`fuzz/run-detached.sh`).
**Verification-gate change:** `cargo test --manifest-path
fuzz/shared/Cargo.toml` (corpus replay) joins AGENTS.md's verification
checklist, as the siblings did.
## 3. Target inventory (all 4)
| # | Target | Drives | Input style | Status |
|---|---|---|---|---|
| 1 | `sse_parser` | `SseParser::feed` + `sse_event_envelope` mapping (`src/adapters/forward.rs`) | chunk-split sequence × eof flag | implemented 2026-09-29 |
| 2 | `ws_chunk_framing` | the WS↔byte-adapter chunk parsers (`WriteChunkWalk`, the write pump's extracted chunk walk) (`src/websocket/byte_adapter.rs`) | byte sequences + message-split boundaries | implemented 2026-09-29 |
| 3 | `spec_import` | `OpenAPISpec::from_str`/`from_yaml`/`from_json`, `CompiledInputSchema`/`harden_closed_by_default`/`validate`, `FromMCP::build_spec` (`src/adapters/openapi_spec.rs`, `from_jsonschema.rs`, `from_mcp/mod.rs`) | attacker-shaped OpenAPI/JSON/YAML docs + schemas | implemented 2026-09-29 |
| 4 | `ws_session_opseq` / `retry_throttle` | stateful op sequences over the WS byte-adapter pumps (write/read pump interleavings, close timing, oversize frames); `RetryAfterMiddleware` throttle-map growth | `#[derive(Arbitrary)]` op enums | implemented 2026-09-29 |
### Target 1 — `sse_parser` (the chunk_header-class target)
The crate's most direct untrusted-byte parser: any subscription over
`from_openapi`/`from_jsonschema` pipes remote HTTP bytes through
`SseParser::feed`.
- **Drives:** incremental `feed` with the fuzzer choosing the chunk
split boundaries; also the full-input single feed, and the
`sse_event_envelope` mapping (FWD-17: JSON payload surfaces as
itself, non-JSON as the `{"data","event"}` wrapper, empty →
`Null`).
- **Invariants:**
- no-panic on any byte sequence, any split, any eof timing;
- splitting transparency: whole-feed and every-slice-split feed
produce identical event streams (the alksocks `datagram_codec`
pattern);
- the buffer-cap check runs *before* the buffer grows
(FWD-14): the reassembly buffer can never exceed
`SSE_EVENT_BUFFER_CAP`; overflow surfaces as `BufferOverflow`,
never a panic or unbounded growth;
- dispatch discipline: an event is dispatched only on a blank line
(or EOF for a pending event with data); `data:` lines join with
`\n`; last `event:` before the blank line wins; `id:`/`retry:`/
comment lines (`:` prefix) never dispatch or alter state;
- shape partition: `BufferOverflow` only when a fed chunk would push
the reassembly buffer over the cap; a cap-sized-and-under input
can never trigger it — never allocate beyond the cap;
- UTF-8 discipline: a multi-byte character split at a chunk boundary
is not corrupted (decode happens once a complete line/EOF bounds
the window); invalid raw UTF-8 lines are skipped silently (WHATWG
tolerance), never panic;
- envelope mapping (FWD-17): every `feed`-produced event maps to a
success `ResponseEnvelope`; the JSON-ness branch agrees with
`serde_json::from_str` on the joined data; the JSON branch never
carries the `event` binding.
- round-trip-ish probe: re-serializing a parsed event as
`data:`/`event:` lines re-parses to an equal event for
JSON-encodable payloads (catches field-name and joining drift;
structural equality only).
- **Exposure:** `SseParser`/`SseEvent`/`sse_event_envelope` are
`pub(crate)`. Shared/invariant access via quinn/rustls-style
exposure if the pure functions can't be reached — alkcall's
campaign never needed `#[cfg(fuzzing)]` after making parse helpers
`pub(crate)` + re-export; the first choice here is the same: a
minimal `#[cfg(fuzzing)] pub mod fuzzing` re-export in
`src/adapters/forward.rs` (nightly-fuzz-only builds see it; stable
builds don't).
### Target 2 — `ws_chunk_framing`
The WS↔byte adapter parses 8-byte chunk headers on the write
(outbound byte→WS-message) side and validates against
`MAX_CHUNK_LEN`; the inbound side reassembles WS messages into the
byte stream that alkcall's demux parses. The hand-rolled pieces are
the chunk-walk loop and the pending accumulator.
- **Drives:** the write-pump parse loop driven directly with the
same logic `run_write_pump` executes on each `WriteMsg::Bytes`
(header scan → `MAX_CHUNK_LEN` gate → drain → 1 MiB message-piece
split → pending cap), and the read-pump inbound path's
message→header reassembly, driven as a byte-sequence target with
the fuzzer controlling both the byte stream and the message split
boundaries.
- **Invariants:**
- no-panic on any byte sequence at any message-split boundary;
- a header claiming `> MAX_CHUNK_LEN` fails the stream loudly
(WS-04/HY-09) at exactly the point the pump reaches the header —
never a silent wait and never an allocate-before-reject;
- the pending accumulator never exceeds `PENDING_BUFFER_CAP` =
`MAX_CHUNK_LEN + 8` (WS-14): bound asserted after every op; a
single write above the cap is rejected in `poll_write` before
queueing, so the cap is invariantly satisfied;
- message-splitting transparency: a chunk split across N
WS-message feeds parses exactly like the whole-buffer feed
(receiver's boundary is the header, not the message —
WS-06); pieces split at any offset ≤ `WS_MESSAGE_CAP`
reassemble losslessly;
- consumption accounting: every parsed byte is either emitted as a
chunk piece or retained in the pending buffer — nothing lost or
duplicated across the walk;
- the over-cap write rejection (`poll_write` > `PENDING_BUFFER_CAP`
→ `InvalidData` before queueing) is exact: no partial state, no
silent truncation.
- **Exposure:** the pure parse helpers are extracted/testable; the
pump functions are `pub(crate)` async — drives via a
`#[cfg(fuzzing)]` re-export and a current-thread runtime with a
mock sink (the alkcall `envelope_frame` Cursor pattern).
### Target 3 — `spec_import` (implemented 2026-09-29)
- **Drives:** raw bytes (and `json.dict`) → `serde_json`/`yaml_serde`
→ `OpenAPISpec::from_str` (JSON-first, YAML-fallback, ADR-051) and
the typed helpers: `yaml_to_json_value` boundary, number/key
conversion; plus `CompiledInputSchema::compile` +
`harden_closed_by_default` + `validate` over attacker-shaped
schemas and input values; plus `FromMCP::build_spec` over arbitrary
`Tool`-shaped JSON.
- **Invariants:** no-panic; parse failures are always clean
`AdapterError::SchemaParse` naming a pointer (never a panic/silent
null); the YAML normalization rejects loudly what
`serde_json::Number` cannot represent (non-finite floats,
non-string keys); `$ref` resolution errors on cycles instead of
unbounded recursion; the compiled validator rejects/accepts
consistently w.r.t. the schema; closed-by-default hardening pins
`additionalProperties: false`; registry-compile shape parity with
alkcall's `spec_parse` invariants (bounded compile, always
`Result`).
- **Implemented shape:** the harness drives one attacker string
through all four lanes — the three spec constructors (str/json,
yaml under `openapi`), the direct YAML→JSON normalization boundary
(merge keys applied; a value the boundary accepted can't contain
non-finite numbers — serde_json-invariant, asserted recursively),
the compile/harden/validate lane against the whole doc (hardening
partition: injected iff absent and the input is an object;
validate determinism probe with an unknown-key input; deterministic
two-call agreement), the MCP tool-shape lane (the doc and a
`{name, inputSchema}` wrapper through `serde_json`→`Tool`→
`build_spec`; success pins the namespace prefix), and the `$ref`
lane (`resolve_refs_recursive` over the raw doc + declared
component schemas when the doc parses). Exposure via
`fuzzing` hubs alongside the wave-1 one:
`openapi_spec::fuzzing`, `input_validation::fuzzing`
(an opaque `FuzzCompiledValidator` newtype so the `pub(crate)`
validator never leaks), `from_mcp::fuzzing` (`mcp` feature).
The shared crate gates `openapi`/`mcp` (both on by default, riding
the main crate's default build); corpus replay passes both with and
without.
- **Note:** yaml_serde (ADR-051: YAML 1.2 + merge keys) is the
deepest third-party parse we own — the campaign should also encode
the already-documented boundaries (`apply_merge` error path,
duplicate-key rejection with line/column) as corpus expectations.
42 committed seeds cover the menu below.
### Target 4 — stateful wave (implemented 2026-09-29)
- **`ws_session_opseq`:** `#[derive(Arbitrary)]` op sequences over
the WS byte adapter driven end-to-end (like alktty's
`session_opseq` / alkcall's `manager_routing`): write-side ops
(well-formed chunks, oversize headers, the pending shape),
read-side ops (binary deliveries, dribble, text, inbound read
error, peer close), a lane-healthy requested close, shutdown vs
close ordering, backpressure scaled 1..=8 read slots. Asserts:
lossless inbound forwarding (forwarded == delivered exactly after
the teardown joins, prefix mid-sequence), the WS-01 walk's
completed-chunk count vs the declared frame count, the loud-gate
close mapping (1002/1011/verbatim-requested + a per-cause reason,
WS-15), lossless write framing (outbound concat == the declared
chunk stream incl. the FWD-21 failure-carry), lane-failure
isolation (the alkcall 0.8.1 class), and pump termination within
the harness bound. Exposure: `byte_adapter::fuzzing`
(`FuzzMockSocket` flavor, `FuzzChannels`/`FuzzWriteSide` channel
bundle, pump spawn funnels, `FuzzInboundChunkProgress`).
- **`retry_throttle`:** bounded-map growth of the inlined
`RetryAfterMiddleware`: capacity bound (exact, distinct-URL
observable partition), expired-first + Farthest-future eviction
policy, ceiling clamp (deadline never grows past the ceiling of
the record moment), refresh contraction (a higher `Retry-After`
never extends the first-seen deadline; the missing-header refresh
is a documented no-op), `Retry-After` parse partition
(non-negative seconds ∪ future HTTP dates, post-clamp), and the
jitter bounds (≤ 25% of a short wait, capped at 2 s). Op sequences
of records/refreshes/queries/rebuilds with seconds, huge-seconds,
HTTP-date, past-date, garbage, and no-header shapes. Exposure:
`client::fuzzing` (constructor, synthetic throttled response,
record/deadline/parse/jitter funnels — the middleware map never
leaks).
- Sequenced after 1–3 stabilize, per the sibling pattern.
## 4. Corpus policy
Committed hand-made seeds per target, generated by
`fuzz/gen_fuzz_seeds.py` (deterministic, in-tree, quiche pattern);
grown corpora and artifacts gitignored. Seed menus:
- `sse_parser`: a minimal valid event; every framing edge (`data:`
multi-line joins, `event:` override, comment, `id:`, `retry:`,
BOM, CRLF/LF, field with no colon, `data` with no space);
truncation at every structural prefix; split at a UTF-8 multibyte
boundary; cap-boundary event (`SSE_EVENT_BUFFER_CAP - 1`,
exactly at cap, one over); an over-cap trailing line; EOF with
pending data / pending no-data; empty stream; pure whitespace.
- `ws_chunk_framing`: a minimal valid chunk (`[id:u32 BE][len:u32
BE][payload]`); `len = 0`, `len = MAX_CHUNK_LEN`,
`len = MAX_CHUNK_LEN + 1`, `len = u32::MAX`; truncation at every
prefix length (1–8 bytes); chunk split across a message boundary
at every offset; channel-id 0 (the control channel); a header
claiming big length whose payload never arrives (the pump-wait
shape); the >cap single write (`poll_write` rejection).
- `spec_import` (when built): a minimal valid OpenAPI doc (JSON);
the same doc as YAML; missing `info`/`paths`; a deep-nesting
probe; duplicate-key YAML; `<<: *anchor` merge; `.inf`/`.nan`
scalars; non-string map keys; a `$ref` cycle; an unreachable
`$ref`; `additionalProperties` absent (hardening probe) and
present both ways.
- Stateful seeds (target 4): the semantic fixtures the campaign must
always replay — a write/echo flow, the oversize loud-gate after a
clean chunk, the text-rejection + surviving write lane, an inbound
read error, a peer close mid-flight + shutdown drain, a requested
close, the throttle-map fill/evict cycle at capacity, the
missing-header refresh no-op, hostile-seconds clamping, refresh
contraction, and a mid-sequence rebuild (16 total; the
`ws_session_opseq`/`retry_throttle` blocks in
`fuzz/gen_fuzz_seeds.py`, hand-encoded against the `arbitrary`
1.4.x derive layout with ceiling-division tags and per-element
keep-going bytes, pinned by seed-decode tests).
## 5. Campaign + gate policy
Identical to the siblings (alkcall §7.9 posture; no hosted CI in this
repo):
- **Corpus replay is the standing fuzz gate:** `cargo test
--manifest-path fuzz/shared/Cargo.toml` — in AGENTS.md's
verification checklist.
- **Campaigns run detached** via `fuzz/run-detached.sh` (never a
foreground child of an agent session); budget 10 min per target for
a smoke campaign, 30+ min before a release or after touching the
SSE parser, the WS adapter, or the import adapters.
- **Grown corpora stay gitignored**: the hash-named files a campaign
drops into `fuzz/corpus/<target>/` after run-detached runs are
ignored via a hash-pattern (the committed `seed-*` files stay);
merge worthy entries into seeds only deliberately.
Wave-1 smoke campaign results (2026-09-29, 10 min per target, both
exited 0, artifact dirs empty — no crash/hang/OOM/leak):
- `sse_parser` — 18.9k execs at ~30/s (each exec replays the
whole-feed + 3-byte-split + cap-probe + envelope-mapping +
round-trip chain, so per-exec work is heavy), coverage 1100 edges /
9510 features / 807 in-memory corpus entries, still discovering when
the budget ended — longer campaigns keep paying.
- `ws_chunk_framing` — 4.8M execs at ~9k/s, coverage saturated at 245
edges / 1071 features / 136 corpus entries.
- **Seeds regenerate deterministically:** `python3
fuzz/gen_fuzz_seeds.py`.
- Toolchain notes live in `fuzz/README.md`; nightly stays confined to
`fuzz/`.
Wave-2 (`spec_import`) smoke campaign (2026-09-29, 10 min, exited 0,
artifact dirs empty):
- `spec_import` — 2.34M execs at ~4k/s (each exec replays the three
spec constructors + the YAML boundary + compile/harden/validate +
the MCP tool lane + `$ref` resolution, so per-exec work is heavy),
coverage still growing at budget end: 14.1k edges / 36.9k features
/ 3046 in-memory corpus entries. One early abort in the *first*
launch, a harness assert too strong for the documented boundary
(an explicitly-quoted `""` YAML mapping key is legal — JSON
has the same key; the nullish-key rejection covers the implicit
null key, not a quoted empty string), fixed before the counted run;
no crate finding.
- **Seeds regenerate deterministically:** `python3
fuzz/gen_fuzz_seeds.py`.
- Toolchain notes live in `fuzz/README.md`; nightly stays confined to
`fuzz/`.
Wave-3 stateful smoke campaigns (2026-09-29, 10 min per target, both
exited 0, artifact dirs empty — no crash/hang/OOM/leak; the first
`ws_session_opseq` launch aborted at 17 s on FWD-21, fixed before
the counted run):
- `ws_session_opseq` — 206k execs at ~340/s (each exec interleaves up
to 64 two-lane ops with yield quiesce points per exec, so per-exec
work is heavy), coverage 2251 edges / 5831 features / 232 in-memory
corpus entries — coverage still growing at budget end.
- `retry_throttle` — 897k execs at ~1.4k/s, coverage saturated at
1154 edges / 6925 features / 324 corpus entries.
- **Seeds regenerate deterministically:** `python3
fuzz/gen_fuzz_seeds.py`.
- Toolchain notes live in `fuzz/README.md`; nightly stays confined to
`fuzz/`.
Wave-4 release-budget campaigns (2026-09-29, 45 min per target,
launched concurrently, five of five):
- `ws_chunk_framing` — exited 0: 13.7M execs at ~5k/s, coverage
saturated (297 edges / 1216 features / 139 corpus).
- `spec_import` — exited 0: 6.85M execs at ~3.2k/s, coverage growing
to budget end (14.9k edges / 43.4k features / 3946 corpus).
- `ws_session_opseq` — exited 0: 715k execs at ~253/s, coverage
growing to budget end (2251 edges / 6337 features / 260 corpus).
- `retry_throttle` — exited 0: 3.3M execs at ~1.3k/s, coverage
saturated (1154 edges / 7071 features / 352 corpus).
- `sse_parser` — aborted at 1913 s on FWD-22 (above; the
interior-CR round-trip loss, a harness fix); before the abort it
had 57k execs at ~35/s (1269 edges / 5584 features / 509 corpus),
still growing. 45-min rerun post-fix exited 0: 96k execs at ~33/s,
coverage still growing at budget end (1378 edges / 6655 features /
734 corpus) — zero findings.
- A stale crash artifact from the wave-2 morning session (quoted-`""`
YAML key, predating that fix) was still present in
`fuzz/artifacts/spec_import/` at campaign start — not produced by
wave 4 (the wave-4 `spec_import` log has zero findings).
- **Seeds regenerate deterministically:** `python3
fuzz/gen_fuzz_seeds.py`.
- Toolchain notes live in `fuzz/README.md`; nightly stays confined to
`fuzz/`.
1. ✅ **Done (2026-09-29, commit 5971c0a)** — infra (workspace
exclude, toolchain pin, runner, seed generator, README) + targets
1–2 + corpora + corpus replay + AGENTS.md gate + smoke campaigns
(clean, see §5). Includes the FWD-20 SSE dribble quadratic fix.
2. ✅ **Done (2026-09-29, commit e6491e3)** — target 3
(`spec_import`): harness + invariants
(`fuzz/shared/src/spec_import.rs`), 42 seeds, `json.dict` extended
with the OpenAPI-token set, fuzzing hubs in the three driven
modules. Smoke campaign clean (see §5).
3. ✅ **Done (2026-09-29, wave 4)** — long (45 min) release-budget
campaigns, all five targets, all exited 0 (see §5; the one
finding, FWD-22, was a harness-assert fix, fixed + re-run clean in
the same wave). Corpus merges remain deliberately deferred (grown
corpora stay gitignored; nothing campaign-dropped has been curated
into seeds).
4. ✅ **Done (2026-09-29, wave 3)** — target 4 stateful
(`ws_session_opseq` + `retry_throttle`): two harnesses + 16 seeds
+ the `byte_adapter::fuzzing` / `client::fuzzing` hubs + FWD-21
(the `WriteChunkWalk` failure-carry drop — a crate contract fix;
user-approved semver-relevant API change). Smoke campaigns clean
(see §5).
Status updates land in §3's table and §6's list as work completes.
@@ -0,0 +1,102 @@
# Review 008 — First HTTP-door consumer evidence: shared auth vs LFS Basic, and the extras route-fallback constraint
## Status
Resolved — 2026-10-17. AH-1: door-surface Basic tolerance landed as
[ADR-073](../architecture/decisions/073-door-surface-basic-auth-tolerance.md)
(`door_auth_middleware` on extras doors; password field through the
same `resolve_from_token` backend; token surfaces stay Bearer-only).
AH-2: recorded on the door-facing surface
([ADR-046](../architecture/decisions/046-assembly-layer-custom-http-routes.md)
§1). Both ride the alkhttp 0.8.0 release (with the alkcall 0.10
ownership wiring, ADR-072). Filed 2026-10-17 as consumer input from
alkgit's LFS
POC (POC-L, 2026-10-16; findings at
`/workspace/@alkdev/alkgit/docs/research/pocl-findings.md`; the POC
crate is `/workspace/lfs-poc`, which served a real `git-lfs` 3.7.0
client through `with_extra_routes` doors). Ask ledger source: alkgit's
`docs/research/substrate-recomposition.md` §Upstream-push
candidates #6/#9 (the alkhttp-named items; the gateway-dispatch half
of #6 is filed where it belongs — alkcall review 012).
## Scope
Two findings, both from the same consumption round (an LFS batch-API
door on the extras router). Neither was discovered by a source sweep;
both anchors were re-verified by targeted read this session. Severity
and shape are the consumer seat's; the disposition belongs to this
crate's own session.
## AH-1 [small, contract-visible] — The shared bearer middleware is Bearer-only; LFS clients authenticate with Basic on the batch
**Anchor.** `src/server/auth.rs` — the shared identity middleware
(`bearer_auth_middleware`); the module doc states the semantics
explicitly: *"Malformed `Authorization` header (not `Bearer <token>`)
→ `None` (treated as no-token, not an error — Bearer-only is the auth
mechanism)"* (lines 1–20, verified this session).
**Evidence.** Real `git-lfs` 3.7.0 authenticates the batch endpoint
with **Basic** credentials (URL userinfo — the spec-shape behavior of
the official client; its config is `http://user:pass@host/repo`). An
extras door mounted on the shared middleware therefore receives the
batch request with the middleware already having resolved `None`
(the Basic header is not `Bearer`) — the door had to parse the
Authorization header a second time (Basic *or* Bearer) to get any
identity at all. The POC's door resolved both schemes locally and
passed the real client; the shared middleware contributed nothing on
this request shape.
**Why it matters beyond LFS.** The middleware is shared by gateway
endpoints and `to_mcp`; any HTTP-door surface whose
real-world client authenticates with Basic (LFS today; git's
smart-HTTP transport's basic auth is tomorrow's `git` door) silently
loses identity resolution at the middleware and every door
re-implements the fallback — the exact quiet-bypass shape review 012
(alkcall) records for the ownership seam, one layer up in the same
stack.
**Recommended shape (own session, to be re-derived)**: tolerate Basic
userinfo in the shared middleware — parse `Basic base64(user:pass)`
into the identity provider's resolve path (or expose a documented
per-door hook that receives the raw header before the None fold-in),
keeping the current "resolve fails → None" fallback semantics. The
doc line "Bearer-only is the auth mechanism" should flip to match
whatever lands. Small surface; the decision is whether Basic
resolution belongs in the shared middleware (uniform) or stays a
door concern (explicit).
**Verification status**: middleware semantics re-read this session;
the client behavior is POC-L's recorded route-shape fact (real
client, live). Re-verify by pointing a stock `git-lfs` at a
shared-middleware door during resolution.
## AH-2 [recorded constraint, no ask yet] — Extras doors cannot carry a route fallback; door route tables must be static shapes
**Anchor/evidence.** POC-L, live: the merged extras router already
owns a fallback route (the decoy), and axum refuses a second merge —
a door attempting `fallback_layer`/merge fallback on the merged
router panics **at construction**. Consequence: door route tables
must be statically enumerated; multi-segment repo namespaces
(`/user/repo/objects/batch` style) need enumerated pattern routes
(`/{user}/{repo}/...` shape), not a dynamic-fallback match. The POC
door's static shapes worked for every e2e case.
**Disposition space.** This is a structural fact of the current
router assembly, currently undocumented on the door-facing surface —
worth one doc line in the extras/hooks documentation ("route tables
are static; no second fallback; enumerate multi-segment patterns
explicitly") regardless of whether any mechanism change is ever
wanted. Not a mechanism ask today; recorded so the next door author
does not rediscover the construction panic.
## What this review asks for
| # | Item | Class |
|---|---|---|
| AH-1 | Basic tolerance (or a documented pre-resolution hook) in the shared identity middleware | small change + doc flip, own session |
| AH-2 | doc line: extras route tables are static; no second fallback | docs-only, ride-along |
Nothing here blocks any door surface that resolves identity
door-side (the proven compensation), but the shared-middleware trap
(AH-1) compounds alkcall review 012's gateway trap for every future
External-facing door — the two are worth one resolution round each.
+2807
View File
File diff suppressed because it is too large. Load diff
+52
View File
@@ -0,0 +1,52 @@
[package]
name = "alkhttp-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
alkhttp-fuzz-shared = { path = "shared" }
[dependencies.alkhttp]
path = ".."
[[bin]]
name = "sse_parser"
path = "fuzz_targets/sse_parser.rs"
test = false
doc = false
bench = false
[[bin]]
name = "ws_chunk_framing"
path = "fuzz_targets/ws_chunk_framing.rs"
test = false
doc = false
bench = false
[[bin]]
name = "spec_import"
path = "fuzz_targets/spec_import.rs"
test = false
doc = false
bench = false
[[bin]]
name = "ws_session_opseq"
path = "fuzz_targets/ws_session_opseq.rs"
test = false
doc = false
bench = false
[[bin]]
name = "retry_throttle"
path = "fuzz_targets/retry_throttle.rs"
test = false
doc = false
bench = false
[workspace]
+65
View File
@@ -0,0 +1,65 @@
# alkhttp fuzzing
cargo-fuzz targets for the HTTP surface's hand-rolled parse paths. The
design and operating rules live in `docs/plans/fuzzing.md` (adopted
from alkcall's `docs/research/fuzzing.md`); this README is the
operational cheat-sheet.
## Layout
- `fuzz_targets/` — nightly-only `fuzz_target!` binaries (thin wrappers).
- `shared/` — stable-toolchain library holding the invariant logic; the
corpus replay tests run here on plain `cargo test`.
- `corpus/<target>/` — committed seeds (regenerate with
`python3 fuzz/gen_fuzz_seeds.py`).
- `artifacts/` — gitignored crash/oom/timeout artifacts + campaign logs.
## Targets
| Target | Drives |
|---|---|
| `sse_parser` | `SseParser::feed` + the FWD-17 envelope mapping (`src/adapters/forward.rs`) — the SSE frame parser every subscription forwards through |
| `ws_chunk_framing` | `WriteChunkWalk` (the write pump's chunk walk) (`src/websocket/byte_adapter.rs`) |
| `spec_import` | `OpenAPISpec::from_str`/`from_yaml`/`from_json`, the YAML normalization boundary, `CompiledInputSchema` compile/harden/validate, `FromMCP::build_spec` (`src/adapters/openapi_spec.rs`, `input_validation.rs`, `from_mcp/mod.rs`) |
| `ws_session_opseq` | stateful op sequences over the WS byte adapter's generic pumps (`run_read_pump`/`run_write_pump` over the mock socket flavor) — interleavings, close timing, loud-gate mapping, backpressure |
| `retry_throttle` | the inlined `RetryAfterMiddleware`'s throttle map: capacity bound, eviction policy, ceiling clamp, refresh contraction, `Retry-After` parse partition |
## Running a campaign — always detached
Agent sessions must never run fuzzing in the foreground (an OOM in a
target can take down the session host; see docs/plans/fuzzing.md §2).
Use the detached runner:
```bash
fuzz/run-detached.sh sse_parser
# poll:
tail -n 50 fuzz/artifacts/sse_parser-*.log
ls fuzz/artifacts/sse_parser/
pgrep -f "cargo fuzz run sse_parser"
```
`FUZZ_RUNTIME_SECS=1800 fuzz/run-detached.sh sse_parser` for a longer
campaign. The runner pins `-fork=1 -rss_limit_mb=2048
-malloc_limit_mb=2048 -timeout=25` and detaches via `setsid` + `nohup`.
## Corpus replay (the standing fuzz gate)
```bash
cargo test --manifest-path fuzz/shared/Cargo.toml
```
Replays every committed seed through the same invariant functions the
fuzzer runs, on stable, no nightly needed. Part of the release
verification checklist (AGENTS.md).
## Toolchain notes
`rust-toolchain.toml` pins nightly for this subtree only (llvm-tools
required by cargo-fuzz); the crate itself stays on stable at MSRV 1.88.
`cargo fuzz build` must be run with the fuzz dir as CWD or trust the
toolchain file (rustup resolves per directory).
Run campaigns before releases and after touching `src/adapters/forward.rs`
(the SSE parser), `src/websocket/byte_adapter.rs` (the WS chunk walk),
or the import adapters (`src/adapters/openapi_spec.rs`,
`input_validation.rs`, `from_mcp/`).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"openapi":"3.0.0","info":{"title":"t","version":"1.0.0"},"paths":{"/x":{"get":{"operationId":"x_get","responses":{}}}}}
+9
View File
@@ -0,0 +1,9 @@
openapi: 3.0.0
info:
title: t
version: 1.0.0
paths:
/x:
get:
operationId: x_get
responses: {}
+1
View File
@@ -0,0 +1 @@
{"openapi":"3.0.0","info":{"title":"t","version":"1"},"paths":{"/items/{id}":{"get":{"operationId":"get_item","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Widget"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Widget"}}}}}}}},"components":{"schemas":{"Widget":{"type":"object","properties":{"id":{"type":"string"},"n":{"minimum":1}}}}}}
+1
View File
@@ -0,0 +1 @@
{"paths":{}}
+1
View File
@@ -0,0 +1 @@
{"info":{"title":"t"}}
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
null
+1
View File
@@ -0,0 +1 @@
42
+1
View File
@@ -0,0 +1 @@
"a string"
+1
View File
@@ -0,0 +1 @@
{
+1
View File
@@ -0,0 +1 @@
}{
+1
View File
@@ -0,0 +1 @@
not a document
View File
Whitespace-only changes.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"openapi":3.0}
+2
View File
@@ -0,0 +1,2 @@
a: 1
a: 2
+5
View File
@@ -0,0 +1,5 @@
base: &b
x: 1
child:
<<: *b
y: 2
+1
View File
@@ -0,0 +1 @@
maximum: .inf
+1
View File
@@ -0,0 +1 @@
minimum: -.inf
+1
View File
@@ -0,0 +1 @@
value: .nan
+1
View File
@@ -0,0 +1 @@
~: v
+2
View File
@@ -0,0 +1,2 @@
? [1, 2]
: v
+2
View File
@@ -0,0 +1,2 @@
responses:
200: {}
+2
View File
@@ -0,0 +1,2 @@
a: yes
b: no
+1
View File
@@ -0,0 +1 @@
a: !!unknown b
+1
View File
@@ -0,0 +1 @@
a: !!str 200
+5
View File
@@ -0,0 +1,5 @@
paths:
/x:
get:
responses:
200: {}
+1
View File
@@ -0,0 +1 @@
{"components":{"schemas":{"A":{"$ref":"#/components/schemas/A"}}}}
+1
View File
@@ -0,0 +1 @@
{"components":{"schemas":{"A":{"type":"object","properties":{"n":{"$ref":"#/components/schemas/B"}}},"B":{"type":"object","properties":{"n":{"$ref":"#/components/schemas/A"}}}}}}
+1
View File
@@ -0,0 +1 @@
{"components":{"schemas":{"A":{"$ref":"#/components/schemas/Missing"}}}}
+1
View File
@@ -0,0 +1 @@
{"components":{"schemas":{"A":{"$ref":"https://other/file.json"}}}}
+1
View File
@@ -0,0 +1 @@
{"components":{"schemas":{"S0":{"type":"object"},"S1":{"type":"object","properties":{"a":{"$ref":"#/components/schemas/S0"},"b":{"$ref":"#/components/schemas/S0"}}},"S2":{"type":"object","properties":{"a":{"$ref":"#/components/schemas/S1"},"b":{"$ref":"#/components/schemas/S1"}}}}}}
+1
View File
@@ -0,0 +1 @@
{"required": "n"}
+1
View File
@@ -0,0 +1 @@
"not an object"
+1
View File
@@ -0,0 +1 @@
{"type":"object","properties":{"q":{"type":"string"}}}
+1
View File
@@ -0,0 +1 @@
{"type":"object","properties":{"q":{"type":"string"}},"additionalProperties":true}
+1
View File
@@ -0,0 +1 @@
{"type":"object","properties":{"level":{"enum":["low","high"]},"n":{"minimum":1},"s":{"maxLength":3}}}
+1
View File
@@ -0,0 +1 @@
{"name":"ok_tool","inputSchema":{"type":"object","properties":{"q":{"type":"string"}}}}
+1
View File
@@ -0,0 +1 @@
{"name":"ns/ok_tool","inputSchema":{"type":"object"}}
+1
View File
@@ -0,0 +1 @@
{"name":"has space","inputSchema":{"type":"object"}}
+1
View File
@@ -0,0 +1 @@
{"name":"","inputSchema":{"type":"object"}}
+1
View File
@@ -0,0 +1 @@
{"name":"ok_tool"}
+2
View File
@@ -0,0 +1,2 @@
data: hello
+2
View File
@@ -0,0 +1,2 @@
data: {"k": 1}
+3
View File
@@ -0,0 +1,3 @@
event: named
data: x
+2
View File
@@ -0,0 +1,2 @@
data: 42
+2
View File
@@ -0,0 +1,2 @@
data:
+3
View File
@@ -0,0 +1,3 @@
event: ping
data: heartbeat
+4
View File
@@ -0,0 +1,4 @@
data: a
data: b
data: c
+2
View File
@@ -0,0 +1,2 @@
data: x
+1
View File
@@ -0,0 +1 @@
data: x␍␍
+2
View File
@@ -0,0 +1,2 @@
data: bom-first
+2
View File
@@ -0,0 +1,2 @@
:comment only
+3
View File
@@ -0,0 +1,3 @@
:c
data: after-comment
+4
View File
@@ -0,0 +1,4 @@
event: e1
event: e2
data: last-wins
+4
View File
@@ -0,0 +1,4 @@
id: 7
retry: 100
data: ignored-fields
+2
View File
@@ -0,0 +1,2 @@
data
+2
View File
@@ -0,0 +1,2 @@
data:only
+4
View File
@@ -0,0 +1,4 @@
event
data: after-bare-event
+3
View File
@@ -0,0 +1,3 @@
+2
View File
@@ -0,0 +1,2 @@
+3
View File
@@ -0,0 +1,3 @@
random text line
another
+2
View File
@@ -0,0 +1,2 @@
event: e
data: v
+1
View File
@@ -0,0 +1 @@
data: no-eof-dispatch
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
data: héllo
+2
View File
@@ -0,0 +1,2 @@
data: ÿþý
+3
View File
@@ -0,0 +1,3 @@
event: é
data: utf8-name
+1
View File
@@ -0,0 +1 @@
e
+1
View File
@@ -0,0 +1 @@
ev
+1
View File
@@ -0,0 +1 @@
eve
+1
View File
@@ -0,0 +1 @@
even
+1
View File
@@ -0,0 +1 @@
event
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff. Show more