Files
alkhttp/docs/reviews/006-alkcall-0.3.0-consequence-review.md
T
glm-5.3-flash 802d94ec07 docs(review 007 Unit 2): WS-31 discovery-shadowing note + record corrections
- WS-31: websocket.md §"Data channels for browsers" + ADR-067's landed
  note record that WS-session discovery is the bootstrap set — the
  hook's bootstrap `services/*` registrations overwrite a
  base-registry `services/*` registration on the WS path by design
  (a deployment's custom `services/list` is shadowed on WS sessions
  only).
- ADR-048's landed note: correction + completion — the WS-26
  retention sentence was aspirational at the landed commit (WS-28) and
  is now real; the UP-02 posture's override half is now an explicit
  surface (`with_ws_op_register_acl` / `OpRegisterAcl`), with the
  note that `ChannelsPolicy` could not carry an op ACL.
- ADR-067's landed note: review-007 notes (WS-28 fix + gate, WS-29
  surface, WS-31 record).
- OQ-05 resolution: the retention claim carries the WS-28 correction.
- review-006 UP-02 log + WS-26 paragraph: corrections marking what the
  pre-fix tree did not have, with the landed remediation named.
- review-002 WS-17: the "bounded at 64 sessions" claim corrected —
  the bare-registry semaphore was per-request and bounded nothing;
  `SessionSlots` is the shared-cap surface.
- review-007 status: open for remediation → remediated, with the
  decisions taken (both "implement" options) and the gate names.

Verification: cargo test 454 passed / 0 failed; cargo doc --no-deps
clean (6 pre-existing warnings, identical at baseline).

Review: docs/reviews/007-ws-data-channel-surface-review.md
2026-09-05 05:45:31 +00:00

28 KiB
Raw Blame History

Review 006 — alkcall 0.3.0 consequence review (WS-24/WS-25 upstream, alkhttp residual)

Status

All units landed: Unit 2 + Unit 3 remediated and verified (030c5ef); Unit 4 (docs reconciliation) landed with the 0.3.1 consumption (Cargo.lock bump + the services/list-peers discovery gate). UP-02 decided (recorded below). UP-03 fixed in alkcall 0.3.1 (fd21230, ADR-030) and asserted by the extended WS-path gate. See Remediation log.

Scope

Consequence review of alkcall 0.3.0 (published 2026-09-04 — the remediation of alkcall reviews 004005, which is the upstream resolution of this repo's review-003 findings WS-24/WS-25) against this crate. The pass answers three questions:

  1. What does the 0.3 bump itself cost here (source breaks, behavior changes)?
  2. Which review-003 findings are now resolved upstream, and which remain alkhttp-side work?
  3. Did the upstream remediation introduce anything this crate must absorb (new protocol codes, new wiring contracts, new decisions)?

Sources read: alkcall review 004 (docs/reviews/004-…md, remediated) and review 005 (docs/reviews/005-…md, resolved G-01..G-05), the 0.3.0 changelog, the ADR amendments (alkcall ADR-047 §4 amendment #2, ADR-022 amendments 2026-09-03 + 2026-09-04), and the published 0.3.0 crate source. Every claim below verified in source at this tree (df86f89, consuming alkcall 0.3.0) or the published crate.

Findings use the new prefix UP (upstream-consequence); review 003's WS numbering is referenced for continuity.

Baseline verification (this pass)

cargo test                                → 453 passed, 0 failed
cargo test --all-features                 → 575 passed, 0 failed
cargo clippy --all-targets -- -D warnings → clean
cargo clippy --all-features --all-targets -- -D warnings → clean
cargo fmt --check                         → clean
cargo doc --no-deps                       → clean

Verified after the 0.2 → 0.3 bump (3dee46a). The bump cost was two source-break classes, both mechanical:

  • ChannelClient::take_call_connection now returns Option<Arc<CallConnection>> (was a bare CallConnection) — WssSession::connect dropped its Arc::new re-wrap (src/adapters/from_wss.rs:268-274).
  • OperationRegistry::register, ChannelOperations::register_on, and ChannelCore::register_openable now take &OperationRegistry (was &mut) — 51 stale mut bindings dropped via clippy --fix across src/adapters/{from_wss,to_mcp, to_openapi}.rs, src/gateway/routes.rs, src/server/adapter.rs, and tests/.

The &self-registration change is the interior-mutability change that makes the per-session fork surface possible; for every existing alkhttp call site it is source-compatible (reborrow). The registration() → owned and list_operations() → owned return-type changes required no edits here (call sites already cloned through the borrow or consumed by value-compatible iteration — verified by the clean build).

Verdict

  • WS-24 and WS-25 are resolved upstream. The two design gaps review 003 Part B identified are now mechanized in alkcall 0.3.0 and decided in its ADRs: the per-session fork is the dispatch-registry mechanism (alkcall ADR-047 §4 amendment #2 — review-003 option (a), the wire-proven shape), and op/register is the client-side registration mechanism (alkcall ADR-022 amendment 2026-09-03 — review-003's bootstrap-op candidate). The alkhttp-side consequences of review-003 Unit 1's candidates are exactly what landed.
  • One upstream remediation result lands here for free: review 005 Unit 1 mechanized the accept-side pending-resolution arms into run_loop_single_stream (the loop our WS channel-0 dispatch runs). Pre-0.3, an accept-side nested-composing handler (a from_call imported-op stub riding the WS connection) could deadlock against the inline dispatch; post-0.3 the loop spawns Once invocations and Sub pumps, so the G-01 hazard class is shut for WS sessions by the bump alone — no alkhttp code involved.
  • The remaining review-003 findings are all still open and now unblocked (WS-20/21/22/26 mechanical, WS-27 cleanup, WS-23 tests): the hook still discards the ChannelManager (src/websocket/upgrade.rs:255), no channel ops / openables / bootstrap discovery are registered, no deployment surface declares openable ALPNs, and no live connection handle is retained. All grep-verified at this tree.
  • One new finding (UP-01): alkcall 0.3 introduced the ALREADY_EXISTS protocol code; this crate's gateway error table has no arm for it and would map it to 500. Unreachable today (only op_register_handler emits it), load-bearing the moment Unit 2 wires op/register.
  • One wiring contract Unit 2 must honor (UP-02, recorded as a decision point, not a defect): the op/register collision policy binds the WS session's handler to the session fork.

Severity legend

Same scale as review 003: [major] — a decided behavior is missing/wrong or a real hazard; [minor] — drift, convention violation, or doc/spec inconsistency with no correctness impact.


Part A — What 0.3.0 resolves for this crate

WS-24 [was major, cross-crate] — dispatch-resolution mechanism → RESOLVED upstream

alkcall ADR-047 §4 amendment #2 (2026-09-03) decides option (a): the install_channel_zero hook forks the deployment's base registry, registers the per-session ops on the fork (ChannelOperations:: register_on, ChannelCore::register_openable, install_bootstrap_discovery — the last closed over the fork so services/list sees per-session openables, review-004 F-06), and dispatches channel 0 over the fork. The connection overlay remains the peer-announced-op landing zone and nested-invocation target, not the dispatch-resolution path. The reference shape is alkcall's fork_registry_open_op_resolves_and_is_discoverable gate (alkcall/src/channels/client.rs:1994 — fork → register_openableinstall_bootstrap_discoveryDispatcher::new(fork, …)), which is the shape Unit 2 must reproduce in install_channel_zero.

Upstream also resolved the fork's discovery half (review-004 F-06): install_bootstrap_discovery registers services/list, services/list-peers, and services/schema closed over the fork itself, so per-session openables are discoverable without a re-registration walk. The bootstrap set is closed at four with op/register (alkcall ADR-022 amendment, services/list-peers added 2026-09-04 per review-005 G-05).

alkhttp consequence: none to code yet; Unit 2 consumes it.

WS-25 [was major, cross-crate] — client-side op registration → RESOLVED upstream

alkcall ADR-022 amendment (2026-09-03) decides the bootstrap-op mechanism: op/register (registry::op_register module) — a peer announces an op spec over channel 0; the hub's per-session handler rebuilds the spec, wraps it as a forwarding stub that nested-calls back over channel 0, and lands it in the connection overlay via register_imported (forced Visibility::Internal, provenance FromCall). The wire stays the six call.* envelope kinds. Collision policy (2026-09-04 sub-amendment, review-005 G-03): an announced op may replace other announced ops (replace governs) but never the serving side's own registrations — ALREADY_EXISTS regardless of replace; the handler gates on the serving registry first, then the overlay.

The serving half it composes on (Dispatcher::serve_single_stream, opt-in via ChannelClient::from_connection_with_serving) landed with review-005 Unit 1's concurrency rework: Once invocations, Sub pumps, and sink response writers are spawned in both single-stream loops, so same-connection nested composition (the flagship hub-composes-peer-op flow) resolves concurrently instead of deadlocking to the 30 s sweeper (review-005 G-01, empirically verified upstream with a since-removed probe, productized as two 5s-bounded gates).

alkhttp consequences: Unit 2 wires op/register on WS sessions by registering op_register_spec(acl) + op_register_handler(connection, fork) on the per-session fork (the collision set is the fork — exactly what the session dispatches over, mirroring the upstream gates' production shape). Two additional facts from review 005 apply:

  • The channel-0 Connection must carry the peer identity for compose_root_env to attach the connection's overlay (keyed by identity.id). alkhttp's hook already does this (src/websocket/upgrade.rs:264 — identity propagated from the upgrade's AuthContext); verified correct as-is.
  • The serving loop's concurrency means a hub handler that composes an announced op resolves live (no sweeper); no alkhttp-side scheduling work is needed.

ADR-048's browser bidirectionality promise is now mechanized on the hub half; the browser half (announcing via WS framing) is a client-side concern with no alkhttp code beyond serving the op. The re-point edits in ADR-048/OQ-05 (committed df86f89) record this.

Fixed-upstream items this crate inherits

  • Accept-side nested-composition hazard (review-005 G-01's second consequence): mechanized shut by the pending-resolution arms now in run_loop_single_stream — inherited by the bump, no alkhttp code.
  • resource_id_path wire round-trip (review-005 G-04): a from_call-imported or announced op declaring ownership-scoped resource extraction previously rebuilt with resource_id: None (ACL checks ran without the resource id). Both round-trip halves now carry the field. from_wss's import path inherits the fix.
  • Head-of-line blocking / interleaved directions: a serving side's outbound calls resolve while it serves a Sub (the interleaved-directions gate upstream) — relevant to any future serving-enabled from_wss (see Part C).

Part B — Findings at this tree

UP-01 [minor] — The ALREADY_EXISTS protocol code is unmapped in the gateway error table

Verified: YES. alkcall 0.3 added CallError::already_exists ("ALREADY_EXISTS", non-retryable — alkcall-0.3.0/src/protocol/wire.rs:124) for the op/register collision policy; its own ADR-022 amendment records the code. alkhttp's status map (src/gateway/error.rs:57-80) has no arm for it: NOT_FOUND, FORBIDDEN, INVALID_INPUT, INVALID_OPERATION_TYPE, TIMEOUT, INTERNAL, and HTTP_* prefixed codes are mapped; anything else falls to STATUS_INTERNAL (500). alkcall's ADR-016 code list (six codes) was not amended for the seventh — upstream doc drift, noted for that repo.

Reachability today: none — the only emitter is op_register_handler, which no alkhttp path invokes yet (grep verified; the gateway dispatches base-registry ops directly). It becomes reachable exactly when Unit 2 wires op/register on WS sessions, at which point a collision rejection would surface to wire callers as 500 with an ALREADY_EXISTS body instead of a conflict status. Because the collision path is also how a peer experiences the reconnect-reannounce contract (replace refused for serving-registry names), fidelity matters there.

Fix (Unit 2, ride-along): map ALREADY_EXISTS → 409 Conflict in call_error_to_http_status_with_identity (plus the test arm), matching ADR-023's HTTP_<status> discipline for the new code. One-line change + test; no consumer breaks (the code is new).

UP-02 [minor, decision point] — op/register's AccessControl on the WS surface needs a decided default

Not a defect — a decision Unit 2 cannot avoid. alkcall's op_register_spec(access_control) takes the ACL as a parameter; its doc notes the default (AccessControl::default()) lets any peer register, and the op is Mutation/External-typed. This crate's convention (AGENTS.md §13, ADR-015): adapter-registered ops are Visibility::Internal by default — but op/register cannot be Internal on the wire surface (the peer must call it; Internal ops resolve spec-404 to wire callers). The analogous precedent here is the built-in gateway's NoCap posture for channel opens (review-001 SRV-10: crate default is permissive, deployments gate via the ChannelsPolicy extension). The consistent shape: register op/register on the per-session fork with AccessControl::default() on the built-in surface and let an assembly-layer override pass a stricter ACL the same way ChannelsPolicy/OpenableAlpn flow — record the posture in the Unit-2 task and in ADR-048's amendment note when the wiring lands.

WS-20/WS-21/WS-22/WS-26 [major] — unchanged, now unblocked

Re-verified at this tree; all four stand as review 003 found them:

  • install_channel_zero still discards the manager (src/websocket/upgrade.rs:255_manager unused); no ChannelCore, no ChannelOperations::register_on (grep: zero callers in this crate), no openables.
  • No OpenableAlpn surface exists (src/server/adapter.rs builders unchanged: with_decoy / with_extra_routes / with_ws_max_sessions / with_ws_idle_timeout).
  • No live Arc<CallConnection> handle is retained by the session (the hook moves channel 0's connection into the spawned task); WsSessions still tracks pump handles only.

The fix shape from review 003 Unit 2 stands, now with the upstream mechanism named concretely: per-session fork() in the hook → ChannelOperations::register_on(&fork) → openables via register_openable(spec, handler, &fork, auth)install_bootstrap_discovery(&fork)op_register_spec + op_register_handler(Arc<CallConnection>, fork) → dispatch over the fork (Dispatcher::run_loop_single_stream unchanged as the loop). The OpenableAlpn list + with_ws_openable_alpns threading (RouterState → SessionState → hook, request-extension fallback mirroring ChannelsPolicy) is unchanged, as is the WS-26 handle-retention design (eviction hooks into WsSessions).

WS-27 [minor] — unchanged; folds into the Unit-2 hook rework

adapter_install_channel_zero (src/websocket/upgrade.rs:290-295) is still #[allow(dead_code)] with its only caller test-gated (src/adapters/from_wss.rs test server). The rework gives both callers a real hook; fold the cfg cleanup there.

WS-23 [minor] — unchanged; Unit 3's e2e gates gain two upstream-mirroring gates

The browser-opened-channel e2e test is still absent (no test opens a data channel through a live WS session). Unit 3's five scenarios stand as written. Two gates to add, mirroring what review 005 taught: services/list on a live WS session lists the per-session openable (the F-06 shape), and the op/register announce → hub nested-composition → routes-back-to-consumer flow (the stub path — the one review-005's original gate missed and G-02 called out).


Part C — Noted, not findings

  • Connect-side serving for from_wss: alkcall 0.3's from_connection_with_serving makes the WSS consumer half capable of serving inbound calls (the from_wss session could answer hub→consumer calls). from_wss keeps the pure-consumer default (from_connection, resolution-only pump — verified unchanged); opt-in serving is a deliberate later decision (it changes from_wss's security posture: a dial would then execute inbound requests against a caller-chosen registry). Recorded here so the opportunity is visible; no finding, no unit.
  • op/register forced-Internal semantics vs AGENTS.md §13: the announced op lands Internal/FromCall on the hub — consistent with the adapter-registered-ops-are-Internal convention (it is composition material, not wire-callable from the hub side). No conflict; noted to preempt an apparent tension.
  • list_operations returns External only — the to_openapi/to_mcp projections and the gateway /search continue to see exactly the External set; unchanged from 0.2.
  • Upstream doc drift (alkcall-side, not ours): ADR-016's protocol-code list ("six codes", last amended by ADR-021) does not name ALREADY_EXISTS. The 2026-09-04 ADR-022 collision sub-amendment records the code's use; ADR-016's list is stale. Flagged for the next alkcall doc pass; UP-01 is the alkhttp-side half of the same gap.

Remediation log

Unit 2 — alkhttp wiring (WS-20/21/22/25/26/27 + UP-01) — LANDED (030c5ef)

Hook rework (WS-20/21/25). install_channel_zero (src/websocket/upgrade.rs) now takes (registry, sessions, policy, openable_alpns) and, per the alkcall ADR-047 §4 amendment #2 shape (verified against alkcall's own e2e reference fork_registry_open_op_resolves_and_is_discoverable):

  1. Forks the deployment's base registry.
  2. Registers on the fork: ChannelOperations::register_on (the generic channel/close / channel/control / channel/resources/subscribe — WS-21), the deployment's openables via ChannelCore::register_openable (WS-22), install_bootstrap_discovery closed over the fork (F-06 — the session's services/list sees its own openables), and op/register (WS-25 — op_register_spec + op_register_handler(connection, fork); the collision set is the fork, the production shape review-005's gates use).
  3. Dispatches over the fork (Dispatcher::run_loop_single_stream unchanged as the loop; the loop's pending-resolution arms are the review-005 G-01 mechanization the bump inherited).

UP-02 decided. op/register is registered with AccessControl::default() on the built-in surface (the SRV-10 precedent: permissive crate default), and an assembly layer overrides via a stricter policy passed through the hook — the posture mirrors ChannelsPolicy. The override surface rides the existing policy parameter; the assembly layer's ChannelsPolicy value is the same object the open wrappers consult. (Review 007 WS-29 correction: the ChannelsPolicy object is an Arc<dyn ChannelLifecyclePolicy> — the channel-cap policy — and cannot carry an operation AccessControl; at this commit the hook hardcoded the permissive default and no override surface existed. Remediation implemented the surface as decided: HttpAdapter::with_ws_op_register_acl + the OpRegisterAcl request extension, review 007 Unit 2. The posture above stands as recorded; only the "rides the existing policy parameter" mechanism was wrong.)

Openable-ALPN surface (WS-22). OpenableAlpn { spec, open_handler } + HttpAdapter::with_ws_openable_alpns(Vec<..>), threaded RouterStateSessionStaterun_channels_session → hook, with the OpenableAlpns request-extension fallback on the upgrade handler (mirroring ChannelsPolicy/WsTimeouts). Default: no openables (channel 0 only).

WS-26. WsSessions gains a connections map + live_connections() / live_connection_count(); the hook retains the channel-0 Arc<CallConnection> under a self-removing ConnectionGuard (dropped when the channel-0 task ends — any teardown path). Shared only when a shared WsSessions instance is in play (the bare-registry/test-server path passes None). (Review 007 WS-28 correction: at this commit the guard was bound inside the if let block and dropped microseconds after insertion — the retention sentence above described the intended scope, not the landed one. Remediation bound the guard as an Option<ConnectionGuard> in the channel-0 task's frame; the gate live_connections_visible_mid_session_and_drain_after_teardown pins it.)

Policy threading. The hook takes the session's resolved ChannelsPolicy (the extension resolution, NoCap default) and passes it to both ChannelOperations and ChannelCore — one policy instance across the open wrappers and the demux-loop teardown path. (First cut used a fresh default_policy() per hook; the ledger-decrement gate caught it — the ChannelsPolicy extension was never consulted. Fixed by threading; that interaction is exactly the seam SRV-10's extension was built for.)

UP-01. ALREADY_EXISTS → 409 Conflict in call_error_to_http_status_with_identity + the test arm; the module doc names the seventh code.

from_wss import exclusion (new in this unit, surfaced by the gates). The reworked hook serves the bootstrap + channel-lifecycle set on every WS session, so a from_wss import's services/list listing now carries them. Proxying session-scoped machinery would be nonsense (channel/close proxied to the remote would forward a close for this session's channel ids; op/register proxied duplicates the import itself). FromWss::import now runs discovery first and passes the listing minus the protocol-session ops (services/list, services/schema, services/list-peers, op/register, channel/close, channel/control, channel/resources/subscribe) as the operation_filter — the from_call filter surface, used for the first time by alkhttp. The three from_wss discovery tests assert the original op sets and pass.

WS-27. adapter_install_channel_zero cfg-gated to exactly its caller's cfg (all(test, feature = "server", feature = "wss")); the #[allow(dead_code)] is gone; it inherits the reworked hook (the from_wss test-server producer now serves the session ops too).

Gate results (Unit 3, tests/ws_upgrade_session.rs):

  • data_channel_open_discoverable_and_bytes_round_tripservices/list lists the session's openable + generic channel ops
    • op/register (F-06 shape); open op returns an even channel_id; a data chunk echoes back through the OpenHandler's Connection.
  • channel_close_resolves_and_decrements_the_opener_ledger — close resolves {"closed": true}; the ledger drops to 0.
  • channel_cap_denies_open_over_the_limit — the second open over the cap denies with the channel:too_many_channels code (the ChannelsPolicy extension is consulted).
  • disconnect_mid_open_tears_down_and_decrements — hard disconnect → the demux teardown decrements the ledger, no leak.
  • too_large_data_channel_chunk_survives_demux_resync — a MAX_CHUNK_LEN + 1 declared chunk is skipped (the skip consumes the declared payload off the transport, so the gate sends the filler), the next chunk routes and echoes — resync through the WS path (scenario 5).
  • op_register_served_per_session_and_collision_is_already_exists — announce resolves registered: true; a second announce without replace rejects with the overlay gate's ALREADY_EXISTS; announcing the serving side's own op name with replace: true rejects ALREADY_EXISTS (the G-03 gate, through the WS path).
  • disconnect_mid_call_does_not_hang_the_server and the full pre-existing suite stay green.

Test-harness hardening the gates required (recorded so the next pass doesn't re-derive it): call_and_await filters by request id (a prior Sub's trailing call.completed may interleave) and tolerates data-channel chunks; await_envelope is the same filter for hand-built frames; CallError-payload envelopes serialize flat (env.payload["code"], not env.payload["error"]["code"]).

Verification (post-fix):

cargo test                                → 454 passed, 0 failed
cargo test --all-features                 → 582 passed, 0 failed
cargo clippy --all-targets -- -D warnings → clean
cargo clippy --all-features --all-targets -- -D warnings → clean
cargo fmt --check                         → clean
cargo doc --no-deps                       → clean

Unit 4 — spec reconciliation (docs) — LANDED (with the 0.3.1 consumption)

As planned in this review's remediation plan: OQ-05 resolved, ADR-067 v1-cut note + ADR-048 amendment note updated to the landed state, websocket.md §"Data channels for browsers" status block removed (the section now documents the landed surface: with_ws_openable_alpns, the OpenableAlpns fallback, cap policy, discovery, gates), the idle-knob deployment note surfaced (silent data channels make the 60 s default bite more often; semantics unchanged), the op/register ACL posture (UP-02's decision) recorded in ADR-048's landed note, and the UP-03 alkcall-side gap closed by 0.3.1 and asserted alkhttp-side (the extended op/register gate). The alknet-ADR-044 §5 pointer was checked: not stale (the rationale lives in this crate's ADR-044 §5; the alknet-ADR-043 pointers are correctly marked parked/not-ported).


Findings added during remediation

UP-03 [minor, alkcall-side] — services/list-peers cannot list peer-announced ops; PeerCompositeEnv does not override peer_operations

Discovered by the Unit-3 gates: the first draft asserted announced-op discoverability through services/list-peers and failed — the peers array carries only peer_id entries with empty operation lists.

Verified in both the published 0.3.0 crate and the alkcall tree: services_list_peers_handler maps ctx.env.peer_operations(&peer_id) per peer (alkcall/src/registry/discovery.rs:347-351), but PeerCompositeEnv overrides peer_ids only — peer_operations falls to the trait default (Vec::new(), alkcall/src/registry/env.rs:63-65); the connections map and connection_order it would list from are in scope but unused there. OverlayOperationEnv likewise overrides contains only. Net effect: the peer-keyed listing shows peer_id entries whose operations array is always empty, and the ADR-022 amendment's "announced op is discoverable via services/list-peers" promise does not resolve on the wire today. The alkcall gates never caught it because the F-05 gate calls the announced op directly (bypassing discovery) and the G-02 gate composes it — neither lists peers after announcing.

Fix lives in alkcall (one method override: PeerCompositeEnv::peer_operations(peer) → the connection env's overlay registration names, ACL-filtered like the local listing; OverlayOperationEnv may want a matching helper). Flagged for the next alkcall review pass; the alkhttp-side gate (op_register_served_per_session_and_collision_is_already_exists) asserts the overlay-landing + collision semantics instead, which is the behavior Unit 2 actually wires, and is not blocked by UP-03.

LANDED in alkcall 0.3.1 (2026-09-04, fd21230): implemented as alkcall ADR-030 — OperationEnv gains a defaulted list_operation_names; OverlayOperationEnv overrides it; PeerCompositeEnv::peer_operations delegates to the peer overlay, and its list_operation_names aggregates session + connections + base. Upstream gate: announced_op_is_discoverable_via_services_list_peers. The version pin here is 0.3 (semver-compatible), so the fix arrives through a Cargo.lock bump with no source change; the WS-path gate that originally surfaced UP-03 now asserts the discovery promise — op_register_served_per_session_and_collision_is_already_exists gained the services/list-peers step (announce → the alice peer entry lists consumer/exec) and passes against 0.3.1 (it fails against 0.3.0, verified by the original draft).

UP-02 decision (recorded)

op/register is served per WS session with AccessControl::default() on the built-in surface (the SRV-10 posture: the crate default is permissive, deployments gate via the ChannelsPolicy extension whose value the hook now consults). The wire-callability tension in this review's UP-02 note resolves the same way: the peer must call op/register (it is in the bootstrap set), so Internal visibility was never an option; the ACL parameter is the deployment's gate, not the visibility flag.


Verification log (this pass)

  • Bump verified empirically: cargo check (default + all-features) surfaced exactly the two break classes; post-fix gates all green (453 / 575 / clippy both / fmt / doc).
  • WS-24/WS-25 resolutions read in the published 0.3.0 crate source (registry::op_register handler + collision gate; Dispatcher::serve_single_stream; install_bootstrap_discovery; OperationRegistry::fork) and in the upstream ADR amendment texts; the reference fork shape read at alkcall/src/channels/client.rs:1988-2130.
  • Review-003 residual claims re-verified at this tree: hook discards manager (upgrade.rs:255), zero register_on callers, no openable surface, no retained connection handle, WS-27 caller census unchanged.
  • UP-01 verified: no ALREADY_EXISTS arm in src/gateway/error.rs:57-80; emitter census in 0.3.0 = op_register.rs only; fall-through confirmed to STATUS_INTERNAL.
  • UP-02 verified: op_register_spec signature and its default-ACL doc note; AGENTS.md §13 vs wire-callable tension resolved against the SRV-10 precedent (permissive crate default + extension override).
  • Cross-repo state verified: alkcall review 005 resolved (G-01..G-05 remediation log), 0.3.0 published with cargo semver-checks 196 checks against 0.2.0 (changelog's Changed section matches the two source-break classes this crate experienced).