Commit Graph
100 Commits
Author SHA1 Message Date
glm-5.3-flash 1ce886b9d4 chore: alkcall 0.3 -> 0.4.0 (registry input_schema enforcement)
alkcall 0.4.0 now enforces OperationSpec.input_schema at dispatch
(INVALID_INPUT). All 438+16 tests pass; the gateway path is
unaffected because alkhttp's imported ops validate through their own
CompiledInputSchema closure, which composes with the new registry
check (registry = raw JSON Schema semantics, adapter = closed by
default).
2026-09-05 06:34: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 802d94ec07 docs(review 007 Unit 2): WS-31 discovery-shadowing note + record corrections
- WS-31: websocket.md §"Data channels for browsers" + ADR-067's landed
  note record that WS-session discovery is the bootstrap set — the
  hook's bootstrap `services/*` registrations overwrite a
  base-registry `services/*` registration on the WS path by design
  (a deployment's custom `services/list` is shadowed on WS sessions
  only).
- ADR-048's landed note: correction + completion — the WS-26
  retention sentence was aspirational at the landed commit (WS-28) and
  is now real; the UP-02 posture's override half is now an explicit
  surface (`with_ws_op_register_acl` / `OpRegisterAcl`), with the
  note that `ChannelsPolicy` could not carry an op ACL.
- ADR-067's landed note: review-007 notes (WS-28 fix + gate, WS-29
  surface, WS-31 record).
- OQ-05 resolution: the retention claim carries the WS-28 correction.
- review-006 UP-02 log + WS-26 paragraph: corrections marking what the
  pre-fix tree did not have, with the landed remediation named.
- review-002 WS-17: the "bounded at 64 sessions" claim corrected —
  the bare-registry semaphore was per-request and bounded nothing;
  `SessionSlots` is the shared-cap surface.
- review-007 status: open for remediation → remediated, with the
  decisions taken (both "implement" options) and the gate names.

Verification: cargo test 454 passed / 0 failed; cargo doc --no-deps
clean (6 pre-existing warnings, identical at baseline).

Review: docs/reviews/007-ws-data-channel-surface-review.md
2026-09-05 05:45:31 +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 b36ec49cc1 docs(review 007): fresh-eyes pass over the landed WS data-channel surface
First review whose subject is the landed wiring itself (review 006
Units 2-4, 030c5ef/2053420/64fa10b), not the gap to it.

Findings:
- WS-28 [major] — the WS-26 ConnectionGuard is bound inside the
  `if let` block and drops when it ends, not when the channel-0 task
  ends: live_connections() is permanently empty for every session.
  Reproduced empirically through the live WS path (scratch test,
  run + removed). ADR-048's landed note and review-006's log record
  behavior the code does not have.
- WS-29 [minor] — the op/register ACL override recorded as landed
  (review-006 UP-02, ADR-048) is not implemented; the hook hardcodes
  AccessControl::default() and ChannelsPolicy cannot express an op ACL.
- WS-30 [minor] — the bare-registry default session cap is built per
  request (FromRef) and bounds nothing; corrects review-002 WS-17.
- WS-31 [minor] — install_bootstrap_discovery silently shadows a
  deployment's own services/list on WS sessions (upstream-mandated,
  unrecorded).
- WS-32 [minor] — the router-state openables threading
  (with_ws_openable_alpns) has no gate; every Unit-3 gate rides the
  extension fallback.

Plus non-findings bounding the re-review (UP-01 arm, policy threading,
from_wss exclusion set, extension precedence, gate fidelity) and a
sequenced remediation plan.

Baseline at 64fa10b: 454 / 582 tests, clippy both, fmt, doc — all clean.
2026-09-05 00:46:22 +00:00
glm-5.3-flash 64fa10be31 docs(review 006 Unit 4): spec reconciliation — OQ-05 resolved, WS data-channel docs to the landed state
- OQ-05: deferred → resolved (2026-09-04, review 006 Unit 2+3); the
  consumer-set reframe recorded (WS is also the native-client fallback
  behind hostile NAT/firewall; OQ-04 does not block the wiring).
- ADR-067: status amendment + the v1-cut blockquote gains the Wired
  (2026-09-04) note — per-session-fork shape, openable surface, gates.
- ADR-048: landed-state amendment — §4's hub→browser direction has its
  object (op/register → connection overlay, hub composes via the
  retained Arc<CallConnection>); the op/register ACL posture (UP-02,
  SRV-10 precedent) recorded.
- websocket.md: the step-7 deferral note and the §"Data channels for
  browsers" status block removed (the section now documents the landed
  surface: with_ws_openable_alpns, the OpenableAlpns fallback, cap
  policy, discovery, gates); idle-knob deployment note for silent data
  channels (semantics unchanged; the 60 s default bites more often —
  set None at assembly for long-lived interactive channels).
- Review 003 status → remediated (all findings closed; log in review
  006); its Unit-4 section marked landed.
- alknet-ADR-044 §5 pointer checked: not stale.

Verification: cargo test 454 / 0; --all-features 582 / 0; clippy
(both) clean; fmt clean; doc clean.
2026-09-04 16:17:11 +00:00
glm-5.3-flash 2053420f7d chore(deps): consume alkcall 0.3.1 — list-peers announced-op discovery gate
The alkcall 0.3.1 fix (fd21230, alkhttp review 006 UP-03) lands
PeerCompositeEnv::peer_operations, so services/list-peers now lists
peer-announced ops. Version pin stays 0.3 (semver-compatible); the
lockfile bump carries no source change.

- Extend op_register_served_per_session_and_collision_is_already_exists
  with the discovery assertion that originally surfaced UP-03: after
  the announce resolves, services/list-peers attributes consumer/exec
  to the alice peer entry (fails against 0.3.0, verified by the
  original draft).
- Review 006 UP-03 entry records the 0.3.1 landing (alkcall ADR-030).

Verification: cargo test 454 passed / 0 failed; cargo test
--all-features 582 / 0; clippy (default + all-features, all-targets,
-D warnings) clean; fmt clean; doc clean.
2026-09-04 16:17:07 +00:00
glm-5.3-flash 214fd213ff docs(review 006): remediation log for Unit 2+3; UP-03 filed upstream-side
- Unit 2/Unit 3 recorded as landed (030c5ef): hook rework shape,
  policy threading (the ChannelsPolicy seam the ledger gate caught),
  WS-26 handle retention, from_wss protocol-op exclusion, UP-01, the
  six new gates, and the harness hardening notes (request-id filter,
  flat CallError payload shape).
- UP-02 decided: op/register serves per session with the default ACL
  (SRV-10 posture); the visibility tension resolves as ACL-gate.
- UP-03 [minor, alkcall-side] filed: services/list-peers cannot list
  peer-announced ops — PeerCompositeEnv overrides peer_ids but not
  peer_operations (trait default Vec::new()), so the amendment's
  peer-discovery promise does not resolve on the wire. Discovered by
  the gates; fix lives in alkcall; alkhttp's gate asserts
  overlay-landing + collision semantics instead and is not blocked.
- Unit 4 (docs reconciliation) remains open.

Verification unchanged: cargo test 454/582, clippy both sides, fmt,
doc — clean.
2026-09-04 15:47:48 +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 90790374fa docs(review 006): alkcall 0.3.0 consequence review — WS-24/25 resolved upstream, residual enumerated
Records the review pass of alkcall 0.3.0 against this crate:

- WS-24 (dispatch-resolution mechanism) and WS-25 (client-side op
  registration) verified resolved upstream — per-session fork (alkcall
  ADR-047 §4 amendment #2) + op/register (alkcall ADR-022 amendment
  2026-09-03 + 2026-09-04 collision sub-amendment); the G-01 serving-
  loop concurrency rework lands here via the bump alone.
- One new finding: UP-01 [minor] — ALREADY_EXISTS (new 0.3 protocol
  code) unmapped in the gateway error table (would 500); fix rides
  Unit 2. UP-02 records the op/register ACL posture decision for
  Unit 2.
- WS-20/21/22/26/27/23 re-verified still open, now unblocked; the
  remediation plan re-sequences (Unit 2 wiring with the concrete
  fork shape, Unit 3 gates + two upstream-mirroring additions,
  Unit 4 docs).

Baseline gates at df86f89: cargo test 453/575, clippy both sides,
fmt, doc — all clean.
2026-09-04 14:58:43 +00:00
glm-5.3-flash df86f89440 docs(review 003): land the Unit-1 re-point; extend it to alkcall review 005 + 0.3.0
Commits the re-point edits left uncommitted in the working tree
(5b62307's follow-through; alkcall review 005 verified these lines at
that tree), updated to the post-review-005 state:

- ADR-048 reconciliation note gains the 2026-09-04 update: alkcall
  review 005 remediated the landed mechanisms (serving-loop
  concurrency G-01/G-02, op/register collision policy G-03, spec
  round-trip G-04, bootstrap-list alignment G-05) and alkcall 0.3.0
  shipped them; alkhttp now consumes 0.3. The ADR-022 collision
  sub-amendment binds here: a peer-announced op never shadows the
  serving side's own registrations — the WS session's op/register
  handler gates on the session fork.
- OQ-05 resolution gains the same dated update and extends the
  cross-references to alkcall reviews 004-005.

What remains here is still alkhttp-side wiring only (review 003
Unit 2).
2026-09-04 14:55:23 +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 5b62307be9 docs(review 003): Unit 1 re-pointed at alkcall review 004; bootstrap-op candidate recorded
The alkcall decision work (WS-24 dispatch-resolution mechanism, WS-25
client-side op registration) now lives upstream as alkcall review 004.
Unit 1 records the leading candidates and their alkhttp consequences:

- WS-24 -> option (a), per-session base registry (the only
  wire-proven dispatch shape; fork surface = alkcall F-02)
- WS-25 -> channel-0 bootstrap op (op/register as an assumed op in
  the bootstrap set; requires the alkcall client serving half, F-04)
- discovery composition: services/list on the fork + peer-registered
  ops via the already-built services/list-peers

Consumer-set reframe from WS-23 unchanged; no findings added or
removed.
2026-09-03 14:42:50 +00:00
glm-5.3-flash 19e30c9769 docs(review 003): WS data-channel wiring gap drill-down
Focused review of the OQ-05 deferral (review-001 WS-03): what is
missing to wire browser/native data channels over WS, the deferral
rationale check, and the design gaps found behind the 'nothing new
to design' assumption.

Findings (continuing review-002 numbering):
- WS-20..22 [major]: mechanical wiring gap — install_channel_zero
  discards the ChannelManager, generic channel lifecycle ops never
  registered, no openable-ALPN deployment surface
- WS-23 [minor]: no browser-opened-channel e2e test; OQ-05 reopen
  trigger reframed (native WS consumers are not blocked on OQ-04)
- WS-24 [major, cross-crate]: top-level dispatch never consults the
  connection overlay — the ADR-047 §4 amendment mechanism cannot
  resolve open ops on the wire (needs an alkcall decision)
- WS-25 [major]: no wire mechanism for client-side op registration —
  ADR-048 browser-side bidirectionality is undesignable as specified
- WS-26 [major]: no retained live connection handle; hub cannot
  reach a session's overlay or push toward it
- WS-27 [minor]: dead-code-gated test hook cleanup

Includes a non-findings section (channel-id split, demux hardening,
open-op wrapper completeness, idle-knob posture) and a 4-unit
remediation plan sequenced so the alkcall decision task (Unit 1)
gates the alkhttp wiring shape (Unit 2).

Verification: cargo test (453) / --all-features (575), clippy
(all-targets, both feature sets), fmt — all clean at 58665f2.
2026-09-03 08:31:44 +00:00
glm-5.3-flash 58665f2061 docs: add crate README, MIT/Apache license files
- README covering the gateway surface, both import/export adapter
  families, feature flags, and security posture (all claims verified
  against the current code)
- standard MIT and Apache-2.0 license texts matching the
  license = "MIT OR Apache-2.0" manifest field
- Cargo.toml: point the manifest at README.md and exclude scripts/
  from the published package
2026-09-01 11:18:36 +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 5acc561eca docs(tasks): re-validate bracketed follow-up into 3 tasks (OAI-18 decision, streaming-coverage residue, doc-gate fix)
The bracketed planning task's five candidates, re-validated against
the post-bulk tree (0a932e5, all 23 bulk tasks done, 446/568 green):

1. OAI-18 -> review-002-fu-oai18-decision: still key-allowlist-only;
   bulk blockers (yaml/path-item) both landed. Task records the
   enforce-vs-scope-the-advert decision framework from review 002.
2. CON-08/CON-09 close() lever: deferred still (documented v1
   contracts unchanged, no consumer pressure) — not tasked.
3. Cross-crate: CF-001..004 all open in alkcall, no alkhttp-side task;
   PRJ-16 guard already documents its defense-in-depth re-scope
   disposition (no work when CF-004 lands).
4. Stale-comment sweep: mostly absorbed by the bulk (SRV-11 comment
   rewritten in-fix); residue = 3 cargo-doc warnings + WsTimeouts
   redundant Default -> review-002-fu-doc-warnings.
5. ADR-045 version audit: clean (1.4.0 + annotations + test pin).

Post-bulk coverage re-pass (95.89% regions, bulk's new code fully
covered: PRJ-16 guard, body cap, router reorder, batch cap, WS
knobs, OAI-11 node budget all exercised) surfaced one unanticipated
residue -> review-002-fu-stream-error-coverage (forward_stream
build-error arm never exercised on a Sub op; PEM read-failure arms;
over-cap poll_write; SSE split-CRLF edge; from_value structural
rejects; WsTimeouts Default).

Bracketed pass itself marked completed (planning consumed).
taskgraph: 69 valid, no cycles; 3 pending.
2026-08-31 05:40:46 +00:00
glm-5.3-flash 0a932e5ec3 chore(tasks): mark final wave completed (prj16, gw16, cov-knobs, import-loudness, client-policy-wire-tests) 2026-08-31 02:03:50 +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 2d53957f08 docs(architecture): record the loud unsupported-feature matrix in http-adapters spec (review 001 OAI-06 + review 002)
The OAI-06 matrix lived only in the completed review-001 task notes;
acceptance for the review-002 loudness cluster requires the successor
doc section. New 'Loud unsupported-feature handling' section on
from_openapi: refused/warned/projected feature tables covering cookie
params, style/explode forms, servers, webhooks, callbacks, security,
oneOf requestBodies, unresolvable or content-less requestBody refs,
path-template validation, collision rejection, ref-sibling warns,
discriminator/xml warns, error-projection mappings, and the OAI-17
error-bounding contract.
2026-08-31 01:47:30 +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 6490d15573 test(client): PEM parse-failure arms + config() TLS-reload assertion (CLI-03, COV-11, FWD-12)
- corrupt CA bundle section -> CaBundleParse naming the path (the
  pure-non-PEM variant parses as zero sections and is accepted by
  reqwest, so the arm needs structurally broken PEM)
- garbage client identity -> ClientCertParse naming the cert path,
  with the error asserting key material is never echoed
- config() reflects a reloaded CA bundle on the TLS path (FWD-12)
2026-08-31 01:15:09 +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 d0e9d4e608 test(mcp): from_mcp wraps non-object tool arguments as {"value": …} (COV-12 review-002)
Wire-level round trip through the real rmcp server: a scalar input
reaches the remote tool as {"value": <input>}, matching the
value_to_json_object wrap.
2026-08-31 00:41:37 +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 21e19abb60 test(mcp): rmcp-protocol call_tool round-trip per gateway tool + unknown (COV-11b review-002)
peer.call_tool through the real rmcp streamable-HTTP transport for
schema/call/batch/unknown — the production ServerHandler::call_tool
routing shell, previously only exercised via the invoke_tool bypass
for search.

Verification: cargo test --features 'mcp test-support' --test full_surface
2026-08-31 00:38:38 +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 a7f10ed04c chore(tasks): mark fwd16, fwd13, cli01 completed 2026-08-31 00:23:46 +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 e98e495137 test(full_surface): refresh /openapi.json version pin to 1.3.0 2026-08-30 23:04:25 +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 67ae7cd9fe docs(adr-051): YAML/JSON parity contract for the OAI-12 normalization pipeline
§5 records the post-OAI-12 from_yaml contract: duplicate keys rejected
loudly on YAML (with the empirical correction that serde_json 1.0.151's
Value path last-wins rather than errors — the YAML side is the stricter
one), non-finite floats rejected with JSON-pointer context, merge keys
applied via apply_merge (shallow, referencing keys win; the one
deliberate YAML 1.2 deviation), scalar-key stringification matching the
core schema, and the no-new-bounds note (the walk stays inside
yaml_serde's parse-time limits).

Verification: cargo doc --no-deps clean; module-doc cross-check in
openapi_spec.rs matches this contract.
2026-08-30 22:38:04 +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 8261fefd8f chore(tasks): mark js01, con18-wss, fwd17-19, con18b completed 2026-08-30 22:12:40 +00:00
glm-5.3-flash 57ba9100c9 Merge branch 'wt/review-002-con18b-ws-polish' 2026-08-30 22:04:03 +00:00
glm-5.3-flash 765dfc0858 Merge branch 'wt/review-002-fwd17-19-contract-decisions' 2026-08-30 22:03:02 +00:00
glm-5.3-flash f60f1c0236 Merge branch 'wt/review-002-con18-wss-sweep-exit' 2026-08-30 22:02:24 +00:00
glm-5.3-flash bd87c4613b test(websocket): axum-flavor unit tests for text→1002 and cap-trip arms (WS-19)
- AxumFraming arms were exercised only indirectly via tungstenite
  (shared generic pumps); drive the axum message types directly with
  an in-process fake WebSocket (futures mpsc-backed Sink+Stream
  stand-in for the split halves)
- text test: read pump maps a text message to the WriteMsg close
  carrying 1002 + the text reason
- cap-trip test: an above-MAX_CHUNK_LEN header through the write pump
  closes with 1011 naming the violation

Verification: scripts/verify.sh OK (345 passed), test-support suite
ok, clippy -D warnings clean, fmt clean
2026-08-30 21:56:09 +00:00
glm-5.3-flash 674120ad72 feat(websocket): WsTimeouts request extension for WS pump knobs (WS-17)
- WsTimeouts { idle, write } request extension mirrors ChannelsPolicy:
  a deployment layers it on a WS route (bare-registry routes included)
  to set the pump knobs per route
- precedence: extension present replaces the router state entirely;
  absent falls back to SessionState (adapter-configured idle) and the
  crate default write window — a Default impl never clobbers the
  adapter-configured idle knob
- upgrade.rs module + handler docs now state the real defaults for
  bare-registry routes (60 s idle + 60 s write, 64-session semaphore,
  handler-private WsSessions) and the extension surface
- split_ws_to_bytes_idle_with_write exposes the WS-18 write window to
  run_channels_session; acceptance test drives a bare-registry route
  with a 150 ms extension idle window (1001 eviction observed)

Verification: scripts/verify.sh OK (343 passed), test-support suite
ok, clippy -D warnings clean, fmt clean
2026-08-30 21:48:03 +00:00
glm-5.3-flash da65e5b6db feat(websocket): write-progress timeout on the WS write pump (WS-18)
- DEFAULT_WS_WRITE_TIMEOUT (60 s, WS-01 knob family): one outbound WS
  send that stays unsent past the window (peer stopped reading)
  evicts the connection — the send path was slot-bounded but
  time-unbounded
- bound is per send call: a slow-but-draining peer resets it with
  every message emitted; only a fully stalled sink trips it
- on timeout the pump signals the stream error (InvalidData naming
  the write stall) and ends WITHOUT a close frame — a clogged socket
  cannot receive one and the close send would park on it
- test-support split helper with both knobs explicit backs the
  scaled eviction test (clogged duplex, eviction well inside the
  5 s slack)

Verification: scripts/verify.sh OK (343 passed), clippy -D warnings
clean, fmt clean
2026-08-30 21:36:53 +00:00
glm-5.3-flash bb709bd101 docs(adr 070): note the self-limiting from_wss drop monitor (CON-18)
Documents the CON-18 disposition in the v1 session-lifetime section
(teardown handle still future work, but a dead import no longer leaves
a permanent monitor task) and adds the consequence pair: dead imports
self-clean after EOF + bounded grace; registrations landing past the
grace window fall back to the 30s sweeper deadline.
2026-08-30 21:22:04 +00:00
glm-5.3-flash d8c51dc191 fix(adapters): dead-connection fast-fail + bounded from_wss sweep exit (CON-18)
The review-001 CON-02 monitor swept the pending map once EOF held but
never exited: every fire-and-forget import whose peer died left a
spawned task + pending map + watch receiver alive for the process
lifetime, and once the read pump ended the watch sender dropped so the
old select's 'changed()' error branch spun.

- replaced the 1s never-exiting sweep with a 50ms post-EOF drain
  (fast-fail) and a bounded grace window: the monitor ends after
  PENDING_SWEEP_MAX_POST_EOF (8) consecutive empty drains (~400ms)
- post-EOF registrations now resolve in ~50ms with retryable
  CONNECTION_CLOSED instead of waiting up to 1s for the next sweep
- removed the silent busy-spin: the loop now only wakes on the sweep
  tick or the session-close signal (close path still fails all + exits)
- retained the monitor JoinHandle on WssDropMonitor (test builds) and
  added a lifecycle test asserting the monitor joins after EOF+grace
- tightened call_registered_after_eof test: 500ms bound + CONNECTION_
  CLOSED code assertion (was a pre-sleep + 5s timeout)

Verification:
- cargo test --features wss: 358 passed, 0 failed
- cargo test --all-features: 454 passed, 0 failed
- cargo clippy --all-targets -- -D warnings (default, wss, all): clean
- cargo fmt --check: clean
- scripts/verify.sh + verify.sh --all-features: VERIFY OK
2026-08-30 21:22:01 +00:00
glm-5.3-flash 568954348b fix(adapters): reject unbound placeholders when from_jsonschema input_schema has no properties
The eager placeholder-binding check in spec_name_references_undeclared
short-circuited on an empty properties set, letting
FromJsonSchema::new accept a template whose placeholder could never be
bound (every later call failed INTERNAL, or INVALID_INPUT if a peer
supplied the key). Drop the early return so any placeholder against a
declared-nothing (or properties-less) input_schema is unbound, matching
the from_openapi unbound_placeholders path.

- add construction-time test: no-properties schema + {id} template
  fails with the placeholder error; no-properties schema + placeholder-
  free template still constructs and imports as Visibility::Internal

Verification: scripts/verify.sh (342 passed), scripts/verify.sh
--all-features (454 passed); clippy -D warnings and fmt --check clean
2026-08-30 21:06:08 +00:00
glm-5.3-flash d6d5509c6d fix(websocket): carry per-cause close reasons on CloseWith (WS-15)
- WriteMsg::CloseWith now carries (code, reason); the write pump sent
  the hardcoded "text messages not supported" string for the idle
  (1001) and inbound-frame (1011) closes as well as the text (1002)
  close
- close_reason module: canonical per-cause reason strings, shared by
  the close frames and the stream-error diagnostics (they already
  matched for idle/oversize; now single-sourced)
- reason asserts added to the idle-stall and forever-dribble 1001
  tests (reason names the no-chunk-progress cause) and a new text
  frame test asserts 1002 + the distinct text reason

Verification: scripts/verify.sh OK (342 passed), clippy -D warnings
clean, fmt clean
2026-08-30 21:03:29 +00:00
glm-5.3-flash f74f98e591 docs(adr-066): record FWD-17/18/19 forwarding contract decisions
SSE payload contract (non-JSON frames carry {data, event}; JSON frames
surface as themselves), the placeholder routing rule (placeholder keys
never double-emit as query; structural path values are INVALID_INPUT),
and the literal-percent trade-off (% in values always encoded; % in
assembly-supplied template text survives — the assembly owns the
upstream-semantics choice, per the ADR-066 trust boundary).
2026-08-30 20:56:57 +00:00
glm-5.3-flash 9ef2352568 fix(websocket): reject over-cap writes in poll_write before the queue (WS-14)
- byte-cap check moved ahead of poll_ready/try_send so the mux sees
  the InvalidData stream error at the failing call instead of the
  write pump emitting a mid-stream 1011 close (the chunk was already
  committed to the wire path)
- pump-side over-cap arm replaced by a debug_assert (defense-in-depth
  invariant; the error was unreachable for single-call writes once the
  pre-send check exists)
- write_tx_mut renamed write_tx_ref with an updated doc contract
- WS-05's over-cap pump test replaced by a pre-send rejection test
  (error surfaces at the first write, names the cap) + a cap-edge
  test (exactly PENDING_BUFFER_CAP still flows)

Verification: scripts/verify.sh OK (341 passed), clippy -D warnings
clean, fmt clean
2026-08-30 20:56:56 +00:00
glm-5.3-flash e3fd4443ab docs(adapters): pin routing, SSE-payload, and percent contracts in the module doc (FWD-18/19)
Module doc gains three sections: the FWD-18 routing rule (placeholder
keys never double-emit as query; structural path values are
INVALID_INPUT), the FWD-19 percent trade-off (% in values always
encoded; % in assembly-supplied template/base text survives verbatim —
documented instead of rejected, per the ADR-066 assembly trust
boundary), and the FWD-17 streaming payload contract. FWD-19 behavior
pinned by a wire-level template test.
2026-08-30 20:56:47 +00:00
glm-5.3-flash 857aedb987 feat(adapters): structural path-placeholder values are an INVALID_INPUT error (FWD-18)
The routing half of FWD-18 was already in place (placeholder keys skip
query routing before value-shape inspection); this pins it with a test
and decides the structural-value outcome: an object/array value under a
placeholder key now fails with INVALID_INPUT instead of splicing the
minified JSON into the path segment. Scalars (string/number/bool/null)
render as before.
2026-08-30 20:54:05 +00:00
glm-5.3-flash 3d932be75f feat(adapters): SSE non-JSON payloads carry data+event wrapper (FWD-17)
Option (b) of the FWD-17 decision: payloads decode as JSON when valid
(number stays number, quoted string stays string); non-JSON payloads
surface as {"data": <raw>, "event": <name|null>} instead of
silently degrading to a JSON string. The parser captures the frame's
event: field (WHATWG last-wins) and resets all pending state on every
blank-line dispatch, so an event:-only frame (no data) cannot leak its
name into a later frame. JSON frames surface as themselves even under
a named event; the implicit 'message' default never wraps a payload.

Verification: cargo test (33 forward tests incl. 4 new FWD-17
contract/parser pins), clippy, fmt.
2026-08-30 20:52:44 +00:00
glm-5.3-flash 5244dc46e2 chore(scripts): coordinator verify gate + mark gw15/oai13 completed 2026-08-30 20:45:35 +00:00
glm-5.3-flash f0c9bbafbd Merge branch 'wt/review-002-oai13-path-item-wildcards' 2026-08-30 20:36:33 +00:00
glm-5.3-flash a41d3b2caa Merge branch 'wt/review-002-gw15-publish-body-cap' 2026-08-30 20:35:33 +00:00
glm-5.3-flash c8c7bd0910 docs(adr-068): GW-15 body-limit layer + pre-extend cap in publish body handling 2026-08-30 20:29:35 +00:00
glm-5.3-flash 05ebbef43e fix(gateway): cap /publish body buffering + explicit body-limit layer (GW-15)
Review 002 GW-15 [major]: /publish lost both claimed memory bounds in
the GW-06 streaming rewrite. Unauthenticated POST /publish with chunked
'a'-forever (no newline) grew the heap with the upload until OOM, and
each poll re-scanned the whole buffer (O(n^2) on top).

- BufferedLines: cap the unterminated tail against
  MAX_PUBLISH_LINE_BYTES immediately after every chunk read (checked
  before yielding, even with no \n seen) and re-check on the
  trailing-EOF mem::take path; a breach aborts the whole reader
  (pending lines included) with the same terminal INVALID_INPUT
  LineCap error. Complete lines keep the baseline at-cap semantics.

- Whole gateway router: explicit request-body-limit layer
  (GATEWAY_BODY_LIMIT = 2 MiB + 64 KiB framing headroom). A raw-Body
  handler never consults axum's DefaultBodyLimit (that is an extension
  extractors read), so /publish had no whole-body cap at all. The layer
  pre-rejects oversized declared Content-Length and wraps chunked
  uploads in a counting stream; both answer plain-text 413. It
  deliberately sits above the per-line cap so a single over-cap line
  still surfaces the semantic line-cap error. Upstream body read
  failures are not flagged as limit-exceeded (disconnects are not 413).

- New wire tests: streamed never-newline over-cap rejections (both the
  streamed multi-chunk and trailing-EOF shapes), 413 on chunked
  over-limit uploads, 413 on oversized declared Content-Length, cap
  breach batched with complete lines, at-cap line still round-trips,
  declared-length over-limit pre-rejection.

Module status mapping note (GW-16 tracks the drift): hand-rolled
pre-dispatch rejections use 400/INVALID_INPUT while mid-stream chunk
errors map 422 through gateway::error; normalizing is GW-16, not GW-15.

Verification: cargo test 305+5 pass, cargo test --all-features pass,
cargo clippy --all-targets -- -D warnings pass (--all-features too),
cargo fmt --check pass.
2026-08-30 20:29:35 +00:00
glm-5.3-flash fdb07000e9 docs(adr-066): record 4XX/5XX wildcard projection mapping (OAI-13) 2026-08-30 20:20:42 +00:00
glm-5.3-flash a6c2af717d feat(adapters): path-item params merge, response wildcards, loud webhooks (OAI-13)
- path-item-level `parameters` parse into PathItem and merge into every
  operation's input schema; operation-level entries override shared
  name+in duplicates (last-insert wins)
- success sweep accepts `2XX` after concrete 2XX keys and before
  `default` (SSE detection + output schema; concrete outranks wildcard)
- `4XX`/`5XX` error keys project to their class representative status
  (`HTTP_400`/`HTTP_500`) instead of dropping silently; `default`
  still drops loudly (no implied range)
- top-level `webhooks` fails import naming the feature (inbound
  callbacks are outside the single-endpoint outbound adapter model)
- unbound-placeholder error names the parameter-merge state so the
  diagnosis no longer dead-ends

Verification: cargo test (174 lib tests), cargo fmt
2026-08-30 20:20:32 +00:00
glm-5.3-flash ef6eab020d chore(tasks): summaries + oai11 completion for wave-1 merges 2026-08-30 20:08:44 +00:00
glm-5.3-flash a71592e42c chore(tasks): mark review-002 wave-1 tasks completed (fwd15, ws13, con17, srv11/12) 2026-08-30 20:08:28 +00:00
glm-5.3-flash 85407f62c9 Merge branch 'wt/review-002-oai11-ref-memoization' 2026-08-30 20:05:30 +00:00
glm-5.3-flash 48ae5103e0 Merge branch 'wt/review-002-srv11-srv12-router-ordering' 2026-08-30 19:59:21 +00:00
glm-5.3-flash 76c9e7e178 Merge branch 'wt/review-002-con17-mcp-pagination' 2026-08-30 19:58:43 +00:00
glm-5.3-flash 84bf538c1d Merge branch 'wt/review-002-ws13-idle-progress' 2026-08-30 19:57:04 +00:00
glm-5.3-flash c7873ad5aa fix(adapters): memoize $ref expansion with node-budget accounting (OAI-11)
- memo-hit clones counted against MAX_REF_EXPANSION_NODES (closes the
  unbounded-clone hole that let acyclic diamond chains materialize
  exponentially before tripping the budget)
- split MAX_REF_HOP_DEPTH (64, chain length) from structural nesting
  depth (128, schema height) — legit 40-hop chains were tripping the
  old combined budget
- shared-chain acceptance test: bounded node-budget error inside 1s
  wall (debug-profile margin); single-chain test asserts full expansion
- tested: cargo test 308+5 pass, clippy -D warnings, fmt --check

Verified under systemd-run MemoryMax=2G scope (the previous
unbounded-clone bug OOM-killed the dev server twice during review-002).

Verification:
- cargo test: 308 lib + 5 integration, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
2026-08-30 19:56:44 +00:00
glm-5.3-flash 9de88f45d7 wip(adapters): fix node budget accounting + split hop depth (coordinator) 2026-08-30 19:54:50 +00:00
glm-5.3-flash 84bffae697 wip(adapters): OAI-11 finisher checkpoint 2 2026-08-30 19:34:20 +00:00
glm-5.3-flash 4bc9d4c236 wip(adapters): drop broken visitor assertion from chain test (crashed session residue) 2026-08-30 12:50:30 +00:00
glm-5.3-flash f52b32a68f wip(adapters): OAI-11 staging checkpoint (crashed session) 2026-08-30 12:43:40 +00:00
glm-5.3-flash b73804d9bc test(websocket): axum-path idle-progress mirror tests (WS-13)
Mirrors the WS-13 progress semantics onto the axum upgrade path with
three integration tests over the real HttpAdapter surface (also the
COV-11b dark-knob gate — with_ws_idle_timeout had no test caller):
the forever-dribble client is evicted with 1001 despite arriving
messages; a productive session round-trips calls across many windows
without eviction; the None knob never evicts a dribbling client.
2026-08-30 12:17:43 +00:00