# ADR-053: `channel/resources/subscribe` — Live Resource Enumerators ## Status Accepted — implements ADR-037 §6's dynamic half and ADR-047 §6's discovery split ("what is currently there"). Resolves review 010 S-03 (the `channel/resources/subscribe` stub: a fake one-element error stream shipped nine published versions, pinned by tests, tracked as OQ-40) by implementing the aggregated live subscription. OQ-40's promotion: each ALPN crate provides a resource enumerator; the channels layer aggregates them into a live `Stream`. ## Context The static half is covered (ADR-047's 2026-09-06 amendment: `OperationSpec.description` on `services/list` describes the op — not the produced resource set). The dynamic half — "which tunnel resources may I open, live" — was deferred (OQ-40) and then shipped as a stub; a consumer discovering the op got a capability that does not exist (review 010 S-03). The load-bearing consumer (alktunnels' discovery UI, review 006 E-02) uses config-known op names today, but the stub ruling makes deferral unavailable as a posture: registered means implementable. ADR-037 §6 + ADR-047 §6 pinned the shape: each ALPN crate that registers open ops provides a resource enumerator (which containers are running; which TTY sessions are active); `channel/resources/subscribe` aggregates across registered openable ALPNs; the data source lives in the ALPN crate. What was never pinned: the registration seam, the enumerator contract, and the aggregation semantics. This ADR pins them. ## Decision ### 1. The enumerator contract ```rust pub type ResourceStream = Pin + Send>>; pub trait ResourceEnumerator: Send + Sync + 'static { /// Snapshot-then-changes: the first item is the ALPN's complete /// current resource payload; each subsequent item replaces it /// (a full snapshot, not a delta). The channels layer does not /// interpret the payload. fn subscribe(&self) -> ResourceStream; } ``` Each item is the **ALPN's full current resource payload** — an object merged with `"alpn"` into the wire entry (`{ "resources": [{ "alpn": "alk/tty", ...payload }] }`; a non-object payload wraps as `{ "alpn": …, "data": … }`). The snapshot-then-changes contract lives in the producing crate (a `watch` channel or similar is its own machinery) — the channels layer never polls an enumerator mid-life. ### 2. Registration seam ```rust pub struct ResourceEnumerators; // channels-call; RwLock>> impl ResourceEnumerators { pub fn new() -> Self; pub fn register(&self, alpn: &str, enumerator: Arc); pub fn alpns(&self) -> Vec; } ``` Registered by the composing crate at assembly/install time (the same seam that composes the policy and the openables — the composer has the backend handles). The registry is shared per accepting peer (like `PerIdentityChannelPolicy`, ADR-041): one `Arc` across every channels connection that peer accepts. **Scan-once:** the merged stream's enumerator set is scanned when the subscription starts; ALPNs registered later are invisible to in-flight subscriptions (re-subscribe to see them). Registrations happen at install time, before any subscription — a late-registration notify machinery is machinery nobody needs, and this pin prevents a future half-design of it. ### 3. The op: aggregation semantics `ChannelOperations::with_resource_enumerators(manager, policy, enumerators)` threads the registry to the aggregate handler; the plain `ChannelOperations::new(manager, policy)` constructor keeps an empty registry. `make_resources_subscribe_handler` (the `Sub` op — ADR-021 machinery, unchanged) becomes a real subscription: - **Non-empty registry:** merge every registered enumerator's stream (`futures::stream::select_all`), each item mapped to a one-entry `{ "resources": [{ "alpn": , ...payload }] }` envelope. The first events across the ALPNs are the initial snapshot (each enumerator's first item); subsequent events are the live updates. - **Empty registry:** one `{ "resources": [] }` envelope, then the stream stays open (the poll use case — subscribe, read the first event, cancel — gets its snapshot; per the scan-once pin, nothing further arrives until re-subscribe). - **Zero enumerators ever registered but the op discoverable** is impossible to distinguish from "no resources right now" — by design: the static half answers "which ALPNs accept open ops" (`services/list`), the dynamic half answers "what is currently there"; a peer registering openables without enumerators reports an empty live resource set, which is the truthful answer for "the channels layer does not interpret your resources" (ADR-035 posture). The `access` preview stays dropped (ADR-047 §6 — the spec is the authority). The documented wire shape (channel-operations.md) is unchanged — this is the implementation of the documented op, not a new wire surface. ## Consequences **Positive:** the advertised op serves (S-03 dies); ALPN crates carry their resource truth themselves (the channels layer stays ALPN-blind — it maps and merges opaque values); the hub/dashboard consumer gets the live subscription ADR-037 promised. **Negative:** `ChannelOperations` gains a second constructor (additive — `new` keeps the empty default); an ALPN crate that never registers an enumerator serves an always-empty resource set — truthful, and `services/list` still discloses the ops (the static half is not encumbered by the dynamic one). ## Door type **Two-way (implementation shape) inside the documented wire surface.** The wire shape (`{ "resources": [...] }`, per-ALPN entries, no `access` preview) is ADR-037/047's pre-existing one-way surface, now actually served. The enumerator trait, the scan-once rule, the empty seed, and the registration seam are revisable implementation details — no consumer observes them beyond the stream of envelopes. ## Verification gates - initial snapshot: N registered enumerators → N first events carrying each ALPN's payload merged with `"alpn"`; - live update: an enumerator's second item arrives as the next envelope without re-subscription; - empty registry: exactly one `{ "resources": [] }` envelope, stream stays open, subscriber cancel works (abort cascade, ADR-020); - late registration invisibility: a post-subscription registration does not appear in an in-flight subscription (the scan-once pin); - non-object payload wrapping. ## References - Review 010 S-03 (the stub; the implement ruling), review 011 §4 (downstream-safe; implement preferred) - OQ-40 (the promoted design note this ADR resolves; the "load-bearing for alktunnels' discovery UI" note) - ADR-037 §6 (the dynamic half's contract), ADR-047 §6 (the discovery split; the 2026-09-06 amendment's static half) - ADR-021 (the `StreamingHandler` machinery this op rides), ADR-039 (manager blindness — the enumerators' channels-call home), ADR-041 (the shared-per-peer construction pattern the registry follows)