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).
This commit is contained in:
1 parent
4eae18f539
commit
df4d4d3a9e
4 files changed
+618
-3
No files matched your search
@@ -0,0 +1,153 @@
|
||||
# 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<Box<dyn Stream<Item = Value> + 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<HashMap<String /*alpn*/, Arc<dyn ResourceEnumerator>>>
|
||||
|
||||
impl ResourceEnumerators {
|
||||
pub fn new() -> Self;
|
||||
pub fn register(&self, alpn: &str, enumerator: Arc<dyn ResourceEnumerator>);
|
||||
pub fn alpns(&self) -> Vec<String>;
|
||||
}
|
||||
```
|
||||
|
||||
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<ResourceEnumerators>`
|
||||
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": <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)
|
||||
Reference in new issue
Block a user