feat: allocation moves into the channels establisher (alkcall 0.6.0 / review 007 R-01) — bump to 0.3.0
- Bump alkcall 0.5.0 -> 0.6.0 (Establishment plan payload, ADR-049 amendment 2); alktty 0.2.0 -> 0.3.0 (breaking channels-path allocation-failure shape) - make_tty_establisher runs backend.allocate; failure maps to EstablishmentError::DialFailed (channel:open_failed reason dial_failed); the TtyHandle crosses to the pump handler via a private per-open AllocatedHandle one-shot slot (the handle is not Sync — it cannot be the ChannelPlan directly) - make_tty_open_handler gains the Option<ChannelPlan> parameter; a plan drives the new drive_session_pre_allocated (pumps only), a None plan falls back to drive_session_pre_negotiated (inline validate-and-allocate — defense-in-depth for no-establisher registrations) - tty_open_spec's channel:open_failed ErrorDefinition declares dial_failed (four reachable reasons) - Pinned in-band allocate test flipped: allocate_failure_fails_open_as_dial_failed (end-to-end, no channel survives); establisher unit gate for the plan slot + failure mapping - ADR-010 amended (§2A; §2 kept as historical record), tty-adapter.md, session/channels/adapter docs, CHANGELOG, AGENTS.md alkcall pin updated Verification: cargo test (113) + --all-features (137), clippy -D warnings (native + wasm32, default + all-features), fmt, doc, wasm32 check — all clean
This commit is contained in:
@@ -10,6 +10,19 @@ error frame is retained — two transports, two contracts").
|
||||
Prerequisites: alkcall 0.5.0 (`ChannelCore::register_openable_with_establisher`,
|
||||
`channel:open_failed`, typed `ChannelOpenError`).
|
||||
|
||||
**Amended (2026-09-07, alkcall 0.6.0 / ADR-049 amendment 2 — review
|
||||
007 R-01):** `Establishment` gained its plan payload
|
||||
(`ChannelPlan = Arc<dyn Any + Send + Sync>`), removing §2's
|
||||
payloadless-`Establishment` blocker. Backend `allocate` moves into
|
||||
the establisher; the allocated `TtyHandle` crosses to the pump
|
||||
handler via the plan; `allocate_failed` joins the call-error surface
|
||||
as `details.reason == "dial_failed"` (the target refused or the
|
||||
backend lacked capacity). No failure class arrives in-band on the
|
||||
channels path from a registered producer; the in-band error-frame
|
||||
fallback stays for no-establisher registrations (defense-in-depth)
|
||||
and the direct-ALPN path. See §2A below; §2 is retained as
|
||||
historical record.
|
||||
|
||||
Amends ADR-009's R4 amendment: the in-band error-frame path on the
|
||||
channels path shrinks to one failure class (`allocate_failed`).
|
||||
|
||||
@@ -68,18 +81,26 @@ the vocabulary is alkcall's one-way wire surface):
|
||||
| `carriage != "raw"`, empty `cmd` | `HandlerError` | `handler_error` |
|
||||
| unknown backend | `UnknownResource` | `unknown_resource` |
|
||||
| ownership denial | `HandlerError` | `handler_error` |
|
||||
| **allocation failure** (amended — §2A) | `DialFailed` | `dial_failed` |
|
||||
| establishment deadline | (wrapper) | `timeout` |
|
||||
|
||||
`dial_failed` and `resource_shortage` have no TTY producer path (TTY
|
||||
dials nothing at open time; `allocate` capacity failures are the
|
||||
in-band class below) — the spec's `ErrorDefinition` for
|
||||
`channel:open_failed` declares only the three reachable reasons.
|
||||
`HandlerError` covers both the malformed-negotiation semantics (the
|
||||
request shape is wrong) and the ACL-outcome ownership denial (an
|
||||
authorization failure, not a dial failure).
|
||||
`resource_shortage` has no TTY producer path (allocation-capacity
|
||||
failures map to `dial_failed` — the target refused or the backend
|
||||
lacked capacity; §2A). `HandlerError` covers both the
|
||||
malformed-negotiation semantics (the request shape is wrong) and the
|
||||
ACL-outcome ownership denial (an authorization failure, not a dial
|
||||
failure).
|
||||
|
||||
### 2. Backend allocation stays in the pump handler — `allocate_failed` stays in-band
|
||||
|
||||
> **Amended away (2026-09-07) — see §2A.** This section is the
|
||||
> historical record of the pre-0.3.0 shape; its blocker 1
|
||||
> (payloadless `Establishment`) was resolved by alkcall 0.6.0 (ADR-049
|
||||
> amendment 2, review 007 R-01). Blocker 2 (double-`allocate`
|
||||
> unsoundness) is avoided by construction in the amended shape: the
|
||||
> establisher's handle *is* the handle the handler pumps — there is no
|
||||
> second allocate to be unsound about.
|
||||
|
||||
The one failure class that cannot migrate: `backend.allocate`. Two
|
||||
reasons, both structural:
|
||||
|
||||
@@ -114,6 +135,60 @@ no-establisher registration still compiles against alkcall's API and
|
||||
must not silently EOF), and the R4 seam test pins the handler-side
|
||||
frame.
|
||||
|
||||
### 2A. Amendment (alkcall 0.6.0): allocation moves into the establisher
|
||||
|
||||
alkcall 0.6.0 (ADR-049 amendment 2, review 007 R-01) filled the
|
||||
reserved field: `Establishment { plan: Option<ChannelPlan> }` with
|
||||
`ChannelPlan = Arc<dyn Any + Send + Sync>` — typed-opaque, wrapper
|
||||
threaded to the `OpenHandler`'s new second parameter. §2's blocker 1
|
||||
is gone; the double-`allocate` hazard (blocker 2) is structurally
|
||||
absent — one allocate per open, whose result the handler consumes.
|
||||
|
||||
The amended shape:
|
||||
|
||||
1. **The establisher allocates.** After the validation sequence (§1),
|
||||
it runs `backend.allocate(¶ms)`. Failure maps to
|
||||
`EstablishmentError::DialFailed` (`details.reason == "dial_failed"`
|
||||
— the target refused or the backend lacked capacity; one string
|
||||
message, no nuance — the same granularity the direct path's
|
||||
`allocate_failed` frame always had). The
|
||||
`channel:open_failed` `ErrorDefinition` on `tty_open_spec()`
|
||||
therefore declares four reachable reasons (`dial_failed`,
|
||||
`unknown_resource`, `handler_error`, `timeout`).
|
||||
2. **The handle crosses via the plan.** `TtyHandle` is not `Sync`
|
||||
(boxed dyn streams), so it cannot be the `ChannelPlan` directly.
|
||||
The establisher wraps it in a private `AllocatedHandle` one-shot
|
||||
slot (`Mutex<Option<TtyHandle>>`), created **per establisher
|
||||
invocation** — per open. The wrapper moves this open's plan to this
|
||||
open's handler (alkcall's concurrent-same-resource test pins that
|
||||
plans are never shared), the handler `take()`s it exactly once, and
|
||||
establisher-before-handler ordering is alkcall's wrapper guarantee
|
||||
(ADR-049 §1). The shared-slot race §2 described is unreachable by
|
||||
construction: there is no shared slot.
|
||||
3. **The handler pumps the pre-allocated handle.** With a plan it
|
||||
calls `drive_session_pre_allocated` (new `pub(crate)` driver: the
|
||||
pumps directly, no validation pass). With `plan: None` — a
|
||||
no-establisher registration — it falls back to
|
||||
`drive_session_pre_negotiated`, whose inline validate-and-allocate
|
||||
keeps the full in-band error-frame vocabulary. That fallback is
|
||||
defense-in-depth for the assembly-layer freedom to register without
|
||||
an establisher, not a registered-producer path.
|
||||
4. **Consumer surface.** `TtySession::open_via_channels` sees
|
||||
allocation failure as
|
||||
`ChannelsOpen(CallFailed{ channel:open_failed, reason:
|
||||
dial_failed })` — a typed call error, branchable and retryable
|
||||
like every other establishment class. The `0x00` peek on the
|
||||
channel stream becomes a formality that no registered producer
|
||||
ever exercises; the pinned test flipped with the contract
|
||||
(`allocate_failure_fails_open_as_dial_failed` in `testing.rs`,
|
||||
plus the establisher unit gate `establisher_allocates_and_maps_
|
||||
failure_to_dial_failed` in `channels.rs`).
|
||||
|
||||
The R-02 lifetime contract (the handler's `JoinHandle` tracks the
|
||||
data-plane lifetime) was already alktty's shape — the open handler
|
||||
awaits the driver inline in its spawned task — so the amendment adds
|
||||
no lifetime changes.
|
||||
|
||||
### 3. The direct-ALPN path is unchanged
|
||||
|
||||
`TtyAdapter::handle` / `drive_session` keep the wire-frame
|
||||
@@ -149,10 +224,10 @@ unchanged (`true` direct, `false` channels — the registry's
|
||||
| malformed negotiation (schema-valid) | establisher | `ChannelsOpen(CallFailed{ channel:open_failed, reason: handler_error })` |
|
||||
| unknown backend | establisher | `ChannelsOpen(CallFailed{ channel:open_failed, reason: unknown_resource })` |
|
||||
| ownership denial | establisher | `ChannelsOpen(CallFailed{ channel:open_failed, reason: handler_error })` |
|
||||
| **allocation failure** (amended — §2A) | establisher | **`ChannelsOpen(CallFailed{ channel:open_failed, reason: dial_failed })`** |
|
||||
| establishment timeout | wrapper bound | `ChannelsOpen(CallFailed{ channel:open_failed, reason: timeout })` |
|
||||
| ACL denial (scope) | registry gate | `ChannelsOpen(CallFailed{ FORBIDDEN })` |
|
||||
| channel cap | wrapper `check_open` | `ChannelsOpen(CallFailed{ channel:too_many_channels })` |
|
||||
| **allocate failure** | pump handler, post-open | **`NegotiationRejected{ "allocate_failed" }`** |
|
||||
|
||||
`TtySessionError::ChannelsOpen` now carries alkcall's typed
|
||||
`ChannelOpenError` verbatim (`#[from]`) instead of a flattened
|
||||
@@ -170,7 +245,8 @@ on `establishment_reason()` without unwrapping strings.
|
||||
retry), `timeout` (maybe retry), `allocate_failed` (capacity —
|
||||
still distinguishable in-band).
|
||||
- The consumer's `open_via_channels` failure is a typed call error —
|
||||
no peeking at the data stream for anything but `allocate_failed`.
|
||||
no peeking at the data stream for anything but the (now
|
||||
unreachable-from-registered-producers) error-frame formality.
|
||||
|
||||
**Negative:**
|
||||
|
||||
@@ -180,9 +256,11 @@ on `establishment_reason()` without unwrapping strings.
|
||||
direct path's failure surface is unchanged.
|
||||
- The `channel:open_failed` reason codes the establisher emits join
|
||||
the wire-stable error set (ADR-049's one-way door).
|
||||
- `allocate_failed` remains a second, in-band failure shape on the
|
||||
channels path — one residual asymmetry, pinned and documented until
|
||||
`Establishment` gains a payload.
|
||||
- **Amendment (0.3.0):** allocation failure changed shape again —
|
||||
`NegotiationRejected{ "allocate_failed" }` (in-band) →
|
||||
`ChannelsOpen(CallFailed{ channel:open_failed, reason:
|
||||
dial_failed })`. The direct path's `allocate_failed` frame is
|
||||
unchanged (two transports, two contracts).
|
||||
|
||||
## Door type
|
||||
|
||||
@@ -205,12 +283,16 @@ ADR moves failure *reporting*, not the negotiation *location*.
|
||||
- ADR-009 (the open op's `input` is the negotiation; the R4
|
||||
amendment's in-band error frames — shrunk to `allocate_failed` by
|
||||
this ADR)
|
||||
- ADR-005 (kill-on-`Drop` — why establisher-side allocation is
|
||||
unsound)
|
||||
- ADR-005 (kill-on-`Drop` — why a *second* establisher-side allocate
|
||||
would be unsound; avoided by construction in the §2A amendment —
|
||||
one allocate per open, consumed by the handler)
|
||||
- ADR-001 (the §5 `0x00` disambiguation — retained for the direct
|
||||
path and the channels `allocate_failed` frame)
|
||||
path; the channels-path peek is now a formality)
|
||||
- ADR-002 (backend selection/allocation ownership)
|
||||
- `src/channels.rs` — `make_tty_establisher`, `make_tty_open_handler`,
|
||||
`tty_open_spec`
|
||||
- alkcall ADR-049 amendment 2 / review 007 R-01 (the `Establishment`
|
||||
plan payload — the upstream capability this ADR's §2A adopts)
|
||||
- `src/channels.rs` — `make_tty_establisher`, `AllocatedHandle`,
|
||||
`make_tty_open_handler`, `tty_open_spec`
|
||||
- `src/session.rs` — `TtySessionError::ChannelsOpen`, `open_via_channels`
|
||||
- `src/testing.rs` — the pinned `allocate_failed`-stays-in-band test
|
||||
- `src/testing.rs` — the pinned `allocate_failure_fails_open_as_dial_failed`
|
||||
test (superseded `allocate_failure_still_arrives_in_band_on_channels_path`)
|
||||
Reference in New Issue
Block a user