Commit Graph
130 Commits
Author SHA1 Message Date
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 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 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
glm-5.3-flash 80d6ba0e5a feat(websocket): ride alkcall 0.5.0 — OpenableAlpn establisher + timeout (ADR-049 Unit 3)
Mechanical pass for the alkcall 0.5.0 bump (review 006 sequencing:
ADR-049 → alkcall 0.5.0 → alkhttp mechanical pass):

- bump alkcall 0.4 → 0.5 (gateway feature unchanged)
- OpenableAlpn gains establisher: Option<OpenEstablisher> and
  establisher_timeout: Option<Duration> (both default None via
  OpenableAlpn::new — existing constructions compile unchanged) plus
  a with_establisher(establisher, timeout) builder
- the install_channel_zero ferry calls
  register_openable_with_establisher, threading both fields through
- docs updated (websocket.md, adapter.rs doc comments)

The alkcall 0.5.0 open_channel error-type change (ADR-049 §4) needs
no change here — this crate never calls open_channel (from_wss uses
ChannelClient::from_connection only).

Verification: cargo test (454 passed), cargo test --all-features
(587 passed), cargo clippy --all-targets/-D warnings (default +
all-features), cargo fmt --check
2026-09-06 20:04:25 +00:00
glm-5.3-flash 2e15204aca fix(docs): resolve the 5 pre-existing rustdoc warnings
cargo doc --no-deps was not actually clean before review 007's
remediation — it emitted 5 warnings that went uncounted (grep for
"^error|^warning" in the gate runs matched only on error/warning at
line start, and the review baselines recorded "clean" without a
counting check; the warnings were first noticed while re-verifying
the review-007 gate set). All pre-existing, none introduced by
review 007:

- src/adapters/mod.rs: the bare [`OpenAPISpec`] intra-doc link did
  not resolve (the re-export sits in the same module, but rustdoc
  resolves module-level links against the module's own scope) —
  spelled out as `openapi_spec::OpenAPISpec`
- src/websocket/mod.rs: [`upgrade`] did not resolve from the
  `websocket` module doc (the submodule is not in scope for the
  parent's doc links) — plain `upgrade`
- src/websocket/upgrade.rs:215: [`CallConnection`] had no import in
  scope — spelled out via the full alkcall path
- src/websocket/upgrade.rs:291: [`install_channel_zero`] linked a
  private item from public docs — plain `install_channel_zero` hook
- src/server/adapter.rs:274: [`crate::websocket::OpenHandler`] is
  not re-exported from `websocket` — spelled out via the alkcall
  path

Verification: cargo doc --no-deps 0 warnings (was 5); cargo test 454
passed / 0 failed; cargo test --all-features 587 passed / 0 failed;
clippy (both configs) clean; fmt clean.

Review: docs/reviews/007-ws-data-channel-surface-review.md (the
baseline's "cargo doc → clean" claim was wrong on this dimension)
2026-09-05 05:59:54 +00:00
glm-5.3-flash 24c2e9a224 feat(websocket): WS-29/30/32 — op/register ACL surface, SessionSlots, builder-path gate
Review 007 Unit 2 (the two "implement" decisions taken during
remediation, plus the coverage gap):

- WS-29: the `op/register` override surface review-006 UP-02 and
  ADR-048 recorded as landed is now implemented. The hook threads an
  `op_register_acl` `AccessControl` into `op_register_spec` (the
  permissive `AccessControl::default()` remains the default
  everywhere); the built-in surface sets it via
  `HttpAdapter::with_ws_op_register_acl`, bare-registry/custom routes
  via the `OpRegisterAcl` request extension (mirroring
  `ChannelsPolicy`/`WsTimeouts`/`OpenableAlpns`). A peer whose
  identity does not satisfy the ACL gets `FORBIDDEN` on the announce.
  Gates: builder path (`FORBIDDEN` scope-less / announce-ok scoped)
  + extension path.

- WS-30: the bare-registry `SessionState` is built by `FromRef` per
  request, so its default-cap semaphore bounds nothing across
  requests (corrects review-002 WS-17's "bounded at 64 sessions"
  claim). New `SessionSlots` request extension carries the shared
  semaphore for routes that need an effective cap; the upgrade
  handler prefers it over the state value. Doc comments corrected
  (`SessionState`, `WsTimeouts`, `ws_upgrade_handler`). Gate:
  cap-1 route → 503 over cap → slot freed on session end.

- WS-32: the built-in openables threading
  (`with_ws_openable_alpns` → `RouterState` → `SessionState` → hook)
  gets its first gate — every Unit-3 gate rode the `OpenableAlpns`
  extension fallback. `builder_path_openables_serve_the_data_channel_
  surface` discovers the openable via `services/list`, opens the
  channel, and round-trips bytes through the builder-built router.

Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 587 passed / 0 failed (+5 gates); clippy (both
configs) clean; fmt clean.

Review: docs/reviews/007-ws-data-channel-surface-review.md
2026-09-05 05:44:28 +00:00
glm-5.3-flash 38738943c7 fix(websocket): WS-28 — bind the channel-0 ConnectionGuard in the task's frame
The WS-26 `ConnectionGuard` was bound inside its `if let` block, so it
dropped microseconds after insertion instead of living for the
channel-0 dispatcher task — `live_connections()` /
`live_connection_count()` were permanently empty for every session
(review 007 WS-28 [major], reproduced empirically; the handle
retention ADR-048 + review-006 record was aspirational at that commit).

Remediation (review 007 Unit 1):

- bind the guard as an `Option<ConnectionGuard>` in the channel-0
  task's frame, mirroring `SessionGuard`'s shape in
  `run_channels_session`; the block comment now describes the real
  scope
- gate `live_connections_visible_mid_session_and_drain_after_teardown`
  — handle visible mid-session (after a completed call proves the
  dispatcher is up), drained after teardown; verified to fail against
  the pre-fix tree and pass with the fix

Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 582 passed / 0 failed; clippy (both configs) clean; fmt
clean.

Review: docs/reviews/007-ws-data-channel-surface-review.md (WS-28)
2026-09-05 05:41:53 +00:00
glm-5.3-flash 030c5efa51 feat(websocket): data-channel + op/register wiring (review 006 Unit 2+3)
The WS path wires the alkcall 0.3 per-session mechanisms — the OQ-05
deferred half (review-003 WS-20/21/22/25/26, the decisions WS-24/WS-25
resolved upstream):

- install_channel_zero reworked per the ADR-047 §4 amendment #2 shape:
  fork the deployment's base registry, register the generic channel
  ops (channel/close, channel/control, channel/resources/subscribe —
  WS-21), the deployment's openable ALPNs (ChannelCore::register_openable,
  WS-22), the bootstrap discovery set closed over the fork
  (install_bootstrap_discovery, F-06), and op/register (WS-25; the
  collision set is the fork per review-005 G-03), then dispatch over
  the fork. The session's ChannelsPolicy rides the hook (one policy
  instance across open wrappers and the demux teardown path).
- OpenableAlpn { spec, open_handler } + HttpAdapter::with_ws_openable_alpns,
  threaded RouterState -> SessionState -> hook, with the OpenableAlpns
  request-extension fallback (mirroring ChannelsPolicy/WsTimeouts).
- WsSessions retains the channel-0 Arc<CallConnection> (WS-26) with a
  self-removing guard (ConnectionGuard); live_connections() is the
  deployment-visible surface.
- UP-01: ALREADY_EXISTS maps to 409 Conflict in the gateway error map.
- from_wss import excludes the protocol-session ops (bootstrap set +
  channel lifecycle ops): the fork serves them per session, and proxying
  session-scoped machinery (e.g. channel/close across sessions) would be
  nonsense. Discovery runs first, the filter is the listing minus those
  names.
- adapter_install_channel_zero cfg matches its caller (WS-27); it
  inherits the reworked hook (session ops now served in the from_wss
  test-server producer too).

Gates (Unit 3, tests/ws_upgrade_session.rs; the WS-23 e2e set):
open -> channel_id -> discoverable in services/list -> chunks both
ways -> handler sees bytes; channel/close resolves + ledger decrement;
cap denial (channel:-prefixed); mid-open disconnect teardown; TooLarge
demux resync through the WS path (16 MiB + 1 skip consumed);
op/register announce + overlay-collision + serving-registry-collision
ALREADY_EXISTS through the WS path.

call_and_await now filters by request id and tolerates data-channel
chunks (a prior Sub's trailing call.completed may interleave).

Verification: cargo test 454 (default) / 582 (all-features), clippy
both sides -D warnings clean, fmt clean, doc clean.
2026-09-04 15:47:15 +00:00
glm-5.3-flash 3dee46aead chore(deps): consume alkcall 0.3 — per-session fork, serving loop, op/register
Bump alkcall 0.2 -> 0.3 (published 2026-09-04; the remediation of
alkcall reviews 004-005, the upstream resolution of alkhttp review 003
WS-24/WS-25). Two source-break classes, both mechanical:

- take_call_connection now returns Option<Arc<CallConnection>>
  (was bare value) — drop the double-wrap in WssSession::connect.
- register/register_on/register_openable take &OperationRegistry
  (was &mut) — drop 51 stale `mut` bindings (clippy --fix), which is
  the interior-mutability change that makes the per-session fork
  surface possible.

No behavior change in this crate yet; the 0.3 mechanisms (fork,
serve_single_stream, op/register, install_bootstrap_discovery,
collision policy) are the composition surface for alkhttp review 003
Unit 2 (WS-20..22, WS-26 wiring) — not wired here yet.

Verification: cargo test 453 passed (default) / 575 passed
(all-features), clippy both sides -D warnings clean, fmt clean,
cargo doc --no-deps clean.
2026-09-04 14:55:19 +00:00
glm-5.3-flash a80f9948b8 feat(build): feature-sided builds — server/client sides independently selectable (ADR-039 Amendment 1)
Split the feature graph so consumers pulling only the import adapters
(from_openapi / from_jsonschema / from_mcp) no longer compile the axum
/ hyper server stack, and server-only deployments no longer compile
reqwest. One crate, one import path — sides cut by features, not by a
crate split.

Feature graph:
- server (default): axum host, gateway, WS upgrade, to_openapi, to_mcp
- client (default): client host, forward, from_jsonschema, from_openapi
- openapi: shared OpenAPISpec model (implied by both sides)
- mcp: from_mcp needs client, to_mcp needs server
- wss: tungstenite transport (from_wss); tungstenite half of the
  shared WS↔byte-stream adapter
- h2/http1: hyper protocol features; imply server

Wire-contract neutral: gateway endpoints, ALPNs, and all public API
shapes unchanged; defaults keep both sides on.

Supporting changes:
- forward.rs drops its axum::body::Bytes type leak (bytes crate types)
- bounded_join + error-echo caps move to input_validation (usable by
  both sides; openapi_spec no longer imports from forward)
- byte_adapter: axum flavor compiles under server, tungstenite under
  wss; the generic pumps stay shared (WS-11)
- input_validation / openapi_spec import-only internals gated to the
  side that consumes them
- http-body-util moves to dev-dependencies (was test-only)
- integration-test required-features updated for the new sides
- from_wss unit tests (axum producer harness) gated to server

Verified: cargo test (defaults, 453) and --all-features (575) pass;
lean side builds (client / server / client,mcp / client,wss /
server,wss / openapi-only) build clean with zero warnings;
clippy -D warnings clean across all feature combinations; fmt clean.
2026-08-31 17:19:05 +00:00
glm-5.3-flash 8e8e1f2b14 refactor(gateway): migrate to alkcall 0.2 promoted gateway module
Bump the alkcall dependency to 0.2 (with the gateway feature) and
converge on the promoted shared pieces:

- The local dispatch spine (gateway/dispatch.rs, 721 lines) is deleted;
  GatewayDispatch, schema_disclosure_denial, and DEFAULT_DEADLINE are
  re-exported from alkcall::gateway (alkcall ADR-048). The 30 s default
  deadline preserves the previous behavior exactly.
- gateway/schema_cache.rs (PublishSchemaCache) is deleted: alkcall CF-003
  compiles publish_schema at registration time and exposes
  OperationRegistry::publish_validator; the /publish chunk stream
  resolves against it. Un-compilable schemas are now rejected at
  registration, so the two end-to-end fail-closed tests were reworked
  into a registration-rejection test (a stronger guarantee).
- schema_disclosure_denial consumers (to_mcp, routes) use alkcall's
  promoted implementation; the alkhttp-local copy is gone (ADR-071
  updated: the guard stays as defense-in-depth, the implementation no
  longer forks).
- CF-001: from_wss drop monitor and the WS overlay tests use
  CallError::connection_closed; the review-001-ws-eof-signal race tests
  now assert retryable CONNECTION_CLOSED on both resolution paths (the
  tolerated non-retryable INTERNAL write-failure outcome is gone).
- Added CHANGELOG.md (Keep a Changelog), Unreleased section records the
  bump and convergence.

Verification: cargo test default 453 ok, wss 470 ok, mcp 526 ok,
all-features 575 ok; clippy -D warnings clean (default + all-features,
all-targets); fmt clean; cargo doc warning-free.

Net: -1093 lines.
2026-08-31 10:36:32 +00:00
glm-5.3-flash 2ec02fd578 feat(adapters): enforce advertised input schemas at call time (OAI-18, option a)
Decision: advertise == enforce. The key allowlist (OAI-02) stays as the
first gate with its established unknown-key message; a compiled leaf
validator now runs second, so required/type/enum/pattern/bounds
violations surface as INVALID_INPUT 422 naming the keyword — not as
upstream round-trips.

- new src/adapters/input_validation.rs: CompiledInputSchema compiles an
  op's input_schema once at import with the jsonschema crate (same
  2020-12 dialect publish_schema uses) and validates peer input at call
  time; the compile-time copy is hardened closed-by-default
  (additionalProperties: false injected when absent) so the validator
  reproduces the allowlist's unknown-key semantics; explicit
  additionalProperties:true catch-all and schema values are preserved;
  the original spec value is never mutated
- from_openapi/from_jsonschema import(): compile per registration,
  capture the validator in the handler closure (re-import recompiles —
  the closure capture is the invalidation story); a non-compilable
  input schema fails import loudly (AdapterError::SchemaParse naming
  the operation), matching the publish_schema fail-closed precedent
- from_openapi generated input schemas now carry
  additionalProperties:false explicitly, so the /schema advert states
  the enforced rule and external schema-driven validators reach the
  same verdicts
- forward/forward_stream/build_request gain an
  Option<&CompiledInputSchema> parameter; enforcement runs after the
  allowlist
- round-trip test (review 002 Test-gap 10): the /schema-exported
  input_schema is compiled with the same validator and driven against
  build_request over a 10-input violation matrix — accept-sets exactly
  equal in both directions; the chain-test that lets advertise/enforce
  drift surface as a CI failure
- ADR-066: new decision section (advertise==enforce) with the trust-
  boundary reasoning and the rejected option (b) rationale
- module + enforce_input_schema docs updated to the two-gate shape

cargo test --all-features 596 pass; clippy --all-features/-D warnings,
fmt, doc gates clean.

docs(tasks): mark review-002-fu-oai18-decision done
2026-08-31 07:18:53 +00:00
glm-5.3-flash 7294d19fc7 test(infra): cover review-002 stream/PEM/cap error arms + drop dead WsTimeouts Default
- forward_stream build-error arm (forward.rs): wire-level test asserts
  one INVALID_INPUT envelope then stream end with zero upstream
  contact (a panicking responder counts as the contact guard), plus
  the from_jsonschema integration mirror (undeclared key + non-scalar
  placeholder, each naming its rejection source)
- PEM read-failure arms (http_client.rs): nonexistent CA path →
  CaBundleRead (sync new), nonexistent client-cert path → ClientCertRead
  (async reload, prior generation retained)
- Over-cap poll_write rejection leg (byte_adapter.rs): cap+1 write →
  InvalidData naming the cap; stream stays usable for an at-cap write
  afterwards
- SSE parser edges: CRLF split across feed chunks frames one line;
  invalid-UTF8 data lines drop without killing the frame stream
- from_value structural rejects: non-object doc, missing `info`,
  missing `paths`, non-object `paths` each name the member
- Connection-failure arms: accept-path ConnectionClosed →
  HandlerError::ConnectionClosed via stream_error_to_handler; read-pump
  demux-gone break ends the pump when the byte-stream side is dropped
- Delete the caller-less `impl Default for WsTimeouts` (the extension
  is constructed explicitly)

cargo llvm-cov --all-features: all named arms covered; TOTAL regions
94.18% (was 93.86%), lines 96.04% (was 95.77%); http_client.rs
86.56% lines (was 81.72%).

docs(tasks): mark review-002-fu-stream-error-coverage done
2026-08-31 06:47:29 +00:00
glm-5.3-flash 4b6507c452 docs(websocket,gateway): fix rustdoc link warnings, restore -D warnings doc gate
- dispatch.rs: qualify [invoke]/[invoke_streaming] as
  [GatewayDispatch::invoke]/[::invoke_streaming] (module-level docs
  need the type-qualified path)
- byte_adapter.rs: drop redundant explicit link targets for
  DEFAULT_WS_WRITE_TIMEOUT / DEFAULT_WS_IDLE_TIMEOUT (already
  re-exported at crate::websocket)
- from_wss.rs, byte_adapter.rs: backtick WssSession::drop and the
  private WsFraming (all-features-only link errors, same class)
- Ws pump/timeout module docs spot-checked against WS-13/18/17 impls:
  no drift found

RUSTDOCFLAGS="-D warnings" cargo doc --no-deps now exits clean both
default-features and --all-features.

docs(tasks): mark review-002-fu-doc-warnings done
2026-08-31 06:23:30 +00:00
glm-5.3-flash 091518ed7d Merge branch 'wt/review-002-client-policy-wire-tests' 2026-08-31 01:56:11 +00:00
glm-5.3-flash 3b2a26e021 Merge branch 'wt/review-002-cov-deployment-knobs'
# Conflicts:
#	src/gateway/routes.rs
2026-08-31 01:55:12 +00:00
glm-5.3-flash b224531a61 Merge branch 'wt/review-002-import-loudness-cluster' 2026-08-31 01:53:53 +00:00
glm-5.3-flash 59d77360cc Merge branch 'wt/review-002-gw16-status-drift'
# Conflicts:
#	src/gateway/dispatch.rs
2026-08-31 01:52:37 +00:00
glm-5.3-flash 903a91f1d2 fix(adapters): reject header params colliding with default_headers/credential headers (OAI-19)
build_request inserts header params first, then default_headers, then
credential headers — HeaderMap::insert replaces, so a declared
in: header parameter whose name matched a default or credential header
silently never delivered the peer's value upstream.

check_header_param_collisions runs at import (the assembly's
HttpServiceConfig is visible there): Authorization on an authed
namespace is rejected outright, a default_headers name match (compared
case-insensitively) fails import naming both keys, and an ApiKey
header_name match fails naming the credential header. Per the task's
decision note: the import-time surface does see the adapter's config,
so the loud point stays at import rather than first-call warn-once.

Tests: Authorization+Bearer, X-Tenant/x-tenant case-insensitive
default_headers, x-api-key/X-API-Key, and the no-collision clean path.
2026-08-31 01:46:45 +00:00
glm-5.3-flash 207bca4f23 fix(gateway): block services/schema spec disclosure via the op path (review-002 PRJ-16)
Internal/ACL-restricted op specs were readable through
POST /call {"operation":"services/schema","input":{"name":...}}
(the MCP call/batch tools identically): the outer-name pre-checks pass
(services/schema is External) and alkcall's services_schema_handler
projects any registered spec with no visibility/ACL check of its own
(alkcall CF-004 is the complete fix there).

- GatewayDispatch.invoke/invoke_streaming now apply the GET /schema
  route's is-internal + access-control checks to the meta-op's inner
  name input before dispatch (404 Internal / FORBIDDEN ACL), one
  interception point covering /call, /batch, /subscribe and the MCP
  call/batch tools; /publish cannot reach the Query-typed meta-op
- the visibility+ACL check is one shared fn (schema_disclosure_denial)
  used by the HTTP /schema route, the dispatch guard, and the MCP
  schema tool, so transports cannot drift
- when CF-004 lands, this guard remains as defense-in-depth (ADR-071)

Tests: dispatch-spine guard unit tests; /call 404 + 401/403 matrix,
/batch NOT_FOUND entry, /subscribe error event; MCP call/batch tools
via services/schema with an Internal inner name (mcp feature).

Verify: cargo test (405), --all-features (523), clippy default and
--all-features --all-targets -D warnings, fmt --check — all pass.
2026-08-31 01:32:59 +00:00
glm-5.3-flash fea7565613 fix(adapters): bound import/call error messages from spec-derived lists (OAI-17)
Import errors echoed unbounded spec-derived strings: a 100k-path
servers-override list produced a multi-megabyte SchemaParse message.
forward::bounded_join caps list echoes at 8 items / 128 chars per item
with a ', … (+N more)' suffix, and is applied at the servers/callbacks
/security location lists, the unbound-placeholder and unbound-remnant
lists in build_registration/forward, and the call-time declared-keys
echo. resolve_ref caps interpolated $ref strings at 128 chars.

Tests: 100k-path servers fixture asserts message < 4 KiB (linear,
completes fast); bounded_join unit test pins count+width truncation
with unchanged small-case output.
2026-08-31 01:12:43 +00:00
glm-5.3-flash ac6b4b6c9a fix(gateway): unify INVALID_INPUT to 422 on hand-rolled paths + sink deadline (GW-16, GW-17)
GW-16: empty body / malformed first line / missing header fields /
per-line cap / batch over-cap rejections now route through
call_error_to_http_response_with_identity, mapping INVALID_INPUT to
422 — same status as mid-stream chunk errors. One error class, one
status.

GW-17: invoke_sink wraps the registry sink invoke in the same 30 s
tokio::time::timeout the Once-op invoke uses; a hung sink handler
surfaces as a TIMEOUT (504, retryable) error envelope instead of
holding the HTTP connection forever. The sink wrapper bounds the
whole dispatch (chunk pacing included), matching http-server.md's
deadline contract.

Docs: http-server.md error table documents the 422 triggers and the
sink deadline; http-adapters.md batch cap status corrected.

to_openapi: gateway spec version 1.3.0 -> 1.4.0 (ADR-045 minor):
/publish framing faults and /batch cap reject documented at 422 (the
400 slots moved with the runtime); /publish 400 slot removed; 504
description covers the sink dispatch.

Verification: scripts/verify.sh OK (397 tests); cargo test
--all-features OK (513 tests); clippy --all-features --all-targets -D
warnings OK; cargo fmt --check OK.
2026-08-31 01:03:29 +00:00
glm-5.3-flash f5e75d318a fix(adapters): loud OAI-14 blocks on import — callbacks, security, oneOf requestBody, discriminator/xml warns
The OAI-06 loudness matrix had holes: callbacks, security requirement
blocks, top-level oneOf requestBodies, and schema-level
discriminator/xml keywords all vanished silently at import.

- callbacks + security: OpenAPISpec::validate_import_loud_features
  (new, run from FromOpenAPI::import) rejects with locations and
  remediation. Scoped to the service-import path, not the shared
  from_value parse — the published gateway doc round-trips through
  from_value inside to_openapi and legitimately declares security
  markers for its external clients.
- top-level oneOf requestBody (no content map): refused in
  parse_operation as an unresolvable/content-less body (OAI-15 arm,
  message names OAI-14 oneOf).
- discriminator/xml inside consumed schemas: per-operation
  tracing::warn listing the ignored keys (JSON-forwarding-only stance).

Tests: op-level callbacks, doc-level + op-level security, oneOf
requestBody each fail import naming the feature and location.
2026-08-31 01:03:18 +00:00
glm-5.3-flash 0837e942ab test(client,adapters): retry policy + streaming terminal arms + credential/header-parameter arms (CLI-03, COV-10, COV-12)
- new tests/retry_policy_wire.rs: POST+500 = 1 upstream hit (method
  gate), GET 500/503/200 = 3 hits (retry-to-success), budget exhaustion
  stops retries under a 50-attempt cap (wall-time + hit-count bounds)
- forward_stream terminal arms on the wire: oversized SSE line ->
  one INTERNAL terminal envelope; mid-stream transport abort (staged
  via a notify gate so the abort is genuinely mid-stream) -> terminal
  envelope after the delivered frame; pending event EOF-flush; dead
  port through both forward() and forward_stream()
- Once-path decode arms: malformed application/json 200 -> INTERNAL
  decode envelope; application/octet-stream 200 -> byte-array envelope
- COV-12 credential arms: ApiKey and Basic malformed values fail
  loudly without echoing secret material; declared header-param
  invalid name/value rejections
- new tests/client_config_reload.rs: config() reflects a reloaded
  config (FWD-12 atomicity half)
2026-08-31 01:00:29 +00:00
glm-5.3-flash 3f0b59b7d5 fix(adapters): warn on $ref sibling keys, document 3.0-only reading (OAI-10)
$ref siblings are ignored under OpenAPI 3.0 semantics but would apply
under 3.1 — a 3.1-authored constraint beside a $ref previously
vanished silently, overstating /schema. warn_ref_siblings now fires at
each $ref consumption point (operation parameters, requestBody,
path-item parameters) naming the location and dropped keys, and the
module doc records the version stance: no openapi 3.1 gate, 3.0
reading with the warn as the visibility mechanism.
2026-08-31 00:50:17 +00:00
glm-5.3-flash 688b7e91b2 fix(adapters): self-ref'd or content-less requestBody fails import, not silent body-less op (OAI-15)
A requestBody $ref that could not resolve (or resolved to a shape
without a content map) previously turned into a content-less Operation
registered silently — every call would INVALID_INPUT on the gateway
body key. parse_operation now treats an unresolvable request-body ref
and a resolved-but-content-less body the same unfaithful-modeling way
as an unresolvable parameter ref: the operation fails import via the
OAI-04 'unresolvable $ref' arm, whose message now names the
requestBody case (OAI-15) alongside the parameter one.

Tests: self-ref requestBody, missing-component requestBody ref, and
content-less component requestBody each fail import loudly.
2026-08-31 00:46:14 +00:00
glm-5.3-flash 02a59dfa22 test(adapters): array-of-$ref resolution + style:simple/explode:true rejection (COV-12 review-002)
Array branch of resolve_refs_bounded expands items $refs (memo hit on
repeat resolution); simple+explode=true import fails naming OAI-06.
2026-08-31 00:44:18 +00:00
glm-5.3-flash 847e5ca586 test(adapters): wire tests for same-host/cross-host/hop-cap redirect policy (CLI-02)
- same-host 302 followed with the credential header arriving at the
  followed hop (FWD-03's scrub is cross-host only)
- cross-host 302 surfaced as HTTP_302 with the attacker endpoint
  receiving zero requests (load-bearing FWD-03 property, pinned)
- redirect loop trips the hop cap -> loud INTERNAL transport error
2026-08-31 00:43:39 +00:00
glm-5.3-flash e6077fdb6b test(server): static-site decoy resolves directory requests to index.html (COV-12 review-002) 2026-08-31 00:43:32 +00:00
glm-5.3-flash 5f872f4084 test(gateway): /publish first-line invalid JSON returns 400 INVALID_INPUT (COV-12 review-002) 2026-08-31 00:43:03 +00:00
glm-5.3-flash 8a98a52624 test(mcp): schema/batch missing-argument INVALID_INPUT arms (COV-12 review-002)
schema tool with None arguments and batch tool without a calls array
both return INVALID_INPUT naming the required field, without dispatching.
2026-08-31 00:40:23 +00:00
glm-5.3-flash 9a5b4e7936 fix(adapters): hoist path-template validation into shared forward core, run at from_openapi import (JS-02)
Import-time path-template checks now run on both adapters:
- from_jsonschema: validate_path_template moves to forward.rs (same
  checks, same messages, shared implementation)
- from_openapi: build_registration calls it before building each op, so
  a template like /x{open fails import with 'unterminated placeholder'
  instead of a per-call INTERNAL on first invoke
- new test: unterminated_path_template_fails_import_not_first_call
2026-08-31 00:39:54 +00:00
glm-5.3-flash d9971e9fad fix(adapters): zip-iterate reject_collisions, drop asserts (JS-03)
assert_eq! in library code was a panic-family residue in the import
collision check (review 002 JS-03). The three vectors are built in
lockstep by import(); zipping them preserves the pairwise walk without
the panic path.
2026-08-31 00:33:59 +00:00
glm-5.3-flash d5446c2780 Merge branch 'wt/review-002-cli01-retry-after-budget' 2026-08-31 00:18:57 +00:00
glm-5.3-flash 8c55747029 Merge branch 'wt/review-002-fwd13-dot-segments' 2026-08-31 00:17:57 +00:00
glm-5.3-flash cfbefe6d04 fix(client): budget-aware Retry-After sleep + earliest-deadline re-arm clamp (CLI-01)
A logical request's Retry-After waits are now bounded by
max_total_retry_duration, and a retry storm can no longer re-arm a
full ceiling per attempt:

- BudgetClock anchored per logical request by RetryGateMiddleware
  (into the request Extensions, shared across retry attempts) and
  read by the inner RetryAfterMiddleware every attempt; the monotonic
  anchor projects the hard stop through wall-clock steps.
- maybe_sleep_for truncates the sleep to the remaining budget;
  a spent budget skips the sleep entirely.
- record() keeps the EARLIEST deadline per URL (retry storms cannot
  extend the first-seen deadline); a refresh that cannot make it
  under the budget hard stop drops the entry so the next attempt
  starts immediately instead of parking.
- Middleware without a budget anchor (budget = 0) keeps the prior
  semantics; the shared client now wires
  HttpClientConfig.max_total_retry_duration into the Retry-After
  middleware.
- Wire tests (tests/retry_after_budget.rs): always-429 responder with
  a 300 s Retry-After is bounded by budget + one attempt; separate
  logical requests still honor the recorded throttle window.
- FWD-12 atomic reload pairing and the FWD-15 stream-client split
  untouched (stack built in build_client_with_pems for both).
2026-08-31 00:16:19 +00:00
glm-5.3-flash 58bdba35cf fix(adapters): post-set_path normalization invariant check (FWD-13)
- assemble_request_url now verifies after Url::set_path that the decoded
  URL path segments are byte-identical to base_dir + decoded rendered
  segments; a mismatch (any future normalizer rewrite in the url crate)
  fails loudly with INTERNAL instead of silently re-routing an
  authenticated request
- property-style corpus test over dot/percent/binary values: accepted
  values must survive byte-identical as one literal segment with no lone
  dot segments; failures may only be INVALID_INPUT

Verification: scripts/verify.sh (383 passed) and --all-features (499
passed), clippy -D warnings, fmt --check all pass.
2026-08-31 00:08:12 +00:00
glm-5.3-flash d81e7ef319 fix(adapters): reject lone dot/dot-dot path values normalized away by Url::set_path (FWD-13)
- value_to_path_segment now rejects scalar values whose decoded form is
  exactly '.', '..', '%2e', '%2E', '%2e%2e', or '%2E%2E' (case-insensitive)
  with INVALID_INPUT: url 2.5.8 Url::set_path silently normalizes lone
  dot segments away, so such values would route to a different upstream
  endpoint than the template describes, with namespace credentials attached
- rejection is exact-match on the full decoded segment: dotted values
  like v1.2.3, .hidden-file, ..hidden, ... still render
- error names the failure mode but never echoes the raw value
- empirically pins the set_path normalization behavior in a test
  (tenants/../resources -> /resources, /files/.. -> /, %2E%2E -> normalized)

Verification: scripts/verify.sh (382 passed) and --all-features (498
passed), clippy -D warnings, fmt --check all pass.
2026-08-30 23:57:00 +00:00
glm-5.3-flash a427dc194d fix(adapters): refuse unauthenticated send when capability is absent (FWD-16)
The FWD-08 remediation made every malformed credential fail loudly, but
an authed operation whose registry capability was entirely absent fell
through the build_request match: the request was sent with no credential
and no diagnostic, producing corrupted upstream 401s at call time.

- build_request now returns an INTERNAL error naming the missing
  capability keys (api_key:{ns} / http_token:{ns}) when an auth scheme
  is declared and Capabilities::get is empty; the request is not sent
- auth_scheme: None behavior unchanged (unauthenticated ops stay
  unauthenticated); error message carries key names only, no secret
- module doc: loud-missing matrix now covers malformed name/value AND
  absent capability
- tests: unit loud-error across all three schemes, unchanged-arm pin,
  wire test asserting the upstream receives zero requests (mirrors the
  FWD-08 test family)
- from_openapi/from_jsonschema no_env_vars tests updated: they pinned
  the old silent fall-through; still assert no env material echoes

Verification: cargo test (380 passed), cargo test --all-features
(496 passed), clippy --all-targets -D warnings (default + all-features),
cargo fmt --check — all via scripts/verify.sh
2026-08-30 23:55:42 +00:00
glm-5.3-flash 9819c24b4f chore(tasks): mark yaml-normalization, mcp-batch-cap, cov13, projection-truthfulness completed 2026-08-30 23:36:41 +00:00
glm-5.3-flash 1a5ae8748a Merge branch 'wt/review-002-projection-truthfulness' 2026-08-30 23:08:42 +00:00
glm-5.3-flash 8611920fdb Merge branch 'wt/review-002-cov13-dead-code'
# Conflicts:
#	src/gateway/dispatch.rs
2026-08-30 23:07:39 +00:00
glm-5.3-flash ccef4a7ca9 Merge branch 'wt/review-002-mcp-batch-cap' 2026-08-30 23:06:23 +00:00
glm-5.3-flash 60362c4d0f docs(architecture): align http-adapters.md to_openapi section with the 1.3.0 projection
- /call response map: 415 slot, identity-split 401 oneOf, PRJ-17 merge
  note, PRJ-19 shape-rejection note
- /batch: no-500 statement (PRJ-21) and the BatchError in-band error
  component (PRJ-16b)
- adapter.rs openapi.json test pin refreshed to 1.3.0
2026-08-30 22:56:53 +00:00
glm-5.3-flash 29d98b8c22 fix(adapters): projection doc truthfulness in to_openapi (PRJ-16b/17/18/19/20/21/23, info.version 1.3.0)
- PRJ-16b: BatchResultEntry.error now refs a defined BatchError
  component (oneOf over the six protocol-code envelopes plus a generic
  BatchOperationError arm carrying the operation-declared code enum);
  the dangling #/components/schemas/CallError ref is gone
- PRJ-17: operation-declared errors at protocol statuses with
  HTTP_-prefixed codes merge into the shared protocol response's oneOf
  (per-code CallError_<code> components) instead of clobbering it —
  the runtime genuinely emits both; non-protocol statuses overwrite
  as before
- PRJ-18: /publish 400 dropped the INVALID_OPERATION_TYPE claim
  (runtime reports that at 401 without a token, error.rs); the 401
  entry is the true one and already documented
- PRJ-19: 415 (missing/non-JSON Content-Type) and plain-text 422
  (shape-rejection) extractor slots documented, extending the
  plain-text extractor rejection family; /subscribe gains 415/422,
  /call gains 415 with the shape-rejection noted on 422, /search and
  /schema gain the slots too
- PRJ-20: /call 401 now carries the identity-split oneOf (FORBIDDEN +
  INVALID_OPERATION_TYPE), matching error.rs's 401-without-identity
  mapping for both
- PRJ-21: /batch's unreachable 500 removed (all dispatch failures are
  in-band entries; routes.rs has no 500 path)
- PRJ-23: the OAS-invalid x-operation-error-statuses pseudo-schema key
  inside components.schemas removed (nothing consumed it; any openapiv3
  registry rejects it as an invalid schema name)
- info.version 1.3.0 per ADR-045 (doc-contract corrections, wire
  contract unchanged)

Verification: cargo test (to_openapi suite 42/42 green, incl. the
populated-registry openapiv3 parse and deterministic golden checks)
2026-08-30 22:55:38 +00:00
glm-5.3-flash 4f644a4c2c feat(mcp): cap MCP batch tool at MAX_BATCH_OPERATIONS (PRJ-22)
- enforce the same 100-operation cap the HTTP /batch endpoint enforces;
  over-cap \x60calls\x60 reject with a structured INVALID_INPUT (retryable:
  false, matching CallError::invalid_input) before any dispatch
- hoist MAX_BATCH_OPERATIONS to gateway/mod.rs and reuse it in routes,
  to_openapi (removing a pre-existing duplicate literal), and to_mcp
- state the limit in the batch tool description and add maxItems to the
  input schema (doc previously advertised no limit)
- GatewayDispatch gains a per-instance invoke_count spy accessor so the
  over-cap test proves zero dispatches (process-global counters raced
  under the parallel test runner)
- tests: over-cap -> INVALID_INPUT + invoke_count()==0; at-cap -> 100
  results + invoke_count()==100

verification: scripts/verify.sh (352 passed) and scripts/verify.sh
--all-features (468 passed); cargo clippy --all-targets -D warnings and
cargo fmt --check clean
2026-08-30 22:51:06 +00:00
glm-5.3-flash 08584b229d refactor(gateway): delete dead accessors and FromRef impls (review 002 COV-13)
Coverage-confirmed dead code (every binary, zero hits):

- server/state.rs: drop FromRef<RouterState> impls for
  Arc<OperationRegistry> and Arc<dyn IdentityProvider> — no route
  extracts these types; the auth middleware receives the provider
  directly via from_fn_with_state
- gateway/dispatch.rs: drop identity_provider() and resolve_bearer()
  accessors; resolve_bearer's doc promised an auth hook the middleware
  never calls (spec/code drift). Wire-or-delete resolved to delete:
  bearer resolution lives in the middleware (SRV-11 single-resolve
  ordering), the dispatch spine only needs the per-call
  Option<Identity>. GatewayDispatch::new consequently takes the
  registry alone (GatewayState loses its unused identity_provider
  passthrough; dispatch.rs/to_mcp.rs tests simplified)
- websocket/upgrade.rs: drop FromRef<SessionState> for
  Arc<OperationRegistry> — no router carries SessionState as its state
  type; the inverse FromRef<Arc<OperationRegistry>> for SessionState
  (custom upgrade routes, integration tests) remains

Verification: ./scripts/verify.sh (352 passed), ./scripts/verify.sh
--all-features (466 passed), clippy -D warnings, fmt --check.
2026-08-30 22:50:15 +00:00
glm-5.3-flash 1728b4881e feat(adapters): YAML input normalization — duplicates, .inf/.nan, merge keys, non-string keys (OAI-12)
from_yaml previously went straight yaml_serde::from_str::<serde_json::Value>
with zero post-parse normalization. Three verified corruptions flowed
through unimpeded:

- duplicate mapping keys silently last-won (serde_json's visit_map
  insert semantics — verified against 1.0.151: the JSON path last-wins
  too, so the YAML path is now deliberately the stricter one);
- .inf/-.inf/.nan scalars silently became Value::Null because
  serde_json::Number::from_f64(non-finite) is None;
- YAML 1.1 merge keys (<<: *anchor) survived as literal '<<' properties;
- non-string mapping keys (null keys, collection keys) either stringified
  through YAML's debug rendering or panicked the conversion.

The new from_yaml pipeline: explicit yaml_serde::Value parse (native
loud duplicate-key rejection with line/column) -> apply_merge() (merge
keys applied, shallow per yaml_serde semantics; scalar/invalid merge
values fail loudly) -> one structural normalization pass into
serde_json::Value that rejects non-finite floats and non-string keys
with the offending JSON pointer, and stringifies scalar keys exactly as
the YAML 1.2 core schema renders them (200: -> "200", matching the
JSON path's {"200": ...}).

The walk stays inside yaml_serde's own parse-time bounds (recursion
limit 128, alias jump limit, RepetitionLimitExceeded); no new budgets
and no resolver changes. 14 seam tests added at the from_yaml boundary.

Verification: cargo test 366 passed / 0 failed; clippy
--all-targets -D warnings clean; fmt --check clean (scripts/verify.sh).
2026-08-30 22:37:34 +00:00
glm-5.3-flash 3d77ffe1d3 fix(adapters): drop dead search_filter parameter from to_mcp handle_batch (PRJ-24)
The batch tool's input schema declares no query field, so the parsed
search_filter was computed then discarded with 'let _ ='. Remove the
parameter and the discard; call sites pass only (arguments, identity).
2026-08-30 22:24:03 +00:00
glm-5.3-flash 57ba9100c9 Merge branch 'wt/review-002-con18b-ws-polish' 2026-08-30 22:04:03 +00:00