docs: step-3 rewrites — OQ-39/40 resolved, C-08 closed, channel-operations.md put/control/resources contracts, review 010/011 closed with the S-01 sketch corrected
- open-questions.md: OQ-39 → ADR-052, OQ-40 → ADR-053 (resolutions replace the deferred-until-needed posture) - channel-operations.md: the put contract (initiator allocates + streams, reserved input key, one response, acks-in-result sentence), the control routing (ADR-052), the enumerator aggregation (ADR-053), the connection-owner paragraph's pub-era correction, the ADR table rows - ADR-051 §6: PubTypedOpen → the put translation; ADR-047's 2026-09-06 amendment records its era honestly (the dynamic half is now ADR-053) - review 010: all four findings resolved (with the S-01 sketch correction so the published record stops carrying the wrong-graph reading) - review 011: closed (all seven pins implemented + gates landed) - review 001 C-08: resolved note (odd/even + adopt, implemented) - AGENTS.md: the 'Stub Guard' section — the not_implemented grep is part of cargo test; the registration invariants and the implement-or-stop ruling are release-blocking Verified: cargo test (696+guard), clippy -D warnings, fmt, doc clean.
This commit is contained in:
1 parent
944c300c17
commit
3317ea0963
10 files changed
+205
-43
No files matched your search
@@ -213,6 +213,28 @@ cargo clippy --target wasm32-unknown-unknown -- -D warnings
|
|||||||
cargo semver-checks check-release # before a release
|
cargo semver-checks check-release # before a release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stub Guard (release-blocking)
|
||||||
|
|
||||||
|
Four registered-but-always-failing stubs shipped across nine published
|
||||||
|
versions (reviews 010/011). This class is now release-blocking in both
|
||||||
|
directions:
|
||||||
|
|
||||||
|
1. **Test guard**: `cargo test --test stub_guard` (part of `cargo
|
||||||
|
test`) fails if the string `not_implemented` appears anywhere in
|
||||||
|
`src/`. A "loud failure" stub is still a stub.
|
||||||
|
2. **Registration-time invariants** (ADR-047 amendment 4 §4.8): a
|
||||||
|
marked (`channel_open`) spec refuses the generic registration path
|
||||||
|
(`OperationRegistry::register`, `CallConnection::register_imported`,
|
||||||
|
`op/register`) and refuses `publish_schema`; the sanctioned
|
||||||
|
registrations are the channel wrapper family
|
||||||
|
(`register_openable*`, `register_pub_openable`,
|
||||||
|
`register_relay_openable`).
|
||||||
|
3. **The ruling**: implement-or-stop-advertising before any publish.
|
||||||
|
The only permitted deferral statement is "not this session" — never
|
||||||
|
"next version", "not v1", or any bucket that parks a decision on an
|
||||||
|
unknown future release. Every handler that registers on a
|
||||||
|
discoverable surface must be capable of succeeding.
|
||||||
|
|
||||||
## Architecture Context
|
## Architecture Context
|
||||||
|
|
||||||
- `docs/architecture/` — the authoritative spec. Read it before
|
- `docs/architecture/` — the authoritative spec. Read it before
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ This crate unifies `alknet-call` and `alknet-channels` from the alknet
|
|||||||
mono-repo, plus the vendored core types formerly in `alknet-core`. The
|
mono-repo, plus the vendored core types formerly in `alknet-core`. The
|
||||||
source architecture docs were ported from
|
source architecture docs were ported from
|
||||||
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall
|
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall
|
||||||
ADRs (ADR-001..051). The ALPN strings (`alk/call`, `alk/channels`)
|
ADRs (ADR-001..053). The ALPN strings (`alk/call`, `alk/channels`)
|
||||||
are wire-stable and unchanged — see ADR-004.
|
are wire-stable and unchanged — see ADR-004.
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
@@ -104,6 +104,8 @@ are wire-stable and unchanged — see ADR-004.
|
|||||||
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | `OpenEstablisher` + `register_openable_with_establisher` (awaited, bounded); typed `channel:open_failed` with `details.reason`; `Establishment.plan` (`ChannelPlan`) threaded to the `OpenHandler` (amendment 2); the `JoinHandle` data-plane lifetime contract |
|
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | `OpenEstablisher` + `register_openable_with_establisher` (awaited, bounded); typed `channel:open_failed` with `details.reason`; `Establishment.plan` (`ChannelPlan`) threaded to the `OpenHandler` (amendment 2); the `JoinHandle` data-plane lifetime contract |
|
||||||
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | `channels::pump_bidi` — shutdown-on-completion two-pump data plane pinned in one place (alknet ADR-078) |
|
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | `channels::pump_bidi` — shutdown-on-completion two-pump data plane pinned in one place (alknet ADR-078) |
|
||||||
| [051](decisions/051-channel-relay-and-hub-leg.md) | In-Tree Channel Relay (`ChannelRelay`) and the Hub-Leg Assembly | ADR-042 amended — the relay implementation is an alkcall export; translate hop = ADR-049 establisher (`register_relay_openable`), byte-forward hop = `pump_bidi`; implicit per-channel id mapping; two-phase registration (discover/stash → fork-register); hub-leg install template; ACL layering note |
|
| [051](decisions/051-channel-relay-and-hub-leg.md) | In-Tree Channel Relay (`ChannelRelay`) and the Hub-Leg Assembly | ADR-042 amended — the relay implementation is an alkcall export; translate hop = ADR-049 establisher (`register_relay_openable`), byte-forward hop = `pump_bidi`; implicit per-channel id mapping; two-phase registration (discover/stash → fork-register); hub-leg install template; ACL layering note |
|
||||||
|
| [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | Establisher-contributed `ChannelControl` (`Establishment::with_control`), wrapper-installed/teardown-removed in the manager; `channel:no_control_handle` capability answer; resolves OQ-39 |
|
||||||
|
| [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | `ResourceEnumerators` (peer-shared registry); snapshot-then-changes enumerator contract; scan-once aggregation; empty seed; resolves OQ-40 |
|
||||||
|
|
||||||
## Relevant Open Questions
|
## Relevant Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: current
|
||||||
last_updated: 2026-08-12
|
last_updated: 2026-10-13
|
||||||
---
|
---
|
||||||
|
|
||||||
# channel-operations.md — Channel Lifecycle on the Call Protocol
|
# channel-operations.md — Channel Lifecycle on the Call Protocol
|
||||||
@@ -60,15 +60,18 @@ Response (`call.responded`):
|
|||||||
|-------|------|---------|
|
|-------|------|---------|
|
||||||
| `channel_id` | u32 | Allocated by the connection owner (ADR-047 §5). In the `Sub` case, the responder (producer). Both sides route chunks with this ID to the new channel. |
|
| `channel_id` | u32 | Allocated by the connection owner (ADR-047 §5). In the `Sub` case, the responder (producer). Both sides route chunks with this ID to the new channel. |
|
||||||
|
|
||||||
**`channel_id` allocation (ADR-047 §5): connection-owner allocates.**
|
**`channel_id` allocation (ADR-047 §5, as amended for the put path by
|
||||||
|
ADR-047 amendment 4): connection-owner allocates.**
|
||||||
The side that holds the `ChannelManager` for that connection allocates
|
The side that holds the `ChannelManager` for that connection allocates
|
||||||
the `channel_id` via a monotonic `AtomicU32` (`next_id.fetch_add(1,
|
the `channel_id` via a monotonic `AtomicU32` (odd/even split — connect
|
||||||
Relaxed)`) and returns it in the response. In the `Sub` case the
|
side starts at 1, accept at 2) and returns it in the response. In the
|
||||||
responder owns the connection and allocates; in the `Pub` case the
|
`Sub` case the responder owns the connection and allocates — one
|
||||||
initiator owns and allocates. One round-trip before data flows — the
|
round-trip before data flows (the same round-trip every call makes).
|
||||||
same round-trip the call protocol makes for every operation. All
|
In the `Pub` case (the put contract above) the initiator owns,
|
||||||
current channel types (TTY, tunnel, SSH) already require a negotiation
|
allocates, and streams bytes **before** the open resolves — the
|
||||||
round-trip, so the open round-trip is not additive latency.
|
early-arrival parking covers the adopt race. All current channel
|
||||||
|
types (TTY, tunnel, SSH) already require a negotiation round-trip, so
|
||||||
|
the open round-trip is not additive latency.
|
||||||
|
|
||||||
**Error codes** (`CallError.code` strings):
|
**Error codes** (`CallError.code` strings):
|
||||||
|
|
||||||
@@ -110,19 +113,80 @@ the existing teardown machinery, unchanged. Head-of-line safety: the
|
|||||||
serving loop spawns Once invocations as independent tasks, so a slow
|
serving loop spawns Once invocations as independent tasks, so a slow
|
||||||
establisher on one open op does not block other calls on channel 0.
|
establisher on one open op does not block other calls on channel 0.
|
||||||
|
|
||||||
### `channels/<alpn>/pub` — publish a binary stream
|
### `channels/<alpn>/pub` — publish a binary stream (the put contract, ADR-047 amendment 4)
|
||||||
|
|
||||||
`OperationType::Pub` (ADR-046). The initiator publishes a stream of
|
A marked `Pub`-typed open op is the **put** shape: the data plane is
|
||||||
`call.published` events to the responder; the responder's `SinkHandler`
|
the adopted channel's `BiStream` — never the call plane. The contract,
|
||||||
consumes them and returns a single `call.responded`. The
|
pinned end-to-end (ADR-047 amendment 4; review 011's corrections are
|
||||||
`channel_open` marker on the op spec tells the channels layer the
|
the authoritative shape — a serving-loop `PublishStream` sketch was
|
||||||
stream is binary — the `call.published` chunks carry binary payloads
|
considered and rejected, it is ADR-046's *unmarked* pub semantics):
|
||||||
on a data channel (not JSON on channel 0).
|
|
||||||
|
1. **The initiator allocates and writes immediately.** The putter
|
||||||
|
allocates the channel id on its own side (connect side → odd, per
|
||||||
|
the ADR-047 §5 odd/even split), inserts the **reserved input key
|
||||||
|
`"channel_id"`** into the open op's input, and streams bytes on the
|
||||||
|
channel before the open resolves — the early-arrival parking
|
||||||
|
(ADR-051's tested pre-adopt buffer) covers the responder-adopts
|
||||||
|
race. Zero `call.published` on marked ops: chunks on the call plane
|
||||||
|
are rejected loudly with `channel:publish_chunks_rejected` (the
|
||||||
|
first chunk writes the rejection in the initiator's direction; the
|
||||||
|
handler is never fed). An unmarked `Pub` op is an ordinary ADR-046
|
||||||
|
sink (JSON chunks, `publish_schema`); it does not ride the channel
|
||||||
|
machinery.
|
||||||
|
2. **The responder adopts.** `ChannelCore::register_pub_openable` is
|
||||||
|
the only registration seam: the wrapper strips the reserved key
|
||||||
|
pre-validation (dispatch layer, before `input_schema`) and adopts
|
||||||
|
the given id (`adopt_channel` — the remote parity range enforced;
|
||||||
|
failure replies `channel:open_failed` with reason `adopt_failed`).
|
||||||
|
3. **The wrapper is the responder of record:** `check_open` → adopt →
|
||||||
|
bounded establisher (offer validation + ack fields +
|
||||||
|
`Establishment::with_control`, ADR-049 amendment 3's mechanism) →
|
||||||
|
the `PubOpenHandler` runs on the adopted channel's `Connection`,
|
||||||
|
**awaited inline** → the standard completion teardown → **one
|
||||||
|
response**, merge order pinned:
|
||||||
|
|
||||||
|
```
|
||||||
|
{ "channel_id": <adopted> } → ack fields → handler result
|
||||||
|
```
|
||||||
|
|
||||||
|
(the handler result wins on key collision — its computed result
|
||||||
|
postdates the ack; a handler `Err` IS the response, and the channel
|
||||||
|
tears down).
|
||||||
|
4. **The ack-timing sentence (protocol semantics):** a sink op gets
|
||||||
|
exactly one `call.responded`, produced after the handler completes
|
||||||
|
— a putter stream-waits nothing; the establishment ack is
|
||||||
|
informative-in-result, never a gating ack. The fetch/put asymmetry
|
||||||
|
is direction-inherent: fetch (`Sub`) acks at establishment (the
|
||||||
|
`{channel_id}` response precedes any byte); put (`Pub`) acks in the
|
||||||
|
result. A putter that waits for the ack deadlocks — this sentence
|
||||||
|
is the contract, not per-consumer folklore.
|
||||||
|
5. **The putter-facing client shape:**
|
||||||
|
`ChannelClient::open_pub_channel(operation_id, input, alpn)`
|
||||||
|
allocates the odd id, sends the open op in the background, and
|
||||||
|
returns `(id, send, recv, reply)` immediately; `reply.resolve()`
|
||||||
|
is the awaited merged response.
|
||||||
|
|
||||||
|
**Error codes** (the pub path):
|
||||||
|
|
||||||
|
| code | meaning | retryable |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| `channel:open_failed` | establishment failure (ADR-049 §3 vocabulary) or adoption failure — new reason `adopt_failed` (collisions, cap, out-of-range reserved ids; ADR-047 amendment 4 §4.4) | false |
|
||||||
|
| `channel:publish_chunks_rejected` | `call.published` chunks on a marked op (the data plane is the channel, ADR-047 amendment 4 §4.1) | false |
|
||||||
|
| `INVALID_INPUT` | absent/non-u32 reserved `channel_id` key on a marked pub op (ADR-047 amendment 4 §4.3) | false |
|
||||||
|
|
||||||
The hub-as-proxy pattern (ADR-042, ADR-047 §1) composes on top: a
|
The hub-as-proxy pattern (ADR-042, ADR-047 §1) composes on top: a
|
||||||
worker publishes to the hub, the hub owns the resource, and consumers
|
worker publishes to the hub, the hub owns the resource, consumers
|
||||||
subscribe from the hub. The broker (topic registry, `Pub`↔`Sub`
|
subscribe from the hub. Marked pub openables relay through the put
|
||||||
matching) is a hub concern, not an alkcall concern (ADR-047 §1, Gap B).
|
translation (ADR-047 amendment 4 §4.7): the hub allocates its own
|
||||||
|
connect-side id, opens the spoke op in the background (put
|
||||||
|
acks-in-result — an establisher awaiting the spoke's response would
|
||||||
|
deadlock the leg), pumps consumer bytes → spoke leg, and mirrors the
|
||||||
|
spoke's single response with its reserved key stripped.
|
||||||
|
|
||||||
|
Registration invariants (ADR-047 amendment 4 §4.8 — the stub-class
|
||||||
|
guard): a marked spec refuses the generic registration path and
|
||||||
|
refuses `publish_schema`; `register_openable*` refuse `Pub`-typed
|
||||||
|
specs; `register_pub_openable` refuses unmarked/non-`Pub` specs.
|
||||||
|
|
||||||
## The generic ops (keyed by `channel_id`)
|
## The generic ops (keyed by `channel_id`)
|
||||||
|
|
||||||
@@ -159,7 +223,7 @@ TTY's 5-byte payload format); for tunnels it is the last data byte
|
|||||||
before close. This invariant crosses two channels (the data channel and
|
before close. This invariant crosses two channels (the data channel and
|
||||||
channel 0), so the channels layer owns the ordering guarantee.
|
channel 0), so the channels layer owns the ordering guarantee.
|
||||||
|
|
||||||
### `channel/control` — out-of-band control on channel 0
|
### `channel/control` — out-of-band control on channel 0 (implemented, ADR-052)
|
||||||
|
|
||||||
For control that doesn't need ordering relative to data (resize, signal,
|
For control that doesn't need ordering relative to data (resize, signal,
|
||||||
keepalive):
|
keepalive):
|
||||||
@@ -174,11 +238,23 @@ keepalive):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The channels layer routes `message` to the handler's control handle for
|
The channels layer routes `message` to the channel's control callback
|
||||||
`channel_id`. The `message` JSON is ALPN-specific; the channels layer
|
(ADR-052): the ALPN crate contributes it through its open-op
|
||||||
does not interpret it.
|
establisher (`Establishment::with_control`); the wrapper installs it
|
||||||
|
under the allocated/adopted id and every teardown path removes it. The
|
||||||
|
`message` JSON is ALPN-specific; the channels layer does not interpret
|
||||||
|
it. The reply is `{ "control": <callback output> }`, or:
|
||||||
|
|
||||||
### `channel/resources/subscribe` — live resource discovery
|
| code | meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `channel:unknown_channel` | no such channel (pre-existing class) |
|
||||||
|
| `channel:no_control_handle` | the channel's ALPN registered no control surface — a capability answer, not a failure (ADR-052 §3); data-ordered control rides the data channel (DP-4 below) |
|
||||||
|
| (callback's own code) | the callback's `CallError` verbatim |
|
||||||
|
|
||||||
|
Control that MUST be ordered relative to data rides the data channel's
|
||||||
|
`BiStream` (the control-message division below is unchanged).
|
||||||
|
|
||||||
|
### `channel/resources/subscribe` — live resource discovery (implemented, ADR-053)
|
||||||
|
|
||||||
**This is a `Sub` operation (ADR-021), not a polled Query.** The call
|
**This is a `Sub` operation (ADR-021), not a polled Query.** The call
|
||||||
protocol has `StreamingHandler` / `invoke_streaming` (implemented and
|
protocol has `StreamingHandler` / `invoke_streaming` (implemented and
|
||||||
@@ -193,8 +269,21 @@ start/stop.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The responder registers a `StreamingHandler` that emits a
|
Each openable ALPN crate provides a **resource enumerator**
|
||||||
`ResponseEnvelope` whenever the resource set changes. Each event:
|
(ADR-053 §1 — snapshot-then-changes: the first item is the current
|
||||||
|
payload, subsequent items are full replacements; the producing crate
|
||||||
|
owns its change detection) at assembly/install time:
|
||||||
|
`ResourceEnumerators::register(alpn, enumerator)`, one shared registry
|
||||||
|
per accepting peer. The handler aggregates the enumerators registered
|
||||||
|
at subscription start (the **scan-once** pin — a later registration is
|
||||||
|
invisible to an in-flight subscription; re-subscribe):
|
||||||
|
|
||||||
|
- **non-empty registry:** each enumerator item becomes one event,
|
||||||
|
`{ "output": { "resources": [ { "alpn": …, …payload } ] } }` — the
|
||||||
|
first events across the ALPNs are the initial snapshot;
|
||||||
|
- **empty registry:** exactly one `{ "resources": [] }` event, then
|
||||||
|
the stream stays open (subscribe, read the first event, cancel —
|
||||||
|
the poll use case).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -471,6 +560,8 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
|||||||
| [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler | The machinery `channel/resources/subscribe` uses |
|
| [021](decisions/021-streaming-handler-for-subscriptions.md) | StreamingHandler | The machinery `channel/resources/subscribe` uses |
|
||||||
| [046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | Pub Operation Type | The `Pub`/`Sub` primitives the per-ALPN open ops build on |
|
| [046](decisions/046-publish-operation-type-and-handler-kind-sink.md) | Pub Operation Type | The `Pub`/`Sub` primitives the per-ALPN open ops build on |
|
||||||
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | The establisher hook; typed `channel:open_failed`; `Establishment.plan` (amendment 2); the `JoinHandle` lifetime contract |
|
| [049](decisions/049-channel-open-establishment-phase.md) | Channel-Open Establishment Phase | The establisher hook; typed `channel:open_failed`; `Establishment.plan` (amendment 2); the `JoinHandle` lifetime contract |
|
||||||
|
| [052](decisions/052-channel-control-surface.md) | `channel/control` Control-Handle Surface | The establisher-contributed `ChannelControl`; wrapper-install/teardown-removal; `channel:no_control_handle` |
|
||||||
|
| [053](decisions/053-resource-enumerators.md) | Live Resource Enumerators | The snapshot-then-changes enumerator contract; scan-once aggregation; the empty seed |
|
||||||
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | The data-plane helper openable-ALPN handlers await inline |
|
| [050](decisions/050-pump-bidi-two-pump-helper.md) | `pump_bidi` Two-Pump Helper | The data-plane helper openable-ALPN handlers await inline |
|
||||||
| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed opens (and why the cap is per direct-caller, not per `forwarded_for`) |
|
| [026](decisions/026-forwarded-for-identity.md) | Forwarded-For Identity | The auth chain for hub-relayed opens (and why the cap is per direct-caller, not per `forwarded_for`) |
|
||||||
| [011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | Dynamic Resource Ownership | The parallel — a channel slot is a resource, the cap is a quota check; `resource_id_path` works again under per-ALPN ops |
|
| [011](decisions/011-dynamic-resource-ownership-for-runtime-spawned-resources.md) | Dynamic Resource Ownership | The parallel — a channel slot is a resource, the cap is a quota check; `resource_id_path` works again under per-ALPN ops |
|
||||||
|
|||||||
@@ -366,7 +366,9 @@ half load-bearing for the first time and asked for a decision on the
|
|||||||
static half. Decision: **both halves of the "static per-op" split gain
|
static half. Decision: **both halves of the "static per-op" split gain
|
||||||
what is cheap today; the dynamic half stays deferred** (OQ-40, now
|
what is cheap today; the dynamic half stays deferred** (OQ-40, now
|
||||||
with a "load-bearing for alktunnels discovery UI" note in
|
with a "load-bearing for alktunnels discovery UI" note in
|
||||||
`open-questions.md`; alktunnels v1 uses config-known op names):
|
`open-questions.md`; alktunnels v1 uses config-known op names).
|
||||||
|
*[Amendment 4 era, 2026-10-13: the dynamic half is now implemented —
|
||||||
|
ADR-053; this 2026-09-06 text records the state at its writing.]*
|
||||||
|
|
||||||
- `OperationSpec` gains `description: Option<String>` — a human-
|
- `OperationSpec` gains `description: Option<String>` — a human-
|
||||||
readable op description, set via `with_description` at registration.
|
readable op description, set via `with_description` at registration.
|
||||||
|
|||||||
@@ -205,11 +205,13 @@ same pin.
|
|||||||
|
|
||||||
### 6. Bounds: rejected shapes and residual notes
|
### 6. Bounds: rejected shapes and residual notes
|
||||||
|
|
||||||
- **Pub-typed open ops** — rejected loudly (`channel:pub_open_not_implemented`,
|
- **Pub-typed open ops** — ~~rejected loudly~~ **translated** (ADR-047
|
||||||
the C-08 blocker). The relay registers only `Query`/`Mutation`-typed
|
amendment 4 §4.7 — the put-forwarding translation: the hub allocates
|
||||||
ops (plain forwarding) and `Sub`-typed marked ops (relay openables);
|
its connect-side id, opens the spoke op in the background,
|
||||||
a discovered `Pub`-typed marked spec is an assembly error, not a
|
pumps the consumer bytes → spoke, and mirrors the spoke's single
|
||||||
silent stub.
|
response with its reserved key stripped; `PubTypedOpen` is removed).
|
||||||
|
The rejection remains for the genuinely-unsupported shapes: unmarked
|
||||||
|
specs (`UnmarkedSpec`) only.
|
||||||
- **Compounding establishment bounds** — each hop's establisher is
|
- **Compounding establishment bounds** — each hop's establisher is
|
||||||
bounded independently (ADR-049 §2); an N-hop chain adds N bounds.
|
bounded independently (ADR-049 §2); an N-hop chain adds N bounds.
|
||||||
The consumer's dispatch deadline (`min(deadline, 10s)` per hop)
|
The consumer's dispatch deadline (`min(deadline, 10s)` per hop)
|
||||||
@@ -263,7 +265,6 @@ same pin.
|
|||||||
`ChannelRelay` instead of re-deriving the hub shape.
|
`ChannelRelay` instead of re-deriving the hub shape.
|
||||||
- U-1's gate 2 (the review's silent-failure scenario — flavor-form ops
|
- U-1's gate 2 (the review's silent-failure scenario — flavor-form ops
|
||||||
relaying as plain forwarding stubs) becomes a permanent e2e gate.
|
relaying as plain forwarding stubs) becomes a permanent e2e gate.
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
|
|
||||||
- New pub API (`ChannelRelay`, the registration entry point, the
|
- New pub API (`ChannelRelay`, the registration entry point, the
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: current
|
||||||
last_updated: 2026-08-12
|
last_updated: 2026-10-13
|
||||||
---
|
---
|
||||||
|
|
||||||
# Open Questions
|
# Open Questions
|
||||||
@@ -82,8 +82,8 @@ is the load-bearing piece the broker composes on.
|
|||||||
| OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" |
|
| OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" |
|
||||||
| OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it |
|
| OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it |
|
||||||
| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) |
|
| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) |
|
||||||
| OQ-39 | `channel/control` control-handle surface | open | medium | The `channel/control` handler currently returns `channel:control_not_implemented`. The control-handle surface (per-channel control callbacks registered by ALPN crates, routing `message` to the handler's control handle for `channel_id`) is real design work — each ALPN crate needs a way to register a control callback, and the channels layer needs a control-handle registry keyed by `channel_id`. Deferred until an ALPN crate (TTY, tunnel) needs out-of-band control. |
|
| OQ-39 | `channel/control` control-handle surface | resolved | medium | ADR-052 — the control-handle surface is implemented: per-channel control callbacks contributed by the ALPN establisher (`Establishment::with_control`), wrapper-installed and teardown-removed in the manager; the op routes the ALPN-specific `message` and answers `channel:no_control_handle` when the channel's ALPN registered none. The stub (`channel:control_not_implemented`) was removed with the single-publish fix (reviews 010 S-02 / 011). |
|
||||||
| OQ-40 | `channel/resources/subscribe` live subscription | open | medium | The `channel/resources/subscribe` handler currently returns `channel:resources_not_implemented`. The live subscription aggregated from ALPN-crate resource enumerators (ADR-047 §6) requires each ALPN crate to provide a resource enumerator, and the channels layer to aggregate them into a live `Stream` that emits on any change. The current stub is a one-shot error; the real implementation is deferred until a consumer (hub, dashboard) needs live resource discovery. **Load-bearing for alktunnels' discovery UI** (review 006 E-02): a consumer cannot distinguish produced tunnel resources by op name alone. The static half is covered (0.5.0: `OperationSpec.description` on `services/list` — describes the op, not the live resource set); the dynamic half stays deferred — alktunnels v1 uses config-known op names. |
|
| OQ-40 | `channel/resources/subscribe` live subscription | resolved | medium | ADR-053 — the live subscription is implemented: one `ResourceEnumerator` per openable ALPN (provided by the producing crate, snapshot-then-changes), aggregated by the handler over the scan-once snapshot; an empty registry emits one `{ "resources": [] }` seed and stays open. The one-shot error stub was removed with the single-publish fix (reviews 010 S-03 / 011). The alktunnels discovery-UI note (review 006 E-02) is served by the enumerator surface. |
|
||||||
| OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). |
|
| OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). |
|
||||||
|
|
||||||
## Core Types
|
## Core Types
|
||||||
|
|||||||
@@ -962,7 +962,11 @@ promises.
|
|||||||
vice versa) as the smaller change; record the decision in ADR-047 §5
|
vice versa) as the smaller change; record the decision in ADR-047 §5
|
||||||
or a follow-on ADR. Then implement the adoption path: the
|
or a follow-on ADR. Then implement the adoption path: the
|
||||||
non-allocating side registers the remotely-assigned id in its
|
non-allocating side registers the remotely-assigned id in its
|
||||||
manager/mux.
|
manager/mux. **[RESOLVED 2026-10-06/13: the implemented odd/even
|
||||||
|
split (connect odd, accept even) + `ChannelManager::adopt_channel`;
|
||||||
|
the marked-pub contract rides it end-to-end — ADR-047 amendment 4;
|
||||||
|
the C-08-blocker stub class this decision previously gated on is
|
||||||
|
closed with reviews 010/011's single-publish fixes.]**
|
||||||
- C-12: reject `channel_id: 0` in the `channel/close` handler.
|
- C-12: reject `channel_id: 0` in the `channel/close` handler.
|
||||||
- C-13: `channel/close` must drain (signal EOF to the handler, await
|
- C-13: `channel/close` must drain (signal EOF to the handler, await
|
||||||
pump completion) before `abort`/decrement; implement REQ-CH-06
|
pump completion) before `abort`/decrement; implement REQ-CH-06
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ ops-put task hits `channel:pub_open_not_implemented` when its Sink op
|
|||||||
carrying the `channel_open` marker is invoked, exactly as the code
|
carrying the `channel_open` marker is invoked, exactly as the code
|
||||||
comments predict. The downstream project is paused on this.
|
comments predict. The downstream project is paused on this.
|
||||||
|
|
||||||
|
**All four findings resolved 2026-10-13** — implemented in alkcall
|
||||||
|
main ahead of the single 0.9.0 publish (see the resolution note under
|
||||||
|
S-01's remediation options; the pinned stub tests are deleted and the
|
||||||
|
`not_implemented` source-tree guard is part of the test suite,
|
||||||
|
`tests/stub_guard.rs`, wired into AGENTS.md's "Stub Guard" section).
|
||||||
|
|
||||||
Policy context: shipping stubs to crates.io is now a hard failure per
|
Policy context: shipping stubs to crates.io is now a hard failure per
|
||||||
the repo owner's ruling. Any handler that registers successfully and
|
the repo owner's ruling. Any handler that registers successfully and
|
||||||
returns an error of the form `*:not_implemented` on every invocation
|
returns an error of the form `*:not_implemented` on every invocation
|
||||||
@@ -94,13 +100,37 @@ least fails loudly.
|
|||||||
|
|
||||||
**Remediation options (pick one before the next release):**
|
**Remediation options (pick one before the next release):**
|
||||||
|
|
||||||
|
> **CORRECTED 2026-10-13 (reviews 011 + ADR-047 amendment 4):** the
|
||||||
|
> implemented fix is option 1, but option 1's sketch below was the
|
||||||
|
> wrong shape for marked ops and is superseded — the correct shape is
|
||||||
|
> review 011's (also pinned as ADR-047 amendment 4): the initiator
|
||||||
|
> allocates and streams bytes on the channel before the open
|
||||||
|
> resolves; the responder adopts (`register_pub_openable`); the
|
||||||
|
> wrapper is the responder of record, awaiting the handler and
|
||||||
|
> merging ack fields + handler result into ONE response; zero
|
||||||
|
> `call.published` on marked ops (chunks rejected loudly);
|
||||||
|
> `RelayError::PubTypedOpen` is replaced by the relay put
|
||||||
|
> translation. Handing the `OpenHandler` the serving loop's
|
||||||
|
> `PublishStream` (the sketch below) is ADR-046's *unmarked* pub
|
||||||
|
> semantics and would have re-validated MB-scale binary payloads as
|
||||||
|
> JSON call chunks. The downstream agent caught this in review 011
|
||||||
|
> before the amendment was implemented.
|
||||||
|
|
||||||
|
> **RESOLVED 2026-10-13** — implemented in alkcall main (the single
|
||||||
|
> 0.9.0 publish, review 011 §6): S-01 via the marked-pub contract
|
||||||
|
> above, S-02/S-03 implemented (ADR-052/ADR-053), S-04
|
||||||
|
> forward-through (ADR-022, 2026-10-13 amendment), the
|
||||||
|
> `not_implemented` source-tree guard wired into AGENTS.md's
|
||||||
|
> "Stub Guard" section, and the pinned stub tests deleted.
|
||||||
|
|
||||||
1. **Implement the wrapper** — allocate the channel id, run the
|
1. **Implement the wrapper** — allocate the channel id, run the
|
||||||
establishment phase (reply fields carry the offer ack, per ADR-047
|
establishment phase (reply fields carry the offer ack, per ADR-047
|
||||||
§1 and ops-put's acceptance criteria), hand the `OpenHandler` the
|
§1 and ops-put's acceptance criteria), hand the `OpenHandler` the
|
||||||
`PublishStream` already produced by the serving loop along with the
|
`PublishStream` already produced by the serving loop along with the
|
||||||
channel's `Connection`. All plumbing exists; this is wrapper glue,
|
channel's `Connection`. All plumbing exists; this is wrapper glue,
|
||||||
plus deciding the OpenHandler-reads-channel-vs-reads-PublishStream
|
plus deciding the OpenHandler-reads-channel-vs-reads-PublishStream
|
||||||
question in an amended ADR-047.
|
question in an amended ADR-047. *[Superseded sketch — see the
|
||||||
|
correction above; review 011's shape is the pinned one.]*
|
||||||
2. **Reject at registration** — return an error from
|
2. **Reject at registration** — return an error from
|
||||||
`register_openable_with_establisher` for `OperationType::Pub`,
|
`register_openable_with_establisher` for `OperationType::Pub`,
|
||||||
mirroring `RelayError::PubTypedOpen` (src/channels/relay.rs:147-156),
|
mirroring `RelayError::PubTypedOpen` (src/channels/relay.rs:147-156),
|
||||||
|
|||||||
@@ -2,13 +2,22 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Open — filed 2026-10-13. From alkblobs (the downstream crate paused on
|
Closed — filed 2026-10-13. From alkblobs (the downstream crate paused on
|
||||||
review 010 S-01; its ops-put task gates on this surface). Purpose:
|
review 010 S-01; its ops-put task gates on this surface). Purpose:
|
||||||
correct review 010's S-01 remediation sketch before the amendment is
|
correct review 010's S-01 remediation sketch before the amendment is
|
||||||
drafted, enumerate what the amendment must pin so one publish
|
drafted, enumerate what the amendment must pin so one publish
|
||||||
(0.9.0) fixes all four stubs right, and record the downstream
|
(0.9.0) fixes all four stubs right, and record the downstream
|
||||||
requirements and non-requirements from the consumer seat.
|
requirements and non-requirements from the consumer seat.
|
||||||
|
|
||||||
|
**Closed 2026-10-13:** all seven §2 pins are implemented and pinned in
|
||||||
|
ADR-047 amendment 4 / ADR-052 / ADR-053 / ADR-022's 2026-10-13
|
||||||
|
amendment, with the §3 verification gates landed as tests
|
||||||
|
(`channels::operations::pub_open_tests`,
|
||||||
|
`channels::hub_leg::gate2_tests::gate_pub_relay_put_end_to_end`, the
|
||||||
|
registration-invariant tests in `registry::registration` and
|
||||||
|
`registry::op_register`), the guard is `tests/stub_guard.rs`, and the
|
||||||
|
docs rewrites below are done. The §6 sequencing concluded.
|
||||||
|
|
||||||
Input basis: alkblobs ADR-014
|
Input basis: alkblobs ADR-014
|
||||||
(`/workspace/@alkdev/alkblobs/docs/architecture/decisions/
|
(`/workspace/@alkdev/alkblobs/docs/architecture/decisions/
|
||||||
014-marked-pub-put-contract-and-alkcall-gate.md`, committed 7ac0099)
|
014-marked-pub-put-contract-and-alkcall-gate.md`, committed 7ac0099)
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ impl ChannelRelay {
|
|||||||
/// - `spec` must carry the `channel_open` marker (reconstructed
|
/// - `spec` must carry the `channel_open` marker (reconstructed
|
||||||
/// WITH the marker by discovery, ADR-047 amendment 3). A
|
/// WITH the marker by discovery, ADR-047 amendment 3). A
|
||||||
/// `Pub`-typed marked spec composes through the put-forwarding
|
/// `Pub`-typed marked spec composes through the put-forwarding
|
||||||
/// translation (see [`Self::register_relay_pub_openable`]); an
|
/// translation (`register_relay_pub_openable`; ADR-047 amendment 4
|
||||||
|
/// §4.7); an
|
||||||
/// unmarked spec is a
|
/// unmarked spec is a
|
||||||
/// [`RelayError::UnmarkedSpec`] error — branch on the
|
/// [`RelayError::UnmarkedSpec`] error — branch on the
|
||||||
/// marker before calling.
|
/// marker before calling.
|
||||||
|
|||||||
Reference in new issue
Block a user