Commit Graph
100 Commits
Author SHA1 Message Date
glm-5.2 ef53a03589 feat(call,http): expose EventEnvelope-level dispatch API for non-QUIC transports
Make Dispatcher::dispatch_requested pub and extract abort-cascade handling
into a pub handle_abort method so the WebSocket handler can feed deserialized
EventEnvelopes directly to the shared Dispatcher without a QUIC Connection.

CallConnection gains a new_overlay_only(identity) constructor (Option A) that
holds the Layer 2 overlay, PendingRequestMap, and resolved bearer Identity
without a QUIC Connection; identity() reads the stored field for the non-QUIC
case. compose_root_env uses the new identity() accessor for both paths.

The existing QUIC path (CallAdapter, CallClient, run_loop, handle_stream) is
unchanged — outgoing client methods guard on connection().is_none().
2026-07-01 17:17:02 +00:00
glm-5.2 1900c72deb docs(http): mark http/crate-init completed 2026-07-01 16:44:57 +00:00
glm-5.2 87bfaf159a Merge feat/http/crate-init: Initialize alknet-http crate (ADR-039)
Adds crates/alknet-http with Cargo.toml, src/lib.rs, and five subsystem
module skeletons (server, gateway, client, adapters, websocket). Workspace
members list updated. Dependencies: alknet-core, alknet-call (workspace path),
axum, hyper, reqwest stack, openapiv3; rmcp optional behind mcp feature
(streamable HTTP only, ADR-037). h3/WebTransport absent (ADR-044).
2026-07-01 16:42:38 +00:00
glm-5.2 c2e6ba5b96 feat(http): initialize alknet-http crate with module skeleton
Add crates/alknet-http with Cargo.toml, src/lib.rs, and the five
subsystem modules (server, gateway, client, adapters, websocket) per
ADR-039 (server + client host colocated). The mcp feature gate pulls in
rmcp with streamable HTTP transport features only (ADR-037 — no stdio);
h3/WebTransport is absent (deferred per ADR-044). alknet-core and
alknet-call use workspace path deps. The crate is added to the workspace
members list.
2026-07-01 16:41:14 +00:00
glm-5.2 e855c8c7eb docs(http): decompose alknet-http spec into 19 implementation tasks
Break the alknet-http architecture spec into atomic, dependency-ordered
tasks in tasks/http/, following the taskgraph frontmatter conventions
used by the call/core/vault crates.

Tasks span 7 phases across 5 module subdirectories (server/, gateway/,
client/, adapters/, websocket/):
- Phase 0: crate-init (foundation)
- Phase 1: gateway-dispatch-spine, error-mapping, shared-http-client
  (shared infrastructure)
- Phase 2: http-adapter, bearer-auth-middleware, gateway-endpoints,
  healthz-decoy (HTTP server surface)
- Phase 3: to-openapi (OpenAPI gateway projection)
- Phase 4: from-openapi (OpenAPI adapter, reqwest forwarding)
- Phase 5: dispatcher-transport-abstraction, upgrade-handler,
  connection-overlay (WebSocket browser bidirectional path)
- Phase 6: from-mcp, to-mcp (MCP adapters, feature-gated)
- Phase 7: review-http, review-websocket, review-mcp, review-http-final
  (quality checkpoints)

The gateway-dispatch-spine task implements the thin shared core
recommended by the gateway-factoring research (concrete struct, not a
trait). The dispatcher-transport-abstraction task is a cross-crate
change to alknet-call (exposes EventEnvelope-level dispatch API for
non-QUIC transports) — the highest-risk task. WebTransport/h3 is
deferred per ADR-044 and has no tasks; from_wss is out of scope.

Validated: 19 tasks, no cycles, 8 parallel generations, critical path
length 8 (through the WebSocket strand).
2026-07-01 07:11:17 +00:00
glm-5.2 e0c6f61e6a docs(http): pre-decomposition sanity check fixes — /subscribe POST, direct-call cleanup, from_mcp output handling
Three issues found in the http crate spec sanity check that would have
caused problems during task decomposition, now fixed:

C1 — /subscribe GET→POST: the gateway's /subscribe is an invoke endpoint
carrying { operation, input } in the body, but was listed as GET (which
has no body). Flipped to POST with Accept: text/event-stream negotiating
the SSE response, consistent with /call's flat-JSON-body invariant.
Browsers using EventSource can't POST but use WebSocket for the
bidirectional path; the HTTP gateway's /subscribe is for non-browser
HTTP clients (fetch + ReadableStream). Touches ADR-042, ADR-047,
ADR-048, http-adapters.md, http-server.md.

C2 — stale direct-call references: three spots contradicted ADR-047
(which removed the POST /{service}/{op} direct-call surface) and
ADR-046 §3 (which states /{service}/{op} is no longer reserved).
Cleaned up in http-server.md (custom-routes intro + collision list) and
ADR-046 §6 (default-surface list).

W2 — from_mcp output handling: the spec's fallback for tools without
outputSchema was Type.Unknown(), but the correct fallback is the MCP
ContentBlock union (text|image|audio|resource|resource_link) — a
well-defined MCP type, not Unknown. Fixed http-mcp.md with the full
structuredContent-preferred-over-content-blocks logic (matching the TS
adapter and rmcp SDK), enriched references with specific rmcp source
files. Also added shared-dispatch-spine notes to http-mcp.md and
http-adapters.md cross-referencing the new research findings.

Research (docs/research/alknet-http-gateway-factoring/findings.md):
to_mcp and to_openapi share a dispatch spine (resolve → invoke → map).
Recommendation: extract a thin shared struct now, not a GatewayDispatch
trait — the server-integration layers (axum routes vs rmcp
StreamableHttpService) and wire-framing stay per-gateway. A third
gateway is not on the horizon; if one appears its server-integration
needs its own shape anyway.

Minor: WS route precedence note (websocket.md), OpenAPISpec
shared-type-not-shape clarification (http-adapters.md), date bumps.
2026-07-01 05:41:07 +00:00
glm-5.2 3edc42e3b4 docs(compute): add wonnx + handlebars/wgpu reference implementations
Document the two codebases that inform the ShaderGenerator's op table
and the wgpu+handlebars+remote-GPU patterns:

- wonnx (MIT/Apache-2.0, archived): comprehensive ONNX op set in
  Tera-templated WGSL at wonnx/templates/ — arithmetic, activation,
  gemm, conv, batchnorm, softmax, etc. Port the shader implementations,
  swap Tera for handlebars. compiler.rs's add_raw_template +
  include_str! pattern maps 1:1 to handlebars-rs register_template_string.

- Handlebars + wgpu + remote-GPU patterns (private reference, patterns
  reusable): validates the handlebars-rs side and the vast.ai deployment
  shape. Patterns carried over: {{> partial}} includes for shared
  fragments, inline-able constant tables via switch statements (SHA-256
  k-values, universal across wgpu versions), default-valued template
  parameters, wgpu-on-remote-GPU sync. sha256 as a base shader
  demonstrating non-ML compute on the same dispatch surface.

Updated the WGSL codegen probe POC to reference wonnx's op set as the
porting source.
2026-06-30 13:05:54 +00:00
glm-5.2 303b9a58e2 docs(research): split alknet-tensor into alknet-runtime + alknet-compute + alknet-tensor
Extract the shared JS+wgpu substrate (verified by the alknet-desktop POCs)
as alknet-runtime — the generalized QuickJS-NG + wgpu runtime that both
alknet-desktop (render) and alknet-compute (tensor compute) build on. Key
property driving the split: wgpu on llvmpipe is genuinely useful compute
with no physical GPU (WGSL → optimized SIMD beats JS for non-trivial
workloads), so wgpu is unconditional in the runtime rather than a feature
flag.

Reframes the original alknet-tensor architecture-summary as alknet-compute
(builds on alknet-runtime + alknet-tensor) with ShaderGenerator as a trait
(WGSL first impl, SPIR-V/GLSL/naga-IR later per wgpu multi-input-language
support). alknet-tensor/metatensor-format.md is now clearly the pure binary
format crate (no JS or wgpu dep), usable standalone by a pure-Rust model
server.

Layering: alknet-runtime depends on alknet-call (registry authority stays
per ADR-013); alknet-compute and alknet-desktop depend on alknet-runtime;
alknet-tensor is a pure-format sibling.
2026-06-30 12:44:39 +00:00
glm-5.2 b71db99753 docs(http): add ADR-048 and websocket.md — WS carries native session, not gateway
Promote the WebSocket browser path from a section in http-server.md to a
first-class spec (websocket.md) and commit the contract-pattern decision
(ADR-048): a WS connection carries the native EventEnvelope call-protocol
session, not the HTTP gateway shape. The gateway endpoints are HTTP-only;
discovery on WS is via services/list/services/schema as ordinary call-protocol
ops; subscriptions project as native call.responded events (no SSE).

ADR-044 already decided WS as the v1 browser bidirectional path; ADR-048
clarifies the shape of what ADR-044 committed (§1 implies native session;
the ADR makes it an explicit implementer-visible rule). The from_wss adapter
(importing a remote node's ops over WS) is recorded as out-of-scope with a
concrete reversal trigger so it is not re-derived later.

Spec cleanup: http-server.md WS section collapsed to a stub pointer;
websocket.md Why section references ADRs rather than re-arguing them;
length-prefix decision made canonical (no prefix on WS — message boundary
is the delimiter); default upgrade path pinned (/alknet/call) with HTTP/2
extended CONNECT noted; indexes (README, http/README, overview) updated.
2026-06-30 12:27:00 +00:00
glm-5.2 bfd1621b9b feat(call): add ScopedPeerEnv peer-pinned reachability (ADR-029 §4, call/scoped-peer-env) 2026-06-30 11:07:41 +00:00
glm-5.2 5c4feff468 tasks: add call/scoped-peer-env — ScopedPeerEnv peer-pinned reachability (ADR-029 §4) 2026-06-30 10:31:19 +00:00
glm-5.2 850ac6b7bc fix(call): remove dead test helper and unused mut (clippy -D warnings clean) 2026-06-30 10:30:52 +00:00
glm-5.2 2a6e4c371a docs(http): resolve OQ-39; add ADRs 045-047; record pubsub prior art for WS path
OQ-39 (to_openapi published-spec versioning) resolved by ADR-045:
info.version semver tracks the gateway endpoint contract, not the
operation set — per-caller operations discovered via /search do not
bump the version. The gateway pattern (ADR-042) dissolved most of the
original churn concern.

ADR-046: assembly-layer custom HTTP routes on HttpAdapter. The HTTP
router had no documented extension point for deployment-specific
endpoints (e.g., an OAI-compatible proxy at /v1/chat/completions). Adds
extra_routes: Option<Router> at construction; raw HTTP, not operations;
default surface takes precedence on collision. The mechanism is the
one-way door; specific routes are two-way.

ADR-047: remove the direct-call POST /{service}/{op} HTTP surface. The
gateway /call is the sole invoke path — the simplified contract is a
few fixed endpoints, not a per-operation REST tree. The direct-call
surface re-introduced the 'dump the full API regardless of privs'
failure mode at the HTTP level that the gateway /search was built to
escape. ADR-036's routing decision is superseded; its non-routing
clauses (SSE, Bearer auth, /healthz, stealth, error mapping) survive.
A deployment wanting a REST-like per-operation surface builds it as a
custom route projection (ADR-046).

ADR-044 updated with the tradeoff framing (WSS is the right tool for
the call-protocol-from-browser case; WebTransport is the right tool for
the generalized ALPN-stream-proxy case we don't have yet — coexist, not
migrate) and the @alkdev/pubsub concrete prior art (the EventEnvelope
{type,id,payload} the call protocol was derived from already has a
working WebSocket client/server; the sync is a small adjustment, not a
from-scratch build).

call-protocol.md references the pubsub lineage for the
transport-agnosticism claim.
2026-06-30 09:49:25 +00:00
glm-5.2 3327d585da docs(http): resolve OQ-40 reqwest client config — ClientWithMiddleware + retry/retry-after middleware stack
OQ-40 resolved: alknet-http owns a shared reqwest_middleware::ClientWithMiddleware
(not a bare reqwest::Client) with a two-layer middleware stack —
RetryTransientMiddleware (reqwest-retry, exponential backoff on transient
failures) + inlined RetryAfterMiddleware (from melotic/reqwest-retry-after, MIT,
~50 lines, inlined to bound the upstream's unbounded HashMap storage). The two
are complementary: reqwest-retry's default strategy does not honor Retry-After.

Hot-reload is rebuild-and-swap via ArcSwap (same pattern as
ConfigIdentityProvider, ADR-035); a rebuild drops the connection pool, which
is acceptable since a config change wanting a fresh pool is the trigger. The
three one-way constraints stand unchanged: alknet-http owns its client (no
env-var config, no shared global), credentials inject per-request from
OperationContext.capabilities, outbound TLS uses the system trust store.

Records the downstream layering boundary: the agent crate's provider SSE
normalization (the solid part of aisdk's pattern — Vercel-UI-message
normalization) sits on top of this client, consuming the reqwest::Response
stream; it does not replace the client. The aisdk core/client.rs reference for
client construction is dropped (env-var config + hand-rolled retry are the
anti-patterns discarded); the from_openapi.ts SSE normalization reference in
the forwarding-handler section is kept (separate, solid pattern).

No ADR — the decision is internal to alknet-http: the client type does not
cross crate boundaries (alknet-call never sees reqwest), the library choice is
reversible, and it does not touch the system's structure, constraints, or
cross-crate API surface.

Updates: http-adapters.md (HTTP client section rewritten, references updated,
constraints/OQ bullets updated), http-mcp.md (OQ-40 status flip), open-
questions.md (OQ-40 resolved with full config-shape table), README.md (OQ-40
folded into the existing two-way-doors bucket), and three secondary docs
(crates/http/README.md, overview.md, http-server.md) that carried stale 'open'
OQ-40 references.
2026-06-30 08:02:30 +00:00
glm-5.2 125cb49cc4 docs(http): defer h3/WebTransport (ADR-044); browsers use WebSocket for v1
Working through the WebTransport implementation path surfaced a scope
question distinct from the hedging-as-deferral anti-pattern ADR-038 was
written to correct. Three findings drove the re-evaluation:

1. The browser bidirectional call-protocol path doesn't require
   WebTransport — WebSocket is full-duplex, EventEnvelope fits a WS
   binary message boundary cleanly, and the Dispatcher is stream-
   agnostic (ADR-012). What WebTransport gives over WebSocket (native
   multi-stream multiplexing, the ALPN-as-stream substrate) benefits the
   proxy use case, not the call protocol.
2. WebTransport is a draft standard (-07, not RFC) on an experimental
   Rust dependency stack (wtransport/h3 both self-describe as not
   production-ready). Either choice puts a draft protocol on the
   security surface of the first release.
3. The ALPN-stream-proxy (ADR-040) is speculative — its WASM parser
   consumers (browser SSH/SFTP/git clients) don't exist yet, and the
   downstream crates WebTransport deferral blocks (SSH, git, SFTP)
   expose their ALPNs natively over QUIC regardless.

This is a scope decision (per ADR-009: a decision that 'genuinely
doesn't need to be made yet because the use case isn't concrete'), not
hedging. The reversal trigger is concrete: a real deployment needing
the ALPN-stream-proxy.

ADR-038 is superseded (its anti-pattern correction stands; its specific
'h3 in scope now' decision is reversed). ADR-040 and ADR-043 are
parked, not superseded — their designs revive unchanged when WebTransport
revives, with §2 (bidirectionality) and §3 (no-PeerId overlay) of ADR-043
transferring to WebSocket for v1.

ADR-044 §5 also states the 'browser is not a peer' rationale that
ADR-034 §4 closed without arguing: peer = addressable node in the
call-protocol peer graph (stable PeerId, PeerRef::Specific-reachable,
identity stable across reconnects), not 'any endpoint that exchanges
calls during a live session.' A browser is the second but not the first
(no stable crypto identity of its own, ephemeral, not addressable from
other nodes). ADR-034 §4 and Assumption 2 are amended by reference.

The wtransport-vs-hyperium dependency question is recorded (not
resolved — WebTransport is deferred) in ADR-044 §'Research note' and
webtransport.md so the revival doesn't re-derive it: wtransport probably
isn't the right choice (axum-bridge friction — it owns its own HTTP
serving path); the hyperium stack (h3 + h3-quinn + h3-webtransport) fits
the axum integration better but its server-side WebTransport API needs
verification before commitment.

Reviewed by architecture-review subagent; all critical cross-reference
issues (ADR-034 §5 stale 'in scope' assertion, ADR-036 Context listing
h3 as implemented, webtransport.md Design Decisions table) resolved.
2026-06-30 05:55:55 +00:00
glm-5.2 78b226d31b docs(research): revise alknet-ssh phase-0 — channel decomposition, WebTransport grounding, WASM client
Reframes the SSH scope around the channel multiplexer as the decomposition
point. Each feature (forwarding, SOCKS5, SFTP) is a channel type or a consumer
of channel types, stacking on the core — each layer functional when built,
none shipped broken. Dissolves the 'massive v1' framing that produced hedging
language proposing non-functional or half-built versions.

Three developments since the initial 2026-06-25 research changed the framing:
(1) WebTransport landed as ADRs 038/040/043, grounding SSH-over-WebTransport
as a constraint (the handler must be source-agnostic about its Connection);
(2) russh's runtime abstraction (russh-util swaps tokio::spawn for
wasm_bindgen_futures on wasm32) means the SSH *client* runs in WASM when fed a
WebTransport BiStream — the browser case is real, not speculative;
(3) the http crate intersection (ALPN-stream-proxy depends on SSH handlers
being source-agnostic) is now visible and specified.

The layered build order (1-4 stream+connection+channels+exec, then 5
forwarding, then 6 SOCKS5, then 7 SFTP) doubles as the configuration surface:
each layer beyond the core is an opt-in channel type, gating on the
default-deny ACL baseline inherited from russh.
2026-06-29 13:03:11 +00:00
glm-5.2 0a78306686 docs(http): add ADR-043 WebTransport bidirectional ALPN substrate; fix spec drift from mid-spec pivot
A consistency review of the alknet-http specs found two classes of
issues: internal contradictions from the mid-spec pivot (the to_openapi
gateway pattern landed in prose but not in cross-references), and a
systematic client→server assumption that only holds for the OpenAPI/MCP
case leaking into the WebTransport architecture.

Class 1 (internal contradictions):
- C1: to_openapi was half-refactored — body described the ADR-042
  gateway pattern but the decisions table and ADR-036 still said
  'paths mirror /{service}/{op}'. ADR-036's to_openapi clause is now
  amended as superseded by ADR-042; the stale decisions row and README
  Principle 2 are fixed.
- C2: the axum Router route list didn't include the 5 gateway endpoints
  (/search, /schema, /call, /batch, /subscribe). Added them; clarified
  /openapi.json as the gateway description doc; added gateway paths to
  the decoy exclusion list.
- C3: ADR-034 §5 still talked about the 'h3/WebTransport deferral
  bucket' that ADR-038 eliminated. Amended §5/Consequences/References
  to drop the deferral framing (the auth-model decision stands; only
  the 'when' wording was stale).

Class 2 (one-way direction assumption):
- C4/C5/C6: the WebTransport specs framed the session as browser→hub
  one-way, when the call protocol is bidirectional and WebTransport is
  a general ALPN transport substrate. New ADR-043 reframes WebTransport
  as a bidirectional ALPN transport substrate (call protocol is the
  first/canonical target; needs no WASM parser), names the call
  protocol's bidirectionality over WebTransport sessions, and states
  the inbound no-PeerId connection-local overlay as the mirror of
  ADR-034 §2. webtransport.md is updated to reflect this framing;
  ADR-040 is repositioned (not superseded) as the substrate's non-call-
  ALPN mechanism.
- C7: the HTTP/1.1+HTTP/2 surface's one-directionality is now named as
  a lossy consequence of HTTP request/response; WebTransport is named
  as the surface that restores the bidirectional call model.
- C8: overview.md acknowledges the from/to direction model is
  OpenAPI/MCP-specific, not a call-protocol property.

A review subagent pass on ADR-043 + webtransport.md found no critical
issues; warnings W1-W3 (residual browser-as-subject framing, ADR-009
rationale in spec, opening abstract tone) and suggestions S2/S4/S5
were addressed.
2026-06-29 10:43:18 +00:00
glm-5.2 69ebe58bab docs(http): add ADR-042 OpenAPI gateway pattern for to_openapi
The to_openapi spec was describing one OpenAPI path per alknet operation
— the inverse of from_openapi. That inverse is genuinely messy: the call
protocol's input is a flat JSON object, and generating a traditional
OpenAPI path entry (POST /fs/{path} with path param, body, query params)
requires reverse-engineering which fields are path/query/body — metadata
the call protocol doesn't carry. The three options (leaky HTTP metadata
on OperationSpec, fragile heuristics, manual annotation) are all messy.

ADR-042 replaces this with the gateway pattern (same as ADR-041 for
to_mcp): to_openapi generates 5 fixed endpoints (search, schema, call,
batch, subscribe) that gate access to the full operation registry. The
input is always a flat JSON body — no path/query/body split to
reverse-engineer. JSON Schema is already in the OperationSpec.

The per-caller API surface is the key advantage: /search is
AccessControl-filtered, so the client sees only what it can call. The
Gitea failure mode (dumping admin ops to every caller in a static
OpenAPI doc) is structurally impossible — the per-caller surface is the
default, not an afterthought. OpenAPI has no per-caller filtering
concept; the gateway pattern provides it through /search.

Gateway endpoint set:
- /search -> services/list (AccessControl-filtered, names + descriptions)
- /schema -> services/schema (full OperationSpec)
- /call -> call.requested (Query/Mutation, flat JSON body)
- /batch -> multiple call.requested (correlated IDs)
- /subscribe -> call.requested (Subscription, SSE) — the one endpoint
  the MCP gateway excludes (MCP is request/response; OpenAPI/SSE
  supports streaming)

A traditional per-operation-paths projection is additive (a deployment
that wants the nice Swagger UI builds it with HTTP-specific metadata),
not a replacement. The gateway is the default.

http-adapters.md to_openapi section rewritten: the gateway endpoint
set, per-caller filtering, error fidelity on the /call endpoint, and
the additive traditional projection. The 'Why' section adds the
flat->structured and per-caller-surface rationale.

README/overview ADR tables and the top-level README current-state note
updated for ADR-042.
2026-06-29 09:33:39 +00:00
glm-5.2 5fc074713c docs(http): add ADR-041 MCP tool-gateway pattern for to_mcp
The to_mcp spec was describing one MCP tool per alknet operation — the
tool-bloat problem. An LLM connecting to a node with 200 operations gets
200 MCP tools dumped into its context, degrading reasoning and wasting
context budget.

ADR-041 replaces this with the tool-gateway pattern (same pattern as
opencode's memory and worktree tools): to_mcp exposes 4 fixed meta-tools
(search, schema, call, batch) that gate access to the full operation
registry. The LLM has a few tools in context, discovers operations on
demand through search + schema, then calls. Same principle as Linux's
man command — don't preload all documentation; query on demand.

Gateway tool set:
- search -> services/list (names + descriptions, AccessControl-filtered)
- schema -> services/schema (full OperationSpec for a specific op)
- call -> call.requested (Query/Mutation only, request/response)
- batch -> multiple call.requested (correlated IDs, OQ-14)

Subscription operations are excluded — MCP tool calls are
request/response by protocol design (the client blocks until
CallToolResult returns); streaming subscriptions don't fit. Subscriptions
are filtered out of search results and cannot be invoked via call.

http-mcp.md to_mcp section rewritten: the gateway tool set, Subscription
exclusion, and the service behavior (tools/list returns 4 fixed tools,
tools/call dispatches through the gateway). The 'Why' section adds the
tool-bloat rationale and the memory/worktree tool pattern that informed
the design.

README/overview ADR tables and the top-level README current-state note
updated for ADR-041.
2026-06-29 08:34:44 +00:00
glm-5.2 398e3d512d docs(http): add ADR-040 WebTransport ALPN-stream-proxy and reframe OQ-38
The 'WebTransport proxy' concept was conflating two distinct things;
this pass separates them:

1. In-process ALPN-stream-proxy (ADR-040, in alknet-http): the h3 handler
   hands a WebTransport stream to another ALPN handler (SshAdapter,
   GitAdapter, etc.) as a Connection, so a browser with a WASM parser
   can reach any ALPN service via WebTransport. Path-based routing
   (the CONNECT path declares the target: /alknet/ssh -> SshAdapter).
   HttpAdapter gains Arc<HandlerRegistry> for the lookup. The browser's
   WASM parser implements BiStream (ADR-007) over the WebTransport
   stream. SSH-over-WebTransport is HTTPS-shaped at the network layer
   (anti-censorship: the 'VPN-like without being a VPN' use case on a
   clean foundation). russh-sftp demonstrates WASM targeting is
   feasible; SSH is the next target.

2. Standalone relay service (OQ-38, future alknet-relay crate): a full
   relay - fork of iroh-relay - with WebTransport proxy fallback for
   NAT traversal. This is infrastructure, not a mode of the h3 handler.
   OQ-38 reframed to be the standalone-relay scope question (distinct
   from the in-process proxy now resolved by ADR-040).

webtransport.md updated: three stream destinations (call protocol,
ALPN-handler proxy, other sub-protocols) with path-based routing; new
'ALPN-stream-proxy' section covering the WASM client side, auth model
(bearer token gates the session; protocol's own auth gates the
protocol session), and the HandlerRegistry reference.

README/overview ADR tables and OQ summaries updated for ADR-040.
2026-06-29 07:56:35 +00:00
glm-5.2 ab47dac4ad docs(http): draft alknet-http architecture specs and ADRs 036-039
First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3
server + from_openapi/to_openapi/from_mcp/to_mcp adapters).

Specs (crates/http/):
- README.md, overview.md — crate index, two-roles-in-one-crate framing,
  adapter location map, feature gates (h3, mcp), no-env-vars invariant
- http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream,
  Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy
- http-adapters.md — from_openapi (reqwest) and to_openapi (projection),
  error fidelity (HTTP_<status> per ADR-023), type definitions
- http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only
- webtransport.md — h3/WebTransport handler, browser streaming path,
  HTTP/3 request vs WebTransport session distinguished at framing layer

ADRs:
- ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path
  mapping; to_openapi is projection, not router (the load-bearing one-way
  door from Phase 0 DH-3)
- ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP
  only; stdio is not built (RCE-vector security position)
- ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports
  (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in
  scope, not deferred, per ADR-009 §'What this framework is NOT'
- ADR-039 HTTP Server and Client Host Colocated in alknet-http
  (Proposed) — one crate for server + client host (shared HTTP deps,
  shared operation-spec->HTTP mapping)
- ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation
  crate (the alknet-http -> alknet-call dependency edge)

Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'):
- OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not
  a deferral — the decision is made when the use case becomes concrete)
- OQ-39 to_openapi published-spec versioning (one-way after first
  publication)
- OQ-40 reqwest client config and connection pooling (two-way-door)

Architecture README and overview updated with doc table, ADR table
(036-039), current-state note, and crate graph (alknet-http ->
alknet-call edge).

Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5
suggestion issues found and fixed (missing ADR-039, WebTransport stream
routing conflation, undefined types, stale OQ-37 deferral language,
README OQ table completeness, Bearer-only attribution, cross-references,
ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
2026-06-29 05:53:38 +00:00
glm-5.2 dd5ccf4983 tasks: mark call/review-call-sync complete — all 17 tasks done 2026-06-28 22:29:40 +00:00
glm-5.2 507358b285 review(call): fix fmt drift in adapter.rs and env.rs (call/review-call-sync) 2026-06-28 22:29:10 +00:00
glm-5.2 1af81346d1 tasks: mark call/call-client-verifier-selection complete 2026-06-28 22:24:45 +00:00
glm-5.2 c106f4a37b feat(call): wire CallClient TLS client-auth and server cert verifier selection (call/call-client-verifier-selection)
Replace AcceptAnyServerCertVerifier (a security hole for X.509) with
verifier selection by PeerEntry presence (ADR-034 §3, OQ-29):

- build_client_auth presents the Ed25519 key as an RFC 7250 raw public
  key client cert (replaces with_no_client_auth), activating the
  PeerEntry fingerprint -> peer_id resolution path on quinn.
- select_server_verifier: Some(fingerprint) -> FingerprintPinVerifier
  (fingerprint match for known peers); None -> WebPkiServerVerifier
  (CA verification for public X.509 endpoints). None + Ed25519 raw key
  fails closed at handshake (no CA to fall back to).
- FingerprintPinVerifier matches ed25519:<hex> (raw key extraction) and
  SHA256:<hex> (DER hash); verifies handshake signatures via
  verify_tls13_signature_with_raw_key / verify_tls12/13_signature.
- Extract shared fingerprint logic into alknet_core::fingerprint (pub
  module) reused by endpoint (server-side) and call_client (client-side).
- remote_identity: None is load-bearing (not defaulted to placeholder).
- Integration tests updated to pin the self-signed server cert
  fingerprint (the known-peer path).
2026-06-28 22:24:09 +00:00
glm-5.2 d9227b8123 tasks: mark call/from-call-forwarded-for complete 2026-06-28 22:22:20 +00:00
glm-5.2 f5fede2758 feat(call): wire from_call forwarded_for and peer-keyed collision (call/from-call-forwarded-for) 2026-06-28 22:21:52 +00:00
glm-5.2 95b06fc07f tasks: mark call/dispatch-peer-identity complete 2026-06-28 22:21:44 +00:00
glm-5.2 7f9e5828b9 feat(call): wire dispatch_requested to resolve peer Identity, ACL gate, and forwarded_for (call/dispatch-peer-identity) 2026-06-28 22:21:23 +00:00
glm-5.2 b7acd1d69d tasks: mark call/operation-env-invoke-peer complete 2026-06-28 22:10:06 +00:00
glm-5.2 d04cb9c125 feat(call): add invoke_peer/peer_contains/PeerRef to OperationEnv for peer-keyed routing (call/operation-env-invoke-peer) 2026-06-28 22:09:35 +00:00
glm-5.2 a9792b4010 tasks: mark call/operation-context-forwarded-for complete 2026-06-28 22:08:53 +00:00
glm-5.2 5d6a943ad4 feat(call): add forwarded_for field to OperationContext (call/operation-context-forwarded-for) 2026-06-28 22:08:35 +00:00
glm-5.2 37e430b09d tasks: mark call/services-list-accesscontrol-filtered complete 2026-06-28 22:03:40 +00:00
glm-5.2 877c923244 feat(call): filter services/list by AccessControl and add services/list-peers opt-in (call/services-list-accesscontrol-filtered) 2026-06-28 22:03:29 +00:00
glm-5.2 2902ccff18 tasks: mark call/peer-composite-env complete 2026-06-28 22:02:47 +00:00
glm-5.2 e8219fa550 feat(call): replace CompositeOperationEnv with peer-keyed PeerCompositeEnv (call/peer-composite-env) 2026-06-28 22:02:17 +00:00
glm-5.2 1aeb634a2d tasks: mark call/retire-remote-safe complete 2026-06-28 21:53:20 +00:00
glm-5.2 4490bc251f feat(call): retire remote_safe/trusted_peer/RemoteFilter (call/retire-remote-safe) 2026-06-28 21:52:57 +00:00
glm-5.2 fb510d0887 tasks: mark core/review-core-sync complete 2026-06-28 21:44:26 +00:00
glm-5.2 c63c6ec471 review(core): ADR-029/030/031/034/035 sync verified conformant (core/review-core-sync) 2026-06-28 21:44:18 +00:00
glm-5.2 3eadd47618 tasks: mark core/fingerprint-normalization complete 2026-06-28 21:41:02 +00:00
glm-5.2 ea31200d17 feat(core): normalize Ed25519 raw-key SPKI fingerprint to ed25519:hex (core/fingerprint-normalization) 2026-06-28 21:40:45 +00:00
glm-5.2 3c7cfe5446 tasks: mark core/config-identity-provider-peerentry complete 2026-06-28 21:40:36 +00:00
glm-5.2 50abd346a4 feat(core): wire ConfigIdentityProvider to PeerEntry multi-credential path (core/config-identity-provider-peerentry) 2026-06-28 21:40:27 +00:00
glm-5.2 e980fcc27f tasks: mark core/identity-store-trait complete 2026-06-28 21:37:26 +00:00
glm-5.2 74c1e8d42c feat(core): add IdentityStore async write trait extending IdentityProvider (core/identity-store-trait) 2026-06-28 21:36:14 +00:00
glm-5.2 e0ecc9e370 tasks: mark core/three-remote-roles-docs complete 2026-06-28 21:35:50 +00:00
glm-5.2 221a64b2b4 feat(core): document three remote roles and client-side verifier selection (core/three-remote-roles-docs) 2026-06-28 21:35:31 +00:00
glm-5.2 4c31f19c9c tasks: mark core/peer-entry-model and core/credential-store-trait complete 2026-06-28 21:30:10 +00:00
glm-5.2 f3702196e4 feat(core): add CredentialStore trait, InMemoryCredentialStore, EncryptedData mirror, StoreError (core/credential-store-trait) 2026-06-28 21:29:39 +00:00
glm-5.2 d1b8811432 feat(core): add PeerEntry struct and replace AuthPolicy.authorized_fingerprints with peers (core/peer-entry-model) 2026-06-28 21:27:42 +00:00
glm-5.2 df355c53a9 tasks: decompose ADR-029/030/031/032/034/035 source sync into 17 tasks
Decompose the source-to-spec sync for the core and call crates into atomic,
dependency-ordered tasks for implementation agents:

Core (7 tasks + review):
- peer-entry-model: PeerEntry struct, AuthPolicy.peers (ADR-030 keystone)
- credential-store-trait: CredentialStore/InMemoryCredentialStore/StoreError (ADR-031/035)
- identity-store-trait: IdentityStore async write trait (ADR-035)
- config-identity-provider-peerentry: ConfigIdentityProvider PeerEntry resolution (ADR-030)
- fingerprint-normalization: ed25519:hex for raw keys across quinn/iroh (ADR-030 §6)
- three-remote-roles-docs: document ADR-034 roles and verifier selection
- review-core-sync: phase gate before call consumes new identity semantics

Call (9 tasks + review):
- retire-remote-safe: remove ADR-028 machinery, AccessControl is the gate (ADR-029 §3)
- operation-context-forwarded-for: forwarded_for field, wire-ingress only (ADR-032)
- peer-composite-env: PeerCompositeEnv, PeerId=Identity.id, remove UUID (ADR-029/030)
- operation-env-invoke-peer: invoke_peer/peer_contains/PeerRef (ADR-029 §2)
- services-list-accesscontrol-filtered: AccessControl filter, list-peers opt-in (ADR-029 §6)
- call-client-verifier-selection: TLS client-auth, verifier by PeerEntry (OQ-29, ADR-034)
- from-call-forwarded-for: populate forwarded_for, peer-keyed registration (ADR-029 §5, ADR-032)
- dispatch-peer-identity: AccessControl::check(peer_identity), PeerId from resolution (ADR-029 §3, ADR-030 §5)
- review-call-sync: phase gate for the call sync

Validated: 58 tasks, no cycles, logical topo order, two review checkpoints.
2026-06-28 21:08:41 +00:00
glm-5.2 4a52779460 docs(arch): amend call specs for ADR-029/030/032/034 — peer-keyed routing, PeerEntry, forwarded-for, three roles
Sync the call crate specs to the accepted ADRs 029-034:
- operation-registry: PeerCompositeEnv (peer-keyed overlays), invoke_peer/
  PeerRef routing, retire remote_safe/trusted_peer, AccessControl-based peer
  auth, forwarded_for on OperationContext (ADR-029/030/032)
- call-protocol: peer-keyed compose_root_env, forwarded_for in call.requested
  payload, build_root_context forwarded_for parameter (ADR-029/032)
- client-and-adapters: CallClient verifier selection by PeerEntry presence,
  remote_identity: None load-bearing, three remote roles (ADR-034)
- README: ADR-029/030/032/034 in applicable ADRs table
2026-06-28 21:08:26 +00:00
glm-5.2 0de2cebb1d docs(arch): ADR-035 — concrete persistence adapter shapes, resolve OQ-36
Commits the concrete adapter shape deferred by ADR-033: read-sync /
write-async split with honker NOTIFY/LISTEN for no-restart cache
invalidation, against SQLite, in a separate alknet-store-sqlite crate.

Two constraints drive the design: (1) the hot-path read trait
(IdentityProvider::resolve_from_fingerprint, CredentialStore::get) is
sync — called in the accept loop, no .await — so a SQLite-backed
adapter must cache in memory and serve sync reads from the cache; (2)
auth changes must take effect without a restart (an early issue the
project already fixed for ConfigIdentityProvider via ArcSwap config
reload). honker's SQLite NOTIFY/LISTEN (single-digit-ms wake, no
polling) is the cache-invalidation mechanism that makes both hold:
write commits to SQLite + emits NOTIFY, the running process's LISTEN
wakes, the in-memory index reloads and atomically swaps, the next
read sees the new state. Same ArcSwap-reload pattern as config,
generalized from 'config file is source of truth' to 'SQLite is
source of truth, honker signals when it changed.'

New async IdentityStore write trait (put_peer / update_peer /
remove_peer) extends the sync IdentityProvider read trait for peer
mutations. ConfigIdentityProvider does NOT implement it (config
reload is its write path — a posture enforced by the absence of a
backend, not a type-system constraint); SqliteIdentityProvider
implements both. CredentialStore::put/delete refined to async (within
ADR-031's one-way door — the contract was get/put/delete keyed by
provider persisting EncryptedData never decrypting; sync-vs-async was
unspecified). CredentialStoreError renamed to shared StoreError
covering both traits.

alknet-store-sqlite is one crate implementing both IdentityStore and
CredentialStore with shared SQLite connection + honker LISTEN infra
(splitting later is a two-way door). Schema shape committed (one row
per PeerEntry with JSON columns for fingerprints/scopes/resources;
one row per EncryptedData blob keyed by provider); exact DDL is an
implementation-detail two-way door in the adapter crate. The keypal
adapter-factory pattern is intentionally not ported to Rust (runtime
column-mapping is a TS affordance; in Rust each adapter is a concrete
type, cross-cutting concerns are a shared helper module).

Amends ADR-031 (put/delete async refinement, StoreError rename),
ADR-033 (concrete adapter shape now specified, two-crate framing
collapsed to one), ADR-034 (OQ-36 now resolved), auth.md (IdentityStore
section, cache-invalidation summary, OQ-36 reference), config.md (two
write paths note), and the OQ-36/OQ-34 entries in open-questions.md.
Review fixed 4 criticals (error-type name divergence, duplicate
IdentityProvider sketch, upsert/Duplicate ambiguity, 'shape unchanged'
contradiction), 7 warnings, 5 suggestions.
2026-06-28 11:10:31 +00:00
glm-5.2 6cc8715ccf docs(arch): ADR-034 — outgoing-only X.509 and three peer roles, resolve OQ-37
Untangles the conflation of three distinct remote roles under 'X.509
endpoint': (1) public X.509 endpoint — a remote HTTPS/call-over-TLS
server the local node is a client of (no PeerEntry, no PeerId, not in
the peer graph; CA verification + bearer token); (2) transport relay —
iroh's DERP-equivalent, infrastructure, not an alknet peer; (3) hub /
hosting node — an alknet peer that also exposes a public domain + X.509
for browsers (mixed-fingerprint PeerEntry, already supported by
ADR-030).

The load-bearing one-way door is the client-side verifier selection
rule: known peer (PeerEntry present) → fingerprint pin; unknown X.509
remote → CA verification (WebPkiServerVerifier); unknown Ed25519
remote → fails closed. This closes the AcceptAnyServerCertVerifier
security hole OQ-29 flagged, with the peer-model criterion (PeerEntry
presence) made explicit. The 'make PeerEntry symmetric' instinct is
rejected — pure-client connections to public APIs have no stable
logical identity to pin.

Documents that CallCredentials.remote_identity: None is load-bearing
(None = public X.509 endpoint → CA path, not a missing field; Some =
known peer → fingerprint pin), closing a subtle gap where an
implementer could have defaulted to a placeholder or treated None as
skip-verify.

Records WebTransport relay-as-proxy (deferred with h3/WebTransport,
new OQ-HTTP-07) and on-chain/smart-contract peer discovery (fits the
OQ-36 repo/adapter pattern, no auth-model change) so they aren't lost.

Amends auth.md and client-and-adapters.md with the three-role naming,
the verifier selection rule, and the Option semantics; updates OQ-37
to resolved in open-questions.md, README.md, and both crate READMEs.
2026-06-28 10:47:49 +00:00
glm-5.2 3f011cbb82 docs(arch): tighten door-type framing — reversal cost, not deferral
ADR-009, open-questions.md, and the architect agent spec all had the same
conflation: 'two-way door' was phrased as 'can be decided during
implementation,' which reads as 'defer the decision.' That's not what it
means. A two-way door is a decision you make now and can revert later if
wrong — it's about reversal cost, not urgency.

ADR-009: add §'What this framework is NOT' — explicitly separates door
type (reversal cost) from deferral (scope management). State that
architecture decisions are the architect's regardless of door type.
Reword the two-way-door process from 'can be decided during
implementation' to 'pick the simplest option that works, implement it,
revert if needed.'

open-questions.md: reword the header to clarify door type describes
reversal cost, not urgency. Add 'Door type is separate from whether a
decision is made.'

architect.md: add Key Principle #8 (decisions are made, not deferred),
a new 'Door Types and Decision Urgency' section, and two new anti-patterns
(#8: door type as deferral, #9: hedging language in resolved decisions).
2026-06-28 09:19:10 +00:00
glm-5.2 7d812af8f4 docs(arch): multi-credential PeerEntry, resolve OQ-29, dissolve OQ-35, add OQ-37
Amend ADR-030 with three changes from the auth-type analysis:

1. PeerEntry is now multi-credential: fingerprints: Vec<String> (Ed25519
   and/or X.509) + auth_token_hash: Option<String> (bearer token). All
   resolve to the same peer_id. A peer that authenticates via Ed25519
   today and via auth_token tomorrow gets the same PeerId. The 'peer
   bearer vs auth bearer' distinction was wrong — the correct framing is
   the three credential types (Ed25519, X.509, bearer token) and whether
   the token needs a stable logical id across rotation (PeerEntry) or not
   (ApiKeyEntry).

2. Fingerprint normalization (§6): quinn extracts the raw Ed25519 public
   key from the SPKI cert and formats as ed25519:<hex>, matching iroh.
   The same key has the same fingerprint regardless of transport. X.509
   fingerprints stay as SHA256:<hex of DER>. This also simplifies the
   coming WebTransport relay work.

3. The 'API keys' section is replaced with 'Bearer tokens' — correctly
   framing the three auth types and the two bearer-token paths
   (PeerEntry.auth_token_hash vs ApiKeyEntry).

Resolve OQ-29 (CallClient TLS client-auth): wire quinn client-auth (present
Ed25519 key as raw public key client cert — the server-side extraction
already works); key-type-aware server cert verification (raw key =
fingerprint match, X.509 = CA verification via WebPkiServerVerifier —
AcceptAnyServerCertVerifier is only safe for raw keys); fingerprint
normalization. The iroh path already works (RFC 7250 raw keys, both sides
exchange automatically); the gap was quinn-only.

Dissolve OQ-35: the 'API key asymmetry' framing was wrong. PeerEntry
supports multiple credential paths; ApiKeyEntry is for tokens that ARE the
identity.

Add OQ-37: X.509 outgoing-only case — the three auth types and how X.509
server identity fits the peer model. Not blocking the ADR-029 migration;
downstream (HTTP crate phase).

Update auth.md, config.md, client-and-adapters.md, call/README.md,
core/README.md, open-questions.md, README.md, and call_client.rs source
comment.

Workspace green: 326 tests pass, build clean.
2026-06-28 08:49:36 +00:00
glm-5.2 1d94aaea51 docs(arch): resolve call-crate OQs, promote OQ-29 to load-bearing on ADR-030
Resolve the call-crate open questions where the decision is made —
OQ-27 (auto-re-import), OQ-28 (same-peer collision = error), OQ-30
(PeerRef::Any insertion-order first-match), OQ-31 (services/list-peers
opt-in). These were previously marked 'open' with 'v1' hedging language
despite having a decided default. What remains (refresh(), richer routing,
services/list-peers the op) is genuine feature addition, not unmade
architecture.

Reframe OQ-32 (multi-hop) as a feature extension rather than a 'v1'
deferral — the one-hop model is the architectural commitment; extending
to multi-hop doesn't break downstream.

Promote OQ-29 (CallClient TLS client-auth) from medium to high priority
and surface its real interaction with ADR-030. Previously framed as
'additive — two-way-door remainder,' but ADR-030's PeerEntry fingerprint
→ peer_id resolution requires the client to present a TLS client cert.
With with_no_client_auth(), no fingerprint is extracted, the PeerEntry
path is dormant, and PeerCompositeEnv keys on None or the API-key prefix
instead of the stable peer_id. This is the activation path for ADR-030's
primary use case, not an additive feature. Three options laid out: (a)
wire client-auth with the ADR-029 migration, (b) ship token-only and
switch later (the 'compounds into a mess' path), (c) extend PeerEntry
to cover auth_token-based identity. Requires a decision before the
migration lands.

Clarify OQ-36 (concrete adapter shapes): the trait shapes and in-memory
adapters ship with core — the deferral is only for the persistence
adapters (SQLite, etc.). The in-memory adapters are real implementations
of a full repo pattern, not stubs.

Update call_client.rs source comment to reference OQ-29 instead of the
'v1' / 'two-way-door remainder' framing.

Workspace green: 326 tests pass, build clean.
2026-06-28 05:35:52 +00:00
glm-5.2 f224ea998c docs(arch): ADR-030..033 — repo/adapter pattern, PeerEntry, CredentialStore, forwarded-for
Land the storage and auth strategy research (findings.md) as four
accepted ADRs and amend the core and call specs to match:

- ADR-030: PeerEntry and Identity.id decoupling. Replaces
  authorized_fingerprints with peers: Vec<PeerEntry>; Identity.id becomes
  the stable peer_id, decoupled from the rotating fingerprint. Supersedes
  ADR-029 Assumption 1's UUID source (one-way door preserved, source
  changes). Resolves OQ-33 and the storage-boundary half of OQ-34. Records
  the API-key asymmetry as deliberate (OQ-35).

- ADR-031: CredentialStore repo trait + InMemoryCredentialStore default
  adapter in core. Second repo trait alongside IdentityProvider. Vault
  encrypts; the store persists the EncryptedData blob; assembly layer
  loads into Capabilities. EncryptedData core mirror includes salt for
  wire-format compat.

- ADR-032: Forwarded-for identity. forwarded_for field on call.requested
  and OperationContext — metadata only, never read by AccessControl::check
  (enforced structurally via the check signature). The from_call handler
  populates it. Wire-format one-way door, folded into the ADR-029
  migration window.

- ADR-033: Storage boundary and repo/adapter pattern. Core defines repo
  traits + in-memory defaults; persistence adapters are separate crates;
  assembly layer wires. Resolves OQ-34. Concrete adapter shapes deferred
  for exploration (OQ-36).

Amends auth.md, config.md, operation-registry.md, client-and-adapters.md,
open-questions.md, README.md, crates/core/README.md. Marks ADR-029
Accepted (Assumption 1 carries the ADR-030 superseded note). Marks the
research findings doc reviewed.
2026-06-27 12:12:25 +00:00
glm-5.2 347bff257c docs(research): rewrite storage/auth strategy — concrete repo/adapter design, no deferrals
Reworks the storage strategy doc to commit to concrete design, replacing
the 'when storage arrives' / 'future' / 'later' framing that was putting off
important work.

Key changes from the previous draft:
- §4 (Repo/Adapter Pattern): now an explicit design with the trait contracts
  (IdentityProvider, CredentialStore), the adapter contracts
  (ConfigIdentityProvider with PeerEntry update, SqliteIdentityProvider,
  InMemoryCredentialStore, SqliteCredentialStore), and the concrete table
  schemas. Not a pattern description — a design commitment.
- §4: PeerEntry config model — AuthPolicy gains peers: Vec<PeerEntry>
  replacing authorized_fingerprints: HashSet<String>. This is the
  id-fingerprint decoupling (OQ-33) done as a config change, not a storage
  change. ConfigIdentityProvider resolves fingerprint → PeerEntry →
  Identity { id: peer_id } (stable, not the fingerprint).
- §7 (Decomposition): the 'what goes where' table now has a Status column
  (exists / needs adding / needs building / needs PeerEntry update) instead
  of 'future'. The crate graph is a concrete build plan.
- §10 (Build Order): replaces 'What This Means for the Immediate Path' (which
  had 'when storage arrives' framing) with a 4-tier dependency-driven build
  order. Tier 1 = core repo traits + PeerEntry config model. Tier 2 = SQLite
  adapters. Tier 3 = ADR-029 migration + forwarded_for. Tier 4 = alknet-graphs
  (built when a graph-shaped problem exists, not speculatively).
- §10: explicit 'What does NOT get built (dropped, not deferred)' section —
  multi-tenant, accounts/orgs, secrets module, single storage crate are
  dropped, not deferred.
- All 'future' / 'when X arrives' / 'v1' / 'phase n' language removed for
  things that are needed. The only 'when X is needed' language remaining is
  for genuinely non-existent problems (ACL delegation, workflows, taskgraph)
  — those are built when the problem exists, not speculatively.
2026-06-27 10:36:07 +00:00
glm-5.2 19d010cf73 docs(research): storage and auth strategy — repo pattern, per-node ACL, SQLite+honker, metagraph-as-tool
Synthesizes the multi-thread discussion that surfaced during the peer-graph
routing research (ADR-029) and OQ-33/34 resolution. Three separate threads
(peer identity, filesystem POC, old storage spec) converged on the same
question: where does persistent state live in the alknet crate graph, and
what's the shared infrastructure for it.

Key commitments documented:
- SQLite + honker is the foundation (pattern, not a crate — ~20 lines per
  consumer). The metagraph is one tool built on it, for graph-shaped
  problems. Direct tables are another tool, for table-shaped problems.
- IdentityProvider is the auth repo trait (already exists in core, make the
  pattern explicit). Adapters implement it (Config, SQLite, future
  Redis/remote/automerge). PeerStore is adapter-internal, not core.
- Per-node ACL, no 'trusted' flag. Each node authorizes its direct callers
  via AccessControl::check(identity). No global ACL, no replication. The
  hub authorizes the user; the spoke authorizes the hub. Same mechanism.
- Forwarded-for identity as metadata, not authority. The from_call handler
  includes the original caller's identity in the call payload; the spoke's
  ACL authorizes the hub (direct caller), never the forwarded_for. The ACL
  check signature prevents misuse.
- The ACL check stays table-shaped (flat scope match); the delegation graph
  (future) produces effective scopes at resolution time. They compose at the
  IdentityProvider boundary.
- The hub proxy tangle: ACL (authorize), bucket routing (operation input),
  peer routing (PeerRef) are three separate layers. Bucket-level
  authorization is handler logic, not protocol logic.

What the old spec had that's dropped: multi-tenant (each tenant gets own
setup), secrets module (replaced by vault), metagraph-as-foundation (demoted
to tool), single storage crate (split by concern), accounts/orgs (deferred —
v1 is a peers table).

Reference: kepal (/workspace/keypal) — TypeScript repo-pattern example
(Storage interface + adapters) that alknet's IdentityProvider follows.
2026-06-27 10:02:26 +00:00
glm-5.2 99c6dd9483 docs(arch): resolve OQ-26 (AdapterError variants) + OQ-33 (PeerId = logical id) + OQ-34 (persistent peer registry)
OQ-26 (resolved): AdapterError variants decided — DiscoveryFailed,
SchemaParse, Transport, Unauthorized, SamePeerCollision (replaces flat
Conflict per ADR-029 §5). #[non_exhaustive] for downstream extension.
Two-way door; the initial set is the code's return type.

OQ-33 (resolved): PeerId is a logical identifier, NOT Identity.id. The
research's v1 default (PeerId = fingerprint) is overridden: coupling PeerId
to crypto material breaks every in-flight PeerRef::Specific and every ACL
entry on key rotation. v1 source is a connection-assigned UUID — a
no-storage workaround that works for the immediate use case (head→workers,
reconnect produces fresh PeerRef, in-flight gets NOT_FOUND which is correct).
The one-way door: PeerId is logical, not crypto — this determines
PeerCompositeEnv key type and PeerRef::Specific payload. The id source
(UUID vs configured name vs peer registry) is the two-way-door remainder.

OQ-34 (new): the storage dimension OQ-33 surfaced. The core crates are
deliberately DB-free (smaller, fewer deps, simpler testing) — this served
local-only state (vault, registry) well, but peer identity is the first
cross-node state that wants persistence. The real solution (a persistent
peer registry mapping stable logical name → current crypto material,
surviving key rotation) is not a v1 blocker (UUID works), but tracked so the
no-DB posture's limit is deliberate, not accidental. The storage boundary
(core gets a PeerRegistry trait vs stays storage-free) is the one-way door;
the backend choice is two-way. Key-rotation/ACL note: decoupling PeerId from
crypto keeps the door open for ACL entries that persist across key rotation
— when the peer registry is built, ACLs key on the logical name and key
rotation becomes vault-only with no remote-side ACL update.
2026-06-27 06:34:35 +00:00
glm-5.2 77eb35a8a5 docs(arch): ADR-029 peer-graph routing model — supersedes ADR-028
ADR-028's remote_safe/trusted_peer was a parallel, weaker authorization system
that duplicated the existing AccessControl/Identity machinery and couldn't
express the head→N-workers pattern (the primary use case). The flat-namespace
single-peer overlay model (one connection layer in CompositeOperationEnv)
structurally breaks the moment a head has two workers both exposing
/container/exec.

ADR-029 replaces it with:
- Peer-keyed overlays: PeerCompositeEnv { connections: HashMap<PeerId, ...> }
  replaces CompositeOperationEnv's singular connection layer. A head node
  routes invoke_peer() to the right peer via PeerRef::Specific / PeerRef::Any.
- AccessControl-based peer authorization: the existing AccessControl::check
  (peer_identity) gates peer calls — the same mechanism that gates every other
  call. remote_safe/trusted_peer/RemoteFilter/list_operations_peer_scoped/
  services_list_handler_peer_scoped are retired. The op's AccessControl IS the
  peer-authorization policy; no parallel system.
- ScopedPeerEnv: peer-qualified reachability (peer-pinned allowlist) replaces
  from_call's namespace_prefix as the disambiguation mechanism. Cross-peer
  collision dissolves (separate sub-overlays); same-peer collision stays error.
- services/list-peers opt-in for peer-attributed re-export listing.

POC-validated against real types (scratch module written, type-checked,
removed; build clean, 207 tests pass). Petgraph not needed for v1 (one-hop,
shallow); nested HashMap suffices; extends to multi-hop without redesign (OQ-32).

OQ impact: OQ-25 dissolved (no marking); OQ-28 cross-peer dissolved / same-peer
stays; OQ-26/27/29 stay; new OQ-30 (Any routing policy), OQ-31 (list-peers
semantics), OQ-32 (multi-hop federation).

Research: docs/research/alknet-call-peer-routing/findings.md (POC shapes,
prior art — Ray.io actors, Dapr service invocation, full ADR draft).
ADR-028 marked Superseded; ADR-017 DC-1 amendment updated to point at ADR-029.
2026-06-27 06:04:19 +00:00
glm-5.2 f9c0ab092b docs(arch): sync call-completion specs with implementation — Dispatcher/RemoteFilter, ClientError, OQ-29
Post-implementation spec sync after the call-completion batch landed
(commits e4a2594..a3825f5). The sub-agent review flagged no spec drift, but
comparing the implemented types against the spec sketches surfaced five
details the specs didn't name — filled in here so the spec matches what was
built:

- client-and-adapters.md: name the shared Dispatcher (protocol/dispatch.rs)
  + RemoteFilter mechanism that enforces ADR-028's default-deny at dispatch
  time (the load-bearing security gate — checks remote_safe before building
  context, before any capability material reaches the handler). Add
  ClientError/RemoteIdentity types, the spawn_dispatch lower-level API, and
  the services_list_handler_peer_scoped wiring (the assembly layer must
  register the peer-scoped services/list handler for a CallClient's registry,
  not the plain one). Record the v1 TLS client-auth gap (AcceptAnyServerCertVerifier,
  with_no_client_auth) as OQ-29.
- call-protocol.md: point the adapter dispatch-loop description at the shared
  Dispatcher (dispatch.rs) so readers find the mechanism ADR-017 §1 commits to.
- open-questions.md: OQ-29 — CallClient TLS client-auth + remote-identity
  verification is a two-way-door remainder; the no-env-vars invariant is
  unaffected (auth_token flows via call-protocol payload, not TLS).
- READMEs: current-state now reflects completion done + reviewed (207 lib +
  2 integration tests); OQ-29 added to both OQ summaries.
2026-06-26 13:42:42 +00:00
glm-5.2 2fe471ad4e docs(tasks): mark call/review-completion complete with review findings
Records the conformance review for the alknet-call client/adapter
completion batch. All 14 acceptance criteria pass:

- CallClient, from_call, OperationAdapter, from_jsonschema match
  client-and-adapters.md.
- Peer-scoped default-deny (ADR-028) enforced: non-remote-safe ops return
  NOT_FOUND before capabilities are populated (the load-bearing security
  assertion, verified by three capability-exposure tests).
- Shared dispatch loop is genuinely shared (single Dispatcher; CallAdapter
  and CallClient both delegate to run_loop).
- No-env-vars invariant holds (no std::env::var reads; credentials from
  Capabilities).
- Adapter location map respected (no HTTP deps in alknet-call).
- OQ-25..28 two-way-door remainders recorded with v1 defaults.
- No spec drift requiring amendment.
- 207 lib + 2 integration tests pass; clippy + fmt clean.

This closes the call-completion batch. Unblocks every downstream consumer
(runner, container service, bilateral exchange, NAPI, agent cross-node
dispatch) and alknet-http Phase 1 (OperationAdapter trait).

Refs: tasks/call/review-completion.md
2026-06-26 13:27:08 +00:00
glm-5.2 a3825f57cf feat(call): from_call adapter — discover + register remote ops (ADR-017 §3)
The #2 gap in alknet-call: discovers the remote peer's External operations
via services/list + services/schema and registers them in the connection's
Layer 2 overlay as FromCall-provenance leaves with forwarding handlers. The
discovery mechanism was already implemented in registry/discovery.rs;
from_call is the client-side consumer of that API.

src/client/from_call.rs:
- from_call(connection, FromCallConfig) -> Result<Vec<HandlerRegistration>,
  AdapterError>. Calls services/list then services/schema for each op,
  rebuilds OperationSpec from the schema JSON (parsing op_type, visibility,
  error_schemas, access_control), constructs a forwarding handler that calls
  the remote op via CallConnection::call(), and returns FromCall-provenance
  bundles (composition_authority: None, scoped_env: None, empty capabilities,
  remote_safe: false per ADR-028 §4).
- FromCallConfig { namespace_prefix: Option<String>, operation_filter:
  Option<HashSet<String>> } with builder methods.
- v1 defaults (two-way doors recorded in client-and-adapters.md):
  - error-on-collision (DC-3/OQ-28): applying the (possibly empty) prefix
    produces a name already seen -> AdapterError::Conflict, not silent
    overwrite.
  - auto-on-reconnect (DC-2/OQ-27): the overlay is per-connection (Layer 2,
    ADR-024), so re-import on reconnect is naturally scoped; the assembly
    layer calls from_call immediately after connect().
- Forwarding handler captures an Arc<CallConnection> and, on invocation,
  calls the remote op and returns its ResponseEnvelope. The
  parent_request_id participates in the cross-node abort cascade
  (ADR-016 §6) — if the parent is aborted, the cascade reaches this handler
  which sends call.aborted to the remote node; cross-node abort is
  transparent.
- Trust is transitive (recorded in spec): a from_call-imported op executes
  the remote node's code; scoped_env bounds which ops are reachable, not
  what they do.

OperationContext.internal is now pub (was pub(crate)) so downstream
consumers (assembly layer, integration tests) can construct contexts for
overlay-env dispatch.

Tests (207 lib + 2 integration):
- Unit: rebuild_spec name/prefix/op_type/visibility/error_schemas/acl;
  unknown op_type -> SchemaParse; missing op_type -> SchemaParse;
  FromCallConfig builder; from_call against a mock connection returns
  DiscoveryFailed (no transport); FromCall provenance + leaf fields + remote_safe false.
- Integration (tests/two_node_call.rs): from_call over a real QUIC loopback
  — CallClient connects, from_call discovers server/echo, registers the
  bundle in the overlay, and the forwarding handler round-trips an input
  through the overlay env to the remote op and back.

clippy + fmt + test all green.

Refs: tasks/call/client/from-call.md
Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §3, §6
Refs: docs/architecture/crates/call/client-and-adapters.md §from_call
2026-06-26 13:25:13 +00:00
glm-5.2 4bf897f5ab feat(call): CallClient + shared dispatch loop + peer-scoped default-deny (ADR-017, ADR-028)
The #1 gap in alknet-call: the outbound connection opener. Every downstream
consumer (runner, container service, bilateral exchange, NAPI, agent
cross-node dispatch) is blocked on it.

Shared dispatch loop (ADR-017 §1 — the architectural commitment that keeps
CallClient from becoming a parallel protocol implementation):
- Extracts the accept-path dispatch (sweeper, accept_bi loop, handle_stream,
  dispatch_requested, build_root_context, compose_root_env, fail_all on
  close) out of CallAdapter into a new protocol/dispatch.rs Dispatcher struct.
  Both CallAdapter::handle and CallClient::connect produce a CallConnection
  and hand it to Dispatcher::run_loop — the loop is genuinely shared
  (refactored, not duplicated).
- CallAdapter keeps its public API and test-facing wrappers (pub(crate),
  #[cfg(test)]-gated) that delegate to the Dispatcher.

Peer-scoped default-deny (ADR-028 — the one-way-door security dimension):
- RemoteFilter { trusted_peer: bool } on the Dispatcher. In default-deny
  mode (CallClient::new), an incoming call to an op with remote_safe: false
  returns NOT_FOUND *before* any capability material reaches the handler —
  a remote peer's call must not populate OperationContext.capabilities from
  the local registration bundle unless the op is explicitly remote-safe
  (ADR-028 Context). Trusted-peer mode (CallClient::trusted_peer, explicit
  opt-in) bypasses the filter.
- The accept path (CallAdapter) uses RemoteFilter::trusted() by convention: a
  direct QUIC client is not a filtered CallClient peer in the ADR-028 sense.
- OperationRegistry::list_operations_peer_scoped(trusted_peer) +
  services_list_handler_peer_scoped for the CallClient's services/list
  serving path (ADR-028 Assumption 2: a peer should not see ops it cannot
  call, so discovery and dispatch filters agree).

CallClient (src/client/call_client.rs):
- CallClient { registry, identity_provider, trusted_peer: bool }.
- new() default-deny; trusted_peer() explicit opt-in (ADR-028 §3).
- connect(addr, CallCredentials) dials QUIC on ALPN alknet/call (quinn
  feature), spawns Dispatcher::run_loop, returns a live CallConnection.
- spawn_dispatch(connection) shared path for connect + tests.
- CallCredentials { tls_identity, auth_token, remote_identity } — all from
  Capabilities (ADR-014), never env vars (no-env-vars invariant). v1
  connects without client-auth TLS identity (server uses
  AcceptAnyCertVerifier); RawKey client-auth is a two-way-door remainder.
- RemoteIdentity { fingerprint } — concrete shape is a two-way door (OQ-25
  remainder); the one-way constraint is it comes from Capabilities.
- ClientError { Transport, TlsSetup, ConnectionClosed }.
- CallConnection is now Clone (shares the inner Arcs) so connect can hand
  the caller a live clone while the dispatcher task keeps its clone.

Tests (199 lib + 1 integration):
- Unit: default-deny NOT_FOUND for non-remote-safe; remote_safe dispatches;
  trusted-peer dispatches all External; default-deny does NOT populate
  capabilities (the load-bearing security assertion — verified by a handler
  that inspects context.capabilities and the fact that the handler is never
  reached for non-remote-safe ops); remote_safe op populates capabilities;
  services/list peer-scoped hide/trusted variants; CallClient constructors;
  CallCredentials builder; Send+Sync.
- Integration (tests/two_node_call.rs): real QUIC loopback — CallAdapter
  server (self-signed cert via rcgen) accepts, CallClient connects,
  client.call() round-trips to server/echo. Proves the connect path +
  shared dispatch loop work end-to-end.

clippy + fmt + test all green.

Refs: tasks/call/client/call-client.md
Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §1, §2, §7
Refs: docs/architecture/decisions/028-callclient-peer-scoped-registry-filtering.md
Refs: docs/architecture/crates/call/client-and-adapters.md
2026-06-26 13:19:15 +00:00
glm-5.2 404d00ae1a style(call): apply rustfmt to connection.rs and registration.rs
Pre-existing fmt drift in two files touched during the call-completion
batch (remote_safe field, dispatch helpers). Brings cargo fmt --check
clean for the review gate.
2026-06-26 12:57:14 +00:00
glm-5.2 1e5f94b06b feat(call): OperationAdapter trait + AdapterError + from_jsonschema (ADR-017 §5)
- client module: defines the async OperationAdapter trait
  (import() -> Result<Vec<HandlerRegistration>, AdapterError>) and the
  #[non_exhaustive] AdapterError enum (string-message payloads: DiscoveryFailed,
  SchemaParse, Transport, Unauthorized, Conflict). The trait lives in alknet-call
  where the types live; implementations live with their transport deps.
- from_jsonschema: schema-only registration producing a FromJsonSchema-provenance
  HandlerRegistration with no real handler (placeholder errors if invoked),
  None authority/scoped_env, empty capabilities, remote_safe false (ADR-028 §4).
  Implements OperationAdapter; malformed (non-object) schema returns
  AdapterError::SchemaParse. No network I/O.
- Re-exported from lib.rs.
- Tests: trait compiles for Ok and Err adapters; from_jsonschema bundle shape;
  placeholder handler errors; OperationAdapter import Ok + SchemaParse paths.
  All 178+N tests pass, clippy + fmt clean.

Unblocks alknet-http Phase 1 (from_openapi/from_mcp adapter implementations).

Refs: tasks/call/client/operation-adapter-trait.md, tasks/call/client/from-jsonschema.md
Refs: docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md §5
Refs: docs/architecture/crates/call/client-and-adapters.md
2026-06-26 12:56:28 +00:00
glm-5.2 e4a25947d6 feat(call): remote_safe field on HandlerRegistration (ADR-028)
Adds the v1 data shape for peer-scoped default-deny registry filtering,
the one-way-door piece of the call-completion batch (ADR-028):

- HandlerRegistration gains pub remote_safe: bool, defaulting false across
  all provenance (Local, Session, FromOpenAPI, FromMCP, FromCall,
  FromJsonSchema) per ADR-028 §4. HandlerRegistration::new() keeps its
  existing 6-arg signature (defaults remote_safe: false), so all current
  call sites compile unchanged.
- Chainable HandlerRegistration::remote_safe(bool) setter + a
  OperationRegistryBuilder::remote_safe() helper that marks the
  most-recently-registered op (tracked via last_name, not HashMap
  iteration order which is unspecified).
- Field is data-only here — the filtering behavior (dispatch path +
  services/list hide) is wired in call/client/call-client, not this task.
  services/list is unchanged.
- Tests: default false, setter flips field, all six provenance variants
  default false, builder setter marks last op, existing call sites
  unchanged. 178 tests pass, clippy clean.

Refs: tasks/call/registry/remote-safe-marking.md
Refs: docs/architecture/decisions/028-callclient-peer-scoped-registry-filtering.md
2026-06-26 12:51:18 +00:00
glm-5.2 2649e068e5 docs(arch): call-completion — ADR-028 peer-scoped filtering + client-and-adapters spec + tasks
Resolves the four gap-analysis decisions (DC-1..4) blocking the alknet-call
client/adapter surface specced in ADR-017:

- ADR-028 (new): locks the one-way door for DC-1 — CallClient registry is
  default-deny (remote_safe: bool on HandlerRegistration, default false across
  all provenance); share-global is an explicit trusted-peer opt-in; filtering
  is a dispatch-time read over the single Layer-0 registry, not a copy.
- client-and-adapters.md (new spec): operationally fills the gap ADR-017 left
  to implementation — CallClient, from_call, from_jsonschema, OperationAdapter
  trait, adapter location map, no-env-vars invariant, exchange-of-operations
  pattern. Keeps call-protocol.md and operation-registry.md under the
  700-line split threshold.
- ADR-017 amended: records DC-2/3/4 v1 defaults (auto-on-reconnect,
  error-on-collision, Result error type) and points DC-1 at ADR-028.
- OQ-25..28 (new): two-way-door remainders (remote_safe shape, AdapterError
  variants, re-import trigger, namespace collision) with v1 defaults recorded.
- Index/cross-ref updates across READMEs and the two existing call specs.

Tasks: 6 task files under tasks/call/ decomposing the completion work along
the gap-analysis priority order — remote-safe-marking (one-way door, first)
→ call-client (phase-risk) → from-call → operation-adapter-trait →
from-jsonschema (parallel with call-client) → review-completion. Graph
validated with taskgraph; parallelism designed in (from-jsonschema runs
concurrent with call-client/from-call once the trait lands).
2026-06-26 12:25:13 +00:00
glm-5.2 6940d9858d docs(research): alknet-http phase-0 findings — HTTP server + client + MCP adapters
Phase 0 exploration for alknet-http (greenfield crate, no existing arch):
HTTP server (axum, ProtocolHandler for h2/http1.1, h3 deferred), HTTP client
(reqwest, the from_openapi/from_mcp forwarding handlers), MCP streamable HTTP
(feature-gated, stdio excluded as security position), to_openapi/to_mcp
projections.

Records: 8 design points (DH-3 HTTP→call operation mapping as the load-bearing
one), the settled adapter location map (from alknet-call gap analysis), the
no-env-vars invariant (Capabilities → from_openapi handler → HTTP header as the
credential injection point), and the prerequisite on alknet-call's
OperationAdapter trait being defined first.
2026-06-25 12:46:25 +00:00
glm-5.2 79d8561bb4 docs(research): alknet-call completion gap analysis — CallClient + from_call + OperationAdapter
Gap analysis for completing alknet-call: the server-side core (~5.7k lines,
159 tests) is implemented, but the client side (CallClient), the bilateral
exchange mechanism (from_call), and the adapter contract (OperationAdapter
trait) are specced in ADR-017 and unimplemented.

Records: implementation state (verified against src/), 5 decisions needed
(peer-scoped registry filtering as the load-bearing one), the settled adapter
location map (trait + from_call + from_jsonschema in alknet-call; from_openapi/
from_mcp in alknet-http), the no-env-vars invariant (Capabilities → from_openapi
handler → HTTP header), and the exchange-of-operations runner pattern with
dispatch as the concrete downstream consumer.
2026-06-25 12:44:49 +00:00
glm-5.2 db1dcd362f docs(research): revise alknet-ssh phase-0 — SOCKS5+forwarding in v1, TCP listener for git-over-ssh
Incorporates user clarifications: SOCKS5 and bidirectional port forwarding are
core non-negotiable v1 features (the VPN-like use case + the 3.5k-clones
demand). Adds DP-10 for the bare-TCP SSH listener as a first-class path needed
for future git-over-SSH, with config shape reserved in v1 (off-by-default,
default-deny). Grounds the client/forwarding recommendations in the dispatch
downstream consumer at /workspace/@alkdev/dispatch, which is a textbook russh
SSH client + direct-tcpip forwarder the user wants to replace with this stack.

alknet-ssh now owns both server and client + SOCKS5-server in v1; the SOCKS5
codec may extract to a separate crate later (two-way door).
2026-06-25 08:46:35 +00:00
glm-5.2 d758a71490 docs(research): alknet-ssh phase-0 findings — stream wiring, russh 0.60.2, decision points
Phase 0 exploration for alknet-ssh: confirms SSH-over-QUIC-bistream via
tokio::io::join (no custom adapter needed, per reference impl), russh 0.60.2
generic run_stream/connect_stream, and channel-into-bistream multiplexing.

Surfaces 9 decision points for Phase 1: host key sourcing (vault-derived vs
config), channel policy v1 surface, client + SOCKS5 crate split, crypto
backend, auth method coverage, and a stream-handling POC to close russh's
upstream test gap.
2026-06-25 08:06:45 +00:00
glm-5.2 011db05a52 test: implement coverage #005 Tier-A suggestions (S1-S4, S8)
Add 165 tests covering the directly-testable surface identified in
coverage review #005. Workspace coverage rises 87.1% -> 91.2%
(5759/6615 -> 6505/7135); all 389 tests pass, clippy clean.

- S1 (connection.rs): dispatch_envelope across all five event-type arms
  for Call + Subscribe, plus SubscriptionStream poll_next branches and
  SubscriptionStream::closed.
- S2 (types.rs): map_quinn/iroh_connection_error for TimedOut/Reset/
  ApplicationClosed/other, plus HandlerError + StreamError Debug/Display/
  source for every variant.
- S3 (config.rs): Ed25519SecretKey from_bytes/as_bytes round-trip,
  sign+verify, tampered-message rejection, Debug non-leakage.
- S4 (endpoint.rs): build_rustls_server_config RawKey/SelfSigned/Acme
  arms, build_quinn_server_config_from_rustls, load_private_key/
  load_cert_chain error paths, has_iroh_identity branches,
  AcceptAnyCertVerifier trait methods, Ed25519SigningKey trait impls
  (choose_scheme both branches, algorithm, public_key, sign, scheme),
  RawKeyCertResolver + AlknetEndpoint Debug. endpoint.rs 56% -> 73%.
- S8 (vault protocol.rs): the existing redacted-deserialize test passed
  for the wrong reason (JSON string failed Vec<u8> coercion before the
  guard). Two new tests exercise the guard directly via a [REDACTED] byte
  array (rejected) and a real payload (accepted). protocol.rs -> 100%.

Deferred to follow-up: S5 (loopback quinn integration test, the real
unlock for accept/dispatch/stream paths), S6 (ACME event-loop extraction),
S7 (adapter abort arm). Review #005 updated with the resolution.
2026-06-25 05:43:59 +00:00
glm-5.2 32dcc05658 docs(reviews): add coverage analysis #005
First dedicated coverage pass (cargo-llvm-cov --workspace --all-features).
Workspace at 87.1% line coverage (5759/6615), all 224 tests pass. Vault
and registry layers are essentially fully covered; gaps concentrate in
endpoint.rs (56%), types.rs (57%), and connection.rs (54%), all stemming
from tests using MockConnection whose open_bi/accept_bi return Err.

Eight suggestions (S1-S8) ordered by leverage: pure-function tests for
dispatch_envelope / map_*_connection_error / error Display+Debug (S1-S3),
Tier A directly-callable TLS/rustls helpers in endpoint.rs (S4), one
loopback quinn integration test as the real unlock across four files (S5),
ACME event-loop extraction via synthetic stream (S6, the flagged research
item), and two small remaining gaps (S7-S8). No critical or warning
findings — this is a testing-infrastructure gap, not a logic gap.
2026-06-25 04:32:51 +00:00
glm-5.2 00edfc0889 feat(core): ADR-027 — RawKey decoupling, client cert request, ACME integration
Three tasks implementing ADR-027:

1. core/rawkey-decouple-from-iroh: TlsIdentity::RawKey now uses
   Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
   instead of iroh::SecretKey. RawKeyCertResolver and Ed25519SigningKey
   un-gated from #[cfg(all(quinn, iroh))] to #[cfg(quinn)] only.
   Quinn-only builds (default) now support RFC 7250 raw-key identity.
   iroh transport converts via iroh::SecretKey::from_bytes.

2. core/endpoint-request-client-cert: replaced with_no_client_auth()
   with AcceptAnyCertVerifier — a custom ClientCertVerifier that
   requests client certs but doesn't require them or verify against
   a CA. alknet's identity model is fingerprint-based (the
   authorized_fingerprints set is the trust anchor), not PKI-based.
   Peer certs are extracted at the TLS layer for fingerprinting;
   peers without certs connect normally.

3. core/acme-integration: TlsIdentity::Acme variant (domains,
   cache_dir, directory, contact) + AcmeDirectory enum. TlsSetup
   two-phase construction: synchronous for X509/RawKey/SelfSigned,
   async for Acme (spawns AcmeState event loop, builds ServerConfig
   with ResolvesServerCertAcme). acme-tls/1 ALPN added when ACME is
   active; dispatch_quinn guard closes challenge connections
   gracefully (challenge is TLS-layer-handled). acme feature gate
   keeps rustls-acme out of non-ACME builds.

Workspace: build/test/clippy green across all 3 feature configs
(quinn-only, quinn+iroh, quinn+acme, all-features). 331 tests, 0
failures, 0 warnings.
2026-06-24 20:29:43 +00:00
glm-5.2 d94d7a132a docs(adr-027): TLS identity redesign — ACME + RawKey decoupling
ADR-027 resolves the architectural gap surfaced when ACME integration
became a concrete target:

1. TlsIdentity::Acme variant — static config data (domains, cache_dir,
   directory, contact) with async AcmeState constructed at endpoint
   setup via two-phase TlsSetup (not stuffed into the Clone-able enum).

2. TlsIdentity::RawKey decoupled from the iroh feature — uses
   Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
   instead of iroh::SecretKey. Raw-key TLS identity (RFC 7250, the
   default for most alknet nodes) now works in quinn-only builds.
   iroh transport converts via SecretKey::from_bytes.

3. ACME feature-gated behind new acme feature (rustls-acme optional
   dep). Non-ACME builds don't compile it.

4. dispatch_quinn guard for acme-tls/1 challenge connections — TLS-ALPN-01
   is handled at the rustls cert resolver layer during the handshake;
   the guard closes challenge connections gracefully instead of logging
   a misleading "no handler" warning.

Research confirmed QUIC (quinn) handles ACME challenges differently than
TCP (reverse-proxy): quinn gives no ClientHello peek hook, but the
challenge is fully answered at the cert resolution step before the
connection surfaces to the application. No handler registration needed.

Spec updates: config.md, endpoint.md, open-questions.md (OQ-12),
overview.md + README.md (ADR index), ADR-010 (cross-ref).

Tasks: core/rawkey-decouple-from-iroh (gen 1, no deps),
core/acme-integration (gen 2, depends on rawkey). Graph: 36 tasks.
2026-06-24 12:29:24 +00:00
glm-5.2 97216764ea fix: resolve review #004 findings W1-W4 + close review gate
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into
CallAdapter handle_stream for EVENT_ABORTED. Cascades with
AbortPolicy::AbortDependents, aborts root, no descendant frames on
wire (ADR-016 Decision 2). Two integration tests added.

W2 (core/endpoint-client-fingerprint): extract TLS client cert
fingerprint in dispatch_quinn (SHA256:<hex> of leaf cert DER via
peer_identity) and dispatch_iroh (ed25519:<hex> of peer NodeId).
Fingerprint format documented in auth.md. Server config change
(with_no_client_auth → request-but-don't-require) deferred to new
follow-up task core/endpoint-request-client-cert.

W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug)
with manual redacting impl (phrase: "[REDACTED]"). Seed confirmed
no Debug impl. Redaction test added.

W4 (core/auth-apikey-resources): Option B — drop entry.resources from
spec. External identities (token/fingerprint) grant scopes only;
resource-scoped ACLs are composition-internal (ADR-015/022). auth.md
corrected + limitation documented. Two tests confirm empty resources.

review-post-impl-fixes: all 4 verified, workspace green (326 tests,
0 failures, 0 clippy warnings). Review #004 status → resolved.

Graph: 34 tasks, 12 gens.
2026-06-24 11:00:54 +00:00
glm-5.2 d149932e2a tasks: decompose review #004 findings into 4 fix tasks + review gate
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into CallAdapter
handle_stream for EVENT_ABORTED. W2 (core/endpoint-client-fingerprint):
extract TLS client cert fingerprint in dispatch_quinn/dispatch_iroh.
W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug) with
redacting impl. W4 (core/auth-apikey-resources, level: research): decide
whether ApiKeyEntry should carry resources, then implement or drop from
spec. review-post-impl-fixes gates on all four. Graph: 33 tasks, 12 gens.
2026-06-24 10:02:03 +00:00
glm-5.2 d904dfc243 docs(reviews): add post-implementation sanity check #004
28-task implementation review across alknet-vault, alknet-core, alknet-call.
Zero critical findings; 4 warnings (W1: AbortCascade implemented but never
invoked by CallAdapter; W2: endpoint hard-codes tls_client_fingerprint=None;
W3: Mnemonic Debug leaks seed phrase; W4: ApiKeyEntry missing resources
field); 5 suggestions. Build clean, 332 tests pass, default clippy clean.
2026-06-23 22:13:20 +00:00
glm-5.2 2c83e31e38 tasks: mark call/review-call completed — all 28 tasks done 2026-06-23 15:55:56 +00:00
glm-5.2 4696c9a304 docs(call): record review-call pass — implementation conforms to spec (task: call/review-call)
Review of alknet-call crate against operation-registry.md, call-protocol.md,
and ADRs 005/012/014/015/016/017/022/023/024. All registry types, protocol types,
security constraints, and pattern consistency checks conformant. No source
deviations found. 159 tests pass; build/clippy/fmt/test clean.

Refs: docs/architecture/crates/call/README.md
2026-06-23 15:55:33 +00:00
glm-5.2 23b76a240a docs(call): record review-call pass — implementation conforms to registry/protocol/ADR spec
call/review-call verified the alknet-call crate against operation-registry.md,
call-protocol.md, and ADRs 005/012/014/015/016/017/022/023/024. All
registry types, protocol types, security constraints (Capabilities
non-serializable/zeroized/immutable, metadata non-propagation, internal
ops -> NOT_FOUND, reachability bounds, UUID v4 request IDs), and pattern
consistency (OperationEnv trait, CompositeOperationEnv contains-probe,
authority switch, deadline inheritance) are conformant. 159 tests green;
build/clippy(fmt check) clean. No source changes required.
2026-06-23 15:55:10 +00:00
glm-5.2 93589d4f52 tasks: mark call/protocol/abort-cascade completed 2026-06-23 15:50:38 +00:00
glm-5.2 8aa384ccfa feat(call): implement abort cascade for nested calls (ADR-016) (task: call/protocol/abort-cascade)
Implement AbortCascade in protocol/abort.rs per ADR-016: PendingEntry stores
parent_request_id (Call & Subscribe) and a started flag for tree indexing.
AbortCascade::cascade_abort walks the call tree by parent_request_id and aborts
descendants per AbortPolicy (AbortDependents aborts all; ContinueRunning aborts
only unstarted via mark_started()). Returns sorted list of aborted IDs; unknown
root silently discarded. 20 unit tests covering depth-3 cascade, mixed
Call/Subscribe, determinism, both policies.

Refs: docs/architecture/crates/call/call-protocol.md
Implements: ADR-016
2026-06-23 15:50:05 +00:00
glm-5.2 3317bc8d1a feat(call): implement abort cascade for nested calls (ADR-016) (task: call/protocol/abort-cascade)
- PendingEntry stores parent_request_id (Call and Subscribe) and started flag
  for abort-cascade tree indexing
- register_call/register_subscribe accept optional parent_request_id
- AbortCascade::cascade_abort walks the call tree by parent_request_id and
  aborts descendants per AbortPolicy (AbortDependents: all; ContinueRunning:
  unstarted only). Returns sorted list of aborted request IDs
- call.aborted for unknown request_id silently discarded (empty result)
- Composed child request_ids stay internal (not sent as call.requested)
- mark_started() tracks dispatch state for ContinueRunning decisions
- 20 unit tests covering AbortDependents/ContinueRunning, depth-3 tree,
  unknown root, mixed Call/Subscribe, determinism
2026-06-23 15:49:07 +00:00
glm-5.2 bea19de3cf tasks: mark call/protocol/call-adapter completed 2026-06-23 15:39:54 +00:00
glm-5.2 2ff09a728c feat(call): implement CallAdapter — ProtocolHandler for alknet/call (task: call/protocol/call-adapter)
Implement CallAdapter in protocol/adapter.rs: ProtocolHandler for ALPN
alknet/call with stream handling, per-request identity resolution (auth_token
overrides connection identity, falls back on failure), root context construction
(internal:false, deadline, capabilities+scoped_env from registration bundle),
env composition (CompositeOperationEnv with Layer 0 base + Layer 2 connection
overlay + optional Layer 1 session overlay), operationId leading slash stripped,
ResponseEnvelope→EventEnvelope conversion, PendingRequestMap sweeper, connection
drop fails all pending. SessionOverlaySource trait. 22 unit tests.

Refs: docs/architecture/crates/call/call-protocol.md
Implements: ADR-002, ADR-012, ADR-014, ADR-015, ADR-017, ADR-022, ADR-024
2026-06-23 15:39:28 +00:00
glm-5.2 fc9f93e893 feat(call): implement CallAdapter (ProtocolHandler for alknet/call) with stream handling, identity resolution, root context construction (task: call/protocol/call-adapter)
- CallAdapter struct with registry, identity_provider, session_source, default_timeout (30s)
- new(), with_session_source(), with_timeout() constructors
- SessionOverlaySource trait defined (overlay_for) for agent-crate integration
- ProtocolHandler::alpn() returns b"alknet/call"
- handle() sets connection identity from AuthContext, spawns accept_bi loop,
  reads EventEnvelope frames via FrameFramedReader, dispatches call.requested
  to the operation registry, writes ResponseEnvelope as EventEnvelope via
  FrameFramedWriter
- Per-request identity resolution: AuthContext.identity used by default,
  auth_token in payload overrides via IdentityProvider::resolve_from_token();
  resolution failure falls back to connection-level identity
- build_root_context sets internal: false, deadline (now + default_timeout),
  capabilities and scoped_env from registration bundle, parent_request_id: None
- compose_root_env builds CompositeOperationEnv (Layer 0 curated base +
  Layer 2 connection overlay + optional Layer 1 session overlay)
- operationId leading slash stripped before registry lookup
- ResponseEnvelope -> EventEnvelope conversion (Ok -> call.responded,
  Err -> call.error)
- PendingRequestMap sweeper runs every 10s, evicts expired wire entries
- Connection drop: fail_all pending with INTERNAL "connection closed",
  return Ok(())
- Stream reset: FrameFramedReader error closes stream; other streams unaffected
- Handler panic: stream task isolated via tokio::spawn, sweep cleans entry
- Tests: alpn, constructors, slash strip, identity resolution (override/fallback),
  root context (internal=false, deadline, capabilities, scoped_env), env
  composition (layers aggregate, session overlay), dispatch round-trip,
  internal op from wire -> NOT_FOUND, ACL denied -> FORBIDDEN, auth_token
  overrides connection identity, unknown op -> NOT_FOUND, no-slash resolution,
  ResponseEnvelope -> EventEnvelope conversions
2026-06-23 15:38:50 +00:00
glm-5.2 0d0f0f8da6 tasks: mark core/review-core completed 2026-06-23 15:33:16 +00:00
glm-5.2 061069910b fix(core): gate RawKey TLS helpers on quinn+iroh for clean iroh-only builds (task: core/review-core)
Review of alknet-core found one issue: RawKeyCertResolver/Ed25519SigningKey/
std::path::Path were gated on #[cfg(feature = "iroh")] but only used in the
quinn TLS path — caused clippy -D warnings failures on iroh-only builds.
Re-gated to #[cfg(all(feature = "quinn", feature = "iroh"))]. All 4 feature
combinations now pass clippy -D warnings; 55 tests pass.

Review confirmed: all core types, config, auth, and endpoint implementations
are spec-conformant.
2026-06-23 15:32:23 +00:00
glm-5.2 4774364c72 fix(core): gate RawKey TLS helpers on quinn+iroh for clean iroh-only builds (task: core/review-core)
The RawKeyCertResolver, Ed25519SigningKey, and std::path::Path imports
were gated on #[cfg(feature = "iroh")] but are only used in the quinn
TLS server-config path (build_rustls_server_config RawKey arm). With
iroh-only builds (--no-default-features --features iroh), these became
dead code and triggered clippy -D warnings failures.

Re-gated to #[cfg(all(feature = "quinn", feature = "iroh"))] so they
only compile when both features are active (the combination that
actually uses raw-key TLS via quinn). std::path::Path is now
#[cfg(feature = "quinn")] since it is only used by quinn's
load_cert_chain/load_private_key helpers.

Verified: cargo clippy passes with -D warnings across all four feature
combinations (none, quinn, iroh, quinn+iroh). cargo test --all-features
passes 55 tests. cargo fmt --check clean.
2026-06-23 15:31:42 +00:00
glm-5.2 60556bbe0c tasks: mark core/endpoint and call/protocol/call-connection completed 2026-06-23 15:18:56 +00:00
glm-5.2 c68050ae0f feat(call): implement CallConnection with imported-ops overlay and call/subscribe/abort (task: call/protocol/call-connection)
Implement CallConnection in protocol/connection.rs with Layer 2 imported-ops
overlay (Arc<RwLock<HashMap>>), register_imported/register_imported_all,
overlay_env() returning an OperationEnv that dispatches to imported ops,
and call()/subscribe()/abort() methods that open a stream, send call.requested,
register in PendingRequestMap, spawn a stream reader, and correlate responses
by ID. Connection drop drops the overlay. Exposed MockConnection +
Connection::from_mock in alknet-core for cross-crate testing. 9 new connection
tests (102 total in alknet-call).

Refs: docs/architecture/crates/call/call-protocol.md
Implements: ADR-012, ADR-017, ADR-024
2026-06-23 15:17:55 +00:00
glm-5.2 7b92749acd tasks: mark core/endpoint completed 2026-06-23 15:16:33 +00:00
glm-5.2 ddc6c07fea feat(call): implement CallConnection with imported-ops overlay (Layer 2) and call/subscribe/abort methods
Implements CallConnection in src/protocol/connection.rs representing an
established alknet/call connection (either direction). Holds the Layer 2
imported-ops overlay (ADR-024) as Arc<RwLock<HashMap>>.

- register_imported / register_imported_all add to the connection overlay
- overlay_env returns an OperationEnv dispatching to imported ops; contains()
  returns true only for ops in the overlay
- call() opens a stream, sends call.requested, registers in PendingRequestMap,
  spawns a stream reader, resolves on first call.responded
- subscribe() sends call.requested and yields call.responded until
  call.completed/call.aborted via a SubscriptionStream wrapping the mpsc receiver
- abort() sends call.aborted for the request ID and removes the pending entry
- connection drop drops the overlay (no explicit deregistration needed)

Exposes MockConnection trait and Connection::from_mock in alknet-core so
cross-crate tests can construct mock connections without real QUIC. Removes
two unused test helpers in env.rs that triggered dead-code warnings under
-D warnings. Adds parking_lot dep for the overlay RwLock and pending Mutex.

9 new connection tests (102 total in alknet-call). Clippy clean.
2026-06-23 15:16:10 +00:00
glm-5.2 79bc6ffb31 feat(core): implement AlknetEndpoint, HandlerRegistry, accept loops, TLS identity, graceful shutdown (task: core/endpoint)
Implement the ALPN router and endpoint in endpoint.rs: AlknetEndpoint with
quinn+iroh accept loops (both feature-gated, both Option), HandlerRegistry
(new/register/get/alpn_strings with panic-on-duplicate), dispatch via
tokio::spawn by ALPN, AuthContext construction from connection
(alpn/remote_addr/fingerprint/identity), TLS identity modes (RawKey RFC 7250
via on-the-fly cert resolver, X509 from files, SelfSigned via rcgen),
EndpointError enum, graceful shutdown with drain timeout + force close.
ACME deferred as TODO per task spec. 55 tests (--all-features), 52 (default),
47 (no-default); clippy clean across all 3 feature combos.

Refs: docs/architecture/crates/core/endpoint.md
Implements: ADR-010
2026-06-23 15:14:26 +00:00