diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 14c7041..a9f91fd 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -20,8 +20,10 @@ with their resolutions; new alkhttp OQs start at OQ-01. 8-byte header + payload); the mux writes chunks as contiguous byte sequences. A WebSocket is message-oriented. The adapter's contract needs nailing down before implementation: - (a) inbound buffer bound (bounded channel between the WS read task - and the `AsyncRead` half — what bound, what policy on overflow); + (a) inbound buffer bound (the alknet-tty precedent — bounded mpsc + with `try_send` → `Full` → `Pending` backpressure, and the single- + drainer ordered-write pattern from `pump_session` — is the working + reference; bound value to lock during implementation); (b) write-side chunk boundary parsing (verified against alkcall source: the mux emits one mpsc payload per chunk, but a logical write above the mux — e.g. channel 0's `write_frame`, which issues diff --git a/docs/plans/implementation.md b/docs/plans/implementation.md index 7088ae0..8b1094f 100644 --- a/docs/plans/implementation.md +++ b/docs/plans/implementation.md @@ -39,10 +39,27 @@ unknowns that would have shaped tasks incorrectly: EOF → all channels cleared. The adapter maps WS close to transport EOF and lets alkcall's invariants do the rest. 5. **`Dispatcher::run_loop_single_stream` exists** and is the channel-0 - dispatch loop — no new dispatch code is needed anywhere in alkhttp. + dispatch loop — no dispatch code is needed anywhere in alkhttp. +6. **The alknet-tty crates provide the precedents for the adapter.** + The channels protocol was abstracted from TTY's earlier 5-byte + demux work, and `alknet-tty`'s adapter embodies the two patterns + OQ-01 needed: + - **The drainer pattern** (`pump_session`): all producer pumps feed + one bounded mpsc; a single drainer writes chunks in arrival + order. The transport write side is therefore sequential — a + boundary-parsing state machine in the WS adapter's write path is + sound (no interleaving to handle). + - **Bounded backpressure** (`TestStdinSink::poll_write`): + `try_send` → `Full` → `Poll::Pending` — the inbound buffer + pattern for the WS→bytes direction. This de-risks the two "high" tasks (WS adapter, WS session) from -"unknown design" to "known shape, careful implementation." +"unknown design" to "known shape, careful implementation." A targeted +POC (research task) still validates the full loop — axum WS ↔ adapter +↔ alkcall ChannelsAdapter + channel-0 dispatcher over duplex — before +the production implementation builds on it, since axum's WS API +specifics (message sizing, backpressure interplay) are the one part +neither the specs nor the TTY precedent exercise. ## Build order (dependency spine) diff --git a/tasks/adapters/from-jsonschema.md b/tasks/adapters/from-jsonschema.md new file mode 100644 index 0000000..6ad2b81 --- /dev/null +++ b/tasks/adapters/from-jsonschema.md @@ -0,0 +1,42 @@ +--- +id: adapter-from-jsonschema +name: from_jsonschema single-endpoint adapter +status: pending +depends_on: [client-http-host] +scope: narrow +risk: low +impact: component +level: implementation +tags: [adapters, phase-3] +--- + +## Description + +Port `adapters/from_jsonschema.rs` from +`/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/from_jsonschema.rs` +per ADR-066: caller supplies OperationSpec + HttpServiceConfig + path +template + method; one HandlerRegistration with a reqwest forwarding +handler (same shape as from_openapi's), FromJsonSchema provenance +(leaf, Internal default). Sub op type → StreamingHandler expecting +text/event-stream. Shares the forwarding-handler code path with +from_openapi. + +## Acceptance Criteria + +- [ ] Adapter ported; one registration per call, correct provenance +- [ ] Forwarding round-trip test against a local mock server +- [ ] `cargo test` passes + +## References + +- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md +- docs/architecture/http-adapters.md (§from_jsonschema) +- alkcall ADR-027 (FromJsonSchema provenance) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/adapters/from-openapi.md b/tasks/adapters/from-openapi.md new file mode 100644 index 0000000..779591a --- /dev/null +++ b/tasks/adapters/from-openapi.md @@ -0,0 +1,47 @@ +--- +id: adapter-from-openapi +name: from_openapi adapter (parse + forwarding handlers) +status: pending +depends_on: [client-http-host] +scope: broad +risk: medium +impact: component +level: implementation +tags: [adapters, phase-3] +--- + +## Description + +Port `adapters/from_openapi.rs` (~2k lines, the largest port) from +`/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/from_openapi.rs`: +OpenAPISpec (from_json/from_yaml/from_str JSON-first per ADR-051, +from_value), $ref resolution, HttpServiceConfig, per-op +HandlerRegistration construction (Internal by default, FromOpenAPI +leaf provenance), forwarding handlers (path-template substitution, +query params, auth header from capabilities, content-type branching), +SSE→StreamingHandler for Sub ops (HandlerKind::Stream), error fidelity +(`HTTP_` codes). Type paths move to alkcall::client (adapter +contract) and alkcall::registry. + +## Acceptance Criteria + +- [ ] JSON + YAML + from_str detection ports with tests +- [ ] Handler construction: Query/Mutation→Once, Sub→Stream; Pub never produced (v1) +- [ ] Forwarding: path params, query params, auth injection from Capabilities, error mapping — unit tested (mock HTTP via local axum server) +- [ ] No env-var reads +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-adapters.md (§from_openapi, §Forwarding handler, §Error Fidelity) +- docs/architecture/decisions/023-operation-error-schemas.md, 051-yaml-input-for-from-openapi.md +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/from_openapi.rs` + +## Notes + +> Agent fills during implementation. Largest single port — consider +> splitting internally (parse half / handler half) if review flags it. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/adapters/from-wss.md b/tasks/adapters/from-wss.md new file mode 100644 index 0000000..10c488b --- /dev/null +++ b/tasks/adapters/from-wss.md @@ -0,0 +1,47 @@ +--- +id: adapter-from-wss +name: from_wss consumer adapter (wss feature) +status: pending +depends_on: [ws-byte-adapter, ws-upgrade-session] +scope: moderate +risk: high +impact: component +level: implementation +tags: [adapters, phase-3] +--- + +## Description + +Implement `from_wss` per ADR-070 behind the `wss` feature +(tokio-tungstenite): dial wss:// endpoint → adapt the WS stream with +the shared byte-adapter → `Connection::from_bidi(_, b"alk/channels")` → +consumer half (alkcall ChannelClient machinery: channel 0 install + +client dispatch loop) → services/list + services/schema → one +forwarding HandlerRegistration per discovered op (FromCall provenance, +leaf, Internal default). Forwarding: serialize input → call.requested +frame on channel 0 → correlate by id. Bearer token from Capabilities +(no-env-vars). Reconnect policy: v1 = drop → retryable failures +(OQ-03 disposition). + +## Acceptance Criteria + +- [ ] Adapter behind `wss` feature; base crate compiles without it +- [ ] Round-trip test: from_wss consumer ↔ ws-upgrade-session server (both halves of the adapter exercised together) +- [ ] Discovered ops invoke correctly; identity/ACL enforced end-to-end +- [ ] Connection drop → in-flight calls fail retryable, no hang +- [ ] Credentials flow from Capabilities only (no env-var reads) +- [ ] `cargo test --all-features` passes + +## References + +- docs/architecture/decisions/070-from-wss-consumer-adapter.md +- docs/architecture/websocket.md (§The consumer-side mirror) +- alkcall ADR-028 (from_call pattern), ADR-043 (ChannelClient) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/adapters/mcp.md b/tasks/adapters/mcp.md new file mode 100644 index 0000000..664402b --- /dev/null +++ b/tasks/adapters/mcp.md @@ -0,0 +1,45 @@ +--- +id: adapter-mcp +name: from_mcp + to_mcp (mcp feature) +status: pending +depends_on: [client-http-host, gateway-routes] +scope: broad +risk: medium +impact: component +level: implementation +tags: [adapters, mcp, phase-3] +--- + +## Description + +Port both MCP adapters from +`/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/{from_mcp/`, +`to_mcp.rs}` behind the `mcp` feature (rmcp, streamable HTTP only — +ADR-037). from_mcp: tools/list discovery, per-tool HandlerRegistration +(Mutation, HandlerKind::Once, FromMCP leaf), structuredContent-preferred +output handling, ContentBlock-union fallback, isError→CallError. +to_mcp: 4 fixed gateway tools (search/schema/call/batch — Sub AND Pub +excluded), StreamableHttpService into axum under /mcp with bearer +middleware. Shared dispatch spine with to_openapi via GatewayDispatch. + +## Acceptance Criteria + +- [ ] Both adapters behind `mcp`; default features compile without rmcp +- [ ] from_mcp round-trip against a mock MCP server (tools/list, tools/call, structured + content-block outputs) +- [ ] to_mcp: 4 tools listed; call dispatch filtered by ACL; Pub/Sub ops excluded +- [ ] stdio transport not present anywhere (dependency audit) +- [ ] `cargo test --all-features` passes + +## References + +- docs/architecture/http-mcp.md +- docs/architecture/decisions/037-mcp-stdio-transport-exclusion.md, 041-mcp-tool-gateway-pattern.md +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/from_mcp/`, `to_mcp.rs`, `tests/from_mcp_integration.rs` + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/adapters/to-openapi.md b/tasks/adapters/to-openapi.md new file mode 100644 index 0000000..5c97fdc --- /dev/null +++ b/tasks/adapters/to-openapi.md @@ -0,0 +1,44 @@ +--- +id: adapter-to-openapi +name: to_openapi projection (6-endpoint gateway doc) +status: pending +depends_on: [gateway-routes] +scope: moderate +risk: low +impact: component +level: implementation +tags: [adapters, phase-3] +--- + +## Description + +Port `adapters/to_openapi.rs` from +`/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/to_openapi.rs`: +generate the OpenAPI doc describing the 6 fixed gateway endpoints +(search/schema/call/batch/subscribe + publish), per-caller surface +discovered via /search (not preloaded), `info.version` semver tracking +the gateway contract (ADR-045; minor = /publish addition), error +schemas projected per ADR-023. Wire `GET /openapi.json` in the server +router. Pub ops excluded from /subscribe's tool description but listed +in /search; /publish documents the NDJSON body per OQ-02's resolution. + +## Acceptance Criteria + +- [ ] Projection ported; 6-endpoint doc with correct versioning +- [ ] /openapi.json serves it (integration test) +- [ ] Doc validates against openapiv3 parsing +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-adapters.md (§to_openapi) +- docs/architecture/decisions/042-openapi-gateway-pattern.md, 045-to-openapi-gateway-spec-versioning.md, 068-gateway-publish-endpoint.md +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/adapters/to_openapi.rs` + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/client/http-host.md b/tasks/client/http-host.md new file mode 100644 index 0000000..052f165 --- /dev/null +++ b/tasks/client/http-host.md @@ -0,0 +1,42 @@ +--- +id: client-http-host +name: Shared reqwest client host (middleware stack, retry-after) +status: pending +depends_on: [] +scope: narrow +risk: low +impact: component +level: implementation +tags: [client, phase-3] +--- + +## Description + +Port the outbound HTTP client host from +`/workspace/@alkdev/alknet/crates/alknet-http/src/client/`: +`ClientWithMiddleware` shared across forwarding handlers; +`RetryTransientMiddleware` (exponential backoff) + inlined +`RetryAfterMiddleware` (parse Retry-After on 429/503, bounded URL→time +map); ArcSwap rebuild-and-swap for config hot-reload. Credential +injection is per-request from OperationContext.capabilities — never at +client construction, never from env vars. + +## Acceptance Criteria + +- [ ] Client host ported with the middleware stack; inlined Retry-After (bounded storage) +- [ ] ArcSwap hot-swap tested (config change → new pool) +- [ ] No env-var reads (invariant test) +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-adapters.md (§HTTP client (reqwest)) +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/client/{http_client,retry_after}.rs` + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/gateway/dispatch.md b/tasks/gateway/dispatch.md new file mode 100644 index 0000000..87a6295 --- /dev/null +++ b/tasks/gateway/dispatch.md @@ -0,0 +1,47 @@ +--- +id: gateway-dispatch +name: GatewayDispatch — shared dispatch spine (invoke + streaming) +status: pending +depends_on: [server-core-types] +scope: moderate +risk: medium +impact: component +level: implementation +tags: [gateway, phase-1] +--- + +## Description + +Port the dispatch spine from +`/workspace/@alkdev/alknet/crates/alknet-http/src/gateway/dispatch.rs` +to alkcall's API: `GatewayDispatch::new(registry, identity_provider)`, +`invoke()` (identity resolve → root OperationContext → +`OperationRegistry` invoke → ResponseEnvelope), and `invoke_streaming()` +(`BoxStream` for Sub ops). The security invariants +must hold identically across both: `External`-only (Internal → +NOT_FOUND), `AccessControl::check` gating, no env-var reads. Port the +error mapping (`gateway/error.rs`) too: CallError → HTTP status per +docs/architecture/http-server.md §Error Mapping (NOT_FOUND→404, +FORBIDDEN→401/403, INVALID_INPUT→422, TIMEOUT→504, INTERNAL→500, +`HTTP_` passthrough, retryable→Retry-After hint). + +## Acceptance Criteria + +- [ ] `invoke()` + `invoke_streaming()` ported against alkcall dispatch +- [ ] Error mapping table ported with unit tests per row +- [ ] Internal ops → 404 before ACL; ACL failure → 401/403 distinction preserved +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-server.md (§HTTP-to-call dispatch, §Error Mapping) +- docs/architecture/decisions/023-operation-error-schemas.md +- alkcall ADR-016 (error schemas), ADR-017 (privilege model) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/gateway/publish.md b/tasks/gateway/publish.md new file mode 100644 index 0000000..f30c570 --- /dev/null +++ b/tasks/gateway/publish.md @@ -0,0 +1,46 @@ +--- +id: gateway-publish +name: POST /publish endpoint for Pub operations +status: pending +depends_on: [gateway-routes] +scope: narrow +risk: medium +impact: component +level: implementation +tags: [gateway, phase-3] +--- + +## Description + +Implement `POST /publish` per ADR-068: NDJSON request body (each line = +one published chunk) dispatched through `invoke_sink()` (alkcall +ADR-046); handler's final ResponseEnvelope → 200 JSON or mapped error +status. Resolve OQ-02 during this task: first line carries +`{ "operation": "/{service}/{op}", "chunk": {...} }` (subsequent lines +`chunk`-only); terminal errors are plain HTTP status + JSON body (not +an NDJSON line). Client disconnect drops the body stream → sink +cancelled (PublishStream sees EOF). Non-Pub target → 400 +INVALID_OPERATION_TYPE. Tests: multi-chunk publish round-trip against a +SinkHandler over DuplexStream, early-disconnect, error mapping. + +## Acceptance Criteria + +- [ ] /publish wired; OQ-02 convention implemented and documented in ADR-068 +- [ ] Sink round-trip test (3+ chunks → final envelope) +- [ ] Disconnect mid-stream cancels the handler (no hang) +- [ ] to_openapi gateway doc gains /publish; gateway `info.version` minor bump +- [ ] `cargo test` passes + +## References + +- docs/architecture/decisions/068-gateway-publish-endpoint.md +- docs/architecture/open-questions.md (OQ-02) +- alkcall ADR-046 (Pub/invoke_sink/PublishStream) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/gateway/routes.md b/tasks/gateway/routes.md new file mode 100644 index 0000000..10f907c --- /dev/null +++ b/tasks/gateway/routes.md @@ -0,0 +1,45 @@ +--- +id: gateway-routes +name: The 5 core gateway routes wired into the router +status: pending +depends_on: [gateway-dispatch, server-adapter] +scope: moderate +risk: medium +impact: component +level: implementation +tags: [gateway, phase-1] +--- + +## Description + +Port `server/gateway_routes.rs` from +`/workspace/@alkdev/alknet/crates/alknet-http/src/server/gateway_routes.rs`: +`GET /search` (AccessControl-filtered services/list), `GET /schema`, +`POST /call` (`{operation, input}`), `POST /batch`, and `POST /subscribe` +(SSE projection: `text/event-stream`, `call.responded` → `data:` frames, +terminal error event, abort on client disconnect). Auth middleware +applied to all five. The route set intentionally excludes `/publish` +(separate task). Integration tests over DuplexStream: search filtering +by identity scopes, call round-trip, subscribe stream of multiple +events, batch correlation. + +## Acceptance Criteria + +- [ ] 5 routes ported and wired in server-adapter's router +- [ ] SSE framing tests (multi-event, error-terminates, client-disconnect) +- [ ] /search returns only ops the caller's identity can invoke +- [ ] Full request→dispatch→response test over DuplexStream +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-server.md (§router list, §Streaming projection) +- docs/architecture/decisions/042-openapi-gateway-pattern.md, 047-remove-direct-call-http-surface.md + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/infra/integration-suite.md b/tasks/infra/integration-suite.md new file mode 100644 index 0000000..5afaeb6 --- /dev/null +++ b/tasks/infra/integration-suite.md @@ -0,0 +1,44 @@ +--- +id: infra-integration-suite +name: Full-surface integration suite + docs sync + publish prep +status: pending +depends_on: [gateway-publish, adapter-to-openapi, adapter-from-wss, adapter-mcp, ws-overlay-ops] +scope: moderate +risk: medium +impact: project +level: review +tags: [infra, phase-4] +--- + +## Description + +Phase 4 hardening. (1) Integration suite exercising the full surface +over DuplexStream + in-process WSS: gateway 6 endpoints, WS session +(call + data channel + overlay), from_openapi import → call → error +fidelity, from_wss import over the WS server, mcp feature interplay. +(2) Docs sync: port-notes sections stripped from ported ADRs/specs +after review, open-questions.md statuses updated (OQ-01/02 → resolved), +README doc table final. (3) Publish prep: `cargo publish --dry-run +--allow-dirty`, semver/API surface check against AGENTS.md conventions, +Cargo.toml metadata polish. + +## Acceptance Criteria + +- [ ] `cargo test --all-features` green; clippy `-D warnings` green; fmt green +- [ ] Full-surface integration test passing (gateway + WS + one adapter each direction) +- [ ] Port notes stripped; OQ statuses updated +- [ ] `cargo publish --dry-run --allow-dirty` succeeds +- [ ] taskgraph shows all tasks completed + +## References + +- docs/plans/implementation.md (§Build order, Phase 4) +- AGENTS.md (verification commands, commit conventions) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/server/adapter.md b/tasks/server/adapter.md new file mode 100644 index 0000000..bac01e1 --- /dev/null +++ b/tasks/server/adapter.md @@ -0,0 +1,45 @@ +--- +id: server-adapter +name: HttpAdapter — ProtocolHandler with hyper over BiStream +status: pending +depends_on: [server-core-types, server-auth, server-healthz-decoy] +scope: moderate +risk: medium +impact: component +level: implementation +tags: [server, phase-1] +--- + +## Description + +Port the `HttpAdapter` (`ProtocolHandler` for `http/1.1` + `h2`) from +`/workspace/@alkdev/alknet/crates/alknet-http/src/server/adapter.rs`, +adapted to alkcall's shapes: `handle()` receives a `Connection` +(alkcall::core::types), takes the `BiStream` from `accept_bi()`, wraps +with `TokioIo`, drives hyper's http1/http2 connection builder against +the axum `Router` (built once at construction, `with_decoy` / +`with_extra_routes` builders per ADR-046). Branch on +`connection.remote_alpn()`. Leave a route slot for the WS upgrade +(wired in the websocket tasks). + +## Acceptance Criteria + +- [ ] `HttpAdapter::new/h2/for_alpn` + `with_decoy` + `with_extra_routes` ported +- [ ] `ProtocolHandler` impl drives hyper over the BiStream; returns on connection close +- [ ] Router merges extra routes; default surface wins collisions +- [ ] Integration test: full HTTP request/response cycle over `tokio::io::DuplexStream` → `Connection::from_bidi` → adapter +- [ ] `cargo test` passes; feature gates `h2`/`http1` both compile + +## References + +- docs/architecture/http-server.md (§Running axum over a bidirectional stream) +- docs/architecture/decisions/002-protocol-handler-trait.md +- alkcall: `core::types::Connection::accept_bi` returns joined `BiStream` (alkcall ADR-005) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/server/auth.md b/tasks/server/auth.md new file mode 100644 index 0000000..9d9610a --- /dev/null +++ b/tasks/server/auth.md @@ -0,0 +1,42 @@ +--- +id: server-auth +name: Bearer auth middleware and identity extraction +status: pending +depends_on: [server-core-types] +scope: narrow +risk: low +impact: component +level: implementation +tags: [server, phase-1] +--- + +## Description + +Port `bearer_auth_middleware` and `extract_bearer_identity` + +`ResolvedIdentity` from +`/workspace/@alkdev/alknet/crates/alknet-http/src/server/auth.rs`. +Resolution via `IdentityProvider::resolve_from_token(&AuthToken { raw }) +` (alkcall::core::auth). Middleware behavior: no/invalid token → +identity None (routes decide 401 vs anonymous); token present → +ResolvedIdentity(Some(identity)). Unit tests over the middleware with a +static identity provider. + +## Acceptance Criteria + +- [ ] Middleware ports with tests (missing header, malformed, valid token, unknown token) +- [ ] `set_identity` observability path documented for the WS route's use +- [ ] No env-var reads anywhere (no-env-vars invariant) +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-server.md (§Auth) +- docs/architecture/decisions/004-auth-as-shared-core.md + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/server/core-types.md b/tasks/server/core-types.md new file mode 100644 index 0000000..502f94b --- /dev/null +++ b/tasks/server/core-types.md @@ -0,0 +1,40 @@ +--- +id: server-core-types +name: Shared server state, config, and error types +status: pending +depends_on: [] +scope: narrow +risk: low +impact: component +level: implementation +tags: [server, phase-1] +--- + +## Description + +Port the shared types the whole server subsystem hangs off: +`DecoyConfig` (NotFound/StaticSite/Redirect), `RouterState` (registry, +identity provider, decoy — with axum `FromRef` impls), and the +module skeleton for `src/server/`. Ported from +`/workspace/@alkdev/alknet/crates/alknet-http/src/server/adapter.rs` +(the type definitions) and `server/mod.rs`. + +## Acceptance Criteria + +- [ ] `DecoyConfig`, `RouterState` ported with the 6-endpoint reserved-path doc comments +- [ ] `alkcall::core::auth::IdentityProvider` / `alkcall::registry::registration::OperationRegistry` type paths correct +- [ ] No comments in code (project convention); doc comments on public API only +- [ ] `cargo clippy --all-targets -- -D warnings` clean + +## References + +- docs/architecture/http-server.md (§What — the struct shapes) +- docs/architecture/decisions/046-assembly-layer-custom-http-routes.md + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/server/healthz-decoy.md b/tasks/server/healthz-decoy.md new file mode 100644 index 0000000..9093636 --- /dev/null +++ b/tasks/server/healthz-decoy.md @@ -0,0 +1,39 @@ +--- +id: server-healthz-decoy +name: /healthz raw route and stealth decoy fallback +status: pending +depends_on: [server-core-types] +scope: narrow +risk: low +impact: component +level: implementation +tags: [server, phase-1] +--- + +## Description + +Port `healthz` (`server/healthz.rs` — raw 200 "ok", no auth, no call +protocol) and the decoy fallback (`server/decoy.rs` — fake nginx-style +404, static site, or redirect per `DecoyConfig`). Ported from +`/workspace/@alkdev/alknet/crates/alknet-http/src/server/{healthz,decoy}.rs`. +Tests: healthz responds without auth; decoy serves all three configs. + +## Acceptance Criteria + +- [ ] `/healthz` returns 200 text/plain without auth +- [ ] Decoy fallback for unmatched paths per DecoyConfig (404/static/redirect) +- [ ] Reserved paths (6 gateway + /healthz + /openapi.json + /mcp + /alk/channels) never hit the decoy +- [ ] `cargo test` passes + +## References + +- docs/architecture/http-server.md (§/healthz, §Stealth decoy) +- docs/architecture/decisions/010-alpn-router-and-endpoint.md + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/websocket/byte-adapter.md b/tasks/websocket/byte-adapter.md new file mode 100644 index 0000000..4e82567 --- /dev/null +++ b/tasks/websocket/byte-adapter.md @@ -0,0 +1,54 @@ +--- +id: ws-byte-adapter +name: WS ↔ byte-stream adapter (research POC → production shape) +status: pending +depends_on: [] +scope: moderate +risk: high +impact: phase +level: research +tags: [websocket, phase-2, poc] +--- + +## Description + +Targeted POC validating the WS↔byte-stream adapter contract (OQ-01) +before the production implementation: wrap axum's `WebSocket` as +`AsyncRead + AsyncWrite` such that alkcall's `ChannelsAdapter` demux +and mux run over it correctly. + +Inbound: axum WS messages → shared bounded buffer → `AsyncRead` drains +(backpressure via the alknet-tty precedent: try_send → Full → Pending). +Outbound: `AsyncWrite` accumulates into a pending buffer; a task parses +outgoing 8-byte chunk headers (verified: header+payload are separate +writes; channel 0's write_frame is prefix+body separately) and emits +each complete chunk as one WS binary message; chunks over the WS +message cap (~1 MiB default) split across messages. Close: WS close → +transport EOF (REQ-CH-02); `AsyncWrite::shutdown` → zero-length +sentinel then Close frame (REQ-CH-01). + +## Acceptance Criteria + +- [ ] POC: adapter + ChannelsAdapter + channel-0 Dispatcher over `tokio::io::DuplexStream` pairs — call round-trip works +- [ ] POC test: 16 MiB chunk splits across WS messages and reassembles +- [ ] POC test: interleaved channel writes preserve chunk integrity (no torn chunks) +- [ ] OQ-01 sub-items (a)-(d) resolved with concrete values, written back into open-questions.md +- [ ] Findings + go/pivot recommendation recorded in this task's Summary + +## References + +- docs/architecture/websocket.md (§The WS ↔ byte-stream adapter) +- docs/plans/implementation.md (§What the spike established, point 6) +- alknet-tty precedent: `src/adapter.rs` `pump_session` drainer pattern + `TestStdinSink` backpressure +- alkcall: `channels::wire::write_chunk`, `channels::mux::MuxRunner`, REQ-CH-01/02 in `channels-wire.md` + +## Notes + +> Agent fills during implementation. This is a research task: the POC +> code lands under `.worktrees/research/` or a `#[cfg(test)]` module — +> not as final production code — but the production adapter may grow +> directly from it if the shape holds. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/websocket/overlay-ops.md b/tasks/websocket/overlay-ops.md new file mode 100644 index 0000000..3ce5b4d --- /dev/null +++ b/tasks/websocket/overlay-ops.md @@ -0,0 +1,44 @@ +--- +id: ws-overlay-ops +name: Browser-registered ops — connection-local overlay tests +status: pending +depends_on: [ws-upgrade-session] +scope: narrow +risk: medium +impact: component +level: implementation +tags: [websocket, phase-2] +--- + +## Description + +Port the connection-local Layer 2 overlay verification from +`/workspace/@alkdev/alknet/crates/alknet-http/src/websocket/overlay.rs` +to the channels-over-WS session: a WS client registers ops (via +call-protocol registration on channel 0), the hub reaches them through +the live connection handle's `overlay_env()` — not PeerRef. Tests: +hub→browser call over the same session; overlay ops die on disconnect; +AccessControl gating on browser ops; bidirectional concurrent calls +(both sides initiating on channel 0). + +## Acceptance Criteria + +- [ ] Hub→browser call test passes (browser registered an op, hub invokes it) +- [ ] Disconnect drops overlay; subsequent reach attempts fail cleanly +- [ ] Concurrent bidirectional calls don't deadlock or cross-correlate +- [ ] `cargo test` passes + +## References + +- docs/architecture/websocket.md (§Connection-local overlay, §Bidirectionality) +- docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md (§4) +- alkcall ADR-019 (registry layering) +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/websocket/overlay.rs` + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file diff --git a/tasks/websocket/upgrade-session.md b/tasks/websocket/upgrade-session.md new file mode 100644 index 0000000..246bf5d --- /dev/null +++ b/tasks/websocket/upgrade-session.md @@ -0,0 +1,48 @@ +--- +id: ws-upgrade-session +name: WS upgrade route + channels session (server producer half) +status: pending +depends_on: [ws-byte-adapter, server-adapter] +scope: broad +risk: high +impact: phase +level: implementation +tags: [websocket, phase-2] +--- + +## Description + +Implement the WS upgrade handler per ADR-067/websocket.md, production +shape: axum route at `/alk/channels`; bearer auth on the upgrade +request (401 without token); upgrade → adapt the WS stream with the +byte-adapter (ws-byte-adapter) → `Connection::from_bidi(ws_stream, +b"alk/channels")` → run the channels accept path: alkcall +`ChannelsAdapter` in-line demux with the `install_channel_zero` hook +constructing channel 0's `CallConnection` (identity attached) and +running `Dispatcher::run_loop_single_stream`. Text messages rejected +(WS close 1002). Overlay-only session construction mirrors the alknet +`upgrade.rs::run_ws_session` shape but through the channels machinery. + +## Acceptance Criteria + +- [ ] Upgrade route wired into HttpAdapter's router (reserved-path collision rule respected) +- [ ] End-to-end test: WS client (tokio-tungstenite or axum test client) → upgrade → chunk-framed call.requested on channel 0 → response received +- [ ] Browser-style test: services/list via channel 0, AccessControl-filtered +- [ ] Disconnect mid-call: pending requests failed, overlay dropped, no hang +- [ ] 16 MiB chunk split/reassembly test carried over from the POC +- [ ] `cargo test` passes + +## References + +- docs/architecture/websocket.md (full spec) +- docs/architecture/decisions/067-websocket-carries-channels.md +- alkcall `channels/client.rs` tests (make_install_channel_zero wiring pattern) +- Old source: `/workspace/@alkdev/alknet/crates/alknet-http/src/websocket/upgrade.rs` (session lifecycle; framing superseded) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills on completion. \ No newline at end of file