Files
alkhttp/docs/architecture/decisions/003-crate-decomposition.md
T
glm-5.3-flash 4a825d33e7 feat(infra): full-surface integration suite + docs sync + publish prep
Full-surface integration suite (tests/full_surface.rs, mcp feature):
- one HttpAdapter over real TCP (ProtocolHandler::handle path) serving
  gateway endpoints, /openapi.json, /mcp, and the WS channels session
- gateway: search/schema/call/subscribe/batch/publish presence,
  envelope shapes, error fidelity end-to-end
- from_openapi import -> Internal-by-default invisible from the wire ->
  External facade composes it via env.invoke -> upstream HTTP API
  called end-to-end (ADR-015 composition model exercised)
- to_openapi 6-path doc validated against openapiv3 over the wire
- to_mcp: MCP client connects to /mcp on the served adapter, lists the
  4 gateway tools, search returns ACL-filtered ops (Sub excluded)

Production fix: the WS upgrade route was reserved but never wired into
HttpAdapter's router (the ws-upgrade-session tests built their own
router). Now wired with ws_bearer_auth (401 without a resolvable
token) around ws_upgrade_handler.

Docs sync: all 28 'Port notes' sections/blockquotes stripped from
ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in
overview.md, websocket.md, and the README table (open-questions.md was
already current).

Publish prep: cargo publish --dry-run --allow-dirty succeeds;
cargo doc --no-deps warning-free (ADR link targets fixed); feature
combinations (default / test-support / mcp / wss / all) compile
warning-free under clippy -D warnings.

Verified: cargo test (182 lib default), --all-features (227 lib + 29
integration), clippy -D warnings x3 feature sets, fmt, doc,
publish --dry-run.
2026-08-28 16:07:56 +00:00

8.8 KiB

ADR-003: Crate Decomposition

Ported from alknet ADR-003 (Crate Decomposition); re-targeted to alkhttp.

Status

Accepted

Context

The previous architecture had a monolithic core crate containing transport, interface, server, client, call, auth, config, socks5, credentials, and HTTP — all in one crate with interdependent modules. This created coupling (interface types depended on auth, server depended on call, everything depended on config) and made it impossible to use individual components independently.

The ALPN dispatch model eliminates the need for a shared interface layer. Each handler is self-contained — it receives a byte stream and manages its own protocol. This naturally decomposes into separate crates.

Key constraints:

  • Protocol crates must depend on the shared core for auth/identity/config — but not on each other
  • The vault crate (alkvault) is already standalone (no core dependency) and must remain so (see ADR-008)
  • The CLI binary assembles everything — it's the only crate that depends on all handler crates
  • Handlers with protocol-agnostic cores (SFTP, call protocol) preserve the WASM door — browser clients can implement the wire format over WebTransport (see ADR-009, ADR-013)
  • The call crate includes the call protocol client and adapter traits, not just the server side — this enables agent and NAPI consumers to use it for remote invocation
  • Rust is the canonical implementation language. TypeScript is a reference/browser adaptation, not a parallel implementation (see ADR-013)

Decision

The alknet workspace decomposed into the following crates (now extracted and published as alkvault, alkcall, and the per-protocol crates; alkhttp is the HTTP interface crate):

Crate Responsibility Depends on
shared core (now alkcall, which vendors the core types) ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config tokio, quinn, rustls, iroh (feature-gated, added by ADR-010)
alkvault Local key vault: BIP39/SLIP-0010/AES-GCM key derivation, encryption (standalone, no core dependency)
alknet-ssh SshAdapter (russh, SOCKS5, port forwarding) core, russh
call crate (now alkcall) CallAdapter (JSON-RPC via hand-rolled EventEnvelope framing, operation registry, pub/sub, access control, call protocol client, adapter traits) core
alknet-agent Agent service: LLM execution loop (forked aisdk), tool dispatch via call protocol, provider key retrieval via vault call
alknet-git GitAdapter (gix, pkt-line protocol) core, gix
alknet-sftp SftpAdapter (russh-sftp protocol core) core, russh-sftp
alknet-msg MessageAdapter (E2E encryption, mixnet) core
alkhttp HttpAdapter (axum, REST API, MCP endpoint) alkcall, axum
alknet-dns DnsAdapter (hickory-proto, pkarr, service discovery) core, hickory-proto
alknet-napi Node.js native addon — thin NAPI projection of the call protocol client call, napi-rs
alknet CLI binary — registers handlers, starts endpoint all handler crates, alkvault

Dependency flow:

alkvault (standalone)
core ← all handler crates ← alknet (CLI)
call ← alknet-agent
call ← alknet-napi

No handler crate depends on another handler crate. Cross-handler communication goes through the call protocol (alkcall) or through the core's endpoint.

alknet-agent depends on the call crate (not the core directly) because it uses the call protocol client for tool dispatch and the operation registry for tool registration. It receives LLM provider keys through capabilities injected at the assembly layer (from alkvault), never from environment variables and never over the call protocol. See ADR-008 and ADR-014.

alknet-napi is a thin projection layer — it exposes the Rust call protocol client to Node.js via NAPI. It does not contain business logic or adapter implementations. See ADR-013.

Consequences

Positive:

  • Each handler can be developed, tested, and versioned independently
  • WASM-compatible handlers (sftp, call) don't pull in heavy dependencies (russh, axum)
  • alkvault remains standalone — no circular dependency risk
  • New handlers are added by creating a crate and registering it with the endpoint
  • Clean separation of concerns — each crate has one job

Negative:

  • More crates to manage in the workspace — workspace Cargo.toml and version coordination
  • Shared types (AuthContext, BiStream) must live in the core crate — if they change, all handlers recompile
  • The CLI binary has a large dependency tree (all handlers) — but this is expected for a binary that assembles everything
  • Testing cross-handler behavior requires integration tests in the CLI or a test utility crate

References

  • Pivot proposal (alknet mono-repo): docs/research/pivot/alpn-service-architecture.md
  • ADR-001: ALPN-based protocol dispatch
  • ADR-002: ProtocolHandler trait
  • ADR-004: Auth as shared core (IdentityProvider)
  • ADR-005: irpc as call protocol foundation (superseded by ADR-064)

Amendments

Amendment 1 (2026-06-29): the call crate is a protocol-foundation crate

The Decision table lists the call crate as a handler crate that "depends on the core, irpc." The dependency-flow diagram and the "No handler crate depends on another handler crate" rule were written before the HTTP crate (which implements from_openapi/from_mcp/to_openapi/ to_mcp and therefore needs the call crate's OperationSpec, Handler, HandlerRegistration, and OperationAdapter trait) was specced.

Clarification: the call crate is both a handler crate (it implements ProtocolHandler on ALPN alknet/call) and the protocol-foundation crate that alknet-agent, alknet-napi, and the HTTP crate consume for the operation registry, adapter contract, and call client. The "no handler crate depends on another handler crate" rule applies to peer handler crates (e.g., alkhttp does not depend on alknet-ssh); the call crate is a protocol-foundation crate in the same spirit that the core crate is, just at a different layer (operations/RPC vs. transport/auth/config).

The HTTP crate depending on the call crate is "HTTP uses the call protocol types," not "HTTP depends on SSH." This is within the spirit of this ADR's decomposition. The call-crate → HTTP-crate edge is recorded in the alkhttp crate overview (overview.md) and in the adapter location map (see the alkcall crate docs, client-and-adapters).

Amendment 2 (2026-07-07): alknet-tty does not depend on the call crate

Amendment 1's protocol-foundation framing was extended to alknet-tty in an earlier draft ("alknet-tty depends on the call crate for the FrameFramedReader/FrameFramedWriter framing utility"). A pre-implementation sanity check found this was unsound: FrameFramedReader::read_frame() is hardcoded to deserialize EventEnvelope — the length-prefix read and the type-specific deserialize are one entangled call, not a separable "framing utility." alknet-tty's negotiation frame is a NegotiateRequest, not an EventEnvelope, so read_frame() cannot return what alknet-tty needs; the claimed reuse did not exist in a usable form.

Clarification: alknet-tty does not depend on the call crate. alknet-tty implements its own length-prefixed framing (~30 lines: 4-byte big-endian length + UTF-8 JSON body) directly on tokio's AsyncRead/AsyncWrite. The format coincides with the call crate's framing by convention (both are length-prefixed JSON); the implementations are independent. The Amendment 1 protocol-foundation exception remains for the HTTP/agent/napi consumers (which use the call crate's OperationSpec/Handler/OperationAdapter types — actual type reuse, not framing glue); it no longer covers alknet-tty. See ADR-057 (alknet-tty-no-alknet-call-dep, in the alknet mono-repo ADRs) for the full decision and the three options considered (duplicate / promote to core / use the call crate).

Amendment 3 (2026-07-09): irpc is not a dependency of any crate

The Decision table listed irpc as a dependency of the core crate ("tokio, quinn, rustls, irpc, iroh") and the call crate ("core, irpc"). This was carried over from the previous architecture and never verified against the implementation: no .rs file in the workspace ever imported irpc. The call protocol's wire format (the call protocol's protocol/wire.rs in the call crate) is hand-rolled length-prefixed JSON; the EventEnvelope shape was derived from the @alkdev/pubsub TypeScript prior art (ADR-013), not from irpc. The dead irpc / irpc-derive workspace deps and the call-crate consumer dep were removed in commit 668d777. See ADR-064 (irpc-never-integrated-hand-rolled-framing, in the alknet mono-repo ADRs) for the full record (ADR-005, which accepted "irpc as the call protocol foundation," is superseded).