Commit Graph
606 Commits
Author SHA1 Message Date
glm-5.2 e6062230f9 tasks: fix taskgraph enum values for architecture tasks (level, impact, scope)
The three architecture tasks I seeded in the previous commit used invalid
enum values for the taskgraph frontmatter (level: architecture, impact: local,
scope: small). Corrected to the valid taskgraph enums: level: implementation/
decomposition/planning, impact: project/component, scope: moderate/narrow.
taskgraph validate now passes for tasks/architecture/.

(Pre-existing tasks/call/registry/*.md and tasks/core/ownership-store-trait.md
use status: done instead of completed — out of scope for this fix.)
2026-07-06 16:23:59 +00:00
glm-5.2 1baa619ce9 docs(arch): decompose open-questions.md into per-OQ files under questions/
The monolithic open-questions.md (1310 lines, 47 OQs) was large enough to be
unmanageable, with high size variance (OQ-42 at 220 lines next to OQ-06 at 8).
Decomposed into one file per OQ under docs/architecture/questions/ (NNN-slug.md,
mirroring the ADR convention), with open-questions.md retained as the index:
theme-grouped tables plus a cross-theme Deferred/Blocked section that surfaces
the 6 deferred OQs with their Blocked-on conditions inline (the safe-exit
visibility surface). Per-OQ content moved verbatim; all 62 inbound links stay
valid (none used anchors). README's curated OQ summary dropped (now redundant
with the index tables).

Also seeds tasks/architecture/ with this task plus two follow-ups found during
the decompose: OQ-09/10 missing structured Blocked-on fields, and the
tasks/architecture/ blocker-task half of the Safe Exit protocol being
unenforced.
2026-07-06 16:07:59 +00:00
glm-5.2 e4e6ccfb14 docs(arch): alknet-tty Phase 1 specs — wire format, backend trait, local backend, exit-chunk ordering
Grounded in the alknet-docker POC (seed codec) and the alknet-tty POC
(2026-07-05, validated the control channel, the local-PTY blocking→async
bridge, and the signal-delivery contract).

Four ADRs:
- ADR-052: wire format — alknet/tty ALPN, two-carriage (JSON negotiation
  then raw chunks), fixed channel set 0-3, control as JSON, negotiation-
  error framing disambiguation
- ADR-053: TtyBackend trait + TtyHandle — the backend inversion point;
  exit_code as a Future; REQ-TTY-01 (backends need not be natively async)
- ADR-054: local backend as alknet-tty-local sibling crate behind a
  feature re-export; PTY vs pipe per-session; runner pattern preserved
- ADR-055: exit code on a stream_type 3 control chunk; "exit chunk is
  last" invariant; adapter owns the ordering

Five component specs (crates/tty/): README, overview, tty-wire, tty-
backend, tty-adapter, tty-local. The docker/SSH backends are future
crates (out of scope); the trait shape is committed so they can be
built against it.

Five OQs (OQ-43…47): two resolved (TtyControl Clone, stdin closure),
two deferred(scope) (terminal modes, runner API surface), one open
low-risk (flow control).

Zero critical issues from a general-subagent architecture review;
warnings addressed (inline rationale trimmed to ADR refs, TtyError and
StdinCmd defined, framing disambiguation documented, missing ADRs added
to index tables).
2026-07-06 15:05:36 +00:00
glm-5.2 14c8340380 docs(arch): amend ADR-051 §2 — yaml_serde 0.10.x is YAML 1.2, not 1.1
Implementation verified yaml_serde 0.10.4 (and serde_yaml 0.9) implement the
YAML 1.2 core schema, where bare yes/no/on/off are strings, not booleans. The
original §2 rationale cited YAML 1.1 boolean-coercion as a present hazard
justifying JSON-first detection; it does not exist with this dependency
version.

The JSON-first decision is retained (Accepted ADR) — reframed as a defensive
default that locks the contract against a future YAML-parser swap, not a
guard against a present hazard. §2 heading, Consequences, and Assumption #2
amended; an in-place amendment note records what changed and when. The two
coercion-related tests already document yaml_serde 0.10.x's actual behavior
with explicit comments. Source doc-comments on from_yaml/from_str corrected
to stop propagating the flawed assumption. 256 tests pass; clippy clean.
2026-07-06 13:30:59 +00:00
glm-5.2 21c4f7d252 feat(http): implement ADR-051 YAML input for from_openapi — from_yaml + JSON-first from_str
Adds OpenAPISpec::from_yaml and OpenAPISpec::from_str alongside the
existing from_json. from_str is JSON-first, YAML-fallback (ADR-051 §2):
a valid JSON doc parses through serde_json (stricter grammar, immune to
YAML-specific coercion); only on JSON failure does it fall back to the
YAML parser. yaml_serde 0.10 (resolves to 0.10.4, latest stable) is the
maintained official-YAML-org fork of the deprecated serde_yaml; not
feature-gated per ADR-051 §3 (YAML OpenAPI is first-class).

Note: ADR-051 §2's stated YAML 1.1 boolean-coercion rationale does not
hold for yaml_serde 0.10 (YAML 1.2 core schema — yes/no/on/off are
strings). The JSON-first rule is retained per the Accepted ADR as a
defensive default; tests document the actual 0.10.x behavior with
explicit comments. The ADR rationale should be amended separately.

256 tests pass; clippy + fmt clean.
2026-07-06 13:15:53 +00:00
glm-5.2 193f3a0e24 docs(arch): ADR-051 — YAML input format for from_openapi (gap-fill; JSON-first detection, yaml_serde dep)
from_openapi's spec already stated the contract as "JSON/YAML doc" but the
implementation only delivered the JSON half. vast.ai publishes a YAML
OpenAPI schema, which surfaces the gap. This is gap-filling against an
existing constraint — no architecture invariants touched (OpenAPISpec
stays serde_json::Value-based, forwarding handler / no-env-vars / error
fidelity / to_openapi output unchanged).

ADR-051 records: (1) from_yaml + format-detecting from_str constructors;
(2) JSON-first/YAML-fallback detection as a correctness rule (YAML 1.1
coerces yes/no/on/off to booleans — running JSON through YAML would
silently corrupt string fields); (3) yaml_serde dependency (maintained
official-YAML-org fork of deprecated serde_yaml, not feature-gated);
(4) to_openapi output stays JSON (scope boundary).

Specs updated to reference ADR-051 by number (http-adapters, overview,
both README ADR tables). Implementation task added at
tasks/http/adapters/from-openapi-yaml-input.md.
2026-07-06 12:41:46 +00:00
glm-5.2 d0e3711b46 docs(research): record alknet-tty local-PTY POC findings — REQ-TTY-01 (blocking-backend trait accommodation) and REQ-TTY-02 (process-group signal forwarding)
Built /workspace/alknet-tty-poc against portable_pty 0.9 to validate the
local-PTY path (Step 2 of the build order) before Phase 1 specs. The POC
surfaced two constraints that were not knowable from reading the
portable_pty docs alone and that the architect must carry into the
tty-backend.md and tty-local.md specs:

- REQ-TTY-01: portable_pty is a blocking std::io API; the TtyBackend
  trait must accommodate blocking backends that bridge to async via std
  threads + tokio mpsc. exit_code resolves to a Future the adapter
  awaits (resolves the load-bearing half of OQ-TTY-01).
- REQ-TTY-02: signal forwarding must target the process group
  (kill(-pgid, sig)), which depends on the child being a session leader
  (portable_pty's controlling_tty=true default).

The POC also validated the control channel (stream_type 3), JSON control
messages (DP-3), and exit-code-on-control-chunk (DP-5). OQ-TTY-01 is
marked resolved with the control-as-Clone-trait-object sub-question left
open with a POC-informed recommendation. The POC itself lives in the dev
workspace, not the repo; this doc is the durable record.
2026-07-05 14:43:28 +00:00
glm-5.2 45fb7efbfc docs(research): remove hedging language from alknet-ssh and alknet-tty research
- alknet-ssh: remove paragraphs describing old hedging problem in channel
  decomposition section and DP-4 — state the insight directly
- alknet-tty: replace 'defer for v1' / 'reserved for future use' with
  'not needed for the current scope' in OQ-TTY-02
2026-07-05 13:08:29 +00:00
glm-5.2 73724b8a1d feat(core,call): implement ADR-050 dynamic resource ownership — ownership store, resource_id_path, check signature, dispatch wiring
Implements the 4-task DAG for runtime-spawned resource ownership so
AccessControl::check can answer "does this identity own this specific
container/TTY/process" instead of relying only on static
Identity.resources grants.

1. core/ownership-store-trait: OwnershipProvider (sync read) +
   OwnershipStore (async write) traits + InMemoryOwnershipStore +
   OwnershipError in alknet-core. Fourth instance of the repo/adapter
   pattern (ADR-033), mirroring CredentialStore/store.rs.

2. call/registry/operation-spec-resource-id-path: resource_id_path
   field on OperationSpec — JSON pointer into input for resource ID
   extraction. Single field addition, all ~40 construction sites across
   alknet-call + alknet-http updated to pass None (no semantic change).

3. call/registry/access-control-ownership-check: check() signature
   gains resource_id + Option<&dyn OwnershipProvider>. 3-case decision
   tree: ownership Some + resource_id Some -> owns(); ownership Some +
   resource_id None -> owns_any() (list scope-gate); ownership None ->
   static Identity.resources fallback (backward compat). 7 call sites
   updated to (None, None) — including a 7th in alknet-http/gateway_routes
   not listed in the original spec.

4. call/registry/dispatch-resource-id-extraction: wire the dispatch
   path. OperationContext gains ownership field; Dispatcher/CallAdapter
   gain with_ownership_provider builders; invoke/invoke_streaming/
   invoke_with_policy extract resource_id via spec.resource_id_path
   and thread context.ownership to check(). extract_json_pointer helper
   handles $.field syntax (graceful None on missing/non-string). 20
   OperationContext literals updated across both crates.

Backward compatibility is load-bearing throughout: ownership=None
falls back to the existing static resource-check path. Deployments
without runtime-spawned resources wire nothing and behave identically.

821 tests pass workspace-wide (was ~770); clippy clean; fmt clean.
Task specs marked done.
2026-07-05 12:57:33 +00:00
glm-5.2 de536b82e1 tasks: add ADR-050 implementation tasks — ownership store, resource_id_path, check signature, dispatch wiring
Four tasks forming a DAG for the dynamic resource ownership model (ADR-050):

1. core/ownership-store-trait (no deps) — OwnershipProvider (sync read) +
   OwnershipStore (async write) traits + InMemoryOwnershipStore + OwnershipError
   in alknet-core; fourth instance of the repo/adapter pattern (ADR-033)
2. call/registry/operation-spec-resource-id-path (no deps) — add
   resource_id_path: Option<String> to OperationSpec (JSON pointer into
   input for resource ID extraction)
3. call/registry/access-control-ownership-check (depends on 1) — update
   AccessControl::check signature to accept resource_id + OwnershipProvider;
   backward compatible (ownership=None falls back to static Identity.resources)
4. call/registry/dispatch-resource-id-extraction (depends on 2, 3) — wire
   dispatch path to extract resource_id from input via spec.resource_id_path
   and thread OwnershipProvider to check(); OperationContext gains ownership
   field

Tasks 1 and 2 can run in parallel (different crates, no deps). Task 3
depends on 1. Task 4 depends on 2 and 3. Validated: no cycles, 90 tasks total.
2026-07-05 11:12:44 +00:00
glm-5.2 ad167aa470 docs(arch): update core/call specs for ADR-050 — ownership provider + resource_id_path
operation-registry.md:
- OperationSpec gains resource_id_path: Option<String> (JSON pointer
  into the input for runtime-spawned resource ID extraction)
- AccessControl::check signature updated: consults an OwnershipProvider
  for dynamic resource ownership; falls back to static Identity.resources
  when no provider is wired (backward compatible)
- Dispatch flow updated: step 3 extracts resource_id via
  spec.resource_id_path before the ACL check
- Added composition + dynamic ownership interaction (ADR-050 §4d):
  two orthogonal checks, ADR-015/022 unchanged
- Design Decisions table + Open Questions + References updated

auth.md:
- New 'Ownership Provider and Store (ADR-050)' section: OwnershipProvider
  (sync read trait) + OwnershipStore (async write trait) + InMemoryOwnershipStore
  default adapter; fourth instance of the repo/adapter pattern (ADR-033)
- How it integrates with AccessControl::check
- Access pattern: proxy-only (spawner owns, proxy to share, teardown
  revokes; no grant mechanism in core)
- Per-node ownership (no cross-node propagation in the base model)
- Resource-scoped ACLs table gains the dynamic ownership path
- Design Decisions table + Open Questions updated
2026-07-05 08:50:04 +00:00
glm-5.2 f6ddd37433 docs(arch): add ADR-050 — dynamic resource ownership for runtime-spawned resources
Writes OQ-42's five decisions into ADR format:

1. Storage: reuse the repo/adapter pattern (ADR-033, fourth instance
   alongside IdentityProvider/IdentityStore/CredentialStore). New traits:
   OwnershipProvider (sync read, hot-path) + OwnershipStore (async write,
   handler lifecycle). In-memory default; persistence adapter additive.
2. Integration: AccessControl::check consults the ownership provider
   directly (Option 2). OperationSpec gains resource_id_path (JSON pointer
   into the input). Backward-compatible — ownership=None falls back to
   the static Identity.resources path.
3. Access pattern: proxy-only. Spawner owns, proxy to share via from_call
   + forwarded_for (ADR-032), teardown revokes. No grant mechanism in
   core. Future grant is additive (new trait method), stated as
   reversal-cost classification, not deferral.
4. Four edge specifics: list = scope-gate + result-filter; teardown =
   automatic, handler-driven; fleet = per-node ownership, downstream app
   tracks 'who is this for'; composition = two orthogonal checks,
   ADR-015/022 unchanged.

Reviewed: zero critical issues. Two warnings fixed (None-handling in the
check sketch, missing ADR-004 cross-ref). One suggestion applied
('v1 mechanism' → 'initial mechanism' to avoid hedging misread).
2026-07-04 16:08:04 +00:00
glm-5.2 13dd15ab0b docs(arch): resolve OQ-42 — proxy-only ownership model for runtime-spawned resources
Lock in the dynamic resource ownership model for runtime-spawned
resources (containers, TTYs, runner workspaces). Three decisions:

1. Storage: reuse the repo/adapter pattern (ADR-033, fourth instance
   alongside IdentityProvider/IdentityStore/CredentialStore) with an
   in-memory default adapter; sync read on the dispatch hot path.
2. Integration: AccessControl::check consults an ownership provider
   directly (Option 2); OperationSpec gains resource_id_path (JSON
   pointer into the input) so the spec is fully self-describing for
   authorization.
3. Access pattern: proxy-only — spawner owns, proxy to share via
   from_call + forwarded_for (ADR-032), teardown revokes. No grant
   mechanism in core; 'poking holes' is a downstream-app concern. A
   future grant is additive (new trait method), stated as reversal-cost
   classification, not deferral.

Four edge specifics pinned: list = scope-gate + result-filter; teardown
= automatic, handler-driven; fleet = per-node ownership, downstream app
tracks 'who is this for'; composition = two orthogonal checks, ADR-015/022
unchanged.

Removes the prior hedging language ('decision direction set', 'open for
the ADR') and the contingent qualifiers from specifics 3/4 now that the
proxy-vs-grant call is made. The dependent crate specs (docker, tty,
runner, fleet) can declare their AccessControl shapes against this model.
2026-07-04 16:02:38 +00:00
glm-5.2 3daecd7ab2 fix(process): add architect Safe Exit for deferred decisions, clean hedging language
- Add Safe Exit section to architect spec: when a decision genuinely can't
  be made, mark OQ as deferred(scope) with concrete blocking condition,
  create blocker task, move on
- Add anti-patterns #10-#11 covering hedging synonyms (feature extension,
  additive, not a v1 blocker, for now, resolved with escape hatch)
- Add hedging audit to architect self-review checklist
- Clean hedging language from resolved OQs (OQ-04, OQ-13, OQ-14, OQ-16)
- Reclassify OQ-32 and OQ-41 as deferred(scope) with blocking conditions
- Add deferred(scope) status to OQ format in sdd_process.md
2026-07-04 15:31:04 +00:00
glm-5.2 f390550a06 docs(arch): set OQ-42 decision direction — repo/adapter storage + Option 2 integration
Two structural decisions for dynamic resource ownership (OQ-42), recorded
in the OQ so ADR drafting starts from a clear position:

1. Storage side reuses the repo/adapter pattern (ADR-033) — a fourth
   instance alongside IdentityProvider/IdentityStore/CredentialStore.
   Trait in alknet-core with an in-memory default adapter; persistence
   adapter separable. Sync read with ArcSwap + honker-NOTIFY cache
   invalidation, same shape as ConfigIdentityProvider (ADR-035). No new
   shape invented; no Phase 0 needed for the storage side.

2. Integration point is Option 2 — AccessControl::check consults the
   ownership provider directly. Rejected Option 1 (augment identity with
   a per-request snapshot) because its purity was theatrical — the
   question 'can X exec into container C' was never purely a function of
   identity, it just looked that way because the resource set was static.
   Option 2 makes check's signature honest about what ACL checking is in
   the presence of dynamic resources. Cost is a check signature change
   (one-way door, every call site updates) — implementation cost, not
   semantic cost, per the project's decision principle.

Refinement that makes Option 2 clean: OperationSpec gains resource_id_path
(JSON pointer into the input, e.g. '$.containerId'). Fits naturally with
the existing JSON-Schema-backed input_schema — the pointer is within an
existing schema on the same spec. OperationSpec becomes fully
self-describing for authorization: resource type, action, and which input
field drives the resource lookup, all declared on the spec.

Four specifics remain open for the ADR: the no-specific-resource (list)
case, teardown coupling, fleet representation (spoke resources on the
hub), and composition interaction with dynamic ownership. These were
surfaced by choosing Option 2 rather than by leaving the integration point
undecided.
2026-07-04 13:04:14 +00:00
glm-5.2 e29672942c docs(arch): record OQ-42 — dynamic resource ownership for runtime-spawned resources
The alknet-docker POC research surfaced that containers are a natural
AccessControl resource, but the resource set is dynamic (containers are
created at runtime) and ownership is derived from creation — which the
current static Identity.resources model (config-sourced via
PeerEntry/CompositionAuthority) doesn't fit. The issue generalizes to
every crate that spawns a thing at runtime and exposes it over the call
protocol (docker, tty, opencode-runner wrapper, alknet-container fleet
layer); solving it per-crate would diverge. Recording as OQ-42 in the
centralized tracker with the generalized framing so the architecture
workflow sees it: one-way door at the model level (core/call), two-way
at the mechanism level, high priority, blocks the dependent crate specs.
A Phase 0 research/POC pass is likely warranted before the ADR.
2026-07-04 11:38:23 +00:00
glm-5.2 8c7443c7c6 docs(research): fix alknet-docker POC normalization crate boundary — alknet-compute is a workload not the fleet layer; add head-worker/machine-node model and dispatch reverse-runner prior art 2026-07-03 10:28:53 +00:00
glm-5.2 4500338384 docs(research): add alknet-tty phase-0 findings — terminal session protocol as separate ALPN, TtyBackend trait, dissolves alknet-ssh PTY hedge 2026-07-03 07:48:21 +00:00
glm-5.2 157f1dfb18 docs(research): add alknet-docker POC summary — validates two-carriage model (JSON + raw) for bollard docker ops over framed bidi streams 2026-07-02 17:08:52 +00:00
glm-5.2 e258ce0523 docs(review): mark review-streaming-impl completed — ADR-049 streaming handler review passes all 12 checklist points 2026-07-02 10:12:19 +00:00
glm-5.2 ab610730c0 docs(http): mark http/server/subscribe-sse-streaming completed — /subscribe pipes BoxStream to SSE 2026-07-02 10:10:55 +00:00
glm-5.2 c77024cdf5 fix(http): update websocket subscription tests to expect call.responded (dispatch_requested now routes Subscription via invoke_streaming) 2026-07-02 10:10:42 +00:00
glm-5.2 9e4d17b1c5 feat(http/server/subscribe-sse-streaming): wire /subscribe to invoke_streaming and pipe BoxStream to SSE
Replace the one-event placeholder (subscribe_stream_from_envelope +
envelope_to_sse_stream, which called invoke() and wrapped the single
ResponseEnvelope) with the real streaming path: subscribe_handler now
calls GatewayDispatch::invoke_streaming() and pipes the
BoxStream<ResponseEnvelope> to SSE via subscribe_stream_from_envelope_stream
(futures::StreamExt::map). Each Ok(output) becomes a data: frame; each
Err becomes an event:error frame (terminal — stream ends after it);
natural stream end closes the SSE. Internal ops still return a single
NOT_FOUND error event via subscribe_stream_internal_error (kept). Client
disconnect drops the stream via Rust's Drop (abort cascade per ADR-016).
2026-07-02 10:04:27 +00:00
glm-5.2 c34b4d2df4 docs(call): mark call/protocol/dispatch-streaming-branch completed — server-side streaming dispatch 2026-07-02 09:59:00 +00:00
glm-5.2 2905e55e72 Merge branch 'feat/call/protocol/dispatch-streaming-branch' into develop 2026-07-02 09:58:37 +00:00
glm-5.2 c58eccd5a6 feat(call/protocol/dispatch-streaming-branch): branch handle_stream on op_type; Subscription → invoke_streaming → pump each → call.completed
Add DispatchResult::Once|Stream enum and Dispatcher::dispatch() that branches
on the registered op_type (ADR-049 §6): Query/Mutation → invoke() (unchanged
Once path), Subscription → invoke_streaming() (Stream path). handle_stream
matches on DispatchResult: the Once path writes one call.responded/call.error
frame (no call.completed); the Stream path pumps each ResponseEnvelope to the
wire via pump_stream (ResponseEnvelope.into() → call.responded for Ok,
call.error for Err), then writes call.completed on natural stream end. An Err
envelope is terminal — last_was_error suppresses call.completed after an error.
The streaming branch clears context.deadline to None (subscriptions are
unbounded — ADR-049 §6, call-protocol Timeouts). Abort (ADR-016) needs no new
code: handle_abort removes the pending entry and dropping the pump task
releases handler resources via Drop. dispatch_requested delegates to dispatch
for backward compatibility with existing callers.
2026-07-02 09:56:05 +00:00
glm-5.2 b673b7f317 docs(http): mark http/gateway/invoke-streaming completed — GatewayDispatch::invoke_streaming() 2026-07-02 09:55:15 +00:00
glm-5.2 4ac8d308e6 feat(http/gateway/invoke-streaming): add GatewayDispatch::invoke_streaming
Add the streaming analogue of invoke() returning BoxStream<ResponseEnvelope>.
Security invariants are identical to invoke() (internal: false,
forwarded_for: None, same capabilities/scoped_env/ACL) — shared via a
build_root_context_inner helper with a bounded flag. The streaming path
sets deadline: None (unbounded subscriptions, ADR-049 §6). Calls
OperationRegistry::invoke_streaming() (already on develop). to_mcp is
unchanged (MCP excludes Subscription, ADR-041).

Tests cover: subscription dispatch, leading-slash strip, unknown op
NOT_FOUND, internal op NOT_FOUND (not leaked), None identity FORBIDDEN,
Query op INVALID_OPERATION_TYPE, invoke() on Subscription returns
INVALID_OPERATION_TYPE (guard holds through gateway), and
build_root_context_streaming sets deadline: None while carrying the
registration bundle.
2026-07-02 09:54:14 +00:00
glm-5.2 62bebe5122 docs(http): mark http/adapters/from-openapi-sse-streaming completed — SSE streaming forwarding 2026-07-02 09:48:39 +00:00
glm-5.2 a1e4752fdf Merge branch 'feat/http/adapters/from-openapi-sse-streaming' into develop 2026-07-02 09:48:07 +00:00
glm-5.2 6f05dd8995 feat(http/adapters/from-openapi-sse-streaming): branch from_openapi forwarding on op_type; Subscription → StreamingHandler (SSE → BoxStream<ResponseEnvelope>)
build_registration now branches on op_type: Subscription ops register a
StreamingHandler (HandlerKind::Stream) via make_streaming_handler that
streams SSE response chunks as ResponseEnvelope::ok() items (one per
data: frame); Query/Mutation ops keep the existing Handler
(HandlerKind::Once) via forward(). Closes the gap where a from_openapi-
imported Subscription returned only the last SSE event.

- forward_stream(): non-async fn returning ResponseStream; sends the
  request with Accept: text/event-stream, then streams SSE chunks via
  stream::unfold over response.bytes_stream(), reusing parse_sse_frames
  (multi-event, partial trailing, comments, multi-line data, BOM).
  HTTP error (non-2xx) → single ResponseEnvelope::error(), stream ends;
  SSE stream end → ResponseStream ends (→ call.completed on wire).
- Removed stream_subscription() (the collect-all placeholder that
  truncated to the last event). parse_sse_frames stays (reused).
- Query/Mutation forwarding unchanged (existing forward() path).
- Tests: Subscription registration is HandlerKind::Stream; Query
  registration is HandlerKind::Once; SSE subscription streams multiple
  ResponseEnvelope::ok() (one per data: frame); HTTP error → single
  error envelope; Query forwarding unchanged (single response).
2026-07-02 09:45:55 +00:00
glm-5.2 d841cc35b9 docs(call): mark call/client/from-call-streaming-forwarding completed — streaming forwarding handler 2026-07-02 09:45:48 +00:00
glm-5.2 5c37e5b3af Merge branch 'feat/call/client/from-call-streaming-forwarding' into develop 2026-07-02 09:45:29 +00:00
glm-5.2 67b1adba98 feat(call/client/from-call-streaming-forwarding): branch from_call forwarding on op_type
Subscription ops discovered via services/list + services/schema now
register a StreamingHandler (HandlerKind::Stream) that calls
CallConnection::subscribe_with_payload and forwards the remote stream
end-to-end (ADR-049 §8). Query/Mutation ops keep the existing
make_forwarding_handler (HandlerKind::Once).

- Add CallConnection::subscribe_with_payload(payload) mirroring
  call_with_payload so the forwarding handler can populate forwarded_for
  (ADR-032) + auth_token on the subscription payload. subscribe() now
  delegates to subscribe_with_payload.
- Add make_streaming_forwarding_handler() in from_call.rs using
  make_streaming_handler + futures::stream::once(...).flatten() to await
  subscribe_with_payload then forward its stream.
- Branch build_bundles on spec.op_type (already parsed by rebuild_spec_for).
- Reuse build_forwarded_payload — no new payload-construction code.
- composition_authority: None, scoped_env: None for FromCall streaming
  leaves (same as Query/Mutation FromCall leaves).
- Abort cascade (ADR-016 §6) already wired via PendingRequestMap in
  subscribe_with_payload.

Closes the gap where a from_call-imported Subscription truncated to the
first value.
2026-07-02 09:43:45 +00:00
glm-5.2 f12e227df0 docs(call): mark call/registry/invoke-streaming completed — invoke_streaming() streaming dispatch 2026-07-02 09:41:59 +00:00
glm-5.2 acaa0513e4 feat(call/registry): add OperationRegistry::invoke_streaming() returning ResponseStream
Streaming dispatch path for Subscription operations — counterpart to
invoke(). Same visibility + ACL checks (internal → handler_identity,
external → identity), then dispatches to the StreamingHandler. Pre-handler
errors (not-found, forbidden, INVALID_OPERATION_TYPE for non-Subscription
ops) yield a single error ResponseEnvelope via stream::once and end the
stream. Adds 6 unit tests covering dispatch, not-found, wrong-kind,
internal-from-external, ACL denied, and internal-call handler_identity ACL.

Refs ADR-049 §3, §5.
2026-07-02 09:39:31 +00:00
glm-5.2 185ddb82b5 docs(call): mark call/registry/streaming-handler-handlerkind completed — StreamingHandler/HandlerKind foundation 2026-07-02 09:29:11 +00:00
glm-5.2 9c81129f24 feat(call): introduce StreamingHandler, HandlerKind, ResponseStream + INVALID_OPERATION_TYPE (ADR-049)
Add the foundational types for ADR-049 streaming handlers:
- StreamingHandler, ResponseStream type aliases and HandlerKind enum
  (Once | Stream) in registration.rs, with make_streaming_handler() helper
- CallError::invalid_operation_type() in wire.rs (sixth protocol code,
  retryable: false)
- HandlerRegistration.handler flipped from Handler to HandlerKind;
  HandlerRegistration::new() now takes HandlerKind
- OperationRegistryBuilder absorbs wrapping: with_local/with_leaf/
  with_leaf_provenance wrap raw Handler in HandlerKind::Once for
  Query/Mutation; new with_local_streaming/with_leaf_streaming take a
  StreamingHandler and wrap in HandlerKind::Stream for Subscription.
  Builder validates kind matches spec.op_type (mismatch = startup error)
- OperationRegistry::register() returns Result<(), String> with a clear
  mismatch message; all call sites updated to handle the Result
- invoke() matches on HandlerKind: Once -> existing path; Stream ->
  INVALID_OPERATION_TYPE error envelope (guards against silent
  truncation; invoke_streaming() added in a downstream task)
- OverlayOperationEnv::invoke_with_policy matches on HandlerKind:
  Once -> dispatch; Stream -> INVALID_OPERATION_TYPE (composition is
  request/response-only)
- Migrated every HandlerRegistration::new() construction site (~95)
  to wrap raw Handler in HandlerKind::Once(handler); the builder sites
  are handled by the builder-absorbs-wrapping change
- Updated two websocket subscription tests that relied on Subscription
  ops dispatching via invoke() to expect INVALID_OPERATION_TYPE
- Added unit tests for invoke/register validation and
  make_streaming_handler
2026-07-02 09:28:05 +00:00
glm-5.2 07f7607fbb tasks(decomp): ADR-049 streaming handler — 8 atomic tasks + gitignore .worktrees/
Decompose the ADR-049 streaming handler work into 8 dependency-ordered tasks:
- call/registry/streaming-handler-handlerkind (foundation: StreamingHandler,
  HandlerKind, ResponseStream, INVALID_OPERATION_TYPE, migrate all sites)
- call/registry/invoke-streaming (OperationRegistry::invoke_streaming)
- call/protocol/dispatch-streaming-branch (server-side op_type branch)
- call/client/from-call-streaming-forwarding (Subscription → subscribe())
- http/gateway/invoke-streaming (GatewayDispatch::invoke_streaming)
- http/server/subscribe-sse-streaming (/subscribe pipes BoxStream to SSE)
- http/adapters/from-openapi-sse-streaming (SSE → StreamingHandler)
- review-streaming-impl (phase review checkpoint)

Validated with taskgraph: 86 tasks, no cycles. Also ignore .worktrees/ so
agents' worktree workspaces don't leak into git status.
2026-07-02 08:23:27 +00:00
glm-5.2 7ecc11610a docs(arch): ADR-049 — streaming handler for subscription operations
The call protocol spec describes streaming (call.responded*N +
call.completed, PendingRequestMap::Subscribe, CallConnection::subscribe),
but the server-side Handler type returned a single ResponseEnvelope —
a Subscription op had no way to produce a stream. The TS predecessor
(@alkdev/operations) had separate OperationHandler / SubscriptionHandler
types; the Rust port collapsed them, losing the streaming path. This
restores it end-to-end: StreamingHandler type, HandlerKind on
HandlerRegistration validated against op_type, invoke_streaming() on
OperationRegistry, server-side dispatch branches on op_type, new
INVALID_OPERATION_TYPE protocol code for wrong-dispatch-path misuse,
GatewayDispatch::invoke_streaming() for /subscribe SSE, from_call stream
forwarding via CallConnection::subscribe(), from_openapi SSE forwarding.
OperationEnv::invoke() stays request/response-only (stream composition is
handler-level, not protocol-level). Amends ADR-023's protocol-code list
(five → six). Tracks the stream-operators library as OQ-41 (feature
extension, not an unmade decision).
2026-07-02 07:43:01 +00:00
glm-5.2 139c651eaa docs(http): mark http/review-http-final completed — alknet-http crate review complete
Final crate-wide review passes all 9 checklist areas: crate structure, feature gate
isolation, dependencies, cross-cutting concerns, pattern consistency, ADR conformance
(003-048), absence of deferred/out-of-scope items, test coverage (277+2 alknet-call,
230 default, 265+5 mcp), build cleanliness (fmt/clippy/build all clean).
2026-07-01 23:40:29 +00:00
glm-5.2 5a51734291 docs(http): mark http/review-websocket and http/review-http completed 2026-07-01 23:39:40 +00:00
glm-5.2 b3ab6ef097 docs(http): mark http/adapters/to-openapi completed + fix formatting
to_openapi gateway projection merged: 5-endpoint OpenAPI doc (ADR-042/045), pure
projection, info.version 1.0.0, error fidelity (ADR-023). 230 tests pass. Clippy clean.
2026-07-01 23:37:35 +00:00
glm-5.2 ccaac7e157 Merge feat/http-to-openapi: to_openapi gateway projection (5-endpoint OpenAPI doc, ADR-042/045)
Implements to_openapi(registry) -> OpenAPISpec in src/adapters/to_openapi.rs — pure
projection generating fixed 5-endpoint gateway doc (/search, /schema, /call, /batch,
/subscribe) with info.version = 1.0.0 (ADR-045). /call responses carry protocol-level
errors + operation-level errors mapped by http_status (ADR-023). Per-caller operation
surface NOT preloaded (discovered via /search, ADR-042). /subscribe response is
text/event-stream. Wired GET /openapi.json in adapter.rs. 16 new tests.
2026-07-01 23:36:48 +00:00
glm-5.2 18156ac9d2 Merge origin/develop: integrate connection-overlay (resolve adapter.rs test conflict, keep /openapi.json route test) 2026-07-01 20:17:06 +00:00
glm-5.2 dd6aacc598 feat(http): complete to_openapi gateway projection with error fidelity and route wiring
Refine to_openapi to project operation-level errors (with http_status)
onto /call and /subscribe responses via oneOf merge with protocol-level
errors, preserving HTTP_<status> prefix codes without collision. Fix
BTreeMap→serde_json::Map for Value::Object compatibility. Wire GET
/openapi.json route test. Apply cargo fmt across the crate.
2026-07-01 20:11:09 +00:00
glm-5.2 2695a19502 feat(http): implement to_openapi gateway projection (5-endpoint OpenAPI doc, info.version 1.0.0)
to_openapi(registry) -> OpenAPISpec generates the fixed 5-endpoint
gateway doc (/search, /schema, /call, /batch, /subscribe) — pure
projection (ADR-017 §5), gateway pattern (ADR-042). info.version is
1.0.0 tracking the gateway contract (ADR-045). /call responses carry
protocol-level errors (400/401/403/404/500/504) plus operation-level
errors mapped by http_status (ADR-023). GET /openapi.json wired to
serve the generated spec.
2026-07-01 19:52:57 +00:00
glm-5.2 0b0bceec7a docs(http): mark http/websocket/connection-overlay completed 2026-07-01 19:48:36 +00:00
glm-5.2 468bd84f86 Merge feat/http-connection-overlay: Connection-local Layer 2 overlay for browser-registered ops
Adds AccessControl::check to OverlayOperationEnv::invoke_with_policy in alknet-call
so hub's calls to browser-registered ops are gated by the browser's AccessControl.
Creates src/websocket/overlay.rs with 19 integration tests: overlay scoping (not
PeerCompositeEnv), no PeerId, register_imported/all, overlay_env() routing,
PeerRef::Specific→NOT_FOUND, AccessControl gating, overlay drop on WS close,
ADR-016 abort cascade, bidirectionality, no-ops use-case scoping.
2026-07-01 19:46:56 +00:00
glm-5.2 ad279693ce feat(http): connection-local Layer 2 overlay for browser-registered ops (ADR-024/034/044)
Enforce AccessControl on overlay ops in OverlayOperationEnv::invoke_with_policy
(alknet-call) so the hub's calls to browser-registered ops are gated by the
browser's AccessControl — matching OperationRegistry::invoke semantics for
internal composition (caller identity = parent handler_identity.as_identity()).

Add src/websocket/overlay.rs with 19 integration tests covering the connection-
local overlay acceptance criteria: browser ops land in the per-CallConnection
overlay (not PeerCompositeEnv), no PeerId for the browser, register_imported()/
register_imported_all() populate the overlay, hub outgoing calls route through
overlay_env() (not PeerRef::Specific), PeerRef::Specific('browser-X') routes to
NOT_FOUND, AccessControl gates hub calls (allowed/forbidden/default), overlay is
per-connection isolated and dropped on WS close, WS close aborts in-flight calls
with ADR-016 cascade, bidirectionality, and browser-with-no-ops use-case scoping.
2026-07-01 19:45:36 +00:00