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.
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.
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.
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.
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.
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.
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.
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).
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.
Third POC iteration (alknet-fs-sync-poc, 9/9 tests) proves multi-node
path-tree sync:
- Path tree modeled as automerge CRDT document, synced via automerge's
sync protocol over iroh QUIC connections
- Each node has a local replica; writes are local + immediate (no
network latency); sync is async, gossip-style, eventually consistent
- Concurrent writes to different paths converge cleanly; concurrent
writes to same path resolve via LWW (NFS-equivalent semantics)
- Content (blobs) and metadata (path tree) sync separately — automerge
for path edges, iroh-blobs for file bytes
- Branch inheritance works through automerge sync
Key finding: automerge concurrent put_object on same key creates a
conflict, not a merge. Root structures must be created by one node and
synced before other nodes write. This is a design constraint for the
spec.
24 total tests pass across both POC crates. All remaining unknowns are
implementation-scope, not feasibility blockers.
Validates the three-layer architecture for a content-addressed, branch-aware,
mountable filesystem:
- SQLite path tree over iroh-blobs MemStore (15/15 tests pass)
- Fossil-style branching with free content dedup via BLAKE3 content addressing
- honker-core for notify-on-commit inside the same transaction as path-tree
mutations (transactional outbox pattern)
- Write path: "branch on write, merge on close" reconciles BLAKE3-must-hash-
complete-file with chunked filesystem writes; concurrent readers see old
version until close commits atomically; crash/abort leaves old version intact
- Multi-tenancy via bucket_id column (free isolation, auth is an adapter problem)
Remaining unknowns (FsStore/redb coexistence, distributed incomplete-blob reads,
SFTP wiring, GC/tag management, branch chain depth) are implementation-scope,
not feasibility blockers.
Documents the metatensor format: a binary data format where a TypeBox/jsonschema
schema describes the layout of binary data at schema-computed offsets. Extends
safetensors (fixed TensorRef schema) to arbitrary schemas, enabling struct tensors
(records), blob tensors (variable-length via indirection), and nested layouts.
Key points:
- TypeBox schemas render to standard JSON Schema; the jsonschema Rust crate
validates them with zero translation. Custom typedef.ts kinds (TFloat32,
TInt32, TStruct) map to jsonschema custom keywords via with_keyword().
- This eliminates typebox-rs as a schema engine — replaced by jsonschema +
a small offset-computation module + ~50 lines of custom keyword impls.
- Three tensor kinds: flat (safetensor today), struct (record of typed fields),
blob (struct tensor as index + flat tensor as data store, for variable-length)
- Memory-mappable: parse header, compute offsets, mmap data, typed views per
schema. No copy, no deserialization.
- QUIC-streamable: header is one small JSON message, each tensor is a separate
stream. Lazy loading, parallel transfer, incremental compute.
- ujsx-authorable: <Tensor>, <Struct>, <Field> as layout components, same
reconciler that diffs UI trees diffs model schemas. Model versioning is
tree diffing.
- Category-theory foundation: ujsx as universal typed-tree IR, HostConfig as
interpreter. <Tensor> is no stranger than <div>.
Adds a major section documenting how @alkdev/flowgraph (already npm-published,
uses ujsx) becomes the compute graph authoring and execution layer for
alknet-tensor, replacing webgpu-torch's imperative nn.Module hierarchy and
autograd recording with declarative ujsx templates and reactive DAG execution.
Key points documented:
- The ujsx tree IS the compute graph (CUDA-graphs-shaped but declarative)
- flowgraph's two HostConfigs: GraphologyHostConfig (compile/validate) and
ReactiveHostConfig (execute with signal-driven status propagation)
- nn modules become ujsx components, autograd becomes reverse tree walk
- Conditional/Map components enable dynamic structure CUDA graphs can't express
- Network-callable compute graphs (mix local + remote ops in one template)
- TSX authoring via standard JSX→h transform (ujsx jsx-runtime as target)
- graphology → petgraph port: ~15 API methods map 1:1, removes ~5400 lines of JS
- Updated POC priorities: end-to-end skeleton now includes flowgraph integration,
petgraph host port as a separate POC
Documents the architectural direction for a PyTorch-shaped tensor computation
library built on Rust + wgpu, where QuickJS is a thin API/composition layer
and Rust owns memory, dispatch, and WGSL codegen. Derived from webgpu-torch
as the reference design (op_spec → opgen → WGSL shader pipeline) but not a
port of its code — webgpu-torch is the reference, alknet-tensor is the
production architecture.
Key decisions: JS holds handles (BufferId), Rust owns wgpu::Buffers; ~4-5
high-level Rust ops (create_tensor/dispatch_kernel/register_kernel/read/write)
not ~20 low-level GPU API calls; WgslGenerator as a third handlebars backend
in typebox-rs codegen alongside RustGenerator and TypeScriptGenerator; tensor
ops as OperationSpecs on the registry (network-callable over irpc, verified
protocol-compatible on quickjs by POC 2).
Documents the downstream problems this solves as a side effect: distributed
compute over irpc, LLM-authored model code (toolEnv pattern), edge/embedded
tensor compute, the compositing problem sidestepped (compute has no surface),
and cross-platform by construction (wgpu's many backends).
The quickjs-reactive-probe was extended to load @alkdev/operations (registry,
call protocol, response envelopes, ACL, buildCallHandler) alongside the
reactive core. All five operations assertions pass on QuickJS-NG via rquickjs:
registry/execute/envelope/acl/callHandler. 271 modules loaded total.
This closes the third highest-leverage unknown: the operations protocol is
runtime-agnostic in practice, not just in theory. Adds a new section on the
QuickJS UDF host convergence — a minimal isolate speaking the same bidirectional
operations protocol as the TypeScript reference, the Rust alknet-call port,
and the planned NAPI/Python adapters, without needing Node/Deno/Bun. Connects
to the toolEnv WASM-QuickJS sandbox precedent at /workspace/toolEnv.
Captures the two completed POCs that resolve the highest-leverage unknowns
around the alknet-desktop direction (Rust + wgpu + rquickjs + ujsx over three.js):
- ui-spoke-poc: headless WebGPU rendering in Deno, three.js WebGPURenderer via
device-capture, MSDF text (the '2D UI is rocket surgery' subproblem)
- quickjs-reactive-probe: @preact/signals-core + @alkdev/typebox + @alkdev/ujsx
reconciler verified compatible with QuickJS-NG via rquickjs
Documents the rejected deno-desktop alternative, the established architectural
direction (head-worker over irpc/ALPN, two HostConfigs over one wgpu surface),
headless/headed parity via llvmpipe, the supply-chain surface reduction, and
the open unknowns that remain before SDD can begin.
Phase 5 now references the architect role and SDD process from
docs/sdd_process.md instead of creating ad-hoc spec stubs. Added
key new ADRs and architecture docs the architect will need to produce.
Updated gitserver reference note (MPL concern, archive it).
Kept rudolfs reference (MIT/Apache, fork candidate).
Also removed 'needs-update' status from the lifecycle states since
it's not part of the SDD process — stale docs get annotated with a
note and existing status, not a new status.
- 2.1: Add prerequisites note (verify call::frame module, ControlChannelRouter
wiring) before decomposition
- 2.2: Add raw framing auth design decision (first-frame auth event pattern
instead of per-frame auth) — simpler, more secure, matches InterfaceEvent model
- 2.3: Add InterfaceConfig restructuring note, TransportKind::WebTransport
tag addition (missed in Phase 1), note that TransportKind::Dns removal
is a no-op (never added). Add scheduling note: do 2.3 early since
subsequent tasks reference new trait names. Update ADR reference to 035.
- 2.4: Split into 2.4a (trait+enum+ConfigCredentialProvider) and 2.4b
(SecretStoreCredentialProvider, Phase 3). Clarify that the Phase 2 impl
is config-backed, not secret-backed.
- 2.5: Mark TransportKind::Dns removal as no-op since it was never added.
- 4.5: Note that doc sync round 1 is already done (commit cfc4400).
Second sync needed after implementation to capture any deviations.
- Open questions: Mark OQ-IF-01 and OQ-IF-02 as resolved with ADR-035
and ADR-031 references. Update OQ-P2-01 through P2-04 with ADR-036
and resolution status.
The axum router scaffold now only includes auth middleware and stealth
handoff — no operational routes or path conventions. External HTTP path
routing (from_openapi inverse, custom S3/git/OpenAI paths) is deferred
to Phase 5 since it depends on the spec-generation work.
- rustfs-events-select.md: deep dive into rustfs S3 event notification
system (9 target types, 30+ event types, rule engine, queue store)
and S3 Select (DataFusion-based SQL, CSV/JSON/Parquet input)
- honker-reference.md: deep dive into honker SQLite extension for
pub/sub, queue, and notification — core primitives, SQL API,
wake mechanism, single-machine design, and mapping to alknet
storage patterns
- definitions.md: formal term disambiguation for overloaded concepts
(service, interface, token, identity, domain) with cross-domain mapping
tables (alknet ↔ Keystone, distributed git, rustfs) and 8 open questions
- references/rustfs/: research on rustfs S3 store, Keystone/OIDC integration,
and credential mapping to CredentialSet
- references/gitserver/: research on gitserver library architecture and
integration paths as HTTP MessageInterface and SSH adapter
- references/openstack-keystone/: research on Keystone identity concepts
(tokens, scoping, service catalog, RBAC, trust delegation, federation)
and what alknet should adopt vs skip
- references/distributed-identity/: research on decentralized git, smart
contract ACL, on-chain identity, and Radicle comparison
Three research documents for Phase 2 planning:
- credential-provider.md: Outbound auth (CredentialProvider trait, CredentialSet enum),
account model as storage-layer concern (Identity.id as account UUID), SecretStoreCredentialProvider,
ManagedCredentialProvider, self-hosted service auth analysis (rustfs S3/OIDC, gitea OAuth2),
implementation phases A-D.
- interface-model.md: StreamInterface vs MessageInterface trait design, HTTP interface
as axum handler, DNS as MessageInterface, unified auth across all interfaces
(AuthToken + API keys via resolve_from_token), removal of TransportKind::Dns.
- tls-transport.md: Unified multi-interface architecture on port 443. Byte-peek protocol
detection (existing stealth mode) routes SSH vs axum. Axum multiplexes REST, WebSocket,
SSE, gRPC. QUIC/UDP with ALPN routing for WebTransport and iroh P2P. Single AuthToken
mechanism for all non-SSH interfaces. Four primitive operations (call/batch/schema/subscribe)
map to HTTP, MCP, and DNS.
Update four existing specs (overview, server, napi-and-pubsub, call-protocol) to
reflect Phase 0 decisions: three-layer model, IdentityProvider, ForwardingPolicy,
OperationEnv, static/dynamic config split. Review all 9 Phase 0a ADRs (026-034)
for consistency. Fix 4 critical issues from architecture review: missing OQ-SVC-05
in open-questions.md, deprecated hub terminology, undefined AuthService and noq
terms. Replace inline OQ text with cross-references per format rules. Add
ConfigServiceImpl definition to configuration.md. Port absolute workspace paths
to project-relative links by copying referenced docs (feasibility, certbot,
fail2ban, event_source_types) into docs/research/.
- Replace hub/spoke with head/worker terminology in call-protocol.md,
auth.md, open-questions.md, napi-and-pubsub.md
- Update operation paths from /{spoke}/{service}/{op} to
/{node}/{service}/{op} throughout call-protocol.md
- Unify Identity struct: auth.md already had {id, scopes, resources},
add note clarifying this is canonical (vs research/services.md which
used {node_id, fingerprint, scopes})
- Update integration-plan.md inconsistencies section to track what's
been fixed (hub/spoke, identity model) and expand service naming
to include external services
- Update call-protocol.md last_updated date
ADRs are intentionally left unchanged as historical records.
Organizes findings from the research phase (core, services, configuration,
storage, flow) into an actionable phased plan covering:
- Transport/Interface/Protocol three-layer model
- OperationEnv as universal composition mechanism (not replaced by irpc)
- Phase 0: Architecture foundation (9 ADRs, ~10 spec docs)
- Phase 1: Core modifications (config split, identity, forwarding, auth,
OperationEnv, interface abstraction)
- Phase 2: External crates (alknet-secret, alknet-storage, alknet-flowgraph)
- Phase 3: Integration and wiring
- Phase 4: Advanced features (DNS, WebTransport, app services)
Key clarifications: irpc services are one dispatch backend for OperationEnv,
not a replacement for it. DNS control channel is a (DNS transport, raw framing
interface) pair, not SSH-over-DNS. Call protocol and irpc operate at different
scope boundaries within Layer 3.
Document the OperationContext (request_id, identity, metadata, env, trusted),
OperationEnv (namespaced callables for handler composition), ResponseEnvelope
pattern, and how MCP/OpenAPI adapters map to the irpc service model.
- Replace hub/spoke terminology with head/worker throughout all research docs
- Add irpc service layer architecture (AuthProtocol, SecretProtocol,
ConfigProtocol, StorageProtocol)
- Add BIP39/SLIP-0010 HD key derivation for secrets management
- Add event boundary discipline (domain events vs integration events)
- Add application services layer (Docker, Node, Wallet, Proxy, Compute)
- New docs/research/services.md defining irpc service protocols
- Update core.md with service layer section and head/worker model
- Update configuration.md to delegate auth to AuthService (irpc)
- Update storage.md with secrets/key derivation and event boundaries
- Update flow.md with event boundary decision and cross-references
Unified authentication (ADR-023): SSH and WebTransport auth share the same
Ed25519 key material. Token auth uses signed timestamps verified against the
same authorized_keys set. IdentityProvider trait decouples core from identity
storage.
Bidirectional call protocol (ADR-024): Generalizes control channel (ADR-018)
to support hub→spoke and spoke→hub calls. Operation paths use /{spoke}/{service}/{op}
format for three-level routing. EventEnvelope wire format, five call events,
PendingRequestMap for correlation.
Handler/spec separation (ADR-025): Downstream consumers register operations
without modifying core. OperationRegistry maps paths to specs + handlers.
Service discovery via /services/list and /services/schema.
Resolves OQ-17 (transport-aware auth), OQ-21 (spoke routing), OQ-CFG-04 and
OQ-CFG-06 (WebTransport auth and transport-aware auth layer). Adds OQ-18
through OQ-22 for remaining open questions.