tasks(decomp): ADR-049 streaming handler — 8 atomic tasks + gitignore .worktrees/
Decompose the ADR-049 streaming handler work into 8 dependency-ordered tasks: - call/registry/streaming-handler-handlerkind (foundation: StreamingHandler, HandlerKind, ResponseStream, INVALID_OPERATION_TYPE, migrate all sites) - call/registry/invoke-streaming (OperationRegistry::invoke_streaming) - call/protocol/dispatch-streaming-branch (server-side op_type branch) - call/client/from-call-streaming-forwarding (Subscription → subscribe()) - http/gateway/invoke-streaming (GatewayDispatch::invoke_streaming) - http/server/subscribe-sse-streaming (/subscribe pipes BoxStream to SSE) - http/adapters/from-openapi-sse-streaming (SSE → StreamingHandler) - review-streaming-impl (phase review checkpoint) Validated with taskgraph: 86 tasks, no cycles. Also ignore .worktrees/ so agents' worktree workspaces don't leak into git status.
This commit is contained in:
130
tasks/http/gateway/invoke-streaming.md
Normal file
130
tasks/http/gateway/invoke-streaming.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
id: http/gateway/invoke-streaming
|
||||
name: Implement GatewayDispatch::invoke_streaming() returning BoxStream<ResponseEnvelope>
|
||||
status: pending
|
||||
depends_on: [call/registry/invoke-streaming]
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Add `GatewayDispatch::invoke_streaming()` — the streaming analogue of
|
||||
`invoke()`, returning a `BoxStream<ResponseEnvelope>`. The security invariants
|
||||
are identical to `invoke()`: `internal: false`, `forwarded_for: None`, same
|
||||
capabilities, same `scoped_env`, same ACL check before dispatch. The two methods
|
||||
diverge only on the return shape (stream vs single envelope). The HTTP
|
||||
`/subscribe` handler calls this and pipes the stream to SSE.
|
||||
|
||||
This task depends on `call/registry/invoke-streaming` (which provides
|
||||
`OperationRegistry::invoke_streaming()`). It adds the `GatewayDispatch` method
|
||||
only — the `/subscribe` SSE wiring is `http/server/subscribe-sse-streaming`.
|
||||
|
||||
### invoke_streaming()
|
||||
|
||||
```rust
|
||||
use futures::stream::BoxStream;
|
||||
|
||||
impl GatewayDispatch {
|
||||
/// Invoke a Subscription operation as a wire-ingress caller. The streaming
|
||||
/// analogue of `invoke()`. Security invariants identical to `invoke()`:
|
||||
/// `internal: false`, `forwarded_for: None`, same capabilities, same
|
||||
/// scoped_env, same ACL. Diverges only on return shape (stream vs single
|
||||
/// envelope). Returns a `BoxStream<ResponseEnvelope>`; the `/subscribe`
|
||||
/// handler pipes it to SSE.
|
||||
pub async fn invoke_streaming(
|
||||
&self,
|
||||
identity: Option<Identity>,
|
||||
op: &str,
|
||||
input: Value,
|
||||
) -> BoxStream<'static, ResponseEnvelope> {
|
||||
let operation_name = strip_leading_slash(op).to_string();
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
let context = self.build_root_context(&request_id, &operation_name, identity);
|
||||
// The registry's invoke_streaming returns ResponseStream
|
||||
// (Pin<Box<dyn Stream<Item = ResponseEnvelope> + Send>>), which IS
|
||||
// BoxStream<'static, ResponseEnvelope>. Box::pin / BoxStream are the
|
||||
// same type — the alias just spells it out.
|
||||
self.registry.invoke_streaming(&operation_name, input, context)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`build_root_context` is reused unchanged — it constructs the root
|
||||
`OperationContext` with `internal: false`, `forwarded_for: None`, fresh
|
||||
`request_id`, deadline, registration bundle's `composition_authority` /
|
||||
`capabilities` / `scoped_env`. The security axis is provably identical between
|
||||
`invoke()` and `invoke_streaming()` because they share `build_root_context`.
|
||||
|
||||
### deadline: None for streaming
|
||||
|
||||
The spec says `deadline: None` for subscriptions (unbounded). `build_root_context`
|
||||
sets `deadline: Some(now + 30s)`. For the streaming path, set
|
||||
`context.deadline = None` after `build_root_context`, OR add a streaming flag to
|
||||
`build_root_context`. Coordinate with `call/protocol/dispatch-streaming-branch`
|
||||
which has the same concern — extract a shared approach (e.g., a
|
||||
`build_root_context_streaming` variant or a `deadline: None` override). The
|
||||
gateway and the call dispatcher both need `deadline: None` for subscriptions;
|
||||
don't duplicate the logic.
|
||||
|
||||
### What this task does NOT do
|
||||
|
||||
- **No `/subscribe` SSE wiring.** That's `http/server/subscribe-sse-streaming`.
|
||||
- **No `OperationRegistry` changes.** `invoke_streaming()` is provided by
|
||||
`call/registry/invoke-streaming`.
|
||||
- **No `to_mcp` changes.** MCP excludes `Subscription` (ADR-041); `to_mcp` never
|
||||
calls `invoke_streaming()`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `GatewayDispatch::invoke_streaming()` method exists
|
||||
- [ ] Returns `BoxStream<'static, ResponseEnvelope>` (or `ResponseStream` —
|
||||
same type)
|
||||
- [ ] Builds root `OperationContext` via `build_root_context` (same as `invoke()`)
|
||||
- [ ] Root context has `internal: false`, `forwarded_for: None`, fresh
|
||||
`request_id`
|
||||
- [ ] `handler_identity`, `capabilities`, `scoped_env` from registration bundle
|
||||
(same as `invoke()`)
|
||||
- [ ] `deadline: None` for the streaming path (unbounded subscriptions)
|
||||
- [ ] Calls `OperationRegistry::invoke_streaming()`
|
||||
- [ ] Security invariants identical to `invoke()` (shared `build_root_context`)
|
||||
- [ ] Unit test: `invoke_streaming()` on a registered `Subscription` op returns
|
||||
the handler's stream
|
||||
- [ ] Unit test: `invoke_streaming()` on unknown op returns a stream yielding
|
||||
one `NOT_FOUND`
|
||||
- [ ] Unit test: `invoke_streaming()` on Internal op from external returns
|
||||
stream yielding one `NOT_FOUND` (not leaked)
|
||||
- [ ] Unit test: `invoke_streaming()` with `None` identity + restricted op
|
||||
returns stream yielding one `FORBIDDEN`
|
||||
- [ ] Unit test: `invoke_streaming()` on a `Query` op returns stream yielding
|
||||
one `INVALID_OPERATION_TYPE`
|
||||
- [ ] Unit test: `invoke()` (existing) on a `Subscription` op returns
|
||||
`INVALID_OPERATION_TYPE` (verifies the guard from
|
||||
`streaming-handler-handlerkind` holds through the gateway)
|
||||
- [ ] `cargo test -p alknet-http` succeeds
|
||||
- [ ] `cargo clippy -p alknet-http --all-targets` succeeds with no warnings
|
||||
- [ ] `cargo fmt --check -p alknet-http` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/049-streaming-handler-for-subscriptions.md — ADR-049 §7 (GatewayDispatch::invoke_streaming)
|
||||
- docs/architecture/crates/http/http-server.md — §Streaming projection (invoke_streaming security invariants identical to invoke)
|
||||
- docs/architecture/crates/call/operation-registry.md — §OperationRegistry (invoke_streaming)
|
||||
|
||||
## Notes
|
||||
|
||||
> The security axis MUST be provably identical between `invoke()` and
|
||||
> `invoke_streaming()` — they share `build_root_context`. The two methods
|
||||
> diverge only on the return shape. `deadline: None` for subscriptions is a
|
||||
> shared concern with `call/protocol/dispatch-streaming-branch` — extract a
|
||||
> shared approach (a streaming-variant of `build_root_context` or a
|
||||
> `deadline: None` override) rather than duplicating. `to_mcp` never calls
|
||||
> `invoke_streaming()` (MCP excludes `Subscription` — ADR-041); do not add
|
||||
> streaming to the MCP gateway. The `futures` crate is already a dependency of
|
||||
> `alknet-http`.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user