Commit Graph
27 Commits
Author SHA1 Message Date
glm-5.3-flash 9620ee7b2a test(review 009): all seven coverage findings; errata on three as-filed claims
Review 009's coverage debt in full — the paths the review-008 gates
never walk. No wire or API changes.

- C-1: pin the plain-bundle install-failure arm (un-compilable
  input_schema; the as-filed duplicate-name route does not fail
  registration) — the install task ends before the dispatch loop,
  channel 0 never dispatches. ADR-051 §5 gains the loud-install
  coverage note: relay-openable + plain-bundle arms pinned,
  generic-ops + bootstrap-discovery arms documented as
  best-effort-loud (crate-internal specs compile by construction).
- C-2: the HubLegImports filter — filtered closure path + only
  partition unit-tested (errata: only was already pinned at filing);
  the empty-stash e2e gate (generic ops + discovery only, dropped
  ops resolve NOT_FOUND).
- C-3: the batch-form reserved-reply-key rejection pinned (reason
  handler_error, teardown, ledger decrement, no pump spawn).
- C-4: open_channel_with_reply's failure path pinned e2e (the typed
  error carries the channel:open_failed code + details reason/message).
- C-5: both byte-identical claims golden-pinned — the no-fields reply
  against {"channel_id": 2} and the standard-shape wire payload
  against the full 9-key literal.
- C-6: derivation edge shapes pinned — channels//sub, channels//direct,
  channels → None; the 4-segment strict superset annotated as the
  pre-amendment behavior change (errata: actual is Some("x/sub"), the
  multi-segment-ALPN rule, not the as-filed Some("alk/x/sub")).
- C-7: builder overwrite pinned last-win (single + batch) with the
  doc sentence on with_reply_field.

Verification: 682 tests pass, clippy -D warnings clean, fmt clean,
doc clean, wasm32 check clean.

File: docs/reviews/009 (resolved; errata marked per finding)
2026-09-18 08:02:13 +00:00
glm-5.3-flash 54c2a3f941 fix(review-008 audit): adopted-entry drop guard; explicit-ALPN guards; review 009
Post-landing audit of the 0.7.1 -> 0.8.0 remediation diff: two
hardening guards, one rejection-posture fix, two log/message
corrections, and the deferred coverage debt filed as review 009.

- RelayPlan owns the producer-leg ChannelManager and reclaims the
  adopted spoke channel_id via a Drop guard (replaces the pump
  handler's post-pump_bidi explicit reclaim). Closes the leak
  windows the pump's normal path cannot reach: the wrapper's
  establishment bound expiring after the adopt, and the pump
  handler's early-return arms (plan absent, downcast failure,
  try_unwrap failure, accept_bi failure). The send-half drop still
  EOFs the spoke leg via the mux pump's implicit-EOF sentinel, so
  the spoke-side cascade is unchanged. ADR-051 §6 documents the
  closed post-adopt window (the pre-adopt §6 window and the
  inside-adopt_channel cancellation point stay as documented).
- rebuild_spec_for trims and rejects empty/whitespace
  channel_open_alpn strings — an empty explicit string previously
  overrode a sane name-derived ALPN.
- op_name_is_standard_channel_open_shape applies the same
  empty-segment guard as the derivation: channels//sub no longer
  serializes boolean-only and then reconstructs unmarked (silent
  stub for a marked op); the explicit string rides instead.
- reserved_reply_key_call_error interpolates RESERVED_REPLY_KEY;
  the establisher-bug log fires at warn! (programming error).
- Regression tests: the plan drop guard, the empty-ALPN fallback,
  the empty-segment shape check (672 tests, 3 new).
- CHANGELOG [Unreleased] entry for the audit fixes.
- docs/reviews/009 — the audit's deferred test-coverage gaps
  (template failure arms, filtered/only, batch reserved key,
  wire failure path, golden pins, derivation edge shapes, builder
  overwrite semantics), each with the test to add and gates.

Verification: cargo test 672 passed; clippy --all-targets -D
warnings clean; fmt --check clean; doc --no-deps clean; wasm32
check clean.
2026-09-18 06:19:56 +00:00
glm-5.3-flash 50182d7298 feat(review 008 Unit 3c): the gate-2 e2e harness; 0.8.0 release bookkeeping
- src/channels/gate2_tests.rs (ADR-051 gates 6/7, review 008 gate 2):
  the full consumer -> hub (HubLegTemplate) -> spoke relay e2e —
  the open resolves with the hub-allocated channel_id, `bound`
  survives the relay, data flows both directions with a fake 8-byte
  chunk header riding verbatim (the hub never parses the data plane,
  ADR-034/035), spoke-side close cascades to clean reclaim on both
  legs; hub-side disconnect (the consumer's duplex end dropped via a
  killable transport proxy) tears down both legs with the ledger
  decremented; the mid-establishment window (ADR-051 §6) pinned with
  its two reclaim signals — the consumer leg reclaims at its own
  transport EOF, the spoke channel (allocated before the establisher
  replied) is the honest residual, reclaimed when the spoke-leg
  transport ends; the channels/tty/sub standard-shape companion pins
  no derivation regression.
- src/channels/relay.rs: the relay's adopted producer-leg channel
  entry now reclaims when the relayed pump completes (teardown after
  pump_bidi) — the consumer-leg wrapper's teardown cannot see the
  producer leg's manager; the RelayPlan carries the spoke id for the
  reclaim.
- Release bookkeeping: 0.7.1 -> 0.8.0, the CHANGELOG entry covering
  Units 1-3 (Establishment reply projection, open_channel_with_reply,
  flavor-form discovery derivation, ChannelRelay, HubLegTemplate,
  gate-2 harness); review 008 Status -> Resolved with the U-1/U-2
  commit refs and the 955->945 errata note; ADR-051 Status ->
  all units landed + the §6 mid-establishment residual expanded to
  the two-reclaim-signal shape the gate pins; the stale "0.7.2"
  version mentions in ADR-047/049 corrected to 0.8.0 (the units land
  unreleased).

Verification: cargo test 669 passed / 0 failed; clippy --all-targets
-- -D warnings clean; fmt --check clean; cargo doc --no-deps clean;
cargo check + clippy on wasm32-unknown-unknown clean;
cargo publish --dry-run --allow-dirty passed.
2026-09-18 05:15:29 +00:00
glm-5.3-flash 4e52bd496c feat(review 008 Unit 3b): the hub-leg install template (ADR-051 §5)
- src/channels/hub_leg.rs: HubLegImports (the Clone discover/stash,
  bundles split by the channel_open marker, with the per-consumer
  filter) and HubLegTemplate (the hub-leg install hook: per-connection
  fork + generic channel ops + plain bundles as-is + marked specs via
  ChannelRelay::register_relay_openable + bootstrap discovery closed
  over the fork (review 004 F-06) + serving-identity resolution
  (CF-005) + the single-stream dispatch loop).
- The loud assembly posture holds at install: a Pub-typed (or
  unregistrable) marked spec ends the leg's install task — channel 0
  never dispatches, never a silent stub (ADR-051 §6).
- Assembly rule surfaced by the gate e2e: from_call discovers the
  spoke's bootstrap discovery ops like any plain op; the template
  skips BOOTSTRAP_DISCOVERY_OPS (new pub const, discovery.rs) when
  re-registering plain bundles — the template's own install, closed
  over the fork, supersedes the imported copies.
- Gate tests (7): stash split + filter, the full template e2e
  (services/list shows the re-exposed ops, a plain imported op
  round-trips through the hub, the marked op opens through the relay
  with bound surviving, data both directions), serving-identity
  precedence (scope-bearing override resolves, scope-less fails
  closed), the loud Pub-typed posture, and the filtered-stash subset
  run (NOT_FOUND for the filtered-out marked op).
- Docs: ADR-051 status (3a + 3b implemented, 3c remains); review 008
  Unit 3b landed note (the bootstrap-op skip rule).

Verification: cargo test (665 passed), clippy --all-targets
-D warnings, fmt --check, doc --no-deps — all clean.
2026-09-18 03:48:52 +00:00
glm-5.3-flash 91765446b7 docs(review 008 Unit 3 planning): ADR-051 — in-tree ChannelRelay + hub-leg assembly; plan split into 3a/3b/3c
Design session outcome for the relay unit. The hub/spoke family (hub
or spoke may relay; the hub re-exposes spoke services by ACL without
binding ports) needs the call-half support too, so the unit's scope
grew beyond the as-pinned sketch and is now three sub-units.

- ADR-051 (new): the relay is the wrapper composition — translate hop
  = the ADR-049 establisher shape, byte-forward hop = pump_bidi per
  ADR-050; the relay holds Arc<CallConnection> + ChannelManager per
  producer leg (not a ChannelClient — take_call_connection's detach
  is wrong for a hub with three CallConnection claimants); the reason
  mapping preserves the spoke's code+message (timeout is the one
  non-1:1 case, mapping to dial_failed); the registration seam is
  two-phase (discover/stash → per-connection fork-register — the
  ADR-047 §4 fork mechanism makes a one-call import impossible); the
  ADR-042 relay map dissolves (implicit per-channel mapping) and
  channel/close needs no translation surface (EOF cascade propagates
  with correct ledger accounting on both legs); Pub-typed marked
  specs are a loud assembly error; establishment bounds compound per
  hop (noted, no fix); the ACL layering note is pinned (the spoke's
  AccessControl sees only the hub identity; forwarded_for is never
  checked).
- ADR-042 amended: the §Scope note is revised (implementation is an
  alkcall export; hub crates compose it) and the two mechanism
  supersessions are recorded — the contract and auth-model rationale
  unchanged.
- ADR-047 amendment 3's forward reference and the README ADR index
  updated (001..051).
- Review 008 remediation plan: Unit 3 split into 3a (ChannelRelay
  component + gates), 3b (hub-leg install template — the call-half
  support), 3c (gate-2 e2e incl. the mid-establishment disconnect
  window); sequencing note updated; adoption note records the
  session's decisions.

Verified: cargo doc --no-deps (markdown-only change).
2026-09-17 06:47:48 +00:00
glm-5.3-flash 620180d615 feat(review 008 Unit 2): flavor-form open-op ids in discovery (U-1, ADR-047 amendment 3)
Flavor-form open op ids (channels/tunnel/direct, channels/tunnel/
forwarded — alktunnels ADR-007/008) now survive discovery + the
op/register announce path: the marker reconstructs, so a hub consuming
through discovery wraps the op with relay machinery instead of the
silent plain-forwarding-stub failure.

Per the review-008 plan's combined (b)+(c) shape:

- derive_alpn_from_op_name generalizes from strip-/sub|/pub to
  strip-last-segment (rsplit_once) — a strict superset: standard
  two-suffix shapes derive identically, multi-segment ALPNs survive
  the same way, and the flavor form derives. The boolean marker
  remains the gate (consulted only for marked ops in
  rebuild_spec_for) — a plain op named channels/tty/query is
  unaffected.
- spec_to_json_pub emits channel_open_alpn (the explicit string)
  beside the boolean when the op name is not the standard
  channels/<seg>/(sub|pub) shape; standard shapes stay byte-identical
  to the pre-amendment payload. The advertised operation_spec_schema
  documents the optional ["string","null"] property.
- rebuild_spec_for prefers the explicit string, else boolean →
  generalized derivation. Both wire consumers (from_call import,
  op/register announce) parse through the same parser, so one change
  covers both. A channel_open_alpn without the boolean never marks an
  op (the boolean is the dispatch hint).

Residual ambiguity pinned in the ADR: an ALPN segment colliding with
a flavor name (channels/x/direct → alk/x vs alk/x/direct) is
undecidable from the name alone; the explicit string is the
disambiguator.

Tests (review gates 1 + 3; gate 2 lands with the Unit 3 relay):
- channels/tunnel/direct reconstructs WITH channel_open = alk/tunnel,
  both via the explicit string and via the boolean alone (the
  deployment-skew case: old producer, new consumer)
- standard shapes (channels/tty/sub, multi-segment
  channels/custom/proto/sub) round-trip byte-stable — no new key
- explicit string overrides a colliding derivation; string without
  boolean is ignored
- op/register announced flavor-form spec round-trips with the marker
- derivation unit tests: flavor form, non-op-type suffix not
  special-cased, prior shapes unchanged

Docs: ADR-047 amendment 3 — the convention sentence (mirroring
alktunnels ADR-002 Amendment 1: flavors are new op ids, additive, the
.../sub op is never reused), the wire shape, the preference order,
the residual-ambiguity note, one-way-door timing.

Verification: cargo test 649 passed (12 new); clippy -D warnings
clean; cargo fmt --check clean; cargo doc --no-deps clean.
2026-09-16 10:34:45 +00:00
glm-5.3-flash 82ddddf986 feat(review 008 Unit 1): establisher reply projection (U-2, ADR-049 amendment 3)
The establisher can now contribute to the open-op success reply — the
establisher → opener direction amendment 2 left empty. First consumer:
alktunnels ADR-008's bind-first listen establisher, whose observed
OS-chosen bound address rides the reply as an additive `bound` field
(the SOCKS5 BIND reply#1 BND.ADDR fidelity ask).

- Establishment gains reply_fields (private, builder-constructed):
  with_reply_field / with_reply_fields / reply_fields. The map shape
  generalizes beyond `bound` without a fourth amendment; the
  #[non_exhaustive] carrier (amendment 2) makes the extension additive
  — no construction-site break.
- run_open_wrapper merges contributed fields into the success output
  after reserving `channel_id`. The key is wrapper-owned: an
  establisher supplying it fails the open loudly
  (channel:open_failed, reason handler_error, message naming the
  reserved key), tearing the just-allocated channel down — never
  shadowing. Absent fields leave the reply byte-identical to the
  pre-amendment shape (asserted exactly).
- ChannelClient::open_channel_with_reply returns
  (channel_id, reply, send, recv) — the full success output — so
  consumers read `bound` first-class; open_channel delegates and
  discards the fields, signature unchanged.

Tests (all gates from the review + plan):
- projection produces { channel_id, bound } on the wire; no-fields
  replies are byte-identical (establisher and no-establisher shapes)
- reserved-key establisher fails with reason handler_error; channel
  torn down, ledger decremented, pump handler never spawned
- registry-level register_openable_with_establisher projects fields
- e2e over a real channels connection: bound reaches
  open_channel_with_reply; open_channel unchanged against the same
  accept side
- existing establishment tests (establisher-success round-trip,
  plan-flow, no-establisher compat) pass unchanged

Docs: ADR-049 amendment 3 (projection, reservation, read path,
compatibility posture, door type).

Verification: cargo test 637 passed; clippy -D warnings clean;
cargo fmt --check clean; cargo doc --no-deps clean.
2026-09-16 10:27:12 +00:00
glm-5.3-flash ed741c34e0 feat(cf-006/cf-007): per-call opener identity for open-op hooks; ADR-016 code-list sweep
Batch-fixes every remaining open alkcall-side finding from downstream
consumers so 0.7.0 is the only release they need to absorb.

- CF-006 (the CF-005 corollary): run_open_wrapper derives a per-call
  AuthContext — the opener's dispatch-resolved identity (the same
  identity the ACL gate and cap check saw) overlaid onto the
  install-time context — and passes it to both the establisher and
  the pump handler. Identity-less calls keep the install-time
  identity (no synthetic-anonymous rewrite); transport-truthful
  fields are never rewritten. Signatures unchanged — behavior-only;
  identical on per-connection registries, hub-forwarded opens now
  show the end client. Gates:
  open_wrapper_overlays_per_call_identity_on_install_time_auth +
  open_wrapper_keeps_install_time_identity_when_call_identityless.
- CF-007 (alkhttp review 006 Part C doc drift): ADR-016 amended to
  the eight-code list — ALREADY_EXISTS + CONNECTION_CLOSED in the
  Context, §3 table, and from_openapi collision rule; new §2a
  documents the undelivered-vs-ambiguous write-failure distinction.
- ChannelPlan type doc now states the Send + Sync payload constraint
  (alktunnels POC F-1 re-derived it by compiler error).
- Ledger: CF-006 and CF-007 filed + resolved; CF-005's corollary
  note points at CF-006; the Open section is empty. ADR-049 gains
  the per-call-identity note. Changelog 0.7.0 covers the batch.

Sweep result (all downstream reviews): no other open alkcall items —
alktunnels W3/F-2 are downstream-by-design, alktty R3/P14 are closed
constraints, alknet has none; OQ-24/37/39/40/41 stay
deferred-by-design (no consumer pull yet).

Verification: cargo test (631) + --all-features (648), clippy
(all-targets, all-features, -D warnings), fmt --check, doc
--no-deps, wasm32 check, semver-checks (no update required),
publish --dry-run.
2026-09-07 11:26:12 +00:00
glm-5.3-flash db5530ed34 feat(cf-005): connect-side serving identity — ServingConfig.identity + transport-identity propagation
Verifies and fixes CF-005 (alktunnels reverse-flow POC W1): the
connect-side serving path built channel 0 internally and never set an
identity, so a scope-gated serving op could only be satisfied via the
payload auth_token. Token is now the fallback (hub-forwarding /
browser path); transport/key-based identity is the primary path.

- ServingConfig gains identity: Option<Identity> — the explicit
  override (remediation a). Semver-relevant struct-literal change →
  0.7.0 (minor bump at 0.x, wire surface unchanged).
- from_connection_with_serving propagates the transport
  Connection::identity() to the channel-0 connection via set_identity
  before the serving loop starts (remediation b) — mirrors the accept
  side's install-hook set_identity; process-local, nothing new on the
  wire.
- Dispatch identity precedence on the serving loop: payload
  auth_token → identity_provider, then ServingConfig.identity, then
  transport identity; identity-less dispatch still fails closed
  (FORBIDDEN).
- Public core::auth::NoopIdentityProvider (resolves nothing; the
  ServingConfig::default() provider — three private test copies
  existed).
- Regression gates: four cf005_* e2e tests (transport propagation,
  override precedence, identity-less denial, token fallback +
  precedence).
- Ledger CF-005 → resolved; ADR-022 §connect-side-serving amended;
  README example updated; changelog 0.7.0.

Verification: cargo test (629) + --all-features (646), clippy
(all-targets, all-features, -D warnings), fmt --check, doc
--no-deps, wasm32 check, semver-checks (no update required at
0.7.0), publish --dry-run.
2026-09-07 11:00:47 +00:00
glm-5.3-flash 9c6fec17ca feat(review 007 Unit 3): pump_bidi two-pump helper (R-03, ADR-050)
Extracts the two-pump data-plane helper alknet ADR-078 deferred until
the shapes converged (they have: alktunnels POC pump_halves + alktty's
channels session). Purely additive.

- channels::pump::pump_bidi(channel, peer_read, peer_write) -> (u64, u64):
  two joined pumps, shutdown-on-completion per direction; copy counts
  for observability. The channel side is a single AsyncRead +
  AsyncWrite value (the accept_bi BiStream); the peer side takes split
  halves — the establisher's natural dial result (into_split).
- Return (u64, u64), not the review sketch's io::Result<(u64, u64)>:
  both pumps swallow copy errors by contract (mid-stream error =
  abrupt close, no error channel mid-stream per ADR-049 §6), so an
  Err state would be dead code. Deviation recorded in ADR-050.
- alktty's three-pump session does not fit (exit future as a third
  signal) and stays as-is, per the review's scope.
- Tests reproduce the POC's two-pump semantics through the helper:
  bidirectional flow with exact counts, EOF-from-one-side completes
  the other's shutdown (clean EOF at the far end), dead-source =
  EOF-shaped teardown.
- ADR-050 records the decision, deviations, and two-way door type.

Verification: cargo test (625 passed, +2), clippy -D warnings, fmt
--check, doc clean, test --all-features clean, wasm32-unknown-unknown
check clean.
2026-09-07 08:47:46 +00:00
glm-5.3-flash dc4ad2bc6d feat(review 007 Unit 1): Establishment carries the channel plan (R-01) + lifetime doc (R-02)
Implements ADR-049 amendment 2 — the reserved Establishment payload is
filled, and the OpenHandler lifetime contract is documented.

- Establishment { plan: Option<ChannelPlan> } with ChannelPlan =
  Arc<dyn Any + Send + Sync>: typed-opaque, because the payload an
  establisher hands the pump handler is a live handle (dialed socket,
  TTY handle), not JSON — the review's Option<Value> sketch could not
  satisfy its own verification gate. #[non_exhaustive] keeps a future
  carrier change from being another break. Construction:
  Establishment::new(plan) / Establishment::default().
- OpenHandler gains the plan parameter:
  Fn(Value, Option<ChannelPlan>, Connection, AuthContext) ->
  JoinHandle<()>. Separate parameter (not merged into input) — a
  typed payload cannot ride the JSON input; no schema collision.
  Wire surface unchanged: the plan is process-local (establisher ->
  wrapper -> handler).
- run_open_wrapper threads establishment.plan to the handler; None
  when no establisher is registered. Kills the alktunnels-POC
  side-channel handoff (resource-keyed slot + poll loop) whose
  concurrent same-resource race is now unreachable — each open's
  establisher result flows to its own handler.
- Lifetime contract documented (R-02, doc-only half): the returned
  JoinHandle must track the data-plane lifetime — the wrapper awaits
  it and its completion triggers teardown; early return = teardown
  at birth. Noted on the OpenHandler type docs and both registration
  entry points.
- Breaking at 0.6.0 (the point of landing it before alktunnels
  Phase 1): Ok(Establishment {}) sites become
  Ok(Establishment::default()) mechanically.

Verification: cargo test (621 passed, +4: plan-flows-to-handler,
concurrent same-resource opens get distinct plans, no-establisher
None plan, Establishment construction), clippy -D warnings, fmt
--check, doc clean, test --all-features clean.
2026-09-07 08:43:17 +00:00
glm-5.3-flash f8dad9dbc8 feat(review 006 Unit 2): additive OperationSpec.description disclosed via discovery (E-02)
- OperationSpec gains description: Option<String> (builder
  with_description, defaults None; no struct-literal construction
  sites exist, so additive by construction)
- spec_to_json_pub emits description when set; rebuild_spec_for
  parses it back — the field survives from_call discovery and
  op/register announcement (same round-trip pattern as
  resource_id_path / publish_schema)
- services/list and the local-ops half of services/list-peers emit
  description when set; output-schema docs on both listing specs and
  operation_spec_schema advertise the field
- Tests: builder/default, emit/omit, listing emission, schema
  disclosure, schema-doc presence, round-trip + absent-stays-absent
  (8 new; 616 total)
- Docs: review 006 Unit 2 marked IMPLEMENTED; ADR-047 §6 amendment
  records the E-02 discovery decision (listing enrichment lands, the
  channel/resources/subscribe half stays deferred); OQ-40 gains the
  load-bearing note; operation-registry.md struct + listing docs;
  CHANGELOG

Verification: cargo test (616 pass), clippy -D warnings (host +
wasm32), fmt --check, cargo doc --no-deps, wasm32 check — all clean
2026-09-06 19:26:51 +00:00
glm-5.3-flash 2586c3b217 feat(review 006 Unit 1): channel-open establishment phase + typed client error (E-01, N-1)
Implements ADR-049 Unit 1 — the open-op wrapper gains an awaited,
bounded establishment phase, and the client stops erasing the error.

- OpenEstablisher hook + Establishment/EstablishmentError types:
  register_openable_with_establisher awaits the establisher bounded
  (earlier of dispatch deadline and per-registration timeout, else
  ESTABLISHMENT_TIMEOUT = 10s) after allocation, before the reply and
  before the pump handler is spawned (ADR-049 §1/§2). Implementation
  note: the establisher takes (input, auth) only — the channel's
  yield-once BiStream belongs exclusively to the pump handler
  (amendment recorded in ADR-049).
- Establishment failure: teardown_channel + opener-ledger take +
  policy.on_close un-increment (allocation and teardown balance;
  the ledger take is the atomic gate, ADR-047 §7), reply
  channel:open_failed with details {reason, message} — reason ∈
  dial_failed / unknown_resource / resource_shortage / handler_error
  / timeout (ADR-049 §3). SSH contract consumer-visible: a failed
  open never returns a channel_id.
- register_openable unchanged (no establisher = always-OK; existing
  registrations compile and behave identically — compat gate test).
- ChannelClient::open_channel returns ChannelOpenError (breaking at
  0.5.0): CallFailed { error: CallError } carries the wire error
  verbatim (establishment_reason() branches on details.reason);
  MissingChannelId / AdoptFailed cover the local-only shapes
  (ADR-049 §4, review 006 N-1).
- Tests cover all four verification gates from the review: e2e
  establisher failure through a real channels connection (typed
  reason + no-channel + ledger un-increment), bounded timeout,
  no-establisher compat, establisher-success pump round-trip; plus
  reason-vocabulary mapping and bound arithmetic.
- Bump to 0.5.0 (open_channel error-type change is semver-relevant).

Verification: cargo test (608 passed), clippy --all-targets -D
warnings, fmt --check, doc --no-deps, wasm32 check — all clean.
2026-09-06 19:00:04 +00:00
glm-5.3-flash 48ceeba55c docs: ADR-049 channel-open establishment phase; verify review 006
Verify review 006's findings against source at 88e3f5e (E-01..E-04
all confirmed; E-02 cost corrected — OperationSpec has no description
field, four touchpoints) and file three additional findings from the
same sweep (N-1 client error-type gap, N-2 write-only early-arrival
counter, N-3 pump-panic posture).

ADR-049 resolves E-01 + N-1: split-hook OpenEstablisher awaited
bounded by the open-op wrapper (restoring ADR-047 §3's "channel
plan" shape), teardown + typed channel:open_failed reply on
establishment failure, ChannelClient::open_channel typed error.
Review 006 gains the post-verification remediation plan and verdict
appendix.

Verification: cargo test (597 passed), cargo doc --no-deps clean.
2026-09-06 10:57:20 +00:00
glm-5.3-flash fd212307e2 fix(up-03): PeerCompositeEnv::peer_operations override — list-peers sees peer-announced ops
Surfaced by alkhttp review 006 (UP-03): services/list-peers showed
every peer with an empty operations array. PeerCompositeEnv overrode
peer_ids only, so peer_operations fell to the trait default
(Vec::new()) and the ADR-022 amendment's "announced op is discoverable
via services/list-peers" promise never resolved on the wire. ADR-030
prescribed the fix but it had never been ported into alkcall. The
existing list-peers unit tests passed because they mock
peer_operations with hand-rolled envs.

Implements ADR-030 as specified:
- OperationEnv gains list_operation_names (default Vec::new(),
  back-compat for all existing implementors)
- OverlayOperationEnv overrides it with its overlay's registered names
- PeerCompositeEnv::peer_operations delegates to the peer overlay's
  list_operation_names; PeerCompositeEnv::list_operation_names
  aggregates session + connections + base (mirrors its contains())
- LocalOperationEnv enumerates its registry; ChannelsSessionEnv
  delegates to base

Gate: announced_op_is_discoverable_via_services_list_peers in
src/registry/op_register.rs — announces an op through op/register,
then asserts both the direct peer_operations probe and the
services/list-peers wire shape attribute the announced op to the peer,
over the exact compose_root_env shape (PeerCompositeEnv + attached
connection overlay). Verified load-bearing: reverting the
peer_operations override fails the gate.

ADR-030 status Proposed -> Accepted with the UP-03 provenance note.

Verification: 590 default / 607 all-features tests, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean,
semver-checks 196 pass against v0.3.0 (defaulted trait method is
non-breaking).
2026-09-04 16:00:23 +00:00
glm-5.3-flash d5b2661b38 fix(review 005 Unit 3): resource_id_path wire round-trip + bootstrap-list doc alignment (G-04, G-05)
- resource_id_path rides both halves of the spec wire round-trip:
  spec_to_json_pub serializes it (optional string key), rebuild_spec_for
  parses it. Additive optional field - absent stays absent. Previously
  an announced (or from_call-imported) op declaring ownership-scoped
  resource extraction silently rebuilt with resource_id: None, so ACL
  checks ran without the resource ID.
- Gates: spec_round_trips_resource_id_path (serialize -> parse ->
  field intact) + spec_without_resource_id_path_stays_absent (additive
  field breaks no consumer).
- ADR-022 amendment: bootstrap-op set gains services/list-peers with a
  dated G-05 note (the installer has registered it since the amendment
  landed; the doc lagged the code). Set remains closed at four.

Verification: cargo test 589 / --all-features 606, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.

Refs docs/reviews/005-...md (G-04, G-05; all findings closed).
2026-09-04 09:41:43 +00:00
glm-5.3-flash 23c9b28c6b fix(review 005 Unit 2): op/register serving-registry collision gate (G-03)
- op_register_handler takes the serving registry alongside the
  connection and rejects announced names that collide with the serving
  side's own registrations (ALREADY_EXISTS regardless of replace).
  Peer-announced ops may collide with peer-announced ops (replace
  governs, the reconnect path) but never shadow the deployment's own
  ops: the connection overlay resolves before base in PeerCompositeEnv,
  so an unscreened same-name announce would silently rewrite what a
  wire-dispatched handler's ctx.env.invoke resolves. Composition
  authority (ADR-018) stays with the deployer.
- ADR-022 amendment (2026-09-04): collision policy recorded in the
  2026-09-03 amendment's op/register section (rationale + visibility
  irrelevance); status line notes the sub-amendment.
- Gates: base-External collision rejected even with replace (overlay
  stays clean, serving registration untouched); Internal base op
  equally protected; overlay/overlay collisions still follow replace;
  nested composition of a base op resolves the serving side's own op
  after an unrelated announce (real compose_root_env env shape).
- PeerCompositeEnv resolution order deliberately unchanged.

Verification: cargo test 587 / --all-features 604, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.

Refs docs/reviews/005-...md (G-03; Unit 3 open).
2026-09-04 09:40:11 +00:00
glm-5.3-flash f84d214173 feat: per-session fork registry, connect-side serving loop, op/register (review 004 Units 1-3)
Remediates all six findings of review 004 (per-connection dispatch
resolution and client-side op serving). All claims re-verified in
source before remediation; F-02's member list gains ScopedPeerEnv
(also Clone — fork surface simpler than estimated).

- OperationRegistry: interior mutability (parking_lot RwLock on both
  maps); register takes &self; registration/list_operations return
  owned clones; fork() deep-copies registrations + cached publish-schema
  validators (F-02/F-03); OperationRegistryBuilder::from_registry.
- install_bootstrap_discovery: services/list, services/list-peers,
  services/schema registered closed over the fork itself, so
  per-session openables are discoverable and services/schema answers
  from the fork (F-06).
- Dispatcher::serve_single_stream: full-duplex single-stream loop —
  call.requested dispatches inbound; responded/completed/error resolve
  outbound pendings; aborted tries both tables (in-flight sink aborts
  + pending cascade); published routes inbound sinks (F-04).
- ChannelClient::from_connection_with_serving(connection,
  Option<ServingConfig>): opt-in serving; from_connection keeps the
  pure-consumer default.
- registry::op_register: OpRegisterRequest wire DTO (spec in
  services/schema JSON + replace flag), op_register_spec,
  op_register_handler (rebuild -> forwarding stub -> register_imported,
  forced Internal/FromCall), announce_op; CallError::already_exists;
  spec_to_json_pub; from_call's rebuild_spec_for + forwarding-handler
  constructors crate-shared (F-05).
- ADR-047 §4 amendment #2: per-session fork is the dispatch-registry
  mechanism; overlay stays nested-invocation/peer-announced landing
  zone (F-01/F-02).
- ADR-022 amendment 2026-09-03: bootstrap-op set (services/list,
  services/schema, op/register), opt-in connect-side serving, op/register
  wire shape (F-04/F-05).
- alkhttp ADR-048 reconciliation note + OQ-05 re-pointed at the alkcall
  ADRs (Unit 1b).
- Review 004 status -> remediated; remediation log with gates.

Verification:
- cargo test: 581 passed, 0 failed (565 baseline + 16 new)
- cargo test --all-features: 598 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo clippy --all-features --all-targets -- -D warnings: clean
- cargo clippy --target wasm32-unknown-unknown -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean

Gates: fork_registry_open_op_resolves_and_is_discoverable (open op via
fork + services/list shows openable + services/schema validates),
serving_loop_hub_to_consumer_call_resolves (hub->consumer call through
consumer's serving loop, consumer->hub still resolves),
op_register_announce_then_hub_call_routes_back_to_consumer (announce ->
overlay -> hub call -> forwarding stub -> consumer serves).
2026-09-03 17:32:45 +00:00
glm-5.3-flash d5fd548b8d feat: promote dispatch spine to gateway module (ADR-048, feature-gated)
Promote alkhttp's transport-neutral dispatch spine into alkcall as
alkcall::gateway behind the opt-in gateway cargo feature (default off;
adds no dependencies):

- GatewayDispatch: deadline-bounded invoke spine over OperationRegistry
  (invoke / invoke_streaming / invoke_sink) with the root-context
  discipline (internal: false, forwarded_for: None) hubs and spokes
  relaying calls (ADR-042 translate path) need identically to alkhttp's
  HTTP gateway. The 30 s deadline becomes a constructor knob
  (with_deadline).
- schema_disclosure_denial: the shared is-internal + ACL check for
  services/schema inner-name disclosure; ACL denial returns FORBIDDEN
  (identity-aware refinement), Internal visibility returns spec-404.
  One implementation so transports cannot drift (CF-004).
- MAX_BATCH_OPERATIONS / CallRequest / HTTP error mapping stay in
  alkhttp (projection + transport concerns); alkhttp migrates to this
  module in a follow-up session and drops its local copy.

Docs: ADR-048 (decision + divergence rationale), ADR index entry,
CHANGELOG.

Verification: 574 tests pass with --features gateway (16 new), 558 pass
default, clippy -D warnings clean both feature sets, --all-features
clean, fmt clean, wasm32 target clean, rustdoc warning-free.
2026-08-31 08:45:57 +00:00
deepseek-v4-pro 08e7df2aa0 feat: rename ALPN prefix from alknet/ to alk/ (v0.1.1)
- CHANNELS_ALPN: b"alknet/channels" → b"alk/channels"
- CallAdapter::alpn(): b"alknet/call" → b"alk/call"
- derive_alpn_from_op_name: alknet/ prefix → alk/ prefix
- All ALPN string literals in src/ and docs/ updated
- ADR-004 amended with prefix rename rationale
- AGENTS.md, README.md updated
- Version bumped to 0.1.1

Review: docs/reviews/003-alpn-prefix-rename.md

Verification:
- cargo test: 542 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
2026-08-14 13:55:28 +00:00
glm-5.2 da12d65a03 fix: Unit 7 — small correctness fixes (C-19-misc, C-21, C-23, P-13)
No panic paths in library code, no misleading public fields, honest
error envelopes, latent bugs in derive_alpn_from_op_name and the mux
pump map fixed, per-discovery Box::leak removed, demux observability
counter added, resources-snapshot unbounded loop replaced with an
O(open channels) iterator.

C-21 — write_header (wire.rs) sliced out[..CHUNK_HEADER_LEN] and
panicked on short buffers; the doc comment declared the panic. Returns
Result<_, ChunkError::HeaderTooShort> instead (the variant already
existed for the parse side). write_chunk and all callers updated (they
pass [0u8; 8] today so the Result is always Ok, but the API contract
is typed, not panic-documented). Added a short-buffer test.

C-23 — dispatch_requested's Sink and Stream error arms used
String::new() as the request id (dispatch.rs), producing envelopes
with an empty id. The request_id is now cloned before the move into
dispatch and used in the error arms, so the envelope carries the real
id (matching the Once arm, which already did this correctly).

P-13 — SinkDispatch::chunk_tx was pub (dispatch.rs), exposing the
futures::mpsc::Sender type and the 64-slot buffer size as effective
public API. Made pub(crate); handle_stream and pump_sink (the only
readers) are in the same module, and the dispatch tests are in the
same module too.

C-19 (mux pump map leak + duplicate registration) — MuxRunner::run
inserted each pump's JoinHandle into self.pumps and never removed
finished ones (they accumulated for the connection's lifetime).
Duplicate register(channel_id) silently overwrote the old handle while
the old pump kept running (two pumps, one channel id). Fixed: before
insert, check for an existing entry; if it is_finished(), reap it and
proceed (leak fix); if it is still running, reject the registration by
dropping the responder without sending (the caller's receiver.await
yields RecvError, which ChannelManager maps to ChannelExists). Added
two tests: duplicate-rejected-while-running and
reap-finished-then-reregister.

C-19 (derive_alpn_from_op_name) — from_call.rs took only the first
path segment (rest.split('/').next()), so channels/custom/proto/sub
derived alknet/custom instead of the full ALPN custom/proto. The
segment.starts_with("alknet/") branch was dead (a single segment
cannot contain /), and segment == "alknet" yielded the nonsense ALPN
"alknet". Fixed: strip the known /sub or /pub suffix from rest
instead of taking the first segment, so multi-segment ALPNs survive.
The rule (ADR-047 Negative): alknet/*-prefixed segments and
multi-segment non-alknet/* ALPNs are returned as-is; single-segment
non-alknet names get the alknet/ prefix prepended. Added tests for the
multi-segment non-alknet case, the explicit alknet/ prefix case, the
/pub suffix, and the no-suffix (non-channel-open-op) case.

C-19 (Box::leak per discovery) — from_call.rs leaked a String to
'static on every leak_alpn call (bounded per unique ALPN in theory,
but leaks on every rediscovery of every marked op). Refactored
ChannelOpenSpec::alpn from &'static str to Cow<'static, str>
(spec.rs); the common case (ALPN crates register at compile time with a
&'static str literal) pays no allocation via Into<Cow>, while from_call
supplies an owned String without leaking. Removed leak_alpn. Amended
ADR-047 §2 to record the Cow<'static, str> shape (two-way-door type
detail; the wire format — a boolean channel_open marker — is
unchanged).

C-19 (REQ-CH-04 error counter) — the demux's lenient unknown-channel
drop (manager.rs) only debug!-logged; there was no counter or stats
surface. Added an AtomicU64 dropped_unknown_chunks counter to
ChannelManager, incremented on the unknown-channel drop in
route_payload, with a dropped_unknown_chunks() accessor for
observability. Added two tests: counter increments per drop, and
known-channel routing does not increment it.

C-19 (resources-snapshot unbounded loop) — operations.rs iterated
0..u32::MAX with a per-iteration mutex lock, breaking when
resources.len() >= manager.open_count(). With sparse ids (monotonic
after churn) or a channel closing mid-iteration, this could iterate
millions of times. Replaced with an iterator over ChannelManager::
channel_ids() — a new accessor that returns a point-in-time Vec<u32>
of open channel ids — so the snapshot is O(open channels). Added
channel_ids tests and behavioral tests for the resources/subscribe
handler (open channels emit their ALPNs; no channels emit an empty
set). Also added close/control handler tests (C-25 #8 coverage the
review noted was missing): close rejects channel 0, close on unknown
channel returns NOT_FOUND, control on unknown channel returns
NOT_FOUND, control missing channel_id returns INVALID_INPUT.

Verification:
- cargo test              → 465 passed, 0 failed (was 449; +16 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check       → clean
- cargo doc --no-deps     → 0 warnings
2026-08-13 09:15:20 +00:00
glm-5.2 bfb265e31b fix: Unit 6 — convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24, P-10, P-11)
Conventions satisfied, `cargo doc` clean, no actively-wrong comments,
producer/consumer naming consistent in the call/registry docs.

P-10 — `pump_sink` matched the string literals "call.published",
"call.completed", "call.aborted" (dispatch.rs) instead of the
EVENT_PUBLISHED / EVENT_COMPLETED / EVENT_ABORTED constants the rest
of the file imports. Replaced with the constants — pure refactor
hazard, no behavior change.

C-20 remainder — the wrong "SAFETY:" comment at from_call.rs:271
marked no `unsafe` block and was factually wrong (described a `'static`
return that isn't what `derive_alpn_from_op_name` does — it returns
`Option<String>`; the leak happens in `leak_alpn`). Reworded to a
plain note about the `'static` lifetime requirement. Also reworded
the abort-cancels claims in the `pump_sink` and `pump_stream` doc
comments (dispatch.rs): both claimed `call.aborted` "cancels the task
and drops the handler future" — the handler is actually `join!`-ed to
completion and not yet cancelled (the abort-cancels-Pub mechanism is
review 001 Unit 9). Trimmed step-numbered narration comments in
adapter.rs / client.rs that restated what the code does, keeping the
ordering-constraint and REQ-CH comments. The big reassembly.rs
deliberation landed with Unit 4; this finishes the remainder.

C-09 — fixed the 2 remaining `cargo doc` warnings (was 4; the
register_openable links were fixed in Unit 3):
- `unresolved link to default_policy` (operations.rs:50) — the
  [`default_policy`] intra-doc link resolves to
  super::policy::default_policy; used the full path.
- `env is both a module and a macro` (channels/mod.rs:30) — the
  [`env`] link collided with the std `env!` macro; qualified as
  [`self::env`].
`cargo doc --no-deps` now emits 0 warnings.

C-22 remainder — removed the filler `PhantomData` test at client.rs
(`let _ = std::marker::PhantomData::<ChannelClient>;` — asserts
nothing). The env.rs tautology was already removed in Unit 3.

C-24 — replaced "client→server streaming" with "producer→consumer
streaming" in call-protocol.md, operation-registry.md, README.md, and
open-questions.md (4 occurrences). Per AGENTS.md §8 the convention is
producer/consumer, not server/client. The remaining "client→server"
references in channels ADRs 034/037 are in stream_type table contexts
that Unit 10 (C-26) will handle as part of the spec-doc renumbering.

P-11 — amended ADR-046 §3's SinkHandler type so the stream item type
matches §6. §3 declared `Pin<Box<dyn Stream<Item = Value> + Send>>`;
§6 declared `Pin<Box<dyn Stream<Item = Result<Value, CallError>> +
Send>>`. The code uses §6's shape uniformly (registration.rs:32-40,
aliased as PublishStream). §3's text and the Door-type section are
amended to match §6; the Door-type section already marked the concrete
stream item type a two-way-door detail, so this is a text correction,
not a design change. Added an amendment note dated 2026-08-13.

Verification:
- cargo test --lib         → 449 passed, 0 failed (was 450; -1 removed filler test)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check        → clean
- cargo doc --no-deps      → 0 warnings (was 2)
2026-08-13 08:47:20 +00:00
glm-5.2 8066d08395 fix: Unit 3 — register_openable + per-connection ChannelCore (C-02, C-03, C-09)
A channel-open op could not be registered or invoked (C-02):
ChannelCore::register_openable did not exist, and resolve_channel_manager
(C-03) was a stub returning None — the ADR-047 §4 dynamic-resolution
shape (downcast context.env to &dyn ChannelOperationEnv) was unworkable
as written: context.env is a PeerCompositeEnv, not a single concrete
type that can be downcast to a channels-backed env.

The fix is per-connection registration (ADR-047 §4 amendment,
2026-08-13): a ChannelCore is constructed per channels connection (in
the install_channel_zero hook, which already runs per-connection and
already receives the ChannelManager), and register_openable is called on
that connection's overlay OperationRegistry (Layer 2 per ADR-019). The
wrapper closes over the per-connection ChannelCore and uses
ChannelCore::manager() directly — no context.env downcast. This
preserves every invariant ADR-047 §4 was written to protect (layering,
per-connection resolution) without adding as_any() to OperationEnv
(which would close the session/connection overlay patterns from
ADR-024, AGENTS.md §6).

Changes:
- ChannelCore::register_openable wraps the ALPN's OpenHandler with the
  ACL→check_open→open_channel→spawn→respond flow (ADR-047 §3). Branches
  on spec.op_type: Query/Mutation→Once, Sub→Stream (emits { channel_id }
  and completes; data plane on the channel's BiStream), Pub→Sink (stub:
  channel:pub_open_not_implemented — requires the channel-adoption path,
  C-08/Unit 5). The OpenHandler receives (input, Connection, AuthContext)
  and spawns the ALPN's protocol on the channel's BiStream, returning a
  JoinHandle for teardown.
- ChannelManager::set_handler_task installs the spawned OpenHandler's
  JoinHandle after open_channel (which allocates the channel first to
  get the BiStream halves, then the handler is spawned, then the task is
  recorded for abort on channel/close / connection drop).
- channel:too_many_channels / channel:allocation_failed error codes
  mapped to CallError with details (channel:forbidden is the ACL's
  FORBIDDEN, already handled by the registry before the wrapper).
- resolve_channel_manager stub removed (C-03); ChannelOperationEnv trait
  and ChannelsSessionEnv retained as a two-way-door implementation detail
  for future per-connection routing (not on the open-op path). The
  tautology filler test (C-22 env.rs) removed.
- ADR-047 §4 amendment records the per-connection-registration decision
  (two-way door: the ADR's door-type section explicitly marks the wrapper
  shape as a two-way-door implementation detail; the one-way decisions
  — per-ALPN op names, channel_open marker, removal of channel/open —
  are unchanged).

Acceptance gate (C-02/C-03): one end-to-end test wires ChannelClient ↔
ChannelsAdapter over a real tokio::io::duplex carrying the channels
8-byte chunk header wire format. The accept side's install_channel_zero
hook builds a per-connection ChannelCore, registers a no-op open op
(channels/tty/sub) via register_openable, and runs the dispatch loop.
The client calls call_open_op("channels/tty/sub") on channel 0; the
wrapper does check_open→open_channel→spawn→respond. Asserts the
response carries a non-zero channel_id and that the per-identity quota
was reserved (policy count for the caller incremented to 1).

Verification: 438 tests pass (was 437; +1 e2e), clippy clean, fmt clean,
doc warnings 2 (was 4; fixed the 2 register_openable broken-link
warnings — C-09; the remaining default_policy and env module/macro
warnings are Unit 6 long-tail items).
2026-08-13 04:47:13 +00:00
glm-5.2 495e04ed43 fix: Unit 2 — channel 0 single-stream call mode (C-01, C-25 #1)
Channel 0 was dead in both directions (C-01): the call protocol's
stream-per-request model (open_bi per call) is incompatible with
channel 0's single yield-once BiStream — every call_open_op failed
with StreamClosed on the connect side, and channel 0's Connection was
a black hole on the accept side.

The fix is single-stream call mode (ADR-036 amendment): all
EventEnvelope frames are multiplexed on channel 0's one BiStream.

Changes:
- CallConnection gains single_stream_writer: Option<Arc<SharedFrameWriter>>
  and new_single_stream() constructor. call_with_payload,
  subscribe_with_payload, publish_with_payload, and abort branch on
  is_single_stream() — in single-stream mode they write frames through
  the shared writer (mutex-serialized) instead of opening a fresh
  open_bi per call.
- Dispatcher::run_loop_single_stream reads frames off channel 0's read
  half, dispatches call.requested, writes responses through the shared
  writer, and routes in-flight call.published/call.completed/
  call.aborted to the matching Pub sink's chunk_tx by request_id.
- ChannelsAdapter::handle's InstallChannelZero hook now receives
  channel 0's Connection (built by the adapter) and runs the
  single-stream dispatch loop on it — closing the accept-side black
  hole. Mux runner is spawned BEFORE install_channel_zero so
  mux.register(0) can complete.
- ChannelClient::from_connection uses CallConnection::new_single_stream
  and spawns a read pump (read_single_stream_until_closed) that routes
  channel-0 response frames into the PendingRequestMap via
  dispatch_envelope — closing the connect-side StreamClosed path.
- ADR-036 amendment records the single-stream-mode decision (two-way
  door: implementation detail, wire format unchanged).

Acceptance gate (C-25 #1): three end-to-end tests wire
ChannelClient ↔ ChannelsAdapter over a real tokio::io::duplex pair
carrying the channels 8-byte chunk header wire format:
- channel_0_end_to_end_call_round_trip: a Query op round-trip
- channel_0_end_to_end_unknown_op_returns_not_found: NOT_FOUND
- channel_0_end_to_end_publish_delivers_chunks: a Pub op with 3 chunks

Verification: 437 tests pass (was 434; +3 e2e), clippy clean, fmt
clean, doc warnings unchanged (4, pre-existing C-09).
2026-08-12 21:55:04 +00:00
glm-5.2 f305f8c0a5 feat: implement channels protocol + ADR-047 (openable ALPNs are operations)
ADR-047: the unifying decision that  dissolves into
per-ALPN ops (, ) with a
 marker on . Each openable ALPN registers
its own ops with their own , ,
, and the marker. The  field is replaced
by  (Sub/Pub). The generic ops (channel/close,
channel/control, channel/resources/subscribe) stay, keyed by
channel_id. Resolves Gaps A-G from the research findings (Gap B broker
named out-of-scope for alkcall; Gap C relay wrapper is consumer
concern; Gap D connection-owner allocates; Gap E extension trait;
Gap F boolean marker on wire; Gap G ACL/ownership complementary).

ADR-037 amended:  dissolves;  removed; generic
ops stay;  preview dropped from resources/subscribe.

Spec docs updated: channel-operations.md (unified model, opener ledger,
ACL flow), operation-registry.md (channel_open marker, ChannelOpenSpec),
README.md (ADR-047), open-questions.md (OQ-31..38 resolved).

Source changes:
- spec.rs: ChannelOpenSpec struct, channel_open field on OperationSpec,
  with_channel_open builder, 3 tests
- discovery.rs: spec_to_json emits channel_open boolean,
  operation_spec_schema includes channel_open, 2 tests
- from_call.rs: rebuild_spec_for parses channel_open marker,
  derive_alpn_from_op_name helper, 6 tests

Channels module (src/channels/, 10 files, ~2400 lines):
- wire.rs: 8-byte chunk header (ChunkHeader, parse/write_header,
  read_header/write_chunk/write_eof async helpers), 12 tests
- reassembly.rs: MpscRecvStream (tokio::mpsc::Receiver<Bytes> →
  AsyncRead), MpscSendStream (AsyncWrite → tokio::mpsc::Sender<Bytes>),
  REQ-CH-01 shutdown sentinel, REQ-CH-02 sender-drop EOF, 10 tests
- mux.rs: MuxHandle (clone-able, register(channel_id)),
  MuxRunner (per-channel pump tasks, exits when handles drop),
  OpenerLedger (ADR-047 §7), 4 tests
- manager.rs: ChannelManager (channel map, open_channel,
  install_channel_zero, route_payload, teardown_channel, clear_all),
  11 tests
- source.rs: ChannelBidiStreamSource (yield-once accept_bi),
  channel_source helper, 4 tests
- adapter.rs: ChannelsAdapter (ProtocolHandler for alknet/channels,
  demux loop, install_channel_zero hook), 1 test
- operations.rs: ChannelOperations (registers channel/close,
  channel/control, channel/resources/subscribe), ChannelCore
  (check_open/on_close wrappers), 4 tests
- policy.rs: ChannelLifecyclePolicy trait, NoCap, PerIdentityChannelPolicy
  (default 256, per_identity_caps override), default_policy, 8 tests
- env.rs: ChannelOperationEnv extension trait (ADR-047 §4),
  ChannelsSessionEnv impl, 2 tests
- client.rs: ChannelClient (from_connection, call_open_op,
  take_call_connection), 1 test

Verification: 432 tests pass (66 new channels + 10 marker + 356
existing), clippy clean, fmt clean, cargo doc generates.

Cargo.toml: +bytes dependency.
2026-08-12 12:13:53 +00:00
glm-5.2 ea66398c88 feat: add Pub operation type, HandlerKind::Sink, call.published wire event (ADR-046)
The call protocol had Subscription (server→client streaming) but lacked
the directional complement: client→server streaming, where the initiator
produces a stream and the responder's handler consumes it. This gap was
inherited from the @alkdev/pubsub EventEnvelope prior art, which has
subscribe but no wire-level publish.

ADR-046 adds the Pub primitive:
- OperationType::Pub (client→server streaming)
- OperationType::Subscription renamed to Sub (wire: "subscription" → "sub")
- SinkHandler type + HandlerKind::Sink variant
- PublishStream type alias (Stream<Item = Result<Value, CallError>>)
- OperationRegistry::invoke_sink() dispatch path
- call.published wire event (sixth event type, additive)
- OperationSpec.publish_schema (Option<Value>, validates per-chunk input)
- DispatchResult::Sink + SinkDispatch (handler future + chunk channel)
- Dispatcher::pump_sink (feeds call.published chunks from wire to handler)
- CallConnection::publish() / publish_with_payload() client methods
- from_call sink forwarding handler (make_sink_forwarding_handler)
- make_sink_handler() helper

Fan-out/broker (one producer, N consumers, topic matching) is deferred to
the channels session — the call protocol is point-to-point; the broker is
a routing concern that sits above it. The Pub primitive is the
load-bearing piece the broker will compose on.

- 23 new tests (366 total, up from 343)
- clippy clean, fmt clean

Verification:
  cargo test                                    — 366 passed
  cargo clippy --all-targets -- -D warnings      — clean
  cargo fmt --check                              — clean
2026-08-12 08:06:46 +00:00
glm-5.2 cc470a363a docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet
mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045.

Renumbering map (alknet -> alkcall):
  Core:        001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012
  Call:        005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030
  Shared:      003,009,013 -> 031-033
  Channels:    071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045

3 superseded/reversed ADRs kept for historical trail:
  - ADR-013 (irpc foundation, superseded by ADR-014)
  - ADR-023 (peer-scoped filtering, superseded by ADR-024)
  - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only)

Ported docs (11 spec files + README + open-questions):
  - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md
  - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md
  - README.md (index with doc table, ADR table grouped by category, key principles)
  - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap)

Cross-reference rewriting:
  - All ADR-NNN references rewritten single-pass (no chaining bug)
  - Markdown link paths fixed
  - Title lines aligned with filenames
  - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note

The open-questions.md includes OQ-22 (new): the call protocol pub/sub
gap — subscribe exists but pub does not, needed for channels
channel/resources/subscribe fan-out. This is the next ADR to write
(alkcall ADR-046).
2026-08-12 07:06:57 +00:00