# ADR-006: Self-Contained Negotiation Framing (No alkcall-Internal-Wire-Types Dependency) ## Status Accepted (ported from alknet ADR-057 2026-08-17; the dependency-edge decision carries over unchanged — alktty depends on alkcall (for the `ProtocolHandler` trait, auth, ownership types) but not on alkcall's internal wire types. The framing is self-contained in `src/negotiation.rs`. Cross-references renumbered to alktty's ADR range — ADR-052→001, ADR-053→002, ADR-054→003, ADR-055→004, ADR-056→005, ADR-057→006, ADR-077→007, ADR-093→008. The alknet ADR referenced by alknet number (003) is not ported into alktty's ADR range — it is an alknet-core ADR. The alknet originals at `/workspace/@alkdev/alknet/docs/architecture/decisions/` remain authoritative.) ## Context The alknet-tty specs (alknet ADR-052, 053, 054) and alknet ADR-003 Amendment 1 previously stated that alknet-tty depends on alknet-call for the `FrameFramedReader`/`FrameFramedWriter` "framing utility" — the 4-byte big-endian length prefix + UTF-8 JSON body framing the negotiation frame uses. The claim was "reuse the framing utility, not the `EventEnvelope` type" (alknet ADR-052 §6). A pre-implementation sanity check surfaced that **the framing utility is not actually reusable as the spec described.** `FrameFramedReader` (in alkcall's protocol/wire module) is hardcoded to deserialize `EventEnvelope`: ```rust pub async fn read_frame(&mut self) -> Result { // ... read 4-byte length prefix, read body ... let envelope: EventEnvelope = serde_json::from_slice(&body)?; Ok(envelope) } ``` The framing logic (read 4 bytes → length, read N bytes) and the `EventEnvelope` deserialization are entangled in the same method. The "framing utility" the spec claimed to reuse does not exist as a separable thing — the length-prefix read and the type-specific deserialize are one call. alktty's negotiation frame is a `NegotiateRequest`, not an `EventEnvelope`, so `read_frame()` cannot return what alktty needs. This left three options (see alknet ADR-003 Amendment 2 for the full comparison): 1. **Duplicate the ~30 lines of framing logic in alktty.** The framing is a trivial length-prefix idiom (4-byte BE length + body); the two copies would share an idiom, not a domain abstraction. 2. **Promote a generic length-prefixed framing utility to alkcall::core.** Makes the spec's claim true (a reusable utility exists), but accretes a framing module to the foundation crate for the sake of two consumers — a shared utility pays for itself at ≥2 consumers, but the framing is trivial enough that the cost of the shared abstraction (a new module, a new type, a refactor of alkcall) exceeds the cost of the duplication. 3. **Actually use alkcall** (e.g., model the tty control channel as call-protocol operations). A different architecture, not a dependency-edge fix — the current `ControlMessage` tagged enum (ADR-001) is the two-way-door seam; replacing it with the call protocol is a v2 ALPN decision, not a v1 dependency choice. ## Decision ### 1. alktty does not depend on alkcall's internal wire types alktty implements its own length-prefixed framing for the negotiation frame directly on tokio's `AsyncRead`/`AsyncWrite`. The crate's dependency edge is: ``` alktty └── alkcall (ProtocolHandler, Connection, AuthContext, Identity, AccessControl, OwnershipProvider — alknet ADR-050) ``` No alkcall-internal-wire-types dependency. The crate depends on alkcall (which every handler crate depends on anyway for the `ProtocolHandler` trait) and nothing else for the protocol surface. `portable_pty`, `bollard`, `russh` remain in the backend crates / the `local` feature (ADR-003). ### 2. The framing format coincides with alkcall's by convention, not by code reuse Both alktty's negotiation frame and alkcall's `EventEnvelope` frame use a 4-byte big-endian length prefix + UTF-8 JSON body. This is a shared *format convention* (length-prefixed JSON is a standard framing pattern), not a code dependency. The two implementations are independent: alktty's reader deserializes `NegotiateRequest`; alkcall's `FrameFramedReader` deserializes `EventEnvelope`. They share an idiom (length-prefix framing), not a module. ### 3. The framing logic lives in alktty as a small, self-contained module alktty implements the negotiation framing as a small module (`src/negotiation.rs`, ~30 lines for the framing reader/writer: read 4-byte BE length, bounds-check, read N bytes; write the inverse). The module's types (`NegotiationReader`/`NegotiationWriter`) are private to the crate — they are not a reusable utility for other crates. If a future crate wants length-prefixed JSON framing, it implements its own (the idiom is trivial) or a future ADR promotes a generic utility to alkcall::core at that point (deferred — not needed for the current scope; two consumers is the threshold but the second consumer does not yet exist). ## Consequences **Positive:** - alktty's dependency surface is minimal and correct: alkcall only. No dependency on alkcall's internal wire types for a "framing utility" that wasn't reusable as specced. The "weird" dependency edge (a handler crate depending on another handler crate's internal wire module for 30 lines of glue) is gone. - The spec is honest: it describes what the code does (a self-contained framing module) rather than what a previous draft hoped for (a reusable utility in alkcall that doesn't exist in a separable form). - The framing logic is trivial and self-contained; bugs in it are local to alktty (no cross-crate coordination if alkcall's framing changes for call-protocol reasons). - alknet ADR-003's "no handler crate depends on another handler crate's internal wire types" rule is preserved without the Amendment 1 exception for alktty. (alknet ADR-003 Amendment 1's exception remains for alknet-http/agent/napi, which use alkcall's `OperationSpec`/`Handler`/`OperationAdapter` types — actual type reuse, not framing glue. alktty does not need that exception.) **Negative:** - ~30 lines of framing logic are duplicated between alktty and alkcall. The duplication is an idiom (length-prefix framing), not a domain abstraction; the cost of the shared abstraction (a new module in alkcall::core + a refactor of alkcall) exceeds the cost of the duplication for two consumers. If a third consumer appears, this trade-off should be revisited (promote to alkcall::core). - A bug found in the length-prefix framing edge cases (e.g., a partial-read handling bug) would need fixing in two places. The framing is mature (a standard `read_exact`-based pattern); the edge cases are known and tested in both crates independently. ## Door type **One-way.** alktty not depending on alkcall's internal wire types is a dependency-edge commitment. Adding the dependency back later (if, e.g., a generic framing utility is promoted to alkcall::core) is a new ADR. The framing logic being self-contained in alktty is two-way (it could be refactored to a shared utility in alkcall::core later without a wire-format change), but the dependency edge is one-way. ## Assumptions 1. **The framing logic is trivial enough that duplication is cheaper than a shared abstraction.** Length-prefixed framing (4-byte BE length + body) is a ~30-line idiom. The cost of a shared utility in alkcall::core (a new module, a new type, a refactor of alkcall's wire module to extract the generic layer) is higher than the cost of two independent implementations for two consumers. If a third consumer appears, revisit (promote to alkcall::core). 2. **The format coincidence (both use 4-byte BE length + JSON body) is stable.** Both crates use the same length-prefix convention. If alkcall's framing changes (e.g., a different max-frame-size, a different prefix width), alktty's is unaffected — they are independent implementations that happen to share a format today. ## References - alknet ADR-003 Amendment 1 (protocol-foundation exception for alknet-http/agent/napi) and Amendment 2 (this ADR's effect on the Amendment 1 framing-reuse claim for alktty) - [ADR-001](001-wire-format-and-two-carriage.md) §2 (negotiation frame format), §6 (revised: format coincides by convention, not by code reuse) - [ADR-002](002-ttybackend-trait-and-ttyhandle.md) — the crate decomposition this ADR's dependency edge affects - `src/negotiation.rs` — the self-contained framing reader/writer this ADR commits (`NegotiationReader`, `NegotiationWriter`, `error_response_bytes`) - alkcall's `FrameFramedReader`/`FrameFramedWriter` — the `EventEnvelope`-bound methods that are NOT reused (the entangled length-prefix-read + `EventEnvelope`-deserialize that motivated this ADR) - Spec: [overview.md](../overview.md) (dependency edge), [tty-wire.md](../tty-wire.md) (negotiation framing) - Port origin: alknet ADR-057 at `/workspace/@alkdev/alknet/docs/architecture/decisions/057-alknet-tty-no-alknet-call-dep.md`