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.
- enforce the same 100-operation cap the HTTP /batch endpoint enforces;
over-cap \x60calls\x60 reject with a structured INVALID_INPUT (retryable:
false, matching CallError::invalid_input) before any dispatch
- hoist MAX_BATCH_OPERATIONS to gateway/mod.rs and reuse it in routes,
to_openapi (removing a pre-existing duplicate literal), and to_mcp
- state the limit in the batch tool description and add maxItems to the
input schema (doc previously advertised no limit)
- GatewayDispatch gains a per-instance invoke_count spy accessor so the
over-cap test proves zero dispatches (process-global counters raced
under the parallel test runner)
- tests: over-cap -> INVALID_INPUT + invoke_count()==0; at-cap -> 100
results + invoke_count()==100
verification: scripts/verify.sh (352 passed) and scripts/verify.sh
--all-features (468 passed); cargo clippy --all-targets -D warnings and
cargo fmt --check clean
Coverage-confirmed dead code (every binary, zero hits):
- server/state.rs: drop FromRef<RouterState> impls for
Arc<OperationRegistry> and Arc<dyn IdentityProvider> — no route
extracts these types; the auth middleware receives the provider
directly via from_fn_with_state
- gateway/dispatch.rs: drop identity_provider() and resolve_bearer()
accessors; resolve_bearer's doc promised an auth hook the middleware
never calls (spec/code drift). Wire-or-delete resolved to delete:
bearer resolution lives in the middleware (SRV-11 single-resolve
ordering), the dispatch spine only needs the per-call
Option<Identity>. GatewayDispatch::new consequently takes the
registry alone (GatewayState loses its unused identity_provider
passthrough; dispatch.rs/to_mcp.rs tests simplified)
- websocket/upgrade.rs: drop FromRef<SessionState> for
Arc<OperationRegistry> — no router carries SessionState as its state
type; the inverse FromRef<Arc<OperationRegistry>> for SessionState
(custom upgrade routes, integration tests) remains
Verification: ./scripts/verify.sh (352 passed), ./scripts/verify.sh
--all-features (466 passed), clippy -D warnings, fmt --check.
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).