The BidiStreamSource trait (ADR-070) made Connection hold Box<dyn
BidiStreamSource> so downstream crates can add connection shapes without
editing core — but the public constructor for that path was missing.
The source field is private; the channels crate (or any future crate)
had no way to build a Connection from its own BidiStreamSource impl.
The trait was unusable from outside core.
Add Connection::from_source(source: impl BidiStreamSource, alpn: Vec<u8>)
-> Self: the extension point. Takes impl BidiStreamSource (not Box<dyn>)
to match the ergonomic style of from_stream / from_bidi; boxes the
source internally. No feature gate (always available, like from_stream).
Initializes alpn and identity: OnceLock::new() the same as the other
constructors.
Test: a custom RecordingSource BidiStreamSource impl (not a built-in)
constructed via from_source, verifying remote_alpn / remote_addr /
accept_bi (round-trips real bytes via tokio::io::duplex) / open_bi
(StreamClosed) / close (records code+reason) all delegate to the custom
impl.
Verified: cargo test + clippy clean across default, --no-default-features,
--features iroh, --all-features; cargo check -p alknet-call succeeds
unchanged; cargo fmt --check passes.
Extract stream-yield ops from Connection into a BidiStreamSource trait;
Connection now holds Box<dyn BidiStreamSource> instead of the closed
ConnectionKind enum. Three crate-private impls wrap the existing
constructors: QuinnBidiStreamSource (feature quinn), IrohBidiStreamSource
(feature iroh), StreamBidiStreamSource (no gate, yield-once per ADR-065).
Public Connection API is preserved verbatim — handlers dispatch through
the trait object transparently.
The StreamBidiStreamSource::close impl prefixes code/reason with _ and
documents why they're ignored (the drop is the close — ADR-065). This
resolves the ADR-065 leftover clippy warning under --no-default-features
(REQ-CORE-02).
Add AuthContext::anonymous(alpn) convenience constructor (REQ-CORE-03):
sets identity/fingerprint/remote_addr to None, only alpn is populated.
Removes the four-None-field literal that recurred in handler POCs/tests.
Verified: cargo test + clippy clean across default, --no-default-features,
--features iroh, and --all-features; cargo check -p alknet-call succeeds
unchanged; cargo fmt --check passes.
The from_mcp_integration test invoked bundle.handler directly as a
function, but HandlerRegistration.handler is a HandlerKind enum
(Once/Stream), not a callable. This caused E0618 (expected function,
found HandlerKind) and a downstream E0282 on the response match arm.
Pattern-match HandlerKind::Once to extract the inner Handler before
calling, mirroring the pattern used in protocol/connection.rs and
from_openapi.rs tests.
Add ConnectionKind::Stream: a single read/write pair behind a
Mutex<Option<...>> that accept_bi yields once (then ConnectionClosed).
Add Connection::from_stream(send, recv, alpn, remote_addr) and
Connection::from_bidi(stream, alpn, remote_addr) constructors.
Rename the stream-level SendStreamKind::Mock / RecvStreamKind::Mock to
::Stream (they were already generic Box<dyn AsyncRead/Write> — the
wrong name). Rename from_mock to from_stream.
Remove MockConnection trait + ConnectionKind::Mock entirely. Migrate
all alknet-call test StubConnections to use from_stream with
tokio::io::sink() + tokio::io::empty() (immediate EOF on the read side
causes handle_stream to exit cleanly, then accept_bi returns
ConnectionClosed and the run_loop exits).
The yield-once accept_bi contract: QUIC yields many streams, everything
else yields one. Handlers that loop (TtyAdapter) get one iteration per
single-stream connection; handlers that call once (HttpAdapter) get the
stream directly. Both correct, no branching on transport.
This unblocks TCP+TLS listeners, SSH channel dispatch, WebTransport
streams, and wasm — all via from_stream, all through the same
HandlerRegistry, zero handler code changes.
See docs/research/transport-generalization/findings.md for the full
trace and the yield-once contract.
irpc 0.16 was declared in the workspace Cargo.toml and consumed by
alknet-call, but no .rs file in the workspace imports it. alknet-call's
wire protocol (src/protocol/wire.rs) is hand-rolled length-prefixed JSON.
The dep was dead weight.
Re-add as 0.17 when alknet-blobs lands (it depends on iroh-blobs 0.103
which pulls irpc 0.17 transitively).
Add 18 integration tests in crates/alknet-tty-local/tests/ exercising
the full stack (LocalTtyBackend + TtyAdapter::drive_session over
tokio::io::duplex, real commands), validating the two crates work
together through the TtyBackend trait seam.
Tests live in alknet-tty-local (not alknet-tty) per the feature-gate
deviation: the cyclic dependency alknet-tty → alknet-tty-local →
alknet-tty prevents alknet-tty from depending on alknet-tty-local, so
the integration tests live in the crate that depends on both. Unix-only
tests (signal, process-group) use #[cfg(unix)].
PTY mode (8): happy path echo, interactive cat round-trip, resize,
SIGINT signal, process-group signal (REQ-TTY-02), stdin EOF sentinel,
cancel cleanup (ADR-056), exit-chunk-is-last (ADR-055).
Pipe mode (6): happy path echo, separate stderr, SIGTERM signal,
cancel cleanup, resize no-op, stdout chunk + sentinel.
Negotiation errors (5): unknown_backend, malformed_negotiation (bad
JSON, carriage != raw, empty cmd), allocate_failed (nonexistent
binary).
A shared test harness (tests/common/mod.rs) provides the client-side
wire protocol helpers: write_negotiation, write_chunk, write_control,
try_read_chunk, read_chunk_timeout, read_error_frame (asserts the
0x00 first-byte framing-disambiguation invariant), read_until_exit,
assert_no_more_chunks, close_write_half, plus spawn_session which
wires drive_session over a duplex pair with a test identity carrying
tty:open.
Verification: cargo test -p alknet-tty-local (42 tests pass), cargo
clippy -p alknet-tty-local -- -D warnings (clean), cargo fmt --check
-p alknet-tty-local (clean).
ADR-054 prescribes with a re-export
module, but cargo rejects the cyclic dependency (alknet-tty →
alknet-tty-local → alknet-tty) even with an optional dep. The re-export
is deferred to the assembly layer: consumers depend on alknet-tty-local
directly and register LocalTtyBackend in the TtyAdapter backend map.
The feature gate stays declared (empty) for forward compatibility.
Assembly pattern documented in the crate root doc comment.
Implements the wiring task: LocalTtyBackend::allocate() dispatches to
pty::allocate_pty when TtyParams.terminal is Some (PTY mode, stderr None)
and pipe::allocate_pipe when None (runner mode, stderr Some). Validates
non-empty cmd, ignores backend_params (local backend has no backend-
specific selector fields), and resource_id() returns None (local backend
creates its own resource — process). Adds async-trait dependency.
Implement TtyAdapter (ProtocolHandler on alknet/tty) and the
drive_session three-pump bidirectional driver in adapter.rs.
TtyAdapter holds a HashMap<String, Arc<dyn TtyBackend>> keyed by the
negotiation frame's backend string and an optional OwnershipProvider.
handle() loops accept_bi and spawns drive_session per stream.
drive_session proceeds in three phases:
1. Negotiation — read length-prefixed JSON, parse NegotiateRequest,
validate (carriage == raw, cmd non-empty), look up backend, scope-gate
(tty:open), resource ownership check via backend.resource_id() +
OwnershipProvider::owns(), construct TtyParams. Errors sent as JSON
in negotiation framing, stream closed.
2. Allocation — backend.allocate(¶ms). On failure send
allocate_failed error, close.
3. Raw carriage — three concurrent pumps:
A. stdout/stderr → client (stream_type 1/2), zero-length sentinel on EOF
B. client → backend: stdin chunks → TtyHandle.stdin, control chunks →
Resize/Signal/Eof dispatch (Exit ignored, unknown types ignored)
C. exit → exit chunk: await exit_code; on resolve enqueue exit chunk
Exit-chunk-is-last invariant (ADR-055): adapter waits for BOTH
stdout/stderr pumps to complete AND exit_code to resolve before
enqueueing the exit chunk (tokio::join!). On TtyError → code -1.
Cancel cleanup (ADR-056): on connection drop/stream reset, pump tasks
drop, TtyHandle drops, exit_code future drops without resolve →
backend kill-on-Drop fires. Client write-half close is NOT a cancel —
session runs to completion.
Access control (ADR-050): scope gate at negotiation (tty:open scope,
configurable constant), resource ownership via backend.resource_id() +
OwnershipProvider::owns() when wired.
18 integration tests with a TestBackend fixture over tokio::io::duplex:
happy path, exit-chunk-is-last, stdin EOF, resize/signal control,
unknown control ignored, exit control ignored, unknown_backend,
malformed_negotiation (bad JSON, carriage != raw, empty cmd),
allocate_failed, exit error (-1), cancel cleanup, scope gate forbidden,
ownership check deny/allow, stderr pump, eof control closes stdin.
Also fix two clippy unused_mut warnings in negotiation.rs tests.
Implements the runner case (terminal: None, no PTY) in pipe.rs:
- allocate_pipe spawns via tokio::process::Command with Stdio::piped(),
returning a TtyHandle with separate stdout/stderr (stderr=Some).
- stdin: ChildStdin boxed as Box<dyn AsyncWrite + Send + Unpin>.
- stdout/stderr: ChildStdout/ChildStderr wrapped via tokio_util::io::ReaderStream
into Pin<Box<dyn Stream<Item = Bytes> + Send>> (errors map to stream EOF).
- exit_code: PipeExitFuture holding the Child; poll drives Child::wait() and
disarms the guard on resolve; Drop calls Child::start_kill() on cancel
(ADR-056 spec-compliant kill-on-Drop, not kill_on_drop(true) alone).
- control: PipeControl (no-op resize, libc::kill(pid, sig) signal on Unix,
SIGKILL fallback for unknown names; non-Unix logs and relies on Drop guard).
Doc comment documents the no-process-group limitation of pipe mode.
Adds tokio-util (io) and tokio-stream (dev) deps. 9 unit tests cover the
happy path, stdin round-trip, separate stderr, SIGTERM signal, cancel
cleanup (asserts child killed via kill(pid,0)), resize no-op, unknown
signal SIGKILL fallback, pid recording, and empty-command error.
Implement allocate_pty in src/pty.rs: portable_pty openpty + spawn as
session leader (set_controlling_tty(true)), with the three-thread
blocking→async bridge (REQ-TTY-01): reader → mpsc<Bytes> with
zero-length EOF sentinel, writer draining mpsc<StdinCmd> (Bytes/Eof),
waiter → oneshot<i32>. TtyHandle.stdout is ReceiverStream<Bytes>,
stdin is a StdinSink AsyncWrite wrapper over mpsc::Sender<StdinCmd>
(parks a reserve+send future when the channel is full), stderr is None
(PTY merges), exit_code is LocalExitFuture.
PtyControl implements TtyControl: resize via MasterPty::resize; signal
via libc::kill(-pgid, sig) with kill(pid, sig) fallback (Unix, REQ-TTY-02)
using alknet_tty::signal_from_name; unknown names and non-Unix fall back
to ChildKiller::kill (SIGHUP).
LocalExitFuture wraps the oneshot::Receiver<i32> + a ChildKiller kill
guard (ADR-056). poll delegates to the receiver and disarms the guard
on Ready (no-op Drop on happy path); Drop on cancel calls
ChildKiller::kill (SIGHUP) — best-effort; the waiter thread reaps.
Bump portable-pty 0.8 → 0.9 to match the POC API (MasterPty: Send).
Add tokio-stream, serde_json (dev), tempfile (dev) deps.
Tests: happy path (echo), stdin round-trip (cat), resize, signal INT
kills child, signal reaches process group (bash -c "sleep 60"),
cancel cleanup on drop, unknown signal falls back to ChildKiller.
Replace placeholder negotiation types with the full Phase 1 wire shape:
- NegotiateRequest/TerminalParamsWire with serde(flatten) capturing
backend-specific fields into backend_params
- NegotiationReader/Writer: self-contained 4-byte BE length-prefixed
framing on tokio AsyncRead/AsyncWrite, bounds-checked against MAX_CHUNK_LEN
- NegotiationError (Io, ConnectionClosed, FrameTooLarge, Json) via thiserror
- error_response_bytes helper for server-side JSON error frames
- into_inner reclaims the stream for raw-chunk use
- Error frames stay under 16 MiB so the high byte of the length prefix is
0x00, making the framing-disambiguation trick sound (ADR-052 §5)
Unit tests cover round-trip, serde(flatten) capture, FrameTooLarge,
ConnectionClosed on truncated header/body, error-response shape, and the
0x00-first-byte framing-disambiguation invariant. backend.rs's
From<NegotiateRequest> for TtyParams still compiles (same field shapes).
Create crates/alknet-tty-local with Cargo.toml (alknet-tty workspace dep,
portable-pty, libc unix-only, tokio, bytes, futures-core, tracing,
thiserror), src/lib.rs with module declarations and LocalTtyBackend
re-export, and skeleton module files (pty, pipe, backend) with doc
comments and TODO markers. Add the crate to the workspace members list.
backend.rs defines its own BoxFuture type alias
(Pin<Box<dyn Future + Send>>) using std::future::Future + futures_core,
so the futures crate is unused. Removes the dead dependency.
Port the POC's control channel schema (alknet-tty-poc/src/control.rs)
into crates/alknet-tty/src/control.rs: the ControlMessage tagged enum
(Resize/Signal/Eof/Exit) with snake_case type tag, to_json/from_slice
helpers, and the Unix-only signal_from_name mapping the 9 supported
signal names to libc numbers. Unknown type tags return a serde_json
error; the adapter (later task) ignores that error per the wire spec's
extensibility policy. Adds libc as a target.'cfg(unix)' dependency.
Refs: docs/architecture/crates/tty/tty-wire.md §"Control Channel"
Refs: docs/architecture/decisions/055-exit-code-on-control-chunk.md
Create crates/alknet-tty with Cargo.toml (alknet-core path dep, tokio,
bytes, futures-core, tokio-stream, serde, serde_json, async-trait,
tracing, thiserror — no portable_pty/bollard/russh/alknet-call per
ADR-057), the `local` feature gate (dep wiring deferred to
tty/local-feature-reexport), src/lib.rs with the crate doc comment and
module declarations, and skeleton modules (wire, control, negotiation,
backend, adapter) with doc comments and TODO markers. Add the crate to
the workspace members list.
cargo check, clippy -D warnings, and fmt --check all pass.
Implementation verified yaml_serde 0.10.4 (and serde_yaml 0.9) implement the
YAML 1.2 core schema, where bare yes/no/on/off are strings, not booleans. The
original §2 rationale cited YAML 1.1 boolean-coercion as a present hazard
justifying JSON-first detection; it does not exist with this dependency
version.
The JSON-first decision is retained (Accepted ADR) — reframed as a defensive
default that locks the contract against a future YAML-parser swap, not a
guard against a present hazard. §2 heading, Consequences, and Assumption #2
amended; an in-place amendment note records what changed and when. The two
coercion-related tests already document yaml_serde 0.10.x's actual behavior
with explicit comments. Source doc-comments on from_yaml/from_str corrected
to stop propagating the flawed assumption. 256 tests pass; clippy clean.
Adds OpenAPISpec::from_yaml and OpenAPISpec::from_str alongside the
existing from_json. from_str is JSON-first, YAML-fallback (ADR-051 §2):
a valid JSON doc parses through serde_json (stricter grammar, immune to
YAML-specific coercion); only on JSON failure does it fall back to the
YAML parser. yaml_serde 0.10 (resolves to 0.10.4, latest stable) is the
maintained official-YAML-org fork of the deprecated serde_yaml; not
feature-gated per ADR-051 §3 (YAML OpenAPI is first-class).
Note: ADR-051 §2's stated YAML 1.1 boolean-coercion rationale does not
hold for yaml_serde 0.10 (YAML 1.2 core schema — yes/no/on/off are
strings). The JSON-first rule is retained per the Accepted ADR as a
defensive default; tests document the actual 0.10.x behavior with
explicit comments. The ADR rationale should be amended separately.
256 tests pass; clippy + fmt clean.
Implements the 4-task DAG for runtime-spawned resource ownership so
AccessControl::check can answer "does this identity own this specific
container/TTY/process" instead of relying only on static
Identity.resources grants.
1. core/ownership-store-trait: OwnershipProvider (sync read) +
OwnershipStore (async write) traits + InMemoryOwnershipStore +
OwnershipError in alknet-core. Fourth instance of the repo/adapter
pattern (ADR-033), mirroring CredentialStore/store.rs.
2. call/registry/operation-spec-resource-id-path: resource_id_path
field on OperationSpec — JSON pointer into input for resource ID
extraction. Single field addition, all ~40 construction sites across
alknet-call + alknet-http updated to pass None (no semantic change).
3. call/registry/access-control-ownership-check: check() signature
gains resource_id + Option<&dyn OwnershipProvider>. 3-case decision
tree: ownership Some + resource_id Some -> owns(); ownership Some +
resource_id None -> owns_any() (list scope-gate); ownership None ->
static Identity.resources fallback (backward compat). 7 call sites
updated to (None, None) — including a 7th in alknet-http/gateway_routes
not listed in the original spec.
4. call/registry/dispatch-resource-id-extraction: wire the dispatch
path. OperationContext gains ownership field; Dispatcher/CallAdapter
gain with_ownership_provider builders; invoke/invoke_streaming/
invoke_with_policy extract resource_id via spec.resource_id_path
and thread context.ownership to check(). extract_json_pointer helper
handles $.field syntax (graceful None on missing/non-string). 20
OperationContext literals updated across both crates.
Backward compatibility is load-bearing throughout: ownership=None
falls back to the existing static resource-check path. Deployments
without runtime-spawned resources wire nothing and behave identically.
821 tests pass workspace-wide (was ~770); clippy clean; fmt clean.
Task specs marked done.
Replace the one-event placeholder (subscribe_stream_from_envelope +
envelope_to_sse_stream, which called invoke() and wrapped the single
ResponseEnvelope) with the real streaming path: subscribe_handler now
calls GatewayDispatch::invoke_streaming() and pipes the
BoxStream<ResponseEnvelope> to SSE via subscribe_stream_from_envelope_stream
(futures::StreamExt::map). Each Ok(output) becomes a data: frame; each
Err becomes an event:error frame (terminal — stream ends after it);
natural stream end closes the SSE. Internal ops still return a single
NOT_FOUND error event via subscribe_stream_internal_error (kept). Client
disconnect drops the stream via Rust's Drop (abort cascade per ADR-016).
Add DispatchResult::Once|Stream enum and Dispatcher::dispatch() that branches
on the registered op_type (ADR-049 §6): Query/Mutation → invoke() (unchanged
Once path), Subscription → invoke_streaming() (Stream path). handle_stream
matches on DispatchResult: the Once path writes one call.responded/call.error
frame (no call.completed); the Stream path pumps each ResponseEnvelope to the
wire via pump_stream (ResponseEnvelope.into() → call.responded for Ok,
call.error for Err), then writes call.completed on natural stream end. An Err
envelope is terminal — last_was_error suppresses call.completed after an error.
The streaming branch clears context.deadline to None (subscriptions are
unbounded — ADR-049 §6, call-protocol Timeouts). Abort (ADR-016) needs no new
code: handle_abort removes the pending entry and dropping the pump task
releases handler resources via Drop. dispatch_requested delegates to dispatch
for backward compatibility with existing callers.
Add the streaming analogue of invoke() returning BoxStream<ResponseEnvelope>.
Security invariants are identical to invoke() (internal: false,
forwarded_for: None, same capabilities/scoped_env/ACL) — shared via a
build_root_context_inner helper with a bounded flag. The streaming path
sets deadline: None (unbounded subscriptions, ADR-049 §6). Calls
OperationRegistry::invoke_streaming() (already on develop). to_mcp is
unchanged (MCP excludes Subscription, ADR-041).
Tests cover: subscription dispatch, leading-slash strip, unknown op
NOT_FOUND, internal op NOT_FOUND (not leaked), None identity FORBIDDEN,
Query op INVALID_OPERATION_TYPE, invoke() on Subscription returns
INVALID_OPERATION_TYPE (guard holds through gateway), and
build_root_context_streaming sets deadline: None while carrying the
registration bundle.
build_registration now branches on op_type: Subscription ops register a
StreamingHandler (HandlerKind::Stream) via make_streaming_handler that
streams SSE response chunks as ResponseEnvelope::ok() items (one per
data: frame); Query/Mutation ops keep the existing Handler
(HandlerKind::Once) via forward(). Closes the gap where a from_openapi-
imported Subscription returned only the last SSE event.
- forward_stream(): non-async fn returning ResponseStream; sends the
request with Accept: text/event-stream, then streams SSE chunks via
stream::unfold over response.bytes_stream(), reusing parse_sse_frames
(multi-event, partial trailing, comments, multi-line data, BOM).
HTTP error (non-2xx) → single ResponseEnvelope::error(), stream ends;
SSE stream end → ResponseStream ends (→ call.completed on wire).
- Removed stream_subscription() (the collect-all placeholder that
truncated to the last event). parse_sse_frames stays (reused).
- Query/Mutation forwarding unchanged (existing forward() path).
- Tests: Subscription registration is HandlerKind::Stream; Query
registration is HandlerKind::Once; SSE subscription streams multiple
ResponseEnvelope::ok() (one per data: frame); HTTP error → single
error envelope; Query forwarding unchanged (single response).
Subscription ops discovered via services/list + services/schema now
register a StreamingHandler (HandlerKind::Stream) that calls
CallConnection::subscribe_with_payload and forwards the remote stream
end-to-end (ADR-049 §8). Query/Mutation ops keep the existing
make_forwarding_handler (HandlerKind::Once).
- Add CallConnection::subscribe_with_payload(payload) mirroring
call_with_payload so the forwarding handler can populate forwarded_for
(ADR-032) + auth_token on the subscription payload. subscribe() now
delegates to subscribe_with_payload.
- Add make_streaming_forwarding_handler() in from_call.rs using
make_streaming_handler + futures::stream::once(...).flatten() to await
subscribe_with_payload then forward its stream.
- Branch build_bundles on spec.op_type (already parsed by rebuild_spec_for).
- Reuse build_forwarded_payload — no new payload-construction code.
- composition_authority: None, scoped_env: None for FromCall streaming
leaves (same as Query/Mutation FromCall leaves).
- Abort cascade (ADR-016 §6) already wired via PendingRequestMap in
subscribe_with_payload.
Closes the gap where a from_call-imported Subscription truncated to the
first value.
Streaming dispatch path for Subscription operations — counterpart to
invoke(). Same visibility + ACL checks (internal → handler_identity,
external → identity), then dispatches to the StreamingHandler. Pre-handler
errors (not-found, forbidden, INVALID_OPERATION_TYPE for non-Subscription
ops) yield a single error ResponseEnvelope via stream::once and end the
stream. Adds 6 unit tests covering dispatch, not-found, wrong-kind,
internal-from-external, ACL denied, and internal-call handler_identity ACL.
Refs ADR-049 §3, §5.
Add the foundational types for ADR-049 streaming handlers:
- StreamingHandler, ResponseStream type aliases and HandlerKind enum
(Once | Stream) in registration.rs, with make_streaming_handler() helper
- CallError::invalid_operation_type() in wire.rs (sixth protocol code,
retryable: false)
- HandlerRegistration.handler flipped from Handler to HandlerKind;
HandlerRegistration::new() now takes HandlerKind
- OperationRegistryBuilder absorbs wrapping: with_local/with_leaf/
with_leaf_provenance wrap raw Handler in HandlerKind::Once for
Query/Mutation; new with_local_streaming/with_leaf_streaming take a
StreamingHandler and wrap in HandlerKind::Stream for Subscription.
Builder validates kind matches spec.op_type (mismatch = startup error)
- OperationRegistry::register() returns Result<(), String> with a clear
mismatch message; all call sites updated to handle the Result
- invoke() matches on HandlerKind: Once -> existing path; Stream ->
INVALID_OPERATION_TYPE error envelope (guards against silent
truncation; invoke_streaming() added in a downstream task)
- OverlayOperationEnv::invoke_with_policy matches on HandlerKind:
Once -> dispatch; Stream -> INVALID_OPERATION_TYPE (composition is
request/response-only)
- Migrated every HandlerRegistration::new() construction site (~95)
to wrap raw Handler in HandlerKind::Once(handler); the builder sites
are handled by the builder-absorbs-wrapping change
- Updated two websocket subscription tests that relied on Subscription
ops dispatching via invoke() to expect INVALID_OPERATION_TYPE
- Added unit tests for invoke/register validation and
make_streaming_handler
Refine to_openapi to project operation-level errors (with http_status)
onto /call and /subscribe responses via oneOf merge with protocol-level
errors, preserving HTTP_<status> prefix codes without collision. Fix
BTreeMap→serde_json::Map for Value::Object compatibility. Wire GET
/openapi.json route test. Apply cargo fmt across the crate.
Adds AccessControl::check to OverlayOperationEnv::invoke_with_policy in alknet-call
so hub's calls to browser-registered ops are gated by the browser's AccessControl.
Creates src/websocket/overlay.rs with 19 integration tests: overlay scoping (not
PeerCompositeEnv), no PeerId, register_imported/all, overlay_env() routing,
PeerRef::Specific→NOT_FOUND, AccessControl gating, overlay drop on WS close,
ADR-016 abort cascade, bidirectionality, no-ops use-case scoping.
Enforce AccessControl on overlay ops in OverlayOperationEnv::invoke_with_policy
(alknet-call) so the hub's calls to browser-registered ops are gated by the
browser's AccessControl — matching OperationRegistry::invoke semantics for
internal composition (caller identity = parent handler_identity.as_identity()).
Add src/websocket/overlay.rs with 19 integration tests covering the connection-
local overlay acceptance criteria: browser ops land in the per-CallConnection
overlay (not PeerCompositeEnv), no PeerId for the browser, register_imported()/
register_imported_all() populate the overlay, hub outgoing calls route through
overlay_env() (not PeerRef::Specific), PeerRef::Specific('browser-X') routes to
NOT_FOUND, AccessControl gates hub calls (allowed/forbidden/default), overlay is
per-connection isolated and dropped on WS close, WS close aborts in-flight calls
with ADR-016 cascade, bidirectionality, and browser-with-no-ops use-case scoping.
Implements src/server/gateway_routes.rs: POST /call, GET /search, GET /schema,
POST /batch, POST /subscribe (SSE). All delegate to GatewayDispatch::invoke; auth
via ResolvedIdentity extractor; errors mapped via call_error_to_http_response
(identity-aware 401/403 split). Internal ops → 404. /schema adds ACL pre-check.
/subscribe projects ResponseEnvelope as SSE. /batch loops over invoke. Wired real
handlers into adapter.rs replacing placeholder 501s. 157 tests pass.
Note: /subscribe SSE completes after single event (registry invoke returns single
ResponseEnvelope, no streaming handler yet — research §6 OQ#5).
# Conflicts:
# crates/alknet-http/src/server/adapter.rs