7cdff8c1272148a08b80dac8ad6610411994f882
46
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
762d9c7bd2 |
docs(architecture): remove removed-thing remnants from spec docs
Spec docs should describe WHAT IS, not WHAT WAS. ADRs and OQ files are historical records by design and are left alone; the ADR-index Status column records ADR status (stable fact). What changed in the specs: - API code blocks no longer list removed methods. ChannelClient::connect_quic (channel-client.md) and CallClient::connect (client-and-adapters.md) are gone from the impl blocks; surrounding prose describes the current from_connection / spawn_dispatch primary + AlknetClient dial shape. - Amendment (ADR-093): stream_types field is removed blockquotes dropped from channel-client.md and channel-operations.md (the code already reflects the current state). - Historical is-removed / reversed-by / amended-by prose rewritten to current state across channels crate, call crate, hub, tls, core, endpoint, client READMEs, and the top-level README/overview. - Dropped the EndpointError — removed subsection from endpoint/README.md (the type doesn't exist anymore, so it shouldn't have a subsection). - Replaced stale connect() references in flow descriptions with the dial (in AlknetClient) since connect() is no longer a method. - Removed strikethrough ADR-028 / from_jsonschema-clause rows from client-and-adapters.md and operation-registry.md ADR tables. - Rewrote the ADR-066 update blockquote in operation-registry.md to describe FromJsonSchema's current shape. 19 files modified, net -116 lines. No ADRs or OQ files touched. |
||
|
|
3b10fc1817 |
refactor(core,tls,client): align ConnectionCredentials field name with ADR-091 (tls_identity -> local_identity)
ADR-091 decided `ConnectionCredentials.local_identity`; the code implemented
`tls_identity` (tasks/core/connection-credentials.md deferred the rename as
"path of least resistance" during the extraction). The tangle that made the
rename hard no longer exists, so align the code with the decision.
Scope is the `ConnectionCredentials` field + builder only:
- alknet-core/credentials.rs: field, with_local_identity, doc, test
- alknet-tls/client.rs: field access in TlsClientConfig::new, test builders, docs
- alknet-client/dial/quinn.rs: test builder
NOT renamed (distinct concepts sharing the words):
- StaticConfig.tls_identity (server-side static config; ADR-082/027/083)
- TlsIdentity enum type name
- alknet-tls server fn params named tls_identity (&TlsIdentity value)
Also fixes dial_iroh.rs doc comments that claimed the local key is extracted
from creds.local_identity — the key is actually on the pre-built iroh endpoint
(set at with_iroh time); the dial reads only creds.remote_identity and ignores
creds.local_identity (per client/README.md §iroh).
Architecture specs updated to match (call/client-and-adapters.md, tls/README.md,
client/README.md). Historical ADR context describing the old CallCredentials
field stays as-is; tasks/ and docs/research/ are historical artifacts.
Resolves follow-up #2 from the post-extraction spec sync (
|
||
|
|
c6eef730e4 |
docs(architecture): sync specs to post-extraction state (phases 0-5)
The crate-extraction migration (phases 0-5) is complete in the code;
the specs still carried forward/migration framing ("was welded",
"after the refactor", "currently duplicated", "does not exist yet",
"What moves from X to Y" tables, "Implementation ordering") that
described the migration rather than the resulting state. Updated 10
spec files to describe the current state cleanly.
Spec/code mismatches fixed:
- core/README.md: a stale paragraph said CallCredentials "stays in
alknet-call" while ADR-091 Am. 2026-07-17 removed it. Now consistent.
- tls/README.md: TlsClientConfig API described a planned
ClientVerifierContext + for_tcp_tls(&self) + rustls_config(&self);
the actual code is new(&ConnectionCredentials, alpn) +
for_quinn(self) + into_rustls_config(self). Updated to match.
- client/README.md, call/client-and-adapters.md: ConnectionCredentials
field is tls_identity / with_tls_identity in the code, not
local_identity / with_local_identity. Updated the specs describing
the current API (ADR-091 body keeps local_identity as the decided
name).
- client/README.md: dial_iroh description said the local key is
"extracted from creds.local_identity" — the code uses the pre-built
iroh endpoint's key (set at with_iroh time) and reads only
creds.remote_identity for the NodeId. Fixed.
- overview.md: said core has "no quinn/iroh deps" — core keeps
quinn/iroh for Connection::from_quinn/from_iroh. Fixed.
- call/client-and-adapters.md: a /// doc-comment block and
pub struct RemoteIdentity were floating outside any code fence
(orphaned closing backticks). Fixed.
- tls/README.md: TlsError sketch shows the full ADR-088 6-variant
enum; the code has a simplified 3-variant enum. Added an
implementation note flagging the divergence; ADR-088 shape kept as
target.
- call/README.md: review note said "ADR-029 migration pending" (stale
— migration landed). Updated to reflect phase 5 completion (pure
protocol crate, no TLS/transport deps, verified against Cargo.toml).
Migration framing removed (present-state descriptions instead):
- tls/README.md: "What moves from" tables -> module-contents tables;
"Implementation ordering / greenfield" section removed; "after the
refactor" section -> "What AlknetEndpoint does"; references to
extraction-source files (alknet-core/src/endpoint.rs,
alknet-call/src/client/call_client.rs) replaced with current file
locations (alknet-tls/src/{server,client,pem,signing}.rs).
- endpoint/README.md: "was two things welded" framing removed;
"after the extraction" section -> "What alknet-core looks like".
- core/endpoint.md: "Historical summary" section removed; clean
deprecation pointer.
- README.md, overview.md, open-questions.md: dates + present-tense
cleanup.
|
||
|
|
e91d943857 |
docs: remove CallCredentials — dead field, dead from_call path, auth_token is per-request payload
ADR-091 amended 2026-07-17: CallCredentials removed (not retained in alknet-call). Trace showed CallCredentials.auth_token had no reader (connect() read only tls_identity + remote_identity; spawn_dispatch takes no credentials; from_call's credentials_auth_token was a different type, always None, never connected). auth_token is a per-request payload field — browsers send it in the WS payload; the HTTP gateway resolves bearer → Identity at its boundary. from_call's credentials_auth_token dead path removed in the same pass (OpSummary field, handler params, build_forwarded_payload param, and the two tests asserting the never-exercised Some path). ADR-089 §5 further amended, ADR-080 noted, all spec READMEs and overview updated. Migration plan (findings.md) corrected: Phase 5 prune now includes CallCredentials removal + from_call dead-path removal; test audit corrected (4 unchanged + 2 move to core, not 6 unchanged); integration-test split documented; all 'or' hedges resolved. |
||
|
|
613bf680cd |
docs(arch): ConnectionCredentials decouples dial from call protocol (ADR-091) + migration plan cleanups
ADR-091: ConnectionCredentials — decouple the dial credential bundle from the call protocol. CallCredentials (call-protocol-level, carries auth_token) was being used as the dial's credential type, coupling the dial to the call protocol. The auth_token is a hub-layer identity correlation mechanism (browsers, alknet/register), not a transport credential. ConnectionCredentials (transport-level: local_identity + remote_identity) is the dial's credential bundle; all three dial signatures unify on &ConnectionCredentials; dial_iroh's node_id parameter is derived from remote_identity.fingerprint. CallCredentials stays in alknet-call with auth_token. The shape is validated by the future dial_ssh pattern (russh's check_server_key + authenticate_publickey consume the same two dimensions). Amends ADR-089 §3 (dial signatures) and §5 (move consequence), ADR-087 (input framing). Updates client/tls/call/core crate specs and the extraction plan's Phase 0/3/4/5. Migration plan cleanups (findings.md): - Remove duplicated ordering-rationale bullets (copy-paste artifact) - TL;DR: six phases -> seven (Phase 0 promoted); four compilable intermediate states -> each phase leaves workspace compilable - Remove inline 'Wait — that's 10, not 8' self-correction; fix Category B header to (10 tests) - Replace contradictory line ranges in Net Phase 5 test impact with name-based references - Decide quinn feature fate: removed (not no-op) - Note webpki-roots always-present per ADR-088 §5 in Phase 1 dep list |
||
|
|
bf6ce957c0 |
docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps
Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client) + revised core found compile-blocking inconsistencies, stale claims, and dep-graph contradictions. All resolved: Critical: - C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not delegated) — keeping them as thin wrappers over AlknetClient::dial_quic would make protocol crates depend on alknet-client, contradicting the dep graph. Callers compose dial + take-over (2 lines). - C2: alknet-client feature gates now pull alknet-core/quinn + alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh). - C3: rustls-native-certs + webpki-roots added to alknet-tls deps (always-present, not feature-gated — CA-verify path is transport-agnostic). Warning: - W1: CallCredentials/RemoteIdentity moved to alknet-core (from alknet-call) — the dial must not depend on the call protocol; not a two-way-door, it determines the dep graph. - W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) — the code claimed a fallback that never existed; now the store is never empty, NoRootAnchors unreachable, containerized deployments work. - W3: EndpointError removed entirely (BindFailed + HandlerNotFound both vestigial after ADR-083); shutdown() is now infallible. - W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) — alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs entirely; CallClient becomes a pure protocol crate. Plus: ClientError removed (only produced by removed connect); S1 (CallCredentials → ClientVerifierContext mapping + auth_token stripped at TLS boundary documented); amendment notes on ADR-017, ADR-069, ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index updated. 29 files, consistency-reviewed. |
||
|
|
ce7de57973 |
docs(arch): AlknetClient native dial seam — resolves OQ-55 (ADR-089)
Extract the deferred AlknetClient as a new crate alknet-client — the client-side analogue of AlknetEndpoint. Three dial methods (QUIC + TCP+TLS via TlsClientConfig, iroh via key) produce a Connection for CallClient::spawn_dispatch / ChannelClient::from_connection to consume. The deferral collapsed because ADR-086 gave the native endpoint type three dial shapes within one endpoint type, ADR-087 broke the circular hedge, and ADR-083 gave the server-side shape to mirror by symmetry. Names the three concept layers that were tangled throughout the initial development (deployment role / establishment side / ALPN-level category) so the fix is legible. Names alknet/register as a dialable entry-point ALPN (native registration, parallel to HTTP registration in OQ-58); its wire protocol is deferred to OQ-66 (blocked on OQ-58's token model). Cross-references updated across 11 existing docs (README, overview, open-questions, OQ-55, tls, hub, core, channels README/overview/ channel-client, call client-and-adapters) to reflect OQ-55 resolved and the new alknet-client crate. Architecture review passed (2 critical, 7 warnings — all addressed). |
||
|
|
b0cc0a01b7 |
docs(arch): unweld CallClient from QUIC — spawn_dispatch primary, connect convenience (ADR-017 Am. 2026-07-13)
Same fix as ADR-080 (ChannelClient) applied to the call crate. The existing code already had the right structure — spawn_dispatch is not feature-gated (transport-agnostic), connect is #[cfg(feature = "quinn")] — but the docs inverted the framing: connect was 'primary,' spawn_dispatch was 'lower-level.' That welding masked the call protocol's transport-agnosticism (ADR-012 EventEnvelope; ADR-065 from_stream/from_bidi accept any AsyncRead + AsyncWrite). Changes: - client-and-adapters.md: reframe spawn_dispatch as the transport- agnostic primary constructor (one-way door), connect as the QUIC convenience (two-way door, feature-gated on quinn). Mirror the from_connection / connect_quic pattern from ADR-080/channel-client.md. Fix all 'QUIC-backed' / 'over QUIC' / 'opens a QUIC connection' framing to be transport-agnostic. Fix from_call description, adapter location map, exchange-of-operations example, Constraints section. - call-protocol.md: 'runs over QUIC bidirectional streams' → 'runs over any ordered, reliable bidirectional stream.' Fix stream model, stream lifecycle (connection drop / stream reset now list all transports, not just QUIC). Fix CallConnection.connection doc comment. - operation-registry.md: FromCall provenance comment 'QUIC forwarding stub' → 'call-protocol forwarding stub.' Fix from_call description. - call README.md: fix client-and-adapters.md description, fix design principle #11 framing. - ADR-017: add Amendment (2026-07-13) documenting the spawn_dispatch / connect reframe, mirroring ADR-080's amendment. - overview.md, architecture README: update ADR-017 and client-and-adapters.md table summaries. - OQ-015, OQ-007: fix 'opens QUIC connections' / 'bidirectional QUIC streams' framing in resolution text. No code changes — spawn_dispatch and connect already exist with the right feature-gate structure. This is a documentation reframe. |
||
|
|
87b2c2a5ef |
docs(arch): hub-wiring cluster — aggregated env, peer_operations, from_call cleanup, alknet-hub crate spec
Three ADRs addressing gaps surfaced by the first hub consumer (alkapi): - ADR-067: Aggregated peer-env wiring — Dispatcher::with_aggregated_env hook so compose_root_env reads a shared PeerCompositeEnv across all calls, not a fresh per-call one. The hub-defining gap (alkapi OQ-08 / G.1). - ADR-068: PeerCompositeEnv::peer_operations override — adds list_operation_names() to OperationEnv, overrides on OverlayOperationEnv and PeerCompositeEnv. Fixes services/list-peers returning empty operation lists for non-local peers (alkapi G.6). - ADR-069: from_call is a manual free function, not auto-wired — reverses the aspirational OQ-27 resolution to match the implementation. Cleans the 'v1 default' hedging language in ADR-017 and client-and-adapters.md (alkapi G.4). New crate spec: alknet-hub — reusable hub pattern (aggregated env, connection lifecycle, worker supervision with backoff, service discovery). Spec fixes: builder API drift (with_local/with_local_streaming separation), ScopedOperationEnv → ScopedPeerEnv type name, OQ count 51→54. Three new OQs: OQ-52 (wait_for_close), OQ-53 (backoff defaults), OQ-54 (inbound hook placement). |
||
|
|
97456bc608 |
docs(arch): ADR-066 — move from_jsonschema to alknet-http as HTTP-backed single-endpoint adapter
from_jsonschema was in alknet-call as a schema-only placeholder with a NOT_FOUND handler — broken (an op in the registry needs a real handler) and in the wrong crate (alknet-call has no HTTP client; a useful from_jsonschema needs reqwest like from_openapi). ADR-066 moves it to alknet-http as a real reqwest-backed single-endpoint adapter for non-standard/non-OpenAPI REST endpoints, functionally similar to from_openapi but one endpoint at a time. FromJsonSchema provenance stays in alknet-call (now a handler-bearing leaf). - New ADR-066 (supersedes ADR-017 §5 from_jsonschema clause + ADR-022 FromJsonSchema row; both amended with strikethrough + pointer) - Updated specs: call/client-and-adapters, call/README, call/operation-registry, http/http-adapters (new from_jsonschema section), http/overview, http/README, arch README + overview - New task: tasks/http/adapters/from-jsonschema.md (depends on from-openapi; includes alknet-call cleanup of the broken placeholder) - Old task tasks/call/client/from-jsonschema.md marked superseded taskgraph validate: 116 tasks valid |
||
|
|
2aa6363e57 |
docs(arch): sync specs to transport generalization sweep (ADR-064, ADR-065)
Three code commits landed a clean sweep discovered when building an
external app against the crates. This syncs the architecture specs to
match the codebase and amends the affected decisions.
ADR-064 (supersedes ADR-005): irpc was never integrated — no .rs file in
the workspace ever imported it. The wire protocol (wire.rs) is hand-rolled
length-prefixed JSON; the EventEnvelope shape was derived from the
@alkdev/pubsub TypeScript prior art (ADR-013), not from irpc. ADR-005's
premise ('irpc as the call protocol foundation') was never implemented as
stated. Superseded with a clear header; the body is kept as historical
record.
ADR-065: Connection::from_stream / from_bidi — Connection now accepts any
AsyncRead + AsyncWrite pair via ConnectionKind::Stream (yield-once
accept_bi contract: QUIC yields many streams, everything else yields one
then ConnectionClosed). Unblocks TCP+TLS, SSH channel dispatch,
WebTransport streams, and wasm streams through the same HandlerRegistry
as QUIC connections, with zero handler code changes. MockConnection /
ConnectionKind::Mock removed (tests use from_stream with sink/empty).
Stream-level Mock variants renamed to Stream (they were already generic).
Amended ADRs: ADR-003 (irpc removed from dep table), ADR-007 (from_stream
opens the server-side door; MockConnection removed), ADR-010 (TCP+TLS can
now dispatch through the registry via from_bidi; iroh 1.0 migration noted).
Updated specs: core-types.md (Connection/SendStream/RecvStream sections),
endpoint.md (TCP section, iroh note), call-protocol.md, operation-registry
(irpc Integration section replaced), overview.md, http-server.md,
webtransport.md, call README. OQ-09 (WASM) resolution amended: the
Connection door is now open via from_stream; the accept-loop runtime door
remains closed (tokio doesn't run on WASM).
See docs/research/transport-generalization/findings.md for the full trace.
|
||
|
|
ad167aa470 |
docs(arch): update core/call specs for ADR-050 — ownership provider + resource_id_path
operation-registry.md: - OperationSpec gains resource_id_path: Option<String> (JSON pointer into the input for runtime-spawned resource ID extraction) - AccessControl::check signature updated: consults an OwnershipProvider for dynamic resource ownership; falls back to static Identity.resources when no provider is wired (backward compatible) - Dispatch flow updated: step 3 extracts resource_id via spec.resource_id_path before the ACL check - Added composition + dynamic ownership interaction (ADR-050 §4d): two orthogonal checks, ADR-015/022 unchanged - Design Decisions table + Open Questions + References updated auth.md: - New 'Ownership Provider and Store (ADR-050)' section: OwnershipProvider (sync read trait) + OwnershipStore (async write trait) + InMemoryOwnershipStore default adapter; fourth instance of the repo/adapter pattern (ADR-033) - How it integrates with AccessControl::check - Access pattern: proxy-only (spawner owns, proxy to share, teardown revokes; no grant mechanism in core) - Per-node ownership (no cross-node propagation in the base model) - Resource-scoped ACLs table gains the dynamic ownership path - Design Decisions table + Open Questions updated |
||
|
|
7ecc11610a |
docs(arch): ADR-049 — streaming handler for subscription operations
The call protocol spec describes streaming (call.responded*N + call.completed, PendingRequestMap::Subscribe, CallConnection::subscribe), but the server-side Handler type returned a single ResponseEnvelope — a Subscription op had no way to produce a stream. The TS predecessor (@alkdev/operations) had separate OperationHandler / SubscriptionHandler types; the Rust port collapsed them, losing the streaming path. This restores it end-to-end: StreamingHandler type, HandlerKind on HandlerRegistration validated against op_type, invoke_streaming() on OperationRegistry, server-side dispatch branches on op_type, new INVALID_OPERATION_TYPE protocol code for wrong-dispatch-path misuse, GatewayDispatch::invoke_streaming() for /subscribe SSE, from_call stream forwarding via CallConnection::subscribe(), from_openapi SSE forwarding. OperationEnv::invoke() stays request/response-only (stream composition is handler-level, not protocol-level). Amends ADR-023's protocol-code list (five → six). Tracks the stream-operators library as OQ-41 (feature extension, not an unmade decision). |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
2e34590522 |
docs(architecture): resolve review #003 — type/API surface completeness
Review #003 found 11 critical, 14 warning, and 6 suggestion findings after reviews #001 (governance/security) and #002 (cross-document consistency/two-way-door audit) were resolved. The theme: types and APIs that were *referenced* but never *defined*, and stale ADR sketches that didn't match the now-updated spec docs. Critical fixes (11): - C1: DerivedKey #[derive(Deserialize)] contradicted the custom Deserialize that rejects "[REDACTED]" — dropped the derive, added explicit manual Serialize/Deserialize impls (protocol.md). - C2: encrypt prose said "derived at PATHS::ENCRYPTION" but the signature takes key_version — updated to encryption_path_for_version (service.md). - C3: derive_encryption_key returned DerivedKey, derive_encryption_key _for_version returned EncryptionKey (same cache) — unified on DerivedKey, defined CachedKey (service.md). - C4: tokio vs std::sync::RwLock contradiction — specified std::sync::RwLock, dropped tokio from vault deps (ADR-018, ADR-025, service.md). - C5: Missing drift rows in vault README — added #9 (key_version ignored) and #10 (rotate not implemented). - C6: ADR-022 build_root_context and invoke() sketches omitted abort_policy (9 fields vs 10) — added the field to both sketches. - C7: Capabilities type referenced 20+ times, never defined — added struct definition to core-types.md with Clone+Send+Sync, Zeroize, sealed builder API, immutability guard. - C8: SessionOverlaySource on CallAdapter but never defined, crate violation (alknet-call can't depend on alknet-agent) — defined the trait in alknet-call (call-protocol.md), matching the IdentityProvider pattern. - C9: CompositeOperationEnv dispatch fall-through was "a two-way door" — added contains() to OperationEnv trait, made the composite probe before dispatching, eliminating the sentinel ambiguity. - C10: No API for Layer 2 (connection overlay) registration, CallConnection undefined — defined CallConnection struct + register_imported() API (call-protocol.md). - C11: with_local signature diverged between two examples (4 args vs 5) — added capabilities as the 5th arg, made both examples consistent. Warning fixes (14): - W1: invoke_with_policy restructured as required method, invoke gets a default impl delegating to it — eliminates duplication across impls. - W2: CachedKey defined (service.md). - W3: EncryptionKey constructor/glue specified, added to re-export list. - W4: Secp256k1ExtendedPrivKey defined, derive_ethereum_key glue shown. - W5: encryption_path_for_version rejects version < 2 (v1 is TS PBKDF2). - W6: Wire payload schemas for all event types + ResponseEnvelope → EventEnvelope conversion table (call-protocol.md). - W7: Timeout section — deadline on OperationContext, composed calls inherit parent's deadline, CallAdapter::with_timeout(). - W8: Request ID generation spec — UUID v4 for composed calls, wire ID vs internal ID relationship for abort cascade. - W9: unlock_new already-unlocked behavior specified (returns AlreadyUnlocked). - W10: KeyType Serialize/Deserialize justification corrected (stale irpc reference removed). - W11: OperationProvenance and CompositionAuthority defined inline in operation-registry.md (were only in ADR-022). - W12: encrypt/decrypt free functions marked pub(crate), relationship to VaultServiceHandle methods stated. - W13: rotate signature removed from encryption.md (it's a VaultServiceHandle method, not a free function). - W14: CallAdapter::new() + with_session_source() + with_timeout() constructors shown. Suggestion fixes (6): Seed: Clone note, VaultServiceInner invariant, ExtendedPrivKey accessor signatures, CURRENT_KEY_VERSION location, ADR-018 stale actor text, derivation helpers re-export note. |
||
|
|
cb98f42cd4 |
docs(architecture): resolve review #002 remaining Tier 4 findings
Add ADR-026 (vault key model — HD derivation) recording the foundational HD-derivation decision, 74' coin type reservation, SLIP-0010/Ed25519 default, secp256k1 feature-gating, and AES-256-GCM cipher choice. These were previously inline rationale with no ADR (W9). Extend ADR-018 with an explicit EncryptedData wire format lock — fields, encoding, and semantics are frozen; no removal without a format-version migration (W10). Resolve the remaining guard clauses and spec decisions: - W2: Capabilities must be immutable after construction (no interior mutability). Makes the Arc vs deep-copy clone semantics genuinely two-way. - W5: Published to_* specs are compatibility contracts — best-effort mappings are two-way before first publication, one-way after. Version generated specs. - W6: Salt field clarification — v2 salt is permanently unused; a future KDF is a different derivation family, not a version-indexed path; the field saves a wire-format change only. - W7: unlock_new returns Zeroizing<String> — the mnemonic is the root of trust and must not linger in freed memory. - W17: OQ-09 WASM — server-side dispatch door is honestly closed (Connection is concrete, tokio-bound), not implicitly preserved. - W18: OQ-10 git — composability fork (raw smart protocol vs call-protocol projection) is a separate decision from ERC721 scope. - W20: from_openapi must prefix imported error codes (HTTP_404) to avoid collision with protocol-level codes (NOT_FOUND). Normative rule, not naming convention. - W21: ScopedOperationEnv field is private — construction via new()/ empty(), query via allows(). Makes the future subgraph refactor non-breaking. - C13: Connection::set_identity — the endpoint does not read identity() after handle() returns (Connection is moved into the spawned task). Observability is handler-side logging. Simplest honest answer. - W1: OperationAdapter trait is async, returns Vec<HandlerRegistration>. from_call requires async discovery; ADR-022 changed the return type. - W11: CompositionAuthority::as_identity() defined — constructs a synthetic Identity (label as id, scopes, resources) not resolvable via IdentityProvider. Second Identity construction path, acknowledged. - W14: SecretKey is iroh::SecretKey (Ed25519) — consistent with the endpoint's iroh dependency. - W19: Grandchild abort propagation is inherit-by-default (option a) — invoke() with no explicit policy inherits parent's policy. ContinueRunning auto-propagates to grandchildren unless explicitly overridden. |
||
|
|
7dda6eec68 |
docs(architecture): add ADR-025 — vault local-only dispatch, drop irpc
Drops irpc from alknet-vault entirely. The vault's dispatch is now direct method calls on VaultServiceHandle — no VaultProtocol enum, no VaultMessage, no VaultServiceActor, no mpsc channel, no Service trait, no RemoteService trait, no postcard serialization. The vault is local-only by construction. The core security argument: irpc made the vault remote-capable by default (RemoteService generated unless no_rpc is passed). The IrohProtocol handler forwards all messages without auth. The docs framed 'register an ALPN' as a server-setup change. This is the default-insecure anti-pattern — security should be opt-in, not opt-out. ADR-025 inverts the default: local-only is the only mode, and remote access requires building a separate vault-server crate (a visible architectural act, not a flag flip). The actor path was already dead code — service.md said 'prefer VaultServiceHandle directly — no channel, no serialization.' The actor existed only to make irpc's Service trait work, which existed only to make RemoteService work, which was the footgun. VaultServiceHandle's Arc<RwLock> provides concurrent reads and exclusive writes — better throughput than the actor's sequential processing. DerivedKey serialization simplifies: always redact on serialize (for logging safety), reject '[REDACTED]' on deserialize with an error. No 'postcard preserves bytes' path. This resolves review #002 W8 (silent corruption on JSON-deserialized DerivedKey). Resolves: - OQ-21: remote vault access — resolved (not deferred). Not a vault crate feature; if needed, a separate vault-server crate with its own ADR. - C7: vault-server-crate question decided — not created now, not precluded. - C8: operation access policy table dissolved — all operations local-only by default; if a vault-server crate exposes some remotely, that crate defines the policy. - W8: DerivedKey JSON deserialization — resolved (reject redacted payloads). Amends ADR-005 (irpc remains for alknet-call, not for alknet-vault), ADR-018 (vault is even more standalone — zero RPC framework deps), ADR-019 (vault is the only layer, not just the only direct-caller layer), ADR-008 (vault integration point unchanged, but now local-only by construction). |
||
|
|
cdf340bec7 |
docs(architecture): add ADR-024 — operation registry layering, resolve C6
Diagnoses a conflation in the pre-ADR-024 spec: the OperationRegistry inherited immutability by analogy from ADR-010's HandlerRegistry (ALPN-level), but the TLS-config argument that justifies HandlerRegistry immutability does not apply to the operation registry, which lives behind a single ALPN (alknet/call). This made from_call (which discovers ops over a live connection at runtime) structurally incompatible with the blanket immutability claim. ADR-024 layers the operation registry by trust boundary: curated (Local) ops are static and immutable — the startup trust boundary is where their composition authority is granted; session (Session) and imported (FromCall etc.) ops are dynamic at their respective scopes (per-session, per-connection) — their trust boundaries are per-scope, not per-startup. The principle: immutability follows the trust boundary. Immutability is the security control for composing ops (can escalate privilege); provenance + composition authority are the controls for non-composing ops (can't escalate). The OperationEnv trait becomes the integration point (Arc<dyn OperationEnv>), following the IdentityProvider precedent (ADR-004): the CallAdapter composes the root OperationContext.env per incoming call from the active layers (curated base + connection overlay + session overlay). Children inherit the parent's composite env by Arc::clone — overlay composition happens once at the root and propagates through the composition tree. Resolves review #002 C6 (OperationContext.env type identity crisis): the field is split into scoped_env: ScopedOperationEnv (reachability data, from the registration bundle) and env: Arc<dyn OperationEnv + Send + Sync> (dispatch trait object). One field was being used as two different types (reachability set with .allows() and dispatch trait with .invoke()); Localizes W4 (hot-swap ↔ registry mutability coupling) to the connection scope: no global mutable registry to hot-swap; overlays replace naturally with connect/disconnect and session start/end. Schema-drift on reconnect is a per-connection overlay-rebuild concern, not a global hot-swap protocol. Partially addresses W3 (CallClient registry security): the registry-shape sub-question is resolved by the overlay model; the capability-exposure sub-question (what capabilities a remote peer can trigger) remains for ADR-017 — ADR-024 does not overclaim resolution there. Amends OQ-04 to scope its immutability claim to the HandlerRegistry and cross-reference ADR-024 for the operation registry. Generalizes OQ-19's session-overlay mechanism to also cover connection-scoped remote imports — both are per-scope dynamic overlays on the static curated base, using the same trait-layering mechanism. |
||
|
|
c62a6adc7b |
docs(architecture): resolve review #002 Tiers 1-3 — mechanical and consistency fixes
Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
|
||
|
|
6a7f8f91ad |
docs(architecture): resolve S1 — abort policy on OperationContext, not wire
ADR-016 Decision 6 specifies that the abort policy (abort-dependents vs continue-running) is set on OperationContext and propagated through OperationEnv::invoke() — the composing handler decides the child's policy, not the wire caller. The call.requested payload does not carry an abort policy field. This resolves the TBD that was masquerading as a two-way door: two of the three options ADR-016 floated (wire payload, per-operation declaration) were inconsistent with the ADR's own assumptions. Also marks review #001 as resolved — all 5 critical, 4 warning, and 4 suggestion findings are now addressed. |
||
|
|
3e238a471b |
docs(architecture): add ADR-023, resolve OQ-24 — operation error schemas
ADR-023 adds error_schemas to OperationSpec so operations can declare their domain-level failure modes (FILE_NOT_FOUND, RATE_LIMITED, etc.) distinct from protocol-level codes (NOT_FOUND, FORBIDDEN, etc.). The call.error payload gains an optional 'details' field carrying the typed error payload conforming to the declared schema. from_openapi/to_openapi map OpenAPI response status codes to/from ErrorDefinitions, making the adapter contract from ADR-017 faithful on the error axis. Also fixes W2 (KeyVersionMismatch stale comment in encryption.md — ADR-021 implements rotation without this variant) and W4 (derive_encryption_key_for_version missing from service.md method list). Spec updates: operation-registry.md (OperationSpec, ErrorDefinition, Handler error mapping, services/schema), call-protocol.md (call.error payload, CallError, ResponseEnvelope), README.md, overview.md, open-questions.md (OQ-24), call/README.md, encryption.md, service.md. |
||
|
|
1cedc4eeba |
docs(architecture): add ADR-022, resolve OQ-23 — handler registration, provenance, and composition authority
ADR-022 wires the three controls ADR-015 specified but left without registration paths (C1-C4 from review #001): composition authority, scoped env, and capabilities now enter through a HandlerRegistration bundle. Provenance (Local, FromOpenAPI, FromMCP, FromCall, Session) determines which ops can compose — leaves don't get composition authority. CompositionAuthority replaces handler_identity: Identity (it's a declared authority bundle, not a peer identity). Capabilities are per-request from the bundle (resolves closure-capture vs context ambiguity). Kernel/user analogy: user's authority checked at External gate; handler's composition authority used inside; scoped env bounds reachability. Also fixes W1 (stale ADR-020 path example) and W3 (from_mcp missing from adapter lists in operation-registry.md). Spec updates: operation-registry.md (OperationRegistry, HandlerRegistration, OperationContext, OperationEnv, registration example, capability injection), call-protocol.md (build_root_context), README.md, overview.md, open-questions.md (OQ-23), call/README.md. |
||
|
|
40f6468e18 |
docs(architecture): fix spec/ADR inconsistencies from pre-decomposition review
Critical:
- operation-registry: remove stale duplicate OperationEnv impl that
propagated parent.metadata through composition (violated ADR-014);
collapse to one canonical block with metadata: HashMap::new()
- operation-registry: fix request_id collision — format!("env-{name}")
produced identical IDs across concurrent invocations, corrupting
PendingRequestMap correlation and the abort-cascade tree (ADR-016)
- operation-registry + ADR-015: fix OperationContext.internal visibility —
pub field let handlers mark their own call internal (privilege
escalation per ADR-015); change to pub(crate) with pub fn is_internal
Warnings:
- core-types: add Connection::set_identity/identity (OQ-11) to the
Connection type spec — was specified in auth.md but missing from the
type definition
- operation-registry: add Capabilities: Clone design note — invoke()
clones capabilities through composition; explicit security implication
- call-protocol: add CallAdapter root OperationContext construction
example showing internal: false wire path, complementing
OperationEnv::invoke() internal: true composition path
- overview: remove alknet/agent from ALPN registry — agent is a future
consumer of alknet-call (call-protocol operations), not a separate ALPN
- call-protocol: clarify call.requested payload schema and the
leading-slash convention (wire operationId has slash, registry name
does not)
Suggestions:
- operation-registry: cross-reference ResponseEnvelope definition
- core-types: add StreamError to HandlerError mapping table
|
||
|
|
400c60e7f4 |
docs(architecture): security constraints from security review
Address security review findings by adding explicit constraints to specs and implementation specialist role: Architectural constraints (spec updates): - metadata does not propagate through OperationEnv::invoke() — fresh HashMap for nested calls, closes the back-door leak channel where a handler that puts a secret in metadata would leak it to children and across from_call to remote nodes (ADR-014) - Config reload must be authenticated/local-only — malicious reload = root-equivalent privilege grant (config.md) - from_call trust is transitive — scoped env bounds reachability, not what the remote op does (operation-registry.md) - Token entropy ≥128 bits — prefix is lookup aid not secret, offline hash verification requires high-entropy tokens (auth.md) Implementation constraints (auth.md security constraints section + role spec): - OsRng for cryptographic nonces (AES-GCM IV reuse is catastrophic) - CachedKey derives Zeroize/ZeroizeOnDrop (no secrets in freed heap) - No unwrap()/expect() outside tests (poisoned lock recovery, not crash) - Implementation specialist role spec updated with all four constraints |
||
|
|
c0a322ac29 |
docs(architecture): resolve OQ-11 and OQ-19 — all open questions resolved
OQ-11 (handler-level auth observability): Option B — handlers store resolved identity on Connection via set_identity. Two identity scopes: connection-level (observability, write-once-read-many) and per-request (ACL, on OperationContext). Per-request takes precedence for ACL; connection-level is for logging/audit only. OQ-19 (session-scoped registries): Protocol doesn't need changes. OperationEnv must remain a trait (not concrete) to enable session-overlay pattern. Three-tier registry: core (static, External+Internal), session (dynamic, Internal-only), promotion (curated review). Documented as implementation guard in operation-registry.md. All 19 open questions are now resolved. No open one-way or two-way doors remain. The architecture is ready for review and implementation. |
||
|
|
8f19eb8861 |
docs(architecture): add ADR-017 call protocol client and adapter contract, resolve OQ-15
ADR-017 locks the client/adapter architecture: - CallClient opens QUIC connections, shares dispatch loop with CallAdapter - Connection direction independent of call direction (both sides can call) - from_call adapter: discovers remote ops via services/list + services/schema, registers with forwarding handlers (same pattern as from_openapi/from_mcp) - to_openapi/to_mcp: project local ops to external protocols - OperationAdapter trait: produces (OperationSpec, Handler) pairs - Cross-node call tree: abort cascade propagates through from_call handlers - Credentials from capabilities (ADR-014), adapter ops Internal by default (ADR-015) The dispatch POC at /workspace/@alkdev/dispatch demonstrated head/worker over SSH+axum; under the call protocol it's cross-node composition via from_call. Connection topology (who advertises, who opens) is independent of call direction — runner pattern, dispatch pattern, and P2P all work. |
||
|
|
e2730869ca |
docs(architecture): add ADR-016 abort cascade for nested calls, resolve OQ-17
ADR-016 locks the abort cascade model: - call.aborted cascades to all non-terminal descendants via parent_request_id - Default policy: abort-dependents (abort everything downstream) - Opt-in: continue-running (started descendants continue, pending ones abort) - Server (CallAdapter) discovers descendants and propagates; client sends one abort - Handlers clean up via Rust async drop semantics (Drop guards) - parent_indexed map suffices for tree walking; flowgraph is optional prior art Spec updates: - call-protocol.md abort cascade section references ADR-016 - OQ-17 resolved, ADR-016 referenced across all call crate specs - README.md updated: ADRs 001-016, OQ-17 moved to resolved |
||
|
|
6285779c30 |
docs(architecture): add ADR-015 privilege model and authority context, resolve OQ-18
ADR-015 locks the call protocol's security model: - internal flag switches authority context to handler identity, not skip ACL - Operations have External/Internal visibility (Internal returns NOT_FOUND from wire, excluded from services/list) - OperationContext carries both identity (caller/principal) and handler_identity (handler/agent) - Scoped composition env bounds reachability (handler can only invoke declared operations) - Three controls together: visibility (wire boundary) + handler identity (authority) + scoped env (reachability) = least privilege Spec updates: - OperationSpec gains Visibility field (External/Internal) - OperationContext gains handler_identity field - AccessControl section: ACL runs against caller identity for external, handler identity for internal - LocalOperationEnv propagates handler_identity - services/list only returns External operations - Adapter-registered operations are Internal by default - OQ-18 resolved, ADR-015 referenced across all call crate specs |
||
|
|
b4aadc6b93 |
docs(architecture): add OQ-19 session-scoped registries and agent-written operations
Document the three-tier registry model (core/session/promotion) and the self-improving agent workflow where agents write their own operations in a quickjs sandbox. The POC at /workspace/toolEnv demonstrated the sandbox mechanism (quickjs in Deno web workers, proxy-based env bridge via postMessage) but exposed the full registry to the sandbox — the security gap that OQ-18's scoped composition env addresses. The call protocol doesn't need changes: the OperationEnv trait is the composition point, and a session-scoped env wraps the global env (session registry first, fall through to global). The one-way door this OQ guards against: making OperationEnv concrete instead of a trait, or hardcoding the global registry into the dispatch path, would close the session-overlay pattern. Session-scoped operations are always Internal, run under the handler's identity, and are ephemeral. Promotion to core requires curation review (architect role with promote scope). |
||
|
|
f27d717ac8 |
docs(architecture): reframe OQ-17 and OQ-18 as protocol-level concerns, not agent-specific
The abort cascade and privilege model are call protocol semantics that every consumer inherits — NAPI adapter, Python adapter, agent service, and any future service speaking the EventEnvelope wire format. Framing them as 'needs agent crate in view' let a single consumer's timeline gate a protocol-level decision. The agent use case is a useful test case for edge cases, but the decisions belong to the call protocol. |
||
|
|
fab2c88444 |
docs(architecture): rename trusted to internal, add OQ-17 abort cascade and OQ-18 privilege model
The 'trusted' flag on OperationContext was the wrong word — it implies a trust decision was made, but what actually happens is the call originated internally (from composition) not externally (from the wire). Renamed to 'internal' with clarified semantics: internal calls switch authority context to the handler's identity, not skip ACL. This prevents the privilege escalation vector where composition with 'trusted: true' bypassed all access control (buggy handler + parameterized dispatch). - Rename trusted -> internal across operation-registry.md, ADR-014 - Update OperationContext field description and LocalOperationEnv code - Add OQ-17: abort cascade for nested calls (call.aborted cascades to descendants, default abort-dependents, continue-running opt-in). One-way door on the protocol event schema; mechanism is a two-way door. - Add OQ-18: privilege model and authority context (internal = authority switch not ACL skip, External/Internal operation visibility, scoped composition env + handler identity). Needs agent crate in view. - Add abort cascade section and constraint to call-protocol.md - Update crates/call/README.md with OQ-17, OQ-18, and two new design principles - Update architecture README.md with OQ-17, OQ-18 |
||
|
|
6a7d4b9755 |
docs(architecture): add ADR-014 secret material flow, remove vault ops from call protocol
Resolve the contradiction between ADR-008's "capability source" model and operation-registry.md showing vault operations on the wire. ADR-014 establishes: vault is assembly-layer only, capabilities carry outbound credentials (distinct from inbound identity), call protocol carries no secret material, adapters take credential sources not static tokens. - Add ADR-014 (Secret Material Flow and Capability Injection) - Remove vault/derive, vault/unlock, vault/decrypt from call protocol registration examples and all spec examples - Add Capabilities field to OperationContext, propagate through LocalOperationEnv nested calls - Add Capability Injection section to operation-registry.md - Add no-secret-material wire constraint to call-protocol.md - Add streaming subscribe example (LLM chat with Vercel UI chunks) - Add Security Model section to overview.md (identity vs capabilities) - Trim WASM treatment from ~20 lines to a design-constraint note - Add OQ-16 (resolved: no vault ops on wire), update OQ-08, OQ-15 - Update ADR-003, ADR-008, ADR-013 to remove stale "via call protocol" vault references |
||
|
|
6219a323b6 |
docs(architecture): untangle TLS identity use cases, remove phase framing, add ADR-013 Rust canonical + agent crate
- Rewrite OQ-12: separate two distinct TLS identity use cases (RFC 7250
raw keys as default for P2P, X.509 for domain-hosted/browsers) instead
of conflating them as 'file paths now, ACME later'. ACME is a proven
pattern from the reverse-proxy project, not speculative future work.
- Resolve OQ-13 and OQ-14: remove 'Phase 1' framing from core crate
specs. /{service}/{op} is the correct design for alknet-call, not a
simplification. Batch as correlated call.requested events is the correct
protocol design. Core crates need to be done right from the start.
- Add ADR-013: Rust as canonical implementation language. TypeScript
@alkdev/operations is a reference that informed the design, not a
parallel implementation. The only JS use case is browser SDK adaptation.
Five reasons: memory safety, LLM competence, supply chain attacks,
performance, browser-only JS.
- Add alknet-agent crate to the crate graph (depends on alknet-call, not
alknet-core). Agent service uses call protocol client for tool dispatch
and vault/derive for provider keys — no env vars for secrets. ALPN
alknet/agent added to the registry.
- Add OQ-15: call protocol client and adapter contract. alknet-call needs
both server (CallAdapter) and client (remote invocation over QUIC), plus
the adapter traits (from_*, to_*) that enable composition.
- Clarify alknet-napi as thin NAPI projection layer, not business logic.
- Fix bugs: ProtocolController → ProtocolHandler typo, OperationEnv
invoke() path format inconsistency, RateLimitConfig comment confusion.
- Update endpoint.md TLS section: comprehensive identity model comparison
table, RFC 7250 as default mode, ACME as proven pattern.
|
||
|
|
a596f0d188 |
docs(architecture): add alknet-call crate spec, ADR-012, resolve OQ-07
Add architecture specs for the alknet-call crate: - call-protocol.md: CallAdapter, EventEnvelope wire format, bidirectional stream model with ID-based correlation, PendingRequestMap, protocol operations (call/subscribe/batch/schema), per-request identity resolution, connection/stream lifecycle, error codes - operation-registry.md: OperationSpec, async Handler type, OperationRegistry, AccessControl with trusted call bypass, OperationEnv with context propagation (parent_request_id, identity inheritance), service discovery, irpc integration layering, naming convention (no leading slash in names) - ADR-012: Call protocol uses bidirectional QUIC streams with EventEnvelope framing and ID-based correlation. Protocol is stream-agnostic and symmetric. Resolves OQ-07. Key design decisions: - Handler type is async (Fn returning Pin<Box<dyn Future>>) - OperationEnv::invoke propagates parent context (identity, metadata, parent_request_id) - Identity resolution is per-request, not per-connection - Operation names without leading slash (fs/readFile, not /fs/readFile) - Batch is a client-side pattern, not a protocol primitive (OQ-14) - Phase 1 uses service/op paths, node prefix added later (OQ-13) Also: promote ADR-010 and ADR-011 from Proposed to Accepted, add OQ-13 and OQ-14 to open-questions.md. |