Files
alkcall/docs/architecture/decisions/052-channel-control-surface.md
T
glm-5.3-flash df4d4d3a9e docs(adr): amendments pinning the single-publish fix — marked-pub contract (047 am.4), control surface (052), resource enumerators (053), op/register forward-through + marked refusal (022 am.)
Per review 011 §6 sequencing: the four amendment documents drafted
before code. All pins, no options — the hedged alternatives are stated
as rejected with rationale:
- ADR-047 amendment 4: the marked-pub open contract (review 011 §1/§2's
  seven pins — marker semantics, publish_schema refusal, reserved input
  key + dispatch strip, wrapper-as-responder-of-record, ack-timing
  sentence, relay translation, client put-open capability) plus the
  registration invariants that make stubs unregistrable (§4.8).
- ADR-052 (new): channel/control control-handle surface — establisher-
  contributed ChannelControl, wrapper-owned registry, channel:no_control_handle.
- ADR-053 (new): channel/resources/subscribe live resource enumerators —
  snapshot-then-changes, scan-once aggregation, empty seed.
- ADR-022 amendment: announced unmarked Sub/Pub forward through the real
  forwarding shapes; marked announcements refused (MARKED_SPEC_UNSUPPORTED).
2026-10-13 08:27:50 +00:00

6.8 KiB

ADR-052: The channel/control Control-Handle Surface

Status

Accepted — amends ADR-037 (channel/control's contract) and ADR-047 (amended §1: the generic ops stay). Resolves review 010 S-02 (the channel/control stub shipped nine published versions; the stub was pinned by tests and tracked as OQ-39) by implementing the surface the ADR-037 text always promised. The stub's tracked design work (OQ-39) is promoted here verbatim-ish: ALPN-crate-registered per-channel control callbacks, a channels-layer callback registry keyed by channel_id.

Context

ADR-037 specified channel/control as the out-of-band control op on channel 0 (resize, signal, keepalive — anything that does not need ordering relative to data), with the channels layer routing the ALPN-specific message to a handler's control handle. The implementation shipped a registered, discoverable stub that checked channel existence and then returned channel:control_not_implemented on every invocation — which perversely returned not found for unknown channels and not implemented for real ones (review 010 S-02). The implementation was tracked as OQ-39 with the shape recorded; nothing blocked it except that no ALPN crate had asked yet. alktty's move of its control surface to the channels path (ADR-049 §5's migration list) and the general stub ruling (review 010) make that answer invalid.

The design constraint from ADR-039: ChannelManager is auth-blind, ALPN-blind, transport-blind. The control-handle surface must not reintroduce ALPN interpretation into the manager.

Decision

1. The control callback type and registry

pub type ChannelControl = Arc<
    dyn Fn(Value) -> BoxFuture<'static, Result<Value, CallError>> + Send + Sync,
>;

The callback receives the message value (ALPN-specific JSON; the channels layer does not interpret it) and returns the ALPN-specific reply (or a CallError it wants surfaced verbatim — e.g. target_gone). The live handle: HashMap<u32, ChannelControl> in the ChannelManager's internals (the same map discipline as channels; parking_lot mutex).

The manager stays blind to what a callback does — it stores opaque closures, exactly as ChannelState already stores opaque handler_task handles.

2. Registration: the establisher contributes the control handle

The ALPN crate registers a control callback by returning it from its open-op establisher: Establishment::with_control(callback) (builder, alongside with_reply_field — ADR-049 amendment 3). The wrapper installs it under the allocated/adopted id (manager.set_control_handle(id, callback)) and every teardown path removes it (teardown_channel drops the entry; clear_all drains them) — same closure-lifecycle discipline as the opener ledger.

Why the establisher, not the handler: the establisher already returns typed payloads the wrapper routes (Establishment), it runs where the ALPN crate has prepared its backend (the resize target is the backend the establisher dialled/planned), and adding a registration path through the OpenHandler would require handing the handler the manager and the id — a signature break across every published consumer for zero benefit. The wrapper-owned installation also makes the callback's lifetime exactly the channel's lifetime by construction.

3. The op: routing and error surface

make_control_handler (the ADR-037 op, registered by ChannelOps::register_on) becomes: validate channel_id (unchanged) → channel exists? (unchanged; channel:unknown_channel) → callback registered for the id? →

case reply
unknown channel channel:unknown_channel (existing class)
no callback registered channel:no_control_handle — typed, retryable false, message: the channel's ALPN registered no control surface (details: { alpn })
callback ok { "control": <callback reply> }
callback err the callback's CallError verbatim

channel:no_control_handle is a real capability answer, not a stub: a consumer that calls channel/control on a channel whose ALPN chose not to expose control learns exactly that. The message body is ALPN-specific JSON (unchanged; the channels layer does not read it).

The callback receives the message only — no identity, no OperationContext. The authorization home is the op's own access_control (checked by the registry before the handler, per ADR-010/017 — the same op-level ACL every other surface runs); a per-owner resize is expressed as ADR-011 resource_id_path on the channel/control spec for ALPNs that want it, not by widening the callback signature. Wire-visibility: channel:no_control_handle joins the stable error set (ADR-016 — consumers branch on it deliberately); the callback's reply shape is the producing crate's schema concern.

4. What this does not decide

Ordering-sensitive control rides the data channel (unchanged — channel-operations.md's control-message division, DP-4 pinned). The manager keeps its blindness; the enumerators registry of ADR-053 is the analogous ALPN-level surface for discovery, not control.

Consequences

Positive: the advertised op serves; S-02 dies; ALPN crates get out-of-band control for free (resize/signal arrive as one routed call-op instead of per-crate in-band framing); teardown-bound callback lifetime is structural.

Negative: the manager's internals gain one map (opaque closures — no blindness regression); a control callback racing teardown can observe a torn-down backend through its own error (CallError) — the same shape the handler EOF path already produces, not a new class.

Door type

One-way (wire-visible). channel:no_control_handle and the reply envelope shape join the stable surface once consumers branch on them. The callback type, the establisher carrier, and the map are two-way-door implementation details inside it (the Establishment carrier is #[non_exhaustive] — ADR-049 amendment 2's posture).

Verification gates

  • channel/control routes to the establisher-contributed callback and the reply is the callback's output (e2e over a real channels connection);
  • channel:no_control_handle on a channel whose ALPN registered none;
  • channel:unknown_channel preserved (the pre-existing error);
  • teardown removes the handle (post-teardown control answers channel:unknown_channel, never a stale callback);
  • callback Err passes through verbatim.

References

  • Review 010 S-02 (the stub; the implement ruling), review 011 §4 (downstream-safe; implement preferred)
  • OQ-39 (the promoted design note this ADR resolves)
  • ADR-037 (the op's contract), ADR-047 (openable ALPNs; the wrapper this registration rides), ADR-049 (the establisher carrier this registers through), ADR-039 (manager blindness), ADR-011 (resource_id_path — the per-owner control-gate lever)